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 SetResourceAmountExplicit(targ, RESOURCE_ARMOR, 0);
599 targ.spawnshieldtime = 0;
600 SetResourceAmountExplicit(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(deathtype != DEATH_FIRE.m_id)
813 attacker.typehitsound += 1;
815 if(complainteamdamage > 0)
816 if(time > CS(attacker).teamkill_complain)
818 CS(attacker).teamkill_complain = time + 5;
819 CS(attacker).teamkill_soundtime = time + 0.4;
820 CS(attacker).teamkill_soundsource = targ;
828 if (targ.damageforcescale)
830 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
832 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
833 if(targ.move_movetype == MOVETYPE_PHYSICS)
835 entity farcent = new(farce);
836 farcent.enemy = targ;
837 farcent.movedir = farce * 10;
839 farcent.movedir = farcent.movedir * targ.mass;
840 farcent.origin = hitloc;
841 farcent.forcetype = FORCETYPE_FORCEATPOS;
842 farcent.nextthink = time + 0.1;
843 setthink(farcent, SUB_Remove);
847 targ.velocity = targ.velocity + farce;
849 UNSET_ONGROUND(targ);
850 UpdateCSQCProjectile(targ);
853 if (damage != 0 || (targ.damageforcescale && force))
854 if (targ.event_damage)
855 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
857 // apply mirror damage if any
858 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
859 if(mirrordamage > 0 || mirrorforce > 0)
861 attacker = attacker_save;
863 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
864 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
868 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
869 float inflictorselfdamage, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
870 // Returns total damage applies to creatures
874 float total_damage_to_creatures;
879 float stat_damagedone;
881 if(RadiusDamage_running)
883 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
887 RadiusDamage_running = 1;
889 tfloordmg = autocvar_g_throughfloor_damage;
890 tfloorforce = autocvar_g_throughfloor_force;
892 total_damage_to_creatures = 0;
894 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
895 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
897 force = inflictorvelocity;
901 force = normalize(force);
902 if(forceintensity >= 0)
903 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
905 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
910 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
914 if ((targ != inflictor) || inflictorselfdamage)
915 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
922 // LordHavoc: measure distance to nearest point on target (not origin)
923 // (this guarentees 100% damage on a touch impact)
924 nearest = targ.WarpZone_findradius_nearest;
925 diff = targ.WarpZone_findradius_dist;
926 // round up a little on the damage to ensure full damage on impacts
927 // and turn the distance into a fraction of the radius
928 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
930 //bprint(ftos(power));
931 //if (targ == attacker)
932 // print(ftos(power), "\n");
938 finaldmg = coredamage * power + edgedamage * (1 - power);
944 vector myblastorigin;
947 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
949 // if it's a player, use the view origin as reference
950 center = CENTER_OR_VIEWOFS(targ);
952 force = normalize(center - myblastorigin);
953 force = force * (finaldmg / coredamage) * forceintensity;
956 if(deathtype & WEP_BLASTER.m_id)
957 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
959 if(targ != directhitentity)
964 float mininv_f, mininv_d;
966 // test line of sight to multiple positions on box,
967 // and do damage if any of them hit
970 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
971 // so for a given max stddev:
972 // n = (1 / (2 * max stddev of hitratio))^2
974 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
975 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
977 if(autocvar_g_throughfloor_debug)
978 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
981 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
983 if(autocvar_g_throughfloor_debug)
984 LOG_INFOF(" steps=%f", total);
988 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
990 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
992 if(autocvar_g_throughfloor_debug)
993 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
995 for(c = 0; c < total; ++c)
997 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
998 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
999 if (trace_fraction == 1 || trace_ent == targ)
1003 hitloc = hitloc + nearest;
1007 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1008 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1009 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1012 nearest = hitloc * (1 / max(1, hits));
1013 hitratio = (hits / total);
1014 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1015 finaldmg = finaldmg * a;
1016 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1019 if(autocvar_g_throughfloor_debug)
1020 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1023 //if (targ == attacker)
1025 // print("hits ", ftos(hits), " / ", ftos(total));
1026 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1027 // print(" (", ftos(a), ")\n");
1029 if(finaldmg || force)
1033 total_damage_to_creatures += finaldmg;
1035 if(accuracy_isgooddamage(attacker, targ))
1036 stat_damagedone += finaldmg;
1039 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1040 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1042 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1050 RadiusDamage_running = 0;
1052 if(!DEATH_ISSPECIAL(deathtype))
1053 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1055 return total_damage_to_creatures;
1058 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1060 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, 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;