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/weapons/weaponsystem.qh>
40 #include <server/world.qh>
42 void UpdateFrags(entity player, int f)
44 GameRules_scoring_add_team(player, SCORE, f);
47 void GiveFrags(entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
49 // TODO route through PlayerScores instead
50 if(game_stopped) return;
57 GameRules_scoring_add(attacker, SUICIDES, 1);
62 GameRules_scoring_add(attacker, TEAMKILLS, 1);
68 GameRules_scoring_add(attacker, KILLS, 1);
69 if(!warmup_stage && targ.playerid)
70 PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
73 GameRules_scoring_add(targ, DEATHS, 1);
75 // FIXME fix the mess this is (we have REAL points now!)
76 if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
79 attacker.totalfrags += f;
82 UpdateFrags(attacker, f);
85 string AppendItemcodes(string s, entity player)
87 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
89 .entity weaponentity = weaponentities[slot];
90 int w = player.(weaponentity).m_weapon.m_id;
92 w = player.(weaponentity).cnt; // previous weapon
93 if(w != 0 || slot == 0)
94 s = strcat(s, ftos(w));
96 if(time < STAT(STRENGTH_FINISHED, player))
98 if(time < STAT(INVINCIBLE_FINISHED, player))
100 if(PHYS_INPUT_BUTTON_CHAT(player))
102 // TODO: include these codes as a flag on the item itself
103 MUTATOR_CALLHOOK(LogDeath_AppendItemCodes, player, s);
104 s = M_ARGV(1, string);
108 void LogDeath(string mode, int deathtype, entity killer, entity killed)
111 if(!autocvar_sv_eventlog)
113 s = strcat(":kill:", mode);
114 s = strcat(s, ":", ftos(killer.playerid));
115 s = strcat(s, ":", ftos(killed.playerid));
116 s = strcat(s, ":type=", Deathtype_Name(deathtype));
117 s = strcat(s, ":items=");
118 s = AppendItemcodes(s, killer);
121 s = strcat(s, ":victimitems=");
122 s = AppendItemcodes(s, killed);
127 void Obituary_SpecialDeath(
131 string s1, string s2, string s3,
132 float f1, float f2, float f3)
134 if(!DEATH_ISSPECIAL(deathtype))
136 backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
140 entity deathent = REGISTRY_GET(Deathtypes, deathtype - DT_FIRST);
143 backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
147 if(g_cts && deathtype == DEATH_KILL.m_id)
148 return; // TODO: somehow put this in CTS gamemode file!
150 Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
153 Send_Notification_WOCOVA(
161 Send_Notification_WOCOVA(
165 death_message.nent_msginfo,
172 float Obituary_WeaponDeath(
176 string s1, string s2, string s3,
179 Weapon death_weapon = DEATH_WEAPONOF(deathtype);
180 if (death_weapon == WEP_Null)
183 w_deathtype = deathtype;
184 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
189 Send_Notification_WOCOVA(
197 // send the info part to everyone
198 Send_Notification_WOCOVA(
202 death_message.nent_msginfo,
210 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %s!\n",
219 bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target, string attacker_name)
221 if(deathtype == DEATH_FIRE.m_id)
223 Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
224 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));
228 return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
231 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
234 if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
237 float notif_firstblood = false;
238 float kill_count_to_attacker, kill_count_to_target;
239 bool notif_anonymous = false;
240 string attacker_name = attacker.netname;
242 // Set final information for the death
243 targ.death_origin = targ.origin;
244 string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
246 // Abort now if a mutator requests it
247 if (MUTATOR_CALLHOOK(ClientObituary, inflictor, attacker, targ, deathtype, attacker.(weaponentity))) { CS(targ).killcount = 0; return; }
248 notif_anonymous = M_ARGV(5, bool);
251 attacker_name = "Anonymous player";
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);
289 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
295 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
297 backtrace("SUICIDE: what the hell happened here?\n");
300 LogDeath("suicide", deathtype, targ, targ);
301 if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
302 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
308 else if(IS_PLAYER(attacker))
310 if(SAME_TEAM(attacker, targ))
312 LogDeath("tk", deathtype, attacker, targ);
313 GiveFrags(attacker, targ, -1, deathtype, weaponentity);
315 CS(attacker).killcount = 0;
317 Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
318 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker_name);
319 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker_name, deathlocation, CS(targ).killcount);
321 // In this case, the death message will ALWAYS be "foo was betrayed by bar"
322 // No need for specific death/weapon messages...
326 LogDeath("frag", deathtype, attacker, targ);
327 GiveFrags(attacker, targ, 1, deathtype, weaponentity);
329 CS(attacker).taunt_soundtime = time + 1;
330 CS(attacker).killcount = CS(attacker).killcount + 1;
332 attacker.killsound += 1;
334 // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
335 // these 2 macros are spread over multiple files
336 #define SPREE_ITEM(counta,countb,center,normal,gentle) \
338 Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
340 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
343 switch(CS(attacker).killcount)
350 if(!warmup_stage && !checkrules_firstblood)
352 checkrules_firstblood = true;
353 notif_firstblood = true; // modify the current messages so that they too show firstblood information
354 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
355 PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
357 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
358 kill_count_to_attacker = -1;
359 kill_count_to_target = -2;
363 kill_count_to_attacker = CS(attacker).killcount;
364 kill_count_to_target = 0;
375 kill_count_to_attacker,
376 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
384 kill_count_to_target,
385 GetResource(attacker, RES_HEALTH),
386 GetResource(attacker, RES_ARMOR),
387 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
390 else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target, attacker_name))
398 kill_count_to_attacker,
399 (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
407 kill_count_to_target,
408 GetResource(attacker, RES_HEALTH),
409 GetResource(attacker, RES_ARMOR),
410 (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
415 if(deathtype == DEATH_BUFF.m_id)
416 f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
418 if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker))
419 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
428 switch(DEATH_ENT(deathtype))
430 // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
431 // Later on you will only be able to make custom messages using DEATH_CUSTOM,
432 // and there will be a REAL DEATH_VOID implementation which mappers will use.
433 case DEATH_HURTTRIGGER:
435 Obituary_SpecialDeath(targ, false, deathtype,
447 Obituary_SpecialDeath(targ, false, deathtype,
449 ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
459 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
464 LogDeath("accident", deathtype, targ, targ);
465 GiveFrags(targ, targ, -1, deathtype, weaponentity);
467 if(GameRules_scoring_add(targ, SCORE, 0) == -5)
469 Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
472 PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
477 // reset target kill count
478 CS(targ).killcount = 0;
481 void Ice_Think(entity this)
483 if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
488 vector ice_org = this.owner.origin - '0 0 16';
489 if (this.origin != ice_org)
490 setorigin(this, ice_org);
491 this.nextthink = time;
494 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
496 if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
499 if(STAT(FROZEN, targ))
502 float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
504 STAT(FROZEN, targ) = frozen_type;
505 STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
506 SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
507 targ.revive_speed = revivespeed;
509 IL_REMOVE(g_bot_targets, targ);
510 targ.bot_attack = false;
511 targ.freeze_time = time;
513 entity ice = new(ice);
515 ice.scale = targ.scale;
516 // set_movetype(ice, MOVETYPE_FOLLOW) would rotate the ice model with the player
517 setthink(ice, Ice_Think);
518 ice.nextthink = time;
519 ice.frame = floor(random() * 21); // ice model has 20 different looking frames
520 setmodel(ice, MDL_ICE);
522 ice.colormod = Team_ColorRGB(targ.team);
523 ice.glowmod = ice.colormod;
525 targ.revival_time = 0;
529 RemoveGrapplingHooks(targ);
531 FOREACH_CLIENT(IS_PLAYER(it),
533 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
535 .entity weaponentity = weaponentities[slot];
536 if(it.(weaponentity).hook.aiment == targ)
537 RemoveHook(it.(weaponentity).hook);
542 if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
543 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
546 void Unfreeze(entity targ, bool reset_health)
548 if(!STAT(FROZEN, targ))
551 if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
552 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
554 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
556 STAT(FROZEN, targ) = 0;
557 STAT(REVIVE_PROGRESS, targ) = 0;
558 targ.revival_time = time;
560 IL_PUSH(g_bot_targets, targ);
561 targ.bot_attack = true;
563 WaypointSprite_Kill(targ.waypointsprite_attached);
565 FOREACH_CLIENT(IS_PLAYER(it),
567 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
569 .entity weaponentity = weaponentities[slot];
570 if(it.(weaponentity).hook.aiment == targ)
571 RemoveHook(it.(weaponentity).hook);
575 // remove the ice block
577 delete(targ.iceblock);
578 targ.iceblock = NULL;
580 MUTATOR_CALLHOOK(Unfreeze, targ);
583 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
585 float complainteamdamage = 0;
586 float mirrordamage = 0;
587 float mirrorforce = 0;
589 if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
592 entity attacker_save = attacker;
594 // special rule: gravity bombs and sound-based attacks do not affect team mates (other than for disconnecting the hook)
595 if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || (deathtype & HITTYPE_SOUND))
597 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
603 if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
605 // exit the vehicle before killing (fixes a crash)
606 if(IS_PLAYER(targ) && targ.vehicle)
607 vehicles_exit(targ.vehicle, VHEF_RELEASE);
609 // These are ALWAYS lethal
610 // No damage modification here
611 // Instead, prepare the victim for his death...
612 SetResourceExplicit(targ, RES_ARMOR, 0);
613 targ.spawnshieldtime = 0;
614 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
615 targ.flags -= targ.flags & FL_GODMODE;
618 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
624 // nullify damage if teamplay is on
625 if(deathtype != DEATH_TELEFRAG.m_id)
626 if(IS_PLAYER(attacker))
628 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
633 else if(SAME_TEAM(attacker, targ))
635 if(autocvar_teamplay_mode == 1)
637 else if(attacker != targ)
639 if(autocvar_teamplay_mode == 2)
641 if(IS_PLAYER(targ) && !IS_DEAD(targ))
643 attacker.dmg_team = attacker.dmg_team + damage;
644 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
647 else if(autocvar_teamplay_mode == 3)
649 else if(autocvar_teamplay_mode == 4)
651 if(IS_PLAYER(targ) && !IS_DEAD(targ))
653 attacker.dmg_team = attacker.dmg_team + damage;
654 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
655 if(complainteamdamage > 0)
656 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
657 mirrorforce = autocvar_g_mirrordamage * vlen(force);
658 damage = autocvar_g_friendlyfire * damage;
659 // mirrordamage will be used LATER
661 if(autocvar_g_mirrordamage_virtual)
663 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
664 attacker.dmg_take += v.x;
665 attacker.dmg_save += v.y;
666 attacker.dmg_inflictor = inflictor;
671 if(autocvar_g_friendlyfire_virtual)
673 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
674 targ.dmg_take += v.x;
675 targ.dmg_save += v.y;
676 targ.dmg_inflictor = inflictor;
678 if(!autocvar_g_friendlyfire_virtual_force)
682 else if(!targ.canteamdamage)
689 if (!DEATH_ISSPECIAL(deathtype))
691 damage *= autocvar_g_weapondamagefactor;
692 mirrordamage *= autocvar_g_weapondamagefactor;
693 complainteamdamage *= autocvar_g_weapondamagefactor;
694 force = force * autocvar_g_weaponforcefactor;
695 mirrorforce *= autocvar_g_weaponforcefactor;
698 // should this be changed at all? If so, in what way?
699 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
700 damage = M_ARGV(4, float);
701 mirrordamage = M_ARGV(5, float);
702 force = M_ARGV(6, vector);
704 if(IS_PLAYER(targ) && damage > 0 && attacker)
706 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
708 .entity went = weaponentities[slot];
709 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
710 RemoveHook(targ.(went).hook);
714 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
715 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
717 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
719 Unfreeze(targ, false);
720 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
721 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
722 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
723 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
727 force *= autocvar_g_frozen_force;
730 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
731 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
733 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
735 entity spot = SelectSpawnPoint(targ, false);
739 targ.deadflag = DEAD_NO;
741 targ.angles = spot.angles;
744 targ.effects |= EF_TELEPORT_BIT;
746 targ.angles_z = 0; // never spawn tilted even if the spot says to
747 targ.fixangle = true; // turn this way immediately
748 targ.velocity = '0 0 0';
749 targ.avelocity = '0 0 0';
750 targ.punchangle = '0 0 0';
751 targ.punchvector = '0 0 0';
752 targ.oldvelocity = targ.velocity;
754 targ.spawnorigin = spot.origin;
755 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
756 // don't reset back to last position, even if new position is stuck in solid
757 targ.oldorigin = targ.origin;
759 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
763 if(!MUTATOR_IS_ENABLED(mutator_instagib))
765 // apply strength multiplier
766 if (attacker.items & ITEM_Strength.m_itemid)
770 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
771 force = force * autocvar_g_balance_powerup_strength_selfforce;
775 damage = damage * autocvar_g_balance_powerup_strength_damage;
776 force = force * autocvar_g_balance_powerup_strength_force;
780 // apply invincibility multiplier
781 if (targ.items & ITEM_Shield.m_itemid)
783 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
784 if (targ != attacker)
786 force = force * autocvar_g_balance_powerup_invincible_takeforce;
791 if (targ == attacker)
792 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
797 if(deathtype != DEATH_BUFF.m_id)
798 if(targ.takedamage == DAMAGE_AIM)
802 if(IS_VEHICLE(targ) && targ.owner)
807 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
809 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
813 if(deathtype != DEATH_FIRE.m_id)
815 if(PHYS_INPUT_BUTTON_CHAT(victim))
816 attacker.typehitsound += 1;
818 attacker.damage_dealt += damage;
821 damage_goodhits += 1;
822 damage_gooddamage += damage;
824 if (!DEATH_ISSPECIAL(deathtype))
826 if(IS_PLAYER(targ)) // don't do this for vehicles
832 else if(IS_PLAYER(attacker))
834 // if enemy gets frozen in this frame and receives other damage don't
835 // play the typehitsound e.g. when hit by multiple bullets of the shotgun
836 if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
838 attacker.typehitsound += 1;
840 if(complainteamdamage > 0)
841 if(time > CS(attacker).teamkill_complain)
843 CS(attacker).teamkill_complain = time + 5;
844 CS(attacker).teamkill_soundtime = time + 0.4;
845 CS(attacker).teamkill_soundsource = targ;
853 if (targ.damageforcescale)
855 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
857 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
858 if(targ.move_movetype == MOVETYPE_PHYSICS)
860 entity farcent = new(farce);
861 farcent.enemy = targ;
862 farcent.movedir = farce * 10;
864 farcent.movedir = farcent.movedir * targ.mass;
865 farcent.origin = hitloc;
866 farcent.forcetype = FORCETYPE_FORCEATPOS;
867 farcent.nextthink = time + 0.1;
868 setthink(farcent, SUB_Remove);
870 else if(targ.move_movetype != MOVETYPE_NOCLIP)
872 targ.velocity = targ.velocity + farce;
874 UNSET_ONGROUND(targ);
875 UpdateCSQCProjectile(targ);
878 if (damage != 0 || (targ.damageforcescale && force))
879 if (targ.event_damage)
880 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
882 // apply mirror damage if any
883 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
884 if(mirrordamage > 0 || mirrorforce > 0)
886 attacker = attacker_save;
888 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
889 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
893 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
894 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
895 // Returns total damage applies to creatures
899 float total_damage_to_creatures;
904 float stat_damagedone;
906 if(RadiusDamage_running)
908 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
912 RadiusDamage_running = 1;
914 tfloordmg = autocvar_g_throughfloor_damage;
915 tfloorforce = autocvar_g_throughfloor_force;
917 total_damage_to_creatures = 0;
919 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
920 if(!(deathtype & HITTYPE_SOUND)) // do not send radial sound damage (bandwidth hog)
922 force = inflictorvelocity;
926 force = normalize(force);
927 if(forceintensity >= 0)
928 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
930 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
935 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
939 if ((targ != inflictor) || inflictorselfdamage)
940 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
947 // LordHavoc: measure distance to nearest point on target (not origin)
948 // (this guarentees 100% damage on a touch impact)
949 nearest = targ.WarpZone_findradius_nearest;
950 diff = targ.WarpZone_findradius_dist;
951 // round up a little on the damage to ensure full damage on impacts
952 // and turn the distance into a fraction of the radius
953 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
955 //bprint(ftos(power));
956 //if (targ == attacker)
957 // print(ftos(power), "\n");
963 finaldmg = coredamage * power + edgedamage * (1 - power);
969 vector myblastorigin;
972 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
974 // if it's a player, use the view origin as reference
975 center = CENTER_OR_VIEWOFS(targ);
977 force = normalize(center - myblastorigin);
978 force = force * (finaldmg / coredamage) * forceintensity;
981 // apply special scaling along the z axis if set
982 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
984 force.z *= forcezscale;
986 if(targ != directhitentity)
991 float mininv_f, mininv_d;
993 // test line of sight to multiple positions on box,
994 // and do damage if any of them hit
997 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
998 // so for a given max stddev:
999 // n = (1 / (2 * max stddev of hitratio))^2
1001 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
1002 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
1004 if(autocvar_g_throughfloor_debug)
1005 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1008 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1010 if(autocvar_g_throughfloor_debug)
1011 LOG_INFOF(" steps=%f", total);
1014 if (IS_PLAYER(targ))
1015 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1017 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1019 if(autocvar_g_throughfloor_debug)
1020 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1022 for(c = 0; c < total; ++c)
1024 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1025 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1026 if (trace_fraction == 1 || trace_ent == targ)
1030 hitloc = hitloc + nearest;
1034 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1035 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1036 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1039 nearest = hitloc * (1 / max(1, hits));
1040 hitratio = (hits / total);
1041 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1042 finaldmg = finaldmg * a;
1043 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1046 if(autocvar_g_throughfloor_debug)
1047 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1050 //if (targ == attacker)
1052 // print("hits ", ftos(hits), " / ", ftos(total));
1053 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1054 // print(" (", ftos(a), ")\n");
1056 if(finaldmg || force)
1060 total_damage_to_creatures += finaldmg;
1062 if(accuracy_isgooddamage(attacker, targ))
1063 stat_damagedone += finaldmg;
1066 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1067 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1069 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1077 RadiusDamage_running = 0;
1079 if(!DEATH_ISSPECIAL(deathtype))
1080 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1082 return total_damage_to_creatures;
1085 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1087 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1088 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1091 bool Heal(entity targ, entity inflictor, float amount, float limit)
1093 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1096 bool healed = false;
1098 healed = targ.event_heal(targ, inflictor, amount, limit);
1099 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1100 // TODO: healing fx!
1101 // TODO: armor healing?
1105 float Fire_IsBurning(entity e)
1107 return (time < e.fire_endtime);
1110 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1113 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1124 // print("adding a fire burner to ", e.classname, "\n");
1125 e.fire_burner = new(fireburner);
1126 setthink(e.fire_burner, fireburner_think);
1127 e.fire_burner.nextthink = time;
1128 e.fire_burner.owner = e;
1134 if(Fire_IsBurning(e))
1136 mintime = e.fire_endtime - time;
1137 maxtime = max(mintime, t);
1139 mindps = e.fire_damagepersec;
1140 maxdps = max(mindps, dps);
1142 if(maxtime > mintime || maxdps > mindps)
1146 // damage we have right now
1147 mindamage = mindps * mintime;
1149 // damage we want to get
1150 maxdamage = mindamage + d;
1152 // but we can't exceed maxtime * maxdps!
1153 totaldamage = min(maxdamage, maxtime * maxdps);
1157 // totaldamage = min(mindamage + d, maxtime * maxdps)
1159 // totaldamage <= maxtime * maxdps
1160 // ==> totaldamage / maxdps <= maxtime.
1162 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1163 // >= min(mintime, maxtime)
1164 // ==> totaldamage / maxdps >= mintime.
1167 // how long do we damage then?
1168 // at least as long as before
1169 // but, never exceed maxdps
1170 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1174 // at most as long as maximum allowed
1175 // but, never below mindps
1176 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1178 // assuming t > mintime, dps > mindps:
1179 // we get d = t * dps = maxtime * maxdps
1180 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1181 // totaldamage / maxdps = maxtime
1182 // totaldamage / mindps > totaldamage / maxdps = maxtime
1184 // a) totaltime = max(mintime, maxtime) = maxtime
1185 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1187 // assuming t <= mintime:
1188 // we get maxtime = mintime
1189 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1190 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1192 // assuming dps <= mindps:
1193 // we get mindps = maxdps.
1194 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1195 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1196 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1198 e.fire_damagepersec = totaldamage / totaltime;
1199 e.fire_endtime = time + totaltime;
1200 if(totaldamage > 1.2 * mindamage)
1202 e.fire_deathtype = dt;
1203 if(e.fire_owner != o)
1206 e.fire_hitsound = false;
1209 if(accuracy_isgooddamage(o, e))
1210 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1211 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1218 e.fire_damagepersec = dps;
1219 e.fire_endtime = time + t;
1220 e.fire_deathtype = dt;
1222 e.fire_hitsound = false;
1223 if(accuracy_isgooddamage(o, e))
1224 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1229 void Fire_ApplyDamage(entity e)
1234 if (!Fire_IsBurning(e))
1237 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1238 if(IS_NOT_A_CLIENT(o))
1241 // water and slime stop fire
1243 if(e.watertype != CONTENT_LAVA)
1250 t = min(frametime, e.fire_endtime - time);
1251 d = e.fire_damagepersec * t;
1253 hi = e.fire_owner.damage_dealt;
1254 ty = e.fire_owner.typehitsound;
1255 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1256 if(e.fire_hitsound && e.fire_owner)
1258 e.fire_owner.damage_dealt = hi;
1259 e.fire_owner.typehitsound = ty;
1261 e.fire_hitsound = true;
1263 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1265 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1267 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1268 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1270 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1271 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1272 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1278 void Fire_ApplyEffect(entity e)
1280 if(Fire_IsBurning(e))
1281 e.effects |= EF_FLAME;
1283 e.effects &= ~EF_FLAME;
1286 void fireburner_think(entity this)
1288 // for players, this is done in the regular loop
1289 if(wasfreed(this.owner))
1294 Fire_ApplyEffect(this.owner);
1295 if(!Fire_IsBurning(this.owner))
1297 this.owner.fire_burner = NULL;
1301 Fire_ApplyDamage(this.owner);
1302 this.nextthink = time;