package game import ( "fmt" "math/rand" "sort" "strconv" "strings" "thehouseoficarus/internal/action" "thehouseoficarus/internal/color" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) func (g *Game) resolveCraftVar(sess *net.Session, varSpec string, craft *object.CraftDef, outputID string, byproducts []string, hasItem func(string) bool) string { if !strings.Contains(varSpec, "%") { return varSpec } outDef, _ := g.ItemStore.Load(outputID) outputName := outputID if outDef != nil { outputName = g.itemColorize(sess, outDef, outDef.Name) } varSpec = strings.ReplaceAll(varSpec, "%n", outputName) for i, e := range craft.Consume { key := fmt.Sprintf("%%i%d", i+1) if !strings.Contains(varSpec, key) { continue } var matchedID string for _, id := range e.Items { if hasItem(id) { matchedID = id break } } if matchedID == "" && len(e.Items) > 0 { matchedID = e.Items[0] } if def, err := g.ItemStore.Load(matchedID); err == nil { varSpec = strings.ReplaceAll(varSpec, key, g.itemColorize(sess, def, def.Name)) } else { varSpec = strings.ReplaceAll(varSpec, key, matchedID) } } for i, bpID := range byproducts { key := fmt.Sprintf("%%b%d", i+1) if !strings.Contains(varSpec, key) { continue } if def, err := g.ItemStore.Load(bpID); err == nil { varSpec = strings.ReplaceAll(varSpec, key, g.itemColorize(sess, def, def.Name)) } else { varSpec = strings.ReplaceAll(varSpec, key, bpID) } } return color.ExpandTags(g.colorMode(sess), varSpec) } func (g *Game) resolveCraftMessage(sess *net.Session, itemMsg, defaultMsg string, craft *object.CraftDef, outputID string, byproducts []string, hasItem func(string) bool) string { msg := itemMsg if msg == "" { msg = defaultMsg } if msg == "" { return "" } return g.resolveCraftVar(sess, msg, craft, outputID, byproducts, hasItem) } func (g *Game) startProduction(sess *net.Session, p *player.Player, item *object.ItemDef, craft *object.CraftDef, actionType action.ActionType, displayVerb, startMsg, endMsg string, count int) { g.cancelAction(p) g.cancelBgAction(p) skill := craft.EffectiveSkill() if skill != "" { skillLevel := p.Level(player.SkillName(skill)) if skillLevel < craft.Level { sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", craft.Level, skill)) g.reprompt(sess) return } } if !craftHasAllItemsQty(craft, p.CountItem) { sess.WriteLine("You don't have the required materials.") g.reprompt(sess) return } outDef, _ := g.ItemStore.Load(item.ID) wait := craft.Wait if wait <= 0 { wait = 4 } outputName := item.ID if outDef != nil { outputName = outDef.Name } p.Action = &action.Action{ Type: actionType, TargetID: item.ID, TargetName: outputName, Data: &action.ProductionData{ ItemID: item.ID, Phase: 0, Wait: wait, StartMsg: startMsg, EndMsg: endMsg, Remaining: count, }, WaitLeft: engine.ToTicks(1), } g.broadcastAction(sess, "%s starts %s.", p.Name, displayVerb) } func (g *Game) startProductionFromItem(sess *net.Session, p *player.Player, item *object.ItemDef, count int) { craft := item.FirstCraft() if craft == nil { return } _, stationName := g.findStation(p.RoomID, craft.Station) if stationName == "" { stationName = "inventory" } info, ok := productionTypes[craft.Type] if !ok { info = productionTypeInfo{ ActionType: action.ActionType(craft.Type), DisplayVerb: craft.Type, StartMessage: "You start " + craft.Type + " %i1.", SuccessMessage: "You produce %n.", EndMessage: "You've finished " + craft.Type + ".", } } startMsg := g.resolveCraftMessage(sess, craft.StartMessage, info.StartMessage, craft, item.ID, nil, p.HasItem) if stationName != "inventory" { startMsg += " on the " + stationName + "." } endMsg := g.resolveCraftMessage(sess, craft.EndMessage, info.EndMessage, craft, item.ID, nil, p.HasItem) g.startProduction(sess, p, item, craft, info.ActionType, info.DisplayVerb, startMsg, endMsg, count) } func (g *Game) loadCraftItem(itemID string) *object.ItemDef { item, err := g.ItemStore.Load(itemID) if err != nil || len(item.Craft) == 0 { return nil } return item } func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { d, ok := p.Action.Data.(*action.ProductionData) if !ok { g.cancelAction(p) return false } itemID := d.ItemID phase := d.Phase wait := d.Wait startMsg := d.StartMsg endMsg := d.EndMsg remaining := d.Remaining item := g.loadCraftItem(itemID) if item == nil { g.cancelAction(p) return false } craft := item.FirstCraft() if phase == 0 { if startMsg != "" { sess.WriteLine(startMsg) } if len(craft.Steps) > 0 { d.NextStepIndex = 0 d.StepsAccumulatedTicks = 0 } d.Phase = 1 p.Action.WaitLeft = engine.ToTicks(wait) return true } if stepsIdx := d.NextStepIndex; stepsIdx < len(craft.Steps) { accTicks := d.StepsAccumulatedTicks accTicks += wait d.StepsAccumulatedTicks = accTicks for i := stepsIdx; i < len(craft.Steps); i++ { if accTicks >= craft.Steps[i].Tick { sess.WriteLine(g.resolveCraftVar(sess, craft.Steps[i].Message, craft, item.ID, nil, p.HasItem)) d.NextStepIndex = i + 1 } } } skill := craft.EffectiveSkill() skillLevel := p.Level(player.SkillName(skill)) chance := 1.0 if craft.Success != nil { chance = action.SuccessChance(action.SuccessFormula(*craft.Success), skillLevel, craft.Level) } info, _ := productionTypes[craft.Type] if rand.Float64() < chance { outputQty := craft.OutputQty if outputQty <= 0 { outputQty = 1 } byproducts := g.collectByproducts(p, item) placed := false outDef, _ := g.ItemStore.Load(item.ID) if outDef != nil && outDef.Stackable { for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot != nil && slot.ItemID == item.ID { craftConsumeAll(craft, p.HasItem, p.RemoveItem) slot.Quantity += outputQty placed = true break } } } if !placed { craftConsumeAll(craft, p.HasItem, p.RemoveItem) freeSlot := p.FirstFreeSlot() if freeSlot == -1 { sess.WriteLine("Your inventory is too full!") g.cancelAction(p) return false } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: item.ID, Quantity: outputQty}) } for _, bp := range byproducts { if slot := p.FirstFreeSlot(); slot >= 0 { p.SetInvSlot(slot, &player.InventorySlot{ItemID: bp, Quantity: 1}) } } if craft.XP > 0 { if newLevel := p.AddSkillXP(player.SkillName(skill), craft.XP); newLevel > 0 { sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, skill))) } } g.AccountStore.SaveCharacter(p) var defaultSuccess string if info.SuccessMessage != "" { defaultSuccess = info.SuccessMessage } else { defaultSuccess = "You produce %n." } msg := g.resolveCraftMessage(sess, craft.Message, defaultSuccess, craft, item.ID, byproducts, p.HasItem) if p.OptionBool("xp_drops") && craft.XP > 0 { msg += g.formatXpDropSingle(sess, p, player.SkillName(skill), craft.XP) } sess.WriteLine(msg) } else { craftConsumeAll(craft, p.HasItem, p.RemoveItem) if craft.Fail != "" { freeSlot := p.FirstFreeSlot() if freeSlot >= 0 { p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: craft.Fail, Quantity: 1}) } } g.AccountStore.SaveCharacter(p) msg := g.resolveCraftMessage(sess, craft.FailMessage, info.FailMessage, craft, item.ID, nil, p.HasItem) if msg != "" { sess.WriteLine(g.colorize(sess, "damage_taken", msg)) } } if remaining > 0 { remaining-- d.Remaining = remaining if remaining <= 0 { if endMsg != "" { sess.WriteLine(endMsg) } g.cancelAction(p) return false } } if g.canContinueProduction(p, item) { p.Action.WaitLeft = engine.ToTicks(wait) return true } if endMsg != "" { sess.WriteLine(endMsg) } g.cancelAction(p) return false } func (g *Game) collectByproducts(p *player.Player, item *object.ItemDef) []string { craft := item.FirstCraft() if craft == nil { return nil } var byproducts []string for _, e := range craft.Consume { if len(e.Byproducts) == 0 { continue } for i, id := range e.Items { if p.HasItem(id) && i < len(e.Byproducts) && e.Byproducts[i] != "" { byproducts = append(byproducts, e.Byproducts[i]) break } } } return byproducts } func (g *Game) canContinueProduction(p *player.Player, item *object.ItemDef) bool { craft := item.FirstCraft() if craft == nil { return false } if !craftHasAllItemsQty(craft, p.CountItem) { return false } outputQty := craft.OutputQty if outputQty <= 0 { outputQty = 1 } outDef, _ := g.ItemStore.Load(item.ID) if outDef != nil && outDef.Stackable { for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot != nil && slot.ItemID == item.ID { return true } } } return p.FirstFreeSlot() >= 0 } 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 || object.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 *object.ItemDef) string { craft := item.FirstCraft() if craft == nil { return "" } var parts []string for _, e := range craft.Consume { 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 []*object.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 := color.Parse("240") tbl := &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) { 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 []*object.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() p.Flags[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 (g *Game) findTool(p *player.Player, toolTypes []string) (toolSpeed float64, toolName string, found bool) { toolSpeed = -1 if itemID, ok := p.Equipment[object.SlotMainHand]; ok { if def, err := g.ItemStore.Load(itemID); err == nil { for _, t := range toolTypes { if def.ToolType == t { return def.ToolSpeed, def.Name, true } } } } for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot == nil { continue } def, err := g.ItemStore.Load(slot.ItemID) if err != nil { continue } for _, t := range toolTypes { if def.ToolType == t { return def.ToolSpeed, def.Name, true } } } return -1, "", false } func (g *Game) hasToolType(p *player.Player, toolType string) bool { _, _, found := g.findTool(p, []string{toolType}) return found } type productionTypeInfo struct { ActionType action.ActionType DisplayVerb string StartMessage string SuccessMessage string EndMessage string FailMessage string } var productionTypes = map[string]productionTypeInfo{ "cooking": { ActionType: action.TypeCook, DisplayVerb: "cooking", StartMessage: "You start cooking %i1.", SuccessMessage: "Cooked to perfection. %n looks great!", EndMessage: "You've finished cooking.", FailMessage: "You accidentally burn the %i1.", }, "smelting": { ActionType: action.TypeSmelt, DisplayVerb: "smelting", StartMessage: "You place the %i1 into the furnace.", SuccessMessage: "You remove a white hot %n!", EndMessage: "You've finished smelting.", FailMessage: "The %i1 is consumed by the flames.", }, "smithing": { ActionType: action.TypeSmith, DisplayVerb: "smithing", StartMessage: "You begin smithing %i1.", SuccessMessage: "You smith a %n.", EndMessage: "You've finished smithing.", }, "crafting": { ActionType: action.TypeCraft, DisplayVerb: "crafting", StartMessage: "You begin crafting %i1.", SuccessMessage: "You craft a %n.", EndMessage: "You've finished crafting.", }, "combine": { ActionType: action.TypeCombine, DisplayVerb: "combining", StartMessage: "You start combining %i1.", SuccessMessage: "You produce %n.", EndMessage: "You've finished combining.", }, "fletching": { ActionType: action.TypeFletch, DisplayVerb: "fletching", StartMessage: "You begin fletching %i1.", SuccessMessage: "You fletch a %n.", EndMessage: "You've finished fletching.", }, "pharmacy": { ActionType: action.TypeMix, DisplayVerb: "mixing", StartMessage: "You start mixing %i1.", SuccessMessage: "You mix a %n.", EndMessage: "You've finished mixing.", }, "construction": { ActionType: action.TypeConstruct, DisplayVerb: "constructing", StartMessage: "You begin constructing %i1.", SuccessMessage: "You construct a %n.", EndMessage: "You've finished constructing.", }, } var productionActionTypes map[string]bool func init() { productionActionTypes = make(map[string]bool) for _, pt := range productionTypes { productionActionTypes[string(pt.ActionType)] = true } } 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 []*object.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 *object.ItemDef Count int } func (g *Game) resolveCraftByInput(input string, items []*object.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 object.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 := &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) { sess.WriteLine(line) } } func (g *Game) itemName(sess *net.Session, item *object.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 []*object.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) }