diff options
| author | historia <[not public]> | 2026-09-17 22:00:33 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-17 22:00:33 -0400 |
| commit | 32a3c59cebf2a16277e54d715a67adfdbe00b456 (patch) | |
| tree | 9bc87011513ef92f0613d9339fc55689f130915d /internal/game/production_engine.go | |
| parent | fea2cc6839ec3ee17940a2dea69622dc6f4deabf (diff) | |
| download | thehouseoficarus-32a3c59cebf2a16277e54d715a67adfdbe00b456.tar.gz | |
fix: exact casino settlement/refund coverage, journal damage handling, craft output ordering, aggregate ingredient planning
Diffstat (limited to 'internal/game/production_engine.go')
| -rw-r--r-- | internal/game/production_engine.go | 76 |
1 files changed, 44 insertions, 32 deletions
diff --git a/internal/game/production_engine.go b/internal/game/production_engine.go index 9643483..2cd9b1d 100644 --- a/internal/game/production_engine.go +++ b/internal/game/production_engine.go @@ -215,36 +215,39 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { info, _ := productionTypes[cKey] + // A full inventory must never destroy materials: verify BOTH possible + // outcomes can place their item (the output on success, the fail + // replacement on failure) BEFORE the roll and any consumption. + failDef, _ := g.ItemStore.Load(craft.Fail) + if !craftOutputFits(p, itemID, item) || (failDef != nil && !craftOutputFits(p, craft.Fail, failDef)) { + sess.WriteLine("Your inventory is too full!") + g.cancelAction(p) + return false + } + if rand.Float64() < chance { outputQty := craft.OutputQty if outputQty <= 0 { outputQty = 1 } + // 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) - placed := false - outDef, _ := g.ItemStore.Load(item.ID) - if outDef != nil && outDef.Stackable { - for i := 0; i < 28; i++ { - slot := p.InvSlot(i) - if slot != nil && slot.ItemID == item.ID { - consumeIngredients(craft, p.CountItem, p.RemoveItem) - slot.Quantity += outputQty - placed = true - break - } - } + if !consumeIngredients(craft, p.CountItem, p.RemoveItem) { + // Cannot happen after the availability gate above, but never + // produce output from a failed consumption. + g.cancelAction(p) + return false } - if !placed { - consumeIngredients(craft, p.CountItem, p.RemoveItem) - freeSlot := p.FirstFreeSlot() - if freeSlot == -1 { - sess.WriteLine("Your inventory is too full!") - g.cancelAction(p) - return false - } - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: item.ID, Quantity: outputQty}) + if !placeCraftOutput(p, itemID, item, outputQty) { + // Defensive: the fits check above should have guaranteed a + // stack or slot. + sess.WriteLine("Your inventory is too full!") + g.cancelAction(p) + return false } for _, bp := range byproducts { @@ -272,7 +275,10 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { } sess.WriteLine(msg) } else { - consumeIngredients(craft, p.CountItem, p.RemoveItem) + if !consumeIngredients(craft, p.CountItem, p.RemoveItem) { + g.cancelAction(p) + return false + } if craft.Fail != "" { freeSlot := p.FirstFreeSlot() @@ -317,20 +323,26 @@ func (g *Game) collectByproducts(p *player.Player, item *item.ItemDef) []string if craft == nil { return nil } + // The byproduct must match the alternative that will actually be + // consumed, resolved with the SAME aggregate plan consumeIngredients + // uses (shared stock reserved across entries), not the first + // alternative merely present: with a bucket and a jug of water both + // in the inventory, consuming the bucket must return the empty + // bucket — and when entries share stock, the plan decides who gets it. + plan := planIngredients(craft, p.CountItem) + if plan == nil { + return nil + } var byproducts []string - for _, e := range craft.Ingredients { + for i := range craft.Ingredients { + e := &craft.Ingredients[i] if len(e.Byproducts) == 0 { continue } - // The byproduct must match the alternative that will actually be - // consumed (the same craftIngredientSource resolution - // consumeIngredients uses), not the first alternative merely - // present: with a bucket and a jug of water both in the inventory, - // consuming the bucket must return the empty bucket. - id, _ := craftIngredientSource(&e, p.CountItem) - for i, alt := range e.Items { - if alt == id && i < len(e.Byproducts) && e.Byproducts[i] != "" { - byproducts = append(byproducts, e.Byproducts[i]) + chosen := plan[i].id + for j, alt := range e.Items { + if alt == chosen && j < len(e.Byproducts) && e.Byproducts[j] != "" { + byproducts = append(byproducts, e.Byproducts[j]) break } } |
