mirror of
https://github.com/ZDoom/gzdoom-gles.git
synced 2024-11-24 21:21:04 +00:00
7028 lines
203 KiB
C++
7028 lines
203 KiB
C++
//-----------------------------------------------------------------------------
|
|
//
|
|
// 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 <stdlib.h>
|
|
#include <math.h>
|
|
#include <algorithm>
|
|
|
|
#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_t> spechit;
|
|
TArray<spechit_t> 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; i<newsec->e->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<DAngle>(0., bottompitch));
|
|
if (floorportalstate) EnterSectorPortal(sector_t::floor, 0, lastsector, MAX<DAngle>(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<DAngle>(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<DAngle>(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<SRailHit> RailHits;
|
|
TArray<SPortalHit> 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<double>(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<double>(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<double>(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<double>(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<int>(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<int>(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<AActor *> 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<sector_t *> & 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;
|
|
}
|