3 #include <common/constants.qh>
4 #include <common/deathtypes/all.qh>
5 #include <common/effects/all.qh>
6 #include <common/gamemodes/_mod.qh>
7 #include <common/gamemodes/rules.qh>
8 #include <common/items/_mod.qh>
9 #include <common/mapobjects/defs.qh>
10 #include <common/mapobjects/triggers.qh>
11 #include <common/mutators/mutator/buffs/buffs.qh>
12 #include <common/mutators/mutator/buffs/sv_buffs.qh>
13 #include <common/mutators/mutator/instagib/sv_instagib.qh>
14 #include <common/mutators/mutator/status_effects/_mod.qh>
15 #include <common/mutators/mutator/waypoints/waypointsprites.qh>
16 #include <common/notifications/all.qh>
17 #include <common/physics/movetypes/movetypes.qh>
18 #include <common/physics/player.qh>
19 #include <common/playerstats.qh>
20 #include <common/resources/sv_resources.qh>
21 #include <common/state.qh>
22 #include <common/teams.qh>
23 #include <common/util.qh>
24 #include <common/vehicles/all.qh>
25 #include <common/weapons/_all.qh>
26 #include <lib/csqcmodel/sv_model.qh>
27 #include <lib/warpzone/common.qh>
28 #include <server/bot/api.qh>
29 #include <server/client.qh>
30 #include <server/gamelog.qh>
31 #include <server/hook.qh>
32 #include <server/items/items.qh>
33 #include <server/main.qh>
34 #include <server/mutators/_mod.qh>
35 #include <server/scores.qh>
36 #include <server/spawnpoints.qh>
37 #include <server/teamplay.qh>
38 #include <server/weapons/accuracy.qh>
39 #include <server/weapons/csqcprojectile.qh>
40 #include <server/weapons/selection.qh>
41 #include <server/weapons/weaponsystem.qh>
42 #include <server/world.qh>
44 void UpdateFrags(entity player, int f)
46 GameRules_scoring_add_team(player, SCORE, f);
49 void GiveFrags(entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
51 // TODO route through PlayerScores instead
52 if(game_stopped) return;
59 GameRules_scoring_add(attacker, SUICIDES, 1);
64 GameRules_scoring_add(attacker, TEAMKILLS, 1);
70 GameRules_scoring_add(attacker, KILLS, 1);
71 if(!warmup_stage && targ.playerid)
72 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
75 GameRules_scoring_add(targ, DEATHS, 1);
77 // FIXME fix the mess this is (we have REAL points now!)
78 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
81 attacker.totalfrags += f;
84 UpdateFrags(attacker, f);
87 string AppendItemcodes(string s, entity player)
89 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
91 .entity weaponentity = weaponentities[slot];
92 int w = player.(weaponentity).m_weapon.m_id;
94 w = player.(weaponentity).cnt; // previous weapon
95 if(w != 0 || slot == 0)
96 s = strcat(s, ftos(w));
98 if(PHYS_INPUT_BUTTON_CHAT(player))
100 // TODO: include these codes as a flag on the item itself
101 MUTATOR_CALLHOOK(LogDeath_AppendItemCodes, player, s);
102 s = M_ARGV(1, string);
106 void LogDeath(string mode, int deathtype, entity killer, entity killed)
109 if(!autocvar_sv_eventlog)
111 s = strcat(":kill:", mode);
112 s = strcat(s, ":", ftos(killer.playerid));
113 s = strcat(s, ":", ftos(killed.playerid));
114 s = strcat(s, ":type=", Deathtype_Name(deathtype));
115 s = strcat(s, ":items=");
116 s = AppendItemcodes(s, killer);
119 s = strcat(s, ":victimitems=");
120 s = AppendItemcodes(s, killed);
125 void Obituary_SpecialDeath(
130 string s1, string s2, string s3,
131 float f1, float f2, float f3)
133 if(!DEATH_ISSPECIAL(deathtype))
135 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
139 entity deathent = REGISTRY_GET(Deathtypes, deathtype - DT_FIRST);
142 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
146 if(g_cts && deathtype == DEATH_KILL.m_id)
147 return; // TODO: somehow put this in CTS gamemode file!
149 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
152 Send_Notification_WOCOVA(
160 Send_Notification_WOCOVA(
164 death_message.nent_msginfo,
170 if(deathtype == DEATH_TELEFRAG.m_id) {
171 Give_Medal(attacker, TELEFRAG);
175 float Obituary_WeaponDeath(
180 string s1, string s2, string s3,
183 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
184 if (death_weapon == WEP_Null)
187 w_deathtype = deathtype;
188 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
193 Send_Notification_WOCOVA(
201 // send the info part to everyone
202 Send_Notification_WOCOVA(
206 death_message.nent_msginfo,
211 // z411 special medals
213 switch(death_message) {
214 case WEAPON_SHOTGUN_MURDER_SLAP:
215 if(!cvar("g_melee_only")) { // don't spam humiliation if we're in melee_only mode
216 Give_Medal(attacker, HUMILIATION);
219 case WEAPON_ELECTRO_MURDER_COMBO:
220 Give_Medal(attacker, ELECTROBITCH);
228 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %s!\n",
237 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target, string attacker_name)
239 if(deathtype == DEATH_FIRE.m_id)
241 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
242 Send_Notification(NOTIF_ONE, targ, MSG_CHOICE, CHOICE_FRAGGED_FIRE, attacker_name, kill_count_to_target, GetResource(attacker, RES_HEALTH), GetResource(attacker, RES_ARMOR), (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping));
246 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
249 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
252 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
255 float notif_firstblood = false;
256 float kill_count_to_attacker, kill_count_to_target;
257 bool notif_anonymous = false;
258 string attacker_name = attacker.netname;
260 // Set final information for the death
261 targ.death_origin = targ.origin;
262 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
264 // Abort now if a mutator requests it
265 if (MUTATOR_CALLHOOK(ClientObituary, inflictor, attacker, targ, deathtype, attacker.(weaponentity))) { CS(targ).killcount = 0; return; }
266 notif_anonymous = M_ARGV(5, bool);
268 // TODO: Replace "???" with a translatable "Anonymous player" string
269 // https://gitlab.com/xonotic/xonotic-data.pk3dir/-/issues/2839
271 attacker_name = "???";
273 #ifdef NOTIFICATIONS_DEBUG
276 "Obituary(%s, %s, %s, %s = %d);\n",
280 Deathtype_Name(deathtype),
291 if(DEATH_ISSPECIAL(deathtype))
293 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
295 Obituary_SpecialDeath(targ, NULL, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
299 switch(DEATH_ENT(deathtype))
301 case DEATH_MIRRORDAMAGE:
303 Obituary_SpecialDeath(targ, NULL, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
306 case DEATH_HURTTRIGGER:
307 Obituary_SpecialDeath(targ, NULL, false, deathtype, targ.netname, inflictor.message, deathlocation, CS(targ).killcount, 0, 0);
311 Obituary_SpecialDeath(targ, NULL, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
317 else if (!Obituary_WeaponDeath(targ, NULL, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
319 backtrace("SUICIDE: what the hell happened here?\n");
322 LogDeath("suicide", deathtype, targ, targ);
323 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_SUICIDE);
324 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
325 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
331 else if(IS_PLAYER(attacker))
333 if(SAME_TEAM(attacker, targ))
335 LogDeath("tk", deathtype, attacker, targ);
336 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
338 CS(attacker).killcount = 0;
340 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
341 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker_name);
342 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL),
343 playername(targ.netname, targ.team, true), playername(attacker_name, attacker.team, true),
344 deathlocation, CS(targ).killcount);
346 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
347 // No need for specific death/weapon messages...
351 LogDeath("frag", deathtype, attacker, targ);
352 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
354 CS(attacker).taunt_soundtime = time + 1;
355 CS(attacker).killcount = CS(attacker).killcount + 1;
357 attacker.killsound += 1;
359 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
360 // these 2 macros are spread over multiple files
361 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
363 Give_Medal(attacker, KILLSTREAK_##countb); \
365 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
368 switch(CS(attacker).killcount)
375 if(!warmup_stage && !checkrules_firstblood)
377 checkrules_firstblood = true;
378 notif_firstblood = true; // modify the current messages so that they too show firstblood information
379 Give_Medal(attacker, FIRSTBLOOD);
380 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
381 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
383 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
384 kill_count_to_attacker = -1;
385 kill_count_to_target = -2;
389 kill_count_to_attacker = CS(attacker).killcount;
390 kill_count_to_target = 0;
394 if(attacker.lastkill && attacker.lastkill > time - autocvar_g_medals_excellent_time) {
395 Give_Medal(attacker, EXCELLENT);
397 attacker.lastkill = time;
407 kill_count_to_attacker,
408 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
416 kill_count_to_target,
417 GetResource(attacker, RES_HEALTH),
418 GetResource(attacker, RES_ARMOR),
419 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
422 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target, attacker_name))
430 kill_count_to_attacker,
431 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
439 kill_count_to_target,
440 GetResource(attacker, RES_HEALTH),
441 GetResource(attacker, RES_ARMOR),
442 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
447 if(deathtype == DEATH_BUFF.m_id)
448 f3 = buff_FirstFromFlags(attacker).m_id;
450 if (!Obituary_WeaponDeath(targ, attacker, true, deathtype, playername(targ.netname, targ.team, true), playername(attacker_name, attacker.team, true), deathlocation, CS(targ).killcount, kill_count_to_attacker))
451 Obituary_SpecialDeath(targ, attacker, true, deathtype, playername(targ.netname, targ.team, true), playername(attacker_name, attacker.team, true), deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
460 switch(DEATH_ENT(deathtype))
462 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
463 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
464 // and there will be a REAL DEATH_VOID implementation which mappers will use.
465 case DEATH_HURTTRIGGER:
467 Obituary_SpecialDeath(targ, NULL, false, deathtype,
468 playername(targ.netname, targ.team, true),
479 Obituary_SpecialDeath(targ, NULL, false, deathtype,
480 playername(targ.netname, targ.team, true),
481 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
491 Obituary_SpecialDeath(targ, NULL, false, deathtype, playername(targ.netname, targ.team, true), deathlocation, "", CS(targ).killcount, 0, 0);
496 LogDeath("accident", deathtype, targ, targ);
497 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACCIDENT);
498 GiveFrags(targ, targ, -1, deathtype, weaponentity);
500 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
502 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
505 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
510 // reset target kill count
511 CS(targ).killcount = 0;
514 void Ice_Think(entity this)
516 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
521 vector ice_org = this.owner.origin - '0 0 16';
522 if (this.origin != ice_org)
523 setorigin(this, ice_org);
524 this.nextthink = time;
527 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
529 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
532 if(STAT(FROZEN, targ))
535 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
537 STAT(FROZEN, targ) = frozen_type;
538 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
539 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
540 targ.revive_speed = revivespeed;
542 IL_REMOVE(g_bot_targets, targ);
543 targ.bot_attack = false;
544 targ.freeze_time = time;
546 entity ice = new(ice);
548 ice.scale = targ.scale;
549 // set_movetype(ice, MOVETYPE_FOLLOW) would rotate the ice model with the player
550 setthink(ice, Ice_Think);
551 ice.nextthink = time;
552 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
553 setmodel(ice, MDL_ICE);
555 ice.colormod = Team_ColorRGB(targ.team);
556 ice.glowmod = ice.colormod;
558 targ.revival_time = 0;
562 RemoveGrapplingHooks(targ);
564 FOREACH_CLIENT(IS_PLAYER(it),
566 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
568 .entity weaponentity = weaponentities[slot];
569 if(it.(weaponentity).hook.aiment == targ)
570 RemoveHook(it.(weaponentity).hook);
575 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
576 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
579 void Unfreeze(entity targ, bool reset_health)
581 if(!STAT(FROZEN, targ))
584 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
585 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
587 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
589 STAT(FROZEN, targ) = 0;
590 STAT(REVIVE_PROGRESS, targ) = 0;
591 targ.revival_time = time;
593 IL_PUSH(g_bot_targets, targ);
594 targ.bot_attack = true;
596 WaypointSprite_Kill(targ.waypointsprite_attached);
598 FOREACH_CLIENT(IS_PLAYER(it),
600 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
602 .entity weaponentity = weaponentities[slot];
603 if(it.(weaponentity).hook.aiment == targ)
604 RemoveHook(it.(weaponentity).hook);
608 // remove the ice block
610 delete(targ.iceblock);
611 targ.iceblock = NULL;
613 MUTATOR_CALLHOOK(Unfreeze, targ);
616 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
618 float complainteamdamage = 0;
619 float mirrordamage = 0;
620 float mirrorforce = 0;
622 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
625 entity attacker_save = attacker;
627 // special rule: gravity bombs and sound-based attacks do not affect team mates (other than for disconnecting the hook)
628 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || (deathtype & HITTYPE_SOUND))
630 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
636 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
638 // exit the vehicle before killing (fixes a crash)
639 if(IS_PLAYER(targ) && targ.vehicle)
640 vehicles_exit(targ.vehicle, VHEF_RELEASE);
642 // These are ALWAYS lethal
643 // No damage modification here
644 // Instead, prepare the victim for their death...
645 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
647 SetResourceExplicit(targ, RES_ARMOR, 0);
648 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
650 StatusEffects_remove(STATUSEFFECT_SpawnShield, targ, STATUSEFFECT_REMOVE_CLEAR);
651 targ.flags -= targ.flags & FL_GODMODE;
654 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
660 // nullify damage if teamplay is on
661 if(deathtype != DEATH_TELEFRAG.m_id)
662 if(IS_PLAYER(attacker))
664 // avoid dealing damage or force to other independent players
665 // and avoid dealing damage or force to things owned by other independent players
666 if((IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ))) ||
667 (targ.realowner && IS_INDEPENDENT_PLAYER(targ.realowner) && attacker != targ.realowner))
672 else if(!STAT(FROZEN, targ) && SAME_TEAM(attacker, targ))
674 if(autocvar_teamplay_mode == 1)
676 else if(attacker != targ)
678 if(autocvar_teamplay_mode == 2)
680 if(IS_PLAYER(targ) && !IS_DEAD(targ))
682 attacker.dmg_team = attacker.dmg_team + damage;
683 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
686 else if(autocvar_teamplay_mode == 3)
688 else if(autocvar_teamplay_mode == 4)
690 if(IS_PLAYER(targ) && !IS_DEAD(targ))
692 attacker.dmg_team = attacker.dmg_team + damage;
693 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
694 if(complainteamdamage > 0)
695 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
696 mirrorforce = autocvar_g_mirrordamage * vlen(force);
697 damage = autocvar_g_friendlyfire * damage;
698 // mirrordamage will be used LATER
700 if(autocvar_g_mirrordamage_virtual)
702 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
703 attacker.dmg_take += v.x;
704 attacker.dmg_save += v.y;
705 attacker.dmg_inflictor = inflictor;
710 if(autocvar_g_friendlyfire_virtual)
712 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
713 targ.dmg_take += v.x;
714 targ.dmg_save += v.y;
715 targ.dmg_inflictor = inflictor;
717 if(!autocvar_g_friendlyfire_virtual_force)
721 else if(!targ.canteamdamage)
728 if (!DEATH_ISSPECIAL(deathtype))
730 damage *= autocvar_g_weapondamagefactor;
731 mirrordamage *= autocvar_g_weapondamagefactor;
732 complainteamdamage *= autocvar_g_weapondamagefactor;
733 force = force * autocvar_g_weaponforcefactor;
734 mirrorforce *= autocvar_g_weaponforcefactor;
737 // should this be changed at all? If so, in what way?
738 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
739 damage = M_ARGV(4, float);
740 mirrordamage = M_ARGV(5, float);
741 force = M_ARGV(6, vector);
743 if(IS_PLAYER(targ) && damage > 0 && attacker)
745 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
747 .entity went = weaponentities[slot];
748 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
749 RemoveHook(targ.(went).hook);
753 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
754 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
756 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
758 Unfreeze(targ, false);
759 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
760 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
761 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
762 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
766 force *= autocvar_g_frozen_force;
769 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
770 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
772 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
774 entity spot = SelectSpawnPoint(targ, false);
778 targ.deadflag = DEAD_NO;
780 targ.angles = spot.angles;
783 targ.effects |= EF_TELEPORT_BIT;
785 targ.angles_z = 0; // never spawn tilted even if the spot says to
786 targ.fixangle = true; // turn this way immediately
787 targ.velocity = '0 0 0';
788 targ.avelocity = '0 0 0';
789 targ.punchangle = '0 0 0';
790 targ.punchvector = '0 0 0';
791 targ.oldvelocity = targ.velocity;
793 targ.spawnorigin = spot.origin;
794 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
795 // don't reset back to last position, even if new position is stuck in solid
796 targ.oldorigin = targ.origin;
798 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
802 if (targ == attacker)
803 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
808 if(deathtype != DEATH_BUFF.m_id)
809 if(targ.takedamage == DAMAGE_AIM)
813 if(IS_VEHICLE(targ) && targ.owner)
818 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
820 if (DIFF_TEAM(victim, attacker))
824 if(deathtype != DEATH_FIRE.m_id)
826 if(PHYS_INPUT_BUTTON_CHAT(victim))
827 attacker.typehitsound += 1;
829 attacker.hitsound_damage_dealt += damage;
832 impressive_hits += 1;
834 if (!DEATH_ISSPECIAL(deathtype))
836 if(IS_PLAYER(targ)) // don't do this for vehicles
842 else if (IS_PLAYER(attacker) && !STAT(FROZEN, victim)) // same team
844 if (deathtype != DEATH_FIRE.m_id)
846 attacker.typehitsound += 1;
848 if(complainteamdamage > 0)
849 if(time > CS(attacker).teamkill_complain)
851 CS(attacker).teamkill_complain = time + 5;
852 CS(attacker).teamkill_soundtime = time + 0.4;
853 CS(attacker).teamkill_soundsource = targ;
861 if (targ.damageforcescale)
863 if (!IS_PLAYER(targ) || !StatusEffects_active(STATUSEFFECT_SpawnShield, targ) || targ == attacker)
865 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
866 if(targ.move_movetype == MOVETYPE_PHYSICS)
868 entity farcent = new(farce);
869 farcent.enemy = targ;
870 farcent.movedir = farce * 10;
872 farcent.movedir = farcent.movedir * targ.mass;
873 farcent.origin = hitloc;
874 farcent.forcetype = FORCETYPE_FORCEATPOS;
875 farcent.nextthink = time + 0.1;
876 setthink(farcent, SUB_Remove);
878 else if(targ.move_movetype != MOVETYPE_NOCLIP)
880 targ.velocity = targ.velocity + farce;
882 UNSET_ONGROUND(targ);
883 UpdateCSQCProjectile(targ);
886 if (damage != 0 || (targ.damageforcescale && force))
887 if (targ.event_damage)
888 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
890 // apply mirror damage if any
891 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
892 if(mirrordamage > 0 || mirrorforce > 0)
894 attacker = attacker_save;
896 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
897 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
901 // Returns total damage applies to creatures
902 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
903 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
907 float total_damage_to_creatures;
912 float stat_damagedone;
914 if(RadiusDamage_running)
916 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
920 if (rad < 0) rad = 0;
922 RadiusDamage_running = 1;
924 tfloordmg = autocvar_g_throughfloor_damage;
925 tfloorforce = autocvar_g_throughfloor_force;
927 total_damage_to_creatures = 0;
929 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
930 if(!(deathtype & HITTYPE_SOUND)) // do not send radial sound damage (bandwidth hog)
932 force = inflictorvelocity;
936 force = normalize(force);
937 if(forceintensity >= 0)
938 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
940 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
945 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
949 if ((targ != inflictor) || inflictorselfdamage)
950 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
953 // measure distance from nearest point on target (not origin)
954 // to nearest point on inflictor (not origin)
955 vector nearest = targ.WarpZone_findradius_nearest;
956 vector inflictornearest = NearestPointOnBoundingBox(
957 inflictororigin - (inflictor.maxs - inflictor.mins) * 0.5,
958 inflictororigin + (inflictor.maxs - inflictor.mins) * 0.5,
960 vector diff = inflictornearest - nearest;
962 // round up a little on the damage to ensure full damage on impacts
963 // and turn the distance into a fraction of the radius
964 float dist = max(0, vlen(diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS));
967 float f = (rad > 0) ? 1 - (dist / rad) : 1;
968 // at this point f can't be < 0 or > 1
969 float finaldmg = coredamage * f + edgedamage * (1 - f);
975 vector myblastorigin;
978 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
980 // if it's a player, use the view origin as reference
981 center = CENTER_OR_VIEWOFS(targ);
983 force = normalize(center - myblastorigin);
984 force = force * (finaldmg / max(coredamage, edgedamage)) * forceintensity;
987 // apply special scaling along the z axis if set
988 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
990 force.z *= forcezscale;
992 if(targ != directhitentity)
997 float mininv_f, mininv_d;
999 // test line of sight to multiple positions on box,
1000 // and do damage if any of them hit
1003 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
1004 // so for a given max stddev:
1005 // n = (1 / (2 * max stddev of hitratio))^2
1007 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
1008 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
1010 if(autocvar_g_throughfloor_debug)
1011 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1014 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1016 if(autocvar_g_throughfloor_debug)
1017 LOG_INFOF(" steps=%f", total);
1020 if (IS_PLAYER(targ))
1021 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1023 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1025 if(autocvar_g_throughfloor_debug)
1026 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1028 for(c = 0; c < total; ++c)
1030 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1031 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1032 if (trace_fraction == 1 || trace_ent == targ)
1036 hitloc = hitloc + nearest;
1040 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1041 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1042 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1045 nearest = hitloc * (1 / max(1, hits));
1046 hitratio = (hits / total);
1047 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1048 finaldmg = finaldmg * a;
1049 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1052 if(autocvar_g_throughfloor_debug)
1053 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1055 /*if (targ == attacker)
1057 print("hits ", ftos(hits), " / ", ftos(total));
1058 print(" finaldmg ", ftos(finaldmg), " force ", ftos(vlen(force)));
1059 print(" (", vtos(force), ") (", ftos(a), ")\n");
1063 if(finaldmg || force)
1067 total_damage_to_creatures += finaldmg;
1069 if(accuracy_isgooddamage(attacker, targ))
1070 stat_damagedone += finaldmg;
1073 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1074 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1076 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1084 RadiusDamage_running = 0;
1086 if(!DEATH_ISSPECIAL(deathtype))
1087 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(max(coredamage, edgedamage), stat_damagedone));
1089 return total_damage_to_creatures;
1092 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1094 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1095 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1098 bool Heal(entity targ, entity inflictor, float amount, float limit)
1100 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1103 bool healed = false;
1105 healed = targ.event_heal(targ, inflictor, amount, limit);
1106 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1107 // TODO: healing fx!
1108 // TODO: armor healing?
1112 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1115 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1125 if(StatusEffects_active(STATUSEFFECT_Burning, e))
1127 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1129 mintime = fireendtime - time;
1130 maxtime = max(mintime, t);
1132 mindps = e.fire_damagepersec;
1133 maxdps = max(mindps, dps);
1135 if(maxtime > mintime || maxdps > mindps)
1139 // damage we have right now
1140 mindamage = mindps * mintime;
1142 // damage we want to get
1143 maxdamage = mindamage + d;
1145 // but we can't exceed maxtime * maxdps!
1146 totaldamage = min(maxdamage, maxtime * maxdps);
1150 // totaldamage = min(mindamage + d, maxtime * maxdps)
1152 // totaldamage <= maxtime * maxdps
1153 // ==> totaldamage / maxdps <= maxtime.
1155 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1156 // >= min(mintime, maxtime)
1157 // ==> totaldamage / maxdps >= mintime.
1160 // how long do we damage then?
1161 // at least as long as before
1162 // but, never exceed maxdps
1163 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1167 // at most as long as maximum allowed
1168 // but, never below mindps
1169 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1171 // assuming t > mintime, dps > mindps:
1172 // we get d = t * dps = maxtime * maxdps
1173 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1174 // totaldamage / maxdps = maxtime
1175 // totaldamage / mindps > totaldamage / maxdps = maxtime
1177 // a) totaltime = max(mintime, maxtime) = maxtime
1178 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1180 // assuming t <= mintime:
1181 // we get maxtime = mintime
1182 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1183 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1185 // assuming dps <= mindps:
1186 // we get mindps = maxdps.
1187 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1188 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1189 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1191 e.fire_damagepersec = totaldamage / totaltime;
1192 StatusEffects_apply(STATUSEFFECT_Burning, e, time + totaltime, 0);
1193 if(totaldamage > 1.2 * mindamage)
1195 e.fire_deathtype = dt;
1196 if(e.fire_owner != o)
1199 e.fire_hitsound = false;
1202 if(accuracy_isgooddamage(o, e))
1203 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1204 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1211 e.fire_damagepersec = dps;
1212 StatusEffects_apply(STATUSEFFECT_Burning, e, time + t, 0);
1213 e.fire_deathtype = dt;
1215 e.fire_hitsound = false;
1216 if(accuracy_isgooddamage(o, e))
1217 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1222 void Fire_ApplyDamage(entity e)
1227 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1228 if(IS_NOT_A_CLIENT(o))
1231 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1232 t = min(frametime, fireendtime - time);
1233 d = e.fire_damagepersec * t;
1235 hi = e.fire_owner.hitsound_damage_dealt;
1236 ty = e.fire_owner.typehitsound;
1237 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1238 if(e.fire_hitsound && e.fire_owner)
1240 e.fire_owner.hitsound_damage_dealt = hi;
1241 e.fire_owner.typehitsound = ty;
1243 e.fire_hitsound = true;
1245 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1247 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1249 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1250 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1252 t = autocvar_g_balance_firetransfer_time * (fireendtime - time);
1253 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1254 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);