package game import ( "fmt" "sort" "strconv" "strings" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/color" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" "thehouseoficarus/internal/ui" ) func (g *Game) handleRecipeChoice(sess *net.Session, input string) { input = strings.TrimSpace(input) menuData := sess.PendingMenu if len(menuData) == 0 { sess.State = net.StateGame g.reprompt(sess) return } if input == "" { sess.PendingMenu = nil sess.State = net.StateGame sess.WriteLine("Never mind.") g.reprompt(sess) return } if choice, err := strconv.Atoi(input); err == nil { sess.PendingMenu = nil sess.State = net.StateGame if choice <= 0 || choice > len(menuData) { sess.WriteLine("Never mind.") g.reprompt(sess) return } g.dispatchMenuEntry(sess, menuData[choice-1]) return } lower := strings.ToLower(input) matchIdx := -1 for i, entry := range menuData { name := g.menuEntryName(entry) if name == "" { continue } if strings.ToLower(name) == lower || behavior.WordPrefixMatch(input, name) { if matchIdx >= 0 { sess.WriteLine("That's ambiguous.") return } matchIdx = i } } if matchIdx >= 0 { sess.PendingMenu = nil sess.State = net.StateGame g.dispatchMenuEntry(sess, menuData[matchIdx]) return } sess.PendingMenu = nil sess.State = net.StateGame sess.WriteLine("Never mind.") g.reprompt(sess) } func (g *Game) menuEntryName(entry map[string]string) string { if iid, ok := entry["item_id"]; ok { if def, _ := g.ItemStore.Load(iid); def != nil { return def.Name } return iid } if barID, ok := entry["bar_id"]; ok { if def, _ := g.ItemStore.Load(barID); def != nil { return def.Name } return barID } return "" } func (g *Game) dispatchMenuEntry(sess *net.Session, entry map[string]string) { if iid, ok := entry["item_id"]; ok { g.promptHowMany(sess, iid) return } if barID, ok := entry["bar_id"]; ok { g.showSmithTable(sess, sess.Player, barID) return } g.reprompt(sess) } func (g *Game) formatMaterials(sess *net.Session, item *item.ItemDef) string { craft := item.FirstCraft() if craft == nil { return "" } var parts []string for _, e := range craft.Ingredients { itemName := e.Items[0] if def, err := g.ItemStore.Load(e.Items[0]); err == nil { itemName = g.itemColorize(sess, def, def.Name) } if e.Quantity > 1 { parts = append(parts, fmt.Sprintf("%s x%d", itemName, e.Quantity)) } else { parts = append(parts, itemName) } } return strings.Join(parts, ", ") } func (g *Game) showProductionTable(sess *net.Session, p *player.Player, items []*item.ItemDef, title, skill, lastFlagKey, promptVerb string, background bool) { sort.Slice(items, func(i, j int) bool { return items[i].FirstCraft().Level < items[j].FirstCraft().Level }) mode := g.colorMode(sess) skillLevel := p.Level(player.SkillName(skill)) dimSpec := g.resolveConstant(sess, "production_locked") tbl := &ui.Table{ Title: title, Columns: []string{"#", "Product", "Materials", "Level"}, } for i, item := range items { craft := item.FirstCraft() outDef, _ := g.ItemStore.Load(item.ID) productName := item.ID if outDef != nil { productName = outDef.Name } outputQty := craft.OutputQty if outputQty <= 0 { outputQty = 1 } if outDef != nil && outDef.Stackable && outputQty > 1 { productName = fmt.Sprintf("%s x%d", productName, outputQty) } matStr := g.formatMaterials(sess, item) levelStr := fmt.Sprint(craft.Level) numStr := fmt.Sprintf("%d", i+1) canMake := skillLevel >= craft.Level && craftHasAllItemsQty(craft, p.CountItem) if canMake { productName = g.itemColorize(sess, outDef, productName) } else { productName = color.Render(mode, dimSpec, productName) matStr = color.Render(mode, dimSpec, matStr) levelStr = color.Render(mode, dimSpec, levelStr) numStr = color.Render(mode, dimSpec, numStr) } tbl.Rows = append(tbl.Rows, []string{numStr, productName, matStr, levelStr}) } unicode := p.OptionBool("unicode") sess.WriteLine("") for _, line := range tbl.Render(unicode, p.OptionInt("wrap_width")) { sess.WriteLine(line) } lastItemID, _ := p.Flags[lastFlagKey].(string) hint := "" if lastItemID != "" { for _, item := range items { if item.ID == lastItemID { if outDef, err := g.ItemStore.Load(item.ID); err == nil { hint = outDef.Name } break } } } if hint != "" { sess.Write(fmt.Sprintf("%s what (enter for all %s): ", promptVerb, hint)) } else { sess.Write(fmt.Sprintf("%s what: ", promptVerb)) } sess.PendingMenu = itemMenuData(itemIDs(items)) sess.PendingSkill = skill sess.PendingLastFlag = lastFlagKey sess.PendingBackground = background sess.State = net.StateProductChoice } func (g *Game) handleProductChoice(sess *net.Session, input string) { p := sess.Player menuData := sess.PendingMenu skill := sess.PendingSkill lastFlagKey := sess.PendingLastFlag background := sess.PendingBackground sess.PendingMenu = nil sess.PendingSkill = "" sess.PendingLastFlag = "" sess.PendingBackground = false sess.State = net.StateGame input = strings.TrimSpace(input) var items []*item.ItemDef for _, entry := range menuData { iid := entry["item_id"] if item := g.loadCraftItem(iid); item != nil { items = append(items, item) } } if len(items) == 0 { g.reprompt(sess) return } sort.Slice(items, func(i, j int) bool { return items[i].FirstCraft().Level < items[j].FirstCraft().Level }) lastItemID, _ := p.Flags[lastFlagKey].(string) sel, errMsg := g.resolveCraftByInput(input, items, lastItemID) switch errMsg { case "ambiguous": sess.WriteLine("That's ambiguous.") g.reprompt(sess) return case "never_mind": sess.WriteLine("Never mind.") g.reprompt(sess) return } craft := sel.Item.FirstCraft() skillLevel := p.Level(player.SkillName(skill)) if skillLevel < craft.Level { sess.WriteLine(fmt.Sprintf("You need level %d %s to make that.", craft.Level, skill)) g.reprompt(sess) return } if !craftHasAllItemsQty(craft, p.CountItem) { sess.WriteLine("You don't have the materials for that.") g.reprompt(sess) return } p.EnsureFlags() g.setPlayerFlag(p, lastFlagKey, sel.Item.ID) g.AccountStore.SaveCharacter(p) if background { g.startFletchAction(sess, p, sel.Item, sel.Count) } else { g.startProductionFromItem(sess, p, sel.Item, sel.Count) } } func itemMenuData(itemIDs []string) []map[string]string { out := make([]map[string]string, len(itemIDs)) for i, id := range itemIDs { out[i] = map[string]string{"item_id": id} } return out } func itemIDs(defs []*item.ItemDef) []string { ids := make([]string, len(defs)) for i, d := range defs { ids[i] = d.ID } return ids } func (g *Game) promptHowMany(sess *net.Session, itemID string) { sess.PendingItemID = itemID sess.State = net.StateHowMany sess.Write("How many (return for all)?: ") } type craftSelection struct { Item *item.ItemDef Count int } func (g *Game) resolveCraftByInput(input string, items []*item.ItemDef, lastItemID string) (sel craftSelection, errMsg string) { if input == "" { if lastItemID == "" { return sel, "never_mind" } for i := range items { if items[i].ID == lastItemID { sel.Item = items[i] return sel, "" } } return sel, "never_mind" } qty, productName := parseQty(input) productName = strings.TrimSpace(productName) if idx, err := strconv.Atoi(productName); err == nil && idx > 0 && idx <= len(items) { sel.Item = items[idx-1] sel.Count = qty return sel, "" } matchCount := 0 for i := range items { outDef, _ := g.ItemStore.Load(items[i].ID) name := items[i].ID if outDef != nil { name = outDef.Name } if behavior.WordPrefixMatch(productName, name) { if matchCount == 0 { sel.Item = items[i] sel.Count = qty } matchCount++ } } if matchCount > 1 { return sel, "ambiguous" } if sel.Item == nil { return sel, "never_mind" } return sel, "" } func (g *Game) showMenuTable(sess *net.Session, title string, names []string) { p := sess.Player tbl := &ui.Table{Title: title} for i, name := range names { tbl.Rows = append(tbl.Rows, []string{fmt.Sprintf("%d", i+1), name}) } unicode := p.OptionBool("unicode") sess.WriteLine("") for _, line := range tbl.Render(unicode, p.OptionInt("wrap_width")) { sess.WriteLine(line) } } func (g *Game) itemName(sess *net.Session, item *item.ItemDef) string { if item != nil { if def, err := g.ItemStore.Load(item.ID); err == nil { return g.itemColorize(sess, def, def.Name) } return item.ID } return "" } func (g *Game) showRecipeChoice(sess *net.Session, p *player.Player, items []*item.ItemDef, emptyMsg, title string, autoOption string) { if len(items) == 0 { sess.WriteLine(emptyMsg) return } if len(items) == 1 { if autoOption != "" && p.OptionBool(autoOption) { g.startProductionFromItem(sess, p, items[0], 0) return } g.promptHowMany(sess, items[0].ID) return } sess.State = net.StateRecipeChoice sess.PendingMenu = itemMenuData(itemIDs(items)) var names []string for _, item := range items { names = append(names, g.itemName(sess, item)) } g.showMenuTable(sess, title, names) } func (g *Game) handleHowMany(sess *net.Session, input string) { p := sess.Player itemID := sess.PendingItemID sess.PendingItemID = "" sess.State = net.StateGame input = strings.TrimSpace(input) var count int if input == "" { count = 0 } else if n, err := strconv.Atoi(input); err == nil && n > 0 { count = n } else { sess.WriteLine("Never mind.") g.reprompt(sess) return } item := g.loadCraftItem(itemID) if item == nil { g.reprompt(sess) return } g.startProductionFromItem(sess, p, item, count) }