aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-05 17:49:48 -0400
committerhistoria <[not public]>2026-10-05 17:49:48 -0400
commit7a15a5d70004b28bf2a710fa0f536af2446e5b17 (patch)
tree920705853f79bb13a65fcb2f89cee38e6f2efab8 /internal/game
parentec703a39738fefb60817f5659f76f8e683f8d301 (diff)
downloadthehouseoficarus-7a15a5d70004b28bf2a710fa0f536af2446e5b17.tar.gz
fix: production multi-step recipes, drop-weight validation, journal versioning
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/act_combine.go11
-rw-r--r--internal/game/act_fletch.go52
-rw-r--r--internal/game/cmd_fletch.go78
-rw-r--r--internal/game/cmd_room_remove.go59
-rw-r--r--internal/game/correct_audit_fixes_test.go20
-rw-r--r--internal/game/correct_combine_skill_test.go2
-rw-r--r--internal/game/hardening_craft_combine_fletch_test.go4
-rw-r--r--internal/game/production_combine_regression_test.go2
-rw-r--r--internal/game/production_engine.go58
-rw-r--r--internal/game/production_ingredients_test.go4
-rw-r--r--internal/game/production_menu.go10
11 files changed, 171 insertions, 129 deletions
diff --git a/internal/game/act_combine.go b/internal/game/act_combine.go
index 701aaec..d7aed04 100644
--- a/internal/game/act_combine.go
+++ b/internal/game/act_combine.go
@@ -8,7 +8,7 @@ import (
"thehouseoficarus/internal/player"
)
-func (g *Game) startCombine(sess *net.Session, p *player.Player, item *item.ItemDef, craft *item.CraftDef, count int) {
+func (g *Game) startCombine(sess *net.Session, p *player.Player, item *item.ItemDef, craft *item.CraftDef, count int, craftIndex int) {
g.cancelAction(p)
g.cancelBgAction(p)
@@ -33,6 +33,7 @@ func (g *Game) startCombine(sess *net.Session, p *player.Player, item *item.Item
TargetName: outputName,
Data: &behavior.CombineData{
ItemID: item.ID,
+ CraftIndex: craftIndex,
Phase: 0,
StepIndex: 0,
Remaining: count,
@@ -72,7 +73,11 @@ func (g *Game) advanceCombine(sess *net.Session, p *player.Player) {
g.cancelAction(p)
return
}
- craft := item.FirstCraft()
+ idx := d.CraftIndex
+ if idx < 0 || idx >= len(item.Craft) {
+ idx = 0
+ }
+ craft := &item.Craft[idx]
switch d.Phase {
case 0:
@@ -127,7 +132,7 @@ func (g *Game) advanceCombine(sess *net.Session, p *player.Player) {
// Byproducts must be collected BEFORE consumption: the resolver maps
// each ingredient entry to the alternative actually being consumed
// (bucket vs jug of water), which requires the items still present.
- byproducts := g.collectByproducts(p, item)
+ byproducts := g.collectByproducts(p, craft)
hasItem := craftConsumedHasItem(craft, p.CountItem, p.HasItem)
if !consumeIngredients(craft, p.CountItem, p.RemoveItem) {
diff --git a/internal/game/act_fletch.go b/internal/game/act_fletch.go
index d4adff0..ba55516 100644
--- a/internal/game/act_fletch.go
+++ b/internal/game/act_fletch.go
@@ -15,16 +15,41 @@ func (g *Game) startFletchAction(sess *net.Session, p *player.Player, def *item.
g.setPlayerFlag(p, "last_fletch", def.ID)
g.saveCharacter(p)
- if def.FirstCraft().TicksPerCycle <= 0 {
- g.doInstantFletch(sess, p, def, count)
+ idx := g.usableFletchCraftIndex(p, def)
+ if idx < 0 {
+ sess.WriteLine("You don't have the materials for that.")
+ return
+ }
+ if def.Craft[idx].TicksPerCycle <= 0 {
+ g.doInstantFletch(sess, p, def, idx, count)
return
}
- g.startTimedFletch(sess, p, def, count)
+ g.startTimedFletch(sess, p, def, idx, count)
}
-func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, def *item.ItemDef, count int) {
- c := def.FirstCraft()
+// usableFletchCraftIndex selects the first fletching recipe the player both
+// satisfies and has the tools for. Availability scans every recipe, so
+// execution must use the same one instead of always Craft[0].
+func (g *Game) usableFletchCraftIndex(p *player.Player, def *item.ItemDef) int {
+ for i := range def.Craft {
+ c := &def.Craft[i]
+ if p.Level(player.Fletching) < c.Level {
+ continue
+ }
+ if !craftHasAllItemsQty(c, p.CountItem) {
+ continue
+ }
+ if !g.hasFletchTools(p, def) {
+ continue
+ }
+ return i
+ }
+ return -1
+}
+
+func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, def *item.ItemDef, craftIdx, count int) {
+ c := &def.Craft[craftIdx]
outputQty := c.OutputQty
if outputQty <= 0 {
outputQty = 1
@@ -47,7 +72,7 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, def *item.It
break
}
- byproducts := g.collectByproducts(p, def)
+ byproducts := g.collectByproducts(p, c)
if !consumeIngredients(c, p.CountItem, p.RemoveItem) {
break
@@ -101,8 +126,8 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, def *item.It
sess.WriteLine(msg)
}
-func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, def *item.ItemDef, count int) {
- craft := def.FirstCraft()
+func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, def *item.ItemDef, craftIdx, count int) {
+ craft := &def.Craft[craftIdx]
startMsg := g.resolveCraftMessage(sess, craft.StartMessage, "You begin fletching %i1.", craft, def.ID, nil, p.HasItem)
@@ -121,6 +146,7 @@ func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, def *item.I
TargetName: def.Name,
Data: &behavior.FletchData{
ItemID: def.ID,
+ CraftIndex: craftIdx,
Phase: 0,
TicksPerCycle: craft.TicksPerCycle,
Remaining: count,
@@ -145,7 +171,11 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) {
g.cancelBgAction(p)
return
}
- c := def.FirstCraft()
+ idx := data.CraftIndex
+ if idx < 0 || idx >= len(def.Craft) {
+ idx = 0
+ }
+ c := &def.Craft[idx]
if phase == 0 {
data.Phase = 1
@@ -174,7 +204,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) {
}
skill := player.SkillName(c.EffectiveSkill())
- byproducts := g.collectByproducts(p, def)
+ byproducts := g.collectByproducts(p, c)
hasItem := craftConsumedHasItem(c, p.CountItem, p.HasItem)
if !consumeIngredients(c, p.CountItem, p.RemoveItem) {
@@ -222,7 +252,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) {
}
}
- if !g.canContinueProduction(p, def) {
+ if !g.canContinueProduction(p, def, c) {
sess.WriteLine("You've run out of fletching materials.")
g.cancelBgAction(p)
return
diff --git a/internal/game/cmd_fletch.go b/internal/game/cmd_fletch.go
index af27897..9c147c3 100644
--- a/internal/game/cmd_fletch.go
+++ b/internal/game/cmd_fletch.go
@@ -189,51 +189,57 @@ func (g *Game) findFletchItems(itemAID, itemBID string) []*item.ItemDef {
var matched []*item.ItemDef
for _, item := range items {
- c := item.FirstCraft()
-
- if c.Tool != "" {
- if c.Tool == toolTypeA && craftMatchesEntry(c, itemBID) {
- matched = append(matched, item)
- continue
- }
- if c.Tool == toolTypeB && craftMatchesEntry(c, itemAID) {
+ for i := range item.Craft {
+ if fletchCraftMatchesInputs(&item.Craft[i], item, itemAID, itemBID, toolTypeA, toolTypeB, isToolA, isToolB) {
matched = append(matched, item)
- continue
+ break
}
}
+ }
+ return matched
+}
- aMatch := false
- bMatch := false
- for _, e := range c.Ingredients {
- for _, id := range e.Items {
- if id == itemAID {
- aMatch = true
- }
- if id == itemBID {
- bMatch = true
- }
- }
+// fletchCraftMatchesInputs reports whether one fletching recipe consumes the
+// given two inputs (or a tool plus one input). findFletchItems must scan every
+// recipe, not just the first, or multi-recipe items are unreachable.
+func fletchCraftMatchesInputs(c *item.CraftDef, item *item.ItemDef, itemAID, itemBID, toolTypeA, toolTypeB string, isToolA, isToolB bool) bool {
+ if c.Tool != "" {
+ if c.Tool == toolTypeA && craftMatchesEntry(c, itemBID) {
+ return true
}
-
- if aMatch && bMatch {
- matched = append(matched, item)
- continue
+ if c.Tool == toolTypeB && craftMatchesEntry(c, itemAID) {
+ return true
}
+ }
- if isToolA && bMatch {
- if (fletchNeedsKnife(item) && toolTypeA == "knife") ||
- (fletchNeedsChisel(item) && toolTypeA == "chisel") {
- matched = append(matched, item)
- continue
+ aMatch := false
+ bMatch := false
+ for _, e := range c.Ingredients {
+ for _, id := range e.Items {
+ if id == itemAID {
+ aMatch = true
}
- }
- if isToolB && aMatch {
- if (fletchNeedsKnife(item) && toolTypeB == "knife") ||
- (fletchNeedsChisel(item) && toolTypeB == "chisel") {
- matched = append(matched, item)
- continue
+ if id == itemBID {
+ bMatch = true
}
}
}
- return matched
+
+ if aMatch && bMatch {
+ return true
+ }
+
+ if isToolA && bMatch {
+ if (fletchNeedsKnife(item) && toolTypeA == "knife") ||
+ (fletchNeedsChisel(item) && toolTypeA == "chisel") {
+ return true
+ }
+ }
+ if isToolB && aMatch {
+ if (fletchNeedsKnife(item) && toolTypeB == "knife") ||
+ (fletchNeedsChisel(item) && toolTypeB == "chisel") {
+ return true
+ }
+ }
+ return false
}
diff --git a/internal/game/cmd_room_remove.go b/internal/game/cmd_room_remove.go
index 79b84d8..c84e747 100644
--- a/internal/game/cmd_room_remove.go
+++ b/internal/game/cmd_room_remove.go
@@ -2,11 +2,6 @@ package game
import (
"fmt"
- "os"
- "path/filepath"
- "regexp"
- "strconv"
- "strings"
"gopkg.in/yaml.v3"
"thehouseoficarus/internal/net"
@@ -125,7 +120,7 @@ func (g *Game) roomRemove(sess *net.Session, args []string) {
// Guard 5: no other room points into B. The only allowed inbound edges are
// A→dir→B and C→oppositeDir→B.
- if extra := g.countExtraInbound(bID, aID, cID, dir, oppositeDir); extra > 0 {
+ if extra := world.CountExtraInbound(g.DataDir, bID, aID, cID, dir, oppositeDir); extra > 0 {
sess.WriteLine(fmt.Sprintf(
"Cannot remove: %d other exit(s) point into %s (it was modified since insertion).",
extra, bName))
@@ -244,53 +239,5 @@ func (g *Game) roomRemove(sess *net.Session, args []string) {
g.doLook(sess)
}
-// countExtraInbound returns the number of exits in rooms OTHER than A and C
-// that point into B, plus any A/C exits to B other than the expected
-// (A→dir→B, C→oppositeDir→B). A nonzero result means B has been modified since
-// it was inserted and the remove would silently orphan edges. It walks
-// data/rooms/ and matches \b<id>\b against raw file contents to skip rooms
-// that don't mention bID at all, then unmarshals the matching files.
-func (g *Game) countExtraInbound(bID, aID, cID int, dir, oppositeDir world.ExitDir) int {
- roomsDir := filepath.Join(g.DataDir, "rooms")
- re := regexp.MustCompile(fmt.Sprintf(`\b%d\b`, bID))
- var extra int
- _ = filepath.WalkDir(roomsDir, func(path string, d os.DirEntry, err error) error {
- if err != nil || d.IsDir() || filepath.Ext(path) != ".yaml" {
- return nil
- }
- data, rerr := os.ReadFile(path)
- if rerr != nil {
- return nil
- }
- if !re.MatchString(string(data)) {
- return nil
- }
- idStr := strings.TrimSuffix(filepath.Base(path), ".yaml")
- rid, serr := strconv.Atoi(idStr)
- if serr != nil || rid == bID {
- return nil
- }
- var room world.Room
- if yerr := yaml.Unmarshal(data, &room); yerr != nil {
- return nil
- }
- if room.Exits == nil {
- return nil
- }
- for ed, exitDef := range room.Exits {
- if exitDef.Room != bID {
- continue
- }
- // Allowed: A→dir→B and C→oppositeDir→B.
- if rid == aID && ed == dir {
- continue
- }
- if rid == cID && ed == oppositeDir {
- continue
- }
- extra++
- }
- return nil
- })
- return extra
-}
+// countExtraInbound was moved to world.CountExtraInbound so the in-game and
+// admin removal paths share one implementation.
diff --git a/internal/game/correct_audit_fixes_test.go b/internal/game/correct_audit_fixes_test.go
index 681843c..6177eac 100644
--- a/internal/game/correct_audit_fixes_test.go
+++ b/internal/game/correct_audit_fixes_test.go
@@ -7,6 +7,7 @@ import (
"thehouseoficarus/internal/casino"
"thehouseoficarus/internal/item"
"thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
"thehouseoficarus/internal/prediction"
)
@@ -106,6 +107,25 @@ func TestPickupCreditsHonorsRequestedQuantity(t *testing.T) {
// Regression: the player-file classifier must canonicalize repeated "/" and
// "data/" prefixes so a raw-editor path cannot dodge the online check.
+// Regression: availability scans every recipe, but execution used Craft[0], so
+// a player satisfying a later recipe (e.g. oak arrow shafts) got a start-then-
+// abort. usableCraftIndex must pick the satisfied recipe.
+func TestUsableCraftIndexSelectsSatisfiedRecipe(t *testing.T) {
+ g := &Game{}
+ p := player.New("alice")
+ def := &item.ItemDef{
+ ID: "arrow_shaft",
+ Craft: item.CraftList{
+ {Type: "fletching", Level: 1, Ingredients: []item.IngredientEntry{{Items: []string{"logs"}, Quantity: 1}}},
+ {Type: "fletching", Level: 1, Ingredients: []item.IngredientEntry{{Items: []string{"oak_logs"}, Quantity: 1}}},
+ },
+ }
+ p.SetInvSlot(0, &player.InventorySlot{ItemID: "oak_logs", Quantity: 5})
+ if idx := g.usableCraftIndex(p, def); idx != 1 {
+ t.Fatalf("usableCraftIndex = %d, want 1 (the satisfied oak recipe)", idx)
+ }
+}
+
func TestPlayerDataFileNameCanonicalizesPrefixes(t *testing.T) {
for _, in := range []string{
"players/characters/alice.yaml",
diff --git a/internal/game/correct_combine_skill_test.go b/internal/game/correct_combine_skill_test.go
index d6de229..7d9c725 100644
--- a/internal/game/correct_combine_skill_test.go
+++ b/internal/game/correct_combine_skill_test.go
@@ -68,7 +68,7 @@ func TestCombineXPGoesToCraftingNotAPhantomSkill(t *testing.T) {
h.g.ItemStore.Reload(h.dir)
p.SetInvSlot(0, &player.InventorySlot{ItemID: "correct_combine_part", Quantity: 1})
def := mustLoadItem(t, h.g, "correct_combine_out")
- h.g.startCombine(sess, p, def, def.FirstCraft(), 1)
+ h.g.startCombine(sess, p, def, def.FirstCraft(), 1, 0)
if p.Action == nil {
t.Fatal("combine did not start")
}
diff --git a/internal/game/hardening_craft_combine_fletch_test.go b/internal/game/hardening_craft_combine_fletch_test.go
index f3f6832..5b5d1c1 100644
--- a/internal/game/hardening_craft_combine_fletch_test.go
+++ b/internal/game/hardening_craft_combine_fletch_test.go
@@ -124,7 +124,7 @@ func TestCombineHonorsSuccessXPAndByproducts(t *testing.T) {
sess.SetState(net.StateGame)
def := mustLoadItem(t, g, "test_combine_out")
- g.startCombine(sess, p, def, def.FirstCraft(), 1)
+ g.startCombine(sess, p, def, def.FirstCraft(), 1, 0)
if p.Action == nil {
t.Fatal("combine did not start")
}
@@ -165,7 +165,7 @@ func TestCombineCreatesFailOnFailure(t *testing.T) {
sess.SetState(net.StateGame)
def := mustLoadItem(t, g, "test_combine_fail_out")
- g.startCombine(sess, p, def, def.FirstCraft(), 1)
+ g.startCombine(sess, p, def, def.FirstCraft(), 1, 0)
g.advanceCombine(sess, p)
g.advanceCombine(sess, p)
g.advanceCombine(sess, p)
diff --git a/internal/game/production_combine_regression_test.go b/internal/game/production_combine_regression_test.go
index c617565..80e0651 100644
--- a/internal/game/production_combine_regression_test.go
+++ b/internal/game/production_combine_regression_test.go
@@ -92,7 +92,7 @@ func TestCombineAlternativesConsumeVerifiedSource(t *testing.T) {
sess := &net.Session{Conn: conn, Player: p}
sess.SetState(net.StateGame)
- h.g.startCombine(sess, p, dough, dough.FirstCraft(), 1)
+ h.g.startCombine(sess, p, dough, dough.FirstCraft(), 1, 0)
if p.Action == nil {
t.Error("combine action did not start")
return
diff --git a/internal/game/production_engine.go b/internal/game/production_engine.go
index f80daa2..4717fbf 100644
--- a/internal/game/production_engine.go
+++ b/internal/game/production_engine.go
@@ -72,7 +72,7 @@ func (g *Game) resolveCraftMessage(sess *net.Session, itemMsg, defaultMsg string
return g.resolveCraftVar(sess, msg, craft, outputID, byproducts, hasItem)
}
-func (g *Game) startProduction(sess *net.Session, p *player.Player, item *item.ItemDef, craft *item.CraftDef, actionType behavior.ActionType, displayVerb, startMsg, endMsg string, count int) {
+func (g *Game) startProduction(sess *net.Session, p *player.Player, item *item.ItemDef, craft *item.CraftDef, actionType behavior.ActionType, displayVerb, startMsg, endMsg string, count int, craftIndex int) {
g.cancelAction(p)
g.cancelBgAction(p)
@@ -110,6 +110,7 @@ func (g *Game) startProduction(sess *net.Session, p *player.Player, item *item.I
TargetName: outputName,
Data: &behavior.ProductionData{
ItemID: item.ID,
+ CraftIndex: craftIndex,
Phase: 0,
TicksPerCycle: wait,
StartMessage: startMsg,
@@ -122,11 +123,42 @@ func (g *Game) startProduction(sess *net.Session, p *player.Player, item *item.I
g.broadcastAction(sess, "%s starts %s.", p.Name, displayVerb)
}
+// usableCraftIndex returns the index of the first recipe of item the player
+// satisfies (level and ingredients), or -1. Availability checks scan every
+// recipe, so execution must select the same one instead of always Craft[0].
+func (g *Game) usableCraftIndex(p *player.Player, item *item.ItemDef) int {
+ for i := range item.Craft {
+ c := &item.Craft[i]
+ skill := c.EffectiveSkill()
+ if skill != "" && p.Level(player.SkillName(skill)) < c.Level {
+ continue
+ }
+ if !craftHasAllItemsQty(c, p.CountItem) {
+ continue
+ }
+ return i
+ }
+ return -1
+}
+
+// usableCraft is usableCraftIndex with a first-recipe fallback for callers
+// that only need a display recipe.
+func (g *Game) usableCraft(p *player.Player, item *item.ItemDef) *item.CraftDef {
+ if idx := g.usableCraftIndex(p, item); idx >= 0 {
+ return &item.Craft[idx]
+ }
+ return item.FirstCraft()
+}
+
func (g *Game) startProductionFromItem(sess *net.Session, p *player.Player, item *item.ItemDef, count int) {
- craft := item.FirstCraft()
- if craft == nil {
+ idx := g.usableCraftIndex(p, item)
+ if idx < 0 {
+ idx = 0
+ }
+ if idx >= len(item.Craft) {
return
}
+ craft := &item.Craft[idx]
// Normalize the empty craft type to "combine" here as well as in the
// craft index: an omitted `type:` is combine, but the index's normalized
@@ -137,7 +169,7 @@ func (g *Game) startProductionFromItem(sess *net.Session, p *player.Player, item
craftType = "combine"
}
if craftType == "combine" {
- g.startCombine(sess, p, item, craft, count)
+ g.startCombine(sess, p, item, craft, count, idx)
return
}
@@ -172,7 +204,7 @@ func (g *Game) startProductionFromItem(sess *net.Session, p *player.Player, item
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)
+ g.startProduction(sess, p, item, craft, info.ActionType, info.DisplayVerb, startMsg, endMsg, count, idx)
}
func (g *Game) loadCraftItem(itemID string) *item.ItemDef {
@@ -201,7 +233,11 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) {
g.cancelAction(p)
return
}
- craft := item.FirstCraft()
+ idx := d.CraftIndex
+ if idx < 0 || idx >= len(item.Craft) {
+ idx = 0
+ }
+ craft := &item.Craft[idx]
if phase == 0 {
if startMsg != "" {
@@ -250,7 +286,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) {
// Byproducts must be collected BEFORE consumption: the resolver
// maps each ingredient entry to the alternative actually being
// consumed, which requires the items still present.
- byproducts := g.collectByproducts(p, item)
+ byproducts := g.collectByproducts(p, craft)
if !consumeIngredients(craft, p.CountItem, p.RemoveItem) {
// Cannot happen after the availability gate above, but never
@@ -323,7 +359,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) {
}
}
- if g.canContinueProduction(p, item) {
+ if g.canContinueProduction(p, item, craft) {
p.Action.WaitLeft = engine.ToTicks(wait)
return
}
@@ -334,8 +370,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) {
g.cancelAction(p)
}
-func (g *Game) collectByproducts(p *player.Player, item *item.ItemDef) []string {
- craft := item.FirstCraft()
+func (g *Game) collectByproducts(p *player.Player, craft *item.CraftDef) []string {
if craft == nil {
return nil
}
@@ -433,8 +468,7 @@ func (g *Game) placeCraftByproducts(p *player.Player, byproducts []string) {
}
}
-func (g *Game) canContinueProduction(p *player.Player, item *item.ItemDef) bool {
- craft := item.FirstCraft()
+func (g *Game) canContinueProduction(p *player.Player, item *item.ItemDef, craft *item.CraftDef) bool {
if craft == nil {
return false
}
diff --git a/internal/game/production_ingredients_test.go b/internal/game/production_ingredients_test.go
index d799df8..5206346 100644
--- a/internal/game/production_ingredients_test.go
+++ b/internal/game/production_ingredients_test.go
@@ -111,14 +111,14 @@ func TestCollectByproductsMatchesConsumedAlternative(t *testing.T) {
// Only the jug is present: the empty jug returns.
p := invOf(t, map[string]int{"jug_of_water": 1})
- if got := g.collectByproducts(p, &item.ItemDef{Craft: item.CraftList{*craft}}); len(got) != 1 || got[0] != "empty_jug" {
+ if got := g.collectByproducts(p, craft); len(got) != 1 || got[0] != "empty_jug" {
t.Errorf("byproducts = %v, want [empty_jug]", got)
}
// Both present: consumption picks the FIRST alternative (the bucket),
// so the byproduct must be the empty bucket.
p = invOf(t, map[string]int{"bucket_of_water": 1, "jug_of_water": 1})
- if got := g.collectByproducts(p, &item.ItemDef{Craft: item.CraftList{*craft}}); len(got) != 1 || got[0] != "empty_bucket" {
+ if got := g.collectByproducts(p, craft); len(got) != 1 || got[0] != "empty_bucket" {
t.Errorf("byproducts = %v, want [empty_bucket]", got)
}
}
diff --git a/internal/game/production_menu.go b/internal/game/production_menu.go
index 9b9f8e6..5d712da 100644
--- a/internal/game/production_menu.go
+++ b/internal/game/production_menu.go
@@ -101,7 +101,7 @@ func (g *Game) dispatchMenuEntry(sess *net.Session, entry map[string]string) {
}
func (g *Game) formatMaterials(sess *net.Session, item *item.ItemDef) string {
- craft := item.FirstCraft()
+ craft := g.usableCraft(sess.Player, item)
if craft == nil {
return ""
}
@@ -125,7 +125,7 @@ func (g *Game) formatMaterials(sess *net.Session, item *item.ItemDef) string {
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
+ return g.usableCraft(p, items[i]).Level < g.usableCraft(p, items[j]).Level
})
mode := g.colorMode(sess)
@@ -138,7 +138,7 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, items []
}
for i, item := range items {
- craft := item.FirstCraft()
+ craft := g.usableCraft(p, item)
outDef, _ := g.ItemStore.Load(item.ID)
productName := item.ID
if outDef != nil {
@@ -230,7 +230,7 @@ func (g *Game) handleProductChoice(sess *net.Session, input string) {
}
sort.Slice(items, func(i, j int) bool {
- return items[i].FirstCraft().Level < items[j].FirstCraft().Level
+ return g.usableCraft(p, items[i]).Level < g.usableCraft(p, items[j]).Level
})
lastItemID, _ := p.Flags[lastFlagKey].(string)
@@ -247,7 +247,7 @@ func (g *Game) handleProductChoice(sess *net.Session, input string) {
return
}
- craft := sel.Item.FirstCraft()
+ craft := g.usableCraft(p, sel.Item)
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))