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) }