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 < STAT(INVINCIBLE_FINISHED, player))
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(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
687 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
689 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
691 Unfreeze(targ, false);
692 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
693 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
694 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
695 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
699 force *= autocvar_g_frozen_force;
702 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
703 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
705 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
707 entity spot = SelectSpawnPoint(targ, false);
711 targ.deadflag = DEAD_NO;
713 targ.angles = spot.angles;
716 targ.effects |= EF_TELEPORT_BIT;
718 targ.angles_z = 0; // never spawn tilted even if the spot says to
719 targ.fixangle = true; // turn this way immediately
720 targ.velocity = '0 0 0';
721 targ.avelocity = '0 0 0';
722 targ.punchangle = '0 0 0';
723 targ.punchvector = '0 0 0';
724 targ.oldvelocity = targ.velocity;
726 targ.spawnorigin = spot.origin;
727 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
728 // don't reset back to last position, even if new position is stuck in solid
729 targ.oldorigin = targ.origin;
731 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
735 if(!MUTATOR_IS_ENABLED(mutator_instagib))
737 // apply strength multiplier
738 if (attacker.items & ITEM_Strength.m_itemid)
742 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
743 force = force * autocvar_g_balance_powerup_strength_selfforce;
747 damage = damage * autocvar_g_balance_powerup_strength_damage;
748 force = force * autocvar_g_balance_powerup_strength_force;
752 // apply invincibility multiplier
753 if (targ.items & ITEM_Shield.m_itemid)
755 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
756 if (targ != attacker)
758 force = force * autocvar_g_balance_powerup_invincible_takeforce;
763 if (targ == attacker)
764 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
769 if(deathtype != DEATH_BUFF.m_id)
770 if(targ.takedamage == DAMAGE_AIM)
774 if(IS_VEHICLE(targ) && targ.owner)
779 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
781 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
785 if(deathtype != DEATH_FIRE.m_id)
787 if(PHYS_INPUT_BUTTON_CHAT(victim))
788 attacker.typehitsound += 1;
790 attacker.damage_dealt += damage;
793 damage_goodhits += 1;
794 damage_gooddamage += damage;
796 if (!DEATH_ISSPECIAL(deathtype))
798 if(IS_PLAYER(targ)) // don't do this for vehicles
804 else if(IS_PLAYER(attacker))
806 // if enemy gets frozen in this frame and receives other damage don't
807 // play the typehitsound e.g. when hit by multiple bullets of the shotgun
808 if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
810 attacker.typehitsound += 1;
812 if(complainteamdamage > 0)
813 if(time > CS(attacker).teamkill_complain)
815 CS(attacker).teamkill_complain = time + 5;
816 CS(attacker).teamkill_soundtime = time + 0.4;
817 CS(attacker).teamkill_soundsource = targ;
825 if (targ.damageforcescale)
827 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
829 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
830 if(targ.move_movetype == MOVETYPE_PHYSICS)
832 entity farcent = new(farce);
833 farcent.enemy = targ;
834 farcent.movedir = farce * 10;
836 farcent.movedir = farcent.movedir * targ.mass;
837 farcent.origin = hitloc;
838 farcent.forcetype = FORCETYPE_FORCEATPOS;
839 farcent.nextthink = time + 0.1;
840 setthink(farcent, SUB_Remove);
844 targ.velocity = targ.velocity + farce;
846 UNSET_ONGROUND(targ);
847 UpdateCSQCProjectile(targ);
850 if (damage != 0 || (targ.damageforcescale && force))
851 if (targ.event_damage)
852 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
854 // apply mirror damage if any
855 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
856 if(mirrordamage > 0 || mirrorforce > 0)
858 attacker = attacker_save;
860 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
861 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
865 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
866 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
867 // Returns total damage applies to creatures
871 float total_damage_to_creatures;
876 float stat_damagedone;
878 if(RadiusDamage_running)
880 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
884 RadiusDamage_running = 1;
886 tfloordmg = autocvar_g_throughfloor_damage;
887 tfloorforce = autocvar_g_throughfloor_force;
889 total_damage_to_creatures = 0;
891 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
892 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
894 force = inflictorvelocity;
898 force = normalize(force);
899 if(forceintensity >= 0)
900 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
902 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
907 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
911 if ((targ != inflictor) || inflictorselfdamage)
912 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
919 // LordHavoc: measure distance to nearest point on target (not origin)
920 // (this guarentees 100% damage on a touch impact)
921 nearest = targ.WarpZone_findradius_nearest;
922 diff = targ.WarpZone_findradius_dist;
923 // round up a little on the damage to ensure full damage on impacts
924 // and turn the distance into a fraction of the radius
925 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
927 //bprint(ftos(power));
928 //if (targ == attacker)
929 // print(ftos(power), "\n");
935 finaldmg = coredamage * power + edgedamage * (1 - power);
941 vector myblastorigin;
944 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
946 // if it's a player, use the view origin as reference
947 center = CENTER_OR_VIEWOFS(targ);
949 force = normalize(center - myblastorigin);
950 force = force * (finaldmg / coredamage) * forceintensity;
953 // apply special scaling along the z axis if set
954 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
956 force.z *= forcezscale;
958 if(targ != directhitentity)
963 float mininv_f, mininv_d;
965 // test line of sight to multiple positions on box,
966 // and do damage if any of them hit
969 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
970 // so for a given max stddev:
971 // n = (1 / (2 * max stddev of hitratio))^2
973 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
974 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
976 if(autocvar_g_throughfloor_debug)
977 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
980 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
982 if(autocvar_g_throughfloor_debug)
983 LOG_INFOF(" steps=%f", total);
987 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
989 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
991 if(autocvar_g_throughfloor_debug)
992 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
994 for(c = 0; c < total; ++c)
996 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
997 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
998 if (trace_fraction == 1 || trace_ent == targ)
1002 hitloc = hitloc + nearest;
1006 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1007 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1008 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1011 nearest = hitloc * (1 / max(1, hits));
1012 hitratio = (hits / total);
1013 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1014 finaldmg = finaldmg * a;
1015 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1018 if(autocvar_g_throughfloor_debug)
1019 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1022 //if (targ == attacker)
1024 // print("hits ", ftos(hits), " / ", ftos(total));
1025 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1026 // print(" (", ftos(a), ")\n");
1028 if(finaldmg || force)
1032 total_damage_to_creatures += finaldmg;
1034 if(accuracy_isgooddamage(attacker, targ))
1035 stat_damagedone += finaldmg;
1038 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1039 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1041 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1049 RadiusDamage_running = 0;
1051 if(!DEATH_ISSPECIAL(deathtype))
1052 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1054 return total_damage_to_creatures;
1057 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1059 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1060 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1063 bool Heal(entity targ, entity inflictor, float amount, float limit)
1065 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1068 bool healed = false;
1070 healed = targ.event_heal(targ, inflictor, amount, limit);
1071 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1072 // TODO: healing fx!
1073 // TODO: armor healing?
1077 float Fire_IsBurning(entity e)
1079 return (time < e.fire_endtime);
1082 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1085 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1096 // print("adding a fire burner to ", e.classname, "\n");
1097 e.fire_burner = new(fireburner);
1098 setthink(e.fire_burner, fireburner_think);
1099 e.fire_burner.nextthink = time;
1100 e.fire_burner.owner = e;
1106 if(Fire_IsBurning(e))
1108 mintime = e.fire_endtime - time;
1109 maxtime = max(mintime, t);
1111 mindps = e.fire_damagepersec;
1112 maxdps = max(mindps, dps);
1114 if(maxtime > mintime || maxdps > mindps)
1118 // damage we have right now
1119 mindamage = mindps * mintime;
1121 // damage we want to get
1122 maxdamage = mindamage + d;
1124 // but we can't exceed maxtime * maxdps!
1125 totaldamage = min(maxdamage, maxtime * maxdps);
1129 // totaldamage = min(mindamage + d, maxtime * maxdps)
1131 // totaldamage <= maxtime * maxdps
1132 // ==> totaldamage / maxdps <= maxtime.
1134 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1135 // >= min(mintime, maxtime)
1136 // ==> totaldamage / maxdps >= mintime.
1139 // how long do we damage then?
1140 // at least as long as before
1141 // but, never exceed maxdps
1142 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1146 // at most as long as maximum allowed
1147 // but, never below mindps
1148 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1150 // assuming t > mintime, dps > mindps:
1151 // we get d = t * dps = maxtime * maxdps
1152 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1153 // totaldamage / maxdps = maxtime
1154 // totaldamage / mindps > totaldamage / maxdps = maxtime
1156 // a) totaltime = max(mintime, maxtime) = maxtime
1157 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1159 // assuming t <= mintime:
1160 // we get maxtime = mintime
1161 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1162 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1164 // assuming dps <= mindps:
1165 // we get mindps = maxdps.
1166 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1167 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1168 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1170 e.fire_damagepersec = totaldamage / totaltime;
1171 e.fire_endtime = time + totaltime;
1172 if(totaldamage > 1.2 * mindamage)
1174 e.fire_deathtype = dt;
1175 if(e.fire_owner != o)
1178 e.fire_hitsound = false;
1181 if(accuracy_isgooddamage(o, e))
1182 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1183 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1190 e.fire_damagepersec = dps;
1191 e.fire_endtime = time + t;
1192 e.fire_deathtype = dt;
1194 e.fire_hitsound = false;
1195 if(accuracy_isgooddamage(o, e))
1196 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1201 void Fire_ApplyDamage(entity e)
1206 if (!Fire_IsBurning(e))
1209 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1210 if(IS_NOT_A_CLIENT(o))
1213 // water and slime stop fire
1215 if(e.watertype != CONTENT_LAVA)
1222 t = min(frametime, e.fire_endtime - time);
1223 d = e.fire_damagepersec * t;
1225 hi = e.fire_owner.damage_dealt;
1226 ty = e.fire_owner.typehitsound;
1227 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1228 if(e.fire_hitsound && e.fire_owner)
1230 e.fire_owner.damage_dealt = hi;
1231 e.fire_owner.typehitsound = ty;
1233 e.fire_hitsound = true;
1235 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1237 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1239 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1240 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1242 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1243 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1244 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1250 void Fire_ApplyEffect(entity e)
1252 if(Fire_IsBurning(e))
1253 e.effects |= EF_FLAME;
1255 e.effects &= ~EF_FLAME;
1258 void fireburner_think(entity this)
1260 // for players, this is done in the regular loop
1261 if(wasfreed(this.owner))
1266 Fire_ApplyEffect(this.owner);
1267 if(!Fire_IsBurning(this.owner))
1269 this.owner.fire_burner = NULL;
1273 Fire_ApplyDamage(this.owner);
1274 this.nextthink = time;