More progress.

- AMBUSH flag on nodes makes them blind and untargetable when getting `startnode` and `goalnode` for `FindPath()`. This is useful for indicating a node should be skipped when tele/portaling, so the current path can be preserved.
- Setup is simple: place nodes behind lines that tele/portal entities and mark them as AMBUSH.

Other changes:

- Restored global array since blockmap is not a viable option here.
- Added MAPINFO `pathing` flag which enables pathing by default.
- Added NOPATHING flag to disable pathing entirely, useful for maps that have pathing enabled.
- Added `ReachedNode(Actor mo)` virtual, responsible for handling node traversal.
- Nodes now make use of MeleeRange to limit their sight checking functions.
This commit is contained in:
Major Cooke 2024-02-16 09:22:20 -06:00 committed by Rachael Alexanderson
parent ad52e2cc1e
commit 81ebd8c8c4
14 changed files with 210 additions and 82 deletions

View file

@ -2506,26 +2506,25 @@ static void ReconstructPath(TMap<AActor*, AActor*> &cameFrom, AActor* current, T
while(tmp = cameFrom.CheckKey(*tmp)); while(tmp = cameFrom.CheckKey(*tmp));
} }
static AActor* FindClosestNode(AActor* from, double maxSearch) static AActor* FindClosestNode(AActor* from)
{ {
static PClass * nodeCls = PClass::FindClass(NAME_PathNode); static PClass * nodeCls = PClass::FindClass(NAME_PathNode);
FPortalGroupArray check(FPortalGroupArray::PGA_Full3d);
FMultiBlockThingsIterator it(check, from->Level, from->Pos().X, from->Pos().Y, from->Pos().Z - ((from->Height + maxSearch) / 2), from->Height + maxSearch, from->radius + maxSearch, false, from->Sector);
FMultiBlockThingsIterator::CheckResult res;
AActor * closest = nullptr; AActor * closest = nullptr;
double closestDist = DBL_MAX; double closestDist = DBL_MAX;
while(it.Next(&res)) for (int i = 0; i < from->Level->PathNodes.Size(); i++)
{ {
if(nodeCls->IsAncestorOf(res.thing->GetClass())) AActor* node = from->Level->PathNodes[i];
if(node && !(node->flags & MF_AMBUSH) && nodeCls->IsAncestorOf(node->GetClass()))
{ {
double dst = res.thing->Distance3D(from); double dst = node->Distance3DSquared(from);
if(dst < closestDist && P_CheckSight(res.thing, from)) bool mrange = (dst < closestDist && (node->meleerange <= 0.0 || dst < (node->meleerange * node->meleerange)));
if(mrange && P_CheckSight(node, from))
{ {
closestDist = dst; closestDist = dst;
closest = res.thing; closest = node;
} }
} }
} }
@ -2540,7 +2539,7 @@ static V GetOr(TMap<K, V> map, const K &key, V alt)
return k ? *k : alt; return k ? *k : alt;
} }
static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>> &path) static bool FindPathAStar(AActor* startnode, AActor* goalnode, TArray<TObjPtr<AActor*>> &path)
{ {
TArray<AActor*> openSet; TArray<AActor*> openSet;
@ -2548,9 +2547,9 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>>
TMap<AActor*, double> gScore; TMap<AActor*, double> gScore;
TMap<AActor*, double> fScore; TMap<AActor*, double> fScore;
openSet.Push(start); openSet.Push(startnode);
gScore.Insert(start, 0); gScore.Insert(startnode, 0);
fScore.Insert(start, start->Distance3D(goal)); fScore.Insert(startnode, startnode->Distance3DSquared(goalnode));
auto lt_fScore = [&fScore](AActor* lhs, AActor* rhs) auto lt_fScore = [&fScore](AActor* lhs, AActor* rhs)
{ {
@ -2561,7 +2560,7 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>>
{ {
AActor * current = openSet[0]; AActor * current = openSet[0];
openSet.Delete(0); openSet.Delete(0);
if(current == goal) if(current == goalnode)
{ {
ReconstructPath(cameFrom, current, path); ReconstructPath(cameFrom, current, path);
return true; return true;
@ -2572,7 +2571,7 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>>
for(AActor * neighbor : GetPathNodeNeighbors(current)) for(AActor * neighbor : GetPathNodeNeighbors(current))
{ {
double tentative_gScore = current_gScore + current->Distance3D(neighbor); double tentative_gScore = current_gScore + current->Distance3DSquared(neighbor);
double neighbor_gScore = GetOr(gScore, neighbor, DBL_MAX); double neighbor_gScore = GetOr(gScore, neighbor, DBL_MAX);
@ -2581,7 +2580,7 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>>
openSet.SortedDelete(neighbor, lt_fScore); openSet.SortedDelete(neighbor, lt_fScore);
cameFrom.Insert(neighbor, current); cameFrom.Insert(neighbor, current);
gScore.Insert(neighbor, tentative_gScore); gScore.Insert(neighbor, tentative_gScore);
fScore.Insert(neighbor, tentative_gScore + neighbor->Distance3D(goal)); fScore.Insert(neighbor, tentative_gScore + neighbor->Distance3DSquared(goalnode));
openSet.SortedInsert(neighbor, lt_fScore); openSet.SortedInsert(neighbor, lt_fScore);
} }
} }
@ -2589,20 +2588,19 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>>
return false; return false;
} }
bool FLevelLocals::FindPath(AActor* start, AActor* goal, AActor* startNode, AActor* goalNode, double maxSearch) bool FLevelLocals::FindPath(AActor* chaser, AActor* target, AActor* startNode, AActor* goalNode)
{ {
static PClass * nodeCls = PClass::FindClass(NAME_PathNode); if (!chaser || !target)
if (!start || !goal)
{ {
return false; return false;
} }
static PClass* nodeCls = PClass::FindClass(NAME_PathNode);
assert(startNode == nullptr || nodeCls->IsAncestorOf(startNode->GetClass())); assert(startNode == nullptr || nodeCls->IsAncestorOf(startNode->GetClass()));
assert(goalNode == nullptr || nodeCls->IsAncestorOf(goalNode->GetClass())); assert(goalNode == nullptr || nodeCls->IsAncestorOf(goalNode->GetClass()));
if(startNode == nullptr) startNode = FindClosestNode(start, maxSearch); if(startNode == nullptr) startNode = FindClosestNode(chaser);
if(goalNode == nullptr) goalNode = FindClosestNode(goal, maxSearch); if(goalNode == nullptr) goalNode = FindClosestNode(target);
// Incomplete graph. // Incomplete graph.
if (!startNode || !goalNode) if (!startNode || !goalNode)
@ -2612,16 +2610,16 @@ bool FLevelLocals::FindPath(AActor* start, AActor* goal, AActor* startNode, AAct
if (startNode == goalNode) if (startNode == goalNode)
{ {
start->ClearPath(); chaser->ClearPath();
start->Path.Push(MakeObjPtr<AActor*>(startNode)); chaser->Path.Push(MakeObjPtr<AActor*>(startNode));
return true; return true;
} }
if(FindPathAStar(startNode, goalNode, start->Path)) if (FindPathAStar(startNode, goalNode, chaser->Path))
{ {
if (start->goal && nodeCls->IsAncestorOf(start->goal->GetClass())) if (chaser->goal && nodeCls->IsAncestorOf(chaser->goal->GetClass()))
{ {
start->goal = nullptr; chaser->goal = nullptr;
} }
return true; return true;
} }
@ -2636,6 +2634,22 @@ DEFINE_ACTION_FUNCTION(FLevelLocals, FindPath)
PARAM_OBJECT(target, AActor); PARAM_OBJECT(target, AActor);
PARAM_OBJECT(startnode, AActor); PARAM_OBJECT(startnode, AActor);
PARAM_OBJECT(goalnode, AActor); PARAM_OBJECT(goalnode, AActor);
PARAM_FLOAT(maxSearch); return self->FindPath(chaser, target, startnode, goalnode);
return self->FindPath(chaser, target, startnode, goalnode, maxSearch); }
DEFINE_ACTION_FUNCTION(FLevelLocals, HandlePathNode)
{
PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals);
PARAM_OBJECT(node, AActor);
PARAM_BOOL(add);
if (node)
{
if (add)
{
if (self->PathNodes.Find(node) >= self->PathNodes.Size())
self->PathNodes.Push(node);
}
else self->PathNodes.Delete(self->PathNodes.Find(node));
}
return 0;
} }

View file

@ -444,8 +444,9 @@ public:
void SetMusic(); void SetMusic();
bool FindPath(AActor *chaser, AActor *target, AActor *startnode = nullptr, AActor *goalnode = nullptr, double maxSearch = 256.0); bool FindPath(AActor *chaser, AActor *target, AActor *startnode = nullptr, AActor *goalnode = nullptr);
TArray<AActor *> PathNodes;
TArray<vertex_t> vertexes; TArray<vertex_t> vertexes;
TArray<sector_t> sectors; TArray<sector_t> sectors;
TArray<extsector_t> extsectors; // container for non-trivial sector information. sector_t must be trivially copyable for *_fakeflat to work as intended. TArray<extsector_t> extsectors; // container for non-trivial sector information. sector_t must be trivially copyable for *_fakeflat to work as intended.

View file

@ -1819,6 +1819,7 @@ MapFlagHandlers[] =
{ "disableskyboxao", MITYPE_CLRFLAG3, LEVEL3_SKYBOXAO, 0 }, { "disableskyboxao", MITYPE_CLRFLAG3, LEVEL3_SKYBOXAO, 0 },
{ "avoidmelee", MITYPE_SETFLAG3, LEVEL3_AVOIDMELEE, 0 }, { "avoidmelee", MITYPE_SETFLAG3, LEVEL3_AVOIDMELEE, 0 },
{ "attenuatelights", MITYPE_SETFLAG3, LEVEL3_ATTENUATE, 0 }, { "attenuatelights", MITYPE_SETFLAG3, LEVEL3_ATTENUATE, 0 },
{ "pathing", MITYPE_SETFLAG3, LEVEL3_PATHING, 0 },
{ "nobotnodes", MITYPE_IGNORE, 0, 0 }, // Skulltag option: nobotnodes { "nobotnodes", MITYPE_IGNORE, 0, 0 }, // Skulltag option: nobotnodes
{ "nopassover", MITYPE_COMPATFLAG, COMPATF_NO_PASSMOBJ, 0 }, { "nopassover", MITYPE_COMPATFLAG, COMPATF_NO_PASSMOBJ, 0 },
{ "passover", MITYPE_CLRCOMPATFLAG, COMPATF_NO_PASSMOBJ, 0 }, { "passover", MITYPE_CLRCOMPATFLAG, COMPATF_NO_PASSMOBJ, 0 },

View file

@ -270,6 +270,7 @@ enum ELevelFlags : unsigned int
LEVEL3_AVOIDMELEE = 0x00020000, // global flag needed for proper MBF support. LEVEL3_AVOIDMELEE = 0x00020000, // global flag needed for proper MBF support.
LEVEL3_NOJUMPDOWN = 0x00040000, // only for MBF21. Inverse of MBF's dog_jumping flag. LEVEL3_NOJUMPDOWN = 0x00040000, // only for MBF21. Inverse of MBF's dog_jumping flag.
LEVEL3_LIGHTCREATED = 0x00080000, // a light had been created in the last frame LEVEL3_LIGHTCREATED = 0x00080000, // a light had been created in the last frame
LEVEL3_PATHING = 0x00100000, // enable pathfinding by default
}; };

View file

@ -326,6 +326,7 @@ void FLevelLocals::ClearLevelData(bool fullgc)
} }
ClearPortals(); ClearPortals();
PathNodes.Clear();
tagManager.Clear(); tagManager.Clear();
ClearTIDHashes(); ClearTIDHashes();
if (SpotState) SpotState->Destroy(); if (SpotState) SpotState->Destroy();

View file

@ -445,6 +445,7 @@ enum ActorFlag9
MF9_DECOUPLEDANIMATIONS = 0x00000010, // [RL0] Decouple model animations from states MF9_DECOUPLEDANIMATIONS = 0x00000010, // [RL0] Decouple model animations from states
MF9_PATHING = 0x00000020, // [MC] Enables monsters to do pathfinding, such as A*. MF9_PATHING = 0x00000020, // [MC] Enables monsters to do pathfinding, such as A*.
MF9_KEEPPATH = 0x00000040, // [MC] Forces monsters to keep to the path when target's in sight. MF9_KEEPPATH = 0x00000040, // [MC] Forces monsters to keep to the path when target's in sight.
MF9_NOPATHING = 0x00000080, // [MC] override the mapinfo "pathfinding"
}; };
// --- mobj.renderflags --- // --- mobj.renderflags ---
@ -1104,6 +1105,8 @@ public:
void SetDynamicLights(); void SetDynamicLights();
void ClearPath(); void ClearPath();
bool CanPathfind();
void CallReachedNode(AActor *node);
// info for drawing // info for drawing
// NOTE: The first member variable *must* be snext. // NOTE: The first member variable *must* be snext.

View file

@ -2208,8 +2208,12 @@ DEFINE_ACTION_FUNCTION(AActor, A_ClearLastHeard)
void AActor::ClearPath() void AActor::ClearPath()
{ {
Path.Clear(); Path.Clear();
if (goal && goal->IsKindOf(NAME_PathNode)) if (goal)
{
static PClass* nodeCls = PClass::FindClass(NAME_PathNode);
if (nodeCls->IsAncestorOf(goal->GetClass()))
goal = nullptr; goal = nullptr;
}
} }
DEFINE_ACTION_FUNCTION(AActor, ClearPath) DEFINE_ACTION_FUNCTION(AActor, ClearPath)
@ -2219,6 +2223,42 @@ DEFINE_ACTION_FUNCTION(AActor, ClearPath)
return 0; return 0;
} }
bool AActor::CanPathfind()
{
if ((!(flags9 & MF9_NOPATHING) && !(Sector->MoreFlags & SECMF_NOPATHING)) &&
(flags9 & MF9_PATHING || Level->flags3 & LEVEL3_PATHING))
{
if ((flags6 & MF6_NOFEAR))
return true;
// Can't pathfind while feared.
if (!(flags4 & MF4_FRIGHTENED))
{
if (!target)
return true;
if (!target->flags8 & MF8_FRIGHTENING)
return (!target->player || !(target->player->cheats & CF_FRIGHTENING));
}
}
return false;
}
DEFINE_ACTION_FUNCTION(AActor, CanPathfind)
{
PARAM_SELF_PROLOGUE(AActor);
return self->CanPathfind();
}
void AActor::CallReachedNode(AActor *node)
{
IFVIRTUAL(AActor, ReachedNode)
{
VMValue params[2] = { this, node };
VMCall(func, params, 2, nullptr, 0);
}
}
//========================================================================== //==========================================================================
// //
// A_Wander // A_Wander
@ -2515,30 +2555,8 @@ void A_DoChase (AActor *actor, bool fastchase, FState *meleestate, FState *missi
return; return;
} }
if (actor->target && actor->flags9 & MF9_PATHING)
{
if (actor->goal && actor->goal->IsKindOf(NAME_PathNode))
{
AActor* temp = actor->target;
actor->target = actor->goal;
bool result = P_CheckMeleeRange(actor);
actor->target = temp;
if (result) // TO DO
{
}
}
if (!actor->goal)
{
if (actor->Path.Size() < 1 && actor->Level->FindPath(actor, actor->target))
actor->goal = actor->Path[0];
}
}
// [RH] Don't attack if just moving toward goal // [RH] Don't attack if just moving toward goal
else if (actor->target == actor->goal || (actor->flags5&MF5_CHASEGOAL && actor->goal != nullptr)) if (actor->target == actor->goal || (actor->flags5&MF5_CHASEGOAL && actor->goal != nullptr))
{ {
AActor * savedtarget = actor->target; AActor * savedtarget = actor->target;
actor->target = actor->goal; actor->target = actor->goal;
@ -2617,7 +2635,45 @@ void A_DoChase (AActor *actor, bool fastchase, FState *meleestate, FState *missi
} }
} }
} }
}
if (actor->target && actor->CanPathfind())
{
if (actor->goal && !(actor->goal->flags & MF_AMBUSH) && actor->goal->IsKindOf(NAME_PathNode))
{
AActor* temp = actor->target;
actor->target = actor->goal;
bool reached = (P_CheckMeleeRange(actor));
actor->target = temp;
if (reached)
{
actor->CallReachedNode(actor->goal);
/*
AActor* next = nullptr;
if (!(actor->flags9 & MF9_KEEPPATH) &&
P_CheckSight(actor, actor->target, SF_IGNOREWATERBOUNDARY | SF_IGNOREVISIBILITY))
actor->Path.Clear();
else
{
unsigned int index = actor->Path.Find(actor->goal);
while (++index < actor->Path.Size() - 1)
{
next = actor->Path[index];
if (next && next != actor->goal)
break;
}
if (!next) actor->ClearPath();
else actor->goal = next;
}
*/
}
}
if (!actor->goal)
{
if (actor->Path.Size() > 0 || actor->Level->FindPath(actor, actor->target))
actor->goal = actor->Path[0];
}
} }
// [RH] Scared monsters attack less frequently // [RH] Scared monsters attack less frequently
@ -2670,7 +2726,7 @@ void A_DoChase (AActor *actor, bool fastchase, FState *meleestate, FState *missi
lookForBetter = true; lookForBetter = true;
} }
AActor * oldtarget = actor->target; AActor * oldtarget = actor->target;
gotNew = P_LookForPlayers (actor, !(flags & CHF_DONTLOOKALLAROUND), NULL); gotNew = P_LookForPlayers (actor, !(flags & CHF_DONTLOOKALLAROUND), nullptr);
if (lookForBetter) if (lookForBetter)
{ {
actor->flags3 |= MF3_NOSIGHTCHECK; actor->flags3 |= MF3_NOSIGHTCHECK;
@ -2678,6 +2734,7 @@ void A_DoChase (AActor *actor, bool fastchase, FState *meleestate, FState *missi
if (gotNew && actor->target != oldtarget) if (gotNew && actor->target != oldtarget)
{ {
actor->flags7 &= ~MF7_INCHASE; actor->flags7 &= ~MF7_INCHASE;
actor->ClearPath();
return; // got a new target return; // got a new target
} }
} }

View file

@ -592,7 +592,7 @@ bool P_TeleportMove(AActor* thing, const DVector3 &pos, bool telefrag, bool modi
thing->CheckSectorTransition(oldsec); thing->CheckSectorTransition(oldsec);
} }
} }
thing->CallReachedNode(thing->goal);
return true; return true;
} }
@ -2613,6 +2613,7 @@ bool P_TryMove(AActor *thing, const DVector2 &pos,
thing->LinkToWorld(&ctx); thing->LinkToWorld(&ctx);
P_FindFloorCeiling(thing); P_FindFloorCeiling(thing);
thing->ClearInterpolation(); thing->ClearInterpolation();
thing->CallReachedNode(thing->goal);
portalcrossed = true; portalcrossed = true;
tm.portalstep = false; tm.portalstep = false;
} }

View file

@ -355,6 +355,7 @@ static FFlagDef ActorFlagDefs[]=
DEFINE_FLAG(MF9, DECOUPLEDANIMATIONS, AActor, flags9), DEFINE_FLAG(MF9, DECOUPLEDANIMATIONS, AActor, flags9),
DEFINE_FLAG(MF9, PATHING, AActor, flags9), DEFINE_FLAG(MF9, PATHING, AActor, flags9),
DEFINE_FLAG(MF9, KEEPPATH, AActor, flags9), DEFINE_FLAG(MF9, KEEPPATH, AActor, flags9),
DEFINE_FLAG(MF9, NOPATHING, AActor, flags9),
// Effect flags // Effect flags
DEFINE_FLAG(FX, VISIBILITYPULSE, AActor, effects), DEFINE_FLAG(FX, VISIBILITYPULSE, AActor, effects),

View file

@ -2786,6 +2786,7 @@ DEFINE_FIELD_X(LevelInfo, level_info_t, RedirectMapName)
DEFINE_FIELD_X(LevelInfo, level_info_t, teamdamage) DEFINE_FIELD_X(LevelInfo, level_info_t, teamdamage)
DEFINE_GLOBAL_NAMED(currentVMLevel, level) DEFINE_GLOBAL_NAMED(currentVMLevel, level)
DEFINE_FIELD(FLevelLocals, PathNodes)
DEFINE_FIELD(FLevelLocals, sectors) DEFINE_FIELD(FLevelLocals, sectors)
DEFINE_FIELD(FLevelLocals, lines) DEFINE_FIELD(FLevelLocals, lines)
DEFINE_FIELD(FLevelLocals, sides) DEFINE_FIELD(FLevelLocals, sides)
@ -2853,6 +2854,7 @@ DEFINE_FIELD_BIT(FLevelLocals, flags2, infinite_flight, LEVEL2_INFINITE_FLIGHT)
DEFINE_FIELD_BIT(FLevelLocals, flags2, no_dlg_freeze, LEVEL2_CONV_SINGLE_UNFREEZE) DEFINE_FIELD_BIT(FLevelLocals, flags2, no_dlg_freeze, LEVEL2_CONV_SINGLE_UNFREEZE)
DEFINE_FIELD_BIT(FLevelLocals, flags2, keepfullinventory, LEVEL2_KEEPFULLINVENTORY) DEFINE_FIELD_BIT(FLevelLocals, flags2, keepfullinventory, LEVEL2_KEEPFULLINVENTORY)
DEFINE_FIELD_BIT(FLevelLocals, flags3, removeitems, LEVEL3_REMOVEITEMS) DEFINE_FIELD_BIT(FLevelLocals, flags3, removeitems, LEVEL3_REMOVEITEMS)
DEFINE_FIELD_BIT(FLevelLocals, flags3, pathing, LEVEL3_PATHING)
DEFINE_FIELD_X(Sector, sector_t, floorplane) DEFINE_FIELD_X(Sector, sector_t, floorplane)
DEFINE_FIELD_X(Sector, sector_t, ceilingplane) DEFINE_FIELD_X(Sector, sector_t, ceilingplane)

View file

@ -44,6 +44,7 @@ DoomEdNums
5065 = InvisibleBridge8 5065 = InvisibleBridge8
9001 = MapSpot 9001 = MapSpot
9013 = MapSpotGravity 9013 = MapSpotGravity
9022 = PathNode
9024 = PatrolPoint 9024 = PatrolPoint
9025 = SecurityCamera 9025 = SecurityCamera
9026 = Spark 9026 = Spark
@ -285,7 +286,6 @@ DoomEdNums
14163 = MusicChanger, 63 14163 = MusicChanger, 63
14164 = MusicChanger, 64 14164 = MusicChanger, 64
14165 = MusicChanger 14165 = MusicChanger
14166 = PathNode
32000 = DoomBuilderCamera 32000 = DoomBuilderCamera
} }

View file

@ -664,7 +664,7 @@ class Actor : Thinker native
// called before and after triggering a teleporter // called before and after triggering a teleporter
// return false in PreTeleport() to cancel the action early // return false in PreTeleport() to cancel the action early
virtual bool PreTeleport( Vector3 destpos, double destangle, int flags ) { return true; } virtual bool PreTeleport( Vector3 destpos, double destangle, int flags ) { return true; }
virtual void PostTeleport( Vector3 destpos, double destangle, int flags ) {} virtual void PostTeleport( Vector3 destpos, double destangle, int flags ) { }
native virtual bool OkayToSwitchTarget(Actor other); native virtual bool OkayToSwitchTarget(Actor other);
native clearscope static class<Actor> GetReplacement(class<Actor> cls); native clearscope static class<Actor> GetReplacement(class<Actor> cls);
@ -706,7 +706,6 @@ class Actor : Thinker native
native void ClearFOVInterpolation(); native void ClearFOVInterpolation();
native clearscope Vector3 PosRelative(sector sec) const; native clearscope Vector3 PosRelative(sector sec) const;
native void RailAttack(FRailParams p); native void RailAttack(FRailParams p);
native void ClearPath();
native void HandleSpawnFlags(); native void HandleSpawnFlags();
native void ExplodeMissile(line lin = null, Actor target = null, bool onsky = false); native void ExplodeMissile(line lin = null, Actor target = null, bool onsky = false);
@ -799,6 +798,46 @@ class Actor : Thinker native
return true; return true;
} }
native void ClearPath();
native clearscope bool CanPathfind() const;
virtual void ReachedNode(Actor mo)
{
if (!mo && !goal)
return;
mo = goal;
let node = PathNode(mo);
if (!node || !target || (!bKEEPPATH && CheckSight(target)))
{
ClearPath();
return;
}
int i = Path.Find(node) + 1;
int end = Path.Size();
for (i; i < end; i++)
{
PathNode next = Path[i];
if (!next || next == node)
continue;
// Monsters will never 'reach' AMBUSH flagged nodes. Instead, the engine
// indicates they're reached the moment they tele/portal.
if (node.bAMBUSH && next.bAMBUSH)
continue;
goal = next;
break;
}
if (i >= end)
ClearPath();
}
native bool TryMove(vector2 newpos, int dropoff, bool missilecheck = false, FCheckPosition tm = null); native bool TryMove(vector2 newpos, int dropoff, bool missilecheck = false, FCheckPosition tm = null);
native bool CheckMove(vector2 newpos, int flags = 0, FCheckPosition tm = null); native bool CheckMove(vector2 newpos, int flags = 0, FCheckPosition tm = null);
native void NewChaseDir(); native void NewChaseDir();

View file

@ -246,36 +246,34 @@ class SpeakerIcon : Unknown
} }
} }
//=============================================================== /*
// Path Nodes =================================================================
//=============================================================== Path Nodes
=================================================================
Special flags are as follows:
AMBUSH
Node is blind. Things cannot "touch" these nodes with A_Chase.
Useful for tele/portals since the engine makes them "touch"
upon transitioning. These nodes are fast forwarded over in Actor's
ReachedNode() function.
*/
class PathNode : Actor class PathNode : Actor
{ {
// For non-connected paths. Stamina will be used to set this. Necessary for tele/portals.
private int group;
Array<PathNode> neighbors; Array<PathNode> neighbors;
Default Default
{ {
//$Arg0 "TID 1" +NOINTERACTION
//$Arg1 "TID 2"
//$Arg2 "TID 3"
//$Arg3 "TID 4"
//$Arg4 "TID 5"
//$Arg0Type 14
//$Arg1Type 14
//$Arg2Type 14
//$Arg3Type 14
//$Arg4Type 14
+NOBLOCKMAP +NOBLOCKMAP
+INVISIBLE +INVISIBLE
+DONTSPLASH +DONTSPLASH
+NOTONAUTOMAP +NOTONAUTOMAP
+NOGRAVITY // TO DO: Look into 3D variant for traversing up and down 3D floors and floating monsters. +NOGRAVITY
Radius 16;
Height 56;
RenderStyle "None"; RenderStyle "None";
MeleeRange 2048; // Sight checks limited to this. 0 = infinite. MeleeRange 0; // Sight checks limited to this. 0 = infinite. Set within map editor.
} }
// Args are TIDs. Can be one way to force single directions. // Args are TIDs. Can be one way to force single directions.
@ -295,6 +293,13 @@ class PathNode : Actor
} while (node = PathNode(it.Next())) } while (node = PathNode(it.Next()))
} }
level.HandlePathNode(self, true);
}
override void OnDestroy()
{
level.HandlePathNode(self, false);
Super.OnDestroy();
} }
// For ACS access with ScriptCall. // For ACS access with ScriptCall.

View file

@ -408,7 +408,7 @@ struct LevelLocals native
const CLUSTER_HUB = 0x00000001; // Cluster uses hub behavior const CLUSTER_HUB = 0x00000001; // Cluster uses hub behavior
native readonly Array<PathNode> PathNodes;
native Array<@Sector> Sectors; native Array<@Sector> Sectors;
native Array<@Line> Lines; native Array<@Line> Lines;
native Array<@Side> Sides; native Array<@Side> Sides;
@ -476,6 +476,7 @@ struct LevelLocals native
native readonly int compatflags; native readonly int compatflags;
native readonly int compatflags2; native readonly int compatflags2;
native readonly LevelInfo info; native readonly LevelInfo info;
native readonly bool pathing;
native String GetUDMFString(int type, int index, Name key); native String GetUDMFString(int type, int index, Name key);
native int GetUDMFInt(int type, int index, Name key); native int GetUDMFInt(int type, int index, Name key);
@ -554,7 +555,8 @@ struct LevelLocals native
native void SpawnParticle(FSpawnParticleParams p); native void SpawnParticle(FSpawnParticleParams p);
native VisualThinker SpawnVisualThinker(Class<VisualThinker> type); native VisualThinker SpawnVisualThinker(Class<VisualThinker> type);
native bool FindPath(Actor chaser, Actor target, Actor startnode = null, Actor goalnode = null, double maxSearch = 256.0); native bool FindPath(Actor chaser, Actor target, PathNode startnode = null, PathNode goalnode = null);
native void HandlePathNode(PathNode node, bool add);
} }
// a few values of this need to be readable by the play code. // a few values of this need to be readable by the play code.