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 accessibility text +## Containers & Recipes + +_Containers hold stuff. Recipes turn containers into "Crafting machines"._ + +accessibility text + ## 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 {