//----------------------------------------------------------------------------- // // Copyright 1993-1996 id Software // Copyright 1994-1996 Raven Software // Copyright 1998-1998 Chi Hoang, Lee Killough, Jim Flynn, Rand Phares, Ty Halderman // Copyright 1999-2016 Randy Heit // Copyright 2002-2017 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: // Moving object handling. Spawn functions. // //----------------------------------------------------------------------------- /* For code that originates from ZDoom the following applies: ** **--------------------------------------------------------------------------- ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** ** 1. Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** 3. The name of the author may not be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **--------------------------------------------------------------------------- ** */ // HEADER FILES ------------------------------------------------------------ #include #include "templates.h" #include "i_system.h" #include "m_random.h" #include "doomdef.h" #include "p_local.h" #include "p_maputl.h" #include "p_lnspec.h" #include "p_effect.h" #include "p_terrain.h" #include "hu_stuff.h" #include "v_video.h" #include "c_dispatch.h" #include "b_bot.h" //Added by MC: #include "a_sharedglobal.h" #include "gi.h" #include "sbar.h" #include "p_acs.h" #include "cmdlib.h" #include "decallib.h" #include "a_keys.h" #include "p_conversation.h" #include "g_game.h" #include "teaminfo.h" #include "r_sky.h" #include "d_event.h" #include "p_enemy.h" #include "gstrings.h" #include "r_renderer.h" #include "po_man.h" #include "p_spec.h" #include "p_checkposition.h" #include "serializer.h" #include "r_utility.h" #include "thingdef.h" #include "d_player.h" #include "g_levellocals.h" #include "a_morph.h" #include "events.h" #include "actorinlines.h" #include "a_dynlight.h" #include "fragglescript/t_fs.h" // MACROS ------------------------------------------------------------------ #define WATER_SINK_FACTOR 0.125 #define WATER_SINK_SMALL_FACTOR 0.25 #define WATER_SINK_SPEED 0.5 #define WATER_JUMP_SPEED 3.5 // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- void G_PlayerReborn (int player); // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- static void PlayerLandedOnThing (AActor *mo, AActor *onmobj); // EXTERNAL DATA DECLARATIONS ---------------------------------------------- EXTERN_CVAR (Int, cl_rockettrails) // PRIVATE DATA DEFINITIONS ------------------------------------------------ static FRandom pr_explodemissile ("ExplodeMissile"); FRandom pr_bounce ("Bounce"); static FRandom pr_reflect ("Reflect"); static FRandom pr_nightmarerespawn ("NightmareRespawn"); static FRandom pr_botspawnmobj ("BotSpawnActor"); static FRandom pr_spawnmapthing ("SpawnMapThing"); static FRandom pr_spawnpuff ("SpawnPuff"); static FRandom pr_spawnblood ("SpawnBlood"); static FRandom pr_splatter ("BloodSplatter"); static FRandom pr_takedamage ("TakeDamage"); static FRandom pr_splat ("FAxeSplatter"); static FRandom pr_ripperblood ("RipperBlood"); static FRandom pr_chunk ("Chunk"); static FRandom pr_checkmissilespawn ("CheckMissileSpawn"); static FRandom pr_spawnmissile ("SpawnMissile"); static FRandom pr_missiledamage ("MissileDamage"); static FRandom pr_multiclasschoice ("MultiClassChoice"); static FRandom pr_rockettrail("RocketTrail"); static FRandom pr_uniquetid("UniqueTID"); // PUBLIC DATA DEFINITIONS ------------------------------------------------- FRandom pr_spawnmobj ("SpawnActor"); CUSTOM_CVAR (Float, sv_gravity, 800.f, CVAR_SERVERINFO|CVAR_NOSAVE) { level.gravity = self; } CVAR (Bool, cl_missiledecals, true, CVAR_ARCHIVE) CVAR (Bool, addrocketexplosion, false, CVAR_ARCHIVE) CVAR (Int, cl_pufftype, 0, CVAR_ARCHIVE); CVAR (Int, cl_bloodtype, 0, CVAR_ARCHIVE); // CODE -------------------------------------------------------------------- IMPLEMENT_CLASS(AActor, false, true) IMPLEMENT_POINTERS_START(AActor) IMPLEMENT_POINTER(target) IMPLEMENT_POINTER(lastenemy) IMPLEMENT_POINTER(tracer) IMPLEMENT_POINTER(goal) IMPLEMENT_POINTER(LastLookActor) IMPLEMENT_POINTER(Inventory) IMPLEMENT_POINTER(LastHeard) IMPLEMENT_POINTER(master) IMPLEMENT_POINTER(Poisoner) IMPLEMENT_POINTER(alternative) IMPLEMENT_POINTERS_END AActor::~AActor () { // Please avoid calling the destructor directly (or through delete)! // Use Destroy() instead. } //========================================================================== // // AActor :: Serialize // //========================================================================== #define A(a,b) ((a), (b), def->b) void AActor::Serialize(FSerializer &arc) { AActor *def = GetDefault(); Super::Serialize(arc); arc .Sprite("sprite", sprite, &def->sprite) A("pos", __Pos) A("angles", Angles) A("frame", frame) A("scale", Scale) A("renderstyle", RenderStyle) A("renderflags", renderflags) A("picnum", picnum) A("floorpic", floorpic) A("ceilingpic", ceilingpic) A("tidtohate", TIDtoHate) A("lastlookpn", LastLookPlayerNumber) ("lastlookactor", LastLookActor) A("effects", effects) A("fountaincolor", fountaincolor) A("alpha", Alpha) A("fillcolor", fillcolor) A("sector", Sector) A("floorz", floorz) A("ceilingz", ceilingz) A("dropoffz", dropoffz) A("floorsector", floorsector) A("ceilingsector", ceilingsector) A("radius", radius) A("renderradius", renderradius) A("height", Height) A("ppassheight", projectilepassheight) A("vel", Vel) A("tics", tics) A("state", state) A("damage", DamageVal) .Terrain("floorterrain", floorterrain, &def->floorterrain) A("projectilekickback", projectileKickback) A("flags", flags) A("flags2", flags2) A("flags3", flags3) A("flags4", flags4) A("flags5", flags5) A("flags6", flags6) A("flags7", flags7) A("flags8", flags8) A("weaponspecial", weaponspecial) A("special1", special1) A("special2", special2) A("specialf1", specialf1) A("specialf2", specialf2) A("health", health) A("movedir", movedir) A("visdir", visdir) A("movecount", movecount) A("strafecount", strafecount) ("target", target) ("lastenemy", lastenemy) ("lastheard", LastHeard) A("reactiontime", reactiontime) A("threshold", threshold) A("player", player) A("spawnpoint", SpawnPoint) A("spawnangle", SpawnAngle) A("starthealth", StartHealth) A("skillrespawncount", skillrespawncount) ("tracer", tracer) A("floorclip", Floorclip) A("tid", tid) A("special", special) .Args("args", args, def->args, special) A("accuracy", accuracy) A("stamina", stamina) ("goal", goal) A("waterlevel", waterlevel) A("boomwaterlevel", boomwaterlevel) A("minmissilechance", MinMissileChance) A("spawnflags", SpawnFlags) ("inventory", Inventory) A("inventoryid", InventoryID) A("floatbobphase", FloatBobPhase) A("floatbobstrength", FloatBobStrength) A("translation", Translation) A("bloodcolor", BloodColor) A("bloodtranslation", BloodTranslation) A("seesound", SeeSound) A("attacksound", AttackSound) A("paimsound", PainSound) A("deathsound", DeathSound) A("activesound", ActiveSound) A("usesound", UseSound) A("bouncesound", BounceSound) A("wallbouncesound", WallBounceSound) A("crushpainsound", CrushPainSound) A("speed", Speed) A("floatspeed", FloatSpeed) A("mass", Mass) A("painchance", PainChance) A("spawnstate", SpawnState) A("seestate", SeeState) A("meleestate", MeleeState) A("missilestate", MissileState) A("maxdropoffheight", MaxDropOffHeight) A("maxstepheight", MaxStepHeight) A("bounceflags", BounceFlags) A("bouncefactor", bouncefactor) A("wallbouncefactor", wallbouncefactor) A("bouncecount", bouncecount) A("maxtargetrange", maxtargetrange) A("meleethreshold", meleethreshold) A("meleerange", meleerange) A("damagetype", DamageType) A("damagetypereceived", DamageTypeReceived) A("paintype", PainType) A("deathtype", DeathType) A("gravity", Gravity) A("fastchasestrafecount", FastChaseStrafeCount) ("master", master) A("smokecounter", smokecounter) ("blockingmobj", BlockingMobj) A("blockingline", BlockingLine) A("blocking3dfloor", Blocking3DFloor) A("blockingceiling", BlockingCeiling) A("blockingfloor", BlockingFloor) A("visibletoteam", VisibleToTeam) A("pushfactor", pushfactor) A("species", Species) A("score", Score) A("designatedteam", DesignatedTeam) A("lastpush", lastpush) A("activationtype", activationtype) A("lastbump", lastbump) A("painthreshold", PainThreshold) A("damagefactor", DamageFactor) A("damagemultiply", DamageMultiply) A("waveindexxy", WeaveIndexXY) A("weaveindexz", WeaveIndexZ) A("pdmgreceived", PoisonDamageReceived) A("pdurreceived", PoisonDurationReceived) A("ppreceived", PoisonPeriodReceived) ("poisoner", Poisoner) A("posiondamage", PoisonDamage) A("poisonduration", PoisonDuration) A("poisonperiod", PoisonPeriod) A("poisondamagetype", PoisonDamageType) A("poisondmgtypereceived", PoisonDamageTypeReceived) A("conversationroot", ConversationRoot) A("conversation", Conversation) A("friendplayer", FriendPlayer) A("telefogsourcetype", TeleFogSourceType) A("telefogdesttype", TeleFogDestType) A("ripperlevel", RipperLevel) A("riplevelmin", RipLevelMin) A("riplevelmax", RipLevelMax) A("devthreshold", DefThreshold) A("spriteangle", SpriteAngle) A("spriterotation", SpriteRotation) ("alternative", alternative) A("cameraheight", CameraHeight) A("camerafov", CameraFOV) A("tag", Tag) A("visiblestartangle",VisibleStartAngle) A("visibleendangle",VisibleEndAngle) A("visiblestartpitch",VisibleStartPitch) A("visibleendpitch",VisibleEndPitch) A("woundhealth", WoundHealth) A("rdfactor", RadiusDamageFactor) A("selfdamagefactor", SelfDamageFactor) A("stealthalpha", StealthAlpha) A("renderhidden", RenderHidden) A("renderrequired", RenderRequired) A("friendlyseeblocks", friendlyseeblocks) A("spawntime", SpawnTime) A("spawnorder", SpawnOrder) A("friction", Friction); } #undef A //========================================================================== // // This must be done after the world is set up. // //========================================================================== void AActor::PostSerialize() { touching_sectorlist = nullptr; touching_rendersectors = nullptr; LinkToWorld(nullptr, false, Sector); AddToHash(); if (player) { if (playeringame[player - players] && player->cls != NULL && !(flags4 & MF4_NOSKIN) && state->sprite == GetDefaultByType(player->cls)->SpawnState->sprite) { // Give player back the skin sprite = Skins[player->userinfo.GetSkin()].sprite; } if (Speed == 0) { Speed = GetDefault()->Speed; } } ClearInterpolation(); UpdateWaterLevel(false); } AActor &AActor::operator= (const AActor &other) { memcpy (&snext, &other.snext, (uint8_t *)&this[1] - (uint8_t *)&snext); return *this; } //========================================================================== // // AActor::InStateSequence // // Checks whether the current state is in a contiguous sequence that // starts with basestate // //========================================================================== static int InStateSequence(FState * newstate, FState * basestate) { if (basestate == NULL) return false; FState * thisstate = basestate; do { if (newstate == thisstate) return true; basestate = thisstate; thisstate = thisstate->GetNextState(); } while (thisstate == basestate+1); return false; } DEFINE_ACTION_FUNCTION_NATIVE(AActor, InStateSequence, InStateSequence) { PARAM_PROLOGUE; PARAM_POINTER(newstate, FState); PARAM_POINTER(basestate, FState); ACTION_RETURN_BOOL(InStateSequence(newstate, basestate)); } DEFINE_ACTION_FUNCTION_NATIVE(FState, InStateSequence, InStateSequence) { PARAM_SELF_STRUCT_PROLOGUE(FState); PARAM_POINTER(basestate, FState); ACTION_RETURN_BOOL(InStateSequence(self, basestate)); } bool AActor::IsMapActor() { // [SP] Don't remove owned inventory objects. return (!IsKindOf(NAME_Inventory) || GC::ReadBarrier(PointerVar(NAME_Owner)) == nullptr); } //========================================================================== // // AActor::GetTics // // Get the actual duration of the next state // We are using a state flag now to indicate a state that should be // accelerated in Fast mode or slowed in Slow mode. // //========================================================================== int AActor::GetTics(FState * newstate) { int tics = newstate->GetTics(); if (isFast() && newstate->GetFast()) { return tics - (tics>>1); } else if (isSlow() && newstate->GetSlow()) { return tics<<1; } return tics; } //========================================================================== // // AActor::SetState // // Returns true if the mobj is still present. // //========================================================================== bool AActor::SetState (FState *newstate, bool nofunction) { if (debugfile && player && (player->cheats & CF_PREDICTING)) fprintf (debugfile, "for pl %td: SetState while predicting!\n", player-players); do { if (newstate == NULL) { state = NULL; Destroy (); return false; } int prevsprite, newsprite; if (state != NULL) { prevsprite = state->sprite; } else { prevsprite = -1; } if (!(newstate->UseFlags & SUF_ACTOR)) { Printf(TEXTCOLOR_RED "State %s in %s not flagged for use as an actor sprite\n", FState::StaticGetStateName(newstate).GetChars(), GetClass()->TypeName.GetChars()); state = nullptr; Destroy(); return false; } state = newstate; tics = GetTics(newstate); renderflags = (renderflags & ~RF_FULLBRIGHT) | ActorRenderFlags::FromInt (newstate->GetFullbright()); newsprite = newstate->sprite; if (newsprite != SPR_FIXED) { // okay to change sprite and/or frame if (!newstate->GetSameFrame()) { // okay to change frame frame = newstate->GetFrame(); } if (newsprite != SPR_NOCHANGE) { // okay to change sprite if (!(flags4 & MF4_NOSKIN) && newsprite == SpawnState->sprite) { // [RH] If the new sprite is the same as the original sprite, and // this actor is attached to a player, use the player's skin's // sprite. If a player is not attached, do not change the sprite // unless it is different from the previous state's sprite; a // player may have been attached, died, and respawned elsewhere, // and we do not want to lose the skin on the body. If it wasn't // for Dehacked, I would move sprite changing out of the states // altogether, since actors rarely change their sprites after // spawning. if (player != NULL && Skins.Size() > 0) { sprite = Skins[player->userinfo.GetSkin()].sprite; } else if (newsprite != prevsprite) { sprite = newsprite; } } else { sprite = newsprite; } } } if (!nofunction) { FState *returned_state; FStateParamInfo stp = { newstate, STATE_Actor, PSP_WEAPON }; if (newstate->CallAction(this, this, &stp, &returned_state)) { // Check whether the called action function resulted in destroying the actor if (ObjectFlags & OF_EuthanizeMe) { return false; } if (returned_state != NULL) { // The action was an A_Jump-style function that wants to change the next state. newstate = returned_state; tics = 0; // make sure we loop and set the new state properly continue; } } } newstate = newstate->GetNextState(); } while (tics == 0); SetDynamicLights(); return true; } DEFINE_ACTION_FUNCTION(AActor, SetState) { PARAM_SELF_PROLOGUE(AActor); PARAM_POINTER(state, FState); PARAM_BOOL(nofunction); ACTION_RETURN_BOOL(self->SetState(state, nofunction)); }; //============================================================================ // // AActor :: DestroyAllInventory // //============================================================================ void AActor::DestroyAllInventory () { AActor *inv = Inventory; if (inv != nullptr) { TArray toDelete; // Delete the list in a two stage approach. // This is necessary because an item may destroy another item (e.g. sister weapons) // which would break the list and leave parts of it undestroyed, maybe doing bad things later. while (inv != nullptr) { toDelete.Push(inv); auto item = inv->Inventory; inv->Inventory = nullptr; inv->PointerVar(NAME_Owner) = nullptr; inv = item; } for (auto p : toDelete) { // the item may already have been deleted by another one, so check this here to avoid problems. if (!(p->ObjectFlags & OF_EuthanizeMe)) { p->Destroy(); } } } } DEFINE_ACTION_FUNCTION(AActor, DestroyAllInventory) { PARAM_SELF_PROLOGUE(AActor); self->DestroyAllInventory(); return 0; } //============================================================================ // // AActor :: UseInventory // // Attempts to use an item. If the use succeeds, one copy of the item is // removed from the inventory. If all copies are removed, then the item is // destroyed. // //============================================================================ bool AActor::UseInventory (AActor *item) { IFVIRTUAL(AActor, UseInventory) { VMValue params[] = { this, item }; int retval = 0; VMReturn ret(&retval); VMCall(func, params, 2, &ret, 1); return !!retval; } return false; } //=========================================================================== // // AActor :: DropInventory // // Removes a single copy of an item and throws it out in front of the actor. // //=========================================================================== AActor *AActor::DropInventory (AActor *item, int amt) { IFVM(Actor, DropInventory) { VMValue params[] = { this, item, amt }; AActor *retval = 0; VMReturn ret((void**)&retval); VMCall(func, params, 3, &ret, 1); return retval; } return nullptr; } //============================================================================ // // AActor :: FindInventory // //============================================================================ AActor *AActor::FindInventory (PClassActor *type, bool subclass) { AActor *item; if (type == NULL) { return NULL; } for (item = Inventory; item != NULL; item = item->Inventory) { if (!subclass) { if (item->GetClass() == type) { break; } } else { if (item->IsKindOf(type)) { break; } } } return item; } AActor *AActor::FindInventory (FName type, bool subclass) { return FindInventory(PClass::FindActor(type), subclass); } DEFINE_ACTION_FUNCTION(AActor, FindInventory) { PARAM_SELF_PROLOGUE(AActor); PARAM_CLASS(type, AActor); PARAM_BOOL(subclass); ACTION_RETURN_OBJECT(self->FindInventory(type, subclass)); } //============================================================================ // // AActor :: GiveInventoryType // //============================================================================ AActor *AActor::GiveInventoryType (PClassActor *type) { if (type != nullptr) { auto item = Spawn (type); if (!CallTryPickup (item, this)) { item->Destroy (); return nullptr; } return item; } return nullptr; } DEFINE_ACTION_FUNCTION(AActor, GiveInventoryType) { PARAM_SELF_PROLOGUE(AActor); PARAM_CLASS(type, AActor); ACTION_RETURN_OBJECT(self->GiveInventoryType(type)); } //============================================================================ // // AActor :: ClearInventory // // Clears the inventory of a single actor. // //============================================================================ void AActor::ClearInventory() { IFVIRTUAL(AActor, ClearInventory) { VMValue params[] = { this }; VMCall(func, params, 1, nullptr, 0); } } //============================================================================ // // AActor :: CopyFriendliness // // Makes this actor hate (or like) the same things another actor does. // //============================================================================ void AActor::CopyFriendliness (AActor *other, bool changeTarget, bool resetHealth) { level.total_monsters -= CountsAsKill(); TIDtoHate = other->TIDtoHate; LastLookActor = other->LastLookActor; LastLookPlayerNumber = other->LastLookPlayerNumber; flags = (flags & ~MF_FRIENDLY) | (other->flags & MF_FRIENDLY); flags3 = (flags3 & ~(MF3_NOSIGHTCHECK | MF3_HUNTPLAYERS)) | (other->flags3 & (MF3_NOSIGHTCHECK | MF3_HUNTPLAYERS)); flags4 = (flags4 & ~(MF4_NOHATEPLAYERS | MF4_BOSSSPAWNED)) | (other->flags4 & (MF4_NOHATEPLAYERS | MF4_BOSSSPAWNED)); FriendPlayer = other->FriendPlayer; DesignatedTeam = other->DesignatedTeam; if (changeTarget && other->target != NULL && !(other->target->flags3 & MF3_NOTARGET) && !(other->target->flags7 & MF7_NEVERTARGET)) { // LastHeard must be set as well so that A_Look can react to the new target if called LastHeard = target = other->target; } if (resetHealth) health = SpawnHealth(); level.total_monsters += CountsAsKill(); } DEFINE_ACTION_FUNCTION(AActor, CopyFriendliness) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT_NOT_NULL(other, AActor); PARAM_BOOL(changetarget); PARAM_BOOL(resethealth); self->CopyFriendliness(other, changetarget, resethealth); return 0; } //--------------------------------------------------------------------------- // // FUNC P_GetRealMaxHealth // // Taken out of P_GiveBody so that the bot code can also use it to decide // whether to pick up an item or not. // //--------------------------------------------------------------------------- int P_GetRealMaxHealth(AActor *actor, int max) { // Max is 0 by default, preserving default behavior for P_GiveBody() // calls while supporting health pickups. auto player = actor->player; if (max <= 0) { max = actor->GetMaxHealth(true); // [MH] First step in predictable generic morph effects if (player->morphTics) { if (player->MorphStyle & MORPH_FULLHEALTH) { if (!(player->MorphStyle & MORPH_ADDSTAMINA)) { max -= actor->stamina + actor->IntVar(NAME_BonusHealth); } } else // old health behaviour { max = MAXMORPHHEALTH; if (player->MorphStyle & MORPH_ADDSTAMINA) { max += actor->stamina + actor->IntVar(NAME_BonusHealth); } } } } else { // Bonus health should be added on top of the item's limit. if (player->morphTics == 0 || (player->MorphStyle & MORPH_ADDSTAMINA)) { max += actor->IntVar(NAME_BonusHealth); } } return max; } //--------------------------------------------------------------------------- // // FUNC P_GiveBody // // Returns false if the body isn't needed at all. // //--------------------------------------------------------------------------- bool P_GiveBody(AActor *actor, int num, int max) { if (actor->health <= 0 || (actor->player != NULL && actor->player->playerstate == PST_DEAD)) { // Do not heal dead things. return false; } player_t *player = actor->player; num = clamp(num, -65536, 65536); // prevent overflows for bad values if (player != NULL) { max = P_GetRealMaxHealth(player->mo, max); // do not pass voodoo dolls in here. // [RH] For Strife: A negative value sets you up with a percentage of your full health. if (num < 0) { num = max * -num / 100; if (player->health < num) { player->health = num; actor->health = num; return true; } } else if (num > 0) { if (player->health < max) { num = int(num * G_SkillProperty(SKILLP_HealthFactor)); if (num < 1) num = 1; player->health += num; if (player->health > max) { player->health = max; } actor->health = player->health; return true; } } } else { // Parameter value for max is ignored on monsters, preserving original // behaviour of health as well as on existing calls to P_GiveBody(). max = actor->SpawnHealth(); if (num < 0) { num = max * -num / 100; if (actor->health < num) { actor->health = num; return true; } } else if (actor->health < max) { actor->health += num; if (actor->health > max) { actor->health = max; } return true; } } return false; } DEFINE_ACTION_FUNCTION(AActor, GiveBody) { PARAM_SELF_PROLOGUE(AActor); PARAM_INT(num); PARAM_INT(max); ACTION_RETURN_BOOL(P_GiveBody(self, num, max)); } //============================================================================ // // AActor :: CheckLocalView // // Returns true if this actor is local for the player. Here, local means the // player is either looking out this actor's eyes, or this actor is the player // and the player is looking out the eyes of something non-"sentient." // //============================================================================ bool AActor::CheckLocalView (int playernum) const { if (players[playernum].camera == this) { return true; } if (players[playernum].mo != this || players[playernum].camera == NULL) { return false; } if (players[playernum].camera->player == NULL && !(players[playernum].camera->flags3 & MF3_ISMONSTER)) { return true; } return false; } DEFINE_ACTION_FUNCTION(AActor, CheckLocalView) { PARAM_SELF_PROLOGUE(AActor); PARAM_INT(cp); ACTION_RETURN_BOOL(self->CheckLocalView(cp)); } //============================================================================ // // AActor :: IsInsideVisibleAngles // // Returns true if this actor is within viewing angle/pitch visibility. // //============================================================================ bool AActor::IsInsideVisibleAngles() const { // Don't bother masking if not wanted. if (!(renderflags & RF_MASKROTATION)) return true; if (players[consoleplayer].camera == nullptr) return true; DAngle anglestart = VisibleStartAngle; DAngle angleend = VisibleEndAngle; DAngle pitchstart = VisibleStartPitch; DAngle pitchend = VisibleEndPitch; if (anglestart > angleend) { DAngle temp = anglestart; anglestart = angleend; angleend = temp; } if (pitchstart > pitchend) { DAngle temp = pitchstart; pitchstart = pitchend; pitchend = temp; } AActor *mo = players[consoleplayer].camera; if (mo != nullptr) { DVector3 diffang = r_viewpoint.Pos - Pos(); DAngle to = diffang.Angle(); if (!(renderflags & RF_ABSMASKANGLE)) to = deltaangle(Angles.Yaw, to); if ((to >= anglestart && to <= angleend)) { to = diffang.Pitch(); if (!(renderflags & RF_ABSMASKPITCH)) to = deltaangle(Angles.Pitch, to); return !!(to >= pitchstart && to <= pitchend); } else return false; } return true; } //============================================================================ // // AActor :: IsVisibleToPlayer // // Returns true if this actor should be seen by the console player. // //============================================================================ bool AActor::IsVisibleToPlayer() const { // [BB] Safety check. This should never be NULL. Nevertheless, we return true to leave the default ZDoom behavior unaltered. if ( players[consoleplayer].camera == NULL ) return true; if (VisibleToTeam != 0 && teamplay && (signed)(VisibleToTeam-1) != players[consoleplayer].userinfo.GetTeam() ) return false; auto &vis = GetInfo()->VisibleToPlayerClass; if (vis.Size() == 0) return true; // early out for the most common case. const player_t* pPlayer = players[consoleplayer].camera->player; if (pPlayer) { for(auto cls : vis) { if (cls && pPlayer->mo->GetClass()->IsDescendantOf(cls)) { return true; } } return false; } // [BB] Passed all checks. return true; } //============================================================================ // // AActor :: ConversationAnimation // // Plays a conversation-related animation: // 0 = greeting // 1 = "yes" // 2 = "no" // //============================================================================ void AActor::ConversationAnimation (int animnum) { FState * state = NULL; switch (animnum) { case 0: state = FindState(NAME_Greetings); break; case 1: state = FindState(NAME_Yes); break; case 2: state = FindState(NAME_No); break; } if (state != NULL) SetState(state); } //============================================================================ // // AActor :: Touch // // Something just touched this actor. Normally used only for inventory items, // but some Strife monsters also use it. // //============================================================================ void AActor::Touch (AActor *toucher) { } DEFINE_ACTION_FUNCTION(AActor, Touch) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT_NOT_NULL(toucher, AActor); self->Touch(toucher); return 0; } void AActor::CallTouch(AActor *toucher) { IFVIRTUAL(AActor, Touch) { VMValue params[2] = { (DObject*)this, toucher }; VMCall(func, params, 2, nullptr, 0); } else Touch(toucher); } //============================================================================ // // AActor :: Grind // // Handles the an actor being crushed by a door, crusher or polyobject. // Originally part of P_DoCrunch(), it has been made into its own actor // function so that it could be called from a polyobject without hassle. // Bool items is true if it should destroy() dropped items, false otherwise. //============================================================================ bool AActor::Grind(bool items) { // crunch bodies to giblets if ((flags & MF_CORPSE) && !(flags3 & MF3_DONTGIB) && (health <= 0)) { FState * state = FindState(NAME_Crush); // In Heretic and Chex Quest we don't change the actor's sprite, just its size. if (state == NULL && gameinfo.dontcrunchcorpses) { flags &= ~MF_SOLID; flags3 |= MF3_DONTGIB; Height = 0; radius = 0; return false; } bool isgeneric = false; // ZDoom behavior differs from standard as crushed corpses cannot be raised. // The reason for the change was originally because of a problem with players, // see rh_log entry for February 21, 1999. Don't know if it is still relevant. if (state == NULL // Only use the default crushed state if: && !(flags & MF_NOBLOOD) // 1. the monster bleeeds, && (i_compatflags & COMPATF_CORPSEGIBS) // 2. the compat setting is on, && player == NULL) // 3. and the thing isn't a player. { isgeneric = true; state = FindState(NAME_GenericCrush); if (state != NULL && (sprites[state->sprite].numframes <= 0)) state = NULL; // If one of these tests fails, do not use that state. } if (state != NULL && !(flags & MF_ICECORPSE)) { if (this->flags4 & MF4_BOSSDEATH) { A_BossDeath(this); } flags &= ~MF_SOLID; flags3 |= MF3_DONTGIB; Height = 0; radius = 0; SetState (state); if (isgeneric) // Not a custom crush state, so colorize it appropriately. { S_Sound (this, CHAN_BODY, "misc/fallingsplat", 1, ATTN_IDLE); Translation = BloodTranslation; } return false; } if (!(flags & MF_NOBLOOD)) { if (this->flags4 & MF4_BOSSDEATH) { A_BossDeath(this); } PClassActor *i = PClass::FindActor("RealGibs"); if (i != NULL) { i = i->GetReplacement(); const AActor *defaults = GetDefaultByType (i); if (defaults->SpawnState == NULL || sprites[defaults->SpawnState->sprite].numframes == 0) { i = NULL; } } if (i == NULL) { // if there's no gib sprite don't crunch it. flags &= ~MF_SOLID; flags3 |= MF3_DONTGIB; Height = 0; radius = 0; return false; } AActor *gib = Spawn (i, Pos(), ALLOW_REPLACE); if (gib != NULL) { gib->RenderStyle = RenderStyle; gib->Alpha = Alpha; gib->Height = 0; gib->radius = 0; gib->Translation = BloodTranslation; } S_Sound (this, CHAN_BODY, "misc/fallingsplat", 1, ATTN_IDLE); } if (flags & MF_ICECORPSE) { tics = 1; Vel.Zero(); } else if (player) { flags |= MF_NOCLIP; flags3 |= MF3_DONTGIB; renderflags |= RF_INVISIBLE; } else { Destroy (); } return false; // keep checking } // killough 11/98: kill touchy things immediately if (flags6 & MF6_TOUCHY && (flags6 & MF6_ARMED || IsSentient())) { flags6 &= ~MF6_ARMED; // Disarm P_DamageMobj (this, NULL, NULL, health, NAME_Crush, DMG_FORCED); // kill object return true; // keep checking } if (!(flags & MF_SOLID) || (flags & MF_NOCLIP)) { return false; } if (!(flags & MF_SHOOTABLE)) { return false; // assume it is bloody gibs or something } return true; } DEFINE_ACTION_FUNCTION(AActor, Grind) { PARAM_SELF_PROLOGUE(AActor); PARAM_BOOL(items); ACTION_RETURN_BOOL(self->Grind(items)); } bool AActor::CallGrind(bool items) { IFVIRTUAL(AActor, Grind) { VMValue params[] = { (DObject*)this, items }; int retv; VMReturn ret(&retv); VMCall(func, params, 2, &ret, 1); return !!retv; } return Grind(items); } //============================================================================ // // AActor :: Massacre // // Called by the massacre cheat to kill monsters. Returns true if the monster // was killed and false if it was already dead. //============================================================================ bool AActor::Massacre () { int prevhealth; if (health > 0) { auto f = flags; auto f2 = flags2; flags |= MF_SHOOTABLE; flags2 &= ~(MF2_DORMANT|MF2_INVULNERABLE); do { prevhealth = health; P_DamageMobj (this, NULL, NULL, TELEFRAG_DAMAGE, NAME_Massacre); } while (health != prevhealth && health > 0); //abort if the actor wasn't hurt. if (health > 0) { // restore flags if this did not kill the monster. flags = f; flags2 = f2; } return health <= 0; } return false; } //---------------------------------------------------------------------------- // // PROC P_ExplodeMissile // //---------------------------------------------------------------------------- void P_ExplodeMissile (AActor *mo, line_t *line, AActor *target, bool onsky) { // [ZZ] line damage callback if (line) { P_ProjectileHitLinedef(mo, line); } if (mo->flags3 & MF3_EXPLOCOUNT) { if (++mo->threshold < mo->DefThreshold) { return; } } mo->Vel.Zero(); mo->effects = 0; // [RH] mo->flags &= ~MF_SHOOTABLE; FState *nextstate = nullptr; if (target != nullptr) { if (mo->flags7 & MF7_HITTARGET) mo->target = target; if (mo->flags7 & MF7_HITMASTER) mo->master = target; if (mo->flags7 & MF7_HITTRACER) mo->tracer = target; if ((target->flags & (MF_SHOOTABLE | MF_CORPSE)) || (target->flags6 & MF6_KILLED)) { if (target->flags & MF_NOBLOOD) nextstate = mo->FindState(NAME_Crash); if (nextstate == NULL) nextstate = mo->FindState(NAME_Death, NAME_Extreme); } } if (nextstate == NULL) nextstate = mo->FindState(NAME_Death); if (onsky || (line != NULL && line->special == Line_Horizon)) { if (!(mo->flags3 & MF3_SKYEXPLODE)) { // [RH] Don't explode missiles on horizon lines. mo->Destroy(); return; } nextstate = mo->FindState(NAME_Death, NAME_Sky); } if (line != NULL && cl_missiledecals) { DVector3 pos = mo->PosRelative(line); int side = P_PointOnLineSidePrecise (pos, line); if (line->sidedef[side] == NULL) side ^= 1; if (line->sidedef[side] != NULL) { FDecalBase *base = mo->DecalGenerator; if (base != NULL) { // Find the nearest point on the line, and stick a decal there DVector3 linepos; double den, frac; den = line->Delta().LengthSquared(); if (den != 0) { frac = clamp((mo->Pos().XY() - line->v1->fPos()) | line->Delta(), 0, den) / den; linepos = DVector3(line->v1->fPos() + line->Delta() * frac, pos.Z); F3DFloor * ffloor=NULL; if (line->sidedef[side^1] != NULL) { sector_t * backsector = line->sidedef[side^1]->sector; extsector_t::xfloor &xf = backsector->e->XFloor; // find a 3D-floor to stick to for(unsigned int i=0;iflags&(FF_EXISTS|FF_SOLID|FF_RENDERSIDES))==(FF_EXISTS|FF_SOLID|FF_RENDERSIDES)) { if (pos.Z <= rover->top.plane->ZatPoint(linepos) && pos.Z >= rover->bottom.plane->ZatPoint(linepos)) { ffloor = rover; break; } } } } DImpactDecal::StaticCreate(base->GetDecal(), linepos, line->sidedef[side], ffloor); } } } } // play the sound before changing the state, so that AActor::OnDestroy can call S_RelinkSounds on it and the death state can override it. if (mo->DeathSound) { S_Sound (mo, CHAN_VOICE, mo->DeathSound, 1, (mo->flags3 & MF3_FULLVOLDEATH) ? ATTN_NONE : ATTN_NORM); } mo->SetState (nextstate); if (!(mo->ObjectFlags & OF_EuthanizeMe)) { // The rest only applies if the missile actor still exists. // [RH] Change render style of exploding rockets if (mo->flags5 & MF5_DEHEXPLOSION) { if (deh.ExplosionStyle == 255) { if (addrocketexplosion) { mo->RenderStyle = STYLE_Add; mo->Alpha = 1.; } else { mo->RenderStyle = STYLE_Translucent; mo->Alpha = 0.6666; } } else { mo->RenderStyle = ERenderStyle(deh.ExplosionStyle); mo->Alpha = deh.ExplosionAlpha; } } if (mo->flags4 & MF4_RANDOMIZE) { mo->tics -= (pr_explodemissile() & 3) * TICRATE / 35; if (mo->tics < 1) mo->tics = 1; } mo->flags &= ~MF_MISSILE; } } DEFINE_ACTION_FUNCTION(AActor, ExplodeMissile) { PARAM_SELF_PROLOGUE(AActor); PARAM_POINTER(line, line_t); PARAM_OBJECT(target, AActor); P_ExplodeMissile(self, line, target); return 0; } void AActor::PlayBounceSound(bool onfloor) { if (!onfloor && (BounceFlags & BOUNCE_NoWallSound)) { return; } if (!(BounceFlags & BOUNCE_Quiet)) { if (BounceFlags & BOUNCE_UseSeeSound) { S_Sound (this, CHAN_VOICE, SeeSound, 1, ATTN_IDLE); } else if (onfloor || WallBounceSound <= 0) { S_Sound (this, CHAN_VOICE, BounceSound, 1, ATTN_IDLE); } else { S_Sound (this, CHAN_VOICE, WallBounceSound, 1, ATTN_IDLE); } } } //---------------------------------------------------------------------------- // // PROC P_FloorBounceMissile // // Returns true if the missile was destroyed //---------------------------------------------------------------------------- bool AActor::FloorBounceMissile (secplane_t &plane) { // [ZZ] if bouncing missile hits a damageable sector(plane), it dies if (P_ProjectileHitPlane(this, -1) && bouncecount > 0) { Vel.Zero(); Speed = 0; bouncecount = 0; if (flags & MF_MISSILE) P_ExplodeMissile(this, nullptr, nullptr); else CallDie(nullptr, nullptr); return true; } if (Z() <= floorz && P_HitFloor (this)) { // Landed in some sort of liquid if (BounceFlags & BOUNCE_ExplodeOnWater) { if (flags & MF_MISSILE) P_ExplodeMissile(this, NULL, NULL); else CallDie(NULL, NULL); return true; } if (!(BounceFlags & BOUNCE_CanBounceWater)) { Destroy (); return true; } } bool onsky; if (plane.fC() < 0) { // on ceiling if (!(BounceFlags & BOUNCE_Ceilings)) return true; onsky = ceilingpic == skyflatnum; } else { // on floor if (!(BounceFlags & BOUNCE_Floors)) return true; onsky = floorpic == skyflatnum; } if (onsky && (BounceFlags & BOUNCE_NotOnSky)) { Destroy(); return true; } // The amount of bounces is limited if (bouncecount>0 && --bouncecount==0) { if (flags & MF_MISSILE) P_ExplodeMissile(this, NULL, NULL); else CallDie(NULL, NULL); return true; } double dot = (Vel | plane.Normal()) * 2; if (BounceFlags & (BOUNCE_HereticType | BOUNCE_MBF)) { Vel -= plane.Normal() * dot; AngleFromVel(); if (!(BounceFlags & BOUNCE_MBF)) // Heretic projectiles die, MBF projectiles don't. { flags |= MF_INBOUNCE; SetState (FindState(NAME_Death)); flags &= ~MF_INBOUNCE; return false; } else Vel.Z *= bouncefactor; } else // Don't run through this for MBF-style bounces { // The reflected velocity keeps only about 70% of its original speed Vel = (Vel - plane.Normal() * dot) * bouncefactor; AngleFromVel(); } PlayBounceSound(true); // Set bounce state if (BounceFlags & BOUNCE_UseBounceState) { FName names[2] = { NAME_Bounce, plane.fC() < 0 ? NAME_Ceiling : NAME_Floor }; FState *bouncestate = FindState(2, names); if (bouncestate != nullptr) { SetState(bouncestate); } } if (BounceFlags & BOUNCE_MBF) // Bring it to rest below a certain speed { if (fabs(Vel.Z) < Mass * GetGravity() / 64) Vel.Z = 0; } else if (BounceFlags & (BOUNCE_AutoOff|BOUNCE_AutoOffFloorOnly)) { if (plane.fC() > 0 || (BounceFlags & BOUNCE_AutoOff)) { // AutoOff only works when bouncing off a floor, not a ceiling (or in compatibility mode.) if (!(flags & MF_NOGRAVITY) && (Vel.Z < 3)) BounceFlags &= ~BOUNCE_TypeMask; } } return false; } //---------------------------------------------------------------------------- // // FUNC P_FaceMobj // // Returns 1 if 'source' needs to turn clockwise, or 0 if 'source' needs // to turn counter clockwise. 'delta' is set to the amount 'source' // needs to turn. // //---------------------------------------------------------------------------- int P_FaceMobj (AActor *source, AActor *target, DAngle *delta) { DAngle diff; diff = deltaangle(source->Angles.Yaw, source->AngleTo(target)); if (diff > 0) { *delta = diff; return 1; } else { *delta = -diff; return 0; } } //---------------------------------------------------------------------------- // // CanSeek // // Checks if a seeker missile can home in on its target // //---------------------------------------------------------------------------- bool AActor::CanSeek(AActor *target) const { if (target->flags5 & MF5_CANTSEEK) return false; if ((flags2 & MF2_DONTSEEKINVISIBLE) && ((target->flags & MF_SHADOW) || (target->renderflags & RF_INVISIBLE) || !target->RenderStyle.IsVisible(target->Alpha) ) ) return false; return true; } DEFINE_ACTION_FUNCTION(AActor, CanSeek) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT_NOT_NULL(target, AActor); ACTION_RETURN_BOOL(self->CanSeek(target)); } //---------------------------------------------------------------------------- // // FUNC P_SeekerMissile // // The missile's tracer field must be the target. Returns true if // target was tracked, false if not. // //---------------------------------------------------------------------------- bool P_SeekerMissile (AActor *actor, double thresh, double turnMax, bool precise, bool usecurspeed) { int dir; DAngle delta; AActor *target; double speed; speed = !usecurspeed ? actor->Speed : actor->VelToSpeed(); target = actor->tracer; if (target == NULL || !actor->CanSeek(target)) { return false; } if (!(target->flags & MF_SHOOTABLE)) { // Target died actor->tracer = nullptr; return false; } if (speed == 0) { // Technically, we're not seeking since our speed is 0, but the target *is* seekable. return true; } dir = P_FaceMobj (actor, target, &delta); if (delta > thresh) { delta /= 2; if (delta > turnMax) { delta = turnMax; } } if (dir) { // Turn clockwise actor->Angles.Yaw += delta; } else { // Turn counter clockwise actor->Angles.Yaw -= delta; } if (!precise) { actor->VelFromAngle(speed); if (!(actor->flags3 & (MF3_FLOORHUGGER|MF3_CEILINGHUGGER))) { if (actor->Top() < target->Z() || target->Top() < actor->Z()) { // Need to seek vertically actor->Vel.Z = (target->Center() - actor->Center()) / actor->DistanceBySpeed(target, speed); } } } else { DAngle pitch = 0.; if (!(actor->flags3 & (MF3_FLOORHUGGER|MF3_CEILINGHUGGER))) { // Need to seek vertically double dist = MAX(1., actor->Distance2D(target)); // Aim at a player's eyes and at the middle of the actor for everything else. double aimheight = target->Height/2; if (target->player) { aimheight = target->player->DefaultViewHeight(); } pitch = DVector2(dist, target->Z() + aimheight - actor->Center()).Angle(); } actor->Vel3DFromAngle(-pitch, speed); } return true; } // // P_XYMovement // // Returns the actor's old floorz. // #define STOPSPEED (0x1000/65536.) #define CARRYSTOPSPEED ((0x1000*32/3)/65536.) double P_XYMovement (AActor *mo, DVector2 scroll) { static int pushtime = 0; bool bForceSlide = !scroll.isZero(); DVector2 ptry; player_t *player; DVector2 move; const secplane_t * walkplane; static const double windTab[3] = { 5 / 32., 10 / 32., 25 / 32. }; int steps, step, totalsteps; DVector2 start; double Oldfloorz = mo->floorz; double oldz = mo->Z(); double maxmove = (mo->waterlevel < 1) || (mo->flags & MF_MISSILE) || (mo->player && mo->player->crouchoffset<-10) ? MAXMOVE : MAXMOVE/4; if (mo->flags2 & MF2_WINDTHRUST && mo->waterlevel < 2 && !(mo->flags & MF_NOCLIP)) { int special = mo->Sector->special; switch (special) { case 40: case 41: case 42: // Wind_East mo->Thrust(0., windTab[special-40]); break; case 43: case 44: case 45: // Wind_North mo->Thrust(90., windTab[special-43]); break; case 46: case 47: case 48: // Wind_South mo->Thrust(270., windTab[special-46]); break; case 49: case 50: case 51: // Wind_West mo->Thrust(180., windTab[special-49]); break; } } // [RH] No need to clamp these now. However, wall running needs it so // that large thrusts can't propel an actor through a wall, because wall // running depends on the player's original movement continuing even after // it gets blocked. // // [anon] When friction is turned off, turn off the crouching and water // speed caps as well, since it is a sort of friction, and the modders // most likely want to deal with that themselves. if ((mo->player != NULL && (i_compatflags & COMPATF_WALLRUN)) || ((mo->waterlevel >= 1 || (mo->player != NULL && mo->player->crouchfactor < 0.75)) && !(mo->flags8 & MF8_NOFRICTION))) { // preserve the direction instead of clamping x and y independently. double cx = mo->Vel.X == 0 ? 1. : clamp(mo->Vel.X, -maxmove, maxmove) / mo->Vel.X; double cy = mo->Vel.Y == 0 ? 1. : clamp(mo->Vel.Y, -maxmove, maxmove) / mo->Vel.Y; double fac = MIN(cx, cy); mo->Vel.X *= fac; mo->Vel.Y *= fac; } const double VELOCITY_THRESHOLD = 5000; // don't let it move faster than this. Fixed point overflowed at 32768 but that's too much to make this safe. if (mo->Vel.LengthSquared() >= VELOCITY_THRESHOLD*VELOCITY_THRESHOLD) { mo->Vel.MakeResize(VELOCITY_THRESHOLD); } move = mo->Vel; // [RH] Carrying sectors didn't work with low speeds in BOOM. This is // because BOOM relied on the speed being fast enough to accumulate // despite friction. If the speed is too low, then its movement will get // cancelled, and it won't accumulate to the desired speed. mo->flags4 &= ~MF4_SCROLLMOVE; if (fabs(scroll.X) > CARRYSTOPSPEED) { scroll.X *= CARRYFACTOR; mo->Vel.X += scroll.X; mo->flags4 |= MF4_SCROLLMOVE; } if (fabs(scroll.Y) > CARRYSTOPSPEED) { scroll.Y *= CARRYFACTOR; mo->Vel.Y += scroll.Y; mo->flags4 |= MF4_SCROLLMOVE; } move += scroll; if (move.isZero()) { if (mo->flags & MF_SKULLFLY) { // the skull slammed into something mo->flags &= ~MF_SKULLFLY; mo->Vel.Zero(); if (!(mo->flags2 & MF2_DORMANT)) { if (mo->SeeState != NULL) mo->SetState (mo->SeeState); else mo->SetIdle(); } else { mo->SetIdle(); mo->tics = -1; } } return Oldfloorz; } player = mo->player; // [RH] Adjust player movement on sloped floors DVector2 startmove = move; walkplane = P_CheckSlopeWalk (mo, move); // [RH] Take smaller steps when moving faster than the object's size permits. // Moving as fast as the object's "diameter" is bad because it could skip // some lines because the actor could land such that it is just touching the // line. For Doom to detect that the line is there, it needs to actually cut // through the actor. { double maxmove = mo->radius - 1; if (maxmove <= 0) { // gibs can have radius 0, so don't divide by zero below! maxmove = MAXMOVE; } const double xspeed = fabs (move.X); const double yspeed = fabs (move.Y); steps = 1; if (xspeed > yspeed) { if (xspeed > maxmove) { steps = int(1 + xspeed / maxmove); } } else { if (yspeed > maxmove) { steps = int(1 + yspeed / maxmove); } } } // P_SlideMove needs to know the step size before P_CheckSlopeWalk // because it also calls P_CheckSlopeWalk on its clipped steps. DVector2 onestep = startmove / steps; start = mo->Pos(); step = 1; totalsteps = steps; // [RH] Instead of doing ripping damage each step, do it each tic. // This makes it compatible with Heretic and Hexen, which only did // one step for their missiles with ripping damage (excluding those // that don't use P_XYMovement). It's also more intuitive since it // makes the damage done dependant on the amount of time the projectile // spends inside a target rather than on the projectile's size. The // last actor ripped through is recorded so that if the projectile // passes through more than one actor this tic, each one takes damage // and not just the first one. pushtime++; FCheckPosition tm(!!(mo->flags2 & MF2_RIP)); DAngle oldangle = mo->Angles.Yaw; do { if (i_compatflags & COMPATF_WALLRUN) pushtime++; tm.PushTime = pushtime; ptry = start + move * step / steps; DVector2 startvel = mo->Vel; // killough 3/15/98: Allow objects to drop off // [RH] If walking on a slope, stay on the slope if (!P_TryMove (mo, ptry, true, walkplane, tm)) { // blocked move AActor *BlockingMobj = mo->BlockingMobj; line_t *BlockingLine = mo->BlockingLine; // [ZZ] if (!BlockingLine && !BlockingMobj) // hit floor or ceiling while XY movement - sector actions { int hitpart = -1; sector_t* hitsector = nullptr; secplane_t* hitplane = nullptr; if (tm.ceilingsector && mo->Z() + mo->Height > tm.ceilingsector->ceilingplane.ZatPoint(tm.pos.XY())) mo->BlockingCeiling = tm.ceilingsector; if (tm.floorsector && mo->Z() < tm.floorsector->floorplane.ZatPoint(tm.pos.XY())) mo->BlockingFloor = tm.floorsector; // the following two only set the appropriate field - to avoid issues caused by running actions right in the middle of XY movement P_CheckFor3DFloorHit(mo, mo->floorz, false); P_CheckFor3DCeilingHit(mo, mo->ceilingz, false); } if (!(mo->flags & MF_MISSILE) && (mo->BounceFlags & BOUNCE_MBF) && (BlockingMobj != NULL ? P_BounceActor(mo, BlockingMobj, false) : P_BounceWall(mo))) { // Do nothing, relevant actions already done in the condition. // This allows to avoid setting velocities to 0 in the final else of this series. } else if ((mo->flags2 & (MF2_SLIDE|MF2_BLASTED) || bForceSlide) && !(mo->flags&MF_MISSILE)) { // try to slide along it if (BlockingMobj == NULL) { // slide against wall if (BlockingLine != NULL && mo->player && mo->waterlevel && mo->waterlevel < 3 && (mo->player->cmd.ucmd.forwardmove | mo->player->cmd.ucmd.sidemove) && mo->BlockingLine->sidedef[1] != NULL) { mo->Vel.Z = WATER_JUMP_SPEED; } // If the blocked move executed any push specials that changed the // actor's velocity, do not attempt to slide. if (mo->Vel.XY() == startvel) { if (player && (i_compatflags & COMPATF_WALLRUN)) { // [RH] Here is the key to wall running: The move is clipped using its full speed. // If the move is done a second time (because it was too fast for one move), it // is still clipped against the wall at its full speed, so you effectively // execute two moves in one tic. P_SlideMove (mo, mo->Vel, 1); } else { P_SlideMove (mo, onestep, totalsteps); } if (mo->Vel.XY().isZero()) { steps = 0; } else { if (!player || !(i_compatflags & COMPATF_WALLRUN)) { move = mo->Vel; onestep = move / steps; P_CheckSlopeWalk (mo, move); } start = mo->Pos().XY() - move * step / steps; } } else { steps = 0; } } else { // slide against another actor DVector2 t; t.X = 0, t.Y = onestep.Y; walkplane = P_CheckSlopeWalk (mo, t); if (P_TryMove (mo, mo->Pos() + t, true, walkplane, tm)) { mo->Vel.X = 0; } else { t.X = onestep.X, t.Y = 0; walkplane = P_CheckSlopeWalk (mo, t); if (P_TryMove (mo, mo->Pos() + t, true, walkplane, tm)) { mo->Vel.Y = 0; } else { mo->Vel.X = mo->Vel.Y = 0; } } if (player && player->mo == mo) { if (mo->Vel.X == 0) player->Vel.X = 0; if (mo->Vel.Y == 0) player->Vel.Y = 0; } steps = 0; } } else if (mo->flags & MF_MISSILE) { steps = 0; if (BlockingMobj) { if (mo->BounceFlags & BOUNCE_Actors) { // Bounce test and code moved to P_BounceActor if (!P_BounceActor(mo, BlockingMobj, false)) { // Struck a player/creature P_ExplodeMissile (mo, NULL, BlockingMobj); } return Oldfloorz; } } else { // Struck a wall if (P_BounceWall (mo)) { mo->PlayBounceSound(false); return Oldfloorz; } } if (BlockingMobj && (BlockingMobj->flags2 & MF2_REFLECTIVE)) { bool seeker = (mo->flags2 & MF2_SEEKERMISSILE) ? true : false; // Don't change the angle if there's THRUREFLECT on the monster. if (!(BlockingMobj->flags7 & MF7_THRUREFLECT)) { DAngle angle = BlockingMobj->AngleTo(mo); bool dontReflect = (mo->AdjustReflectionAngle(BlockingMobj, angle)); // Change angle for deflection/reflection if (!dontReflect) { bool tg = (mo->target != NULL); bool blockingtg = (BlockingMobj->target != NULL); if ((BlockingMobj->flags7 & MF7_AIMREFLECT) && (tg | blockingtg)) { AActor *origin = tg ? mo->target : BlockingMobj->target; //dest->x - source->x DVector3 vect = mo->Vec3To(origin); vect.Z += origin->Height / 2; mo->Vel = vect.Resized(mo->Speed); } else { if ((BlockingMobj->flags7 & MF7_MIRRORREFLECT) && (tg | blockingtg)) { mo->Angles.Yaw += 180.; mo->Vel *= -.5; } else { mo->Angles.Yaw = angle; mo->VelFromAngle(mo->Speed / 2); mo->Vel.Z *= -.5; } } } else { goto explode; } } if (mo->flags2 & MF2_SEEKERMISSILE) { mo->tracer = mo->target; } mo->target = BlockingMobj; return Oldfloorz; } explode: // explode a missile bool onsky = false; if (tm.ceilingline && tm.ceilingline->hitSkyWall(mo)) { if (!(mo->flags3 & MF3_SKYEXPLODE)) { // Hack to prevent missiles exploding against the sky. // Does not handle sky floors. mo->Destroy(); return Oldfloorz; } else onsky = true; } // [RH] Don't explode on horizon lines. if (mo->BlockingLine != NULL && mo->BlockingLine->special == Line_Horizon) { if (!(mo->flags3 & MF3_SKYEXPLODE)) { mo->Destroy(); return Oldfloorz; } else onsky = true; } if (mo->BlockingCeiling) // hit floor or ceiling while XY movement { P_ProjectileHitPlane(mo, SECPART_Ceiling); } if (mo->BlockingFloor) { P_ProjectileHitPlane(mo, SECPART_Floor); } P_ExplodeMissile (mo, mo->BlockingLine, BlockingMobj, onsky); return Oldfloorz; } else { mo->Vel.X = mo->Vel.Y = 0; steps = 0; } } else { if (mo->Pos().XY() != ptry) { // If the new position does not match the desired position, the player // must have gone through a teleporter or portal. if (mo->Vel.X == 0 && mo->Vel.Y == 0) { // Stop moving right now if it was a regular teleporter. step = steps; } else { // It was a portal, line-to-line or fogless teleporter, so the move should continue. // For that we need to adjust the start point, and the movement vector. DAngle anglediff = deltaangle(oldangle, mo->Angles.Yaw); if (anglediff != 0) { move = move.Rotated(anglediff); oldangle = mo->Angles.Yaw; } start = mo->Pos() - move * step / steps; } } } } while (++step <= steps); // Friction if (player && player->mo == mo && player->cheats & CF_NOVELOCITY) { // debug option for no sliding at all mo->Vel.X = mo->Vel.Y = 0; player->Vel.X = player->Vel.Y = 0; return Oldfloorz; } if (mo->flags & (MF_MISSILE | MF_SKULLFLY) || mo->flags8 & MF8_NOFRICTION) { // no friction for missiles return Oldfloorz; } if (mo->Z() > mo->floorz && !(mo->flags2 & MF2_ONMOBJ) && !mo->IsNoClip2() && (!(mo->flags2 & MF2_FLY) || !(mo->flags & MF_NOGRAVITY)) && !mo->waterlevel) { // [RH] Friction when falling is available for larger aircontrols if (player != NULL && level.airfriction != 1.) { mo->Vel.X *= level.airfriction; mo->Vel.Y *= level.airfriction; if (player->mo == mo) // Not voodoo dolls { player->Vel.X *= level.airfriction; player->Vel.Y *= level.airfriction; } } return Oldfloorz; } // killough 8/11/98: add bouncers // killough 9/15/98: add objects falling off ledges // killough 11/98: only include bouncers hanging off ledges if ((mo->flags & MF_CORPSE) || (mo->BounceFlags & BOUNCE_MBF && mo->Z() > mo->dropoffz) || (mo->flags6 & MF6_FALLING)) { // Don't stop sliding if halfway off a step with some velocity if (fabs(mo->Vel.X) > 0.25 || fabs(mo->Vel.Y) > 0.25) { if (mo->floorz > mo->Sector->floorplane.ZatPoint(mo)) { if (mo->dropoffz != mo->floorz) // 3DMidtex or other special cases that must be excluded { unsigned i; for(i=0;iSector->e->XFloor.ffloors.Size();i++) { // Sliding around on 3D floors looks extremely bad so // if the floor comes from one in the current sector stop sliding the corpse! F3DFloor * rover=mo->Sector->e->XFloor.ffloors[i]; if (!(rover->flags&FF_EXISTS)) continue; if (rover->flags&FF_SOLID && rover->top.plane->ZatPoint(mo) == mo->floorz) break; } if (i==mo->Sector->e->XFloor.ffloors.Size()) return Oldfloorz; } } } } // killough 11/98: // Stop voodoo dolls that have come to rest, despite any // moving corresponding player: if (fabs(mo->Vel.X) < STOPSPEED && fabs(mo->Vel.Y) < STOPSPEED && (!player || (player->mo != mo) || !(player->cmd.ucmd.forwardmove | player->cmd.ucmd.sidemove))) { // if in a walking frame, stop moving // killough 10/98: // Don't affect main player when voodoo dolls stop: if (player && player->mo == mo && !(player->cheats & CF_PREDICTING)) { PlayIdle (player->mo); } mo->Vel.X = mo->Vel.Y = 0; mo->flags4 &= ~MF4_SCROLLMOVE; // killough 10/98: kill any bobbing velocity too (except in voodoo dolls) if (player && player->mo == mo) player->Vel.X = player->Vel.Y = 0; } else { // phares 3/17/98 // Friction will have been adjusted by friction thinkers for icy // or muddy floors. Otherwise it was never touched and // remained set at ORIG_FRICTION // // killough 8/28/98: removed inefficient thinker algorithm, // instead using touching_sectorlist in P_GetFriction() to // determine friction (and thus only when it is needed). // // killough 10/98: changed to work with new bobbing method. // Reducing player velocity is no longer needed to reduce // bobbing, so ice works much better now. double friction = P_GetFriction (mo, NULL); mo->Vel.X *= friction; mo->Vel.Y *= friction; // killough 10/98: Always decrease player bobbing by ORIG_FRICTION. // This prevents problems with bobbing on ice, where it was not being // reduced fast enough, leading to all sorts of kludges being developed. if (player && player->mo == mo) // Not voodoo dolls { player->Vel.X *= ORIG_FRICTION; player->Vel.Y *= ORIG_FRICTION; } // Don't let the velocity become less than the smallest representable fixed point value. if (fabs(mo->Vel.X) < MinVel) mo->Vel.X = 0; if (fabs(mo->Vel.Y) < MinVel) mo->Vel.Y = 0; if (player && player->mo == mo) // Not voodoo dolls { if (fabs(player->Vel.X) < MinVel) player->Vel.X = 0; if (fabs(player->Vel.Y) < MinVel) player->Vel.Y = 0; } } return Oldfloorz; } // Move this to p_inter *** void P_MonsterFallingDamage (AActor *mo) { int damage; double vel; if (!(level.flags2 & LEVEL2_MONSTERFALLINGDAMAGE)) return; if (mo->floorsector->Flags & SECF_NOFALLINGDAMAGE) return; vel = fabs(mo->Vel.Z); if (vel > 35) { // automatic death damage = TELEFRAG_DAMAGE; } else { damage = int((vel - 23)*6); } damage = TELEFRAG_DAMAGE; // always kill 'em P_DamageMobj (mo, NULL, NULL, damage, NAME_Falling); } // // P_ZMovement // void P_ZMovement (AActor *mo, double oldfloorz) { double dist; double delta; double oldz = mo->Z(); double grav = mo->GetGravity(); // // check for smooth step up // if (mo->player && mo->player->mo == mo && mo->Z() < mo->floorz) { mo->player->viewheight -= mo->floorz - mo->Z(); mo->player->deltaviewheight = mo->player->GetDeltaViewHeight(); } mo->AddZ(mo->Vel.Z); // // apply gravity // if (mo->Z() > mo->floorz && !(mo->flags & MF_NOGRAVITY)) { double startvelz = mo->Vel.Z; if (mo->waterlevel == 0 || (mo->player && !(mo->player->cmd.ucmd.forwardmove | mo->player->cmd.ucmd.sidemove))) { // [RH] Double gravity only if running off a ledge. Coming down from // an upward thrust (e.g. a jump) should not double it. if (mo->Vel.Z == 0 && oldfloorz > mo->floorz && mo->Z() == oldfloorz) { mo->Vel.Z -= grav + grav; } else { mo->Vel.Z -= grav; } } if (mo->player == NULL) { if (mo->waterlevel >= 1) { double sinkspeed; if ((mo->flags & MF_SPECIAL) && !(mo->flags3 & MF3_ISMONSTER)) { // Pickup items don't sink if placed and drop slowly if dropped sinkspeed = (mo->flags & MF_DROPPED) ? -WATER_SINK_SPEED / 8 : 0; } else { sinkspeed = -WATER_SINK_SPEED; // If it's not a player, scale sinkspeed by its mass, with // 100 being equivalent to a player. if (mo->player == NULL) { sinkspeed = sinkspeed * clamp(mo->Mass, 1, 4000) / 100; } } if (mo->Vel.Z < sinkspeed) { // Dropping too fast, so slow down toward sinkspeed. mo->Vel.Z -= MAX(sinkspeed*2, -8.); if (mo->Vel.Z > sinkspeed) { mo->Vel.Z = sinkspeed; } } else if (mo->Vel.Z > sinkspeed) { // Dropping too slow/going up, so trend toward sinkspeed. mo->Vel.Z = startvelz + MAX(sinkspeed/3, -8.); if (mo->Vel.Z < sinkspeed) { mo->Vel.Z = sinkspeed; } } } } else { if (mo->waterlevel > 1) { double sinkspeed = -WATER_SINK_SPEED; if (mo->Vel.Z < sinkspeed) { mo->Vel.Z = (startvelz < sinkspeed) ? startvelz : sinkspeed; } else { mo->Vel.Z = startvelz + ((mo->Vel.Z - startvelz) * (mo->waterlevel == 1 ? WATER_SINK_SMALL_FACTOR : WATER_SINK_FACTOR)); } } } } // Hexen compatibility handling for floatbobbing. Ugh... // Hexen yanked all items to the floor, except those being spawned at map start in the air. // Those were kept at their original height. // Do this only if the item was actually spawned by the map above ground to avoid problems. if (mo->specialf1 > 0 && (mo->flags2 & MF2_FLOATBOB) && (ib_compatflags & BCOMPATF_FLOATBOB)) { mo->SetZ(mo->floorz + mo->specialf1); } // // adjust height // if ((mo->flags & MF_FLOAT) && !(mo->flags2 & MF2_DORMANT) && mo->target) { // float down towards target if too close if (!(mo->flags & (MF_SKULLFLY | MF_INFLOAT))) { dist = mo->Distance2D (mo->target); delta = (mo->target->Center()) - mo->Z(); if (delta < 0 && dist < -(delta*3)) mo->AddZ(-mo->FloatSpeed); else if (delta > 0 && dist < (delta*3)) mo->AddZ(mo->FloatSpeed); } } if (mo->player && (mo->flags & MF_NOGRAVITY) && (mo->Z() > mo->floorz)) { if (!mo->IsNoClip2()) { mo->AddZ(DAngle(360 / 80.f * level.maptime).Sin() / 8); } if (!(mo->flags8 & MF8_NOFRICTION)) { mo->Vel.Z *= FRICTION_FLY; } } if (mo->waterlevel && !(mo->flags & MF_NOGRAVITY) && !(mo->flags8 & MF8_NOFRICTION)) { double friction = -1; // Check 3D floors -- might be the source of the waterlevel for (auto rover : mo->Sector->e->XFloor.ffloors) { if (!(rover->flags & FF_EXISTS)) continue; if (!(rover->flags & FF_SWIMMABLE)) continue; if (mo->Z() >= rover->top.plane->ZatPoint(mo) || mo->Center() < rover->bottom.plane->ZatPoint(mo)) continue; friction = rover->model->GetFriction(rover->top.isceiling); break; } if (friction < 0) friction = mo->Sector->GetFriction(); // get real friction, even if from a terrain definition mo->Vel.Z *= friction; } // // clip movement // if (mo->Z() <= mo->floorz) { // Hit the floor if ((!mo->player || !(mo->player->cheats & CF_PREDICTING)) && mo->Sector->SecActTarget != NULL && mo->Sector->floorplane.ZatPoint(mo) == mo->floorz) { // [RH] Let the sector do something to the actor mo->Sector->TriggerSectorActions(mo, SECSPAC_HitFloor); } P_CheckFor3DFloorHit(mo, mo->floorz, true); // [RH] Need to recheck this because the sector action might have // teleported the actor so it is no longer below the floor. if (mo->Z() <= mo->floorz) { mo->BlockingFloor = mo->Sector; if ((mo->flags & MF_MISSILE) && !(mo->flags & MF_NOCLIP)) { mo->SetZ(mo->floorz); if (mo->BounceFlags & BOUNCE_Floors) { mo->FloorBounceMissile (mo->floorsector->floorplane); /* if (!(mo->flags6 & MF6_CANJUMP)) */ return; } else if (mo->flags3 & MF3_NOEXPLODEFLOOR) { P_HitFloor (mo); mo->Vel.Z = 0; return; } else if ((mo->flags3 & MF3_FLOORHUGGER) && !(mo->flags5 & MF5_NODROPOFF)) { // Floor huggers can go up steps return; } else { bool onsky = false; if (mo->floorpic == skyflatnum) { if (!(mo->flags3 & MF3_SKYEXPLODE)) { // [RH] Just remove the missile without exploding it // if this is a sky floor. mo->Destroy(); return; } else onsky = true; } P_HitFloor (mo); // hit floor: direct damage callback P_ProjectileHitPlane(mo, SECPART_Floor); P_ExplodeMissile (mo, NULL, NULL, onsky); return; } } else if (mo->BounceFlags & BOUNCE_MBF && mo->Vel.Z) // check for MBF-like bounce on non-missiles { mo->FloorBounceMissile(mo->floorsector->floorplane); } if (mo->flags3 & MF3_ISMONSTER) // Blasted mobj falling { if (mo->Vel.Z < -23) { P_MonsterFallingDamage (mo); } } mo->SetZ(mo->floorz); if (mo->Vel.Z < 0) { const double minvel = -8; // landing speed from a jump with normal gravity // Spawn splashes, etc. P_HitFloor (mo); if (mo->DamageType == NAME_Ice && mo->Vel.Z < minvel) { mo->tics = 1; mo->Vel.Zero(); return; } // Let the actor do something special for hitting the floor if (mo->flags7 & MF7_SMASHABLE) { P_DamageMobj(mo, nullptr, nullptr, mo->health, NAME_Smash); } if (mo->player) { if (mo->player->jumpTics < 0 || mo->Vel.Z < minvel) { // delay any jumping for a short while mo->player->jumpTics = 7; } if (mo->Vel.Z < minvel && !(mo->flags & MF_NOGRAVITY)) { // Squat down. // Decrease viewheight for a moment after hitting the ground (hard), // and utter appropriate sound. PlayerLandedOnThing (mo, NULL); } } mo->Vel.Z = 0; } if (mo->flags & MF_SKULLFLY) { // The skull slammed into something mo->Vel.Z = -mo->Vel.Z; } mo->Crash(); } } if (mo->flags2 & MF2_FLOORCLIP) { mo->AdjustFloorClip (); } if (mo->Top() > mo->ceilingz) { // hit the ceiling if ((!mo->player || !(mo->player->cheats & CF_PREDICTING)) && mo->Sector->SecActTarget != NULL && mo->Sector->ceilingplane.ZatPoint(mo) == mo->ceilingz) { // [RH] Let the sector do something to the actor mo->Sector->TriggerSectorActions (mo, SECSPAC_HitCeiling); } P_CheckFor3DCeilingHit(mo, mo->ceilingz, true); // [RH] Need to recheck this because the sector action might have // teleported the actor so it is no longer above the ceiling. if (mo->Top() > mo->ceilingz) { mo->BlockingCeiling = mo->Sector; mo->SetZ(mo->ceilingz - mo->Height); if (mo->BounceFlags & BOUNCE_Ceilings) { // ceiling bounce mo->FloorBounceMissile (mo->ceilingsector->ceilingplane); /*if (!(mo->flags6 & MF6_CANJUMP))*/ return; } if (mo->flags & MF_SKULLFLY) { // the skull slammed into something mo->Vel.Z = -mo->Vel.Z; } if (mo->Vel.Z > 0) mo->Vel.Z = 0; if ((mo->flags & MF_MISSILE) && !(mo->flags & MF_NOCLIP)) { if (mo->flags3 & MF3_CEILINGHUGGER) { return; } bool onsky = false; if (mo->ceilingpic == skyflatnum) { if (!(mo->flags3 & MF3_SKYEXPLODE)) { mo->Destroy(); return; } else onsky = true; } // hit ceiling: direct damage callback P_ProjectileHitPlane(mo, SECPART_Ceiling); P_ExplodeMissile (mo, NULL, NULL, onsky); return; } } } P_CheckFakeFloorTriggers (mo, oldz); } void P_CheckFakeFloorTriggers (AActor *mo, double oldz, bool oldz_has_viewheight) { if (mo->player && (mo->player->cheats & CF_PREDICTING)) { return; } sector_t *sec = mo->Sector; assert (sec != NULL); if (sec == NULL) { return; } if (sec->heightsec != NULL && sec->SecActTarget != NULL) { sector_t *hs = sec->heightsec; double waterz = hs->floorplane.ZatPoint(mo); double newz; double viewheight; if (mo->player != NULL) { viewheight = mo->player->viewheight; } else { viewheight = mo->Height; } if (oldz > waterz && mo->Z() <= waterz) { // Feet hit fake floor sec->TriggerSectorActions (mo, SECSPAC_HitFakeFloor); } newz = mo->Z() + viewheight; if (!oldz_has_viewheight) { oldz += viewheight; } if (oldz <= waterz && newz > waterz) { // View went above fake floor sec->TriggerSectorActions (mo, SECSPAC_EyesSurface); } else if (oldz > waterz && newz <= waterz) { // View went below fake floor sec->TriggerSectorActions (mo, SECSPAC_EyesDive); } if (!(hs->MoreFlags & SECMF_FAKEFLOORONLY)) { waterz = hs->ceilingplane.ZatPoint(mo); if (oldz <= waterz && newz > waterz) { // View went above fake ceiling sec->TriggerSectorActions (mo, SECSPAC_EyesAboveC); } else if (oldz > waterz && newz <= waterz) { // View went below fake ceiling sec->TriggerSectorActions (mo, SECSPAC_EyesBelowC); } } } } DEFINE_ACTION_FUNCTION(AActor, CheckFakeFloorTriggers) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(oldz); PARAM_BOOL(oldz_has_viewh); P_CheckFakeFloorTriggers(self, oldz, oldz_has_viewh); return 0; } //=========================================================================== // // PlayerLandedOnThing // //=========================================================================== static void PlayerLandedOnThing (AActor *mo, AActor *onmobj) { bool grunted; if (!mo->player) return; if (mo->player->mo == mo) { mo->player->deltaviewheight = mo->Vel.Z / 8.; } if (mo->player->cheats & CF_PREDICTING) return; P_FallingDamage (mo); // [RH] only make noise if alive if (!mo->player->morphTics && mo->health > 0) { grunted = false; // Why should this number vary by gravity? if (mo->health > 0 && mo->Vel.Z < -mo->player->mo->FloatVar(NAME_GruntSpeed)) { S_Sound (mo, CHAN_VOICE, "*grunt", 1, ATTN_NORM); grunted = true; } if (onmobj != NULL || !Terrains[P_GetThingFloorType (mo)].IsLiquid) { if (!grunted || !S_AreSoundsEquivalent (mo, "*grunt", "*land")) { S_Sound (mo, CHAN_AUTO, "*land", 1, ATTN_NORM); } } } // mo->player->centering = true; } // // P_NightmareRespawn // void P_NightmareRespawn (AActor *mobj) { double z; AActor *mo; AActor *info = mobj->GetDefault(); mobj->skillrespawncount++; // spawn the new monster (assume the spawn will be good) if (info->flags & MF_SPAWNCEILING) z = ONCEILINGZ; else if (info->flags2 & MF2_SPAWNFLOAT) z = FLOATRANDZ; else z = ONFLOORZ; // spawn it mo = AActor::StaticSpawn(mobj->GetClass(), DVector3(mobj->SpawnPoint.X, mobj->SpawnPoint.Y, z), NO_REPLACE, true); mo->health = mobj->SpawnHealth(); if (z == ONFLOORZ) { mo->AddZ(mobj->SpawnPoint.Z); if (mo->Z() < mo->floorz) { // Do not respawn monsters in the floor, even if that's where they // started. The initial P_ZMovement() call would have put them on // the floor right away, but we need them on the floor now so we // can use P_CheckPosition() properly. mo->SetZ(mo->floorz); } if (mo->Top() > mo->ceilingz) { mo->SetZ(mo->ceilingz- mo->Height); } } else if (z == ONCEILINGZ) { mo->AddZ(-mobj->SpawnPoint.Z); } // If there are 3D floors, we need to find floor/ceiling again. P_FindFloorCeiling(mo, FFCF_SAMESECTOR | FFCF_ONLY3DFLOORS | FFCF_3DRESTRICT); if (z == ONFLOORZ) { if (mo->Z() < mo->floorz) { // Do not respawn monsters in the floor, even if that's where they // started. The initial P_ZMovement() call would have put them on // the floor right away, but we need them on the floor now so we // can use P_CheckPosition() properly. mo->SetZ(mo->floorz); } if (mo->Top() > mo->ceilingz) { // Do the same for the ceiling. mo->SetZ(mo->ceilingz - mo->Height); } } // something is occupying its position? if (!P_CheckPosition(mo, mo->Pos(), true)) { //[GrafZahl] MF_COUNTKILL still needs to be checked here. mo->ClearCounters(); mo->Destroy (); return; // no respawn } z = mo->Z(); // inherit attributes from deceased one mo->SpawnPoint = mobj->SpawnPoint; mo->SpawnAngle = mobj->SpawnAngle; mo->SpawnFlags = mobj->SpawnFlags & ~MTF_DORMANT; // It wasn't dormant when it died, so it's not dormant now, either. mo->Angles.Yaw = (double)mobj->SpawnAngle; mo->HandleSpawnFlags (); mo->reactiontime = 18; mo->CopyFriendliness (mobj, false); mo->Translation = mobj->Translation; mo->skillrespawncount = mobj->skillrespawncount; mo->Prev.Z = z; // Do not interpolate Z position if we changed it since spawning. // spawn a teleport fog at old spot because of removal of the body? P_SpawnTeleportFog(mobj, mobj->Pos(), true, true); // spawn a teleport fog at the new spot P_SpawnTeleportFog(mobj, DVector3(mobj->SpawnPoint, z), false, true); // remove the old monster mobj->Destroy (); } // // P_AddMobjToHash // // Inserts an mobj into the correct chain based on its tid. // If its tid is 0, this function does nothing. // void AActor::AddToHash () { if (tid == 0) { iprev = NULL; inext = NULL; return; } else { int hash = TIDHASH (tid); auto &slot = level.TIDHash[hash]; inext = slot; iprev = &slot; slot = this; if (inext) { inext->iprev = &inext; } } } // // P_RemoveMobjFromHash // // Removes an mobj from its hash chain. // void AActor::RemoveFromHash () { if (tid != 0 && iprev) { *iprev = inext; if (inext) { inext->iprev = iprev; } iprev = NULL; inext = NULL; } tid = 0; } //========================================================================== // // P_IsTIDUsed // // Returns true if there is at least one actor with the specified TID // (dead or alive). // //========================================================================== bool FLevelLocals::IsTIDUsed(int tid) { AActor *probe = TIDHash[tid & 127]; while (probe != NULL) { if (probe->tid == tid) { return true; } probe = probe->inext; } return false; } //========================================================================== // // P_FindUniqueTID // // Returns an unused TID. If start_tid is 0, then a random TID will be // chosen. Otherwise, it will perform a linear search starting from // start_tid. If limit is non-0, then it will not check more than // number of TIDs. Returns 0 if no suitable TID was found. // //========================================================================== int FLevelLocals::FindUniqueTID(int start_tid, int limit) { int tid; if (start_tid != 0) { // Do a linear search. if (start_tid > INT_MAX-limit+1) { // If 'limit+start_tid-1' overflows, clamp 'limit' to INT_MAX limit = INT_MAX; } else { limit += start_tid-1; } for (tid = start_tid; tid <= limit; ++tid) { if (tid != 0 && !IsTIDUsed(tid)) { return tid; } } // Nothing free found. return 0; } // Do a random search. To try and be a bit more performant, this // actually does several linear searches. In the case of *very* // dense TID usage, this could potentially perform worse than doing // a complete linear scan starting at 1. However, you would need // to use an absolutely ridiculous number of actors before this // becomes a real concern. if (limit == 0) { limit = INT_MAX; } for (int i = 0; i < limit; i += 5) { // Use a positive starting TID. tid = pr_uniquetid.GenRand32() & INT_MAX; tid = FindUniqueTID(tid == 0 ? 1 : tid, 5); if (tid != 0) { return tid; } } // Nothing free found. return 0; } CCMD(utid) { Printf("%d\n", level.FindUniqueTID(argv.argc() > 1 ? atoi(argv[1]) : 0, (argv.argc() > 2 && atoi(argv[2]) >= 0) ? atoi(argv[2]) : 0)); } //========================================================================== // // AActor :: GetMissileDamage // // If the actor's damage amount is an expression, evaluate it and return // the result. Otherwise, return ((random() & mask) + add) * damage. // //========================================================================== int AActor::GetMissileDamage (int mask, int add) { if (DamageVal >= 0) { if (mask == 0) { return add * DamageVal; } else { return ((pr_missiledamage() & mask) + add) * DamageVal; } } if (DamageFunc == nullptr) { // This should never happen assert(false && "No damage function found"); return 0; } VMValue param = this; VMReturn result; int amount; result.IntAt(&amount); if (VMCall(DamageFunc, ¶m, 1, &result, 1) < 1) { // No results return 0; } return amount; } void AActor::Howl () { FSoundID howl = SoundVar(NAME_HowlSound); if (!S_IsActorPlayingSomething(this, CHAN_BODY, howl)) { S_Sound (this, CHAN_BODY, howl, 1, ATTN_NORM); } } DEFINE_ACTION_FUNCTION(AActor, Howl) { PARAM_SELF_PROLOGUE(AActor); self->Howl(); return 0; } bool AActor::Slam (AActor *thing) { flags &= ~MF_SKULLFLY; Vel.Zero(); if (health > 0) { if (!(flags2 & MF2_DORMANT)) { int dam = GetMissileDamage (7, 1); int newdam = P_DamageMobj (thing, this, this, dam, NAME_Melee); P_TraceBleed (newdam > 0 ? newdam : dam, thing, this); // The charging monster may have died by the target's actions here. if (health > 0) { if (SeeState != NULL) SetState (SeeState); else SetIdle(); } } else { SetIdle(); tics = -1; } } return false; // stop moving } DEFINE_ACTION_FUNCTION(AActor, Slam) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT(thing, AActor); ACTION_RETURN_BOOL(self->Slam(thing)); } bool AActor::CallSlam(AActor *thing) { IFVIRTUAL(AActor, Slam) { VMValue params[2] = { (DObject*)this, thing }; VMReturn ret; int retval; ret.IntAt(&retval); VMCall(func, params, 2, &ret, 1); return !!retval; } else return Slam(thing); } // This virtual method only exists on the script side. int AActor::SpecialMissileHit (AActor *victim) { IFVIRTUAL(AActor, SpecialMissileHit) { VMValue params[2] = { (DObject*)this, victim }; VMReturn ret; int retval; ret.IntAt(&retval); VMCall(func, params, 2, &ret, 1); return retval; } else return -1; } bool AActor::AdjustReflectionAngle (AActor *thing, DAngle &angle) { if (flags2 & MF2_DONTREFLECT) return true; if (thing->flags7 & MF7_THRUREFLECT) return false; // Change angle for reflection if (thing->flags4&MF4_SHIELDREFLECT) { // Shield reflection (from the Centaur) if (absangle(angle, thing->Angles.Yaw) > 45) return true; // Let missile explode if (thing->flags7 & MF7_NOSHIELDREFLECT) return true; if (pr_reflect () < 128) angle += 45; else angle -= 45; } else if (thing->flags4&MF4_DEFLECT) { // deflect (like the Heresiarch) if(pr_reflect() < 128) angle += 45; else angle -= 45; } else { angle += ((pr_reflect() % 16) - 8); } //Always check for AIMREFLECT, no matter what else is checked above. if (thing->flags7 & MF7_AIMREFLECT) { if (this->target != NULL) { A_Face(this, this->target); } else if (thing->target != NULL) { A_Face(this, thing->target); } } return false; } int AActor::AbsorbDamage(int damage, FName dmgtype) { for (AActor *item = Inventory; item != nullptr; item = item->Inventory) { IFVIRTUALPTRNAME(item, NAME_Inventory, AbsorbDamage) { VMValue params[4] = { item, damage, dmgtype.GetIndex(), &damage }; VMCall(func, params, 4, nullptr, 0); } } return damage; } void AActor::AlterWeaponSprite(visstyle_t *vis) { int changed = 0; TArray items; // This needs to go backwards through the items but the list has no backlinks. for (AActor *item = Inventory; item != nullptr; item = item->Inventory) { items.Push(item); } for(int i=items.Size()-1;i>=0;i--) { IFVIRTUALPTRNAME(items[i], NAME_Inventory, AlterWeaponSprite) { VMValue params[3] = { items[i], vis, &changed }; VMCall(func, params, 3, nullptr, 0); } } } void AActor::PlayActiveSound () { if (ActiveSound && !S_IsActorPlayingSomething (this, CHAN_VOICE, -1)) { S_Sound (this, CHAN_VOICE, ActiveSound, 1, (flags3 & MF3_FULLVOLACTIVE) ? ATTN_NONE : ATTN_IDLE); } } DEFINE_ACTION_FUNCTION(AActor, PlayActiveSound) { PARAM_SELF_PROLOGUE(AActor); self->PlayActiveSound(); return 0; } bool AActor::IsOkayToAttack (AActor *link) { // Standard things to eliminate: an actor shouldn't attack itself, // or a non-shootable, dormant, non-player-and-non-monster actor. if (link == this) return false; if (!(link->player||(link->flags3 & MF3_ISMONSTER)))return false; if (!(link->flags & MF_SHOOTABLE)) return false; if (link->flags2 & MF2_DORMANT) return false; if (link->flags7 & MF7_NEVERTARGET) return false; // NEVERTARGET means just that. // An actor shouldn't attack friendly actors. The reference depends // on the type of actor: for a player's actor, itself; for a projectile, // its target; and for a summoned minion, its tracer. AActor * Friend; if (flags5 & MF5_SUMMONEDMONSTER) Friend = tracer; else if (flags2 & MF2_SEEKERMISSILE) Friend = target; else if ((flags & MF_FRIENDLY) && FriendPlayer) Friend = players[FriendPlayer-1].mo; else Friend = this; // Friend checks if (link == Friend) return false; if (Friend == NULL) return false; if (Friend->IsFriend(link)) return false; if ((link->flags5 & MF5_SUMMONEDMONSTER) // No attack against minions on the same side && (link->tracer == Friend)) return false; if (multiplayer && !deathmatch // No attack against fellow players in coop && link->player && Friend->player) return false; if (((flags & link->flags) & MF_FRIENDLY) // No friendly infighting amongst minions && IsFriend(link)) return false; // Now that all the actor checks are made, the line of sight can be checked if (P_CheckSight (this, link)) { // AMageStaffFX2::IsOkayToAttack had an extra check here, generalized with a flag, // to only allow the check to succeed if the enemy was in a ~84� FOV of the player if (flags3 & MF3_SCREENSEEKER) { DAngle angle = absangle(Friend->AngleTo(link), Friend->Angles.Yaw); if (angle < 30 * (256./360.)) { return true; } } // Other actors are not concerned by this check else return true; } // The sight check was failed, or the angle wasn't right for a screenseeker return false; } void AActor::SetShade (uint32_t rgb) { PalEntry *entry = (PalEntry *)&rgb; fillcolor = (rgb & 0xffffff) | (ColorMatcher.Pick (entry->r, entry->g, entry->b) << 24); } void AActor::SetShade (int r, int g, int b) { fillcolor = MAKEARGB(ColorMatcher.Pick (r, g, b), r, g, b); } DEFINE_ACTION_FUNCTION(AActor, SetShade) { PARAM_SELF_PROLOGUE(AActor); PARAM_INT(color); self->SetShade(color); return 0; } void AActor::SetPitch(DAngle p, bool interpolate, bool forceclamp) { if (player != NULL || forceclamp) { // clamp the pitch we set DAngle min, max; if (player != NULL) { min = player->MinPitch; max = player->MaxPitch; } else { min = -89.; max = 89.; } p = clamp(p, min, max); } if (p != Angles.Pitch) { Angles.Pitch = p; if (player != NULL && interpolate) { player->cheats |= CF_INTERPVIEW; } } } void AActor::SetAngle(DAngle ang, bool interpolate) { if (ang != Angles.Yaw) { Angles.Yaw = ang; if (player != NULL && interpolate) { player->cheats |= CF_INTERPVIEW; } } } void AActor::SetRoll(DAngle r, bool interpolate) { if (r != Angles.Roll) { Angles.Roll = r; if (player != NULL && interpolate) { player->cheats |= CF_INTERPVIEW; } } } PClassActor *AActor::GetBloodType(int type) const { IFVIRTUAL(AActor, GetBloodType) { VMValue params[] = { (DObject*)this, type }; PClassActor *res; VMReturn ret((void**)&res); VMCall(func, params, countof(params), &ret, 1); return res; } return nullptr; } DVector3 AActor::GetPortalTransition(double byoffset, sector_t **pSec) { bool moved = false; sector_t *sec = Sector; double testz = Z() + byoffset; DVector3 pos = Pos(); while (!sec->PortalBlocksMovement(sector_t::ceiling)) { if (testz >= sec->GetPortalPlaneZ(sector_t::ceiling)) { pos = PosRelative(sec->GetOppositePortalGroup(sector_t::ceiling)); sec = P_PointInSector(pos); moved = true; } else break; } if (!moved) { while (!sec->PortalBlocksMovement(sector_t::floor)) { if (testz < sec->GetPortalPlaneZ(sector_t::floor)) { pos = PosRelative(sec->GetOppositePortalGroup(sector_t::floor)); sec = P_PointInSector(pos); } else break; } } if (pSec) *pSec = sec; return pos; } void AActor::CheckPortalTransition(bool islinked) { bool moved = false; FLinkContext ctx; while (!Sector->PortalBlocksMovement(sector_t::ceiling)) { if (Z() >= Sector->GetPortalPlaneZ(sector_t::ceiling)) { DVector3 oldpos = Pos(); if (islinked && !moved) UnlinkFromWorld(&ctx); SetXYZ(PosRelative(Sector->GetOppositePortalGroup(sector_t::ceiling))); Prev += Pos() - oldpos; Sector = P_PointInSector(Pos()); PrevPortalGroup = Sector->PortalGroup; moved = true; } else break; } if (!moved) { while (!Sector->PortalBlocksMovement(sector_t::floor)) { double portalz = Sector->GetPortalPlaneZ(sector_t::floor); if (Z() < portalz && floorz < portalz) { DVector3 oldpos = Pos(); if (islinked && !moved) UnlinkFromWorld(&ctx); SetXYZ(PosRelative(Sector->GetOppositePortalGroup(sector_t::floor))); Prev += Pos() - oldpos; Sector = P_PointInSector(Pos()); PrevPortalGroup = Sector->PortalGroup; moved = true; } else break; } } if (islinked && moved) LinkToWorld(&ctx); } DEFINE_ACTION_FUNCTION(AActor, CheckPortalTransition) { PARAM_SELF_PROLOGUE(AActor); PARAM_BOOL(linked); self->CheckPortalTransition(linked); return 0; } // // P_MobjThinker // void AActor::Tick () { // [RH] Data for Heretic/Hexen scrolling sectors static const int8_t HexenCompatSpeeds[] = {-25, 0, -10, -5, 0, 5, 10, 0, 25 }; static const int8_t HexenScrollies[24][2] = { { 0, 1 }, { 0, 2 }, { 0, 4 }, { -1, 0 }, { -2, 0 }, { -4, 0 }, { 0, -1 }, { 0, -2 }, { 0, -4 }, { 1, 0 }, { 2, 0 }, { 4, 0 }, { 1, 1 }, { 2, 2 }, { 4, 4 }, { -1, 1 }, { -2, 2 }, { -4, 4 }, { -1, -1 }, { -2, -2 }, { -4, -4 }, { 1, -1 }, { 2, -2 }, { 4, -4 } }; static const uint8_t HereticScrollDirs[4] = { 6, 9, 1, 4 }; static const uint8_t HereticSpeedMuls[5] = { 5, 10, 25, 30, 35 }; AActor *onmo; //assert (state != NULL); if (state == NULL) { Printf("Actor of type %s at (%f,%f) left without a state\n", GetClass()->TypeName.GetChars(), X(), Y()); Destroy(); return; } if (flags5 & MF5_NOINTERACTION) { // only do the minimally necessary things here to save time: // Check the time freezer // apply velocity // ensure that the actor is not linked into the blockmap if (!(flags5 & MF5_NOTIMEFREEZE)) { //Added by MC: Freeze mode. if (bglobal.freeze || level.flags2 & LEVEL2_FROZEN) { // Boss cubes shouldn't be accelerated by timefreeze if (flags6 & MF6_BOSSCUBE) { special2++; } return; } } if (!Vel.isZero() || !(flags & MF_NOBLOCKMAP)) { FLinkContext ctx; UnlinkFromWorld(&ctx); flags |= MF_NOBLOCKMAP; SetXYZ(Vec3Offset(Vel)); CheckPortalTransition(false); LinkToWorld(&ctx); } flags8 &= ~MF8_INSCROLLSEC; } else { if (!player || !(player->cheats & CF_PREDICTING)) { // Handle powerup effects here so that the order is controlled // by the order in the inventory, not the order in the thinker table AActor *item = Inventory; while (item != NULL) { IFVIRTUALPTRNAME(item, NAME_Inventory, DoEffect) { VMValue params[1] = { item }; VMCall(func, params, 1, nullptr, 0); } item = item->Inventory; } } if (flags & MF_UNMORPHED) { return; } if (!(flags5 & MF5_NOTIMEFREEZE)) { // Boss cubes shouldn't be accelerated by timefreeze if (flags6 & MF6_BOSSCUBE) { special2++; } //Added by MC: Freeze mode. if (bglobal.freeze && !(player && player->Bot == NULL)) { return; } // Apply freeze mode. if ((level.flags2 & LEVEL2_FROZEN) && (player == NULL || player->timefreezer == 0)) { return; } } if (effects & FX_ROCKET) { if (++smokecounter == 4) { // add some smoke behind the rocket smokecounter = 0; AActor *th = Spawn("RocketSmokeTrail", Vec3Offset(-Vel), ALLOW_REPLACE); if (th) { th->tics -= pr_rockettrail()&3; if (th->tics < 1) th->tics = 1; if (!(cl_rockettrails & 2)) th->renderflags |= RF_INVISIBLE; } } } else if (effects & FX_GRENADE) { if (++smokecounter == 8) { smokecounter = 0; DAngle moveangle = Vel.Angle(); double xo = -moveangle.Cos() * radius * 2 + pr_rockettrail() / 64.; double yo = -moveangle.Sin() * radius * 2 + pr_rockettrail() / 64.; double zo = -Height * Vel.Z / 8. + Height * (2 / 3.); AActor * th = Spawn("GrenadeSmokeTrail", Vec3Offset(xo, yo, zo), ALLOW_REPLACE); if (th) { th->tics -= pr_rockettrail()&3; if (th->tics < 1) th->tics = 1; if (!(cl_rockettrails & 2)) th->renderflags |= RF_INVISIBLE; } } } double oldz = Z(); // [RH] Give the pain elemental vertical friction // This used to be in APainElemental::Tick but in order to use // A_PainAttack with other monsters it has to be here if (flags4 & MF4_VFRICTION) { if (health >0) { if (fabs (Vel.Z) < 0.25) { Vel.Z = 0; flags4 &= ~MF4_VFRICTION; } else { Vel.Z *= (0xe800 / 65536.); } } } // [RH] Pulse in and out of visibility if (effects & FX_VISIBILITYPULSE) { if (visdir > 0) { Alpha += 1/32.; if (Alpha >= 1.) { Alpha = 1.; visdir = -1; } } else { Alpha -= 1/32.; if (Alpha <= 0.25) { Alpha = 0.25; visdir = 1; } } } else if (flags & MF_STEALTH) { // [RH] Fade a stealth monster in and out of visibility RenderStyle.Flags &= ~STYLEF_Alpha1; if (visdir > 0) { Alpha += 2./TICRATE; if (Alpha > 1.) { Alpha = 1.; visdir = 0; } } else if (visdir < 0) { Alpha -= 1.5/TICRATE; if (Alpha < StealthAlpha) { Alpha = StealthAlpha; visdir = 0; } } } if (bglobal.botnum && !demoplayback && ((flags & (MF_SPECIAL|MF_MISSILE)) || (flags3 & MF3_ISMONSTER))) { bglobal.BotTick(this); } // [RH] Consider carrying sectors here DVector2 cumm(0, 0); if ((((flags8 & MF8_INSCROLLSEC) && level.Scrolls.Size() > 0) || player != NULL) && !(flags & MF_NOCLIP) && !(flags & MF_NOSECTOR)) { double height, waterheight; // killough 4/4/98: add waterheight const msecnode_t *node; int countx, county; // Clear the flag for the next frame. flags8 &= ~MF8_INSCROLLSEC; // killough 3/7/98: Carry things on floor // killough 3/20/98: use new sector list which reflects true members // killough 3/27/98: fix carrier bug // killough 4/4/98: Underwater, carry things even w/o gravity // Move objects only if on floor or underwater, // non-floating, and clipped. countx = county = 0; for (node = touching_sectorlist; node; node = node->m_tnext) { sector_t *sec = node->m_sector; DVector2 scrollv; if (level.Scrolls.Size() > unsigned(sec->Index())) { scrollv = level.Scrolls[sec->Index()]; } else { scrollv.Zero(); } if (player != NULL) { int scrolltype = sec->special; if (scrolltype >= Scroll_North_Slow && scrolltype <= Scroll_SouthWest_Fast) { // Hexen scroll special scrolltype -= Scroll_North_Slow; if (i_compatflags&COMPATF_RAVENSCROLL) { scrollv.X -= HexenCompatSpeeds[HexenScrollies[scrolltype][0]+4] * (1. / (32 * CARRYFACTOR)); scrollv.Y += HexenCompatSpeeds[HexenScrollies[scrolltype][1]+4] * (1. / (32 * CARRYFACTOR)); } else { // Use speeds that actually match the scrolling textures! scrollv.X -= HexenScrollies[scrolltype][0] * 0.5; scrollv.Y += HexenScrollies[scrolltype][1] * 0.5; } } else if (scrolltype >= Carry_East5 && scrolltype <= Carry_West35) { // Heretic scroll special scrolltype -= Carry_East5; uint8_t dir = HereticScrollDirs[scrolltype / 5]; double carryspeed = HereticSpeedMuls[scrolltype % 5] * (1. / (32 * CARRYFACTOR)); if (scrolltype < 5 && !(i_compatflags&COMPATF_RAVENSCROLL)) { // Use speeds that actually match the scrolling textures! carryspeed = (1 << ((scrolltype % 5) + 15)) / 65536.; } scrollv.X += carryspeed * ((dir & 3) - 1); scrollv.Y += carryspeed * (((dir & 12) >> 2) - 1); } else if (scrolltype == dScroll_EastLavaDamage) { // Special Heretic scroll special if (i_compatflags&COMPATF_RAVENSCROLL) { scrollv.X += 28. / (32*CARRYFACTOR); } else { // Use a speed that actually matches the scrolling texture! scrollv.X += 12. / (32 * CARRYFACTOR); } } else if (scrolltype == Scroll_StrifeCurrent) { // Strife scroll special int anglespeed = level.GetFirstSectorTag(sec) - 100; double carryspeed = (anglespeed % 10) / (16 * CARRYFACTOR); DAngle angle = ((anglespeed / 10) * 45.); scrollv += angle.ToVector(carryspeed); } } if (scrollv.isZero()) { continue; } sector_t *heightsec = sec->GetHeightSec(); if (flags & MF_NOGRAVITY && heightsec == NULL) { continue; } DVector3 pos = PosRelative(sec); height = sec->floorplane.ZatPoint (pos); double height2 = sec->floorplane.ZatPoint(this); if (isAbove(height)) { if (heightsec == NULL) { continue; } waterheight = heightsec->floorplane.ZatPoint (pos); if (waterheight > height && Z() >= waterheight) { continue; } } cumm += scrollv; if (scrollv.X) countx++; if (scrollv.Y) county++; } // Some levels designed with Boom in mind actually want things to accelerate // at neighboring scrolling sector boundaries. But it is only important for // non-player objects. if (player != NULL || !(i_compatflags & COMPATF_BOOMSCROLL)) { if (countx > 1) { cumm.X /= countx; } if (county > 1) { cumm.Y /= county; } } } // [RH] If standing on a steep slope, fall down it if ((flags & MF_SOLID) && !(flags & (MF_NOCLIP|MF_NOGRAVITY)) && !(flags & MF_NOBLOCKMAP) && Vel.Z <= 0 && floorz == Z()) { secplane_t floorplane; // Check 3D floors as well floorplane = P_FindFloorPlane(floorsector, PosAtZ(floorz)); if (floorplane.fC() < STEEPSLOPE && floorplane.ZatPoint (PosRelative(floorsector)) <= floorz) { const msecnode_t *node; bool dopush = true; if (floorplane.fC() > STEEPSLOPE*2/3) { for (node = touching_sectorlist; node; node = node->m_tnext) { const sector_t *sec = node->m_sector; if (sec->floorplane.fC() >= STEEPSLOPE) { if (floorplane.ZatPoint(PosRelative(node->m_sector)) >= Z() - MaxStepHeight) { dopush = false; break; } } } } if (dopush) { Vel += floorplane.Normal().XY(); } } } // [RH] Missiles moving perfectly vertical need some X/Y movement, or they // won't hurt anything. Don't do this if damage is 0! That way, you can // still have missiles that go straight up and down through actors without // damaging anything. // (for backwards compatibility this must check for lack of damage function, not for zero damage!) if ((flags & MF_MISSILE) && Vel.X == 0 && Vel.Y == 0 && !IsZeroDamage()) { Vel.X = MinVel; } // Handle X and Y velocities BlockingMobj = nullptr; sector_t* oldBlockingCeiling = BlockingCeiling; sector_t* oldBlockingFloor = BlockingFloor; Blocking3DFloor = nullptr; BlockingFloor = nullptr; BlockingCeiling = nullptr; double oldfloorz = P_XYMovement (this, cumm); if (ObjectFlags & OF_EuthanizeMe) { // actor was destroyed return; } // [ZZ] trigger hit floor/hit ceiling actions from XY movement if (BlockingFloor && BlockingFloor != oldBlockingFloor && (!player || !(player->cheats & CF_PREDICTING)) && BlockingFloor->SecActTarget) BlockingFloor->TriggerSectorActions(this, SECSPAC_HitFloor); if (BlockingCeiling && BlockingCeiling != oldBlockingCeiling && (!player || !(player->cheats & CF_PREDICTING)) && BlockingCeiling->SecActTarget) BlockingCeiling->TriggerSectorActions(this, SECSPAC_HitCeiling); if (Vel.X == 0 && Vel.Y == 0) // Actors at rest { if (flags2 & MF2_BLASTED) { // Reset to not blasted when velocities are gone flags2 &= ~MF2_BLASTED; } if ((flags6 & MF6_TOUCHY) && !IsSentient()) { // Arm a mine which has come to rest flags6 |= MF6_ARMED; } } if (Vel.Z != 0 || BlockingMobj || Z() != floorz) { // Handle Z velocity and gravity if (((flags2 & MF2_PASSMOBJ) || (flags & MF_SPECIAL)) && !(i_compatflags & COMPATF_NO_PASSMOBJ)) { if (!(onmo = P_CheckOnmobj (this))) { P_ZMovement (this, oldfloorz); flags2 &= ~MF2_ONMOBJ; } else { if (player) { if (Vel.Z < level.gravity * Sector->gravity * (-1./100)// -655.36f) && !(flags&MF_NOGRAVITY)) { PlayerLandedOnThing (this, onmo); } } if (onmo->Top() - Z() <= MaxStepHeight) { if (player && player->mo == this) { player->viewheight -= onmo->Top() - Z(); double deltaview = player->GetDeltaViewHeight(); if (deltaview > player->deltaviewheight) { player->deltaviewheight = deltaview; } } SetZ(onmo->Top()); } // Check for MF6_BUMPSPECIAL // By default, only players can activate things by bumping into them // We trigger specials as long as we are on top of it and not just when // we land on it. This could be considered as gravity making us continually // bump into it, but it also avoids having to worry about walking on to // something without dropping and not triggering anything. if ((onmo->flags6 & MF6_BUMPSPECIAL) && ((player != NULL) || ((onmo->activationtype & THINGSPEC_MonsterTrigger) && (flags3 & MF3_ISMONSTER)) || ((onmo->activationtype & THINGSPEC_MissileTrigger) && (flags & MF_MISSILE)) ) && (level.maptime > onmo->lastbump)) // Leave the bumper enough time to go away { if (player == NULL || !(player->cheats & CF_PREDICTING)) { if (P_ActivateThingSpecial(onmo, this)) onmo->lastbump = level.maptime + TICRATE; } } if (Vel.Z != 0 && (BounceFlags & BOUNCE_Actors)) { P_BounceActor(this, onmo, true); } else { flags2 |= MF2_ONMOBJ; Vel.Z = 0; Crash(); } } } else { P_ZMovement (this, oldfloorz); } if (ObjectFlags & OF_EuthanizeMe) return; // actor was destroyed } else if (Z() <= floorz) { Crash(); if (ObjectFlags & OF_EuthanizeMe) return; // actor was destroyed } CheckPortalTransition(true); UpdateWaterLevel (); // [RH] Don't advance if predicting a player if (player && (player->cheats & CF_PREDICTING)) { return; } // Check for poison damage, but only once per PoisonPeriod tics (or once per second if none). if (PoisonDurationReceived && (level.time % (PoisonPeriodReceived ? PoisonPeriodReceived : TICRATE) == 0)) { P_DamageMobj(this, NULL, Poisoner, PoisonDamageReceived, PoisonDamageTypeReceived ? PoisonDamageTypeReceived : (FName)NAME_Poison, 0); --PoisonDurationReceived; // Must clear damage when duration is done, otherwise it // could be added to with ADDITIVEPOISONDAMAGE. if (!PoisonDurationReceived) PoisonDamageReceived = 0; } } assert (state != NULL); if (state == NULL) { Destroy(); return; } if (!CheckNoDelay()) return; // freed itself // cycle through states, calling action functions at transitions UpdateRenderSectorList(); if (tics != -1) { // [RH] Use tics <= 0 instead of == 0 so that spawnstates // of 0 tics work as expected. if (--tics <= 0) { if (!SetState(state->GetNextState())) return; // freed itself } } if (tics == -1 || state->GetCanRaise()) { int respawn_monsters = G_SkillProperty(SKILLP_Respawn); // check for nightmare respawn if (!(flags5 & MF5_ALWAYSRESPAWN)) { if (!respawn_monsters || !(flags3 & MF3_ISMONSTER) || (flags2 & MF2_DORMANT) || (flags5 & MF5_NEVERRESPAWN)) return; int limit = G_SkillProperty (SKILLP_RespawnLimit); if (limit > 0 && skillrespawncount >= limit) return; } movecount++; if (movecount < respawn_monsters) return; if (level.time & 31) return; if (pr_nightmarerespawn() > 4) return; P_NightmareRespawn (this); } } //========================================================================== // // AActor :: CheckNoDelay // //========================================================================== bool AActor::CheckNoDelay() { if ((flags7 & MF7_HANDLENODELAY) && !(flags2 & MF2_DORMANT)) { flags7 &= ~MF7_HANDLENODELAY; if (state->GetNoDelay()) { // For immediately spawned objects with the NoDelay flag set for their // Spawn state, explicitly call the current state's function. FState *newstate; FStateParamInfo stp = { state, STATE_Actor, PSP_WEAPON }; if (state->CallAction(this, this, &stp, &newstate)) { if (ObjectFlags & OF_EuthanizeMe) { return false; // freed itself } if (newstate != NULL) { return SetState(newstate); } } } } return true; } DEFINE_ACTION_FUNCTION(AActor, CheckNoDelay) { PARAM_SELF_PROLOGUE(AActor); ACTION_RETURN_BOOL(self->CheckNoDelay()); } //========================================================================== // // AActor :: CheckSectorTransition // // Fire off some sector triggers if the actor has changed sectors. // //========================================================================== void AActor::CheckSectorTransition(sector_t *oldsec) { if (oldsec != Sector) { if (oldsec->SecActTarget != NULL) { oldsec->TriggerSectorActions(this, SECSPAC_Exit); } if (Sector->SecActTarget != NULL) { int act = SECSPAC_Enter; if (Z() <= Sector->floorplane.ZatPoint(this)) { act |= SECSPAC_HitFloor; } if (Top() >= Sector->ceilingplane.ZatPoint(this)) { act |= SECSPAC_HitCeiling; } if (Sector->heightsec != NULL && Z() == Sector->heightsec->floorplane.ZatPoint(this)) { act |= SECSPAC_HitFakeFloor; } Sector->TriggerSectorActions(this, act); } if (Z() == floorz) { P_CheckFor3DFloorHit(this, Z(), true); } if (Top() == ceilingz) { P_CheckFor3DCeilingHit(this, Top(), true); } } } //========================================================================== // // AActor::SplashCheck // // Returns true if actor should splash // //========================================================================== void AActor::SplashCheck() { double fh = -FLT_MAX; bool reset = false; waterlevel = 0; if (Sector == NULL) { return; } if (Sector->MoreFlags & SECMF_UNDERWATER) // intentionally not SECMF_UNDERWATERMASK { waterlevel = 3; } else { const sector_t *hsec = Sector->GetHeightSec(); if (hsec != NULL) { fh = hsec->floorplane.ZatPoint(this); //if (hsec->MoreFlags & SECMF_UNDERWATERMASK) // also check Boom-style non-swimmable sectors { if (Z() < fh) { waterlevel = 1; if (Center() < fh) { waterlevel = 2; if ((player && Z() + player->viewheight <= fh) || (Top() <= fh)) { waterlevel = 3; } } } else if (!(hsec->MoreFlags & SECMF_FAKEFLOORONLY) && (Top() > hsec->ceilingplane.ZatPoint(this))) { waterlevel = 3; } else { waterlevel = 0; } } } else { // Check 3D floors as well! for (auto rover : Sector->e->XFloor.ffloors) { if (!(rover->flags & FF_EXISTS)) continue; if (rover->flags & FF_SOLID) continue; bool reset = !(rover->flags & FF_SWIMMABLE); if (reset && rover->alpha == 0) continue; double ff_bottom = rover->bottom.plane->ZatPoint(this); double ff_top = rover->top.plane->ZatPoint(this); if (ff_top <= Z() || ff_bottom > (Center())) continue; fh = ff_top; if (Z() < fh) { waterlevel = 1; if (Center() < fh) { waterlevel = 2; if ((player && Z() + player->viewheight <= fh) || (Top() <= fh)) { waterlevel = 3; } } } break; } } } // some additional checks to make deep sectors like Boom's splash without setting // the water flags. if (boomwaterlevel == 0 && waterlevel != 0) { P_HitWater(this, Sector, PosAtZ(fh), true); } boomwaterlevel = waterlevel; return; } //========================================================================== // // AActor::UpdateWaterLevel // // Returns true if actor should splash // //========================================================================== bool AActor::UpdateWaterLevel(bool dosplash) { if (dosplash) SplashCheck(); double fh = -FLT_MAX; bool reset = false; int oldlevel = waterlevel; waterlevel = 0; if (Sector != nullptr) { if (Sector->MoreFlags & SECMF_UNDERWATER) // intentionally not SECMF_UNDERWATERMASK { waterlevel = 3; } else { const sector_t *hsec = Sector->GetHeightSec(); if (hsec != NULL) { fh = hsec->floorplane.ZatPoint(this); if (hsec->MoreFlags & SECMF_UNDERWATERMASK) // also check Boom-style non-swimmable sectors { if (Z() < fh) { waterlevel = 1; if (Center() < fh) { waterlevel = 2; if ((player && Z() + player->viewheight <= fh) || (Top() <= fh)) { waterlevel = 3; } } } else if (!(hsec->MoreFlags & SECMF_FAKEFLOORONLY) && (Top() > hsec->ceilingplane.ZatPoint(this))) { waterlevel = 3; } else { waterlevel = 0; } } } else { // Check 3D floors as well! for (auto rover : Sector->e->XFloor.ffloors) { if (!(rover->flags & FF_EXISTS)) continue; if (rover->flags & FF_SOLID) continue; if (!(rover->flags & FF_SWIMMABLE)) continue; double ff_bottom = rover->bottom.plane->ZatPoint(this); double ff_top = rover->top.plane->ZatPoint(this); if (ff_top <= Z() || ff_bottom > (Center())) continue; fh = ff_top; if (Z() < fh) { waterlevel = 1; if (Center() < fh) { waterlevel = 2; if ((player && Z() + player->viewheight <= fh) || (Top() <= fh)) { waterlevel = 3; } } } break; } } } // Play surfacing and diving sounds, as appropriate. if (player != nullptr) { if (oldlevel < 3 && waterlevel == 3) { // Our head just went under. S_Sound(this, CHAN_VOICE, "*dive", 1, ATTN_NORM); } else if (oldlevel == 3 && waterlevel < 3) { // Our head just came up. if (player->air_finished > level.time) { // We hadn't run out of air yet. S_Sound(this, CHAN_VOICE, "*surface", 1, ATTN_NORM); } // If we were running out of air, then ResetAirSupply() will play *gasp. } } } return false; // we did the splash ourselves } DEFINE_ACTION_FUNCTION(AActor, UpdateWaterLevel) { PARAM_SELF_PROLOGUE(AActor); PARAM_BOOL(splash); ACTION_RETURN_BOOL(self->UpdateWaterLevel(splash)); } //========================================================================== // // P_SpawnMobj // //========================================================================== AActor *AActor::StaticSpawn (PClassActor *type, const DVector3 &pos, replace_t allowreplacement, bool SpawningMapThing) { if (type == NULL) { I_Error ("Tried to spawn a class-less actor\n"); } if (allowreplacement) { type = type->GetReplacement(); } AActor *actor; actor = static_cast(const_cast(type)->CreateNew ()); actor->SpawnTime = level.totaltime; actor->SpawnOrder = level.spawnindex++; // Set default dialogue actor->ConversationRoot = level.GetConversation(actor->GetClass()->TypeName); if (actor->ConversationRoot != -1) { actor->Conversation = level.StrifeDialogues[actor->ConversationRoot]; } else { actor->Conversation = NULL; } actor->SetXYZ(pos); actor->OldRenderPos = { FLT_MAX, FLT_MAX, FLT_MAX }; actor->picnum.SetInvalid(); actor->health = actor->SpawnHealth(); // Actors with zero gravity need the NOGRAVITY flag set. if (actor->Gravity == 0) actor->flags |= MF_NOGRAVITY; FRandom &rng = bglobal.m_Thinking ? pr_botspawnmobj : pr_spawnmobj; if (actor->isFast() && actor->flags3 & MF3_ISMONSTER) actor->reactiontime = 0; if (actor->flags3 & MF3_ISMONSTER) { actor->LastLookPlayerNumber = rng() % MAXPLAYERS; actor->TIDtoHate = 0; } // Set the state, but do not use SetState, because action // routines can't be called yet. If the spawnstate has an action // routine, it will not be called. FState *st = actor->SpawnState; actor->state = st; actor->tics = st->GetTics(); actor->sprite = st->sprite; actor->frame = st->GetFrame(); actor->renderflags = (actor->renderflags & ~RF_FULLBRIGHT) | ActorRenderFlags::FromInt (st->GetFullbright()); actor->touching_sectorlist = nullptr; // NULL head of sector list // phares 3/13/98 actor->touching_rendersectors = nullptr; if (G_SkillProperty(SKILLP_FastMonsters)) { double f = actor->FloatVar(NAME_FastSpeed); if (f >= 0) actor->Speed = f; } // set subsector and/or block links actor->LinkToWorld (nullptr, SpawningMapThing); actor->ClearInterpolation(); actor->dropoffz = actor->floorz = actor->Sector->floorplane.ZatPoint(pos); actor->ceilingz = actor->Sector->ceilingplane.ZatPoint(pos); // The z-coordinate needs to be set once before calling P_FindFloorCeiling // For FLOATRANDZ just use the floor here. if (pos.Z == ONFLOORZ || pos.Z == FLOATRANDZ) { actor->SetZ(actor->floorz); } else if (pos.Z == ONCEILINGZ) { actor->SetZ(actor->ceilingz - actor->Height); } if (SpawningMapThing || !type->IsDescendantOf (NAME_PlayerPawn)) { // Check if there's something solid to stand on between the current position and the // current sector's floor. For map spawns this must be delayed until after setting the // z-coordinate. if (!SpawningMapThing) { P_FindFloorCeiling(actor, FFCF_ONLYSPAWNPOS); } else { actor->floorsector = actor->Sector; actor->floorpic = actor->floorsector->GetTexture(sector_t::floor); actor->floorterrain = actor->floorsector->GetTerrain(sector_t::floor); actor->ceilingsector = actor->Sector; actor->ceilingpic = actor->ceilingsector->GetTexture(sector_t::ceiling); } } else if (!(actor->flags5 & MF5_NOINTERACTION)) { P_FindFloorCeiling (actor); } else { actor->floorpic = actor->Sector->GetTexture(sector_t::floor); actor->floorterrain = actor->Sector->GetTerrain(sector_t::floor); actor->floorsector = actor->Sector; actor->ceilingpic = actor->Sector->GetTexture(sector_t::ceiling); actor->ceilingsector = actor->Sector; } actor->SpawnPoint.X = pos.X; actor->SpawnPoint.Y = pos.Y; // do not copy Z! if (pos.Z == ONFLOORZ) { actor->SetZ(actor->floorz); } else if (pos.Z == ONCEILINGZ) { actor->SetZ(actor->ceilingz - actor->Height); } else if (pos.Z == FLOATRANDZ) { double space = actor->ceilingz - actor->Height - actor->floorz; if (space > 48) { space -= 40; actor->SetZ( space * rng() / 256. + actor->floorz + 40); } else { actor->SetZ(actor->floorz); } } else { actor->SpawnPoint.Z = (actor->Z() - actor->Sector->floorplane.ZatPoint(actor)); } if (actor->FloatBobPhase == (uint8_t)-1) actor->FloatBobPhase = rng(); // Don't make everything bob in sync (unless deliberately told to do) if (actor->flags2 & MF2_FLOORCLIP) { actor->AdjustFloorClip (); } else { actor->Floorclip = 0; } actor->UpdateWaterLevel (false); if (!SpawningMapThing) { actor->CallBeginPlay (); if (actor->ObjectFlags & OF_EuthanizeMe) { return NULL; } } if (level.flags & LEVEL_NOALLIES && !actor->player) { actor->flags &= ~MF_FRIENDLY; } // [RH] Count monsters whenever they are spawned. if (actor->CountsAsKill()) { level.total_monsters++; } // [RH] Same, for items if (actor->flags & MF_COUNTITEM) { level.total_items++; } // And for secrets if (actor->flags5 & MF5_COUNTSECRET) { level.total_secrets++; } // force scroller check in the first tic. actor->flags8 |= MF8_INSCROLLSEC; return actor; } DEFINE_ACTION_FUNCTION(AActor, Spawn) { PARAM_PROLOGUE; PARAM_CLASS_NOT_NULL(type, AActor); PARAM_FLOAT(x); PARAM_FLOAT(y); PARAM_FLOAT(z); PARAM_INT(flags); ACTION_RETURN_OBJECT(AActor::StaticSpawn(type, DVector3(x, y, z), replace_t(flags))); } PClassActor *ClassForSpawn(FName classname) { PClass *cls = PClass::FindClass(classname); if (cls == NULL) { I_Error("Attempt to spawn actor of unknown type '%s'\n", classname.GetChars()); } if (!cls->IsDescendantOf(RUNTIME_CLASS(AActor))) { I_Error("Attempt to spawn non-actor of type '%s'\n", classname.GetChars()); } return static_cast(cls); } void AActor::LevelSpawned () { if (tics > 0 && !(flags4 & MF4_SYNCHRONIZED)) { tics = 1 + (pr_spawnmapthing() % tics); } // [RH] Clear MF_DROPPED flag if the default version doesn't have it set. // (Inventory.BeginPlay() makes all inventory items spawn with it set.) if (!(GetDefault()->flags & MF_DROPPED)) { flags &= ~MF_DROPPED; } HandleSpawnFlags (); } void AActor::HandleSpawnFlags () { if (SpawnFlags & MTF_AMBUSH) { flags |= MF_AMBUSH; } if (SpawnFlags & MTF_DORMANT) { CallDeactivate (NULL); } if (SpawnFlags & MTF_STANDSTILL) { flags4 |= MF4_STANDSTILL; } if (SpawnFlags & MTF_FRIENDLY) { flags |= MF_FRIENDLY; // Friendlies don't count as kills! if (flags & MF_COUNTKILL) { flags &= ~MF_COUNTKILL; level.total_monsters--; } } if (SpawnFlags & MTF_SHADOW) { flags |= MF_SHADOW; RenderStyle = STYLE_Translucent; Alpha = 0.25; } else if (SpawnFlags & MTF_ALTSHADOW) { RenderStyle = STYLE_None; } if (SpawnFlags & MTF_SECRET) { if (!(flags5 & MF5_COUNTSECRET)) { //Printf("Secret %s in sector %i!\n", GetTag(), Sector->sectornum); flags5 |= MF5_COUNTSECRET; level.total_secrets++; } } } DEFINE_ACTION_FUNCTION(AActor, HandleSpawnFlags) { PARAM_SELF_PROLOGUE(AActor); self->HandleSpawnFlags(); return 0; } void AActor::BeginPlay () { // If the actor is spawned with the dormant flag set, clear it, and use // the normal deactivation logic to make it properly dormant. if (flags2 & MF2_DORMANT) { flags2 &= ~MF2_DORMANT; CallDeactivate (NULL); } } DEFINE_ACTION_FUNCTION(AActor, BeginPlay) { PARAM_SELF_PROLOGUE(AActor); self->BeginPlay(); return 0; } void AActor::CallBeginPlay() { IFVIRTUAL(AActor, BeginPlay) { // Without the type cast this picks the 'void *' assignment... VMValue params[1] = { (DObject*)this }; VMCall(func, params, 1, nullptr, 0); } else BeginPlay(); } void AActor::PostBeginPlay () { SetDynamicLights(); PrevAngles = Angles; flags7 |= MF7_HANDLENODELAY; } void AActor::CallPostBeginPlay() { Super::CallPostBeginPlay(); E_WorldThingSpawned(this); } bool AActor::isFast() { if (flags5&MF5_ALWAYSFAST) return true; if (flags5&MF5_NEVERFAST) return false; return !!G_SkillProperty(SKILLP_FastMonsters); } bool AActor::isSlow() { return !!G_SkillProperty(SKILLP_SlowMonsters); } //=========================================================================== // // Activate // //=========================================================================== void AActor::Activate (AActor *activator) { if ((flags3 & MF3_ISMONSTER) && (health > 0 || (flags & MF_ICECORPSE))) { if (flags2 & MF2_DORMANT) { flags2 &= ~MF2_DORMANT; FState *state = FindState(NAME_Active); if (state != NULL) { SetState(state); } else { tics = 1; } } } } DEFINE_ACTION_FUNCTION(AActor, Activate) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT(activator, AActor); self->Activate(activator); return 0; } void AActor::CallActivate(AActor *activator) { IFVIRTUAL(AActor, Activate) { // Without the type cast this picks the 'void *' assignment... VMValue params[2] = { (DObject*)this, (DObject*)activator }; VMCall(func, params, 2, nullptr, 0); } else Activate(activator); } //=========================================================================== // // Deactivate // //=========================================================================== void AActor::Deactivate (AActor *activator) { if ((flags3 & MF3_ISMONSTER) && (health > 0 || (flags & MF_ICECORPSE))) { if (!(flags2 & MF2_DORMANT)) { flags2 |= MF2_DORMANT; FState *state = FindState(NAME_Inactive); if (state != NULL) { SetState(state); } else { tics = -1; } } } } DEFINE_ACTION_FUNCTION(AActor, Deactivate) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT(activator, AActor); self->Deactivate(activator); return 0; } void AActor::CallDeactivate(AActor *activator) { IFVIRTUAL(AActor, Deactivate) { // Without the type cast this picks the 'void *' assignment... VMValue params[2] = { (DObject*)this, (DObject*)activator }; VMCall(func, params, 2, nullptr, 0); } else Deactivate(activator); } //=========================================================================== // // Destroy // //=========================================================================== void AActor::OnDestroy () { // [ZZ] call destroy event hook. // note that this differs from ThingSpawned in that you can actually override OnDestroy to avoid calling the hook. // but you can't really do that without utterly breaking the game, so it's ok. // note: if OnDestroy is ever made optional, E_WorldThingDestroyed should still be called for ANY thing. E_WorldThingDestroyed(this); DeleteAttachedLights(); ClearRenderSectorList(); ClearRenderLineList(); // [RH] Destroy any inventory this actor is carrying DestroyAllInventory (); // [RH] Unlink from tid chain RemoveFromHash (); // unlink from sector and block lists UnlinkFromWorld (nullptr); flags |= MF_NOSECTOR|MF_NOBLOCKMAP; // Transform any playing sound into positioned, non-actor sounds. S_RelinkSound (this, NULL); Super::OnDestroy(); } //=========================================================================== // // AdjustFloorClip // //=========================================================================== void AActor::AdjustFloorClip () { if (flags3 & MF3_SPECIALFLOORCLIP) { return; } double oldclip = Floorclip; double shallowestclip = INT_MAX; const msecnode_t *m; // possibly standing on a 3D-floor if (Sector->e->XFloor.ffloors.Size() && Z() > Sector->floorplane.ZatPoint(this)) Floorclip = 0; // [RH] clip based on shallowest floor player is standing on // If the sector has a deep water effect, then let that effect // do the floorclipping instead of the terrain type. for (m = touching_sectorlist; m; m = m->m_tnext) { DVector3 pos = PosRelative(m->m_sector); sector_t *hsec = m->m_sector->GetHeightSec(); if (hsec == NULL && m->m_sector->floorplane.ZatPoint (pos) == Z()) { double clip = Terrains[m->m_sector->GetTerrain(sector_t::floor)].FootClip; if (clip < shallowestclip) { shallowestclip = clip; } } } if (shallowestclip == INT_MAX) { Floorclip = 0; } else { Floorclip = shallowestclip; } if (player && player->mo == this && oldclip != Floorclip) { player->viewheight -= (oldclip - Floorclip); player->deltaviewheight = player->GetDeltaViewHeight(); } } DEFINE_ACTION_FUNCTION(AActor, AdjustFloorClip) { PARAM_SELF_PROLOGUE(AActor); self->AdjustFloorClip(); return 0; } // // P_SpawnPlayer // Called when a player is spawned on the level. // Most of the player structure stays unchanged between levels. // EXTERN_CVAR (Bool, chasedemo) EXTERN_CVAR(Bool, sv_singleplayerrespawn) EXTERN_CVAR(Float, fov) extern bool demonew; AActor *P_SpawnPlayer (FPlayerStart *mthing, int playernum, int flags) { player_t *p; AActor *mobj, *oldactor; uint8_t state; DVector3 spawn; DAngle SpawnAngle; if (mthing == NULL) { return NULL; } // not playing? if ((unsigned)playernum >= (unsigned)MAXPLAYERS || !playeringame[playernum]) return NULL; // Old lerp data needs to go if (playernum == consoleplayer) { P_PredictionLerpReset(); } p = &players[playernum]; if (p->cls == NULL) { // [GRB] Pick a class from player class list if (PlayerClasses.Size () > 1) { int type; if (!deathmatch || !multiplayer) { type = SinglePlayerClass[playernum]; } else { type = p->userinfo.GetPlayerClassNum(); if (type < 0) { type = pr_multiclasschoice() % PlayerClasses.Size (); } } p->CurrentPlayerClass = type; } else { p->CurrentPlayerClass = 0; } p->cls = PlayerClasses[p->CurrentPlayerClass].Type; } if (( dmflags2 & DF2_SAME_SPAWN_SPOT ) && ( p->playerstate == PST_REBORN ) && ( deathmatch == false ) && ( gameaction != ga_worlddone ) && ( p->mo != NULL ) && ( !(p->mo->Sector->Flags & SECF_NORESPAWN) ) && ( NULL != p->attacker ) && // don't respawn on damaging floors ( p->mo->Sector->damageamount < TELEFRAG_DAMAGE )) // this really should be a bit smarter... { spawn = p->mo->Pos(); SpawnAngle = p->mo->Angles.Yaw; } else { spawn.X = mthing->pos.X; spawn.Y = mthing->pos.Y; // Allow full angular precision SpawnAngle = (double)mthing->angle; if (i_compatflags2 & COMPATF2_BADANGLES) { SpawnAngle += 0.01; } if (GetDefaultByType(p->cls)->flags & MF_SPAWNCEILING) spawn.Z = ONCEILINGZ; else if (GetDefaultByType(p->cls)->flags2 & MF2_SPAWNFLOAT) spawn.Z = FLOATRANDZ; else spawn.Z = ONFLOORZ; } mobj = Spawn (p->cls, spawn, NO_REPLACE); if (level.flags & LEVEL_USEPLAYERSTARTZ) { if (spawn.Z == ONFLOORZ) mobj->AddZ(mthing->pos.Z); else if (spawn.Z == ONCEILINGZ) mobj->AddZ(-mthing->pos.Z); P_FindFloorCeiling(mobj, FFCF_SAMESECTOR | FFCF_ONLY3DFLOORS | FFCF_3DRESTRICT); } mobj->FriendPlayer = playernum + 1; // [RH] players are their own friends oldactor = p->mo; p->mo = mobj; mobj->player = p; state = p->playerstate; if (state == PST_REBORN || state == PST_ENTER) { G_PlayerReborn (playernum); } else if (oldactor != NULL && oldactor->player == p && !(flags & SPF_TEMPPLAYER)) { // Move the voodoo doll's inventory to the new player. IFVM(Actor, ObtainInventory) { VMValue params[] = { mobj, oldactor }; VMCall(func, params, 2, nullptr, 0); } level.Behaviors.StopMyScripts (oldactor); // cancel all ENTER/RESPAWN scripts for the voodoo doll } // [GRB] Reset skin p->userinfo.SkinNumChanged(R_FindSkin (Skins[p->userinfo.GetSkin()].Name, p->CurrentPlayerClass)); if (!(mobj->flags2 & MF2_DONTTRANSLATE)) { // [RH] Be sure the player has the right translation R_BuildPlayerTranslation (playernum); // [RH] set color translations for player sprites mobj->Translation = TRANSLATION(TRANSLATION_Players,playernum); } mobj->Angles.Yaw = SpawnAngle; mobj->Angles.Pitch = mobj->Angles.Roll = 0.; mobj->health = p->health; // [RH] Set player sprite based on skin if (!(mobj->flags4 & MF4_NOSKIN)) { mobj->sprite = Skins[p->userinfo.GetSkin()].sprite; } p->DesiredFOV = p->FOV = fov; p->camera = p->mo; p->playerstate = PST_LIVE; p->refire = 0; p->damagecount = 0; p->bonuscount = 0; p->morphTics = 0; p->MorphedPlayerClass = 0; p->MorphStyle = 0; p->MorphExitFlash = NULL; p->extralight = 0; p->fixedcolormap = NOFIXEDCOLORMAP; p->fixedlightlevel = -1; p->viewheight = p->DefaultViewHeight(); p->inconsistant = 0; p->attacker = nullptr; p->spreecount = 0; p->multicount = 0; p->lastkilltime = 0; p->BlendR = p->BlendG = p->BlendB = p->BlendA = 0.f; p->Uncrouch(); p->MinPitch = p->MaxPitch = 0.; // will be filled in by PostBeginPlay()/netcode p->MUSINFOactor = nullptr; p->MUSINFOtics = -1; p->Vel.Zero(); // killough 10/98: initialize bobbing to 0. IFVIRTUALPTRNAME(p->mo, NAME_PlayerPawn, ResetAirSupply) { VMValue params[] = { p->mo, false }; VMCall(func, params, 2, nullptr, 0); } for (int ii = 0; ii < MAXPLAYERS; ++ii) { if (playeringame[ii] && players[ii].camera == oldactor) { players[ii].camera = mobj; } } // [RH] Allow chasecam for demo watching if ((demoplayback || demonew) && chasedemo) p->cheats = CF_CHASECAM; // setup gun psprite if (!(flags & SPF_TEMPPLAYER)) { // This can also start a script so don't do it for the dummy player. P_SetupPsprites (p, !!(flags & SPF_WEAPONFULLYUP)); } if (deathmatch) { // Give all cards in death match mode. IFVIRTUALPTRNAME(p->mo, NAME_PlayerPawn, GiveDeathmatchInventory) { VMValue params[1] = { p->mo }; VMCall(func, params, 1, nullptr, 0); } } else if ((multiplayer || (level.flags2 & LEVEL2_ALLOWRESPAWN) || sv_singleplayerrespawn || !!G_SkillProperty(SKILLP_PlayerRespawn)) && state == PST_REBORN && oldactor != NULL) { // Special inventory handling for respawning in coop IFVM(PlayerPawn, FilterCoopRespawnInventory) { VMValue params[] = { p->mo, oldactor }; VMCall(func, params, 2, nullptr, 0); } } if (oldactor != NULL) { // Remove any inventory left from the old actor. Coop handles // it above, but the other modes don't. oldactor->DestroyAllInventory(); } // [BC] Handle temporary invulnerability when respawned if (state == PST_REBORN || state == PST_ENTER) { IFVIRTUALPTRNAME(p->mo, NAME_PlayerPawn, OnRespawn) { VMValue param = p->mo; VMCall(func, ¶m, 1, nullptr, 0); } } if (StatusBar != NULL && (playernum == consoleplayer || StatusBar->GetPlayer() == playernum)) { StatusBar->AttachToPlayer (p); } if (multiplayer) { P_SpawnTeleportFog(mobj, mobj->Vec3Angle(20., mobj->Angles.Yaw, 0.), false, true); } // "Fix" for one of the starts on exec.wad MAP01: If you start inside the ceiling, // drop down below it, even if that means sinking into the floor. if (mobj->Top() > mobj->ceilingz) { mobj->SetZ(mobj->ceilingz - mobj->Height, false); } // [BC] Do script stuff if (!(flags & SPF_TEMPPLAYER)) { if (state == PST_ENTER || (state == PST_LIVE && !savegamerestore)) { level.Behaviors.StartTypedScripts (SCRIPT_Enter, p->mo, true); } else if (state == PST_REBORN) { assert (oldactor != NULL); // before relocating all pointers to the player all sound targets // pointing to the old actor have to be NULLed. Otherwise all // monsters who last targeted this player will wake up immediately // after the player has respawned. AActor *th; TThinkerIterator it; while ((th = it.Next())) { if (th->LastHeard == oldactor) th->LastHeard = nullptr; } for(auto &sec : level.sectors) { if (sec.SoundTarget == oldactor) sec.SoundTarget = nullptr; } DObject::StaticPointerSubstitution (oldactor, p->mo); E_PlayerRespawned(int(p - players)); level.Behaviors.StartTypedScripts (SCRIPT_Respawn, p->mo, true); } } return mobj; } // // P_SpawnMapThing // The fields of the mapthing should // already be in host byte order. // // [RH] position is used to weed out unwanted start spots AActor *P_SpawnMapThing (FMapThing *mthing, int position) { PClassActor *i; int mask; AActor *mobj; if (mthing->EdNum == 0 || mthing->EdNum == -1) return NULL; // find which type to spawn FDoomEdEntry *mentry = mthing->info; if (mentry == NULL) { // [RH] Don't die if the map tries to spawn an unknown thing Printf("Unknown type %i at (%.1f, %.1f)\n", mthing->EdNum, mthing->pos.X, mthing->pos.Y); mentry = DoomEdMap.CheckKey(0); if (mentry == NULL) // we need a valid entry for the rest of this function so if we can't find a default, let's exit right away. { return NULL; } } if (mentry->Type == NULL && mentry->Special <= 0) { // has been explicitly set to not spawning anything. return NULL; } // copy args to mapthing so that we have them in one place for the rest of this function if (mentry->ArgsDefined > 0) { if (mentry->Type!= NULL) mthing->special = mentry->Special; memcpy(mthing->args, mentry->Args, sizeof(mthing->args[0]) * mentry->ArgsDefined); } int pnum = -1; if (mentry->Type == NULL) { switch (mentry->Special) { case SMT_DeathmatchStart: { // count deathmatch start positions FPlayerStart start(mthing, 0); level.deathmatchstarts.Push(start); return NULL; } case SMT_PolyAnchor: case SMT_PolySpawn: case SMT_PolySpawnCrush: case SMT_PolySpawnHurt: return nullptr; case SMT_Player1Start: case SMT_Player2Start: case SMT_Player3Start: case SMT_Player4Start: case SMT_Player5Start: case SMT_Player6Start: case SMT_Player7Start: case SMT_Player8Start: pnum = mentry->Special - SMT_Player1Start; break; // Sound sequence override will be handled later default: break; } } if (pnum == -1 || (level.flags & LEVEL_FILTERSTARTS)) { // check for appropriate game type if (deathmatch) { mask = MTF_DEATHMATCH; } else if (multiplayer) { mask = MTF_COOPERATIVE; } else { mask = MTF_SINGLE; } if (!(mthing->flags & mask)) { return NULL; } mask = G_SkillProperty(SKILLP_SpawnFilter); if (!(mthing->SkillFilter & mask) && !mentry->NoSkillFlags) { return NULL; } // Check class spawn masks. Now with player classes available // this is enabled for all games. if (!multiplayer) { // Single player int spawnmask = players[consoleplayer].GetSpawnClass(); if (spawnmask != 0 && (mthing->ClassFilter & spawnmask) == 0) { // Not for current class return NULL; } } else if (!deathmatch) { // Cooperative mask = 0; for (int i = 0; i < MAXPLAYERS; i++) { if (playeringame[i]) { int spawnmask = players[i].GetSpawnClass(); if (spawnmask != 0) mask |= spawnmask; else mask = -1; } } if (mask != -1 && (mthing->ClassFilter & mask) == 0) { return NULL; } } } if (pnum != -1) { // [RH] Only spawn spots that match position. if (mthing->args[0] != position) return NULL; // save spots for respawning in network games FPlayerStart start(mthing, pnum+1); level.playerstarts[pnum] = start; if (level.flags2 & LEVEL2_RANDOMPLAYERSTARTS) { // When using random player starts, all starts count level.AllPlayerStarts.Push(start); } else { // When not using random player starts, later single player // starts should override earlier ones, since the earlier // ones are for voodoo dolls and not likely to be ideal for // spawning regular players. unsigned i; for (i = 0; i < level.AllPlayerStarts.Size(); ++i) { if (level.AllPlayerStarts[i].type == pnum+1) { level.AllPlayerStarts[i] = start; break; } } if (i == level.AllPlayerStarts.Size()) { level.AllPlayerStarts.Push(start); } } if (!deathmatch && !(level.flags2 & LEVEL2_RANDOMPLAYERSTARTS)) { return P_SpawnPlayer(&start, pnum, (level.flags2 & LEVEL2_PRERAISEWEAPON) ? SPF_WEAPONFULLYUP : 0); } return NULL; } // [RH] sound sequence overriders if (mentry->Type == NULL && mentry->Special == SMT_SSeqOverride) { int type = mthing->args[0]; if (type == 255) type = -1; if (type > 63) { Printf ("Sound sequence %d out of range\n", type); } else { P_PointInSector (mthing->pos)->seqType = type; } return NULL; } // [RH] If the thing's corresponding sprite has no frames, also map // it to the unknown thing. // Handle decorate replacements explicitly here // to check for missing frames in the replacement object. i = mentry->Type->GetReplacement(); const AActor *defaults = GetDefaultByType (i); if (defaults->SpawnState == NULL || sprites[defaults->SpawnState->sprite].numframes == 0) { // We don't load mods for shareware games so we'll just ignore // missing actors. Heretic needs this since the shareware includes // the retail weapons in Deathmatch. if (gameinfo.flags & GI_SHAREWARE) return NULL; Printf ("%s at (%.1f, %.1f) has no frames\n", i->TypeName.GetChars(), mthing->pos.X, mthing->pos.Y); i = PClass::FindActor("Unknown"); assert(i->IsDescendantOf(RUNTIME_CLASS(AActor))); } const AActor *info = GetDefaultByType (i); // don't spawn keycards and players in deathmatch if (deathmatch && info->flags & MF_NOTDMATCH) return NULL; // [RH] don't spawn extra weapons in coop if so desired if (multiplayer && !deathmatch && (dmflags & DF_NO_COOP_WEAPON_SPAWN)) { if (GetDefaultByType(i)->flags7 & MF7_WEAPONSPAWN) { if ((mthing->flags & (MTF_DEATHMATCH|MTF_SINGLE)) == MTF_DEATHMATCH) return NULL; } } // don't spawn any monsters if -nomonsters if (((level.flags2 & LEVEL2_NOMONSTERS) || (dmflags & DF_NO_MONSTERS)) && info->flags3 & MF3_ISMONSTER ) { return NULL; } auto it = GetDefaultByType(i); IFVIRTUALPTR(it, AActor, ShouldSpawn) { int ret; VMValue param = it; VMReturn rett(&ret); VMCall(func, ¶m, 1, &rett, 1); if (!ret) return nullptr; } // spawn it double sz; if (info->flags & MF_SPAWNCEILING) sz = ONCEILINGZ; else if (info->flags2 & MF2_SPAWNFLOAT) sz = FLOATRANDZ; else sz = ONFLOORZ; mobj = AActor::StaticSpawn (i, DVector3(mthing->pos, sz), NO_REPLACE, true); if (sz == ONFLOORZ) { mobj->AddZ(mthing->pos.Z); if ((mobj->flags2 & MF2_FLOATBOB) && (ib_compatflags & BCOMPATF_FLOATBOB)) { mobj->specialf1 = mthing->pos.Z; } } else if (sz == ONCEILINGZ) mobj->AddZ(-mthing->pos.Z); mobj->SpawnPoint = mthing->pos; mobj->SpawnAngle = mthing->angle; mobj->SpawnFlags = mthing->flags; if (mthing->friendlyseeblocks > 0) mobj->friendlyseeblocks = mthing->friendlyseeblocks; if (mthing->FloatbobPhase >= 0 && mthing->FloatbobPhase < 64) mobj->FloatBobPhase = mthing->FloatbobPhase; if (mthing->Gravity < 0) mobj->Gravity = -mthing->Gravity; else if (mthing->Gravity > 0) mobj->Gravity *= mthing->Gravity; else { mobj->flags |= MF_NOGRAVITY; mobj->Gravity = 0; } // For Hexen floatbob 'compatibility' we do not really want to alter the floorz. if (mobj->specialf1 == 0 || !(mobj->flags2 & MF2_FLOATBOB) || !(ib_compatflags & BCOMPATF_FLOATBOB)) { P_FindFloorCeiling(mobj, FFCF_SAMESECTOR | FFCF_ONLY3DFLOORS | FFCF_3DRESTRICT); } // if the actor got args defined either in DECORATE or MAPINFO we must ignore the map's properties. if (!(mobj->flags2 & MF2_ARGSDEFINED)) { // [RH] Set the thing's special mobj->special = mthing->special; for(int j=0;j<5;j++) mobj->args[j]=mthing->args[j]; } // [RH] Add ThingID to mobj and link it in with the others mobj->tid = mthing->thingid; mobj->AddToHash (); mobj->PrevAngles.Yaw = mobj->Angles.Yaw = (double)mthing->angle; // Check if this actor's mapthing has a conversation defined if (mthing->Conversation > 0) { // Make sure that this does not partially overwrite the default dialogue settings. int root = level.GetConversation(mthing->Conversation); if (root != -1) { mobj->ConversationRoot = root; mobj->Conversation = level.StrifeDialogues[mobj->ConversationRoot]; } } // Set various UDMF options if (mthing->Alpha >= 0) mobj->Alpha = mthing->Alpha; if (mthing->RenderStyle != STYLE_Count) mobj->RenderStyle = (ERenderStyle)mthing->RenderStyle; if (mthing->Scale.X != 0) mobj->Scale.X = mthing->Scale.X * mobj->Scale.X; if (mthing->Scale.Y != 0) mobj->Scale.Y = mthing->Scale.Y * mobj->Scale.Y; if (mthing->pitch) mobj->Angles.Pitch = (double)mthing->pitch; if (mthing->roll) mobj->Angles.Roll = (double)mthing->roll; if (mthing->score) mobj->Score = mthing->score; if (mthing->fillcolor) mobj->fillcolor = (mthing->fillcolor & 0xffffff) | (ColorMatcher.Pick((mthing->fillcolor & 0xff0000) >> 16, (mthing->fillcolor & 0xff00) >> 8, (mthing->fillcolor & 0xff)) << 24); // allow color strings for lights and reshuffle the args for spot lights if (i->IsDescendantOf(NAME_DynamicLight)) { if (mthing->arg0str != NAME_None) { PalEntry color = V_GetColor(nullptr, mthing->arg0str); mobj->args[0] = color.r; mobj->args[1] = color.g; mobj->args[2] = color.b; } else if (mobj->IntVar(NAME_lightflags) & LF_SPOT) { mobj->args[0] = RPART(mthing->args[0]); mobj->args[1] = GPART(mthing->args[0]); mobj->args[2] = BPART(mthing->args[0]); } if (mobj->IntVar(NAME_lightflags) & LF_SPOT) { mobj->AngleVar(NAME_SpotInnerAngle) = double(mthing->args[1]); mobj->AngleVar(NAME_SpotOuterAngle) = double(mthing->args[2]); } } mobj->CallBeginPlay (); if (!(mobj->ObjectFlags & OF_EuthanizeMe)) { mobj->LevelSpawned (); } if (mthing->Health > 0) mobj->health = int(mobj->health * mthing->Health); else mobj->health = -int(mthing->Health); if (mthing->Health == 0) mobj->CallDie(NULL, NULL); else if (mthing->Health != 1) mobj->StartHealth = mobj->health; return mobj; } //=========================================================================== // // SpawnMapThing // //=========================================================================== CVAR(Bool, dumpspawnedthings, false, 0) AActor *SpawnMapThing(int index, FMapThing *mt, int position) { AActor *spawned = P_SpawnMapThing(mt, position); if (dumpspawnedthings) { Printf("%5d: (%5f, %5f, %5f), doomednum = %5d, flags = %04x, type = %s\n", index, mt->pos.X, mt->pos.Y, mt->pos.Z, mt->EdNum, mt->flags, spawned ? spawned->GetClass()->TypeName.GetChars() : "(none)"); } T_AddSpawnedThing(&level, spawned); return spawned; } // // GAME SPAWN FUNCTIONS // // // P_SpawnPuff // AActor *P_SpawnPuff (AActor *source, PClassActor *pufftype, const DVector3 &pos1, DAngle hitdir, DAngle particledir, int updown, int flags, AActor *vict) { AActor *puff; DVector3 pos = pos1; if (pufftype == nullptr) return nullptr; if (!(flags & PF_NORANDOMZ)) pos.Z += pr_spawnpuff.Random2() / 64.; puff = Spawn(pufftype, pos, ALLOW_REPLACE); if (puff == NULL) return NULL; if ((puff->flags4 & MF4_RANDOMIZE) && puff->tics > 0) { puff->tics -= pr_spawnpuff() & 3; if (puff->tics < 1) puff->tics = 1; } //Moved puff creation and target/master/tracer setting to here. if (puff && vict) { if (puff->flags7 & MF7_HITTARGET) puff->target = vict; if (puff->flags7 & MF7_HITMASTER) puff->master = vict; if (puff->flags7 & MF7_HITTRACER) puff->tracer = vict; } // [BB] If the puff came from a player, set the target of the puff to this player. if ( puff && (puff->flags5 & MF5_PUFFGETSOWNER)) puff->target = source; // Angle is the opposite of the hit direction (i.e. the puff faces the source.) puff->Angles.Yaw = hitdir + 180; // If a puff has a crash state and an actor was not hit, // it will enter the crash state. This is used by the StrifeSpark // and BlasterPuff. FState *crashstate; if ((flags & PF_HITSKY) && (crashstate = puff->FindState(NAME_Death, NAME_Sky, true)) != NULL) { puff->SetState (crashstate); } else if (!(flags & PF_HITTHING) && (crashstate = puff->FindState(NAME_Crash)) != NULL) { puff->SetState (crashstate); } else if ((flags & PF_HITTHINGBLEED) && (crashstate = puff->FindState(NAME_Death, NAME_Extreme, true)) != NULL) { puff->SetState (crashstate); } else if ((flags & PF_MELEERANGE) && puff->MeleeState != NULL) { // handle the hard coded state jump of Doom's bullet puff // in a more flexible manner. puff->SetState (puff->MeleeState); } if (!(flags & PF_TEMPORARY)) { if (cl_pufftype && updown != 3 && (puff->flags4 & MF4_ALLOWPARTICLES)) { P_DrawSplash2 (32, pos, particledir, updown, 1); if (cl_pufftype == 1) puff->renderflags |= RF_INVISIBLE; } if ((flags & PF_HITTHING) && puff->SeeSound) { // Hit thing sound S_Sound (puff, CHAN_BODY, puff->SeeSound, 1, ATTN_NORM); } else if (puff->AttackSound) { S_Sound (puff, CHAN_BODY, puff->AttackSound, 1, ATTN_NORM); } } return puff; } DEFINE_ACTION_FUNCTION(AActor, SpawnPuff) { PARAM_SELF_PROLOGUE(AActor); PARAM_CLASS(pufftype, AActor); PARAM_FLOAT(x); PARAM_FLOAT(y); PARAM_FLOAT(z); PARAM_ANGLE(hitdir); PARAM_ANGLE(particledir); PARAM_INT(updown); PARAM_INT(flags); PARAM_OBJECT(victim, AActor); ACTION_RETURN_OBJECT(P_SpawnPuff(self, pufftype, DVector3(x, y, z), hitdir, particledir, updown, flags, victim)); } //--------------------------------------------------------------------------- // // P_SpawnBlood // //--------------------------------------------------------------------------- void P_SpawnBlood (const DVector3 &pos1, DAngle dir, int damage, AActor *originator) { AActor *th; PClassActor *bloodcls = originator->GetBloodType(); DVector3 pos = pos1; pos.Z += pr_spawnblood.Random2() / 64.; int bloodtype = cl_bloodtype; if (bloodcls != NULL && !(GetDefaultByType(bloodcls)->flags4 & MF4_ALLOWPARTICLES)) bloodtype = 0; if (bloodcls != NULL) { th = Spawn(bloodcls, pos, NO_REPLACE); // GetBloodType already performed the replacement th->Vel.Z = 2; th->Angles.Yaw = dir; // [NG] Applying PUFFGETSOWNER to the blood will make it target the owner if (th->flags5 & MF5_PUFFGETSOWNER) th->target = originator; if (gameinfo.gametype & GAME_DoomChex) { th->tics -= pr_spawnblood() & 3; if (th->tics < 1) th->tics = 1; } // colorize the blood if (!(th->flags2 & MF2_DONTTRANSLATE)) { th->Translation = originator->BloodTranslation; } // Moved out of the blood actor so that replacing blood is easier if (gameinfo.gametype & GAME_DoomStrifeChex) { if (gameinfo.gametype == GAME_Strife) { if (damage > 13) { FState *state = th->FindState(NAME_Spray); if (state != NULL) { th->SetState (state); goto statedone; } } else damage += 2; } int advance = 0; if (damage <= 12 && damage >= 9) { advance = 1; } else if (damage < 9) { advance = 2; } PClassActor *cls = th->GetClass(); while (cls != RUNTIME_CLASS(AActor)) { int checked_advance = advance; if (cls->OwnsState(th->SpawnState)) { for (; checked_advance > 0; --checked_advance) { // [RH] Do not set to a state we do not own. if (cls->OwnsState(th->SpawnState + checked_advance)) { th->SetState(th->SpawnState + checked_advance); goto statedone; } } } // We can safely assume the ParentClass is of type PClassActor // since we stop when we see the Actor base class. cls = static_cast(cls->ParentClass); } } statedone: if (!(bloodtype <= 1)) th->renderflags |= RF_INVISIBLE; } if (bloodtype >= 1) P_DrawSplash2 (40, pos, dir, 2, originator->BloodColor); } DEFINE_ACTION_FUNCTION(AActor, SpawnBlood) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(x); PARAM_FLOAT(y); PARAM_FLOAT(z); PARAM_ANGLE(dir); PARAM_INT(damage); P_SpawnBlood(DVector3(x, y, z), dir, damage, self); return 0; } //--------------------------------------------------------------------------- // // PROC P_BloodSplatter // //--------------------------------------------------------------------------- void P_BloodSplatter (const DVector3 &pos, AActor *originator, DAngle hitangle) { PClassActor *bloodcls = originator->GetBloodType(1); int bloodtype = cl_bloodtype; if (bloodcls != NULL && !(GetDefaultByType(bloodcls)->flags4 & MF4_ALLOWPARTICLES)) bloodtype = 0; if (bloodcls != NULL) { AActor *mo; mo = Spawn(bloodcls, pos, NO_REPLACE); // GetBloodType already performed the replacement mo->target = originator; mo->Vel.X = pr_splatter.Random2 () / 64.; mo->Vel.Y = pr_splatter.Random2() / 64.; mo->Vel.Z = 3; // colorize the blood! if (!(mo->flags2 & MF2_DONTTRANSLATE)) { mo->Translation = originator->BloodTranslation; } if (!(bloodtype <= 1)) mo->renderflags |= RF_INVISIBLE; } if (bloodtype >= 1) { P_DrawSplash2 (40, pos, hitangle-180., 2, originator->BloodColor); } } //=========================================================================== // // P_BloodSplatter2 // //=========================================================================== void P_BloodSplatter2 (const DVector3 &pos, AActor *originator, DAngle hitangle) { PClassActor *bloodcls = originator->GetBloodType(2); int bloodtype = cl_bloodtype; if (bloodcls != NULL && !(GetDefaultByType(bloodcls)->flags4 & MF4_ALLOWPARTICLES)) bloodtype = 0; DVector2 add; add.X = (pr_splat() - 128) / 32.; add.Y = (pr_splat() - 128) / 32.; if (bloodcls != NULL) { AActor *mo; mo = Spawn (bloodcls, pos + add, NO_REPLACE); // GetBloodType already performed the replacement mo->target = originator; // colorize the blood! if (!(mo->flags2 & MF2_DONTTRANSLATE)) { mo->Translation = originator->BloodTranslation; } if (!(bloodtype <= 1)) mo->renderflags |= RF_INVISIBLE; } if (bloodtype >= 1) { P_DrawSplash2(40, pos + add, hitangle - 180., 2, originator->BloodColor); } } DEFINE_ACTION_FUNCTION(AActor, BloodSplatter) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(x); PARAM_FLOAT(y); PARAM_FLOAT(z); PARAM_ANGLE(dir); PARAM_BOOL(axe); if (axe) P_BloodSplatter2(DVector3(x, y, z), self, dir); else P_BloodSplatter(DVector3(x, y, z), self, dir); return 0; } //--------------------------------------------------------------------------- // // PROC P_RipperBlood // //--------------------------------------------------------------------------- void P_RipperBlood (AActor *mo, AActor *bleeder) { PClassActor *bloodcls = bleeder->GetBloodType(); double xo = pr_ripperblood.Random2() / 16.; double yo = pr_ripperblood.Random2() / 16.; double zo = pr_ripperblood.Random2() / 16.; DVector3 pos = mo->Vec3Offset(xo, yo, zo); int bloodtype = cl_bloodtype; if (bloodcls != NULL && !(GetDefaultByType(bloodcls)->flags4 & MF4_ALLOWPARTICLES)) bloodtype = 0; if (bloodcls != NULL) { AActor *th; th = Spawn (bloodcls, pos, NO_REPLACE); // GetBloodType already performed the replacement // [NG] Applying PUFFGETSOWNER to the blood will make it target the owner if (th->flags5 & MF5_PUFFGETSOWNER) th->target = bleeder; if (gameinfo.gametype == GAME_Heretic) th->flags |= MF_NOGRAVITY; th->Vel.X = mo->Vel.X / 2; th->Vel.Y = mo->Vel.Y / 2; th->tics += pr_ripperblood () & 3; // colorize the blood! if (!(th->flags2 & MF2_DONTTRANSLATE)) { th->Translation = bleeder->BloodTranslation; } if (!(bloodtype <= 1)) th->renderflags |= RF_INVISIBLE; } if (bloodtype >= 1) { P_DrawSplash2(28, pos, bleeder->AngleTo(mo) + 180., 0, bleeder->BloodColor); } } //--------------------------------------------------------------------------- // // FUNC P_GetThingFloorType // //--------------------------------------------------------------------------- int P_GetThingFloorType (AActor *thing) { if (thing->floorterrain >= 0) { return thing->floorterrain; } else { return thing->Sector->GetTerrain(sector_t::floor); } } //--------------------------------------------------------------------------- // // FUNC P_HitWater // // Returns true if hit liquid and splashed, false if not. //--------------------------------------------------------------------------- bool P_HitWater (AActor * thing, sector_t * sec, const DVector3 &pos, bool checkabove, bool alert, bool force) { if (thing->player && (thing->player->cheats & CF_PREDICTING)) return false; AActor *mo = NULL; FSplashDef *splash; int terrainnum; sector_t *hsec = NULL; // don't splash above the object if (checkabove) { double compare_z = thing->Center(); // Missiles are typically small and fast, so they might // end up submerged by the move that calls P_HitWater. if (thing->flags & MF_MISSILE) compare_z -= thing->Vel.Z; if (pos.Z > compare_z) return false; } #if 0 // needs some rethinking before activation // This avoids spawning splashes on invisible self referencing sectors. // For network consistency do this only in single player though because // it is not guaranteed that all players have GL nodes loaded. if (!multiplayer && thing->subsector->sector != thing->subsector->render_sector) { double zs = thing->subsector->sector->floorplane.ZatPoint(pos); double zr = thing->subsector->render_sector->floorplane.ZatPoint(pos); if (zs > zr && thing->Z() >= zs) return false; } #endif // 'force' means, we want this sector's terrain, no matter what. if (!force) { for (unsigned int i = 0; ie->XFloor.ffloors.Size(); i++) { F3DFloor * rover = sec->e->XFloor.ffloors[i]; if (!(rover->flags & FF_EXISTS)) continue; double planez = rover->top.plane->ZatPoint(pos); if (pos.Z > planez - 0.5 && pos.Z < planez + 0.5) // allow minor imprecisions { if ((rover->flags & (FF_SOLID | FF_SWIMMABLE)) || rover->alpha > 0) { terrainnum = rover->model->GetTerrain(rover->top.isceiling); goto foundone; } } planez = rover->bottom.plane->ZatPoint(pos); if (planez < pos.Z && !(planez < thing->floorz)) return false; } } hsec = sec->GetHeightSec(); if (force || hsec == NULL || !(hsec->MoreFlags & SECMF_CLIPFAKEPLANES)) { terrainnum = sec->GetTerrain(sector_t::floor); } else { terrainnum = hsec->GetTerrain(sector_t::floor); } foundone: int splashnum = Terrains[terrainnum].Splash; bool smallsplash = false; const secplane_t *plane; if (splashnum == -1) return Terrains[terrainnum].IsLiquid; // don't splash when touching an underwater floor if (thing->waterlevel >= 1 && pos.Z <= thing->floorz) return Terrains[terrainnum].IsLiquid; plane = hsec != NULL? &sec->heightsec->floorplane : &sec->floorplane; // Don't splash for living things with small vertical velocities. // There are levels where the constant splashing from the monsters gets extremely annoying if (((thing->flags3&MF3_ISMONSTER || thing->player) && thing->Vel.Z >= -6) && !force) return Terrains[terrainnum].IsLiquid; splash = &Splashes[splashnum]; // Small splash for small masses if (thing->Mass < 10) smallsplash = true; if (!(thing->flags3 & MF3_DONTSPLASH)) { if (smallsplash && splash->SmallSplash) { mo = Spawn(splash->SmallSplash, pos, ALLOW_REPLACE); if (mo) mo->Floorclip += splash->SmallSplashClip; } else { if (splash->SplashChunk) { mo = Spawn(splash->SplashChunk, pos, ALLOW_REPLACE); mo->target = thing; if (splash->ChunkXVelShift != 255) { mo->Vel.X = (pr_chunk.Random2() << splash->ChunkXVelShift) / 65536.; } if (splash->ChunkYVelShift != 255) { mo->Vel.Y = (pr_chunk.Random2() << splash->ChunkYVelShift) / 65536.; } mo->Vel.Z = splash->ChunkBaseZVel + (pr_chunk() << splash->ChunkZVelShift) / 65536.; } if (splash->SplashBase) { mo = Spawn(splash->SplashBase, pos, ALLOW_REPLACE); } if (thing->player && !splash->NoAlert && alert) { P_NoiseAlert(thing, thing, true); } } if (mo) { S_Sound(mo, CHAN_ITEM, smallsplash ? splash->SmallSplashSound : splash->NormalSplashSound, 1, ATTN_IDLE); } else { S_Sound(pos, CHAN_ITEM, smallsplash ? splash->SmallSplashSound : splash->NormalSplashSound, 1, ATTN_IDLE); } } // Don't let deep water eat missiles return plane == &sec->floorplane ? Terrains[terrainnum].IsLiquid : false; } DEFINE_ACTION_FUNCTION(AActor, HitWater) { PARAM_SELF_PROLOGUE(AActor); PARAM_POINTER_NOT_NULL(sec, sector_t); PARAM_FLOAT(x); PARAM_FLOAT(y); PARAM_FLOAT(z); PARAM_BOOL(checkabove); PARAM_BOOL(alert); PARAM_BOOL(force); ACTION_RETURN_BOOL(P_HitWater(self, sec, DVector3(x, y, z), checkabove, alert, force)); } //--------------------------------------------------------------------------- // // FUNC P_HitFloor // // Returns true if hit liquid and splashed, false if not. //--------------------------------------------------------------------------- bool P_HitFloor (AActor *thing) { const msecnode_t *m; // killough 11/98: touchy objects explode on impact // Allow very short drops to be safe, so that a touchy can be summoned without exploding. if (thing->flags6 & MF6_TOUCHY && ((thing->flags6 & MF6_ARMED) || thing->IsSentient()) && thing->Vel.Z < -5) { thing->flags6 &= ~MF6_ARMED; // Disarm P_DamageMobj (thing, NULL, NULL, thing->health, NAME_Crush, DMG_FORCED); // kill object return false; } // don't splash if landing on the edge above water/lava/etc.... DVector3 pos; for (m = thing->touching_sectorlist; m; m = m->m_tnext) { pos = thing->PosRelative(m->m_sector); if (thing->Z() == m->m_sector->floorplane.ZatPoint(pos)) { break; } // Check 3D floors for(unsigned int i=0;im_sector->e->XFloor.ffloors.Size();i++) { F3DFloor * rover = m->m_sector->e->XFloor.ffloors[i]; if (!(rover->flags & FF_EXISTS)) continue; if (rover->flags & (FF_SOLID|FF_SWIMMABLE)) { if (rover->top.plane->ZatPoint(pos) == thing->Z()) { return P_HitWater (thing, m->m_sector, pos); } } } } if (m == NULL || m->m_sector->GetHeightSec() != NULL) { return false; } return P_HitWater (thing, m->m_sector, pos); } DEFINE_ACTION_FUNCTION(AActor, HitFloor) { PARAM_SELF_PROLOGUE(AActor); ACTION_RETURN_BOOL(P_HitFloor(self)); } //--------------------------------------------------------------------------- // // FUNC P_CheckMissileSpawn // // Returns true if the missile is at a valid spawn point, otherwise // explodes it and returns false. // //--------------------------------------------------------------------------- bool P_CheckMissileSpawn (AActor* th, double maxdist) { // [RH] Don't decrement tics if they are already less than 1 if ((th->flags4 & MF4_RANDOMIZE) && th->tics > 0) { th->tics -= pr_checkmissilespawn() & 3; if (th->tics < 1) th->tics = 1; } DVector3 newpos = { 0,0,0 }; if (maxdist > 0) { // move a little forward so an angle can be computed if it immediately explodes DVector3 advance = th->Vel; double maxsquared = maxdist*maxdist; // Keep halving the advance vector until we get something less than maxdist // units away, since we still want to spawn the missile inside the shooter. do { advance *= 0.5f; } while (advance.XY().LengthSquared() >= maxsquared); newpos += advance; } newpos = th->Vec3Offset(newpos); th->SetXYZ(newpos); th->Sector = P_PointInSector(th->Pos()); FCheckPosition tm(!!(th->flags2 & MF2_RIP)); // killough 8/12/98: for non-missile objects (e.g. grenades) // // [GZ] MBF excludes non-missile objects from the P_TryMove test // and subsequent potential P_ExplodeMissile call. That is because // in MBF, a projectile is not an actor with the MF_MISSILE flag // but an actor with either or both the MF_MISSILE and MF_BOUNCES // flags, and a grenade is identified by not having MF_MISSILE. // Killough wanted grenades not to explode directly when spawned, // therefore they can be fired safely even when humping a wall as // they will then just drop on the floor at their shooter's feet. // // However, ZDoom does allow non-missiles to be shot as well, so // Killough's check for non-missiles is inadequate here. So let's // replace it by a check for non-missile and MBF bounce type. // This should allow MBF behavior where relevant without altering // established ZDoom behavior for crazy stuff like a cacodemon cannon. bool MBFGrenade = (!(th->flags & MF_MISSILE) || (th->BounceFlags & BOUNCE_MBF)); // killough 3/15/98: no dropoff (really = don't care for missiles) auto oldf2 = th->flags2; th->flags2 &= ~(MF2_MCROSS|MF2_PCROSS); // The following check is not supposed to activate missile triggers. if (!(P_TryMove (th, newpos, false, NULL, tm, true))) { // [RH] Don't explode ripping missiles that spawn inside something if (th->BlockingMobj == NULL || !(th->flags2 & MF2_RIP) || (th->BlockingMobj->flags5 & MF5_DONTRIP)) { // If this is a monster spawned by A_SpawnProjectile subtract it from the counter. th->ClearCounters(); // [RH] Don't explode missiles that spawn on top of horizon lines if (th->BlockingLine != NULL && th->BlockingLine->special == Line_Horizon) { th->Destroy (); } else if (MBFGrenade && th->BlockingLine != NULL) { P_BounceWall(th); } else { P_ExplodeMissile (th, NULL, th->BlockingMobj); } return false; } } th->flags2 = oldf2; th->ClearInterpolation(); return true; } DEFINE_ACTION_FUNCTION(AActor, CheckMissileSpawn) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(add); ACTION_RETURN_BOOL(P_CheckMissileSpawn(self, add)); } //--------------------------------------------------------------------------- // // FUNC P_PlaySpawnSound // // Plays a missiles spawn sound. Location depends on the // MF_SPAWNSOUNDSOURCE flag. // //--------------------------------------------------------------------------- void P_PlaySpawnSound(AActor *missile, AActor *spawner) { if (missile->SeeSound != 0) { if (!(missile->flags & MF_SPAWNSOUNDSOURCE)) { S_Sound (missile, CHAN_VOICE, missile->SeeSound, 1, ATTN_NORM); } else if (spawner != NULL) { S_Sound (spawner, CHAN_WEAPON, missile->SeeSound, 1, ATTN_NORM); } else { // If there is no spawner use the spawn position. // But not in a silenced sector. if (!(missile->Sector->Flags & SECF_SILENT)) S_Sound (missile->Pos(), CHAN_WEAPON, missile->SeeSound, 1, ATTN_NORM); } } } DEFINE_ACTION_FUNCTION(AActor, PlaySpawnSound) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT_NOT_NULL(missile, AActor); P_PlaySpawnSound(missile, self); return 0; } double GetDefaultSpeed(PClassActor *type) { if (type == NULL) return 0; auto def = GetDefaultByType(type); if (G_SkillProperty(SKILLP_FastMonsters)) { double f = def->FloatVar(NAME_FastSpeed); if (f >= 0) return f; } return def->Speed; } //--------------------------------------------------------------------------- // // FUNC P_SpawnMissile // // Returns NULL if the missile exploded immediately, otherwise returns // a mobj_t pointer to the missile. // //--------------------------------------------------------------------------- AActor *P_SpawnMissileXYZ (DVector3 pos, AActor *source, AActor *dest, PClassActor *type, bool checkspawn, AActor *owner) { if (source == nullptr || type == nullptr) { return nullptr; } if (dest == NULL) { Printf ("P_SpawnMissilyXYZ: Tried to shoot %s from %s with no dest\n", type->TypeName.GetChars(), source->GetClass()->TypeName.GetChars()); return NULL; } if (pos.Z != ONFLOORZ && pos.Z != ONCEILINGZ) { pos.Z -= source->Floorclip; } AActor *th = Spawn (type, pos, ALLOW_REPLACE); P_PlaySpawnSound(th, source); // record missile's originator if (owner == NULL) owner = source; th->target = owner; double speed = th->Speed; // [RH] // Hexen calculates the missile velocity based on the source's location. // Would it be more useful to base it on the actual position of the // missile? // Answer: No, because this way, you can set up sets of parallel missiles. DVector3 velocity = source->Vec3To(dest); // Floor and ceiling huggers should never have a vertical component to their velocity if (th->flags3 & (MF3_FLOORHUGGER|MF3_CEILINGHUGGER)) { velocity.Z = 0; } // [RH] Adjust the trajectory if the missile will go over the target's head. else if (pos.Z - source->Z() >= dest->Height) { velocity.Z += (dest->Height - pos.Z + source->Z()); } th->Vel = velocity.Resized(speed); // invisible target: rotate velocity vector in 2D // [RC] Now monsters can aim at invisible player as if they were fully visible. if (dest->flags & MF_SHADOW && !(source->flags6 & MF6_SEEINVISIBLE)) { DAngle an = pr_spawnmissile.Random2() * (22.5 / 256); double c = an.Cos(); double s = an.Sin(); double newx = th->Vel.X * c - th->Vel.Y * s; double newy = th->Vel.X * s + th->Vel.Y * c; th->Vel.X = newx; th->Vel.Y = newy; } th->AngleFromVel(); if (th->flags4 & MF4_SPECTRAL) { th->SetFriendPlayer(owner->player); } return (!checkspawn || P_CheckMissileSpawn (th, source->radius)) ? th : NULL; } DEFINE_ACTION_FUNCTION(AActor, SpawnMissileXYZ) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(x); PARAM_FLOAT(y); PARAM_FLOAT(z); PARAM_OBJECT_NOT_NULL(dest, AActor); PARAM_CLASS(type, AActor); PARAM_BOOL(check); PARAM_OBJECT(owner, AActor); ACTION_RETURN_OBJECT(P_SpawnMissileXYZ(DVector3(x,y,z), self, dest, type, check, owner)); } AActor *P_SpawnMissile(AActor *source, AActor *dest, PClassActor *type, AActor *owner) { if (source == nullptr) { return nullptr; } return P_SpawnMissileXYZ(source->PosPlusZ(32 + source->GetBobOffset()), source, dest, type, true, owner); } DEFINE_ACTION_FUNCTION(AActor, SpawnMissile) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT_NOT_NULL(dest, AActor); PARAM_CLASS(type, AActor); PARAM_OBJECT(owner, AActor); ACTION_RETURN_OBJECT(P_SpawnMissile(self, dest, type, owner)); } AActor *P_SpawnMissileZ(AActor *source, double z, AActor *dest, PClassActor *type) { if (source == nullptr) { return nullptr; } return P_SpawnMissileXYZ(source->PosAtZ(z), source, dest, type); } DEFINE_ACTION_FUNCTION(AActor, SpawnMissileZ) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(z); PARAM_OBJECT_NOT_NULL(dest, AActor); PARAM_CLASS(type, AActor); ACTION_RETURN_OBJECT(P_SpawnMissileZ(self, z, dest, type)); } AActor *P_OldSpawnMissile(AActor *source, AActor *owner, AActor *dest, PClassActor *type) { if (source == nullptr || type == nullptr) { return nullptr; } AActor *th = Spawn (type, source->PosPlusZ(32.), ALLOW_REPLACE); P_PlaySpawnSound(th, source); th->target = owner; // record missile's originator th->Angles.Yaw = source->AngleTo(dest); th->VelFromAngle(); double dist = source->DistanceBySpeed(dest, MAX(1., th->Speed)); th->Vel.Z = (dest->Z() - source->Z()) / dist; if (th->flags4 & MF4_SPECTRAL) { th->SetFriendPlayer(owner->player); } P_CheckMissileSpawn(th, source->radius); return th; } DEFINE_ACTION_FUNCTION(AActor, OldSpawnMissile) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT_NOT_NULL(dest, AActor); PARAM_CLASS(type, AActor); PARAM_OBJECT(owner, AActor); ACTION_RETURN_OBJECT(P_OldSpawnMissile(self, owner, dest, type)); } //--------------------------------------------------------------------------- // // FUNC P_SpawnMissileAngle // // Returns NULL if the missile exploded immediately, otherwise returns // a mobj_t pointer to the missile. // //--------------------------------------------------------------------------- AActor *P_SpawnMissileAngle (AActor *source, PClassActor *type, DAngle angle, double vz) { if (source == nullptr || type == nullptr) { return NULL; } return P_SpawnMissileAngleZSpeed (source, source->Z() + 32 + source->GetBobOffset(), type, angle, vz, GetDefaultSpeed (type)); } AActor *P_SpawnMissileAngleZ (AActor *source, double z, PClassActor *type, DAngle angle, double vz) { if (type == nullptr) { return nullptr; } return P_SpawnMissileAngleZSpeed (source, z, type, angle, vz, GetDefaultSpeed (type)); } AActor *P_SpawnMissileZAimed (AActor *source, double z, AActor *dest, PClassActor *type) { if (source == nullptr || type == nullptr) { return nullptr; } DAngle an; double dist; double speed; double vz; an = source->Angles.Yaw; if (dest->flags & MF_SHADOW) { an += pr_spawnmissile.Random2() * (16. / 360.); } dist = source->Distance2D (dest); speed = GetDefaultSpeed (type); dist /= speed; vz = dist != 0 ? (dest->Z() - source->Z())/dist : speed; return P_SpawnMissileAngleZSpeed (source, z, type, an, vz, speed); } DEFINE_ACTION_FUNCTION(AActor, SpawnMissileZAimed) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(z); PARAM_OBJECT_NOT_NULL(dest, AActor); PARAM_CLASS(type, AActor); ACTION_RETURN_OBJECT(P_SpawnMissileZAimed(self, z, dest, type)); } //--------------------------------------------------------------------------- // // FUNC P_SpawnMissileAngleZSpeed // // Returns NULL if the missile exploded immediately, otherwise returns // a mobj_t pointer to the missile. // //--------------------------------------------------------------------------- AActor *P_SpawnMissileAngleZSpeed (AActor *source, double z, PClassActor *type, DAngle angle, double vz, double speed, AActor *owner, bool checkspawn) { if (source == nullptr || type == nullptr) { return nullptr; } AActor *mo; if (z != ONFLOORZ && z != ONCEILINGZ) { z -= source->Floorclip; } mo = Spawn (type, source->PosAtZ(z), ALLOW_REPLACE); P_PlaySpawnSound(mo, source); if (owner == NULL) owner = source; mo->target = owner; mo->Angles.Yaw = angle; mo->VelFromAngle(speed); mo->Vel.Z = vz; if (mo->flags4 & MF4_SPECTRAL) { mo->SetFriendPlayer(owner->player); } return (!checkspawn || P_CheckMissileSpawn(mo, source->radius)) ? mo : NULL; } DEFINE_ACTION_FUNCTION(AActor, SpawnMissileAngleZSpeed) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(z); PARAM_CLASS(type, AActor); PARAM_ANGLE(angle); PARAM_FLOAT(vz); PARAM_FLOAT(speed); PARAM_OBJECT(owner, AActor); PARAM_BOOL(checkspawn); ACTION_RETURN_OBJECT(P_SpawnMissileAngleZSpeed(self, z, type, angle, vz, speed, owner, checkspawn)); } AActor *P_SpawnSubMissile(AActor *source, PClassActor *type, AActor *target) { AActor *other = Spawn(type, source->Pos(), ALLOW_REPLACE); if (source == nullptr || type == nullptr) { return nullptr; } other->target = target; other->Angles.Yaw = source->Angles.Yaw; other->VelFromAngle(); if (other->flags4 & MF4_SPECTRAL) { if (source->flags & MF_MISSILE && source->flags4 & MF4_SPECTRAL) { other->FriendPlayer = source->FriendPlayer; } else { other->SetFriendPlayer(target->player); } } if (P_CheckMissileSpawn(other, source->radius)) { DAngle pitch = P_AimLineAttack(source, source->Angles.Yaw, 1024.); other->Vel.Z = -other->Speed * pitch.Sin(); return other; } return NULL; } DEFINE_ACTION_FUNCTION(AActor, SpawnSubMissile) { PARAM_SELF_PROLOGUE(AActor); PARAM_CLASS(cls, AActor); PARAM_OBJECT_NOT_NULL(target, AActor); ACTION_RETURN_OBJECT(P_SpawnSubMissile(self, cls, target)); } /* ================ = = P_SpawnPlayerMissile = = Tries to aim at a nearby monster ================ */ AActor *P_SpawnPlayerMissile (AActor *source, double x, double y, double z, PClassActor *type, DAngle angle, FTranslatedLineTarget *pLineTarget, AActor **pMissileActor, bool nofreeaim, bool noautoaim, int aimflags) { if (source == nullptr || type == nullptr) { return nullptr; } static const double angdiff[3] = { -5.625, 5.625, 0 }; DAngle an = angle; DAngle pitch; FTranslatedLineTarget scratch; AActor *defaultobject = GetDefaultByType(type); DAngle vrange = nofreeaim ? 35. : 0.; if (!pLineTarget) pLineTarget = &scratch; if (!(aimflags & ALF_NOWEAPONCHECK) && source->player && source->player->ReadyWeapon && ((source->player->ReadyWeapon->IntVar(NAME_WeaponFlags) & WIF_NOAUTOAIM) || noautoaim)) { // Keep exactly the same angle and pitch as the player's own aim an = angle; pitch = source->Angles.Pitch; pLineTarget->linetarget = NULL; } else // see which target is to be aimed at { // [XA] If MaxTargetRange is defined in the spawned projectile, use this as the // maximum range for the P_AimLineAttack call later; this allows MaxTargetRange // to function as a "maximum tracer-acquisition range" for seeker missiles. double linetargetrange = defaultobject->maxtargetrange > 0 ? defaultobject->maxtargetrange*64 : 16*64.; int i = 2; do { an = angle + angdiff[i]; pitch = P_AimLineAttack (source, an, linetargetrange, pLineTarget, vrange, aimflags); if (source->player != NULL && !nofreeaim && level.IsFreelookAllowed() && source->player->userinfo.GetAimDist() <= 0.5) { break; } } while (pLineTarget->linetarget == NULL && --i >= 0); if (pLineTarget->linetarget == NULL) { an = angle; if (nofreeaim || !level.IsFreelookAllowed()) { pitch = 0.; } } } if (z != ONFLOORZ && z != ONCEILINGZ) { // Doom spawns missiles 4 units lower than hitscan attacks for players. z += source->Center() - source->Floorclip + source->AttackOffset(-4); // Do not fire beneath the floor. if (z < source->floorz) { z = source->floorz; } } DVector3 pos = source->Vec2OffsetZ(x, y, z); AActor *MissileActor = Spawn (type, pos, ALLOW_REPLACE); if (pMissileActor) *pMissileActor = MissileActor; P_PlaySpawnSound(MissileActor, source); MissileActor->target = source; MissileActor->Angles.Yaw = an; if (MissileActor->flags3 & (MF3_FLOORHUGGER | MF3_CEILINGHUGGER)) { MissileActor->VelFromAngle(); } else { MissileActor->Vel3DFromAngle(pitch, MissileActor->Speed); } if (MissileActor->flags4 & MF4_SPECTRAL) { MissileActor->SetFriendPlayer(source->player); } if (P_CheckMissileSpawn (MissileActor, source->radius)) { return MissileActor; } return NULL; } DEFINE_ACTION_FUNCTION(AActor, SpawnPlayerMissile) { PARAM_SELF_PROLOGUE(AActor); PARAM_CLASS(type, AActor); PARAM_ANGLE(angle); PARAM_FLOAT(x); PARAM_FLOAT(y); PARAM_FLOAT(z); PARAM_OUTPOINTER(lt, FTranslatedLineTarget); PARAM_BOOL(nofreeaim); PARAM_BOOL(noautoaim); PARAM_INT(aimflags); AActor *missileactor; if (angle == 1e37) angle = self->Angles.Yaw; AActor *misl = P_SpawnPlayerMissile(self, x, y, z, type, angle, lt, &missileactor, nofreeaim, noautoaim, aimflags); if (numret > 0) ret[0].SetObject(misl); if (numret > 1) ret[1].SetObject(missileactor), numret = 2; return numret; } int AActor::GetTeam() { if (player) { return player->userinfo.GetTeam(); } int myTeam = DesignatedTeam; // Check for monsters that belong to a player on the team but aren't part of the team themselves. if (myTeam == TEAM_NONE && FriendPlayer != 0) { myTeam = players[FriendPlayer - 1].userinfo.GetTeam(); } return myTeam; } bool AActor::IsTeammate (AActor *other) { if (!other) { return false; } else if (!deathmatch && player && other->player) { return (!((flags ^ other->flags) & MF_FRIENDLY)); } else if (teamplay) { int myTeam = GetTeam(); int otherTeam = other->GetTeam(); return (myTeam != TEAM_NONE && myTeam == otherTeam); } return false; } //========================================================================== // // AActor :: GetSpecies // // Species is defined as the lowest base class that is a monster // with no non-monster class in between. If the actor specifies an explicit // species (i.e. not 'None'), that is used. This is virtualized, so special // monsters can change this behavior if they like. // //========================================================================== FName AActor::GetSpecies() { if (Species != NAME_None) { return Species; } PClassActor *thistype = GetClass(); if (GetDefaultByType(thistype)->flags3 & MF3_ISMONSTER) { while (thistype->ParentClass) { if (GetDefaultByType(thistype->ParentClass)->flags3 & MF3_ISMONSTER) thistype = static_cast(thistype->ParentClass); else break; } } return Species = thistype->TypeName; // [GZ] Speeds up future calls. } //========================================================================== // // AActor :: IsFriend // // Checks if two monsters have to be considered friendly. // //========================================================================== bool AActor::IsFriend (AActor *other) { if (flags & other->flags & MF_FRIENDLY) { if (deathmatch && teamplay) return IsTeammate(other) || (FriendPlayer != 0 && other->FriendPlayer != 0 && players[FriendPlayer-1].mo->IsTeammate(players[other->FriendPlayer-1].mo)); return !deathmatch || FriendPlayer == other->FriendPlayer || FriendPlayer == 0 || other->FriendPlayer == 0 || players[FriendPlayer-1].mo->IsTeammate(players[other->FriendPlayer-1].mo); } // [SP] If friendly flags match, then they are on the same team. /*if (!((flags ^ other->flags) & MF_FRIENDLY)) return true;*/ return false; } //========================================================================== // // AActor :: IsHostile // // Checks if two monsters have to be considered hostile under any circumstances // //========================================================================== bool AActor::IsHostile (AActor *other) { // Both monsters are non-friendlies so hostilities depend on infighting settings if (!((flags | other->flags) & MF_FRIENDLY)) return false; // Both monsters are friendly and belong to the same player if applicable. if (flags & other->flags & MF_FRIENDLY) { if (deathmatch && teamplay) return !IsTeammate(other) && !(FriendPlayer != 0 && other->FriendPlayer != 0 && players[FriendPlayer-1].mo->IsTeammate(players[other->FriendPlayer-1].mo)); return deathmatch && FriendPlayer != other->FriendPlayer && FriendPlayer !=0 && other->FriendPlayer != 0 && !players[FriendPlayer-1].mo->IsTeammate(players[other->FriendPlayer-1].mo); } return true; } //========================================================================== // // AActor :: DoSpecialDamage // // override this for special damage effects. // //========================================================================== int AActor::DoSpecialDamage (AActor *target, int damage, FName damagetype) { if (target->player && target->player->mo == target && damage < 1000 && (target->player->cheats & CF_GODMODE || target->player->cheats & CF_GODMODE2)) { return -1; } else { if (target->player) { // Only do this for old style poison damage. if (PoisonDamage > 0 && PoisonDuration == INT_MIN) { P_PoisonPlayer (target->player, this, this->target, PoisonDamage); damage >>= 1; } } return damage; } } DEFINE_ACTION_FUNCTION(AActor, DoSpecialDamage) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT_NOT_NULL(target, AActor); PARAM_INT(damage); PARAM_NAME(damagetype); ACTION_RETURN_INT(self->DoSpecialDamage(target, damage, damagetype)); } int AActor::CallDoSpecialDamage(AActor *target, int damage, FName damagetype) { IFVIRTUAL(AActor, DoSpecialDamage) { // Without the type cast this picks the 'void *' assignment... VMValue params[4] = { (DObject*)this, (DObject*)target, damage, damagetype.GetIndex() }; VMReturn ret; int retval; ret.IntAt(&retval); VMCall(func, params, 4, &ret, 1); return retval; } else return DoSpecialDamage(target, damage, damagetype); } //========================================================================== // // AActor :: TakeSpecialDamage // //========================================================================== int AActor::TakeSpecialDamage (AActor *inflictor, AActor *source, int damage, FName damagetype) { FState *death; // If the actor does not have a corresponding death state, then it does not take damage. // Note that DeathState matches every kind of damagetype, so an actor has that, it can // be hurt with any type of damage. Exception: Massacre damage always succeeds, because // it needs to work. // Always kill if there is a regular death state or no death states at all. if (FindState (NAME_Death) != NULL || !HasSpecialDeathStates() || damagetype == NAME_Massacre) { return damage; } if (inflictor && inflictor->DeathType != NAME_None) damagetype = inflictor->DeathType; if (damagetype == NAME_Ice) { death = FindState (NAME_Death, NAME_Ice, true); if (death == NULL && !deh.NoAutofreeze && !(flags4 & MF4_NOICEDEATH) && (player || (flags3 & MF3_ISMONSTER))) { death = FindState(NAME_GenericFreezeDeath); } } else { death = FindState (NAME_Death, damagetype); } return (death == NULL) ? -1 : damage; } DEFINE_ACTION_FUNCTION(AActor, TakeSpecialDamage) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT(inflictor, AActor); PARAM_OBJECT(source, AActor); PARAM_INT(damage); PARAM_NAME(damagetype); ACTION_RETURN_INT(self->TakeSpecialDamage(inflictor, source, damage, damagetype)); } int AActor::CallTakeSpecialDamage(AActor *inflictor, AActor *source, int damage, FName damagetype) { IFVIRTUAL(AActor, TakeSpecialDamage) { VMValue params[5] = { (DObject*)this, inflictor, source, damage, damagetype.GetIndex() }; VMReturn ret; int retval; ret.IntAt(&retval); VMCall(func, params, 5, &ret, 1); return retval; } else return TakeSpecialDamage(inflictor, source, damage, damagetype); } void AActor::Crash() { // [RC] Weird that this forces the Crash state regardless of flag. if(!(flags6 & MF6_DONTCORPSE)) { if (((flags & MF_CORPSE) || (flags6 & MF6_KILLED)) && !(flags3 & MF3_CRASHED) && !(flags & MF_ICECORPSE)) { FState *crashstate = NULL; int gibh = GetGibHealth(); if (DamageType != NAME_None) { if (health < gibh) { // Extreme death FName labels[] = { NAME_Crash, NAME_Extreme, DamageType }; crashstate = FindState (3, labels, true); } if (crashstate == NULL) { // Normal death crashstate = FindState(NAME_Crash, DamageType, true); } } if (crashstate == NULL) { if (health < gibh) { // Extreme death crashstate = FindState(NAME_Crash, NAME_Extreme); } else { // Normal death crashstate = FindState(NAME_Crash); } } if (crashstate != NULL) SetState(crashstate); // Set MF3_CRASHED regardless of the presence of a crash state // so this code doesn't have to be executed repeatedly. flags3 |= MF3_CRASHED; } } } void AActor::SetIdle(bool nofunction) { FState *idle = FindState (NAME_Idle); if (idle == NULL) idle = SpawnState; SetState(idle, nofunction); } int AActor::SpawnHealth() const { int defhealth = StartHealth ? StartHealth : GetDefault()->health; if (!(flags3 & MF3_ISMONSTER) || defhealth == 0) { return defhealth; } else if (flags & MF_FRIENDLY) { int adj = int(defhealth * G_SkillProperty(SKILLP_FriendlyHealth)); return (adj <= 0) ? 1 : adj; } else { int adj = int(defhealth * G_SkillProperty(SKILLP_MonsterHealth)); return (adj <= 0) ? 1 : adj; } } int AActor::GetMaxHealth(bool withupgrades) const { int ret = 100; IFVIRTUAL(AActor, GetMaxHealth) { VMValue param[] = { const_cast(this), withupgrades }; VMReturn r(&ret); VMCall(func, param, 2, &r, 1); } return ret; } FState *AActor::GetRaiseState() { if (!(flags & MF_CORPSE)) { return NULL; // not a monster } if (tics != -1 && // not lying still yet !state->GetCanRaise()) // or not ready to be raised yet { return NULL; } if (IsKindOf(NAME_PlayerPawn)) { return NULL; // do not resurrect players } return FindState(NAME_Raise); } void AActor::Revive() { AActor *info = GetDefault(); FLinkContext ctx; bool flagchange = (flags & (MF_NOBLOCKMAP | MF_NOSECTOR)) != (info->flags & (MF_NOBLOCKMAP | MF_NOSECTOR)); if (flagchange) UnlinkFromWorld(&ctx); flags = info->flags; if (flagchange) LinkToWorld(&ctx); flags2 = info->flags2; flags3 = info->flags3; flags4 = info->flags4; flags5 = info->flags5; flags6 = info->flags6; flags7 = info->flags7; if (SpawnFlags & MTF_FRIENDLY) flags |= MF_FRIENDLY; DamageType = info->DamageType; health = SpawnHealth(); target = nullptr; lastenemy = nullptr; // [RH] If it's a monster, it gets to count as another kill if (CountsAsKill()) { level.total_monsters++; } // [ZZ] resurrect hook E_WorldThingRevived(this); } int AActor::GetGibHealth() const { IFVIRTUAL(AActor, GetGibHealth) { VMValue params[] = { (DObject*)this }; int h; VMReturn ret(&h); VMCall(func, params, 1, &ret, 1); return h; } return -SpawnHealth(); } // killough 11/98: // Whether an object is "sentient" or not. Used for environmental influences. // (left precisely the same as MBF even though it doesn't make much sense.) bool AActor::IsSentient() const { return health > 0 && SeeState != NULL; } FSharedStringArena AActor::mStringPropertyData; const char *AActor::GetTag(const char *def) const { if (Tag != NULL) { const char *tag = Tag->GetChars(); if (tag[0] == '$') { return GStrings(tag + 1); } else { return tag; } } else if (def) { return def; } else { return GetClass()->TypeName.GetChars(); } } void AActor::SetTag(const char *def) { if (def == NULL || *def == 0) Tag = nullptr; else Tag = mStringPropertyData.Alloc(def); } void AActor::ClearCounters() { if (CountsAsKill() && health > 0) { level.total_monsters--; flags &= ~MF_COUNTKILL; } // Same, for items if (flags & MF_COUNTITEM) { level.total_items--; flags &= ~MF_COUNTITEM; } // And finally for secrets if (flags5 & MF5_COUNTSECRET) { level.total_secrets--; flags5 &= ~MF5_COUNTSECRET; } } int AActor::GetModifiedDamage(FName damagetype, int damage, bool passive) { auto inv = Inventory; while (inv != nullptr) { IFVIRTUALPTRNAME(inv, NAME_Inventory, ModifyDamage) { VMValue params[5] = { (DObject*)inv, damage, int(damagetype), &damage, passive }; VMCall(func, params, 5, nullptr, 0); } inv = inv->Inventory; } return damage; } int AActor::ApplyDamageFactor(FName damagetype, int damage) const { damage = int(damage * DamageFactor); if (damage > 0) { damage = DamageTypeDefinition::ApplyMobjDamageFactor(damage, damagetype, &GetInfo()->DamageFactors); } return damage; } void AActor::SetTranslation(FName trname) { // There is no constant for the empty name... if (trname.GetChars()[0] == 0) { // an empty string resets to the default Translation = GetDefault()->Translation; return; } int tnum = R_FindCustomTranslation(trname); if (tnum >= 0) { Translation = tnum; } // silently ignore if the name does not exist, this would create some insane message spam otherwise. } //--------------------------------------------------------------------------- // // PROP A_RestoreSpecialPosition // //--------------------------------------------------------------------------- static FRandom pr_restore("RestorePos"); void AActor::RestoreSpecialPosition() { // Move item back to its original location DVector2 sp = SpawnPoint; FLinkContext ctx; UnlinkFromWorld(&ctx); SetXY(sp); LinkToWorld(&ctx, true); SetZ(Sector->floorplane.ZatPoint(sp)); P_FindFloorCeiling(this, FFCF_ONLYSPAWNPOS | FFCF_NOPORTALS); // no portal checks here so that things get spawned in this sector. if (flags & MF_SPAWNCEILING) { SetZ(ceilingz - Height - SpawnPoint.Z); } else if (flags2 & MF2_SPAWNFLOAT) { double space = ceilingz - Height - floorz; if (space > 48) { space -= 40; SetZ((space * pr_restore()) / 256. + floorz + 40); } else { SetZ(floorz); } } else { SetZ(SpawnPoint.Z + floorz); } // Redo floor/ceiling check, in case of 3D floors and portals P_FindFloorCeiling(this, FFCF_SAMESECTOR | FFCF_ONLY3DFLOORS | FFCF_3DRESTRICT); if (Z() < floorz) { // Do not reappear under the floor, even if that's where we were for the // initial spawn. SetZ(floorz); } if ((flags & MF_SOLID) && (Top() > ceilingz)) { // Do the same for the ceiling. SetZ(ceilingz - Height); } // Do not interpolate from the position the actor was at when it was // picked up, in case that is different from where it is now. ClearInterpolation(); } //---------------------------------------------------------------------------- // // DropItem handling // //---------------------------------------------------------------------------- DEFINE_FIELD(FDropItem, Next) DEFINE_FIELD(FDropItem, Name) DEFINE_FIELD(FDropItem, Probability) DEFINE_FIELD(FDropItem, Amount) void PrintMiscActorInfo(AActor *query) { if (query) { int flagi; int querystyle = STYLE_Count; for (int style = STYLE_None; style < STYLE_Count; ++style) { // Check for a legacy render style that matches. if (LegacyRenderStyles[style] == query->RenderStyle) { querystyle = style; break; } } static const char * renderstyles[]= {"None", "Normal", "Fuzzy", "SoulTrans", "OptFuzzy", "Stencil", "Translucent", "Add", "Shaded", "TranslucentStencil", "Shadow", "Subtract", "AddStencil", "AddShaded"}; FLineSpecial *spec = P_GetLineSpecialInfo(query->special); Printf("%s @ %p has the following flags:\n flags: %x", query->GetTag(), query, query->flags.GetValue()); for (flagi = 0; flagi <= 31; flagi++) if (query->flags & ActorFlags::FromInt(1<flags2.GetValue()); for (flagi = 0; flagi <= 31; flagi++) if (query->flags2 & ActorFlags2::FromInt(1<flags3.GetValue()); for (flagi = 0; flagi <= 31; flagi++) if (query->flags3 & ActorFlags3::FromInt(1<flags4.GetValue()); for (flagi = 0; flagi <= 31; flagi++) if (query->flags4 & ActorFlags4::FromInt(1<flags5.GetValue()); for (flagi = 0; flagi <= 31; flagi++) if (query->flags5 & ActorFlags5::FromInt(1<flags6.GetValue()); for (flagi = 0; flagi <= 31; flagi++) if (query->flags6 & ActorFlags6::FromInt(1<flags7.GetValue()); for (flagi = 0; flagi <= 31; flagi++) if (query->flags7 & ActorFlags7::FromInt(1<flags8.GetValue()); for (flagi = 0; flagi <= 31; flagi++) if (query->flags8 & ActorFlags8::FromInt(1<BounceFlags.GetValue(), query->bouncefactor, query->wallbouncefactor); /*for (flagi = 0; flagi < 31; flagi++) if (query->BounceFlags & 1<Alpha, query->renderflags.GetValue()); /*for (flagi = 0; flagi < 31; flagi++) if (query->renderflags & 1<name : "None"), query->args[0], query->args[1], query->args[2], query->args[3], query->args[4], query->special1, query->special2); Printf("\nTID: %d", query->tid); Printf("\nCoord= x: %f, y: %f, z:%f, floor:%f, ceiling:%f, height= %f", query->X(), query->Y(), query->Z(), query->floorz, query->ceilingz, query->Height); Printf("\nSpeed= %f, velocity= x:%f, y:%f, z:%f, combined:%f.\n", query->Speed, query->Vel.X, query->Vel.Y, query->Vel.Z, query->Vel.Length()); Printf("Scale: x:%f, y:%f\n", query->Scale.X, query->Scale.Y); Printf("FriendlySeeBlocks: %d\n", query->friendlyseeblocks); Printf("Target: %s\n", query->target ? query->target->GetClass()->TypeName.GetChars() : "-"); Printf("Last enemy: %s\n", query->lastenemy ? query->lastenemy->GetClass()->TypeName.GetChars() : "-"); } }