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