From 97a1a908b9e6e6d3516628c139c9b64262b4fe08 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 18 Jun 2026 20:26:03 -0400 Subject: feat: crafting roughly implemented (leather, gems, gold, clay, spinning). --- internal/action/behavior.go | 2 + internal/action/recipe.go | 8 + internal/game/action.go | 29 ++-- internal/game/action_fletch.go | 15 +- internal/game/action_gather.go | 5 + internal/game/action_production.go | 299 ++++++++++++++++++++++++++++++++----- internal/game/action_talk.go | 6 + internal/game/cmd_attack.go | 8 +- internal/game/cmd_craft.go | 190 +++++++++++++++++++++++ internal/game/cmd_fletch.go | 175 +++------------------- internal/game/cmd_smelt.go | 4 +- internal/game/cmd_smith.go | 173 +-------------------- internal/game/cmd_use.go | 50 ++++++- internal/game/game.go | 11 +- internal/net/server.go | 5 + 15 files changed, 587 insertions(+), 393 deletions(-) create mode 100644 internal/game/cmd_craft.go (limited to 'internal') diff --git a/internal/action/behavior.go b/internal/action/behavior.go index 67baab0..ccd95bf 100644 --- a/internal/action/behavior.go +++ b/internal/action/behavior.go @@ -49,6 +49,7 @@ type NodeAction struct { TakeItem string `yaml:"take_item"` Teleport int `yaml:"teleport"` Heal int `yaml:"heal"` + Cost int `yaml:"cost"` } type Condition struct { @@ -57,6 +58,7 @@ type Condition struct { Not bool `yaml:"not"` PlayerFlag string `yaml:"player_flag"` HasItem string `yaml:"has_item"` + MinCredits int `yaml:"min_credits"` AllOf []Condition `yaml:"all_of"` AnyOf []Condition `yaml:"any_of"` } diff --git a/internal/action/recipe.go b/internal/action/recipe.go index 2e93c06..fee3434 100644 --- a/internal/action/recipe.go +++ b/internal/action/recipe.go @@ -21,6 +21,7 @@ type RecipeDef struct { Level int `yaml:"level"` XP int `yaml:"xp"` Station []string `yaml:"station"` + Tool string `yaml:"tool"` Consume []ConsumeEntry `yaml:"consume"` Output string `yaml:"output"` OutputQty int `yaml:"output_qty"` @@ -153,6 +154,13 @@ func (r *RecipeDef) DisplayName() string { return r.Output } +func (r *RecipeDef) EffectiveSkill() string { + if r.Skill != "" { + return r.Skill + } + return r.Type +} + func (s *RecipeStore) FindByNameOrID(query string, recipeType string) (*RecipeDef, error) { all, err := s.LoadAll() if err != nil { diff --git a/internal/game/action.go b/internal/game/action.go index 3d0223a..ca8506a 100644 --- a/internal/game/action.go +++ b/internal/game/action.go @@ -19,6 +19,7 @@ var verbAliases = map[string]string{ "chop": "gather", "fish": "gather", "cut": "gather", + "shear": "gather", "talk": "talk", "speak": "talk", "ask": "talk", @@ -27,10 +28,11 @@ var verbAliases = map[string]string{ } var verbSkill = map[string]string{ - "mine": "mining", - "chop": "woodcutting", - "cut": "woodcutting", - "fish": "fishing", + "mine": "mining", + "chop": "woodcutting", + "cut": "woodcutting", + "fish": "fishing", + "shear": "crafting", } func normalizeVerb(v string) string { @@ -231,14 +233,10 @@ func (g *Game) AdvanceActions() { g.advanceStoke(sess, p) case "search": g.advanceSearch(sess, p) - case "cook": - g.advanceProduction(sess, p) - case "smelt": - g.advanceProduction(sess, p) - case "smith": - g.advanceProduction(sess, p) - case "combine": - g.advanceProduction(sess, p) + default: + if productionActionTypes[p.Action.Type] { + g.advanceProduction(sess, p) + } } if p.Action == nil { g.writePrompt(sess) @@ -308,5 +306,12 @@ func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool { } return has } + if c.MinCredits > 0 { + has := p != nil && p.Credits >= c.MinCredits + if c.Not { + return !has + } + return has + } return true } diff --git a/internal/game/action_fletch.go b/internal/game/action_fletch.go index 734453b..25f2966 100644 --- a/internal/game/action_fletch.go +++ b/internal/game/action_fletch.go @@ -33,6 +33,7 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, recipe *acti batches := 0 totalOutput := 0 + skill := player.SkillName(recipe.EffectiveSkill()) for { if !g.canDoFletchRecipe(p, recipe) { break @@ -60,8 +61,8 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, recipe *acti } if recipe.XP > 0 { - if newLevel := p.AddSkillXP(player.Fletching, recipe.XP); newLevel > 0 { - sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d fletching! ***", newLevel))) + if newLevel := p.AddSkillXP(skill, recipe.XP); newLevel > 0 { + sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, recipe.EffectiveSkill()))) } } @@ -91,7 +92,7 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, recipe *acti msg := fmt.Sprintf("You fletch %d %s.", totalOutput, outputName) if p.OptionBool("xp_drops") && recipe.XP > 0 { totalXP := recipe.XP * batches - abbr := player.SkillAbbr[player.Fletching] + abbr := player.SkillAbbr[skill] msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", totalXP, abbr)) } sess.WriteLine(msg) @@ -151,6 +152,8 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { outputQty = 1 } + skill := player.SkillName(recipe.EffectiveSkill()) + placed := false if outDef != nil && outDef.Stackable { for i := 0; i < 28; i++ { @@ -175,8 +178,8 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { } if recipe.XP > 0 { - if newLevel := p.AddSkillXP(player.Fletching, recipe.XP); newLevel > 0 { - sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d fletching! ***", newLevel))) + if newLevel := p.AddSkillXP(skill, recipe.XP); newLevel > 0 { + sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, recipe.EffectiveSkill()))) } } g.AccountStore.SaveCharacter(p) @@ -190,7 +193,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { msg = fmt.Sprintf("You fletch %s.", outputName) } if p.OptionBool("xp_drops") && recipe.XP > 0 { - abbr := player.SkillAbbr[player.Fletching] + abbr := player.SkillAbbr[skill] msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr)) } sess.WriteLine(msg) diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go index ffd30e1..8b45129 100644 --- a/internal/game/action_gather.go +++ b/internal/game/action_gather.go @@ -180,6 +180,11 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { _, shared := p.Action.Data["deplete_timer"] if step == 0 { + if p.FirstFreeSlot() == -1 { + sess.WriteLine(g.colorize(sess, "error", "Your inventory is too full!")) + g.CancelAction(p) + return + } if cfg.Bait != "" { if !p.HasItem(cfg.Bait) { baitName := cfg.Bait diff --git a/internal/game/action_production.go b/internal/game/action_production.go index d4e2274..df23198 100644 --- a/internal/game/action_production.go +++ b/internal/game/action_production.go @@ -3,10 +3,12 @@ package game import ( "fmt" "math/rand" + "sort" "strconv" "strings" "thehouseoficarus/internal/action" + "thehouseoficarus/internal/color" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" @@ -19,6 +21,29 @@ type recipeEntry struct { Recipe action.RecipeDef } +type productionTypeInfo struct { + ActionType string + DisplayVerb string +} + +var productionTypes = map[string]productionTypeInfo{ + "cooking": {"cook", "cooking"}, + "smelting": {"smelt", "smelting"}, + "smithing": {"smith", "smithing"}, + "crafting": {"craft", "crafting"}, + "combine": {"combine", "combining"}, + "fletching": {"fletch", "fletching"}, +} + +var productionActionTypes map[string]bool + +func init() { + productionActionTypes = make(map[string]bool) + for _, pt := range productionTypes { + productionActionTypes[pt.ActionType] = true + } +} + func recipeMenuData(recipes []action.RecipeDef) []map[string]string { out := make([]map[string]string, len(recipes)) for i, r := range recipes { @@ -183,10 +208,11 @@ func (g *Game) startProduction(sess *net.Session, p *player.Player, recipe *acti g.CancelAction(p) g.CancelBackgroundAction(p) - if recipe.Skill != "" { - skillLevel := p.Level(player.SkillName(recipe.Skill)) + skill := recipe.EffectiveSkill() + if skill != "" { + skillLevel := p.Level(player.SkillName(skill)) if skillLevel < recipe.Level { - sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", recipe.Level, recipe.Skill)) + sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", recipe.Level, skill)) g.reprompt(sess) return } @@ -242,38 +268,20 @@ func (g *Game) startProductionFromRecipe(sess *net.Session, p *player.Player, re return def.Name, true }) - var actionType, displayVerb, startMsg, endMsg string - switch recipe.Type { - case "cooking": - actionType = "cook" - displayVerb = "cooking" - if stationName != "inventory" { - startMsg = fmt.Sprintf("You start cooking %s on the %s.", firstItem, stationName) - } else { - startMsg = fmt.Sprintf("You start making %s.", firstItem) - } - endMsg = "You've cooked everything you can." - case "smelting": - actionType = "smelt" - displayVerb = "smelting" - startMsg = fmt.Sprintf("You begin to process %s in the furnace.", firstItem) - endMsg = "You've processed all the ore in your inventory." - case "smithing": - actionType = "smith" - displayVerb = "smithing" - startMsg = fmt.Sprintf("You hammer the %s on the anvil...", firstItem) - endMsg = "You've used all the bars you can." - case "combine": - actionType = "combine" - displayVerb = "combining" - startMsg = fmt.Sprintf("You start combining %s.", firstItem) - endMsg = "You've run out of materials." - default: - actionType = "craft" - displayVerb = "crafting" - startMsg = fmt.Sprintf("You start crafting with %s.", firstItem) - endMsg = "You've run out of materials." + info, ok := productionTypes[recipe.Type] + if !ok { + info = productionTypeInfo{recipe.Type, recipe.Type} + } + actionType := info.ActionType + displayVerb := info.DisplayVerb + + var startMsg, endMsg string + if stationName != "inventory" { + startMsg = fmt.Sprintf("You start %s %s on the %s.", displayVerb, firstItem, stationName) + } else { + startMsg = fmt.Sprintf("You start %s %s.", displayVerb, firstItem) } + endMsg = fmt.Sprintf("You've finished %s.", displayVerb) g.startProduction(sess, p, recipe, actionType, displayVerb, startMsg, endMsg, count) } @@ -319,7 +327,8 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { return true } - skillLevel := p.Level(player.SkillName(recipe.Skill)) + skill := recipe.EffectiveSkill() + skillLevel := p.Level(player.SkillName(skill)) chance := 1.0 if recipe.Success != nil { chance = action.SuccessChance(*recipe.Success, skillLevel, recipe.Level) @@ -364,8 +373,8 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { } if recipe.XP > 0 { - if newLevel := p.AddSkillXP(player.SkillName(recipe.Skill), recipe.XP); newLevel > 0 { - sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, recipe.Skill))) + if newLevel := p.AddSkillXP(player.SkillName(skill), recipe.XP); newLevel > 0 { + sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, skill))) } } g.AccountStore.SaveCharacter(p) @@ -379,7 +388,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { msg = fmt.Sprintf("You produce %s.", outputName) } if p.OptionBool("xp_drops") && recipe.XP > 0 { - abbr := player.SkillAbbr[player.SkillName(recipe.Skill)] + abbr := player.SkillAbbr[player.SkillName(skill)] msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr)) } sess.WriteLine(msg) @@ -531,6 +540,17 @@ func (g *Game) menuEntryName(entry map[string]string) string { } } } + if rid, ok := entry["fletch_recipe_id"]; ok { + all, _ := g.RecipeStore.LoadAll() + for _, r := range all { + if r.ID == rid { + if def, err := g.ItemStore.Load(r.Output); err == nil { + return def.Name + } + return r.Output + } + } + } if barID, ok := entry["bar_id"]; ok { if def, _ := g.ItemStore.Load(barID); def != nil { return def.Name @@ -557,9 +577,212 @@ func (g *Game) dispatchMenuEntry(sess *net.Session, entry map[string]string) { g.showSmithTable(sess, p, barID, allRecipes) return } + if rid, ok := entry["fletch_recipe_id"]; ok { + p := sess.Player.(*player.Player) + allRecipes, _ := g.RecipeStore.LoadAll() + for _, r := range allRecipes { + if r.ID == rid { + g.startFletchAction(sess, p, &r, 0) + return + } + } + g.reprompt(sess) + return + } if rid, ok := entry["recipe_id"]; ok { g.promptHowMany(sess, rid) return } g.reprompt(sess) } + +func (g *Game) formatRecipeMaterials(r *action.RecipeDef) string { + var parts []string + for _, e := range r.Consume { + itemName := e.Items[0] + if def, err := g.ItemStore.Load(e.Items[0]); err == nil { + itemName = def.Name + } + if e.Qty > 1 { + parts = append(parts, fmt.Sprintf("%s x%d", itemName, e.Qty)) + } else { + parts = append(parts, itemName) + } + } + return strings.Join(parts, ", ") +} + +func (g *Game) showProductionTable(sess *net.Session, p *player.Player, recipes []action.RecipeDef, title, skill, lastFlagKey, promptVerb string, background bool) { + sort.Slice(recipes, func(i, j int) bool { + return recipes[i].Level < recipes[j].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, r := range recipes { + outDef, _ := g.ItemStore.Load(r.Output) + productName := r.Output + if outDef != nil { + productName = outDef.Name + } + + outputQty := r.OutputQty + if outputQty <= 0 { + outputQty = 1 + } + if outDef != nil && outDef.Stackable && outputQty > 1 { + productName = fmt.Sprintf("%s x%d", productName, outputQty) + } + + matStr := g.formatRecipeMaterials(&r) + levelStr := fmt.Sprint(r.Level) + numStr := fmt.Sprintf("%d", i+1) + + canMake := skillLevel >= r.Level && r.HasAllItemsQty(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) + } + + lastRecipeID, _ := p.Flags[lastFlagKey].(string) + hint := "" + if lastRecipeID != "" { + for _, r := range recipes { + if r.ID == lastRecipeID { + if outDef, err := g.ItemStore.Load(r.Output); 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 = recipeMenuData(recipes) + 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.(*player.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) + + allRecipes, err := g.RecipeStore.LoadAll() + if err != nil { + g.reprompt(sess) + return + } + + var recipes []action.RecipeDef + for _, entry := range menuData { + rid := entry["recipe_id"] + for _, r := range allRecipes { + if r.ID == rid { + recipes = append(recipes, r) + break + } + } + } + + if len(recipes) == 0 { + g.reprompt(sess) + return + } + + sort.Slice(recipes, func(i, j int) bool { + return recipes[i].Level < recipes[j].Level + }) + + lastRecipeID, _ := p.Flags[lastFlagKey].(string) + + sel, errMsg := g.resolveRecipeByInput(input, recipes, lastRecipeID) + switch errMsg { + case "ambiguous": + sess.WriteLine("That's ambiguous.") + g.reprompt(sess) + return + case "never_mind": + sess.WriteLine("Never mind.") + g.reprompt(sess) + return + } + + skillLevel := p.Level(player.SkillName(skill)) + if skillLevel < sel.Recipe.Level { + sess.WriteLine(fmt.Sprintf("You need level %d %s to make that.", sel.Recipe.Level, skill)) + g.reprompt(sess) + return + } + if !sel.Recipe.HasAllItemsQty(p.CountItem) { + sess.WriteLine("You don't have the materials for that.") + g.reprompt(sess) + return + } + + if p.Flags == nil { + p.Flags = make(map[string]any) + } + p.Flags[lastFlagKey] = sel.Recipe.ID + g.AccountStore.SaveCharacter(p) + + if background { + g.startFletchAction(sess, p, sel.Recipe, sel.Count) + } else { + g.startProductionFromRecipe(sess, p, sel.Recipe, sel.Count) + } +} + +func (g *Game) hasToolType(p *player.Player, toolType string) bool { + if itemID, ok := p.Equipment[object.SlotMainHand]; ok { + if def, err := g.ItemStore.Load(itemID); err == nil && def.ToolType == toolType { + return true + } + } + for i := 0; i < 28; i++ { + slot := p.InvSlot(i) + if slot == nil { + continue + } + if def, err := g.ItemStore.Load(slot.ItemID); err == nil && def.ToolType == toolType { + return true + } + } + return false +} diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go index 30bf206..2d4a814 100644 --- a/internal/game/action_talk.go +++ b/internal/game/action_talk.go @@ -210,4 +210,10 @@ func (g *Game) applyNodeAction(sess *net.Session, na *action.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 + g.AccountStore.SaveCharacter(p) + } + } } diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index 51ac54a..4c6941a 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -332,7 +332,8 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst if !mob.Unique { dropper = "The " + mob.Name } - sess.WriteLine(g.colorize(sess, "drop_message", fmt.Sprintf(" %s drops: %s", dropper, name))) + coloredName := g.itemColorize(sess, def, name) + sess.WriteLine(fmt.Sprintf(" %s %s", g.colorize(sess, "drop_message", dropper+" drops:"), coloredName)) } if len(mob.Drops.Loot) > 0 { @@ -352,10 +353,11 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst if !mob.Unique { dropper = "The " + mob.Name } + coloredName := g.itemColorize(sess, def, name) if qty > 1 { - sess.WriteLine(g.colorize(sess, "drop_message", fmt.Sprintf(" %s drops: %d x %s", dropper, qty, name))) + sess.WriteLine(fmt.Sprintf(" %s %d x %s", g.colorize(sess, "drop_message", dropper+" drops:"), qty, coloredName)) } else { - sess.WriteLine(g.colorize(sess, "drop_message", fmt.Sprintf(" %s drops: %s", dropper, name))) + sess.WriteLine(fmt.Sprintf(" %s %s", g.colorize(sess, "drop_message", dropper+" drops:"), coloredName)) } } } diff --git a/internal/game/cmd_craft.go b/internal/game/cmd_craft.go new file mode 100644 index 0000000..50a99e0 --- /dev/null +++ b/internal/game/cmd_craft.go @@ -0,0 +1,190 @@ +package game + +import ( + "strings" + + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func (g *Game) doCraft(sess *net.Session, input string) { + p := sess.Player.(*player.Player) + g.CancelAction(p) + + allRecipes, err := g.RecipeStore.LoadAll() + if err != nil { + sess.WriteLine("Error loading recipes.") + return + } + + var craftRecipes []action.RecipeDef + for _, r := range allRecipes { + if r.Type != "crafting" { + continue + } + if !g.craftRecipeVisible(p, &r) { + continue + } + craftRecipes = append(craftRecipes, r) + } + + if input != "" { + g.doCraftWithInput(sess, p, craftRecipes, input) + return + } + + lastRecipeID, _ := p.Flags["last_craft"].(string) + if lastRecipeID != "" { + for i := range craftRecipes { + if craftRecipes[i].ID == lastRecipeID { + r := &craftRecipes[i] + if g.canDoCraftRecipe(p, r) { + g.startCraftRecipe(sess, p, r, 0) + return + } + break + } + } + } + + available := g.availableCraftRecipes(p, craftRecipes) + if len(available) == 0 { + sess.WriteLine("You don't have any materials to craft.") + return + } + + if p.OptionBool("craft_all") && len(available) == 1 { + g.startCraftRecipe(sess, p, &available[0], 0) + return + } + + g.showProductionTable(sess, p, available, "Crafting", "crafting", "last_craft", "Craft", false) +} + +func (g *Game) doCraftWithInput(sess *net.Session, p *player.Player, craftRecipes []action.RecipeDef, input string) { + qty, productName := parseQty(input) + productName = strings.TrimSpace(productName) + + var matched []action.RecipeDef + for _, r := range craftRecipes { + outDef, _ := g.ItemStore.Load(r.Output) + name := r.Output + if outDef != nil { + name = outDef.Name + } + if world.WordPrefixMatch(productName, name) { + matched = append(matched, r) + } + } + + if len(matched) == 0 { + sess.WriteLine("You can't craft that.") + return + } + + var available []action.RecipeDef + for _, r := range matched { + if g.canDoCraftRecipe(p, &r) { + available = append(available, r) + } + } + + if len(available) == 0 { + sess.WriteLine("You don't have the materials or level for that.") + return + } + + if len(available) == 1 { + g.startCraftRecipe(sess, p, &available[0], qty) + return + } + + g.showProductionTable(sess, p, available, "Crafting", "crafting", "last_craft", "Craft", false) +} + +func (g *Game) craftRecipeVisible(p *player.Player, r *action.RecipeDef) bool { + if len(r.Station) > 0 { + stID, _ := g.findStation(p.RoomID, r.Station) + if stID == "" { + return false + } + } + if r.Tool != "" && !g.hasToolType(p, r.Tool) { + return false + } + return true +} + +func (g *Game) canDoCraftRecipe(p *player.Player, r *action.RecipeDef) bool { + if p.Level(player.Crafting) < r.Level { + return false + } + if !r.HasAllItemsQty(p.CountItem) { + return false + } + if len(r.Station) > 0 { + stID, _ := g.findStation(p.RoomID, r.Station) + if stID == "" { + return false + } + } + if r.Tool != "" && !g.hasToolType(p, r.Tool) { + return false + } + return true +} + +func (g *Game) availableCraftRecipes(p *player.Player, allCraft []action.RecipeDef) []action.RecipeDef { + var result []action.RecipeDef + for _, r := range allCraft { + if r.HasAllItemsQty(p.CountItem) { + result = append(result, r) + } + } + return result +} + +func (g *Game) startCraftRecipe(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) { + if p.Flags == nil { + p.Flags = make(map[string]any) + } + p.Flags["last_craft"] = recipe.ID + g.AccountStore.SaveCharacter(p) + + g.startProductionFromRecipe(sess, p, recipe, count) +} + +func (g *Game) findCraftRecipesForItems(p *player.Player, itemAID, itemBID string) []action.RecipeDef { + allRecipes, err := g.RecipeStore.LoadAll() + if err != nil { + return nil + } + + toolTypeA := "" + toolTypeB := "" + if defA, _ := g.ItemStore.Load(itemAID); defA != nil { + toolTypeA = defA.ToolType + } + if defB, _ := g.ItemStore.Load(itemBID); defB != nil { + toolTypeB = defB.ToolType + } + + var matched []action.RecipeDef + for _, r := range allRecipes { + if r.Type != "crafting" { + continue + } + if r.Tool == "" { + continue + } + + if r.Tool == toolTypeA && r.MatchesEntry(itemBID) { + matched = append(matched, r) + } else if r.Tool == toolTypeB && r.MatchesEntry(itemAID) { + matched = append(matched, r) + } + } + return matched +} diff --git a/internal/game/cmd_fletch.go b/internal/game/cmd_fletch.go index fe8c0b3..60acb92 100644 --- a/internal/game/cmd_fletch.go +++ b/internal/game/cmd_fletch.go @@ -1,12 +1,9 @@ package game import ( - "fmt" - "sort" "strings" "thehouseoficarus/internal/action" - "thehouseoficarus/internal/color" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" @@ -24,6 +21,9 @@ var fletchGemItems = map[string]bool{ } func fletchNeedsKnife(r *action.RecipeDef) bool { + if r.Tool == "knife" { + return true + } for _, e := range r.Consume { for _, id := range e.Items { if fletchLogItems[id] { @@ -37,6 +37,9 @@ func fletchNeedsKnife(r *action.RecipeDef) bool { } func fletchNeedsChisel(r *action.RecipeDef) bool { + if r.Tool == "chisel" { + return true + } for _, e := range r.Consume { for _, id := range e.Items { if fletchGemItems[id] { @@ -104,7 +107,7 @@ func (g *Game) doFletch(sess *net.Session, input string) { return } - g.showFletchTable(sess, p, available) + g.showProductionTable(sess, p, available, "Fletching", "fletching", "last_fletch", "Fletch", true) } func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, fletchRecipes []action.RecipeDef, input string) { @@ -146,10 +149,13 @@ func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, fletchReci return } - g.showFletchTable(sess, p, available) + g.showProductionTable(sess, p, available, "Fletching", "fletching", "last_fletch", "Fletch", true) } func (g *Game) hasFletchTools(p *player.Player, r *action.RecipeDef) bool { + if r.Tool != "" { + return g.hasToolType(p, r.Tool) + } if fletchNeedsKnife(r) && !g.hasToolType(p, "knife") { return false } @@ -175,154 +181,6 @@ func (g *Game) availableFletchRecipes(p *player.Player, allFletch []action.Recip return result } -func (g *Game) showFletchTable(sess *net.Session, p *player.Player, available []action.RecipeDef) { - sort.Slice(available, func(i, j int) bool { - return available[i].Level < available[j].Level - }) - - mode := g.colorMode(sess) - skillLevel := p.Level(player.Fletching) - dimSpec := color.Parse("240") - - tbl := &Table{ - Title: "Fletching Actions", - Columns: []string{"#", "Product", "Materials", "Level"}, - } - - for i, r := range available { - outDef, _ := g.ItemStore.Load(r.Output) - productName := r.Output - if outDef != nil { - productName = outDef.Name - } - - outputQty := r.OutputQty - if outputQty <= 0 { - outputQty = 1 - } - if outDef != nil && outDef.Stackable && outputQty > 1 { - productName = fmt.Sprintf("%s x%d", productName, outputQty) - } - - var matParts []string - for _, e := range r.Consume { - itemName := e.Items[0] - if def, err := g.ItemStore.Load(e.Items[0]); err == nil { - itemName = def.Name - } - if e.Qty > 1 { - matParts = append(matParts, fmt.Sprintf("%s x%d", itemName, e.Qty)) - } else { - matParts = append(matParts, itemName) - } - } - matStr := strings.Join(matParts, ", ") - levelStr := fmt.Sprint(r.Level) - numStr := fmt.Sprintf("%d", i+1) - - canMake := skillLevel >= r.Level - 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) - } - - lastRecipeID, _ := p.Flags["last_fletch"].(string) - hint := "" - if lastRecipeID != "" { - for _, r := range available { - if r.ID == lastRecipeID { - if outDef, err := g.ItemStore.Load(r.Output); err == nil { - hint = outDef.Name - } - break - } - } - } - - if hint != "" { - sess.Write(fmt.Sprintf("Fletch what (enter for all %s): ", hint)) - } else { - sess.Write("Fletch what: ") - } - - sess.PendingMenu = recipeMenuData(available) - sess.State = net.StateFletchProduct -} - -func (g *Game) handleFletchProduct(sess *net.Session, input string) { - p := sess.Player.(*player.Player) - menuData := sess.PendingMenu - sess.PendingMenu = nil - sess.State = net.StateGame - - input = strings.TrimSpace(input) - - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - g.reprompt(sess) - return - } - - var recipes []action.RecipeDef - for _, entry := range menuData { - rid := entry["recipe_id"] - for _, r := range allRecipes { - if r.ID == rid { - recipes = append(recipes, r) - break - } - } - } - - if len(recipes) == 0 { - g.reprompt(sess) - return - } - - sort.Slice(recipes, func(i, j int) bool { - return recipes[i].Level < recipes[j].Level - }) - - lastRecipeID, _ := p.Flags["last_fletch"].(string) - - sel, errMsg := g.resolveRecipeByInput(input, recipes, lastRecipeID) - switch errMsg { - case "ambiguous": - sess.WriteLine("That's ambiguous.") - g.reprompt(sess) - return - case "never_mind": - sess.WriteLine("Never mind.") - g.reprompt(sess) - return - } - - if !g.canDoFletchRecipe(p, sel.Recipe) { - if p.Level(player.Fletching) < sel.Recipe.Level { - sess.WriteLine(fmt.Sprintf("You need level %d fletching to make that.", sel.Recipe.Level)) - } else { - sess.WriteLine("You don't have the materials for that.") - } - g.reprompt(sess) - return - } - - g.startFletchAction(sess, p, sel.Recipe, sel.Count) -} - func (g *Game) findFletchRecipesForItems(p *player.Player, itemAID, itemBID string) []action.RecipeDef { allRecipes, err := g.RecipeStore.LoadAll() if err != nil { @@ -346,6 +204,17 @@ func (g *Game) findFletchRecipesForItems(p *player.Player, itemAID, itemBID stri continue } + if r.Tool != "" { + if r.Tool == toolTypeA && r.MatchesEntry(itemBID) { + matched = append(matched, r) + continue + } + if r.Tool == toolTypeB && r.MatchesEntry(itemAID) { + matched = append(matched, r) + continue + } + } + aMatch := false bMatch := false for _, e := range r.Consume { diff --git a/internal/game/cmd_smelt.go b/internal/game/cmd_smelt.go index a5bbc06..46f2607 100644 --- a/internal/game/cmd_smelt.go +++ b/internal/game/cmd_smelt.go @@ -48,7 +48,7 @@ func (g *Game) doSmelt(sess *net.Session, input string) { if r.Type != "smelting" || !stationMatch(stationDefID, r.Station) || !r.MatchesEntry(itemID) { continue } - if !r.HasAllItemsQty(p.CountItem) || p.Level(player.SkillName(r.Skill)) < r.Level { + if !r.HasAllItemsQty(p.CountItem) || p.Level(player.SkillName(r.EffectiveSkill())) < r.Level { continue } recipes = append(recipes, r) @@ -81,7 +81,7 @@ func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, allRecipes []a if r.Type != "smelting" || !stationMatch(stationDefID, r.Station) { continue } - if seen[r.ID] || !r.HasAllItemsQty(p.CountItem) || p.Level(player.SkillName(r.Skill)) < r.Level { + if seen[r.ID] || !r.HasAllItemsQty(p.CountItem) || p.Level(player.SkillName(r.EffectiveSkill())) < r.Level { continue } seen[r.ID] = true diff --git a/internal/game/cmd_smith.go b/internal/game/cmd_smith.go index f2787a5..b539640 100644 --- a/internal/game/cmd_smith.go +++ b/internal/game/cmd_smith.go @@ -6,9 +6,7 @@ import ( "strings" "thehouseoficarus/internal/action" - "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -103,162 +101,8 @@ func (g *Game) showSmithTable(sess *net.Session, p *player.Player, barID string, return } - sort.Slice(recipes, func(i, j int) bool { - return recipes[i].Level < recipes[j].Level - }) - - mode := g.colorMode(sess) - skillLevel := p.Level(player.Smithing) - barCount := p.CountItem(barID) - dimSpec := color.Parse("240") - - tbl := &Table{ - Title: titleCase(barName) + " Smithing", - Columns: []string{"#", "Product", "Inputs", "Level Req."}, - } - - for i, r := range recipes { - outDef, _ := g.ItemStore.Load(r.Output) - productName := r.Output - if outDef != nil { - productName = outDef.Name - } - - outputQty := r.OutputQty - if outputQty <= 0 { - outputQty = 1 - } - if outDef != nil && outDef.Stackable && outputQty > 1 { - productName = fmt.Sprintf("%s x%d", productName, outputQty) - } - - bars := barsRequired(r, barID) - inputStr := "1 bar" - if bars > 1 { - inputStr = fmt.Sprintf("%d bars", bars) - } - levelStr := fmt.Sprint(r.Level) - numStr := fmt.Sprintf("%d", i+1) - - canMake := barCount >= bars && skillLevel >= r.Level - if canMake { - productName = g.itemColorize(sess, outDef, productName) - } else { - productName = color.Render(mode, dimSpec, productName) - inputStr = color.Render(mode, dimSpec, inputStr) - levelStr = color.Render(mode, dimSpec, levelStr) - numStr = color.Render(mode, dimSpec, numStr) - } - - tbl.Rows = append(tbl.Rows, []string{numStr, productName, inputStr, levelStr}) - } - - unicode := p.OptionBool("unicode") - sess.WriteLine("") - for _, line := range tbl.Render(unicode) { - sess.WriteLine(line) - } - lastFlag := fmt.Sprintf("last_smith_%s", barID) - lastRecipeID, _ := p.Flags[lastFlag].(string) - hint := "" - if lastRecipeID != "" { - for _, r := range recipes { - if r.ID == lastRecipeID { - if outDef, err := g.ItemStore.Load(r.Output); err == nil { - hint = outDef.Name - } - break - } - } - } - - if hint != "" { - sess.Write(fmt.Sprintf("Produce what (enter for all %s): ", hint)) - } else { - sess.Write("Produce what: ") - } - - sess.PendingRecipeID = barID - sess.State = net.StateSmithProduct -} - -func (g *Game) handleSmithProduct(sess *net.Session, input string) { - p := sess.Player.(*player.Player) - barID := sess.PendingRecipeID - sess.PendingRecipeID = "" - sess.State = net.StateGame - - input = strings.TrimSpace(input) - - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - g.reprompt(sess) - return - } - var recipes []action.RecipeDef - for _, r := range allRecipes { - if r.Type == "smithing" && r.MatchesEntry(barID) { - recipes = append(recipes, r) - } - } - - sort.Slice(recipes, func(i, j int) bool { - return recipes[i].Level < recipes[j].Level - }) - - lastFlag := fmt.Sprintf("last_smith_%s", barID) - lastRecipeID, _ := p.Flags[lastFlag].(string) - - sel, errMsg := g.resolveRecipeByInput(input, recipes, lastRecipeID) - switch errMsg { - case "ambiguous": - sess.WriteLine("That's ambiguous.") - g.reprompt(sess) - return - case "never_mind": - sess.WriteLine("Never mind.") - g.reprompt(sess) - return - } - - skillLevel := p.Level(player.Smithing) - if skillLevel < sel.Recipe.Level { - sess.WriteLine(fmt.Sprintf("You need level %d smithing to make that.", sel.Recipe.Level)) - g.reprompt(sess) - return - } - if !sel.Recipe.HasAllItemsQty(p.CountItem) { - sess.WriteLine("You don't have enough bars.") - g.reprompt(sess) - return - } - - if p.Flags == nil { - p.Flags = make(map[string]any) - } - p.Flags[lastFlag] = sel.Recipe.ID - g.AccountStore.SaveCharacter(p) - - g.startProductionFromRecipe(sess, p, sel.Recipe, sel.Count) -} - -func (g *Game) hasToolType(p *player.Player, toolType string) bool { - if itemID, ok := p.Equipment[object.SlotMainHand]; ok { - if def, err := g.ItemStore.Load(itemID); err == nil && def.ToolType == toolType { - return true - } - } - for i := 0; i < 28; i++ { - slot := p.InvSlot(i) - if slot == nil { - continue - } - if def, err := g.ItemStore.Load(slot.ItemID); err == nil && def.ToolType == toolType { - return true - } - } - return false + g.showProductionTable(sess, p, recipes, titleCase(barName)+" Smithing", "smithing", lastFlag, "Produce", false) } func hasSmithingRecipes(allRecipes []action.RecipeDef, barID string) bool { @@ -292,21 +136,6 @@ func findSmithableBars(allRecipes []action.RecipeDef, p *player.Player) []string return bars } -func barsRequired(r action.RecipeDef, barID string) int { - for _, e := range r.Consume { - for _, id := range e.Items { - if id == barID { - qty := e.Qty - if qty <= 0 { - qty = 1 - } - return qty - } - } - } - return 1 -} - func barMenuData(barIDs []string) []map[string]string { out := make([]map[string]string, len(barIDs)) for i, id := range barIDs { diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go index 8b8f43e..4d4ce88 100644 --- a/internal/game/cmd_use.go +++ b/internal/game/cmd_use.go @@ -86,7 +86,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, if !r.HasAllItemsQty(p.CountItem) { continue } - skillLevel := p.Level(player.SkillName(r.Skill)) + skillLevel := p.Level(player.SkillName(r.EffectiveSkill())) if skillLevel < r.Level { continue } @@ -171,7 +171,53 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, itemBID := matchesB[0].ID + craftRecipes := g.findCraftRecipesForItems(p, itemAID, itemBID) fletchRecipes := g.findFletchRecipesForItems(p, itemAID, itemBID) + + if len(craftRecipes) > 0 && len(fletchRecipes) > 0 { + var menu []map[string]string + var names []string + for _, r := range craftRecipes { + menu = append(menu, map[string]string{"recipe_id": r.ID}) + outDef, _ := g.ItemStore.Load(r.Output) + name := r.Output + if outDef != nil { + name = outDef.Name + } + names = append(names, fmt.Sprintf("%s (Crafting)", name)) + } + for _, r := range fletchRecipes { + menu = append(menu, map[string]string{"fletch_recipe_id": r.ID}) + outDef, _ := g.ItemStore.Load(r.Output) + name := r.Output + if outDef != nil { + name = outDef.Name + } + names = append(names, fmt.Sprintf("%s (Fletching)", name)) + } + sess.PendingMenu = menu + sess.State = net.StateRecipeChoice + g.showMenuTable(sess, "What would you like to make?", names) + return + } + + if len(craftRecipes) > 0 { + var available []action.RecipeDef + for _, r := range craftRecipes { + if g.canDoCraftRecipe(p, &r) { + available = append(available, r) + } + } + if len(available) == 1 { + g.startCraftRecipe(sess, p, &available[0], 0) + return + } + if len(available) > 1 { + g.showProductionTable(sess, p, available, "Crafting", "crafting", "last_craft", "Craft", false) + return + } + } + if len(fletchRecipes) > 0 { var available []action.RecipeDef for _, r := range fletchRecipes { @@ -184,7 +230,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, return } if len(available) > 1 { - g.showFletchTable(sess, p, available) + g.showProductionTable(sess, p, available, "Fletching", "fletching", "last_fletch", "Fletch", true) return } } diff --git a/internal/game/game.go b/internal/game/game.go index ef81fca..a1cba83 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -126,10 +126,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) { g.handleRecipeChoice(sess, input) case net.StateHowMany: g.handleHowMany(sess, input) - case net.StateSmithProduct: - g.handleSmithProduct(sess, input) - case net.StateFletchProduct: - g.handleFletchProduct(sess, input) + case net.StateSmithProduct, net.StateFletchProduct, net.StateCraftProduct, net.StateProductChoice: + g.handleProductChoice(sess, input) case net.StateColorChoice: g.handleColorChoice(sess, input) } @@ -149,7 +147,7 @@ func classifyCommand(cmd string) CommandClass { "attack", "kill", "north", "n", "south", "s", "east", "e", "west", "w", "up", "u", "down", "d", - "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith": + "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft": return ClassActive case "eat", "fletch": return ClassFree @@ -378,6 +376,9 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI case "smith": g.doSmith(sess, strings.Join(args, " ")) return + case "craft": + g.doCraft(sess, strings.Join(args, " ")) + return case "eat": g.doEat(sess, strings.Join(args, " ")) return diff --git a/internal/net/server.go b/internal/net/server.go index e74f15f..f6776fc 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -35,6 +35,8 @@ const ( StateHowMany StateSmithProduct StateFletchProduct + StateCraftProduct + StateProductChoice StateColorChoice ) @@ -47,6 +49,9 @@ type Session struct { PendingPass string PendingMenu []map[string]string PendingRecipeID string + PendingSkill string + PendingLastFlag string + PendingBackground bool Disconnecting bool DisconnectTicks int } -- cgit v1.2.3