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(STAT(FROZEN, targ))
693 if(deathtype != DEATH_HURTTRIGGER.m_id && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
695 if(autocvar_g_frozen_revive_falldamage > 0)
696 if(deathtype == DEATH_FALL.m_id)
697 if(damage >= autocvar_g_frozen_revive_falldamage)
699 Unfreeze(targ, false);
700 SetResourceAmount(targ, RESOURCE_HEALTH, autocvar_g_frozen_revive_falldamage_health);
701 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
702 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
703 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
707 force *= autocvar_g_frozen_force;
710 if(STAT(FROZEN, targ) && deathtype == DEATH_HURTTRIGGER.m_id && !autocvar_g_frozen_damage_trigger)
712 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
714 entity spot = SelectSpawnPoint (targ, false);
719 targ.deadflag = DEAD_NO;
721 targ.angles = spot.angles;
724 targ.effects |= EF_TELEPORT_BIT;
726 targ.angles_z = 0; // never spawn tilted even if the spot says to
727 targ.fixangle = true; // turn this way immediately
728 targ.velocity = '0 0 0';
729 targ.avelocity = '0 0 0';
730 targ.punchangle = '0 0 0';
731 targ.punchvector = '0 0 0';
732 targ.oldvelocity = targ.velocity;
734 targ.spawnorigin = spot.origin;
735 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
736 // don't reset back to last position, even if new position is stuck in solid
737 targ.oldorigin = targ.origin;
739 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
743 if(!MUTATOR_IS_ENABLED(mutator_instagib))
745 // apply strength multiplier
746 if (attacker.items & ITEM_Strength.m_itemid)
750 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
751 force = force * autocvar_g_balance_powerup_strength_selfforce;
755 damage = damage * autocvar_g_balance_powerup_strength_damage;
756 force = force * autocvar_g_balance_powerup_strength_force;
760 // apply invincibility multiplier
761 if (targ.items & ITEM_Shield.m_itemid)
763 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
764 if (targ != attacker)
766 force = force * autocvar_g_balance_powerup_invincible_takeforce;
771 if (targ == attacker)
772 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
777 if(deathtype != DEATH_BUFF.m_id)
778 if(targ.takedamage == DAMAGE_AIM)
782 if(IS_VEHICLE(targ) && targ.owner)
787 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
789 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
793 if(deathtype != DEATH_FIRE.m_id)
795 if(PHYS_INPUT_BUTTON_CHAT(victim))
796 attacker.typehitsound += 1;
798 attacker.damage_dealt += damage;
801 damage_goodhits += 1;
802 damage_gooddamage += damage;
804 if (!DEATH_ISSPECIAL(deathtype))
806 if(IS_PLAYER(targ)) // don't do this for vehicles
812 else if(IS_PLAYER(attacker))
814 if(deathtype != DEATH_FIRE.m_id)
816 attacker.typehitsound += 1;
818 if(complainteamdamage > 0)
819 if(time > CS(attacker).teamkill_complain)
821 CS(attacker).teamkill_complain = time + 5;
822 CS(attacker).teamkill_soundtime = time + 0.4;
823 CS(attacker).teamkill_soundsource = targ;
831 if (targ.damageforcescale)
833 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
835 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
836 if(targ.move_movetype == MOVETYPE_PHYSICS)
838 entity farcent = new(farce);
839 farcent.enemy = targ;
840 farcent.movedir = farce * 10;
842 farcent.movedir = farcent.movedir * targ.mass;
843 farcent.origin = hitloc;
844 farcent.forcetype = FORCETYPE_FORCEATPOS;
845 farcent.nextthink = time + 0.1;
846 setthink(farcent, SUB_Remove);
850 targ.velocity = targ.velocity + farce;
852 UNSET_ONGROUND(targ);
853 UpdateCSQCProjectile(targ);
856 if (damage != 0 || (targ.damageforcescale && force))
857 if (targ.event_damage)
858 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
860 // apply mirror damage if any
861 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
862 if(mirrordamage > 0 || mirrorforce > 0)
864 attacker = attacker_save;
866 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
867 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
871 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
872 float inflictorselfdamage, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
873 // Returns total damage applies to creatures
877 float total_damage_to_creatures;
882 float stat_damagedone;
884 if(RadiusDamage_running)
886 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
890 RadiusDamage_running = 1;
892 tfloordmg = autocvar_g_throughfloor_damage;
893 tfloorforce = autocvar_g_throughfloor_force;
895 total_damage_to_creatures = 0;
897 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
898 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
900 force = inflictorvelocity;
904 force = normalize(force);
905 if(forceintensity >= 0)
906 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
908 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
913 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
917 if ((targ != inflictor) || inflictorselfdamage)
918 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
925 // LordHavoc: measure distance to nearest point on target (not origin)
926 // (this guarentees 100% damage on a touch impact)
927 nearest = targ.WarpZone_findradius_nearest;
928 diff = targ.WarpZone_findradius_dist;
929 // round up a little on the damage to ensure full damage on impacts
930 // and turn the distance into a fraction of the radius
931 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
933 //bprint(ftos(power));
934 //if (targ == attacker)
935 // print(ftos(power), "\n");
941 finaldmg = coredamage * power + edgedamage * (1 - power);
947 vector myblastorigin;
950 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
952 // if it's a player, use the view origin as reference
953 center = CENTER_OR_VIEWOFS(targ);
955 force = normalize(center - myblastorigin);
956 force = force * (finaldmg / coredamage) * forceintensity;
959 if(deathtype & WEP_BLASTER.m_id)
960 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
962 if(targ != directhitentity)
967 float mininv_f, mininv_d;
969 // test line of sight to multiple positions on box,
970 // and do damage if any of them hit
973 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
974 // so for a given max stddev:
975 // n = (1 / (2 * max stddev of hitratio))^2
977 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
978 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
980 if(autocvar_g_throughfloor_debug)
981 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
984 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
986 if(autocvar_g_throughfloor_debug)
987 LOG_INFOF(" steps=%f", total);
991 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
993 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
995 if(autocvar_g_throughfloor_debug)
996 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
998 for(c = 0; c < total; ++c)
1000 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1001 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1002 if (trace_fraction == 1 || trace_ent == targ)
1006 hitloc = hitloc + nearest;
1010 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1011 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1012 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1015 nearest = hitloc * (1 / max(1, hits));
1016 hitratio = (hits / total);
1017 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1018 finaldmg = finaldmg * a;
1019 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1022 if(autocvar_g_throughfloor_debug)
1023 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1026 //if (targ == attacker)
1028 // print("hits ", ftos(hits), " / ", ftos(total));
1029 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1030 // print(" (", ftos(a), ")\n");
1032 if(finaldmg || force)
1036 total_damage_to_creatures += finaldmg;
1038 if(accuracy_isgooddamage(attacker, targ))
1039 stat_damagedone += finaldmg;
1042 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1043 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1045 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1053 RadiusDamage_running = 0;
1055 if(!DEATH_ISSPECIAL(deathtype))
1056 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1058 return total_damage_to_creatures;
1061 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1063 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, weaponentity, directhitentity);
1066 bool Heal(entity targ, entity inflictor, float amount, float limit)
1068 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1071 bool healed = false;
1073 healed = targ.event_heal(targ, inflictor, amount, limit);
1074 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1075 // TODO: healing fx!
1076 // TODO: armor healing?
1080 float Fire_IsBurning(entity e)
1082 return (time < e.fire_endtime);
1085 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1088 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1099 // print("adding a fire burner to ", e.classname, "\n");
1100 e.fire_burner = new(fireburner);
1101 setthink(e.fire_burner, fireburner_think);
1102 e.fire_burner.nextthink = time;
1103 e.fire_burner.owner = e;
1109 if(Fire_IsBurning(e))
1111 mintime = e.fire_endtime - time;
1112 maxtime = max(mintime, t);
1114 mindps = e.fire_damagepersec;
1115 maxdps = max(mindps, dps);
1117 if(maxtime > mintime || maxdps > mindps)
1121 // damage we have right now
1122 mindamage = mindps * mintime;
1124 // damage we want to get
1125 maxdamage = mindamage + d;
1127 // but we can't exceed maxtime * maxdps!
1128 totaldamage = min(maxdamage, maxtime * maxdps);
1132 // totaldamage = min(mindamage + d, maxtime * maxdps)
1134 // totaldamage <= maxtime * maxdps
1135 // ==> totaldamage / maxdps <= maxtime.
1137 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1138 // >= min(mintime, maxtime)
1139 // ==> totaldamage / maxdps >= mintime.
1142 // how long do we damage then?
1143 // at least as long as before
1144 // but, never exceed maxdps
1145 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1149 // at most as long as maximum allowed
1150 // but, never below mindps
1151 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1153 // assuming t > mintime, dps > mindps:
1154 // we get d = t * dps = maxtime * maxdps
1155 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1156 // totaldamage / maxdps = maxtime
1157 // totaldamage / mindps > totaldamage / maxdps = maxtime
1159 // a) totaltime = max(mintime, maxtime) = maxtime
1160 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1162 // assuming t <= mintime:
1163 // we get maxtime = mintime
1164 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1165 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1167 // assuming dps <= mindps:
1168 // we get mindps = maxdps.
1169 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1170 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1171 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1173 e.fire_damagepersec = totaldamage / totaltime;
1174 e.fire_endtime = time + totaltime;
1175 if(totaldamage > 1.2 * mindamage)
1177 e.fire_deathtype = dt;
1178 if(e.fire_owner != o)
1181 e.fire_hitsound = false;
1184 if(accuracy_isgooddamage(o, e))
1185 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1186 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1193 e.fire_damagepersec = dps;
1194 e.fire_endtime = time + t;
1195 e.fire_deathtype = dt;
1197 e.fire_hitsound = false;
1198 if(accuracy_isgooddamage(o, e))
1199 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1204 void Fire_ApplyDamage(entity e)
1209 if (!Fire_IsBurning(e))
1212 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1213 if(IS_NOT_A_CLIENT(o))
1216 // water and slime stop fire
1218 if(e.watertype != CONTENT_LAVA)
1225 t = min(frametime, e.fire_endtime - time);
1226 d = e.fire_damagepersec * t;
1228 hi = e.fire_owner.damage_dealt;
1229 ty = e.fire_owner.typehitsound;
1230 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1231 if(e.fire_hitsound && e.fire_owner)
1233 e.fire_owner.damage_dealt = hi;
1234 e.fire_owner.typehitsound = ty;
1236 e.fire_hitsound = true;
1238 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1240 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1242 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1243 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1245 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1246 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1247 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1253 void Fire_ApplyEffect(entity e)
1255 if(Fire_IsBurning(e))
1256 e.effects |= EF_FLAME;
1258 e.effects &= ~EF_FLAME;
1261 void fireburner_think(entity this)
1263 // for players, this is done in the regular loop
1264 if(wasfreed(this.owner))
1269 Fire_ApplyEffect(this.owner);
1270 if(!Fire_IsBurning(this.owner))
1272 this.owner.fire_burner = NULL;
1276 Fire_ApplyDamage(this.owner);
1277 this.nextthink = time;