3 #include <common/effects/all.qh>
6 #include "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 "weapons/accuracy.qh"
18 #include "weapons/csqcprojectile.qh"
19 #include "weapons/selection.qh"
20 #include "../common/constants.qh"
21 #include "../common/deathtypes/all.qh"
22 #include "../common/notifications/all.qh"
23 #include "../common/physics/movetypes/movetypes.qh"
24 #include "../common/playerstats.qh"
25 #include "../common/teams.qh"
26 #include "../common/util.qh"
27 #include <common/weapons/_all.qh>
28 #include "../lib/csqcmodel/sv_model.qh"
29 #include "../lib/warpzone/common.qh"
31 void UpdateFrags(entity player, int f)
33 GameRules_scoring_add_team(player, SCORE, f);
36 void GiveFrags (entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
38 // TODO route through PlayerScores instead
39 if(game_stopped) return;
46 GameRules_scoring_add(attacker, SUICIDES, 1);
51 GameRules_scoring_add(attacker, TEAMKILLS, 1);
57 GameRules_scoring_add(attacker, KILLS, 1);
58 if(!warmup_stage && targ.playerid)
59 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
62 GameRules_scoring_add(targ, DEATHS, 1);
64 // FIXME fix the mess this is (we have REAL points now!)
65 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
68 attacker.totalfrags += f;
71 UpdateFrags(attacker, f);
76 string AppendItemcodes(string s, entity player)
78 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
80 .entity weaponentity = weaponentities[slot];
81 int w = player.(weaponentity).m_weapon.m_id;
83 w = player.(weaponentity).cnt; // previous weapon
84 if(w != 0 || slot == 0)
85 s = strcat(s, ftos(w));
87 if(time < player.strength_finished)
89 if(time < player.invincible_finished)
91 if(player.flagcarried != NULL)
93 if(PHYS_INPUT_BUTTON_CHAT(player))
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 = Deathtypes_from(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 %d!\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, GetResourceAmount(attacker, RESOURCE_HEALTH), GetResourceAmount(attacker, RESOURCE_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 entity buff_FirstFromFlags(int _buffs);
224 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
227 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
230 float notif_firstblood = false;
231 float kill_count_to_attacker, kill_count_to_target;
233 // Set final information for the death
234 targ.death_origin = targ.origin;
235 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
237 #ifdef NOTIFICATIONS_DEBUG
240 "Obituary(%s, %s, %s, %s = %d);\n",
244 Deathtype_Name(deathtype),
255 if(DEATH_ISSPECIAL(deathtype))
257 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
259 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
263 switch(DEATH_ENT(deathtype))
265 case DEATH_MIRRORDAMAGE:
267 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
273 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
279 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
281 backtrace("SUICIDE: what the hell happened here?\n");
284 LogDeath("suicide", deathtype, targ, targ);
285 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
286 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
292 else if(IS_PLAYER(attacker))
294 if(SAME_TEAM(attacker, targ))
296 LogDeath("tk", deathtype, attacker, targ);
297 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
299 CS(attacker).killcount = 0;
301 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
302 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker.netname);
303 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker.netname, deathlocation, CS(targ).killcount);
305 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
306 // No need for specific death/weapon messages...
310 LogDeath("frag", deathtype, attacker, targ);
311 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
313 CS(attacker).taunt_soundtime = time + 1;
314 CS(attacker).killcount = CS(attacker).killcount + 1;
316 attacker.killsound += 1;
318 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
319 // these 2 macros are spread over multiple files
320 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
323 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
326 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
330 switch(CS(attacker).killcount)
337 if(!warmup_stage && !checkrules_firstblood)
339 checkrules_firstblood = true;
340 notif_firstblood = true; // modify the current messages so that they too show firstblood information
341 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
342 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
344 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
345 kill_count_to_attacker = -1;
346 kill_count_to_target = -2;
350 kill_count_to_attacker = CS(attacker).killcount;
351 kill_count_to_target = 0;
362 kill_count_to_attacker,
363 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
371 kill_count_to_target,
372 GetResourceAmount(attacker, RESOURCE_HEALTH),
373 GetResourceAmount(attacker, RESOURCE_ARMOR),
374 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
377 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target))
385 kill_count_to_attacker,
386 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
394 kill_count_to_target,
395 GetResourceAmount(attacker, RESOURCE_HEALTH),
396 GetResourceAmount(attacker, RESOURCE_ARMOR),
397 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
402 if(deathtype == DEATH_BUFF.m_id)
403 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
405 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker))
406 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
415 switch(DEATH_ENT(deathtype))
417 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
418 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
419 // and there will be a REAL DEATH_VOID implementation which mappers will use.
420 case DEATH_HURTTRIGGER:
422 Obituary_SpecialDeath(targ, false, deathtype,
434 Obituary_SpecialDeath(targ, false, deathtype,
436 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
446 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
451 LogDeath("accident", deathtype, targ, targ);
452 GiveFrags(targ, targ, -1, deathtype, weaponentity);
454 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
456 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
459 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
464 // reset target kill count
465 CS(targ).killcount = 0;
468 void Ice_Think(entity this)
470 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
475 setorigin(this, this.owner.origin - '0 0 16');
476 this.nextthink = time;
479 void Freeze (entity targ, float revivespeed, float frozen_type, float show_waypoint)
481 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // only specified entities can be freezed
484 if(STAT(FROZEN, targ))
487 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
489 STAT(FROZEN, targ) = frozen_type;
490 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == 3) ? 1 : 0);
491 SetResourceAmount(targ, RESOURCE_HEALTH, ((frozen_type == 3) ? targ_maxhealth : 1));
492 targ.revive_speed = revivespeed;
494 IL_REMOVE(g_bot_targets, targ);
495 targ.bot_attack = false;
496 targ.freeze_time = time;
498 entity ice = new(ice);
500 ice.scale = targ.scale;
501 setthink(ice, Ice_Think);
502 ice.nextthink = time;
503 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
504 setmodel(ice, MDL_ICE);
506 ice.colormod = Team_ColorRGB(targ.team);
507 ice.glowmod = ice.colormod;
509 targ.revival_time = 0;
513 RemoveGrapplingHooks(targ);
515 FOREACH_CLIENT(IS_PLAYER(it),
517 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
519 .entity weaponentity = weaponentities[slot];
520 if(it.(weaponentity).hook.aiment == targ)
521 RemoveHook(it.(weaponentity).hook);
527 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
530 void Unfreeze (entity targ)
532 if(!STAT(FROZEN, targ))
535 if(STAT(FROZEN, targ) && STAT(FROZEN, targ) != 3) // only reset health if target was frozen
537 SetResourceAmount(targ, RESOURCE_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
538 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
541 STAT(FROZEN, targ) = 0;
542 STAT(REVIVE_PROGRESS, targ) = 0;
543 targ.revival_time = time;
545 IL_PUSH(g_bot_targets, targ);
546 targ.bot_attack = true;
548 WaypointSprite_Kill(targ.waypointsprite_attached);
550 FOREACH_CLIENT(IS_PLAYER(it),
552 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
554 .entity weaponentity = weaponentities[slot];
555 if(it.(weaponentity).hook.aiment == targ)
556 RemoveHook(it.(weaponentity).hook);
560 // remove the ice block
562 delete(targ.iceblock);
563 targ.iceblock = NULL;
566 void Damage (entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
568 float complainteamdamage = 0;
569 float mirrordamage = 0;
570 float mirrorforce = 0;
572 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
575 entity attacker_save = attacker;
577 // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
578 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
580 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
586 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
588 // exit the vehicle before killing (fixes a crash)
589 if(IS_PLAYER(targ) && targ.vehicle)
590 vehicles_exit(targ.vehicle, VHEF_RELEASE);
592 // These are ALWAYS lethal
593 // No damage modification here
594 // Instead, prepare the victim for his death...
595 SetResourceAmount(targ, RESOURCE_ARMOR, 0);
596 targ.spawnshieldtime = 0;
597 SetResourceAmount(targ, RESOURCE_HEALTH, 0.9); // this is < 1
598 targ.flags -= targ.flags & FL_GODMODE;
601 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
607 // nullify damage if teamplay is on
608 if(deathtype != DEATH_TELEFRAG.m_id)
609 if(IS_PLAYER(attacker))
611 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
616 else if(SAME_TEAM(attacker, targ))
618 if(autocvar_teamplay_mode == 1)
620 else if(attacker != targ)
622 if(autocvar_teamplay_mode == 3)
624 else if(autocvar_teamplay_mode == 4)
626 if(IS_PLAYER(targ) && !IS_DEAD(targ))
628 attacker.dmg_team = attacker.dmg_team + damage;
629 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
630 if(complainteamdamage > 0)
631 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
632 mirrorforce = autocvar_g_mirrordamage * vlen(force);
633 damage = autocvar_g_friendlyfire * damage;
634 // mirrordamage will be used LATER
636 if(autocvar_g_mirrordamage_virtual)
638 vector v = healtharmor_applydamage(attacker.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
639 attacker.dmg_take += v.x;
640 attacker.dmg_save += v.y;
641 attacker.dmg_inflictor = inflictor;
646 if(autocvar_g_friendlyfire_virtual)
648 vector v = healtharmor_applydamage(targ.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, damage);
649 targ.dmg_take += v.x;
650 targ.dmg_save += v.y;
651 targ.dmg_inflictor = inflictor;
653 if(!autocvar_g_friendlyfire_virtual_force)
657 else if(!targ.canteamdamage)
664 if (!DEATH_ISSPECIAL(deathtype))
666 damage *= g_weapondamagefactor;
667 mirrordamage *= g_weapondamagefactor;
668 complainteamdamage *= g_weapondamagefactor;
669 force = force * g_weaponforcefactor;
670 mirrorforce *= g_weaponforcefactor;
673 // should this be changed at all? If so, in what way?
674 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
675 damage = M_ARGV(4, float);
676 mirrordamage = M_ARGV(5, float);
677 force = M_ARGV(6, vector);
679 if(IS_PLAYER(targ) && damage > 0 && attacker)
681 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
683 .entity went = weaponentities[slot];
684 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
685 RemoveHook(targ.(went).hook);
689 if(STAT(FROZEN, targ))
690 if(deathtype != DEATH_HURTTRIGGER.m_id && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
692 if(autocvar_g_frozen_revive_falldamage > 0)
693 if(deathtype == DEATH_FALL.m_id)
694 if(damage >= autocvar_g_frozen_revive_falldamage)
697 SetResourceAmount(targ, RESOURCE_HEALTH, autocvar_g_frozen_revive_falldamage_health);
698 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
699 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
700 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
704 force *= autocvar_g_frozen_force;
707 if(STAT(FROZEN, targ) && deathtype == DEATH_HURTTRIGGER.m_id && !autocvar_g_frozen_damage_trigger)
709 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
711 entity spot = SelectSpawnPoint (targ, false);
716 targ.deadflag = DEAD_NO;
718 targ.angles = spot.angles;
721 targ.effects |= EF_TELEPORT_BIT;
723 targ.angles_z = 0; // never spawn tilted even if the spot says to
724 targ.fixangle = true; // turn this way immediately
725 targ.velocity = '0 0 0';
726 targ.avelocity = '0 0 0';
727 targ.punchangle = '0 0 0';
728 targ.punchvector = '0 0 0';
729 targ.oldvelocity = targ.velocity;
731 targ.spawnorigin = spot.origin;
732 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
733 // don't reset back to last position, even if new position is stuck in solid
734 targ.oldorigin = targ.origin;
736 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
740 if(!MUTATOR_IS_ENABLED(mutator_instagib))
742 // apply strength multiplier
743 if (attacker.items & ITEM_Strength.m_itemid)
747 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
748 force = force * autocvar_g_balance_powerup_strength_selfforce;
752 damage = damage * autocvar_g_balance_powerup_strength_damage;
753 force = force * autocvar_g_balance_powerup_strength_force;
757 // apply invincibility multiplier
758 if (targ.items & ITEM_Shield.m_itemid)
760 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
761 if (targ != attacker)
763 force = force * autocvar_g_balance_powerup_invincible_takeforce;
768 if (targ == attacker)
769 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
774 if(deathtype != DEATH_BUFF.m_id)
775 if(targ.takedamage == DAMAGE_AIM)
779 if(IS_VEHICLE(targ) && targ.owner)
784 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
786 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
790 if(deathtype != DEATH_FIRE.m_id)
792 if(PHYS_INPUT_BUTTON_CHAT(victim))
793 attacker.typehitsound += 1;
795 attacker.damage_dealt += damage;
798 damage_goodhits += 1;
799 damage_gooddamage += damage;
801 if (!DEATH_ISSPECIAL(deathtype))
803 if(IS_PLAYER(targ)) // don't do this for vehicles
809 else if(IS_PLAYER(attacker))
811 if(deathtype != DEATH_FIRE.m_id)
813 attacker.typehitsound += 1;
815 if(complainteamdamage > 0)
816 if(time > CS(attacker).teamkill_complain)
818 CS(attacker).teamkill_complain = time + 5;
819 CS(attacker).teamkill_soundtime = time + 0.4;
820 CS(attacker).teamkill_soundsource = targ;
828 if (targ.damageforcescale)
830 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
832 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
833 if(targ.move_movetype == MOVETYPE_PHYSICS)
835 entity farcent = new(farce);
836 farcent.enemy = targ;
837 farcent.movedir = farce * 10;
839 farcent.movedir = farcent.movedir * targ.mass;
840 farcent.origin = hitloc;
841 farcent.forcetype = FORCETYPE_FORCEATPOS;
842 farcent.nextthink = time + 0.1;
843 setthink(farcent, SUB_Remove);
847 targ.velocity = targ.velocity + farce;
849 UNSET_ONGROUND(targ);
850 UpdateCSQCProjectile(targ);
853 if (damage != 0 || (targ.damageforcescale && force))
854 if (targ.event_damage)
855 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
857 // apply mirror damage if any
858 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
859 if(mirrordamage > 0 || mirrorforce > 0)
861 attacker = attacker_save;
863 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
864 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
868 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
869 float inflictorselfdamage, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
870 // Returns total damage applies to creatures
874 float total_damage_to_creatures;
879 float stat_damagedone;
881 if(RadiusDamage_running)
883 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
887 RadiusDamage_running = 1;
889 tfloordmg = autocvar_g_throughfloor_damage;
890 tfloorforce = autocvar_g_throughfloor_force;
892 total_damage_to_creatures = 0;
894 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
895 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
897 force = inflictorvelocity;
901 force = normalize(force);
902 if(forceintensity >= 0)
903 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
905 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
910 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
914 if ((targ != inflictor) || inflictorselfdamage)
915 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
922 // LordHavoc: measure distance to nearest point on target (not origin)
923 // (this guarentees 100% damage on a touch impact)
924 nearest = targ.WarpZone_findradius_nearest;
925 diff = targ.WarpZone_findradius_dist;
926 // round up a little on the damage to ensure full damage on impacts
927 // and turn the distance into a fraction of the radius
928 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
930 //bprint(ftos(power));
931 //if (targ == attacker)
932 // print(ftos(power), "\n");
938 finaldmg = coredamage * power + edgedamage * (1 - power);
944 vector myblastorigin;
947 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
949 // if it's a player, use the view origin as reference
950 center = CENTER_OR_VIEWOFS(targ);
952 force = normalize(center - myblastorigin);
953 force = force * (finaldmg / coredamage) * forceintensity;
956 if(deathtype & WEP_BLASTER.m_id)
957 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
959 if(targ != directhitentity)
964 float mininv_f, mininv_d;
966 // test line of sight to multiple positions on box,
967 // and do damage if any of them hit
970 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
971 // so for a given max stddev:
972 // n = (1 / (2 * max stddev of hitratio))^2
974 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
975 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
977 if(autocvar_g_throughfloor_debug)
978 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
981 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
983 if(autocvar_g_throughfloor_debug)
984 LOG_INFOF(" steps=%f", total);
988 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
990 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
992 if(autocvar_g_throughfloor_debug)
993 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
995 for(c = 0; c < total; ++c)
997 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
998 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
999 if (trace_fraction == 1 || trace_ent == targ)
1003 hitloc = hitloc + nearest;
1007 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1008 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1009 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1012 nearest = hitloc * (1 / max(1, hits));
1013 hitratio = (hits / total);
1014 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1015 finaldmg = finaldmg * a;
1016 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1019 if(autocvar_g_throughfloor_debug)
1020 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1023 //if (targ == attacker)
1025 // print("hits ", ftos(hits), " / ", ftos(total));
1026 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1027 // print(" (", ftos(a), ")\n");
1029 if(finaldmg || force)
1033 total_damage_to_creatures += finaldmg;
1035 if(accuracy_isgooddamage(attacker, targ))
1036 stat_damagedone += finaldmg;
1039 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1040 Damage (targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1042 Damage (targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1050 RadiusDamage_running = 0;
1052 if(!DEATH_ISSPECIAL(deathtype))
1053 accuracy_add(attacker, DEATH_WEAPONOF(deathtype).m_id, 0, min(coredamage, stat_damagedone));
1055 return total_damage_to_creatures;
1058 float RadiusDamage (entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1060 return RadiusDamageForSource (inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, weaponentity, directhitentity);
1063 float Fire_IsBurning(entity e)
1065 return (time < e.fire_endtime);
1068 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1071 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1082 // print("adding a fire burner to ", e.classname, "\n");
1083 e.fire_burner = new(fireburner);
1084 setthink(e.fire_burner, fireburner_think);
1085 e.fire_burner.nextthink = time;
1086 e.fire_burner.owner = e;
1092 if(Fire_IsBurning(e))
1094 mintime = e.fire_endtime - time;
1095 maxtime = max(mintime, t);
1097 mindps = e.fire_damagepersec;
1098 maxdps = max(mindps, dps);
1100 if(maxtime > mintime || maxdps > mindps)
1104 // damage we have right now
1105 mindamage = mindps * mintime;
1107 // damage we want to get
1108 maxdamage = mindamage + d;
1110 // but we can't exceed maxtime * maxdps!
1111 totaldamage = min(maxdamage, maxtime * maxdps);
1115 // totaldamage = min(mindamage + d, maxtime * maxdps)
1117 // totaldamage <= maxtime * maxdps
1118 // ==> totaldamage / maxdps <= maxtime.
1120 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1121 // >= min(mintime, maxtime)
1122 // ==> totaldamage / maxdps >= mintime.
1125 // how long do we damage then?
1126 // at least as long as before
1127 // but, never exceed maxdps
1128 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1132 // at most as long as maximum allowed
1133 // but, never below mindps
1134 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1136 // assuming t > mintime, dps > mindps:
1137 // we get d = t * dps = maxtime * maxdps
1138 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1139 // totaldamage / maxdps = maxtime
1140 // totaldamage / mindps > totaldamage / maxdps = maxtime
1142 // a) totaltime = max(mintime, maxtime) = maxtime
1143 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1145 // assuming t <= mintime:
1146 // we get maxtime = mintime
1147 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1148 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1150 // assuming dps <= mindps:
1151 // we get mindps = maxdps.
1152 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1153 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1154 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1156 e.fire_damagepersec = totaldamage / totaltime;
1157 e.fire_endtime = time + totaltime;
1158 if(totaldamage > 1.2 * mindamage)
1160 e.fire_deathtype = dt;
1161 if(e.fire_owner != o)
1164 e.fire_hitsound = false;
1167 if(accuracy_isgooddamage(o, e))
1168 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, max(0, totaldamage - mindamage));
1169 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1176 e.fire_damagepersec = dps;
1177 e.fire_endtime = time + t;
1178 e.fire_deathtype = dt;
1180 e.fire_hitsound = false;
1181 if(accuracy_isgooddamage(o, e))
1182 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, d);
1187 void Fire_ApplyDamage(entity e)
1192 if (!Fire_IsBurning(e))
1195 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1196 if(IS_NOT_A_CLIENT(o))
1199 // water and slime stop fire
1201 if(e.watertype != CONTENT_LAVA)
1208 t = min(frametime, e.fire_endtime - time);
1209 d = e.fire_damagepersec * t;
1211 hi = e.fire_owner.damage_dealt;
1212 ty = e.fire_owner.typehitsound;
1213 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1214 if(e.fire_hitsound && e.fire_owner)
1216 e.fire_owner.damage_dealt = hi;
1217 e.fire_owner.typehitsound = ty;
1219 e.fire_hitsound = true;
1221 if(!IS_INDEPENDENT_PLAYER(e))
1222 if(!STAT(FROZEN, e))
1223 FOREACH_CLIENT(IS_PLAYER(it) && it != e, {
1225 if(!IS_INDEPENDENT_PLAYER(it))
1226 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1228 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1229 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1230 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1235 void Fire_ApplyEffect(entity e)
1237 if(Fire_IsBurning(e))
1238 e.effects |= EF_FLAME;
1240 e.effects &= ~EF_FLAME;
1243 void fireburner_think(entity this)
1245 // for players, this is done in the regular loop
1246 if(wasfreed(this.owner))
1251 Fire_ApplyEffect(this.owner);
1252 if(!Fire_IsBurning(this.owner))
1254 this.owner.fire_burner = NULL;
1258 Fire_ApplyDamage(this.owner);
1259 this.nextthink = time;