diff --git a/_datafiles/world/default/keywords.yaml b/_datafiles/world/default/keywords.yaml index 3a6340d6..9e0834a9 100644 --- a/_datafiles/world/default/keywords.yaml +++ b/_datafiles/world/default/keywords.yaml @@ -87,7 +87,6 @@ help: - online - quit parties: - - follow - party - share locks: diff --git a/_datafiles/world/default/mobs/frostfang/scripts/2-guard.js b/_datafiles/world/default/mobs/frostfang/scripts/2-guard.js index 71d0ce5a..9efd35c2 100644 --- a/_datafiles/world/default/mobs/frostfang/scripts/2-guard.js +++ b/_datafiles/world/default/mobs/frostfang/scripts/2-guard.js @@ -9,6 +9,10 @@ var PathTargets = [ function onIdle(mob, room) { + if ( mob.PathingAtWaypoint() && mob.IsHome() ) { + mob.SetAdjective("patrolling", false); + } + var random = Math.floor(Math.random() * 10); switch (random) { case 0: @@ -19,8 +23,10 @@ function onIdle(mob, room) { case 2: // Start a patrol path var randomPath = Math.floor(Math.random() * PathTargets.length); - var selectedPath = PathTargets[randomPath] + var selectedPath = PathTargets[randomPath]; + mob.SetAdjective("patrolling", true); mob.Command("pathto "+selectedPath.join(' ')); + return true; case 3: // wander randomly. diff --git a/_datafiles/world/empty/keywords.yaml b/_datafiles/world/empty/keywords.yaml index 3a6340d6..9e0834a9 100644 --- a/_datafiles/world/empty/keywords.yaml +++ b/_datafiles/world/empty/keywords.yaml @@ -87,7 +87,6 @@ help: - online - quit parties: - - follow - party - share locks: diff --git a/_datafiles/world/empty/templates/help/follow.template b/_datafiles/world/empty/templates/help/follow.template deleted file mode 100644 index fa768e34..00000000 --- a/_datafiles/world/empty/templates/help/follow.template +++ /dev/null @@ -1,9 +0,0 @@ -.: Help for follow - -The follow follows another player so that when they leave a room, you go with them. - -Usage: - - follow Jim - This would follow Jim from now on. - diff --git a/internal/characters/character.go b/internal/characters/character.go index f9f559ea..5da6e568 100644 --- a/internal/characters/character.go +++ b/internal/characters/character.go @@ -85,7 +85,6 @@ type Character struct { roomHistory []int // A stack FILO of the last X rooms the character has been in PlayerDamage map[int]int `yaml:"-"` // key = who, value = how much LastPlayerDamage uint64 `yaml:"-"` // last round a player damaged this character - followers []int // everyone following this user permaBuffIds []int // Buff Id's that are always present for this character userId int // User ID of the character if any } @@ -532,10 +531,6 @@ func (c *Character) GetRandomItem() (items.Item, bool) { return c.Items[util.Rand(len(c.Items))], true } -func (c *Character) AddFollower(uId int) { - c.followers = append(c.followers, uId) -} - // USERNAME appears to be func (c *Character) GetHealthAppearance() string { @@ -561,10 +556,6 @@ func (c *Character) GetHealthAppearance() string { return fmt.Sprintf(`%s is in perfect health.`, c.Name, className) } -func (c *Character) GetFollowers() []int { - return append([]int{}, c.followers...) -} - func (c *Character) GetAllSkillRanks() map[string]int { retMap := make(map[string]int) for skillName, skillLevel := range c.Skills { diff --git a/internal/events/eventtypes.go b/internal/events/eventtypes.go index a5ecee6d..88f36eb7 100644 --- a/internal/events/eventtypes.go +++ b/internal/events/eventtypes.go @@ -153,6 +153,13 @@ type NewTurn struct { func (n NewTurn) Type() string { return `NewTurn` } +// Anytime a mob is idle +type MobIdle struct { + MobInstanceId int +} + +func (i MobIdle) Type() string { return `MobIdle` } + // Gained or lost an item type EquipmentChange struct { UserId int @@ -339,6 +346,8 @@ type Party struct { Position map[int]string } +func (p Party) Type() string { return `Party` } + type RedrawPrompt struct { UserId int OnlyIfChanged bool diff --git a/internal/events/listeners.go b/internal/events/listeners.go index c6a71b55..5b9f79b6 100644 --- a/internal/events/listeners.go +++ b/internal/events/listeners.go @@ -91,16 +91,16 @@ func RegisterListener(emptyEvent any, cbFunc Listener, qFlag ...QueueFlag) Liste } else if listenerDetails.isFinal { eventListeners[eType] = append(eventListeners[eType], listenerDetails) - } else { + } else { // end of the list, but before any "final" listeners insertPosition := 0 - for idx := 0; idx < len(eventListeners[eType]); idx++ { + + for idx := len(eventListeners[eType]) - 1; idx >= 0; idx-- { // If we're looking at a "final" listener, we can't go any farther down the list if !eventListeners[eType][idx].isFinal { - insertPosition = idx - continue + insertPosition = idx + 1 + break } - break } eventListeners[eType] = append(eventListeners[eType], ListenerWrapper{}) diff --git a/internal/hooks/MobIdle_HandleIdleMobs.go b/internal/hooks/MobIdle_HandleIdleMobs.go new file mode 100644 index 00000000..5332e0c4 --- /dev/null +++ b/internal/hooks/MobIdle_HandleIdleMobs.go @@ -0,0 +1,73 @@ +package hooks + +import ( + "github.com/GoMudEngine/GoMud/internal/configs" + "github.com/GoMudEngine/GoMud/internal/events" + "github.com/GoMudEngine/GoMud/internal/mobcommands" + "github.com/GoMudEngine/GoMud/internal/mobs" + "github.com/GoMudEngine/GoMud/internal/rooms" + "github.com/GoMudEngine/GoMud/internal/scripting" + "github.com/GoMudEngine/GoMud/internal/util" +) + +// +// Handles default mob idle behavior +// + +func HandleIdleMobs(e events.Event) events.ListenerReturn { + + evt := e.(events.MobIdle) + + mob := mobs.GetInstance(evt.MobInstanceId) + if mob == nil { + return events.Cancel + } + + // 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 mob.CanConverse() && 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 + } + } + + // If they have idle commands, maybe do one of them? + 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() { + // Only some mobs can apply first aid + if mob.Character.KnowsFirstAid() { + mob.Command(`lookforaid`) + } + } else { + + idleCmd := `lookfortrouble` + if util.Rand(100) < mob.ActivityLevel { + idleCmd = mob.GetIdleCommand() + if idleCmd == `` { + idleCmd = `lookfortrouble` + } + } + mob.Command(idleCmd) + } + } + + return events.Continue +} diff --git a/internal/hooks/NewRound_IdleMobs.go b/internal/hooks/NewRound_IdleMobs.go index 3df73b40..f89ef515 100644 --- a/internal/hooks/NewRound_IdleMobs.go +++ b/internal/hooks/NewRound_IdleMobs.go @@ -8,10 +8,8 @@ import ( "github.com/GoMudEngine/GoMud/internal/configs" "github.com/GoMudEngine/GoMud/internal/events" - "github.com/GoMudEngine/GoMud/internal/mobcommands" "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" ) @@ -25,10 +23,8 @@ func IdleMobs(e events.Event) events.ListenerReturn { mobPathAnnounce := false // useful for debugging purposes. mc := configs.GetMemoryConfig() - gp := configs.GetGamePlayConfig() maxBoredom := uint8(mc.MaxMobBoredom) - globalConverseChance := int(gp.MobConverseChance) allMobInstances := mobs.GetAllMobInstanceIds() @@ -143,55 +139,7 @@ func IdleMobs(e events.Event) events.ListenerReturn { mob.Path.Clear() } - // 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") - continue - } - - if mob.CanConverse() && util.Rand(100) < globalConverseChance { - 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 - //mob.Command(`converse`) - } - continue - } - - // If they have idle commands, maybe do one of them? - 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`) - continue - } - - } - - // - // Look for trouble - // - if mob.Character.IsCharmed() { - // Only some mobs can apply first aid - if mob.Character.KnowsFirstAid() { - mob.Command(`lookforaid`) - } - } else { - - idleCmd := `lookfortrouble` - if util.Rand(100) < mob.ActivityLevel { - idleCmd = mob.GetIdleCommand() - if idleCmd == `` { - idleCmd = `lookfortrouble` - } - } - mob.Command(idleCmd) - } - } + events.AddToQueue(events.MobIdle{MobInstanceId: mobId}) } diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index 717614d3..b2874a95 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -31,6 +31,7 @@ func RegisterListeners() { // events.RegisterListener(events.NewRound{}, AutoHeal) events.RegisterListener(events.NewRound{}, IdleMobs) + events.RegisterListener(events.MobIdle{}, HandleIdleMobs) // Turn Hooks events.RegisterListener(events.NewTurn{}, CleanupZombies) diff --git a/internal/mobcommands/mobcommands.go b/internal/mobcommands/mobcommands.go index 1845d613..2b986e9b 100644 --- a/internal/mobcommands/mobcommands.go +++ b/internal/mobcommands/mobcommands.go @@ -110,6 +110,24 @@ func TryCommand(cmd string, rest string, mobId int) (bool, error) { } */ + if alias := keywords.TryCommandAlias(cmd); alias != cmd { + // If it's a multi-word aliase, we need to extract the first word to replace the command + // The rest will be combined with any "rest" the mob provided. + if strings.Contains(alias, ` `) { + parts := strings.Split(alias, ` `) + // grab the first word as the new cmd + cmd = parts[0] + // Add the "rest" to the end if any + if len(rest) > 0 { + rest = strings.TrimPrefix(alias, cmd+` `) + ` ` + rest + } else { + rest = strings.TrimPrefix(alias, cmd+` `) + } + } else { + cmd = alias + } + } + if cmdInfo, ok := mobCommands[cmd]; ok { if mobDisabled && !cmdInfo.AllowedWhenDowned { diff --git a/internal/scripting/actor_func.go b/internal/scripting/actor_func.go index 144442ad..686692f0 100644 --- a/internal/scripting/actor_func.go +++ b/internal/scripting/actor_func.go @@ -597,6 +597,13 @@ func (a ScriptActor) SetAdjective(adj string, addIt bool) { a.characterRecord.SetAdjective(adj, addIt) } +func (a ScriptActor) IsHome() bool { + if a.mobRecord != nil { + return a.mobRecord.HomeRoomId == a.characterRecord.RoomId + } + return false +} + func (a ScriptActor) GetCharmCount() int { return len(a.characterRecord.GetCharmIds()) } diff --git a/internal/usercommands/follow.go b/internal/usercommands/follow.go deleted file mode 100644 index d4a1692e..00000000 --- a/internal/usercommands/follow.go +++ /dev/null @@ -1,46 +0,0 @@ -package usercommands - -import ( - "fmt" - - "github.com/GoMudEngine/GoMud/internal/events" - "github.com/GoMudEngine/GoMud/internal/rooms" - "github.com/GoMudEngine/GoMud/internal/users" -) - -func Follow(rest string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) { - - if rest == "" { - user.SendText("Follow whom?") - return true, nil - } - - playerId, _ := room.FindByName(rest) - - if playerId == 0 { - user.SendText(fmt.Sprintf(`%s - not found`, rest)) - return true, nil - } - - if playerId == user.UserId { - user.SendText(`You can't follow yourself`) - return true, nil - } - - if playerId > 0 { - - followUser := users.GetByUserId(playerId) - - user.SendText( - fmt.Sprintf(`You follow %s.`, followUser.Character.Name), - ) - - followUser.SendText( - fmt.Sprintf(`%s is following you.`, user.Character.Name), - ) - - followUser.Character.AddFollower(user.UserId) - } - - return true, nil -} diff --git a/internal/usercommands/usercommands.go b/internal/usercommands/usercommands.go index f0b54303..0b89648e 100644 --- a/internal/usercommands/usercommands.go +++ b/internal/usercommands/usercommands.go @@ -77,7 +77,6 @@ var ( `experience`: {Experience, true, false}, `equip`: {Equip, false, false}, `flee`: {Flee, false, false}, - `follow`: {Follow, false, false}, `gearup`: {Gearup, false, false}, `get`: {Get, false, false}, `give`: {Give, false, false}, @@ -283,20 +282,36 @@ func TryCommand(cmd string, rest string, userId int, flags events.EventFlag) (bo } else { if alias := user.TryCommandAlias(cmd); alias != cmd { + // If it's a multi-word aliase, we need to extract the first word to replace the command + // The rest will be combined with any "rest" the player provided. if strings.Contains(alias, ` `) { parts := strings.Split(alias, ` `) - cmd = parts[0] // grab the first word as the new cmd - rest = strings.TrimPrefix(alias, cmd+` `) + ` ` + rest // add the remaining alias to the rest + // grab the first word as the new cmd + cmd = parts[0] + // Add the "rest" to the end if any + if len(rest) > 0 { + rest = strings.TrimPrefix(alias, cmd+` `) + ` ` + rest + } else { + rest = strings.TrimPrefix(alias, cmd+` `) + } } else { cmd = alias } } if alias := keywords.TryCommandAlias(cmd); alias != cmd { + // If it's a multi-word aliase, we need to extract the first word to replace the command + // The rest will be combined with any "rest" the player provided. if strings.Contains(alias, ` `) { parts := strings.Split(alias, ` `) - cmd = parts[0] // grab the first word as the new cmd - rest = strings.TrimPrefix(alias, cmd+` `) + ` ` + rest // add the remaining alias to the rest + // grab the first word as the new cmd + cmd = parts[0] + // Add the "rest" to the end if any + if len(rest) > 0 { + rest = strings.TrimPrefix(alias, cmd+` `) + ` ` + rest + } else { + rest = strings.TrimPrefix(alias, cmd+` `) + } } else { cmd = alias } diff --git a/modules/follow/files/data-overlays/keywords.yaml b/modules/follow/files/data-overlays/keywords.yaml new file mode 100644 index 00000000..84b945a9 --- /dev/null +++ b/modules/follow/files/data-overlays/keywords.yaml @@ -0,0 +1,7 @@ +help: + command: + parties: + - follow +command-aliases: + 'follow stop': ['unfollow'] + 'follow lose': ['lose'] diff --git a/_datafiles/world/default/templates/help/follow.template b/modules/follow/files/datafiles/templates/help/follow.template similarity index 67% rename from _datafiles/world/default/templates/help/follow.template rename to modules/follow/files/datafiles/templates/help/follow.template index fa768e34..642f410d 100644 --- a/_datafiles/world/default/templates/help/follow.template +++ b/modules/follow/files/datafiles/templates/help/follow.template @@ -7,3 +7,9 @@ The follow follows another player so that when they le follow Jim This would follow Jim from now on. + follow stop + Stop following anyone + + follow lose + Shake off anyone following you, so that they aren't anymore. + diff --git a/modules/follow/follow.go b/modules/follow/follow.go new file mode 100644 index 00000000..7043d181 --- /dev/null +++ b/modules/follow/follow.go @@ -0,0 +1,484 @@ +package follow + +import ( + "embed" + "fmt" + "strings" + + "github.com/GoMudEngine/GoMud/internal/events" + "github.com/GoMudEngine/GoMud/internal/mobs" + "github.com/GoMudEngine/GoMud/internal/parties" + "github.com/GoMudEngine/GoMud/internal/plugins" + "github.com/GoMudEngine/GoMud/internal/rooms" + "github.com/GoMudEngine/GoMud/internal/users" + "github.com/GoMudEngine/GoMud/internal/util" +) + +var ( + + ////////////////////////////////////////////////////////////////////// + // NOTE: The below //go:embed directive is important! + // It embeds the relative path into the var below it. + ////////////////////////////////////////////////////////////////////// + + //go:embed files/* + files embed.FS +) + +// //////////////////////////////////////////////////////////////////// +// NOTE: The init function in Go is a special function that is +// automatically executed before the main function within a package. +// It is used to initialize variables, set up configurations, or +// perform any other setup tasks that need to be done before the +// program starts running. +// //////////////////////////////////////////////////////////////////// +func init() { + // + // We can use all functions only, but this demonstrates + // how to use a struct + // + f := FollowModule{ + plug: plugins.New(`follow`, `1.0`), + followed: make(map[followId][]followId), + followers: make(map[followId]followId), + } + + // + // Add the embedded filesystem + // + if err := f.plug.AttachFileSystem(files); err != nil { + panic(err) + } + // + // Register any user/mob commands + // + f.plug.AddUserCommand(`follow`, f.followUserCommand, true, false) + f.plug.AddMobCommand(`follow`, f.followMobCommand, true) + + events.RegisterListener(events.RoomChange{}, f.roomChangeHandler) + events.RegisterListener(events.PlayerDespawn{}, f.playerDespawnHandler) + events.RegisterListener(events.MobIdle{}, f.idleMobHandler, events.First) + events.RegisterListener(events.PartyUpdated{}, f.onPartyChange) +} + +////////////////////////////////////////////////////////////////////// +// NOTE: What follows is all custom code. For this module. +////////////////////////////////////////////////////////////////////// + +type followId struct { + userId int + mobInstanceId int +} + +// Using a struct gives a way to store longer term data. +type FollowModule struct { + // Keep a reference to the plugin when we create it so that we can call ReadBytes() and WriteBytes() on it. + plug *plugins.Plugin + + followed map[followId][]followId // key => who's followed. value ([]followId{}) => who's following them + followers map[followId]followId // key => who's following someone. value => who's being followed +} + +// Get all followeres attached to a target +func (f *FollowModule) isFollowing(followCheck followId) bool { + _, ok := f.followers[followCheck] + return ok +} + +// Get all followeres attached to a target +func (f *FollowModule) getFollowers(followTarget followId) []followId { + + if _, ok := f.followed[followTarget]; !ok { + return []followId{} + } + + followerResults := make([]followId, len(f.followed[followTarget])) + copy(followerResults, f.followed[followTarget]) + + return followerResults +} + +// Add a single follower to a target +func (f *FollowModule) startFollow(followTarget followId, followSource followId) { + + // Make sure they no longer follow whoever they were before. + f.stopFollowing(followSource) + + f.followers[followSource] = followTarget + if _, ok := f.followed[followTarget]; !ok { + f.followed[followTarget] = []followId{} + } + + f.followed[followTarget] = append(f.followed[followTarget], followSource) +} + +// Remove a single follower from whoever they are following (if any) +func (f *FollowModule) stopFollowing(followSource followId) followId { + + wasFollowing := followId{} + + if followTarget, ok := f.followers[followSource]; ok { + delete(f.followers, followSource) + + wasFollowing = followTarget + + for idx, fId := range f.followed[followTarget] { + if fId == followSource { + f.followed[followTarget] = append(f.followed[followTarget][0:idx], f.followed[followTarget][idx+1:]...) + + if len(f.followed[followTarget]) == 0 { + delete(f.followed, followTarget) + } + + break + } + } + } + + return wasFollowing +} + +// Remove all followers from a target +func (f *FollowModule) loseFollowers(followTarget followId) []followId { + allFollowers := f.getFollowers(followTarget) + for _, followSource := range allFollowers { + f.stopFollowing(followSource) + } + return allFollowers +} + +// +// Event Handlers +// + +// If players make changes (into/out of party) +// Just make sure they aren't following anyone. +// This is just basic cleanup/precaution +func (f *FollowModule) onPartyChange(e events.Event) events.ListenerReturn { + + evt := e.(events.PartyUpdated) + + for _, uId := range evt.UserIds { + f.stopFollowing(followId{userId: uId}) + } + + return events.Continue +} + +// Interrupt the idle action of mobs if they are currently following someone. +func (f *FollowModule) idleMobHandler(e events.Event) events.ListenerReturn { + evt := e.(events.MobIdle) + + if f.isFollowing(followId{mobInstanceId: evt.MobInstanceId}) { + return events.Cancel + } + + return events.Continue +} + +func (f *FollowModule) roomChangeHandler(e events.Event) events.ListenerReturn { + evt := e.(events.RoomChange) + + moverId := followId{userId: evt.UserId, mobInstanceId: evt.MobInstanceId} + + allFollowers := f.getFollowers(moverId) + if len(allFollowers) == 0 { + return events.Continue + } + + fromRoom := rooms.LoadRoom(evt.FromRoomId) + if fromRoom == nil { + return events.Continue + } + + followExitName := `` + for exitName, exitInfo := range fromRoom.Exits { + if exitInfo.RoomId == evt.ToRoomId { + followExitName = exitName + break + } + } + + if followExitName == `` { + for exitName, exitInfo := range fromRoom.ExitsTemp { + if exitInfo.RoomId == evt.ToRoomId { + followExitName = exitName + break + } + } + } + + // The exit they went through is gone/missing? (Teleported?) + // End the follow + if followExitName == `` { + if evt.UserId > 0 { + if user := users.GetByUserId(evt.UserId); user != nil { + user.Command(`follow lose`) + } + } + } else { + + for _, fId := range allFollowers { + + if fId.mobInstanceId > 0 { + + if mob := mobs.GetInstance(fId.mobInstanceId); mob != nil { + if fromRoom.RoomId == mob.Character.RoomId { + mob.Command(followExitName, .25) + continue + } + + mob.Command(`follow stop`) + } + f.stopFollowing(fId) + + } else if fId.userId > 0 { + + if user := users.GetByUserId(fId.userId); user != nil { + if fromRoom.RoomId == user.Character.RoomId { + user.Command(followExitName, .25) + continue + } + + user.Command(`follow stop`) + } + f.stopFollowing(fId) + + } + + } + + } + + return events.Continue +} + +func (f *FollowModule) playerDespawnHandler(e events.Event) events.ListenerReturn { + // Don't really care about the event data for this + evt, typeOk := e.(events.PlayerDespawn) + if !typeOk { + return events.Cancel + } + + f.loseFollowers(followId{userId: evt.UserId}) + + return events.Continue +} + +// +// Commands +// + +func (f *FollowModule) followUserCommand(rest string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) { + + if rest == "" { + user.SendText(`Follow whom? Try help command`) + return true, nil + } + + if parties.Get(user.UserId) != nil { + user.SendText(`You can't use this command while in a party.`) + return true, nil + } + + args := util.SplitButRespectQuotes(strings.ToLower(rest)) + + followTargetName := args[0] + followAction := `follow` + + if rest == `stop` || rest == `lose` { + followAction = rest + followTargetName = `` + } + + userId, mobInstId := 0, 0 + if len(followTargetName) > 0 { + userId, mobInstId = room.FindByName(followTargetName) + } + + followCommandTarget := followId{userId: userId, mobInstanceId: mobInstId} + followCommandSource := followId{userId: user.UserId} + + if followCommandTarget.userId == followCommandSource.userId { + user.SendText(`You can't target yourself.`) + return true, nil + } + + // Lose any followers + if followAction == `lose` { + + if lostFollowers := f.loseFollowers(followCommandSource); len(lostFollowers) > 0 { + + // Tell all the followers they + for _, fId := range lostFollowers { + if fId.userId == 0 { + continue + } + + if followerUser := users.GetByUserId(fId.userId); followerUser != nil { + followerUser.SendText(fmt.Sprintf(`You are no longer following %s.`, user.Character.Name)) + } + } + + } + + user.SendText(fmt.Sprintf(`Nobody is following you.`)) + + return true, nil + } + + // Stop following someone? + if followAction == `stop` { + + wasFollowing := f.stopFollowing(followCommandSource) + + if wasFollowing.userId > 0 { + + if followUser := users.GetByUserId(wasFollowing.userId); followUser != nil { + followUser.SendText(fmt.Sprintf(`%s stopped following you.`, followUser.Character.Name)) + user.SendText(fmt.Sprintf(`You are no longer following %s.`, followUser.Character.Name)) + return true, nil + } + + } + + if wasFollowing.mobInstanceId > 0 { + + if followMob := mobs.GetInstance(wasFollowing.mobInstanceId); followMob != nil { + user.SendText(fmt.Sprintf(`You are no longer following %s.`, followMob.Character.Name)) + return true, nil + } + + } + + user.SendText(`You aren't following anyone.`) + + return true, nil + } + + // Default behavior is follow + if followCommandTarget.userId > 0 { + + f.startFollow(followCommandTarget, followCommandSource) + + targetUser := users.GetByUserId(followCommandTarget.userId) + + user.SendText(fmt.Sprintf(`You start following %s.`, targetUser.Character.Name)) + + targetUser.SendText(fmt.Sprintf(`%s is following you.`, user.Character.Name)) + + return true, nil + } + + if followCommandTarget.mobInstanceId > 0 { + + targetMob := mobs.GetInstance(followCommandTarget.mobInstanceId) + + if targetMob.HatesAlignment(user.Character.Alignment) { + user.SendText(fmt.Sprintf(`%s won't let you follow them.`, targetMob.Character.Name)) + } else { + f.startFollow(followCommandTarget, followCommandSource) + + user.SendText(fmt.Sprintf(`You start following %s.`, targetMob.Character.Name)) + } + + return true, nil + } + + user.SendText(`Follow whom?`) + + return true, nil +} + +func (f *FollowModule) followMobCommand(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) { + + if rest == "" { + return true, nil + } + + args := util.SplitButRespectQuotes(strings.ToLower(rest)) + + followTargetName := args[0] + followAction := `follow` + + if rest == `stop` || rest == `lose` { + followAction = rest + followTargetName = `` + } + + userId, mobInstId := 0, 0 + if len(followTargetName) > 0 { + userId, mobInstId = room.FindByName(followTargetName) + } + + followCommandTarget := followId{userId: userId, mobInstanceId: mobInstId} + followCommandSource := followId{mobInstanceId: mob.InstanceId} + + if followCommandTarget.mobInstanceId == followCommandSource.mobInstanceId { + return true, nil + } + + // Lose any followers + if followAction == `lose` { + + if lostFollowers := f.loseFollowers(followCommandSource); len(lostFollowers) > 0 { + + // Tell all the followers they + for _, fId := range lostFollowers { + if fId.userId == 0 { + continue + } + + if followerUser := users.GetByUserId(fId.userId); followerUser != nil { + followerUser.SendText(fmt.Sprintf(`You are no longer following %s.`, mob.Character.Name)) + } + } + + return true, nil + } + + return true, nil + } + + // Stop following someone? + if followAction == `stop` { + + wasFollowing := f.stopFollowing(followCommandSource) + + if wasFollowing.userId > 0 { + + if followUser := users.GetByUserId(wasFollowing.userId); followUser != nil { + followUser.SendText(fmt.Sprintf(`%s stopped following you.`, followUser.Character.Name)) + return true, nil + } + + } + + return true, nil + } + + // Default behavior is follow + + // If they are on a path, clear it. The follow takes priority. + mob.Path.Clear() + + if followCommandTarget.userId > 0 { + + f.startFollow(followCommandTarget, followCommandSource) + + targetUser := users.GetByUserId(followCommandTarget.userId) + + targetUser.SendText(fmt.Sprintf(`%s is following you.`, mob.Character.Name)) + + return true, nil + } + + if followCommandTarget.mobInstanceId > 0 { + + f.startFollow(followCommandTarget, followCommandSource) + + return true, nil + } + + return false, nil +}