3 #include <common/effects/all.qh>
6 #include <server/gamelog.qh>
7 #include <server/mutators/_mod.qh>
10 #include "spawnpoints.qh"
11 #include "../common/state.qh"
12 #include "../common/physics/player.qh"
13 #include "../common/t_items.qh"
14 #include "resources.qh"
15 #include "../common/vehicles/all.qh"
16 #include "../common/items/_mod.qh"
17 #include "../common/mutators/mutator/waypoints/waypointsprites.qh"
18 #include "../common/mutators/mutator/instagib/sv_instagib.qh"
19 #include "../common/mutators/mutator/buffs/buffs.qh"
20 #include "weapons/accuracy.qh"
21 #include "weapons/csqcprojectile.qh"
22 #include "weapons/selection.qh"
23 #include "../common/constants.qh"
24 #include "../common/deathtypes/all.qh"
25 #include "../common/notifications/all.qh"
26 #include "../common/physics/movetypes/movetypes.qh"
27 #include "../common/playerstats.qh"
28 #include "../common/teams.qh"
29 #include "../common/util.qh"
30 #include <common/gamemodes/_mod.qh>
31 #include <common/gamemodes/rules.qh>
32 #include <common/weapons/_all.qh>
33 #include "../lib/csqcmodel/sv_model.qh"
34 #include "../lib/warpzone/common.qh"
36 void UpdateFrags(entity player, int f)
38 GameRules_scoring_add_team(player, SCORE, f);
41 void GiveFrags(entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
43 // TODO route through PlayerScores instead
44 if(game_stopped) return;
51 GameRules_scoring_add(attacker, SUICIDES, 1);
56 GameRules_scoring_add(attacker, TEAMKILLS, 1);
62 GameRules_scoring_add(attacker, KILLS, 1);
63 if(!warmup_stage && targ.playerid)
64 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
67 GameRules_scoring_add(targ, DEATHS, 1);
69 // FIXME fix the mess this is (we have REAL points now!)
70 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
73 attacker.totalfrags += f;
76 UpdateFrags(attacker, f);
79 string AppendItemcodes(string s, entity player)
81 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
83 .entity weaponentity = weaponentities[slot];
84 int w = player.(weaponentity).m_weapon.m_id;
86 w = player.(weaponentity).cnt; // previous weapon
87 if(w != 0 || slot == 0)
88 s = strcat(s, ftos(w));
90 if(time < STAT(STRENGTH_FINISHED, player))
92 if(time < STAT(INVINCIBLE_FINISHED, player))
94 if(PHYS_INPUT_BUTTON_CHAT(player))
96 // TODO: include these codes as a flag on the item itself
97 MUTATOR_CALLHOOK(LogDeath_AppendItemCodes, player, s);
98 s = M_ARGV(1, string);
102 void LogDeath(string mode, int deathtype, entity killer, entity killed)
105 if(!autocvar_sv_eventlog)
107 s = strcat(":kill:", mode);
108 s = strcat(s, ":", ftos(killer.playerid));
109 s = strcat(s, ":", ftos(killed.playerid));
110 s = strcat(s, ":type=", Deathtype_Name(deathtype));
111 s = strcat(s, ":items=");
112 s = AppendItemcodes(s, killer);
115 s = strcat(s, ":victimitems=");
116 s = AppendItemcodes(s, killed);
121 void Obituary_SpecialDeath(
125 string s1, string s2, string s3,
126 float f1, float f2, float f3)
128 if(!DEATH_ISSPECIAL(deathtype))
130 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
134 entity deathent = REGISTRY_GET(Deathtypes, deathtype - DT_FIRST);
137 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
141 if(g_cts && deathtype == DEATH_KILL.m_id)
142 return; // TODO: somehow put this in CTS gamemode file!
144 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
147 Send_Notification_WOCOVA(
155 Send_Notification_WOCOVA(
159 death_message.nent_msginfo,
166 float Obituary_WeaponDeath(
170 string s1, string s2, string s3,
173 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
174 if (death_weapon == WEP_Null)
177 w_deathtype = deathtype;
178 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
183 Send_Notification_WOCOVA(
191 // send the info part to everyone
192 Send_Notification_WOCOVA(
196 death_message.nent_msginfo,
204 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %s!\n",
213 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target)
215 if(deathtype == DEATH_FIRE.m_id)
217 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
218 Send_Notification(NOTIF_ONE, targ, MSG_CHOICE, CHOICE_FRAGGED_FIRE, attacker.netname, kill_count_to_target, GetResource(attacker, RES_HEALTH), GetResource(attacker, RES_ARMOR), (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping));
222 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
225 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
228 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
231 float notif_firstblood = false;
232 float kill_count_to_attacker, kill_count_to_target;
234 // Set final information for the death
235 targ.death_origin = targ.origin;
236 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
238 #ifdef NOTIFICATIONS_DEBUG
241 "Obituary(%s, %s, %s, %s = %d);\n",
245 Deathtype_Name(deathtype),
256 if(DEATH_ISSPECIAL(deathtype))
258 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
260 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
264 switch(DEATH_ENT(deathtype))
266 case DEATH_MIRRORDAMAGE:
268 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
274 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
280 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
282 backtrace("SUICIDE: what the hell happened here?\n");
285 LogDeath("suicide", deathtype, targ, targ);
286 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
287 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
293 else if(IS_PLAYER(attacker))
295 if(SAME_TEAM(attacker, targ))
297 LogDeath("tk", deathtype, attacker, targ);
298 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
300 CS(attacker).killcount = 0;
302 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
303 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker.netname);
304 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker.netname, deathlocation, CS(targ).killcount);
306 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
307 // No need for specific death/weapon messages...
311 LogDeath("frag", deathtype, attacker, targ);
312 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
314 CS(attacker).taunt_soundtime = time + 1;
315 CS(attacker).killcount = CS(attacker).killcount + 1;
317 attacker.killsound += 1;
319 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
320 // these 2 macros are spread over multiple files
321 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
323 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
325 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
328 switch(CS(attacker).killcount)
335 if(!warmup_stage && !checkrules_firstblood)
337 checkrules_firstblood = true;
338 notif_firstblood = true; // modify the current messages so that they too show firstblood information
339 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
340 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
342 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
343 kill_count_to_attacker = -1;
344 kill_count_to_target = -2;
348 kill_count_to_attacker = CS(attacker).killcount;
349 kill_count_to_target = 0;
360 kill_count_to_attacker,
361 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
369 kill_count_to_target,
370 GetResource(attacker, RES_HEALTH),
371 GetResource(attacker, RES_ARMOR),
372 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
375 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target))
383 kill_count_to_attacker,
384 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
392 kill_count_to_target,
393 GetResource(attacker, RES_HEALTH),
394 GetResource(attacker, RES_ARMOR),
395 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
400 if(deathtype == DEATH_BUFF.m_id)
401 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
403 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker))
404 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
413 switch(DEATH_ENT(deathtype))
415 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
416 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
417 // and there will be a REAL DEATH_VOID implementation which mappers will use.
418 case DEATH_HURTTRIGGER:
420 Obituary_SpecialDeath(targ, false, deathtype,
432 Obituary_SpecialDeath(targ, false, deathtype,
434 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
444 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
449 LogDeath("accident", deathtype, targ, targ);
450 GiveFrags(targ, targ, -1, deathtype, weaponentity);
452 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
454 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
457 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
462 // reset target kill count
463 CS(targ).killcount = 0;
466 void Ice_Think(entity this)
468 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
473 vector ice_org = this.owner.origin - '0 0 16';
474 if (this.origin != ice_org)
475 setorigin(this, ice_org);
476 this.nextthink = time;
479 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
481 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
484 if(STAT(FROZEN, targ))
487 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
489 STAT(FROZEN, targ) = frozen_type;
490 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
491 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
492 targ.revive_speed = revivespeed;
494 IL_REMOVE(g_bot_targets, targ);
495 targ.bot_attack = false;
496 targ.freeze_time = time;
498 entity ice = new(ice);
500 ice.scale = targ.scale;
501 // set_movetype(ice, MOVETYPE_FOLLOW) would rotate the ice model with the player
502 setthink(ice, Ice_Think);
503 ice.nextthink = time;
504 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
505 setmodel(ice, MDL_ICE);
507 ice.colormod = Team_ColorRGB(targ.team);
508 ice.glowmod = ice.colormod;
510 targ.revival_time = 0;
514 RemoveGrapplingHooks(targ);
516 FOREACH_CLIENT(IS_PLAYER(it),
518 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
520 .entity weaponentity = weaponentities[slot];
521 if(it.(weaponentity).hook.aiment == targ)
522 RemoveHook(it.(weaponentity).hook);
527 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
528 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
531 void Unfreeze(entity targ, bool reset_health)
533 if(!STAT(FROZEN, targ))
536 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
537 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
539 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
541 STAT(FROZEN, targ) = 0;
542 STAT(REVIVE_PROGRESS, targ) = 0;
543 targ.revival_time = time;
545 IL_PUSH(g_bot_targets, targ);
546 targ.bot_attack = true;
548 WaypointSprite_Kill(targ.waypointsprite_attached);
550 FOREACH_CLIENT(IS_PLAYER(it),
552 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
554 .entity weaponentity = weaponentities[slot];
555 if(it.(weaponentity).hook.aiment == targ)
556 RemoveHook(it.(weaponentity).hook);
560 // remove the ice block
562 delete(targ.iceblock);
563 targ.iceblock = NULL;
565 MUTATOR_CALLHOOK(Unfreeze, targ);
568 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
570 float complainteamdamage = 0;
571 float mirrordamage = 0;
572 float mirrorforce = 0;
574 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
577 entity attacker_save = attacker;
579 // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
580 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
582 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
588 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
590 // exit the vehicle before killing (fixes a crash)
591 if(IS_PLAYER(targ) && targ.vehicle)
592 vehicles_exit(targ.vehicle, VHEF_RELEASE);
594 // These are ALWAYS lethal
595 // No damage modification here
596 // Instead, prepare the victim for his death...
597 SetResourceExplicit(targ, RES_ARMOR, 0);
598 targ.spawnshieldtime = 0;
599 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
600 targ.flags -= targ.flags & FL_GODMODE;
603 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
609 // nullify damage if teamplay is on
610 if(deathtype != DEATH_TELEFRAG.m_id)
611 if(IS_PLAYER(attacker))
613 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
618 else if(SAME_TEAM(attacker, targ))
620 if(autocvar_teamplay_mode == 1)
622 else if(attacker != targ)
624 if(autocvar_teamplay_mode == 2)
626 if(IS_PLAYER(targ) && !IS_DEAD(targ))
628 attacker.dmg_team = attacker.dmg_team + damage;
629 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
632 else if(autocvar_teamplay_mode == 3)
634 else if(autocvar_teamplay_mode == 4)
636 if(IS_PLAYER(targ) && !IS_DEAD(targ))
638 attacker.dmg_team = attacker.dmg_team + damage;
639 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
640 if(complainteamdamage > 0)
641 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
642 mirrorforce = autocvar_g_mirrordamage * vlen(force);
643 damage = autocvar_g_friendlyfire * damage;
644 // mirrordamage will be used LATER
646 if(autocvar_g_mirrordamage_virtual)
648 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
649 attacker.dmg_take += v.x;
650 attacker.dmg_save += v.y;
651 attacker.dmg_inflictor = inflictor;
656 if(autocvar_g_friendlyfire_virtual)
658 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
659 targ.dmg_take += v.x;
660 targ.dmg_save += v.y;
661 targ.dmg_inflictor = inflictor;
663 if(!autocvar_g_friendlyfire_virtual_force)
667 else if(!targ.canteamdamage)
674 if (!DEATH_ISSPECIAL(deathtype))
676 damage *= g_weapondamagefactor;
677 mirrordamage *= g_weapondamagefactor;
678 complainteamdamage *= g_weapondamagefactor;
679 force = force * g_weaponforcefactor;
680 mirrorforce *= g_weaponforcefactor;
683 // should this be changed at all? If so, in what way?
684 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
685 damage = M_ARGV(4, float);
686 mirrordamage = M_ARGV(5, float);
687 force = M_ARGV(6, vector);
689 if(IS_PLAYER(targ) && damage > 0 && attacker)
691 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
693 .entity went = weaponentities[slot];
694 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
695 RemoveHook(targ.(went).hook);
699 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
700 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
702 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
704 Unfreeze(targ, false);
705 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
706 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
707 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
708 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
712 force *= autocvar_g_frozen_force;
715 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
716 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
718 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
720 entity spot = SelectSpawnPoint(targ, false);
724 targ.deadflag = DEAD_NO;
726 targ.angles = spot.angles;
729 targ.effects |= EF_TELEPORT_BIT;
731 targ.angles_z = 0; // never spawn tilted even if the spot says to
732 targ.fixangle = true; // turn this way immediately
733 targ.velocity = '0 0 0';
734 targ.avelocity = '0 0 0';
735 targ.punchangle = '0 0 0';
736 targ.punchvector = '0 0 0';
737 targ.oldvelocity = targ.velocity;
739 targ.spawnorigin = spot.origin;
740 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
741 // don't reset back to last position, even if new position is stuck in solid
742 targ.oldorigin = targ.origin;
744 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
748 if(!MUTATOR_IS_ENABLED(mutator_instagib))
750 // apply strength multiplier
751 if (attacker.items & ITEM_Strength.m_itemid)
755 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
756 force = force * autocvar_g_balance_powerup_strength_selfforce;
760 damage = damage * autocvar_g_balance_powerup_strength_damage;
761 force = force * autocvar_g_balance_powerup_strength_force;
765 // apply invincibility multiplier
766 if (targ.items & ITEM_Shield.m_itemid)
768 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
769 if (targ != attacker)
771 force = force * autocvar_g_balance_powerup_invincible_takeforce;
776 if (targ == attacker)
777 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
782 if(deathtype != DEATH_BUFF.m_id)
783 if(targ.takedamage == DAMAGE_AIM)
787 if(IS_VEHICLE(targ) && targ.owner)
792 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
794 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
798 if(deathtype != DEATH_FIRE.m_id)
800 if(PHYS_INPUT_BUTTON_CHAT(victim))
801 attacker.typehitsound += 1;
803 attacker.damage_dealt += damage;
806 damage_goodhits += 1;
807 damage_gooddamage += damage;
809 if (!DEATH_ISSPECIAL(deathtype))
811 if(IS_PLAYER(targ)) // don't do this for vehicles
817 else if(IS_PLAYER(attacker))
819 // if enemy gets frozen in this frame and receives other damage don't
820 // play the typehitsound e.g. when hit by multiple bullets of the shotgun
821 if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
823 attacker.typehitsound += 1;
825 if(complainteamdamage > 0)
826 if(time > CS(attacker).teamkill_complain)
828 CS(attacker).teamkill_complain = time + 5;
829 CS(attacker).teamkill_soundtime = time + 0.4;
830 CS(attacker).teamkill_soundsource = targ;
838 if (targ.damageforcescale)
840 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
842 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
843 if(targ.move_movetype == MOVETYPE_PHYSICS)
845 entity farcent = new(farce);
846 farcent.enemy = targ;
847 farcent.movedir = farce * 10;
849 farcent.movedir = farcent.movedir * targ.mass;
850 farcent.origin = hitloc;
851 farcent.forcetype = FORCETYPE_FORCEATPOS;
852 farcent.nextthink = time + 0.1;
853 setthink(farcent, SUB_Remove);
857 targ.velocity = targ.velocity + farce;
859 UNSET_ONGROUND(targ);
860 UpdateCSQCProjectile(targ);
863 if (damage != 0 || (targ.damageforcescale && force))
864 if (targ.event_damage)
865 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
867 // apply mirror damage if any
868 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
869 if(mirrordamage > 0 || mirrorforce > 0)
871 attacker = attacker_save;
873 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
874 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
878 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
879 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
880 // Returns total damage applies to creatures
884 float total_damage_to_creatures;
889 float stat_damagedone;
891 if(RadiusDamage_running)
893 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
897 RadiusDamage_running = 1;
899 tfloordmg = autocvar_g_throughfloor_damage;
900 tfloorforce = autocvar_g_throughfloor_force;
902 total_damage_to_creatures = 0;
904 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
905 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
907 force = inflictorvelocity;
911 force = normalize(force);
912 if(forceintensity >= 0)
913 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
915 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
920 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
924 if ((targ != inflictor) || inflictorselfdamage)
925 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
932 // LordHavoc: measure distance to nearest point on target (not origin)
933 // (this guarentees 100% damage on a touch impact)
934 nearest = targ.WarpZone_findradius_nearest;
935 diff = targ.WarpZone_findradius_dist;
936 // round up a little on the damage to ensure full damage on impacts
937 // and turn the distance into a fraction of the radius
938 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
940 //bprint(ftos(power));
941 //if (targ == attacker)
942 // print(ftos(power), "\n");
948 finaldmg = coredamage * power + edgedamage * (1 - power);
954 vector myblastorigin;
957 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
959 // if it's a player, use the view origin as reference
960 center = CENTER_OR_VIEWOFS(targ);
962 force = normalize(center - myblastorigin);
963 force = force * (finaldmg / coredamage) * forceintensity;
966 // apply special scaling along the z axis if set
967 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
969 force.z *= forcezscale;
971 if(targ != directhitentity)
976 float mininv_f, mininv_d;
978 // test line of sight to multiple positions on box,
979 // and do damage if any of them hit
982 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
983 // so for a given max stddev:
984 // n = (1 / (2 * max stddev of hitratio))^2
986 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
987 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
989 if(autocvar_g_throughfloor_debug)
990 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
993 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
995 if(autocvar_g_throughfloor_debug)
996 LOG_INFOF(" steps=%f", total);
1000 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1002 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1004 if(autocvar_g_throughfloor_debug)
1005 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1007 for(c = 0; c < total; ++c)
1009 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1010 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1011 if (trace_fraction == 1 || trace_ent == targ)
1015 hitloc = hitloc + nearest;
1019 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1020 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1021 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1024 nearest = hitloc * (1 / max(1, hits));
1025 hitratio = (hits / total);
1026 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1027 finaldmg = finaldmg * a;
1028 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1031 if(autocvar_g_throughfloor_debug)
1032 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1035 //if (targ == attacker)
1037 // print("hits ", ftos(hits), " / ", ftos(total));
1038 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1039 // print(" (", ftos(a), ")\n");
1041 if(finaldmg || force)
1045 total_damage_to_creatures += finaldmg;
1047 if(accuracy_isgooddamage(attacker, targ))
1048 stat_damagedone += finaldmg;
1051 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1052 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1054 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1062 RadiusDamage_running = 0;
1064 if(!DEATH_ISSPECIAL(deathtype))
1065 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1067 return total_damage_to_creatures;
1070 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1072 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1073 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1076 bool Heal(entity targ, entity inflictor, float amount, float limit)
1078 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1081 bool healed = false;
1083 healed = targ.event_heal(targ, inflictor, amount, limit);
1084 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1085 // TODO: healing fx!
1086 // TODO: armor healing?
1090 float Fire_IsBurning(entity e)
1092 return (time < e.fire_endtime);
1095 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1098 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1109 // print("adding a fire burner to ", e.classname, "\n");
1110 e.fire_burner = new(fireburner);
1111 setthink(e.fire_burner, fireburner_think);
1112 e.fire_burner.nextthink = time;
1113 e.fire_burner.owner = e;
1119 if(Fire_IsBurning(e))
1121 mintime = e.fire_endtime - time;
1122 maxtime = max(mintime, t);
1124 mindps = e.fire_damagepersec;
1125 maxdps = max(mindps, dps);
1127 if(maxtime > mintime || maxdps > mindps)
1131 // damage we have right now
1132 mindamage = mindps * mintime;
1134 // damage we want to get
1135 maxdamage = mindamage + d;
1137 // but we can't exceed maxtime * maxdps!
1138 totaldamage = min(maxdamage, maxtime * maxdps);
1142 // totaldamage = min(mindamage + d, maxtime * maxdps)
1144 // totaldamage <= maxtime * maxdps
1145 // ==> totaldamage / maxdps <= maxtime.
1147 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1148 // >= min(mintime, maxtime)
1149 // ==> totaldamage / maxdps >= mintime.
1152 // how long do we damage then?
1153 // at least as long as before
1154 // but, never exceed maxdps
1155 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1159 // at most as long as maximum allowed
1160 // but, never below mindps
1161 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1163 // assuming t > mintime, dps > mindps:
1164 // we get d = t * dps = maxtime * maxdps
1165 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1166 // totaldamage / maxdps = maxtime
1167 // totaldamage / mindps > totaldamage / maxdps = maxtime
1169 // a) totaltime = max(mintime, maxtime) = maxtime
1170 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1172 // assuming t <= mintime:
1173 // we get maxtime = mintime
1174 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1175 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1177 // assuming dps <= mindps:
1178 // we get mindps = maxdps.
1179 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1180 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1181 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1183 e.fire_damagepersec = totaldamage / totaltime;
1184 e.fire_endtime = time + totaltime;
1185 if(totaldamage > 1.2 * mindamage)
1187 e.fire_deathtype = dt;
1188 if(e.fire_owner != o)
1191 e.fire_hitsound = false;
1194 if(accuracy_isgooddamage(o, e))
1195 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1196 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1203 e.fire_damagepersec = dps;
1204 e.fire_endtime = time + t;
1205 e.fire_deathtype = dt;
1207 e.fire_hitsound = false;
1208 if(accuracy_isgooddamage(o, e))
1209 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1214 void Fire_ApplyDamage(entity e)
1219 if (!Fire_IsBurning(e))
1222 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1223 if(IS_NOT_A_CLIENT(o))
1226 // water and slime stop fire
1228 if(e.watertype != CONTENT_LAVA)
1235 t = min(frametime, e.fire_endtime - time);
1236 d = e.fire_damagepersec * t;
1238 hi = e.fire_owner.damage_dealt;
1239 ty = e.fire_owner.typehitsound;
1240 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1241 if(e.fire_hitsound && e.fire_owner)
1243 e.fire_owner.damage_dealt = hi;
1244 e.fire_owner.typehitsound = ty;
1246 e.fire_hitsound = true;
1248 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1250 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1252 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1253 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1255 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1256 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1257 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1263 void Fire_ApplyEffect(entity e)
1265 if(Fire_IsBurning(e))
1266 e.effects |= EF_FLAME;
1268 e.effects &= ~EF_FLAME;
1271 void fireburner_think(entity this)
1273 // for players, this is done in the regular loop
1274 if(wasfreed(this.owner))
1279 Fire_ApplyEffect(this.owner);
1280 if(!Fire_IsBurning(this.owner))
1282 this.owner.fire_burner = NULL;
1286 Fire_ApplyDamage(this.owner);
1287 this.nextthink = time;