package game import ( "sync" "thehouseoficarus/internal/item" ) type CraftIndex struct { mu sync.RWMutex byType map[string][]*item.ItemDef bySubtype map[string][]*item.ItemDef byInput map[string][]*item.ItemDef byStation map[string][]*item.ItemDef } func NewCraftIndex() *CraftIndex { return &CraftIndex{ byType: make(map[string][]*item.ItemDef), bySubtype: make(map[string][]*item.ItemDef), byInput: make(map[string][]*item.ItemDef), byStation: make(map[string][]*item.ItemDef), } } func (idx *CraftIndex) Build(items []*item.ItemDef) { idx.mu.Lock() defer idx.mu.Unlock() idx.byType = make(map[string][]*item.ItemDef) idx.bySubtype = make(map[string][]*item.ItemDef) idx.byInput = make(map[string][]*item.ItemDef) idx.byStation = make(map[string][]*item.ItemDef) for _, def := range items { if len(def.Craft) == 0 { continue } for ci := range def.Craft { craft := &def.Craft[ci] t := craft.Type if t == "" { t = "combine" } idx.byType[t] = appendUnique(idx.byType[t], def) if craft.Subtype != "" { idx.bySubtype[craft.Subtype] = appendUnique(idx.bySubtype[craft.Subtype], def) } for _, e := range craft.Ingredients { for _, id := range e.Items { idx.byInput[id] = appendUnique(idx.byInput[id], def) } } for _, sid := range craft.Station { idx.byStation[sid] = appendUnique(idx.byStation[sid], def) } } } } func appendUnique(items []*item.ItemDef, item *item.ItemDef) []*item.ItemDef { for _, existing := range items { if existing == item { return items } } return append(items, item) } func (idx *CraftIndex) ByType(craftType string) []*item.ItemDef { idx.mu.RLock() defer idx.mu.RUnlock() items := idx.byType[craftType] out := make([]*item.ItemDef, len(items)) copy(out, items) return out } func (idx *CraftIndex) BySubtype(subtype string) []*item.ItemDef { idx.mu.RLock() defer idx.mu.RUnlock() items := idx.bySubtype[subtype] out := make([]*item.ItemDef, len(items)) copy(out, items) return out } func (idx *CraftIndex) ByStation(stationDefID string) []*item.ItemDef { idx.mu.RLock() defer idx.mu.RUnlock() items := idx.byStation[stationDefID] out := make([]*item.ItemDef, len(items)) copy(out, items) return out } func (idx *CraftIndex) ByInput(itemID string) []*item.ItemDef { idx.mu.RLock() defer idx.mu.RUnlock() items := idx.byInput[itemID] out := make([]*item.ItemDef, len(items)) copy(out, items) return out } func (idx *CraftIndex) FindByTwoInputs(itemA, itemB string) []*item.ItemDef { idx.mu.RLock() defer idx.mu.RUnlock() candidatesA := idx.byInput[itemA] if len(candidatesA) == 0 { return nil } candidateSetB := make(map[string]bool) for _, def := range idx.byInput[itemB] { candidateSetB[def.ID] = true } var results []*item.ItemDef for _, def := range candidatesA { if !candidateSetB[def.ID] { continue } for _, craft := range def.Craft { entryA := craftEntry(craft, itemA) entryB := craftEntry(craft, itemB) if entryA >= 0 && entryB >= 0 && entryA != entryB { results = append(results, def) break } } } return results } func (idx *CraftIndex) FindByStationInput(stationDefID, itemID string) []*item.ItemDef { idx.mu.RLock() defer idx.mu.RUnlock() var results []*item.ItemDef for _, def := range idx.byStation[stationDefID] { for _, craft := range def.Craft { if craftMatchesEntry(&craft, itemID) { results = append(results, def) break } } } return results } func craftEntry(craft item.CraftDef, itemID string) int { for ei, e := range craft.Ingredients { for _, id := range e.Items { if id == itemID { return ei } } } return -1 } func craftMatchesEntry(c *item.CraftDef, itemID string) bool { if c == nil { return false } for _, e := range c.Ingredients { for _, id := range e.Items { if id == itemID { return true } } } 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 "", qty } func craftHasAllItemsQty(c *item.CraftDef, countItem func(string) int) bool { if c == nil { return false } for i := range c.Ingredients { if id, _ := craftIngredientSource(&c.Ingredients[i], countItem); id == "" { return false } } return true } // 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 true } 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 }