3 #include <common/effects/all.qh>
6 #include <server/client.qh>
7 #include <server/gamelog.qh>
8 #include <server/items/items.qh>
9 #include <server/mutators/_mod.qh>
10 #include <server/main.qh>
11 #include "teamplay.qh"
13 #include "spawnpoints.qh"
14 #include "../common/state.qh"
15 #include "../common/physics/player.qh"
16 #include "resources.qh"
17 #include "../common/vehicles/all.qh"
18 #include "../common/items/_mod.qh"
19 #include "../common/mutators/mutator/waypoints/waypointsprites.qh"
20 #include "../common/mutators/mutator/instagib/sv_instagib.qh"
21 #include "../common/mutators/mutator/buffs/buffs.qh"
22 #include "weapons/accuracy.qh"
23 #include "weapons/csqcprojectile.qh"
24 #include "weapons/selection.qh"
25 #include "../common/constants.qh"
26 #include "../common/deathtypes/all.qh"
27 #include <common/mapobjects/defs.qh>
28 #include <common/mapobjects/triggers.qh>
29 #include "../common/notifications/all.qh"
30 #include "../common/physics/movetypes/movetypes.qh"
31 #include "../common/playerstats.qh"
32 #include "../common/teams.qh"
33 #include "../common/util.qh"
34 #include <common/gamemodes/_mod.qh>
35 #include <common/gamemodes/rules.qh>
36 #include <common/weapons/_all.qh>
37 #include "../lib/csqcmodel/sv_model.qh"
38 #include "../lib/warpzone/common.qh"
40 void UpdateFrags(entity player, int f)
42 GameRules_scoring_add_team(player, SCORE, f);
45 void GiveFrags(entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
47 // TODO route through PlayerScores instead
48 if(game_stopped) return;
55 GameRules_scoring_add(attacker, SUICIDES, 1);
60 GameRules_scoring_add(attacker, TEAMKILLS, 1);
66 GameRules_scoring_add(attacker, KILLS, 1);
67 if(!warmup_stage && targ.playerid)
68 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
71 GameRules_scoring_add(targ, DEATHS, 1);
73 // FIXME fix the mess this is (we have REAL points now!)
74 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
77 attacker.totalfrags += f;
80 UpdateFrags(attacker, f);
83 string AppendItemcodes(string s, entity player)
85 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
87 .entity weaponentity = weaponentities[slot];
88 int w = player.(weaponentity).m_weapon.m_id;
90 w = player.(weaponentity).cnt; // previous weapon
91 if(w != 0 || slot == 0)
92 s = strcat(s, ftos(w));
94 if(time < STAT(STRENGTH_FINISHED, player))
96 if(time < STAT(INVINCIBLE_FINISHED, player))
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);
287 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
293 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
295 backtrace("SUICIDE: what the hell happened here?\n");
298 LogDeath("suicide", deathtype, targ, targ);
299 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
300 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
306 else if(IS_PLAYER(attacker))
308 if(SAME_TEAM(attacker, targ))
310 LogDeath("tk", deathtype, attacker, targ);
311 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
313 CS(attacker).killcount = 0;
315 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
316 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker_name);
317 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker_name, deathlocation, CS(targ).killcount);
319 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
320 // No need for specific death/weapon messages...
324 LogDeath("frag", deathtype, attacker, targ);
325 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
327 CS(attacker).taunt_soundtime = time + 1;
328 CS(attacker).killcount = CS(attacker).killcount + 1;
330 attacker.killsound += 1;
332 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
333 // these 2 macros are spread over multiple files
334 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
336 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
338 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
341 switch(CS(attacker).killcount)
348 if(!warmup_stage && !checkrules_firstblood)
350 checkrules_firstblood = true;
351 notif_firstblood = true; // modify the current messages so that they too show firstblood information
352 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
353 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
355 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
356 kill_count_to_attacker = -1;
357 kill_count_to_target = -2;
361 kill_count_to_attacker = CS(attacker).killcount;
362 kill_count_to_target = 0;
373 kill_count_to_attacker,
374 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
382 kill_count_to_target,
383 GetResource(attacker, RES_HEALTH),
384 GetResource(attacker, RES_ARMOR),
385 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
388 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target, attacker_name))
396 kill_count_to_attacker,
397 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
405 kill_count_to_target,
406 GetResource(attacker, RES_HEALTH),
407 GetResource(attacker, RES_ARMOR),
408 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
413 if(deathtype == DEATH_BUFF.m_id)
414 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
416 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker))
417 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
426 switch(DEATH_ENT(deathtype))
428 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
429 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
430 // and there will be a REAL DEATH_VOID implementation which mappers will use.
431 case DEATH_HURTTRIGGER:
433 Obituary_SpecialDeath(targ, false, deathtype,
445 Obituary_SpecialDeath(targ, false, deathtype,
447 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
457 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
462 LogDeath("accident", deathtype, targ, targ);
463 GiveFrags(targ, targ, -1, deathtype, weaponentity);
465 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
467 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
470 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
475 // reset target kill count
476 CS(targ).killcount = 0;
479 void Ice_Think(entity this)
481 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
486 vector ice_org = this.owner.origin - '0 0 16';
487 if (this.origin != ice_org)
488 setorigin(this, ice_org);
489 this.nextthink = time;
492 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
494 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
497 if(STAT(FROZEN, targ))
500 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
502 STAT(FROZEN, targ) = frozen_type;
503 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
504 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
505 targ.revive_speed = revivespeed;
507 IL_REMOVE(g_bot_targets, targ);
508 targ.bot_attack = false;
509 targ.freeze_time = time;
511 entity ice = new(ice);
513 ice.scale = targ.scale;
514 // set_movetype(ice, MOVETYPE_FOLLOW) would rotate the ice model with the player
515 setthink(ice, Ice_Think);
516 ice.nextthink = time;
517 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
518 setmodel(ice, MDL_ICE);
520 ice.colormod = Team_ColorRGB(targ.team);
521 ice.glowmod = ice.colormod;
523 targ.revival_time = 0;
527 RemoveGrapplingHooks(targ);
529 FOREACH_CLIENT(IS_PLAYER(it),
531 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
533 .entity weaponentity = weaponentities[slot];
534 if(it.(weaponentity).hook.aiment == targ)
535 RemoveHook(it.(weaponentity).hook);
540 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
541 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
544 void Unfreeze(entity targ, bool reset_health)
546 if(!STAT(FROZEN, targ))
549 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
550 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
552 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
554 STAT(FROZEN, targ) = 0;
555 STAT(REVIVE_PROGRESS, targ) = 0;
556 targ.revival_time = time;
558 IL_PUSH(g_bot_targets, targ);
559 targ.bot_attack = true;
561 WaypointSprite_Kill(targ.waypointsprite_attached);
563 FOREACH_CLIENT(IS_PLAYER(it),
565 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
567 .entity weaponentity = weaponentities[slot];
568 if(it.(weaponentity).hook.aiment == targ)
569 RemoveHook(it.(weaponentity).hook);
573 // remove the ice block
575 delete(targ.iceblock);
576 targ.iceblock = NULL;
578 MUTATOR_CALLHOOK(Unfreeze, targ);
581 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
583 float complainteamdamage = 0;
584 float mirrordamage = 0;
585 float mirrorforce = 0;
587 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
590 entity attacker_save = attacker;
592 // special rule: gravity bombs and sound-based attacks do not affect team mates (other than for disconnecting the hook)
593 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || (deathtype & HITTYPE_SOUND))
595 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
601 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
603 // exit the vehicle before killing (fixes a crash)
604 if(IS_PLAYER(targ) && targ.vehicle)
605 vehicles_exit(targ.vehicle, VHEF_RELEASE);
607 // These are ALWAYS lethal
608 // No damage modification here
609 // Instead, prepare the victim for his death...
610 SetResourceExplicit(targ, RES_ARMOR, 0);
611 targ.spawnshieldtime = 0;
612 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
613 targ.flags -= targ.flags & FL_GODMODE;
616 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
622 // nullify damage if teamplay is on
623 if(deathtype != DEATH_TELEFRAG.m_id)
624 if(IS_PLAYER(attacker))
626 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
631 else if(SAME_TEAM(attacker, targ))
633 if(autocvar_teamplay_mode == 1)
635 else if(attacker != targ)
637 if(autocvar_teamplay_mode == 2)
639 if(IS_PLAYER(targ) && !IS_DEAD(targ))
641 attacker.dmg_team = attacker.dmg_team + damage;
642 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
645 else if(autocvar_teamplay_mode == 3)
647 else if(autocvar_teamplay_mode == 4)
649 if(IS_PLAYER(targ) && !IS_DEAD(targ))
651 attacker.dmg_team = attacker.dmg_team + damage;
652 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
653 if(complainteamdamage > 0)
654 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
655 mirrorforce = autocvar_g_mirrordamage * vlen(force);
656 damage = autocvar_g_friendlyfire * damage;
657 // mirrordamage will be used LATER
659 if(autocvar_g_mirrordamage_virtual)
661 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
662 attacker.dmg_take += v.x;
663 attacker.dmg_save += v.y;
664 attacker.dmg_inflictor = inflictor;
669 if(autocvar_g_friendlyfire_virtual)
671 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
672 targ.dmg_take += v.x;
673 targ.dmg_save += v.y;
674 targ.dmg_inflictor = inflictor;
676 if(!autocvar_g_friendlyfire_virtual_force)
680 else if(!targ.canteamdamage)
687 if (!DEATH_ISSPECIAL(deathtype))
689 damage *= autocvar_g_weapondamagefactor;
690 mirrordamage *= autocvar_g_weapondamagefactor;
691 complainteamdamage *= autocvar_g_weapondamagefactor;
692 force = force * autocvar_g_weaponforcefactor;
693 mirrorforce *= autocvar_g_weaponforcefactor;
696 // should this be changed at all? If so, in what way?
697 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
698 damage = M_ARGV(4, float);
699 mirrordamage = M_ARGV(5, float);
700 force = M_ARGV(6, vector);
702 if(IS_PLAYER(targ) && damage > 0 && attacker)
704 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
706 .entity went = weaponentities[slot];
707 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
708 RemoveHook(targ.(went).hook);
712 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
713 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
715 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
717 Unfreeze(targ, false);
718 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
719 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
720 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
721 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
725 force *= autocvar_g_frozen_force;
728 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
729 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
731 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
733 entity spot = SelectSpawnPoint(targ, false);
737 targ.deadflag = DEAD_NO;
739 targ.angles = spot.angles;
742 targ.effects |= EF_TELEPORT_BIT;
744 targ.angles_z = 0; // never spawn tilted even if the spot says to
745 targ.fixangle = true; // turn this way immediately
746 targ.velocity = '0 0 0';
747 targ.avelocity = '0 0 0';
748 targ.punchangle = '0 0 0';
749 targ.punchvector = '0 0 0';
750 targ.oldvelocity = targ.velocity;
752 targ.spawnorigin = spot.origin;
753 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
754 // don't reset back to last position, even if new position is stuck in solid
755 targ.oldorigin = targ.origin;
757 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
761 if(!MUTATOR_IS_ENABLED(mutator_instagib))
763 // apply strength multiplier
764 if (attacker.items & ITEM_Strength.m_itemid)
768 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
769 force = force * autocvar_g_balance_powerup_strength_selfforce;
773 damage = damage * autocvar_g_balance_powerup_strength_damage;
774 force = force * autocvar_g_balance_powerup_strength_force;
778 // apply invincibility multiplier
779 if (targ.items & ITEM_Shield.m_itemid)
781 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
782 if (targ != attacker)
784 force = force * autocvar_g_balance_powerup_invincible_takeforce;
789 if (targ == attacker)
790 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
795 if(deathtype != DEATH_BUFF.m_id)
796 if(targ.takedamage == DAMAGE_AIM)
800 if(IS_VEHICLE(targ) && targ.owner)
805 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
807 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
811 if(deathtype != DEATH_FIRE.m_id)
813 if(PHYS_INPUT_BUTTON_CHAT(victim))
814 attacker.typehitsound += 1;
816 attacker.damage_dealt += damage;
819 damage_goodhits += 1;
820 damage_gooddamage += damage;
822 if (!DEATH_ISSPECIAL(deathtype))
824 if(IS_PLAYER(targ)) // don't do this for vehicles
830 else if(IS_PLAYER(attacker))
832 // if enemy gets frozen in this frame and receives other damage don't
833 // play the typehitsound e.g. when hit by multiple bullets of the shotgun
834 if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
836 attacker.typehitsound += 1;
838 if(complainteamdamage > 0)
839 if(time > CS(attacker).teamkill_complain)
841 CS(attacker).teamkill_complain = time + 5;
842 CS(attacker).teamkill_soundtime = time + 0.4;
843 CS(attacker).teamkill_soundsource = targ;
851 if (targ.damageforcescale)
853 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
855 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
856 if(targ.move_movetype == MOVETYPE_PHYSICS)
858 entity farcent = new(farce);
859 farcent.enemy = targ;
860 farcent.movedir = farce * 10;
862 farcent.movedir = farcent.movedir * targ.mass;
863 farcent.origin = hitloc;
864 farcent.forcetype = FORCETYPE_FORCEATPOS;
865 farcent.nextthink = time + 0.1;
866 setthink(farcent, SUB_Remove);
868 else if(targ.move_movetype != MOVETYPE_NOCLIP)
870 targ.velocity = targ.velocity + farce;
872 UNSET_ONGROUND(targ);
873 UpdateCSQCProjectile(targ);
876 if (damage != 0 || (targ.damageforcescale && force))
877 if (targ.event_damage)
878 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
880 // apply mirror damage if any
881 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
882 if(mirrordamage > 0 || mirrorforce > 0)
884 attacker = attacker_save;
886 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
887 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
891 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
892 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
893 // Returns total damage applies to creatures
897 float total_damage_to_creatures;
902 float stat_damagedone;
904 if(RadiusDamage_running)
906 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
910 RadiusDamage_running = 1;
912 tfloordmg = autocvar_g_throughfloor_damage;
913 tfloorforce = autocvar_g_throughfloor_force;
915 total_damage_to_creatures = 0;
917 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
918 if(!(deathtype & HITTYPE_SOUND)) // do not send radial sound damage (bandwidth hog)
920 force = inflictorvelocity;
924 force = normalize(force);
925 if(forceintensity >= 0)
926 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
928 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
933 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
937 if ((targ != inflictor) || inflictorselfdamage)
938 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
945 // LordHavoc: measure distance to nearest point on target (not origin)
946 // (this guarentees 100% damage on a touch impact)
947 nearest = targ.WarpZone_findradius_nearest;
948 diff = targ.WarpZone_findradius_dist;
949 // round up a little on the damage to ensure full damage on impacts
950 // and turn the distance into a fraction of the radius
951 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
953 //bprint(ftos(power));
954 //if (targ == attacker)
955 // print(ftos(power), "\n");
961 finaldmg = coredamage * power + edgedamage * (1 - power);
967 vector myblastorigin;
970 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
972 // if it's a player, use the view origin as reference
973 center = CENTER_OR_VIEWOFS(targ);
975 force = normalize(center - myblastorigin);
976 force = force * (finaldmg / coredamage) * forceintensity;
979 // apply special scaling along the z axis if set
980 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
982 force.z *= forcezscale;
984 if(targ != directhitentity)
989 float mininv_f, mininv_d;
991 // test line of sight to multiple positions on box,
992 // and do damage if any of them hit
995 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
996 // so for a given max stddev:
997 // n = (1 / (2 * max stddev of hitratio))^2
999 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
1000 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
1002 if(autocvar_g_throughfloor_debug)
1003 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1006 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1008 if(autocvar_g_throughfloor_debug)
1009 LOG_INFOF(" steps=%f", total);
1012 if (IS_PLAYER(targ))
1013 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1015 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1017 if(autocvar_g_throughfloor_debug)
1018 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1020 for(c = 0; c < total; ++c)
1022 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1023 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1024 if (trace_fraction == 1 || trace_ent == targ)
1028 hitloc = hitloc + nearest;
1032 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1033 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1034 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1037 nearest = hitloc * (1 / max(1, hits));
1038 hitratio = (hits / total);
1039 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1040 finaldmg = finaldmg * a;
1041 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1044 if(autocvar_g_throughfloor_debug)
1045 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1048 //if (targ == attacker)
1050 // print("hits ", ftos(hits), " / ", ftos(total));
1051 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1052 // print(" (", ftos(a), ")\n");
1054 if(finaldmg || force)
1058 total_damage_to_creatures += finaldmg;
1060 if(accuracy_isgooddamage(attacker, targ))
1061 stat_damagedone += finaldmg;
1064 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1065 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1067 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1075 RadiusDamage_running = 0;
1077 if(!DEATH_ISSPECIAL(deathtype))
1078 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1080 return total_damage_to_creatures;
1083 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1085 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1086 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1089 bool Heal(entity targ, entity inflictor, float amount, float limit)
1091 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1094 bool healed = false;
1096 healed = targ.event_heal(targ, inflictor, amount, limit);
1097 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1098 // TODO: healing fx!
1099 // TODO: armor healing?
1103 float Fire_IsBurning(entity e)
1105 return (time < e.fire_endtime);
1108 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1111 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1122 // print("adding a fire burner to ", e.classname, "\n");
1123 e.fire_burner = new(fireburner);
1124 setthink(e.fire_burner, fireburner_think);
1125 e.fire_burner.nextthink = time;
1126 e.fire_burner.owner = e;
1132 if(Fire_IsBurning(e))
1134 mintime = e.fire_endtime - time;
1135 maxtime = max(mintime, t);
1137 mindps = e.fire_damagepersec;
1138 maxdps = max(mindps, dps);
1140 if(maxtime > mintime || maxdps > mindps)
1144 // damage we have right now
1145 mindamage = mindps * mintime;
1147 // damage we want to get
1148 maxdamage = mindamage + d;
1150 // but we can't exceed maxtime * maxdps!
1151 totaldamage = min(maxdamage, maxtime * maxdps);
1155 // totaldamage = min(mindamage + d, maxtime * maxdps)
1157 // totaldamage <= maxtime * maxdps
1158 // ==> totaldamage / maxdps <= maxtime.
1160 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1161 // >= min(mintime, maxtime)
1162 // ==> totaldamage / maxdps >= mintime.
1165 // how long do we damage then?
1166 // at least as long as before
1167 // but, never exceed maxdps
1168 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1172 // at most as long as maximum allowed
1173 // but, never below mindps
1174 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1176 // assuming t > mintime, dps > mindps:
1177 // we get d = t * dps = maxtime * maxdps
1178 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1179 // totaldamage / maxdps = maxtime
1180 // totaldamage / mindps > totaldamage / maxdps = maxtime
1182 // a) totaltime = max(mintime, maxtime) = maxtime
1183 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1185 // assuming t <= mintime:
1186 // we get maxtime = mintime
1187 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1188 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1190 // assuming dps <= mindps:
1191 // we get mindps = maxdps.
1192 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1193 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1194 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1196 e.fire_damagepersec = totaldamage / totaltime;
1197 e.fire_endtime = time + totaltime;
1198 if(totaldamage > 1.2 * mindamage)
1200 e.fire_deathtype = dt;
1201 if(e.fire_owner != o)
1204 e.fire_hitsound = false;
1207 if(accuracy_isgooddamage(o, e))
1208 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1209 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1216 e.fire_damagepersec = dps;
1217 e.fire_endtime = time + t;
1218 e.fire_deathtype = dt;
1220 e.fire_hitsound = false;
1221 if(accuracy_isgooddamage(o, e))
1222 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1227 void Fire_ApplyDamage(entity e)
1232 if (!Fire_IsBurning(e))
1235 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1236 if(IS_NOT_A_CLIENT(o))
1239 // water and slime stop fire
1241 if(e.watertype != CONTENT_LAVA)
1248 t = min(frametime, e.fire_endtime - time);
1249 d = e.fire_damagepersec * t;
1251 hi = e.fire_owner.damage_dealt;
1252 ty = e.fire_owner.typehitsound;
1253 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1254 if(e.fire_hitsound && e.fire_owner)
1256 e.fire_owner.damage_dealt = hi;
1257 e.fire_owner.typehitsound = ty;
1259 e.fire_hitsound = true;
1261 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1263 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1265 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1266 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1268 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1269 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1270 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1276 void Fire_ApplyEffect(entity e)
1278 if(Fire_IsBurning(e))
1279 e.effects |= EF_FLAME;
1281 e.effects &= ~EF_FLAME;
1284 void fireburner_think(entity this)
1286 // for players, this is done in the regular loop
1287 if(wasfreed(this.owner))
1292 Fire_ApplyEffect(this.owner);
1293 if(!Fire_IsBurning(this.owner))
1295 this.owner.fire_burner = NULL;
1299 Fire_ApplyDamage(this.owner);
1300 this.nextthink = time;