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 // measure distance from nearest point on target (not origin)
923 // to nearest point on inflictor (not origin)
924 vector nearest = targ.WarpZone_findradius_nearest;
925 vector inflictororigin_wz = targ.WarpZone_findradius_nearest + targ.WarpZone_findradius_dist;
926 vector inflictornearest = NearestPointOnBoundingBox(
927 inflictororigin_wz + inflictor.mins, inflictororigin_wz + inflictor.maxs, nearest);
928 vector diff = inflictornearest - nearest;
930 // round up a little on the damage to ensure full damage on impacts
931 // and turn the distance into a fraction of the radius
932 float dist = max(0, vlen(diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS));
935 float f = (rad > 0) ? 1 - (dist / rad) : 1;
936 // at this point f can't be < 0 or > 1
937 float finaldmg = coredamage * f + edgedamage * (1 - f);
943 vector myblastorigin;
946 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
948 // if it's a player, use the view origin as reference
949 center = CENTER_OR_VIEWOFS(targ);
951 force = normalize(center - myblastorigin);
952 force = force * (finaldmg / max(coredamage, edgedamage)) * forceintensity;
955 // apply special scaling along the z axis if set
956 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
958 force.z *= forcezscale;
960 if(targ != directhitentity)
965 float mininv_f, mininv_d;
967 // test line of sight to multiple positions on box,
968 // and do damage if any of them hit
971 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
972 // so for a given max stddev:
973 // n = (1 / (2 * max stddev of hitratio))^2
975 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
976 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
978 if(autocvar_g_throughfloor_debug)
979 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
982 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
984 if(autocvar_g_throughfloor_debug)
985 LOG_INFOF(" steps=%f", total);
989 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
991 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
993 if(autocvar_g_throughfloor_debug)
994 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
996 for(c = 0; c < total; ++c)
998 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
999 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1000 if (trace_fraction == 1 || trace_ent == targ)
1004 hitloc = hitloc + nearest;
1008 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1009 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1010 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1013 nearest = hitloc * (1 / max(1, hits));
1014 hitratio = (hits / total);
1015 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1016 finaldmg = finaldmg * a;
1017 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1020 if(autocvar_g_throughfloor_debug)
1021 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1023 /*if (targ == attacker)
1025 print("hits ", ftos(hits), " / ", ftos(total));
1026 print(" finaldmg ", ftos(finaldmg), " force ", ftos(vlen(force)));
1027 print(" (", vtos(force), ") (", ftos(a), ")\n");
1031 if(finaldmg || force)
1035 total_damage_to_creatures += finaldmg;
1037 if(accuracy_isgooddamage(attacker, targ))
1038 stat_damagedone += finaldmg;
1041 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1042 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1044 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1052 RadiusDamage_running = 0;
1054 if(!DEATH_ISSPECIAL(deathtype))
1055 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(max(coredamage, edgedamage), stat_damagedone));
1057 return total_damage_to_creatures;
1060 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1062 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1063 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1066 bool Heal(entity targ, entity inflictor, float amount, float limit)
1068 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1071 bool healed = false;
1073 healed = targ.event_heal(targ, inflictor, amount, limit);
1074 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1075 // TODO: healing fx!
1076 // TODO: armor healing?
1080 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1083 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1093 if(StatusEffects_active(STATUSEFFECT_Burning, e))
1095 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1097 mintime = fireendtime - time;
1098 maxtime = max(mintime, t);
1100 mindps = e.fire_damagepersec;
1101 maxdps = max(mindps, dps);
1103 if(maxtime > mintime || maxdps > mindps)
1107 // damage we have right now
1108 mindamage = mindps * mintime;
1110 // damage we want to get
1111 maxdamage = mindamage + d;
1113 // but we can't exceed maxtime * maxdps!
1114 totaldamage = min(maxdamage, maxtime * maxdps);
1118 // totaldamage = min(mindamage + d, maxtime * maxdps)
1120 // totaldamage <= maxtime * maxdps
1121 // ==> totaldamage / maxdps <= maxtime.
1123 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1124 // >= min(mintime, maxtime)
1125 // ==> totaldamage / maxdps >= mintime.
1128 // how long do we damage then?
1129 // at least as long as before
1130 // but, never exceed maxdps
1131 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1135 // at most as long as maximum allowed
1136 // but, never below mindps
1137 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1139 // assuming t > mintime, dps > mindps:
1140 // we get d = t * dps = maxtime * maxdps
1141 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1142 // totaldamage / maxdps = maxtime
1143 // totaldamage / mindps > totaldamage / maxdps = maxtime
1145 // a) totaltime = max(mintime, maxtime) = maxtime
1146 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1148 // assuming t <= mintime:
1149 // we get maxtime = mintime
1150 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1151 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1153 // assuming dps <= mindps:
1154 // we get mindps = maxdps.
1155 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1156 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1157 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1159 e.fire_damagepersec = totaldamage / totaltime;
1160 StatusEffects_apply(STATUSEFFECT_Burning, e, time + totaltime, 0);
1161 if(totaldamage > 1.2 * mindamage)
1163 e.fire_deathtype = dt;
1164 if(e.fire_owner != o)
1167 e.fire_hitsound = false;
1170 if(accuracy_isgooddamage(o, e))
1171 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1172 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1179 e.fire_damagepersec = dps;
1180 StatusEffects_apply(STATUSEFFECT_Burning, e, time + t, 0);
1181 e.fire_deathtype = dt;
1183 e.fire_hitsound = false;
1184 if(accuracy_isgooddamage(o, e))
1185 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1190 void Fire_ApplyDamage(entity e)
1195 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1196 if(IS_NOT_A_CLIENT(o))
1199 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1200 t = min(frametime, fireendtime - time);
1201 d = e.fire_damagepersec * t;
1203 hi = e.fire_owner.hitsound_damage_dealt;
1204 ty = e.fire_owner.typehitsound;
1205 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1206 if(e.fire_hitsound && e.fire_owner)
1208 e.fire_owner.hitsound_damage_dealt = hi;
1209 e.fire_owner.typehitsound = ty;
1211 e.fire_hitsound = true;
1213 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1215 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1217 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1218 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1220 t = autocvar_g_balance_firetransfer_time * (fireendtime - time);
1221 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1222 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);