//----------------------------------------------------------------------------- // // 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: // Movement, collision handling. // Shooting and aiming. // //----------------------------------------------------------------------------- /* 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. **--------------------------------------------------------------------------- ** */ #include #include #include #include "templates.h" #include "m_bbox.h" #include "m_random.h" #include "i_system.h" #include "c_dispatch.h" #include "math/cmath.h" #include "doomdef.h" #include "p_local.h" #include "p_spec.h" #include "d_player.h" #include "p_maputl.h" #include "p_lnspec.h" #include "p_effect.h" #include "p_terrain.h" #include "p_trace.h" #include "p_checkposition.h" #include "p_linetracedata.h" #include "r_utility.h" #include "p_blockmap.h" #include "p_3dmidtex.h" #include "vm.h" #include "s_sound.h" #include "decallib.h" // State. #include "doomstat.h" #include "r_state.h" #include "gi.h" #include "a_sharedglobal.h" #include "p_conversation.h" #include "r_data/r_translate.h" #include "g_level.h" #include "r_sky.h" #include "g_levellocals.h" #include "actorinlines.h" CVAR(Bool, cl_bloodsplats, true, CVAR_ARCHIVE) CVAR(Int, sv_smartaim, 0, CVAR_ARCHIVE | CVAR_SERVERINFO) CVAR(Bool, cl_doautoaim, false, CVAR_ARCHIVE) static void CheckForPushSpecial(line_t *line, int side, AActor *mobj, DVector2 * posforwindowcheck = NULL); static void SpawnShootDecal(AActor *t1, const FTraceResults &trace); static void SpawnDeepSplash(AActor *t1, const FTraceResults &trace, AActor *puff); static FRandom pr_tracebleed("TraceBleed"); static FRandom pr_checkthing("CheckThing"); static FRandom pr_lineattack("LineAttack"); static FRandom pr_crunch("DoCrunch"); // keep track of special lines as they are hit, // but don't process them until the move is proven valid TArray spechit; TArray portalhit; // FCheckPosition requires explicit contstruction and destruction when used in the VM DEFINE_ACTION_FUNCTION(_FCheckPosition, _Constructor) { PARAM_SELF_STRUCT_PROLOGUE(FCheckPosition); new(self) FCheckPosition; return 0; } DEFINE_ACTION_FUNCTION(_FCheckPosition, _Destructor) { PARAM_SELF_STRUCT_PROLOGUE(FCheckPosition); self->~FCheckPosition(); return 0; } DEFINE_ACTION_FUNCTION(_FCheckPosition, ClearLastRipped) { PARAM_SELF_STRUCT_PROLOGUE(FCheckPosition); self->LastRipped.Clear(); return 0; } DEFINE_FIELD_X(FCheckPosition, FCheckPosition, thing); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, pos); DEFINE_FIELD_NAMED_X(FCheckPosition, FCheckPosition, sector, cursector); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, floorz); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, ceilingz); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, dropoffz); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, floorpic); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, floorterrain); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, floorsector); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, ceilingpic); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, ceilingsector); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, touchmidtex); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, abovemidtex); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, floatok); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, FromPMove); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, ceilingline); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, stepthing); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, DoRipping); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, portalstep); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, portalgroup); DEFINE_FIELD_X(FCheckPosition, FCheckPosition, PushTime); //========================================================================== // // CanCollideWith // // Checks if an actor can collide with another one, calling virtual script functions to check. // //========================================================================== bool P_CanCollideWith(AActor *tmthing, AActor *thing) { static unsigned VIndex = ~0u; if (VIndex == ~0u) { VIndex = GetVirtualIndex(RUNTIME_CLASS(AActor), "CanCollideWith"); assert(VIndex != ~0u); } VMValue params[3] = { tmthing, thing, false }; VMReturn ret; int retval; ret.IntAt(&retval); auto clss = tmthing->GetClass(); VMFunction *func = clss->Virtuals.Size() > VIndex ? clss->Virtuals[VIndex] : nullptr; if (func != nullptr) { VMCall(func, params, 3, &ret, 1); if (!retval) return false; } std::swap(params[0].a, params[1].a); params[2].i = true; // re-get for the other actor. clss = thing->GetClass(); func = clss->Virtuals.Size() > VIndex ? clss->Virtuals[VIndex] : nullptr; if (func != nullptr) { VMCall(func, params, 3, &ret, 1); if (!retval) return false; } return true; } //========================================================================== // // FindRefPoint // // Finds the point on the line closest to the given coordinate // //========================================================================== static DVector2 FindRefPoint(line_t *ld, const DVector2 &pos) { // If there's any chance of slopes getting in the way we need to get a proper refpoint, otherwise we can save the work. // Slopes can get in here when: // - the actual sector planes are sloped // - there's 3D floors in this sector // - there's a crossable floor portal (for which the dropoff needs to be calculated within P_LineOpening, and the lower sector can easily have slopes) // // Todo: check if this bootload of checks even helps or if it adds more than it saves // if (ld->frontsector->floorplane.isSlope() || ld->backsector->floorplane.isSlope() || ld->frontsector->ceilingplane.isSlope() || ld->backsector->ceilingplane. isSlope() || ld->backsector->e->XFloor.ffloors.Size() != 0 || ld->frontsector->e->XFloor.ffloors.Size() != 0 || !ld->frontsector->PortalBlocksMovement(sector_t::ceiling) || !ld->frontsector->PortalBlocksMovement(sector_t::floor)) { DVector2 v1 = ld->v1->fPos(); DVector2 d = ld->Delta(); double r = clamp(((pos.X - v1.X) * d.X + (pos.Y - v1.Y) * d.Y) / (d.X*d.X + d.Y*d.Y), 0., 1.); return v1 + d*r; } return pos; } //========================================================================== // // PIT_FindFloorCeiling // // only3d set means to only check against 3D floors and midtexes. // //========================================================================== bool ffcf_verbose; static bool PIT_FindFloorCeiling(FMultiBlockLinesIterator &mit, FMultiBlockLinesIterator::CheckResult &cres, const FBoundingBox &box, FCheckPosition &tmf, int flags) { line_t *ld = cres.line; if (!box.inRange(ld) || box.BoxOnLineSide(ld) != -1) return true; // A line has been hit if (ffcf_verbose) { Printf("Hit line %d at position %f,%f, group %d\n", ld->Index(), cres.Position.X, cres.Position.Y, ld->frontsector->PortalGroup); } if (!ld->backsector) { // One sided line return true; } DVector2 refpoint = FindRefPoint(ld, cres.Position); FLineOpening open; P_LineOpening(open, tmf.thing, ld, refpoint, &cres.Position, flags); // adjust floor / ceiling heights if (!(flags & FFCF_NOCEILING)) { if (open.top < tmf.ceilingz) { tmf.ceilingz = open.top; if (open.topsec != NULL) tmf.ceilingsector = open.topsec; if (ffcf_verbose) Printf(" Adjust ceilingz to %f\n", open.top); mit.StopUp(); } } // If we are stepping through a portal the line's opening must be checked, regardless of the NOFLOOR flag if (!(flags & FFCF_NOFLOOR)) { if (open.bottom > tmf.floorz) { tmf.floorz = open.bottom; if (open.bottomsec != NULL) tmf.floorsector = open.bottomsec; tmf.touchmidtex = open.touchmidtex; tmf.abovemidtex = open.abovemidtex; if (ffcf_verbose) Printf(" Adjust floorz to %f\n", open.bottom); if (tmf.floorz > tmf.dropoffz + tmf.thing->MaxDropOffHeight) mit.StopDown(); } else if (open.bottom == tmf.floorz) { tmf.touchmidtex |= open.touchmidtex; tmf.abovemidtex |= open.abovemidtex; } if (open.lowfloor < tmf.dropoffz && open.lowfloor > LINEOPEN_MIN) { tmf.dropoffz = open.lowfloor; if (ffcf_verbose) Printf(" Adjust dropoffz to %f\n", open.bottom); if (tmf.floorz > tmf.dropoffz + tmf.thing->MaxDropOffHeight) mit.StopDown(); } } return true; } //========================================================================== // // calculates the actual floor and ceiling position at a given // coordinate. Traverses through portals unless being told not to. // //========================================================================== void P_GetFloorCeilingZ(FCheckPosition &tmf, int flags) { sector_t *sec = (!(flags & FFCF_SAMESECTOR) || tmf.thing->Sector == NULL)? P_PointInSector(tmf.pos) : tmf.sector; F3DFloor *ffc, *fff; tmf.ceilingz = sec->NextHighestCeilingAt(tmf.pos.X, tmf.pos.Y, tmf.pos.Z, tmf.pos.Z + tmf.thing->Height, flags, &tmf.ceilingsector, &ffc); tmf.floorz = tmf.dropoffz = sec->NextLowestFloorAt(tmf.pos.X, tmf.pos.Y, tmf.pos.Z, flags, tmf.thing->MaxStepHeight, &tmf.floorsector, &fff); if (fff) { tmf.floorpic = *fff->top.texture; tmf.floorterrain = fff->model->GetTerrain(fff->top.isceiling); } else { tmf.floorpic = tmf.floorsector->GetTexture(sector_t::floor); tmf.floorterrain = tmf.floorsector->GetTerrain(sector_t::floor); } tmf.ceilingpic = ffc ? *ffc->bottom.texture : tmf.ceilingsector->GetTexture(sector_t::ceiling); tmf.sector = sec; } //========================================================================== // // P_FindFloorCeiling // //========================================================================== void P_FindFloorCeiling(AActor *actor, int flags) { FCheckPosition tmf; tmf.thing = actor; tmf.pos = actor->Pos(); if (flags & FFCF_ONLYSPAWNPOS) { flags |= FFCF_3DRESTRICT; } if (flags & FFCF_SAMESECTOR) { tmf.sector = actor->Sector; } P_GetFloorCeilingZ(tmf, flags); assert(tmf.thing->Sector != NULL); actor->floorz = tmf.floorz; actor->dropoffz = tmf.dropoffz; actor->ceilingz = tmf.ceilingz; actor->floorpic = tmf.floorpic; actor->floorterrain = tmf.floorterrain; actor->floorsector = tmf.floorsector; actor->ceilingpic = tmf.ceilingpic; actor->ceilingsector = tmf.ceilingsector; if (ffcf_verbose) Printf("Starting with ceilingz = %f, floorz = %f\n", tmf.ceilingz, tmf.floorz); tmf.touchmidtex = false; tmf.abovemidtex = false; validcount++; FPortalGroupArray grouplist; FMultiBlockLinesIterator mit(grouplist, actor); FMultiBlockLinesIterator::CheckResult cres; // if we already have a valid floor/ceiling sector within the current sector, // we do not need to iterate through plane portals to find a floor or ceiling. if (actor->floorsector == actor->Sector) mit.StopDown(); if (actor->ceilingsector == actor->Sector) mit.StopUp(); while ((mit.Next(&cres))) { PIT_FindFloorCeiling(mit, cres, mit.Box(), tmf, flags|cres.portalflags); } if (tmf.touchmidtex) tmf.dropoffz = tmf.floorz; bool usetmf = !(flags & FFCF_ONLYSPAWNPOS) || (tmf.abovemidtex && (tmf.floorz <= actor->Z())); // when actual floor or ceiling are beyond a portal plane we also need to use the result of the blockmap iterator, regardless of the flags being specified. if (usetmf || tmf.floorsector->PortalGroup != actor->Sector->PortalGroup) { actor->floorz = tmf.floorz; actor->dropoffz = tmf.dropoffz; actor->floorpic = tmf.floorpic; actor->floorterrain = tmf.floorterrain; actor->floorsector = tmf.floorsector; } if (usetmf || tmf.ceilingsector->PortalGroup != actor->Sector->PortalGroup) { actor->ceilingz = tmf.ceilingz; actor->ceilingpic = tmf.ceilingpic; actor->ceilingsector = tmf.ceilingsector; } } DEFINE_ACTION_FUNCTION(AActor, FindFloorCeiling) { PARAM_SELF_PROLOGUE(AActor); PARAM_INT_DEF(flags); P_FindFloorCeiling(self, flags); return 0; } // Debug CCMD for checking errors in the MultiBlockLinesIterator (needs to be removed when this code is complete) CCMD(ffcf) { ffcf_verbose = true; P_FindFloorCeiling(players[0].mo, 0); ffcf_verbose = false; } //========================================================================== // // TELEPORT MOVE // // // P_TeleportMove // // [RH] Added telefrag parameter: When true, anything in the spawn spot // will always be telefragged, and the move will be successful. // Added z parameter. Originally, the thing's z was set *after* the // move was made, so the height checking I added for 1.13 could // potentially erroneously indicate the move was okay if the thing // was being teleported between two non-overlapping height ranges. // //========================================================================== bool P_TeleportMove(AActor* thing, const DVector3 &pos, bool telefrag, bool modifyactor) { FCheckPosition tmf; sector_t *oldsec = thing->Sector; // kill anything occupying the position // The base floor/ceiling is from the subsector that contains the point. // Any contacted lines the step closer together will adjust them. tmf.thing = thing; tmf.pos = pos; tmf.touchmidtex = false; tmf.abovemidtex = false; P_GetFloorCeilingZ(tmf, 0); spechit.Clear(); // this is needed so that no more specials get activated after crossing a teleporter. bool StompAlwaysFrags = ((thing->flags2 & MF2_TELESTOMP) || (level.flags & LEVEL_MONSTERSTELEFRAG) || telefrag) && !(thing->flags7 & MF7_NOTELESTOMP); // P_LineOpening requires the thing's z to be the destination z in order to work. double savedz = thing->Z(); thing->SetZ(pos.Z); sector_t *sector = P_PointInSector(pos); FPortalGroupArray grouplist; FMultiBlockLinesIterator mit(grouplist, pos.X, pos.Y, pos.Z, thing->Height, thing->radius, sector); FMultiBlockLinesIterator::CheckResult cres; while (mit.Next(&cres)) { PIT_FindFloorCeiling(mit, cres, mit.Box(), tmf, 0); } thing->SetZ(savedz); if (tmf.touchmidtex) tmf.dropoffz = tmf.floorz; FMultiBlockThingsIterator mit2(grouplist, pos.X, pos.Y, pos.Z, thing->Height, thing->radius, false, sector); FMultiBlockThingsIterator::CheckResult cres2; while (mit2.Next(&cres2)) { AActor *th = cres2.thing; if (!(th->flags & MF_SHOOTABLE)) continue; // don't clip against self if (th == thing) continue; double blockdist = th->radius + tmf.thing->radius; if (fabs(th->X() - cres2.Position.X) >= blockdist || fabs(th->Y() - cres2.Position.Y) >= blockdist) continue; if ((th->flags2 | tmf.thing->flags2) & MF2_THRUACTORS) continue; if (tmf.thing->flags6 & MF6_THRUSPECIES && tmf.thing->GetSpecies() == th->GetSpecies()) continue; // [RH] Z-Check // But not if not MF2_PASSMOBJ or MF3_DONTOVERLAP are set! // Otherwise those things would get stuck inside each other. if ((thing->flags2 & MF2_PASSMOBJ || th->flags4 & MF4_ACTLIKEBRIDGE) && !(i_compatflags & COMPATF_NO_PASSMOBJ)) { if (!(th->flags3 & thing->flags3 & MF3_DONTOVERLAP)) { if (pos.Z > th->Top() || // overhead pos.Z + thing->Height < th->Z()) // underneath continue; } } if (!P_CanCollideWith(tmf.thing, th)) continue; // Don't let players and monsters block item teleports (all other actor types will still block.) if (thing->IsKindOf(RUNTIME_CLASS(AInventory)) && !(thing->flags & MF_SOLID) && ((th->flags3 & MF3_ISMONSTER) || th->player != nullptr)) continue; // monsters don't stomp things except on boss level // [RH] Some Heretic/Hexen monsters can telestomp // ... and some items can never be telefragged while others will be telefragged by everything that teleports upon them. if ((StompAlwaysFrags && !(th->flags6 & MF6_NOTELEFRAG)) || (th->flags7 & MF7_ALWAYSTELEFRAG)) { // Don't actually damage if predicting a teleport if (thing->player == NULL || !(thing->player->cheats & CF_PREDICTING)) P_DamageMobj(th, thing, thing, TELEFRAG_DAMAGE, NAME_Telefrag, DMG_THRUSTLESS); continue; } return false; } if (modifyactor) { // the move is ok, so link the thing into its new position thing->SetOrigin(pos, false); thing->floorz = tmf.floorz; thing->ceilingz = tmf.ceilingz; thing->floorsector = tmf.floorsector; thing->floorpic = tmf.floorpic; thing->floorterrain = tmf.floorterrain; thing->ceilingsector = tmf.ceilingsector; thing->ceilingpic = tmf.ceilingpic; thing->dropoffz = tmf.dropoffz; // killough 11/98 thing->BlockingLine = NULL; if (thing->flags2 & MF2_FLOORCLIP) { thing->AdjustFloorClip(); } if (thing == players[consoleplayer].camera) { R_ResetViewInterpolation(); } // If this teleport was caused by a move, P_TryMove() will handle the // sector transition messages better than we can here. // This needs to be compatibility optioned because some older maps exploited this missing feature. if (!(thing->flags6 & MF6_INTRYMOVE) && !(i_compatflags2 & COMPATF2_TELEPORT)) { thing->CheckSectorTransition(oldsec); } } return true; } DEFINE_ACTION_FUNCTION(AActor, TeleportMove) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(x); PARAM_FLOAT(y); PARAM_FLOAT(z); PARAM_BOOL(telefrag); PARAM_BOOL_DEF(modify); ACTION_RETURN_BOOL(P_TeleportMove(self, DVector3(x, y, z), telefrag, modify)); } //========================================================================== // // [RH] P_PlayerStartStomp // // Like P_TeleportMove, but it doesn't move anything, and only monsters and other // players get telefragged. // //========================================================================== void P_PlayerStartStomp(AActor *actor, bool mononly) { FPortalGroupArray grouplist; FMultiBlockThingsIterator mit(grouplist, actor); FMultiBlockThingsIterator::CheckResult cres; while ((mit.Next(&cres))) { AActor *th = cres.thing; if (!(th->flags & MF_SHOOTABLE)) continue; // don't clip against self, and don't kill your own voodoo dolls if (th == actor || (th->player == actor->player && th->player != NULL)) continue; double blockdist = th->radius + actor->radius; if (fabs(th->X() - cres.Position.X) >= blockdist || fabs(th->Y() - cres.Position.Y) >= blockdist) continue; // only kill monsters and other players if (th->player == NULL && !(th->flags3 & MF3_ISMONSTER)) continue; if (th->player != NULL && mononly) continue; if (actor->Z() > th->Top()) continue; // overhead if (actor->Top() < th->Z()) continue; // underneath P_DamageMobj(th, actor, actor, TELEFRAG_DAMAGE, NAME_Telefrag); } } //========================================================================== // // killough 8/28/98: // // P_GetFriction() // // Returns the friction associated with a particular mobj. // //========================================================================== double P_GetFriction(const AActor *mo, double *frictionfactor) { double friction = ORIG_FRICTION; double movefactor = ORIG_FRICTION_FACTOR; double newfriction; double newmf; const msecnode_t *m; sector_t *sec; if (mo->IsNoClip2()) { // The default values are fine for noclip2 mode } else if (mo->flags2 & MF2_FLY && mo->flags & MF_NOGRAVITY) { friction = FRICTION_FLY; } else if ((!(mo->flags & MF_NOGRAVITY) && mo->waterlevel > 1) || (mo->waterlevel == 1 && mo->Z() > mo->floorz+ 6)) { friction = mo->Sector->GetFriction(sector_t::floor, &movefactor); movefactor *= 0.5; // Check 3D floors -- might be the source of the waterlevel for (unsigned i = 0; i < mo->Sector->e->XFloor.ffloors.Size(); i++) { F3DFloor *rover = mo->Sector->e->XFloor.ffloors[i]; if (!(rover->flags & FF_EXISTS)) continue; if (!(rover->flags & FF_SWIMMABLE)) continue; if (mo->Z() > rover->top.plane->ZatPoint(mo) || mo->Z() < rover->bottom.plane->ZatPoint(mo)) continue; newfriction = rover->model->GetFriction(rover->top.isceiling, &newmf); if (newfriction < friction || friction == ORIG_FRICTION) { friction = newfriction; movefactor = newmf * 0.5; } } } else if (var_friction && !(mo->flags & (MF_NOCLIP | MF_NOGRAVITY))) { // When the object is straddling sectors with the same // floor height that have different frictions, use the lowest // friction value (muddy has precedence over icy). for (m = mo->touching_sectorlist; m; m = m->m_tnext) { sec = m->m_sector; DVector3 pos = mo->PosRelative(sec); // 3D floors must be checked, too for (unsigned i = 0; i < sec->e->XFloor.ffloors.Size(); i++) { F3DFloor *rover = sec->e->XFloor.ffloors[i]; if (!(rover->flags & FF_EXISTS)) continue; if (rover->flags & FF_SOLID) { // Must be standing on a solid floor if (!mo->isAtZ(rover->top.plane->ZatPoint(pos))) continue; } else if (rover->flags & FF_SWIMMABLE) { // Or on or inside a swimmable floor (e.g. in shallow water) if (mo->Z() > rover->top.plane->ZatPoint(pos) || (mo->Top()) < rover->bottom.plane->ZatPoint(pos)) continue; } else continue; newfriction = rover->model->GetFriction(rover->top.isceiling, &newmf); if (newfriction < friction || friction == ORIG_FRICTION) { friction = newfriction; movefactor = newmf; } } if (!(sec->Flags & SECF_FRICTION) && Terrains[sec->GetTerrain(sector_t::floor)].Friction == 0) { continue; } newfriction = sec->GetFriction(sector_t::floor, &newmf); if ((newfriction < friction || friction == ORIG_FRICTION) && (mo->Z() <= sec->floorplane.ZatPoint(pos) || (sec->GetHeightSec() != NULL && mo->Z() <= sec->heightsec->floorplane.ZatPoint(pos)))) { friction = newfriction; movefactor = newmf; } } } if (mo->Friction != 1) { friction = clamp((friction * mo->Friction), 0., 1.); movefactor = FrictionToMoveFactor(friction); } if (frictionfactor) *frictionfactor = movefactor; return friction; } //========================================================================== // // phares 3/19/98 // P_GetMoveFactor() returns the value by which the x,y // movements are multiplied to add to player movement. // // killough 8/28/98: rewritten // //========================================================================== double P_GetMoveFactor(const AActor *mo, double *frictionp) { double movefactor, friction; // If the floor is icy or muddy, it's harder to get moving. This is where // the different friction factors are applied to 'trying to move'. In // p_mobj.c, the friction factors are applied as you coast and slow down. if ((friction = P_GetFriction(mo, &movefactor)) < ORIG_FRICTION) { // phares 3/11/98: you start off slowly, then increase as // you get better footing double velocity = mo->VelXYToSpeed(); if (velocity > MORE_FRICTION_VELOCITY * 4) movefactor *= 8; else if (velocity > MORE_FRICTION_VELOCITY * 2) movefactor *= 4; else if (velocity > MORE_FRICTION_VELOCITY) movefactor *= 2; } if (frictionp) *frictionp = friction; return movefactor; } DEFINE_ACTION_FUNCTION(AActor, GetFriction) { PARAM_SELF_PROLOGUE(AActor); double friction, movefactor = P_GetMoveFactor(self, &friction); if (numret > 1) { numret = 2; ret[1].SetFloat(movefactor); } if (numret > 0) { ret[0].SetFloat(friction); } return numret; } //========================================================================== // // Checks if the line intersects with the actor // returns // - 1 when above/below // - 0 when intersecting // - -1 when outside the portal // // Note that this check is done from the 'other' side of the portal // so plane names seem to be inverted. // //========================================================================== static int LineIsAbove(line_t *line, AActor *actor) { if (line->frontsector->PortalBlocksMovement(sector_t::floor)) return -1; return line->frontsector->GetPortalPlaneZ(sector_t::floor) >= actor->Top(); } static int LineIsBelow(line_t *line, AActor *actor) { if (line->frontsector->PortalBlocksMovement(sector_t::ceiling)) return -1; return line->frontsector->GetPortalPlaneZ(sector_t::ceiling) <= actor->Z(); } // // MOVEMENT ITERATOR FUNCTIONS // //========================================================================== // // // PIT_CheckLine // Adjusts tmfloorz and tmceilingz as lines are contacted // // //========================================================================== static // killough 3/26/98: make static bool PIT_CheckLine(FMultiBlockLinesIterator &mit, FMultiBlockLinesIterator::CheckResult &cres, const FBoundingBox &box, FCheckPosition &tm, const bool wasfit) { line_t *ld = cres.line; bool rail = false; if (!box.inRange(ld) || box.BoxOnLineSide(ld) != -1) return true; // A line has been hit /* = = The moving thing's destination position will cross the given line. = If this should not be allowed, return false. = If the line is special, keep track of it to process later if the move = is proven ok. NOTE: specials are NOT sorted by order, so two special lines = that are only 8 pixels apart could be crossed in either order. */ if (!ld->backsector) { // One sided line // Needed for polyobject portals. if (cres.line->isLinePortal() && (cres.line->sidedef[0]->Flags & WALLF_POLYOBJ)) { spechit_t spec; spec.line = ld; spec.Refpos = cres.Position; spec.Oldrefpos = tm.thing->PosRelative(ld); portalhit.Push(spec); return true; } if (((cres.portalflags & FFCF_NOFLOOR) && LineIsAbove(cres.line, tm.thing) != 0) || ((cres.portalflags & FFCF_NOCEILING) && LineIsBelow(cres.line, tm.thing) != 0)) { // this blocking line is in a different vertical layer and does not intersect with the actor that is being checked. // Since a one-sided line does not have an opening there's nothing left to do about it. return true; } // check if the actor can step through the ceiling portal. In this case one-sided lines in the current area should not block // Use the same rules for stepping through a portal as for non-portal case. bool ismissile = (tm.thing->flags & MF_MISSILE) && !(tm.thing->flags6 & MF6_STEPMISSILE) && !(tm.thing->flags3 & MF3_FLOORHUGGER); if (!ismissile && !cres.line->frontsector->PortalBlocksMovement(sector_t::ceiling)) { double portz = cres.line->frontsector->GetPortalPlaneZ(sector_t::ceiling); if (tm.thing->Z() < portz && tm.thing->Z() + tm.thing->MaxStepHeight >= portz && tm.floorz < portz) { if (wasfit) { tm.floorz = portz; tm.floorsector = cres.line->frontsector; tm.floorpic = cres.line->sidedef[0]->GetTexture(side_t::mid); tm.floorterrain = 0; tm.portalstep = true; tm.portalgroup = cres.line->frontsector->GetOppositePortalGroup(sector_t::ceiling); } return true; } } if (tm.thing->flags2 & MF2_BLASTED) { P_DamageMobj(tm.thing, NULL, NULL, tm.thing->Mass >> 5, NAME_Melee); } if (wasfit) { tm.thing->BlockingLine = ld; } CheckForPushSpecial(ld, 0, tm.thing); return false; } // MBF bouncers are treated as missiles here. bool Projectile = (tm.thing->flags & MF_MISSILE || tm.thing->BounceFlags & BOUNCE_MBF); // MBF considers that friendly monsters are not blocked by monster-blocking lines. // This is added here as a compatibility option. Note that monsters that are dehacked // into being friendly with the MBF flag automatically gain MF3_NOBLOCKMONST, so this // just optionally generalizes the behavior to other friendly monsters. bool NotBlocked = ((tm.thing->flags3 & MF3_NOBLOCKMONST) || ((i_compatflags & COMPATF_NOBLOCKFRIENDS) && (tm.thing->flags & MF_FRIENDLY))); if (!(Projectile) || (ld->flags & (ML_BLOCKEVERYTHING | ML_BLOCKPROJECTILE))) { if (ld->flags & ML_RAILING) { rail = true; } else if ((ld->flags & (ML_BLOCKING | ML_BLOCKEVERYTHING)) || // explicitly blocking everything (!(NotBlocked) && (ld->flags & ML_BLOCKMONSTERS)) || // block monsters only (tm.thing->player != NULL && (ld->flags & ML_BLOCK_PLAYERS)) || // block players ((Projectile) && (ld->flags & ML_BLOCKPROJECTILE)) || // block projectiles ((tm.thing->flags & MF_FLOAT) && (ld->flags & ML_BLOCK_FLOATERS))) // block floaters { if (cres.portalflags & FFCF_NOFLOOR) { int state = LineIsAbove(cres.line, tm.thing); if (state == -1) return true; if (state == 1) { if (wasfit) { // the line should not block but we should set the ceilingz to the portal boundary so that we can't float up into that line. double portalz = cres.line->frontsector->GetPortalPlaneZ(sector_t::floor); if (portalz < tm.ceilingz) { tm.ceilingz = portalz; tm.ceilingsector = cres.line->frontsector; } } return true; } } else if (cres.portalflags & FFCF_NOCEILING) { // same, but for downward portals int state = LineIsBelow(cres.line, tm.thing); if (state == -1) return true; if (state == 1) { if (wasfit) { double portalz = cres.line->frontsector->GetPortalPlaneZ(sector_t::ceiling); if (portalz > tm.floorz) { tm.floorz = portalz; tm.floorsector = cres.line->frontsector; tm.floorterrain = 0; } } return true; } } else { if (tm.thing->flags2 & MF2_BLASTED) { P_DamageMobj(tm.thing, NULL, NULL, tm.thing->Mass >> 5, NAME_Melee); } if (wasfit) { tm.thing->BlockingLine = ld; } // Calculate line side based on the actor's original position, not the new one. CheckForPushSpecial(ld, P_PointOnLineSide(cres.Position, ld), tm.thing); return false; } } } DVector2 ref = FindRefPoint(ld, cres.Position); FLineOpening open; P_LineOpening(open, tm.thing, ld, ref, &cres.Position, cres.portalflags); // Use the same rules for stepping through a portal as for non-portal case. bool ismissile = (tm.thing->flags & MF_MISSILE) && !(tm.thing->flags6 & MF6_STEPMISSILE) && !(tm.thing->flags3 & MF3_FLOORHUGGER); if (!ismissile && !tm.thing->Sector->PortalBlocksMovement(sector_t::ceiling)) { sector_t *oppsec = cres.line->frontsector == tm.thing->Sector ? cres.line->backsector : cres.line->frontsector; if (oppsec->PortalBlocksMovement(sector_t::ceiling)) { double portz = tm.thing->Sector->GetPortalPlaneZ(sector_t::ceiling); if (tm.thing->Z() < portz && tm.thing->Z() + tm.thing->MaxStepHeight >= portz && tm.floorz < portz) { if (wasfit) { // Actor is stepping through a portal. tm.portalstep = true; tm.portalgroup = tm.thing->Sector->GetOppositePortalGroup(sector_t::ceiling); } return true; } } } // [RH] Steep sectors count as dropoffs, if the actor touches the boundary between a steep slope and something else if (!(tm.thing->flags & MF_DROPOFF) && !(tm.thing->flags & (MF_NOGRAVITY | MF_NOCLIP))) { if ((open.frontfloorplane.fC() < STEEPSLOPE) != (open.backfloorplane.fC() < STEEPSLOPE)) { // on the boundary of a steep slope return false; } } // If the floor planes on both sides match we should recalculate open.bottom at the actual position we are checking // This is to avoid bumpy movement when crossing a linedef with the same slope on both sides. // This should never narrow down the opening, though, only widen it. if (open.frontfloorplane == open.backfloorplane && open.bottom > LINEOPEN_MIN) { auto newopen = open.frontfloorplane.ZatPoint(cres.Position); if (newopen < open.bottom) open.bottom = newopen; } if (rail && // Eww! Gross! This check means the rail only exists if you stand on the // high side of the rail. So if you're walking on the low side of the rail, // it's possible to get stuck in the rail until you jump out. Unfortunately, // there is an area on Strife MAP04 that requires this behavior. Still, it's // better than Strife's handling of rails, which lets you jump into rails // from either side. How long until somebody reports this as a bug and I'm // forced to say, "It's not a bug. It's a feature?" Ugh. (!(level.flags2 & LEVEL2_RAILINGHACK) || open.bottom == tm.thing->Sector->floorplane.ZatPoint(ref))) { open.bottom += 32; } if (wasfit) { // adjust floor / ceiling heights if (!(cres.portalflags & FFCF_NOCEILING)) { if (open.top < tm.ceilingz) { tm.ceilingz = open.top; tm.ceilingsector = open.topsec; tm.ceilingpic = open.ceilingpic; tm.ceilingline = ld; tm.thing->BlockingLine = ld; } } // If we are stepping through a portal the line's opening must be checked, regardless of the NOFLOOR flag if (!(cres.portalflags & FFCF_NOFLOOR) || (tm.portalstep && open.bottomsec->PortalGroup == tm.portalgroup)) { if (open.bottom > tm.floorz) { tm.floorz = open.bottom; tm.floorsector = open.bottomsec; tm.floorpic = open.floorpic; tm.floorterrain = open.floorterrain; tm.touchmidtex = open.touchmidtex; tm.abovemidtex = open.abovemidtex; tm.thing->BlockingLine = ld; } else if (open.bottom == tm.floorz) { tm.touchmidtex |= open.touchmidtex; tm.abovemidtex |= open.abovemidtex; } if (open.lowfloor < tm.dropoffz) { tm.dropoffz = open.lowfloor; } } } // if contacted a special line, add it to the list spechit_t spec; if (ld->special) { spec.line = ld; spec.Refpos = cres.Position; spec.Oldrefpos = tm.thing->PosRelative(ld); spechit.Push(spec); } if (ld->isLinePortal()) { spec.line = ld; spec.Refpos = cres.Position; spec.Oldrefpos = tm.thing->PosRelative(ld); portalhit.Push(spec); } return true; } //========================================================================== // // // PIT_CheckPortal // This checks the destination side of a non-static line portal // We cannot run a full P_CheckPosition there because it'd set // multiple fields to values that can cause problems in other // parts of the code // // What this does is starting a separate BlockLinesIterator // and only taking the absolutely necessary information // (i.e. floor and ceiling height plus terrain) // // //========================================================================== static bool PIT_CheckPortal(FMultiBlockLinesIterator &mit, FMultiBlockLinesIterator::CheckResult cres, const FBoundingBox &box, FCheckPosition &tm) { // if in another vertical section let's just ignore it. if (cres.portalflags & (FFCF_NOCEILING | FFCF_NOFLOOR)) return false; if (!box.inRange(cres.line) || box.BoxOnLineSide(cres.line) != -1) return false; line_t *lp = cres.line->getPortalDestination(); double zofs = 0; P_TranslatePortalXY(cres.line, cres.Position.X, cres.Position.Y); P_TranslatePortalZ(cres.line, zofs); // fudge a bit with the portal line so that this gets included in the checks that normally only get run on two-sided lines sector_t *sec = lp->backsector; if (lp->backsector == NULL) lp->backsector = lp->frontsector; tm.thing->AddZ(zofs); FBoundingBox pbox(cres.Position.X, cres.Position.Y, tm.thing->radius); FBlockLinesIterator it(pbox); bool ret = false; line_t *ld; // Check all lines at the destination while ((ld = it.Next())) { if (!pbox.inRange(ld) || pbox.BoxOnLineSide(ld) != -1) continue; if (ld->backsector == NULL) continue; DVector2 ref = FindRefPoint(ld, cres.Position); FLineOpening open; P_LineOpening(open, tm.thing, ld, ref, &cres.Position, 0); // adjust floor / ceiling heights if (open.top - zofs < tm.ceilingz) { tm.ceilingz = open.top - zofs; tm.ceilingpic = open.ceilingpic; /* tm.ceilingsector = open.topsec; tm.ceilingline = ld; tm.thing->BlockingLine = ld; */ ret = true; } if (open.bottom - zofs > tm.floorz) { tm.floorz = open.bottom - zofs; tm.floorpic = open.floorpic; tm.floorterrain = open.floorterrain; /* tm.floorsector = open.bottomsec; tm.touchmidtex = open.touchmidtex; tm.abovemidtex = open.abovemidtex; tm.thing->BlockingLine = ld; */ ret = true; } if (open.lowfloor - zofs < tm.dropoffz) tm.dropoffz = open.lowfloor - zofs; } tm.thing->AddZ(-zofs); lp->backsector = sec; return ret; } //========================================================================== // // Isolated to keep the code readable and fix the logic // //========================================================================== static bool CheckRipLevel(AActor *victim, AActor *projectile) { if (victim->RipLevelMin > 0 && projectile->RipperLevel < victim->RipLevelMin) return false; if (victim->RipLevelMax > 0 && projectile->RipperLevel > victim->RipLevelMax) return false; return true; } //========================================================================== // // Isolated to keep the code readable and allow reuse in other attacks // //========================================================================== static bool CanAttackHurt(AActor *victim, AActor *shooter) { // players are never subject to infighting settings and are always allowed // to harm / be harmed by anything. if (!victim->player && !shooter->player) { int infight = G_SkillProperty(SKILLP_Infight); if (infight < 0 && (victim->flags7 & MF7_FORCEINFIGHTING)) infight = 0; // This must override the 'no infight' setting to take effect. if (infight < 0) { // -1: Monsters cannot hurt each other, but make exceptions for // friendliness and hate status. if (shooter->flags & MF_SHOOTABLE) { // Question: Should monsters be allowed to shoot barrels in this mode? // The old code does not. if (victim->flags3 & MF3_ISMONSTER) { // Monsters that are clearly hostile can always hurt each other if (!victim->IsHostile(shooter)) { // The same if the shooter hates the target if (victim->tid == 0 || shooter->TIDtoHate != victim->tid) { return false; } } } } } else if (infight == 0) { // 0: Monsters cannot hurt same species except // cases where they are clearly supposed to do that if (victim->IsFriend(shooter)) { // Friends never harm each other, unless the shooter has the HARMFRIENDS set. if (!(shooter->flags7 & MF7_HARMFRIENDS)) return false; } else { if (victim->TIDtoHate != 0 && victim->TIDtoHate == shooter->TIDtoHate) { // [RH] Don't hurt monsters that hate the same victim as you do return false; } if (victim->GetSpecies() == shooter->GetSpecies() && !(victim->flags6 & MF6_DOHARMSPECIES)) { // Don't hurt same species or any relative - // but only if the target isn't one's hostile. if (!victim->IsHostile(shooter)) { // Allow hurting monsters the shooter hates. if (victim->tid == 0 || shooter->TIDtoHate != victim->tid) { return false; } } } } } // else if (infight==1) every shot hurts anything - no further tests needed } return true; } //========================================================================== // // PIT_CheckThing // //========================================================================== bool PIT_CheckThing(FMultiBlockThingsIterator &it, FMultiBlockThingsIterator::CheckResult &cres, const FBoundingBox &box, FCheckPosition &tm) { AActor *thing = cres.thing; double topz; bool solid; int damage; // don't clip against self if (thing == tm.thing) return true; if (!((thing->flags & (MF_SOLID | MF_SPECIAL | MF_SHOOTABLE)) || thing->flags6 & MF6_TOUCHY)) return true; // can't hit thing double blockdist = thing->radius + tm.thing->radius; if (fabs(thing->X() - cres.Position.X) >= blockdist || fabs(thing->Y() - cres.Position.Y) >= blockdist) return true; if ((thing->flags2 | tm.thing->flags2) & MF2_THRUACTORS) return true; if ((tm.thing->flags6 & MF6_THRUSPECIES) && (tm.thing->GetSpecies() == thing->GetSpecies())) return true; tm.thing->BlockingMobj = thing; topz = thing->Top(); // Both things overlap in x or y direction bool unblocking = false; // walking on other actors and unblocking is too messy through restricted portal types so disable it. if (!(cres.portalflags & FFCF_RESTRICTEDPORTAL)) { if (!(i_compatflags & COMPATF_NO_PASSMOBJ) && !(tm.thing->flags & (MF_FLOAT | MF_MISSILE | MF_SKULLFLY | MF_NOGRAVITY)) && (thing->flags & MF_SOLID) && (thing->flags4 & MF4_ACTLIKEBRIDGE)) { // [RH] Let monsters walk on actors as well as floors if ((tm.thing->flags3 & MF3_ISMONSTER) && topz >= tm.floorz && topz <= tm.thing->Z() + tm.thing->MaxStepHeight) { // The commented-out if is an attempt to prevent monsters from walking off a // thing further than they would walk off a ledge. I can't think of an easy // way to do this, so I restrict them to only walking on bridges instead. // Uncommenting the if here makes it almost impossible for them to walk on // anything, bridge or otherwise. // if (abs(thing->x - tmx) <= thing->radius && // abs(thing->y - tmy) <= thing->radius) { tm.stepthing = thing; tm.floorz = topz; } } } if (((tm.FromPMove || tm.thing->player != NULL) && thing->flags&MF_SOLID)) { DVector3 oldpos = tm.thing->PosRelative(thing); // Both actors already overlap. To prevent them from remaining stuck allow the move if it // takes them further apart or the move does not change the position (when called from P_ChangeSector.) if (tm.pos.X == tm.thing->X() && tm.pos.Y == tm.thing->Y()) { unblocking = true; } else if (fabs(thing->X() - oldpos.X) < (thing->radius + tm.thing->radius) && fabs(thing->Y() - oldpos.Y) < (thing->radius + tm.thing->radius)) { double newdist = thing->Distance2D(cres.Position.X, cres.Position.Y); double olddist = thing->Distance2D(oldpos.X, oldpos.Y); if (newdist > olddist) { // unblock only if there's already a vertical overlap (or both actors are flagged not to overlap) unblocking = (tm.thing->Top() > thing->Z() && tm.thing->Z() < topz) || (tm.thing->flags3 & thing->flags3 & MF3_DONTOVERLAP); } } } } // [RH] If the other thing is a bridge, then treat the moving thing as if it had MF2_PASSMOBJ, so // you can use a scrolling floor to move scenery items underneath a bridge. if ((tm.thing->flags2 & MF2_PASSMOBJ || thing->flags4 & MF4_ACTLIKEBRIDGE) && !(i_compatflags & COMPATF_NO_PASSMOBJ)) { // check if a mobj passed over/under another object if (!(tm.thing->flags & MF_MISSILE) || !(tm.thing->flags2 & MF2_RIP) || (thing->flags5 & MF5_DONTRIP) || ((tm.thing->flags6 & MF6_NOBOSSRIP) && (thing->flags2 & MF2_BOSS))) { // Some things prefer not to overlap each other, if possible (Q: Is this even needed anymore? It was just for dealing with some deficiencies in the code below in Heretic.) if (!(tm.thing->flags3 & thing->flags3 & MF3_DONTOVERLAP)) { if ((tm.thing->Z() >= topz) || (tm.thing->Top() <= thing->Z())) return true; } else return unblocking; // This may not really make sense, but Heretic depends on the broken implementation. } } // Call the script callback. This must be done before any other checks that perform some actual action or may already return a 'block'. // The checks here are to do this only for conditions that would later result in an action, calling this for everything would be too much of a drag if // too many scripted overrides were being used, as PIT_CheckThing is even called for touching all the monster corpses lying around. if (((thing->flags & MF_SOLID) || (thing->flags6 & (MF6_TOUCHY | MF6_BUMPSPECIAL))) && ((tm.thing->flags & (MF_SOLID|MF_MISSILE)) || (tm.thing->flags2 & MF2_BLASTED) || (tm.thing->flags6 & MF6_BLOCKEDBYSOLIDACTORS) || (tm.thing->BounceFlags & BOUNCE_MBF))) { if (!P_CanCollideWith(tm.thing, thing)) return true; } if (tm.thing->player == NULL || !(tm.thing->player->cheats & CF_PREDICTING)) { // touchy object is alive, toucher is solid if (thing->flags6 & MF6_TOUCHY && tm.thing->flags & MF_SOLID && thing->health > 0 && // Thing is an armed mine or a sentient thing (thing->flags6 & MF6_ARMED || thing->IsSentient()) && // either different classes or players (thing->player || thing->GetClass() != tm.thing->GetClass()) && // or different species if DONTHARMSPECIES (!(thing->flags6 & MF6_DONTHARMSPECIES) || thing->GetSpecies() != tm.thing->GetSpecies()) && // touches vertically topz >= tm.thing->Z() && tm.thing->Top() >= thing->Z() && // prevents lost souls from exploding when fired by pain elementals (thing->master != tm.thing && tm.thing->master != thing)) // Difference with MBF: MBF hardcodes the LS/PE check and lets actors of the same species // but different classes trigger the touchiness, but that seems less straightforwards. { thing->flags6 &= ~MF6_ARMED; // Disarm P_DamageMobj(thing, NULL, NULL, thing->health, NAME_None, DMG_FORCED); // kill object return true; } // Check for MF6_BUMPSPECIAL // By default, only players can activate things by bumping into them if ((thing->flags6 & MF6_BUMPSPECIAL) && ((tm.thing->player != NULL) || ((thing->activationtype & THINGSPEC_MonsterTrigger) && (tm.thing->flags3 & MF3_ISMONSTER)) || ((thing->activationtype & THINGSPEC_MissileTrigger) && (tm.thing->flags & MF_MISSILE)) ) && (level.maptime > thing->lastbump)) // Leave the bumper enough time to go away { if (P_ActivateThingSpecial(thing, tm.thing)) thing->lastbump = level.maptime + TICRATE; } } // Check for skulls slamming into things if (tm.thing->flags & MF_SKULLFLY) { bool res = tm.thing->CallSlam(tm.thing->BlockingMobj); tm.thing->BlockingMobj = NULL; return res; } // [ED850] Player Prediction ends here. There is nothing else they could/should do. if (tm.thing->player != NULL && (tm.thing->player->cheats & CF_PREDICTING)) { solid = (thing->flags & MF_SOLID) && !(thing->flags & MF_NOCLIP) && ((tm.thing->flags & MF_SOLID) || (tm.thing->flags6 & MF6_BLOCKEDBYSOLIDACTORS)); return !solid || unblocking; } // Check for blasted thing running into another if ((tm.thing->flags2 & MF2_BLASTED) && (thing->flags & MF_SHOOTABLE)) { if (!(thing->flags2 & MF2_BOSS) && (thing->flags3 & MF3_ISMONSTER) && !(thing->flags3 & MF3_DONTBLAST)) { // ideally this should take the mass factor into account thing->Vel += tm.thing->Vel.XY(); if (fabs(thing->Vel.X) + fabs(thing->Vel.Y) > 3.) { int newdam; damage = (tm.thing->Mass / 100) + 1; newdam = P_DamageMobj(thing, tm.thing, tm.thing, damage, tm.thing->DamageType); P_TraceBleed(newdam > 0 ? newdam : damage, thing, tm.thing); damage = (thing->Mass / 100) + 1; newdam = P_DamageMobj(tm.thing, thing, thing, damage >> 2, tm.thing->DamageType); P_TraceBleed(newdam > 0 ? newdam : damage, tm.thing, thing); } return false; } } // Check for missile or non-solid MBF bouncer if (tm.thing->flags & MF_MISSILE || ((tm.thing->BounceFlags & BOUNCE_MBF) && !(tm.thing->flags & MF_SOLID))) { // Check for a non-shootable mobj if (thing->flags2 & MF2_NONSHOOTABLE) { return true; } // Check for passing through a ghost if ((thing->flags3 & MF3_GHOST) && (tm.thing->flags2 & MF2_THRUGHOST)) { return true; } if ((tm.thing->flags6 & MF6_MTHRUSPECIES) && tm.thing->target // NULL pointer check && (tm.thing->target->GetSpecies() == thing->GetSpecies())) return true; // Check for rippers passing through corpses if ((thing->flags & MF_CORPSE) && (tm.thing->flags2 & MF2_RIP) && !(thing->flags & MF_SHOOTABLE)) { return true; } double clipheight; if (thing->projectilepassheight > 0) { clipheight = thing->projectilepassheight; } else if (thing->projectilepassheight < 0 && (i_compatflags & COMPATF_MISSILECLIP)) { clipheight = -thing->projectilepassheight; } else { clipheight = thing->Height; } // Check if it went over / under if (tm.thing->Z() > thing->Z() + clipheight) { // Over thing return true; } if (tm.thing->Top() < thing->Z()) { // Under thing return true; } // [RH] What is the point of this check, again? In Hexen, it is unconditional, // but here we only do it if the missile's damage is 0. // MBF bouncer might have a non-0 damage value, but they must not deal damage on impact either. if ((tm.thing->BounceFlags & BOUNCE_Actors) && (tm.thing->IsZeroDamage() || !(tm.thing->flags & MF_MISSILE))) { return (tm.thing->target == thing || !(thing->flags & MF_SOLID)); } switch (tm.thing->SpecialMissileHit(thing)) { case 0: return false; case 1: return true; default: break; } // [RH] Extend DeHacked infighting to allow for monsters // to never fight each other if (tm.thing->target != NULL) { if (thing == tm.thing->target) { // Don't missile self return true; } if (!CanAttackHurt(thing, tm.thing->target)) { return false; } } if (!(thing->flags & MF_SHOOTABLE)) { // Didn't do any damage return !(thing->flags & MF_SOLID); } if ((thing->flags4 & MF4_SPECTRAL) && !(tm.thing->flags4 & MF4_SPECTRAL)) { return true; } if ((tm.DoRipping && !(thing->flags5 & MF5_DONTRIP)) && CheckRipLevel(thing, tm.thing)) { if (!(tm.thing->flags6 & MF6_NOBOSSRIP) || !(thing->flags2 & MF2_BOSS)) { bool *check = tm.LastRipped.CheckKey(thing); if (check == NULL || !*check) { tm.LastRipped[thing] = true; if (!(thing->flags & MF_NOBLOOD) && !(thing->flags2 & MF2_REFLECTIVE) && !(tm.thing->flags3 & MF3_BLOODLESSIMPACT) && !(thing->flags2 & (MF2_INVULNERABLE | MF2_DORMANT))) { // Ok to spawn blood P_RipperBlood(tm.thing, thing); } S_Sound(tm.thing, CHAN_BODY, "misc/ripslop", 1, ATTN_IDLE); // Do poisoning (if using new style poison) if (tm.thing->PoisonDamage > 0 && tm.thing->PoisonDuration != INT_MIN) { P_PoisonMobj(thing, tm.thing, tm.thing->target, tm.thing->PoisonDamage, tm.thing->PoisonDuration, tm.thing->PoisonPeriod, tm.thing->PoisonDamageType); } damage = tm.thing->GetMissileDamage(3, 2); int newdam = P_DamageMobj(thing, tm.thing, tm.thing->target, damage, tm.thing->DamageType); if (!(tm.thing->flags3 & MF3_BLOODLESSIMPACT)) { P_TraceBleed(newdam > 0 ? newdam : damage, thing, tm.thing); } if (thing->flags2 & MF2_PUSHABLE && !(tm.thing->flags2 & MF2_CANNOTPUSH)) { // Push thing if (thing->lastpush != tm.PushTime) { thing->Vel += tm.thing->Vel.XY() * thing->pushfactor; thing->lastpush = tm.PushTime; } } } spechit.Clear(); return true; } } // Do poisoning (if using new style poison) if (tm.thing->PoisonDamage > 0 && tm.thing->PoisonDuration != INT_MIN) { P_PoisonMobj(thing, tm.thing, tm.thing->target, tm.thing->PoisonDamage, tm.thing->PoisonDuration, tm.thing->PoisonPeriod, tm.thing->PoisonDamageType); } // Do damage damage = tm.thing->GetMissileDamage((tm.thing->flags4 & MF4_STRIFEDAMAGE) ? 3 : 7, 1); if ((damage > 0) || (tm.thing->flags6 & MF6_FORCEPAIN) || (tm.thing->flags7 & MF7_CAUSEPAIN)) { int newdam = P_DamageMobj(thing, tm.thing, tm.thing->target, damage, tm.thing->DamageType); if (damage > 0) { if ((tm.thing->flags5 & MF5_BLOODSPLATTER) && !(thing->flags & MF_NOBLOOD) && !(thing->flags2 & MF2_REFLECTIVE) && !(thing->flags2 & (MF2_INVULNERABLE | MF2_DORMANT)) && !(tm.thing->flags3 & MF3_BLOODLESSIMPACT) && (pr_checkthing() < 192)) { P_BloodSplatter(tm.thing->Pos(), thing, tm.thing->AngleTo(thing)); } if (!(tm.thing->flags3 & MF3_BLOODLESSIMPACT)) { P_TraceBleed(newdam > 0 ? newdam : damage, thing, tm.thing); } } } else { P_GiveBody(thing, -damage); } if ((thing->flags7 & MF7_THRUREFLECT) && (thing->flags2 & MF2_REFLECTIVE) && (tm.thing->flags & MF_MISSILE)) { if (tm.thing->flags2 & MF2_SEEKERMISSILE) { tm.thing->tracer = tm.thing->target; } tm.thing->target = thing; return true; } return false; // don't traverse any more } if (thing->flags2 & MF2_PUSHABLE && !(tm.thing->flags2 & MF2_CANNOTPUSH)) { // Push thing if (thing->lastpush != tm.PushTime) { thing->Vel += tm.thing->Vel.XY() * thing->pushfactor; thing->lastpush = tm.PushTime; } } solid = (thing->flags & MF_SOLID) && !(thing->flags & MF_NOCLIP) && ((tm.thing->flags & MF_SOLID) || (tm.thing->flags6 & MF6_BLOCKEDBYSOLIDACTORS)); // Check for special pickup if ((thing->flags & MF_SPECIAL) && (tm.thing->flags & MF_PICKUP) // [RH] The next condition is to compensate for the extra height // that gets added by P_CheckPosition() so that you cannot pick // up things that are above your true height. && thing->Z() < tm.thing->Top() - tm.thing->MaxStepHeight) { // Can be picked up by tmthing P_TouchSpecialThing(thing, tm.thing); // can remove thing } // killough 3/16/98: Allow non-solid moving objects to move through solid // ones, by allowing the moving thing (tmthing) to move if it's non-solid, // despite another solid thing being in the way. // killough 4/11/98: Treat no-clipping things as not blocking return !solid || unblocking; // return !(thing->flags & MF_SOLID); // old code -- killough } /* =============================================================================== MOVEMENT CLIPPING =============================================================================== */ //========================================================================== // // P_CheckPosition // This is purely informative, nothing is modified // (except things picked up and missile damage applied). // // in: // a AActor (can be valid or invalid) // a position to be checked // (doesn't need to be related to the AActor->x,y) // // during: // special things are touched if MF_PICKUP // early out on solid lines? // // out: // newsubsec // floorz // ceilingz // tmdropoffz = the lowest point contacted (monsters won't move to a dropoff) // speciallines[] // numspeciallines // AActor *BlockingMobj = pointer to thing that blocked position (NULL if not // blocked, or blocked by a line). // //========================================================================== bool P_CheckPosition(AActor *thing, const DVector2 &pos, FCheckPosition &tm, bool actorsonly) { sector_t *newsec; AActor *thingblocker; double realHeight = thing->Height; tm.thing = thing; tm.pos.X = pos.X; tm.pos.Y = pos.Y; tm.pos.Z = thing->Z(); newsec = tm.sector = P_PointInSector(pos); tm.ceilingline = thing->BlockingLine = NULL; // Retrieve the base floor / ceiling from the target location. // Any contacted lines the step closer together will adjust them. if (!thing->IsNoClip2()) { if (!newsec->PortalBlocksMovement(sector_t::ceiling) || !newsec->PortalBlocksMovement(sector_t::floor)) { // Use P_GetFloorCeilingZ only if there's portals to consider. Its logic is subtly different than what is needed here for 3D floors. P_GetFloorCeilingZ(tm, FFCF_SAMESECTOR); } else { tm.floorz = tm.dropoffz = newsec->floorplane.ZatPoint(pos); tm.floorpic = newsec->GetTexture(sector_t::floor); tm.ceilingz = newsec->ceilingplane.ZatPoint(pos); tm.ceilingpic = newsec->GetTexture(sector_t::ceiling); tm.floorsector = tm.ceilingsector = newsec; tm.floorterrain = newsec->GetTerrain(sector_t::floor); } F3DFloor* rover; double thingtop = thing->Height > 0 ? thing->Top() : thing->Z() + 1; for (unsigned i = 0; ie->XFloor.ffloors.Size(); i++) { rover = newsec->e->XFloor.ffloors[i]; if (!(rover->flags & FF_SOLID) || !(rover->flags & FF_EXISTS)) continue; double ff_bottom = rover->bottom.plane->ZatPoint(pos); double ff_top = rover->top.plane->ZatPoint(pos); double delta1 = thing->Z() - (ff_bottom + ((ff_top - ff_bottom) / 2)); double delta2 = thingtop - (ff_bottom + ((ff_top - ff_bottom) / 2)); if (ff_top > tm.floorz && fabs(delta1) < fabs(delta2)) { tm.floorz = tm.dropoffz = ff_top; tm.floorpic = *rover->top.texture; tm.floorterrain = rover->model->GetTerrain(rover->top.isceiling); } if (ff_bottom < tm.ceilingz && fabs(delta1) >= fabs(delta2)) { tm.ceilingz = ff_bottom; tm.ceilingpic = *rover->bottom.texture; } } } else { // With noclip2, we must ignore 3D floors and go right to the uppermost ceiling and lowermost floor. tm.floorz = tm.dropoffz = newsec->LowestFloorAt(pos, &tm.floorsector); tm.ceilingz = newsec->HighestCeilingAt(pos, &tm.ceilingsector); tm.floorpic = tm.floorsector->GetTexture(sector_t::floor); tm.floorterrain = tm.floorsector->GetTerrain(sector_t::floor); tm.ceilingpic = tm.ceilingsector->GetTexture(sector_t::ceiling); } tm.touchmidtex = false; tm.abovemidtex = false; validcount++; if ((thing->flags & MF_NOCLIP) && !(thing->flags & MF_SKULLFLY)) return true; // Check things first, possibly picking things up. thing->BlockingMobj = NULL; thingblocker = NULL; if (thing->player) { // [RH] Fake taller height to catch stepping up into things. thing->Height = realHeight + thing->MaxStepHeight; } tm.stepthing = NULL; FBoundingBox box(pos.X, pos.Y, thing->radius); FPortalGroupArray pcheck; FMultiBlockThingsIterator it2(pcheck, pos.X, pos.Y, thing->Z(), thing->Height, thing->radius, false, newsec); FMultiBlockThingsIterator::CheckResult tcres; while ((it2.Next(&tcres))) { if (!PIT_CheckThing(it2, tcres, it2.Box(), tm)) { // [RH] If a thing can be stepped up on, we need to continue checking // other things in the blocks and see if we hit something that is // definitely blocking. Otherwise, we need to check the lines, or we // could end up stuck inside a wall. AActor *BlockingMobj = thing->BlockingMobj; // If this blocks through a restricted line portal, it will always completely block. if (BlockingMobj == NULL || (i_compatflags & COMPATF_NO_PASSMOBJ) || (tcres.portalflags & FFCF_RESTRICTEDPORTAL)) { // Thing slammed into something; don't let it move now. thing->Height = realHeight; return false; } else if (!BlockingMobj->player && !(thing->flags & (MF_FLOAT | MF_MISSILE | MF_SKULLFLY)) && BlockingMobj->Top() - thing->Z() <= thing->MaxStepHeight) { if (thingblocker == NULL || BlockingMobj->Z() > thingblocker->Z()) { thingblocker = BlockingMobj; } thing->BlockingMobj = NULL; } else if (thing->player && thing->Top() - BlockingMobj->Z() <= thing->MaxStepHeight) { if (thingblocker) { // There is something to step up on. Return this thing as // the blocker so that we don't step up. thing->Height = realHeight; return false; } // Nothing is blocking us, but this actor potentially could // if there is something else to step on. thing->BlockingMobj = NULL; } else { // Definitely blocking thing->Height = realHeight; return false; } } } // check lines // [RH] We need to increment validcount again, because a function above may // have already set some lines to equal the current validcount. // // Specifically, when DehackedPickup spawns a new item in its TryPickup() // function, that new actor will set the lines around it to match validcount // when it links itself into the world. If we just leave validcount alone, // that will give the player the freedom to walk through walls at will near // a pickup they cannot get, because their validcount will prevent them from // being considered for collision with the player. validcount++; thing->BlockingMobj = NULL; thing->Height = realHeight; if (actorsonly || (thing->flags & MF_NOCLIP)) return (thing->BlockingMobj = thingblocker) == NULL; spechit.Clear(); portalhit.Clear(); FMultiBlockLinesIterator it(pcheck, pos.X, pos.Y, thing->Z(), thing->Height, thing->radius, newsec); FMultiBlockLinesIterator::CheckResult lcres; double thingdropoffz = tm.floorz; //bool onthing = (thingdropoffz != tmdropoffz); tm.floorz = tm.dropoffz; bool good = true; while (it.Next(&lcres)) { bool thisresult = PIT_CheckLine(it, lcres, it.Box(), tm, good); good &= thisresult; if (thisresult) { FLinePortal *port = lcres.line->getPortal(); if (port != NULL && port->mFlags & PORTF_PASSABLE && port->mType != PORTT_LINKED) { // Checking the other side of the portal completely is too costly, // but checking the portal's destination line is necessary to // retrieve the proper sector heights on the other side. if (PIT_CheckPortal(it, lcres, it.Box(), tm)) { tm.thing->BlockingLine = lcres.line; } } } } if (!good) { return false; } if (tm.ceilingz - tm.floorz < thing->Height) { return false; } if (tm.touchmidtex) { tm.dropoffz = tm.floorz; } else if (tm.stepthing != NULL) { tm.dropoffz = thingdropoffz; } return (thing->BlockingMobj = thingblocker) == NULL; } bool P_CheckPosition(AActor *thing, const DVector2 &pos, bool actorsonly) { FCheckPosition tm; return P_CheckPosition(thing, pos, tm, actorsonly); } DEFINE_ACTION_FUNCTION(AActor, CheckPosition) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(x); PARAM_FLOAT(y); PARAM_BOOL_DEF(actorsonly); PARAM_POINTER_DEF(tm, FCheckPosition); if (tm) { ACTION_RETURN_BOOL(P_CheckPosition(self, DVector2(x, y), *tm, actorsonly)); } else { ACTION_RETURN_BOOL(P_CheckPosition(self, DVector2(x, y), actorsonly)); } } //---------------------------------------------------------------------------- // // FUNC P_TestMobjLocation // // Returns true if the mobj is not blocked by anything at its current // location, otherwise returns false. // //---------------------------------------------------------------------------- bool P_TestMobjLocation(AActor *mobj) { ActorFlags flags; flags = mobj->flags; mobj->flags &= ~MF_PICKUP; if (P_CheckPosition(mobj, mobj->Pos())) { // XY is ok, now check Z mobj->flags = flags; if ((mobj->Z() < mobj->floorz) || (mobj->Top() > mobj->ceilingz)) { // Bad Z return false; } return true; } mobj->flags = flags; return false; } DEFINE_ACTION_FUNCTION(AActor, TestMobjLocation) { PARAM_SELF_PROLOGUE(AActor); ACTION_RETURN_BOOL(P_TestMobjLocation(self)); } //============================================================================= // // P_CheckOnmobj(AActor *thing) // // Checks if the new Z position is legal //============================================================================= AActor *P_CheckOnmobj(AActor *thing) { double oldz; bool good; AActor *onmobj; oldz = thing->Z(); P_FakeZMovement(thing); good = P_TestMobjZ(thing, false, &onmobj); thing->SetZ(oldz); return good ? NULL : onmobj; } //============================================================================= // // P_TestMobjZ // //============================================================================= bool P_TestMobjZ(AActor *actor, bool quick, AActor **pOnmobj) { AActor *onmobj = NULL; if (actor->flags & MF_NOCLIP) { if (pOnmobj) *pOnmobj = NULL; return true; } FPortalGroupArray check; FMultiBlockThingsIterator it(check, actor, -1, true); FMultiBlockThingsIterator::CheckResult cres; while (it.Next(&cres)) { AActor *thing = cres.thing; double blockdist = thing->radius + actor->radius; if (fabs(thing->X() - cres.Position.X) >= blockdist || fabs(thing->Y() - cres.Position.Y) >= blockdist) { continue; } if ((actor->flags2 | thing->flags2) & MF2_THRUACTORS) { continue; } if ((actor->flags6 & MF6_THRUSPECIES) && (thing->GetSpecies() == actor->GetSpecies())) { continue; } if (!(thing->flags & MF_SOLID)) { // Can't hit thing continue; } if (thing->flags & (MF_SPECIAL | MF_NOCLIP)) { // [RH] Specials and noclippers don't block moves continue; } if (thing->flags & (MF_CORPSE)) { // Corpses need a few more checks if (!(actor->flags & MF_ICECORPSE)) continue; } if (!(thing->flags4 & MF4_ACTLIKEBRIDGE) && (actor->flags & MF_SPECIAL)) { // [RH] Only bridges block pickup items continue; } if (thing == actor) { // Don't clip against self continue; } if ((actor->flags & MF_MISSILE) && (thing == actor->target)) { // Don't clip against whoever shot the missile. continue; } if (actor->Z() > thing->Top()) { // over thing continue; } else if (actor->Top() <= thing->Z()) { // under thing continue; } else if (!quick && onmobj != NULL && thing->Top() < onmobj->Top()) { // something higher is in the way continue; } else if (!P_CanCollideWith(actor, thing)) { // If they cannot collide, they cannot block each other. continue; } onmobj = thing; if (quick) break; } if (pOnmobj) *pOnmobj = onmobj; return onmobj == NULL; } DEFINE_ACTION_FUNCTION(AActor, TestMobjZ) { PARAM_SELF_PROLOGUE(AActor); PARAM_BOOL_DEF(quick); AActor *on = nullptr;; bool retv = P_TestMobjZ(self, quick, &on); if (numret > 1) { numret = 2; ret[1].SetObject(on); } if (numret > 0) { ret[0].SetInt(retv); } return numret; } //============================================================================= // // P_FakeZMovement // // Fake the zmovement so that we can check if a move is legal //============================================================================= void P_FakeZMovement(AActor *mo) { // // adjust height // mo->AddZ(mo->Vel.Z); if ((mo->flags&MF_FLOAT) && mo->target) { // float down towards target if too close if (!(mo->flags & MF_SKULLFLY) && !(mo->flags & MF_INFLOAT)) { double dist = mo->Distance2D(mo->target); double 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) && !mo->IsNoClip2()) { mo->AddZ(DAngle(4.5 * level.maptime).Sin()); } // // clip movement // if (mo->Z() <= mo->floorz) { // hit the floor mo->SetZ(mo->floorz); } if (mo->Top() > mo->ceilingz) { // hit the ceiling mo->SetZ(mo->ceilingz - mo->Height); } } //=========================================================================== // // CheckForPushSpecial // //=========================================================================== static void CheckForPushSpecial(line_t *line, int side, AActor *mobj, DVector2 *posforwindowcheck) { if (line->special && !(mobj->flags6 & MF6_NOTRIGGER)) { if (posforwindowcheck && !(i_compatflags2 & COMPATF2_PUSHWINDOW) && line->backsector != NULL) { // Make sure this line actually blocks us and is not a window // or similar construct we are standing inside of. DVector3 pos = mobj->PosRelative(line); double fzt = line->frontsector->ceilingplane.ZatPoint(*posforwindowcheck); double fzb = line->frontsector->floorplane.ZatPoint(*posforwindowcheck); double bzt = line->backsector->ceilingplane.ZatPoint(*posforwindowcheck); double bzb = line->backsector->floorplane.ZatPoint(*posforwindowcheck); if (fzt >= mobj->Top() && bzt >= mobj->Top() && fzb <= mobj->Z() && bzb <= mobj->Z()) { if (line->flags & ML_3DMIDTEX) { double top, bot; P_GetMidTexturePosition(line, side, &top, &bot); if (bot < mobj->Top() && top > mobj->Z()) { goto isblocking; } } // we must also check if some 3D floor in the backsector may be blocking for (auto rover : line->backsector->e->XFloor.ffloors) { if (!(rover->flags & FF_SOLID) || !(rover->flags & FF_EXISTS)) continue; double ff_bottom = rover->bottom.plane->ZatPoint(*posforwindowcheck); double ff_top = rover->top.plane->ZatPoint(*posforwindowcheck); if (ff_bottom < mobj->Top() && ff_top > mobj->Z()) { goto isblocking; } } return; } } isblocking: if (mobj->flags2 & MF2_PUSHWALL) { P_ActivateLine(line, mobj, side, SPAC_Push); } else if (mobj->flags2 & MF2_IMPACT) { if ((level.flags2 & LEVEL2_MISSILESACTIVATEIMPACT) || !(mobj->flags & MF_MISSILE) || (mobj->target == NULL)) { P_ActivateLine(line, mobj, side, SPAC_Impact); } else { P_ActivateLine(line, mobj->target, side, SPAC_Impact); } } } } //========================================================================== // // P_TryMove // Attempt to move to a new position, // crossing special lines unless MF_TELEPORT is set. // //========================================================================== bool P_TryMove(AActor *thing, const DVector2 &pos, int dropoff, // killough 3/15/98: allow dropoff as option const secplane_t *onfloor, // [RH] Let P_TryMove keep the thing on the floor FCheckPosition &tm, bool missileCheck) // [GZ] Fired missiles ignore the drop-off test { sector_t *oldsector; double oldz; int side; int oldside; sector_t* oldsec = thing->Sector; // [RH] for sector actions sector_t* newsec; tm.floatok = false; tm.portalstep = false; oldz = thing->Z(); if (onfloor) { thing->SetZ(onfloor->ZatPoint(pos)); } thing->flags6 |= MF6_INTRYMOVE; if (!P_CheckPosition(thing, pos, tm)) { AActor *BlockingMobj = thing->BlockingMobj; // Solid wall or thing if (!BlockingMobj || BlockingMobj->player || !thing->player) { goto pushline; } else { if (BlockingMobj->player || !thing->player) { goto pushline; } else if (BlockingMobj->Top() - thing->Z() > thing->MaxStepHeight || ((BlockingMobj->Sector->ceilingplane.ZatPoint(pos) - (BlockingMobj->Top()) < thing->Height) && BlockingMobj->Sector->PortalBlocksMovement(sector_t::ceiling)) || (tm.ceilingz - (BlockingMobj->Top()) < thing->Height)) { goto pushline; } } if (!(tm.thing->flags2 & MF2_PASSMOBJ) || (i_compatflags & COMPATF_NO_PASSMOBJ)) { thing->SetZ(oldz); thing->flags6 &= ~MF6_INTRYMOVE; return false; } } if (thing->flags3 & MF3_FLOORHUGGER) { thing->SetZ(tm.floorz); } else if (thing->flags3 & MF3_CEILINGHUGGER) { thing->SetZ(tm.ceilingz - thing->Height); } if (onfloor && tm.floorsector == thing->floorsector) { thing->SetZ(tm.floorz); } if (!(thing->flags & MF_NOCLIP)) { if (tm.ceilingz - tm.floorz < thing->Height) { goto pushline; // doesn't fit } tm.floatok = true; if (!(thing->flags & MF_TELEPORT) && tm.ceilingz < thing->Top() && !(thing->flags3 & MF3_CEILINGHUGGER) && (!(thing->flags2 & MF2_FLY) || !(thing->flags & MF_NOGRAVITY))) { goto pushline; // mobj must lower itself to fit } if (thing->flags2 & MF2_FLY && thing->flags & MF_NOGRAVITY) { #if 1 if (thing->Top() > tm.ceilingz) goto pushline; #else // When flying, slide up or down blocking lines until the actor // is not blocked. if (thing->Top() > tm.ceilingz) { thing->Vel.Z = -8; goto pushline; } else if (thing->Z() < tm.floorz && tm.floorz - tm.dropoffz > thing->MaxDropOffHeight) { thing->Vel.Z = 8; goto pushline; } #endif } if (!(thing->flags & MF_TELEPORT) && (!(thing->flags3 & MF3_FLOORHUGGER) || thing->flags5 & MF5_NODROPOFF)) { if ((thing->flags & MF_MISSILE) && !(thing->flags6 & MF6_STEPMISSILE) && tm.floorz > thing->Z() && !(thing->flags3 & MF3_FLOORHUGGER)) { // [RH] Don't let normal missiles climb steps goto pushline; } if (tm.floorz - thing->Z() > thing->MaxStepHeight) { // too big a step up goto pushline; } else if (thing->Z() < tm.floorz) { // [RH] Check to make sure there's nothing in the way for the step up double savedz = thing->Z(); bool good; thing->SetZ(tm.floorz); good = P_TestMobjZ(thing); thing->SetZ(savedz); if (!good) { goto pushline; } if (thing->flags6 & MF6_STEPMISSILE) { thing->SetZ(tm.floorz); // If moving down, cancel vertical component of the velocity if (thing->Vel.Z < 0) { // If it's a bouncer, let it bounce off its new floor, too. if (thing->BounceFlags & BOUNCE_Floors) { thing->FloorBounceMissile(tm.floorsector->floorplane); } else { thing->Vel.Z = 0; } } } } } // compatibility check: Doom originally did not allow monsters to cross dropoffs at all. // If the compatibility flag is on, only allow this when the velocity comes from a scroller if ((i_compatflags & COMPATF_CROSSDROPOFF) && !(thing->flags4 & MF4_SCROLLMOVE)) { dropoff = false; } if (dropoff == 2 && // large jump down (e.g. dogs) (tm.floorz - tm.dropoffz > 128. || thing->target == NULL || thing->target->Z() >tm.dropoffz)) { dropoff = false; } // killough 3/15/98: Allow certain objects to drop off if ((!dropoff && !(thing->flags & (MF_DROPOFF | MF_FLOAT | MF_MISSILE))) || (thing->flags5&MF5_NODROPOFF)) { if (!(thing->flags5&MF5_AVOIDINGDROPOFF)) { double floorz = tm.floorz; // [RH] If the thing is standing on something, use its current z as the floorz. // This is so that it does not walk off of things onto a drop off. if (thing->flags2 & MF2_ONMOBJ) { floorz = MAX(thing->Z(), tm.floorz); } if (floorz - tm.dropoffz > thing->MaxDropOffHeight && !(thing->flags2 & MF2_BLASTED) && !missileCheck) { // Can't move over a dropoff unless it's been blasted // [GZ] Or missile-spawned thing->SetZ(oldz); thing->flags6 &= ~MF6_INTRYMOVE; return false; } } else { // special logic to move a monster off a dropoff // this intentionally does not check for standing on things. if (thing->floorz - tm.floorz > thing->MaxDropOffHeight || thing->dropoffz - tm.dropoffz > thing->MaxDropOffHeight) { thing->flags6 &= ~MF6_INTRYMOVE; return false; } } } if (thing->flags2 & MF2_CANTLEAVEFLOORPIC && (tm.floorpic != thing->floorpic || tm.floorz - thing->Z() != 0)) { // must stay within a sector of a certain floor type thing->SetZ(oldz); thing->flags6 &= ~MF6_INTRYMOVE; return false; } //Added by MC: To prevent bot from getting into dangerous sectors. if (thing->player && thing->player->Bot != NULL && thing->flags & MF_SHOOTABLE) { if (tm.sector != thing->Sector && bglobal.IsDangerous(tm.sector)) { thing->player->Bot->prev = thing->player->Bot->dest; thing->player->Bot->dest = NULL; thing->Vel.X = thing->Vel.Y = 0; thing->SetZ(oldz); thing->flags6 &= ~MF6_INTRYMOVE; return false; } } } // [RH] Check status of eyes against fake floor/ceiling in case // it slopes or the player's eyes are bobbing in and out. bool oldAboveFakeFloor, oldAboveFakeCeiling; double viewheight; // pacify GCC viewheight = thing->player ? thing->player->viewheight : thing->Height / 2; oldAboveFakeFloor = oldAboveFakeCeiling = false; if (oldsec->heightsec) { double eyez = oldz + viewheight; oldAboveFakeFloor = eyez > oldsec->heightsec->floorplane.ZatPoint(thing); oldAboveFakeCeiling = eyez > oldsec->heightsec->ceilingplane.ZatPoint(thing); } // Borrowed from MBF: if (thing->BounceFlags & BOUNCE_MBF && // killough 8/13/98 !(thing->flags & (MF_MISSILE | MF_NOGRAVITY)) && !thing->IsSentient() && tm.floorz - thing->Z() > 16) { // too big a step up for MBF bouncers under gravity thing->flags6 &= ~MF6_INTRYMOVE; return false; } // Check for crossed portals bool portalcrossed; portalcrossed = false; while (true) { double bestfrac = 1.1; spechit_t besthit; int besthitnum; // find the portal nearest to the crossing actor for (unsigned i = 0; i < portalhit.Size();i++) { auto &spec = portalhit[i]; line_t *ld = spec.line; if (ld->frontsector->PortalGroup != thing->Sector->PortalGroup) continue; // must be in the same group to be considered valid. // see if the line was crossed oldside = P_PointOnLineSide(spec.Oldrefpos, ld); side = P_PointOnLineSide(spec.Refpos, ld); if (oldside == 0 && side == 1) { divline_t dl2 = { ld->v1->fX(), ld->v1->fY(), ld->Delta().X, ld->Delta().Y }; divline_t dl1 = { spec.Oldrefpos.X, spec.Oldrefpos.Y, spec.Refpos.X - spec.Oldrefpos.X, spec.Refpos.Y - spec.Oldrefpos.Y }; double frac = P_InterceptVector(&dl1, &dl2); if (frac < bestfrac) { besthit = spec; bestfrac = frac; besthitnum = i; } } } if (bestfrac < 1.1) { portalhit.Delete(besthitnum); line_t *ld = besthit.line; FLinePortal *port = ld->getPortal(); if (port->mType == PORTT_LINKED) { FLinkContext ctx; thing->UnlinkFromWorld(&ctx); thing->SetXY(tm.pos + port->mDisplacement); thing->Prev += port->mDisplacement; thing->LinkToWorld(&ctx); P_FindFloorCeiling(thing); portalcrossed = true; tm.portalstep = false; } else if (!portalcrossed) { DVector3 pos(tm.pos, thing->Z()); DVector3 oldthingpos = thing->Pos(); DVector2 thingpos = oldthingpos; P_TranslatePortalXY(ld, pos.X, pos.Y); P_TranslatePortalXY(ld, thingpos.X, thingpos.Y); P_TranslatePortalZ(ld, pos.Z); thing->SetXYZ(thingpos.X, thingpos.Y, pos.Z); if (!P_CheckPosition(thing, pos, true)) // check if some actor blocks us on the other side. (No line checks, because of the mess that'd create.) { thing->SetXYZ(oldthingpos); thing->flags6 &= ~MF6_INTRYMOVE; return false; } FLinkContext ctx; thing->UnlinkFromWorld(&ctx); thing->SetXYZ(pos); P_TranslatePortalVXVY(ld, thing->Vel.X, thing->Vel.Y); P_TranslatePortalAngle(ld, thing->Angles.Yaw); thing->LinkToWorld(&ctx); P_FindFloorCeiling(thing); thing->ClearInterpolation(); portalcrossed = true; tm.portalstep = false; } // if this is the current camera we need to store the point where the portal was crossed and the exit // so that the renderer can properly calculate an interpolated position along the movement path. if (thing == players[consoleplayer].camera) { divline_t dl1 = { besthit.Oldrefpos.X,besthit.Oldrefpos.Y, besthit.Refpos.X - besthit.Oldrefpos.X, besthit.Refpos.Y - besthit.Oldrefpos.Y }; DVector3a hit = { {dl1.x + dl1.dx * bestfrac, dl1.y + dl1.dy * bestfrac, 0.},0. }; R_AddInterpolationPoint(hit); if (port->mType == PORTT_LINKED) { hit.pos.X += port->mDisplacement.X; hit.pos.Y += port->mDisplacement.Y; } else { P_TranslatePortalXY(ld, hit.pos.X, hit.pos.Y); P_TranslatePortalZ(ld, hit.pos.Z); players[consoleplayer].viewz += hit.pos.Z; // needs to be done here because otherwise the renderer will not catch the change. P_TranslatePortalAngle(ld, hit.angle); } R_AddInterpolationPoint(hit); } if (port->mType == PORTT_LINKED) { continue; } } break; } if (!portalcrossed) { // the move is ok, so link the thing into its new position FLinkContext ctx; thing->UnlinkFromWorld(&ctx); oldsector = thing->Sector; thing->floorz = tm.floorz; thing->ceilingz= tm.ceilingz; thing->dropoffz = tm.dropoffz; // killough 11/98: keep track of dropoffs thing->floorpic = tm.floorpic; thing->floorterrain = tm.floorterrain; thing->floorsector = tm.floorsector; thing->ceilingpic = tm.ceilingpic; thing->ceilingsector = tm.ceilingsector; thing->SetXY(pos); thing->LinkToWorld(&ctx); } if (thing->flags2 & MF2_FLOORCLIP) { thing->AdjustFloorClip(); } // if any special lines were hit, do the effect if (!(thing->flags & (MF_TELEPORT | MF_NOCLIP))) { spechit_t spec; DVector2 lastpos = thing->Pos(); while (spechit.Pop(spec)) { line_t *ld = spec.line; // see if the line was crossed side = P_PointOnLineSide(spec.Refpos, ld); oldside = P_PointOnLineSide(spec.Oldrefpos, ld); if (side != oldside && ld->special && !(thing->flags6 & MF6_NOTRIGGER)) { if (thing->player && (thing->player->cheats & CF_PREDICTING)) { P_PredictLine(ld, thing, oldside, SPAC_Cross); } else if (thing->player) { P_ActivateLine(ld, thing, oldside, SPAC_Cross); } else if (thing->flags2 & MF2_MCROSS) { P_ActivateLine(ld, thing, oldside, SPAC_MCross); } else if (thing->flags2 & MF2_PCROSS) { P_ActivateLine(ld, thing, oldside, SPAC_PCross); } else if ((ld->special == Teleport || ld->special == Teleport_NoFog || ld->special == Teleport_Line)) { // [RH] Just a little hack for BOOM compatibility P_ActivateLine(ld, thing, oldside, SPAC_MCross); } else { P_ActivateLine(ld, thing, oldside, SPAC_AnyCross); } } } } // [RH] Don't activate anything if just predicting if (thing->player && (thing->player->cheats & CF_PREDICTING)) { thing->flags6 &= ~MF6_INTRYMOVE; return true; } // [RH] Check for crossing fake floor/ceiling newsec = thing->Sector; if (newsec->heightsec && oldsec->heightsec && newsec->SecActTarget) { const sector_t *hs = newsec->heightsec; double eyez = thing->Z() + viewheight; double fakez = hs->floorplane.ZatPoint(pos); if (!oldAboveFakeFloor && eyez > fakez) { // View went above fake floor newsec->TriggerSectorActions(thing, SECSPAC_EyesSurface); } else if (oldAboveFakeFloor && eyez <= fakez) { // View went below fake floor newsec->TriggerSectorActions(thing, SECSPAC_EyesDive); } if (!(hs->MoreFlags & SECF_FAKEFLOORONLY)) { fakez = hs->ceilingplane.ZatPoint(pos); if (!oldAboveFakeCeiling && eyez > fakez) { // View went above fake ceiling newsec->TriggerSectorActions(thing, SECSPAC_EyesAboveC); } else if (oldAboveFakeCeiling && eyez <= fakez) { // View went below fake ceiling newsec->TriggerSectorActions(thing, SECSPAC_EyesBelowC); } } } // If the actor stepped through a ceiling portal we need to reacquire the actual position info after the transition if (tm.portalstep) { FLinkContext ctx; DVector3 oldpos = thing->Pos(); thing->UnlinkFromWorld(&ctx); thing->SetXYZ(thing->PosRelative(tm.portalgroup)); thing->Prev += thing->Pos() - oldpos; thing->Sector = P_PointInSector(thing->Pos()); thing->PrevPortalGroup = thing->Sector->PortalGroup; thing->LinkToWorld(&ctx); P_FindFloorCeiling(thing); } // [RH] If changing sectors, trigger transitions thing->CheckSectorTransition(oldsec); thing->flags6 &= ~MF6_INTRYMOVE; return true; pushline: thing->flags6 &= ~MF6_INTRYMOVE; // [RH] Don't activate anything if just predicting if (thing->player && (thing->player->cheats & CF_PREDICTING)) { return false; } thing->SetZ(oldz); if (!(thing->flags&(MF_TELEPORT | MF_NOCLIP))) { int numSpecHitTemp; if (tm.thing->flags2 & MF2_BLASTED) { P_DamageMobj(tm.thing, NULL, NULL, tm.thing->Mass >> 5, NAME_Melee); } numSpecHitTemp = (int)spechit.Size(); while (numSpecHitTemp > 0) { // see which lines were pushed spechit_t &spec = spechit[--numSpecHitTemp]; side = P_PointOnLineSide(spec.Refpos, spec.line); CheckForPushSpecial(spec.line, side, thing, &spec.Refpos); } } return false; } bool P_TryMove(AActor *thing, const DVector2 &pos, int dropoff, // killough 3/15/98: allow dropoff as option const secplane_t *onfloor, bool missilecheck) // [RH] Let P_TryMove keep the thing on the floor { FCheckPosition tm; return P_TryMove(thing, pos, dropoff, onfloor, tm, missilecheck); } DEFINE_ACTION_FUNCTION(AActor, TryMove) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(x); PARAM_FLOAT(y); PARAM_INT(dropoff); PARAM_BOOL_DEF(missilecheck); PARAM_POINTER_DEF(tm, FCheckPosition); if (tm == nullptr) { ACTION_RETURN_BOOL(P_TryMove(self, DVector2(x, y), dropoff, nullptr, missilecheck)); } else { ACTION_RETURN_BOOL(P_TryMove(self, DVector2(x, y), dropoff, nullptr, *tm, missilecheck)); } } //========================================================================== // // P_CheckMove // Similar to P_TryMove but doesn't actually move the actor. Used for polyobject crushing // //========================================================================== static bool P_CheckMove(AActor *thing, const DVector2 &pos, FCheckPosition& tm, int flags) { double newz = thing->Z(); auto f1 = thing->flags & MF_PICKUP; thing->flags &= ~MF_PICKUP; auto res = P_CheckPosition(thing, pos, tm); thing->flags |= f1; if (!res) { // Ignore PCM_DROPOFF. Not necessary here: a little later it is. if (!flags || (!(flags & PCM_NOACTORS) && !(flags & PCM_NOLINES))) { return false; } if (!(flags & PCM_NOACTORS) && thing->BlockingMobj) { return false; } if (!(flags & PCM_NOLINES) && thing->BlockingLine) { return false; } } if (thing->flags3 & MF3_FLOORHUGGER) { newz = tm.floorz; } else if (thing->flags3 & MF3_CEILINGHUGGER) { newz = tm.ceilingz - thing->Height; } if (!(thing->flags & MF_NOCLIP)) { if (tm.ceilingz - tm.floorz < thing->Height) { return false; } if (!(thing->flags & MF_TELEPORT) && tm.ceilingz - newz < thing->Height && !(thing->flags3 & MF3_CEILINGHUGGER) && (!(thing->flags2 & MF2_FLY) || !(thing->flags & MF_NOGRAVITY))) { return false; } if (thing->flags2 & MF2_FLY && thing->flags & MF_NOGRAVITY) { if (thing->Top() > tm.ceilingz) return false; } if (!(thing->flags & MF_TELEPORT) && (!(thing->flags3 & MF3_FLOORHUGGER) || thing->flags5 & MF5_NODROPOFF)) { if (tm.floorz - newz > thing->MaxStepHeight) { // too big a step up return false; } else if ((thing->flags & MF_MISSILE) && !(thing->flags6 & MF6_STEPMISSILE) && tm.floorz > newz && !(thing->flags3 & MF3_FLOORHUGGER)) { // [RH] Don't let normal missiles climb steps return false; } else if (newz < tm.floorz) { // [RH] Check to make sure there's nothing in the way for the step up double savedz = thing->Z(); thing->SetZ(newz = tm.floorz); bool good = P_TestMobjZ(thing); thing->SetZ(savedz); if (!good) { return false; } } else if ((flags & PCM_DROPOFF) && !(thing->flags & (MF_FLOAT|MF_DROPOFF))) { if (newz - tm.dropoffz > thing->MaxDropOffHeight) { return false; } } } if (thing->flags2 & MF2_CANTLEAVEFLOORPIC && (tm.floorpic != thing->floorpic || tm.floorz - newz != 0)) { // must stay within a sector of a certain floor type return false; } } return true; } bool P_CheckMove(AActor *thing, const DVector2 &pos, int flags) { FCheckPosition tm; return P_CheckMove(thing, pos, tm, flags); } DEFINE_ACTION_FUNCTION(AActor, CheckMove) { PARAM_SELF_PROLOGUE(AActor); PARAM_FLOAT(x); PARAM_FLOAT(y); PARAM_INT_DEF(flags); PARAM_POINTER_DEF(tm, FCheckPosition); if (tm == nullptr) { ACTION_RETURN_BOOL(P_CheckMove(self, DVector2(x, y), flags)); } else { ACTION_RETURN_BOOL(P_CheckMove(self, DVector2(x, y), *tm, flags)); } } //========================================================================== // // SLIDE MOVE // Allows the player to slide along any angled walls. // //========================================================================== struct FSlide { double bestSlidefrac; double secondSlidefrac; line_t* bestslideline; line_t* secondslideline; AActor* slidemo; DVector2 tmmove; void HitSlideLine(line_t *ld); void SlideTraverse(const DVector2 &start, const DVector2 &end); void SlideMove(AActor *mo, DVector2 tryp, int numsteps); // The bouncing code uses the same data structure bool BounceTraverse(const DVector2 &start, const DVector2 &end); bool BounceWall(AActor *mo); }; //========================================================================== // // P_HitSlideLine // Adjusts the xmove / ymove // so that the next move will slide along the wall. // If the floor is icy, then you can bounce off a wall. // phares // //========================================================================== void FSlide::HitSlideLine(line_t* ld) { int side; DAngle lineangle; DAngle moveangle; DAngle deltaangle; double movelen; bool icyfloor; // is floor icy? // phares // | // Under icy conditions, if the angle of approach to the wall // V // is more than 45 degrees, then you'll bounce and lose half // your velocity. If less than 45 degrees, you'll slide along // the wall. 45 is arbitrary and is believable. // Check for the special cases of horz or vert walls. // killough 10/98: only bounce if hit hard (prevents wobbling) icyfloor = tmmove.LengthSquared() > 4*4 && var_friction && // killough 8/28/98: calc friction on demand slidemo->Z() <= slidemo->floorz && P_GetFriction(slidemo, NULL) > ORIG_FRICTION; if (ld->Delta().X == 0) { // ST_VERTICAL if (icyfloor && (fabs(tmmove.X) > fabs(tmmove.Y))) { tmmove.X = -tmmove.X / 2; tmmove.Y /= 2; // absorb half the velocity if (slidemo->player && slidemo->health > 0 && !(slidemo->player->cheats & CF_PREDICTING)) { S_Sound(slidemo, CHAN_VOICE, "*grunt", 1, ATTN_IDLE); // oooff!// ^ } } // | else // phares tmmove.X = 0; // no more movement in the X direction return; } if (ld->Delta().Y == 0) { // ST_HORIZONTAL if (icyfloor && (fabs(tmmove.Y) > fabs(tmmove.X))) { tmmove.X /= 2; // absorb half the velocity tmmove.Y = -tmmove.Y / 2; if (slidemo->player && slidemo->health > 0 && !(slidemo->player->cheats & CF_PREDICTING)) { S_Sound(slidemo, CHAN_VOICE, "*grunt", 1, ATTN_IDLE); // oooff! } } else tmmove.Y = 0; // no more movement in the Y direction return; } // The wall is angled. Bounce if the angle of approach is // phares // less than 45 degrees. // phares DVector3 pos = slidemo->PosRelative(ld); side = P_PointOnLineSide(pos, ld); lineangle = ld->Delta().Angle(); if (side == 1) lineangle += 180.; moveangle = tmmove.Angle(); // prevents sudden path reversal due to rounding error | // phares moveangle += 3600/65536.*65536.; // Boom added 10 to the angle here. deltaangle = ::deltaangle(lineangle, moveangle); // V movelen = tmmove.Length(); if (icyfloor && (deltaangle > 45) && (deltaangle < 135)) { moveangle = ::deltaangle(deltaangle, lineangle); movelen /= 2; // absorb if (slidemo->player && slidemo->health > 0 && !(slidemo->player->cheats & CF_PREDICTING)) { S_Sound(slidemo, CHAN_VOICE, "*grunt", 1, ATTN_IDLE); // oooff! } tmmove = moveangle.ToVector(movelen); } else { // The compatibility option that used to be here had to be removed because // with floating point math it was no longer possible to reproduce. #if 0 // with full precision this should work now. Needs some testing if (deltaangle < 0) deltaangle += 180.; tmmove = lineangle.ToVector(movelen * deltaangle.Cos()); #else divline_t dll, dlv; double inter1, inter2, inter3; P_MakeDivline(ld, &dll); dlv.x = pos.X; dlv.y = pos.Y; dlv.dx = dll.dy; dlv.dy = -dll.dx; inter1 = P_InterceptVector(&dll, &dlv); dlv.dx = tmmove.X; dlv.dy = tmmove.Y; inter2 = P_InterceptVector(&dll, &dlv); inter3 = P_InterceptVector(&dlv, &dll); if (inter3 != 0) { tmmove.X = (inter2 - inter1) * dll.dx / inter3; tmmove.Y = (inter2 - inter1) * dll.dy / inter3; } else { tmmove.Zero(); } #endif } // phares } //========================================================================== // // PTR_SlideTraverse // //========================================================================== void FSlide::SlideTraverse(const DVector2 &start, const DVector2 &end) { FLineOpening open; FPathTraverse it(start.X, start.Y, end.X, end.Y, PT_ADDLINES); intercept_t *in; while ((in = it.Next())) { line_t* li; if (!in->isaline) { // should never happen Printf("PTR_SlideTraverse: not a line?"); continue; } li = in->d.line; if (!(li->flags & ML_TWOSIDED) || !li->backsector) { DVector3 pos = slidemo->PosRelative(li); if (P_PointOnLineSide(pos, li)) { // don't hit the back side continue; } goto isblocking; } if (li->flags & (ML_BLOCKING | ML_BLOCKEVERYTHING)) { goto isblocking; } if (li->flags & ML_BLOCK_PLAYERS && slidemo->player != NULL) { goto isblocking; } if (li->flags & ML_BLOCKMONSTERS && !((slidemo->flags3 & MF3_NOBLOCKMONST) || ((i_compatflags & COMPATF_NOBLOCKFRIENDS) && (slidemo->flags & MF_FRIENDLY)))) { goto isblocking; } // set openrange, opentop, openbottom P_LineOpening(open, slidemo, li, it.InterceptPoint(in)); if (open.range < slidemo->Height) goto isblocking; // doesn't fit if (open.top < slidemo->Top()) goto isblocking; // mobj is too high if (open.bottom - slidemo->Z() > slidemo->MaxStepHeight) { goto isblocking; // too big a step up } else if (slidemo->Z() < open.bottom) { // [RH] Check to make sure there's nothing in the way for the step up double savedz = slidemo->Z(); slidemo->SetZ(open.bottom); bool good = P_TestMobjZ(slidemo); slidemo->SetZ(savedz); if (!good) { goto isblocking; } } // this line doesn't block movement continue; // the line does block movement, // see if it is closer than best so far isblocking: if (in->frac < bestSlidefrac) { secondSlidefrac = bestSlidefrac; secondslideline = bestslideline; bestSlidefrac = in->frac; bestslideline = li; } return; // stop } } //========================================================================== // // P_SlideMove // // The vel.x / vel.y move is bad, so try to slide along a wall. // // Find the first line hit, move flush to it, and slide along it // // This is a kludgy mess. // //========================================================================== void FSlide::SlideMove(AActor *mo, DVector2 tryp, int numsteps) { DVector2 lead; DVector2 trail; DVector2 newpos; DVector2 move; const secplane_t * walkplane; int hitcount; hitcount = 3; slidemo = mo; if (mo->player && mo->player->mo == mo && mo->reactiontime > 0) return; // player coming right out of a teleporter. retry: if (!--hitcount) goto stairstep; // don't loop forever // trace along the three leading corners if (tryp.X > 0) { lead.X = mo->X() + mo->radius; trail.X = mo->X() - mo->radius; } else { lead.X = mo->X() - mo->radius; trail.X = mo->X() + mo->radius; } if (tryp.Y > 0) { lead.Y = mo->Y() + mo->radius; trail.Y = mo->Y() - mo->radius; } else { lead.Y = mo->Y() - mo->radius; trail.Y = mo->Y() + mo->radius; } bestSlidefrac = 1.01; SlideTraverse(lead, lead + tryp); SlideTraverse(DVector2(trail.X, lead.Y), tryp + DVector2(trail.X, lead.Y)); SlideTraverse(DVector2(lead.X, trail.Y), tryp + DVector2(lead.X, trail.Y)); // move up to the wall if (bestSlidefrac > 1) { // the move must have hit the middle, so stairstep stairstep: // killough 3/15/98: Allow objects to drop off ledges move = { 0, tryp.Y }; walkplane = P_CheckSlopeWalk(mo, move); if (!P_TryMove(mo, mo->Pos() + move, true, walkplane)) { move = { tryp.X, 0 }; walkplane = P_CheckSlopeWalk(mo, move); P_TryMove(mo, mo->Pos() + move, true, walkplane); } return; } // fudge a bit to make sure it doesn't hit bestSlidefrac -= 1. / 32; if (bestSlidefrac > 0) { newpos = tryp * bestSlidefrac; // [BL] We need to abandon this function if we end up going through a teleporter const DVector2 startvel = mo->Vel.XY(); // killough 3/15/98: Allow objects to drop off ledges if (!P_TryMove(mo, mo->Pos() + newpos, true)) goto stairstep; if (mo->Vel.XY() != startvel) return; } // Now continue along the wall. bestSlidefrac = 1. - (bestSlidefrac + 1. / 32); // remainder if (bestSlidefrac > 1) bestSlidefrac = 1; else if (bestSlidefrac <= 0) return; tryp = tmmove = tryp*bestSlidefrac; HitSlideLine(bestslideline); // clip the moves mo->Vel.X = tmmove.X * numsteps; mo->Vel.Y = tmmove.Y * numsteps; // killough 10/98: affect the bobbing the same way (but not voodoo dolls) if (mo->player && mo->player->mo == mo) { if (fabs(mo->player->Vel.X) > fabs(mo->Vel.X)) mo->player->Vel.X = mo->Vel.X; if (fabs(mo->player->Vel.Y) > fabs(mo->Vel.Y)) mo->player->Vel.Y = mo->Vel.Y; } walkplane = P_CheckSlopeWalk(mo, tmmove); // killough 3/15/98: Allow objects to drop off ledges if (!P_TryMove(mo, mo->Pos() + tmmove, true, walkplane)) { goto retry; } } void P_SlideMove(AActor *mo, const DVector2 &pos, int numsteps) { FSlide slide; slide.SlideMove(mo, pos, numsteps); } //============================================================================ // // P_CheckSlopeWalk // //============================================================================ const secplane_t * P_CheckSlopeWalk(AActor *actor, DVector2 &move) { static secplane_t copyplane; if (actor->flags & MF_NOGRAVITY) { return NULL; } DVector3 pos = actor->PosRelative(actor->floorsector); const secplane_t *plane = &actor->floorsector->floorplane; double planezhere = plane->ZatPoint(pos); for (auto rover : actor->floorsector->e->XFloor.ffloors) { if (!(rover->flags & FF_SOLID) || !(rover->flags & FF_EXISTS)) continue; double thisplanez = rover->top.plane->ZatPoint(pos); if (thisplanez > planezhere && thisplanez <= actor->Z() + actor->MaxStepHeight) { copyplane = *rover->top.plane; if (copyplane.fC() < 0) copyplane.FlipVert(); plane = ©plane; planezhere = thisplanez; } } if (actor->floorsector != actor->Sector) { for (auto rover : actor->Sector->e->XFloor.ffloors) { if (!(rover->flags & FF_SOLID) || !(rover->flags & FF_EXISTS)) continue; double thisplanez = rover->top.plane->ZatPoint(actor); if (thisplanez > planezhere && thisplanez <= actor->Z() + actor->MaxStepHeight) { copyplane = *rover->top.plane; if (copyplane.fC() < 0) copyplane.FlipVert(); plane = ©plane; planezhere = thisplanez; } } } if (actor->floorsector != actor->Sector) { // this additional check prevents sliding on sloped dropoffs if (planezhere>actor->floorz + 4) return NULL; } if (actor->Z() - planezhere > 1) { // not on floor return NULL; } if (plane->isSlope()) { DVector2 dest; double t; dest = actor->Pos() + move; t = (plane->Normal() | DVector3(dest, actor->Z())) + plane->fD(); if (t < 0) { // Desired location is behind (below) the plane // (i.e. Walking up the plane) if (plane->fC() < STEEPSLOPE) { // Can't climb up slopes of ~45 degrees or more if (actor->flags & MF_NOCLIP) { return (actor->floorsector == actor->Sector) ? plane : NULL; } else { const msecnode_t *node; bool dopush = true; if (plane->fC() > STEEPSLOPE * 2 / 3) { for (node = actor->touching_sectorlist; node; node = node->m_tnext) { sector_t *sec = node->m_sector; if (sec->floorplane.fC() >= STEEPSLOPE) { DVector3 pos = actor->PosRelative(sec) +move; if (sec->floorplane.ZatPoint(pos) >= actor->Z() - actor->MaxStepHeight) { dopush = false; break; } } } } if (dopush) { move = plane->Normal() * 2; actor->Vel.X = move.X; actor->Vel.Y = move.Y; } return (actor->floorsector == actor->Sector) ? plane : NULL; } } // Slide the desired location along the plane's normal // so that it lies on the plane's surface dest -= plane->Normal() * t; move = dest - actor->Pos().XY(); return (actor->floorsector == actor->Sector) ? plane : NULL; } else if (t > 0) { // Desired location is in front of (above) the plane if (actor->isAtZ(planezhere)) // it is very important not to be too precise here. { // Actor's current spot is on/in the plane, so walk down it // Same principle as walking up, except reversed dest += plane->Normal() * t; move = dest - actor->Pos().XY(); return (actor->floorsector == actor->Sector) ? plane : NULL; } } } return NULL; } //============================================================================ // // PTR_BounceTraverse // //============================================================================ bool FSlide::BounceTraverse(const DVector2 &start, const DVector2 &end) { FLineOpening open; FPathTraverse it(start.X, start.Y, end.X, end.Y, PT_ADDLINES); intercept_t *in; while ((in = it.Next())) { line_t *li; if (!in->isaline) { Printf("PTR_BounceTraverse: not a line?"); continue; } li = in->d.line; if (li->flags & ML_BLOCKEVERYTHING) { goto bounceblocking; } if (!(li->flags&ML_TWOSIDED) || !li->backsector) { if (P_PointOnLineSide(slidemo->Pos(), li)) continue; // don't hit the back side goto bounceblocking; } P_LineOpening(open, slidemo, li, it.InterceptPoint(in)); // set openrange, opentop, openbottom if (open.range < slidemo->Height) goto bounceblocking; // doesn't fit if (open.top < slidemo->Top()) goto bounceblocking; // mobj is too high if (open.bottom > slidemo->Z()) goto bounceblocking; // mobj is too low continue; // this line doesn't block movement // the line does block movement, see if it is closer than best so far bounceblocking: if (in->frac < bestSlidefrac) { secondSlidefrac = bestSlidefrac; secondslideline = bestslideline; bestSlidefrac = in->frac; bestslideline = li; } return false; // stop } return true; } //============================================================================ // // P_BounceWall // //============================================================================ bool FSlide::BounceWall(AActor *mo) { DVector2 lead; int side; DAngle lineangle, moveangle, deltaangle; double movelen; line_t *line; if (!(mo->BounceFlags & BOUNCE_Walls)) { return false; } slidemo = mo; // // trace along the three leading corners // if (mo->Vel.X > 0) { lead.X = mo->X() + mo->radius; } else { lead.X = mo->X() - mo->radius; } if (mo->Vel.Y > 0) { lead.Y = mo->Y() + mo->radius; } else { lead.Y = mo->Y() - mo->radius; } bestSlidefrac = 1.01; bestslideline = mo->BlockingLine; if (BounceTraverse(lead, lead+mo->Vel.XY()) && mo->BlockingLine == NULL) { // Could not find a wall, so bounce off the floor/ceiling instead. double floordist = mo->Z() - mo->floorz; double ceildist = mo->ceilingz - mo->Z(); if (floordist <= ceildist) { mo->FloorBounceMissile(mo->Sector->floorplane); return true; } else { mo->FloorBounceMissile(mo->Sector->ceilingplane); return true; } } line = bestslideline; if (line->special == Line_Horizon) { mo->SeeSound = 0; // it might make a sound otherwise mo->Destroy(); return true; } // The amount of bounces is limited if (mo->bouncecount>0 && --mo->bouncecount == 0) { if (mo->flags & MF_MISSILE) P_ExplodeMissile(mo, line, NULL); else mo->CallDie(NULL, NULL); return true; } side = P_PointOnLineSide(mo->Pos(), line); lineangle = line->Delta().Angle(); if (side == 1) { lineangle += 180; } moveangle = mo->Vel.Angle(); deltaangle = (lineangle * 2) - moveangle; mo->Angles.Yaw = deltaangle; movelen = mo->Vel.XY().Length() * mo->wallbouncefactor; FBoundingBox box(mo->X(), mo->Y(), mo->radius); if (box.BoxOnLineSide(line) == -1) { DVector2 ofs = deltaangle.ToVector(mo->radius); DVector3 pos = mo->Vec3Offset(ofs.X, ofs.Y, 0.); mo->SetOrigin(pos, true); } if (movelen < 1) { movelen = 2; } DVector2 vel = deltaangle.ToVector(movelen); mo->Vel.X = vel.X; mo->Vel.Y = vel.Y; if (mo->BounceFlags & BOUNCE_UseBounceState) { FState *bouncestate = mo->FindState(NAME_Bounce, NAME_Wall); if (bouncestate != NULL) { mo->SetState(bouncestate); } } return true; } bool P_BounceWall(AActor *mo) { FSlide slide; return slide.BounceWall(mo); } //========================================================================== // // // //========================================================================== extern FRandom pr_bounce; bool P_BounceActor(AActor *mo, AActor *BlockingMobj, bool ontop) { //Don't go through all of this if the actor is reflective and wants things to pass through them. if (BlockingMobj && ((BlockingMobj->flags2 & MF2_REFLECTIVE) && (BlockingMobj->flags7 & MF7_THRUREFLECT))) return true; if (mo && BlockingMobj && ((mo->BounceFlags & BOUNCE_AllActors) || ((mo->flags & MF_MISSILE) && (!(mo->flags2 & MF2_RIP) || (BlockingMobj->flags5 & MF5_DONTRIP) || ((mo->flags6 & MF6_NOBOSSRIP) && (BlockingMobj->flags2 & MF2_BOSS))) && (BlockingMobj->flags2 & MF2_REFLECTIVE)) || ((BlockingMobj->player == NULL) && (!(BlockingMobj->flags3 & MF3_ISMONSTER))))) { // Rippers should not bounce off shootable actors, since they rip through them. if ((mo->flags & MF_MISSILE) && (mo->flags2 & MF2_RIP) && BlockingMobj->flags & MF_SHOOTABLE) return true; if (BlockingMobj->flags & MF_SHOOTABLE && mo->BounceFlags & BOUNCE_NotOnShootables) { mo->bouncecount = 1; // let it explode now. } if (mo->bouncecount>0 && --mo->bouncecount == 0) { if (mo->flags & MF_MISSILE) P_ExplodeMissile(mo, nullptr, BlockingMobj); else mo->CallDie(BlockingMobj, nullptr); return true; } if (mo->flags7 & MF7_HITTARGET) mo->target = BlockingMobj; if (mo->flags7 & MF7_HITMASTER) mo->master = BlockingMobj; if (mo->flags7 & MF7_HITTRACER) mo->tracer = BlockingMobj; if (!ontop) { DAngle angle = BlockingMobj->AngleTo(mo) + ((pr_bounce() % 16) - 8); double speed = mo->VelXYToSpeed() * mo->wallbouncefactor; // [GZ] was 0.75, using wallbouncefactor seems more consistent if (fabs(speed) < EQUAL_EPSILON) speed = 0; mo->Angles.Yaw = angle; mo->VelFromAngle(speed); mo->PlayBounceSound(true); if (mo->BounceFlags & BOUNCE_UseBounceState) { FName names[] = { NAME_Bounce, NAME_Actor, NAME_Creature }; FState *bouncestate; int count = 2; if ((BlockingMobj->flags & MF_SHOOTABLE) && !(BlockingMobj->flags & MF_NOBLOOD)) { count = 3; } bouncestate = mo->FindState(count, names); if (bouncestate != NULL) { mo->SetState(bouncestate); } } } else { double dot = mo->Vel.Z; if (mo->BounceFlags & (BOUNCE_HereticType | BOUNCE_MBF)) { mo->Vel.Z -= 2. * dot; if (!(mo->BounceFlags & BOUNCE_MBF)) // Heretic projectiles die, MBF projectiles don't. { mo->flags |= MF_INBOUNCE; mo->SetState(mo->FindState(NAME_Death)); mo->flags &= ~MF_INBOUNCE; return false; } else { mo->Vel.Z *= mo->bouncefactor; } } else // Don't run through this for MBF-style bounces { // The reflected velocity keeps only about 70% of its original speed mo->Vel.Z = (mo->Vel.Z - 2. * dot) * mo->bouncefactor; } mo->PlayBounceSound(true); if (mo->BounceFlags & BOUNCE_MBF) // Bring it to rest below a certain speed { if (fabs(mo->Vel.Z) < mo->Mass * mo->GetGravity() / 64) mo->Vel.Z = 0; } else if (mo->BounceFlags & (BOUNCE_AutoOff | BOUNCE_AutoOffFloorOnly)) { if (!(mo->flags & MF_NOGRAVITY) && (mo->Vel.Z < 3.)) mo->BounceFlags &= ~BOUNCE_TypeMask; } } return true; } return false; } //============================================================================ // // Aiming // //============================================================================ CVAR(Bool, aimdebug, false, 0) struct AimTarget : public FTranslatedLineTarget { DAngle pitch; double frac; void Clear() { memset(this, 0, sizeof(*this)); frac = FLT_MAX; } }; struct aim_t { enum { aim_up = 1, aim_down = 2 }; DAngle aimpitch; double attackrange; double shootz; // Height if not aiming up or down double limitz; // height limit for portals to avoid bad setups AActor* shootthing; AActor* friender; // actor to check friendliness again AActor* aimtarget; // if we want to aim at precisely this target. DAngle toppitch, bottompitch; AimTarget linetarget; AimTarget thing_friend, thing_other; int flags; sector_t * lastsector; secplane_t * lastfloorplane; secplane_t * lastceilingplane; int aimdir; DVector3 startpos; DVector2 aimtrace; double startfrac; bool crossedffloors; bool unlinked; // Creates a clone of this structure with the basic info copied. aim_t Clone() { aim_t cloned; cloned.aimtrace = aimtrace; cloned.aimpitch = aimpitch; cloned.aimtarget = aimtarget; cloned.attackrange = attackrange; cloned.shootthing = shootthing; cloned.friender = friender; cloned.shootz = shootz; cloned.unlinked = unlinked; cloned.flags = flags; return cloned; } //============================================================================ // // SetResult // //============================================================================ void SetResult(AimTarget &res, double frac, AActor *th, DAngle pitch) { if (res.frac > frac) { res.linetarget = th; res.pitch = pitch; res.angleFromSource = (th->Pos() - startpos).Angle(); res.attackAngleFromSource = res.angleFromSource; // at this point we do not have an attack angle so it's the same as the actual angle between actors. res.unlinked = unlinked; res.frac = frac; } } void SetResult(AimTarget &res, AimTarget &set) { if (res.frac > set.frac) { res = set; } } //============================================================================ // // Result // //============================================================================ AimTarget *Result() { AimTarget *result = &linetarget; if (result->linetarget == NULL) { if (thing_other.linetarget != NULL) { result = &thing_other; } else if (thing_friend.linetarget != NULL) { result = &thing_friend; } } return result; } //============================================================================ // // AimTraverse3DFloors // //============================================================================ bool AimTraverse3DFloors(const divline_t &trace, intercept_t * in, int frontflag, int *planestocheck) { sector_t * nextsector; secplane_t * nexttopplane, *nextbottomplane; line_t * li = in->d.line; nextsector = NULL; nexttopplane = nextbottomplane = NULL; *planestocheck = aimdir; if (li->backsector == NULL) return true; // shouldn't really happen but crashed once for me... if (li->frontsector->e->XFloor.ffloors.Size() || li->backsector->e->XFloor.ffloors.Size()) { F3DFloor* rover; DAngle highpitch, lowpitch; double trX = trace.x + trace.dx * in->frac; double trY = trace.y + trace.dy * in->frac; double dist = attackrange * in->frac; // 3D floor check. This is not 100% accurate but normally sufficient when // combined with a final sight check for (int i = 1; i <= 2; i++) { sector_t * s = i == 1 ? li->frontsector : li->backsector; for (unsigned k = 0; k < s->e->XFloor.ffloors.Size(); k++) { crossedffloors = true; rover = s->e->XFloor.ffloors[k]; if ((rover->flags & FF_SHOOTTHROUGH) || !(rover->flags & FF_EXISTS)) continue; double ff_bottom = rover->bottom.plane->ZatPoint(trX, trY); double ff_top = rover->top.plane->ZatPoint(trX, trY); highpitch = -VecToAngle(dist, ff_top - shootz); lowpitch = -VecToAngle(dist, ff_bottom - shootz); if (highpitch <= toppitch) { // blocks completely if (lowpitch >= bottompitch) return false; // blocks upper edge of view if (lowpitch > toppitch) { toppitch = lowpitch; if (frontflag != i - 1) { nexttopplane = rover->bottom.plane; *planestocheck &= ~aim_up; } } } else if (lowpitch >= bottompitch) { // blocks lower edge of view if (highpitch < bottompitch) { bottompitch = highpitch; if (frontflag != i - 1) { nextbottomplane = rover->top.plane; *planestocheck &= ~aim_down; } } } // trace is leaving a sector with a 3d-floor if (frontflag == i - 1) { if (s == lastsector) { // upper slope intersects with this 3d-floor if (rover->bottom.plane == lastceilingplane && lowpitch > toppitch) { toppitch = lowpitch; } // lower slope intersects with this 3d-floor if (rover->top.plane == lastfloorplane && highpitch < bottompitch) { bottompitch = highpitch; } } } if (toppitch >= bottompitch) return false; // stop } } } lastsector = nextsector; lastceilingplane = nexttopplane; lastfloorplane = nextbottomplane; return true; } //============================================================================ // // traverses a sector portal // //============================================================================ void EnterSectorPortal(int position, double frac, sector_t *entersec, DAngle newtoppitch, DAngle newbottompitch) { double portalz = entersec->GetPortalPlaneZ(position); if (position == sector_t::ceiling && portalz < limitz) return; else if (position == sector_t::floor && portalz > limitz) return; aim_t newtrace = Clone(); newtrace.toppitch = newtoppitch; newtrace.bottompitch = newbottompitch; newtrace.aimdir = position == sector_t::ceiling? aim_t::aim_up : aim_t::aim_down; newtrace.startpos = startpos + entersec->GetPortalDisplacement(position); newtrace.startfrac = frac + 1. / attackrange; // this is to skip the transition line to the portal which would produce a bogus opening newtrace.lastsector = P_PointInSector(newtrace.startpos + aimtrace * newtrace.startfrac); newtrace.limitz = portalz; if (aimdebug) Printf("-----Entering %s portal from sector %d to sector %d\n", position ? "ceiling" : "floor", lastsector->sectornum, newtrace.lastsector->sectornum); newtrace.AimTraverse(); SetResult(linetarget, newtrace.linetarget); SetResult(thing_friend, newtrace.thing_friend); SetResult(thing_other, newtrace.thing_other); if (aimdebug) Printf("-----Exiting %s portal\n", position ? "ceiling" : "floor"); } //============================================================================ // // traverses a line portal // //============================================================================ void EnterLinePortal(line_t *li, double frac) { aim_t newtrace = Clone(); FLinePortal *port = li->getPortal(); if (port->mType != PORTT_LINKED && (flags & ALF_PORTALRESTRICT)) return; newtrace.toppitch = toppitch; newtrace.bottompitch = bottompitch; newtrace.aimdir = aimdir; newtrace.unlinked = (port->mType != PORTT_LINKED); newtrace.startpos = startpos; newtrace.aimtrace = aimtrace; P_TranslatePortalXY(li, newtrace.startpos.X, newtrace.startpos.Y); P_TranslatePortalZ(li, newtrace.startpos.Z); P_TranslatePortalVXVY(li, newtrace.aimtrace.X, newtrace.aimtrace.Y); newtrace.startfrac = frac + 1 / attackrange; // this is to skip the transition line to the portal which would produce a bogus opening DVector2 pos = newtrace.startpos + newtrace.aimtrace * newtrace.startfrac; newtrace.lastsector = P_PointInSector(pos); P_TranslatePortalZ(li, limitz); if (aimdebug) Printf("-----Entering line portal from sector %d to sector %d\n", lastsector->sectornum, newtrace.lastsector->sectornum); newtrace.AimTraverse(); SetResult(linetarget, newtrace.linetarget); SetResult(thing_friend, newtrace.thing_friend); SetResult(thing_other, newtrace.thing_other); } //============================================================================ // // PTR_AimTraverse // Sets linetaget and aimpitch when a target is aimed at. // //============================================================================ void AimTraverse() { // for smart aiming linetarget.Clear(); thing_friend.Clear(); thing_other.Clear(); crossedffloors = lastsector->e->XFloor.ffloors.Size() != 0; lastfloorplane = lastceilingplane = NULL; // check the initial sector for 3D-floors and portals bool ceilingportalstate = (aimdir & aim_t::aim_up) && toppitch < 0 && !lastsector->PortalBlocksMovement(sector_t::ceiling); bool floorportalstate = (aimdir & aim_t::aim_down) && bottompitch > 0 && !lastsector->PortalBlocksMovement(sector_t::floor); for (auto rover : lastsector->e->XFloor.ffloors) { if ((rover->flags & FF_SHOOTTHROUGH) || !(rover->flags & FF_EXISTS)) continue; double bottomz = rover->bottom.plane->ZatPoint(startpos); if (bottomz >= startpos.Z + shootthing->Height) { lastceilingplane = rover->bottom.plane; // no ceiling portal if below a 3D floor ceilingportalstate = false; } bottomz = rover->top.plane->ZatPoint(startpos); if (bottomz <= startpos.Z) { lastfloorplane = rover->top.plane; // no floor portal if above a 3D floor floorportalstate = false; } } if (ceilingportalstate) EnterSectorPortal(sector_t::ceiling, 0, lastsector, toppitch, MIN(0., bottompitch)); if (floorportalstate) EnterSectorPortal(sector_t::floor, 0, lastsector, MAX(0., toppitch), bottompitch); FPathTraverse it(startpos.X, startpos.Y, aimtrace.X, aimtrace.Y, PT_ADDLINES | PT_ADDTHINGS | PT_COMPATIBLE | PT_DELTA, startfrac); intercept_t *in; if (aimdebug) Printf("Start AimTraverse, start = %f,%f,%f, vect = %f,%f\n", startpos.X / 65536., startpos.Y / 65536., startpos.Z / 65536., aimtrace.X / 65536., aimtrace.Y / 65536.); while ((in = it.Next())) { line_t* li; AActor* th; DAngle pitch; DAngle thingtoppitch; DAngle thingbottompitch; double dist; DAngle thingpitch; if (linetarget.linetarget != NULL && in->frac > linetarget.frac) return; // we already found something better in another portal section. if (in->isaline) { li = in->d.line; int frontflag = P_PointOnLineSidePrecise(startpos, li); if (aimdebug) Printf("Found line %d: toppitch = %f, bottompitch = %f\n", li->Index(), toppitch.Degrees, bottompitch.Degrees); if (li->isLinePortal() && frontflag == 0) { EnterLinePortal(li, in->frac); return; } if (!(li->flags & ML_TWOSIDED) || (li->flags & ML_BLOCKEVERYTHING)) return; // stop // Crosses a two sided line. // A two sided line will restrict the possible target ranges. FLineOpening open; P_LineOpening(open, NULL, li, it.InterceptPoint(in), (DVector2*)nullptr, FFCF_NODROPOFF); // The following code assumes that portals on the front of the line have already been processed. if (open.range <= 0 || open.bottom >= open.top) return; dist = attackrange * in->frac; if (open.bottom != LINEOPEN_MIN) { pitch = -VecToAngle(dist, open.bottom - shootz); if (pitch < bottompitch) bottompitch = pitch; } if (open.top != LINEOPEN_MAX) { pitch = -VecToAngle(dist, open.top - shootz); if (pitch > toppitch) toppitch = pitch; } if (toppitch >= bottompitch) return; int planestocheck; if (!AimTraverse3DFloors(it.Trace(), in, frontflag, &planestocheck)) return; if (aimdebug) Printf("After line %d: toppitch = %f, bottompitch = %f, planestocheck = %d\n", li->Index(), toppitch.Degrees, bottompitch.Degrees, planestocheck); sector_t *entersec = frontflag ? li->frontsector : li->backsector; sector_t *exitsec = frontflag ? li->backsector : li->frontsector; lastsector = entersec; // check portal in backsector when aiming up/downward is possible, the line doesn't have portals on both sides and there's actually a portal in the backsector if ((planestocheck & aim_up) && toppitch < 0 && open.top != LINEOPEN_MAX && !entersec->PortalBlocksMovement(sector_t::ceiling)) { EnterSectorPortal(sector_t::ceiling, in->frac, entersec, toppitch, MIN(0., bottompitch)); } if ((planestocheck & aim_down) && bottompitch > 0 && open.bottom != LINEOPEN_MIN && !entersec->PortalBlocksMovement(sector_t::floor)) { EnterSectorPortal(sector_t::floor, in->frac, entersec, MAX(0., toppitch), bottompitch); } continue; // shot continues } // shoot a thing th = in->d.thing; if (th == shootthing) continue; // can't shoot self if (aimtarget != NULL && th != aimtarget) continue; // only care about target, and you're not it // If we want to start a conversation anything that has one should be // found, regardless of other settings. if (!(flags & ALF_CHECKCONVERSATION) || th->Conversation == NULL) { if (!(flags & ALF_CHECKNONSHOOTABLE)) // For info CCMD, ignore stuff about GHOST and SHOOTABLE flags { if (!(th->flags&MF_SHOOTABLE)) continue; // corpse or something // check for physical attacks on a ghost if ((th->flags3 & MF3_GHOST) && shootthing->player && // [RH] Be sure shootthing is a player shootthing->player->ReadyWeapon && (shootthing->player->ReadyWeapon->flags2 & MF2_THRUGHOST)) { continue; } } } dist = attackrange * in->frac; // Don't autoaim certain special actors if (!cl_doautoaim && th->flags6 & MF6_NOTAUTOAIMED) { continue; } // we must do one last check whether the trace has crossed a 3D floor if (lastsector == th->Sector && th->Sector->e->XFloor.ffloors.Size()) { if (lastceilingplane) { double ff_top = lastceilingplane->ZatPoint(th); DAngle pitch = -VecToAngle(dist, ff_top - shootz); // upper slope intersects with this 3d-floor if (pitch > toppitch) { toppitch = pitch; } } if (lastfloorplane) { double ff_bottom = lastfloorplane->ZatPoint(th); DAngle pitch = -VecToAngle(dist, ff_bottom - shootz); // lower slope intersects with this 3d-floor if (pitch < bottompitch) { bottompitch = pitch; } } } // check angles to see if the thing can be aimed at thingtoppitch = -VecToAngle(dist, th->Top() - shootz); if (thingtoppitch > bottompitch) continue; // shot over the thing thingbottompitch = -VecToAngle(dist, th->Z() - shootz); if (thingbottompitch < toppitch) continue; // shot under the thing if (crossedffloors) { // if 3D floors were in the way do an extra visibility check for safety if (!unlinked && !P_CheckSight(shootthing, th, SF_IGNOREVISIBILITY | SF_IGNOREWATERBOUNDARY)) { // the thing can't be seen so we can safely exclude its range from our aiming field if (thingtoppitch < toppitch) { if (thingbottompitch > toppitch) toppitch = thingbottompitch; } else if (thingbottompitch>bottompitch) { if (thingtoppitch < bottompitch) bottompitch = thingtoppitch; } if (toppitch < bottompitch) continue; else return; } } // this thing can be hit! if (thingtoppitch < toppitch) thingtoppitch = toppitch; if (thingbottompitch > bottompitch) thingbottompitch = bottompitch; thingpitch = thingtoppitch / 2 + thingbottompitch / 2; if (flags & ALF_CHECK3D) { // We need to do a 3D distance check here because this is nearly always used in // combination with P_LineAttack. P_LineAttack uses 3D distance but FPathTraverse // only 2D. This causes some problems with Hexen's weapons that use different // attack modes based on distance to target double cosine = thingpitch.Cos(); if (cosine != 0) { double tracelen = DVector2(it.Trace().dx, it.Trace().dy).Length(); double d3 = tracelen * in->frac / cosine; if (d3 > attackrange) { return; } } } if ((flags & ALF_NOFRIENDS) && th->IsFriend(friender) && aimtarget == NULL) { continue; } else if (sv_smartaim != 0 && !(flags & ALF_FORCENOSMART) && aimtarget == NULL) { // try to be a little smarter about what to aim at! // In particular avoid autoaiming at friends and barrels. if (th->IsFriend(friender)) { if (sv_smartaim < 2) { // friends don't aim at friends (except players), at least not first if (aimdebug) Printf("Hit friend %s at %f,%f,%f\n", th->GetClass()->TypeName.GetChars(), th->X(), th->Y(), th->Z()); SetResult(thing_friend, in->frac, th, thingpitch); } } else if (!(th->flags3 & MF3_ISMONSTER) && th->player == NULL) { if (sv_smartaim < 3) { // don't autoaim at barrels and other shootable stuff unless no monsters have been found if (aimdebug) Printf("Hit other %s at %f,%f,%f\n", th->GetClass()->TypeName.GetChars(), th->X(), th->Y(), th->Z()); SetResult(thing_other, in->frac, th, thingpitch); } } else { if (aimdebug) Printf("Hit target %s at %f,%f,%f\n", th->GetClass()->TypeName.GetChars(), th->X(), th->Y(), th->Z()); SetResult(linetarget, in->frac, th, thingpitch); return; } } else { if (aimdebug) Printf("Hit target %s at %f,%f,%f\n", th->GetClass()->TypeName.GetChars(), th->X(), th->Y(), th->Z()); SetResult(linetarget, in->frac, th, thingpitch); return; } } } }; //============================================================================ // // P_AimLineAttack // //============================================================================ DAngle P_AimLineAttack(AActor *t1, DAngle angle, double distance, FTranslatedLineTarget *pLineTarget, DAngle vrange, int flags, AActor *target, AActor *friender) { double shootz = t1->Center() - t1->Floorclip; if (t1->player != NULL) { shootz += t1->player->mo->AttackZOffset * t1->player->crouchfactor; } else { shootz += 8; } // can't shoot outside view angles if (vrange == 0) { if (t1->player == NULL || !level.IsFreelookAllowed()) { vrange = 35.; } else { // [BB] Disable autoaim on weapons with WIF_NOAUTOAIM. AWeapon *weapon = t1->player->ReadyWeapon; if (weapon && (weapon->WeaponFlags & WIF_NOAUTOAIM)) { vrange = 0.5; } else { // 35 degrees is approximately what Doom used. You cannot have a // vrange of 0 degrees, because then toppitch and bottompitch will // be equal, and PTR_AimTraverse will never find anything to shoot at // if it crosses a line. vrange = clamp(t1->player->userinfo.GetAimDist(), 0.5, 35.); } } } aim_t aim; aim.flags = flags; aim.shootthing = t1; aim.friender = (friender == NULL) ? t1 : friender; aim.aimdir = aim_t::aim_up | aim_t::aim_down; aim.startpos = t1->Pos(); aim.aimtrace = angle.ToVector(distance); aim.limitz = aim.shootz = shootz; aim.toppitch = t1->Angles.Pitch - vrange; aim.bottompitch = t1->Angles.Pitch + vrange; aim.attackrange = distance; aim.aimpitch = t1->Angles.Pitch; aim.lastsector = t1->Sector; aim.startfrac = 0; aim.unlinked = false; aim.aimtarget = target; aim.AimTraverse(); AimTarget *result = aim.Result(); if (pLineTarget) { *pLineTarget = *result; } return result->linetarget ? result->pitch : t1->Angles.Pitch; } DEFINE_ACTION_FUNCTION(AActor, AimLineAttack) { PARAM_SELF_PROLOGUE(AActor); PARAM_ANGLE(angle); PARAM_FLOAT(distance); PARAM_POINTER_DEF(pLineTarget, FTranslatedLineTarget); PARAM_ANGLE_DEF(vrange); PARAM_INT_DEF(flags); PARAM_OBJECT_DEF(target, AActor); PARAM_OBJECT_DEF(friender, AActor); ACTION_RETURN_FLOAT(P_AimLineAttack(self, angle, distance, pLineTarget, vrange, flags, target, friender).Degrees); } //========================================================================== // // Helper stuff for P_LineAttack // //========================================================================== struct Origin { AActor *Caller; FNameNoInit PuffSpecies; bool hitGhosts; bool MThruSpecies; bool ThruSpecies; bool ThruActors; }; static ETraceStatus CheckForActor(FTraceResults &res, void *userdata) { if (res.HitType != TRACE_HitActor) { return TRACE_Stop; } Origin *data = (Origin *)userdata; // Skip actors if the puff has: // 1. THRUACTORS or SPECTRAL // 2. MTHRUSPECIES on puff and the shooter has same species as the hit actor // 3. THRUSPECIES on puff and the puff has same species as the hit actor // 4. THRUGHOST on puff and the GHOST flag on the hit actor if ((data->ThruActors) || (res.Actor->flags4 & MF4_SPECTRAL) || (data->MThruSpecies && res.Actor->GetSpecies() == data->Caller->GetSpecies()) || (data->ThruSpecies && res.Actor->GetSpecies() == data->PuffSpecies) || (data->hitGhosts && res.Actor->flags3 & MF3_GHOST)) { return TRACE_Skip; } return TRACE_Stop; } //========================================================================== // // P_LineAttack // // if damage == 0, it is just a test trace that will leave linetarget set // //========================================================================== AActor *P_LineAttack(AActor *t1, DAngle angle, double distance, DAngle pitch, int damage, FName damageType, PClassActor *pufftype, int flags, FTranslatedLineTarget*victim, int *actualdamage, double sz, double offsetforward, double offsetside) { bool nointeract = !!(flags & LAF_NOINTERACT); DVector3 direction; double shootz; FTraceResults trace; Origin TData; TData.Caller = t1; bool killPuff = false; AActor *puff = NULL; int pflag = 0; int puffFlags = (flags & LAF_ISMELEEATTACK) ? PF_MELEERANGE : 0; bool spawnSky = false; if (flags & LAF_NORANDOMPUFFZ) puffFlags |= PF_NORANDOMZ; if (victim != NULL) { memset(victim, 0, sizeof(*victim)); } if (actualdamage != NULL) { *actualdamage = 0; } double pc = pitch.Cos(); direction = { pc * angle.Cos(), pc * angle.Sin(), -pitch.Sin() }; shootz = t1->Center() - t1->Floorclip; if (t1->player != NULL) { shootz += t1->player->mo->AttackZOffset * t1->player->crouchfactor; if (damageType == NAME_Melee || damageType == NAME_Hitscan) { // this is coming from a weapon attack function which needs to transfer information to the obituary code, // We need to preserve this info from the damage type because the actual damage type can get overridden by the puff pflag = DMG_PLAYERATTACK; } } else { shootz += 8; } // [MC] If overriding, set it to the base of the actor. // Offset by the amount specified. if (flags & LAF_OVERRIDEZ) shootz = t1->Z(); shootz += sz; // We need to check the defaults of the replacement here AActor *puffDefaults = GetDefaultByType(pufftype->GetReplacement()); TData.hitGhosts = (t1->player != NULL && t1->player->ReadyWeapon != NULL && (t1->player->ReadyWeapon->flags2 & MF2_THRUGHOST)) || (puffDefaults && (puffDefaults->flags2 & MF2_THRUGHOST)); spawnSky = (puffDefaults && (puffDefaults->flags3 & MF3_SKYEXPLODE)); TData.MThruSpecies = (puffDefaults && (puffDefaults->flags6 & MF6_MTHRUSPECIES)); TData.PuffSpecies = NAME_None; // [MC] To prevent possible mod breakage, this flag is pretty much necessary. // Somewhere, someone is relying on these to spawn on actors and move through them. if ((puffDefaults->flags7 & MF7_ALLOWTHRUFLAGS)) { TData.ThruSpecies = (puffDefaults && (puffDefaults->flags6 & MF6_THRUSPECIES)); TData.ThruActors = (puffDefaults && (puffDefaults->flags2 & MF2_THRUACTORS)); // [MC] Because this is a one-hit trace event, we need to spawn the puff, get the species // and destroy it. Assume there is no species unless tempuff isn't NULL. We cannot get // a proper species the same way as puffDefaults flags it appears... AActor *tempuff = NULL; if (pufftype != NULL) tempuff = Spawn(pufftype, t1->Pos(), ALLOW_REPLACE); if (tempuff != NULL) { TData.PuffSpecies = tempuff->GetSpecies(); tempuff->Destroy(); } } else { TData.ThruSpecies = false; TData.ThruActors = false; } // if the puff uses a non-standard damage type, this will override default, hitscan and melee damage type. // All other explicitly passed damage types (currenty only MDK) will be preserved. if ((damageType == NAME_None || damageType == NAME_Melee || damageType == NAME_Hitscan) && puffDefaults != NULL && puffDefaults->DamageType != NAME_None) { damageType = puffDefaults->DamageType; } int tflags; if (nointeract || (puffDefaults && puffDefaults->flags6 & MF6_NOTRIGGER)) tflags = TRACE_NoSky; else tflags = TRACE_NoSky | TRACE_Impact; // [MC] Check the flags and set the position according to what is desired. // LAF_ABSPOSITION: Treat the offset parameters as direct coordinates. // LAF_ABSOFFSET: Ignore the angle. DVector3 tempos; if (flags & LAF_ABSPOSITION) { tempos = DVector3(offsetforward, offsetside, sz); } else if (flags & LAF_ABSOFFSET) { tempos = t1->Vec2OffsetZ(offsetforward, offsetside, shootz); } else if (0.0 == offsetforward && 0.0 == offsetside) { // Default case so exact comparison is enough tempos = t1->PosAtZ(shootz); } else { const double s = angle.Sin(); const double c = angle.Cos(); tempos = t1->Vec2OffsetZ(offsetforward * c + offsetside * s, offsetforward * s - offsetside * c, shootz); } // Perform the trace. if (!Trace(tempos, t1->Sector, direction, distance, MF_SHOOTABLE, ML_BLOCKEVERYTHING | ML_BLOCKHITSCAN, t1, trace, tflags, CheckForActor, &TData)) { // hit nothing if (!nointeract && puffDefaults && puffDefaults->ActiveSound) { // Play miss sound S_Sound(t1, CHAN_WEAPON, puffDefaults->ActiveSound, 1, ATTN_NORM); } // [MC] LAF_NOINTERACT guarantees puff spawning and returns it directly to the calling function. // No damage caused, no sounds played, no blood splatters. if (nointeract || (puffDefaults && puffDefaults->flags3 & MF3_ALWAYSPUFF)) { // Spawn the puff anyway puff = P_SpawnPuff(t1, pufftype, trace.HitPos, trace.SrcAngleFromTarget, trace.SrcAngleFromTarget, 2, puffFlags); if (nointeract) { return puff; } } else { return nullptr; } } else { if (trace.HitType != TRACE_HitActor) { // position a bit closer for puffs if (nointeract || trace.HitType != TRACE_HitWall || ((trace.Line->special != Line_Horizon) || spawnSky)) { DVector2 pos = P_GetOffsetPosition(trace.HitPos.X, trace.HitPos.Y, -trace.HitVector.X * 4, -trace.HitVector.Y * 4); puff = P_SpawnPuff(t1, pufftype, DVector3(pos, trace.HitPos.Z - trace.HitVector.Z * 4), trace.SrcAngleFromTarget, trace.SrcAngleFromTarget - 90, 0, puffFlags); puff->radius = 1/65536.; if (nointeract) { return puff; } } // [RH] Spawn a decal if (trace.HitType == TRACE_HitWall && trace.Line->special != Line_Horizon && !trace.Line->isVisualPortal() && !(flags & LAF_NOIMPACTDECAL) && !(puffDefaults->flags7 & MF7_NODECAL)) { // [TN] If the actor or weapon has a decal defined, use that one. if (t1->DecalGenerator != NULL || (t1->player != NULL && t1->player->ReadyWeapon != NULL && t1->player->ReadyWeapon->DecalGenerator != NULL)) { // [ZK] If puff has FORCEDECAL set, do not use the weapon's decal if (puffDefaults->flags7 & MF7_FORCEDECAL && puff != NULL && puff->DecalGenerator) SpawnShootDecal(puff, trace); else SpawnShootDecal(t1, trace); } // Else, look if the bulletpuff has a decal defined. else if (puff != NULL && puff->DecalGenerator) { SpawnShootDecal(puff, trace); } else { SpawnShootDecal(t1, trace); } } else if (puff != NULL && trace.CrossedWater == NULL && trace.Sector->heightsec == NULL && trace.HitType == TRACE_HitFloor) { P_HitWater(puff, trace.Sector, trace.HitPos); } } else { // Hit a thing, so it could be either a puff or blood DVector3 bleedpos = trace.HitPos; // position a bit closer for puffs/blood if using compatibility mode. if (i_compatflags & COMPATF_HITSCAN) { DVector2 ofs = P_GetOffsetPosition(bleedpos.X, bleedpos.Y, -10 * trace.HitVector.X, -10 * trace.HitVector.Y); bleedpos.X = ofs.X; bleedpos.Y = ofs.Y; bleedpos.Z -= -10 * trace.HitVector.Z; } // Spawn bullet puffs or blood spots, depending on target type. if (nointeract || (puffDefaults && puffDefaults->flags3 & MF3_PUFFONACTORS) || (trace.Actor->flags & MF_NOBLOOD) || (trace.Actor->flags2 & (MF2_INVULNERABLE | MF2_DORMANT))) { if (!(trace.Actor->flags & MF_NOBLOOD)) puffFlags |= PF_HITTHINGBLEED; // We must pass the unreplaced puff type here puff = P_SpawnPuff(t1, pufftype, bleedpos, trace.SrcAngleFromTarget, trace.SrcAngleFromTarget - 90, 2, puffFlags | PF_HITTHING, trace.Actor); if (nointeract) { return puff; } } // Allow puffs to inflict poison damage, so that hitscans can poison, too. if (puffDefaults != NULL && puffDefaults->PoisonDamage > 0 && puffDefaults->PoisonDuration != INT_MIN) { P_PoisonMobj(trace.Actor, puff ? puff : t1, t1, puffDefaults->PoisonDamage, puffDefaults->PoisonDuration, puffDefaults->PoisonPeriod, puffDefaults->PoisonDamageType); } // [GZ] If MF6_FORCEPAIN is set, we need to call P_DamageMobj even if damage is 0! // Note: The puff may not yet be spawned here so we must check the class defaults, not the actor. int newdam = damage; if (damage || (puffDefaults != NULL && ((puffDefaults->flags6 & MF6_FORCEPAIN) || (puffDefaults->flags7 & MF7_CAUSEPAIN)))) { int dmgflags = DMG_INFLICTOR_IS_PUFF | pflag; // Allow MF5_PIERCEARMOR on a weapon as well. if (t1->player != NULL && (dmgflags & DMG_PLAYERATTACK) && t1->player->ReadyWeapon != NULL && t1->player->ReadyWeapon->flags5 & MF5_PIERCEARMOR) { dmgflags |= DMG_NO_ARMOR; } if (puff == NULL) { // Since the puff is the damage inflictor we need it here // regardless of whether it is displayed or not. puff = P_SpawnPuff(t1, pufftype, bleedpos, 0., 0., 2, puffFlags | PF_HITTHING | PF_TEMPORARY); killPuff = true; } auto src = t1; if ((flags & LAF_TARGETISSOURCE) && t1 && t1->target) src = t1->target; newdam = P_DamageMobj(trace.Actor, puff ? puff : t1, src, damage, damageType, dmgflags|DMG_USEANGLE, trace.SrcAngleFromTarget); if (actualdamage != NULL) { *actualdamage = newdam; } } if (!(puffDefaults != NULL && puffDefaults->flags3&MF3_BLOODLESSIMPACT)) { bool bloodsplatter = (t1->flags5 & MF5_BLOODSPLATTER) || (t1->player != nullptr && t1->player->ReadyWeapon != nullptr && (t1->player->ReadyWeapon->WeaponFlags & WIF_AXEBLOOD)); bool axeBlood = (t1->player != nullptr && t1->player->ReadyWeapon != nullptr && (t1->player->ReadyWeapon->WeaponFlags & WIF_AXEBLOOD)); if (!bloodsplatter && !axeBlood && !(trace.Actor->flags & MF_NOBLOOD) && !(trace.Actor->flags2 & (MF2_INVULNERABLE | MF2_DORMANT))) { P_SpawnBlood(bleedpos, trace.SrcAngleFromTarget, newdam > 0 ? newdam : damage, trace.Actor); } if (damage) { if (bloodsplatter || axeBlood) { if (!(trace.Actor->flags&MF_NOBLOOD) && !(trace.Actor->flags2&(MF2_INVULNERABLE | MF2_DORMANT))) { if (axeBlood) { P_BloodSplatter2(bleedpos, trace.Actor, trace.SrcAngleFromTarget); } if (pr_lineattack() < 192) { P_BloodSplatter(bleedpos, trace.Actor, trace.SrcAngleFromTarget); } } } // [RH] Stick blood to walls P_TraceBleed(newdam > 0 ? newdam : damage, trace.HitPos, trace.Actor, trace.SrcAngleFromTarget, pitch); } } if (victim != NULL) { victim->linetarget = trace.Actor; victim->attackAngleFromSource = trace.SrcAngleFromTarget; // With arbitrary portals this cannot be calculated so using the actual attack angle is the only option. victim->angleFromSource = trace.unlinked? victim->attackAngleFromSource : t1->AngleTo(trace.Actor); victim->unlinked = trace.unlinked; } } if (trace.Crossed3DWater || trace.CrossedWater) { if (puff == NULL) { // Spawn puff just to get a mass for the splash puff = P_SpawnPuff(t1, pufftype, trace.HitPos, 0., 0., 2, puffFlags | PF_HITTHING | PF_TEMPORARY); killPuff = true; } SpawnDeepSplash(t1, trace, puff); } } if (killPuff && puff != NULL) { puff->Destroy(); puff = NULL; } return puff; } AActor *P_LineAttack(AActor *t1, DAngle angle, double distance, DAngle pitch, int damage, FName damageType, FName pufftype, int flags, FTranslatedLineTarget *victim, int *actualdamage, double sz, double offsetforward, double offsetside) { PClassActor *type = PClass::FindActor(pufftype); if (type == NULL) { if (victim != NULL) { memset(victim, 0, sizeof(*victim)); } Printf("Attempt to spawn unknown actor type '%s'\n", pufftype.GetChars()); return NULL; } else { return P_LineAttack(t1, angle, distance, pitch, damage, damageType, type, flags, victim, actualdamage, sz, offsetforward, offsetside); } } DEFINE_ACTION_FUNCTION(AActor, LineAttack) { PARAM_SELF_PROLOGUE(AActor); PARAM_ANGLE(angle); PARAM_FLOAT(distance); PARAM_ANGLE(pitch); PARAM_INT(damage); PARAM_NAME(damageType); PARAM_CLASS(puffType, AActor); PARAM_INT_DEF(flags); PARAM_POINTER_DEF(victim, FTranslatedLineTarget); PARAM_FLOAT_DEF(offsetz); PARAM_FLOAT_DEF(offsetforward); PARAM_FLOAT_DEF(offsetside); int acdmg; if (puffType == nullptr) puffType = PClass::FindActor("BulletPuff"); // P_LineAttack does not work without a puff to take info from. auto puff = P_LineAttack(self, angle, distance, pitch, damage, damageType, puffType, flags, victim, &acdmg, offsetz, offsetforward, offsetside); if (numret > 0) ret[0].SetObject(puff); if (numret > 1) ret[1].SetInt(acdmg), numret = 2; return numret; } //========================================================================== // // P_LineTrace // //========================================================================== struct CheckLineData { AActor *Caller; bool ThruSpecies; bool ThruActors; int NumPortals; }; static ETraceStatus CheckLineTrace(FTraceResults &res, void *userdata) { CheckLineData *TData = (CheckLineData *)userdata; if ( res.HitType == TRACE_CrossingPortal ) { TData->NumPortals++; return TRACE_Skip; } if ( res.HitType != TRACE_HitActor ) { return TRACE_Stop; } if ( (TData->ThruActors) || (TData->ThruSpecies && res.Actor->GetSpecies() == TData->Caller->GetSpecies()) ) { return TRACE_Skip; } return TRACE_Stop; } bool P_LineTrace(AActor *t1, DAngle angle, double distance, DAngle pitch, int flags, double sz, double offsetforward, double offsetside, FLineTraceData *outdata) { FTraceResults trace; CheckLineData TData; TData.Caller = t1; TData.ThruSpecies = !!(flags & TRF_THRUSPECIES); TData.ThruActors = !!(flags & TRF_THRUACTORS); TData.NumPortals = 0; DVector3 direction; double pc = pitch.Cos(); direction = { pc * angle.Cos(), pc * angle.Sin(), -pitch.Sin() }; DVector3 startpos; double startz = t1->Z() - t1->Floorclip; startz += sz; if ( flags & TRF_ABSPOSITION ) { startpos = DVector3(offsetforward, offsetside, sz); } else if ( flags & TRF_ABSOFFSET ) { startpos = t1->Vec2OffsetZ(offsetforward, offsetside, startz); } else if ( (offsetforward == 0.0) && (offsetside == 0.0) ) { startpos = t1->PosAtZ(startz); } else { const double s = angle.Sin(); const double c = angle.Cos(); startpos = t1->Vec2OffsetZ(offsetforward * c + offsetside * s, offsetforward * s - offsetside * c, startz); } ActorFlags aflags = (flags & TRF_ALLACTORS) ? ActorFlags::FromInt(0xFFFFFFFF) : MF_SHOOTABLE; int lflags = 0; if ( !(lflags & TRF_THRUBLOCK) ) lflags |= ML_BLOCKEVERYTHING; if ( !(lflags & TRF_THRUHITSCAN) ) lflags |= ML_BLOCKHITSCAN; int tflags = TRACE_ReportPortals; if ( flags & TRF_NOSKY ) tflags |= TRACE_NoSky; // Do trace bool ret = Trace(startpos, t1->Sector, direction, distance, aflags, lflags, t1, trace, tflags, CheckLineTrace, &TData); if ( outdata ) { memset(outdata,0,sizeof(*outdata)); outdata->HitActor = trace.Actor; outdata->HitLine = trace.Line; outdata->HitSector = trace.Sector; outdata->Hit3DFloor = trace.ffloor; outdata->SectorPlane = (trace.HitType == TRACE_HitCeiling) ? 1 : 0; if ( trace.HitType == TRACE_HitWall ) { outdata->LineSide = trace.Side; int txpart; switch ( trace.Tier ) { case TIER_Middle: outdata->LinePart = 1; outdata->HitTexture = trace.Line->sidedef[trace.Side]->textures[1].texture; break; case TIER_Upper: outdata->LinePart = 0; outdata->HitTexture = trace.Line->sidedef[trace.Side]->textures[0].texture; break; case TIER_Lower: outdata->LinePart = 2; outdata->HitTexture = trace.Line->sidedef[trace.Side]->textures[2].texture; break; case TIER_FFloor: txpart = (trace.ffloor->flags & FF_UPPERTEXTURE) ? 0 : (trace.ffloor->flags & FF_LOWERTEXTURE) ? 2 : 1; outdata->HitTexture = trace.ffloor->master->sidedef[0]->textures[txpart].texture; break; } } else outdata->HitTexture = trace.HitTexture; outdata->HitLocation = trace.HitPos; outdata->HitDir = trace.HitVector; outdata->Distance = trace.Distance; outdata->NumPortals = TData.NumPortals; outdata->HitType = trace.HitType; } return ret; } DEFINE_FIELD_X(FLineTraceData, FLineTraceData, HitActor); DEFINE_FIELD_X(FLineTraceData, FLineTraceData, HitLine); DEFINE_FIELD_X(FLineTraceData, FLineTraceData, HitSector); DEFINE_FIELD_X(FLineTraceData, FLineTraceData, Hit3DFloor); DEFINE_FIELD_X(FLineTraceData, FLineTraceData, HitTexture); DEFINE_FIELD_X(FLineTraceData, FLineTraceData, HitLocation); DEFINE_FIELD_X(FLineTraceData, FLineTraceData, Distance); DEFINE_FIELD_X(FLineTraceData, FLineTraceData, NumPortals); DEFINE_FIELD_X(FLineTraceData, FLineTraceData, LineSide); DEFINE_FIELD_X(FLineTraceData, FLineTraceData, LinePart); DEFINE_FIELD_X(FLineTraceData, FLineTraceData, SectorPlane); DEFINE_FIELD_X(FLineTraceData, FLineTraceData, HitType); DEFINE_ACTION_FUNCTION(AActor, LineTrace) { PARAM_SELF_PROLOGUE(AActor); PARAM_ANGLE(angle); PARAM_FLOAT(distance); PARAM_ANGLE(pitch); PARAM_INT_DEF(flags); PARAM_FLOAT_DEF(offsetz); PARAM_FLOAT_DEF(offsetforward); PARAM_FLOAT_DEF(offsetside); PARAM_POINTER_DEF(data, FLineTraceData); ACTION_RETURN_BOOL(P_LineTrace(self,angle,distance,pitch,flags,offsetz,offsetforward,offsetside,data)); } //========================================================================== // // P_LinePickActor // //========================================================================== AActor *P_LinePickActor(AActor *t1, DAngle angle, double distance, DAngle pitch, ActorFlags actorMask, uint32_t wallMask) { DVector3 direction; double shootz; double pc = pitch.Cos(); direction = { pc * angle.Cos(), pc * angle.Sin(), -pitch.Sin() }; shootz = t1->Center() - t1->Floorclip; if (t1->player != NULL) { shootz += t1->player->mo->AttackZOffset * t1->player->crouchfactor; } else { shootz += 8; } FTraceResults trace; Origin TData; TData.Caller = t1; TData.hitGhosts = true; TData.MThruSpecies = false; TData.ThruActors = false; TData.ThruSpecies = false; if (Trace(t1->PosAtZ(shootz), t1->Sector, direction, distance, actorMask, wallMask, t1, trace, TRACE_NoSky | TRACE_PortalRestrict, CheckForActor, &TData)) { if (trace.HitType == TRACE_HitActor) { return trace.Actor; } } return NULL; } //========================================================================== // // // //========================================================================== void P_TraceBleed(int damage, const DVector3 &pos, AActor *actor, DAngle angle, DAngle pitch) { if (!cl_bloodsplats) return; const char *bloodType = "BloodSplat"; int count; double noise; if ((actor->flags & MF_NOBLOOD) || (actor->flags5 & MF5_NOBLOODDECALS) || (actor->flags2 & (MF2_INVULNERABLE | MF2_DORMANT)) || (actor->player && actor->player->cheats & CF_GODMODE)) { return; } if (damage < 15) { // For low damages, there is a chance to not spray blood at all if (damage <= 10) { if (pr_tracebleed() < 160) { return; } } count = 1; noise = 11.25 / 256.; } else if (damage < 25) { count = 2; noise = 22.5 / 256.; } else { // For high damages, there is a chance to spray just one big glob of blood if (pr_tracebleed() < 24) { bloodType = "BloodSmear"; count = 1; noise = 45. / 256.; } else { count = 3; noise = 45. / 256.; } } for (; count; --count) { FTraceResults bleedtrace; DAngle bleedang = angle + (pr_tracebleed() - 128) * noise; DAngle bleedpitch = pitch + (pr_tracebleed() - 128) * noise; double cosp = bleedpitch.Cos(); DVector3 vdir = DVector3(cosp * bleedang.Cos(), cosp * bleedang.Sin(), -bleedpitch.Sin()); if (Trace(pos, actor->Sector, vdir, 172, 0, ML_BLOCKEVERYTHING, actor, bleedtrace, TRACE_NoSky)) { if (bleedtrace.HitType == TRACE_HitWall) { PalEntry bloodcolor = actor->BloodColor; if (bloodcolor != 0) { bloodcolor.r >>= 1; // the full color is too bright for blood decals bloodcolor.g >>= 1; bloodcolor.b >>= 1; bloodcolor.a = 1; } DImpactDecal::StaticCreate(bloodType, bleedtrace.HitPos, bleedtrace.Line->sidedef[bleedtrace.Side], bleedtrace.ffloor, bloodcolor); } } } } void P_TraceBleed(int damage, AActor *target, DAngle angle, DAngle pitch) { P_TraceBleed(damage, target->PosPlusZ(target->Height/2), target, angle, pitch); } DEFINE_ACTION_FUNCTION(AActor, TraceBleedAngle) { PARAM_SELF_PROLOGUE(AActor); PARAM_INT(damage); PARAM_FLOAT(angle); PARAM_FLOAT(pitch); P_TraceBleed(damage, self, angle, pitch); return 0; } //========================================================================== // // // //========================================================================== void P_TraceBleed(int damage, AActor *target, AActor *missile) { DAngle pitch; if (target == NULL || missile->flags3 & MF3_BLOODLESSIMPACT) { return; } if (missile->Vel.Z != 0) { double aim; aim = g_atan(missile->Vel.Z / target->Distance2D(missile)); pitch = -DAngle::ToDegrees(aim); } else { pitch = 0.; } P_TraceBleed(damage, target->PosPlusZ(target->Height/2), target, missile->AngleTo(target), pitch); } //========================================================================== // // // //========================================================================== void P_TraceBleed(int damage, FTranslatedLineTarget *t, AActor *puff) { if (t->linetarget == NULL || puff->flags3 & MF3_BLOODLESSIMPACT) { return; } DAngle pitch = (pr_tracebleed() - 128) * (360 / 65536.); P_TraceBleed(damage, t->linetarget->PosPlusZ(t->linetarget->Height/2), t->linetarget, t->angleFromSource, pitch); } DEFINE_ACTION_FUNCTION(_FTranslatedLineTarget, TraceBleed) { PARAM_SELF_STRUCT_PROLOGUE(FTranslatedLineTarget); PARAM_INT(damage); PARAM_OBJECT_NOT_NULL(missile, AActor); P_TraceBleed(damage, self, missile); return 0; } //========================================================================== // // // //========================================================================== void P_TraceBleed(int damage, AActor *target) { if (target != NULL) { DAngle angle = pr_tracebleed() * (360 / 256.); DAngle pitch = (pr_tracebleed() - 128) * (360 / 65536.); P_TraceBleed(damage, target->PosPlusZ(target->Height / 2), target, angle, pitch); } } DEFINE_ACTION_FUNCTION(AActor, TraceBleed) { PARAM_SELF_PROLOGUE(AActor); PARAM_INT(damage); PARAM_OBJECT(missile, AActor); if (missile) P_TraceBleed(damage, self, missile); else P_TraceBleed(damage, self); return 0; } //========================================================================== // // [RH] Rail gun stuffage // //========================================================================== struct SRailHit { AActor *HitActor; DVector3 HitPos; DAngle HitAngle; }; struct RailData { AActor *Caller; TArray RailHits; TArray PortalHits; FNameNoInit PuffSpecies; bool StopAtOne; bool StopAtInvul; bool ThruGhosts; bool ThruSpecies; bool MThruSpecies; bool ThruActors; int limit; int count; }; static ETraceStatus ProcessRailHit(FTraceResults &res, void *userdata) { RailData *data = (RailData *)userdata; if (res.HitType == TRACE_CrossingPortal) { SPortalHit newhit; newhit.HitPos = res.HitPos; newhit.ContPos = res.SrcFromTarget; newhit.OutDir = res.HitVector; data->PortalHits.Push(newhit); return TRACE_Continue; } if (res.HitType != TRACE_HitActor) { return TRACE_Stop; } // Invulnerable things completely block the shot if (data->StopAtInvul && res.Actor->flags2 & MF2_INVULNERABLE) { return TRACE_Stop; } // Skip actors if the puff has: // 1. THRUACTORS (This one did NOT include a check for spectral) // 2. MTHRUSPECIES on puff and the shooter has same species as the hit actor // 3. THRUSPECIES on puff and the puff has same species as the hit actor // 4. THRUGHOST on puff and the GHOST flag on the hit actor if ((data->ThruActors) || (data->MThruSpecies && res.Actor->GetSpecies() == data->Caller->GetSpecies()) || (data->ThruSpecies && res.Actor->GetSpecies() == data->PuffSpecies) || (data->ThruGhosts && res.Actor->flags3 & MF3_GHOST)) { return TRACE_Skip; } // Save this thing for damaging later, and continue the trace SRailHit newhit; newhit.HitActor = res.Actor; newhit.HitPos = res.HitPos; newhit.HitAngle = res.SrcAngleFromTarget; if (i_compatflags & COMPATF_HITSCAN) { DVector2 ofs = P_GetOffsetPosition(newhit.HitPos.X, newhit.HitPos.Y, -10 * res.HitVector.X, -10 * res.HitVector.Y); newhit.HitPos.X = ofs.X; newhit.HitPos.Y = ofs.Y; newhit.HitPos.Z -= -10 * res.HitVector.Z; } data->RailHits.Push(newhit); if (data->limit) { data->count++; } return (data->StopAtOne || (data->limit && (data->count >= data->limit))) ? TRACE_Stop : TRACE_Continue; } //========================================================================== // // // //========================================================================== void P_RailAttack(FRailParams *p) { DVector3 start; FTraceResults trace; PClassActor *puffclass = p->puff; if (puffclass == NULL) { puffclass = PClass::FindActor(NAME_BulletPuff); } AActor *source = p->source; DAngle pitch = source->Angles.Pitch + p->pitchoffset; DAngle angle = source->Angles.Yaw + p->angleoffset; DVector3 vec(DRotator(-pitch, angle, angle)); double shootz = source->Center() - source->FloatSpeed + p->offset_z - source->Floorclip; if (!(p->flags & RAF_CENTERZ)) { if (source->player != NULL) { shootz += source->player->mo->AttackZOffset * source->player->crouchfactor; } else { shootz += 8; } } int puffflags = 0; if (p->flags & RAF_NORANDOMPUFFZ) { puffflags |= PF_NORANDOMZ; } DVector2 xy = source->Vec2Angle(p->offset_xy, angle - 90.); RailData rail_data; rail_data.Caller = source; rail_data.limit = p->limit; rail_data.count = 0; rail_data.StopAtOne = !!(p->flags & RAF_NOPIERCE); start.X = xy.X; start.Y = xy.Y; start.Z = shootz; int flags; assert(puffclass != NULL); // Because we set it to a default above AActor *puffDefaults = GetDefaultByType(puffclass->GetReplacement()); //Contains all the flags such as FOILINVUL, etc. // disabled because not complete yet. flags = (puffDefaults->flags6 & MF6_NOTRIGGER) ? TRACE_ReportPortals : TRACE_PCross | TRACE_Impact | TRACE_ReportPortals; rail_data.StopAtInvul = (puffDefaults->flags3 & MF3_FOILINVUL) ? false : true; rail_data.MThruSpecies = ((puffDefaults->flags6 & MF6_MTHRUSPECIES)) ? true : false; // Prevent mod breakage as somewhere, someone is relying on these to spawn on an actor // and move through them... if ((puffDefaults->flags7 & MF7_ALLOWTHRUFLAGS)) { rail_data.ThruGhosts = !!(puffDefaults->flags2 & MF2_THRUGHOST); rail_data.ThruSpecies = !!(puffDefaults->flags6 & MF6_THRUSPECIES); rail_data.ThruActors = !!(puffDefaults->flags2 & MF2_THRUACTORS); } else { rail_data.ThruGhosts = false; rail_data.MThruSpecies = false; rail_data.ThruActors = false; } // used as damage inflictor AActor *thepuff = NULL; if (puffclass != NULL) thepuff = Spawn(puffclass, source->Pos(), ALLOW_REPLACE); rail_data.PuffSpecies = (thepuff != NULL) ? thepuff->GetSpecies() : NAME_None; Trace(start, source->Sector, vec, p->distance, MF_SHOOTABLE, ML_BLOCKEVERYTHING, source, trace, flags, ProcessRailHit, &rail_data); // Hurt anything the trace hit unsigned int i; FName damagetype = (puffDefaults == NULL || puffDefaults->DamageType == NAME_None) ? FName(NAME_Railgun) : puffDefaults->DamageType; for (i = 0; i < rail_data.RailHits.Size(); i++) { bool spawnpuff; bool bleed = false; int actorpuffflags = puffflags | PF_HITTHING; AActor *hitactor = rail_data.RailHits[i].HitActor; DVector3 &hitpos = rail_data.RailHits[i].HitPos; DAngle hitangle = rail_data.RailHits[i].HitAngle; if ((hitactor->flags & MF_NOBLOOD) || (hitactor->flags2 & MF2_DORMANT || ((hitactor->flags2 & MF2_INVULNERABLE) && !(puffDefaults->flags3 & MF3_FOILINVUL)))) { spawnpuff = (puffclass != NULL); } else { spawnpuff = (puffclass != NULL && puffDefaults->flags3 & MF3_ALWAYSPUFF); actorpuffflags |= PF_HITTHINGBLEED; // [XA] Allow for puffs to jump to XDeath state. if (!(puffDefaults->flags3 & MF3_BLOODLESSIMPACT)) { bleed = true; } } if (spawnpuff) { P_SpawnPuff(source, puffclass, hitpos, hitangle, hitangle - 90, 1, actorpuffflags, hitactor); } int dmgFlagPass = DMG_INFLICTOR_IS_PUFF; if (puffDefaults != NULL) // is this even possible? { if (puffDefaults->PoisonDamage > 0 && puffDefaults->PoisonDuration != INT_MIN) { P_PoisonMobj(hitactor, thepuff ? thepuff : source, source, puffDefaults->PoisonDamage, puffDefaults->PoisonDuration, puffDefaults->PoisonPeriod, puffDefaults->PoisonDamageType); } if (puffDefaults->flags3 & MF3_FOILINVUL) dmgFlagPass |= DMG_FOILINVUL; if (puffDefaults->flags7 & MF7_FOILBUDDHA) dmgFlagPass |= DMG_FOILBUDDHA; } // [RK] If the attack source is a player, send the DMG_PLAYERATTACK flag. int newdam = P_DamageMobj(hitactor, thepuff ? thepuff : source, source, p->damage, damagetype, dmgFlagPass | DMG_USEANGLE | (source->player ? DMG_PLAYERATTACK : 0), hitangle); if (bleed) { P_SpawnBlood(hitpos, hitangle, newdam > 0 ? newdam : p->damage, hitactor); P_TraceBleed(newdam > 0 ? newdam : p->damage, hitpos, hitactor, hitangle, pitch); } } // Spawn a decal or puff at the point where the trace ended. if (trace.HitType == TRACE_HitWall) { AActor* puff = NULL; if (puffclass != NULL && puffDefaults->flags3 & MF3_ALWAYSPUFF) { puff = P_SpawnPuff(source, puffclass, trace.HitPos, trace.SrcAngleFromTarget, trace.SrcAngleFromTarget - 90, 1, puffflags); if (puff && (trace.Line != NULL) && (trace.Line->special == Line_Horizon) && !(puff->flags3 & MF3_SKYEXPLODE)) puff->Destroy(); } if (puff != NULL && puffDefaults->flags7 & MF7_FORCEDECAL && puff->DecalGenerator) SpawnShootDecal(puff, trace); else SpawnShootDecal(source, trace); } if (trace.HitType == TRACE_HitFloor || trace.HitType == TRACE_HitCeiling) { AActor* puff = NULL; if (puffclass != NULL && puffDefaults->flags3 & MF3_ALWAYSPUFF) { puff = P_SpawnPuff(source, puffclass, trace.HitPos, trace.SrcAngleFromTarget, trace.SrcAngleFromTarget - 90, 1, puffflags); if (puff && !(puff->flags3 & MF3_SKYEXPLODE) && (((trace.HitType == TRACE_HitFloor) && (puff->floorpic == skyflatnum)) || ((trace.HitType == TRACE_HitCeiling) && (puff->ceilingpic == skyflatnum)))) { puff->Destroy(); } } } if (thepuff != NULL) { if (trace.Crossed3DWater || trace.CrossedWater) { SpawnDeepSplash(source, trace, thepuff); } else if (trace.HitType == TRACE_HitFloor && trace.Sector->heightsec == NULL) { P_HitWater(thepuff, trace.Sector, trace.HitPos); } thepuff->Destroy(); } // Draw the slug's trail. P_DrawRailTrail(source, rail_data.PortalHits, p->color1, p->color2, p->maxdiff, p->flags, p->spawnclass, angle, p->duration, p->sparsity, p->drift, p->SpiralOffset, pitch); } //========================================================================== // // [RH] P_AimCamera // //========================================================================== CVAR(Float, chase_height, -8.f, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) CVAR(Float, chase_dist, 90.f, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) void P_AimCamera(AActor *t1, DVector3 &campos, DAngle &camangle, sector_t *&CameraSector, bool &unlinked) { double distance = clamp(chase_dist, 0, 30000); DAngle angle = t1->Angles.Yaw - 180; DAngle pitch = t1->Angles.Pitch; FTraceResults trace; DVector3 vvec; double sz; double pc = pitch.Cos(); vvec = { pc * angle.Cos(), pc * angle.Sin(), pitch.Sin() }; sz = t1->Top() - t1->Floorclip + clamp(chase_height, -1000, 1000); if (Trace(t1->PosAtZ(sz), t1->Sector, vvec, distance, 0, 0, NULL, trace) && trace.Distance > 10) { // Position camera slightly in front of hit thing campos = t1->PosAtZ(sz) + vvec *(trace.Distance - 5); } else { campos = trace.HitPos - trace.HitVector * 1/256.; } CameraSector = trace.Sector; unlinked = trace.unlinked; camangle = trace.SrcAngleFromTarget - 180.; } //========================================================================== // // P_TalkFacing // // Looks for something within 5.625 degrees left or right of the player // to talk to. // //========================================================================== bool P_TalkFacing(AActor *player) { static const double angleofs[] = { 0, 90./16, -90./16 }; FTranslatedLineTarget t; for (double angle : angleofs) { P_AimLineAttack(player, player->Angles.Yaw + angle, TALKRANGE, &t, 35., ALF_FORCENOSMART | ALF_CHECKCONVERSATION | ALF_PORTALRESTRICT); if (t.linetarget != NULL) { if (t.linetarget->health > 0 && // Dead things can't talk. !(t.linetarget->flags4 & MF4_INCOMBAT) && // Fighting things don't talk either. t.linetarget->Conversation != NULL) { // Give the NPC a chance to play a brief animation t.linetarget->ConversationAnimation(0); P_StartConversation(t.linetarget, player, true, true); return true; } return false; } } return false; } //========================================================================== // // USE LINES // //========================================================================== bool P_UseTraverse(AActor *usething, const DVector2 &start, const DVector2 &end, bool &foundline) { FPathTraverse it(start.X, start.Y, end.X, end.Y, PT_ADDLINES | PT_ADDTHINGS); intercept_t *in; DVector3 xpos = { start.X, start.Y, usething->Z() }; while ((in = it.Next())) { // [RH] Check for things to talk with or use a puzzle item on if (!in->isaline) { AActor * const mobj = in->d.thing; if (mobj == usething) continue; // Check thing // Check for puzzle item use or USESPECIAL flag // Extended to use the same activationtype mechanism as BUMPSPECIAL does if (mobj->flags5 & MF5_USESPECIAL || mobj->special == UsePuzzleItem) { if (P_ActivateThingSpecial(mobj, usething)) return true; } IFVIRTUALPTR(mobj, AActor, Used) { VMValue params[] = { mobj, usething }; int ret; VMReturn vret(&ret); VMCall(func, params, 2, &vret, 1); if (ret) return true; } continue; } if (it.PortalRelocate(in, PT_ADDLINES | PT_ADDTHINGS, &xpos)) { continue; } FLineOpening open; if (in->d.line->special == 0 || !(in->d.line->activation & (SPAC_Use | SPAC_UseThrough | SPAC_UseBack))) { blocked: if (in->d.line->flags & (ML_BLOCKEVERYTHING | ML_BLOCKUSE)) { open.range = 0; } else { P_LineOpening(open, NULL, in->d.line, it.InterceptPoint(in)); } if (open.range <= 0 || (in->d.line->special != 0 && (i_compatflags & COMPATF_USEBLOCKING))) { // [RH] Give sector a chance to intercept the use sector_t * sec; sec = usething->Sector; if (sec->SecActTarget && sec->TriggerSectorActions(usething, SECSPAC_Use)) { return true; } sec = P_PointOnLineSide(xpos, in->d.line) == 0 ? in->d.line->frontsector : in->d.line->backsector; if (sec != NULL && sec->SecActTarget && sec->TriggerSectorActions(usething, SECSPAC_UseWall)) { return true; } if (usething->player) { S_Sound(usething, CHAN_VOICE, "*usefail", 1, ATTN_IDLE); } return true; // can't use through a wall } foundline = true; continue; // not a special line, but keep checking } if (P_PointOnLineSide(xpos, in->d.line) == 1) { if (!(in->d.line->activation & SPAC_UseBack)) { // [RH] continue traversal for two-sided lines //return in->d.line->backsector != NULL; // don't use back side goto blocked; // do a proper check for back sides of triggers } else { P_ActivateLine(in->d.line, usething, 1, SPAC_UseBack, &xpos); return true; } } else { if ((in->d.line->activation & (SPAC_Use | SPAC_UseThrough | SPAC_UseBack)) == SPAC_UseBack) { goto blocked; // Line cannot be used from front side so treat it as a non-trigger line } P_ActivateLine(in->d.line, usething, 0, SPAC_Use, &xpos); //WAS can't use more than one special line in a row //jff 3/21/98 NOW multiple use allowed with enabling line flag //[RH] And now I've changed it again. If the line is of type // SPAC_USE, then it eats the use. Everything else passes // it through, including SPAC_USETHROUGH. if (i_compatflags & COMPATF_USEBLOCKING) { if (in->d.line->activation & SPAC_UseThrough) continue; else return true; } else { if (!(in->d.line->activation & SPAC_Use)) continue; else return true; } } } return false; } //========================================================================== // // Returns false if a "oof" sound should be made because of a blocking // linedef. Makes 2s middles which are impassable, as well as 2s uppers // and lowers which block the player, cause the sound effect when the // player tries to activate them. Specials are excluded, although it is // assumed that all special linedefs within reach have been considered // and rejected already (see P_UseLines). // // by Lee Killough // //========================================================================== bool P_NoWayTraverse(AActor *usething, const DVector2 &start, const DVector2 &end) { FPathTraverse it(start.X, start.Y, end.X, end.Y, PT_ADDLINES); intercept_t *in; while ((in = it.Next())) { line_t *ld = in->d.line; FLineOpening open; // [GrafZahl] de-obfuscated. Was I the only one who was unable to make sense out of // this convoluted mess? if (ld->special) continue; if (ld->isLinePortal()) return false; if (ld->flags&(ML_BLOCKING | ML_BLOCKEVERYTHING | ML_BLOCK_PLAYERS)) return true; P_LineOpening(open, NULL, ld, it.InterceptPoint(in)); if (open.range <= 0 || open.bottom > usething->Z() + usething->MaxStepHeight || open.top < usething->Top()) return true; } return false; } //========================================================================== // // P_UseLines // // Looks for special lines in front of the player to activate // //========================================================================== void P_UseLines(player_t *player) { bool foundline = false; // If the player is transitioning a portal, use the group that is at its vertical center. DVector2 start = player->mo->GetPortalTransition(player->mo->Height / 2); // [NS] Now queries the Player's UseRange. DVector2 end = start + player->mo->Angles.Yaw.ToVector(player->mo->UseRange); // old code: // This added test makes the "oof" sound work on 2s lines -- killough: if (!P_UseTraverse(player->mo, start, end, foundline)) { // [RH] Give sector a chance to eat the use sector_t *sec = player->mo->Sector; int spac = SECSPAC_Use; if (foundline) spac |= SECSPAC_UseWall; if ((!sec->SecActTarget || !sec->TriggerSectorActions(player->mo, spac)) && P_NoWayTraverse(player->mo, start, end)) { S_Sound(player->mo, CHAN_VOICE, "*usefail", 1, ATTN_IDLE); } } } //========================================================================== // // P_UsePuzzleItem // // Returns true if the puzzle item was used on a line or a thing. // //========================================================================== bool P_UsePuzzleItem(AActor *PuzzleItemUser, int PuzzleItemType) { DVector2 start; DVector2 end; double usedist; // [NS] If it's a Player, get their UseRange. if (PuzzleItemUser->player) usedist = PuzzleItemUser->player->mo->UseRange; else usedist = USERANGE; start = PuzzleItemUser->GetPortalTransition(PuzzleItemUser->Height / 2); end = PuzzleItemUser->Angles.Yaw.ToVector(usedist); FPathTraverse it(start.X, start.Y, end.X, end.Y, PT_DELTA | PT_ADDLINES | PT_ADDTHINGS); intercept_t *in; while ((in = it.Next())) { AActor *mobj; FLineOpening open; if (in->isaline) { // Check line if (in->d.line->special != UsePuzzleItem) { P_LineOpening(open, NULL, in->d.line, it.InterceptPoint(in)); if (open.range <= 0) { return false; // can't use through a wall } continue; } if (P_PointOnLineSide(PuzzleItemUser->Pos(), in->d.line) == 1) { // Don't use back sides return false; } if (PuzzleItemType != in->d.line->args[0]) { // Item type doesn't match return false; } int args[3] = { in->d.line->args[2], in->d.line->args[3], in->d.line->args[4] }; P_StartScript(PuzzleItemUser, in->d.line, in->d.line->args[1], NULL, args, 3, ACS_ALWAYS); in->d.line->special = 0; return true; } // Check thing mobj = in->d.thing; if (mobj->special != UsePuzzleItem) { // Wrong special continue; } if (PuzzleItemType != mobj->args[0]) { // Item type doesn't match continue; } int args[3] = { mobj->args[2], mobj->args[3], mobj->args[4] }; P_StartScript(PuzzleItemUser, NULL, mobj->args[1], NULL, args, 3, ACS_ALWAYS); mobj->special = 0; return true; } return false; } DEFINE_ACTION_FUNCTION(AActor, UsePuzzleItem) { PARAM_SELF_PROLOGUE(AActor); PARAM_INT(puzznum); ACTION_RETURN_BOOL(P_UsePuzzleItem(self, puzznum)); } //========================================================================== // // RADIUS ATTACK // // //========================================================================== // [RH] Damage scale to apply to thing that shot the missile. static float selfthrustscale; CUSTOM_CVAR(Float, splashfactor, 1.f, CVAR_SERVERINFO) { if (self <= 0.f) self = 1.f; else selfthrustscale = 1.f / self; } //========================================================================== // // P_GetRadiusDamage // // Part of P_RadiusAttack, separated so the GetRadiusAttack function can // exist without needing to maintain more than one function. // // Used by anything without OLDRADIUSDMG flag //========================================================================== static double P_GetRadiusDamage(bool fromaction, AActor *bombspot, AActor *thing, int bombdamage, int bombdistance, int fulldamagedistance, bool thingbombsource) { // [RH] New code. The bounding box only covers the // height of the thing and not the height of the map. double points; double len; double dx, dy; double boxradius; double bombdistancefloat = 1. / (double)(bombdistance - fulldamagedistance); double bombdamagefloat = (double)bombdamage; DVector2 vec = bombspot->Vec2To(thing); dx = fabs(vec.X); dy = fabs(vec.Y); boxradius = thing->radius; // The damage pattern is square, not circular. len = double(dx > dy ? dx : dy); if (bombspot->Z() < thing->Z() || bombspot->Z() >= thing->Top()) { double dz; if (bombspot->Z() > thing->Z()) { dz = double(bombspot->Z() - thing->Top()); } else { dz = double(thing->Z() - bombspot->Z()); } if (len <= boxradius) { len = dz; } else { len -= boxradius; len = g_sqrt(len*len + dz*dz); } } else { len -= boxradius; if (len < 0.f) len = 0.f; } len = clamp(len - (double)fulldamagedistance, 0, len); points = bombdamagefloat * (1. - len * bombdistancefloat); // Calculate the splash and radius damage factor if called by P_RadiusAttack. // Otherwise, just get the raw damage. This allows modders to manipulate it // however they want. if (!fromaction) { if (thingbombsource) //thing is bomb source { points = points * splashfactor; } points *= thing->RadiusDamageFactor; } return points; } //========================================================================== // // P_GetOldRadiusDamage // // Part of P_RadiusAttack, separated so the GetRadiusAttack function can // exist without needing to maintain more than one function. // // Used by barrels (OLDRADIUSDMG flag). Returns calculated damage // based on XY distance. //========================================================================== static int P_GetOldRadiusDamage(bool fromaction, AActor *bombspot, AActor *thing, int bombdamage, int bombdistance, int fulldamagedistance) { const int ret = fromaction ? 0 : -1; // -1 is specifically for P_RadiusAttack; continue onto another actor. double dx, dy, dist; DVector2 vec = bombspot->Vec2To(thing); dx = fabs(vec.X); dy = fabs(vec.Y); dist = dx>dy ? dx : dy; dist -= thing->radius; if (dist < 0) dist = 0; if (dist >= bombdistance) return ret; // out of range // When called from the action function, ignore the sight check. if (fromaction || P_CheckSight(thing, bombspot, SF_IGNOREVISIBILITY | SF_IGNOREWATERBOUNDARY)) { dist = clamp(dist - fulldamagedistance, 0, dist); int damage = Scale(bombdamage, bombdistance - int(dist), bombdistance); if (!fromaction) { double factor = splashfactor * thing->RadiusDamageFactor; damage = int(damage * factor); } return damage; } return ret; // Not in sight. } //========================================================================== // // GetRadiusDamage // // Returns the falloff damage from an A_Explode attack without doing any // damage and not taking into account any damage reduction. //========================================================================== DEFINE_ACTION_FUNCTION(AActor, GetRadiusDamage) { PARAM_SELF_PROLOGUE(AActor); PARAM_OBJECT(thing, AActor); PARAM_INT(damage); PARAM_INT(distance); PARAM_INT_DEF(fulldmgdistance); PARAM_BOOL_DEF(oldradiusdmg); if (!thing) { ACTION_RETURN_INT(0); } else if (thing == self) { // No point in calculating falloff in this case since it is the bomb spot. ACTION_RETURN_INT(damage); } fulldmgdistance = clamp(fulldmgdistance, 0, distance - 1); // Mirroring A_Explode's behavior. if (distance <= 0) distance = damage; const int newdam = oldradiusdmg ? P_GetOldRadiusDamage(true, self, thing, damage, distance, fulldmgdistance) : int(P_GetRadiusDamage(true, self, thing, damage, distance, fulldmgdistance, false)); ACTION_RETURN_INT(newdam); } //========================================================================== // // P_RadiusAttack // Source is the creature that caused the explosion at spot. // //========================================================================== int P_RadiusAttack(AActor *bombspot, AActor *bombsource, int bombdamage, int bombdistance, FName bombmod, int flags, int fulldamagedistance) { if (bombdistance <= 0) return 0; fulldamagedistance = clamp(fulldamagedistance, 0, bombdistance - 1); FPortalGroupArray grouplist(FPortalGroupArray::PGA_Full3d); FMultiBlockThingsIterator it(grouplist, bombspot->X(), bombspot->Y(), bombspot->Z() - bombdistance, bombspot->Height + bombdistance*2, bombdistance, false, bombspot->Sector); FMultiBlockThingsIterator::CheckResult cres; if (flags & RADF_SOURCEISSPOT) { // The source is actually the same as the spot, even if that wasn't what we received. bombsource = bombspot; } int count = 0; while ((it.Next(&cres))) { AActor *thing = cres.thing; // Vulnerable actors can be damaged by radius attacks even if not shootable // Used to emulate MBF's vulnerability of non-missile bouncers to explosions. if (!((thing->flags & MF_SHOOTABLE) || (thing->flags6 & MF6_VULNERABLE))) continue; // Boss spider and cyborg and Heretic's ep >= 2 bosses // take no damage from concussion. if (thing->flags3 & MF3_NORADIUSDMG && !(bombspot->flags4 & MF4_FORCERADIUSDMG)) continue; if (!(flags & RADF_HURTSOURCE) && (thing == bombsource || thing == bombspot)) { // don't damage the source of the explosion continue; } // a much needed option: monsters that fire explosive projectiles cannot // be hurt by projectiles fired by a monster of the same type. // Controlled by the DONTHARMCLASS and DONTHARMSPECIES flags. if ((bombsource && !thing->player) // code common to both checks && ( // Class check first ((bombsource->flags4 & MF4_DONTHARMCLASS) && (thing->GetClass() == bombsource->GetClass())) || // Nigh-identical species check second ((bombsource->flags6 & MF6_DONTHARMSPECIES) && (thing->GetSpecies() == bombsource->GetSpecies())) ) ) continue; // Barrels always use the original code, since this makes // them far too "active." BossBrains also use the old code // because some user levels require they have a height of 16, // which can make them near impossible to hit with the new code. if ((flags & RADF_NODAMAGE) || !((bombspot->flags5 | thing->flags5) & MF5_OLDRADIUSDMG)) { double points = P_GetRadiusDamage(false, bombspot, thing, bombdamage, bombdistance, fulldamagedistance, bombsource == thing); double check = int(points) * bombdamage; // points and bombdamage should be the same sign (the double cast of 'points' is needed to prevent overflows and incorrect values slipping through.) if ((check > 0 || (check == 0 && bombspot->flags7 & MF7_FORCEZERORADIUSDMG)) && P_CheckSight(thing, bombspot, SF_IGNOREVISIBILITY | SF_IGNOREWATERBOUNDARY)) { // OK to damage; target is in direct path double vz; double thrust; int damage = abs((int)points); int newdam = damage; if (!(flags & RADF_NODAMAGE)) { //[MC] Don't count actors saved by buddha if already at 1 health. int prehealth = thing->health; newdam = P_DamageMobj(thing, bombspot, bombsource, damage, bombmod); if (thing->health < prehealth) count++; } else if (thing->player == NULL && (!(flags & RADF_NOIMPACTDAMAGE) && !(thing->flags7 & MF7_DONTTHRUST))) thing->flags2 |= MF2_BLASTED; if (!(thing->flags & MF_ICECORPSE)) { if (!(flags & RADF_NODAMAGE) && !(bombspot->flags3 & MF3_BLOODLESSIMPACT)) P_TraceBleed(newdam > 0 ? newdam : damage, thing, bombspot); if ((flags & RADF_NODAMAGE) || !(bombspot->flags2 & MF2_NODMGTHRUST)) { if (bombsource == NULL || !(bombsource->flags2 & MF2_NODMGTHRUST)) { if (!(thing->flags7 & MF7_DONTTHRUST)) { thrust = points * 0.5 / (double)thing->Mass; if (bombsource == thing) { thrust *= selfthrustscale; } vz = (thing->Center() - bombspot->Z()) * thrust; if (bombsource != thing) { vz *= 0.5; } else { vz *= 0.8; } thing->Thrust(bombspot->AngleTo(thing), thrust); if (!(flags & RADF_NODAMAGE) || (flags & RADF_THRUSTZ)) thing->Vel.Z += vz; // this really doesn't work well } } } } } } else { // [RH] Old code just for barrels int damage = P_GetOldRadiusDamage(false, bombspot, thing, bombdamage, bombdistance, fulldamagedistance); if (damage < 0) continue; // Sight check failed. else if (damage > 0 || (bombspot->flags7 & MF7_FORCEZERORADIUSDMG)) { // OK to damage; target is in direct path //[MC] Don't count actors saved by buddha if already at 1 health. int prehealth = thing->health; int newdam = P_DamageMobj(thing, bombspot, bombsource, damage, bombmod); P_TraceBleed(newdam > 0 ? newdam : damage, thing, bombspot); if (thing->health < prehealth) count++; } } } return count; } //========================================================================== // // SECTOR HEIGHT CHANGING // After modifying a sector's floor or ceiling height, // call this routine to adjust the positions // of all things that touch the sector. // // If anything doesn't fit anymore, true will be returned. // // [RH] If crushchange is non-negative, they will take the // specified amount of damage as they are being crushed. // If crushchange is negative, you should set the sector // height back the way it was and call P_ChangeSector() // again to undo the changes. // Note that this is very different from the original // true/false usage of crushchange! If you want regular // DOOM crushing behavior set crushchange to 10 or -1 // if no crushing is desired. // //========================================================================== struct FChangePosition { sector_t *sector; double moveamt; int crushchange; bool nofit; bool movemidtex; bool instant; }; TArray intersectors; EXTERN_CVAR(Int, cl_bloodtype) //============================================================================= // // P_AdjustFloorCeil // //============================================================================= bool P_AdjustFloorCeil(AActor *thing, FChangePosition *cpos) { ActorFlags2 flags2 = thing->flags2 & MF2_PASSMOBJ; FCheckPosition tm; if ((thing->flags2 & MF2_PASSMOBJ) && (thing->flags3 & MF3_ISMONSTER)) { tm.FromPMove = true; } if (cpos->movemidtex) { // From Eternity: // ALL things must be treated as PASSMOBJ when moving // 3DMidTex lines, otherwise you get stuck in them. thing->flags2 |= MF2_PASSMOBJ; } bool isgood = P_CheckPosition(thing, thing->Pos(), tm); if (!(thing->flags4 & MF4_ACTLIKEBRIDGE)) { thing->floorz = tm.floorz; thing->ceilingz = tm.ceilingz; thing->dropoffz = tm.dropoffz; // killough 11/98: remember dropoffs thing->floorpic = tm.floorpic; thing->floorterrain = tm.floorterrain; thing->floorsector = tm.floorsector; thing->ceilingpic = tm.ceilingpic; thing->ceilingsector = tm.ceilingsector; } else { // Bridges only keep the info at their spawn position // This is necessary to prevent moving sectors from altering the bridge's z-position. // The bridge should remain at its current z, even if the sector change would cause // floorz or ceilingz to be changed in a way that would make P_ZMovement adjust the bridge. P_FindFloorCeiling(thing, FFCF_ONLYSPAWNPOS); } // restore the PASSMOBJ flag but leave the other flags alone. thing->flags2 = (thing->flags2 & ~MF2_PASSMOBJ) | flags2; return isgood; } //============================================================================= // // P_FindAboveIntersectors // //============================================================================= void P_FindAboveIntersectors(AActor *actor) { if (actor->flags & MF_NOCLIP) return; if (!(actor->flags & MF_SOLID)) return; FPortalGroupArray check; FMultiBlockThingsIterator it(check, actor); FMultiBlockThingsIterator::CheckResult cres; while (it.Next(&cres)) { AActor *thing = cres.thing; double blockdist = actor->radius + thing->radius; if (fabs(thing->X() - cres.Position.X) >= blockdist || fabs(thing->Y() - cres.Position.Y) >= blockdist) continue; if (!(thing->flags & MF_SOLID)) { // Can't hit thing continue; } if (thing->flags & (MF_SPECIAL)) { // [RH] Corpses and specials don't block moves continue; } if (thing->flags & (MF_CORPSE)) { // Corpses need a few more checks if (!(actor->flags & MF_ICECORPSE)) continue; } if (thing == actor) { // Don't clip against self continue; } if (!((thing->flags2 | actor->flags2) & MF2_PASSMOBJ) && !((thing->flags3 | actor->flags3) & MF3_ISMONSTER)) { // Don't bother if both things don't have MF2_PASSMOBJ set and aren't monsters. // These things would always block each other which in nearly every situation is // not what is wanted here. continue; } if (thing->Z() > actor->Z() && thing->Z() <= actor->Top()) { // Thing intersects above the base intersectors.Push(thing); } } } //============================================================================= // // P_FindBelowIntersectors // //============================================================================= void P_FindBelowIntersectors(AActor *actor) { if (actor->flags & MF_NOCLIP) return; if (!(actor->flags & MF_SOLID)) return; FPortalGroupArray check; FMultiBlockThingsIterator it(check, actor); FMultiBlockThingsIterator::CheckResult cres; while (it.Next(&cres)) { AActor *thing = cres.thing; double blockdist = actor->radius + thing->radius; if (fabs(thing->X() - cres.Position.X) >= blockdist || fabs(thing->Y() - cres.Position.Y) >= blockdist) continue; if (!(thing->flags & MF_SOLID)) { // Can't hit thing continue; } if (thing->flags & (MF_SPECIAL)) { // [RH] Corpses and specials don't block moves continue; } if (thing->flags & (MF_CORPSE)) { // Corpses need a few more checks if (!(actor->flags & MF_ICECORPSE)) continue; } if (thing == actor) { // Don't clip against self continue; } if (!((thing->flags2 | actor->flags2) & MF2_PASSMOBJ) && !((thing->flags3 | actor->flags3) & MF3_ISMONSTER)) { // Don't bother if both things don't have MF2_PASSMOBJ set and aren't monsters. // These things would always block each other which in nearly every situation is // not what is wanted here. continue; } if (thing->Top() <= actor->Top() && thing->Top() > actor->Z()) { // Thing intersects below the base intersectors.Push(thing); } } } //============================================================================= // // P_DoCrunch // //============================================================================= void P_DoCrunch(AActor *thing, FChangePosition *cpos) { if (!(thing && thing->Grind(true) && cpos)) return; cpos->nofit = true; if ((cpos->crushchange > 0) && !(level.maptime & 3)) { int newdam = P_DamageMobj(thing, NULL, NULL, cpos->crushchange, NAME_Crush); // spray blood in a random direction if (!(thing->flags2&(MF2_INVULNERABLE | MF2_DORMANT))) { if (!(thing->flags&MF_NOBLOOD)) { PClassActor *bloodcls = thing->GetBloodType(); P_TraceBleed (newdam > 0 ? newdam : cpos->crushchange, thing); if (bloodcls != NULL) { AActor *mo; mo = Spawn(bloodcls, thing->PosPlusZ(thing->Height / 2), ALLOW_REPLACE); mo->Vel.X = pr_crunch.Random2() / 16.; mo->Vel.Y = pr_crunch.Random2() / 16.; if (thing->BloodTranslation != 0 && !(mo->flags2 & MF2_DONTTRANSLATE)) { mo->Translation = thing->BloodTranslation; } if (mo->flags5 & MF5_PUFFGETSOWNER) { mo->target = thing; } if (!(cl_bloodtype <= 1)) mo->renderflags |= RF_INVISIBLE; } DAngle an = (M_Random() - 128) * (360./256); if (cl_bloodtype >= 1) { P_DrawSplash2(32, thing->PosPlusZ(thing->Height/2), an, 2, thing->BloodColor); } } if (thing->CrushPainSound != 0 && !S_GetSoundPlayingInfo(thing, thing->CrushPainSound)) { S_Sound(thing, CHAN_VOICE, thing->CrushPainSound, 1.f, ATTN_NORM); } } } // keep checking (crush other things) return; } //============================================================================= // // P_PushUp // // Returns 0 if thing fits, 1 if ceiling got in the way, or 2 if something // above it didn't fit. //============================================================================= int P_PushUp(AActor *thing, FChangePosition *cpos) { unsigned int firstintersect = intersectors.Size(); unsigned int lastintersect; int mymass = thing->Mass; if (thing->Top() > thing->ceilingz) { return 1; } // [GZ] Skip thing intersect test for THRUACTORS things. if (thing->flags2 & MF2_THRUACTORS) return 0; P_FindAboveIntersectors(thing); lastintersect = intersectors.Size(); for (; firstintersect < lastintersect; firstintersect++) { AActor *intersect = intersectors[firstintersect]; // [GZ] Skip this iteration for THRUSPECIES things // Should there be MF2_THRUGHOST / MF3_GHOST checks there too for consistency? // Or would that risk breaking established behavior? THRUGHOST, like MTHRUSPECIES, // is normally for projectiles which would have exploded by now anyway... if (thing->flags6 & MF6_THRUSPECIES && thing->GetSpecies() == intersect->GetSpecies()) continue; if ((thing->flags & MF_MISSILE) && (intersect->flags2 & MF2_REFLECTIVE) && (intersect->flags7 & MF7_THRUREFLECT)) continue; if (!P_CanCollideWith(thing, intersect)) continue; if (!(intersect->flags2 & MF2_PASSMOBJ) || (!(intersect->flags3 & MF3_ISMONSTER) && intersect->Mass > mymass) || (intersect->flags4 & MF4_ACTLIKEBRIDGE) ) { // Can't push bridges or things more massive than ourself return 2; } double oldz = intersect->Z(); P_AdjustFloorCeil(intersect, cpos); intersect->SetZ(thing->Top() + 1/65536.); if (P_PushUp(intersect, cpos)) { // Move blocked P_DoCrunch(intersect, cpos); intersect->SetZ(oldz); return 2; } if (cpos->instant) { intersect->Prev.Z += intersect->Z() - oldz; if (intersect->CheckLocalView(consoleplayer)) R_ResetViewInterpolation(); } intersect->UpdateRenderSectorList(); } thing->CheckPortalTransition(true); return 0; } //============================================================================= // // P_PushDown // // Returns 0 if thing fits, 1 if floor got in the way, or 2 if something // below it didn't fit. //============================================================================= int P_PushDown(AActor *thing, FChangePosition *cpos) { unsigned int firstintersect = intersectors.Size(); unsigned int lastintersect; int mymass = thing->Mass; if (thing->Z() <= thing->floorz) { return 1; } P_FindBelowIntersectors(thing); lastintersect = intersectors.Size(); for (; firstintersect < lastintersect; firstintersect++) { AActor *intersect = intersectors[firstintersect]; // [GZ] Skip this iteration for THRUSPECIES things // Should there be MF2_THRUGHOST / MF3_GHOST checks there too for consistency? // Or would that risk breaking established behavior? THRUGHOST, like MTHRUSPECIES, // is normally for projectiles which would have exploded by now anyway... if (thing->flags6 & MF6_THRUSPECIES && thing->GetSpecies() == intersect->GetSpecies()) continue; if ((thing->flags & MF_MISSILE) && (intersect->flags2 & MF2_REFLECTIVE) && (intersect->flags7 & MF7_THRUREFLECT)) continue; if (!P_CanCollideWith(thing, intersect)) continue; if (!(intersect->flags2 & MF2_PASSMOBJ) || (!(intersect->flags3 & MF3_ISMONSTER) && intersect->Mass > mymass) || (intersect->flags4 & MF4_ACTLIKEBRIDGE) ) { // Can't push bridges or things more massive than ourself return 2; } double oldz = intersect->Z(); P_AdjustFloorCeil(intersect, cpos); if (oldz > thing->Z() - intersect->Height) { // Only push things down, not up. intersect->SetZ(thing->Z() - intersect->Height); if (P_PushDown(intersect, cpos)) { // Move blocked P_DoCrunch(intersect, cpos); intersect->SetZ(oldz); return 2; } intersect->UpdateRenderSectorList(); } } thing->CheckPortalTransition(true); return 0; } //============================================================================= // // PIT_FloorDrop // //============================================================================= void PIT_FloorDrop(AActor *thing, FChangePosition *cpos) { double oldfloorz = thing->floorz; double oldz = thing->Z(); P_AdjustFloorCeil(thing, cpos); if (oldfloorz == thing->floorz) return; if (thing->flags4 & MF4_ACTLIKEBRIDGE) return; // do not move bridge things if (thing->Vel.Z == 0 && (!(thing->flags & MF_NOGRAVITY) || (thing->Z() == oldfloorz && !(thing->flags & MF_NOLIFTDROP)))) { if ((thing->flags & MF_NOGRAVITY) || (thing->flags5 & MF5_MOVEWITHSECTOR) || (((cpos->sector->Flags & SECF_FLOORDROP) || cpos->moveamt < 9) && thing->Z() - thing->floorz <= cpos->moveamt)) { if (cpos->instant) { thing->Prev.Z += thing->floorz - oldz; if (thing->CheckLocalView(consoleplayer)) R_ResetViewInterpolation(); } thing->SetZ(thing->floorz); P_CheckFakeFloorTriggers(thing, oldz); thing->UpdateRenderSectorList(); } } else if ((thing->Z() != oldfloorz && !(thing->flags & MF_NOLIFTDROP))) { if ((thing->flags & MF_NOGRAVITY) && (thing->flags6 & MF6_RELATIVETOFLOOR)) { if (cpos->instant) { thing->Prev.Z += -oldfloorz + thing->floorz; if (thing->CheckLocalView(consoleplayer)) R_ResetViewInterpolation(); } thing->AddZ(-oldfloorz + thing->floorz); P_CheckFakeFloorTriggers(thing, oldz); thing->UpdateRenderSectorList(); } } if (thing->player && thing->player->mo == thing) { thing->player->viewz += thing->Z() - oldz; } } //============================================================================= // // PIT_FloorRaise // //============================================================================= void PIT_FloorRaise(AActor *thing, FChangePosition *cpos) { double oldfloorz = thing->floorz; double oldz = thing->Z(); P_AdjustFloorCeil(thing, cpos); if (oldfloorz == thing->floorz) return; // Move things intersecting the floor up if (thing->Z() <= thing->floorz) { if (thing->flags4 & MF4_ACTLIKEBRIDGE) { cpos->nofit = true; return; // do not move bridge things } intersectors.Clear(); if (cpos->instant) { thing->Prev.Z += thing->floorz - thing->Z(); if (thing->CheckLocalView(consoleplayer)) R_ResetViewInterpolation(); } thing->SetZ(thing->floorz); } else { if ((thing->flags & MF_NOGRAVITY) && (thing->flags6 & MF6_RELATIVETOFLOOR)) { intersectors.Clear(); thing->AddZ(-oldfloorz + thing->floorz); if (cpos->instant) { thing->Prev.Z += -oldfloorz + thing->floorz; if (thing->CheckLocalView(consoleplayer)) R_ResetViewInterpolation(); } } else return; } switch (P_PushUp(thing, cpos)) { default: P_CheckFakeFloorTriggers(thing, oldz); thing->UpdateRenderSectorList(); break; case 1: P_DoCrunch(thing, cpos); P_CheckFakeFloorTriggers(thing, oldz); thing->UpdateRenderSectorList(); break; case 2: P_DoCrunch(thing, cpos); thing->SetZ(oldz); break; } if (thing->player && thing->player->mo == thing) { thing->player->viewz += thing->Z() - oldz; } } //============================================================================= // // PIT_CeilingLower // //============================================================================= void PIT_CeilingLower(AActor *thing, FChangePosition *cpos) { bool onfloor; double oldz = thing->Z(); onfloor = thing->Z() <= thing->floorz; P_AdjustFloorCeil(thing, cpos); if (thing->Top() > thing->ceilingz) { if (thing->flags4 & MF4_ACTLIKEBRIDGE) { cpos->nofit = true; return; // do not move bridge things } intersectors.Clear(); if (thing->ceilingz - thing->Height >= thing->floorz) { thing->SetZ(thing->ceilingz - thing->Height); } else { thing->SetZ(thing->floorz); } thing->UpdateRenderSectorList(); switch (P_PushDown(thing, cpos)) { case 2: // intentional fall-through case 1: if (onfloor) thing->SetZ(thing->floorz); P_DoCrunch(thing, cpos); P_CheckFakeFloorTriggers(thing, oldz); thing->UpdateRenderSectorList(); break; default: P_CheckFakeFloorTriggers(thing, oldz); thing->UpdateRenderSectorList(); break; } } if (thing->player && thing->player->mo == thing) { thing->player->viewz += thing->Z() - oldz; } } //============================================================================= // // PIT_CeilingRaise // //============================================================================= void PIT_CeilingRaise(AActor *thing, FChangePosition *cpos) { bool isgood = P_AdjustFloorCeil(thing, cpos); double oldz = thing->Z(); if (thing->flags4 & MF4_ACTLIKEBRIDGE) return; // do not move bridge things // For DOOM compatibility, only move things that are inside the floor. // (or something else?) Things marked as hanging from the ceiling will // stay where they are. if (thing->Z() < thing->floorz && thing->Top() >= thing->ceilingz - cpos->moveamt && !(thing->flags & MF_NOLIFTDROP)) { thing->SetZ(thing->floorz); if (thing->Top() > thing->ceilingz) { thing->SetZ(thing->ceilingz - thing->Height); } P_CheckFakeFloorTriggers(thing, oldz); thing->UpdateRenderSectorList(); } else if ((thing->flags2 & MF2_PASSMOBJ) && !isgood && thing->Top() < thing->ceilingz) { AActor *onmobj; if (!P_TestMobjZ(thing, true, &onmobj) && onmobj->Z() <= thing->Z()) { thing->SetZ(MIN(thing->ceilingz - thing->Height, onmobj->Top())); thing->UpdateRenderSectorList(); } } if (thing->player && thing->player->mo == thing) { thing->player->viewz += thing->Z() - oldz; } } //============================================================================= // // P_ChangeSector [RH] Was P_CheckSector in BOOM // // jff 3/19/98 added to just check monsters on the periphery // of a moving sector instead of all in bounding box of the // sector. Both more accurate and faster. // //============================================================================= bool P_ChangeSector(sector_t *sector, int crunch, double amt, int floorOrCeil, bool isreset, bool instant) { FChangePosition cpos; void(*iterator)(AActor *, FChangePosition *); void(*iterator2)(AActor *, FChangePosition *) = NULL; msecnode_t *n; cpos.nofit = false; cpos.crushchange = crunch; cpos.moveamt = fabs(amt); cpos.movemidtex = false; cpos.sector = sector; cpos.instant = instant; // Also process all sectors that have 3D floors transferred from the // changed sector. if (sector->e->XFloor.attached.Size() && floorOrCeil != 2) { unsigned i; sector_t* sec; // Use different functions for the four different types of sector movement. // for 3D-floors the meaning of floor and ceiling is inverted!!! if (floorOrCeil == 1) { iterator = (amt >= 0) ? PIT_FloorRaise : PIT_FloorDrop; } else { iterator = (amt >= 0) ? PIT_CeilingRaise : PIT_CeilingLower; } for (i = 0; i < sector->e->XFloor.attached.Size(); i++) { sec = sector->e->XFloor.attached[i]; P_Recalculate3DFloors(sec); // Must recalculate the 3d floor and light lists // no thing checks for attached sectors because of heightsec if (sec->heightsec == sector) continue; for (n = sec->touching_thinglist; n; n = n->m_snext) n->visited = false; do { for (n = sec->touching_thinglist; n; n = n->m_snext) { if (!n->visited) { n->visited = true; if (!(n->m_thing->flags & MF_NOBLOCKMAP) || //jff 4/7/98 don't do these (n->m_thing->flags5 & MF5_MOVEWITHSECTOR)) { iterator(n->m_thing, &cpos); } break; } } } while (n); sec->CheckPortalPlane(!floorOrCeil); } } P_Recalculate3DFloors(sector); // Must recalculate the 3d floor and light lists // [RH] Use different functions for the four different types of sector // movement. switch (floorOrCeil) { case 0: // floor iterator = (amt < 0) ? PIT_FloorDrop : PIT_FloorRaise; break; case 1: // ceiling iterator = (amt < 0) ? PIT_CeilingLower : PIT_CeilingRaise; break; case 2: // 3dmidtex // This must check both floor and ceiling iterator = (amt < 0) ? PIT_FloorDrop : PIT_FloorRaise; iterator2 = (amt < 0) ? PIT_CeilingLower : PIT_CeilingRaise; cpos.movemidtex = true; break; default: // invalid assert(floorOrCeil > 0 && floorOrCeil < 2); return false; } // killough 4/4/98: scan list front-to-back until empty or exhausted, // restarting from beginning after each thing is processed. Avoids // crashes, and is sure to examine all things in the sector, and only // the things which are in the sector, until a steady-state is reached. // Things can arbitrarily be inserted and removed and it won't mess up. // // killough 4/7/98: simplified to avoid using complicated counter // Mark all things invalid for (n = sector->touching_thinglist; n; n = n->m_snext) n->visited = false; do { for (n = sector->touching_thinglist; n; n = n->m_snext) // go through list { if (!n->visited) // unprocessed thing found { n->visited = true; // mark thing as processed if (!(n->m_thing->flags & MF_NOBLOCKMAP) || //jff 4/7/98 don't do these (n->m_thing->flags5 & MF5_MOVEWITHSECTOR)) { iterator(n->m_thing, &cpos); // process it if (iterator2 != NULL) iterator2(n->m_thing, &cpos); } break; // exit and start over } } } while (n); // repeat from scratch until all things left are marked valid if (floorOrCeil != 2) sector->CheckPortalPlane(floorOrCeil); // check for portal obstructions after everything is done. if (!cpos.nofit && !isreset /* && sector->MoreFlags & (SECF_UNDERWATERMASK)*/) { // If this is a control sector for a deep water transfer, all actors in affected // sectors need to have their waterlevel information updated and if applicable, // execute appropriate sector actions. // Only check if the sector move was successful. TArray & secs = sector->e->FakeFloor.Sectors; for (unsigned i = 0; i < secs.Size(); i++) { sector_t * s = secs[i]; for (n = s->touching_thinglist; n; n = n->m_snext) n->visited = false; do { for (n = s->touching_thinglist; n; n = n->m_snext) // go through list { if (!n->visited && n->m_thing->Sector == s) // unprocessed thing found { n->visited = true; // mark thing as processed n->m_thing->UpdateWaterLevel(false); P_CheckFakeFloorTriggers(n->m_thing, n->m_thing->Z() - amt); } } } while (n); // repeat from scratch until all things left are marked valid } } return cpos.nofit; } //========================================================================== // // // //========================================================================== void SpawnShootDecal(AActor *t1, const FTraceResults &trace) { FDecalBase *decalbase = NULL; if (t1->player != NULL && t1->player->ReadyWeapon != NULL) { decalbase = t1->player->ReadyWeapon->GetDefault()->DecalGenerator; } else { decalbase = t1->DecalGenerator; } if (decalbase != NULL) { DImpactDecal::StaticCreate(decalbase->GetDecal(), trace.HitPos, trace.Line->sidedef[trace.Side], trace.ffloor); } } //========================================================================== // // // //========================================================================== static void SpawnDeepSplash(AActor *t1, const FTraceResults &trace, AActor *puff) { const DVector3 *hitpos; if (trace.Crossed3DWater) { hitpos = &trace.Crossed3DWaterPos; } else if (trace.CrossedWater && trace.CrossedWater->heightsec) { hitpos = &trace.CrossedWaterPos; } else return; P_HitWater(puff != NULL ? puff : t1, P_PointInSector(*hitpos), *hitpos); } //============================================================================= // // P_ActivateThingSpecial // // Handles the code for things activated by death, USESPECIAL or BUMPSPECIAL // //============================================================================= bool P_ActivateThingSpecial(AActor * thing, AActor * trigger, bool death) { bool res = false; // Target switching mechanism if (thing->activationtype & THINGSPEC_ThingTargets) thing->target = trigger; if (thing->activationtype & THINGSPEC_TriggerTargets) trigger->target = thing; // State change mechanism. The thing needs to be not dead and to have at least one of the relevant flags if (!death && (thing->activationtype & (THINGSPEC_Activate | THINGSPEC_Deactivate | THINGSPEC_Switch))) { // If a switchable thing does not know whether it should be activated // or deactivated, the default is to activate it. if ((thing->activationtype & THINGSPEC_Switch) && !(thing->activationtype & (THINGSPEC_Activate | THINGSPEC_Deactivate))) { thing->activationtype |= THINGSPEC_Activate; } // Can it be activated? if (thing->activationtype & THINGSPEC_Activate) { thing->activationtype &= ~THINGSPEC_Activate; // Clear flag if (thing->activationtype & THINGSPEC_Switch) // Set other flag if switching thing->activationtype |= THINGSPEC_Deactivate; thing->CallActivate(trigger); res = true; } // If not, can it be deactivated? else if (thing->activationtype & THINGSPEC_Deactivate) { thing->activationtype &= ~THINGSPEC_Deactivate; // Clear flag if (thing->activationtype & THINGSPEC_Switch) // Set other flag if switching thing->activationtype |= THINGSPEC_Activate; thing->CallDeactivate(trigger); res = true; } } // Run the special, if any if (thing->special) { res = !!P_ExecuteSpecial(thing->special, NULL, // TriggerActs overrides the level flag, which only concerns thing activated by death (((death && level.flags & LEVEL_ACTOWNSPECIAL && !(thing->activationtype & THINGSPEC_TriggerActs)) || (thing->activationtype & THINGSPEC_ThingActs)) // Who triggers? ? thing : trigger), false, thing->args[0], thing->args[1], thing->args[2], thing->args[3], thing->args[4]); // Clears the special if it was run on thing's death or if flag is set. // Note that Hexen originally did not clear the special which some original maps depend on (e.g. the bell in HEXDD.) if ((death && !(level.flags2 & LEVEL2_HEXENHACK)) || (thing->activationtype & THINGSPEC_ClearSpecial && res)) thing->special = 0; } // Returns the result return res; }