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);
950 // if it's a player, use the view origin as reference
951 center = CENTER_OR_VIEWOFS(targ);
953 force = normalize(center - inflictororigin_wz);
954 force = force * (finaldmg / max(coredamage, edgedamage)) * forceintensity;
957 // apply special scaling along the z axis if set
958 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
960 force.z *= forcezscale;
962 if(targ != directhitentity)
967 float mininv_f, mininv_d;
969 // test line of sight to multiple positions on box,
970 // and do damage if any of them hit
973 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
974 // so for a given max stddev:
975 // n = (1 / (2 * max stddev of hitratio))^2
977 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
978 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
980 if(autocvar_g_throughfloor_debug)
981 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
984 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
986 if(autocvar_g_throughfloor_debug)
987 LOG_INFOF(" steps=%f", total);
991 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
993 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
995 if(autocvar_g_throughfloor_debug)
996 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
998 for(c = 0; c < total; ++c)
1000 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1001 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1002 if (trace_fraction == 1 || trace_ent == targ)
1006 hitloc = hitloc + nearest;
1010 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1011 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1012 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1015 nearest = hitloc * (1 / max(1, hits));
1016 hitratio = (hits / total);
1017 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1018 finaldmg = finaldmg * a;
1019 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1022 if(autocvar_g_throughfloor_debug)
1023 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1025 /*if (targ == attacker)
1027 print("hits ", ftos(hits), " / ", ftos(total));
1028 print(" finaldmg ", ftos(finaldmg), " force ", ftos(vlen(force)));
1029 print(" (", vtos(force), ") (", ftos(a), ")\n");
1033 if(finaldmg || force)
1037 total_damage_to_creatures += finaldmg;
1039 if(accuracy_isgooddamage(attacker, targ))
1040 stat_damagedone += finaldmg;
1043 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1044 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1046 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1054 RadiusDamage_running = 0;
1056 if(!DEATH_ISSPECIAL(deathtype))
1057 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(max(coredamage, edgedamage), stat_damagedone));
1059 return total_damage_to_creatures;
1062 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1064 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1065 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1068 bool Heal(entity targ, entity inflictor, float amount, float limit)
1070 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1073 bool healed = false;
1075 healed = targ.event_heal(targ, inflictor, amount, limit);
1076 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1077 // TODO: healing fx!
1078 // TODO: armor healing?
1082 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1085 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1095 if(StatusEffects_active(STATUSEFFECT_Burning, e))
1097 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1099 mintime = fireendtime - time;
1100 maxtime = max(mintime, t);
1102 mindps = e.fire_damagepersec;
1103 maxdps = max(mindps, dps);
1105 if(maxtime > mintime || maxdps > mindps)
1109 // damage we have right now
1110 mindamage = mindps * mintime;
1112 // damage we want to get
1113 maxdamage = mindamage + d;
1115 // but we can't exceed maxtime * maxdps!
1116 totaldamage = min(maxdamage, maxtime * maxdps);
1120 // totaldamage = min(mindamage + d, maxtime * maxdps)
1122 // totaldamage <= maxtime * maxdps
1123 // ==> totaldamage / maxdps <= maxtime.
1125 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1126 // >= min(mintime, maxtime)
1127 // ==> totaldamage / maxdps >= mintime.
1130 // how long do we damage then?
1131 // at least as long as before
1132 // but, never exceed maxdps
1133 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1137 // at most as long as maximum allowed
1138 // but, never below mindps
1139 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1141 // assuming t > mintime, dps > mindps:
1142 // we get d = t * dps = maxtime * maxdps
1143 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1144 // totaldamage / maxdps = maxtime
1145 // totaldamage / mindps > totaldamage / maxdps = maxtime
1147 // a) totaltime = max(mintime, maxtime) = maxtime
1148 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1150 // assuming t <= mintime:
1151 // we get maxtime = mintime
1152 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1153 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1155 // assuming dps <= mindps:
1156 // we get mindps = maxdps.
1157 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1158 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1159 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1161 e.fire_damagepersec = totaldamage / totaltime;
1162 StatusEffects_apply(STATUSEFFECT_Burning, e, time + totaltime, 0);
1163 if(totaldamage > 1.2 * mindamage)
1165 e.fire_deathtype = dt;
1166 if(e.fire_owner != o)
1169 e.fire_hitsound = false;
1172 if(accuracy_isgooddamage(o, e))
1173 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1174 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1181 e.fire_damagepersec = dps;
1182 StatusEffects_apply(STATUSEFFECT_Burning, e, time + t, 0);
1183 e.fire_deathtype = dt;
1185 e.fire_hitsound = false;
1186 if(accuracy_isgooddamage(o, e))
1187 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1192 void Fire_ApplyDamage(entity e)
1197 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1198 if(IS_NOT_A_CLIENT(o))
1201 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1202 t = min(frametime, fireendtime - time);
1203 d = e.fire_damagepersec * t;
1205 hi = e.fire_owner.hitsound_damage_dealt;
1206 ty = e.fire_owner.typehitsound;
1207 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1208 if(e.fire_hitsound && e.fire_owner)
1210 e.fire_owner.hitsound_damage_dealt = hi;
1211 e.fire_owner.typehitsound = ty;
1213 e.fire_hitsound = true;
1215 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1217 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1219 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1220 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1222 t = autocvar_g_balance_firetransfer_time * (fireendtime - time);
1223 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1224 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);