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, 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 SetResourceAmountExplicit(targ, RESOURCE_ARMOR, 0);
597 targ.spawnshieldtime = 0;
598 SetResourceAmountExplicit(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(GetResourceAmount(attacker, RESOURCE_ARMOR), 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(GetResourceAmount(targ, RESOURCE_ARMOR), 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 bool Heal(entity targ, entity inflictor, float amount, float limit)
1066 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ))
1069 bool healed = false;
1071 healed = targ.event_heal(targ, inflictor, amount, limit);
1072 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1073 // TODO: healing fx!
1077 float Fire_IsBurning(entity e)
1079 return (time < e.fire_endtime);
1082 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1085 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1096 // print("adding a fire burner to ", e.classname, "\n");
1097 e.fire_burner = new(fireburner);
1098 setthink(e.fire_burner, fireburner_think);
1099 e.fire_burner.nextthink = time;
1100 e.fire_burner.owner = e;
1106 if(Fire_IsBurning(e))
1108 mintime = e.fire_endtime - time;
1109 maxtime = max(mintime, t);
1111 mindps = e.fire_damagepersec;
1112 maxdps = max(mindps, dps);
1114 if(maxtime > mintime || maxdps > mindps)
1118 // damage we have right now
1119 mindamage = mindps * mintime;
1121 // damage we want to get
1122 maxdamage = mindamage + d;
1124 // but we can't exceed maxtime * maxdps!
1125 totaldamage = min(maxdamage, maxtime * maxdps);
1129 // totaldamage = min(mindamage + d, maxtime * maxdps)
1131 // totaldamage <= maxtime * maxdps
1132 // ==> totaldamage / maxdps <= maxtime.
1134 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1135 // >= min(mintime, maxtime)
1136 // ==> totaldamage / maxdps >= mintime.
1139 // how long do we damage then?
1140 // at least as long as before
1141 // but, never exceed maxdps
1142 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1146 // at most as long as maximum allowed
1147 // but, never below mindps
1148 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1150 // assuming t > mintime, dps > mindps:
1151 // we get d = t * dps = maxtime * maxdps
1152 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1153 // totaldamage / maxdps = maxtime
1154 // totaldamage / mindps > totaldamage / maxdps = maxtime
1156 // a) totaltime = max(mintime, maxtime) = maxtime
1157 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1159 // assuming t <= mintime:
1160 // we get maxtime = mintime
1161 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1162 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1164 // assuming dps <= mindps:
1165 // we get mindps = maxdps.
1166 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1167 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1168 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1170 e.fire_damagepersec = totaldamage / totaltime;
1171 e.fire_endtime = time + totaltime;
1172 if(totaldamage > 1.2 * mindamage)
1174 e.fire_deathtype = dt;
1175 if(e.fire_owner != o)
1178 e.fire_hitsound = false;
1181 if(accuracy_isgooddamage(o, e))
1182 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, max(0, totaldamage - mindamage));
1183 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1190 e.fire_damagepersec = dps;
1191 e.fire_endtime = time + t;
1192 e.fire_deathtype = dt;
1194 e.fire_hitsound = false;
1195 if(accuracy_isgooddamage(o, e))
1196 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, d);
1201 void Fire_ApplyDamage(entity e)
1206 if (!Fire_IsBurning(e))
1209 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1210 if(IS_NOT_A_CLIENT(o))
1213 // water and slime stop fire
1215 if(e.watertype != CONTENT_LAVA)
1222 t = min(frametime, e.fire_endtime - time);
1223 d = e.fire_damagepersec * t;
1225 hi = e.fire_owner.damage_dealt;
1226 ty = e.fire_owner.typehitsound;
1227 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1228 if(e.fire_hitsound && e.fire_owner)
1230 e.fire_owner.damage_dealt = hi;
1231 e.fire_owner.typehitsound = ty;
1233 e.fire_hitsound = true;
1235 if(!IS_INDEPENDENT_PLAYER(e))
1236 if(!STAT(FROZEN, e))
1237 FOREACH_CLIENT(IS_PLAYER(it) && it != e, {
1239 if(!IS_INDEPENDENT_PLAYER(it))
1240 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1242 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1243 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1244 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1249 void Fire_ApplyEffect(entity e)
1251 if(Fire_IsBurning(e))
1252 e.effects |= EF_FLAME;
1254 e.effects &= ~EF_FLAME;
1257 void fireburner_think(entity this)
1259 // for players, this is done in the regular loop
1260 if(wasfreed(this.owner))
1265 Fire_ApplyEffect(this.owner);
1266 if(!Fire_IsBurning(this.owner))
1268 this.owner.fire_burner = NULL;
1272 Fire_ApplyDamage(this.owner);
1273 this.nextthink = time;