aboutsummaryrefslogtreecommitdiff
path: root/internal/game/action_production.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-18 20:26:03 -0400
committerhistoria <[not public]>2026-06-18 20:26:03 -0400
commit97a1a908b9e6e6d3516628c139c9b64262b4fe08 (patch)
treed82953974edbfb4051dff1a48f1b264b9c264d28 /internal/game/action_production.go
parent17e7725f252c7f5d3be2e9c37d62751c631f196f (diff)
downloadthehouseoficarus-97a1a908b9e6e6d3516628c139c9b64262b4fe08.tar.gz
feat: crafting roughly implemented (leather, gems, gold, clay, spinning).
Diffstat (limited to 'internal/game/action_production.go')
-rw-r--r--internal/game/action_production.go299
1 files changed, 261 insertions, 38 deletions
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
+}