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, GetResource(attacker, RES_HEALTH), GetResource(attacker, RES_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) \
324 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
326 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
329 switch(CS(attacker).killcount)
336 if(!warmup_stage && !checkrules_firstblood)
338 checkrules_firstblood = true;
339 notif_firstblood = true; // modify the current messages so that they too show firstblood information
340 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
341 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
343 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
344 kill_count_to_attacker = -1;
345 kill_count_to_target = -2;
349 kill_count_to_attacker = CS(attacker).killcount;
350 kill_count_to_target = 0;
361 kill_count_to_attacker,
362 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
370 kill_count_to_target,
371 GetResource(attacker, RES_HEALTH),
372 GetResource(attacker, RES_ARMOR),
373 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
376 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target))
384 kill_count_to_attacker,
385 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
393 kill_count_to_target,
394 GetResource(attacker, RES_HEALTH),
395 GetResource(attacker, RES_ARMOR),
396 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
401 if(deathtype == DEATH_BUFF.m_id)
402 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
404 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker))
405 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
414 switch(DEATH_ENT(deathtype))
416 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
417 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
418 // and there will be a REAL DEATH_VOID implementation which mappers will use.
419 case DEATH_HURTTRIGGER:
421 Obituary_SpecialDeath(targ, false, deathtype,
433 Obituary_SpecialDeath(targ, false, deathtype,
435 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
445 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
450 LogDeath("accident", deathtype, targ, targ);
451 GiveFrags(targ, targ, -1, deathtype, weaponentity);
453 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
455 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
458 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
463 // reset target kill count
464 CS(targ).killcount = 0;
467 void Ice_Think(entity this)
469 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
474 setorigin(this, this.owner.origin - '0 0 16');
475 this.nextthink = time;
478 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
480 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
483 if(STAT(FROZEN, targ))
486 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
488 STAT(FROZEN, targ) = frozen_type;
489 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
490 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
491 targ.revive_speed = revivespeed;
493 IL_REMOVE(g_bot_targets, targ);
494 targ.bot_attack = false;
495 targ.freeze_time = time;
497 entity ice = new(ice);
499 ice.scale = targ.scale;
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 == 3)
624 else if(autocvar_teamplay_mode == 4)
626 if(IS_PLAYER(targ) && !IS_DEAD(targ))
628 attacker.dmg_team = attacker.dmg_team + damage;
629 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
630 if(complainteamdamage > 0)
631 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
632 mirrorforce = autocvar_g_mirrordamage * vlen(force);
633 damage = autocvar_g_friendlyfire * damage;
634 // mirrordamage will be used LATER
636 if(autocvar_g_mirrordamage_virtual)
638 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
639 attacker.dmg_take += v.x;
640 attacker.dmg_save += v.y;
641 attacker.dmg_inflictor = inflictor;
646 if(autocvar_g_friendlyfire_virtual)
648 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
649 targ.dmg_take += v.x;
650 targ.dmg_save += v.y;
651 targ.dmg_inflictor = inflictor;
653 if(!autocvar_g_friendlyfire_virtual_force)
657 else if(!targ.canteamdamage)
664 if (!DEATH_ISSPECIAL(deathtype))
666 damage *= g_weapondamagefactor;
667 mirrordamage *= g_weapondamagefactor;
668 complainteamdamage *= g_weapondamagefactor;
669 force = force * g_weaponforcefactor;
670 mirrorforce *= g_weaponforcefactor;
673 // should this be changed at all? If so, in what way?
674 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
675 damage = M_ARGV(4, float);
676 mirrordamage = M_ARGV(5, float);
677 force = M_ARGV(6, vector);
679 if(IS_PLAYER(targ) && damage > 0 && attacker)
681 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
683 .entity went = weaponentities[slot];
684 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
685 RemoveHook(targ.(went).hook);
689 if(deathtype != DEATH_HURTTRIGGER.m_id && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id && STAT(FROZEN, targ))
691 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
693 Unfreeze(targ, false);
694 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
695 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
696 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
697 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
701 force *= autocvar_g_frozen_force;
704 if(IS_PLAYER(targ) && STAT(FROZEN, targ) && deathtype == DEATH_HURTTRIGGER.m_id && !autocvar_g_frozen_damage_trigger)
706 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
708 entity spot = SelectSpawnPoint (targ, false);
713 targ.deadflag = DEAD_NO;
715 targ.angles = spot.angles;
718 targ.effects |= EF_TELEPORT_BIT;
720 targ.angles_z = 0; // never spawn tilted even if the spot says to
721 targ.fixangle = true; // turn this way immediately
722 targ.velocity = '0 0 0';
723 targ.avelocity = '0 0 0';
724 targ.punchangle = '0 0 0';
725 targ.punchvector = '0 0 0';
726 targ.oldvelocity = targ.velocity;
728 targ.spawnorigin = spot.origin;
729 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
730 // don't reset back to last position, even if new position is stuck in solid
731 targ.oldorigin = targ.origin;
733 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
737 if(!MUTATOR_IS_ENABLED(mutator_instagib))
739 // apply strength multiplier
740 if (attacker.items & ITEM_Strength.m_itemid)
744 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
745 force = force * autocvar_g_balance_powerup_strength_selfforce;
749 damage = damage * autocvar_g_balance_powerup_strength_damage;
750 force = force * autocvar_g_balance_powerup_strength_force;
754 // apply invincibility multiplier
755 if (targ.items & ITEM_Shield.m_itemid)
757 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
758 if (targ != attacker)
760 force = force * autocvar_g_balance_powerup_invincible_takeforce;
765 if (targ == attacker)
766 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
771 if(deathtype != DEATH_BUFF.m_id)
772 if(targ.takedamage == DAMAGE_AIM)
776 if(IS_VEHICLE(targ) && targ.owner)
781 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
783 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
787 if(deathtype != DEATH_FIRE.m_id)
789 if(PHYS_INPUT_BUTTON_CHAT(victim))
790 attacker.typehitsound += 1;
792 attacker.damage_dealt += damage;
795 damage_goodhits += 1;
796 damage_gooddamage += damage;
798 if (!DEATH_ISSPECIAL(deathtype))
800 if(IS_PLAYER(targ)) // don't do this for vehicles
806 else if(IS_PLAYER(attacker))
808 // if enemy gets frozen in this frame and receives other damage don't
809 // play the typehitsound e.g. when hit by multiple bullets of the shotgun
810 if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
812 attacker.typehitsound += 1;
814 if(complainteamdamage > 0)
815 if(time > CS(attacker).teamkill_complain)
817 CS(attacker).teamkill_complain = time + 5;
818 CS(attacker).teamkill_soundtime = time + 0.4;
819 CS(attacker).teamkill_soundsource = targ;
827 if (targ.damageforcescale)
829 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
831 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
832 if(targ.move_movetype == MOVETYPE_PHYSICS)
834 entity farcent = new(farce);
835 farcent.enemy = targ;
836 farcent.movedir = farce * 10;
838 farcent.movedir = farcent.movedir * targ.mass;
839 farcent.origin = hitloc;
840 farcent.forcetype = FORCETYPE_FORCEATPOS;
841 farcent.nextthink = time + 0.1;
842 setthink(farcent, SUB_Remove);
846 targ.velocity = targ.velocity + farce;
848 UNSET_ONGROUND(targ);
849 UpdateCSQCProjectile(targ);
852 if (damage != 0 || (targ.damageforcescale && force))
853 if (targ.event_damage)
854 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
856 // apply mirror damage if any
857 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
858 if(mirrordamage > 0 || mirrorforce > 0)
860 attacker = attacker_save;
862 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
863 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
867 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
868 float inflictorselfdamage, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
869 // Returns total damage applies to creatures
873 float total_damage_to_creatures;
878 float stat_damagedone;
880 if(RadiusDamage_running)
882 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
886 RadiusDamage_running = 1;
888 tfloordmg = autocvar_g_throughfloor_damage;
889 tfloorforce = autocvar_g_throughfloor_force;
891 total_damage_to_creatures = 0;
893 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
894 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
896 force = inflictorvelocity;
900 force = normalize(force);
901 if(forceintensity >= 0)
902 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
904 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
909 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
913 if ((targ != inflictor) || inflictorselfdamage)
914 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
921 // LordHavoc: measure distance to nearest point on target (not origin)
922 // (this guarentees 100% damage on a touch impact)
923 nearest = targ.WarpZone_findradius_nearest;
924 diff = targ.WarpZone_findradius_dist;
925 // round up a little on the damage to ensure full damage on impacts
926 // and turn the distance into a fraction of the radius
927 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
929 //bprint(ftos(power));
930 //if (targ == attacker)
931 // print(ftos(power), "\n");
937 finaldmg = coredamage * power + edgedamage * (1 - power);
943 vector myblastorigin;
946 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
948 // if it's a player, use the view origin as reference
949 center = CENTER_OR_VIEWOFS(targ);
951 force = normalize(center - myblastorigin);
952 force = force * (finaldmg / coredamage) * forceintensity;
955 if(deathtype & WEP_BLASTER.m_id)
956 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
958 if(targ != directhitentity)
963 float mininv_f, mininv_d;
965 // test line of sight to multiple positions on box,
966 // and do damage if any of them hit
969 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
970 // so for a given max stddev:
971 // n = (1 / (2 * max stddev of hitratio))^2
973 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
974 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
976 if(autocvar_g_throughfloor_debug)
977 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
980 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
982 if(autocvar_g_throughfloor_debug)
983 LOG_INFOF(" steps=%f", total);
987 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
989 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
991 if(autocvar_g_throughfloor_debug)
992 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
994 for(c = 0; c < total; ++c)
996 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
997 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
998 if (trace_fraction == 1 || trace_ent == targ)
1002 hitloc = hitloc + nearest;
1006 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1007 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1008 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1011 nearest = hitloc * (1 / max(1, hits));
1012 hitratio = (hits / total);
1013 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1014 finaldmg = finaldmg * a;
1015 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1018 if(autocvar_g_throughfloor_debug)
1019 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1022 //if (targ == attacker)
1024 // print("hits ", ftos(hits), " / ", ftos(total));
1025 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1026 // print(" (", ftos(a), ")\n");
1028 if(finaldmg || force)
1032 total_damage_to_creatures += finaldmg;
1034 if(accuracy_isgooddamage(attacker, targ))
1035 stat_damagedone += finaldmg;
1038 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1039 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1041 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1049 RadiusDamage_running = 0;
1051 if(!DEATH_ISSPECIAL(deathtype))
1052 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1054 return total_damage_to_creatures;
1057 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1059 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, weaponentity, directhitentity);
1062 bool Heal(entity targ, entity inflictor, float amount, float limit)
1064 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1067 bool healed = false;
1069 healed = targ.event_heal(targ, inflictor, amount, limit);
1070 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1071 // TODO: healing fx!
1072 // TODO: armor healing?
1076 float Fire_IsBurning(entity e)
1078 return (time < e.fire_endtime);
1081 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1084 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1095 // print("adding a fire burner to ", e.classname, "\n");
1096 e.fire_burner = new(fireburner);
1097 setthink(e.fire_burner, fireburner_think);
1098 e.fire_burner.nextthink = time;
1099 e.fire_burner.owner = e;
1105 if(Fire_IsBurning(e))
1107 mintime = e.fire_endtime - time;
1108 maxtime = max(mintime, t);
1110 mindps = e.fire_damagepersec;
1111 maxdps = max(mindps, dps);
1113 if(maxtime > mintime || maxdps > mindps)
1117 // damage we have right now
1118 mindamage = mindps * mintime;
1120 // damage we want to get
1121 maxdamage = mindamage + d;
1123 // but we can't exceed maxtime * maxdps!
1124 totaldamage = min(maxdamage, maxtime * maxdps);
1128 // totaldamage = min(mindamage + d, maxtime * maxdps)
1130 // totaldamage <= maxtime * maxdps
1131 // ==> totaldamage / maxdps <= maxtime.
1133 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1134 // >= min(mintime, maxtime)
1135 // ==> totaldamage / maxdps >= mintime.
1138 // how long do we damage then?
1139 // at least as long as before
1140 // but, never exceed maxdps
1141 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1145 // at most as long as maximum allowed
1146 // but, never below mindps
1147 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1149 // assuming t > mintime, dps > mindps:
1150 // we get d = t * dps = maxtime * maxdps
1151 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1152 // totaldamage / maxdps = maxtime
1153 // totaldamage / mindps > totaldamage / maxdps = maxtime
1155 // a) totaltime = max(mintime, maxtime) = maxtime
1156 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1158 // assuming t <= mintime:
1159 // we get maxtime = mintime
1160 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1161 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1163 // assuming dps <= mindps:
1164 // we get mindps = maxdps.
1165 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1166 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1167 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1169 e.fire_damagepersec = totaldamage / totaltime;
1170 e.fire_endtime = time + totaltime;
1171 if(totaldamage > 1.2 * mindamage)
1173 e.fire_deathtype = dt;
1174 if(e.fire_owner != o)
1177 e.fire_hitsound = false;
1180 if(accuracy_isgooddamage(o, e))
1181 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1182 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1189 e.fire_damagepersec = dps;
1190 e.fire_endtime = time + t;
1191 e.fire_deathtype = dt;
1193 e.fire_hitsound = false;
1194 if(accuracy_isgooddamage(o, e))
1195 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1200 void Fire_ApplyDamage(entity e)
1205 if (!Fire_IsBurning(e))
1208 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1209 if(IS_NOT_A_CLIENT(o))
1212 // water and slime stop fire
1214 if(e.watertype != CONTENT_LAVA)
1221 t = min(frametime, e.fire_endtime - time);
1222 d = e.fire_damagepersec * t;
1224 hi = e.fire_owner.damage_dealt;
1225 ty = e.fire_owner.typehitsound;
1226 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1227 if(e.fire_hitsound && e.fire_owner)
1229 e.fire_owner.damage_dealt = hi;
1230 e.fire_owner.typehitsound = ty;
1232 e.fire_hitsound = true;
1234 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1236 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1238 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1239 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1241 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1242 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1243 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1249 void Fire_ApplyEffect(entity e)
1251 if(Fire_IsBurning(e))
1252 e.effects |= EF_FLAME;
1254 e.effects &= ~EF_FLAME;
1257 void fireburner_think(entity this)
1259 // for players, this is done in the regular loop
1260 if(wasfreed(this.owner))
1265 Fire_ApplyEffect(this.owner);
1266 if(!Fire_IsBurning(this.owner))
1268 this.owner.fire_burner = NULL;
1272 Fire_ApplyDamage(this.owner);
1273 this.nextthink = time;