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);
713 // Quake 3 DeFRaG's cpm physic's rocketlauncher has 1.2x horizontal force multiplier
714 // This aims to reproduce it in Quake 3 DeFRaG cpm and XDF physics if damagepush_speedfactor is 0
715 // CPMA cpm physics should be active in .arena maps and Quake 3 DeFRaG cpm physics in .defi maps
716 // It is only intended to be used with 250 base force for devastator which matches Q3 if our
717 // target's damageforcescale is also 2
719 if(q3compat & Q3COMPAT_DEFI)
720 if(autocvar_g_balance_damagepush_speedfactor == 0)
721 if(attacker.(weaponentity).m_weapon == WEP_DEVASTATOR)
722 if(autocvar_g_balance_devastator_force == 250)
723 if(targ.damageforcescale == 2)
724 if((cvar_string("g_mod_physics") == "CPMA") || (cvar_string("g_mod_physics") == "XDF"))
730 if(IS_PLAYER(targ) && damage > 0 && attacker)
732 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
734 .entity went = weaponentities[slot];
735 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
736 RemoveHook(targ.(went).hook);
740 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
741 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
743 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
745 Unfreeze(targ, false);
746 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
747 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
748 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
749 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
753 force *= autocvar_g_frozen_force;
756 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
757 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
759 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
761 entity spot = SelectSpawnPoint(targ, false);
765 targ.deadflag = DEAD_NO;
767 targ.angles = spot.angles;
770 targ.effects |= EF_TELEPORT_BIT;
772 targ.angles_z = 0; // never spawn tilted even if the spot says to
773 targ.fixangle = true; // turn this way immediately
774 targ.velocity = '0 0 0';
775 targ.avelocity = '0 0 0';
776 targ.punchangle = '0 0 0';
777 targ.punchvector = '0 0 0';
778 targ.oldvelocity = targ.velocity;
780 targ.spawnorigin = spot.origin;
781 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
782 // don't reset back to last position, even if new position is stuck in solid
783 targ.oldorigin = targ.origin;
785 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
789 if (targ == attacker)
790 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
795 if(deathtype != DEATH_BUFF.m_id)
796 if(targ.takedamage == DAMAGE_AIM)
800 if(IS_VEHICLE(targ) && targ.owner)
805 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
807 if (DIFF_TEAM(victim, attacker))
811 if(deathtype != DEATH_FIRE.m_id)
813 if(PHYS_INPUT_BUTTON_CHAT(victim))
814 attacker.typehitsound += 1;
816 attacker.hitsound_damage_dealt += damage;
819 impressive_hits += 1;
821 if (!DEATH_ISSPECIAL(deathtype))
823 if(IS_PLAYER(targ)) // don't do this for vehicles
829 else if (IS_PLAYER(attacker) && !STAT(FROZEN, victim)) // same team
831 if (deathtype != DEATH_FIRE.m_id)
833 attacker.typehitsound += 1;
835 if(complainteamdamage > 0)
836 if(time > CS(attacker).teamkill_complain)
838 CS(attacker).teamkill_complain = time + 5;
839 CS(attacker).teamkill_soundtime = time + 0.4;
840 CS(attacker).teamkill_soundsource = targ;
848 if (targ.damageforcescale)
850 if (!IS_PLAYER(targ) || !StatusEffects_active(STATUSEFFECT_SpawnShield, targ) || targ == attacker)
852 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
853 if(targ.move_movetype == MOVETYPE_PHYSICS)
855 entity farcent = new(farce);
856 farcent.enemy = targ;
857 farcent.movedir = farce * 10;
859 farcent.movedir = farcent.movedir * targ.mass;
860 farcent.origin = hitloc;
861 farcent.forcetype = FORCETYPE_FORCEATPOS;
862 farcent.nextthink = time + 0.1;
863 setthink(farcent, SUB_Remove);
865 else if(targ.move_movetype != MOVETYPE_NOCLIP)
867 targ.velocity = targ.velocity + farce;
869 UNSET_ONGROUND(targ);
870 UpdateCSQCProjectile(targ);
873 if (damage != 0 || (targ.damageforcescale && force))
874 if (targ.event_damage)
875 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
877 // apply mirror damage if any
878 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
879 if(mirrordamage > 0 || mirrorforce > 0)
881 attacker = attacker_save;
883 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
884 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
888 // Returns total damage applies to creatures
889 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
890 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
894 float total_damage_to_creatures;
899 float stat_damagedone;
901 if(RadiusDamage_running)
903 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
907 if (rad < 0) rad = 0;
909 RadiusDamage_running = 1;
911 tfloordmg = autocvar_g_throughfloor_damage;
912 tfloorforce = autocvar_g_throughfloor_force;
914 total_damage_to_creatures = 0;
916 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
917 if(!(deathtype & HITTYPE_SOUND)) // do not send radial sound damage (bandwidth hog)
919 force = inflictorvelocity;
923 force = normalize(force);
924 if(forceintensity >= 0)
925 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
927 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
932 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
936 if ((targ != inflictor) || inflictorselfdamage)
937 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
940 // measure distance from nearest point on target (not origin)
941 // to nearest point on inflictor (not origin)
942 vector nearest = targ.WarpZone_findradius_nearest;
943 vector inflictornearest = NearestPointOnBoundingBox(
944 inflictororigin - (inflictor.maxs - inflictor.mins) * 0.5,
945 inflictororigin + (inflictor.maxs - inflictor.mins) * 0.5,
947 vector diff = inflictornearest - nearest;
949 // round up a little on the damage to ensure full damage on impacts
950 // and turn the distance into a fraction of the radius
951 float dist = max(0, vlen(diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS));
954 float f = (rad > 0) ? 1 - (dist / rad) : 1;
955 // at this point f can't be < 0 or > 1
956 float finaldmg = coredamage * f + edgedamage * (1 - f);
962 vector myblastorigin;
965 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
967 // if it's a player, use the view origin as reference
968 center = CENTER_OR_VIEWOFS(targ);
970 force = normalize(center - myblastorigin);
971 force = force * (finaldmg / max(coredamage, edgedamage)) * forceintensity;
974 // apply special scaling along the z axis if set
975 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
977 force.z *= forcezscale;
979 if(targ != directhitentity)
984 float mininv_f, mininv_d;
986 // test line of sight to multiple positions on box,
987 // and do damage if any of them hit
990 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
991 // so for a given max stddev:
992 // n = (1 / (2 * max stddev of hitratio))^2
994 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
995 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
997 if(autocvar_g_throughfloor_debug)
998 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1001 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1003 if(autocvar_g_throughfloor_debug)
1004 LOG_INFOF(" steps=%f", total);
1007 if (IS_PLAYER(targ))
1008 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1010 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1012 if(autocvar_g_throughfloor_debug)
1013 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1015 for(c = 0; c < total; ++c)
1017 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1018 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1019 if (trace_fraction == 1 || trace_ent == targ)
1023 hitloc = hitloc + nearest;
1027 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1028 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1029 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1032 nearest = hitloc * (1 / max(1, hits));
1033 hitratio = (hits / total);
1034 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1035 finaldmg = finaldmg * a;
1036 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1039 if(autocvar_g_throughfloor_debug)
1040 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1042 /*if (targ == attacker)
1044 print("hits ", ftos(hits), " / ", ftos(total));
1045 print(" finaldmg ", ftos(finaldmg), " force ", ftos(vlen(force)));
1046 print(" (", vtos(force), ") (", ftos(a), ")\n");
1050 if(finaldmg || force)
1054 total_damage_to_creatures += finaldmg;
1056 if(accuracy_isgooddamage(attacker, targ))
1057 stat_damagedone += finaldmg;
1060 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1061 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1063 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1071 RadiusDamage_running = 0;
1073 if(!DEATH_ISSPECIAL(deathtype))
1074 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(max(coredamage, edgedamage), stat_damagedone));
1076 return total_damage_to_creatures;
1079 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1081 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1082 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1085 bool Heal(entity targ, entity inflictor, float amount, float limit)
1087 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1090 bool healed = false;
1092 healed = targ.event_heal(targ, inflictor, amount, limit);
1093 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1094 // TODO: healing fx!
1095 // TODO: armor healing?
1099 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1102 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1112 if(StatusEffects_active(STATUSEFFECT_Burning, e))
1114 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1116 mintime = fireendtime - time;
1117 maxtime = max(mintime, t);
1119 mindps = e.fire_damagepersec;
1120 maxdps = max(mindps, dps);
1122 if(maxtime > mintime || maxdps > mindps)
1126 // damage we have right now
1127 mindamage = mindps * mintime;
1129 // damage we want to get
1130 maxdamage = mindamage + d;
1132 // but we can't exceed maxtime * maxdps!
1133 totaldamage = min(maxdamage, maxtime * maxdps);
1137 // totaldamage = min(mindamage + d, maxtime * maxdps)
1139 // totaldamage <= maxtime * maxdps
1140 // ==> totaldamage / maxdps <= maxtime.
1142 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1143 // >= min(mintime, maxtime)
1144 // ==> totaldamage / maxdps >= mintime.
1147 // how long do we damage then?
1148 // at least as long as before
1149 // but, never exceed maxdps
1150 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1154 // at most as long as maximum allowed
1155 // but, never below mindps
1156 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1158 // assuming t > mintime, dps > mindps:
1159 // we get d = t * dps = maxtime * maxdps
1160 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1161 // totaldamage / maxdps = maxtime
1162 // totaldamage / mindps > totaldamage / maxdps = maxtime
1164 // a) totaltime = max(mintime, maxtime) = maxtime
1165 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1167 // assuming t <= mintime:
1168 // we get maxtime = mintime
1169 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1170 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1172 // assuming dps <= mindps:
1173 // we get mindps = maxdps.
1174 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1175 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1176 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1178 e.fire_damagepersec = totaldamage / totaltime;
1179 StatusEffects_apply(STATUSEFFECT_Burning, e, time + totaltime, 0);
1180 if(totaldamage > 1.2 * mindamage)
1182 e.fire_deathtype = dt;
1183 if(e.fire_owner != o)
1186 e.fire_hitsound = false;
1189 if(accuracy_isgooddamage(o, e))
1190 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1191 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1198 e.fire_damagepersec = dps;
1199 StatusEffects_apply(STATUSEFFECT_Burning, e, time + t, 0);
1200 e.fire_deathtype = dt;
1202 e.fire_hitsound = false;
1203 if(accuracy_isgooddamage(o, e))
1204 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1209 void Fire_ApplyDamage(entity e)
1214 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1215 if(IS_NOT_A_CLIENT(o))
1218 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1219 t = min(frametime, fireendtime - time);
1220 d = e.fire_damagepersec * t;
1222 hi = e.fire_owner.hitsound_damage_dealt;
1223 ty = e.fire_owner.typehitsound;
1224 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1225 if(e.fire_hitsound && e.fire_owner)
1227 e.fire_owner.hitsound_damage_dealt = hi;
1228 e.fire_owner.typehitsound = ty;
1230 e.fire_hitsound = true;
1232 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1234 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1236 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1237 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1239 t = autocvar_g_balance_firetransfer_time * (fireendtime - time);
1240 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1241 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);