diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/act_steal.go | 10 | ||||
| -rw-r--r-- | internal/game/act_talk.go | 53 | ||||
| -rw-r--r-- | internal/game/cmd_registry.go | 284 | ||||
| -rw-r--r-- | internal/game/cmd_shop.go | 525 | ||||
| -rw-r--r-- | internal/game/game.go | 6 |
5 files changed, 411 insertions, 467 deletions
diff --git a/internal/game/act_steal.go b/internal/game/act_steal.go index 1c76474..f5d3dbf 100644 --- a/internal/game/act_steal.go +++ b/internal/game/act_steal.go @@ -28,7 +28,7 @@ func (g *Game) executeSteal(sess *net.Session, args []string, rawInput string) { var stallGuardTalk = &behavior.TalkConfig{ Nodes: map[string]behavior.TalkNode{ "start": { - Message: behavior.MessageList{"Caught you red-handed! You have three options, thief."}, + Messages: []string{"Caught you red-handed! You have three options, thief."}, Options: []behavior.TalkOption{ {Text: "\"I'll pay a fine. (500 credits)\"", Goto: "bribe", Condition: &behavior.Condition{MinCredits: 500}}, {Text: "\"Take me to jail.\"", Goto: "jail"}, @@ -36,17 +36,17 @@ var stallGuardTalk = &behavior.TalkConfig{ }, }, "bribe": { - Message: behavior.MessageList{"Smart choice. Hand over 500 credits and we'll forget this happened."}, - Action: &behavior.NodeAction{Cost: 500}, + Messages: []string{"Smart choice. Hand over 500 credits and we'll forget this happened."}, + Action: &behavior.NodeAction{Credits: -500}, Options: []behavior.TalkOption{{Text: "\"Fine, take it.\""}}, }, "jail": { - Message: behavior.MessageList{"Off to the detention cell with you!"}, + Messages: []string{"Off to the detention cell with you!"}, Action: &behavior.NodeAction{Teleport: 162}, Options: []behavior.TalkOption{{Text: "(You are dragged away)"}}, }, "fight": { - Message: behavior.MessageList{"Then defend yourself!"}, + Messages: []string{"Then defend yourself!"}, Action: &behavior.NodeAction{SetFlags: map[string]any{"guard_hostile": true}}, Options: []behavior.TalkOption{{Text: "(The guard attacks!)"}}, }, diff --git a/internal/game/act_talk.go b/internal/game/act_talk.go index bd5a091..7cfdf7c 100644 --- a/internal/game/act_talk.go +++ b/internal/game/act_talk.go @@ -58,11 +58,6 @@ func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.Object } func (g *Game) handleNodeAction(sess *net.Session, na *behavior.NodeAction) (intercepted bool) { - if na.Shop != nil { - g.applyNodeAction(sess, na) - g.enterShop(sess, na.Shop) - return true - } g.applyNodeAction(sess, na) if v, ok := g.Flags.Get("guard_hostile"); ok && v != nil { @@ -97,28 +92,24 @@ func (g *Game) showTalkNode(sess *net.Session, node behavior.TalkNode) { for { if node.Condition == nil || g.checkCondition(sess, node.Condition) { - if len(node.Sequence) > 0 { + if len(node.Messages) > 0 { if node.Action != nil { if g.handleNodeAction(sess, node.Action) { return } } td.SeqIndex = 0 - msg := node.Sequence[0] + msg := node.Messages[0] if msg != "" { sess.WriteLine(color.ExpandDialogTags(g.colorMode(sess), dialogSpec, msg)) } td.SeqIndex = 1 - if td.SeqIndex < len(node.Sequence) { + if td.SeqIndex < len(node.Messages) { sess.State = net.StateTalkSequence sess.Write(g.colorize(sess, "dialog_options", "[enter to continue]\n")) return } } else { - msg := node.Message.Pick() - if msg != "" { - sess.WriteLine(color.ExpandDialogTags(g.colorMode(sess), dialogSpec, msg)) - } if node.Action != nil { if g.handleNodeAction(sess, node.Action) { return @@ -301,14 +292,14 @@ func (g *Game) advanceTalk(sess *net.Session, p *player.Player) { return } dialogSpec := g.resolveColor(sess, "dialog") - if td.SeqIndex < len(node.Sequence) { - msg := node.Sequence[td.SeqIndex] + if td.SeqIndex < len(node.Messages) { + msg := node.Messages[td.SeqIndex] if msg != "" { sess.WriteLine(color.ExpandDialogTags(g.colorMode(sess), dialogSpec, msg)) } td.SeqIndex++ } - if td.SeqIndex >= len(node.Sequence) { + if td.SeqIndex >= len(node.Messages) { g.finishSequence(sess, node) return } @@ -341,28 +332,12 @@ func (g *Game) advanceTalk(sess *net.Session, p *player.Player) { } } -func (g *Game) enterShop(sess *net.Session, cfg *behavior.ShopConfig) { - sess.Shop = cfg - sess.State = net.StateShop - g.showShopBrowse(sess) - g.writeShopPrompt(sess) -} - func (g *Game) applyNodeAction(sess *net.Session, na *behavior.NodeAction) { p := sess.Player if p == nil { return } - if na.AssignTask { - g.assignAssassinTask(sess, p) - } - if na.SkipTask { - g.skipAssassinTask(sess, p) - } - if na.ExtendTask { - g.extendAssassinTask(sess, p) - } if na.ReputationCost > 0 { rep := getPlayerFlagInt(p, "assassin_reputation") if rep < na.ReputationCost { @@ -414,15 +389,19 @@ func (g *Game) applyNodeAction(sess *net.Session, na *behavior.NodeAction) { g.AccountStore.SaveCharacter(p) sess.WriteLine(fmt.Sprintf("You regain %d hitpoints.", na.Heal)) } - if na.Cost > 0 { - if p.Credits >= na.Cost { - p.Credits -= na.Cost + if na.Credits != 0 { + if na.Credits < 0 { + if p.Credits >= -na.Credits { + p.Credits += na.Credits + g.AccountStore.SaveCharacter(p) + } else { + sess.WriteLine(fmt.Sprintf("You don't have enough credits. (Need %d, have %d)", -na.Credits, p.Credits)) + } + } else { + p.Credits += na.Credits g.AccountStore.SaveCharacter(p) } } - if na.Sawmill { - g.processSawmill(sess, p) - } if na.ApsNode { g.setPlayerFlag(p, "aps_node_"+strconv.Itoa(p.RoomID), true) } diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go index bc77936..1247fc3 100644 --- a/internal/game/cmd_registry.go +++ b/internal/game/cmd_registry.go @@ -15,147 +15,151 @@ type commandDef struct { } var commandRegistry = map[string]commandDef{ - "get": {(*Game).executeGet, ClassActive}, - "take": {(*Game).executeGet, ClassActive}, - "grab": {(*Game).executeGet, ClassActive}, - "pick": {(*Game).executeGet, ClassActive}, - "drop": {(*Game).executeDrop, ClassActive}, - "attack": {(*Game).executeAttack, ClassActive}, - "kill": {(*Game).executeAttack, ClassActive}, - "work": {(*Game).executeWork, ClassActive}, - "style": {(*Game).executeStyle, ClassInstant}, - "look": {(*Game).executeLook, ClassInstant}, - "l": {(*Game).executeLook, ClassInstant}, - "north": {(*Game).executeMove, ClassActive}, - "n": {(*Game).executeMove, ClassActive}, - "south": {(*Game).executeMove, ClassActive}, - "s": {(*Game).executeMove, ClassActive}, - "east": {(*Game).executeMove, ClassActive}, - "e": {(*Game).executeMove, ClassActive}, - "west": {(*Game).executeMove, ClassActive}, - "w": {(*Game).executeMove, ClassActive}, - "northeast": {(*Game).executeMove, ClassActive}, - "ne": {(*Game).executeMove, ClassActive}, - "northwest": {(*Game).executeMove, ClassActive}, - "nw": {(*Game).executeMove, ClassActive}, - "southeast": {(*Game).executeMove, ClassActive}, - "se": {(*Game).executeMove, ClassActive}, - "southwest": {(*Game).executeMove, ClassActive}, - "sw": {(*Game).executeMove, ClassActive}, - "up": {(*Game).executeMove, ClassActive}, - "u": {(*Game).executeMove, ClassActive}, - "down": {(*Game).executeMove, ClassActive}, - "d": {(*Game).executeMove, ClassActive}, - "say": {(*Game).executeSay, ClassInstant}, - "global": {(*Game).executeGlobal, ClassInstant}, - "who": {(*Game).executeWho, ClassInstant}, - "sc": {(*Game).executeScore, ClassInstant}, - "score": {(*Game).executeScore, ClassInstant}, - "skills": {(*Game).executeSkills, ClassInstant}, - "sk": {(*Game).executeSkills, ClassInstant}, - "task": {(*Game).executeTask, ClassInstant}, - "stats": {(*Game).executeStats, ClassInstant}, - "tech": {(*Game).executeTech, ClassInstant}, - "t": {(*Game).executeTech, ClassInstant}, - "i": {(*Game).executeInventory, ClassInstant}, - "inv": {(*Game).executeInventory, ClassInstant}, - "inventory": {(*Game).executeInventory, ClassInstant}, - "quit": {(*Game).executeQuit, ClassActive}, - "description": {(*Game).executeDescription, ClassInstant}, - "desc": {(*Game).executeDescription, ClassInstant}, - "option": {(*Game).executeOption, ClassInstant}, - "options": {(*Game).executeOption, ClassInstant}, - "color": {(*Game).executeColor, ClassInstant}, - "colors": {(*Game).executeColor, ClassInstant}, - "colortable": {(*Game).executeColortable, ClassInstant}, - "prompt": {(*Game).executePrompt, ClassInstant}, - "exits": {(*Game).executeExits, ClassInstant}, - "map": {(*Game).executeMap, ClassInstant}, - "symbol": {(*Game).executeSymbol, ClassInstant}, - "queued": {(*Game).executeQueued, ClassInstant}, - "help": {(*Game).executeHelp, ClassInstant}, - "mine": {(*Game).executeGather, ClassActive}, - "chop": {(*Game).executeGather, ClassActive}, - "fish": {(*Game).executeGather, ClassActive}, - "cut": {(*Game).executeGather, ClassActive}, - "pull": {(*Game).executeUse, ClassActive}, - "push": {(*Game).executeUse, ClassActive}, - "use": {(*Game).executeUse, ClassActive}, - "cook": {(*Game).executeCook, ClassActive}, - "smelt": {(*Game).executeSmelt, ClassActive}, - "smith": {(*Game).executeSmith, ClassActive}, - "craft": {(*Game).executeCraft, ClassActive}, - "eat": {(*Game).executeEat, ClassFree}, - "drink": {(*Game).executeDrink, ClassFree}, - "fletch": {(*Game).executeFletch, ClassFree}, - "clean": {(*Game).executeClean, ClassFree}, - "mix": {(*Game).executeMix, ClassActive}, - "id": {(*Game).executeIdentify, ClassActive}, - "identify": {(*Game).executeIdentify, ClassActive}, - "autotrigger": {(*Game).executeAutotrigger, ClassInstant}, - "auto": {(*Game).executeAutotrigger, ClassInstant}, - "mods": {(*Game).executeMods, ClassInstant}, - "modlist": {(*Game).executeMods, ClassInstant}, - "mod": {(*Game).executeMods, ClassInstant}, - "modules": {(*Game).executeMods, ClassInstant}, - "module": {(*Game).executeMods, ClassInstant}, - "sneak": {(*Game).executeSneak, ClassInstant}, - "trigger": {(*Game).executeTrigger, ClassActive}, - "home": {(*Game).executeHome, ClassInstant}, - "recall": {(*Game).executeHome, ClassInstant}, - "talk": {(*Game).executeTalk, ClassActive}, - "speak": {(*Game).executeTalk, ClassActive}, - "ask": {(*Game).executeTalk, ClassActive}, - "burn": {(*Game).executeBurn, ClassActive}, - "stoke": {(*Game).executeStoke, ClassActive}, - "alias": {(*Game).executeAlias, ClassInstant}, - "unalias": {(*Game).executeUnalias, ClassInstant}, - "search": {(*Game).executeSearch, ClassActive}, - "steal": {(*Game).executeSteal, ClassActive}, - "thieve": {(*Game).executeSteal, ClassActive}, - "plant": {(*Game).executePlant, ClassActive}, - "harvest": {(*Game).executeHarvest, ClassActive}, - "rake": {(*Game).executeRake, ClassActive}, - "water": {(*Game).executeWater, ClassActive}, - "cure": {(*Game).executeCure, ClassActive}, - "inspect": {(*Game).executeInspect, ClassInstant}, - "walk": {(*Game).executeWalk, ClassActive}, - "bank": {(*Game).executeBank, ClassActive}, - "construct": {(*Game).executeConstruct, ClassActive}, - "make": {(*Game).executeConstruct, ClassActive}, - "jack": {(*Game).executeJack, ClassActive}, - "jackin": {(*Game).executeJack, ClassActive}, - "eq": {(*Game).executeWear, ClassFree}, - "equipment": {(*Game).executeWear, ClassFree}, - "equip": {(*Game).executeWear, ClassFree}, - "wear": {(*Game).executeWear, ClassFree}, - "wield": {(*Game).executeWear, ClassFree}, - "remove": {(*Game).executeRemove, ClassFree}, - "unwear": {(*Game).executeRemove, ClassFree}, - "unwield": {(*Game).executeRemove, ClassFree}, - "aps": {(*Game).executeAps, ClassActive}, - "verbs": {(*Game).executeVerbs, ClassInstant}, - "what": {(*Game).executeVerbs, ClassInstant}, - "syntax": {(*Game).executeVerbs, ClassInstant}, - "commands": {(*Game).executeVerbs, ClassInstant}, - "stop": {(*Game).executeStop, ClassInstant}, - "hide": {(*Game).executeHide, ClassActive}, - "safespot": {(*Game).executeHide, ClassActive}, - "unhide": {(*Game).executeUnhide, ClassActive}, - "unsafespot": {(*Game).executeUnhide, ClassActive}, - "goto": {(*Game).executeGoto, ClassInstant}, - "summon": {(*Game).executeSummon, ClassInstant}, - "dig": {(*Game).executeDig, ClassInstant}, - "setflag": {(*Game).executeSetFlag, ClassInstant}, + "get": {(*Game).executeGet, ClassActive}, + "take": {(*Game).executeGet, ClassActive}, + "grab": {(*Game).executeGet, ClassActive}, + "pick": {(*Game).executeGet, ClassActive}, + "drop": {(*Game).executeDrop, ClassActive}, + "attack": {(*Game).executeAttack, ClassActive}, + "kill": {(*Game).executeAttack, ClassActive}, + "work": {(*Game).executeWork, ClassActive}, + "style": {(*Game).executeStyle, ClassInstant}, + "look": {(*Game).executeLook, ClassInstant}, + "l": {(*Game).executeLook, ClassInstant}, + "north": {(*Game).executeMove, ClassActive}, + "n": {(*Game).executeMove, ClassActive}, + "south": {(*Game).executeMove, ClassActive}, + "s": {(*Game).executeMove, ClassActive}, + "east": {(*Game).executeMove, ClassActive}, + "e": {(*Game).executeMove, ClassActive}, + "west": {(*Game).executeMove, ClassActive}, + "w": {(*Game).executeMove, ClassActive}, + "northeast": {(*Game).executeMove, ClassActive}, + "ne": {(*Game).executeMove, ClassActive}, + "northwest": {(*Game).executeMove, ClassActive}, + "nw": {(*Game).executeMove, ClassActive}, + "southeast": {(*Game).executeMove, ClassActive}, + "se": {(*Game).executeMove, ClassActive}, + "southwest": {(*Game).executeMove, ClassActive}, + "sw": {(*Game).executeMove, ClassActive}, + "up": {(*Game).executeMove, ClassActive}, + "u": {(*Game).executeMove, ClassActive}, + "down": {(*Game).executeMove, ClassActive}, + "d": {(*Game).executeMove, ClassActive}, + "say": {(*Game).executeSay, ClassInstant}, + "global": {(*Game).executeGlobal, ClassInstant}, + "who": {(*Game).executeWho, ClassInstant}, + "sc": {(*Game).executeScore, ClassInstant}, + "score": {(*Game).executeScore, ClassInstant}, + "skills": {(*Game).executeSkills, ClassInstant}, + "sk": {(*Game).executeSkills, ClassInstant}, + "task": {(*Game).executeTask, ClassInstant}, + "stats": {(*Game).executeStats, ClassInstant}, + "tech": {(*Game).executeTech, ClassInstant}, + "t": {(*Game).executeTech, ClassInstant}, + "i": {(*Game).executeInventory, ClassInstant}, + "inv": {(*Game).executeInventory, ClassInstant}, + "inventory": {(*Game).executeInventory, ClassInstant}, + "quit": {(*Game).executeQuit, ClassActive}, + "description": {(*Game).executeDescription, ClassInstant}, + "desc": {(*Game).executeDescription, ClassInstant}, + "option": {(*Game).executeOption, ClassInstant}, + "options": {(*Game).executeOption, ClassInstant}, + "color": {(*Game).executeColor, ClassInstant}, + "colors": {(*Game).executeColor, ClassInstant}, + "colortable": {(*Game).executeColortable, ClassInstant}, + "prompt": {(*Game).executePrompt, ClassInstant}, + "exits": {(*Game).executeExits, ClassInstant}, + "map": {(*Game).executeMap, ClassInstant}, + "symbol": {(*Game).executeSymbol, ClassInstant}, + "queued": {(*Game).executeQueued, ClassInstant}, + "help": {(*Game).executeHelp, ClassInstant}, + "mine": {(*Game).executeGather, ClassActive}, + "chop": {(*Game).executeGather, ClassActive}, + "fish": {(*Game).executeGather, ClassActive}, + "cut": {(*Game).executeGather, ClassActive}, + "pull": {(*Game).executeUse, ClassActive}, + "push": {(*Game).executeUse, ClassActive}, + "use": {(*Game).executeUse, ClassActive}, + "cook": {(*Game).executeCook, ClassActive}, + "smelt": {(*Game).executeSmelt, ClassActive}, + "smith": {(*Game).executeSmith, ClassActive}, + "craft": {(*Game).executeCraft, ClassActive}, + "eat": {(*Game).executeEat, ClassFree}, + "drink": {(*Game).executeDrink, ClassFree}, + "fletch": {(*Game).executeFletch, ClassFree}, + "clean": {(*Game).executeClean, ClassFree}, + "mix": {(*Game).executeMix, ClassActive}, + "id": {(*Game).executeIdentify, ClassActive}, + "identify": {(*Game).executeIdentify, ClassActive}, + "autotrigger": {(*Game).executeAutotrigger, ClassInstant}, + "auto": {(*Game).executeAutotrigger, ClassInstant}, + "mods": {(*Game).executeMods, ClassInstant}, + "modlist": {(*Game).executeMods, ClassInstant}, + "mod": {(*Game).executeMods, ClassInstant}, + "modules": {(*Game).executeMods, ClassInstant}, + "module": {(*Game).executeMods, ClassInstant}, + "sneak": {(*Game).executeSneak, ClassInstant}, + "trigger": {(*Game).executeTrigger, ClassActive}, + "home": {(*Game).executeHome, ClassInstant}, + "recall": {(*Game).executeHome, ClassInstant}, + "talk": {(*Game).executeTalk, ClassActive}, + "speak": {(*Game).executeTalk, ClassActive}, + "ask": {(*Game).executeTalk, ClassActive}, + "burn": {(*Game).executeBurn, ClassActive}, + "stoke": {(*Game).executeStoke, ClassActive}, + "alias": {(*Game).executeAlias, ClassInstant}, + "unalias": {(*Game).executeUnalias, ClassInstant}, + "search": {(*Game).executeSearch, ClassActive}, + "steal": {(*Game).executeSteal, ClassActive}, + "thieve": {(*Game).executeSteal, ClassActive}, + "plant": {(*Game).executePlant, ClassActive}, + "harvest": {(*Game).executeHarvest, ClassActive}, + "rake": {(*Game).executeRake, ClassActive}, + "water": {(*Game).executeWater, ClassActive}, + "cure": {(*Game).executeCure, ClassActive}, + "inspect": {(*Game).executeInspect, ClassInstant}, + "walk": {(*Game).executeWalk, ClassActive}, + "bank": {(*Game).executeBank, ClassActive}, + "list": {(*Game).executeShopList, ClassInstant}, + "browse": {(*Game).executeShopList, ClassInstant}, + "buy": {(*Game).executeShopBuy, ClassInstant}, + "sell": {(*Game).executeShopSell, ClassInstant}, + "construct": {(*Game).executeConstruct, ClassActive}, + "make": {(*Game).executeConstruct, ClassActive}, + "jack": {(*Game).executeJack, ClassActive}, + "jackin": {(*Game).executeJack, ClassActive}, + "eq": {(*Game).executeWear, ClassFree}, + "equipment": {(*Game).executeWear, ClassFree}, + "equip": {(*Game).executeWear, ClassFree}, + "wear": {(*Game).executeWear, ClassFree}, + "wield": {(*Game).executeWear, ClassFree}, + "remove": {(*Game).executeRemove, ClassFree}, + "unwear": {(*Game).executeRemove, ClassFree}, + "unwield": {(*Game).executeRemove, ClassFree}, + "aps": {(*Game).executeAps, ClassActive}, + "verbs": {(*Game).executeVerbs, ClassInstant}, + "what": {(*Game).executeVerbs, ClassInstant}, + "syntax": {(*Game).executeVerbs, ClassInstant}, + "commands": {(*Game).executeVerbs, ClassInstant}, + "stop": {(*Game).executeStop, ClassInstant}, + "hide": {(*Game).executeHide, ClassActive}, + "safespot": {(*Game).executeHide, ClassActive}, + "unhide": {(*Game).executeUnhide, ClassActive}, + "unsafespot": {(*Game).executeUnhide, ClassActive}, + "goto": {(*Game).executeGoto, ClassInstant}, + "summon": {(*Game).executeSummon, ClassInstant}, + "dig": {(*Game).executeDig, ClassInstant}, + "setflag": {(*Game).executeSetFlag, ClassInstant}, "setplayerflag": {(*Game).executeSetPlayerFlag, ClassInstant}, - "reload": {(*Game).executeReload, ClassInstant}, - "shutdown": {(*Game).executeShutdown, ClassInstant}, - "room": {(*Game).executeRoom, ClassInstant}, - "swapid": {(*Game).executeSwapID, ClassInstant}, - "undig": {(*Game).executeUndig, ClassInstant}, - "close": {(*Game).executeClose, ClassInstant}, - "god": {(*Game).executeGod, ClassInstant}, - "ungod": {(*Game).executeUnGod, ClassInstant}, + "reload": {(*Game).executeReload, ClassInstant}, + "shutdown": {(*Game).executeShutdown, ClassInstant}, + "room": {(*Game).executeRoom, ClassInstant}, + "swapid": {(*Game).executeSwapID, ClassInstant}, + "undig": {(*Game).executeUndig, ClassInstant}, + "close": {(*Game).executeClose, ClassInstant}, + "god": {(*Game).executeGod, ClassInstant}, + "ungod": {(*Game).executeUnGod, ClassInstant}, } func classifyCommand(cmd string) CommandClass { diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go index 69de750..e47c84d 100644 --- a/internal/game/cmd_shop.go +++ b/internal/game/cmd_shop.go @@ -8,39 +8,72 @@ import ( "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" "thehouseoficarus/internal/ui" + "thehouseoficarus/internal/world" ) -func (g *Game) handleShopInput(sess *net.Session, input string) { - if sess.Player == nil { - sess.State = net.StateGame - g.writePrompt(sess) +// ---- Command entry points ---- + +func (g *Game) executeShopList(sess *net.Session, args []string, rawInput string) { + p := sess.Player + if p == nil { return } - - input = strings.TrimSpace(input) - parts := strings.Fields(strings.ToLower(input)) - if len(parts) == 0 { - g.showShopBrowse(sess) + mob := g.resolveShopMob(sess, p, strings.Join(args, " ")) + if mob == nil { return } + g.showShopInventory(sess, mob) +} - cmd := parts[0] +func (g *Game) executeShopBuy(sess *net.Session, args []string, rawInput string) { + p := sess.Player + if p == nil { + return + } + itemArgs, mobName := splitOnKeyword(args, "from") + qty, itemName, all := parseShopQty(itemArgs) + if itemName == "" { + sess.WriteLine("Buy what? Use 'list' to see what's for sale.") + return + } + mob := g.resolveShopMob(sess, p, mobName) + if mob == nil { + return + } + g.doShopBuy(sess, mob, itemName, qty, all) +} - switch cmd { - case "buy": - g.cmdShopBuy(sess, parts[1:]) - case "sell": - g.cmdShopSell(sess, parts[1:]) - case "browse", "list": - g.showShopBrowse(sess) - case "leave", "bye", "exit", "quit": - g.leaveShop(sess) +func (g *Game) executeShopSell(sess *net.Session, args []string, rawInput string) { + p := sess.Player + if p == nil { + return + } + itemArgs, mobName := splitOnKeyword(args, "to") + qty, itemName, all := parseShopQty(itemArgs) + if itemName == "" { + sess.WriteLine("Sell what?") + return + } + mob := g.resolveShopMob(sess, p, mobName) + if mob == nil { return - default: - sess.WriteLine("Commands: buy <item>, sell <item>, browse, leave") } - g.writeShopPrompt(sess) + g.doShopSell(sess, mob, itemName, qty, all) +} + +// ---- Helpers ---- + +// splitOnKeyword splits args around the first standalone keyword (e.g. "from", +// "to"), returning the words before it and the joined words after it. +func splitOnKeyword(args []string, kw string) (before []string, after string) { + for i, a := range args { + if strings.EqualFold(a, kw) { + return args[:i], strings.Join(args[i+1:], " ") + } + } + return args, "" } func parseShopQty(args []string) (int, string, bool) { @@ -48,8 +81,7 @@ func parseShopQty(args []string) (int, string, bool) { return 1, "", false } if strings.EqualFold(args[0], "all") { - rest := strings.Join(args[1:], " ") - return 0, rest, true + return 0, strings.Join(args[1:], " "), true } if n, err := strconv.Atoi(args[0]); err == nil && n > 0 { return n, strings.Join(args[1:], " "), false @@ -57,63 +89,100 @@ func parseShopQty(args []string) (int, string, bool) { return 1, strings.Join(args, " "), false } -func (g *Game) cmdShopBuy(sess *net.Session, args []string) { - cfg := g.shopConfig(sess) - if cfg == nil { - return +// resolveShopMob finds the shop-owning mob in the player's room. If name is +// empty and exactly one shop exists it is returned; multiple shops prompt +// "Which shop?". A non-empty name matches a specific mob. Errors are written to +// the session and nil is returned when resolution fails. +func (g *Game) resolveShopMob(sess *net.Session, p *player.Player, name string) *world.MobInstance { + var shops []*world.MobInstance + for _, m := range g.MobStore.MobsInRoom(p.RoomID) { + if m.HasShop() { + shops = append(shops, m) + } } - - qty, itemName, all := parseShopQty(args) - if itemName == "" { - sess.WriteLine("Buy what? Use 'browse' to see available items.") - return + if len(shops) == 0 { + sess.WriteLine("There is no shop here.") + return nil } - g.doShopBuy(sess, itemName, qty, all) -} - -func (g *Game) cmdShopSell(sess *net.Session, args []string) { - cfg := g.shopConfig(sess) - if cfg == nil { - return + name = strings.TrimSpace(name) + if name != "" { + var matched []*world.MobInstance + for _, m := range shops { + if m.MatchQuality(name) > world.MatchNone { + matched = append(matched, m) + } + } + if len(matched) == 0 { + sess.WriteLine(fmt.Sprintf("There is no shop here run by \"%s\".", name)) + return nil + } + if len(matched) > 1 { + g.promptWhichShop(sess, matched) + return nil + } + return matched[0] } - qty, itemName, all := parseShopQty(args) - if itemName == "" { - sess.WriteLine("Sell what?") - return + if len(shops) == 1 { + return shops[0] } - - g.doShopSell(sess, itemName, qty, all) -} - -func (g *Game) writeShopPrompt(sess *net.Session) { - sess.WritePrompt(g.colorize(sess, "dialog", "> ")) + g.promptWhichShop(sess, shops) + return nil } -func (g *Game) shopConfig(sess *net.Session) *behavior.ShopConfig { - cfg := sess.Shop - return cfg +func (g *Game) promptWhichShop(sess *net.Session, shops []*world.MobInstance) { + sess.WriteLine("Which shop? Try again naming one of:") + for _, m := range shops { + sess.WriteLine(" - " + m.Name) + } } -type shopMatch struct { - si *behavior.ShopItem - name string - def *item.ItemDef +type shopCandidate struct { + itemID string + def *item.ItemDef + name string + stock int } -func (g *Game) showShopBrowse(sess *net.Session) { - cfg := g.shopConfig(sess) - if cfg == nil { - return +// shopCandidates lists everything the shop currently offers for sale: its +// configured items (even when out of stock) plus any dynamically-held items +// with stock remaining. +func (g *Game) shopCandidates(mob *world.MobInstance) []shopCandidate { + stock := g.MobStore.ShopStockSnapshot(mob.InstanceID) + seen := map[string]bool{} + var out []shopCandidate + add := func(id string) { + if id == "" || seen[id] { + return + } + seen[id] = true + def, _ := g.ItemStore.Load(id) + name := id + if def != nil { + name = def.Name + } + out = append(out, shopCandidate{itemID: id, def: def, name: name, stock: stock[id]}) + } + for i := range mob.Shop.Items { + add(mob.Shop.Items[i].ItemID) } + for id, n := range stock { + if n > 0 { + add(id) + } + } + return out +} - if cfg.Message != "" { - sess.WriteLine(fmt.Sprintf("%s", g.colorize(sess, "dialog", cfg.Message))) +func (g *Game) showShopInventory(sess *net.Session, mob *world.MobInstance) { + if mob.Shop.Message != "" { + sess.WriteLine(g.colorize(sess, "dialog", mob.Shop.Message)) } - if len(cfg.Items) == 0 { - sess.WriteLine("This shop has nothing for sale.") + candidates := g.shopCandidates(mob) + if len(candidates) == 0 { + sess.WriteLine(fmt.Sprintf("%s has nothing for sale.", mob.Name)) return } @@ -125,199 +194,113 @@ func (g *Game) showShopBrowse(sess *net.Session) { } t := ui.Table{ - Title: "Shop Inventory", - Columns: []string{"Item", "Buy Price", "Sell Price"}, + Title: mob.Name, + Columns: []string{"Item", "Stock", "Buy", "Sell"}, } - - for _, si := range cfg.Items { - def, _ := g.ItemStore.Load(si.ItemID) - itemName := si.ItemID - if def != nil { - itemName = def.Name + for _, c := range candidates { + value := 0 + if c.def != nil { + value = c.def.Value } - buyStr := fmt.Sprintf("%d cr", si.BuyPrice) sellStr := "\u2014" - if si.SellPrice > 0 { - sellStr = fmt.Sprintf("%d cr", si.SellPrice) + if mob.Shop.FindItem(c.itemID) != nil || mob.Shop.Buys() { + sellStr = fmt.Sprintf("%d cr", mob.Shop.SellPrice(value, c.stock)) } t.Rows = append(t.Rows, []string{ - g.itemColorize(sess, def, itemName), - g.colorize(sess, "credits_pickup", buyStr), + g.itemColorize(sess, c.def, c.name), + fmt.Sprintf("%d", c.stock), + g.colorize(sess, "credits_pickup", fmt.Sprintf("%d cr", value)), g.colorize(sess, "credits_pickup", sellStr), }) } - for _, line := range t.Render(unicode, wrapWidth) { sess.WriteLine(line) } } -func (g *Game) findShopMatches(input string, cfg *behavior.ShopConfig) []shopMatch { - var matches []shopMatch - for i := range cfg.Items { - si := &cfg.Items[i] - itemName := si.ItemID - def, _ := g.ItemStore.Load(si.ItemID) - if def != nil { - itemName = def.Name +func (g *Game) doShopBuy(sess *net.Session, mob *world.MobInstance, input string, qty int, all bool) { + var matches []shopCandidate + for _, c := range g.shopCandidates(mob) { + if behavior.WordPrefixMatch(input, c.name) { + matches = append(matches, c) } - if behavior.WordPrefixMatch(input, itemName) { - matches = append(matches, shopMatch{si: si, name: itemName, def: def}) - } - } - return matches -} - -func (g *Game) doShopBuy(sess *net.Session, input string, qty int, all bool) { - cfg := g.shopConfig(sess) - if cfg == nil { - return } - - p := sess.Player - - matches := g.findShopMatches(input, cfg) if len(matches) == 0 { - sess.WriteLine("That item isn't for sale here. Use 'browse' to see shop inventory.") + sess.WriteLine("That item isn't for sale here. Use 'list' to see the inventory.") return } - if len(matches) > 1 { sess.WriteLine("That's ambiguous, which one?") for _, m := range matches { - sess.WriteLine(fmt.Sprintf(" - %s", g.itemColorize(sess, m.def, m.name))) + sess.WriteLine(" - " + g.itemColorize(sess, m.def, m.name)) } return } - si := matches[0].si - def := matches[0].def - - actualQty := qty - if all || qty == 0 { - affordable := p.Credits / si.BuyPrice - if def != nil && def.Stackable { - actualQty = affordable - } else { - freeSlots := p.FreeSlots() - actualQty = affordable - if actualQty > freeSlots { - actualQty = freeSlots - } - } + c := matches[0] + p := sess.Player + price := 0 + if c.def != nil { + price = c.def.Value } - if actualQty <= 0 { - if p.Credits < si.BuyPrice { - sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits)) - } else { - sess.WriteLine("Your inventory is full.") - } + stock := g.MobStore.ShopStockOf(mob.InstanceID, c.itemID) + if stock <= 0 { + sess.WriteLine(fmt.Sprintf("%s is out of stock.", g.itemColorize(sess, c.def, c.name))) return } - g.buyShopItem(sess, *si, actualQty) -} - -func (g *Game) buyShopItem(sess *net.Session, si behavior.ShopItem, qty int) { - p := sess.Player - - totalCost := si.BuyPrice * qty - if p.Credits < totalCost { - qty = p.Credits / si.BuyPrice - if qty <= 0 { - sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits)) - return - } - totalCost = si.BuyPrice * qty + const unlimited = 1 << 30 + want := qty + if all || qty == 0 { + want = unlimited } - - def, _ := g.ItemStore.Load(si.ItemID) - displayName := si.ItemID - if def != nil { - displayName = def.Name + if want > stock { + want = stock } - - if def != nil && def.Stackable { - for i := 0; i < 28; i++ { - slot := p.InvSlot(i) - if slot != nil && slot.ItemID == si.ItemID { - p.Credits -= totalCost - slot.Quantity += qty - g.AccountStore.SaveCharacter(p) - itemColor := g.itemColorize(sess, def, displayName) - if qty == 1 { - sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice)))) - } else { - sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", qty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCost)))) - } - return - } - } - - freeSlot := p.FirstFreeSlot() - if freeSlot == -1 { - sess.WriteLine("Your inventory is full.") - return - } - - p.Credits -= totalCost - p.SetInvSlot(freeSlot, g.newInventorySlot(si.ItemID, qty)) - g.AccountStore.SaveCharacter(p) - - itemColor := g.itemColorize(sess, def, displayName) - if qty == 1 { - sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice)))) - } else { - sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", qty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCost)))) - } - return + capacity := g.shopInvCapacity(p, c.itemID, c.def) + if want > capacity { + want = capacity } - - freeSlots := p.FreeSlots() - if qty > freeSlots { - if freeSlots == 0 { - sess.WriteLine("Your inventory is full.") - return - } - qty = freeSlots - totalCost = si.BuyPrice * qty - if p.Credits < totalCost { - qty = p.Credits / si.BuyPrice - totalCost = si.BuyPrice * qty + if price > 0 { + if afford := p.Credits / price; want > afford { + want = afford } } - if qty <= 0 { - if p.Credits < si.BuyPrice { - sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits)) - } else { + if want <= 0 { + switch { + case capacity <= 0: sess.WriteLine("Your inventory is full.") + case price > 0 && p.Credits < price: + sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", price, p.Credits)) + default: + sess.WriteLine("You can't buy that right now.") } return } - p.Credits -= totalCost - for j := 0; j < qty; j++ { - freeSlot := p.FirstFreeSlot() - p.SetInvSlot(freeSlot, g.newInventorySlot(si.ItemID, 1)) + taken := g.MobStore.ShopTake(mob.InstanceID, c.itemID, want) + if taken <= 0 { + sess.WriteLine(fmt.Sprintf("%s is out of stock.", g.itemColorize(sess, c.def, c.name))) + return } + + total := price * taken + p.Credits -= total + g.giveShopItems(p, c.itemID, taken, c.def) g.AccountStore.SaveCharacter(p) - itemColor := g.itemColorize(sess, def, displayName) - if qty == 1 { - sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice)))) + itemColor := g.itemColorize(sess, c.def, c.name) + credits := g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", total)) + if taken == 1 { + sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, credits)) } else { - sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", qty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCost)))) + sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", taken, itemColor, credits)) } } -func (g *Game) doShopSell(sess *net.Session, input string, qty int, all bool) { - cfg := g.shopConfig(sess) - if cfg == nil { - return - } - +func (g *Game) doShopSell(sess *net.Session, mob *world.MobInstance, input string, qty int, all bool) { p := sess.Player matches := g.findInventoryMatches(input, p) @@ -325,35 +308,28 @@ func (g *Game) doShopSell(sess *net.Session, input string, qty int, all bool) { sess.WriteLine("You don't have that item.") return } - if len(matches) > 1 { sess.WriteLine("That's ambiguous, which one?") for _, m := range matches { def, _ := g.ItemStore.Load(m.ID) - sess.WriteLine(fmt.Sprintf(" - %s", g.itemColorize(sess, def, m.Name))) + sess.WriteLine(" - " + g.itemColorize(sess, def, m.Name)) } return } match := matches[0] + def, _ := g.ItemStore.Load(match.ID) + displayName := match.Name + itemColor := g.itemColorize(sess, def, displayName) - var shopItem *behavior.ShopItem - for i := range cfg.Items { - si := &cfg.Items[i] - if si.ItemID == match.ID && si.SellPrice > 0 { - shopItem = si - break - } + if mob.Shop.FindItem(match.ID) == nil && !mob.Shop.Buys() { + sess.WriteLine(fmt.Sprintf("%s doesn't want to buy %s.", mob.Name, itemColor)) + return } - if shopItem == nil { - def, _ := g.ItemStore.Load(match.ID) - displayName := match.ID - if def != nil { - displayName = def.Name - } - sess.WriteLine(fmt.Sprintf("The shop doesn't want to buy %s.", g.itemColorize(sess, def, displayName))) - return + value := 0 + if def != nil { + value = def.Value } totalInInv := p.CountItem(match.ID) @@ -363,79 +339,66 @@ func (g *Game) doShopSell(sess *net.Session, input string, qty int, all bool) { } else if actualQty > totalInInv { actualQty = totalInInv } - if actualQty <= 0 { sess.WriteLine("You don't have that item.") return } + stock := g.MobStore.ShopStockOf(mob.InstanceID, match.ID) + total := 0 + for k := 0; k < actualQty; k++ { + total += mob.Shop.SellPrice(value, stock+k) + } + p.RemoveItem(match.ID, actualQty) - p.Credits += shopItem.SellPrice * actualQty + p.Credits += total + g.MobStore.ShopAdd(mob.InstanceID, match.ID, actualQty) g.AccountStore.SaveCharacter(p) - def, _ := g.ItemStore.Load(match.ID) - displayName := match.Name - itemColor := g.itemColorize(sess, def, displayName) + credits := g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", total)) if actualQty == 1 { - sess.WriteLine(fmt.Sprintf("You sell a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", shopItem.SellPrice)))) + sess.WriteLine(fmt.Sprintf("You sell a %s for %s credits.", itemColor, credits)) } else { - totalCredits := shopItem.SellPrice * actualQty - sess.WriteLine(fmt.Sprintf("You sell %d %s for %s credits.", actualQty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCredits)))) + sess.WriteLine(fmt.Sprintf("You sell %d %s for %s credits.", actualQty, itemColor, credits)) } } -func (g *Game) leaveShop(sess *net.Session) { - sess.Shop = nil - p := sess.Player - if p == nil || p.Action == nil || p.Action.Type != "talk" { - sess.State = net.StateGame - if p != nil { - g.cancelAction(p) +// shopInvCapacity reports how many of an item the player can still carry. +func (g *Game) shopInvCapacity(p *player.Player, itemID string, def *item.ItemDef) int { + if def != nil && def.Stackable { + for i := 0; i < 28; i++ { + if slot := p.InvSlot(i); slot != nil && slot.ItemID == itemID { + return 1 << 30 + } } - g.writePrompt(sess) - return - } - - td, ok := p.Action.Data.(*behavior.TalkData) - if !ok || td.Cfg == nil { - sess.State = net.StateGame - g.cancelAction(p) - g.writePrompt(sess) - return + if p.FirstFreeSlot() != -1 { + return 1 << 30 + } + return 0 } + return p.FreeSlots() +} - node, ok := td.Cfg.Nodes[td.Node] - if !ok { - sess.State = net.StateGame - g.cancelAction(p) - g.writePrompt(sess) +// giveShopItems places qty of an item into the player's inventory, stacking when +// possible. Callers must ensure capacity via shopInvCapacity first. +func (g *Game) giveShopItems(p *player.Player, itemID string, qty int, def *item.ItemDef) { + if def != nil && def.Stackable { + for i := 0; i < 28; i++ { + if slot := p.InvSlot(i); slot != nil && slot.ItemID == itemID { + slot.Quantity += qty + return + } + } + if slot := p.FirstFreeSlot(); slot != -1 { + p.SetInvSlot(slot, g.newInventorySlot(itemID, qty)) + } return } - - for { - visible := g.visibleOptions(sess, node.Options) - if len(visible) > 0 { - for i, opt := range visible { - sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, opt.Text)) - } - sess.State = net.StateTalk - g.reprompt(sess) + for j := 0; j < qty; j++ { + slot := p.FirstFreeSlot() + if slot == -1 { return } - - if node.Goto != "" { - next, ok := td.Cfg.Nodes[node.Goto] - if !ok { - break - } - td.Node = node.Goto - node = next - } else { - break - } + p.SetInvSlot(slot, g.newInventorySlot(itemID, 1)) } - - sess.State = net.StateGame - g.cancelAction(p) - g.writePrompt(sess) } diff --git a/internal/game/game.go b/internal/game/game.go index c30f9aa..4cd3b30 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -56,14 +56,14 @@ type Game struct { queue *CommandQueue safespot *SafespotManager ValidationConfig config.ValidationConfig - StartingRoom int + StartingRoom int // Per-tick / per-session runtime bookkeeping. charsMu sync.Mutex loggedInChars map[string]*net.Session restTimers map[string]uint64 guardWatchTimers map[string]int - hackingStates map[string]*hacking.Session + hackingStates map[string]*hacking.Session pendingDepletions []pendingDepletion farmTickCounter int enterMu sync.Mutex @@ -162,8 +162,6 @@ func (g *Game) HandleSession(sess *net.Session, input string) { g.handleDescChange(sess, input) case net.StateTalk, net.StateTalkSequence: g.handleTalkInput(sess, input) - case net.StateShop: - g.handleShopInput(sess, input) case net.StateBank: g.handleBankInput(sess, input) case net.StateDropAllConfirm: |
