3 #include <common/effects/all.qh>
6 #include <server/mutators/_mod.qh>
9 #include "spawnpoints.qh"
10 #include "../common/state.qh"
11 #include "../common/physics/player.qh"
12 #include "../common/t_items.qh"
13 #include "resources.qh"
14 #include "../common/vehicles/all.qh"
15 #include "../common/items/_mod.qh"
16 #include "../common/mutators/mutator/waypoints/waypointsprites.qh"
17 #include "../common/mutators/mutator/instagib/sv_instagib.qh"
18 #include "../common/mutators/mutator/buffs/buffs.qh"
19 #include "weapons/accuracy.qh"
20 #include "weapons/csqcprojectile.qh"
21 #include "weapons/selection.qh"
22 #include "../common/constants.qh"
23 #include "../common/deathtypes/all.qh"
24 #include "../common/notifications/all.qh"
25 #include "../common/physics/movetypes/movetypes.qh"
26 #include "../common/playerstats.qh"
27 #include "../common/teams.qh"
28 #include "../common/util.qh"
29 #include <common/gamemodes/rules.qh>
30 #include <common/weapons/_all.qh>
31 #include "../lib/csqcmodel/sv_model.qh"
32 #include "../lib/warpzone/common.qh"
34 void UpdateFrags(entity player, int f)
36 GameRules_scoring_add_team(player, SCORE, f);
39 void GiveFrags(entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
41 // TODO route through PlayerScores instead
42 if(game_stopped) return;
49 GameRules_scoring_add(attacker, SUICIDES, 1);
54 GameRules_scoring_add(attacker, TEAMKILLS, 1);
60 GameRules_scoring_add(attacker, KILLS, 1);
61 if(!warmup_stage && targ.playerid)
62 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
65 GameRules_scoring_add(targ, DEATHS, 1);
67 // FIXME fix the mess this is (we have REAL points now!)
68 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
71 attacker.totalfrags += f;
74 UpdateFrags(attacker, f);
77 string AppendItemcodes(string s, entity player)
79 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
81 .entity weaponentity = weaponentities[slot];
82 int w = player.(weaponentity).m_weapon.m_id;
84 w = player.(weaponentity).cnt; // previous weapon
85 if(w != 0 || slot == 0)
86 s = strcat(s, ftos(w));
88 if(time < STAT(STRENGTH_FINISHED, player))
90 if(time < player.invincible_finished)
92 if(PHYS_INPUT_BUTTON_CHAT(player))
94 // TODO: include these codes as a flag on the item itself
95 MUTATOR_CALLHOOK(LogDeath_AppendItemCodes, player, s);
96 s = M_ARGV(1, string);
100 void LogDeath(string mode, int deathtype, entity killer, entity killed)
103 if(!autocvar_sv_eventlog)
105 s = strcat(":kill:", mode);
106 s = strcat(s, ":", ftos(killer.playerid));
107 s = strcat(s, ":", ftos(killed.playerid));
108 s = strcat(s, ":type=", Deathtype_Name(deathtype));
109 s = strcat(s, ":items=");
110 s = AppendItemcodes(s, killer);
113 s = strcat(s, ":victimitems=");
114 s = AppendItemcodes(s, killed);
119 void Obituary_SpecialDeath(
123 string s1, string s2, string s3,
124 float f1, float f2, float f3)
126 if(!DEATH_ISSPECIAL(deathtype))
128 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
132 entity deathent = Deathtypes_from(deathtype - DT_FIRST);
135 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
139 if(g_cts && deathtype == DEATH_KILL.m_id)
140 return; // TODO: somehow put this in CTS gamemode file!
142 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
145 Send_Notification_WOCOVA(
153 Send_Notification_WOCOVA(
157 death_message.nent_msginfo,
164 float Obituary_WeaponDeath(
168 string s1, string s2, string s3,
171 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
172 if (death_weapon == WEP_Null)
175 w_deathtype = deathtype;
176 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
181 Send_Notification_WOCOVA(
189 // send the info part to everyone
190 Send_Notification_WOCOVA(
194 death_message.nent_msginfo,
202 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %d!\n",
211 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target)
213 if(deathtype == DEATH_FIRE.m_id)
215 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
216 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));
220 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
223 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
226 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
229 float notif_firstblood = false;
230 float kill_count_to_attacker, kill_count_to_target;
232 // Set final information for the death
233 targ.death_origin = targ.origin;
234 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
236 #ifdef NOTIFICATIONS_DEBUG
239 "Obituary(%s, %s, %s, %s = %d);\n",
243 Deathtype_Name(deathtype),
254 if(DEATH_ISSPECIAL(deathtype))
256 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
258 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
262 switch(DEATH_ENT(deathtype))
264 case DEATH_MIRRORDAMAGE:
266 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
272 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
278 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
280 backtrace("SUICIDE: what the hell happened here?\n");
283 LogDeath("suicide", deathtype, targ, targ);
284 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
285 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
291 else if(IS_PLAYER(attacker))
293 if(SAME_TEAM(attacker, targ))
295 LogDeath("tk", deathtype, attacker, targ);
296 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
298 CS(attacker).killcount = 0;
300 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
301 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker.netname);
302 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker.netname, deathlocation, CS(targ).killcount);
304 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
305 // No need for specific death/weapon messages...
309 LogDeath("frag", deathtype, attacker, targ);
310 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
312 CS(attacker).taunt_soundtime = time + 1;
313 CS(attacker).killcount = CS(attacker).killcount + 1;
315 attacker.killsound += 1;
317 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
318 // these 2 macros are spread over multiple files
319 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
321 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
323 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
326 switch(CS(attacker).killcount)
333 if(!warmup_stage && !checkrules_firstblood)
335 checkrules_firstblood = true;
336 notif_firstblood = true; // modify the current messages so that they too show firstblood information
337 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
338 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
340 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
341 kill_count_to_attacker = -1;
342 kill_count_to_target = -2;
346 kill_count_to_attacker = CS(attacker).killcount;
347 kill_count_to_target = 0;
358 kill_count_to_attacker,
359 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
367 kill_count_to_target,
368 GetResource(attacker, RES_HEALTH),
369 GetResource(attacker, RES_ARMOR),
370 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
373 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target))
381 kill_count_to_attacker,
382 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
390 kill_count_to_target,
391 GetResource(attacker, RES_HEALTH),
392 GetResource(attacker, RES_ARMOR),
393 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
398 if(deathtype == DEATH_BUFF.m_id)
399 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
401 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker))
402 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
411 switch(DEATH_ENT(deathtype))
413 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
414 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
415 // and there will be a REAL DEATH_VOID implementation which mappers will use.
416 case DEATH_HURTTRIGGER:
418 Obituary_SpecialDeath(targ, false, deathtype,
430 Obituary_SpecialDeath(targ, false, deathtype,
432 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
442 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
447 LogDeath("accident", deathtype, targ, targ);
448 GiveFrags(targ, targ, -1, deathtype, weaponentity);
450 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
452 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
455 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
460 // reset target kill count
461 CS(targ).killcount = 0;
464 void Ice_Think(entity this)
466 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
471 setorigin(this, this.owner.origin - '0 0 16');
472 this.nextthink = time;
475 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
477 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
480 if(STAT(FROZEN, targ))
483 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
485 STAT(FROZEN, targ) = frozen_type;
486 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
487 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
488 targ.revive_speed = revivespeed;
490 IL_REMOVE(g_bot_targets, targ);
491 targ.bot_attack = false;
492 targ.freeze_time = time;
494 entity ice = new(ice);
496 ice.scale = targ.scale;
497 setthink(ice, Ice_Think);
498 ice.nextthink = time;
499 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
500 setmodel(ice, MDL_ICE);
502 ice.colormod = Team_ColorRGB(targ.team);
503 ice.glowmod = ice.colormod;
505 targ.revival_time = 0;
509 RemoveGrapplingHooks(targ);
511 FOREACH_CLIENT(IS_PLAYER(it),
513 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
515 .entity weaponentity = weaponentities[slot];
516 if(it.(weaponentity).hook.aiment == targ)
517 RemoveHook(it.(weaponentity).hook);
522 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
523 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
526 void Unfreeze(entity targ, bool reset_health)
528 if(!STAT(FROZEN, targ))
531 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
532 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
534 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
536 STAT(FROZEN, targ) = 0;
537 STAT(REVIVE_PROGRESS, targ) = 0;
538 targ.revival_time = time;
540 IL_PUSH(g_bot_targets, targ);
541 targ.bot_attack = true;
543 WaypointSprite_Kill(targ.waypointsprite_attached);
545 FOREACH_CLIENT(IS_PLAYER(it),
547 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
549 .entity weaponentity = weaponentities[slot];
550 if(it.(weaponentity).hook.aiment == targ)
551 RemoveHook(it.(weaponentity).hook);
555 // remove the ice block
557 delete(targ.iceblock);
558 targ.iceblock = NULL;
560 MUTATOR_CALLHOOK(Unfreeze, targ);
563 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
565 float complainteamdamage = 0;
566 float mirrordamage = 0;
567 float mirrorforce = 0;
569 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
572 entity attacker_save = attacker;
574 // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
575 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
577 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
583 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
585 // exit the vehicle before killing (fixes a crash)
586 if(IS_PLAYER(targ) && targ.vehicle)
587 vehicles_exit(targ.vehicle, VHEF_RELEASE);
589 // These are ALWAYS lethal
590 // No damage modification here
591 // Instead, prepare the victim for his death...
592 SetResourceExplicit(targ, RES_ARMOR, 0);
593 targ.spawnshieldtime = 0;
594 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
595 targ.flags -= targ.flags & FL_GODMODE;
598 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
604 // nullify damage if teamplay is on
605 if(deathtype != DEATH_TELEFRAG.m_id)
606 if(IS_PLAYER(attacker))
608 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
613 else if(SAME_TEAM(attacker, targ))
615 if(autocvar_teamplay_mode == 1)
617 else if(attacker != targ)
619 if(autocvar_teamplay_mode == 3)
621 else if(autocvar_teamplay_mode == 4)
623 if(IS_PLAYER(targ) && !IS_DEAD(targ))
625 attacker.dmg_team = attacker.dmg_team + damage;
626 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
627 if(complainteamdamage > 0)
628 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
629 mirrorforce = autocvar_g_mirrordamage * vlen(force);
630 damage = autocvar_g_friendlyfire * damage;
631 // mirrordamage will be used LATER
633 if(autocvar_g_mirrordamage_virtual)
635 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
636 attacker.dmg_take += v.x;
637 attacker.dmg_save += v.y;
638 attacker.dmg_inflictor = inflictor;
643 if(autocvar_g_friendlyfire_virtual)
645 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
646 targ.dmg_take += v.x;
647 targ.dmg_save += v.y;
648 targ.dmg_inflictor = inflictor;
650 if(!autocvar_g_friendlyfire_virtual_force)
654 else if(!targ.canteamdamage)
661 if (!DEATH_ISSPECIAL(deathtype))
663 damage *= g_weapondamagefactor;
664 mirrordamage *= g_weapondamagefactor;
665 complainteamdamage *= g_weapondamagefactor;
666 force = force * g_weaponforcefactor;
667 mirrorforce *= g_weaponforcefactor;
670 // should this be changed at all? If so, in what way?
671 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
672 damage = M_ARGV(4, float);
673 mirrordamage = M_ARGV(5, float);
674 force = M_ARGV(6, vector);
676 if(IS_PLAYER(targ) && damage > 0 && attacker)
678 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
680 .entity went = weaponentities[slot];
681 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
682 RemoveHook(targ.(went).hook);
686 if(deathtype != DEATH_HURTTRIGGER.m_id && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id && STAT(FROZEN, targ))
688 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
690 Unfreeze(targ, false);
691 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
692 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
693 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
694 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
698 force *= autocvar_g_frozen_force;
701 if(IS_PLAYER(targ) && STAT(FROZEN, targ) && deathtype == DEATH_HURTTRIGGER.m_id && !autocvar_g_frozen_damage_trigger)
703 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
705 entity spot = SelectSpawnPoint (targ, false);
710 targ.deadflag = DEAD_NO;
712 targ.angles = spot.angles;
715 targ.effects |= EF_TELEPORT_BIT;
717 targ.angles_z = 0; // never spawn tilted even if the spot says to
718 targ.fixangle = true; // turn this way immediately
719 targ.velocity = '0 0 0';
720 targ.avelocity = '0 0 0';
721 targ.punchangle = '0 0 0';
722 targ.punchvector = '0 0 0';
723 targ.oldvelocity = targ.velocity;
725 targ.spawnorigin = spot.origin;
726 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
727 // don't reset back to last position, even if new position is stuck in solid
728 targ.oldorigin = targ.origin;
730 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
734 if(!MUTATOR_IS_ENABLED(mutator_instagib))
736 // apply strength multiplier
737 if (attacker.items & ITEM_Strength.m_itemid)
741 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
742 force = force * autocvar_g_balance_powerup_strength_selfforce;
746 damage = damage * autocvar_g_balance_powerup_strength_damage;
747 force = force * autocvar_g_balance_powerup_strength_force;
751 // apply invincibility multiplier
752 if (targ.items & ITEM_Shield.m_itemid)
754 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
755 if (targ != attacker)
757 force = force * autocvar_g_balance_powerup_invincible_takeforce;
762 if (targ == attacker)
763 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
768 if(deathtype != DEATH_BUFF.m_id)
769 if(targ.takedamage == DAMAGE_AIM)
773 if(IS_VEHICLE(targ) && targ.owner)
778 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
780 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
784 if(deathtype != DEATH_FIRE.m_id)
786 if(PHYS_INPUT_BUTTON_CHAT(victim))
787 attacker.typehitsound += 1;
789 attacker.damage_dealt += damage;
792 damage_goodhits += 1;
793 damage_gooddamage += damage;
795 if (!DEATH_ISSPECIAL(deathtype))
797 if(IS_PLAYER(targ)) // don't do this for vehicles
803 else if(IS_PLAYER(attacker))
805 // if enemy gets frozen in this frame and receives other damage don't
806 // play the typehitsound e.g. when hit by multiple bullets of the shotgun
807 if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
809 attacker.typehitsound += 1;
811 if(complainteamdamage > 0)
812 if(time > CS(attacker).teamkill_complain)
814 CS(attacker).teamkill_complain = time + 5;
815 CS(attacker).teamkill_soundtime = time + 0.4;
816 CS(attacker).teamkill_soundsource = targ;
824 if (targ.damageforcescale)
826 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
828 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
829 if(targ.move_movetype == MOVETYPE_PHYSICS)
831 entity farcent = new(farce);
832 farcent.enemy = targ;
833 farcent.movedir = farce * 10;
835 farcent.movedir = farcent.movedir * targ.mass;
836 farcent.origin = hitloc;
837 farcent.forcetype = FORCETYPE_FORCEATPOS;
838 farcent.nextthink = time + 0.1;
839 setthink(farcent, SUB_Remove);
843 targ.velocity = targ.velocity + farce;
845 UNSET_ONGROUND(targ);
846 UpdateCSQCProjectile(targ);
849 if (damage != 0 || (targ.damageforcescale && force))
850 if (targ.event_damage)
851 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
853 // apply mirror damage if any
854 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
855 if(mirrordamage > 0 || mirrorforce > 0)
857 attacker = attacker_save;
859 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
860 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
864 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
865 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
866 // Returns total damage applies to creatures
870 float total_damage_to_creatures;
875 float stat_damagedone;
877 if(RadiusDamage_running)
879 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
883 RadiusDamage_running = 1;
885 tfloordmg = autocvar_g_throughfloor_damage;
886 tfloorforce = autocvar_g_throughfloor_force;
888 total_damage_to_creatures = 0;
890 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
891 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
893 force = inflictorvelocity;
897 force = normalize(force);
898 if(forceintensity >= 0)
899 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
901 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
906 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
910 if ((targ != inflictor) || inflictorselfdamage)
911 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
918 // LordHavoc: measure distance to nearest point on target (not origin)
919 // (this guarentees 100% damage on a touch impact)
920 nearest = targ.WarpZone_findradius_nearest;
921 diff = targ.WarpZone_findradius_dist;
922 // round up a little on the damage to ensure full damage on impacts
923 // and turn the distance into a fraction of the radius
924 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
926 //bprint(ftos(power));
927 //if (targ == attacker)
928 // print(ftos(power), "\n");
934 finaldmg = coredamage * power + edgedamage * (1 - power);
940 vector myblastorigin;
943 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
945 // if it's a player, use the view origin as reference
946 center = CENTER_OR_VIEWOFS(targ);
948 force = normalize(center - myblastorigin);
949 force = force * (finaldmg / coredamage) * forceintensity;
952 // apply special scaling along the z axis if set
953 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
955 force.z *= forcezscale;
957 if(targ != directhitentity)
962 float mininv_f, mininv_d;
964 // test line of sight to multiple positions on box,
965 // and do damage if any of them hit
968 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
969 // so for a given max stddev:
970 // n = (1 / (2 * max stddev of hitratio))^2
972 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
973 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
975 if(autocvar_g_throughfloor_debug)
976 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
979 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
981 if(autocvar_g_throughfloor_debug)
982 LOG_INFOF(" steps=%f", total);
986 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
988 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
990 if(autocvar_g_throughfloor_debug)
991 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
993 for(c = 0; c < total; ++c)
995 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
996 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
997 if (trace_fraction == 1 || trace_ent == targ)
1001 hitloc = hitloc + nearest;
1005 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1006 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1007 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1010 nearest = hitloc * (1 / max(1, hits));
1011 hitratio = (hits / total);
1012 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1013 finaldmg = finaldmg * a;
1014 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1017 if(autocvar_g_throughfloor_debug)
1018 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1021 //if (targ == attacker)
1023 // print("hits ", ftos(hits), " / ", ftos(total));
1024 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1025 // print(" (", ftos(a), ")\n");
1027 if(finaldmg || force)
1031 total_damage_to_creatures += finaldmg;
1033 if(accuracy_isgooddamage(attacker, targ))
1034 stat_damagedone += finaldmg;
1037 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1038 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1040 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1048 RadiusDamage_running = 0;
1050 if(!DEATH_ISSPECIAL(deathtype))
1051 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1053 return total_damage_to_creatures;
1056 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1058 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1059 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1062 bool Heal(entity targ, entity inflictor, float amount, float limit)
1064 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1067 bool healed = false;
1069 healed = targ.event_heal(targ, inflictor, amount, limit);
1070 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1071 // TODO: healing fx!
1072 // TODO: armor healing?
1076 float Fire_IsBurning(entity e)
1078 return (time < e.fire_endtime);
1081 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1084 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1095 // print("adding a fire burner to ", e.classname, "\n");
1096 e.fire_burner = new(fireburner);
1097 setthink(e.fire_burner, fireburner_think);
1098 e.fire_burner.nextthink = time;
1099 e.fire_burner.owner = e;
1105 if(Fire_IsBurning(e))
1107 mintime = e.fire_endtime - time;
1108 maxtime = max(mintime, t);
1110 mindps = e.fire_damagepersec;
1111 maxdps = max(mindps, dps);
1113 if(maxtime > mintime || maxdps > mindps)
1117 // damage we have right now
1118 mindamage = mindps * mintime;
1120 // damage we want to get
1121 maxdamage = mindamage + d;
1123 // but we can't exceed maxtime * maxdps!
1124 totaldamage = min(maxdamage, maxtime * maxdps);
1128 // totaldamage = min(mindamage + d, maxtime * maxdps)
1130 // totaldamage <= maxtime * maxdps
1131 // ==> totaldamage / maxdps <= maxtime.
1133 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1134 // >= min(mintime, maxtime)
1135 // ==> totaldamage / maxdps >= mintime.
1138 // how long do we damage then?
1139 // at least as long as before
1140 // but, never exceed maxdps
1141 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1145 // at most as long as maximum allowed
1146 // but, never below mindps
1147 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1149 // assuming t > mintime, dps > mindps:
1150 // we get d = t * dps = maxtime * maxdps
1151 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1152 // totaldamage / maxdps = maxtime
1153 // totaldamage / mindps > totaldamage / maxdps = maxtime
1155 // a) totaltime = max(mintime, maxtime) = maxtime
1156 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1158 // assuming t <= mintime:
1159 // we get maxtime = mintime
1160 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1161 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1163 // assuming dps <= mindps:
1164 // we get mindps = maxdps.
1165 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1166 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1167 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1169 e.fire_damagepersec = totaldamage / totaltime;
1170 e.fire_endtime = time + totaltime;
1171 if(totaldamage > 1.2 * mindamage)
1173 e.fire_deathtype = dt;
1174 if(e.fire_owner != o)
1177 e.fire_hitsound = false;
1180 if(accuracy_isgooddamage(o, e))
1181 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1182 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1189 e.fire_damagepersec = dps;
1190 e.fire_endtime = time + t;
1191 e.fire_deathtype = dt;
1193 e.fire_hitsound = false;
1194 if(accuracy_isgooddamage(o, e))
1195 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1200 void Fire_ApplyDamage(entity e)
1205 if (!Fire_IsBurning(e))
1208 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1209 if(IS_NOT_A_CLIENT(o))
1212 // water and slime stop fire
1214 if(e.watertype != CONTENT_LAVA)
1221 t = min(frametime, e.fire_endtime - time);
1222 d = e.fire_damagepersec * t;
1224 hi = e.fire_owner.damage_dealt;
1225 ty = e.fire_owner.typehitsound;
1226 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1227 if(e.fire_hitsound && e.fire_owner)
1229 e.fire_owner.damage_dealt = hi;
1230 e.fire_owner.typehitsound = ty;
1232 e.fire_hitsound = true;
1234 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1236 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1238 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1239 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1241 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1242 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1243 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1249 void Fire_ApplyEffect(entity e)
1251 if(Fire_IsBurning(e))
1252 e.effects |= EF_FLAME;
1254 e.effects &= ~EF_FLAME;
1257 void fireburner_think(entity this)
1259 // for players, this is done in the regular loop
1260 if(wasfreed(this.owner))
1265 Fire_ApplyEffect(this.owner);
1266 if(!Fire_IsBurning(this.owner))
1268 this.owner.fire_burner = NULL;
1272 Fire_ApplyDamage(this.owner);
1273 this.nextthink = time;