From fea2cc6839ec3ee17940a2dea69622dc6f4deabf Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 16 Sep 2026 21:35:55 -0400 Subject: fix: unify crafting ingredient resolution; fix superheat alternatives, quantity gates, and dropped combine byproducts --- internal/game/production_index.go | 92 ++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 41 deletions(-) (limited to 'internal/game/production_index.go') diff --git a/internal/game/production_index.go b/internal/game/production_index.go index 2646a43..8c0207c 100644 --- a/internal/game/production_index.go +++ b/internal/game/production_index.go @@ -178,63 +178,73 @@ func craftMatchesEntry(c *item.CraftDef, itemID string) bool { return false } -func craftHasAllItems(c *item.CraftDef, hasItem func(string) bool) bool { - if c == nil { - return false - } - for _, e := range c.Ingredients { - found := false - for _, id := range e.Items { - if hasItem(id) { - found = true - break - } - } - if !found { - return false +// craftHasAllItems reports whether every ingredient entry can be satisfied: +// one listed alternative covering the entry's full quantity (not merely one +// of each item being present — a recipe needing 15 feathers is not offered +// on 1). Resolves through craftIngredientSource so availability checks can +// never disagree with consumption. +func craftHasAllItems(c *item.CraftDef, countItem func(string) int) bool { + return craftHasAllItemsQty(c, countItem) +} + +// craftIngredientSource resolves one ingredient entry: the FIRST listed +// alternative the player holds in the entry's full (normalized) quantity, +// plus that quantity. Availability checks and consumption both resolve +// through this helper so they always pick the same alternative — an entry +// listing several alternatives (e.g. bucket or jug of water) consumes the +// one the check verified, never a partially-stocked other one. +// Returns "" when no alternative covers the quantity. +func craftIngredientSource(e *item.IngredientEntry, countItem func(string) int) (string, int) { + qty := e.Quantity + if qty <= 0 { + qty = 1 + } + for _, id := range e.Items { + if countItem(id) >= qty { + return id, qty } } - return true + return "", qty } func craftHasAllItemsQty(c *item.CraftDef, countItem func(string) int) bool { if c == nil { return false } - for _, e := range c.Ingredients { - found := false - for _, id := range e.Items { - qty := e.Quantity - if qty <= 0 { - qty = 1 - } - if countItem(id) >= qty { - found = true - break - } - } - if !found { + for i := range c.Ingredients { + if id, _ := craftIngredientSource(&c.Ingredients[i], countItem); id == "" { return false } } return true } -func consumeIngredients(c *item.CraftDef, hasItem func(string) bool, removeItem func(string, int) bool) { +// consumeIngredients removes one batch of every ingredient entry. Each +// entry consumes the same alternative the availability checks verify (the +// first one covering the FULL quantity), and consumption is two-pass: +// nothing is removed unless every entry resolves, so a craft can never +// partially consume an alternative and silently keep the rest. Callers that +// already ran a craftHasAllItems* check cannot see a false return; it +// guards callers whose own pre-check is weaker. Callers that check the +// result must abort the craft without producing output. +func consumeIngredients(c *item.CraftDef, countItem func(string) int, removeItem func(string, int) bool) bool { if c == nil { - return + return true } - for _, e := range c.Ingredients { - found := false - for _, id := range e.Items { - if hasItem(id) { - removeItem(id, e.Quantity) - found = true - break - } - } - if !found { - return + type consumption struct { + id string + qty int + } + plan := make([]consumption, 0, len(c.Ingredients)) + for i := range c.Ingredients { + id, qty := craftIngredientSource(&c.Ingredients[i], countItem) + if id == "" { + return false } + plan = append(plan, consumption{id: id, qty: qty}) + } + for _, s := range plan { + removeItem(s.id, s.qty) } + return true } -- cgit v1.2.3