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/instagib/sv_instagib.qh>
13 #include <common/mutators/mutator/waypoints/waypointsprites.qh>
14 #include <common/notifications/all.qh>
15 #include <common/physics/movetypes/movetypes.qh>
16 #include <common/physics/player.qh>
17 #include <common/playerstats.qh>
18 #include <common/state.qh>
19 #include <common/teams.qh>
20 #include <common/util.qh>
21 #include <common/vehicles/all.qh>
22 #include <common/weapons/_all.qh>
23 #include <lib/csqcmodel/sv_model.qh>
24 #include <lib/warpzone/common.qh>
25 #include <server/bot/api.qh>
26 #include <server/client.qh>
27 #include <server/gamelog.qh>
28 #include <server/hook.qh>
29 #include <server/items/items.qh>
30 #include <server/main.qh>
31 #include <server/mutators/_mod.qh>
32 #include <server/resources.qh>
33 #include <server/scores.qh>
34 #include <server/spawnpoints.qh>
35 #include <server/teamplay.qh>
36 #include <server/weapons/accuracy.qh>
37 #include <server/weapons/csqcprojectile.qh>
38 #include <server/weapons/selection.qh>
39 #include <server/world.qh>
41 void UpdateFrags(entity player, int f)
43 GameRules_scoring_add_team(player, SCORE, f);
46 void GiveFrags(entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
48 // TODO route through PlayerScores instead
49 if(game_stopped) return;
56 GameRules_scoring_add(attacker, SUICIDES, 1);
61 GameRules_scoring_add(attacker, TEAMKILLS, 1);
67 GameRules_scoring_add(attacker, KILLS, 1);
68 if(!warmup_stage && targ.playerid)
69 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
72 GameRules_scoring_add(targ, DEATHS, 1);
74 // FIXME fix the mess this is (we have REAL points now!)
75 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
78 attacker.totalfrags += f;
81 UpdateFrags(attacker, f);
84 string AppendItemcodes(string s, entity player)
86 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
88 .entity weaponentity = weaponentities[slot];
89 int w = player.(weaponentity).m_weapon.m_id;
91 w = player.(weaponentity).cnt; // previous weapon
92 if(w != 0 || slot == 0)
93 s = strcat(s, ftos(w));
95 if(time < STAT(STRENGTH_FINISHED, player))
97 if(time < STAT(INVINCIBLE_FINISHED, player))
99 if(PHYS_INPUT_BUTTON_CHAT(player))
101 // TODO: include these codes as a flag on the item itself
102 MUTATOR_CALLHOOK(LogDeath_AppendItemCodes, player, s);
103 s = M_ARGV(1, string);
107 void LogDeath(string mode, int deathtype, entity killer, entity killed)
110 if(!autocvar_sv_eventlog)
112 s = strcat(":kill:", mode);
113 s = strcat(s, ":", ftos(killer.playerid));
114 s = strcat(s, ":", ftos(killed.playerid));
115 s = strcat(s, ":type=", Deathtype_Name(deathtype));
116 s = strcat(s, ":items=");
117 s = AppendItemcodes(s, killer);
120 s = strcat(s, ":victimitems=");
121 s = AppendItemcodes(s, killed);
126 void Obituary_SpecialDeath(
130 string s1, string s2, string s3,
131 float f1, float f2, float f3)
133 if(!DEATH_ISSPECIAL(deathtype))
135 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
139 entity deathent = REGISTRY_GET(Deathtypes, deathtype - DT_FIRST);
142 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
146 if(g_cts && deathtype == DEATH_KILL.m_id)
147 return; // TODO: somehow put this in CTS gamemode file!
149 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
152 Send_Notification_WOCOVA(
160 Send_Notification_WOCOVA(
164 death_message.nent_msginfo,
171 float Obituary_WeaponDeath(
175 string s1, string s2, string s3,
178 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
179 if (death_weapon == WEP_Null)
182 w_deathtype = deathtype;
183 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
188 Send_Notification_WOCOVA(
196 // send the info part to everyone
197 Send_Notification_WOCOVA(
201 death_message.nent_msginfo,
209 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %s!\n",
218 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target, string attacker_name)
220 if(deathtype == DEATH_FIRE.m_id)
222 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
223 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));
227 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
230 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
233 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
236 float notif_firstblood = false;
237 float kill_count_to_attacker, kill_count_to_target;
238 bool notif_anonymous = false;
239 string attacker_name = attacker.netname;
241 // Set final information for the death
242 targ.death_origin = targ.origin;
243 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
245 // Abort now if a mutator requests it
246 if (MUTATOR_CALLHOOK(ClientObituary, inflictor, attacker, targ, deathtype, attacker.(weaponentity))) { CS(targ).killcount = 0; return; }
247 notif_anonymous = M_ARGV(5, bool);
250 attacker_name = "Anonymous player";
252 #ifdef NOTIFICATIONS_DEBUG
255 "Obituary(%s, %s, %s, %s = %d);\n",
259 Deathtype_Name(deathtype),
270 if(DEATH_ISSPECIAL(deathtype))
272 if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
274 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
278 switch(DEATH_ENT(deathtype))
280 case DEATH_MIRRORDAMAGE:
282 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
288 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
294 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
296 backtrace("SUICIDE: what the hell happened here?\n");
299 LogDeath("suicide", deathtype, targ, targ);
300 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
301 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
307 else if(IS_PLAYER(attacker))
309 if(SAME_TEAM(attacker, targ))
311 LogDeath("tk", deathtype, attacker, targ);
312 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
314 CS(attacker).killcount = 0;
316 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
317 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker_name);
318 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker_name, deathlocation, CS(targ).killcount);
320 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
321 // No need for specific death/weapon messages...
325 LogDeath("frag", deathtype, attacker, targ);
326 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
328 CS(attacker).taunt_soundtime = time + 1;
329 CS(attacker).killcount = CS(attacker).killcount + 1;
331 attacker.killsound += 1;
333 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
334 // these 2 macros are spread over multiple files
335 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
337 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
339 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
342 switch(CS(attacker).killcount)
349 if(!warmup_stage && !checkrules_firstblood)
351 checkrules_firstblood = true;
352 notif_firstblood = true; // modify the current messages so that they too show firstblood information
353 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
354 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
356 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
357 kill_count_to_attacker = -1;
358 kill_count_to_target = -2;
362 kill_count_to_attacker = CS(attacker).killcount;
363 kill_count_to_target = 0;
374 kill_count_to_attacker,
375 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
383 kill_count_to_target,
384 GetResource(attacker, RES_HEALTH),
385 GetResource(attacker, RES_ARMOR),
386 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
389 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target, attacker_name))
397 kill_count_to_attacker,
398 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
406 kill_count_to_target,
407 GetResource(attacker, RES_HEALTH),
408 GetResource(attacker, RES_ARMOR),
409 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
414 if(deathtype == DEATH_BUFF.m_id)
415 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
417 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker))
418 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
427 switch(DEATH_ENT(deathtype))
429 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
430 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
431 // and there will be a REAL DEATH_VOID implementation which mappers will use.
432 case DEATH_HURTTRIGGER:
434 Obituary_SpecialDeath(targ, false, deathtype,
446 Obituary_SpecialDeath(targ, false, deathtype,
448 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
458 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
463 LogDeath("accident", deathtype, targ, targ);
464 GiveFrags(targ, targ, -1, deathtype, weaponentity);
466 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
468 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
471 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
476 // reset target kill count
477 CS(targ).killcount = 0;
480 void Ice_Think(entity this)
482 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
487 vector ice_org = this.owner.origin - '0 0 16';
488 if (this.origin != ice_org)
489 setorigin(this, ice_org);
490 this.nextthink = time;
493 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
495 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
498 if(STAT(FROZEN, targ))
501 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
503 STAT(FROZEN, targ) = frozen_type;
504 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
505 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
506 targ.revive_speed = revivespeed;
508 IL_REMOVE(g_bot_targets, targ);
509 targ.bot_attack = false;
510 targ.freeze_time = time;
512 entity ice = new(ice);
514 ice.scale = targ.scale;
515 // set_movetype(ice, MOVETYPE_FOLLOW) would rotate the ice model with the player
516 setthink(ice, Ice_Think);
517 ice.nextthink = time;
518 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
519 setmodel(ice, MDL_ICE);
521 ice.colormod = Team_ColorRGB(targ.team);
522 ice.glowmod = ice.colormod;
524 targ.revival_time = 0;
528 RemoveGrapplingHooks(targ);
530 FOREACH_CLIENT(IS_PLAYER(it),
532 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
534 .entity weaponentity = weaponentities[slot];
535 if(it.(weaponentity).hook.aiment == targ)
536 RemoveHook(it.(weaponentity).hook);
541 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
542 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
545 void Unfreeze(entity targ, bool reset_health)
547 if(!STAT(FROZEN, targ))
550 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
551 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
553 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
555 STAT(FROZEN, targ) = 0;
556 STAT(REVIVE_PROGRESS, targ) = 0;
557 targ.revival_time = time;
559 IL_PUSH(g_bot_targets, targ);
560 targ.bot_attack = true;
562 WaypointSprite_Kill(targ.waypointsprite_attached);
564 FOREACH_CLIENT(IS_PLAYER(it),
566 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
568 .entity weaponentity = weaponentities[slot];
569 if(it.(weaponentity).hook.aiment == targ)
570 RemoveHook(it.(weaponentity).hook);
574 // remove the ice block
576 delete(targ.iceblock);
577 targ.iceblock = NULL;
579 MUTATOR_CALLHOOK(Unfreeze, targ);
582 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
584 float complainteamdamage = 0;
585 float mirrordamage = 0;
586 float mirrorforce = 0;
588 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
591 entity attacker_save = attacker;
593 // special rule: gravity bombs and sound-based attacks do not affect team mates (other than for disconnecting the hook)
594 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || (deathtype & HITTYPE_SOUND))
596 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
602 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
604 // exit the vehicle before killing (fixes a crash)
605 if(IS_PLAYER(targ) && targ.vehicle)
606 vehicles_exit(targ.vehicle, VHEF_RELEASE);
608 // These are ALWAYS lethal
609 // No damage modification here
610 // Instead, prepare the victim for his death...
611 SetResourceExplicit(targ, RES_ARMOR, 0);
612 targ.spawnshieldtime = 0;
613 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
614 targ.flags -= targ.flags & FL_GODMODE;
617 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
623 // nullify damage if teamplay is on
624 if(deathtype != DEATH_TELEFRAG.m_id)
625 if(IS_PLAYER(attacker))
627 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
632 else if(SAME_TEAM(attacker, targ))
634 if(autocvar_teamplay_mode == 1)
636 else if(attacker != targ)
638 if(autocvar_teamplay_mode == 2)
640 if(IS_PLAYER(targ) && !IS_DEAD(targ))
642 attacker.dmg_team = attacker.dmg_team + damage;
643 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
646 else if(autocvar_teamplay_mode == 3)
648 else if(autocvar_teamplay_mode == 4)
650 if(IS_PLAYER(targ) && !IS_DEAD(targ))
652 attacker.dmg_team = attacker.dmg_team + damage;
653 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
654 if(complainteamdamage > 0)
655 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
656 mirrorforce = autocvar_g_mirrordamage * vlen(force);
657 damage = autocvar_g_friendlyfire * damage;
658 // mirrordamage will be used LATER
660 if(autocvar_g_mirrordamage_virtual)
662 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
663 attacker.dmg_take += v.x;
664 attacker.dmg_save += v.y;
665 attacker.dmg_inflictor = inflictor;
670 if(autocvar_g_friendlyfire_virtual)
672 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
673 targ.dmg_take += v.x;
674 targ.dmg_save += v.y;
675 targ.dmg_inflictor = inflictor;
677 if(!autocvar_g_friendlyfire_virtual_force)
681 else if(!targ.canteamdamage)
688 if (!DEATH_ISSPECIAL(deathtype))
690 damage *= autocvar_g_weapondamagefactor;
691 mirrordamage *= autocvar_g_weapondamagefactor;
692 complainteamdamage *= autocvar_g_weapondamagefactor;
693 force = force * autocvar_g_weaponforcefactor;
694 mirrorforce *= autocvar_g_weaponforcefactor;
697 // should this be changed at all? If so, in what way?
698 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
699 damage = M_ARGV(4, float);
700 mirrordamage = M_ARGV(5, float);
701 force = M_ARGV(6, vector);
703 if(IS_PLAYER(targ) && damage > 0 && attacker)
705 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
707 .entity went = weaponentities[slot];
708 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
709 RemoveHook(targ.(went).hook);
713 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
714 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
716 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
718 Unfreeze(targ, false);
719 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
720 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
721 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
722 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
726 force *= autocvar_g_frozen_force;
729 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
730 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
732 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
734 entity spot = SelectSpawnPoint(targ, false);
738 targ.deadflag = DEAD_NO;
740 targ.angles = spot.angles;
743 targ.effects |= EF_TELEPORT_BIT;
745 targ.angles_z = 0; // never spawn tilted even if the spot says to
746 targ.fixangle = true; // turn this way immediately
747 targ.velocity = '0 0 0';
748 targ.avelocity = '0 0 0';
749 targ.punchangle = '0 0 0';
750 targ.punchvector = '0 0 0';
751 targ.oldvelocity = targ.velocity;
753 targ.spawnorigin = spot.origin;
754 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
755 // don't reset back to last position, even if new position is stuck in solid
756 targ.oldorigin = targ.origin;
758 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
762 if(!MUTATOR_IS_ENABLED(mutator_instagib))
764 // apply strength multiplier
765 if (attacker.items & ITEM_Strength.m_itemid)
769 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
770 force = force * autocvar_g_balance_powerup_strength_selfforce;
774 damage = damage * autocvar_g_balance_powerup_strength_damage;
775 force = force * autocvar_g_balance_powerup_strength_force;
779 // apply invincibility multiplier
780 if (targ.items & ITEM_Shield.m_itemid)
782 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
783 if (targ != attacker)
785 force = force * autocvar_g_balance_powerup_invincible_takeforce;
790 if (targ == attacker)
791 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
796 if(deathtype != DEATH_BUFF.m_id)
797 if(targ.takedamage == DAMAGE_AIM)
801 if(IS_VEHICLE(targ) && targ.owner)
806 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
808 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
812 if(deathtype != DEATH_FIRE.m_id)
814 if(PHYS_INPUT_BUTTON_CHAT(victim))
815 attacker.typehitsound += 1;
817 attacker.damage_dealt += damage;
820 damage_goodhits += 1;
821 damage_gooddamage += damage;
823 if (!DEATH_ISSPECIAL(deathtype))
825 if(IS_PLAYER(targ)) // don't do this for vehicles
831 else if(IS_PLAYER(attacker))
833 // if enemy gets frozen in this frame and receives other damage don't
834 // play the typehitsound e.g. when hit by multiple bullets of the shotgun
835 if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
837 attacker.typehitsound += 1;
839 if(complainteamdamage > 0)
840 if(time > CS(attacker).teamkill_complain)
842 CS(attacker).teamkill_complain = time + 5;
843 CS(attacker).teamkill_soundtime = time + 0.4;
844 CS(attacker).teamkill_soundsource = targ;
852 if (targ.damageforcescale)
854 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
856 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
857 if(targ.move_movetype == MOVETYPE_PHYSICS)
859 entity farcent = new(farce);
860 farcent.enemy = targ;
861 farcent.movedir = farce * 10;
863 farcent.movedir = farcent.movedir * targ.mass;
864 farcent.origin = hitloc;
865 farcent.forcetype = FORCETYPE_FORCEATPOS;
866 farcent.nextthink = time + 0.1;
867 setthink(farcent, SUB_Remove);
869 else if(targ.move_movetype != MOVETYPE_NOCLIP)
871 targ.velocity = targ.velocity + farce;
873 UNSET_ONGROUND(targ);
874 UpdateCSQCProjectile(targ);
877 if (damage != 0 || (targ.damageforcescale && force))
878 if (targ.event_damage)
879 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
881 // apply mirror damage if any
882 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
883 if(mirrordamage > 0 || mirrorforce > 0)
885 attacker = attacker_save;
887 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
888 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
892 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
893 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
894 // Returns total damage applies to creatures
898 float total_damage_to_creatures;
903 float stat_damagedone;
905 if(RadiusDamage_running)
907 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
911 RadiusDamage_running = 1;
913 tfloordmg = autocvar_g_throughfloor_damage;
914 tfloorforce = autocvar_g_throughfloor_force;
916 total_damage_to_creatures = 0;
918 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
919 if(!(deathtype & HITTYPE_SOUND)) // do not send radial sound damage (bandwidth hog)
921 force = inflictorvelocity;
925 force = normalize(force);
926 if(forceintensity >= 0)
927 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
929 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
934 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
938 if ((targ != inflictor) || inflictorselfdamage)
939 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
946 // LordHavoc: measure distance to nearest point on target (not origin)
947 // (this guarentees 100% damage on a touch impact)
948 nearest = targ.WarpZone_findradius_nearest;
949 diff = targ.WarpZone_findradius_dist;
950 // round up a little on the damage to ensure full damage on impacts
951 // and turn the distance into a fraction of the radius
952 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
954 //bprint(ftos(power));
955 //if (targ == attacker)
956 // print(ftos(power), "\n");
962 finaldmg = coredamage * power + edgedamage * (1 - power);
968 vector myblastorigin;
971 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
973 // if it's a player, use the view origin as reference
974 center = CENTER_OR_VIEWOFS(targ);
976 force = normalize(center - myblastorigin);
977 force = force * (finaldmg / coredamage) * forceintensity;
980 // apply special scaling along the z axis if set
981 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
983 force.z *= forcezscale;
985 if(targ != directhitentity)
990 float mininv_f, mininv_d;
992 // test line of sight to multiple positions on box,
993 // and do damage if any of them hit
996 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
997 // so for a given max stddev:
998 // n = (1 / (2 * max stddev of hitratio))^2
1000 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
1001 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
1003 if(autocvar_g_throughfloor_debug)
1004 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1007 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1009 if(autocvar_g_throughfloor_debug)
1010 LOG_INFOF(" steps=%f", total);
1013 if (IS_PLAYER(targ))
1014 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1016 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1018 if(autocvar_g_throughfloor_debug)
1019 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1021 for(c = 0; c < total; ++c)
1023 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1024 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1025 if (trace_fraction == 1 || trace_ent == targ)
1029 hitloc = hitloc + nearest;
1033 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1034 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1035 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1038 nearest = hitloc * (1 / max(1, hits));
1039 hitratio = (hits / total);
1040 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1041 finaldmg = finaldmg * a;
1042 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1045 if(autocvar_g_throughfloor_debug)
1046 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1049 //if (targ == attacker)
1051 // print("hits ", ftos(hits), " / ", ftos(total));
1052 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1053 // print(" (", ftos(a), ")\n");
1055 if(finaldmg || force)
1059 total_damage_to_creatures += finaldmg;
1061 if(accuracy_isgooddamage(attacker, targ))
1062 stat_damagedone += finaldmg;
1065 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1066 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1068 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1076 RadiusDamage_running = 0;
1078 if(!DEATH_ISSPECIAL(deathtype))
1079 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1081 return total_damage_to_creatures;
1084 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1086 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1087 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1090 bool Heal(entity targ, entity inflictor, float amount, float limit)
1092 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1095 bool healed = false;
1097 healed = targ.event_heal(targ, inflictor, amount, limit);
1098 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1099 // TODO: healing fx!
1100 // TODO: armor healing?
1104 float Fire_IsBurning(entity e)
1106 return (time < e.fire_endtime);
1109 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1112 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1123 // print("adding a fire burner to ", e.classname, "\n");
1124 e.fire_burner = new(fireburner);
1125 setthink(e.fire_burner, fireburner_think);
1126 e.fire_burner.nextthink = time;
1127 e.fire_burner.owner = e;
1133 if(Fire_IsBurning(e))
1135 mintime = e.fire_endtime - time;
1136 maxtime = max(mintime, t);
1138 mindps = e.fire_damagepersec;
1139 maxdps = max(mindps, dps);
1141 if(maxtime > mintime || maxdps > mindps)
1145 // damage we have right now
1146 mindamage = mindps * mintime;
1148 // damage we want to get
1149 maxdamage = mindamage + d;
1151 // but we can't exceed maxtime * maxdps!
1152 totaldamage = min(maxdamage, maxtime * maxdps);
1156 // totaldamage = min(mindamage + d, maxtime * maxdps)
1158 // totaldamage <= maxtime * maxdps
1159 // ==> totaldamage / maxdps <= maxtime.
1161 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1162 // >= min(mintime, maxtime)
1163 // ==> totaldamage / maxdps >= mintime.
1166 // how long do we damage then?
1167 // at least as long as before
1168 // but, never exceed maxdps
1169 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1173 // at most as long as maximum allowed
1174 // but, never below mindps
1175 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1177 // assuming t > mintime, dps > mindps:
1178 // we get d = t * dps = maxtime * maxdps
1179 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1180 // totaldamage / maxdps = maxtime
1181 // totaldamage / mindps > totaldamage / maxdps = maxtime
1183 // a) totaltime = max(mintime, maxtime) = maxtime
1184 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1186 // assuming t <= mintime:
1187 // we get maxtime = mintime
1188 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1189 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1191 // assuming dps <= mindps:
1192 // we get mindps = maxdps.
1193 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1194 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1195 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1197 e.fire_damagepersec = totaldamage / totaltime;
1198 e.fire_endtime = time + totaltime;
1199 if(totaldamage > 1.2 * mindamage)
1201 e.fire_deathtype = dt;
1202 if(e.fire_owner != o)
1205 e.fire_hitsound = false;
1208 if(accuracy_isgooddamage(o, e))
1209 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1210 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1217 e.fire_damagepersec = dps;
1218 e.fire_endtime = time + t;
1219 e.fire_deathtype = dt;
1221 e.fire_hitsound = false;
1222 if(accuracy_isgooddamage(o, e))
1223 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1228 void Fire_ApplyDamage(entity e)
1233 if (!Fire_IsBurning(e))
1236 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1237 if(IS_NOT_A_CLIENT(o))
1240 // water and slime stop fire
1242 if(e.watertype != CONTENT_LAVA)
1249 t = min(frametime, e.fire_endtime - time);
1250 d = e.fire_damagepersec * t;
1252 hi = e.fire_owner.damage_dealt;
1253 ty = e.fire_owner.typehitsound;
1254 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1255 if(e.fire_hitsound && e.fire_owner)
1257 e.fire_owner.damage_dealt = hi;
1258 e.fire_owner.typehitsound = ty;
1260 e.fire_hitsound = true;
1262 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1264 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1266 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1267 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1269 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1270 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1271 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1277 void Fire_ApplyEffect(entity e)
1279 if(Fire_IsBurning(e))
1280 e.effects |= EF_FLAME;
1282 e.effects &= ~EF_FLAME;
1285 void fireburner_think(entity this)
1287 // for players, this is done in the regular loop
1288 if(wasfreed(this.owner))
1293 Fire_ApplyEffect(this.owner);
1294 if(!Fire_IsBurning(this.owner))
1296 this.owner.fire_burner = NULL;
1300 Fire_ApplyDamage(this.owner);
1301 this.nextthink = time;