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 "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/gamemodes/rules.qh>
28 #include <common/weapons/_all.qh>
29 #include "../lib/csqcmodel/sv_model.qh"
30 #include "../lib/warpzone/common.qh"
32 void UpdateFrags(entity player, int f)
34 GameRules_scoring_add_team(player, SCORE, f);
37 void GiveFrags (entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
39 // TODO route through PlayerScores instead
40 if(game_stopped) return;
47 GameRules_scoring_add(attacker, SUICIDES, 1);
52 GameRules_scoring_add(attacker, TEAMKILLS, 1);
58 GameRules_scoring_add(attacker, KILLS, 1);
59 if(!warmup_stage && targ.playerid)
60 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
63 GameRules_scoring_add(targ, DEATHS, 1);
65 // FIXME fix the mess this is (we have REAL points now!)
66 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
69 attacker.totalfrags += f;
72 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 < player.strength_finished)
90 if(time < player.invincible_finished)
92 if(player.flagcarried != NULL)
94 if(PHYS_INPUT_BUTTON_CHAT(player))
101 void LogDeath(string mode, int deathtype, entity killer, entity killed)
104 if(!autocvar_sv_eventlog)
106 s = strcat(":kill:", mode);
107 s = strcat(s, ":", ftos(killer.playerid));
108 s = strcat(s, ":", ftos(killed.playerid));
109 s = strcat(s, ":type=", Deathtype_Name(deathtype));
110 s = strcat(s, ":items=");
111 s = AppendItemcodes(s, killer);
114 s = strcat(s, ":victimitems=");
115 s = AppendItemcodes(s, killed);
120 void Obituary_SpecialDeath(
124 string s1, string s2, string s3,
125 float f1, float f2, float f3)
127 if(!DEATH_ISSPECIAL(deathtype))
129 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
133 entity deathent = Deathtypes_from(deathtype - DT_FIRST);
136 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
140 if(g_cts && deathtype == DEATH_KILL.m_id)
141 return; // TODO: somehow put this in CTS gamemode file!
143 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
146 Send_Notification_WOCOVA(
154 Send_Notification_WOCOVA(
158 death_message.nent_msginfo,
165 float Obituary_WeaponDeath(
169 string s1, string s2, string s3,
172 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
173 if (death_weapon == WEP_Null)
176 w_deathtype = deathtype;
177 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
182 Send_Notification_WOCOVA(
190 // send the info part to everyone
191 Send_Notification_WOCOVA(
195 death_message.nent_msginfo,
203 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %d!\n",
212 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target)
214 if(deathtype == DEATH_FIRE.m_id)
216 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
217 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));
221 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
224 entity buff_FirstFromFlags(int _buffs);
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, float frozen_type, float show_waypoint)
482 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // 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);
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;
567 void Damage (entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
569 float complainteamdamage = 0;
570 float mirrordamage = 0;
571 float mirrorforce = 0;
573 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
576 entity attacker_save = attacker;
578 // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
579 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
581 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
587 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
589 // exit the vehicle before killing (fixes a crash)
590 if(IS_PLAYER(targ) && targ.vehicle)
591 vehicles_exit(targ.vehicle, VHEF_RELEASE);
593 // These are ALWAYS lethal
594 // No damage modification here
595 // Instead, prepare the victim for his death...
596 SetResourceAmount(targ, RESOURCE_ARMOR, 0);
597 targ.spawnshieldtime = 0;
598 SetResourceAmount(targ, RESOURCE_HEALTH, 0.9); // this is < 1
599 targ.flags -= targ.flags & FL_GODMODE;
602 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
608 // nullify damage if teamplay is on
609 if(deathtype != DEATH_TELEFRAG.m_id)
610 if(IS_PLAYER(attacker))
612 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
617 else if(SAME_TEAM(attacker, targ))
619 if(autocvar_teamplay_mode == 1)
621 else if(attacker != targ)
623 if(autocvar_teamplay_mode == 3)
625 else if(autocvar_teamplay_mode == 4)
627 if(IS_PLAYER(targ) && !IS_DEAD(targ))
629 attacker.dmg_team = attacker.dmg_team + damage;
630 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
631 if(complainteamdamage > 0)
632 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
633 mirrorforce = autocvar_g_mirrordamage * vlen(force);
634 damage = autocvar_g_friendlyfire * damage;
635 // mirrordamage will be used LATER
637 if(autocvar_g_mirrordamage_virtual)
639 vector v = healtharmor_applydamage(attacker.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
640 attacker.dmg_take += v.x;
641 attacker.dmg_save += v.y;
642 attacker.dmg_inflictor = inflictor;
647 if(autocvar_g_friendlyfire_virtual)
649 vector v = healtharmor_applydamage(targ.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, damage);
650 targ.dmg_take += v.x;
651 targ.dmg_save += v.y;
652 targ.dmg_inflictor = inflictor;
654 if(!autocvar_g_friendlyfire_virtual_force)
658 else if(!targ.canteamdamage)
665 if (!DEATH_ISSPECIAL(deathtype))
667 damage *= g_weapondamagefactor;
668 mirrordamage *= g_weapondamagefactor;
669 complainteamdamage *= g_weapondamagefactor;
670 force = force * g_weaponforcefactor;
671 mirrorforce *= g_weaponforcefactor;
674 // should this be changed at all? If so, in what way?
675 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
676 damage = M_ARGV(4, float);
677 mirrordamage = M_ARGV(5, float);
678 force = M_ARGV(6, vector);
680 if(IS_PLAYER(targ) && damage > 0 && attacker)
682 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
684 .entity went = weaponentities[slot];
685 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
686 RemoveHook(targ.(went).hook);
690 if(STAT(FROZEN, targ))
691 if(deathtype != DEATH_HURTTRIGGER.m_id && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
693 if(autocvar_g_frozen_revive_falldamage > 0)
694 if(deathtype == DEATH_FALL.m_id)
695 if(damage >= autocvar_g_frozen_revive_falldamage)
698 SetResourceAmount(targ, RESOURCE_HEALTH, autocvar_g_frozen_revive_falldamage_health);
699 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
700 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
701 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
705 force *= autocvar_g_frozen_force;
708 if(STAT(FROZEN, targ) && deathtype == DEATH_HURTTRIGGER.m_id && !autocvar_g_frozen_damage_trigger)
710 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
712 entity spot = SelectSpawnPoint (targ, false);
717 targ.deadflag = DEAD_NO;
719 targ.angles = spot.angles;
722 targ.effects |= EF_TELEPORT_BIT;
724 targ.angles_z = 0; // never spawn tilted even if the spot says to
725 targ.fixangle = true; // turn this way immediately
726 targ.velocity = '0 0 0';
727 targ.avelocity = '0 0 0';
728 targ.punchangle = '0 0 0';
729 targ.punchvector = '0 0 0';
730 targ.oldvelocity = targ.velocity;
732 targ.spawnorigin = spot.origin;
733 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
734 // don't reset back to last position, even if new position is stuck in solid
735 targ.oldorigin = targ.origin;
737 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
741 if(!MUTATOR_IS_ENABLED(mutator_instagib))
743 // apply strength multiplier
744 if (attacker.items & ITEM_Strength.m_itemid)
748 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
749 force = force * autocvar_g_balance_powerup_strength_selfforce;
753 damage = damage * autocvar_g_balance_powerup_strength_damage;
754 force = force * autocvar_g_balance_powerup_strength_force;
758 // apply invincibility multiplier
759 if (targ.items & ITEM_Shield.m_itemid)
761 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
762 if (targ != attacker)
764 force = force * autocvar_g_balance_powerup_invincible_takeforce;
769 if (targ == attacker)
770 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
775 if(deathtype != DEATH_BUFF.m_id)
776 if(targ.takedamage == DAMAGE_AIM)
780 if(IS_VEHICLE(targ) && targ.owner)
785 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
787 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
791 if(deathtype != DEATH_FIRE.m_id)
793 if(PHYS_INPUT_BUTTON_CHAT(victim))
794 attacker.typehitsound += 1;
796 attacker.damage_dealt += damage;
799 damage_goodhits += 1;
800 damage_gooddamage += damage;
802 if (!DEATH_ISSPECIAL(deathtype))
804 if(IS_PLAYER(targ)) // don't do this for vehicles
810 else if(IS_PLAYER(attacker))
812 if(deathtype != DEATH_FIRE.m_id)
814 attacker.typehitsound += 1;
816 if(complainteamdamage > 0)
817 if(time > CS(attacker).teamkill_complain)
819 CS(attacker).teamkill_complain = time + 5;
820 CS(attacker).teamkill_soundtime = time + 0.4;
821 CS(attacker).teamkill_soundsource = targ;
829 if (targ.damageforcescale)
831 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
833 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
834 if(targ.move_movetype == MOVETYPE_PHYSICS)
836 entity farcent = new(farce);
837 farcent.enemy = targ;
838 farcent.movedir = farce * 10;
840 farcent.movedir = farcent.movedir * targ.mass;
841 farcent.origin = hitloc;
842 farcent.forcetype = FORCETYPE_FORCEATPOS;
843 farcent.nextthink = time + 0.1;
844 setthink(farcent, SUB_Remove);
848 targ.velocity = targ.velocity + farce;
850 UNSET_ONGROUND(targ);
851 UpdateCSQCProjectile(targ);
854 if (damage != 0 || (targ.damageforcescale && force))
855 if (targ.event_damage)
856 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
858 // apply mirror damage if any
859 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
860 if(mirrordamage > 0 || mirrorforce > 0)
862 attacker = attacker_save;
864 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
865 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
869 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
870 float inflictorselfdamage, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
871 // Returns total damage applies to creatures
875 float total_damage_to_creatures;
880 float stat_damagedone;
882 if(RadiusDamage_running)
884 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
888 RadiusDamage_running = 1;
890 tfloordmg = autocvar_g_throughfloor_damage;
891 tfloorforce = autocvar_g_throughfloor_force;
893 total_damage_to_creatures = 0;
895 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
896 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
898 force = inflictorvelocity;
902 force = normalize(force);
903 if(forceintensity >= 0)
904 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
906 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
911 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
915 if ((targ != inflictor) || inflictorselfdamage)
916 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
923 // LordHavoc: measure distance to nearest point on target (not origin)
924 // (this guarentees 100% damage on a touch impact)
925 nearest = targ.WarpZone_findradius_nearest;
926 diff = targ.WarpZone_findradius_dist;
927 // round up a little on the damage to ensure full damage on impacts
928 // and turn the distance into a fraction of the radius
929 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
931 //bprint(ftos(power));
932 //if (targ == attacker)
933 // print(ftos(power), "\n");
939 finaldmg = coredamage * power + edgedamage * (1 - power);
945 vector myblastorigin;
948 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
950 // if it's a player, use the view origin as reference
951 center = CENTER_OR_VIEWOFS(targ);
953 force = normalize(center - myblastorigin);
954 force = force * (finaldmg / coredamage) * forceintensity;
957 if(deathtype & WEP_BLASTER.m_id)
958 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
960 if(targ != directhitentity)
965 float mininv_f, mininv_d;
967 // test line of sight to multiple positions on box,
968 // and do damage if any of them hit
971 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
972 // so for a given max stddev:
973 // n = (1 / (2 * max stddev of hitratio))^2
975 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
976 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
978 if(autocvar_g_throughfloor_debug)
979 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
982 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
984 if(autocvar_g_throughfloor_debug)
985 LOG_INFOF(" steps=%f", total);
989 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
991 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
993 if(autocvar_g_throughfloor_debug)
994 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
996 for(c = 0; c < total; ++c)
998 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
999 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1000 if (trace_fraction == 1 || trace_ent == targ)
1004 hitloc = hitloc + nearest;
1008 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1009 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1010 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1013 nearest = hitloc * (1 / max(1, hits));
1014 hitratio = (hits / total);
1015 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1016 finaldmg = finaldmg * a;
1017 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1020 if(autocvar_g_throughfloor_debug)
1021 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1024 //if (targ == attacker)
1026 // print("hits ", ftos(hits), " / ", ftos(total));
1027 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1028 // print(" (", ftos(a), ")\n");
1030 if(finaldmg || force)
1034 total_damage_to_creatures += finaldmg;
1036 if(accuracy_isgooddamage(attacker, targ))
1037 stat_damagedone += finaldmg;
1040 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1041 Damage (targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1043 Damage (targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1051 RadiusDamage_running = 0;
1053 if(!DEATH_ISSPECIAL(deathtype))
1054 accuracy_add(attacker, DEATH_WEAPONOF(deathtype).m_id, 0, min(coredamage, stat_damagedone));
1056 return total_damage_to_creatures;
1059 float RadiusDamage (entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1061 return RadiusDamageForSource (inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, weaponentity, directhitentity);
1064 float Fire_IsBurning(entity e)
1066 return (time < e.fire_endtime);
1069 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1072 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1083 // print("adding a fire burner to ", e.classname, "\n");
1084 e.fire_burner = new(fireburner);
1085 setthink(e.fire_burner, fireburner_think);
1086 e.fire_burner.nextthink = time;
1087 e.fire_burner.owner = e;
1093 if(Fire_IsBurning(e))
1095 mintime = e.fire_endtime - time;
1096 maxtime = max(mintime, t);
1098 mindps = e.fire_damagepersec;
1099 maxdps = max(mindps, dps);
1101 if(maxtime > mintime || maxdps > mindps)
1105 // damage we have right now
1106 mindamage = mindps * mintime;
1108 // damage we want to get
1109 maxdamage = mindamage + d;
1111 // but we can't exceed maxtime * maxdps!
1112 totaldamage = min(maxdamage, maxtime * maxdps);
1116 // totaldamage = min(mindamage + d, maxtime * maxdps)
1118 // totaldamage <= maxtime * maxdps
1119 // ==> totaldamage / maxdps <= maxtime.
1121 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1122 // >= min(mintime, maxtime)
1123 // ==> totaldamage / maxdps >= mintime.
1126 // how long do we damage then?
1127 // at least as long as before
1128 // but, never exceed maxdps
1129 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1133 // at most as long as maximum allowed
1134 // but, never below mindps
1135 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1137 // assuming t > mintime, dps > mindps:
1138 // we get d = t * dps = maxtime * maxdps
1139 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1140 // totaldamage / maxdps = maxtime
1141 // totaldamage / mindps > totaldamage / maxdps = maxtime
1143 // a) totaltime = max(mintime, maxtime) = maxtime
1144 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1146 // assuming t <= mintime:
1147 // we get maxtime = mintime
1148 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1149 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1151 // assuming dps <= mindps:
1152 // we get mindps = maxdps.
1153 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1154 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1155 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1157 e.fire_damagepersec = totaldamage / totaltime;
1158 e.fire_endtime = time + totaltime;
1159 if(totaldamage > 1.2 * mindamage)
1161 e.fire_deathtype = dt;
1162 if(e.fire_owner != o)
1165 e.fire_hitsound = false;
1168 if(accuracy_isgooddamage(o, e))
1169 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, max(0, totaldamage - mindamage));
1170 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1177 e.fire_damagepersec = dps;
1178 e.fire_endtime = time + t;
1179 e.fire_deathtype = dt;
1181 e.fire_hitsound = false;
1182 if(accuracy_isgooddamage(o, e))
1183 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, d);
1188 void Fire_ApplyDamage(entity e)
1193 if (!Fire_IsBurning(e))
1196 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1197 if(IS_NOT_A_CLIENT(o))
1200 // water and slime stop fire
1202 if(e.watertype != CONTENT_LAVA)
1209 t = min(frametime, e.fire_endtime - time);
1210 d = e.fire_damagepersec * t;
1212 hi = e.fire_owner.damage_dealt;
1213 ty = e.fire_owner.typehitsound;
1214 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1215 if(e.fire_hitsound && e.fire_owner)
1217 e.fire_owner.damage_dealt = hi;
1218 e.fire_owner.typehitsound = ty;
1220 e.fire_hitsound = true;
1222 if(!IS_INDEPENDENT_PLAYER(e))
1223 if(!STAT(FROZEN, e))
1224 FOREACH_CLIENT(IS_PLAYER(it) && it != e, {
1226 if(!IS_INDEPENDENT_PLAYER(it))
1227 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1229 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1230 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1231 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1236 void Fire_ApplyEffect(entity e)
1238 if(Fire_IsBurning(e))
1239 e.effects |= EF_FLAME;
1241 e.effects &= ~EF_FLAME;
1244 void fireburner_think(entity this)
1246 // for players, this is done in the regular loop
1247 if(wasfreed(this.owner))
1252 Fire_ApplyEffect(this.owner);
1253 if(!Fire_IsBurning(this.owner))
1255 this.owner.fire_burner = NULL;
1259 Fire_ApplyDamage(this.owner);
1260 this.nextthink = time;