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 < player.strength_finished)
90 if(time < player.invincible_finished)
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 = Deathtypes_from(deathtype - DT_FIRST);
135 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
139 if(g_cts && deathtype == DEATH_KILL.m_id)
140 return; // TODO: somehow put this in CTS gamemode file!
142 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
145 Send_Notification_WOCOVA(
153 Send_Notification_WOCOVA(
157 death_message.nent_msginfo,
164 float Obituary_WeaponDeath(
168 string s1, string s2, string s3,
171 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
172 if (death_weapon == WEP_Null)
175 w_deathtype = deathtype;
176 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
181 Send_Notification_WOCOVA(
189 // send the info part to everyone
190 Send_Notification_WOCOVA(
194 death_message.nent_msginfo,
202 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %d!\n",
211 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target)
213 if(deathtype == DEATH_FIRE.m_id)
215 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
216 Send_Notification(NOTIF_ONE, targ, MSG_CHOICE, CHOICE_FRAGGED_FIRE, attacker.netname, kill_count_to_target, 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 setorigin(this, this.owner.origin - '0 0 16');
472 this.nextthink = time;
475 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
477 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
480 if(STAT(FROZEN, targ))
483 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
485 STAT(FROZEN, targ) = frozen_type;
486 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
487 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
488 targ.revive_speed = revivespeed;
490 IL_REMOVE(g_bot_targets, targ);
491 targ.bot_attack = false;
492 targ.freeze_time = time;
494 entity ice = new(ice);
496 ice.scale = targ.scale;
497 setthink(ice, Ice_Think);
498 ice.nextthink = time;
499 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
500 setmodel(ice, MDL_ICE);
502 ice.colormod = Team_ColorRGB(targ.team);
503 ice.glowmod = ice.colormod;
505 targ.revival_time = 0;
509 RemoveGrapplingHooks(targ);
511 FOREACH_CLIENT(IS_PLAYER(it),
513 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
515 .entity weaponentity = weaponentities[slot];
516 if(it.(weaponentity).hook.aiment == targ)
517 RemoveHook(it.(weaponentity).hook);
522 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
523 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
526 void Unfreeze(entity targ, bool reset_health)
528 if(!STAT(FROZEN, targ))
531 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
532 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
534 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
536 STAT(FROZEN, targ) = 0;
537 STAT(REVIVE_PROGRESS, targ) = 0;
538 targ.revival_time = time;
540 IL_PUSH(g_bot_targets, targ);
541 targ.bot_attack = true;
543 WaypointSprite_Kill(targ.waypointsprite_attached);
545 FOREACH_CLIENT(IS_PLAYER(it),
547 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
549 .entity weaponentity = weaponentities[slot];
550 if(it.(weaponentity).hook.aiment == targ)
551 RemoveHook(it.(weaponentity).hook);
555 // remove the ice block
557 delete(targ.iceblock);
558 targ.iceblock = NULL;
560 MUTATOR_CALLHOOK(Unfreeze, targ);
563 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
565 float complainteamdamage = 0;
566 float mirrordamage = 0;
567 float mirrorforce = 0;
569 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
572 entity attacker_save = attacker;
574 // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
575 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
577 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
583 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
585 // exit the vehicle before killing (fixes a crash)
586 if(IS_PLAYER(targ) && targ.vehicle)
587 vehicles_exit(targ.vehicle, VHEF_RELEASE);
589 // These are ALWAYS lethal
590 // No damage modification here
591 // Instead, prepare the victim for his death...
592 SetResourceExplicit(targ, RES_ARMOR, 0);
593 targ.spawnshieldtime = 0;
594 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
595 targ.flags -= targ.flags & FL_GODMODE;
598 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
604 // nullify damage if teamplay is on
605 if(deathtype != DEATH_TELEFRAG.m_id)
606 if(IS_PLAYER(attacker))
608 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
613 else if(SAME_TEAM(attacker, targ))
615 if(autocvar_teamplay_mode == 1)
617 else if(attacker != targ)
619 if(autocvar_teamplay_mode == 3)
621 else if(autocvar_teamplay_mode == 4)
623 if(IS_PLAYER(targ) && !IS_DEAD(targ))
625 attacker.dmg_team = attacker.dmg_team + damage;
626 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
627 if(complainteamdamage > 0)
628 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
629 mirrorforce = autocvar_g_mirrordamage * vlen(force);
630 damage = autocvar_g_friendlyfire * damage;
631 // mirrordamage will be used LATER
633 if(autocvar_g_mirrordamage_virtual)
635 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
636 attacker.dmg_take += v.x;
637 attacker.dmg_save += v.y;
638 attacker.dmg_inflictor = inflictor;
643 if(autocvar_g_friendlyfire_virtual)
645 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
646 targ.dmg_take += v.x;
647 targ.dmg_save += v.y;
648 targ.dmg_inflictor = inflictor;
650 if(!autocvar_g_friendlyfire_virtual_force)
654 else if(!targ.canteamdamage)
661 if (!DEATH_ISSPECIAL(deathtype))
663 damage *= g_weapondamagefactor;
664 mirrordamage *= g_weapondamagefactor;
665 complainteamdamage *= g_weapondamagefactor;
666 force = force * g_weaponforcefactor;
667 mirrorforce *= g_weaponforcefactor;
670 // should this be changed at all? If so, in what way?
671 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
672 damage = M_ARGV(4, float);
673 mirrordamage = M_ARGV(5, float);
674 force = M_ARGV(6, vector);
676 if(IS_PLAYER(targ) && damage > 0 && attacker)
678 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
680 .entity went = weaponentities[slot];
681 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
682 RemoveHook(targ.(went).hook);
686 if(deathtype != DEATH_HURTTRIGGER.m_id && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id && STAT(FROZEN, targ))
688 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
690 Unfreeze(targ, false);
691 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
692 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
693 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
694 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
698 force *= autocvar_g_frozen_force;
701 if(IS_PLAYER(targ) && STAT(FROZEN, targ) && deathtype == DEATH_HURTTRIGGER.m_id && !autocvar_g_frozen_damage_trigger)
703 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
705 entity spot = SelectSpawnPoint (targ, false);
710 targ.deadflag = DEAD_NO;
712 targ.angles = spot.angles;
715 targ.effects |= EF_TELEPORT_BIT;
717 targ.angles_z = 0; // never spawn tilted even if the spot says to
718 targ.fixangle = true; // turn this way immediately
719 targ.velocity = '0 0 0';
720 targ.avelocity = '0 0 0';
721 targ.punchangle = '0 0 0';
722 targ.punchvector = '0 0 0';
723 targ.oldvelocity = targ.velocity;
725 targ.spawnorigin = spot.origin;
726 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
727 // don't reset back to last position, even if new position is stuck in solid
728 targ.oldorigin = targ.origin;
730 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
734 if(!MUTATOR_IS_ENABLED(mutator_instagib))
736 // apply strength multiplier
737 if (attacker.items & ITEM_Strength.m_itemid)
741 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
742 force = force * autocvar_g_balance_powerup_strength_selfforce;
746 damage = damage * autocvar_g_balance_powerup_strength_damage;
747 force = force * autocvar_g_balance_powerup_strength_force;
751 // apply invincibility multiplier
752 if (targ.items & ITEM_Shield.m_itemid)
754 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
755 if (targ != attacker)
757 force = force * autocvar_g_balance_powerup_invincible_takeforce;
762 if (targ == attacker)
763 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
768 if(deathtype != DEATH_BUFF.m_id)
769 if(targ.takedamage == DAMAGE_AIM)
773 if(IS_VEHICLE(targ) && targ.owner)
778 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
780 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
784 if(deathtype != DEATH_FIRE.m_id)
786 if(PHYS_INPUT_BUTTON_CHAT(victim))
787 attacker.typehitsound += 1;
789 attacker.damage_dealt += damage;
792 damage_goodhits += 1;
793 damage_gooddamage += damage;
795 if (!DEATH_ISSPECIAL(deathtype))
797 if(IS_PLAYER(targ)) // don't do this for vehicles
803 else if(IS_PLAYER(attacker))
805 // if enemy gets frozen in this frame and receives other damage don't
806 // play the typehitsound e.g. when hit by multiple bullets of the shotgun
807 if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
809 attacker.typehitsound += 1;
811 if(complainteamdamage > 0)
812 if(time > CS(attacker).teamkill_complain)
814 CS(attacker).teamkill_complain = time + 5;
815 CS(attacker).teamkill_soundtime = time + 0.4;
816 CS(attacker).teamkill_soundsource = targ;
824 if (targ.damageforcescale)
826 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
828 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
829 if(targ.move_movetype == MOVETYPE_PHYSICS)
831 entity farcent = new(farce);
832 farcent.enemy = targ;
833 farcent.movedir = farce * 10;
835 farcent.movedir = farcent.movedir * targ.mass;
836 farcent.origin = hitloc;
837 farcent.forcetype = FORCETYPE_FORCEATPOS;
838 farcent.nextthink = time + 0.1;
839 setthink(farcent, SUB_Remove);
843 targ.velocity = targ.velocity + farce;
845 UNSET_ONGROUND(targ);
846 UpdateCSQCProjectile(targ);
849 if (damage != 0 || (targ.damageforcescale && force))
850 if (targ.event_damage)
851 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
853 // apply mirror damage if any
854 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
855 if(mirrordamage > 0 || mirrorforce > 0)
857 attacker = attacker_save;
859 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
860 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
864 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
865 float inflictorselfdamage, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
866 // Returns total damage applies to creatures
870 float total_damage_to_creatures;
875 float stat_damagedone;
877 if(RadiusDamage_running)
879 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
883 RadiusDamage_running = 1;
885 tfloordmg = autocvar_g_throughfloor_damage;
886 tfloorforce = autocvar_g_throughfloor_force;
888 total_damage_to_creatures = 0;
890 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
891 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
893 force = inflictorvelocity;
897 force = normalize(force);
898 if(forceintensity >= 0)
899 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
901 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
906 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
910 if ((targ != inflictor) || inflictorselfdamage)
911 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
918 // LordHavoc: measure distance to nearest point on target (not origin)
919 // (this guarentees 100% damage on a touch impact)
920 nearest = targ.WarpZone_findradius_nearest;
921 diff = targ.WarpZone_findradius_dist;
922 // round up a little on the damage to ensure full damage on impacts
923 // and turn the distance into a fraction of the radius
924 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
926 //bprint(ftos(power));
927 //if (targ == attacker)
928 // print(ftos(power), "\n");
934 finaldmg = coredamage * power + edgedamage * (1 - power);
940 vector myblastorigin;
943 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
945 // if it's a player, use the view origin as reference
946 center = CENTER_OR_VIEWOFS(targ);
948 force = normalize(center - myblastorigin);
949 force = force * (finaldmg / coredamage) * forceintensity;
952 if(deathtype & WEP_BLASTER.m_id)
953 force.z *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
955 if(targ != directhitentity)
960 float mininv_f, mininv_d;
962 // test line of sight to multiple positions on box,
963 // and do damage if any of them hit
966 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
967 // so for a given max stddev:
968 // n = (1 / (2 * max stddev of hitratio))^2
970 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
971 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
973 if(autocvar_g_throughfloor_debug)
974 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
977 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
979 if(autocvar_g_throughfloor_debug)
980 LOG_INFOF(" steps=%f", total);
984 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
986 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
988 if(autocvar_g_throughfloor_debug)
989 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
991 for(c = 0; c < total; ++c)
993 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
994 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
995 if (trace_fraction == 1 || trace_ent == targ)
999 hitloc = hitloc + nearest;
1003 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1004 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1005 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1008 nearest = hitloc * (1 / max(1, hits));
1009 hitratio = (hits / total);
1010 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1011 finaldmg = finaldmg * a;
1012 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1015 if(autocvar_g_throughfloor_debug)
1016 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1019 //if (targ == attacker)
1021 // print("hits ", ftos(hits), " / ", ftos(total));
1022 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1023 // print(" (", ftos(a), ")\n");
1025 if(finaldmg || force)
1029 total_damage_to_creatures += finaldmg;
1031 if(accuracy_isgooddamage(attacker, targ))
1032 stat_damagedone += finaldmg;
1035 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1036 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1038 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1046 RadiusDamage_running = 0;
1048 if(!DEATH_ISSPECIAL(deathtype))
1049 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1051 return total_damage_to_creatures;
1054 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1056 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, weaponentity, directhitentity);
1059 bool Heal(entity targ, entity inflictor, float amount, float limit)
1061 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1064 bool healed = false;
1066 healed = targ.event_heal(targ, inflictor, amount, limit);
1067 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1068 // TODO: healing fx!
1069 // TODO: armor healing?
1073 float Fire_IsBurning(entity e)
1075 return (time < e.fire_endtime);
1078 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1081 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1092 // print("adding a fire burner to ", e.classname, "\n");
1093 e.fire_burner = new(fireburner);
1094 setthink(e.fire_burner, fireburner_think);
1095 e.fire_burner.nextthink = time;
1096 e.fire_burner.owner = e;
1102 if(Fire_IsBurning(e))
1104 mintime = e.fire_endtime - time;
1105 maxtime = max(mintime, t);
1107 mindps = e.fire_damagepersec;
1108 maxdps = max(mindps, dps);
1110 if(maxtime > mintime || maxdps > mindps)
1114 // damage we have right now
1115 mindamage = mindps * mintime;
1117 // damage we want to get
1118 maxdamage = mindamage + d;
1120 // but we can't exceed maxtime * maxdps!
1121 totaldamage = min(maxdamage, maxtime * maxdps);
1125 // totaldamage = min(mindamage + d, maxtime * maxdps)
1127 // totaldamage <= maxtime * maxdps
1128 // ==> totaldamage / maxdps <= maxtime.
1130 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1131 // >= min(mintime, maxtime)
1132 // ==> totaldamage / maxdps >= mintime.
1135 // how long do we damage then?
1136 // at least as long as before
1137 // but, never exceed maxdps
1138 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1142 // at most as long as maximum allowed
1143 // but, never below mindps
1144 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1146 // assuming t > mintime, dps > mindps:
1147 // we get d = t * dps = maxtime * maxdps
1148 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1149 // totaldamage / maxdps = maxtime
1150 // totaldamage / mindps > totaldamage / maxdps = maxtime
1152 // a) totaltime = max(mintime, maxtime) = maxtime
1153 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1155 // assuming t <= mintime:
1156 // we get maxtime = mintime
1157 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1158 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1160 // assuming dps <= mindps:
1161 // we get mindps = maxdps.
1162 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1163 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1164 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1166 e.fire_damagepersec = totaldamage / totaltime;
1167 e.fire_endtime = time + totaltime;
1168 if(totaldamage > 1.2 * mindamage)
1170 e.fire_deathtype = dt;
1171 if(e.fire_owner != o)
1174 e.fire_hitsound = false;
1177 if(accuracy_isgooddamage(o, e))
1178 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1179 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1186 e.fire_damagepersec = dps;
1187 e.fire_endtime = time + t;
1188 e.fire_deathtype = dt;
1190 e.fire_hitsound = false;
1191 if(accuracy_isgooddamage(o, e))
1192 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1197 void Fire_ApplyDamage(entity e)
1202 if (!Fire_IsBurning(e))
1205 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1206 if(IS_NOT_A_CLIENT(o))
1209 // water and slime stop fire
1211 if(e.watertype != CONTENT_LAVA)
1218 t = min(frametime, e.fire_endtime - time);
1219 d = e.fire_damagepersec * t;
1221 hi = e.fire_owner.damage_dealt;
1222 ty = e.fire_owner.typehitsound;
1223 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1224 if(e.fire_hitsound && e.fire_owner)
1226 e.fire_owner.damage_dealt = hi;
1227 e.fire_owner.typehitsound = ty;
1229 e.fire_hitsound = true;
1231 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1233 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1235 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1236 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1238 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1239 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1240 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1246 void Fire_ApplyEffect(entity e)
1248 if(Fire_IsBurning(e))
1249 e.effects |= EF_FLAME;
1251 e.effects &= ~EF_FLAME;
1254 void fireburner_think(entity this)
1256 // for players, this is done in the regular loop
1257 if(wasfreed(this.owner))
1262 Fire_ApplyEffect(this.owner);
1263 if(!Fire_IsBurning(this.owner))
1265 this.owner.fire_burner = NULL;
1269 Fire_ApplyDamage(this.owner);
1270 this.nextthink = time;