diff --git a/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md b/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md
index 481d75f5..58a3e536 100644
--- a/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md
+++ b/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md
@@ -81,6 +81,9 @@ ActorObjects are the basic object that represents Users and NPCs
- [ActorObject.SetTameMastery(mobId int, newSkillLevel int)](#actorobjectsettamemasterymobid-int-newskilllevel-int)
- [ActorObject.GetChanceToTame(target ScriptActor) int](#actorobjectgetchancetotametarget-scriptactor-int)
- [ActorObject.GetStatMod(statModName string) int](#actorobjectgetstatmodstatmodname-string-int)
+ - [ActorObject.IsHome() bool](#actorobjectishome-bool)
+ - [ActorObject.Pathing() bool](#actorobjectpathing-bool)
+ - [ActorObject.PathingAtWaypoint() bool](#actorobjectpathingatwaypoint-bool)
@@ -553,3 +556,12 @@ returns the total specific statmod from worn items and buffs
| Argument | Explanation |
| --- | --- |
| statModName | The name of the special stat mod, such as "strength" or "tame" |
+
+## [ActorObject.IsHome() bool](/internal/scripting/actor_func.go)
+(mobs only) Returns true if the actor is at their home roomId
+
+## [ActorObject.Pathing() bool](/internal/scripting/actor_func.go)
+(mobs only) Returns true if actor is currently pathing
+
+## [ActorObject.PathingAtWaypoint() bool](/internal/scripting/actor_func.go)
+(mobs only) Returns true if actor is pathing and at a waypoint.
diff --git a/_datafiles/guides/building/scripting/SCRIPTING_MOBS.md b/_datafiles/guides/building/scripting/SCRIPTING_MOBS.md
index b1f2f478..68de1242 100644
--- a/_datafiles/guides/building/scripting/SCRIPTING_MOBS.md
+++ b/_datafiles/guides/building/scripting/SCRIPTING_MOBS.md
@@ -190,3 +190,16 @@ function onDie(mob ActorObject, room RoomObject, eventDetails object) {
---
+```
+function onAsk(mob ActorObject, room RoomObject, eventDetails object) {
+}
+```
+
+`onPath()` is called when mob is asked something. Returning `true` will end the pathing and skip any additional path processing.
+NOTE: You can safely start a new path with `mob.Command('pathto 123')` before returning true, since the command will be executed slightly later in the event chain.
+
+| Argument | Explanation |
+| --- | --- |
+| mob | [ActorObject](FUNCTIONS_ACTORS.md) |
+| room | [RoomObject](FUNCTIONS_ROOMS.md) |
+| eventDetails.status | `start`, `waypoint`, or `end` |
diff --git a/_datafiles/world/default/conversations/frostfang/40.yaml b/_datafiles/world/default/conversations/frostfang/40.yaml
new file mode 100644
index 00000000..168094f1
--- /dev/null
+++ b/_datafiles/world/default/conversations/frostfang/40.yaml
@@ -0,0 +1,9 @@
+-
+ Supported: # A map of lowercase names of "Initiator" (#1) to array of "Participant" (#2) names allowed to use this conversation.
+ "rodric": ["wench"]
+ Conversation:
+ - ["#1 sayto #2 I'll 'ave a mug 'o ale, wench!"]
+ - ["#2 say Very well, Rodric, but be off with you. We don't need you lingering around the guests smelling like death."]
+ - ["#2 emote hands Rodric a mug."]
+ - ["#1 say Aye, i'll be off, then."]
+ - ["#2 emote mumbles something under their breath."]
diff --git a/_datafiles/world/default/mobs/frostfang_slums/40-rodric.yaml b/_datafiles/world/default/mobs/frostfang/40-rodric.yaml
similarity index 97%
rename from _datafiles/world/default/mobs/frostfang_slums/40-rodric.yaml
rename to _datafiles/world/default/mobs/frostfang/40-rodric.yaml
index 99a248d3..51df8bf6 100644
--- a/_datafiles/world/default/mobs/frostfang_slums/40-rodric.yaml
+++ b/_datafiles/world/default/mobs/frostfang/40-rodric.yaml
@@ -1,5 +1,5 @@
mobid: 40
-zone: Frostfang Slums
+zone: Frostfang
itemdropchance: 2
hostile: false
maxwander: 5
diff --git a/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-lakeworker.js b/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-lakeworker.js
index caf657dc..7c63e8c5 100644
--- a/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-lakeworker.js
+++ b/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-lakeworker.js
@@ -1,36 +1,37 @@
-SAID_STUFF = false;
-WALK_DIRECTION = 1;
-WALK_POSITION = 0;
-WALK_PATH = [362, // old dock
- 333,
- 332,
- 331,
- 330,
- 329,
- 328,
- 327,
- 326,
- 325,
- 324,
- 323,
- 322,
- 321,
- 320,
- 319] // crashing waves leading to the rocky island
+var SAID_STUFF = false; // Whether lakework has spoken this "visit" to the crash site
+const HOME_ROOM_ID = 362; // old dock
+const TRASH_PICKUP_SPOTS = [331,327,323,322]; // miscellaneous points along the way to do a random emote
+const CRASH_ROOM_ID = 319; // crashing waves leading to the rocky island
const boatNouns = ["boats", "oars", "ships", "paddles"];
const crashNouns = ["rocks", "crash", "choppy", "water", "waves"];
+// Emotes randomly selected at various waypoints.
+const randomEmotes = [
+ "emote picks up some trash along the shoreline.",
+ "emote moves some driftwood to a pile.",
+ "emote picks up a small rock and throws it into the lake.",
+ "emote gently brushes leaves off a bench.",
+ "emote tosses a fallen branch off the trail.",
+ "emote stoops to examine a patch of wildflowers.",
+ "emote skips a stone across the water.",
+ "emote watches a dragonfly hover near the reeds.",
+ "emote nudges a small frog back toward the water.",
+ "emote smooths out a scuffed trail marker.",
+ "emote pauses to listen to the rustling trees.",
+ "emote adjusts a loose rock on the path.",
+]
+
function onAsk(mob, room, eventDetails) {
- roomId = room.RoomId();
-
- match = UtilFindMatchIn(eventDetails.askText, boatNouns);
+ var roomId = room.RoomId();
+
+ var match = UtilFindMatchIn(eventDetails.askText, boatNouns);
if ( match.found ) {
- if ( roomId == 319 ) {
+ if ( roomId == CRASH_ROOM_ID ) {
mob.Command("say I hit those rocks just over there and lost all of our oars.");
mob.Command("emote points to the northwest.");
@@ -49,7 +50,7 @@ function onAsk(mob, room, eventDetails) {
if ( match.found ) {
- if ( roomId == 319 ) {
+ if ( roomId == CRASH_ROOM_ID ) {
mob.Command("say I hit those rocks just over there and lost all of our oars.");
mob.Command("emote points to the northwest.");
@@ -79,6 +80,20 @@ function onAsk(mob, room, eventDetails) {
}
+
+function onPath(mob, room, eventDetails) {
+
+ if ( eventDetails.status == "waypoint" && mob.GetRoomId() != CRASH_ROOM_ID ) {
+
+ if ( UtilDiceRoll(1, 5) == 1 ) {
+ var emoteSelection = UtilDiceRoll(1, randomEmotes.length)-1;
+ mob.Command(randomEmotes[emoteSelection]);
+ }
+
+ }
+
+}
+
function onGive(mob, room, eventDetails) {
if (eventDetails.item) {
@@ -99,61 +114,30 @@ function onGive(mob, room, eventDetails) {
// Invoked once every round if mob is idle
function onIdle(mob, room) {
-
- if ( mob.GetRoomId() == 319 ) {
+ if ( mob.GetRoomId() == CRASH_ROOM_ID ) {
if ( !SAID_STUFF ) {
mob.Command("emote squints and peers towards a rocky island in the lake to the northwest.");
mob.Command("emote mutters to himself.");
- if ( UtilDiceRoll(1, 2) == 1 ) {
- mob.Command("say Ever since I crashed my boat on those rocks, I've been demoted to cleaning up the lakeshore.");
- }
+ mob.Command("say Ever since I crashed my boat on those rocks, I've been demoted to cleaning up the lakeshore.", 2);
SAID_STUFF = true;
return true;
}
+ }
- SAID_STUFF = false; // reset
-
- } else if ( UtilDiceRoll(1, 2) > 1 ) {
+ if ( UtilDiceRoll(1, 2) > 1 ) {
return true;
}
-
-
- if ( WALK_POSITION < 0 ) {
- WALK_POSITION = 0;
- } else if ( WALK_POSITION > WALK_PATH.length - 1) {
- WALK_POSITION = WALK_PATH.length - 1;
+ if ( mob.IsHome() ) {
+ SAID_STUFF = false; // reset once they get home
+ mob.Command("pathto " + TRASH_PICKUP_SPOTS.join(" ") + " " + String(CRASH_ROOM_ID));
+ return true
}
- roomNow = WALK_PATH[WALK_POSITION];
-
- if ( roomNow != mob.GetRoomId() ) {
-
- WALK_POSITION = 0;
- WALK_DIRECTION = 1;
- mob.MoveRoom(WALK_PATH[WALK_POSITION]);
-
- } else {
-
- if ( WALK_POSITION >= WALK_PATH.length -1 ) {
- WALK_DIRECTION = -1;
- }
- if ( WALK_POSITION < 0 ) {
- WALK_DIRECTION = 1;
- }
-
- WALK_POSITION += WALK_DIRECTION;
-
- exitList = room.GetExits();
- for (var key in exitList) {
- if ( exitList[key].RoomId == WALK_PATH[WALK_POSITION] ) {
- mob.Command( exitList[key].Name );
-
- }
- }
-
+ if ( mob.GetRoomId() == CRASH_ROOM_ID ) {
+ mob.Command("pathto " + TRASH_PICKUP_SPOTS.slice().reverse().join(" ") + " home");
}
return true;
diff --git a/_datafiles/world/default/mobs/frostfang_slums/scripts/40-rodric.js b/_datafiles/world/default/mobs/frostfang/scripts/40-rodric.js
similarity index 92%
rename from _datafiles/world/default/mobs/frostfang_slums/scripts/40-rodric.js
rename to _datafiles/world/default/mobs/frostfang/scripts/40-rodric.js
index 2d0f4199..d13d60cd 100644
--- a/_datafiles/world/default/mobs/frostfang_slums/scripts/40-rodric.js
+++ b/_datafiles/world/default/mobs/frostfang/scripts/40-rodric.js
@@ -1,6 +1,7 @@
const startNouns = ["rat", "rats", "too many", "problem"];
const thievesNouns = ["thief", "thieves", "guild", "hideout", "entrance", "dogs", "slums"];
+const INN_ROOM_ID = 61;
function onAsk(mob, room, eventDetails) {
@@ -111,24 +112,38 @@ function onGive(mob, room, eventDetails) {
}
-RANDOM_IDLE = [
+function onPath(mob, room, eventDetails) {
+
+ if ( eventDetails.status == "waypoint" && room.RoomId() == INN_ROOM_ID ) {
+ mob.Command("converse 1");
+ }
+
+}
+
+const RANDOM_IDLE = [
"emote shakes his head in disbelief.",
"emote attempts to fix a rat trap.",
"say There's just too many rats. We'll never get rid of them all.",
"say I'm so tired. I need a break.",
"say I'm running out of traps. I need to find more.",
"say I'm worried about the rats in the slums. They're everywhere!",
- "say I'm running out of traps and don't seem to be making a dent in the rat numbers."
+ "say I'm running out of traps and don't seem to be making a dent in the rat numbers.",
+ "pathto "+String(INN_ROOM_ID),
];
// Invoked once every round if mob is idle
function onIdle(mob, room) {
+ if ( room.RoomId() == INN_ROOM_ID ) {
+ mob.Command("pathto home");
+ return true;
+ }
+
if ( UtilGetRoundNumber()%3 != 0 ) {
return true;
}
- randNum = UtilDiceRoll(1, 10)-1;
+ randNum = UtilDiceRoll(1, 12)-1;
if ( randNum < RANDOM_IDLE.length ) {
mob.Command(RANDOM_IDLE[randNum]);
return true;
diff --git a/internal/conversations/conversations.go b/internal/conversations/conversations.go
index 22e9b247..fd2c04a1 100644
--- a/internal/conversations/conversations.go
+++ b/internal/conversations/conversations.go
@@ -3,6 +3,7 @@ package conversations
import (
"fmt"
"os"
+ "strconv"
"strings"
"github.com/GoMudEngine/GoMud/internal/configs"
@@ -12,6 +13,7 @@ import (
)
var (
+ converseCheckCache = map[string]bool{}
conversations = map[int]*Conversation{}
conversationCounter = map[string]int{}
conversationUniqueId = 0
@@ -77,7 +79,6 @@ func AttemptConversation(initiatorMobId int, initatorInstanceId int, initiatorNa
}
lowestCount := -1
-
for _, index := range possibleConversations {
val := conversationCounter[fmt.Sprintf(`%s:%d`, fileName, index)]
if val < lowestCount || lowestCount == -1 {
@@ -155,6 +156,13 @@ func HasConverseFile(mobId int, zone string) bool {
zone = ZoneNameSanitize(zone)
+ cacheKey := strconv.Itoa(mobId) + `-` + zone
+ if result, ok := converseCheckCache[cacheKey]; ok {
+ if result == false {
+ return false
+ }
+ }
+
convFolder := string(configs.GetFilePathsConfig().DataFiles) + `/conversations`
fileName := fmt.Sprintf("%s/%d.yaml", zone, mobId)
@@ -162,9 +170,12 @@ func HasConverseFile(mobId int, zone string) bool {
filePath := util.FilePath(convFolder + `/` + fileName)
if _, err := os.Stat(filePath); err != nil {
+ converseCheckCache[cacheKey] = false
return false
}
+ converseCheckCache[cacheKey] = true
+
return true
}
diff --git a/internal/hooks/MobIdle_HandleIdleMobs.go b/internal/hooks/MobIdle_HandleIdleMobs.go
index 5332e0c4..7017a7ea 100644
--- a/internal/hooks/MobIdle_HandleIdleMobs.go
+++ b/internal/hooks/MobIdle_HandleIdleMobs.go
@@ -2,6 +2,7 @@ package hooks
import (
"github.com/GoMudEngine/GoMud/internal/configs"
+ "github.com/GoMudEngine/GoMud/internal/conversations"
"github.com/GoMudEngine/GoMud/internal/events"
"github.com/GoMudEngine/GoMud/internal/mobcommands"
"github.com/GoMudEngine/GoMud/internal/mobs"
@@ -23,14 +24,18 @@ func HandleIdleMobs(e events.Event) events.ListenerReturn {
return events.Cancel
}
+ isCharmed := mob.Character.IsCharmed()
+
// if a mob shouldn't be allowed to leave their area (via wandering)
// but has somehow been displaced, such as pulling through combat, spells, or otherwise
// tell them to path back home
if mob.MaxWander == 0 && mob.Character.RoomId != mob.HomeRoomId {
- mob.Command("pathto home")
+ if !isCharmed {
+ mob.Command("pathto home")
+ }
}
- if mob.CanConverse() && util.Rand(100) < int(configs.GetGamePlayConfig().MobConverseChance) {
+ if conversations.HasConverseFile(int(mob.MobId), mob.Character.Zone) && util.Rand(100) < int(configs.GetGamePlayConfig().MobConverseChance) {
if mobRoom := rooms.LoadRoom(mob.Character.RoomId); mobRoom != nil {
mobcommands.Converse(``, mob, mobRoom) // Execute this directly so that target mob doesn't leave the room before this command executes
}
@@ -40,32 +45,35 @@ func HandleIdleMobs(e events.Event) events.ListenerReturn {
handled, _ := scripting.TryMobScriptEvent("onIdle", mob.InstanceId, 0, ``, nil)
if !handled {
- if !mob.Character.IsCharmed() { // Won't do this stuff if befriended
-
- if mob.MaxWander > -1 && mob.WanderCount > mob.MaxWander {
- mob.Command(`pathto home`)
- }
-
- }
-
- //
- // Look for trouble
- //
- if mob.Character.IsCharmed() {
+ if isCharmed {
// Only some mobs can apply first aid
+ // If a charmed mob can aid someone, try.
if mob.Character.KnowsFirstAid() {
mob.Command(`lookforaid`)
}
} else {
- idleCmd := `lookfortrouble`
- if util.Rand(100) < mob.ActivityLevel {
- idleCmd = mob.GetIdleCommand()
- if idleCmd == `` {
- idleCmd = `lookfortrouble`
+ if mob.MaxWander > -1 && mob.WanderCount > mob.MaxWander {
+
+ // Not charmed and far from home, and should never leave home.
+ // So go home.
+ mob.Command(`pathto home`)
+
+ } else {
+
+ //
+ // Look for trouble
+ //
+
+ idleCmd := `lookfortrouble`
+ if util.Rand(100) < mob.ActivityLevel {
+ idleCmd = mob.GetIdleCommand()
+ if idleCmd == `` {
+ idleCmd = `lookfortrouble`
+ }
}
+ mob.Command(idleCmd)
}
- mob.Command(idleCmd)
}
}
diff --git a/internal/hooks/NewRound_IdleMobs.go b/internal/hooks/NewRound_IdleMobs.go
index f89ef515..2e386afc 100644
--- a/internal/hooks/NewRound_IdleMobs.go
+++ b/internal/hooks/NewRound_IdleMobs.go
@@ -10,6 +10,7 @@ import (
"github.com/GoMudEngine/GoMud/internal/events"
"github.com/GoMudEngine/GoMud/internal/mobs"
"github.com/GoMudEngine/GoMud/internal/rooms"
+ "github.com/GoMudEngine/GoMud/internal/scripting"
"github.com/GoMudEngine/GoMud/internal/users"
"github.com/GoMudEngine/GoMud/internal/util"
)
@@ -85,12 +86,18 @@ func IdleMobs(e events.Event) events.ListenerReturn {
if currentStep := mob.Path.Current(); currentStep != nil || mob.Path.Len() > 0 {
if currentStep == nil {
+
+ if endPathingAndSkip, _ := scripting.TryMobScriptEvent("onPath", mob.InstanceId, 0, ``, map[string]any{`status`: `start`}); endPathingAndSkip {
+ mob.Path.Clear()
+ continue
+ }
+
if mobPathAnnounce {
mob.Command(`say I'm beginning a new path.`)
}
} else {
- // If their currentStep isnt' actually the room they are in
+ // If their currentStep isn't actually the room they are in
// They've somehow been moved. Reclaculate a new path.
if currentStep.RoomId() != mob.Character.RoomId {
if mobPathAnnounce {
@@ -133,10 +140,16 @@ func IdleMobs(e events.Event) events.ListenerReturn {
}
+ mob.Path.Clear()
+
+ if endPathingAndSkip, _ := scripting.TryMobScriptEvent("onPath", mob.InstanceId, 0, ``, map[string]any{`status`: `end`}); endPathingAndSkip {
+ continue
+ }
+
if mobPathAnnounce {
mob.Command(`say I'm.... done.`)
}
- mob.Path.Clear()
+
}
events.AddToQueue(events.MobIdle{MobInstanceId: mobId})
diff --git a/internal/mapper/mapper.path.go b/internal/mapper/mapper.path.go
index 7957cf5a..7a5e7f4f 100644
--- a/internal/mapper/mapper.path.go
+++ b/internal/mapper/mapper.path.go
@@ -9,13 +9,25 @@ import (
"github.com/GoMudEngine/GoMud/internal/rooms"
"github.com/GoMudEngine/GoMud/internal/util"
+ lru "github.com/hashicorp/golang-lru/v2"
)
var (
ErrPathNotFound = errors.New(`path not found`)
ErrPathDestMatch = errors.New(`path destination is same as source`)
+ pathCache, _ = lru.New[pathCacheKey, pathCacheValue](128)
)
+type pathCacheKey struct {
+ startRoomId int
+ endRoomId int
+}
+
+type pathCacheValue struct {
+ steps []pathStep
+ err error
+}
+
// pathStep is one move: take ExitName to arrive in RoomID.
type pathStep struct {
exitName string
@@ -202,6 +214,7 @@ func GetPath(startRoomId int, endRoomId ...int) ([]pathStep, error) {
return []pathStep{}, fmt.Errorf("%d => %d (mapper not fond): %w", startRoomId, endRoomId, ErrPathNotFound)
}
+ cacheKey := pathCacheKey{}
rNow := startRoomId
finalPath := []pathStep{}
for _, roomId := range endRoomId {
@@ -210,15 +223,35 @@ func GetPath(startRoomId int, endRoomId ...int) ([]pathStep, error) {
continue
}
+ cacheKey.startRoomId = rNow
+ cacheKey.endRoomId = roomId
+
+ if pCache, ok := pathCache.Get(cacheKey); ok {
+
+ if pCache.err != nil {
+ return pCache.steps, fmt.Errorf("%d => %d: %w", rNow, roomId, pCache.err)
+ }
+
+ finalPath = append(finalPath, pCache.steps...)
+ rNow = roomId
+ continue
+ }
+
if !m.HasRoom(roomId) {
- return []pathStep{}, fmt.Errorf("%d => %d (room not in mapper): %w", rNow, roomId, ErrPathNotFound)
+ err := fmt.Errorf("%d => %d (room not in mapper): %w", rNow, roomId, ErrPathNotFound)
+ pathCache.Add(cacheKey, pathCacheValue{steps: nil, err: err})
+ return []pathStep{}, err
}
p, err := m.findPath(rNow, roomId)
if err != nil {
+ pathCache.Add(cacheKey, pathCacheValue{steps: p, err: err})
return []pathStep{}, fmt.Errorf("%d => %d: %w", rNow, roomId, ErrPathNotFound)
}
+ // Add to LRU cache
+ pathCache.Add(cacheKey, pathCacheValue{steps: p, err: nil})
+
finalPath = append(finalPath, p...)
rNow = roomId
}
diff --git a/internal/mobcommands/converse.go b/internal/mobcommands/converse.go
index bad7bf5e..cc635175 100644
--- a/internal/mobcommands/converse.go
+++ b/internal/mobcommands/converse.go
@@ -12,15 +12,11 @@ import (
func Converse(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) {
// Don't bother if no players are present
- if room.PlayerCt() < 1 {
- // return true, nil
- }
-
if mob.InConversation() {
return true, nil
}
- if !mob.CanConverse() {
+ if !conversations.HasConverseFile(int(mob.MobId), mob.Character.Zone) {
return true, nil
}
@@ -46,9 +42,13 @@ func Converse(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) {
conversationId := 0
if rest != `` {
forceIndex, _ := strconv.Atoi(rest)
- conversationId = conversations.AttemptConversation(int(mob.MobId), mob.InstanceId, mob.Character.Name, m.InstanceId, m.Character.Name, m.Character.Zone, forceIndex)
+ conversationId = conversations.AttemptConversation(int(mob.MobId), mob.InstanceId, mob.Character.Name,
+ m.InstanceId, m.Character.Name,
+ mob.Character.Zone, forceIndex)
} else {
- conversationId = conversations.AttemptConversation(int(mob.MobId), mob.InstanceId, mob.Character.Name, m.InstanceId, m.Character.Name, m.Character.Zone)
+ conversationId = conversations.AttemptConversation(int(mob.MobId), mob.InstanceId, mob.Character.Name,
+ m.InstanceId, m.Character.Name,
+ mob.Character.Zone)
}
if conversationId > 0 {
diff --git a/internal/mobcommands/go.go b/internal/mobcommands/go.go
index 298bdec8..65ad9bb5 100644
--- a/internal/mobcommands/go.go
+++ b/internal/mobcommands/go.go
@@ -7,6 +7,7 @@ import (
"github.com/GoMudEngine/GoMud/internal/configs"
"github.com/GoMudEngine/GoMud/internal/mobs"
"github.com/GoMudEngine/GoMud/internal/rooms"
+ "github.com/GoMudEngine/GoMud/internal/scripting"
)
func Go(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) {
@@ -86,6 +87,18 @@ func Go(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) {
room.PlaySound(`room-exit`, `movement`)
destRoom.PlaySound(`room-enter`, `movement`)
+ // We want the `waypoint` onPath event triggered right after they enter the room.
+ if currentStep := mob.Path.Current(); currentStep != nil && currentStep.Waypoint() {
+
+ // Anytime a mob reaches a waypoint, introduce a 1 second delay before they can perform any additional commands.
+ // This gives a more natural feel to mob behavior, and gives those following a moment to catch up before the mob does something.
+ mob.Command("noop", 1)
+
+ if endPathingAndSkip, _ := scripting.TryMobScriptEvent("onPath", mob.InstanceId, 0, ``, map[string]any{`status`: `waypoint`}); endPathingAndSkip {
+ mob.Path.Clear()
+ }
+ }
+
return true, nil
}
diff --git a/internal/mobcommands/pathto.go b/internal/mobcommands/pathto.go
index 2c53a40d..ea3646df 100644
--- a/internal/mobcommands/pathto.go
+++ b/internal/mobcommands/pathto.go
@@ -12,6 +12,14 @@ import (
func Pathto(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) {
+ // If only going home, check whether a path home was already tried and marked as impossible.
+ if rest == `home` {
+ cantGoHome := mob.GetTempData(`home-impossible`)
+ if cantGoHome != nil && cantGoHome.(bool) == true {
+ return true, nil
+ }
+ }
+
toRoomIds := []int{}
for _, roomIdStr := range util.SplitButRespectQuotes(strings.ToLower(rest)) {
@@ -32,14 +40,24 @@ func Pathto(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) {
path, err := mapper.GetPath(mob.Character.RoomId, toRoomIds...)
if err != nil {
+ if rest == `home` {
+ mob.SetTempData(`home-impossible`, true)
+ mob.Character.SetAdjective(`lost`, true)
+ }
return false, err
}
- newPath := []mobs.PathRoom{}
+ if rest == `home` && len(path) == 0 {
+ mob.SetTempData(`home-impossible`, true)
+ mob.Character.SetAdjective(`lost`, true)
+ return true, nil
+ }
+
+ newPath := make([]mobs.PathRoom, len(path))
// Copy everything over
- for _, p := range path {
- newPath = append(newPath, p)
+ for idx, p := range path {
+ newPath[idx] = p
}
mob.Path.SetPath(newPath)
diff --git a/internal/mobcommands/portal.go b/internal/mobcommands/portal.go
index efee988f..4990db86 100644
--- a/internal/mobcommands/portal.go
+++ b/internal/mobcommands/portal.go
@@ -48,7 +48,7 @@ func Portal(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) {
mob.Command(`portal home;drop all`)
- return true, fmt.Errorf("failed to find temporary exit to room")
+ return true, fmt.Errorf("failed to find worthy room with loot")
}
portalTargetRoomId = mostItemRoomId
}
diff --git a/internal/mobcommands/shout.go b/internal/mobcommands/shout.go
index 2166f012..99fd49b9 100644
--- a/internal/mobcommands/shout.go
+++ b/internal/mobcommands/shout.go
@@ -11,19 +11,14 @@ import (
func Shout(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) {
- // Don't bother if no players are present
- if room.PlayerCt() < 1 {
- return true, nil
- }
-
isSneaking := mob.Character.HasBuffFlag(buffs.Hidden)
rest = strings.ToUpper(rest)
if isSneaking {
- room.SendText(fmt.Sprintf(`someone shouts, "%s"`, rest), mob.InstanceId)
+ room.SendText(fmt.Sprintf(`someone shouts, "%s"`, rest))
} else {
- room.SendText(fmt.Sprintf(`%s shouts, "%s"`, mob.Character.Name, rest), mob.InstanceId)
+ room.SendText(fmt.Sprintf(`%s shouts, "%s"`, mob.Character.Name, rest))
}
for _, roomInfo := range room.Exits {
diff --git a/internal/mobs/mobs.go b/internal/mobs/mobs.go
index 9c908acb..0fd0927d 100644
--- a/internal/mobs/mobs.go
+++ b/internal/mobs/mobs.go
@@ -76,7 +76,6 @@ type Mob struct {
BuffIds []int `yaml:"buffids,omitempty"` // Buff Id's this mob always has upon spawn
tempDataStore map[string]any
conversationId int // Identifier of conversation currently involved in.
- hasConverseFile bool // whether they have a converse file to look for conversations in
Path PathQueue `yaml:"-"` // a pre-calculated path the mob is following.
}
@@ -258,10 +257,6 @@ func (m *Mob) AddBuff(buffId int, source string) {
}
-func (m *Mob) CanConverse() bool {
- return m.hasConverseFile
-}
-
func (m *Mob) InConversation() bool {
return m.conversationId > 0
}
@@ -588,8 +583,6 @@ func (r *Mob) Validate() error {
r.ActivityLevel = 100
}
- r.hasConverseFile = conversations.HasConverseFile(int(r.MobId), r.Zone)
-
r.Character.Validate()
return nil
}
diff --git a/world.go b/world.go
index e8f4d6eb..08571d30 100644
--- a/world.go
+++ b/world.go
@@ -46,8 +46,9 @@ type World struct {
logoutConnectionId chan connections.ConnectionId
zombieFlag chan [2]int
//
- eventRequeue []events.Event
- eventTracker map[int]struct{}
+ eventRequeue []events.Event
+ userInputEventTracker map[int]struct{}
+ mobInputEventTracker map[int]struct{}
}
func NewWorld(osSignalChan chan os.Signal) *World {
@@ -60,8 +61,9 @@ func NewWorld(osSignalChan chan os.Signal) *World {
logoutConnectionId: make(chan connections.ConnectionId),
zombieFlag: make(chan [2]int),
//
- eventRequeue: []events.Event{},
- eventTracker: map[int]struct{}{},
+ eventRequeue: []events.Event{},
+ userInputEventTracker: map[int]struct{}{},
+ mobInputEventTracker: map[int]struct{}{},
}
// System commands
@@ -87,16 +89,36 @@ func (w *World) HandleInputEvents(e events.Event) events.ListenerReturn {
// If it's a mob
if input.MobInstanceId > 0 {
- if input.ReadyTurn <= turnCt {
+
+ // If an event was already processed for this user this turn, skip
+ // We put this first, so that any delayed command for a mob will block
+ // the command pipeline for the mob until executed.
+ if _, ok := w.mobInputEventTracker[input.MobInstanceId]; ok {
+ return events.CancelAndRequeue
+ }
+
+ // 0 and below, process immediately and don't count towards limit
+ if input.ReadyTurn <= 0 {
w.processMobInput(input.MobInstanceId, input.InputText)
- } else {
+ return events.Continue
+ }
+
+ // This will cause any pending command to block all further pending commands
+ // This is important, otherwise we issue a command with a delay, but other commands
+ // Get executed while we wait.
+ w.mobInputEventTracker[input.MobInstanceId] = struct{}{}
+
+ if input.ReadyTurn > turnCt {
return events.CancelAndRequeue
}
+
+ w.processMobInput(input.MobInstanceId, input.InputText)
+
return events.Continue
}
// 0 and below, process immediately and don't count towards limit
- if input.ReadyTurn == 0 {
+ if input.ReadyTurn <= 0 {
// If this command was potentially blocking input, unblock it now.
if input.Flags.Has(events.CmdUnBlockInput) {
@@ -116,7 +138,7 @@ func (w *World) HandleInputEvents(e events.Event) events.ListenerReturn {
}
// If an event was already processed for this user this turn, skip
- if _, ok := w.eventTracker[input.UserId]; ok {
+ if _, ok := w.userInputEventTracker[input.UserId]; ok {
return events.CancelAndRequeue
}
@@ -155,7 +177,7 @@ func (w *World) HandleInputEvents(e events.Event) events.ListenerReturn {
w.processInput(input.UserId, input.InputText, events.EventFlag(input.Flags))
- w.eventTracker[input.UserId] = struct{}{}
+ w.userInputEventTracker[input.UserId] = struct{}{}
return events.Continue
}
@@ -1092,5 +1114,6 @@ func (w *World) EventLoop() {
events.AddToQueue(e)
}
- clear(w.eventTracker)
+ clear(w.userInputEventTracker)
+ clear(w.mobInputEventTracker)
}