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);
79 string AppendItemcodes(string s, entity player)
81 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
83 .entity weaponentity = weaponentities[slot];
84 int w = player.(weaponentity).m_weapon.m_id;
86 w = player.(weaponentity).cnt; // previous weapon
87 if(w != 0 || slot == 0)
88 s = strcat(s, ftos(w));
90 if(time < player.strength_finished)
92 if(time < player.invincible_finished)
94 if(player.flagcarried != NULL)
96 if(PHYS_INPUT_BUTTON_CHAT(player))
103 void LogDeath(string mode, int deathtype, entity killer, entity killed)
106 if(!autocvar_sv_eventlog)
108 s = strcat(":kill:", mode);
109 s = strcat(s, ":", ftos(killer.playerid));
110 s = strcat(s, ":", ftos(killed.playerid));
111 s = strcat(s, ":type=", Deathtype_Name(deathtype));
112 s = strcat(s, ":items=");
113 s = AppendItemcodes(s, killer);
116 s = strcat(s, ":victimitems=");
117 s = AppendItemcodes(s, killed);
122 void Obituary_SpecialDeath(
126 string s1, string s2, string s3,
127 float f1, float f2, float f3)
129 if(!DEATH_ISSPECIAL(deathtype))
131 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
135 entity deathent = Deathtypes_from(deathtype - DT_FIRST);
138 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
142 if(g_cts && deathtype == DEATH_KILL.m_id)
143 return; // TODO: somehow put this in CTS gamemode file!
145 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
148 Send_Notification_WOCOVA(
156 Send_Notification_WOCOVA(
160 death_message.nent_msginfo,
167 float Obituary_WeaponDeath(
171 string s1, string s2, string s3,
174 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
175 if (death_weapon == WEP_Null)
178 w_deathtype = deathtype;
179 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
184 Send_Notification_WOCOVA(
192 // send the info part to everyone
193 Send_Notification_WOCOVA(
197 death_message.nent_msginfo,
205 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %d!\n",
214 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target)
216 if(deathtype == DEATH_FIRE.m_id)
218 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
219 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));
223 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
226 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
229 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
232 float notif_firstblood = false;
233 float kill_count_to_attacker, kill_count_to_target;
235 // Set final information for the death
236 targ.death_origin = targ.origin;
237 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
239 #ifdef NOTIFICATIONS_DEBUG
242 "Obituary(%s, %s, %s, %s = %d);\n",
246 Deathtype_Name(deathtype),
257 if(DEATH_ISSPECIAL(deathtype))
259 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
261 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
265 switch(DEATH_ENT(deathtype))
267 case DEATH_MIRRORDAMAGE:
269 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
275 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
281 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
283 backtrace("SUICIDE: what the hell happened here?\n");
286 LogDeath("suicide", deathtype, targ, targ);
287 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
288 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
294 else if(IS_PLAYER(attacker))
296 if(SAME_TEAM(attacker, targ))
298 LogDeath("tk", deathtype, attacker, targ);
299 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
301 CS(attacker).killcount = 0;
303 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
304 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker.netname);
305 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker.netname, deathlocation, CS(targ).killcount);
307 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
308 // No need for specific death/weapon messages...
312 LogDeath("frag", deathtype, attacker, targ);
313 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
315 CS(attacker).taunt_soundtime = time + 1;
316 CS(attacker).killcount = CS(attacker).killcount + 1;
318 attacker.killsound += 1;
320 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
321 // these 2 macros are spread over multiple files
322 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
325 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
328 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
332 switch(CS(attacker).killcount)
339 if(!warmup_stage && !checkrules_firstblood)
341 checkrules_firstblood = true;
342 notif_firstblood = true; // modify the current messages so that they too show firstblood information
343 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
344 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
346 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
347 kill_count_to_attacker = -1;
348 kill_count_to_target = -2;
352 kill_count_to_attacker = CS(attacker).killcount;
353 kill_count_to_target = 0;
364 kill_count_to_attacker,
365 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
373 kill_count_to_target,
374 GetResourceAmount(attacker, RESOURCE_HEALTH),
375 GetResourceAmount(attacker, RESOURCE_ARMOR),
376 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
379 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target))
387 kill_count_to_attacker,
388 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
396 kill_count_to_target,
397 GetResourceAmount(attacker, RESOURCE_HEALTH),
398 GetResourceAmount(attacker, RESOURCE_ARMOR),
399 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
404 if(deathtype == DEATH_BUFF.m_id)
405 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
407 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker))
408 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
417 switch(DEATH_ENT(deathtype))
419 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
420 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
421 // and there will be a REAL DEATH_VOID implementation which mappers will use.
422 case DEATH_HURTTRIGGER:
424 Obituary_SpecialDeath(targ, false, deathtype,
436 Obituary_SpecialDeath(targ, false, deathtype,
438 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
448 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
453 LogDeath("accident", deathtype, targ, targ);
454 GiveFrags(targ, targ, -1, deathtype, weaponentity);
456 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
458 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
461 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
466 // reset target kill count
467 CS(targ).killcount = 0;
470 void Ice_Think(entity this)
472 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
477 setorigin(this, this.owner.origin - '0 0 16');
478 this.nextthink = time;
481 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
483 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
486 if(STAT(FROZEN, targ))
489 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
491 STAT(FROZEN, targ) = frozen_type;
492 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == 3) ? 1 : 0);
493 SetResourceAmount(targ, RESOURCE_HEALTH, ((frozen_type == 3) ? targ_maxhealth : 1));
494 targ.revive_speed = revivespeed;
496 IL_REMOVE(g_bot_targets, targ);
497 targ.bot_attack = false;
498 targ.freeze_time = time;
500 entity ice = new(ice);
502 ice.scale = targ.scale;
503 setthink(ice, Ice_Think);
504 ice.nextthink = time;
505 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
506 setmodel(ice, MDL_ICE);
508 ice.colormod = Team_ColorRGB(targ.team);
509 ice.glowmod = ice.colormod;
511 targ.revival_time = 0;
515 RemoveGrapplingHooks(targ);
517 FOREACH_CLIENT(IS_PLAYER(it),
519 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
521 .entity weaponentity = weaponentities[slot];
522 if(it.(weaponentity).hook.aiment == targ)
523 RemoveHook(it.(weaponentity).hook);
528 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
529 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
532 void Unfreeze(entity targ)
534 if(!STAT(FROZEN, targ))
537 if(STAT(FROZEN, targ) && STAT(FROZEN, targ) != 3) // only reset health if target was frozen
539 SetResourceAmount(targ, RESOURCE_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
540 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
543 STAT(FROZEN, targ) = 0;
544 STAT(REVIVE_PROGRESS, targ) = 0;
545 targ.revival_time = time;
547 IL_PUSH(g_bot_targets, targ);
548 targ.bot_attack = true;
550 WaypointSprite_Kill(targ.waypointsprite_attached);
552 FOREACH_CLIENT(IS_PLAYER(it),
554 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
556 .entity weaponentity = weaponentities[slot];
557 if(it.(weaponentity).hook.aiment == targ)
558 RemoveHook(it.(weaponentity).hook);
562 // remove the ice block
564 delete(targ.iceblock);
565 targ.iceblock = NULL;
567 MUTATOR_CALLHOOK(Unfreeze, targ);
570 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
572 float complainteamdamage = 0;
573 float mirrordamage = 0;
574 float mirrorforce = 0;
576 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
579 entity attacker_save = attacker;
581 // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
582 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
584 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
590 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
592 // exit the vehicle before killing (fixes a crash)
593 if(IS_PLAYER(targ) && targ.vehicle)
594 vehicles_exit(targ.vehicle, VHEF_RELEASE);
596 // These are ALWAYS lethal
597 // No damage modification here
598 // Instead, prepare the victim for his death...
599 SetResourceAmountExplicit(targ, RESOURCE_ARMOR, 0);
600 targ.spawnshieldtime = 0;
601 SetResourceAmountExplicit(targ, RESOURCE_HEALTH, 0.9); // this is < 1
602 targ.flags -= targ.flags & FL_GODMODE;
605 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
611 // nullify damage if teamplay is on
612 if(deathtype != DEATH_TELEFRAG.m_id)
613 if(IS_PLAYER(attacker))
615 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
620 else if(SAME_TEAM(attacker, targ))
622 if(autocvar_teamplay_mode == 1)
624 else if(attacker != targ)
626 if(autocvar_teamplay_mode == 3)
628 else if(autocvar_teamplay_mode == 4)
630 if(IS_PLAYER(targ) && !IS_DEAD(targ))
632 attacker.dmg_team = attacker.dmg_team + damage;
633 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
634 if(complainteamdamage > 0)
635 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
636 mirrorforce = autocvar_g_mirrordamage * vlen(force);
637 damage = autocvar_g_friendlyfire * damage;
638 // mirrordamage will be used LATER
640 if(autocvar_g_mirrordamage_virtual)
642 vector v = healtharmor_applydamage(GetResourceAmount(attacker, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
643 attacker.dmg_take += v.x;
644 attacker.dmg_save += v.y;
645 attacker.dmg_inflictor = inflictor;
650 if(autocvar_g_friendlyfire_virtual)
652 vector v = healtharmor_applydamage(GetResourceAmount(targ, RESOURCE_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
653 targ.dmg_take += v.x;
654 targ.dmg_save += v.y;
655 targ.dmg_inflictor = inflictor;
657 if(!autocvar_g_friendlyfire_virtual_force)
661 else if(!targ.canteamdamage)
668 if (!DEATH_ISSPECIAL(deathtype))
670 damage *= g_weapondamagefactor;
671 mirrordamage *= g_weapondamagefactor;
672 complainteamdamage *= g_weapondamagefactor;
673 force = force * g_weaponforcefactor;
674 mirrorforce *= g_weaponforcefactor;
677 // should this be changed at all? If so, in what way?
678 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
679 damage = M_ARGV(4, float);
680 mirrordamage = M_ARGV(5, float);
681 force = M_ARGV(6, vector);
683 if(IS_PLAYER(targ) && damage > 0 && attacker)
685 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
687 .entity went = weaponentities[slot];
688 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
689 RemoveHook(targ.(went).hook);
693 if(STAT(FROZEN, targ))
694 if(deathtype != DEATH_HURTTRIGGER.m_id && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
696 if(autocvar_g_frozen_revive_falldamage > 0)
697 if(deathtype == DEATH_FALL.m_id)
698 if(damage >= autocvar_g_frozen_revive_falldamage)
701 SetResourceAmount(targ, RESOURCE_HEALTH, autocvar_g_frozen_revive_falldamage_health);
702 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
703 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
704 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
708 force *= autocvar_g_frozen_force;
711 if(STAT(FROZEN, targ) && deathtype == DEATH_HURTTRIGGER.m_id && !autocvar_g_frozen_damage_trigger)
713 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
715 entity spot = SelectSpawnPoint (targ, false);
720 targ.deadflag = DEAD_NO;
722 targ.angles = spot.angles;
725 targ.effects |= EF_TELEPORT_BIT;
727 targ.angles_z = 0; // never spawn tilted even if the spot says to
728 targ.fixangle = true; // turn this way immediately
729 targ.velocity = '0 0 0';
730 targ.avelocity = '0 0 0';
731 targ.punchangle = '0 0 0';
732 targ.punchvector = '0 0 0';
733 targ.oldvelocity = targ.velocity;
735 targ.spawnorigin = spot.origin;
736 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
737 // don't reset back to last position, even if new position is stuck in solid
738 targ.oldorigin = targ.origin;
740 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
744 if(!MUTATOR_IS_ENABLED(mutator_instagib))
746 // apply strength multiplier
747 if (attacker.items & ITEM_Strength.m_itemid)
751 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
752 force = force * autocvar_g_balance_powerup_strength_selfforce;
756 damage = damage * autocvar_g_balance_powerup_strength_damage;
757 force = force * autocvar_g_balance_powerup_strength_force;
761 // apply invincibility multiplier
762 if (targ.items & ITEM_Shield.m_itemid)
764 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
765 if (targ != attacker)
767 force = force * autocvar_g_balance_powerup_invincible_takeforce;
772 if (targ == attacker)
773 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
778 if(deathtype != DEATH_BUFF.m_id)
779 if(targ.takedamage == DAMAGE_AIM)
783 if(IS_VEHICLE(targ) && targ.owner)
788 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
790 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
794 if(deathtype != DEATH_FIRE.m_id)
796 if(PHYS_INPUT_BUTTON_CHAT(victim))
797 attacker.typehitsound += 1;
799 attacker.damage_dealt += damage;
802 damage_goodhits += 1;
803 damage_gooddamage += damage;
805 if (!DEATH_ISSPECIAL(deathtype))
807 if(IS_PLAYER(targ)) // don't do this for vehicles
813 else if(IS_PLAYER(attacker))
815 if(deathtype != DEATH_FIRE.m_id)
817 attacker.typehitsound += 1;
819 if(complainteamdamage > 0)
820 if(time > CS(attacker).teamkill_complain)
822 CS(attacker).teamkill_complain = time + 5;
823 CS(attacker).teamkill_soundtime = time + 0.4;
824 CS(attacker).teamkill_soundsource = targ;
832 if (targ.damageforcescale)
834 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
836 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
837 if(targ.move_movetype == MOVETYPE_PHYSICS)
839 entity farcent = new(farce);
840 farcent.enemy = targ;
841 farcent.movedir = farce * 10;
843 farcent.movedir = farcent.movedir * targ.mass;
844 farcent.origin = hitloc;
845 farcent.forcetype = FORCETYPE_FORCEATPOS;
846 farcent.nextthink = time + 0.1;
847 setthink(farcent, SUB_Remove);
851 targ.velocity = targ.velocity + farce;
853 UNSET_ONGROUND(targ);
854 UpdateCSQCProjectile(targ);
857 if (damage != 0 || (targ.damageforcescale && force))
858 if (targ.event_damage)
859 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
861 // apply mirror damage if any
862 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
863 if(mirrordamage > 0 || mirrorforce > 0)
865 attacker = attacker_save;
867 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
868 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
872 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
873 float inflictorselfdamage, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
874 // Returns total damage applies to creatures
878 float total_damage_to_creatures;
883 float stat_damagedone;
885 if(RadiusDamage_running)
887 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
891 RadiusDamage_running = 1;
893 tfloordmg = autocvar_g_throughfloor_damage;
894 tfloorforce = autocvar_g_throughfloor_force;
896 total_damage_to_creatures = 0;
898 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
899 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
901 force = inflictorvelocity;
905 force = normalize(force);
906 if(forceintensity >= 0)
907 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
909 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
914 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
918 if ((targ != inflictor) || inflictorselfdamage)
919 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
926 // LordHavoc: measure distance to nearest point on target (not origin)
927 // (this guarentees 100% damage on a touch impact)
928 nearest = targ.WarpZone_findradius_nearest;
929 diff = targ.WarpZone_findradius_dist;
930 // round up a little on the damage to ensure full damage on impacts
931 // and turn the distance into a fraction of the radius
932 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
934 //bprint(ftos(power));
935 //if (targ == attacker)
936 // print(ftos(power), "\n");
942 finaldmg = coredamage * power + edgedamage * (1 - power);
948 vector myblastorigin;
951 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
953 // if it's a player, use the view origin as reference
954 center = CENTER_OR_VIEWOFS(targ);
956 force = normalize(center - myblastorigin);
957 force = force * (finaldmg / coredamage) * forceintensity;
960 if(deathtype & WEP_BLASTER.m_id)
961 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
963 if(targ != directhitentity)
968 float mininv_f, mininv_d;
970 // test line of sight to multiple positions on box,
971 // and do damage if any of them hit
974 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
975 // so for a given max stddev:
976 // n = (1 / (2 * max stddev of hitratio))^2
978 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
979 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
981 if(autocvar_g_throughfloor_debug)
982 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
985 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
987 if(autocvar_g_throughfloor_debug)
988 LOG_INFOF(" steps=%f", total);
992 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
994 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
996 if(autocvar_g_throughfloor_debug)
997 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
999 for(c = 0; c < total; ++c)
1001 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1002 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1003 if (trace_fraction == 1 || trace_ent == targ)
1007 hitloc = hitloc + nearest;
1011 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1012 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1013 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1016 nearest = hitloc * (1 / max(1, hits));
1017 hitratio = (hits / total);
1018 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1019 finaldmg = finaldmg * a;
1020 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1023 if(autocvar_g_throughfloor_debug)
1024 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1027 //if (targ == attacker)
1029 // print("hits ", ftos(hits), " / ", ftos(total));
1030 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1031 // print(" (", ftos(a), ")\n");
1033 if(finaldmg || force)
1037 total_damage_to_creatures += finaldmg;
1039 if(accuracy_isgooddamage(attacker, targ))
1040 stat_damagedone += finaldmg;
1043 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1044 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1046 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1054 RadiusDamage_running = 0;
1056 if(!DEATH_ISSPECIAL(deathtype))
1057 accuracy_add(attacker, DEATH_WEAPONOF(deathtype).m_id, 0, min(coredamage, stat_damagedone));
1059 return total_damage_to_creatures;
1062 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1064 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, weaponentity, directhitentity);
1067 bool Heal(entity targ, entity inflictor, float amount, float limit)
1069 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1072 bool healed = false;
1074 healed = targ.event_heal(targ, inflictor, amount, limit);
1075 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1076 // TODO: healing fx!
1077 // TODO: armor healing?
1081 float Fire_IsBurning(entity e)
1083 return (time < e.fire_endtime);
1086 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1089 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1100 // print("adding a fire burner to ", e.classname, "\n");
1101 e.fire_burner = new(fireburner);
1102 setthink(e.fire_burner, fireburner_think);
1103 e.fire_burner.nextthink = time;
1104 e.fire_burner.owner = e;
1110 if(Fire_IsBurning(e))
1112 mintime = e.fire_endtime - time;
1113 maxtime = max(mintime, t);
1115 mindps = e.fire_damagepersec;
1116 maxdps = max(mindps, dps);
1118 if(maxtime > mintime || maxdps > mindps)
1122 // damage we have right now
1123 mindamage = mindps * mintime;
1125 // damage we want to get
1126 maxdamage = mindamage + d;
1128 // but we can't exceed maxtime * maxdps!
1129 totaldamage = min(maxdamage, maxtime * maxdps);
1133 // totaldamage = min(mindamage + d, maxtime * maxdps)
1135 // totaldamage <= maxtime * maxdps
1136 // ==> totaldamage / maxdps <= maxtime.
1138 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1139 // >= min(mintime, maxtime)
1140 // ==> totaldamage / maxdps >= mintime.
1143 // how long do we damage then?
1144 // at least as long as before
1145 // but, never exceed maxdps
1146 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1150 // at most as long as maximum allowed
1151 // but, never below mindps
1152 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1154 // assuming t > mintime, dps > mindps:
1155 // we get d = t * dps = maxtime * maxdps
1156 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1157 // totaldamage / maxdps = maxtime
1158 // totaldamage / mindps > totaldamage / maxdps = maxtime
1160 // a) totaltime = max(mintime, maxtime) = maxtime
1161 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1163 // assuming t <= mintime:
1164 // we get maxtime = mintime
1165 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1166 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1168 // assuming dps <= mindps:
1169 // we get mindps = maxdps.
1170 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1171 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1172 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1174 e.fire_damagepersec = totaldamage / totaltime;
1175 e.fire_endtime = time + totaltime;
1176 if(totaldamage > 1.2 * mindamage)
1178 e.fire_deathtype = dt;
1179 if(e.fire_owner != o)
1182 e.fire_hitsound = false;
1185 if(accuracy_isgooddamage(o, e))
1186 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, max(0, totaldamage - mindamage));
1187 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1194 e.fire_damagepersec = dps;
1195 e.fire_endtime = time + t;
1196 e.fire_deathtype = dt;
1198 e.fire_hitsound = false;
1199 if(accuracy_isgooddamage(o, e))
1200 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, d);
1205 void Fire_ApplyDamage(entity e)
1210 if (!Fire_IsBurning(e))
1213 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1214 if(IS_NOT_A_CLIENT(o))
1217 // water and slime stop fire
1219 if(e.watertype != CONTENT_LAVA)
1226 t = min(frametime, e.fire_endtime - time);
1227 d = e.fire_damagepersec * t;
1229 hi = e.fire_owner.damage_dealt;
1230 ty = e.fire_owner.typehitsound;
1231 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1232 if(e.fire_hitsound && e.fire_owner)
1234 e.fire_owner.damage_dealt = hi;
1235 e.fire_owner.typehitsound = ty;
1237 e.fire_hitsound = true;
1239 if(!IS_INDEPENDENT_PLAYER(e))
1240 if(!STAT(FROZEN, e))
1241 FOREACH_CLIENT(IS_PLAYER(it) && it != e, {
1243 if(!IS_INDEPENDENT_PLAYER(it))
1244 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1246 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1247 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1248 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1253 void Fire_ApplyEffect(entity e)
1255 if(Fire_IsBurning(e))
1256 e.effects |= EF_FLAME;
1258 e.effects &= ~EF_FLAME;
1261 void fireburner_think(entity this)
1263 // for players, this is done in the regular loop
1264 if(wasfreed(this.owner))
1269 Fire_ApplyEffect(this.owner);
1270 if(!Fire_IsBurning(this.owner))
1272 this.owner.fire_burner = NULL;
1276 Fire_ApplyDamage(this.owner);
1277 this.nextthink = time;