diff --git a/_datafiles/config.yaml b/_datafiles/config.yaml
index ef610b9f..3a508c7d 100755
--- a/_datafiles/config.yaml
+++ b/_datafiles/config.yaml
@@ -87,12 +87,10 @@ XPScale: 100
# processed per turn. This is not the same as rounds, which is how often the
# mobs choose their actions, but if multiple actions are queued up, they will
# be processed in order once per turn.
-# The effect of this can be easily seen in the initial login to the game,
-# where several commands are queued up at once and processed in order for the
-# player.
-# 100 is a good balance between handling input from players and not bogging
+# 50 is a good balance between handling input from players and not bogging
# down the server.
-TurnMs: 100
+# Note: If your automapper/walker seems slow, this is probably the culprit.
+TurnMs: 50
# - RoundSeconds -
# How many seconds per round. Anything gated by rounds will be affected by
# this. This is the main setting for controlling the pace of the game.
diff --git a/_datafiles/world/default/templates/admincommands/help/command.room.template b/_datafiles/world/default/templates/admincommands/help/command.room.template
index 1b5e6175..fc14fa04 100644
--- a/_datafiles/world/default/templates/admincommands/help/command.room.template
+++ b/_datafiles/world/default/templates/admincommands/help/command.room.template
@@ -31,6 +31,10 @@ Set a property of the room. This updates basic properties of the room you are in
room nouns - List all nouns for the room
room noun [name] [description] - Add or overwrite a noun
+ Containers:
+ You can interactively add/remove/edit containers using the command:
+ room edit containers
+
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.
!Beware! if the spacial relationship with compass direction rooms is done incorrectly,
diff --git a/_datafiles/world/default/templates/tables/numbered-list-doubled.template b/_datafiles/world/default/templates/tables/numbered-list-doubled.template
index b37d556a..0ba83352 100644
--- a/_datafiles/world/default/templates/tables/numbered-list-doubled.template
+++ b/_datafiles/world/default/templates/tables/numbered-list-doubled.template
@@ -1,2 +1,2 @@
-{{ range $idx, $itemInfo := . }} {{ printf "%2d." (add $idx 1) }} {{ printf "%-33s" $itemInfo.Name }}{{ if eq (mod $idx 2) 1 }}{{ printf "\n" }}{{ end }}{{ end }}
+{{ range $idx, $itemInfo := . }} {{ printf "%2d." (add $idx 1) }} {{ if $itemInfo.Marked }}{{else}}{{end}}{{ if $itemInfo.Marked }}*{{ printf "%-32s" $itemInfo.Name }}{{else}}{{ printf "%-33s" $itemInfo.Name }}{{end}}{{ if eq (mod $idx 2) 1 }}{{ printf "\n" }}{{ end }}{{ end }}
diff --git a/_datafiles/world/default/templates/tables/numbered-list.template b/_datafiles/world/default/templates/tables/numbered-list.template
index f7c726c1..7ed035d4 100644
--- a/_datafiles/world/default/templates/tables/numbered-list.template
+++ b/_datafiles/world/default/templates/tables/numbered-list.template
@@ -1,3 +1,3 @@
-{{ range $idx, $itemInfo := . }} {{ printf "%2d." (add $idx 1) }} {{ printf "%-17s" $itemInfo.Name }}{{ if ne $itemInfo.Description "" }} - {{ splitstring $itemInfo.Description 54 " " }}{{ end }}
+{{ range $idx, $itemInfo := . }} {{ printf "%2d." (add $idx 1) }} {{ if $itemInfo.Marked }}{{else}}{{end}}{{ if $itemInfo.Marked }}*{{ printf "%-16s" $itemInfo.Name }}{{else}}{{ printf "%-17s" $itemInfo.Name }}{{end}}{{ if ne $itemInfo.Description "" }} - {{ splitstring $itemInfo.Description 54 " " }}{{ end }}
{{ end }}
diff --git a/_datafiles/world/empty/templates/admincommands/help/command.room.template b/_datafiles/world/empty/templates/admincommands/help/command.room.template
index 1b5e6175..fc14fa04 100644
--- a/_datafiles/world/empty/templates/admincommands/help/command.room.template
+++ b/_datafiles/world/empty/templates/admincommands/help/command.room.template
@@ -31,6 +31,10 @@ Set a property of the room. This updates basic properties of the room you are in
room nouns - List all nouns for the room
room noun [name] [description] - Add or overwrite a noun
+ Containers:
+ You can interactively add/remove/edit containers using the command:
+ room edit containers
+
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.
!Beware! if the spacial relationship with compass direction rooms is done incorrectly,
diff --git a/_datafiles/world/empty/templates/tables/numbered-list-doubled.template b/_datafiles/world/empty/templates/tables/numbered-list-doubled.template
index b37d556a..0ba83352 100644
--- a/_datafiles/world/empty/templates/tables/numbered-list-doubled.template
+++ b/_datafiles/world/empty/templates/tables/numbered-list-doubled.template
@@ -1,2 +1,2 @@
-{{ range $idx, $itemInfo := . }} {{ printf "%2d." (add $idx 1) }} {{ printf "%-33s" $itemInfo.Name }}{{ if eq (mod $idx 2) 1 }}{{ printf "\n" }}{{ end }}{{ end }}
+{{ range $idx, $itemInfo := . }} {{ printf "%2d." (add $idx 1) }} {{ if $itemInfo.Marked }}{{else}}{{end}}{{ if $itemInfo.Marked }}*{{ printf "%-32s" $itemInfo.Name }}{{else}}{{ printf "%-33s" $itemInfo.Name }}{{end}}{{ if eq (mod $idx 2) 1 }}{{ printf "\n" }}{{ end }}{{ end }}
diff --git a/_datafiles/world/empty/templates/tables/numbered-list.template b/_datafiles/world/empty/templates/tables/numbered-list.template
index f7c726c1..7ed035d4 100644
--- a/_datafiles/world/empty/templates/tables/numbered-list.template
+++ b/_datafiles/world/empty/templates/tables/numbered-list.template
@@ -1,3 +1,3 @@
-{{ range $idx, $itemInfo := . }} {{ printf "%2d." (add $idx 1) }} {{ printf "%-17s" $itemInfo.Name }}{{ if ne $itemInfo.Description "" }} - {{ splitstring $itemInfo.Description 54 " " }}{{ end }}
+{{ range $idx, $itemInfo := . }} {{ printf "%2d." (add $idx 1) }} {{ if $itemInfo.Marked }}{{else}}{{end}}{{ if $itemInfo.Marked }}*{{ printf "%-16s" $itemInfo.Name }}{{else}}{{ printf "%-17s" $itemInfo.Name }}{{end}}{{ if ne $itemInfo.Description "" }} - {{ splitstring $itemInfo.Description 54 " " }}{{ end }}
{{ end }}
diff --git a/feature-screenshots/README.md b/feature-screenshots/README.md
index 7b23f72f..ffac187a 100644
--- a/feature-screenshots/README.md
+++ b/feature-screenshots/README.md
@@ -65,6 +65,12 @@ _Pets are special famliars that help the player and provide special bonuses or u
+## Containers & Recipes
+
+_Containers hold stuff. Recipes turn containers into "Crafting machines"._
+
+
+
## Custom Prompts
_Players can customize their prompts, including colors._
diff --git a/feature-screenshots/container-recipes.png b/feature-screenshots/container-recipes.png
new file mode 100644
index 00000000..0e5ed3ae
Binary files /dev/null and b/feature-screenshots/container-recipes.png differ
diff --git a/internal/connections/connections.go b/internal/connections/connections.go
index 60f68944..3b4cbba3 100644
--- a/internal/connections/connections.go
+++ b/internal/connections/connections.go
@@ -146,11 +146,12 @@ func Remove(id ConnectionId) (err error) {
return errors.New("connection not found")
}
-func Broadcast(colorizedText []byte) {
+func Broadcast(colorizedText []byte) []ConnectionId {
lock.Lock()
removeIds := []ConnectionId{}
+ sentToIds := []ConnectionId{}
for id, cd := range netConnections {
@@ -169,12 +170,15 @@ func Broadcast(colorizedText []byte) {
removeIds = append(removeIds, id)
}
+ sentToIds = append(sentToIds, id)
}
lock.Unlock()
for _, id := range removeIds {
Remove(id)
}
+
+ return sentToIds
}
func SendTo(b []byte, ids ...ConnectionId) {
diff --git a/internal/gamelock/gamelock.go b/internal/gamelock/gamelock.go
index bbbb3d41..472cd719 100644
--- a/internal/gamelock/gamelock.go
+++ b/internal/gamelock/gamelock.go
@@ -5,6 +5,10 @@ import (
"github.com/volte6/gomud/internal/util"
)
+const (
+ DefaultRelockTime = `1 hour`
+)
+
type Lock struct {
Difficulty uint8 `yaml:"difficulty,omitempty"` // 0 - no lock. greater than zero = difficulty to unlock.
UnlockedRound uint64 `yaml:"-"` // What round it was unlocked at, when util.GetRoundCount() > UnlockedUntil, it is relocked (set to zero).
@@ -26,7 +30,7 @@ func (l Lock) IsLocked() bool {
gd := gametime.GetDate(rndNow)
if l.RelockInterval == `` {
- return rndNow >= gd.AddPeriod(`1 hour`)
+ return rndNow >= gd.AddPeriod(DefaultRelockTime)
}
return rndNow >= gd.AddPeriod(l.RelockInterval)
diff --git a/internal/items/itemspec.go b/internal/items/itemspec.go
index 9ff97af8..fb58f58b 100644
--- a/internal/items/itemspec.go
+++ b/internal/items/itemspec.go
@@ -40,7 +40,9 @@ type ItemTypeInfo struct {
func ItemTypes() []ItemTypeInfo {
return []ItemTypeInfo{
// Equipment
+ // Equipment - Weapons
{string(Weapon), `This can be wielded as a weapon.`, 0, 10000, 19999},
+ // Equipment - Armor
{string(Offhand), `This can be worn in the offhand.`, 0, 20000, 29999},
{string(Head), `This can be worn in the players head equipment slot.`, 0, 20000, 29999},
{string(Neck), `This can be worn in the players neck equipment slot.`, 0, 20000, 29999},
diff --git a/internal/items/newitemfile.go b/internal/items/newitemfile.go
index 0aa48cc0..1a73e3a5 100644
--- a/internal/items/newitemfile.go
+++ b/internal/items/newitemfile.go
@@ -67,7 +67,7 @@ func getNextItemId(t ItemType) int {
lowestFreeId := 1
for _, iSpec := range items {
- if iSpec.Type != t {
+ if iSpec.ItemId < rangeMin || iSpec.ItemId > rangeMax {
continue
}
diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go
index 5eb902fa..ffdf4b1d 100644
--- a/internal/prompt/prompt.go
+++ b/internal/prompt/prompt.go
@@ -39,9 +39,10 @@ type Question struct {
}
type Prompt struct {
- Command string // Where does it call when complete?
- Rest string // What is the 'rest' of the command
- Questions []*Question // All questions so far
+ Command string // Where does it call when complete?
+ Rest string // What is the 'rest' of the command
+ Questions []*Question // All questions so far
+ State map[string]any // Optional place to store state
}
func New(command string, rest string) *Prompt {
@@ -49,6 +50,7 @@ func New(command string, rest string) *Prompt {
Command: command,
Rest: rest,
Questions: make([]*Question, 0),
+ State: map[string]any{},
}
}
@@ -102,6 +104,17 @@ func (p *Prompt) GetNextQuestion() *Question {
return nil
}
+func (p *Prompt) Store(name string, val any) {
+ p.State[name] = val
+}
+
+func (p *Prompt) Recall(name string) (any, bool) {
+ if v, ok := p.State[name]; ok {
+ return v, true
+ }
+ return nil, false
+}
+
func (q *Question) Reset() {
q.Done = false
}
diff --git a/internal/templates/name_description.go b/internal/templates/name_description.go
index 6b105afe..3118d5df 100644
--- a/internal/templates/name_description.go
+++ b/internal/templates/name_description.go
@@ -2,7 +2,8 @@ package templates
// A common structure used in templating
type NameDescription struct {
- Id any // optional identifier.
+ Id any // optional identifier.
+ Marked bool // mark in some way?
Name string
Description string
}
diff --git a/internal/usercommands/admin.room.go b/internal/usercommands/admin.room.go
index f4f7af88..95fe22c3 100644
--- a/internal/usercommands/admin.room.go
+++ b/internal/usercommands/admin.room.go
@@ -2,9 +2,13 @@ package usercommands
import (
"fmt"
+ "sort"
"strconv"
"strings"
+ "github.com/volte6/gomud/internal/buffs"
+ "github.com/volte6/gomud/internal/gamelock"
+ "github.com/volte6/gomud/internal/items"
"github.com/volte6/gomud/internal/mobs"
"github.com/volte6/gomud/internal/mutators"
"github.com/volte6/gomud/internal/parties"
@@ -35,6 +39,15 @@ func Room(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) {
var roomId int = 0
roomCmd := strings.ToLower(args[0])
+ // 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`)
+ return true, nil
+ }
+
if roomCmd == `noun` || roomCmd == `nouns` {
// room noun chair "a chair for sitting"
@@ -132,7 +145,7 @@ func Room(rest string, user *users.UserRecord, room *rooms.Room) (bool, error) {
`room`: targetRoom,
`zone`: rooms.GetZoneConfig(targetRoom.Zone),
}
- fmt.Println(targetRoom.Exits)
+
infoOutput, _ := templates.Process("admincommands/ingame/roominfo", roomInfo)
user.SendText(infoOutput)
@@ -398,3 +411,590 @@ 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
+ type ContainerEdit struct {
+ Name string
+ NameNew string
+ Container rooms.Container
+ Exists bool
+ }
+
+ containerOptions := []templates.NameDescription{}
+
+ for name, c := range room.Containers {
+
+ // If it's ephemeral, don't bother.
+ if c.DespawnRound != 0 {
+ continue
+ }
+
+ containerOption := templates.NameDescription{Name: name}
+
+ if c.Lock.Difficulty > 0 {
+ containerOption.Description += fmt.Sprintf(`[Lvl %d Lock] `, c.Lock.Difficulty)
+ }
+
+ if len(c.Recipes) > 0 {
+ containerOption.Description += fmt.Sprintf(`[%d Recipe(s)] `, len(c.Recipes))
+ }
+
+ containerOptions = append(containerOptions, containerOption)
+
+ }
+
+ // Must sort since maps will often change between iterations
+ sort.SliceStable(containerOptions, func(i, j int) bool {
+ return containerOptions[i].Name < containerOptions[j].Name
+ })
+
+ //
+ // Create a holder for container editing data
+ //
+ currentlyEditing := ContainerEdit{}
+
+ cmdPrompt, _ := user.StartPrompt(`room edit containers`, rest)
+
+ question := cmdPrompt.Ask(`Choose one:`, []string{`new`}, `new`)
+ if !question.Done {
+ tplTxt, _ := templates.Process("tables/numbered-list", containerOptions)
+ user.SendText(tplTxt)
+ return true, nil
+ }
+
+ currentlyEditing.Name = question.Response
+
+ if restNum, err := strconv.Atoi(currentlyEditing.Name); err == nil {
+ if restNum > 0 && restNum <= len(containerOptions) {
+ currentlyEditing.Name = containerOptions[restNum-1].Name
+ }
+ }
+
+ for _, o := range containerOptions {
+ if strings.EqualFold(o.Name, currentlyEditing.Name) {
+ currentlyEditing.Name = o.Name
+ break
+ }
+ }
+
+ // Load the (possible) existing container
+ currentlyEditing.Container, currentlyEditing.Exists = room.Containers[currentlyEditing.Name]
+
+ // If they entered a container name...
+ if currentlyEditing.Name != `new` {
+
+ // Does the container 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 container that exists, lets get the question of delete out of the way immediately.
+ question := cmdPrompt.Ask(`Delete this container?`, []string{`yes`, `no`}, `no`)
+ if !question.Done {
+ return true, nil
+ }
+
+ // Delete the container if that's what they want!
+ if question.Response == `yes` {
+
+ delete(room.Containers, 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 container, we don't want that to become a viable container name, lets empty it
+ if currentlyEditing.Name == `new` {
+ currentlyEditing.Name = ``
+ }
+
+ // allow them to name/rename the container.
+ question := cmdPrompt.Ask(`Choose a name for this container:`, []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.Containers[currentlyEditing.NameNew]; ok {
+
+ user.SendText(`A container with that name already exists!`)
+ question.RejectResponse()
+ return true, nil
+
+ }
+ }
+
+ }
+
+ //
+ // Lock Options
+ //
+ {
+ question := cmdPrompt.Ask(`Will this container be locked?`, []string{`yes`, `no`}, util.BoolYN(currentlyEditing.Container.Lock.Difficulty > 0))
+ if !question.Done {
+ return true, nil
+ }
+
+ if question.Response == `yes` {
+
+ defaultDifficultyAnswer := ``
+ if currentlyEditing.Container.Lock.Difficulty > 0 {
+ defaultDifficultyAnswer = strconv.Itoa(int(currentlyEditing.Container.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.Container.Lock.Difficulty = uint8(difficultyInt)
+
+ } else {
+ // reset the lock state if there is no lock.
+ currentlyEditing.Container.Lock = gamelock.Lock{}
+ }
+
+ if currentlyEditing.Container.Lock.Difficulty > 0 {
+ //
+ // Lock Trap Options
+ //
+ question = cmdPrompt.Ask(`Will this lock have a trap?`, []string{`yes`, `no`}, util.BoolYN(len(currentlyEditing.Container.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.Container.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.Container.Lock.TrapBuffIds = cb.([]int)
+ }
+
+ if currentlyEditing.Container.Lock.RelockInterval == `` {
+ currentlyEditing.Container.Lock.RelockInterval = gamelock.DefaultRelockTime
+ }
+
+ question = cmdPrompt.Ask(`How long until it automatically relocks?`, []string{currentlyEditing.Container.Lock.RelockInterval}, currentlyEditing.Container.Lock.RelockInterval)
+ if !question.Done {
+ return true, nil
+ }
+
+ currentlyEditing.Container.Lock.RelockInterval = question.Response
+
+ // If the default time is chosen, can just leave it blank.
+ if currentlyEditing.Container.Lock.RelockInterval == gamelock.DefaultRelockTime {
+ currentlyEditing.Container.Lock.RelockInterval = ``
+ }
+
+ }
+ }
+
+ //
+ // Recipe Options
+ //
+ {
+ question := cmdPrompt.Ask(`Will this container have recipes?`, []string{`yes`, `no`}, util.BoolYN(len(currentlyEditing.Container.Recipes) > 0))
+ if !question.Done {
+ return true, nil
+ }
+
+ if question.Response == `yes` {
+
+ currentRecipes := map[int][]int{}
+ if cr, ok := cmdPrompt.Recall(`recipes`); ok {
+ currentRecipes = cr.(map[int][]int)
+ }
+
+ if len(currentRecipes) == 0 {
+ for k, v := range currentlyEditing.Container.Recipes {
+ currentRecipes[k] = append([]int{}, v...)
+ }
+ }
+
+ recipeNow := 0
+ if rNow, ok := cmdPrompt.Recall(`recipeNow`); ok {
+ recipeNow = rNow.(int)
+ }
+
+ if recipeNow != 0 && items.GetItemSpec(recipeNow) == nil {
+ user.SendText(`Invalid selection.`)
+ question.RejectResponse()
+ return true, nil
+ }
+
+ // Keep track of the state
+ cmdPrompt.Store(`recipes`, currentRecipes)
+ cmdPrompt.Store(`recipeNow`, recipeNow)
+
+ // Select recipe to modify
+ if _, ok := currentRecipes[recipeNow]; !ok {
+ recipeOptions := []templates.NameDescription{}
+ for productItemId, recipeItemList := range currentRecipes {
+
+ itm := items.New(productItemId)
+ productName := fmt.Sprintf(`%d (%s)`, productItemId, itm.DisplayName())
+
+ allRequiredItems := []string{}
+ for _, iId := range recipeItemList {
+ itm := items.New(iId)
+ allRequiredItems = append(allRequiredItems, fmt.Sprintf(`%d (%s)`, iId, itm.DisplayName()))
+ }
+
+ recipeOptions = append(recipeOptions,
+ templates.NameDescription{
+ Id: productItemId,
+ Marked: recipeNow == productItemId,
+ Name: productName,
+ Description: strings.Join(allRequiredItems, `, `),
+ })
+
+ }
+
+ recipeOptions = append(recipeOptions,
+ templates.NameDescription{
+ Id: 0,
+ Marked: false,
+ Name: `new`,
+ Description: `create a new recipe`,
+ })
+
+ recipeOptions = append(recipeOptions,
+ templates.NameDescription{
+ Id: -1,
+ Marked: false,
+ Name: `skip`,
+ Description: `skip this step`,
+ })
+
+ question := cmdPrompt.Ask(`Modify which (or new)?`, []string{`skip`}, `skip`)
+ if !question.Done {
+ tplTxt, _ := templates.Process("tables/numbered-list", recipeOptions)
+ user.SendText(tplTxt)
+ return true, nil
+ }
+
+ recipeSelected := question.Response
+ if restNum, err := strconv.Atoi(recipeSelected); err == nil {
+ if restNum > 0 && restNum <= len(recipeOptions) {
+ recipeNow = recipeOptions[restNum-1].Id.(int)
+ }
+ }
+
+ if recipeNow == 0 {
+ for _, b := range recipeOptions {
+ if strings.EqualFold(b.Name, recipeSelected) {
+ recipeNow = b.Id.(int)
+ break
+ }
+ }
+ }
+
+ if question.Response == `new` {
+
+ question := cmdPrompt.Ask(`What itemId will be created?`, []string{})
+ if !question.Done {
+ return true, nil
+ }
+
+ itemIdInt, _ := strconv.Atoi(question.Response)
+ if items.GetItemSpec(itemIdInt) == nil {
+
+ user.SendText("Invalid itemId.")
+ question.RejectResponse()
+
+ return true, nil
+ }
+
+ if _, ok := currentRecipes[itemIdInt]; !ok {
+ currentRecipes[itemIdInt] = []int{}
+ }
+
+ recipeNow = itemIdInt
+
+ // Keep track of the state
+ cmdPrompt.Store(`recipes`, currentRecipes)
+ cmdPrompt.Store(`recipeNow`, recipeNow)
+ }
+ }
+
+ // If they're editing a recipe, lets add ingredients
+ if recipeNow != -1 {
+
+ neededItems := map[int]int{}
+ for _, inputItemId := range currentRecipes[recipeNow] {
+ neededItems[inputItemId] = neededItems[inputItemId] + 1
+ }
+
+ question = cmdPrompt.Ask(`Enter an itemId to add to the recipe, or nothing to continue:`, []string{``}, `skip`)
+ if !question.Done {
+ // They have a recipe to modify, ask for item id's
+ user.SendText(``)
+ user.SendText(`Positive numbers add items, negative numbers remove items.`)
+
+ room_Edit_Containers_SendRecipes(user, recipeNow, neededItems)
+
+ return true, nil
+ }
+
+ if question.Response != `skip` {
+
+ removeItem := false
+ if question.Response[0] == '-' {
+ removeItem = true
+ question.Response = question.Response[1:]
+ }
+
+ recipeAdjustment := items.FindItem(question.Response)
+
+ if itemSpec := items.GetItemSpec(recipeAdjustment); itemSpec == nil {
+ user.SendText(`Invalid ItemId provided.`)
+
+ room_Edit_Containers_SendRecipes(user, recipeNow, neededItems)
+
+ question.RejectResponse()
+ return true, nil
+ }
+
+ if removeItem {
+
+ for idx, itemId := range currentRecipes[recipeNow] {
+
+ if itemId == recipeAdjustment {
+ currentRecipes[recipeNow] = append(currentRecipes[recipeNow][0:idx], currentRecipes[recipeNow][idx+1:]...)
+
+ neededItems[recipeAdjustment] -= 1
+
+ if neededItems[recipeAdjustment] == 0 {
+ delete(neededItems, recipeAdjustment)
+ }
+
+ break
+ }
+
+ }
+
+ } else {
+ currentRecipes[recipeNow] = append(currentRecipes[recipeNow], recipeAdjustment)
+ neededItems[recipeAdjustment] += 1
+ }
+
+ // Keep track of the state
+ cmdPrompt.Store(`recipes`, currentRecipes)
+ cmdPrompt.Store(`recipeNow`, recipeNow)
+
+ room_Edit_Containers_SendRecipes(user, recipeNow, neededItems)
+
+ question.RejectResponse()
+ return true, nil
+
+ }
+
+ }
+
+ if allRecipes, ok := cmdPrompt.Recall(`recipes`); ok {
+ currentlyEditing.Container.Recipes = allRecipes.(map[int][]int)
+
+ for i, itms := range currentlyEditing.Container.Recipes {
+ if len(itms) == 0 {
+ delete(currentlyEditing.Container.Recipes, i)
+ }
+ }
+ }
+
+ } else {
+ clear(currentlyEditing.Container.Recipes)
+ }
+
+ }
+
+ //
+ // Done editing. Save results
+ //
+ if currentlyEditing.Name != `` {
+ delete(room.Containers, currentlyEditing.Name)
+ }
+
+ room.Containers[currentlyEditing.NameNew] = currentlyEditing.Container
+ rooms.SaveRoom(*room)
+
+ 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
+}
diff --git a/internal/util/util.go b/internal/util/util.go
index 8a6764f9..b2fb4c3d 100644
--- a/internal/util/util.go
+++ b/internal/util/util.go
@@ -856,3 +856,10 @@ func ValidateWorldFiles(exampleWorldPath string, worldPath string) error {
return nil
}
+
+func BoolYN(b bool) string {
+ if b {
+ return `yes`
+ }
+ return `no`
+}
diff --git a/world.go b/world.go
index 30e61a64..21418963 100644
--- a/world.go
+++ b/world.go
@@ -769,12 +769,10 @@ func (w *World) processInput(userId int, inputText string) {
return
}
- connId := user.ConnectionId()
-
var activeQuestion *prompt.Question = nil
-
+ hadPrompt := false
if cmdPrompt := user.GetPrompt(); cmdPrompt != nil {
-
+ hadPrompt = true
if activeQuestion = cmdPrompt.GetNextQuestion(); activeQuestion != nil {
activeQuestion.Answer(string(inputText))
@@ -845,6 +843,9 @@ func (w *World) processInput(userId int, inputText string) {
}
}
+ } else {
+ connId := user.ConnectionId()
+ connections.SendTo([]byte(templates.AnsiParse(user.GetCommandPrompt(true))), connId)
}
if !handled {
@@ -857,7 +858,15 @@ func (w *World) processInput(userId int, inputText string) {
}
}
- connections.SendTo([]byte(templates.AnsiParse(user.GetCommandPrompt(true))), connId)
+ // If they had an input prompt, but now they don't, lets make sure to resend a status prompt
+ if hadPrompt {
+ connId := user.ConnectionId()
+ connections.SendTo([]byte(templates.AnsiParse(user.GetCommandPrompt(true))), connId)
+ }
+ // Removing this as possibly redundant.
+ // Leaving in case I need to remember that I did it...
+ //connId := user.ConnectionId()
+ //connections.SendTo([]byte(templates.AnsiParse(user.GetCommandPrompt(true))), connId)
}
@@ -1054,6 +1063,8 @@ func (w *World) MessageTick() {
}
+ redrawPrompts := make(map[uint64]string)
+
//
// System-wide broadcasts
//
@@ -1070,18 +1081,27 @@ func (w *World) MessageTick() {
messageColorized := templates.AnsiParse(broadcast.Text)
+ var sentToConnectionIds []connections.ConnectionId
+
if broadcast.SkipLineRefresh {
- connections.Broadcast([]byte(messageColorized))
- return
+ sentToConnectionIds = connections.Broadcast(
+ []byte(messageColorized),
+ )
+ } else {
+
+ sentToConnectionIds = connections.Broadcast(
+ []byte(term.AnsiMoveCursorColumn.String() + term.AnsiEraseLine.String() + messageColorized),
+ )
}
- connections.Broadcast(
- []byte(term.AnsiMoveCursorColumn.String() + term.AnsiEraseLine.String() + messageColorized),
- )
+ for _, connId := range sentToConnectionIds {
+ if _, ok := redrawPrompts[connId]; !ok {
+ user := users.GetByConnectionId(connId)
+ redrawPrompts[connId] = templates.AnsiParse(user.GetCommandPrompt(true))
+ }
+ }
}
- redrawPrompts := make(map[uint64]string)
-
eq = events.GetQueue(events.WebClientCommand{})
for eq.Len() > 0 {