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 dv = v_right * -md.y + v_up * md.z;
965 vector mi = '0 0 0', ma = '0 0 0';
967 if(IS_CLIENT(targ)) // no antilag for non-clients!
969 if(CS(targ).antilag_debug)
970 tracebox_antilag(targ, center, mi, ma, center + dv, MOVE_NORMAL, targ, CS(targ).antilag_debug);
972 tracebox_antilag(targ, center, mi, ma, center + dv, MOVE_NORMAL, targ, ANTILAG_LATENCY(targ));
975 tracebox(center, mi, ma, center + dv, MOVE_NORMAL, targ);
977 center.z = trace_endpos.z;
979 // very cheap way but it skips move into solid checks which is fine most of the time for now AFAIK
980 // this should only really be an issue with some rare edge cases where
981 // shot origin was prevented from going into a ceiling but it still explodes at the ceiling
982 // shot origin wasn't raised as high as possible and the shooter gets upwards knockback
983 // TL;DR: no bugs if vertical shot origin is always within player bbox
984 center.z = center.z + targ.(weaponentity).movedir.z;
990 print(sprintf("origin vec %v\n", targ.origin));
991 print(sprintf("movedir vec %v\n", targ.(weaponentity).movedir));
992 print(sprintf("old def vec %v\n", CENTER_OR_VIEWOFS(targ)));
993 print(sprintf("origin+vofs %v\n", targ.origin + targ.view_ofs));
994 print(sprintf("bbox center %v\n", (targ.origin + ((targ.mins + targ.maxs) * 0.5))));
995 print(sprintf("center vec %v\n", center));
996 print(sprintf("shotorg vec %v\n", w_shotorg));
1000 force = normalize(center - inflictororigin_wz);
1001 force = force * (finaldmg / max(coredamage, edgedamage)) * forceintensity;
1004 // apply special scaling along the z axis if set
1005 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
1007 force.z *= forcezscale;
1009 if(targ != directhitentity)
1014 float mininv_f, mininv_d;
1016 // test line of sight to multiple positions on box,
1017 // and do damage if any of them hit
1020 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
1021 // so for a given max stddev:
1022 // n = (1 / (2 * max stddev of hitratio))^2
1024 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
1025 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
1027 if(autocvar_g_throughfloor_debug)
1028 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1031 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1033 if(autocvar_g_throughfloor_debug)
1034 LOG_INFOF(" steps=%f", total);
1037 if (IS_PLAYER(targ))
1038 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1040 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1042 if(autocvar_g_throughfloor_debug)
1043 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1045 for(c = 0; c < total; ++c)
1047 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1048 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1049 if (trace_fraction == 1 || trace_ent == targ)
1053 hitloc = hitloc + nearest;
1057 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1058 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1059 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1062 nearest = hitloc * (1 / max(1, hits));
1063 hitratio = (hits / total);
1064 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1065 finaldmg = finaldmg * a;
1066 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1069 if(autocvar_g_throughfloor_debug)
1070 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1072 /*if (targ == attacker)
1074 print("hits ", ftos(hits), " / ", ftos(total));
1075 print(" finaldmg ", ftos(finaldmg), " force ", ftos(vlen(force)));
1076 print(" (", vtos(force), ") (", ftos(a), ")\n");
1080 if(finaldmg || force)
1084 total_damage_to_creatures += finaldmg;
1086 if(accuracy_isgooddamage(attacker, targ))
1087 stat_damagedone += finaldmg;
1090 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1091 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1093 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1101 RadiusDamage_running = 0;
1103 if(!DEATH_ISSPECIAL(deathtype))
1104 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(max(coredamage, edgedamage), stat_damagedone));
1106 return total_damage_to_creatures;
1109 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1111 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1112 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1115 bool Heal(entity targ, entity inflictor, float amount, float limit)
1117 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1120 bool healed = false;
1122 healed = targ.event_heal(targ, inflictor, amount, limit);
1123 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1124 // TODO: healing fx!
1125 // TODO: armor healing?
1129 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1132 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1142 if(StatusEffects_active(STATUSEFFECT_Burning, e))
1144 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1146 mintime = fireendtime - time;
1147 maxtime = max(mintime, t);
1149 mindps = e.fire_damagepersec;
1150 maxdps = max(mindps, dps);
1152 if(maxtime > mintime || maxdps > mindps)
1156 // damage we have right now
1157 mindamage = mindps * mintime;
1159 // damage we want to get
1160 maxdamage = mindamage + d;
1162 // but we can't exceed maxtime * maxdps!
1163 totaldamage = min(maxdamage, maxtime * maxdps);
1167 // totaldamage = min(mindamage + d, maxtime * maxdps)
1169 // totaldamage <= maxtime * maxdps
1170 // ==> totaldamage / maxdps <= maxtime.
1172 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1173 // >= min(mintime, maxtime)
1174 // ==> totaldamage / maxdps >= mintime.
1177 // how long do we damage then?
1178 // at least as long as before
1179 // but, never exceed maxdps
1180 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1184 // at most as long as maximum allowed
1185 // but, never below mindps
1186 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1188 // assuming t > mintime, dps > mindps:
1189 // we get d = t * dps = maxtime * maxdps
1190 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1191 // totaldamage / maxdps = maxtime
1192 // totaldamage / mindps > totaldamage / maxdps = maxtime
1194 // a) totaltime = max(mintime, maxtime) = maxtime
1195 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1197 // assuming t <= mintime:
1198 // we get maxtime = mintime
1199 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1200 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1202 // assuming dps <= mindps:
1203 // we get mindps = maxdps.
1204 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1205 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1206 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1208 e.fire_damagepersec = totaldamage / totaltime;
1209 StatusEffects_apply(STATUSEFFECT_Burning, e, time + totaltime, 0);
1210 if(totaldamage > 1.2 * mindamage)
1212 e.fire_deathtype = dt;
1213 if(e.fire_owner != o)
1216 e.fire_hitsound = false;
1219 if(accuracy_isgooddamage(o, e))
1220 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1221 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1228 e.fire_damagepersec = dps;
1229 StatusEffects_apply(STATUSEFFECT_Burning, e, time + t, 0);
1230 e.fire_deathtype = dt;
1232 e.fire_hitsound = false;
1233 if(accuracy_isgooddamage(o, e))
1234 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1239 void Fire_ApplyDamage(entity e)
1244 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1245 if(IS_NOT_A_CLIENT(o))
1248 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1249 t = min(frametime, fireendtime - time);
1250 d = e.fire_damagepersec * t;
1252 hi = e.fire_owner.hitsound_damage_dealt;
1253 ty = e.fire_owner.typehitsound;
1254 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1255 if(e.fire_hitsound && e.fire_owner)
1257 e.fire_owner.hitsound_damage_dealt = hi;
1258 e.fire_owner.typehitsound = ty;
1260 e.fire_hitsound = true;
1262 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1264 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1266 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1267 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1269 t = autocvar_g_balance_firetransfer_time * (fireendtime - time);
1270 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1271 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);