diff --git a/_datafiles/world/default/templates/admincommands/help/command.room.template b/_datafiles/world/default/templates/admincommands/help/command.room.template index fc14fa04..e95f9b14 100644 --- a/_datafiles/world/default/templates/admincommands/help/command.room.template +++ b/_datafiles/world/default/templates/admincommands/help/command.room.template @@ -34,6 +34,9 @@ Set a property of the room. This updates basic properties of the room you are in Containers: You can interactively add/remove/edit containers using the command: room edit containers + Exits: + You can interactively add/remove/edit exits using the command: + room edit exits room exit [exit_name] [room_id] - e.g. room exit west 159 This will create a new exit that links to a specific room_id using the exit_name provided. diff --git a/_datafiles/world/empty/templates/admincommands/help/command.room.template b/_datafiles/world/empty/templates/admincommands/help/command.room.template index fc14fa04..e95f9b14 100644 --- a/_datafiles/world/empty/templates/admincommands/help/command.room.template +++ b/_datafiles/world/empty/templates/admincommands/help/command.room.template @@ -34,6 +34,9 @@ Set a property of the room. This updates basic properties of the room you are in Containers: You can interactively add/remove/edit containers using the command: room edit containers + Exits: + You can interactively add/remove/edit exits using the command: + room edit exits room exit [exit_name] [room_id] - e.g. room exit west 159 This will create a new exit that links to a specific room_id using the exit_name provided. diff --git a/internal/items/itemspec.go b/internal/items/itemspec.go index fb58f58b..8672a950 100644 --- a/internal/items/itemspec.go +++ b/internal/items/itemspec.go @@ -264,6 +264,20 @@ func FindItem(nameOrId string) int { return FindItemByName(nameOrId) } +func FindKeyByLockId(lockId string) int { + + for _, item := range items { + if item.Type != Key { + continue + } + if item.KeyLockId == lockId { + return item.ItemId + } + } + + return 0 +} + func FindItemByName(name string) int { name = strings.ToLower(name) diff --git a/internal/rooms/rooms.go b/internal/rooms/rooms.go index 04caceaf..21f57a95 100644 --- a/internal/rooms/rooms.go +++ b/internal/rooms/rooms.go @@ -90,7 +90,7 @@ type Room struct { LastIdleMessage uint8 `yaml:"-"` // index of the last idle message displayed LongTermDataStore map[string]any `yaml:"longtermdatastore,omitempty"` // Long term data store for the room Mutators mutators.MutatorList `yaml:"mutators,omitempty"` // mutators this room spawns with. - Pvp bool `yaml:"pvp,omitempty"` // config pvp is set to `limited`, uses this value + Pvp bool `yaml:"pvp,omitempty"` // if config pvp is set to `limited`, uses this value players []int `yaml:"-"` // list of user IDs currently in the room mobs []int `yaml:"-"` // list of mob instance IDs currently in the room. Does not get saved. visitors map[VisitorType]map[int]uint64 `yaml:"-"` // list of user IDs that have visited this room, and the last round they did diff --git a/internal/usercommands/admin.room.go b/internal/usercommands/admin.room.go index 95fe22c3..921ec2e4 100644 --- a/internal/usercommands/admin.room.go +++ b/internal/usercommands/admin.room.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/volte6/gomud/internal/buffs" + "github.com/volte6/gomud/internal/configs" + "github.com/volte6/gomud/internal/exit" "github.com/volte6/gomud/internal/gamelock" "github.com/volte6/gomud/internal/items" "github.com/volte6/gomud/internal/mobs" @@ -41,10 +43,16 @@ func Room(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { // Interactive Editing if roomCmd == `edit` { + if rest == `edit container` || rest == `edit containers` { return room_Edit_Containers(``, user, room) } - user.SendText(`edit WHAT? Try: room edit containers`) + + if rest == `edit exit` || rest == `edit exits` { + return room_Edit_Exits(``, user, room) + } + + user.SendText(`edit WHAT? Try: help room`) return true, nil } @@ -412,31 +420,6 @@ func Room(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { return handled, nil } -func room_Edit_Containers_SendRecipes(user *users.UserRecord, recipeResultItemId int, recipeItems map[int]int) { - - itm := items.New(recipeResultItemId) - - user.SendText(``) - user.SendText(fmt.Sprintf(` Current Recipe for %d (%s):`, recipeResultItemId, itm.DisplayName())) - - itemsList := []string{} - for itemId, qty := range recipeItems { - itm := items.New(itemId) - itemsList = append(itemsList, fmt.Sprintf(` [x%d] %d (%s)`, qty, itemId, itm.DisplayName())) - } - - // Must sort since maps will often change between iterations - sort.SliceStable(itemsList, func(i, j int) bool { - return itemsList[i] < itemsList[j] - }) - - for _, txt := range itemsList { - user.SendText(txt) - } - - user.SendText(``) -} - func room_Edit_Containers(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { // This basic struct will be used to keep track of what we're editing @@ -986,14 +969,417 @@ func room_Edit_Containers(rest string, user *users.UserRecord, room *rooms.Room) room.Containers[currentlyEditing.NameNew] = currentlyEditing.Container rooms.SaveRoom(*room) + user.SendText(``) + + if currentlyEditing.Container.Lock.Difficulty > 0 { + lockId := fmt.Sprintf(`%d-%s`, room.RoomId, currentlyEditing.NameNew) + user.SendText(fmt.Sprintf(`To Create Key - LockId: %s`, lockId)) + + seqString := `` + for _, dir := range util.GetLockSequence(lockId, int(currentlyEditing.Container.Lock.Difficulty), string(configs.GetConfig().Seed)) { + seqString += string(dir) + " " + } + user.SendText(fmt.Sprintf(`To pick lock - Sequence: %s`, seqString)) + } + user.SendText(``) user.SendText(`Changes saved.`) user.SendText(``) - if currentlyEditing.Container.Lock.Difficulty > 0 { - user.SendText(fmt.Sprintf(`NOTE: If you create a key for this lock, the lock id must be: %d-%s`, room.RoomId, currentlyEditing.NameNew)) + user.ClearPrompt() + + return true, nil +} + +func room_Edit_Containers_SendRecipes(user *users.UserRecord, recipeResultItemId int, recipeItems map[int]int) { + + itm := items.New(recipeResultItemId) + + user.SendText(``) + user.SendText(fmt.Sprintf(` Current Recipe for %d (%s):`, recipeResultItemId, itm.DisplayName())) + + itemsList := []string{} + for itemId, qty := range recipeItems { + itm := items.New(itemId) + itemsList = append(itemsList, fmt.Sprintf(` [x%d] %d (%s)`, qty, itemId, itm.DisplayName())) + } + + // Must sort since maps will often change between iterations + sort.SliceStable(itemsList, func(i, j int) bool { + return itemsList[i] < itemsList[j] + }) + + for _, txt := range itemsList { + user.SendText(txt) + } + + user.SendText(``) +} + +func room_Edit_Exits(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) { + + // This basic struct will be used to keep track of what we're editing + type ExitEdit struct { + Name string + NameNew string + Exit exit.RoomExit + Exists bool + } + + exitOptions := []templates.NameDescription{} + + for name, c := range room.Exits { + + exitOpt := templates.NameDescription{Name: name} + + if c.Lock.Difficulty > 0 { + exitOpt.Description += fmt.Sprintf(`[Lvl %d Lock] `, c.Lock.Difficulty) + } + + if c.Secret { + exitOpt.Description += `[secret] ` + } + + exitOptions = append(exitOptions, exitOpt) + + } + + // Must sort since maps will often change between iterations + sort.SliceStable(exitOptions, func(i, j int) bool { + return exitOptions[i].Name < exitOptions[j].Name + }) + + // + // Create a holder for exit editing data + // + currentlyEditing := ExitEdit{} + + cmdPrompt, _ := user.StartPrompt(`room edit exits`, rest) + + question := cmdPrompt.Ask(`Choose one:`, []string{`new`}, `new`) + if !question.Done { + tplTxt, _ := templates.Process("tables/numbered-list", exitOptions) + user.SendText(tplTxt) + return true, nil + } + + currentlyEditing.Name = question.Response + + if restNum, err := strconv.Atoi(currentlyEditing.Name); err == nil { + if restNum > 0 && restNum <= len(exitOptions) { + currentlyEditing.Name = exitOptions[restNum-1].Name + } + } + + for _, o := range exitOptions { + if strings.EqualFold(o.Name, currentlyEditing.Name) { + currentlyEditing.Name = o.Name + break + } + } + + // Load the (possible) existing exit + currentlyEditing.Exit, currentlyEditing.Exists = room.Exits[currentlyEditing.Name] + + // If they entered a exit name... + if currentlyEditing.Name != `new` { + + // Does the exit name they entered not exist? Failure! + if !currentlyEditing.Exists { + user.SendText("Invalid option selected.") + user.SendText("Aborting...") + user.ClearPrompt() + return true, nil + } + + // Since they picked a exit that exists, lets get the question of delete out of the way immediately. + question := cmdPrompt.Ask(`Delete this exit?`, []string{`yes`, `no`}, `no`) + if !question.Done { + return true, nil + } + + // Delete the exit if that's what they want! + if question.Response == `yes` { + + delete(room.Exits, currentlyEditing.Name) + rooms.SaveRoom(*room) + + user.SendText(``) + user.SendText(fmt.Sprintf(`%s deleted from the room.`, currentlyEditing.Name)) + user.SendText(``) + + user.ClearPrompt() + return true, nil + } + + } + + // + // Name Selection + // + { + // If they are creating a new exit, we don't want that to become a viable exit name, lets empty it + if currentlyEditing.Name == `new` { + currentlyEditing.Name = `` + } + + // allow them to name/rename the exit. + question := cmdPrompt.Ask(`Choose a name for this exit:`, []string{currentlyEditing.Name}, currentlyEditing.Name) + if !question.Done { + return true, nil + } + currentlyEditing.NameNew = question.Response + + // Make sure they aren't using any reserved names. + if currentlyEditing.NameNew == `quit` || currentlyEditing.NameNew == `new` { + user.SendText("Invalid new name selected.") + user.SendText("Aborting...") + user.ClearPrompt() + return true, nil + } + + // Make sure the new name isn't a duplicate + if currentlyEditing.Name != currentlyEditing.NameNew { + if _, ok := room.Exits[currentlyEditing.NameNew]; ok { + + user.SendText(`An exit with that name already exists!`) + question.RejectResponse() + return true, nil + + } + } + + } + + // + // Target RoomId + // + { + // allow them to name/rename the exit. + question := cmdPrompt.Ask(`What RoomId will this exit lead to?`, []string{strconv.Itoa(currentlyEditing.Exit.RoomId)}, strconv.Itoa(currentlyEditing.Exit.RoomId)) + if !question.Done { + return true, nil + } + + currentlyEditing.Exit.RoomId, _ = strconv.Atoi(question.Response) + + // Make sure they aren't using any reserved names. + if rooms.LoadRoom(currentlyEditing.Exit.RoomId) == nil { + user.SendText("Invalid RoomId provided.") + question.RejectResponse() + return true, nil + } + + } + + // + // Lock Options + // + { + question := cmdPrompt.Ask(`Will this exit be locked?`, []string{`yes`, `no`}, util.BoolYN(currentlyEditing.Exit.Lock.Difficulty > 0)) + if !question.Done { + return true, nil + } + + if question.Response == `yes` { + + defaultDifficultyAnswer := `` + if currentlyEditing.Exit.Lock.Difficulty > 0 { + defaultDifficultyAnswer = strconv.Itoa(int(currentlyEditing.Exit.Lock.Difficulty)) + } + + question := cmdPrompt.Ask(`What difficulty will the lock be (2-32)?`, []string{defaultDifficultyAnswer}, defaultDifficultyAnswer) + if !question.Done { + return true, nil + } + + difficultyInt, _ := strconv.Atoi(question.Response) + + // Make sure the provided difficulty is within acceptable range. + if difficultyInt < 2 || difficultyInt > 32 { + user.SendText("Difficulty must between 2 and 32, inclusive.") + question.RejectResponse() + return true, nil + } + + currentlyEditing.Exit.Lock.Difficulty = uint8(difficultyInt) + + } else { + // reset the lock state if there is no lock. + currentlyEditing.Exit.Lock = gamelock.Lock{} + } + + if currentlyEditing.Exit.Lock.Difficulty > 0 { + // + // Lock Trap Options + // + question = cmdPrompt.Ask(`Will this lock have a trap?`, []string{`yes`, `no`}, util.BoolYN(len(currentlyEditing.Exit.Lock.TrapBuffIds) > 0)) + if !question.Done { + return true, nil + } + + if question.Response == `yes` { + + selectedBuffList := []int{} + if cb, ok := cmdPrompt.Recall(`trapBuffs`); ok { + selectedBuffList = cb.([]int) + } + + if len(selectedBuffList) == 0 { + selectedBuffList = append(selectedBuffList, currentlyEditing.Exit.Lock.TrapBuffIds...) + } + + // Keep track of the state + cmdPrompt.Store(`trapBuffs`, selectedBuffList) + + selectedBuffLookup := map[int]bool{} + for _, bId := range selectedBuffList { + selectedBuffLookup[bId] = true + } + + buffOptions := []templates.NameDescription{} + + for _, buffId := range buffs.GetAllBuffIds() { + if b := buffs.GetBuffSpec(buffId); b != nil { + + if b.Name == `empty` { + continue + } + + marked := false + if _, ok := selectedBuffLookup[buffId]; ok { + marked = true + } + + buffOptions = append(buffOptions, templates.NameDescription{Id: buffId, Marked: marked, Name: b.Name}) + } + } + + sort.SliceStable(buffOptions, func(i, j int) bool { + return buffOptions[i].Name < buffOptions[j].Name + }) + + question := cmdPrompt.Ask(`Select a buff to add to the trap, or nothing to continue:`, []string{}, `0`) + if !question.Done { + tplTxt, _ := templates.Process("tables/numbered-list-doubled", buffOptions) + user.SendText(tplTxt) + return true, nil + } + + buffSelected := question.Response + + if buffSelected != `0` { + + buffSelectedInt := 0 + + if restNum, err := strconv.Atoi(buffSelected); err == nil { + if restNum > 0 && restNum <= len(buffOptions) { + buffSelectedInt = buffOptions[restNum-1].Id.(int) + } + } + + if buffSelectedInt == 0 { + for _, b := range buffOptions { + if strings.EqualFold(b.Name, buffSelected) { + buffSelectedInt = b.Id.(int) + break + } + } + } + + if buffSelectedInt == 0 { + + user.SendText("Invalid selection.") + question.RejectResponse() + + tplTxt, _ := templates.Process("tables/numbered-list-doubled", buffOptions) + user.SendText(tplTxt) + return true, nil + } + + if _, ok := selectedBuffLookup[buffSelectedInt]; ok { + + delete(selectedBuffLookup, buffSelectedInt) + for idx, buffId := range selectedBuffList { + if buffId == buffSelectedInt { + selectedBuffList = append(selectedBuffList[0:idx], selectedBuffList[idx+1:]...) + } + } + + } else { + + selectedBuffList = append(selectedBuffList, buffSelectedInt) + selectedBuffLookup[buffSelectedInt] = true + + } + + cmdPrompt.Store(`trapBuffs`, selectedBuffList) + + question.RejectResponse() + + for idx, data := range buffOptions { + _, data.Marked = selectedBuffLookup[data.Id.(int)] + buffOptions[idx] = data + } + + tplTxt, _ := templates.Process("tables/numbered-list-doubled", buffOptions) + user.SendText(tplTxt) + return true, nil + + } + + } + + if cb, ok := cmdPrompt.Recall(`trapBuffs`); ok { + currentlyEditing.Exit.Lock.TrapBuffIds = cb.([]int) + } + + if currentlyEditing.Exit.Lock.RelockInterval == `` { + currentlyEditing.Exit.Lock.RelockInterval = gamelock.DefaultRelockTime + } + + question = cmdPrompt.Ask(`How long until it automatically relocks?`, []string{currentlyEditing.Exit.Lock.RelockInterval}, currentlyEditing.Exit.Lock.RelockInterval) + if !question.Done { + return true, nil + } + + currentlyEditing.Exit.Lock.RelockInterval = question.Response + + // If the default time is chosen, can just leave it blank. + if currentlyEditing.Exit.Lock.RelockInterval == gamelock.DefaultRelockTime { + currentlyEditing.Exit.Lock.RelockInterval = `` + } + + } + } + + // + // Done editing. Save results + // + if currentlyEditing.Name != `` { + delete(room.Exits, currentlyEditing.Name) + } + + room.Exits[currentlyEditing.NameNew] = currentlyEditing.Exit + rooms.SaveRoom(*room) + + user.SendText(``) + + if currentlyEditing.Exit.Lock.Difficulty > 0 { + lockId := fmt.Sprintf(`%d-%s`, room.RoomId, currentlyEditing.NameNew) + user.SendText(fmt.Sprintf(`To Create Key - LockId: %s`, lockId)) + + seqString := `` + for _, dir := range util.GetLockSequence(lockId, int(currentlyEditing.Exit.Lock.Difficulty), string(configs.GetConfig().Seed)) { + seqString += string(dir) + " " + } + user.SendText(fmt.Sprintf(`To pick lock - Sequence: %s`, seqString)) } + user.SendText(``) + user.SendText(`Changes saved.`) + user.SendText(``) + user.ClearPrompt() return true, nil