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 SetResourceExplicit(targ, RES_ARMOR, 0);
615 targ.spawnshieldtime = 0;
616 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
617 targ.flags -= targ.flags & FL_GODMODE;
620 else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
626 // nullify damage if teamplay is on
627 if(deathtype != DEATH_TELEFRAG.m_id)
628 if(IS_PLAYER(attacker))
630 if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
635 else if(!STAT(FROZEN, targ) && SAME_TEAM(attacker, targ))
637 if(autocvar_teamplay_mode == 1)
639 else if(attacker != targ)
641 if(autocvar_teamplay_mode == 2)
643 if(IS_PLAYER(targ) && !IS_DEAD(targ))
645 attacker.dmg_team = attacker.dmg_team + damage;
646 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
649 else if(autocvar_teamplay_mode == 3)
651 else if(autocvar_teamplay_mode == 4)
653 if(IS_PLAYER(targ) && !IS_DEAD(targ))
655 attacker.dmg_team = attacker.dmg_team + damage;
656 complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
657 if(complainteamdamage > 0)
658 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
659 mirrorforce = autocvar_g_mirrordamage * vlen(force);
660 damage = autocvar_g_friendlyfire * damage;
661 // mirrordamage will be used LATER
663 if(autocvar_g_mirrordamage_virtual)
665 vector v = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
666 attacker.dmg_take += v.x;
667 attacker.dmg_save += v.y;
668 attacker.dmg_inflictor = inflictor;
673 if(autocvar_g_friendlyfire_virtual)
675 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
676 targ.dmg_take += v.x;
677 targ.dmg_save += v.y;
678 targ.dmg_inflictor = inflictor;
680 if(!autocvar_g_friendlyfire_virtual_force)
684 else if(!targ.canteamdamage)
691 if (!DEATH_ISSPECIAL(deathtype))
693 damage *= autocvar_g_weapondamagefactor;
694 mirrordamage *= autocvar_g_weapondamagefactor;
695 complainteamdamage *= autocvar_g_weapondamagefactor;
696 force = force * autocvar_g_weaponforcefactor;
697 mirrorforce *= autocvar_g_weaponforcefactor;
700 // should this be changed at all? If so, in what way?
701 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
702 damage = M_ARGV(4, float);
703 mirrordamage = M_ARGV(5, float);
704 force = M_ARGV(6, vector);
706 if(IS_PLAYER(targ) && damage > 0 && attacker)
708 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
710 .entity went = weaponentities[slot];
711 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
712 RemoveHook(targ.(went).hook);
716 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
717 && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
719 if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
721 Unfreeze(targ, false);
722 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
723 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
724 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
725 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
729 force *= autocvar_g_frozen_force;
732 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
733 && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
735 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
737 entity spot = SelectSpawnPoint(targ, false);
741 targ.deadflag = DEAD_NO;
743 targ.angles = spot.angles;
746 targ.effects |= EF_TELEPORT_BIT;
748 targ.angles_z = 0; // never spawn tilted even if the spot says to
749 targ.fixangle = true; // turn this way immediately
750 targ.velocity = '0 0 0';
751 targ.avelocity = '0 0 0';
752 targ.punchangle = '0 0 0';
753 targ.punchvector = '0 0 0';
754 targ.oldvelocity = targ.velocity;
756 targ.spawnorigin = spot.origin;
757 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
758 // don't reset back to last position, even if new position is stuck in solid
759 targ.oldorigin = targ.origin;
761 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
765 if(!MUTATOR_IS_ENABLED(mutator_instagib))
767 // apply strength multiplier
768 if (attacker.items & ITEM_Strength.m_itemid)
772 damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
773 force = force * autocvar_g_balance_powerup_strength_selfforce;
777 damage = damage * autocvar_g_balance_powerup_strength_damage;
778 force = force * autocvar_g_balance_powerup_strength_force;
782 // apply invincibility multiplier
783 if (targ.items & ITEM_Shield.m_itemid)
785 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
786 if (targ != attacker)
788 force = force * autocvar_g_balance_powerup_invincible_takeforce;
793 if (targ == attacker)
794 damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
799 if(deathtype != DEATH_BUFF.m_id)
800 if(targ.takedamage == DAMAGE_AIM)
804 if(IS_VEHICLE(targ) && targ.owner)
809 if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
811 if (DIFF_TEAM(victim, attacker))
815 if(deathtype != DEATH_FIRE.m_id)
817 if(PHYS_INPUT_BUTTON_CHAT(victim))
818 attacker.typehitsound += 1;
820 attacker.damage_dealt += damage;
823 impressive_hits += 1;
825 if (!DEATH_ISSPECIAL(deathtype))
827 if(IS_PLAYER(targ)) // don't do this for vehicles
833 else if (IS_PLAYER(attacker) && !STAT(FROZEN, victim)) // same team
835 if (deathtype != DEATH_FIRE.m_id)
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 // Returns total damage applies to creatures
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)
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 // water, slime and ice stop fire
1234 if (STAT(FROZEN, e) || (e.waterlevel && (e.watertype != CONTENT_LAVA)))
1237 if (!Fire_IsBurning(e))
1240 for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1241 if(IS_NOT_A_CLIENT(o))
1244 t = min(frametime, e.fire_endtime - time);
1245 d = e.fire_damagepersec * t;
1247 hi = e.fire_owner.damage_dealt;
1248 ty = e.fire_owner.typehitsound;
1249 Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1250 if(e.fire_hitsound && e.fire_owner)
1252 e.fire_owner.damage_dealt = hi;
1253 e.fire_owner.typehitsound = ty;
1255 e.fire_hitsound = true;
1257 if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1259 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1261 if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1262 if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1264 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1265 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1266 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1272 void Fire_ApplyEffect(entity e)
1274 if(Fire_IsBurning(e))
1275 e.effects |= EF_FLAME;
1277 e.effects &= ~EF_FLAME;
1280 void fireburner_think(entity this)
1282 // for players, this is done in the regular loop
1283 if(wasfreed(this.owner))
1288 Fire_ApplyEffect(this.owner);
1289 if(!Fire_IsBurning(this.owner))
1291 this.owner.fire_burner = NULL;
1295 Fire_ApplyDamage(this.owner);
1296 this.nextthink = time;