3 #include <common/constants.qh>
4 #include <common/deathtypes/all.qh>
5 #include <common/effects/all.qh>
6 #include <common/gamemodes/_mod.qh>
7 #include <common/gamemodes/rules.qh>
8 #include <common/items/_mod.qh>
9 #include <common/mapobjects/defs.qh>
10 #include <common/mapobjects/triggers.qh>
11 #include <common/mutators/mutator/buffs/buffs.qh>
12 #include <common/mutators/mutator/buffs/sv_buffs.qh>
13 #include <common/mutators/mutator/instagib/sv_instagib.qh>
14 #include <common/mutators/mutator/status_effects/_mod.qh>
15 #include <common/mutators/mutator/waypoints/waypointsprites.qh>
16 #include <common/notifications/all.qh>
17 #include <common/physics/movetypes/movetypes.qh>
18 #include <common/physics/player.qh>
19 #include <common/playerstats.qh>
20 #include <common/resources/sv_resources.qh>
21 #include <common/state.qh>
22 #include <common/teams.qh>
23 #include <common/util.qh>
24 #include <common/vehicles/all.qh>
25 #include <common/weapons/_all.qh>
26 #include <lib/csqcmodel/sv_model.qh>
27 #include <lib/warpzone/common.qh>
28 #include <server/bot/api.qh>
29 #include <server/client.qh>
30 #include <server/gamelog.qh>
31 #include <server/hook.qh>
32 #include <server/items/items.qh>
33 #include <server/main.qh>
34 #include <server/mutators/_mod.qh>
35 #include <server/scores.qh>
36 #include <server/spawnpoints.qh>
37 #include <server/teamplay.qh>
38 #include <server/weapons/accuracy.qh>
39 #include <server/weapons/csqcprojectile.qh>
40 #include <server/weapons/selection.qh>
41 #include <server/weapons/weaponsystem.qh>
42 #include <server/world.qh>
44 void UpdateFrags(entity player, int f)
46 GameRules_scoring_add_team(player, SCORE, f);
49 void GiveFrags(entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
51 // TODO route through PlayerScores instead
52 if(game_stopped) return;
59 GameRules_scoring_add(attacker, SUICIDES, 1);
64 GameRules_scoring_add(attacker, TEAMKILLS, 1);
70 GameRules_scoring_add(attacker, KILLS, 1);
71 if(!warmup_stage && targ.playerid)
72 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
75 GameRules_scoring_add(targ, DEATHS, 1);
77 // FIXME fix the mess this is (we have REAL points now!)
78 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
81 attacker.totalfrags += f;
84 UpdateFrags(attacker, f);
87 string AppendItemcodes(string s, entity player)
89 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
91 .entity weaponentity = weaponentities[slot];
92 int w = player.(weaponentity).m_weapon.m_id;
94 w = player.(weaponentity).cnt; // previous weapon
95 if(w != 0 || slot == 0)
96 s = strcat(s, ftos(w));
98 if(PHYS_INPUT_BUTTON_CHAT(player))
100 // TODO: include these codes as a flag on the item itself
101 MUTATOR_CALLHOOK(LogDeath_AppendItemCodes, player, s);
102 s = M_ARGV(1, string);
106 void LogDeath(string mode, int deathtype, entity killer, entity killed)
109 if(!autocvar_sv_eventlog)
111 s = strcat(":kill:", mode);
112 s = strcat(s, ":", ftos(killer.playerid));
113 s = strcat(s, ":", ftos(killed.playerid));
114 s = strcat(s, ":type=", Deathtype_Name(deathtype));
115 s = strcat(s, ":items=");
116 s = AppendItemcodes(s, killer);
119 s = strcat(s, ":victimitems=");
120 s = AppendItemcodes(s, killed);
125 void Obituary_SpecialDeath(
129 string s1, string s2, string s3,
130 float f1, float f2, float f3)
132 if(!DEATH_ISSPECIAL(deathtype))
134 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
138 entity deathent = REGISTRY_GET(Deathtypes, deathtype - DT_FIRST);
141 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
145 if(g_cts && deathtype == DEATH_KILL.m_id)
146 return; // TODO: somehow put this in CTS gamemode file!
148 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
151 Send_Notification_WOCOVA(
159 Send_Notification_WOCOVA(
163 death_message.nent_msginfo,
170 float Obituary_WeaponDeath(
174 string s1, string s2, string s3,
177 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
178 if (death_weapon == WEP_Null)
181 w_deathtype = deathtype;
182 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
187 Send_Notification_WOCOVA(
195 // send the info part to everyone
196 Send_Notification_WOCOVA(
200 death_message.nent_msginfo,
208 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %s!\n",
217 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target, string attacker_name)
219 if(deathtype == DEATH_FIRE.m_id)
221 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
222 Send_Notification(NOTIF_ONE, targ, MSG_CHOICE, CHOICE_FRAGGED_FIRE, attacker_name, kill_count_to_target, GetResource(attacker, RES_HEALTH), GetResource(attacker, RES_ARMOR), (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping));
226 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
229 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
232 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
235 float notif_firstblood = false;
236 float kill_count_to_attacker, kill_count_to_target;
237 bool notif_anonymous = false;
238 string attacker_name = attacker.netname;
240 // Set final information for the death
241 targ.death_origin = targ.origin;
242 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
244 // Abort now if a mutator requests it
245 if (MUTATOR_CALLHOOK(ClientObituary, inflictor, attacker, targ, deathtype, attacker.(weaponentity))) { CS(targ).killcount = 0; return; }
246 notif_anonymous = M_ARGV(5, bool);
249 attacker_name = "Anonymous player";
251 #ifdef NOTIFICATIONS_DEBUG
254 "Obituary(%s, %s, %s, %s = %d);\n",
258 Deathtype_Name(deathtype),
269 if(DEATH_ISSPECIAL(deathtype))
271 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
273 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
277 switch(DEATH_ENT(deathtype))
279 case DEATH_MIRRORDAMAGE:
281 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
284 case DEATH_HURTTRIGGER:
285 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, inflictor.message, deathlocation, CS(targ).killcount, 0, 0);
289 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
295 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
297 backtrace("SUICIDE: what the hell happened here?\n");
300 LogDeath("suicide", deathtype, targ, targ);
301 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
302 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
308 else if(IS_PLAYER(attacker))
310 if(SAME_TEAM(attacker, targ))
312 LogDeath("tk", deathtype, attacker, targ);
313 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
315 CS(attacker).killcount = 0;
317 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
318 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker_name);
319 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker_name, deathlocation, CS(targ).killcount);
321 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
322 // No need for specific death/weapon messages...
326 LogDeath("frag", deathtype, attacker, targ);
327 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
329 CS(attacker).taunt_soundtime = time + 1;
330 CS(attacker).killcount = CS(attacker).killcount + 1;
332 attacker.killsound += 1;
334 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
335 // these 2 macros are spread over multiple files
336 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
338 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
340 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
343 switch(CS(attacker).killcount)
350 if(!warmup_stage && !checkrules_firstblood)
352 checkrules_firstblood = true;
353 notif_firstblood = true; // modify the current messages so that they too show firstblood information
354 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
355 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
357 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
358 kill_count_to_attacker = -1;
359 kill_count_to_target = -2;
363 kill_count_to_attacker = CS(attacker).killcount;
364 kill_count_to_target = 0;
375 kill_count_to_attacker,
376 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
384 kill_count_to_target,
385 GetResource(attacker, RES_HEALTH),
386 GetResource(attacker, RES_ARMOR),
387 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
390 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target, attacker_name))
398 kill_count_to_attacker,
399 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
407 kill_count_to_target,
408 GetResource(attacker, RES_HEALTH),
409 GetResource(attacker, RES_ARMOR),
410 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
415 if(deathtype == DEATH_BUFF.m_id)
416 f3 = buff_FirstFromFlags(attacker).m_id;
418 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker))
419 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
428 switch(DEATH_ENT(deathtype))
430 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
431 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
432 // and there will be a REAL DEATH_VOID implementation which mappers will use.
433 case DEATH_HURTTRIGGER:
435 Obituary_SpecialDeath(targ, false, deathtype,
447 Obituary_SpecialDeath(targ, false, deathtype,
449 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
459 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
464 LogDeath("accident", deathtype, targ, targ);
465 GiveFrags(targ, targ, -1, deathtype, weaponentity);
467 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
469 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
472 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
477 // reset target kill count
478 CS(targ).killcount = 0;
481 void Ice_Think(entity this)
483 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
488 vector ice_org = this.owner.origin - '0 0 16';
489 if (this.origin != ice_org)
490 setorigin(this, ice_org);
491 this.nextthink = time;
494 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
496 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
499 if(STAT(FROZEN, targ))
502 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
504 STAT(FROZEN, targ) = frozen_type;
505 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
506 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
507 targ.revive_speed = revivespeed;
509 IL_REMOVE(g_bot_targets, targ);
510 targ.bot_attack = false;
511 targ.freeze_time = time;
513 entity ice = new(ice);
515 ice.scale = targ.scale;
516 // set_movetype(ice, MOVETYPE_FOLLOW) would rotate the ice model with the player
517 setthink(ice, Ice_Think);
518 ice.nextthink = time;
519 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
520 setmodel(ice, MDL_ICE);
522 ice.colormod = Team_ColorRGB(targ.team);
523 ice.glowmod = ice.colormod;
525 targ.revival_time = 0;
529 RemoveGrapplingHooks(targ);
531 FOREACH_CLIENT(IS_PLAYER(it),
533 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
535 .entity weaponentity = weaponentities[slot];
536 if(it.(weaponentity).hook.aiment == targ)
537 RemoveHook(it.(weaponentity).hook);
542 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
543 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
546 void Unfreeze(entity targ, bool reset_health)
548 if(!STAT(FROZEN, targ))
551 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
552 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
554 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
556 STAT(FROZEN, targ) = 0;
557 STAT(REVIVE_PROGRESS, targ) = 0;
558 targ.revival_time = time;
560 IL_PUSH(g_bot_targets, targ);
561 targ.bot_attack = true;
563 WaypointSprite_Kill(targ.waypointsprite_attached);
565 FOREACH_CLIENT(IS_PLAYER(it),
567 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
569 .entity weaponentity = weaponentities[slot];
570 if(it.(weaponentity).hook.aiment == targ)
571 RemoveHook(it.(weaponentity).hook);
575 // remove the ice block
577 delete(targ.iceblock);
578 targ.iceblock = NULL;
580 MUTATOR_CALLHOOK(Unfreeze, targ);
583 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
585 float complainteamdamage = 0;
586 float mirrordamage = 0;
587 float mirrorforce = 0;
589 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
592 entity attacker_save = attacker;
594 // special rule: gravity bombs and sound-based attacks do not affect team mates (other than for disconnecting the hook)
595 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || (deathtype & HITTYPE_SOUND))
597 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
603 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
605 // exit the vehicle before killing (fixes a crash)
606 if(IS_PLAYER(targ) && targ.vehicle)
607 vehicles_exit(targ.vehicle, VHEF_RELEASE);
609 // These are ALWAYS lethal
610 // No damage modification here
611 // Instead, prepare the victim for their death...
612 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
614 SetResourceExplicit(targ, RES_ARMOR, 0);
615 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
617 StatusEffects_remove(STATUSEFFECT_SpawnShield, targ, STATUSEFFECT_REMOVE_CLEAR);
618 targ.flags -= targ.flags & FL_GODMODE;
621 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
627 // nullify damage if teamplay is on
628 if(deathtype != DEATH_TELEFRAG.m_id)
629 if(IS_PLAYER(attacker))
631 // avoid dealing damage or force to other independent players
632 // and avoid dealing damage or force to things owned by other independent players
633 if((IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ))) ||
634 (targ.realowner && IS_INDEPENDENT_PLAYER(targ.realowner) && attacker != targ.realowner))
639 else if(!STAT(FROZEN, targ) && SAME_TEAM(attacker, targ))
641 if(autocvar_teamplay_mode == 1)
643 else if(attacker != targ)
645 if(autocvar_teamplay_mode == 2)
647 if(IS_PLAYER(targ) && !IS_DEAD(targ))
649 attacker.dmg_team = attacker.dmg_team + damage;
650 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
653 else if(autocvar_teamplay_mode == 3)
655 else if(autocvar_teamplay_mode == 4)
657 if(IS_PLAYER(targ) && !IS_DEAD(targ))
659 attacker.dmg_team = attacker.dmg_team + damage;
660 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
661 if(complainteamdamage > 0)
662 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
663 mirrorforce = autocvar_g_mirrordamage * vlen(force);
664 damage = autocvar_g_friendlyfire * damage;
665 // mirrordamage will be used LATER
667 if(autocvar_g_mirrordamage_virtual)
669 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
670 attacker.dmg_take += v.x;
671 attacker.dmg_save += v.y;
672 attacker.dmg_inflictor = inflictor;
677 if(autocvar_g_friendlyfire_virtual)
679 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
680 targ.dmg_take += v.x;
681 targ.dmg_save += v.y;
682 targ.dmg_inflictor = inflictor;
684 if(!autocvar_g_friendlyfire_virtual_force)
688 else if(!targ.canteamdamage)
695 if (!DEATH_ISSPECIAL(deathtype))
697 damage *= autocvar_g_weapondamagefactor;
698 mirrordamage *= autocvar_g_weapondamagefactor;
699 complainteamdamage *= autocvar_g_weapondamagefactor;
700 force = force * autocvar_g_weaponforcefactor;
701 mirrorforce *= autocvar_g_weaponforcefactor;
704 // should this be changed at all? If so, in what way?
705 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
706 damage = M_ARGV(4, float);
707 mirrordamage = M_ARGV(5, float);
708 force = M_ARGV(6, vector);
710 if(IS_PLAYER(targ) && damage > 0 && attacker)
712 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
714 .entity went = weaponentities[slot];
715 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
716 RemoveHook(targ.(went).hook);
720 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
721 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
723 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
725 Unfreeze(targ, false);
726 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
727 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
728 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
729 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
733 force *= autocvar_g_frozen_force;
736 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
737 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
739 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
741 entity spot = SelectSpawnPoint(targ, false);
745 targ.deadflag = DEAD_NO;
747 targ.angles = spot.angles;
750 targ.effects |= EF_TELEPORT_BIT;
752 targ.angles_z = 0; // never spawn tilted even if the spot says to
753 targ.fixangle = true; // turn this way immediately
754 targ.velocity = '0 0 0';
755 targ.avelocity = '0 0 0';
756 targ.punchangle = '0 0 0';
757 targ.punchvector = '0 0 0';
758 targ.oldvelocity = targ.velocity;
760 targ.spawnorigin = spot.origin;
761 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
762 // don't reset back to last position, even if new position is stuck in solid
763 targ.oldorigin = targ.origin;
765 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
769 if (targ == attacker)
770 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
775 if(deathtype != DEATH_BUFF.m_id)
776 if(targ.takedamage == DAMAGE_AIM)
780 if(IS_VEHICLE(targ) && targ.owner)
785 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
787 if (DIFF_TEAM(victim, attacker))
791 if(deathtype != DEATH_FIRE.m_id)
793 if(PHYS_INPUT_BUTTON_CHAT(victim))
794 attacker.typehitsound += 1;
796 attacker.hitsound_damage_dealt += damage;
799 impressive_hits += 1;
801 if (!DEATH_ISSPECIAL(deathtype))
803 if(IS_PLAYER(targ)) // don't do this for vehicles
809 else if (IS_PLAYER(attacker) && !STAT(FROZEN, victim)) // same team
811 if (deathtype != DEATH_FIRE.m_id)
813 attacker.typehitsound += 1;
815 if(complainteamdamage > 0)
816 if(time > CS(attacker).teamkill_complain)
818 CS(attacker).teamkill_complain = time + 5;
819 CS(attacker).teamkill_soundtime = time + 0.4;
820 CS(attacker).teamkill_soundsource = targ;
828 if (targ.damageforcescale)
830 if (!IS_PLAYER(targ) || !StatusEffects_active(STATUSEFFECT_SpawnShield, targ) || targ == attacker)
832 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
833 if(targ.move_movetype == MOVETYPE_PHYSICS)
835 entity farcent = new(farce);
836 farcent.enemy = targ;
837 farcent.movedir = farce * 10;
839 farcent.movedir = farcent.movedir * targ.mass;
840 farcent.origin = hitloc;
841 farcent.forcetype = FORCETYPE_FORCEATPOS;
842 farcent.nextthink = time + 0.1;
843 setthink(farcent, SUB_Remove);
845 else if(targ.move_movetype != MOVETYPE_NOCLIP)
847 targ.velocity = targ.velocity + farce;
849 UNSET_ONGROUND(targ);
850 UpdateCSQCProjectile(targ);
853 if (damage != 0 || (targ.damageforcescale && force))
854 if (targ.event_damage)
855 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
857 // apply mirror damage if any
858 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
859 if(mirrordamage > 0 || mirrorforce > 0)
861 attacker = attacker_save;
863 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
864 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
868 // Returns total damage applies to creatures
869 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
870 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
874 float total_damage_to_creatures;
879 float stat_damagedone;
881 if(RadiusDamage_running)
883 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
887 if (rad < 0) rad = 0;
889 RadiusDamage_running = 1;
891 tfloordmg = autocvar_g_throughfloor_damage;
892 tfloorforce = autocvar_g_throughfloor_force;
894 total_damage_to_creatures = 0;
896 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
897 if(!(deathtype & HITTYPE_SOUND)) // do not send radial sound damage (bandwidth hog)
899 force = inflictorvelocity;
903 force = normalize(force);
904 if(forceintensity >= 0)
905 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
907 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
912 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
916 if ((targ != inflictor) || inflictorselfdamage)
917 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
920 // measure distance from nearest point on target (not origin)
921 // to nearest point on inflictor (not origin)
922 vector nearest = targ.WarpZone_findradius_nearest;
923 vector inflictornearest = NearestPointOnBoundingBox(
924 inflictororigin - (inflictor.maxs - inflictor.mins) * 0.5,
925 inflictororigin + (inflictor.maxs - inflictor.mins) * 0.5,
927 vector diff = inflictornearest - nearest;
929 // round up a little on the damage to ensure full damage on impacts
930 // and turn the distance into a fraction of the radius
931 float dist = max(0, vlen(diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS));
934 float f = (rad > 0) ? 1 - (dist / rad) : 1;
935 // at this point f can't be < 0 or > 1
936 float finaldmg = coredamage * f + edgedamage * (1 - f);
942 vector myblastorigin;
945 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
947 // if it's a player, use the view origin as reference
948 center = CENTER_OR_VIEWOFS(targ);
950 force = normalize(center - myblastorigin);
951 force = force * (finaldmg / max(coredamage, edgedamage)) * forceintensity;
954 // apply special scaling along the z axis if set
955 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
957 force.z *= forcezscale;
959 if(targ != directhitentity)
964 float mininv_f, mininv_d;
966 // test line of sight to multiple positions on box,
967 // and do damage if any of them hit
970 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
971 // so for a given max stddev:
972 // n = (1 / (2 * max stddev of hitratio))^2
974 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
975 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
977 if(autocvar_g_throughfloor_debug)
978 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
981 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
983 if(autocvar_g_throughfloor_debug)
984 LOG_INFOF(" steps=%f", total);
988 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
990 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
992 if(autocvar_g_throughfloor_debug)
993 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
995 for(c = 0; c < total; ++c)
997 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
998 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
999 if (trace_fraction == 1 || trace_ent == targ)
1003 hitloc = hitloc + nearest;
1007 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1008 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1009 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1012 nearest = hitloc * (1 / max(1, hits));
1013 hitratio = (hits / total);
1014 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1015 finaldmg = finaldmg * a;
1016 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1019 if(autocvar_g_throughfloor_debug)
1020 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1022 /*if (targ == attacker)
1024 print("hits ", ftos(hits), " / ", ftos(total));
1025 print(" finaldmg ", ftos(finaldmg), " force ", ftos(vlen(force)));
1026 print(" (", vtos(force), ") (", ftos(a), ")\n");
1030 if(finaldmg || force)
1034 total_damage_to_creatures += finaldmg;
1036 if(accuracy_isgooddamage(attacker, targ))
1037 stat_damagedone += finaldmg;
1040 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1041 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1043 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1051 RadiusDamage_running = 0;
1053 if(!DEATH_ISSPECIAL(deathtype))
1054 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(max(coredamage, edgedamage), stat_damagedone));
1056 return total_damage_to_creatures;
1059 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1061 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1062 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1065 bool Heal(entity targ, entity inflictor, float amount, float limit)
1067 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1070 bool healed = false;
1072 healed = targ.event_heal(targ, inflictor, amount, limit);
1073 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1074 // TODO: healing fx!
1075 // TODO: armor healing?
1079 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1082 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1092 if(StatusEffects_active(STATUSEFFECT_Burning, e))
1094 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1096 mintime = fireendtime - time;
1097 maxtime = max(mintime, t);
1099 mindps = e.fire_damagepersec;
1100 maxdps = max(mindps, dps);
1102 if(maxtime > mintime || maxdps > mindps)
1106 // damage we have right now
1107 mindamage = mindps * mintime;
1109 // damage we want to get
1110 maxdamage = mindamage + d;
1112 // but we can't exceed maxtime * maxdps!
1113 totaldamage = min(maxdamage, maxtime * maxdps);
1117 // totaldamage = min(mindamage + d, maxtime * maxdps)
1119 // totaldamage <= maxtime * maxdps
1120 // ==> totaldamage / maxdps <= maxtime.
1122 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1123 // >= min(mintime, maxtime)
1124 // ==> totaldamage / maxdps >= mintime.
1127 // how long do we damage then?
1128 // at least as long as before
1129 // but, never exceed maxdps
1130 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1134 // at most as long as maximum allowed
1135 // but, never below mindps
1136 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1138 // assuming t > mintime, dps > mindps:
1139 // we get d = t * dps = maxtime * maxdps
1140 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1141 // totaldamage / maxdps = maxtime
1142 // totaldamage / mindps > totaldamage / maxdps = maxtime
1144 // a) totaltime = max(mintime, maxtime) = maxtime
1145 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1147 // assuming t <= mintime:
1148 // we get maxtime = mintime
1149 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1150 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1152 // assuming dps <= mindps:
1153 // we get mindps = maxdps.
1154 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1155 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1156 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1158 e.fire_damagepersec = totaldamage / totaltime;
1159 StatusEffects_apply(STATUSEFFECT_Burning, e, time + totaltime, 0);
1160 if(totaldamage > 1.2 * mindamage)
1162 e.fire_deathtype = dt;
1163 if(e.fire_owner != o)
1166 e.fire_hitsound = false;
1169 if(accuracy_isgooddamage(o, e))
1170 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1171 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1178 e.fire_damagepersec = dps;
1179 StatusEffects_apply(STATUSEFFECT_Burning, e, time + t, 0);
1180 e.fire_deathtype = dt;
1182 e.fire_hitsound = false;
1183 if(accuracy_isgooddamage(o, e))
1184 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1189 void Fire_ApplyDamage(entity e)
1194 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1195 if(IS_NOT_A_CLIENT(o))
1198 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1199 t = min(frametime, fireendtime - time);
1200 d = e.fire_damagepersec * t;
1202 hi = e.fire_owner.hitsound_damage_dealt;
1203 ty = e.fire_owner.typehitsound;
1204 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1205 if(e.fire_hitsound && e.fire_owner)
1207 e.fire_owner.hitsound_damage_dealt = hi;
1208 e.fire_owner.typehitsound = ty;
1210 e.fire_hitsound = true;
1212 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1214 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1216 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1217 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1219 t = autocvar_g_balance_firetransfer_time * (fireendtime - time);
1220 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1221 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);