diff --git a/src/g_level.cpp b/src/g_level.cpp index 467ab6d32..fb53cadef 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -2506,26 +2506,25 @@ static void ReconstructPath(TMap &cameFrom, AActor* current, T while(tmp = cameFrom.CheckKey(*tmp)); } -static AActor* FindClosestNode(AActor* from, double maxSearch) +static AActor* FindClosestNode(AActor* from) { 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; 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); - if(dst < closestDist && P_CheckSight(res.thing, from)) + double dst = node->Distance3DSquared(from); + bool mrange = (dst < closestDist && (node->meleerange <= 0.0 || dst < (node->meleerange * node->meleerange))); + + if(mrange && P_CheckSight(node, from)) { closestDist = dst; - closest = res.thing; + closest = node; } } } @@ -2540,7 +2539,7 @@ static V GetOr(TMap map, const K &key, V alt) return k ? *k : alt; } -static bool FindPathAStar(AActor* start, AActor* goal, TArray> &path) +static bool FindPathAStar(AActor* startnode, AActor* goalnode, TArray> &path) { TArray openSet; @@ -2548,9 +2547,9 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray> TMap gScore; TMap fScore; - openSet.Push(start); - gScore.Insert(start, 0); - fScore.Insert(start, start->Distance3D(goal)); + openSet.Push(startnode); + gScore.Insert(startnode, 0); + fScore.Insert(startnode, startnode->Distance3DSquared(goalnode)); auto lt_fScore = [&fScore](AActor* lhs, AActor* rhs) { @@ -2561,7 +2560,7 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray> { AActor * current = openSet[0]; openSet.Delete(0); - if(current == goal) + if(current == goalnode) { ReconstructPath(cameFrom, current, path); return true; @@ -2572,7 +2571,7 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray> 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); @@ -2581,7 +2580,7 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray> openSet.SortedDelete(neighbor, lt_fScore); cameFrom.Insert(neighbor, current); 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); } } @@ -2589,20 +2588,19 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray> 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 (!start || !goal) + if (!chaser || !target) { return false; } + static PClass* nodeCls = PClass::FindClass(NAME_PathNode); assert(startNode == nullptr || nodeCls->IsAncestorOf(startNode->GetClass())); assert(goalNode == nullptr || nodeCls->IsAncestorOf(goalNode->GetClass())); - if(startNode == nullptr) startNode = FindClosestNode(start, maxSearch); - if(goalNode == nullptr) goalNode = FindClosestNode(goal, maxSearch); + if(startNode == nullptr) startNode = FindClosestNode(chaser); + if(goalNode == nullptr) goalNode = FindClosestNode(target); // Incomplete graph. if (!startNode || !goalNode) @@ -2612,16 +2610,16 @@ bool FLevelLocals::FindPath(AActor* start, AActor* goal, AActor* startNode, AAct if (startNode == goalNode) { - start->ClearPath(); - start->Path.Push(MakeObjPtr(startNode)); + chaser->ClearPath(); + chaser->Path.Push(MakeObjPtr(startNode)); 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; } @@ -2636,6 +2634,22 @@ DEFINE_ACTION_FUNCTION(FLevelLocals, FindPath) PARAM_OBJECT(target, AActor); PARAM_OBJECT(startnode, AActor); PARAM_OBJECT(goalnode, AActor); - PARAM_FLOAT(maxSearch); - return self->FindPath(chaser, target, startnode, goalnode, maxSearch); + return self->FindPath(chaser, target, startnode, goalnode); +} + +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; } diff --git a/src/g_levellocals.h b/src/g_levellocals.h index e7475bedb..589e1490f 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -444,8 +444,9 @@ public: 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 PathNodes; TArray vertexes; TArray sectors; TArray extsectors; // container for non-trivial sector information. sector_t must be trivially copyable for *_fakeflat to work as intended. diff --git a/src/gamedata/g_mapinfo.cpp b/src/gamedata/g_mapinfo.cpp index dfe90b2c9..71addc09e 100644 --- a/src/gamedata/g_mapinfo.cpp +++ b/src/gamedata/g_mapinfo.cpp @@ -1819,6 +1819,7 @@ MapFlagHandlers[] = { "disableskyboxao", MITYPE_CLRFLAG3, LEVEL3_SKYBOXAO, 0 }, { "avoidmelee", MITYPE_SETFLAG3, LEVEL3_AVOIDMELEE, 0 }, { "attenuatelights", MITYPE_SETFLAG3, LEVEL3_ATTENUATE, 0 }, + { "pathing", MITYPE_SETFLAG3, LEVEL3_PATHING, 0 }, { "nobotnodes", MITYPE_IGNORE, 0, 0 }, // Skulltag option: nobotnodes { "nopassover", MITYPE_COMPATFLAG, COMPATF_NO_PASSMOBJ, 0 }, { "passover", MITYPE_CLRCOMPATFLAG, COMPATF_NO_PASSMOBJ, 0 }, diff --git a/src/gamedata/g_mapinfo.h b/src/gamedata/g_mapinfo.h index b56d3b632..a36cc4d5d 100644 --- a/src/gamedata/g_mapinfo.h +++ b/src/gamedata/g_mapinfo.h @@ -270,6 +270,7 @@ enum ELevelFlags : unsigned int LEVEL3_AVOIDMELEE = 0x00020000, // global flag needed for proper MBF support. 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_PATHING = 0x00100000, // enable pathfinding by default }; diff --git a/src/p_setup.cpp b/src/p_setup.cpp index 02f89c860..e3a971af6 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -326,6 +326,7 @@ void FLevelLocals::ClearLevelData(bool fullgc) } ClearPortals(); + PathNodes.Clear(); tagManager.Clear(); ClearTIDHashes(); if (SpotState) SpotState->Destroy(); diff --git a/src/playsim/actor.h b/src/playsim/actor.h index 84af5e50d..93cd34339 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -445,6 +445,7 @@ enum ActorFlag9 MF9_DECOUPLEDANIMATIONS = 0x00000010, // [RL0] Decouple model animations from states 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_NOPATHING = 0x00000080, // [MC] override the mapinfo "pathfinding" }; // --- mobj.renderflags --- @@ -1104,6 +1105,8 @@ public: void SetDynamicLights(); void ClearPath(); + bool CanPathfind(); + void CallReachedNode(AActor *node); // info for drawing // NOTE: The first member variable *must* be snext. diff --git a/src/playsim/p_enemy.cpp b/src/playsim/p_enemy.cpp index 4a11e5a13..946f4ad60 100644 --- a/src/playsim/p_enemy.cpp +++ b/src/playsim/p_enemy.cpp @@ -2208,8 +2208,12 @@ DEFINE_ACTION_FUNCTION(AActor, A_ClearLastHeard) void AActor::ClearPath() { Path.Clear(); - if (goal && goal->IsKindOf(NAME_PathNode)) - goal = nullptr; + if (goal) + { + static PClass* nodeCls = PClass::FindClass(NAME_PathNode); + if (nodeCls->IsAncestorOf(goal->GetClass())) + goal = nullptr; + } } DEFINE_ACTION_FUNCTION(AActor, ClearPath) @@ -2219,6 +2223,42 @@ DEFINE_ACTION_FUNCTION(AActor, ClearPath) 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 @@ -2515,30 +2555,8 @@ void A_DoChase (AActor *actor, bool fastchase, FState *meleestate, FState *missi 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 - 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; 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 @@ -2670,7 +2726,7 @@ void A_DoChase (AActor *actor, bool fastchase, FState *meleestate, FState *missi lookForBetter = true; } AActor * oldtarget = actor->target; - gotNew = P_LookForPlayers (actor, !(flags & CHF_DONTLOOKALLAROUND), NULL); + gotNew = P_LookForPlayers (actor, !(flags & CHF_DONTLOOKALLAROUND), nullptr); if (lookForBetter) { actor->flags3 |= MF3_NOSIGHTCHECK; @@ -2678,6 +2734,7 @@ void A_DoChase (AActor *actor, bool fastchase, FState *meleestate, FState *missi if (gotNew && actor->target != oldtarget) { actor->flags7 &= ~MF7_INCHASE; + actor->ClearPath(); return; // got a new target } } diff --git a/src/playsim/p_map.cpp b/src/playsim/p_map.cpp index d6a828024..57e3b08f0 100644 --- a/src/playsim/p_map.cpp +++ b/src/playsim/p_map.cpp @@ -592,7 +592,7 @@ bool P_TeleportMove(AActor* thing, const DVector3 &pos, bool telefrag, bool modi thing->CheckSectorTransition(oldsec); } } - + thing->CallReachedNode(thing->goal); return true; } @@ -2613,6 +2613,7 @@ bool P_TryMove(AActor *thing, const DVector2 &pos, thing->LinkToWorld(&ctx); P_FindFloorCeiling(thing); thing->ClearInterpolation(); + thing->CallReachedNode(thing->goal); portalcrossed = true; tm.portalstep = false; } diff --git a/src/scripting/thingdef_data.cpp b/src/scripting/thingdef_data.cpp index 363a8ebb3..9f835aff8 100644 --- a/src/scripting/thingdef_data.cpp +++ b/src/scripting/thingdef_data.cpp @@ -355,6 +355,7 @@ static FFlagDef ActorFlagDefs[]= DEFINE_FLAG(MF9, DECOUPLEDANIMATIONS, AActor, flags9), DEFINE_FLAG(MF9, PATHING, AActor, flags9), DEFINE_FLAG(MF9, KEEPPATH, AActor, flags9), + DEFINE_FLAG(MF9, NOPATHING, AActor, flags9), // Effect flags DEFINE_FLAG(FX, VISIBILITYPULSE, AActor, effects), diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp index 450d4552a..766dcddf9 100644 --- a/src/scripting/vmthunks.cpp +++ b/src/scripting/vmthunks.cpp @@ -2786,6 +2786,7 @@ DEFINE_FIELD_X(LevelInfo, level_info_t, RedirectMapName) DEFINE_FIELD_X(LevelInfo, level_info_t, teamdamage) DEFINE_GLOBAL_NAMED(currentVMLevel, level) +DEFINE_FIELD(FLevelLocals, PathNodes) DEFINE_FIELD(FLevelLocals, sectors) DEFINE_FIELD(FLevelLocals, lines) 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, keepfullinventory, LEVEL2_KEEPFULLINVENTORY) 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, ceilingplane) diff --git a/wadsrc/static/mapinfo/common.txt b/wadsrc/static/mapinfo/common.txt index 83482b8b6..c41dca18e 100644 --- a/wadsrc/static/mapinfo/common.txt +++ b/wadsrc/static/mapinfo/common.txt @@ -44,6 +44,7 @@ DoomEdNums 5065 = InvisibleBridge8 9001 = MapSpot 9013 = MapSpotGravity + 9022 = PathNode 9024 = PatrolPoint 9025 = SecurityCamera 9026 = Spark @@ -285,7 +286,6 @@ DoomEdNums 14163 = MusicChanger, 63 14164 = MusicChanger, 64 14165 = MusicChanger - 14166 = PathNode 32000 = DoomBuilderCamera } diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index d725ff129..049661624 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -664,7 +664,7 @@ class Actor : Thinker native // called before and after triggering a teleporter // return false in PreTeleport() to cancel the action early 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 clearscope static class GetReplacement(class cls); @@ -706,7 +706,6 @@ class Actor : Thinker native native void ClearFOVInterpolation(); native clearscope Vector3 PosRelative(sector sec) const; native void RailAttack(FRailParams p); - native void ClearPath(); native void HandleSpawnFlags(); native void ExplodeMissile(line lin = null, Actor target = null, bool onsky = false); @@ -798,6 +797,46 @@ class Actor : Thinker native movecount = random[TryWalk](0, 15); 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 CheckMove(vector2 newpos, int flags = 0, FCheckPosition tm = null); diff --git a/wadsrc/static/zscript/actors/shared/sharedmisc.zs b/wadsrc/static/zscript/actors/shared/sharedmisc.zs index be43c0147..48bb71ed4 100644 --- a/wadsrc/static/zscript/actors/shared/sharedmisc.zs +++ b/wadsrc/static/zscript/actors/shared/sharedmisc.zs @@ -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 { - // For non-connected paths. Stamina will be used to set this. Necessary for tele/portals. - private int group; - Array neighbors; Default { - //$Arg0 "TID 1" - //$Arg1 "TID 2" - //$Arg2 "TID 3" - //$Arg3 "TID 4" - //$Arg4 "TID 5" - //$Arg0Type 14 - //$Arg1Type 14 - //$Arg2Type 14 - //$Arg3Type 14 - //$Arg4Type 14 + +NOINTERACTION +NOBLOCKMAP +INVISIBLE +DONTSPLASH +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"; - 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. @@ -295,6 +293,13 @@ class PathNode : Actor } while (node = PathNode(it.Next())) } + level.HandlePathNode(self, true); + } + + override void OnDestroy() + { + level.HandlePathNode(self, false); + Super.OnDestroy(); } // For ACS access with ScriptCall. diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index 93a556db8..600517df9 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -408,7 +408,7 @@ struct LevelLocals native const CLUSTER_HUB = 0x00000001; // Cluster uses hub behavior - + native readonly Array PathNodes; native Array<@Sector> Sectors; native Array<@Line> Lines; native Array<@Side> Sides; @@ -476,6 +476,7 @@ struct LevelLocals native native readonly int compatflags; native readonly int compatflags2; native readonly LevelInfo info; + native readonly bool pathing; native String GetUDMFString(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 VisualThinker SpawnVisualThinker(Class 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.