3 #include <common/effects/all.qh>
6 #include <server/mutators/_mod.qh>
8 #include "spawnpoints.qh"
9 #include "../common/state.qh"
10 #include "../common/physics/player.qh"
11 #include "../common/t_items.qh"
12 #include "resources.qh"
13 #include "../common/vehicles/all.qh"
14 #include "../common/items/_mod.qh"
15 #include "../common/mutators/mutator/waypoints/waypointsprites.qh"
16 #include "../common/mutators/mutator/instagib/sv_instagib.qh"
17 #include "../common/mutators/mutator/buffs/buffs.qh"
18 #include "weapons/accuracy.qh"
19 #include "weapons/csqcprojectile.qh"
20 #include "weapons/selection.qh"
21 #include "../common/constants.qh"
22 #include "../common/deathtypes/all.qh"
23 #include "../common/notifications/all.qh"
24 #include "../common/physics/movetypes/movetypes.qh"
25 #include "../common/playerstats.qh"
26 #include "../common/teams.qh"
27 #include "../common/util.qh"
28 #include <common/gamemodes/rules.qh>
29 #include <common/weapons/_all.qh>
30 #include "../lib/csqcmodel/sv_model.qh"
31 #include "../lib/warpzone/common.qh"
33 void UpdateFrags(entity player, int f)
35 GameRules_scoring_add_team(player, SCORE, f);
38 void GiveFrags(entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
40 // TODO route through PlayerScores instead
41 if(game_stopped) return;
48 GameRules_scoring_add(attacker, SUICIDES, 1);
53 GameRules_scoring_add(attacker, TEAMKILLS, 1);
59 GameRules_scoring_add(attacker, KILLS, 1);
60 if(!warmup_stage && targ.playerid)
61 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
64 GameRules_scoring_add(targ, DEATHS, 1);
66 // FIXME fix the mess this is (we have REAL points now!)
67 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
70 attacker.totalfrags += f;
73 UpdateFrags(attacker, f);
78 string AppendItemcodes(string s, entity player)
80 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
82 .entity weaponentity = weaponentities[slot];
83 int w = player.(weaponentity).m_weapon.m_id;
85 w = player.(weaponentity).cnt; // previous weapon
86 if(w != 0 || slot == 0)
87 s = strcat(s, ftos(w));
89 if(time < player.strength_finished)
91 if(time < player.invincible_finished)
93 if(player.flagcarried != NULL)
95 if(PHYS_INPUT_BUTTON_CHAT(player))
102 void LogDeath(string mode, int deathtype, entity killer, entity killed)
105 if(!autocvar_sv_eventlog)
107 s = strcat(":kill:", mode);
108 s = strcat(s, ":", ftos(killer.playerid));
109 s = strcat(s, ":", ftos(killed.playerid));
110 s = strcat(s, ":type=", Deathtype_Name(deathtype));
111 s = strcat(s, ":items=");
112 s = AppendItemcodes(s, killer);
115 s = strcat(s, ":victimitems=");
116 s = AppendItemcodes(s, killed);
121 void Obituary_SpecialDeath(
125 string s1, string s2, string s3,
126 float f1, float f2, float f3)
128 if(!DEATH_ISSPECIAL(deathtype))
130 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
134 entity deathent = Deathtypes_from(deathtype - DT_FIRST);
137 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
141 if(g_cts && deathtype == DEATH_KILL.m_id)
142 return; // TODO: somehow put this in CTS gamemode file!
144 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
147 Send_Notification_WOCOVA(
155 Send_Notification_WOCOVA(
159 death_message.nent_msginfo,
166 float Obituary_WeaponDeath(
170 string s1, string s2, string s3,
173 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
174 if (death_weapon == WEP_Null)
177 w_deathtype = deathtype;
178 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
183 Send_Notification_WOCOVA(
191 // send the info part to everyone
192 Send_Notification_WOCOVA(
196 death_message.nent_msginfo,
204 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %d!\n",
213 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target)
215 if(deathtype == DEATH_FIRE.m_id)
217 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
218 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));
222 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
225 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
228 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
231 float notif_firstblood = false;
232 float kill_count_to_attacker, kill_count_to_target;
234 // Set final information for the death
235 targ.death_origin = targ.origin;
236 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
238 #ifdef NOTIFICATIONS_DEBUG
241 "Obituary(%s, %s, %s, %s = %d);\n",
245 Deathtype_Name(deathtype),
256 if(DEATH_ISSPECIAL(deathtype))
258 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
260 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
264 switch(DEATH_ENT(deathtype))
266 case DEATH_MIRRORDAMAGE:
268 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
274 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
280 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
282 backtrace("SUICIDE: what the hell happened here?\n");
285 LogDeath("suicide", deathtype, targ, targ);
286 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
287 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
293 else if(IS_PLAYER(attacker))
295 if(SAME_TEAM(attacker, targ))
297 LogDeath("tk", deathtype, attacker, targ);
298 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
300 CS(attacker).killcount = 0;
302 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
303 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker.netname);
304 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker.netname, deathlocation, CS(targ).killcount);
306 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
307 // No need for specific death/weapon messages...
311 LogDeath("frag", deathtype, attacker, targ);
312 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
314 CS(attacker).taunt_soundtime = time + 1;
315 CS(attacker).killcount = CS(attacker).killcount + 1;
317 attacker.killsound += 1;
319 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
320 // these 2 macros are spread over multiple files
321 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
324 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
327 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
331 switch(CS(attacker).killcount)
338 if(!warmup_stage && !checkrules_firstblood)
340 checkrules_firstblood = true;
341 notif_firstblood = true; // modify the current messages so that they too show firstblood information
342 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
343 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
345 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
346 kill_count_to_attacker = -1;
347 kill_count_to_target = -2;
351 kill_count_to_attacker = CS(attacker).killcount;
352 kill_count_to_target = 0;
363 kill_count_to_attacker,
364 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
372 kill_count_to_target,
373 GetResourceAmount(attacker, RESOURCE_HEALTH),
374 GetResourceAmount(attacker, RESOURCE_ARMOR),
375 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
378 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target))
386 kill_count_to_attacker,
387 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
395 kill_count_to_target,
396 GetResourceAmount(attacker, RESOURCE_HEALTH),
397 GetResourceAmount(attacker, RESOURCE_ARMOR),
398 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
403 if(deathtype == DEATH_BUFF.m_id)
404 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
406 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker))
407 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
416 switch(DEATH_ENT(deathtype))
418 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
419 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
420 // and there will be a REAL DEATH_VOID implementation which mappers will use.
421 case DEATH_HURTTRIGGER:
423 Obituary_SpecialDeath(targ, false, deathtype,
435 Obituary_SpecialDeath(targ, false, deathtype,
437 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
447 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
452 LogDeath("accident", deathtype, targ, targ);
453 GiveFrags(targ, targ, -1, deathtype, weaponentity);
455 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
457 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
460 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
465 // reset target kill count
466 CS(targ).killcount = 0;
469 void Ice_Think(entity this)
471 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
476 setorigin(this, this.owner.origin - '0 0 16');
477 this.nextthink = time;
480 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
482 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
485 if(STAT(FROZEN, targ))
488 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
490 STAT(FROZEN, targ) = frozen_type;
491 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == 3) ? 1 : 0);
492 SetResourceAmount(targ, RESOURCE_HEALTH, ((frozen_type == 3) ? targ_maxhealth : 1));
493 targ.revive_speed = revivespeed;
495 IL_REMOVE(g_bot_targets, targ);
496 targ.bot_attack = false;
497 targ.freeze_time = time;
499 entity ice = new(ice);
501 ice.scale = targ.scale;
502 setthink(ice, Ice_Think);
503 ice.nextthink = time;
504 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
505 setmodel(ice, MDL_ICE);
507 ice.colormod = Team_ColorRGB(targ.team);
508 ice.glowmod = ice.colormod;
510 targ.revival_time = 0;
514 RemoveGrapplingHooks(targ);
516 FOREACH_CLIENT(IS_PLAYER(it),
518 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
520 .entity weaponentity = weaponentities[slot];
521 if(it.(weaponentity).hook.aiment == targ)
522 RemoveHook(it.(weaponentity).hook);
527 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
528 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
531 void Unfreeze(entity targ)
533 if(!STAT(FROZEN, targ))
536 if(STAT(FROZEN, targ) && STAT(FROZEN, targ) != 3) // only reset health if target was frozen
538 SetResourceAmount(targ, RESOURCE_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
539 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)
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).m_id, 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).m_id, 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).m_id, 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))
1239 if(!STAT(FROZEN, e))
1240 FOREACH_CLIENT(IS_PLAYER(it) && it != e, {
1242 if(!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);
1252 void Fire_ApplyEffect(entity e)
1254 if(Fire_IsBurning(e))
1255 e.effects |= EF_FLAME;
1257 e.effects &= ~EF_FLAME;
1260 void fireburner_think(entity this)
1262 // for players, this is done in the regular loop
1263 if(wasfreed(this.owner))
1268 Fire_ApplyEffect(this.owner);
1269 if(!Fire_IsBurning(this.owner))
1271 this.owner.fire_burner = NULL;
1275 Fire_ApplyDamage(this.owner);
1276 this.nextthink = time;