aboutsummaryrefslogtreecommitdiff
path: root/internal/game/production_engine.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-17 22:00:33 -0400
committerhistoria <[not public]>2026-09-17 22:00:33 -0400
commit32a3c59cebf2a16277e54d715a67adfdbe00b456 (patch)
tree9bc87011513ef92f0613d9339fc55689f130915d /internal/game/production_engine.go
parentfea2cc6839ec3ee17940a2dea69622dc6f4deabf (diff)
downloadthehouseoficarus-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.go76
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
}
}