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);
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 < player.strength_finished)
92 if(time < player.invincible_finished)
94 if(player.flagcarried != NULL)
96 if(PHYS_INPUT_BUTTON_CHAT(player))
103 void LogDeath(string mode, int deathtype, entity killer, entity killed)
106 if(!autocvar_sv_eventlog)
108 s = strcat(":kill:", mode);
109 s = strcat(s, ":", ftos(killer.playerid));
110 s = strcat(s, ":", ftos(killed.playerid));
111 s = strcat(s, ":type=", Deathtype_Name(deathtype));
112 s = strcat(s, ":items=");
113 s = AppendItemcodes(s, killer);
116 s = strcat(s, ":victimitems=");
117 s = AppendItemcodes(s, killed);
122 void Obituary_SpecialDeath(
126 string s1, string s2, string s3,
127 float f1, float f2, float f3)
129 if(!DEATH_ISSPECIAL(deathtype))
131 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
135 entity deathent = Deathtypes_from(deathtype - DT_FIRST);
138 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
142 if(g_cts && deathtype == DEATH_KILL.m_id)
143 return; // TODO: somehow put this in CTS gamemode file!
145 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
148 Send_Notification_WOCOVA(
156 Send_Notification_WOCOVA(
160 death_message.nent_msginfo,
167 float Obituary_WeaponDeath(
171 string s1, string s2, string s3,
174 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
175 if (death_weapon == WEP_Null)
178 w_deathtype = deathtype;
179 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
184 Send_Notification_WOCOVA(
192 // send the info part to everyone
193 Send_Notification_WOCOVA(
197 death_message.nent_msginfo,
205 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %d!\n",
214 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target)
216 if(deathtype == DEATH_FIRE.m_id)
218 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
219 Send_Notification(NOTIF_ONE, targ, MSG_CHOICE, CHOICE_FRAGGED_FIRE, attacker.netname, kill_count_to_target, GetResourceAmount(attacker, RESOURCE_HEALTH), GetResourceAmount(attacker, RESOURCE_ARMOR), (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping));
223 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
226 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
229 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
232 float notif_firstblood = false;
233 float kill_count_to_attacker, kill_count_to_target;
235 // Set final information for the death
236 targ.death_origin = targ.origin;
237 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
239 #ifdef NOTIFICATIONS_DEBUG
242 "Obituary(%s, %s, %s, %s = %d);\n",
246 Deathtype_Name(deathtype),
257 if(DEATH_ISSPECIAL(deathtype))
259 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
261 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
265 switch(DEATH_ENT(deathtype))
267 case DEATH_MIRRORDAMAGE:
269 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
275 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
281 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
283 backtrace("SUICIDE: what the hell happened here?\n");
286 LogDeath("suicide", deathtype, targ, targ);
287 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
288 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
294 else if(IS_PLAYER(attacker))
296 if(SAME_TEAM(attacker, targ))
298 LogDeath("tk", deathtype, attacker, targ);
299 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
301 CS(attacker).killcount = 0;
303 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
304 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker.netname);
305 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker.netname, deathlocation, CS(targ).killcount);
307 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
308 // No need for specific death/weapon messages...
312 LogDeath("frag", deathtype, attacker, targ);
313 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
315 CS(attacker).taunt_soundtime = time + 1;
316 CS(attacker).killcount = CS(attacker).killcount + 1;
318 attacker.killsound += 1;
320 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
321 // these 2 macros are spread over multiple files
322 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
325 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
328 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
332 switch(CS(attacker).killcount)
339 if(!warmup_stage && !checkrules_firstblood)
341 checkrules_firstblood = true;
342 notif_firstblood = true; // modify the current messages so that they too show firstblood information
343 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
344 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
346 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
347 kill_count_to_attacker = -1;
348 kill_count_to_target = -2;
352 kill_count_to_attacker = CS(attacker).killcount;
353 kill_count_to_target = 0;
364 kill_count_to_attacker,
365 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
373 kill_count_to_target,
374 GetResourceAmount(attacker, RESOURCE_HEALTH),
375 GetResourceAmount(attacker, RESOURCE_ARMOR),
376 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
379 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target))
387 kill_count_to_attacker,
388 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
396 kill_count_to_target,
397 GetResourceAmount(attacker, RESOURCE_HEALTH),
398 GetResourceAmount(attacker, RESOURCE_ARMOR),
399 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
404 if(deathtype == DEATH_BUFF.m_id)
405 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
407 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker))
408 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
417 switch(DEATH_ENT(deathtype))
419 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
420 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
421 // and there will be a REAL DEATH_VOID implementation which mappers will use.
422 case DEATH_HURTTRIGGER:
424 Obituary_SpecialDeath(targ, false, deathtype,
436 Obituary_SpecialDeath(targ, false, deathtype,
438 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
448 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
453 LogDeath("accident", deathtype, targ, targ);
454 GiveFrags(targ, targ, -1, deathtype, weaponentity);
456 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
458 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
461 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
466 // reset target kill count
467 CS(targ).killcount = 0;
470 void Ice_Think(entity this)
472 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
477 setorigin(this, this.owner.origin - '0 0 16');
478 this.nextthink = time;
481 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
483 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
486 if(STAT(FROZEN, targ))
489 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
491 STAT(FROZEN, targ) = frozen_type;
492 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
493 SetResourceAmount(targ, RESOURCE_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
494 targ.revive_speed = revivespeed;
496 IL_REMOVE(g_bot_targets, targ);
497 targ.bot_attack = false;
498 targ.freeze_time = time;
500 entity ice = new(ice);
502 ice.scale = targ.scale;
503 setthink(ice, Ice_Think);
504 ice.nextthink = time;
505 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
506 setmodel(ice, MDL_ICE);
508 ice.colormod = Team_ColorRGB(targ.team);
509 ice.glowmod = ice.colormod;
511 targ.revival_time = 0;
515 RemoveGrapplingHooks(targ);
517 FOREACH_CLIENT(IS_PLAYER(it),
519 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
521 .entity weaponentity = weaponentities[slot];
522 if(it.(weaponentity).hook.aiment == targ)
523 RemoveHook(it.(weaponentity).hook);
528 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
529 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
532 void Unfreeze(entity targ, bool reset_health)
534 if(!STAT(FROZEN, targ))
537 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
538 SetResourceAmount(targ, RESOURCE_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
540 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
542 STAT(FROZEN, targ) = 0;
543 STAT(REVIVE_PROGRESS, targ) = 0;
544 targ.revival_time = time;
546 IL_PUSH(g_bot_targets, targ);
547 targ.bot_attack = true;
549 WaypointSprite_Kill(targ.waypointsprite_attached);
551 FOREACH_CLIENT(IS_PLAYER(it),
553 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
555 .entity weaponentity = weaponentities[slot];
556 if(it.(weaponentity).hook.aiment == targ)
557 RemoveHook(it.(weaponentity).hook);
561 // remove the ice block
563 delete(targ.iceblock);
564 targ.iceblock = NULL;
566 MUTATOR_CALLHOOK(Unfreeze, targ);
569 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
571 float complainteamdamage = 0;
572 float mirrordamage = 0;
573 float mirrorforce = 0;
575 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
578 entity attacker_save = attacker;
580 // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
581 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
583 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
589 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
591 // exit the vehicle before killing (fixes a crash)
592 if(IS_PLAYER(targ) && targ.vehicle)
593 vehicles_exit(targ.vehicle, VHEF_RELEASE);
595 // These are ALWAYS lethal
596 // No damage modification here
597 // Instead, prepare the victim for his death...
598 SetResourceAmount(targ, RESOURCE_ARMOR, 0);
599 targ.spawnshieldtime = 0;
600 SetResourceAmount(targ, RESOURCE_HEALTH, 0.9); // this is < 1
601 targ.flags -= targ.flags & FL_GODMODE;
604 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
610 // nullify damage if teamplay is on
611 if(deathtype != DEATH_TELEFRAG.m_id)
612 if(IS_PLAYER(attacker))
614 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
619 else if(SAME_TEAM(attacker, targ))
621 if(autocvar_teamplay_mode == 1)
623 else if(attacker != targ)
625 if(autocvar_teamplay_mode == 3)
627 else if(autocvar_teamplay_mode == 4)
629 if(IS_PLAYER(targ) && !IS_DEAD(targ))
631 attacker.dmg_team = attacker.dmg_team + damage;
632 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
633 if(complainteamdamage > 0)
634 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
635 mirrorforce = autocvar_g_mirrordamage * vlen(force);
636 damage = autocvar_g_friendlyfire * damage;
637 // mirrordamage will be used LATER
639 if(autocvar_g_mirrordamage_virtual)
641 vector v = healtharmor_applydamage(GetResourceAmount(attacker, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
642 attacker.dmg_take += v.x;
643 attacker.dmg_save += v.y;
644 attacker.dmg_inflictor = inflictor;
649 if(autocvar_g_friendlyfire_virtual)
651 vector v = healtharmor_applydamage(GetResourceAmount(targ, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
652 targ.dmg_take += v.x;
653 targ.dmg_save += v.y;
654 targ.dmg_inflictor = inflictor;
656 if(!autocvar_g_friendlyfire_virtual_force)
660 else if(!targ.canteamdamage)
667 if (!DEATH_ISSPECIAL(deathtype))
669 damage *= g_weapondamagefactor;
670 mirrordamage *= g_weapondamagefactor;
671 complainteamdamage *= g_weapondamagefactor;
672 force = force * g_weaponforcefactor;
673 mirrorforce *= g_weaponforcefactor;
676 // should this be changed at all? If so, in what way?
677 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
678 damage = M_ARGV(4, float);
679 mirrordamage = M_ARGV(5, float);
680 force = M_ARGV(6, vector);
682 if(IS_PLAYER(targ) && damage > 0 && attacker)
684 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
686 .entity went = weaponentities[slot];
687 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
688 RemoveHook(targ.(went).hook);
692 if(deathtype != DEATH_HURTTRIGGER.m_id && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id && STAT(FROZEN, targ))
694 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
696 Unfreeze(targ, false);
697 SetResourceAmount(targ, RESOURCE_HEALTH, autocvar_g_frozen_revive_falldamage_health);
698 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
699 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
700 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
704 force *= autocvar_g_frozen_force;
707 if(IS_PLAYER(targ) && STAT(FROZEN, targ) && deathtype == DEATH_HURTTRIGGER.m_id && !autocvar_g_frozen_damage_trigger)
709 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
711 entity spot = SelectSpawnPoint (targ, false);
716 targ.deadflag = DEAD_NO;
718 targ.angles = spot.angles;
721 targ.effects |= EF_TELEPORT_BIT;
723 targ.angles_z = 0; // never spawn tilted even if the spot says to
724 targ.fixangle = true; // turn this way immediately
725 targ.velocity = '0 0 0';
726 targ.avelocity = '0 0 0';
727 targ.punchangle = '0 0 0';
728 targ.punchvector = '0 0 0';
729 targ.oldvelocity = targ.velocity;
731 targ.spawnorigin = spot.origin;
732 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
733 // don't reset back to last position, even if new position is stuck in solid
734 targ.oldorigin = targ.origin;
736 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
740 if(!MUTATOR_IS_ENABLED(mutator_instagib))
742 // apply strength multiplier
743 if (attacker.items & ITEM_Strength.m_itemid)
747 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
748 force = force * autocvar_g_balance_powerup_strength_selfforce;
752 damage = damage * autocvar_g_balance_powerup_strength_damage;
753 force = force * autocvar_g_balance_powerup_strength_force;
757 // apply invincibility multiplier
758 if (targ.items & ITEM_Shield.m_itemid)
760 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
761 if (targ != attacker)
763 force = force * autocvar_g_balance_powerup_invincible_takeforce;
768 if (targ == attacker)
769 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
774 if(deathtype != DEATH_BUFF.m_id)
775 if(targ.takedamage == DAMAGE_AIM)
779 if(IS_VEHICLE(targ) && targ.owner)
784 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
786 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
790 if(deathtype != DEATH_FIRE.m_id)
792 if(PHYS_INPUT_BUTTON_CHAT(victim))
793 attacker.typehitsound += 1;
795 attacker.damage_dealt += damage;
798 damage_goodhits += 1;
799 damage_gooddamage += damage;
801 if (!DEATH_ISSPECIAL(deathtype))
803 if(IS_PLAYER(targ)) // don't do this for vehicles
809 else if(IS_PLAYER(attacker))
811 // if enemy gets frozen in this frame and receives other damage don't
812 // play the typehitsound e.g. when hit by multiple bullets of the shotgun
813 if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
815 attacker.typehitsound += 1;
817 if(complainteamdamage > 0)
818 if(time > CS(attacker).teamkill_complain)
820 CS(attacker).teamkill_complain = time + 5;
821 CS(attacker).teamkill_soundtime = time + 0.4;
822 CS(attacker).teamkill_soundsource = targ;
830 if (targ.damageforcescale)
832 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
834 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
835 if(targ.move_movetype == MOVETYPE_PHYSICS)
837 entity farcent = new(farce);
838 farcent.enemy = targ;
839 farcent.movedir = farce * 10;
841 farcent.movedir = farcent.movedir * targ.mass;
842 farcent.origin = hitloc;
843 farcent.forcetype = FORCETYPE_FORCEATPOS;
844 farcent.nextthink = time + 0.1;
845 setthink(farcent, SUB_Remove);
849 targ.velocity = targ.velocity + farce;
851 UNSET_ONGROUND(targ);
852 UpdateCSQCProjectile(targ);
855 if (damage != 0 || (targ.damageforcescale && force))
856 if (targ.event_damage)
857 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
859 // apply mirror damage if any
860 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
861 if(mirrordamage > 0 || mirrorforce > 0)
863 attacker = attacker_save;
865 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
866 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
870 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
871 float inflictorselfdamage, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
872 // Returns total damage applies to creatures
876 float total_damage_to_creatures;
881 float stat_damagedone;
883 if(RadiusDamage_running)
885 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
889 RadiusDamage_running = 1;
891 tfloordmg = autocvar_g_throughfloor_damage;
892 tfloorforce = autocvar_g_throughfloor_force;
894 total_damage_to_creatures = 0;
896 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
897 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
899 force = inflictorvelocity;
903 force = normalize(force);
904 if(forceintensity >= 0)
905 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
907 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
912 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
916 if ((targ != inflictor) || inflictorselfdamage)
917 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
924 // LordHavoc: measure distance to nearest point on target (not origin)
925 // (this guarentees 100% damage on a touch impact)
926 nearest = targ.WarpZone_findradius_nearest;
927 diff = targ.WarpZone_findradius_dist;
928 // round up a little on the damage to ensure full damage on impacts
929 // and turn the distance into a fraction of the radius
930 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
932 //bprint(ftos(power));
933 //if (targ == attacker)
934 // print(ftos(power), "\n");
940 finaldmg = coredamage * power + edgedamage * (1 - power);
946 vector myblastorigin;
949 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
951 // if it's a player, use the view origin as reference
952 center = CENTER_OR_VIEWOFS(targ);
954 force = normalize(center - myblastorigin);
955 force = force * (finaldmg / coredamage) * forceintensity;
958 if(deathtype & WEP_BLASTER.m_id)
959 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
961 if(targ != directhitentity)
966 float mininv_f, mininv_d;
968 // test line of sight to multiple positions on box,
969 // and do damage if any of them hit
972 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
973 // so for a given max stddev:
974 // n = (1 / (2 * max stddev of hitratio))^2
976 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
977 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
979 if(autocvar_g_throughfloor_debug)
980 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
983 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
985 if(autocvar_g_throughfloor_debug)
986 LOG_INFOF(" steps=%f", total);
990 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
992 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
994 if(autocvar_g_throughfloor_debug)
995 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
997 for(c = 0; c < total; ++c)
999 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1000 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1001 if (trace_fraction == 1 || trace_ent == targ)
1005 hitloc = hitloc + nearest;
1009 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1010 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1011 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1014 nearest = hitloc * (1 / max(1, hits));
1015 hitratio = (hits / total);
1016 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1017 finaldmg = finaldmg * a;
1018 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1021 if(autocvar_g_throughfloor_debug)
1022 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1025 //if (targ == attacker)
1027 // print("hits ", ftos(hits), " / ", ftos(total));
1028 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1029 // print(" (", ftos(a), ")\n");
1031 if(finaldmg || force)
1035 total_damage_to_creatures += finaldmg;
1037 if(accuracy_isgooddamage(attacker, targ))
1038 stat_damagedone += finaldmg;
1041 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1042 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1044 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1052 RadiusDamage_running = 0;
1054 if(!DEATH_ISSPECIAL(deathtype))
1055 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1057 return total_damage_to_creatures;
1060 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1062 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, weaponentity, directhitentity);
1065 bool Heal(entity targ, entity inflictor, float amount, float limit)
1067 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1070 bool healed = false;
1072 healed = targ.event_heal(targ, inflictor, amount, limit);
1073 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1074 // TODO: healing fx!
1075 // TODO: armor healing?
1079 float Fire_IsBurning(entity e)
1081 return (time < e.fire_endtime);
1084 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1087 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1098 // print("adding a fire burner to ", e.classname, "\n");
1099 e.fire_burner = new(fireburner);
1100 setthink(e.fire_burner, fireburner_think);
1101 e.fire_burner.nextthink = time;
1102 e.fire_burner.owner = e;
1108 if(Fire_IsBurning(e))
1110 mintime = e.fire_endtime - time;
1111 maxtime = max(mintime, t);
1113 mindps = e.fire_damagepersec;
1114 maxdps = max(mindps, dps);
1116 if(maxtime > mintime || maxdps > mindps)
1120 // damage we have right now
1121 mindamage = mindps * mintime;
1123 // damage we want to get
1124 maxdamage = mindamage + d;
1126 // but we can't exceed maxtime * maxdps!
1127 totaldamage = min(maxdamage, maxtime * maxdps);
1131 // totaldamage = min(mindamage + d, maxtime * maxdps)
1133 // totaldamage <= maxtime * maxdps
1134 // ==> totaldamage / maxdps <= maxtime.
1136 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1137 // >= min(mintime, maxtime)
1138 // ==> totaldamage / maxdps >= mintime.
1141 // how long do we damage then?
1142 // at least as long as before
1143 // but, never exceed maxdps
1144 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1148 // at most as long as maximum allowed
1149 // but, never below mindps
1150 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1152 // assuming t > mintime, dps > mindps:
1153 // we get d = t * dps = maxtime * maxdps
1154 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1155 // totaldamage / maxdps = maxtime
1156 // totaldamage / mindps > totaldamage / maxdps = maxtime
1158 // a) totaltime = max(mintime, maxtime) = maxtime
1159 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1161 // assuming t <= mintime:
1162 // we get maxtime = mintime
1163 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1164 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1166 // assuming dps <= mindps:
1167 // we get mindps = maxdps.
1168 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1169 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1170 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1172 e.fire_damagepersec = totaldamage / totaltime;
1173 e.fire_endtime = time + totaltime;
1174 if(totaldamage > 1.2 * mindamage)
1176 e.fire_deathtype = dt;
1177 if(e.fire_owner != o)
1180 e.fire_hitsound = false;
1183 if(accuracy_isgooddamage(o, e))
1184 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1185 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1192 e.fire_damagepersec = dps;
1193 e.fire_endtime = time + t;
1194 e.fire_deathtype = dt;
1196 e.fire_hitsound = false;
1197 if(accuracy_isgooddamage(o, e))
1198 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1203 void Fire_ApplyDamage(entity e)
1208 if (!Fire_IsBurning(e))
1211 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1212 if(IS_NOT_A_CLIENT(o))
1215 // water and slime stop fire
1217 if(e.watertype != CONTENT_LAVA)
1224 t = min(frametime, e.fire_endtime - time);
1225 d = e.fire_damagepersec * t;
1227 hi = e.fire_owner.damage_dealt;
1228 ty = e.fire_owner.typehitsound;
1229 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1230 if(e.fire_hitsound && e.fire_owner)
1232 e.fire_owner.damage_dealt = hi;
1233 e.fire_owner.typehitsound = ty;
1235 e.fire_hitsound = true;
1237 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1239 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1241 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1242 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1244 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1245 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1246 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1252 void Fire_ApplyEffect(entity e)
1254 if(Fire_IsBurning(e))
1255 e.effects |= EF_FLAME;
1257 e.effects &= ~EF_FLAME;
1260 void fireburner_think(entity this)
1262 // for players, this is done in the regular loop
1263 if(wasfreed(this.owner))
1268 Fire_ApplyEffect(this.owner);
1269 if(!Fire_IsBurning(this.owner))
1271 this.owner.fire_burner = NULL;
1275 Fire_ApplyDamage(this.owner);
1276 this.nextthink = time;