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);
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(STAT(BUFFS, 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 his 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 targ.spawnshieldtime = 0;
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 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
638 else if(!STAT(FROZEN, targ) && SAME_TEAM(attacker, targ))
640 if(autocvar_teamplay_mode == 1)
642 else if(attacker != targ)
644 if(autocvar_teamplay_mode == 2)
646 if(IS_PLAYER(targ) && !IS_DEAD(targ))
648 attacker.dmg_team = attacker.dmg_team + damage;
649 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
652 else if(autocvar_teamplay_mode == 3)
654 else if(autocvar_teamplay_mode == 4)
656 if(IS_PLAYER(targ) && !IS_DEAD(targ))
658 attacker.dmg_team = attacker.dmg_team + damage;
659 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
660 if(complainteamdamage > 0)
661 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
662 mirrorforce = autocvar_g_mirrordamage * vlen(force);
663 damage = autocvar_g_friendlyfire * damage;
664 // mirrordamage will be used LATER
666 if(autocvar_g_mirrordamage_virtual)
668 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
669 attacker.dmg_take += v.x;
670 attacker.dmg_save += v.y;
671 attacker.dmg_inflictor = inflictor;
676 if(autocvar_g_friendlyfire_virtual)
678 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
679 targ.dmg_take += v.x;
680 targ.dmg_save += v.y;
681 targ.dmg_inflictor = inflictor;
683 if(!autocvar_g_friendlyfire_virtual_force)
687 else if(!targ.canteamdamage)
694 if (!DEATH_ISSPECIAL(deathtype))
696 damage *= autocvar_g_weapondamagefactor;
697 mirrordamage *= autocvar_g_weapondamagefactor;
698 complainteamdamage *= autocvar_g_weapondamagefactor;
699 force = force * autocvar_g_weaponforcefactor;
700 mirrorforce *= autocvar_g_weaponforcefactor;
703 // should this be changed at all? If so, in what way?
704 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
705 damage = M_ARGV(4, float);
706 mirrordamage = M_ARGV(5, float);
707 force = M_ARGV(6, vector);
709 if(IS_PLAYER(targ) && damage > 0 && attacker)
711 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
713 .entity went = weaponentities[slot];
714 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
715 RemoveHook(targ.(went).hook);
719 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
720 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
722 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
724 Unfreeze(targ, false);
725 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
726 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
727 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
728 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
732 force *= autocvar_g_frozen_force;
735 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
736 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
738 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
740 entity spot = SelectSpawnPoint(targ, false);
744 targ.deadflag = DEAD_NO;
746 targ.angles = spot.angles;
749 targ.effects |= EF_TELEPORT_BIT;
751 targ.angles_z = 0; // never spawn tilted even if the spot says to
752 targ.fixangle = true; // turn this way immediately
753 targ.velocity = '0 0 0';
754 targ.avelocity = '0 0 0';
755 targ.punchangle = '0 0 0';
756 targ.punchvector = '0 0 0';
757 targ.oldvelocity = targ.velocity;
759 targ.spawnorigin = spot.origin;
760 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
761 // don't reset back to last position, even if new position is stuck in solid
762 targ.oldorigin = targ.origin;
764 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
768 if(!MUTATOR_IS_ENABLED(mutator_instagib))
770 // apply strength multiplier
771 if (attacker.items & ITEM_Strength.m_itemid)
775 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
776 force = force * autocvar_g_balance_powerup_strength_selfforce;
780 damage = damage * autocvar_g_balance_powerup_strength_damage;
781 force = force * autocvar_g_balance_powerup_strength_force;
785 // apply invincibility multiplier
786 if (targ.items & ITEM_Shield.m_itemid)
788 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
789 if (targ != attacker)
791 force = force * autocvar_g_balance_powerup_invincible_takeforce;
796 if (targ == attacker)
797 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
802 if(deathtype != DEATH_BUFF.m_id)
803 if(targ.takedamage == DAMAGE_AIM)
807 if(IS_VEHICLE(targ) && targ.owner)
812 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
814 if (DIFF_TEAM(victim, attacker))
818 if(deathtype != DEATH_FIRE.m_id)
820 if(PHYS_INPUT_BUTTON_CHAT(victim))
821 attacker.typehitsound += 1;
823 attacker.damage_dealt += damage;
826 impressive_hits += 1;
828 if (!DEATH_ISSPECIAL(deathtype))
830 if(IS_PLAYER(targ)) // don't do this for vehicles
836 else if (IS_PLAYER(attacker) && !STAT(FROZEN, victim)) // same team
838 if (deathtype != DEATH_FIRE.m_id)
840 attacker.typehitsound += 1;
842 if(complainteamdamage > 0)
843 if(time > CS(attacker).teamkill_complain)
845 CS(attacker).teamkill_complain = time + 5;
846 CS(attacker).teamkill_soundtime = time + 0.4;
847 CS(attacker).teamkill_soundsource = targ;
855 if (targ.damageforcescale)
857 if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
859 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
860 if(targ.move_movetype == MOVETYPE_PHYSICS)
862 entity farcent = new(farce);
863 farcent.enemy = targ;
864 farcent.movedir = farce * 10;
866 farcent.movedir = farcent.movedir * targ.mass;
867 farcent.origin = hitloc;
868 farcent.forcetype = FORCETYPE_FORCEATPOS;
869 farcent.nextthink = time + 0.1;
870 setthink(farcent, SUB_Remove);
872 else if(targ.move_movetype != MOVETYPE_NOCLIP)
874 targ.velocity = targ.velocity + farce;
876 UNSET_ONGROUND(targ);
877 UpdateCSQCProjectile(targ);
880 if (damage != 0 || (targ.damageforcescale && force))
881 if (targ.event_damage)
882 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
884 // apply mirror damage if any
885 if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
886 if(mirrordamage > 0 || mirrorforce > 0)
888 attacker = attacker_save;
890 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
891 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
895 // Returns total damage applies to creatures
896 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
897 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
901 float total_damage_to_creatures;
906 float stat_damagedone;
908 if(RadiusDamage_running)
910 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
914 RadiusDamage_running = 1;
916 tfloordmg = autocvar_g_throughfloor_damage;
917 tfloorforce = autocvar_g_throughfloor_force;
919 total_damage_to_creatures = 0;
921 if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
922 if(!(deathtype & HITTYPE_SOUND)) // do not send radial sound damage (bandwidth hog)
924 force = inflictorvelocity;
928 force = normalize(force);
929 if(forceintensity >= 0)
930 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
932 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
937 targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
941 if ((targ != inflictor) || inflictorselfdamage)
942 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
949 // LordHavoc: measure distance to nearest point on target (not origin)
950 // (this guarentees 100% damage on a touch impact)
951 nearest = targ.WarpZone_findradius_nearest;
952 diff = targ.WarpZone_findradius_dist;
953 // round up a little on the damage to ensure full damage on impacts
954 // and turn the distance into a fraction of the radius
955 power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
957 //bprint(ftos(power));
958 //if (targ == attacker)
959 // print(ftos(power), "\n");
965 finaldmg = coredamage * power + edgedamage * (1 - power);
971 vector myblastorigin;
974 myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
976 // if it's a player, use the view origin as reference
977 center = CENTER_OR_VIEWOFS(targ);
979 force = normalize(center - myblastorigin);
980 force = force * (finaldmg / coredamage) * forceintensity;
983 // apply special scaling along the z axis if set
984 // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
986 force.z *= forcezscale;
988 if(targ != directhitentity)
993 float mininv_f, mininv_d;
995 // test line of sight to multiple positions on box,
996 // and do damage if any of them hit
999 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
1000 // so for a given max stddev:
1001 // n = (1 / (2 * max stddev of hitratio))^2
1003 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
1004 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
1006 if(autocvar_g_throughfloor_debug)
1007 LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1010 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1012 if(autocvar_g_throughfloor_debug)
1013 LOG_INFOF(" steps=%f", total);
1016 if (IS_PLAYER(targ))
1017 total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1019 total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1021 if(autocvar_g_throughfloor_debug)
1022 LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1024 for(c = 0; c < total; ++c)
1026 //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1027 WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1028 if (trace_fraction == 1 || trace_ent == targ)
1032 hitloc = hitloc + nearest;
1036 nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1037 nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1038 nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1041 nearest = hitloc * (1 / max(1, hits));
1042 hitratio = (hits / total);
1043 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1044 finaldmg = finaldmg * a;
1045 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1048 if(autocvar_g_throughfloor_debug)
1049 LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1052 //if (targ == attacker)
1054 // print("hits ", ftos(hits), " / ", ftos(total));
1055 // print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1056 // print(" (", ftos(a), ")\n");
1058 if(finaldmg || force)
1062 total_damage_to_creatures += finaldmg;
1064 if(accuracy_isgooddamage(attacker, targ))
1065 stat_damagedone += finaldmg;
1068 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1069 Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1071 Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1079 RadiusDamage_running = 0;
1081 if(!DEATH_ISSPECIAL(deathtype))
1082 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1084 return total_damage_to_creatures;
1087 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1089 return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1090 cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1093 bool Heal(entity targ, entity inflictor, float amount, float limit)
1095 if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1098 bool healed = false;
1100 healed = targ.event_heal(targ, inflictor, amount, limit);
1101 // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1102 // TODO: healing fx!
1103 // TODO: armor healing?
1107 float Fire_IsBurning(entity e)
1109 return (time < e.fire_endtime);
1112 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1115 float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1126 // print("adding a fire burner to ", e.classname, "\n");
1127 e.fire_burner = new(fireburner);
1128 setthink(e.fire_burner, fireburner_think);
1129 e.fire_burner.nextthink = time;
1130 e.fire_burner.owner = e;
1136 if(Fire_IsBurning(e))
1138 mintime = e.fire_endtime - time;
1139 maxtime = max(mintime, t);
1141 mindps = e.fire_damagepersec;
1142 maxdps = max(mindps, dps);
1144 if(maxtime > mintime || maxdps > mindps)
1148 // damage we have right now
1149 mindamage = mindps * mintime;
1151 // damage we want to get
1152 maxdamage = mindamage + d;
1154 // but we can't exceed maxtime * maxdps!
1155 totaldamage = min(maxdamage, maxtime * maxdps);
1159 // totaldamage = min(mindamage + d, maxtime * maxdps)
1161 // totaldamage <= maxtime * maxdps
1162 // ==> totaldamage / maxdps <= maxtime.
1164 // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1165 // >= min(mintime, maxtime)
1166 // ==> totaldamage / maxdps >= mintime.
1169 // how long do we damage then?
1170 // at least as long as before
1171 // but, never exceed maxdps
1172 totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1176 // at most as long as maximum allowed
1177 // but, never below mindps
1178 totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1180 // assuming t > mintime, dps > mindps:
1181 // we get d = t * dps = maxtime * maxdps
1182 // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1183 // totaldamage / maxdps = maxtime
1184 // totaldamage / mindps > totaldamage / maxdps = maxtime
1186 // a) totaltime = max(mintime, maxtime) = maxtime
1187 // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1189 // assuming t <= mintime:
1190 // we get maxtime = mintime
1191 // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1192 // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1194 // assuming dps <= mindps:
1195 // we get mindps = maxdps.
1196 // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1197 // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1198 // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1200 e.fire_damagepersec = totaldamage / totaltime;
1201 e.fire_endtime = time + totaltime;
1202 if(totaldamage > 1.2 * mindamage)
1204 e.fire_deathtype = dt;
1205 if(e.fire_owner != o)
1208 e.fire_hitsound = false;
1211 if(accuracy_isgooddamage(o, e))
1212 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1213 return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1220 e.fire_damagepersec = dps;
1221 e.fire_endtime = time + t;
1222 e.fire_deathtype = dt;
1224 e.fire_hitsound = false;
1225 if(accuracy_isgooddamage(o, e))
1226 accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1231 void Fire_ApplyDamage(entity e)
1236 // water, slime and ice stop fire
1237 if (STAT(FROZEN, e) || (e.waterlevel && (e.watertype != CONTENT_LAVA)))
1240 if (!Fire_IsBurning(e))
1243 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1244 if(IS_NOT_A_CLIENT(o))
1247 t = min(frametime, e.fire_endtime - time);
1248 d = e.fire_damagepersec * t;
1250 hi = e.fire_owner.damage_dealt;
1251 ty = e.fire_owner.typehitsound;
1252 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1253 if(e.fire_hitsound && e.fire_owner)
1255 e.fire_owner.damage_dealt = hi;
1256 e.fire_owner.typehitsound = ty;
1258 e.fire_hitsound = true;
1260 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1262 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1264 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1265 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1267 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1268 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1269 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1275 void Fire_ApplyEffect(entity e)
1277 if(Fire_IsBurning(e))
1278 e.effects |= EF_FLAME;
1280 e.effects &= ~EF_FLAME;
1283 void fireburner_think(entity this)
1285 // for players, this is done in the regular loop
1286 if(wasfreed(this.owner))
1291 Fire_ApplyEffect(this.owner);
1292 if(!Fire_IsBurning(this.owner))
1294 this.owner.fire_burner = NULL;
1298 Fire_ApplyDamage(this.owner);
1299 this.nextthink = time;