mirror of
https://github.com/ZDoom/gzdoom.git
synced 2025-02-15 08:41:33 +00:00
This was always used with 'consoleplayer' which really is the only thing making sense here. But this is a part of the global state which should be avoided in play code. In particular, this makes no real sense in case of secondary maps where it should always return false.
1893 lines
50 KiB
C++
1893 lines
50 KiB
C++
//-----------------------------------------------------------------------------
|
|
//
|
|
// Copyright 1993-1996 id Software
|
|
// Copyright 1994-1996 Raven Software
|
|
// Copyright 1999-2016 Randy Heit
|
|
// Copyright 2002-2016 Christoph Oelckers
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see http://www.gnu.org/licenses/
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
//
|
|
// DESCRIPTION:
|
|
// Handling interactions (i.e., collisions).
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// Data.
|
|
#include "doomdef.h"
|
|
#include "gstrings.h"
|
|
|
|
#include "doomstat.h"
|
|
|
|
#include "m_random.h"
|
|
#include "i_system.h"
|
|
#include "announcer.h"
|
|
|
|
#include "am_map.h"
|
|
|
|
#include "c_console.h"
|
|
#include "c_dispatch.h"
|
|
|
|
#include "p_local.h"
|
|
|
|
#include "p_lnspec.h"
|
|
#include "p_effect.h"
|
|
#include "p_acs.h"
|
|
|
|
#include "b_bot.h" //Added by MC:
|
|
|
|
#include "d_player.h"
|
|
#include "gi.h"
|
|
#include "sbar.h"
|
|
#include "d_net.h"
|
|
#include "d_netinf.h"
|
|
#include "a_morph.h"
|
|
#include "vm.h"
|
|
#include "g_levellocals.h"
|
|
#include "events.h"
|
|
#include "actorinlines.h"
|
|
|
|
static FRandom pr_botrespawn ("BotRespawn");
|
|
static FRandom pr_killmobj ("ActorDie");
|
|
FRandom pr_damagemobj ("ActorTakeDamage");
|
|
static FRandom pr_lightning ("LightningDamage");
|
|
static FRandom pr_poison ("PoisonDamage");
|
|
static FRandom pr_switcher ("SwitchTarget");
|
|
|
|
CVAR (Bool, cl_showsprees, true, CVAR_ARCHIVE)
|
|
CVAR (Bool, cl_showmultikills, true, CVAR_ARCHIVE)
|
|
EXTERN_CVAR (Bool, show_obituaries)
|
|
|
|
CVAR (Float, sv_damagefactormobj, 1.0, CVAR_SERVERINFO|CVAR_CHEAT)
|
|
CVAR (Float, sv_damagefactorfriendly, 1.0, CVAR_SERVERINFO|CVAR_CHEAT)
|
|
CVAR (Float, sv_damagefactorplayer, 1.0, CVAR_SERVERINFO|CVAR_CHEAT)
|
|
|
|
//
|
|
// GET STUFF
|
|
//
|
|
|
|
//
|
|
// P_TouchSpecialThing
|
|
//
|
|
void P_TouchSpecialThing (AActor *special, AActor *toucher)
|
|
{
|
|
double delta = special->Z() - toucher->Z();
|
|
|
|
// The pickup is at or above the toucher's feet OR
|
|
// The pickup is below the toucher.
|
|
if (delta > toucher->Height || delta < MIN(-32., -special->Height))
|
|
{ // out of reach
|
|
return;
|
|
}
|
|
|
|
// Dead thing touching.
|
|
// Can happen with a sliding player corpse.
|
|
if (toucher->health <= 0)
|
|
return;
|
|
|
|
//Added by MC: Finished with this destination.
|
|
if (toucher->player != NULL && toucher->player->Bot != NULL && special == toucher->player->Bot->dest)
|
|
{
|
|
toucher->player->Bot->prev = toucher->player->Bot->dest;
|
|
toucher->player->Bot->dest = nullptr;
|
|
}
|
|
special->CallTouch (toucher);
|
|
}
|
|
|
|
|
|
// [RH]
|
|
// PronounMessage: Replace parts of strings with player-specific pronouns
|
|
//
|
|
// The following expansions are performed:
|
|
// %g -> he/she/they/it
|
|
// %h -> him/her/them/it
|
|
// %p -> his/her/their/its
|
|
// %s -> his/hers/theirs/its
|
|
// %r -> he's/she's/they're/it's
|
|
// %o -> other (victim)
|
|
// %k -> killer
|
|
//
|
|
void PronounMessage (const char *from, char *to, int pronoun, const char *victim, const char *killer)
|
|
{
|
|
static const char *pronouns[GENDER_MAX][5] =
|
|
{
|
|
{ "he", "him", "his", "his", "he's" },
|
|
{ "she", "her", "her", "hers", "she's" },
|
|
{ "they", "them", "their", "theirs", "they're" },
|
|
{ "it", "it", "its", "its'", "it's" }
|
|
};
|
|
static const int pronounshift[GENDER_MAX][5] =
|
|
{
|
|
{ 2, 3, 3, 3, 4 },
|
|
{ 3, 3, 3, 4, 5 },
|
|
{ 4, 4, 5, 6, 7 },
|
|
{ 2, 2, 3, 4, 4 }
|
|
};
|
|
const char *substitute = NULL;
|
|
|
|
do
|
|
{
|
|
if (*from != '%')
|
|
{
|
|
*to++ = *from;
|
|
}
|
|
else
|
|
{
|
|
int grammarcase = -1;
|
|
|
|
switch (from[1])
|
|
{
|
|
case 'g': grammarcase = 0; break; // Subject
|
|
case 'h': grammarcase = 1; break; // Object
|
|
case 'p': grammarcase = 2; break; // Possessive Determiner
|
|
case 's': grammarcase = 3; break; // Possessive Pronoun
|
|
case 'r': grammarcase = 4; break; // Perfective
|
|
case 'o': substitute = victim; break;
|
|
case 'k': substitute = killer; break;
|
|
}
|
|
if (substitute != nullptr)
|
|
{
|
|
size_t len = strlen (substitute);
|
|
memcpy (to, substitute, len);
|
|
to += len;
|
|
from++;
|
|
substitute = nullptr;
|
|
}
|
|
else if (grammarcase < 0)
|
|
{
|
|
*to++ = '%';
|
|
}
|
|
else
|
|
{
|
|
strcpy (to, pronouns[pronoun][grammarcase]);
|
|
to += pronounshift[pronoun][grammarcase];
|
|
from++;
|
|
}
|
|
}
|
|
} while (*from++);
|
|
}
|
|
|
|
// [RH]
|
|
// ClientObituary: Show a message when a player dies
|
|
//
|
|
void ClientObituary (AActor *self, AActor *inflictor, AActor *attacker, int dmgflags, FName MeansOfDeath)
|
|
{
|
|
FString ret;
|
|
char gendermessage[1024];
|
|
|
|
// No obituaries for non-players, voodoo dolls or when not wanted
|
|
if (self->player == nullptr || self->player->mo != self || !show_obituaries)
|
|
return;
|
|
|
|
// Treat voodoo dolls as unknown deaths
|
|
if (inflictor && inflictor->player && inflictor->player->mo != inflictor)
|
|
MeansOfDeath = NAME_None;
|
|
|
|
FName mod = MeansOfDeath;
|
|
const char *message = nullptr;
|
|
const char *messagename = nullptr;
|
|
|
|
if (attacker == nullptr || attacker->player != nullptr)
|
|
{
|
|
if (mod == NAME_Telefrag)
|
|
{
|
|
if (AnnounceTelefrag (attacker, self))
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if (AnnounceKill (attacker, self))
|
|
return;
|
|
}
|
|
}
|
|
|
|
FString obit = DamageTypeDefinition::GetObituary(mod);
|
|
if (attacker == nullptr && obit.IsNotEmpty()) messagename = obit;
|
|
else
|
|
{
|
|
switch (mod)
|
|
{
|
|
case NAME_Suicide: messagename = "$OB_SUICIDE"; break;
|
|
case NAME_Falling: messagename = "$OB_FALLING"; break;
|
|
case NAME_Crush: messagename = "$OB_CRUSH"; break;
|
|
case NAME_Exit: messagename = "$OB_EXIT"; break;
|
|
case NAME_Drowning: messagename = "$OB_WATER"; break;
|
|
case NAME_Slime: messagename = "$OB_SLIME"; break;
|
|
case NAME_Fire: messagename = "$OB_LAVA"; break;
|
|
}
|
|
}
|
|
|
|
// Check for being killed by a voodoo doll.
|
|
if (inflictor && inflictor->player && inflictor->player->mo != inflictor)
|
|
{
|
|
messagename = "$OB_VOODOO";
|
|
}
|
|
|
|
if (attacker != NULL && message == NULL)
|
|
{
|
|
if (attacker == self)
|
|
{
|
|
message = "$OB_KILLEDSELF";
|
|
}
|
|
else
|
|
{
|
|
IFVIRTUALPTR(attacker, AActor, GetObituary)
|
|
{
|
|
VMValue params[] = { attacker, self, inflictor, mod.GetIndex(), !!(dmgflags & DMG_PLAYERATTACK) };
|
|
VMReturn rett(&ret);
|
|
VMCall(func, params, countof(params), &rett, 1);
|
|
if (ret.IsNotEmpty()) message = ret;
|
|
}
|
|
}
|
|
}
|
|
if (message == nullptr) message = messagename; // fallback to defaults if possible.
|
|
if (attacker == nullptr) attacker = self; // world
|
|
if (attacker->player == nullptr) attacker = self; // for the message creation
|
|
|
|
if (message != NULL && message[0] == '$')
|
|
{
|
|
message = GStrings[message+1];
|
|
}
|
|
|
|
if (message == NULL)
|
|
{
|
|
message = GStrings("OB_DEFAULT");
|
|
}
|
|
|
|
// [CK] Don't display empty strings
|
|
if (message == NULL || strlen(message) <= 0)
|
|
return;
|
|
|
|
PronounMessage (message, gendermessage, self->player->userinfo.GetGender(),
|
|
self->player->userinfo.GetName(), attacker->player->userinfo.GetName());
|
|
Printf (PRINT_MEDIUM, "%s\n", gendermessage);
|
|
}
|
|
|
|
|
|
//
|
|
// KillMobj
|
|
//
|
|
EXTERN_CVAR (Int, fraglimit)
|
|
|
|
void AActor::Die (AActor *source, AActor *inflictor, int dmgflags, FName MeansOfDeath)
|
|
{
|
|
// Handle possible unmorph on death
|
|
bool wasgibbed = (health < GetGibHealth());
|
|
|
|
{
|
|
IFVIRTUAL(AActor, MorphedDeath)
|
|
{
|
|
AActor *realthis = NULL;
|
|
int realstyle = 0;
|
|
int realhealth = 0;
|
|
|
|
VMValue params[] = { this };
|
|
VMReturn returns[3];
|
|
returns[0].PointerAt((void**)&realthis);
|
|
returns[1].IntAt(&realstyle);
|
|
returns[2].IntAt(&realhealth);
|
|
VMCall(func, params, 1, returns, 3);
|
|
|
|
if (realthis && !(realstyle & MORPH_UNDOBYDEATHSAVES))
|
|
{
|
|
if (wasgibbed)
|
|
{
|
|
int realgibhealth = realthis->GetGibHealth();
|
|
if (realthis->health >= realgibhealth)
|
|
{
|
|
realthis->health = realgibhealth - 1; // if morphed was gibbed, so must original be (where allowed)l
|
|
}
|
|
}
|
|
realthis->CallDie(source, inflictor, dmgflags, MeansOfDeath);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// [SO] 9/2/02 -- It's rather funny to see an exploded player body with the invuln sparkle active :)
|
|
effects &= ~FX_RESPAWNINVUL;
|
|
//flags &= ~MF_INVINCIBLE;
|
|
|
|
// [RH] Notify this actor's items.
|
|
for (AActor *item = Inventory; item != NULL; )
|
|
{
|
|
AActor *next = item->Inventory;
|
|
IFVIRTUALPTRNAME(item, NAME_Inventory, OwnerDied)
|
|
{
|
|
VMValue params[1] = { item };
|
|
VMCall(func, params, 1, nullptr, 0);
|
|
}
|
|
item = next;
|
|
}
|
|
|
|
if (flags & MF_MISSILE)
|
|
{ // [RH] When missiles die, they just explode
|
|
P_ExplodeMissile (this, NULL, NULL);
|
|
return;
|
|
}
|
|
// [RH] Set the target to the thing that killed it. Strife apparently does this.
|
|
if (source != NULL)
|
|
{
|
|
target = source;
|
|
}
|
|
|
|
// [ZZ] Fire WorldThingDied script hook.
|
|
E_WorldThingDied(this, inflictor);
|
|
|
|
// [JM] Fire KILL type scripts for actor. Not needed for players, since they have the "DEATH" script type.
|
|
if (!player && !(flags7 & MF7_NOKILLSCRIPTS) && ((flags7 & MF7_USEKILLSCRIPTS) || gameinfo.forcekillscripts))
|
|
{
|
|
Level->Behaviors.StartTypedScripts(SCRIPT_Kill, this, true, 0, true);
|
|
}
|
|
|
|
flags &= ~(MF_SHOOTABLE|MF_FLOAT|MF_SKULLFLY);
|
|
if (!(flags4 & MF4_DONTFALL)) flags&=~MF_NOGRAVITY;
|
|
flags |= MF_DROPOFF;
|
|
if ((flags3 & MF3_ISMONSTER) || FindState(NAME_Raise) != NULL || IsKindOf(NAME_PlayerPawn))
|
|
{ // [RH] Only monsters get to be corpses.
|
|
// Objects with a raise state should get the flag as well so they can
|
|
// be revived by an Arch-Vile. Batman Doom needs this.
|
|
// [RC] And disable this if DONTCORPSE is set, of course.
|
|
if(!(flags6 & MF6_DONTCORPSE)) flags |= MF_CORPSE;
|
|
}
|
|
flags6 |= MF6_KILLED;
|
|
|
|
IFVIRTUAL(AActor, GetDeathHeight)
|
|
{
|
|
VMValue params[] = { (DObject*)this };
|
|
VMReturn ret(&Height);
|
|
VMCall(func, params, 1, &ret, 1);
|
|
}
|
|
|
|
// [RH] If the thing has a special, execute and remove it
|
|
// Note that the thing that killed it is considered
|
|
// the activator of the script.
|
|
// New: In Hexen, the thing that died is the activator,
|
|
// so now a level flag selects who the activator gets to be.
|
|
// Everything is now moved to P_ActivateThingSpecial().
|
|
if (special && (!(flags & MF_SPECIAL) || (flags3 & MF3_ISMONSTER))
|
|
&& !(activationtype & THINGSPEC_NoDeathSpecial))
|
|
{
|
|
P_ActivateThingSpecial(this, source, true);
|
|
}
|
|
|
|
if (CountsAsKill())
|
|
Level->killed_monsters++;
|
|
|
|
if (source && source->player)
|
|
{
|
|
if (CountsAsKill())
|
|
{ // count for intermission
|
|
source->player->killcount++;
|
|
}
|
|
|
|
// Don't count any frags at level start, because they're just telefrags
|
|
// resulting from insufficient deathmatch starts, and it wouldn't be
|
|
// fair to count them toward a player's score.
|
|
if (player && Level->maptime)
|
|
{
|
|
source->player->frags[player - players]++;
|
|
if (player == source->player) // [RH] Cumulative frag count
|
|
{
|
|
char buff[256];
|
|
|
|
player->fragcount--;
|
|
if (deathmatch && player->spreecount >= 5 && cl_showsprees)
|
|
{
|
|
PronounMessage (GStrings("SPREEKILLSELF"), buff,
|
|
player->userinfo.GetGender(), player->userinfo.GetName(),
|
|
player->userinfo.GetName());
|
|
StatusBar->AttachMessage (Create<DHUDMessageFadeOut>(SmallFont, buff,
|
|
1.5f, 0.2f, 0, 0, CR_WHITE, 3.f, 0.5f), MAKE_ID('K','S','P','R'));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ((dmflags2 & DF2_YES_LOSEFRAG) && deathmatch)
|
|
player->fragcount--;
|
|
|
|
if (this->IsTeammate(source))
|
|
{
|
|
source->player->fragcount--;
|
|
}
|
|
else
|
|
{
|
|
++source->player->fragcount;
|
|
++source->player->spreecount;
|
|
}
|
|
|
|
if (source->player->morphTics)
|
|
{ // Make a super chicken
|
|
source->GiveInventoryType (PClass::FindActor(NAME_PowerWeaponLevel2));
|
|
}
|
|
|
|
if (deathmatch && cl_showsprees)
|
|
{
|
|
const char *spreemsg;
|
|
char buff[256];
|
|
|
|
switch (source->player->spreecount)
|
|
{
|
|
case 5:
|
|
spreemsg = GStrings("SPREE5");
|
|
break;
|
|
case 10:
|
|
spreemsg = GStrings("SPREE10");
|
|
break;
|
|
case 15:
|
|
spreemsg = GStrings("SPREE15");
|
|
break;
|
|
case 20:
|
|
spreemsg = GStrings("SPREE20");
|
|
break;
|
|
case 25:
|
|
spreemsg = GStrings("SPREE25");
|
|
break;
|
|
default:
|
|
spreemsg = NULL;
|
|
break;
|
|
}
|
|
|
|
if (spreemsg == NULL && player->spreecount >= 5)
|
|
{
|
|
if (!AnnounceSpreeLoss (this))
|
|
{
|
|
PronounMessage (GStrings("SPREEOVER"), buff, player->userinfo.GetGender(),
|
|
player->userinfo.GetName(), source->player->userinfo.GetName());
|
|
StatusBar->AttachMessage (Create<DHUDMessageFadeOut> (SmallFont, buff,
|
|
1.5f, 0.2f, 0, 0, CR_WHITE, 3.f, 0.5f), MAKE_ID('K','S','P','R'));
|
|
}
|
|
}
|
|
else if (spreemsg != NULL)
|
|
{
|
|
if (!AnnounceSpree (source))
|
|
{
|
|
PronounMessage (spreemsg, buff, player->userinfo.GetGender(),
|
|
player->userinfo.GetName(), source->player->userinfo.GetName());
|
|
StatusBar->AttachMessage (Create<DHUDMessageFadeOut> (SmallFont, buff,
|
|
1.5f, 0.2f, 0, 0, CR_WHITE, 3.f, 0.5f), MAKE_ID('K','S','P','R'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// [RH] Multikills
|
|
if (player != source->player)
|
|
{
|
|
source->player->multicount++;
|
|
if (source->player->lastkilltime > 0)
|
|
{
|
|
if (source->player->lastkilltime < Level->time - 3*TICRATE)
|
|
{
|
|
source->player->multicount = 1;
|
|
}
|
|
|
|
if (deathmatch &&
|
|
source->CheckLocalView() &&
|
|
cl_showmultikills)
|
|
{
|
|
const char *multimsg;
|
|
|
|
switch (source->player->multicount)
|
|
{
|
|
case 1:
|
|
multimsg = NULL;
|
|
break;
|
|
case 2:
|
|
multimsg = GStrings("MULTI2");
|
|
break;
|
|
case 3:
|
|
multimsg = GStrings("MULTI3");
|
|
break;
|
|
case 4:
|
|
multimsg = GStrings("MULTI4");
|
|
break;
|
|
default:
|
|
multimsg = GStrings("MULTI5");
|
|
break;
|
|
}
|
|
if (multimsg != NULL)
|
|
{
|
|
char buff[256];
|
|
|
|
if (!AnnounceMultikill (source))
|
|
{
|
|
PronounMessage (multimsg, buff, player->userinfo.GetGender(),
|
|
player->userinfo.GetName(), source->player->userinfo.GetName());
|
|
StatusBar->AttachMessage (Create<DHUDMessageFadeOut> (SmallFont, buff,
|
|
1.5f, 0.8f, 0, 0, CR_RED, 3.f, 0.5f), MAKE_ID('M','K','I','L'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
source->player->lastkilltime = Level->time;
|
|
}
|
|
|
|
// [RH] Implement fraglimit
|
|
if (deathmatch && fraglimit &&
|
|
fraglimit <= D_GetFragCount (source->player))
|
|
{
|
|
Printf ("%s\n", GStrings("TXT_FRAGLIMIT"));
|
|
Level->ExitLevel (0, false);
|
|
}
|
|
}
|
|
}
|
|
else if (!multiplayer && CountsAsKill() && Level->isPrimaryLevel())
|
|
{
|
|
// count all monster deaths,
|
|
// even those caused by other monsters
|
|
Level->Players[0]->killcount++;
|
|
}
|
|
|
|
if (player)
|
|
{
|
|
// [RH] Death messages
|
|
ClientObituary (this, inflictor, source, dmgflags, MeansOfDeath);
|
|
|
|
// [ZZ] fire player death hook
|
|
E_PlayerDied(int(player - players));
|
|
|
|
// Death script execution, care of Skull Tag
|
|
Level->Behaviors.StartTypedScripts (SCRIPT_Death, this, true);
|
|
|
|
// [RH] Force a delay between death and respawn
|
|
player->respawn_time = Level->time + TICRATE;
|
|
|
|
//Added by MC: Respawn bots
|
|
if (Level->BotInfo.botnum && !demoplayback)
|
|
{
|
|
if (player->Bot != NULL)
|
|
player->Bot->t_respawn = (pr_botrespawn()%15)+((Level->BotInfo.botnum-1)*2)+TICRATE+1;
|
|
|
|
//Added by MC: Discard enemies.
|
|
for (int i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
DBot *Bot = Level->Players[i]->Bot;
|
|
if (Bot != nullptr && this == Bot->enemy)
|
|
{
|
|
if (Bot->dest == Bot->enemy)
|
|
Bot->dest = nullptr;
|
|
Bot->enemy = nullptr;
|
|
}
|
|
}
|
|
|
|
player->spreecount = 0;
|
|
player->multicount = 0;
|
|
}
|
|
|
|
// count environment kills against you
|
|
if (!source)
|
|
{
|
|
player->frags[player - players]++;
|
|
player->fragcount--; // [RH] Cumulative frag count
|
|
}
|
|
|
|
flags &= ~MF_SOLID;
|
|
player->playerstate = PST_DEAD;
|
|
|
|
IFVM(PlayerPawn, DropWeapon)
|
|
{
|
|
VMValue param = player->mo;
|
|
VMCall(func, ¶m, 1, nullptr, 0);
|
|
}
|
|
|
|
if (this == players[consoleplayer].camera && automapactive)
|
|
{
|
|
// don't die in auto map, switch view prior to dying
|
|
AM_Stop ();
|
|
}
|
|
|
|
// [GRB] Clear extralight. When you killed yourself with weapon that
|
|
// called A_Light1/2 before it called A_Light0, extraligh remained.
|
|
player->extralight = 0;
|
|
}
|
|
|
|
// [RH] If this is the unmorphed version of another monster, destroy this
|
|
// actor, because the morphed version is the one that will stick around.
|
|
if (flags & MF_UNMORPHED)
|
|
{
|
|
Destroy ();
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
FState *diestate = NULL;
|
|
int gibhealth = GetGibHealth();
|
|
ActorFlags4 iflags4 = inflictor == NULL ? ActorFlags4::FromInt(0) : inflictor->flags4;
|
|
bool extremelydead = ((health < gibhealth || iflags4 & MF4_EXTREMEDEATH) && !(iflags4 & MF4_NOEXTREMEDEATH));
|
|
|
|
// Special check for 'extreme' damage type to ensure that it gets recorded properly as an extreme death for subsequent checks.
|
|
if (DamageType == NAME_Extreme)
|
|
{
|
|
extremelydead = true;
|
|
DamageType = NAME_None;
|
|
}
|
|
|
|
// find the appropriate death state. The order is:
|
|
//
|
|
// 1. If damagetype is not 'none' and death is extreme, try a damage type specific extreme death state
|
|
// 2. If no such state is found or death is not extreme try a damage type specific normal death state
|
|
// 3. If damagetype is 'ice' and actor is a monster or player, try the generic freeze death (unless prohibited)
|
|
// 4. If no state has been found and death is extreme, try the extreme death state
|
|
// 5. If no such state is found or death is not extreme try the regular death state.
|
|
// 6. If still no state has been found, destroy the actor immediately.
|
|
|
|
if (DamageType != NAME_None)
|
|
{
|
|
if (extremelydead)
|
|
{
|
|
FName labels[] = { NAME_Death, NAME_Extreme, DamageType };
|
|
diestate = FindState(3, labels, true);
|
|
}
|
|
if (diestate == NULL)
|
|
{
|
|
diestate = FindState (NAME_Death, DamageType, true);
|
|
if (diestate != NULL) extremelydead = false;
|
|
}
|
|
if (diestate == NULL)
|
|
{
|
|
if (DamageType == NAME_Ice)
|
|
{ // If an actor doesn't have an ice death, we can still give them a generic one.
|
|
|
|
if (!deh.NoAutofreeze && !(flags4 & MF4_NOICEDEATH) && (player || (flags3 & MF3_ISMONSTER)))
|
|
{
|
|
diestate = FindState(NAME_GenericFreezeDeath);
|
|
extremelydead = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (diestate == NULL)
|
|
{
|
|
|
|
// Don't pass on a damage type this actor cannot handle.
|
|
// (most importantly, prevent barrels from passing on ice damage.)
|
|
// Massacre must be preserved though.
|
|
if (DamageType != NAME_Massacre)
|
|
{
|
|
DamageType = NAME_None;
|
|
}
|
|
|
|
if (extremelydead)
|
|
{ // Extreme death
|
|
diestate = FindState (NAME_Death, NAME_Extreme, true);
|
|
}
|
|
if (diestate == NULL)
|
|
{ // Normal death
|
|
extremelydead = false;
|
|
diestate = FindState (NAME_Death);
|
|
}
|
|
}
|
|
|
|
if (extremelydead)
|
|
{
|
|
// We'll only get here if an actual extreme death state was used.
|
|
|
|
// For players, mark the appropriate flag.
|
|
if (player != NULL)
|
|
{
|
|
player->cheats |= CF_EXTREMELYDEAD;
|
|
}
|
|
// If a non-player, mark as extremely dead for the crash state.
|
|
else if (health >= gibhealth)
|
|
{
|
|
health = gibhealth - 1;
|
|
}
|
|
}
|
|
|
|
if (diestate != NULL)
|
|
{
|
|
SetState (diestate);
|
|
|
|
if (tics > 1)
|
|
{
|
|
tics -= pr_killmobj() & 3;
|
|
if (tics < 1)
|
|
tics = 1;
|
|
}
|
|
}
|
|
// The following condition is needed to avoid crash when player class has no death states
|
|
// Instance of player pawn will be garbage collected on reloading of level
|
|
else if (player == nullptr)
|
|
{
|
|
Destroy();
|
|
}
|
|
}
|
|
|
|
DEFINE_ACTION_FUNCTION(AActor, Die)
|
|
{
|
|
PARAM_SELF_PROLOGUE(AActor);
|
|
PARAM_OBJECT(source, AActor);
|
|
PARAM_OBJECT(inflictor, AActor);
|
|
PARAM_INT(dmgflags);
|
|
PARAM_NAME(MeansOfDeath);
|
|
self->Die(source, inflictor, dmgflags, MeansOfDeath);
|
|
return 0;
|
|
}
|
|
|
|
void AActor::CallDie(AActor *source, AActor *inflictor, int dmgflags, FName MeansOfDeath)
|
|
{
|
|
IFVIRTUAL(AActor, Die)
|
|
{
|
|
VMValue params[] = { (DObject*)this, source, inflictor, dmgflags, MeansOfDeath.GetIndex() };
|
|
VMCall(func, params, 5, nullptr, 0);
|
|
}
|
|
else return Die(source, inflictor, dmgflags, MeansOfDeath);
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------
|
|
//
|
|
// PROC P_AutoUseHealth
|
|
//
|
|
//---------------------------------------------------------------------------
|
|
|
|
void P_AutoUseHealth(player_t *player, int saveHealth)
|
|
{
|
|
IFVM(PlayerPawn, AutoUseHealth)
|
|
{
|
|
VMValue params[] = { player->mo, saveHealth };
|
|
VMCall(func, params, 2, nullptr, 0);
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
//
|
|
// P_AutoUseStrifeHealth
|
|
//
|
|
//============================================================================
|
|
CVAR(Bool, sv_disableautohealth, false, CVAR_ARCHIVE|CVAR_SERVERINFO)
|
|
|
|
void P_AutoUseStrifeHealth (player_t *player)
|
|
{
|
|
IFVM(PlayerPawn, AutoUseStrifeHealth)
|
|
{
|
|
VMValue params[] = { player->mo };
|
|
VMCall(func, params, 1, nullptr, 0);
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// ReactToDamage
|
|
//
|
|
//==========================================================================
|
|
static bool TriggerPainChance(AActor *target, FName mod, bool forcedPain, bool zscript);
|
|
|
|
static inline bool MustForcePain(AActor *target, AActor *inflictor)
|
|
{
|
|
return (inflictor && (inflictor->flags6 & MF6_FORCEPAIN));
|
|
}
|
|
|
|
static inline bool isFakePain(AActor *target, AActor *inflictor, int damage)
|
|
{
|
|
return (((target->flags7 & MF7_ALLOWPAIN || target->flags5 & MF5_NODAMAGE) && damage > 0) ||
|
|
(inflictor && (inflictor->flags7 & MF7_CAUSEPAIN)));
|
|
}
|
|
|
|
// [MC] Completely ripped out of DamageMobj to make it less messy.
|
|
static void ReactToDamage(AActor *target, AActor *inflictor, AActor *source, int damage, FName mod, int flags, int originaldamage)
|
|
{
|
|
bool justhit = false;
|
|
int painchance = 0;
|
|
FState *woundstate = nullptr;
|
|
bool fakedPain = false;
|
|
bool forcedPain = false;
|
|
bool noPain = false;
|
|
|
|
// Dead or non-existent entity, do not react. Especially if the damage is cancelled.
|
|
if (target == nullptr || target->health < 1 || damage < 0)
|
|
return;
|
|
|
|
player_t *player = target->player;
|
|
if (player)
|
|
{
|
|
if ((player->cheats & CF_GODMODE2) || (player->mo->flags5 & MF5_NOPAIN) ||
|
|
((player->cheats & CF_GODMODE) && damage < TELEFRAG_DAMAGE))
|
|
return;
|
|
}
|
|
|
|
noPain = (flags & DMG_NO_PAIN) || (target->flags5 & MF5_NOPAIN) || (inflictor && (inflictor->flags5 & MF5_PAINLESS));
|
|
|
|
// Are we attempting to cause pain?
|
|
if (!noPain)
|
|
{
|
|
fakedPain = (isFakePain(target, inflictor, originaldamage));
|
|
forcedPain = (MustForcePain(target, inflictor));
|
|
}
|
|
|
|
// [MC] No forced or faked pain so skip it.
|
|
// However the rest of the function must carry on.
|
|
if (!noPain && damage < 1 && !fakedPain && !forcedPain)
|
|
noPain = true;
|
|
|
|
woundstate = target->FindState(NAME_Wound, mod);
|
|
if (woundstate != NULL)
|
|
{
|
|
int woundhealth = target->WoundHealth;
|
|
|
|
if (target->health <= woundhealth)
|
|
{
|
|
target->SetState(woundstate);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!noPain &&
|
|
((target->player != nullptr || !G_SkillProperty(SKILLP_NoPain)) && !(target->flags & MF_SKULLFLY))
|
|
&& damage >= target->PainThreshold)
|
|
{
|
|
if (inflictor && inflictor->PainType != NAME_None)
|
|
mod = inflictor->PainType;
|
|
|
|
// Not called from ZScript.
|
|
justhit = TriggerPainChance(target, mod, forcedPain, false);
|
|
}
|
|
|
|
if (target->player == nullptr) target->reactiontime = 0; // we're awake now...
|
|
if (source)
|
|
{
|
|
if (source == target->target)
|
|
{
|
|
target->threshold = target->DefThreshold;
|
|
if (target->state == target->SpawnState && target->SeeState != NULL)
|
|
{
|
|
target->SetState(target->SeeState);
|
|
}
|
|
}
|
|
else if (source != target->target && target->CallOkayToSwitchTarget(source))
|
|
{
|
|
// Target actor is not intent on another actor,
|
|
// so make him chase after source
|
|
|
|
// killough 2/15/98: remember last enemy, to prevent
|
|
// sleeping early; 2/21/98: Place priority on players
|
|
|
|
if (target->lastenemy == NULL ||
|
|
(target->lastenemy->player == NULL && target->TIDtoHate == 0) ||
|
|
target->lastenemy->health <= 0)
|
|
{
|
|
target->lastenemy = target->target; // remember last enemy - killough
|
|
}
|
|
target->target = source;
|
|
target->threshold = target->DefThreshold;
|
|
if (target->state == target->SpawnState && target->SeeState != NULL)
|
|
{
|
|
target->SetState(target->SeeState);
|
|
}
|
|
}
|
|
}
|
|
// killough 11/98: Don't attack a friend, unless hit by that friend.
|
|
if (justhit && (target->target == source || !target->target || !target->IsFriend(target->target)))
|
|
target->flags |= MF_JUSTHIT; // fight back!
|
|
}
|
|
|
|
static bool TriggerPainChance(AActor *target, FName mod = NAME_None, bool forcedPain = false, bool zscript = false)
|
|
{
|
|
if (target == nullptr || target->flags5 & MF5_NOPAIN || target->health < 1)
|
|
return false;
|
|
|
|
bool justhit = false, flinched = false;
|
|
int painchance = target->PainChance;
|
|
for (auto & pc : target->GetInfo()->PainChances)
|
|
{
|
|
if (pc.first == mod)
|
|
{
|
|
painchance = pc.second;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (forcedPain || (pr_damagemobj() < painchance))
|
|
{
|
|
if (mod == NAME_Electric)
|
|
{
|
|
if (pr_lightning() < 96)
|
|
{
|
|
justhit = true;
|
|
FState *painstate = target->FindState(NAME_Pain, mod);
|
|
if (painstate != NULL)
|
|
{
|
|
flinched = true;
|
|
target->SetState(painstate);
|
|
}
|
|
}
|
|
else
|
|
{ // "electrocute" the target
|
|
target->renderflags |= RF_FULLBRIGHT;
|
|
if ((target->flags3 & MF3_ISMONSTER) && pr_lightning() < 128)
|
|
{
|
|
target->Howl();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
justhit = true;
|
|
FState *painstate = target->FindState(NAME_Pain, mod);
|
|
if (painstate != NULL)
|
|
{
|
|
flinched = true;
|
|
target->SetState(painstate);
|
|
}
|
|
if (mod == NAME_PoisonCloud)
|
|
{
|
|
if ((target->flags3 & MF3_ISMONSTER) && pr_poison() < 128)
|
|
{
|
|
target->Howl();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return (zscript) ? flinched : justhit;
|
|
}
|
|
|
|
// TriggerPainChance directly from DECORATE/ZScript will return if the
|
|
// entity flinched or not.
|
|
|
|
DEFINE_ACTION_FUNCTION(AActor, TriggerPainChance)
|
|
{
|
|
PARAM_SELF_PROLOGUE(AActor);
|
|
PARAM_NAME(mod);
|
|
PARAM_BOOL(forcedPain);
|
|
ACTION_RETURN_BOOL(TriggerPainChance(self, mod, forcedPain, true));
|
|
}
|
|
|
|
/*
|
|
=================
|
|
=
|
|
= P_DamageMobj
|
|
=
|
|
= Damages both enemies and players
|
|
= inflictor is the thing that caused the damage
|
|
= creature or missile, can be NULL (slime, etc)
|
|
= source is the thing to target after taking damage
|
|
= creature or NULL
|
|
= Source and inflictor are the same for melee attacks
|
|
= source can be null for barrel explosions and other environmental stuff
|
|
==================
|
|
*/
|
|
|
|
//===========================================================================
|
|
//
|
|
//
|
|
//
|
|
//===========================================================================
|
|
|
|
static int hasBuddha(player_t *player)
|
|
{
|
|
if (player->playerstate == PST_DEAD) return 0;
|
|
if (player->cheats & CF_BUDDHA2) return 2;
|
|
|
|
if ((player->cheats & CF_BUDDHA) ||
|
|
(player->mo->flags7 & MF7_BUDDHA) ||
|
|
player->mo->FindInventory(PClass::FindActor(NAME_PowerBuddha), true) != nullptr) return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
// Returns the amount of damage actually inflicted upon the target, or -1 if
|
|
// the damage was cancelled.
|
|
static int DamageMobj (AActor *target, AActor *inflictor, AActor *source, int damage, FName mod, int flags, DAngle angle, bool& needevent)
|
|
{
|
|
player_t *player = NULL;
|
|
int temp;
|
|
bool justhit = false;
|
|
bool plrDontThrust = false;
|
|
const int rawdamage = damage;
|
|
const bool telefragDamage = (rawdamage >= TELEFRAG_DAMAGE);
|
|
|
|
if (damage < 0) damage = 0;
|
|
|
|
if (target == NULL || !((target->flags & MF_SHOOTABLE) || (target->flags6 & MF6_VULNERABLE)))
|
|
{ // Shouldn't happen
|
|
return -1;
|
|
}
|
|
FName MeansOfDeath = mod;
|
|
|
|
// Spectral targets only take damage from spectral projectiles.
|
|
if (target->flags4 & MF4_SPECTRAL && !telefragDamage)
|
|
{
|
|
if (inflictor == NULL || !(inflictor->flags4 & MF4_SPECTRAL))
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
if (target->health <= 0)
|
|
{
|
|
if (inflictor && mod == NAME_Ice && !(inflictor->flags7 & MF7_ICESHATTER))
|
|
{
|
|
return -1;
|
|
}
|
|
else if (target->flags & MF_ICECORPSE) // frozen
|
|
{
|
|
target->tics = 1;
|
|
target->flags6 |= MF6_SHATTERING;
|
|
target->Vel.Zero();
|
|
}
|
|
return -1;
|
|
}
|
|
if (target == source && (!telefragDamage || target->flags7 & MF7_LAXTELEFRAGDMG))
|
|
{
|
|
damage = int(damage * target->SelfDamageFactor);
|
|
}
|
|
|
|
// [MC] Changed it to check rawdamage here for consistency, even though that doesn't actually do anything
|
|
// different here. At any rate, invulnerable is being checked before type factoring, which is then being
|
|
// checked by player cheats/invul/buddha followed by monster buddha. This is inconsistent. Don't let the
|
|
// original telefrag damage CHECK (rawdamage) be influenced by outside factors when looking at cheats/invul.
|
|
if ((target->flags2 & MF2_INVULNERABLE) && !telefragDamage && (!(flags & DMG_FORCED)))
|
|
{ // actor is invulnerable
|
|
if (target->player == NULL)
|
|
{
|
|
if (inflictor == NULL || (!(inflictor->flags3 & MF3_FOILINVUL) && !(flags & DMG_FOILINVUL)))
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Players are optionally excluded from getting thrust by damage.
|
|
if (target->IntVar(NAME_PlayerFlags) & PPF_NOTHRUSTWHENINVUL)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (inflictor != NULL)
|
|
{
|
|
if (inflictor->flags5 & MF5_PIERCEARMOR)
|
|
flags |= DMG_NO_ARMOR;
|
|
}
|
|
|
|
// [RH] Andy Baker's Stealth monsters
|
|
if (target->flags & MF_STEALTH)
|
|
{
|
|
target->Alpha = 1.;
|
|
target->visdir = -1;
|
|
}
|
|
if (target->flags & MF_SKULLFLY)
|
|
{
|
|
target->Vel.Zero();
|
|
}
|
|
|
|
player = target->player;
|
|
if (!(flags & DMG_FORCED)) // DMG_FORCED skips all special damage checks, TELEFRAG_DAMAGE may not be reduced at all
|
|
{
|
|
if (target->flags2 & MF2_DORMANT)
|
|
{
|
|
// Invulnerable, and won't wake up
|
|
return -1;
|
|
}
|
|
|
|
if (!telefragDamage || (target->flags7 & MF7_LAXTELEFRAGDMG)) // TELEFRAG_DAMAGE may only be reduced with LAXTELEFRAGDMG or it may not guarantee its effect.
|
|
{
|
|
if (player && damage > 1)
|
|
{
|
|
// Take half damage in trainer mode
|
|
damage = int(damage * G_SkillProperty(SKILLP_DamageFactor) * sv_damagefactorplayer);
|
|
}
|
|
else if (!player && damage > 1 && !(target->flags & MF_FRIENDLY))
|
|
{
|
|
// inflict scaled damage to non-players
|
|
damage = int(damage * sv_damagefactormobj);
|
|
}
|
|
else if (!player && damage > 1 && (target->flags & MF_FRIENDLY))
|
|
{
|
|
// inflict scaled damage to non-player friends
|
|
damage = int(damage * sv_damagefactorfriendly);
|
|
}
|
|
// Special damage types
|
|
if (inflictor)
|
|
{
|
|
if (inflictor->flags4 & MF4_SPECTRAL)
|
|
{
|
|
if (player != NULL)
|
|
{
|
|
if (!deathmatch && inflictor->FriendPlayer > 0)
|
|
return -1;
|
|
}
|
|
else if (target->flags4 & MF4_SPECTRAL)
|
|
{
|
|
if (inflictor->FriendPlayer == 0 && !target->IsHostile(inflictor))
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
damage = inflictor->CallDoSpecialDamage(target, damage, mod);
|
|
if (damage < 0)
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
int olddam = damage;
|
|
|
|
if (damage > 0 && source != NULL)
|
|
{
|
|
damage = int(damage * source->DamageMultiply);
|
|
|
|
// Handle active damage modifiers (e.g. PowerDamage)
|
|
if (damage > 0 && !(flags & DMG_NO_ENHANCE))
|
|
{
|
|
damage = source->GetModifiedDamage(mod, damage, false);
|
|
}
|
|
}
|
|
// Handle passive damage modifiers (e.g. PowerProtection), provided they are not afflicted with protection penetrating powers.
|
|
if (damage > 0 && !(flags & DMG_NO_PROTECT))
|
|
{
|
|
damage = target->GetModifiedDamage(mod, damage, true);
|
|
}
|
|
if (damage > 0 && !(flags & DMG_NO_FACTOR))
|
|
{
|
|
damage = target->ApplyDamageFactor(mod, damage);
|
|
}
|
|
|
|
if (damage >= 0)
|
|
{
|
|
damage = target->CallTakeSpecialDamage(inflictor, source, damage, mod);
|
|
}
|
|
|
|
// '<0' is handled below. This only handles the case where damage gets reduced to 0.
|
|
if (damage == 0 && olddam > 0)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
if (target->flags5 & MF5_NODAMAGE)
|
|
{
|
|
damage = 0;
|
|
}
|
|
}
|
|
if (damage < 0)
|
|
{
|
|
// any negative value means that something in the above chain has cancelled out all damage and all damage effects, including pain.
|
|
return -1;
|
|
}
|
|
|
|
|
|
//[RC] Backported from the Zandronum source.. Mostly.
|
|
if( target->player &&
|
|
damage > 0 &&
|
|
source &&
|
|
mod != NAME_Reflection &&
|
|
target != source)
|
|
|
|
{
|
|
int reflectdamage = 0;
|
|
bool reflecttype = false;
|
|
for (auto p = target->player->mo->Inventory; p != nullptr; p = p->Inventory)
|
|
{
|
|
// This picks the reflection item with the maximum efficiency for the given damage type.
|
|
if (p->IsKindOf(NAME_PowerReflection))
|
|
{
|
|
double alwaysreflect = p->FloatVar(NAME_Strength);
|
|
int alwaysdamage = clamp(int(damage * alwaysreflect), 0, damage);
|
|
int mydamage = alwaysdamage + p->ApplyDamageFactor(mod, damage - alwaysdamage);
|
|
if (mydamage > reflectdamage)
|
|
{
|
|
reflectdamage = mydamage;
|
|
reflecttype = p->BoolVar(NAME_ReflectType);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (reflectdamage > 0)
|
|
{
|
|
// use the reflect item's damage factors to get the final value here.
|
|
P_DamageMobj(source, nullptr, target, reflectdamage, reflecttype? mod : NAME_Reflection );
|
|
|
|
// Reset means of death flag.
|
|
MeansOfDeath = mod;
|
|
}
|
|
}
|
|
|
|
|
|
// Push the target unless the source's weapon's kickback is 0.
|
|
// (i.e. Gauntlets/Chainsaw)
|
|
if (!plrDontThrust && inflictor && inflictor != target // [RH] Not if hurting own self
|
|
&& !(target->flags & MF_NOCLIP)
|
|
&& !(inflictor->flags2 & MF2_NODMGTHRUST)
|
|
&& !(flags & DMG_THRUSTLESS)
|
|
&& !(target->flags7 & MF7_DONTTHRUST)
|
|
&& (source == NULL || source->player == NULL || !(source->flags2 & MF2_NODMGTHRUST)))
|
|
{
|
|
IFVIRTUALPTR(target, AActor, ApplyKickback)
|
|
{
|
|
VMValue params[] = { target, inflictor, source, damage, angle.Degrees, mod.GetIndex(), flags };
|
|
VMCall(func, params, countof(params), nullptr, 0);
|
|
}
|
|
}
|
|
|
|
// [RH] Avoid friendly fire if enabled
|
|
if (!(flags & DMG_FORCED) && source != NULL &&
|
|
((player && player != source->player) || (!player && target != source)) &&
|
|
target->IsTeammate (source))
|
|
{
|
|
//Use the original damage to check for telefrag amount. Don't let the now-amplified damagetypes do it.
|
|
if (!telefragDamage || (target->flags7 & MF7_LAXTELEFRAGDMG))
|
|
{ // Still allow telefragging :-(
|
|
damage = (int)(damage * target->Level->teamdamage);
|
|
if (damage <= 0)
|
|
{
|
|
return (damage < 0) ? -1 : 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// player specific
|
|
//
|
|
if (player)
|
|
{
|
|
// Don't allow DMG_FORCED to work on ultimate degreeslessness/buddha and nodamage.
|
|
if ((player->cheats & (CF_GODMODE2 | CF_BUDDHA2)) || (player->mo->flags5 & MF5_NODAMAGE))
|
|
{
|
|
flags &= ~DMG_FORCED;
|
|
}
|
|
//Added by MC: Lets bots look allround for enemies if they survive an ambush.
|
|
if (player->Bot != NULL)
|
|
{
|
|
player->Bot->allround = true;
|
|
}
|
|
|
|
// end of game hell hack
|
|
if ((target->Sector->Flags & SECF_ENDLEVEL) && damage >= target->health)
|
|
{
|
|
damage = target->health - 1;
|
|
}
|
|
|
|
if (!(flags & DMG_FORCED))
|
|
{
|
|
// check the real player, not a voodoo doll here for invulnerability effects
|
|
if ((!telefragDamage && ((player->mo->flags2 & MF2_INVULNERABLE) ||
|
|
(player->cheats & CF_GODMODE))) ||
|
|
(player->cheats & CF_GODMODE2) || (player->mo->flags5 & MF5_NODAMAGE))
|
|
//Absolutely no hurting if NODAMAGE is involved. Same for GODMODE2.
|
|
{ // player is invulnerable, so don't hurt him
|
|
return 0;
|
|
}
|
|
// Armor for players.
|
|
if (!(flags & DMG_NO_ARMOR) && player->mo->Inventory != NULL)
|
|
{
|
|
int newdam = damage;
|
|
if (damage > 0)
|
|
{
|
|
newdam = player->mo->AbsorbDamage(damage, mod);
|
|
}
|
|
if (!telefragDamage || (player->mo->flags7 & MF7_LAXTELEFRAGDMG)) //rawdamage is never modified.
|
|
{
|
|
// if we are telefragging don't let the damage value go below that magic value. Some further checks would fail otherwise.
|
|
damage = newdam;
|
|
}
|
|
|
|
if (damage <= 0)
|
|
{
|
|
return (damage < 0) ? -1 : 0;
|
|
}
|
|
}
|
|
|
|
if (damage >= player->health && !telefragDamage
|
|
&& (G_SkillProperty(SKILLP_AutoUseHealth) || deathmatch)
|
|
&& !player->morphTics)
|
|
{ // Try to use some inventory health
|
|
P_AutoUseHealth (player, damage - player->health + 1);
|
|
}
|
|
}
|
|
|
|
player->health -= damage; // mirror mobj health here for Dave
|
|
// [RH] Make voodoo dolls and real players record the same health
|
|
target->health = player->mo->health -= damage;
|
|
if (player->health < 50 && !deathmatch && !(flags & DMG_FORCED))
|
|
{
|
|
P_AutoUseStrifeHealth (player);
|
|
player->mo->health = player->health;
|
|
}
|
|
if (player->health <= 0)
|
|
{
|
|
// [SP] Buddha cheat: if the player is about to die, rescue him to 1 health.
|
|
// This does not save the player if damage >= TELEFRAG_DAMAGE, still need to
|
|
// telefrag him right? ;) (Unfortunately the damage is "absorbed" by armor,
|
|
// but telefragging should still do enough damage to kill the player)
|
|
// Ignore players that are already dead.
|
|
// [MC]Buddha2 absorbs telefrag damage, and anything else thrown their way.
|
|
int buddha = hasBuddha(player);
|
|
if (flags & DMG_FORCED) buddha = 0;
|
|
if (telefragDamage && buddha == 1) buddha = 0;
|
|
if (buddha)
|
|
{
|
|
// If this is a voodoo doll we need to handle the real player as well.
|
|
player->mo->health = target->health = player->health = 1;
|
|
}
|
|
else
|
|
{
|
|
player->health = 0;
|
|
}
|
|
}
|
|
player->LastDamageType = mod;
|
|
player->attacker = source;
|
|
player->damagecount += damage; // add damage after armor / invuln
|
|
if (player->damagecount > 100)
|
|
{
|
|
player->damagecount = 100; // teleport stomp does 10k points...
|
|
}
|
|
temp = damage < 100 ? damage : 100;
|
|
if (player == &players[consoleplayer])
|
|
{
|
|
I_Tactile (40,10,40+temp*2);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Armor for monsters.
|
|
if (!(flags & (DMG_NO_ARMOR|DMG_FORCED)) && target->Inventory != NULL && damage > 0)
|
|
{
|
|
int newdam = damage;
|
|
newdam = target->AbsorbDamage(damage, mod);
|
|
damage = newdam;
|
|
if (damage <= 0)
|
|
{
|
|
return (damage < 0) ? -1 : 0;
|
|
}
|
|
}
|
|
|
|
target->health -= damage;
|
|
}
|
|
|
|
//
|
|
// the damage has been dealt; now deal with the consequences
|
|
//
|
|
target->DamageTypeReceived = mod;
|
|
|
|
// If the damaging player has the power of drain, give the player 50% of the damage
|
|
// done in health.
|
|
if ( source && source->player && !(target->flags5 & MF5_DONTDRAIN))
|
|
{
|
|
if (!target->player || target->player != source->player)
|
|
{
|
|
double draindamage = 0;
|
|
for (auto p = source->player->mo->Inventory; p != nullptr; p = p->Inventory)
|
|
{
|
|
// This picks the item with the maximum efficiency.
|
|
if (p->IsKindOf(NAME_PowerDrain))
|
|
{
|
|
double mydamage = p->FloatVar(NAME_Strength);
|
|
if (mydamage > draindamage) draindamage = mydamage;
|
|
}
|
|
}
|
|
|
|
if (draindamage > 0)
|
|
{
|
|
int draindmg = int(draindamage * damage);
|
|
IFVIRTUALPTR(source, AActor, OnDrain)
|
|
{
|
|
VMValue params[] = { source, target, draindmg, mod.GetIndex() };
|
|
VMReturn ret(&draindmg);
|
|
VMCall(func, params, countof(params), &ret, 1);
|
|
}
|
|
if (P_GiveBody(source, draindmg))
|
|
{
|
|
S_Sound(source, CHAN_ITEM, "*drainhealth", 1, ATTN_NORM);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (target->health <= 0)
|
|
{
|
|
//[MC]Buddha flag for monsters.
|
|
if (!(flags & DMG_FORCED) && ((target->flags7 & MF7_BUDDHA) && !telefragDamage && ((inflictor == NULL || !(inflictor->flags7 & MF7_FOILBUDDHA)) && !(flags & DMG_FOILBUDDHA))))
|
|
{ //FOILBUDDHA or Telefrag damage must kill it.
|
|
target->health = 1;
|
|
}
|
|
else
|
|
{
|
|
|
|
// Death
|
|
target->special1 = damage;
|
|
|
|
// use inflictor's death type if it got one.
|
|
if (inflictor && inflictor->DeathType != NAME_None) mod = inflictor->DeathType;
|
|
|
|
// check for special fire damage or ice damage deaths
|
|
if (mod == NAME_Fire)
|
|
{
|
|
if (player && !player->morphTics)
|
|
{ // Check for flame death
|
|
if (!inflictor ||
|
|
((target->health > -50) && (damage > 25)) ||
|
|
!(inflictor->flags5 & MF5_SPECIALFIREDAMAGE))
|
|
{
|
|
target->DamageType = NAME_Fire;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target->DamageType = NAME_Fire;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target->DamageType = mod;
|
|
}
|
|
if (source && source->tracer && (source->flags5 & MF5_SUMMONEDMONSTER))
|
|
{ // Minotaur's kills go to his master
|
|
// Make sure still alive and not a pointer to fighter head
|
|
if (source->tracer->player && (source->tracer->player->mo == source->tracer))
|
|
{
|
|
source = source->tracer;
|
|
}
|
|
}
|
|
|
|
const int realdamage = MAX(0, damage);
|
|
E_WorldThingDamaged(target, inflictor, source, realdamage, mod, flags, angle);
|
|
needevent = false;
|
|
|
|
target->CallDie (source, inflictor, flags, MeansOfDeath);
|
|
return realdamage;
|
|
}
|
|
}
|
|
return MAX(0, damage);
|
|
}
|
|
|
|
static int DoDamageMobj(AActor *target, AActor *inflictor, AActor *source, int damage, FName mod, int flags, DAngle angle)
|
|
{
|
|
// [ZZ] event handlers need the result.
|
|
bool needevent = true;
|
|
int realdamage = DamageMobj(target, inflictor, source, damage, mod, flags, angle, needevent);
|
|
if (realdamage >= 0) //Keep this check separated. Mods relying upon negative numbers may break otherwise.
|
|
ReactToDamage(target, inflictor, source, realdamage, mod, flags, damage);
|
|
|
|
if (realdamage > 0 && needevent)
|
|
{
|
|
// [ZZ] event handlers only need the resultant damage (they can't do anything about it anyway)
|
|
E_WorldThingDamaged(target, inflictor, source, realdamage, mod, flags, angle);
|
|
}
|
|
|
|
return MAX(0, realdamage);
|
|
}
|
|
|
|
DEFINE_ACTION_FUNCTION(AActor, DamageMobj)
|
|
{
|
|
PARAM_SELF_PROLOGUE(AActor);
|
|
PARAM_OBJECT(inflictor, AActor);
|
|
PARAM_OBJECT(source, AActor);
|
|
PARAM_INT(damage);
|
|
PARAM_NAME(mod);
|
|
PARAM_INT(flags);
|
|
PARAM_FLOAT(angle);
|
|
ACTION_RETURN_INT(DoDamageMobj(self, inflictor, source, damage, mod, flags, angle));
|
|
}
|
|
|
|
int P_DamageMobj(AActor *target, AActor *inflictor, AActor *source, int damage, FName mod, int flags, DAngle angle)
|
|
{
|
|
IFVIRTUALPTR(target, AActor, DamageMobj)
|
|
{
|
|
VMValue params[7] = { target, inflictor, source, damage, mod.GetIndex(), flags, angle.Degrees };
|
|
VMReturn ret;
|
|
int retval;
|
|
ret.IntAt(&retval);
|
|
VMCall(func, params, 7, &ret, 1);
|
|
return retval;
|
|
}
|
|
else
|
|
{
|
|
return DoDamageMobj(target, inflictor, source, damage, mod, flags, angle);
|
|
}
|
|
}
|
|
|
|
|
|
void P_PoisonMobj (AActor *target, AActor *inflictor, AActor *source, int damage, int duration, int period, FName type)
|
|
{
|
|
// Check for invulnerability.
|
|
if (!(inflictor->flags6 & MF6_POISONALWAYS))
|
|
{
|
|
if (target->flags2 & MF2_INVULNERABLE)
|
|
{ // actor is invulnerable
|
|
if (target->player == NULL)
|
|
{
|
|
if (!(inflictor->flags3 & MF3_FOILINVUL))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
target->Poisoner = source;
|
|
target->PoisonDamageTypeReceived = type;
|
|
target->PoisonPeriodReceived = period;
|
|
|
|
if (inflictor->flags6 & MF6_ADDITIVEPOISONDAMAGE)
|
|
{
|
|
target->PoisonDamageReceived += damage;
|
|
}
|
|
else
|
|
{
|
|
target->PoisonDamageReceived = damage;
|
|
}
|
|
|
|
if (inflictor->flags6 & MF6_ADDITIVEPOISONDURATION)
|
|
{
|
|
target->PoisonDurationReceived += duration;
|
|
}
|
|
else
|
|
{
|
|
target->PoisonDurationReceived = duration;
|
|
}
|
|
|
|
}
|
|
|
|
DEFINE_ACTION_FUNCTION(AActor, PoisonMobj)
|
|
{
|
|
PARAM_SELF_PROLOGUE(AActor);
|
|
PARAM_OBJECT_NOT_NULL(inflictor, AActor);
|
|
PARAM_OBJECT(source, AActor);
|
|
PARAM_INT(damage);
|
|
PARAM_INT(duration);
|
|
PARAM_INT(period);
|
|
PARAM_NAME(mod);
|
|
P_PoisonMobj(self, inflictor, source, damage, duration, period, mod);
|
|
return 0;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// OkayToSwitchTarget
|
|
//
|
|
//==========================================================================
|
|
|
|
bool AActor::OkayToSwitchTarget(AActor *other)
|
|
{
|
|
if (other == this)
|
|
return false; // [RH] Don't hate self (can happen when shooting barrels)
|
|
|
|
if (other->flags7 & MF7_NEVERTARGET)
|
|
return false; // never EVER target me!
|
|
|
|
if (!(other->flags & MF_SHOOTABLE))
|
|
return false; // Don't attack things that can't be hurt
|
|
|
|
if ((flags4 & MF4_NOTARGETSWITCH) && target != NULL)
|
|
return false; // Don't switch target if not allowed
|
|
|
|
if ((master != NULL && other->IsA(master->GetClass())) || // don't attack your master (or others of its type)
|
|
(other->master != NULL && IsA(other->master->GetClass()))) // don't attack your minion (or those of others of your type)
|
|
{
|
|
if (!IsHostile (other) && // allow target switch if other is considered hostile
|
|
(other->tid != TIDtoHate || TIDtoHate == 0) && // or has the tid we hate
|
|
other->TIDtoHate == TIDtoHate) // or has different hate information
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ((flags7 & MF7_NOINFIGHTSPECIES) && GetSpecies() == other->GetSpecies())
|
|
return false; // Don't fight own species.
|
|
|
|
if ((other->flags3 & MF3_NOTARGET) &&
|
|
(other->tid != TIDtoHate || TIDtoHate == 0) &&
|
|
!IsHostile (other))
|
|
return false;
|
|
if (threshold != 0 && !(flags4 & MF4_QUICKTORETALIATE))
|
|
return false;
|
|
if (IsFriend (other))
|
|
{ // [RH] Friendlies don't target other friendlies
|
|
return false;
|
|
}
|
|
|
|
int infight;
|
|
if (flags7 & MF7_FORCEINFIGHTING) infight = 1;
|
|
else if (flags5 & MF5_NOINFIGHTING) infight = -1;
|
|
else infight = Level->GetInfighting();
|
|
|
|
if (infight < 0 && other->player == NULL && !IsHostile (other))
|
|
{
|
|
return false; // infighting off: Non-friendlies don't target other non-friendlies
|
|
}
|
|
if (TIDtoHate != 0 && TIDtoHate == other->TIDtoHate)
|
|
return false; // [RH] Don't target "teammates"
|
|
if (other->player != NULL && (flags4 & MF4_NOHATEPLAYERS))
|
|
return false; // [RH] Don't target players
|
|
if (target != NULL && target->health > 0 &&
|
|
TIDtoHate != 0 && target->tid == TIDtoHate && pr_switcher() < 128 &&
|
|
P_CheckSight (this, target))
|
|
return false; // [RH] Don't be too quick to give up things we hate
|
|
|
|
return true;
|
|
}
|
|
|
|
DEFINE_ACTION_FUNCTION(AActor, OkayToSwitchTarget)
|
|
{
|
|
PARAM_SELF_PROLOGUE(AActor);
|
|
PARAM_OBJECT(other, AActor);
|
|
ACTION_RETURN_BOOL(self->OkayToSwitchTarget(other));
|
|
}
|
|
|
|
bool AActor::CallOkayToSwitchTarget(AActor *other)
|
|
{
|
|
IFVIRTUAL(AActor, OkayToSwitchTarget)
|
|
{
|
|
VMValue params[] = { (DObject*)this, other };
|
|
int retv;
|
|
VMReturn ret(&retv);
|
|
VMCall(func, params, 2, &ret, 1);
|
|
return !!retv;
|
|
}
|
|
return OkayToSwitchTarget(other);
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// P_PoisonPlayer - Sets up all data concerning poisoning
|
|
//
|
|
// poisoner is the object directly responsible for poisoning the player,
|
|
// such as a missile. source is the actor responsible for creating the
|
|
// poisoner.
|
|
//
|
|
//==========================================================================
|
|
|
|
bool P_PoisonPlayer (player_t *player, AActor *poisoner, AActor *source, int poison)
|
|
{
|
|
if ((player->cheats & CF_GODMODE) || (player->mo->flags2 & MF2_INVULNERABLE) || (player->cheats & CF_GODMODE2) ||
|
|
(player->mo->flags5 & MF5_NODAMAGE))
|
|
{
|
|
return false;
|
|
}
|
|
if (source != NULL && source->player != player && player->mo->IsTeammate (source))
|
|
{
|
|
poison = (int)(poison * player->mo->Level->teamdamage);
|
|
}
|
|
if (poison > 0)
|
|
{
|
|
player->poisoncount += poison;
|
|
player->poisoner = source;
|
|
if (poisoner == NULL)
|
|
{
|
|
player->poisontype = player->poisonpaintype = NAME_None;
|
|
}
|
|
else
|
|
{ // We need to record these in case the poisoner disappears before poisoncount reaches 0.
|
|
player->poisontype = poisoner->DamageType;
|
|
player->poisonpaintype = poisoner->PainType != NAME_None ? poisoner->PainType : poisoner->DamageType;
|
|
}
|
|
if(player->poisoncount > 100)
|
|
{
|
|
player->poisoncount = 100;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
DEFINE_ACTION_FUNCTION(_PlayerInfo, PoisonPlayer)
|
|
{
|
|
PARAM_SELF_STRUCT_PROLOGUE(player_t);
|
|
PARAM_OBJECT(poisoner, AActor);
|
|
PARAM_OBJECT(source, AActor);
|
|
PARAM_INT(poison);
|
|
ACTION_RETURN_BOOL(P_PoisonPlayer(self, poisoner, source, poison));
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// P_PoisonDamage - Similar to P_DamageMobj
|
|
//
|
|
//==========================================================================
|
|
|
|
void P_PoisonDamage (player_t *player, AActor *source, int damage, bool playPainSound)
|
|
{
|
|
AActor *target;
|
|
|
|
if (player == NULL)
|
|
{
|
|
return;
|
|
}
|
|
target = player->mo;
|
|
if (target->health <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// [MC] This must be checked before any modifications. Otherwise, power amplifiers
|
|
// may result in doing too much damage that cannot be negated by regular buddha,
|
|
// which is inconsistent. The raw damage must be the only determining factor for
|
|
// determining if telefrag is actually desired.
|
|
const bool telefragDamage = (damage >= TELEFRAG_DAMAGE && !(target->flags7 & MF7_LAXTELEFRAGDMG));
|
|
|
|
if ((player->cheats & CF_GODMODE2) || (target->flags5 & MF5_NODAMAGE) || //These two are never subjected to telefrag thresholds.
|
|
(!telefragDamage && ((target->flags2 & MF2_INVULNERABLE) || (player->cheats & CF_GODMODE))))
|
|
{ // target is invulnerable
|
|
return;
|
|
}
|
|
// Take half damage in trainer mode
|
|
damage = int(damage * G_SkillProperty(SKILLP_DamageFactor) * sv_damagefactorplayer);
|
|
// Handle passive damage modifiers (e.g. PowerProtection)
|
|
damage = target->GetModifiedDamage(player->poisontype, damage, true);
|
|
// Modify with damage factors
|
|
damage = target->ApplyDamageFactor(player->poisontype, damage);
|
|
|
|
if (damage <= 0)
|
|
{ // Damage was reduced to 0, so don't bother further.
|
|
return;
|
|
}
|
|
if (damage >= player->health
|
|
&& (G_SkillProperty(SKILLP_AutoUseHealth) || deathmatch)
|
|
&& !player->morphTics)
|
|
{ // Try to use some inventory health
|
|
P_AutoUseHealth(player, damage - player->health+1);
|
|
}
|
|
player->health -= damage; // mirror mobj health here for Dave
|
|
if (player->health < 50 && !deathmatch)
|
|
{
|
|
P_AutoUseStrifeHealth(player);
|
|
}
|
|
if (player->health < 0)
|
|
{
|
|
player->health = 0;
|
|
}
|
|
player->attacker = source;
|
|
|
|
//
|
|
// do the damage
|
|
//
|
|
target->health -= damage;
|
|
if (target->health <= 0)
|
|
{ // Death
|
|
int buddha = hasBuddha(player);
|
|
if (telefragDamage && buddha == 1) buddha = 0;
|
|
if (buddha)
|
|
{ // [SP] Save the player...
|
|
player->health = target->health = 1;
|
|
}
|
|
else
|
|
{
|
|
target->special1 = damage;
|
|
if (player && !player->morphTics)
|
|
{ // Check for flame death
|
|
if ((player->poisontype == NAME_Fire) && (target->health > -50) && (damage > 25))
|
|
{
|
|
target->DamageType = NAME_Fire;
|
|
}
|
|
else
|
|
{
|
|
target->DamageType = player->poisontype;
|
|
}
|
|
}
|
|
target->CallDie(source, source);
|
|
return;
|
|
}
|
|
}
|
|
if (!(target->Level->time&63) && playPainSound)
|
|
{
|
|
FState *painstate = target->FindState(NAME_Pain, player->poisonpaintype);
|
|
if (painstate != NULL)
|
|
{
|
|
target->SetState(painstate);
|
|
}
|
|
}
|
|
/*
|
|
if((P_Random() < target->info->painchance)
|
|
&& !(target->flags&MF_SKULLFLY))
|
|
{
|
|
target->flags |= MF_JUSTHIT; // fight back!
|
|
P_SetMobjState(target, target->info->painstate);
|
|
}
|
|
*/
|
|
}
|
|
|
|
DEFINE_ACTION_FUNCTION(_PlayerInfo, PoisonDamage)
|
|
{
|
|
PARAM_SELF_STRUCT_PROLOGUE(player_t);
|
|
PARAM_OBJECT(source, AActor);
|
|
PARAM_INT(damage);
|
|
PARAM_BOOL(playsound);
|
|
P_PoisonDamage(self, source, damage, playsound);
|
|
return 0;
|
|
}
|
|
|
|
CCMD (kill)
|
|
{
|
|
if (argv.argc() > 1)
|
|
{
|
|
if (CheckCheatmode ())
|
|
return;
|
|
|
|
if (!stricmp (argv[1], "monsters"))
|
|
{
|
|
// Kill all the monsters
|
|
if (CheckCheatmode ())
|
|
return;
|
|
|
|
Net_WriteByte (DEM_GENERICCHEAT);
|
|
Net_WriteByte (CHT_MASSACRE);
|
|
}
|
|
else if (!stricmp (argv[1], "baddies"))
|
|
{
|
|
// Kill all the unfriendly monsters
|
|
if (CheckCheatmode ())
|
|
return;
|
|
|
|
Net_WriteByte (DEM_GENERICCHEAT);
|
|
Net_WriteByte (CHT_MASSACRE2);
|
|
}
|
|
else
|
|
{
|
|
Net_WriteByte (DEM_KILLCLASSCHEAT);
|
|
Net_WriteString (argv[1]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If suiciding is disabled, then don't do it.
|
|
if (dmflags2 & DF2_NOSUICIDE)
|
|
return;
|
|
|
|
// Kill the player
|
|
Net_WriteByte (DEM_SUICIDE);
|
|
}
|
|
C_HideConsole ();
|
|
}
|
|
|
|
CCMD(remove)
|
|
{
|
|
if (argv.argc() == 2)
|
|
{
|
|
if (CheckCheatmode())
|
|
return;
|
|
|
|
Net_WriteByte(DEM_REMOVE);
|
|
Net_WriteString(argv[1]);
|
|
C_HideConsole();
|
|
}
|
|
else
|
|
{
|
|
Printf("Usage: remove <actor class name>\n");
|
|
return;
|
|
}
|
|
|
|
}
|