3 #include <common/effects/all.qh>
6 #include <server/mutators/_mod.qh>
9 #include "spawnpoints.qh"
10 #include "../common/state.qh"
11 #include "../common/physics/player.qh"
12 #include "../common/t_items.qh"
13 #include "resources.qh"
14 #include "../common/vehicles/all.qh"
15 #include "../common/items/_mod.qh"
16 #include "../common/mutators/mutator/waypoints/waypointsprites.qh"
17 #include "../common/mutators/mutator/instagib/sv_instagib.qh"
18 #include "../common/mutators/mutator/buffs/buffs.qh"
19 #include "weapons/accuracy.qh"
20 #include "weapons/csqcprojectile.qh"
21 #include "weapons/selection.qh"
22 #include "../common/constants.qh"
23 #include "../common/deathtypes/all.qh"
24 #include "../common/notifications/all.qh"
25 #include "../common/physics/movetypes/movetypes.qh"
26 #include "../common/playerstats.qh"
27 #include "../common/teams.qh"
28 #include "../common/util.qh"
29 #include <common/gamemodes/rules.qh>
30 #include <common/weapons/_all.qh>
31 #include "../lib/csqcmodel/sv_model.qh"
32 #include "../lib/warpzone/common.qh"
34 void UpdateFrags(entity player, int f)
36 GameRules_scoring_add_team(player, SCORE, f);
39 void GiveFrags(entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
41 // TODO route through PlayerScores instead
42 if(game_stopped) return;
49 GameRules_scoring_add(attacker, SUICIDES, 1);
54 GameRules_scoring_add(attacker, TEAMKILLS, 1);
60 GameRules_scoring_add(attacker, KILLS, 1);
61 if(!warmup_stage && targ.playerid)
62 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
65 GameRules_scoring_add(targ, DEATHS, 1);
67 // FIXME fix the mess this is (we have REAL points now!)
68 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
71 attacker.totalfrags += f;
74 UpdateFrags(attacker, f);
77 string AppendItemcodes(string s, entity player)
79 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
81 .entity weaponentity = weaponentities[slot];
82 int w = player.(weaponentity).m_weapon.m_id;
84 w = player.(weaponentity).cnt; // previous weapon
85 if(w != 0 || slot == 0)
86 s = strcat(s, ftos(w));
88 if(time < STAT(STRENGTH_FINISHED, player))
90 if(time < STAT(INVINCIBLE_FINISHED, player))
92 if(PHYS_INPUT_BUTTON_CHAT(player))
94 // TODO: include these codes as a flag on the item itself
95 MUTATOR_CALLHOOK(LogDeath_AppendItemCodes, player, s);
96 s = M_ARGV(1, string);
100 void LogDeath(string mode, int deathtype, entity killer, entity killed)
103 if(!autocvar_sv_eventlog)
105 s = strcat(":kill:", mode);
106 s = strcat(s, ":", ftos(killer.playerid));
107 s = strcat(s, ":", ftos(killed.playerid));
108 s = strcat(s, ":type=", Deathtype_Name(deathtype));
109 s = strcat(s, ":items=");
110 s = AppendItemcodes(s, killer);
113 s = strcat(s, ":victimitems=");
114 s = AppendItemcodes(s, killed);
119 void Obituary_SpecialDeath(
123 string s1, string s2, string s3,
124 float f1, float f2, float f3)
126 if(!DEATH_ISSPECIAL(deathtype))
128 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
132 entity deathent = REGISTRY_GET(Deathtypes, deathtype - DT_FIRST);
135 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
139 if(g_cts && deathtype == DEATH_KILL.m_id)
140 return; // TODO: somehow put this in CTS gamemode file!
142 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
145 Send_Notification_WOCOVA(
153 Send_Notification_WOCOVA(
157 death_message.nent_msginfo,
164 float Obituary_WeaponDeath(
168 string s1, string s2, string s3,
171 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
172 if (death_weapon == WEP_Null)
175 w_deathtype = deathtype;
176 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
181 Send_Notification_WOCOVA(
189 // send the info part to everyone
190 Send_Notification_WOCOVA(
194 death_message.nent_msginfo,
202 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %s!\n",
211 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target)
213 if(deathtype == DEATH_FIRE.m_id)
215 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
216 Send_Notification(NOTIF_ONE, targ, MSG_CHOICE, CHOICE_FRAGGED_FIRE, attacker.netname, kill_count_to_target, GetResource(attacker, RES_HEALTH), GetResource(attacker, RES_ARMOR), (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping));
220 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
223 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
226 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
229 float notif_firstblood = false;
230 float kill_count_to_attacker, kill_count_to_target;
232 // Set final information for the death
233 targ.death_origin = targ.origin;
234 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
236 #ifdef NOTIFICATIONS_DEBUG
239 "Obituary(%s, %s, %s, %s = %d);\n",
243 Deathtype_Name(deathtype),
254 if(DEATH_ISSPECIAL(deathtype))
256 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
258 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
262 switch(DEATH_ENT(deathtype))
264 case DEATH_MIRRORDAMAGE:
266 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
272 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
278 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
280 backtrace("SUICIDE: what the hell happened here?\n");
283 LogDeath("suicide", deathtype, targ, targ);
284 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
285 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
291 else if(IS_PLAYER(attacker))
293 if(SAME_TEAM(attacker, targ))
295 LogDeath("tk", deathtype, attacker, targ);
296 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
298 CS(attacker).killcount = 0;
300 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
301 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker.netname);
302 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker.netname, deathlocation, CS(targ).killcount);
304 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
305 // No need for specific death/weapon messages...
309 LogDeath("frag", deathtype, attacker, targ);
310 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
312 CS(attacker).taunt_soundtime = time + 1;
313 CS(attacker).killcount = CS(attacker).killcount + 1;
315 attacker.killsound += 1;
317 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
318 // these 2 macros are spread over multiple files
319 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
321 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
323 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
326 switch(CS(attacker).killcount)
333 if(!warmup_stage && !checkrules_firstblood)
335 checkrules_firstblood = true;
336 notif_firstblood = true; // modify the current messages so that they too show firstblood information
337 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
338 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
340 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
341 kill_count_to_attacker = -1;
342 kill_count_to_target = -2;
346 kill_count_to_attacker = CS(attacker).killcount;
347 kill_count_to_target = 0;
358 kill_count_to_attacker,
359 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
367 kill_count_to_target,
368 GetResource(attacker, RES_HEALTH),
369 GetResource(attacker, RES_ARMOR),
370 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
373 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target))
381 kill_count_to_attacker,
382 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
390 kill_count_to_target,
391 GetResource(attacker, RES_HEALTH),
392 GetResource(attacker, RES_ARMOR),
393 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
398 if(deathtype == DEATH_BUFF.m_id)
399 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
401 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker))
402 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
411 switch(DEATH_ENT(deathtype))
413 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
414 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
415 // and there will be a REAL DEATH_VOID implementation which mappers will use.
416 case DEATH_HURTTRIGGER:
418 Obituary_SpecialDeath(targ, false, deathtype,
430 Obituary_SpecialDeath(targ, false, deathtype,
432 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
442 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
447 LogDeath("accident", deathtype, targ, targ);
448 GiveFrags(targ, targ, -1, deathtype, weaponentity);
450 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
452 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
455 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
460 // reset target kill count
461 CS(targ).killcount = 0;
464 void Ice_Think(entity this)
466 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
471 vector ice_org = this.owner.origin - '0 0 16';
472 if (this.origin != ice_org)
473 setorigin(this, ice_org);
474 this.nextthink = time;
477 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
479 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
482 if(STAT(FROZEN, targ))
485 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
487 STAT(FROZEN, targ) = frozen_type;
488 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
489 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
490 targ.revive_speed = revivespeed;
492 IL_REMOVE(g_bot_targets, targ);
493 targ.bot_attack = false;
494 targ.freeze_time = time;
496 entity ice = new(ice);
498 ice.scale = targ.scale;
499 // set_movetype(ice, MOVETYPE_FOLLOW) would rotate the ice model with the player
500 setthink(ice, Ice_Think);
501 ice.nextthink = time;
502 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
503 setmodel(ice, MDL_ICE);
505 ice.colormod = Team_ColorRGB(targ.team);
506 ice.glowmod = ice.colormod;
508 targ.revival_time = 0;
512 RemoveGrapplingHooks(targ);
514 FOREACH_CLIENT(IS_PLAYER(it),
516 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
518 .entity weaponentity = weaponentities[slot];
519 if(it.(weaponentity).hook.aiment == targ)
520 RemoveHook(it.(weaponentity).hook);
525 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
526 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
529 void Unfreeze(entity targ, bool reset_health)
531 if(!STAT(FROZEN, targ))
534 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
535 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
537 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
539 STAT(FROZEN, targ) = 0;
540 STAT(REVIVE_PROGRESS, targ) = 0;
541 targ.revival_time = time;
543 IL_PUSH(g_bot_targets, targ);
544 targ.bot_attack = true;
546 WaypointSprite_Kill(targ.waypointsprite_attached);
548 FOREACH_CLIENT(IS_PLAYER(it),
550 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
552 .entity weaponentity = weaponentities[slot];
553 if(it.(weaponentity).hook.aiment == targ)
554 RemoveHook(it.(weaponentity).hook);
558 // remove the ice block
560 delete(targ.iceblock);
561 targ.iceblock = NULL;
563 MUTATOR_CALLHOOK(Unfreeze, targ);
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 SetResourceExplicit(targ, RES_ARMOR, 0);
596 targ.spawnshieldtime = 0;
597 SetResourceExplicit(targ, RES_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 == 2)
624 if(IS_PLAYER(targ) && !IS_DEAD(targ))
626 attacker.dmg_team = attacker.dmg_team + damage;
627 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
630 else if(autocvar_teamplay_mode == 3)
632 else if(autocvar_teamplay_mode == 4)
634 if(IS_PLAYER(targ) && !IS_DEAD(targ))
636 attacker.dmg_team = attacker.dmg_team + damage;
637 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
638 if(complainteamdamage > 0)
639 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
640 mirrorforce = autocvar_g_mirrordamage * vlen(force);
641 damage = autocvar_g_friendlyfire * damage;
642 // mirrordamage will be used LATER
644 if(autocvar_g_mirrordamage_virtual)
646 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
647 attacker.dmg_take += v.x;
648 attacker.dmg_save += v.y;
649 attacker.dmg_inflictor = inflictor;
654 if(autocvar_g_friendlyfire_virtual)
656 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
657 targ.dmg_take += v.x;
658 targ.dmg_save += v.y;
659 targ.dmg_inflictor = inflictor;
661 if(!autocvar_g_friendlyfire_virtual_force)
665 else if(!targ.canteamdamage)
672 if (!DEATH_ISSPECIAL(deathtype))
674 damage *= g_weapondamagefactor;
675 mirrordamage *= g_weapondamagefactor;
676 complainteamdamage *= g_weapondamagefactor;
677 force = force * g_weaponforcefactor;
678 mirrorforce *= g_weaponforcefactor;
681 // should this be changed at all? If so, in what way?
682 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
683 damage = M_ARGV(4, float);
684 mirrordamage = M_ARGV(5, float);
685 force = M_ARGV(6, vector);
687 if(IS_PLAYER(targ) && damage > 0 && attacker)
689 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
691 .entity went = weaponentities[slot];
692 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
693 RemoveHook(targ.(went).hook);
697 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
698 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
700 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
702 Unfreeze(targ, false);
703 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
704 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
705 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
706 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
710 force *= autocvar_g_frozen_force;
713 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
714 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
716 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
718 entity spot = SelectSpawnPoint(targ, false);
722 targ.deadflag = DEAD_NO;
724 targ.angles = spot.angles;
727 targ.effects |= EF_TELEPORT_BIT;
729 targ.angles_z = 0; // never spawn tilted even if the spot says to
730 targ.fixangle = true; // turn this way immediately
731 targ.velocity = '0 0 0';
732 targ.avelocity = '0 0 0';
733 targ.punchangle = '0 0 0';
734 targ.punchvector = '0 0 0';
735 targ.oldvelocity = targ.velocity;
737 targ.spawnorigin = spot.origin;
738 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
739 // don't reset back to last position, even if new position is stuck in solid
740 targ.oldorigin = targ.origin;
742 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
746 if(!MUTATOR_IS_ENABLED(mutator_instagib))
748 // apply strength multiplier
749 if (attacker.items & ITEM_Strength.m_itemid)
753 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
754 force = force * autocvar_g_balance_powerup_strength_selfforce;
758 damage = damage * autocvar_g_balance_powerup_strength_damage;
759 force = force * autocvar_g_balance_powerup_strength_force;
763 // apply invincibility multiplier
764 if (targ.items & ITEM_Shield.m_itemid)
766 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
767 if (targ != attacker)
769 force = force * autocvar_g_balance_powerup_invincible_takeforce;
774 if (targ == attacker)
775 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
780 if(deathtype != DEATH_BUFF.m_id)
781 if(targ.takedamage == DAMAGE_AIM)
785 if(IS_VEHICLE(targ) && targ.owner)
790 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
792 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
796 if(deathtype != DEATH_FIRE.m_id)
798 if(PHYS_INPUT_BUTTON_CHAT(victim))
799 attacker.typehitsound += 1;
801 attacker.damage_dealt += damage;
804 damage_goodhits += 1;
805 damage_gooddamage += damage;
807 if (!DEATH_ISSPECIAL(deathtype))
809 if(IS_PLAYER(targ)) // don't do this for vehicles
815 else if(IS_PLAYER(attacker))
817 // if enemy gets frozen in this frame and receives other damage don't
818 // play the typehitsound e.g. when hit by multiple bullets of the shotgun
819 if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
821 attacker.typehitsound += 1;
823 if(complainteamdamage > 0)
824 if(time > CS(attacker).teamkill_complain)
826 CS(attacker).teamkill_complain = time + 5;
827 CS(attacker).teamkill_soundtime = time + 0.4;
828 CS(attacker).teamkill_soundsource = targ;
836 if (targ.damageforcescale)
838 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
840 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
841 if(targ.move_movetype == MOVETYPE_PHYSICS)
843 entity farcent = new(farce);
844 farcent.enemy = targ;
845 farcent.movedir = farce * 10;
847 farcent.movedir = farcent.movedir * targ.mass;
848 farcent.origin = hitloc;
849 farcent.forcetype = FORCETYPE_FORCEATPOS;
850 farcent.nextthink = time + 0.1;
851 setthink(farcent, SUB_Remove);
855 targ.velocity = targ.velocity + farce;
857 UNSET_ONGROUND(targ);
858 UpdateCSQCProjectile(targ);
861 if (damage != 0 || (targ.damageforcescale && force))
862 if (targ.event_damage)
863 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
865 // apply mirror damage if any
866 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
867 if(mirrordamage > 0 || mirrorforce > 0)
869 attacker = attacker_save;
871 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
872 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
876 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
877 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
878 // Returns total damage applies to creatures
882 float total_damage_to_creatures;
887 float stat_damagedone;
889 if(RadiusDamage_running)
891 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
895 RadiusDamage_running = 1;
897 tfloordmg = autocvar_g_throughfloor_damage;
898 tfloorforce = autocvar_g_throughfloor_force;
900 total_damage_to_creatures = 0;
902 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
903 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
905 force = inflictorvelocity;
909 force = normalize(force);
910 if(forceintensity >= 0)
911 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
913 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
918 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
922 if ((targ != inflictor) || inflictorselfdamage)
923 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
930 // LordHavoc: measure distance to nearest point on target (not origin)
931 // (this guarentees 100% damage on a touch impact)
932 nearest = targ.WarpZone_findradius_nearest;
933 diff = targ.WarpZone_findradius_dist;
934 // round up a little on the damage to ensure full damage on impacts
935 // and turn the distance into a fraction of the radius
936 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
938 //bprint(ftos(power));
939 //if (targ == attacker)
940 // print(ftos(power), "\n");
946 finaldmg = coredamage * power + edgedamage * (1 - power);
952 vector myblastorigin;
955 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
957 // if it's a player, use the view origin as reference
958 center = CENTER_OR_VIEWOFS(targ);
960 force = normalize(center - myblastorigin);
961 force = force * (finaldmg / coredamage) * forceintensity;
964 // apply special scaling along the z axis if set
965 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
967 force.z *= forcezscale;
969 if(targ != directhitentity)
974 float mininv_f, mininv_d;
976 // test line of sight to multiple positions on box,
977 // and do damage if any of them hit
980 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
981 // so for a given max stddev:
982 // n = (1 / (2 * max stddev of hitratio))^2
984 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
985 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
987 if(autocvar_g_throughfloor_debug)
988 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
991 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
993 if(autocvar_g_throughfloor_debug)
994 LOG_INFOF(" steps=%f", total);
998 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1000 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1002 if(autocvar_g_throughfloor_debug)
1003 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1005 for(c = 0; c < total; ++c)
1007 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1008 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1009 if (trace_fraction == 1 || trace_ent == targ)
1013 hitloc = hitloc + nearest;
1017 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1018 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1019 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1022 nearest = hitloc * (1 / max(1, hits));
1023 hitratio = (hits / total);
1024 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1025 finaldmg = finaldmg * a;
1026 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1029 if(autocvar_g_throughfloor_debug)
1030 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1033 //if (targ == attacker)
1035 // print("hits ", ftos(hits), " / ", ftos(total));
1036 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1037 // print(" (", ftos(a), ")\n");
1039 if(finaldmg || force)
1043 total_damage_to_creatures += finaldmg;
1045 if(accuracy_isgooddamage(attacker, targ))
1046 stat_damagedone += finaldmg;
1049 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1050 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1052 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1060 RadiusDamage_running = 0;
1062 if(!DEATH_ISSPECIAL(deathtype))
1063 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1065 return total_damage_to_creatures;
1068 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1070 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1071 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1074 bool Heal(entity targ, entity inflictor, float amount, float limit)
1076 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1079 bool healed = false;
1081 healed = targ.event_heal(targ, inflictor, amount, limit);
1082 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1083 // TODO: healing fx!
1084 // TODO: armor healing?
1088 float Fire_IsBurning(entity e)
1090 return (time < e.fire_endtime);
1093 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1096 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1107 // print("adding a fire burner to ", e.classname, "\n");
1108 e.fire_burner = new(fireburner);
1109 setthink(e.fire_burner, fireburner_think);
1110 e.fire_burner.nextthink = time;
1111 e.fire_burner.owner = e;
1117 if(Fire_IsBurning(e))
1119 mintime = e.fire_endtime - time;
1120 maxtime = max(mintime, t);
1122 mindps = e.fire_damagepersec;
1123 maxdps = max(mindps, dps);
1125 if(maxtime > mintime || maxdps > mindps)
1129 // damage we have right now
1130 mindamage = mindps * mintime;
1132 // damage we want to get
1133 maxdamage = mindamage + d;
1135 // but we can't exceed maxtime * maxdps!
1136 totaldamage = min(maxdamage, maxtime * maxdps);
1140 // totaldamage = min(mindamage + d, maxtime * maxdps)
1142 // totaldamage <= maxtime * maxdps
1143 // ==> totaldamage / maxdps <= maxtime.
1145 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1146 // >= min(mintime, maxtime)
1147 // ==> totaldamage / maxdps >= mintime.
1150 // how long do we damage then?
1151 // at least as long as before
1152 // but, never exceed maxdps
1153 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1157 // at most as long as maximum allowed
1158 // but, never below mindps
1159 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1161 // assuming t > mintime, dps > mindps:
1162 // we get d = t * dps = maxtime * maxdps
1163 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1164 // totaldamage / maxdps = maxtime
1165 // totaldamage / mindps > totaldamage / maxdps = maxtime
1167 // a) totaltime = max(mintime, maxtime) = maxtime
1168 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1170 // assuming t <= mintime:
1171 // we get maxtime = mintime
1172 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1173 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1175 // assuming dps <= mindps:
1176 // we get mindps = maxdps.
1177 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1178 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1179 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1181 e.fire_damagepersec = totaldamage / totaltime;
1182 e.fire_endtime = time + totaltime;
1183 if(totaldamage > 1.2 * mindamage)
1185 e.fire_deathtype = dt;
1186 if(e.fire_owner != o)
1189 e.fire_hitsound = false;
1192 if(accuracy_isgooddamage(o, e))
1193 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1194 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1201 e.fire_damagepersec = dps;
1202 e.fire_endtime = time + t;
1203 e.fire_deathtype = dt;
1205 e.fire_hitsound = false;
1206 if(accuracy_isgooddamage(o, e))
1207 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1212 void Fire_ApplyDamage(entity e)
1217 if (!Fire_IsBurning(e))
1220 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1221 if(IS_NOT_A_CLIENT(o))
1224 // water and slime stop fire
1226 if(e.watertype != CONTENT_LAVA)
1233 t = min(frametime, e.fire_endtime - time);
1234 d = e.fire_damagepersec * t;
1236 hi = e.fire_owner.damage_dealt;
1237 ty = e.fire_owner.typehitsound;
1238 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1239 if(e.fire_hitsound && e.fire_owner)
1241 e.fire_owner.damage_dealt = hi;
1242 e.fire_owner.typehitsound = ty;
1244 e.fire_hitsound = true;
1246 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1248 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1250 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1251 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1253 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1254 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1255 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1261 void Fire_ApplyEffect(entity e)
1263 if(Fire_IsBurning(e))
1264 e.effects |= EF_FLAME;
1266 e.effects &= ~EF_FLAME;
1269 void fireburner_think(entity this)
1271 // for players, this is done in the regular loop
1272 if(wasfreed(this.owner))
1277 Fire_ApplyEffect(this.owner);
1278 if(!Fire_IsBurning(this.owner))
1280 this.owner.fire_burner = NULL;
1284 Fire_ApplyDamage(this.owner);
1285 this.nextthink = time;