3 #include <common/effects/all.qh>
6 #include <server/mutators/_mod.qh>
9 #include "spawnpoints.qh"
10 #include "../common/state.qh"
11 #include "../common/physics/player.qh"
12 #include "../common/t_items.qh"
13 #include "resources.qh"
14 #include "../common/vehicles/all.qh"
15 #include "../common/items/_mod.qh"
16 #include "../common/mutators/mutator/waypoints/waypointsprites.qh"
17 #include "../common/mutators/mutator/instagib/sv_instagib.qh"
18 #include "../common/mutators/mutator/buffs/buffs.qh"
19 #include "weapons/accuracy.qh"
20 #include "weapons/csqcprojectile.qh"
21 #include "weapons/selection.qh"
22 #include "../common/constants.qh"
23 #include "../common/deathtypes/all.qh"
24 #include "../common/notifications/all.qh"
25 #include "../common/physics/movetypes/movetypes.qh"
26 #include "../common/playerstats.qh"
27 #include "../common/teams.qh"
28 #include "../common/util.qh"
29 #include <common/gamemodes/rules.qh>
30 #include <common/weapons/_all.qh>
31 #include "../lib/csqcmodel/sv_model.qh"
32 #include "../lib/warpzone/common.qh"
34 void UpdateFrags(entity player, int f)
36 GameRules_scoring_add_team(player, SCORE, f);
39 void GiveFrags(entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
41 // TODO route through PlayerScores instead
42 if(game_stopped) return;
49 GameRules_scoring_add(attacker, SUICIDES, 1);
54 GameRules_scoring_add(attacker, TEAMKILLS, 1);
60 GameRules_scoring_add(attacker, KILLS, 1);
61 if(!warmup_stage && targ.playerid)
62 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
65 GameRules_scoring_add(targ, DEATHS, 1);
67 // FIXME fix the mess this is (we have REAL points now!)
68 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
71 attacker.totalfrags += f;
74 UpdateFrags(attacker, f);
77 string AppendItemcodes(string s, entity player)
79 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
81 .entity weaponentity = weaponentities[slot];
82 int w = player.(weaponentity).m_weapon.m_id;
84 w = player.(weaponentity).cnt; // previous weapon
85 if(w != 0 || slot == 0)
86 s = strcat(s, ftos(w));
88 if(time < STAT(STRENGTH_FINISHED, player))
90 if(time < STAT(INVINCIBLE_FINISHED, player))
92 if(PHYS_INPUT_BUTTON_CHAT(player))
94 // TODO: include these codes as a flag on the item itself
95 MUTATOR_CALLHOOK(LogDeath_AppendItemCodes, player, s);
96 s = M_ARGV(1, string);
100 void LogDeath(string mode, int deathtype, entity killer, entity killed)
103 if(!autocvar_sv_eventlog)
105 s = strcat(":kill:", mode);
106 s = strcat(s, ":", ftos(killer.playerid));
107 s = strcat(s, ":", ftos(killed.playerid));
108 s = strcat(s, ":type=", Deathtype_Name(deathtype));
109 s = strcat(s, ":items=");
110 s = AppendItemcodes(s, killer);
113 s = strcat(s, ":victimitems=");
114 s = AppendItemcodes(s, killed);
119 void Obituary_SpecialDeath(
123 string s1, string s2, string s3,
124 float f1, float f2, float f3)
126 if(!DEATH_ISSPECIAL(deathtype))
128 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
132 entity deathent = REGISTRY_GET(Deathtypes, deathtype - DT_FIRST);
135 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
139 if(g_cts && deathtype == DEATH_KILL.m_id)
140 return; // TODO: somehow put this in CTS gamemode file!
142 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
145 Send_Notification_WOCOVA(
153 Send_Notification_WOCOVA(
157 death_message.nent_msginfo,
164 float Obituary_WeaponDeath(
168 string s1, string s2, string s3,
171 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
172 if (death_weapon == WEP_Null)
175 w_deathtype = deathtype;
176 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
181 Send_Notification_WOCOVA(
189 // send the info part to everyone
190 Send_Notification_WOCOVA(
194 death_message.nent_msginfo,
202 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %s!\n",
211 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target)
213 if(deathtype == DEATH_FIRE.m_id)
215 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
216 Send_Notification(NOTIF_ONE, targ, MSG_CHOICE, CHOICE_FRAGGED_FIRE, attacker.netname, kill_count_to_target, GetResource(attacker, RES_HEALTH), GetResource(attacker, RES_ARMOR), (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping));
220 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
223 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
226 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
229 float notif_firstblood = false;
230 float kill_count_to_attacker, kill_count_to_target;
232 // Set final information for the death
233 targ.death_origin = targ.origin;
234 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
236 #ifdef NOTIFICATIONS_DEBUG
239 "Obituary(%s, %s, %s, %s = %d);\n",
243 Deathtype_Name(deathtype),
254 if(DEATH_ISSPECIAL(deathtype))
256 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
258 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
262 switch(DEATH_ENT(deathtype))
264 case DEATH_MIRRORDAMAGE:
266 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
272 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
278 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
280 backtrace("SUICIDE: what the hell happened here?\n");
283 LogDeath("suicide", deathtype, targ, targ);
284 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
285 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
291 else if(IS_PLAYER(attacker))
293 if(SAME_TEAM(attacker, targ))
295 LogDeath("tk", deathtype, attacker, targ);
296 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
298 CS(attacker).killcount = 0;
300 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
301 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker.netname);
302 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker.netname, deathlocation, CS(targ).killcount);
304 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
305 // No need for specific death/weapon messages...
309 LogDeath("frag", deathtype, attacker, targ);
310 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
312 CS(attacker).taunt_soundtime = time + 1;
313 CS(attacker).killcount = CS(attacker).killcount + 1;
315 attacker.killsound += 1;
317 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
318 // these 2 macros are spread over multiple files
319 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
321 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
323 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
326 switch(CS(attacker).killcount)
333 if(!warmup_stage && !checkrules_firstblood)
335 checkrules_firstblood = true;
336 notif_firstblood = true; // modify the current messages so that they too show firstblood information
337 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
338 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
340 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
341 kill_count_to_attacker = -1;
342 kill_count_to_target = -2;
346 kill_count_to_attacker = CS(attacker).killcount;
347 kill_count_to_target = 0;
358 kill_count_to_attacker,
359 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
367 kill_count_to_target,
368 GetResource(attacker, RES_HEALTH),
369 GetResource(attacker, RES_ARMOR),
370 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
373 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target))
381 kill_count_to_attacker,
382 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
390 kill_count_to_target,
391 GetResource(attacker, RES_HEALTH),
392 GetResource(attacker, RES_ARMOR),
393 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
398 if(deathtype == DEATH_BUFF.m_id)
399 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
401 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker))
402 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
411 switch(DEATH_ENT(deathtype))
413 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
414 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
415 // and there will be a REAL DEATH_VOID implementation which mappers will use.
416 case DEATH_HURTTRIGGER:
418 Obituary_SpecialDeath(targ, false, deathtype,
430 Obituary_SpecialDeath(targ, false, deathtype,
432 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
442 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
447 LogDeath("accident", deathtype, targ, targ);
448 GiveFrags(targ, targ, -1, deathtype, weaponentity);
450 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
452 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
455 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
460 // reset target kill count
461 CS(targ).killcount = 0;
464 void Ice_Think(entity this)
466 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
471 setorigin(this, this.owner.origin - '0 0 16');
472 this.nextthink = time;
475 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
477 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
480 if(STAT(FROZEN, targ))
483 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
485 STAT(FROZEN, targ) = frozen_type;
486 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
487 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
488 targ.revive_speed = revivespeed;
490 IL_REMOVE(g_bot_targets, targ);
491 targ.bot_attack = false;
492 targ.freeze_time = time;
494 entity ice = new(ice);
496 ice.scale = targ.scale;
497 setthink(ice, Ice_Think);
498 ice.nextthink = time;
499 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
500 setmodel(ice, MDL_ICE);
502 ice.colormod = Team_ColorRGB(targ.team);
503 ice.glowmod = ice.colormod;
505 targ.revival_time = 0;
509 RemoveGrapplingHooks(targ);
511 FOREACH_CLIENT(IS_PLAYER(it),
513 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
515 .entity weaponentity = weaponentities[slot];
516 if(it.(weaponentity).hook.aiment == targ)
517 RemoveHook(it.(weaponentity).hook);
522 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
523 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
526 void Unfreeze(entity targ, bool reset_health)
528 if(!STAT(FROZEN, targ))
531 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
532 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
534 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
536 STAT(FROZEN, targ) = 0;
537 STAT(REVIVE_PROGRESS, targ) = 0;
538 targ.revival_time = time;
540 IL_PUSH(g_bot_targets, targ);
541 targ.bot_attack = true;
543 WaypointSprite_Kill(targ.waypointsprite_attached);
545 FOREACH_CLIENT(IS_PLAYER(it),
547 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
549 .entity weaponentity = weaponentities[slot];
550 if(it.(weaponentity).hook.aiment == targ)
551 RemoveHook(it.(weaponentity).hook);
555 // remove the ice block
557 delete(targ.iceblock);
558 targ.iceblock = NULL;
560 MUTATOR_CALLHOOK(Unfreeze, targ);
563 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
565 float complainteamdamage = 0;
566 float mirrordamage = 0;
567 float mirrorforce = 0;
569 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
572 entity attacker_save = attacker;
574 // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
575 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
577 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
583 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
585 // exit the vehicle before killing (fixes a crash)
586 if(IS_PLAYER(targ) && targ.vehicle)
587 vehicles_exit(targ.vehicle, VHEF_RELEASE);
589 // These are ALWAYS lethal
590 // No damage modification here
591 // Instead, prepare the victim for his death...
592 SetResourceExplicit(targ, RES_ARMOR, 0);
593 targ.spawnshieldtime = 0;
594 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
595 targ.flags -= targ.flags & FL_GODMODE;
598 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
604 // nullify damage if teamplay is on
605 if(deathtype != DEATH_TELEFRAG.m_id)
606 if(IS_PLAYER(attacker))
608 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
613 else if(!STAT(FROZEN, targ) && SAME_TEAM(attacker, targ))
615 if(autocvar_teamplay_mode == 1)
617 else if(attacker != targ)
619 if(autocvar_teamplay_mode == 3)
621 else if(autocvar_teamplay_mode == 4)
623 if(IS_PLAYER(targ) && !IS_DEAD(targ))
625 attacker.dmg_team = attacker.dmg_team + damage;
626 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
627 if(complainteamdamage > 0)
628 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
629 mirrorforce = autocvar_g_mirrordamage * vlen(force);
630 damage = autocvar_g_friendlyfire * damage;
631 // mirrordamage will be used LATER
633 if(autocvar_g_mirrordamage_virtual)
635 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
636 attacker.dmg_take += v.x;
637 attacker.dmg_save += v.y;
638 attacker.dmg_inflictor = inflictor;
643 if(autocvar_g_friendlyfire_virtual)
645 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
646 targ.dmg_take += v.x;
647 targ.dmg_save += v.y;
648 targ.dmg_inflictor = inflictor;
650 if(!autocvar_g_friendlyfire_virtual_force)
654 else if(!targ.canteamdamage)
661 if (!DEATH_ISSPECIAL(deathtype))
663 damage *= g_weapondamagefactor;
664 mirrordamage *= g_weapondamagefactor;
665 complainteamdamage *= g_weapondamagefactor;
666 force = force * g_weaponforcefactor;
667 mirrorforce *= g_weaponforcefactor;
670 // should this be changed at all? If so, in what way?
671 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
672 damage = M_ARGV(4, float);
673 mirrordamage = M_ARGV(5, float);
674 force = M_ARGV(6, vector);
676 if(IS_PLAYER(targ) && damage > 0 && attacker)
678 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
680 .entity went = weaponentities[slot];
681 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
682 RemoveHook(targ.(went).hook);
686 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
687 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
689 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
691 Unfreeze(targ, false);
692 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
693 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
694 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
695 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
699 force *= autocvar_g_frozen_force;
702 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
703 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
705 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
707 entity spot = SelectSpawnPoint(targ, false);
711 targ.deadflag = DEAD_NO;
713 targ.angles = spot.angles;
716 targ.effects |= EF_TELEPORT_BIT;
718 targ.angles_z = 0; // never spawn tilted even if the spot says to
719 targ.fixangle = true; // turn this way immediately
720 targ.velocity = '0 0 0';
721 targ.avelocity = '0 0 0';
722 targ.punchangle = '0 0 0';
723 targ.punchvector = '0 0 0';
724 targ.oldvelocity = targ.velocity;
726 targ.spawnorigin = spot.origin;
727 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
728 // don't reset back to last position, even if new position is stuck in solid
729 targ.oldorigin = targ.origin;
731 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
735 if(!MUTATOR_IS_ENABLED(mutator_instagib))
737 // apply strength multiplier
738 if (attacker.items & ITEM_Strength.m_itemid)
742 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
743 force = force * autocvar_g_balance_powerup_strength_selfforce;
747 damage = damage * autocvar_g_balance_powerup_strength_damage;
748 force = force * autocvar_g_balance_powerup_strength_force;
752 // apply invincibility multiplier
753 if (targ.items & ITEM_Shield.m_itemid)
755 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
756 if (targ != attacker)
758 force = force * autocvar_g_balance_powerup_invincible_takeforce;
763 if (targ == attacker)
764 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
769 if(deathtype != DEATH_BUFF.m_id)
770 if(targ.takedamage == DAMAGE_AIM)
774 if(IS_VEHICLE(targ) && targ.owner)
779 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
781 if (DIFF_TEAM(victim, attacker))
785 if(deathtype != DEATH_FIRE.m_id)
787 if(PHYS_INPUT_BUTTON_CHAT(victim))
788 attacker.typehitsound += 1;
790 attacker.damage_dealt += damage;
793 damage_goodhits += 1;
794 damage_gooddamage += damage;
796 if (!DEATH_ISSPECIAL(deathtype))
798 if(IS_PLAYER(targ)) // don't do this for vehicles
804 else if (IS_PLAYER(attacker) && !STAT(FROZEN, victim)) // same team
806 if (deathtype != DEATH_FIRE.m_id)
808 attacker.typehitsound += 1;
810 if(complainteamdamage > 0)
811 if(time > CS(attacker).teamkill_complain)
813 CS(attacker).teamkill_complain = time + 5;
814 CS(attacker).teamkill_soundtime = time + 0.4;
815 CS(attacker).teamkill_soundsource = targ;
823 if (targ.damageforcescale)
825 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
827 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
828 if(targ.move_movetype == MOVETYPE_PHYSICS)
830 entity farcent = new(farce);
831 farcent.enemy = targ;
832 farcent.movedir = farce * 10;
834 farcent.movedir = farcent.movedir * targ.mass;
835 farcent.origin = hitloc;
836 farcent.forcetype = FORCETYPE_FORCEATPOS;
837 farcent.nextthink = time + 0.1;
838 setthink(farcent, SUB_Remove);
842 targ.velocity = targ.velocity + farce;
844 UNSET_ONGROUND(targ);
845 UpdateCSQCProjectile(targ);
848 if (damage != 0 || (targ.damageforcescale && force))
849 if (targ.event_damage)
850 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
852 // apply mirror damage if any
853 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
854 if(mirrordamage > 0 || mirrorforce > 0)
856 attacker = attacker_save;
858 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
859 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
863 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
864 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
865 // Returns total damage applies to creatures
869 float total_damage_to_creatures;
874 float stat_damagedone;
876 if(RadiusDamage_running)
878 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
882 RadiusDamage_running = 1;
884 tfloordmg = autocvar_g_throughfloor_damage;
885 tfloorforce = autocvar_g_throughfloor_force;
887 total_damage_to_creatures = 0;
889 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
890 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
892 force = inflictorvelocity;
896 force = normalize(force);
897 if(forceintensity >= 0)
898 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
900 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
905 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
909 if ((targ != inflictor) || inflictorselfdamage)
910 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
917 // LordHavoc: measure distance to nearest point on target (not origin)
918 // (this guarentees 100% damage on a touch impact)
919 nearest = targ.WarpZone_findradius_nearest;
920 diff = targ.WarpZone_findradius_dist;
921 // round up a little on the damage to ensure full damage on impacts
922 // and turn the distance into a fraction of the radius
923 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
925 //bprint(ftos(power));
926 //if (targ == attacker)
927 // print(ftos(power), "\n");
933 finaldmg = coredamage * power + edgedamage * (1 - power);
939 vector myblastorigin;
942 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
944 // if it's a player, use the view origin as reference
945 center = CENTER_OR_VIEWOFS(targ);
947 force = normalize(center - myblastorigin);
948 force = force * (finaldmg / coredamage) * forceintensity;
951 // apply special scaling along the z axis if set
952 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
954 force.z *= forcezscale;
956 if(targ != directhitentity)
961 float mininv_f, mininv_d;
963 // test line of sight to multiple positions on box,
964 // and do damage if any of them hit
967 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
968 // so for a given max stddev:
969 // n = (1 / (2 * max stddev of hitratio))^2
971 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
972 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
974 if(autocvar_g_throughfloor_debug)
975 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
978 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
980 if(autocvar_g_throughfloor_debug)
981 LOG_INFOF(" steps=%f", total);
985 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
987 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
989 if(autocvar_g_throughfloor_debug)
990 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
992 for(c = 0; c < total; ++c)
994 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
995 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
996 if (trace_fraction == 1 || trace_ent == targ)
1000 hitloc = hitloc + nearest;
1004 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1005 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1006 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1009 nearest = hitloc * (1 / max(1, hits));
1010 hitratio = (hits / total);
1011 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1012 finaldmg = finaldmg * a;
1013 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1016 if(autocvar_g_throughfloor_debug)
1017 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1020 //if (targ == attacker)
1022 // print("hits ", ftos(hits), " / ", ftos(total));
1023 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1024 // print(" (", ftos(a), ")\n");
1026 if(finaldmg || force)
1030 total_damage_to_creatures += finaldmg;
1032 if(accuracy_isgooddamage(attacker, targ))
1033 stat_damagedone += finaldmg;
1036 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1037 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1039 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1047 RadiusDamage_running = 0;
1049 if(!DEATH_ISSPECIAL(deathtype))
1050 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1052 return total_damage_to_creatures;
1055 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1057 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1058 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1061 bool Heal(entity targ, entity inflictor, float amount, float limit)
1063 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1066 bool healed = false;
1068 healed = targ.event_heal(targ, inflictor, amount, limit);
1069 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1070 // TODO: healing fx!
1071 // TODO: armor healing?
1075 float Fire_IsBurning(entity e)
1077 return (time < e.fire_endtime);
1080 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1083 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1094 // print("adding a fire burner to ", e.classname, "\n");
1095 e.fire_burner = new(fireburner);
1096 setthink(e.fire_burner, fireburner_think);
1097 e.fire_burner.nextthink = time;
1098 e.fire_burner.owner = e;
1104 if(Fire_IsBurning(e))
1106 mintime = e.fire_endtime - time;
1107 maxtime = max(mintime, t);
1109 mindps = e.fire_damagepersec;
1110 maxdps = max(mindps, dps);
1112 if(maxtime > mintime || maxdps > mindps)
1116 // damage we have right now
1117 mindamage = mindps * mintime;
1119 // damage we want to get
1120 maxdamage = mindamage + d;
1122 // but we can't exceed maxtime * maxdps!
1123 totaldamage = min(maxdamage, maxtime * maxdps);
1127 // totaldamage = min(mindamage + d, maxtime * maxdps)
1129 // totaldamage <= maxtime * maxdps
1130 // ==> totaldamage / maxdps <= maxtime.
1132 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1133 // >= min(mintime, maxtime)
1134 // ==> totaldamage / maxdps >= mintime.
1137 // how long do we damage then?
1138 // at least as long as before
1139 // but, never exceed maxdps
1140 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1144 // at most as long as maximum allowed
1145 // but, never below mindps
1146 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1148 // assuming t > mintime, dps > mindps:
1149 // we get d = t * dps = maxtime * maxdps
1150 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1151 // totaldamage / maxdps = maxtime
1152 // totaldamage / mindps > totaldamage / maxdps = maxtime
1154 // a) totaltime = max(mintime, maxtime) = maxtime
1155 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1157 // assuming t <= mintime:
1158 // we get maxtime = mintime
1159 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1160 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1162 // assuming dps <= mindps:
1163 // we get mindps = maxdps.
1164 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1165 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1166 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1168 e.fire_damagepersec = totaldamage / totaltime;
1169 e.fire_endtime = time + totaltime;
1170 if(totaldamage > 1.2 * mindamage)
1172 e.fire_deathtype = dt;
1173 if(e.fire_owner != o)
1176 e.fire_hitsound = false;
1179 if(accuracy_isgooddamage(o, e))
1180 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1181 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1188 e.fire_damagepersec = dps;
1189 e.fire_endtime = time + t;
1190 e.fire_deathtype = dt;
1192 e.fire_hitsound = false;
1193 if(accuracy_isgooddamage(o, e))
1194 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1199 void Fire_ApplyDamage(entity e)
1204 if (!Fire_IsBurning(e))
1207 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1208 if(IS_NOT_A_CLIENT(o))
1211 // water and slime stop fire
1213 if(e.watertype != CONTENT_LAVA)
1220 t = min(frametime, e.fire_endtime - time);
1221 d = e.fire_damagepersec * t;
1223 hi = e.fire_owner.damage_dealt;
1224 ty = e.fire_owner.typehitsound;
1225 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1226 if(e.fire_hitsound && e.fire_owner)
1228 e.fire_owner.damage_dealt = hi;
1229 e.fire_owner.typehitsound = ty;
1231 e.fire_hitsound = true;
1233 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1235 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1237 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1238 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1240 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1241 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1242 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1248 void Fire_ApplyEffect(entity e)
1250 if(Fire_IsBurning(e))
1251 e.effects |= EF_FLAME;
1253 e.effects &= ~EF_FLAME;
1256 void fireburner_think(entity this)
1258 // for players, this is done in the regular loop
1259 if(wasfreed(this.owner))
1264 Fire_ApplyEffect(this.owner);
1265 if(!Fire_IsBurning(this.owner))
1267 this.owner.fire_burner = NULL;
1271 Fire_ApplyDamage(this.owner);
1272 this.nextthink = time;