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(
129 string s1, string s2, string s3,
130 float f1, float f2, float f3)
132 if(!DEATH_ISSPECIAL(deathtype))
134 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
138 entity deathent = REGISTRY_GET(Deathtypes, deathtype - DT_FIRST);
141 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
145 if(g_cts && deathtype == DEATH_KILL.m_id)
146 return; // TODO: somehow put this in CTS gamemode file!
148 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
151 Send_Notification_WOCOVA(
159 Send_Notification_WOCOVA(
163 death_message.nent_msginfo,
170 float Obituary_WeaponDeath(
174 string s1, string s2, string s3,
177 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
178 if (death_weapon == WEP_Null)
181 w_deathtype = deathtype;
182 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
187 Send_Notification_WOCOVA(
195 // send the info part to everyone
196 Send_Notification_WOCOVA(
200 death_message.nent_msginfo,
208 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %s!\n",
217 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target, string attacker_name)
219 if(deathtype == DEATH_FIRE.m_id)
221 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
222 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));
226 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
229 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
232 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
235 float notif_firstblood = false;
236 float kill_count_to_attacker, kill_count_to_target;
237 bool notif_anonymous = false;
238 string attacker_name = attacker.netname;
240 // Set final information for the death
241 targ.death_origin = targ.origin;
242 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
244 // Abort now if a mutator requests it
245 if (MUTATOR_CALLHOOK(ClientObituary, inflictor, attacker, targ, deathtype, attacker.(weaponentity))) { CS(targ).killcount = 0; return; }
246 notif_anonymous = M_ARGV(5, bool);
248 // TODO: Replace "???" with a translatable "Anonymous player" string
249 // https://gitlab.com/xonotic/xonotic-data.pk3dir/-/issues/2839
251 attacker_name = "???";
253 #ifdef NOTIFICATIONS_DEBUG
256 "Obituary(%s, %s, %s, %s = %d);\n",
260 Deathtype_Name(deathtype),
271 if(DEATH_ISSPECIAL(deathtype))
273 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
275 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
279 switch(DEATH_ENT(deathtype))
281 case DEATH_MIRRORDAMAGE:
283 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
286 case DEATH_HURTTRIGGER:
287 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, inflictor.message, deathlocation, CS(targ).killcount, 0, 0);
291 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
297 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
299 backtrace("SUICIDE: what the hell happened here?\n");
302 LogDeath("suicide", deathtype, targ, targ);
303 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
304 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
310 else if(IS_PLAYER(attacker))
312 if(SAME_TEAM(attacker, targ))
314 LogDeath("tk", deathtype, attacker, targ);
315 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
317 CS(attacker).killcount = 0;
319 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
320 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker_name);
321 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker_name, deathlocation, CS(targ).killcount);
323 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
324 // No need for specific death/weapon messages...
328 LogDeath("frag", deathtype, attacker, targ);
329 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
331 CS(attacker).taunt_soundtime = time + 1;
332 CS(attacker).killcount = CS(attacker).killcount + 1;
334 attacker.killsound += 1;
336 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
337 // these 2 macros are spread over multiple files
338 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
340 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
342 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
345 switch(CS(attacker).killcount)
352 if(!warmup_stage && !checkrules_firstblood)
354 checkrules_firstblood = true;
355 notif_firstblood = true; // modify the current messages so that they too show firstblood information
356 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
357 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
359 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
360 kill_count_to_attacker = -1;
361 kill_count_to_target = -2;
365 kill_count_to_attacker = CS(attacker).killcount;
366 kill_count_to_target = 0;
377 kill_count_to_attacker,
378 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
386 kill_count_to_target,
387 GetResource(attacker, RES_HEALTH),
388 GetResource(attacker, RES_ARMOR),
389 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
392 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target, attacker_name))
400 kill_count_to_attacker,
401 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
409 kill_count_to_target,
410 GetResource(attacker, RES_HEALTH),
411 GetResource(attacker, RES_ARMOR),
412 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
417 if(deathtype == DEATH_BUFF.m_id)
418 f3 = buff_FirstFromFlags(attacker).m_id;
420 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker))
421 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
430 switch(DEATH_ENT(deathtype))
432 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
433 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
434 // and there will be a REAL DEATH_VOID implementation which mappers will use.
435 case DEATH_HURTTRIGGER:
437 Obituary_SpecialDeath(targ, false, deathtype,
449 Obituary_SpecialDeath(targ, false, deathtype,
451 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
461 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
466 LogDeath("accident", deathtype, targ, targ);
467 GiveFrags(targ, targ, -1, deathtype, weaponentity);
469 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
471 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
474 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
479 // reset target kill count
480 CS(targ).killcount = 0;
483 void Ice_Think(entity this)
485 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
490 vector ice_org = this.owner.origin - '0 0 16';
491 if (this.origin != ice_org)
492 setorigin(this, ice_org);
493 this.nextthink = time;
496 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
498 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
501 if(STAT(FROZEN, targ))
504 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
506 STAT(FROZEN, targ) = frozen_type;
507 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
508 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
509 targ.revive_speed = revivespeed;
511 IL_REMOVE(g_bot_targets, targ);
512 targ.bot_attack = false;
513 targ.freeze_time = time;
515 entity ice = new(ice);
517 ice.scale = targ.scale;
518 // set_movetype(ice, MOVETYPE_FOLLOW) would rotate the ice model with the player
519 setthink(ice, Ice_Think);
520 ice.nextthink = time;
521 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
522 setmodel(ice, MDL_ICE);
524 ice.colormod = Team_ColorRGB(targ.team);
525 ice.glowmod = ice.colormod;
527 targ.revival_time = 0;
531 RemoveGrapplingHooks(targ);
533 FOREACH_CLIENT(IS_PLAYER(it),
535 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
537 .entity weaponentity = weaponentities[slot];
538 if(it.(weaponentity).hook.aiment == targ)
539 RemoveHook(it.(weaponentity).hook);
544 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
545 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
548 void Unfreeze(entity targ, bool reset_health)
550 if(!STAT(FROZEN, targ))
553 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
554 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
556 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
558 STAT(FROZEN, targ) = 0;
559 STAT(REVIVE_PROGRESS, targ) = 0;
560 targ.revival_time = time;
562 IL_PUSH(g_bot_targets, targ);
563 targ.bot_attack = true;
565 WaypointSprite_Kill(targ.waypointsprite_attached);
567 FOREACH_CLIENT(IS_PLAYER(it),
569 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
571 .entity weaponentity = weaponentities[slot];
572 if(it.(weaponentity).hook.aiment == targ)
573 RemoveHook(it.(weaponentity).hook);
577 // remove the ice block
579 delete(targ.iceblock);
580 targ.iceblock = NULL;
582 MUTATOR_CALLHOOK(Unfreeze, targ);
585 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
587 float complainteamdamage = 0;
588 float mirrordamage = 0;
589 float mirrorforce = 0;
591 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
594 entity attacker_save = attacker;
596 // special rule: gravity bombs and sound-based attacks do not affect team mates (other than for disconnecting the hook)
597 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || (deathtype & HITTYPE_SOUND))
599 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
605 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
607 // exit the vehicle before killing (fixes a crash)
608 if(IS_PLAYER(targ) && targ.vehicle)
609 vehicles_exit(targ.vehicle, VHEF_RELEASE);
611 // These are ALWAYS lethal
612 // No damage modification here
613 // Instead, prepare the victim for their death...
614 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
616 SetResourceExplicit(targ, RES_ARMOR, 0);
617 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
619 StatusEffects_remove(STATUSEFFECT_SpawnShield, targ, STATUSEFFECT_REMOVE_CLEAR);
620 targ.flags -= targ.flags & FL_GODMODE;
623 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
629 // nullify damage if teamplay is on
630 if(deathtype != DEATH_TELEFRAG.m_id)
631 if(IS_PLAYER(attacker))
633 // avoid dealing damage or force to other independent players
634 // and avoid dealing damage or force to things owned by other independent players
635 if((IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ))) ||
636 (targ.realowner && IS_INDEPENDENT_PLAYER(targ.realowner) && attacker != targ.realowner))
641 else if(!STAT(FROZEN, targ) && SAME_TEAM(attacker, targ))
643 if(autocvar_teamplay_mode == 1)
645 else if(attacker != targ)
647 if(autocvar_teamplay_mode == 2)
649 if(IS_PLAYER(targ) && !IS_DEAD(targ))
651 attacker.dmg_team = attacker.dmg_team + damage;
652 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
655 else if(autocvar_teamplay_mode == 3)
657 else if(autocvar_teamplay_mode == 4)
659 if(IS_PLAYER(targ) && !IS_DEAD(targ))
661 attacker.dmg_team = attacker.dmg_team + damage;
662 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
663 if(complainteamdamage > 0)
664 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
665 mirrorforce = autocvar_g_mirrordamage * vlen(force);
666 damage = autocvar_g_friendlyfire * damage;
667 // mirrordamage will be used LATER
669 if(autocvar_g_mirrordamage_virtual)
671 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
672 attacker.dmg_take += v.x;
673 attacker.dmg_save += v.y;
674 attacker.dmg_inflictor = inflictor;
679 if(autocvar_g_friendlyfire_virtual)
681 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
682 targ.dmg_take += v.x;
683 targ.dmg_save += v.y;
684 targ.dmg_inflictor = inflictor;
686 if(!autocvar_g_friendlyfire_virtual_force)
690 else if(!targ.canteamdamage)
697 if (!DEATH_ISSPECIAL(deathtype))
699 damage *= autocvar_g_weapondamagefactor;
700 mirrordamage *= autocvar_g_weapondamagefactor;
701 complainteamdamage *= autocvar_g_weapondamagefactor;
702 force = force * autocvar_g_weaponforcefactor;
703 mirrorforce *= autocvar_g_weaponforcefactor;
706 // should this be changed at all? If so, in what way?
707 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
708 damage = M_ARGV(4, float);
709 mirrordamage = M_ARGV(5, float);
710 force = M_ARGV(6, vector);
712 if(IS_PLAYER(targ) && damage > 0 && attacker)
714 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
716 .entity went = weaponentities[slot];
717 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
718 RemoveHook(targ.(went).hook);
722 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
723 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
725 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
727 Unfreeze(targ, false);
728 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
729 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
730 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
731 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
735 force *= autocvar_g_frozen_force;
738 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
739 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
741 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
743 entity spot = SelectSpawnPoint(targ, false);
747 targ.deadflag = DEAD_NO;
749 targ.angles = spot.angles;
752 targ.effects |= EF_TELEPORT_BIT;
754 targ.angles_z = 0; // never spawn tilted even if the spot says to
755 targ.fixangle = true; // turn this way immediately
756 targ.velocity = '0 0 0';
757 targ.avelocity = '0 0 0';
758 targ.punchangle = '0 0 0';
759 targ.punchvector = '0 0 0';
760 targ.oldvelocity = targ.velocity;
762 targ.spawnorigin = spot.origin;
763 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
764 // don't reset back to last position, even if new position is stuck in solid
765 targ.oldorigin = targ.origin;
767 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
771 if (targ == attacker)
772 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
777 if(deathtype != DEATH_BUFF.m_id)
778 if(targ.takedamage == DAMAGE_AIM)
782 if(IS_VEHICLE(targ) && targ.owner)
787 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
789 if (DIFF_TEAM(victim, attacker))
793 if(deathtype != DEATH_FIRE.m_id)
795 if(PHYS_INPUT_BUTTON_CHAT(victim))
796 attacker.typehitsound += 1;
798 attacker.hitsound_damage_dealt += damage;
801 impressive_hits += 1;
803 if (!DEATH_ISSPECIAL(deathtype))
805 if(IS_PLAYER(targ)) // don't do this for vehicles
811 else if (IS_PLAYER(attacker) && !STAT(FROZEN, victim)) // same team
813 if (deathtype != DEATH_FIRE.m_id)
815 attacker.typehitsound += 1;
817 if(complainteamdamage > 0)
818 if(time > CS(attacker).teamkill_complain)
820 CS(attacker).teamkill_complain = time + 5;
821 CS(attacker).teamkill_soundtime = time + 0.4;
822 CS(attacker).teamkill_soundsource = targ;
830 if (targ.damageforcescale)
832 if (!IS_PLAYER(targ) || !StatusEffects_active(STATUSEFFECT_SpawnShield, targ) || targ == attacker)
834 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
835 if(targ.move_movetype == MOVETYPE_PHYSICS)
837 entity farcent = new(farce);
838 farcent.enemy = targ;
839 farcent.movedir = farce * 10;
841 farcent.movedir = farcent.movedir * targ.mass;
842 farcent.origin = hitloc;
843 farcent.forcetype = FORCETYPE_FORCEATPOS;
844 farcent.nextthink = time + 0.1;
845 setthink(farcent, SUB_Remove);
847 else if(targ.move_movetype != MOVETYPE_NOCLIP)
849 targ.velocity = targ.velocity + farce;
851 UNSET_ONGROUND(targ);
852 UpdateCSQCProjectile(targ);
855 if (damage != 0 || (targ.damageforcescale && force))
856 if (targ.event_damage)
857 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
859 // apply mirror damage if any
860 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
861 if(mirrordamage > 0 || mirrorforce > 0)
863 attacker = attacker_save;
865 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
866 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
870 // Returns total damage applies to creatures
871 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
872 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
876 float total_damage_to_creatures;
881 float stat_damagedone;
883 if(RadiusDamage_running)
885 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
889 if (rad < 0) rad = 0;
891 RadiusDamage_running = 1;
893 tfloordmg = autocvar_g_throughfloor_damage;
894 tfloorforce = autocvar_g_throughfloor_force;
896 total_damage_to_creatures = 0;
898 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
899 if(!(deathtype & HITTYPE_SOUND)) // do not send radial sound damage (bandwidth hog)
901 force = inflictorvelocity;
905 force = normalize(force);
906 if(forceintensity >= 0)
907 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
909 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
914 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
918 if ((targ != inflictor) || inflictorselfdamage)
919 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
922 // calculate distance from nearest point on target to nearest point on inflictor
923 // instead of origin to ensure full damage on impacts
925 vector nearest = targ.WarpZone_findradius_nearest;
927 // optimize code by getting inflictororigin_wz from WarpZone_FindRadius calculations instead of
928 //vector inflictororigin_wz = WarpZone_TransformOrigin(targ, inflictororigin);
930 vector inflictororigin_wz = targ.WarpZone_findradius_nearest + targ.WarpZone_findradius_dist;
931 vector inflictornearest = NearestPointOnBoundingBox(
932 inflictororigin_wz + inflictor.mins, inflictororigin_wz + inflictor.maxs, nearest);
933 vector diff = inflictornearest - nearest;
935 // round up a little on the damage to ensure full damage on impacts
936 // and turn the distance into a fraction of the radius
937 float dist = max(0, vlen(diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS));
940 float f = (rad > 0) ? 1 - (dist / rad) : 1;
941 // at this point f can't be < 0 or > 1
942 float finaldmg = coredamage * f + edgedamage * (1 - f);
949 // if it's a player, use the view origin as reference
950 vector center = CENTER_OR_VIEWOFS(targ);
952 if (autocvar_g_player_damageplayercenter)
954 if (targ != attacker)
956 // always use target's bbox centerpoint
957 center = targ.origin + ((targ.mins + targ.maxs) * 0.5);
959 else // targ == attacker
962 // code stolen from W_SetupShot_Dir_ProjectileSize_Range()
963 vector md = targ.(weaponentity).movedir;
964 vector vecs = ((md.x > 0) ? md : '0 0 0');
965 vector dv = v_right * -vecs.y + v_up * vecs.z;
966 vector mi = '0 0 0', ma = '0 0 0';
968 if(IS_CLIENT(targ)) // no antilag for non-clients!
970 if(CS(targ).antilag_debug)
971 tracebox_antilag(targ, center, mi, ma, center + dv, MOVE_NORMAL, targ, CS(targ).antilag_debug);
973 tracebox_antilag(targ, center, mi, ma, center + dv, MOVE_NORMAL, targ, ANTILAG_LATENCY(targ));
976 tracebox(center, mi, ma, center + dv, MOVE_NORMAL, targ);
978 center.z = trace_endpos.z;
980 // very cheap way but it skips movedir.x > 0 checks and move into solid checks which is fine most of the time for now AFAIK
981 // this should only really be an issue with absurd g_shootfromfixedorigin custom values like "-1 0 9001"
982 center.z = center.z + targ.(weaponentity).movedir.z;
988 print(sprintf("origin vec %v\n", targ.origin));
989 print(sprintf("movedir vec %v\n", targ.(weaponentity).movedir));
990 print(sprintf("old def vec %v\n", CENTER_OR_VIEWOFS(targ)));
991 print(sprintf("origin+vofs %v\n", targ.origin + targ.view_ofs));
992 print(sprintf("bbox center %v\n", (targ.origin + ((targ.mins + targ.maxs) * 0.5))));
993 print(sprintf("center vec %v\n", center));
994 print(sprintf("shotorg vec %v\n", w_shotorg));
998 force = normalize(center - inflictororigin_wz);
999 force = force * (finaldmg / max(coredamage, edgedamage)) * forceintensity;
1002 // apply special scaling along the z axis if set
1003 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
1005 force.z *= forcezscale;
1007 if(targ != directhitentity)
1012 float mininv_f, mininv_d;
1014 // test line of sight to multiple positions on box,
1015 // and do damage if any of them hit
1018 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
1019 // so for a given max stddev:
1020 // n = (1 / (2 * max stddev of hitratio))^2
1022 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
1023 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
1025 if(autocvar_g_throughfloor_debug)
1026 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1029 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1031 if(autocvar_g_throughfloor_debug)
1032 LOG_INFOF(" steps=%f", total);
1035 if (IS_PLAYER(targ))
1036 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1038 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1040 if(autocvar_g_throughfloor_debug)
1041 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1043 for(c = 0; c < total; ++c)
1045 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1046 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1047 if (trace_fraction == 1 || trace_ent == targ)
1051 hitloc = hitloc + nearest;
1055 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1056 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1057 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1060 nearest = hitloc * (1 / max(1, hits));
1061 hitratio = (hits / total);
1062 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1063 finaldmg = finaldmg * a;
1064 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1067 if(autocvar_g_throughfloor_debug)
1068 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1070 /*if (targ == attacker)
1072 print("hits ", ftos(hits), " / ", ftos(total));
1073 print(" finaldmg ", ftos(finaldmg), " force ", ftos(vlen(force)));
1074 print(" (", vtos(force), ") (", ftos(a), ")\n");
1078 if(finaldmg || force)
1082 total_damage_to_creatures += finaldmg;
1084 if(accuracy_isgooddamage(attacker, targ))
1085 stat_damagedone += finaldmg;
1088 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1089 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1091 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1099 RadiusDamage_running = 0;
1101 if(!DEATH_ISSPECIAL(deathtype))
1102 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(max(coredamage, edgedamage), stat_damagedone));
1104 return total_damage_to_creatures;
1107 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1109 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1110 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1113 bool Heal(entity targ, entity inflictor, float amount, float limit)
1115 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1118 bool healed = false;
1120 healed = targ.event_heal(targ, inflictor, amount, limit);
1121 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1122 // TODO: healing fx!
1123 // TODO: armor healing?
1127 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1130 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1140 if(StatusEffects_active(STATUSEFFECT_Burning, e))
1142 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1144 mintime = fireendtime - time;
1145 maxtime = max(mintime, t);
1147 mindps = e.fire_damagepersec;
1148 maxdps = max(mindps, dps);
1150 if(maxtime > mintime || maxdps > mindps)
1154 // damage we have right now
1155 mindamage = mindps * mintime;
1157 // damage we want to get
1158 maxdamage = mindamage + d;
1160 // but we can't exceed maxtime * maxdps!
1161 totaldamage = min(maxdamage, maxtime * maxdps);
1165 // totaldamage = min(mindamage + d, maxtime * maxdps)
1167 // totaldamage <= maxtime * maxdps
1168 // ==> totaldamage / maxdps <= maxtime.
1170 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1171 // >= min(mintime, maxtime)
1172 // ==> totaldamage / maxdps >= mintime.
1175 // how long do we damage then?
1176 // at least as long as before
1177 // but, never exceed maxdps
1178 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1182 // at most as long as maximum allowed
1183 // but, never below mindps
1184 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1186 // assuming t > mintime, dps > mindps:
1187 // we get d = t * dps = maxtime * maxdps
1188 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1189 // totaldamage / maxdps = maxtime
1190 // totaldamage / mindps > totaldamage / maxdps = maxtime
1192 // a) totaltime = max(mintime, maxtime) = maxtime
1193 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1195 // assuming t <= mintime:
1196 // we get maxtime = mintime
1197 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1198 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1200 // assuming dps <= mindps:
1201 // we get mindps = maxdps.
1202 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1203 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1204 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1206 e.fire_damagepersec = totaldamage / totaltime;
1207 StatusEffects_apply(STATUSEFFECT_Burning, e, time + totaltime, 0);
1208 if(totaldamage > 1.2 * mindamage)
1210 e.fire_deathtype = dt;
1211 if(e.fire_owner != o)
1214 e.fire_hitsound = false;
1217 if(accuracy_isgooddamage(o, e))
1218 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1219 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1226 e.fire_damagepersec = dps;
1227 StatusEffects_apply(STATUSEFFECT_Burning, e, time + t, 0);
1228 e.fire_deathtype = dt;
1230 e.fire_hitsound = false;
1231 if(accuracy_isgooddamage(o, e))
1232 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1237 void Fire_ApplyDamage(entity e)
1242 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1243 if(IS_NOT_A_CLIENT(o))
1246 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1247 t = min(frametime, fireendtime - time);
1248 d = e.fire_damagepersec * t;
1250 hi = e.fire_owner.hitsound_damage_dealt;
1251 ty = e.fire_owner.typehitsound;
1252 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1253 if(e.fire_hitsound && e.fire_owner)
1255 e.fire_owner.hitsound_damage_dealt = hi;
1256 e.fire_owner.typehitsound = ty;
1258 e.fire_hitsound = true;
1260 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1262 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1264 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1265 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1267 t = autocvar_g_balance_firetransfer_time * (fireendtime - time);
1268 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1269 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);