aboutsummaryrefslogtreecommitdiff
path: root/internal/game/production_index.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/production_index.go')
-rw-r--r--internal/game/production_index.go109
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
}