diff options
Diffstat (limited to 'internal/game/production_index.go')
| -rw-r--r-- | internal/game/production_index.go | 109 |
1 files changed, 85 insertions, 24 deletions
diff --git a/internal/game/production_index.go b/internal/game/production_index.go index 8c0207c..ffacd8a 100644 --- a/internal/game/production_index.go +++ b/internal/game/production_index.go @@ -4,6 +4,7 @@ import ( "sync" "thehouseoficarus/internal/item" + "thehouseoficarus/internal/player" ) type CraftIndex struct { @@ -208,43 +209,103 @@ func craftIngredientSource(e *item.IngredientEntry, countItem func(string) int) } func craftHasAllItemsQty(c *item.CraftDef, countItem func(string) int) bool { + return planIngredients(c, countItem) != nil +} + +// consumption is one planned ingredient removal: the alternative chosen +// for an entry and the quantity to remove of it. +type consumption struct { + id string + qty int +} + +// planIngredients resolves every ingredient entry against the player's +// stock, reserving quantities ACROSS entries: two entries listing the same +// item (e.g. two one-ore entries) cannot both consume the same single ore. +// Availability checks and consumption both plan through this helper so +// they always agree; returns nil when any entry cannot be satisfied from +// the stock remaining after earlier entries' reservations. +func planIngredients(c *item.CraftDef, countItem func(string) int) []consumption { if c == nil { - return false + return nil } + reserved := make(map[string]int, len(c.Ingredients)) + plan := make([]consumption, 0, len(c.Ingredients)) for i := range c.Ingredients { - if id, _ := craftIngredientSource(&c.Ingredients[i], countItem); id == "" { - return false + e := &c.Ingredients[i] + qty := e.Quantity + if qty <= 0 { + qty = 1 } + chosen := "" + for _, id := range e.Items { + if countItem(id)-reserved[id] >= qty { + chosen = id + break + } + } + if chosen == "" { + return nil + } + reserved[chosen] += qty + plan = append(plan, consumption{id: chosen, qty: qty}) } - return true + return plan } // 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. +// entry consumes the same alternative the availability checks verify, and +// consumption is all-or-nothing: nothing is removed unless every entry can +// be satisfied from the aggregate stock, so a craft can never partially +// consume an ingredient or double-count shared stock. 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 true + plan := planIngredients(c, countItem) + if plan == nil { + return false } - type consumption struct { - id string - qty int + for _, s := range plan { + removeItem(s.id, s.qty) } - plan := make([]consumption, 0, len(c.Ingredients)) - for i := range c.Ingredients { - id, qty := craftIngredientSource(&c.Ingredients[i], countItem) - if id == "" { - return false + return true +} + +// craftOutputFits reports whether outputQty of an item can be placed into +// the inventory right now: merged into an existing stack for stackable +// definitions, otherwise a free slot. Crafters must check BEFORE consuming +// ingredients so a full inventory can never destroy materials without +// producing the output. +func craftOutputFits(p *player.Player, itemID string, outDef *item.ItemDef) bool { + if outDef != nil && outDef.Stackable { + for i := 0; i < 28; i++ { + if slot := p.InvSlot(i); slot != nil && slot.ItemID == itemID { + return true + } } - plan = append(plan, consumption{id: id, qty: qty}) } - for _, s := range plan { - removeItem(s.id, s.qty) + return p.FirstFreeSlot() != -1 +} + +// placeCraftOutput puts outputQty of an item into the inventory, merging +// into an existing stack for stackable definitions. Returns false only +// when nothing fits — callers should have verified craftOutputFits first +// (consumption cannot free fewer slots than the check saw on the owner +// goroutine, so a false return is defensive). +func placeCraftOutput(p *player.Player, itemID string, outDef *item.ItemDef, outputQty int) bool { + if outDef != nil && outDef.Stackable { + for i := 0; i < 28; i++ { + if slot := p.InvSlot(i); slot != nil && slot.ItemID == itemID { + slot.Quantity += outputQty + return true + } + } + } + freeSlot := p.FirstFreeSlot() + if freeSlot == -1 { + return false } + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: itemID, Quantity: outputQty}) return true } |
