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(!STAT(FROZEN, targ) && 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))
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) && !STAT(FROZEN, victim)) // same team
808 if (deathtype != DEATH_FIRE.m_id)
810 attacker.typehitsound += 1;
812 if(complainteamdamage > 0)
813 if(time > CS(attacker).teamkill_complain)
815 CS(attacker).teamkill_complain = time + 5;
816 CS(attacker).teamkill_soundtime = time + 0.4;
817 CS(attacker).teamkill_soundsource = targ;
825 if (targ.damageforcescale)
827 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
829 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
830 if(targ.move_movetype == MOVETYPE_PHYSICS)
832 entity farcent = new(farce);
833 farcent.enemy = targ;
834 farcent.movedir = farce * 10;
836 farcent.movedir = farcent.movedir * targ.mass;
837 farcent.origin = hitloc;
838 farcent.forcetype = FORCETYPE_FORCEATPOS;
839 farcent.nextthink = time + 0.1;
840 setthink(farcent, SUB_Remove);
844 targ.velocity = targ.velocity + farce;
846 UNSET_ONGROUND(targ);
847 UpdateCSQCProjectile(targ);
850 if (damage != 0 || (targ.damageforcescale && force))
851 if (targ.event_damage)
852 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
854 // apply mirror damage if any
855 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
856 if(mirrordamage > 0 || mirrorforce > 0)
858 attacker = attacker_save;
860 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
861 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
865 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
866 float inflictorselfdamage, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
867 // Returns total damage applies to creatures
871 float total_damage_to_creatures;
876 float stat_damagedone;
878 if(RadiusDamage_running)
880 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
884 RadiusDamage_running = 1;
886 tfloordmg = autocvar_g_throughfloor_damage;
887 tfloorforce = autocvar_g_throughfloor_force;
889 total_damage_to_creatures = 0;
891 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
892 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
894 force = inflictorvelocity;
898 force = normalize(force);
899 if(forceintensity >= 0)
900 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
902 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
907 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
911 if ((targ != inflictor) || inflictorselfdamage)
912 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
919 // LordHavoc: measure distance to nearest point on target (not origin)
920 // (this guarentees 100% damage on a touch impact)
921 nearest = targ.WarpZone_findradius_nearest;
922 diff = targ.WarpZone_findradius_dist;
923 // round up a little on the damage to ensure full damage on impacts
924 // and turn the distance into a fraction of the radius
925 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
927 //bprint(ftos(power));
928 //if (targ == attacker)
929 // print(ftos(power), "\n");
935 finaldmg = coredamage * power + edgedamage * (1 - power);
941 vector myblastorigin;
944 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
946 // if it's a player, use the view origin as reference
947 center = CENTER_OR_VIEWOFS(targ);
949 force = normalize(center - myblastorigin);
950 force = force * (finaldmg / coredamage) * forceintensity;
953 if(deathtype & WEP_BLASTER.m_id)
954 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
956 if(targ != directhitentity)
961 float mininv_f, mininv_d;
963 // test line of sight to multiple positions on box,
964 // and do damage if any of them hit
967 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
968 // so for a given max stddev:
969 // n = (1 / (2 * max stddev of hitratio))^2
971 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
972 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
974 if(autocvar_g_throughfloor_debug)
975 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
978 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
980 if(autocvar_g_throughfloor_debug)
981 LOG_INFOF(" steps=%f", total);
985 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
987 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
989 if(autocvar_g_throughfloor_debug)
990 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
992 for(c = 0; c < total; ++c)
994 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
995 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
996 if (trace_fraction == 1 || trace_ent == targ)
1000 hitloc = hitloc + nearest;
1004 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1005 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1006 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1009 nearest = hitloc * (1 / max(1, hits));
1010 hitratio = (hits / total);
1011 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1012 finaldmg = finaldmg * a;
1013 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1016 if(autocvar_g_throughfloor_debug)
1017 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1020 //if (targ == attacker)
1022 // print("hits ", ftos(hits), " / ", ftos(total));
1023 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1024 // print(" (", ftos(a), ")\n");
1026 if(finaldmg || force)
1030 total_damage_to_creatures += finaldmg;
1032 if(accuracy_isgooddamage(attacker, targ))
1033 stat_damagedone += finaldmg;
1036 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1037 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1039 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1047 RadiusDamage_running = 0;
1049 if(!DEATH_ISSPECIAL(deathtype))
1050 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1052 return total_damage_to_creatures;
1055 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1057 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, weaponentity, directhitentity);
1060 bool Heal(entity targ, entity inflictor, float amount, float limit)
1062 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1065 bool healed = false;
1067 healed = targ.event_heal(targ, inflictor, amount, limit);
1068 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1069 // TODO: healing fx!
1070 // TODO: armor healing?
1074 float Fire_IsBurning(entity e)
1076 return (time < e.fire_endtime);
1079 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1082 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1093 // print("adding a fire burner to ", e.classname, "\n");
1094 e.fire_burner = new(fireburner);
1095 setthink(e.fire_burner, fireburner_think);
1096 e.fire_burner.nextthink = time;
1097 e.fire_burner.owner = e;
1103 if(Fire_IsBurning(e))
1105 mintime = e.fire_endtime - time;
1106 maxtime = max(mintime, t);
1108 mindps = e.fire_damagepersec;
1109 maxdps = max(mindps, dps);
1111 if(maxtime > mintime || maxdps > mindps)
1115 // damage we have right now
1116 mindamage = mindps * mintime;
1118 // damage we want to get
1119 maxdamage = mindamage + d;
1121 // but we can't exceed maxtime * maxdps!
1122 totaldamage = min(maxdamage, maxtime * maxdps);
1126 // totaldamage = min(mindamage + d, maxtime * maxdps)
1128 // totaldamage <= maxtime * maxdps
1129 // ==> totaldamage / maxdps <= maxtime.
1131 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1132 // >= min(mintime, maxtime)
1133 // ==> totaldamage / maxdps >= mintime.
1136 // how long do we damage then?
1137 // at least as long as before
1138 // but, never exceed maxdps
1139 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1143 // at most as long as maximum allowed
1144 // but, never below mindps
1145 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1147 // assuming t > mintime, dps > mindps:
1148 // we get d = t * dps = maxtime * maxdps
1149 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1150 // totaldamage / maxdps = maxtime
1151 // totaldamage / mindps > totaldamage / maxdps = maxtime
1153 // a) totaltime = max(mintime, maxtime) = maxtime
1154 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1156 // assuming t <= mintime:
1157 // we get maxtime = mintime
1158 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1159 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1161 // assuming dps <= mindps:
1162 // we get mindps = maxdps.
1163 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1164 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1165 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1167 e.fire_damagepersec = totaldamage / totaltime;
1168 e.fire_endtime = time + totaltime;
1169 if(totaldamage > 1.2 * mindamage)
1171 e.fire_deathtype = dt;
1172 if(e.fire_owner != o)
1175 e.fire_hitsound = false;
1178 if(accuracy_isgooddamage(o, e))
1179 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1180 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1187 e.fire_damagepersec = dps;
1188 e.fire_endtime = time + t;
1189 e.fire_deathtype = dt;
1191 e.fire_hitsound = false;
1192 if(accuracy_isgooddamage(o, e))
1193 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1198 void Fire_ApplyDamage(entity e)
1203 if (!Fire_IsBurning(e))
1206 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1207 if(IS_NOT_A_CLIENT(o))
1210 // water and slime stop fire
1212 if(e.watertype != CONTENT_LAVA)
1219 t = min(frametime, e.fire_endtime - time);
1220 d = e.fire_damagepersec * t;
1222 hi = e.fire_owner.damage_dealt;
1223 ty = e.fire_owner.typehitsound;
1224 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1225 if(e.fire_hitsound && e.fire_owner)
1227 e.fire_owner.damage_dealt = hi;
1228 e.fire_owner.typehitsound = ty;
1230 e.fire_hitsound = true;
1232 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1234 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1236 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1237 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1239 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1240 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1241 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1247 void Fire_ApplyEffect(entity e)
1249 if(Fire_IsBurning(e))
1250 e.effects |= EF_FLAME;
1252 e.effects &= ~EF_FLAME;
1255 void fireburner_think(entity this)
1257 // for players, this is done in the regular loop
1258 if(wasfreed(this.owner))
1263 Fire_ApplyEffect(this.owner);
1264 if(!Fire_IsBurning(this.owner))
1266 this.owner.fire_burner = NULL;
1270 Fire_ApplyDamage(this.owner);
1271 this.nextthink = time;