diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/action_clean.go | 60 | ||||
| -rw-r--r-- | internal/game/action_farm.go | 4 | ||||
| -rw-r--r-- | internal/game/action_fletch.go | 110 | ||||
| -rw-r--r-- | internal/game/action_steal.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_bank.go | 4 | ||||
| -rw-r--r-- | internal/game/cmd_clean.go | 42 | ||||
| -rw-r--r-- | internal/game/cmd_cook.go | 42 | ||||
| -rw-r--r-- | internal/game/cmd_craft.go | 185 | ||||
| -rw-r--r-- | internal/game/cmd_fletch.go | 181 | ||||
| -rw-r--r-- | internal/game/cmd_shop.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_smelt.go | 36 | ||||
| -rw-r--r-- | internal/game/cmd_smith.go | 59 | ||||
| -rw-r--r-- | internal/game/cmd_trigger_utility.go | 44 | ||||
| -rw-r--r-- | internal/game/cmd_use.go | 264 | ||||
| -rw-r--r-- | internal/game/cmd_verbs.go | 4 | ||||
| -rw-r--r-- | internal/game/core_production.go | 406 | ||||
| -rw-r--r-- | internal/game/core_startup.go | 95 | ||||
| -rw-r--r-- | internal/game/craft_index.go | 234 | ||||
| -rw-r--r-- | internal/game/game.go | 14 |
19 files changed, 946 insertions, 842 deletions
diff --git a/internal/game/action_clean.go b/internal/game/action_clean.go index 7330883..b85a3e0 100644 --- a/internal/game/action_clean.go +++ b/internal/game/action_clean.go @@ -19,37 +19,31 @@ func (g *Game) advanceClean(sess *net.Session, p *player.Player) { return } - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - g.cancelBgAction(p) - return - } + items := g.CraftIndex.ByType("clean") - var recipe *cleanMatch - for _, r := range allRecipes { - if r.Type != "clean" { - continue - } + var match *cleanMatch + for _, item := range items { + c := item.FirstCraft() if filter != "" { - if len(r.Consume) == 0 || len(r.Consume[0].Items) == 0 { + if len(c.Consume) == 0 || len(c.Consume[0].Items) == 0 { continue } - if !strings.Contains(r.Consume[0].Items[0], filter) && - !strings.Contains(r.Output, filter) { + if !strings.Contains(c.Consume[0].Items[0], filter) && + !strings.Contains(item.ID, filter) { continue } } - if p.Level(player.Pharmacy) < r.Level { + if p.Level(player.Pharmacy) < c.Level { continue } - if !r.HasAllItems(p.HasItem) { + if !craftHasAllItems(c, p.HasItem) { continue } - recipe = &cleanMatch{r.Consume[0].Items[0], r.Output, r.XP, r.Message} + match = &cleanMatch{c.Consume[0].Items[0], item.ID, c.XP, c.Message} break } - if recipe == nil { + if match == nil { sess.WriteLine("\nYou've finished cleaning herbs.") g.cancelBgAction(p) return @@ -57,47 +51,45 @@ func (g *Game) advanceClean(sess *net.Session, p *player.Player) { for i := 0; i < 28; i++ { slot := p.InvSlot(i) - if slot != nil && slot.ItemID == recipe.consumeID { - slot.ItemID = recipe.outputID + if slot != nil && slot.ItemID == match.consumeID { + slot.ItemID = match.outputID break } } - if recipe.xp > 0 { - g.awardSkillXP(sess, p, player.Pharmacy, recipe.xp) + if match.xp > 0 { + g.awardSkillXP(sess, p, player.Pharmacy, match.xp) } g.AccountStore.SaveCharacter(p) - msg := recipe.message + msg := match.message if msg == "" { - outDef, _ := g.ItemStore.Load(recipe.outputID) - outputName := recipe.outputID + outDef, _ := g.ItemStore.Load(match.outputID) + outputName := match.outputID if outDef != nil { outputName = outDef.Name } msg = fmt.Sprintf("You clean a %s.", outputName) } - if p.OptionBool("xp_drops") && recipe.xp > 0 { + if p.OptionBool("xp_drops") && match.xp > 0 { abbr := player.SkillAbbr[player.Pharmacy] - msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.xp, abbr)) + msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", match.xp, abbr)) } sess.WriteLine(msg) hasMore := false - for _, r := range allRecipes { - if r.Type != "clean" { - continue - } + for _, item := range items { + c := item.FirstCraft() if filter != "" { - if len(r.Consume) == 0 || len(r.Consume[0].Items) == 0 { + if len(c.Consume) == 0 || len(c.Consume[0].Items) == 0 { continue } - if !strings.Contains(r.Consume[0].Items[0], filter) && - !strings.Contains(r.Output, filter) { + if !strings.Contains(c.Consume[0].Items[0], filter) && + !strings.Contains(item.ID, filter) { continue } } - if p.Level(player.Pharmacy) >= r.Level && r.HasAllItems(p.HasItem) { + if p.Level(player.Pharmacy) >= c.Level && craftHasAllItems(c, p.HasItem) { hasMore = true break } diff --git a/internal/game/action_farm.go b/internal/game/action_farm.go index 21604aa..679af83 100644 --- a/internal/game/action_farm.go +++ b/internal/game/action_farm.go @@ -6,8 +6,8 @@ import ( "sort" "strings" - "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) @@ -484,7 +484,7 @@ func (g *Game) farmMatchPrefix(p *player.Player, input string) (prefix string, d lower := strings.ToLower(input) for _, f := range farms { - if strings.Contains(lower, strings.ToLower(f.name)) || action.WordPrefixMatch(lower, f.name) { + if strings.Contains(lower, strings.ToLower(f.name)) || object.WordPrefixMatch(lower, f.name) { return f.prefix, f.defID, f.index } } diff --git a/internal/game/action_fletch.go b/internal/game/action_fletch.go index 3d8c30f..52de86b 100644 --- a/internal/game/action_fletch.go +++ b/internal/game/action_fletch.go @@ -6,43 +6,45 @@ import ( "thehouseoficarus/internal/action" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) -func (g *Game) startFletchAction(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) { +func (g *Game) startFletchAction(sess *net.Session, p *player.Player, item *object.ItemDef, count int) { p.EnsureFlags() - p.Flags["last_fletch"] = recipe.ID + p.Flags["last_fletch"] = item.ID g.AccountStore.SaveCharacter(p) - if recipe.Wait <= 0 { - g.doInstantFletch(sess, p, recipe, count) + if item.FirstCraft().Wait <= 0 { + g.doInstantFletch(sess, p, item, count) return } - g.startTimedFletch(sess, p, recipe, count) + g.startTimedFletch(sess, p, item, count) } -func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) { - outDef, _ := g.ItemStore.Load(recipe.Output) - outputQty := recipe.OutputQty +func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, item *object.ItemDef, count int) { + c := item.FirstCraft() + outputQty := c.OutputQty if outputQty <= 0 { outputQty = 1 } batches := 0 totalOutput := 0 - skill := player.SkillName(recipe.EffectiveSkill()) + skill := player.SkillName(c.EffectiveSkill()) + tmpItem := &object.ItemDef{Craft: object.CraftList{*c}} for { - if !g.canDoFletchRecipe(p, recipe) { + if !g.canDoFletchItem(p, tmpItem) { break } placed := false - if outDef != nil && outDef.Stackable { + if item.Stackable { for i := 0; i < 28; i++ { slot := p.InvSlot(i) - if slot != nil && slot.ItemID == recipe.Output { - recipe.ConsumeAll(p.HasItem, p.RemoveItem) + if slot != nil && slot.ItemID == item.ID { + craftConsumeAll(c, p.HasItem, p.RemoveItem) slot.Quantity += outputQty placed = true break @@ -50,16 +52,16 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, recipe *acti } } if !placed { - recipe.ConsumeAll(p.HasItem, p.RemoveItem) + craftConsumeAll(c, p.HasItem, p.RemoveItem) freeSlot := p.FirstFreeSlot() if freeSlot == -1 { break } - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: outputQty}) + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: item.ID, Quantity: outputQty}) } - if recipe.XP > 0 { - g.awardSkillXP(sess, p, skill, recipe.XP) + if c.XP > 0 { + g.awardSkillXP(sess, p, skill, c.XP) } batches++ @@ -80,57 +82,47 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, recipe *acti g.AccountStore.SaveCharacter(p) - outputName := recipe.Output - if outDef != nil { - outputName = outDef.Name - } - - msg := fmt.Sprintf("You fletch %d %s.", totalOutput, outputName) - if p.OptionBool("xp_drops") && recipe.XP > 0 { - totalXP := recipe.XP * batches + msg := fmt.Sprintf("You fletch %d %s.", totalOutput, item.Name) + if p.OptionBool("xp_drops") && c.XP > 0 { + totalXP := c.XP * batches abbr := player.SkillAbbr[skill] msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", totalXP, abbr)) } sess.WriteLine(msg) } -func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) { - outDef, _ := g.ItemStore.Load(recipe.Output) - outputName := recipe.Output - if outDef != nil { - outputName = outDef.Name - } - +func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, item *object.ItemDef, count int) { p.BackgroundAction = &action.Action{ Type: "fletch", - TargetID: recipe.ID, - TargetName: outputName, + TargetID: item.ID, + TargetName: item.Name, Data: map[string]any{ - "recipe_id": recipe.ID, + "item_id": item.ID, "phase": 0, - "wait": recipe.Wait, + "wait": item.FirstCraft().Wait, "remaining": count, }, WaitLeft: engine.ToTicks(1), } - p.BackgroundActionState = &ActionState{Type: ActionFletching, TargetName: outputName} + p.BackgroundActionState = &ActionState{Type: ActionFletching, TargetName: item.Name} g.broadcastAction(sess, "\n%s starts fletching.", p.Name) - sess.WriteLine(fmt.Sprintf("\nYou begin fletching %s.", outputName)) + sess.WriteLine(fmt.Sprintf("\nYou begin fletching %s.", item.Name)) } func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { - recipeID, _ := p.BackgroundAction.Data["recipe_id"].(string) + itemID, _ := p.BackgroundAction.Data["item_id"].(string) phase, _ := p.BackgroundAction.Data["phase"].(int) wait, _ := p.BackgroundAction.Data["wait"].(float64) remaining, _ := p.BackgroundAction.Data["remaining"].(int) - recipe := g.loadRecipe(recipeID) - if recipe == nil { + item := g.loadCraftItem(itemID) + if item == nil { g.cancelBgAction(p) return } + c := item.FirstCraft() if phase == 0 { p.BackgroundAction.Data["phase"] = 1 @@ -138,26 +130,26 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { return } - if !g.canDoFletchRecipe(p, recipe) { + tmpItem := &object.ItemDef{Craft: object.CraftList{*c}} + if !g.canDoFletchItem(p, tmpItem) { sess.WriteLine("\nYou've run out of fletching materials.") g.cancelBgAction(p) return } - outDef, _ := g.ItemStore.Load(recipe.Output) - outputQty := recipe.OutputQty + outputQty := c.OutputQty if outputQty <= 0 { outputQty = 1 } - skill := player.SkillName(recipe.EffectiveSkill()) + skill := player.SkillName(c.EffectiveSkill()) placed := false - if outDef != nil && outDef.Stackable { + if item.Stackable { for i := 0; i < 28; i++ { slot := p.InvSlot(i) - if slot != nil && slot.ItemID == recipe.Output { - recipe.ConsumeAll(p.HasItem, p.RemoveItem) + if slot != nil && slot.ItemID == item.ID { + craftConsumeAll(c, p.HasItem, p.RemoveItem) slot.Quantity += outputQty placed = true break @@ -165,32 +157,28 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { } } if !placed { - recipe.ConsumeAll(p.HasItem, p.RemoveItem) + craftConsumeAll(c, p.HasItem, p.RemoveItem) freeSlot := p.FirstFreeSlot() if freeSlot == -1 { sess.WriteLine("\nYour inventory is too full!") g.cancelBgAction(p) return } - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: outputQty}) + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: item.ID, Quantity: outputQty}) } - if recipe.XP > 0 { - g.awardSkillXP(sess, p, skill, recipe.XP) + if c.XP > 0 { + g.awardSkillXP(sess, p, skill, c.XP) } g.AccountStore.SaveCharacter(p) - outputName := recipe.Output - if outDef != nil { - outputName = outDef.Name - } - msg := recipe.Message + msg := c.Message if msg == "" { - msg = fmt.Sprintf("You fletch %s.", outputName) + msg = fmt.Sprintf("You fletch %s.", item.Name) } - if p.OptionBool("xp_drops") && recipe.XP > 0 { + if p.OptionBool("xp_drops") && c.XP > 0 { abbr := player.SkillAbbr[skill] - msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr)) + msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", c.XP, abbr)) } sess.WriteLine(msg) @@ -204,7 +192,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { } } - if !g.canContinueProduction(p, recipe) { + if !g.canContinueProduction(p, item) { sess.WriteLine("\nYou've run out of fletching materials.") g.cancelBgAction(p) return diff --git a/internal/game/action_steal.go b/internal/game/action_steal.go index a95be25..259c914 100644 --- a/internal/game/action_steal.go +++ b/internal/game/action_steal.go @@ -169,7 +169,7 @@ func (g *Game) resolveStealTarget(sess *net.Session, p *player.Player, input str var matchingObjs []*object.ObjectDef for _, o := range stealObjs { - if action.WordPrefixMatch(searchName, o.Name) { + if object.WordPrefixMatch(searchName, o.Name) { matchingObjs = append(matchingObjs, o) } } diff --git a/internal/game/cmd_bank.go b/internal/game/cmd_bank.go index d91e115..5f9067d 100644 --- a/internal/game/cmd_bank.go +++ b/internal/game/cmd_bank.go @@ -5,8 +5,8 @@ import ( "sort" "strings" - "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -239,7 +239,7 @@ func (g *Game) doBankWithdraw(sess *net.Session, input string) { } lower := strings.ToLower(input) for _, e := range entries { - if action.WordPrefixMatch(e.name, lower) { + if object.WordPrefixMatch(e.name, lower) { matches = append(matches, e) } } diff --git a/internal/game/cmd_clean.go b/internal/game/cmd_clean.go index 951b41d..28758b8 100644 --- a/internal/game/cmd_clean.go +++ b/internal/game/cmd_clean.go @@ -7,6 +7,7 @@ import ( "thehouseoficarus/internal/combat" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -22,26 +23,19 @@ func (g *Game) doClean(sess *net.Session, input string) { return } - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - sess.WriteLine("Error loading recipes.") - return - } + items := g.CraftIndex.ByType("clean") filter := strings.TrimSpace(input) found := false - for _, r := range allRecipes { - if r.Type != "clean" { - continue - } - if filter != "" && !matchesCleanFilter(r, filter) { + for _, item := range items { + if filter != "" && !matchesCleanFilter(item, filter) { continue } - if p.Level(player.Pharmacy) < r.Level { + if p.Level(player.Pharmacy) < item.FirstCraft().Level { continue } - if !r.HasAllItems(p.HasItem) { + if !craftHasAllItems(item.FirstCraft(), p.HasItem) { continue } found = true @@ -49,7 +43,21 @@ func (g *Game) doClean(sess *net.Session, input string) { } if !found { - sess.WriteLine("You don't have any herbs to clean.") + hasHerbs := false + for _, item := range items { + if filter != "" && !matchesCleanFilter(item, filter) { + continue + } + if craftHasAllItems(item.FirstCraft(), p.HasItem) { + hasHerbs = true + break + } + } + if hasHerbs { + sess.WriteLine("You don't have the Pharmacy level to clean any of your herbs.") + } else { + sess.WriteLine("You don't have any herbs to clean.") + } return } @@ -71,13 +79,13 @@ func (g *Game) doClean(sess *net.Session, input string) { sess.WriteLine("\nYou begin cleaning herbs.") } -func matchesCleanFilter(r action.RecipeDef, filter string) bool { - if len(r.Consume) > 0 && len(r.Consume[0].Items) > 0 { - if strings.Contains(r.Consume[0].Items[0], filter) { +func matchesCleanFilter(item *object.ItemDef, filter string) bool { + if len(item.Craft) > 0 && len(item.FirstCraft().Consume) > 0 && len(item.FirstCraft().Consume[0].Items) > 0 { + if strings.Contains(item.FirstCraft().Consume[0].Items[0], filter) { return true } } - if strings.Contains(r.Output, filter) { + if strings.Contains(item.ID, filter) { return true } return false diff --git a/internal/game/cmd_cook.go b/internal/game/cmd_cook.go index 04c98bb..6dfbcce 100644 --- a/internal/game/cmd_cook.go +++ b/internal/game/cmd_cook.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -23,14 +23,10 @@ func (g *Game) doCook(sess *net.Session, input string) { return } - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - sess.WriteLine("Error loading recipes.") - return - } + items := g.CraftIndex.ByType("cooking") if input == "" { - g.showCookMenu(sess, p, allRecipes, stationDefID, stationName) + g.showCookMenu(sess, p, items, stationDefID, stationName) return } @@ -51,13 +47,11 @@ func (g *Game) doCook(sess *net.Session, input string) { itemID := matches[0].ID - var recipes []action.RecipeDef - for _, r := range allRecipes { - if r.Type != "cooking" { - continue - } - if stationMatch(stationDefID, r.Station) && r.MatchesEntry(itemID) && r.HasAllItems(p.HasItem) { - recipes = append(recipes, r) + var recipes []*object.ItemDef + for _, item := range items { + c := item.FirstCraft() + if stationMatch(stationDefID, c.Station) && craftMatchesEntry(c, itemID) && craftHasAllItems(c, p.HasItem) { + recipes = append(recipes, item) } } @@ -65,25 +59,23 @@ func (g *Game) doCook(sess *net.Session, input string) { g.showRecipeChoice(sess, p, recipes, "You can't cook that.", title, "") } -func (g *Game) showCookMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationDefID, stationName string) { +func (g *Game) showCookMenu(sess *net.Session, p *player.Player, items []*object.ItemDef, stationDefID, stationName string) { seen := make(map[string]bool) - var recipes []action.RecipeDef + var recipes []*object.ItemDef - for _, r := range allRecipes { - if r.Type != "cooking" { - continue - } - if !stationMatch(stationDefID, r.Station) { + for _, item := range items { + c := item.FirstCraft() + if !stationMatch(stationDefID, c.Station) { continue } - if seen[r.ID] { + if seen[item.ID] { continue } - if !r.HasAllItems(p.HasItem) { + if !craftHasAllItems(c, p.HasItem) { continue } - seen[r.ID] = true - recipes = append(recipes, r) + seen[item.ID] = true + recipes = append(recipes, item) } title := fmt.Sprintf("What would you like to cook on the %s?", stationName) diff --git a/internal/game/cmd_craft.go b/internal/game/cmd_craft.go index d061a9c..3290daa 100644 --- a/internal/game/cmd_craft.go +++ b/internal/game/cmd_craft.go @@ -3,8 +3,8 @@ package game import ( "strings" - "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -28,35 +28,27 @@ func (g *Game) doStationSkill(sess *net.Session, input string, recipeType string p := sess.Player g.cancelAction(p) - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - sess.WriteLine("Error loading recipes.") - return - } + items := g.CraftIndex.ByType(recipeType) - var recipes []action.RecipeDef - for _, r := range allRecipes { - if r.Type != recipeType { - continue - } - if !g.recipeVisible(p, &r) { + var filtered []*object.ItemDef + for _, item := range items { + if !g.itemVisible(p, item) { continue } - recipes = append(recipes, r) + filtered = append(filtered, item) } if input != "" { - g.doWithInput(sess, p, recipes, input, skill, flagKey, label, verbCap) + g.doWithInput(sess, p, filtered, input, skill, flagKey, label, verbCap) return } - lastRecipeID, _ := p.Flags[flagKey].(string) - if lastRecipeID != "" { - for i := range recipes { - if recipes[i].ID == lastRecipeID { - r := &recipes[i] - if g.canDoRecipe(p, r, skill) { - g.startRecipe(sess, p, r, 0, flagKey) + lastItemID, _ := p.Flags[flagKey].(string) + if lastItemID != "" { + for _, item := range filtered { + if item.ID == lastItemID { + if g.canDoItem(p, item, skill) { + g.startItem(sess, p, item, 0, flagKey) return } break @@ -64,34 +56,33 @@ func (g *Game) doStationSkill(sess *net.Session, input string, recipeType string } } - available := g.availableRecipes(p, recipes) + available := g.availableItems(p, filtered) if len(available) == 0 { + if recipeType == "pharmacy" && g.hasGrimyHerbs(p) { + sess.WriteLine("Mix what? Grimy herbs? Clean those first!") + return + } sess.WriteLine("You don't have any materials to " + verbPast + ".") return } optionKey := verbPast + "_all" if p.OptionBool(optionKey) && len(available) == 1 { - g.startRecipe(sess, p, &available[0], 0, flagKey) + g.startItem(sess, p, available[0], 0, flagKey) return } g.showProductionTable(sess, p, available, label, recipeType, flagKey, verbCap, false) } -func (g *Game) doWithInput(sess *net.Session, p *player.Player, recipes []action.RecipeDef, input string, skill player.SkillName, flagKey, label, verbCap string) { +func (g *Game) doWithInput(sess *net.Session, p *player.Player, items []*object.ItemDef, input string, skill player.SkillName, flagKey, label, verbCap string) { qty, productName := parseQty(input) productName = strings.TrimSpace(productName) - var matched []action.RecipeDef - for _, r := range recipes { - outDef, _ := g.ItemStore.Load(r.Output) - name := r.Output - if outDef != nil { - name = outDef.Name - } - if action.WordPrefixMatch(productName, name) { - matched = append(matched, r) + var matched []*object.ItemDef + for _, item := range items { + if object.WordPrefixMatch(productName, item.Name) { + matched = append(matched, item) } } @@ -100,10 +91,10 @@ func (g *Game) doWithInput(sess *net.Session, p *player.Player, recipes []action return } - var available []action.RecipeDef - for _, r := range matched { - if g.canDoRecipe(p, &r, skill) { - available = append(available, r) + var available []*object.ItemDef + for _, item := range matched { + if g.canDoItem(p, item, skill) { + available = append(available, item) } } @@ -113,68 +104,85 @@ func (g *Game) doWithInput(sess *net.Session, p *player.Player, recipes []action } if len(available) == 1 { - g.startRecipe(sess, p, &available[0], qty, flagKey) + g.startItem(sess, p, available[0], qty, flagKey) return } g.showProductionTable(sess, p, available, label, label, flagKey, verbCap, false) } -func (g *Game) recipeVisible(p *player.Player, r *action.RecipeDef) bool { - if len(r.Station) > 0 { - stID, _ := g.findStation(p.RoomID, r.Station) - if stID == "" { +func (g *Game) itemVisible(p *player.Player, item *object.ItemDef) bool { + return item.FindCraft(func(c object.CraftDef) bool { + if len(c.Station) > 0 { + stID, _ := g.findStation(p.RoomID, c.Station) + if stID == "" { + return false + } + } + if c.Tool != "" && !g.hasToolType(p, c.Tool) { return false } - } - if r.Tool != "" && !g.hasToolType(p, r.Tool) { - return false - } - return true + return true + }) != nil } -func (g *Game) canDoRecipe(p *player.Player, r *action.RecipeDef, skill player.SkillName) bool { - if p.Level(skill) < r.Level { - return false - } - if !r.HasAllItemsQty(p.CountItem) { - return false - } - if len(r.Station) > 0 { - stID, _ := g.findStation(p.RoomID, r.Station) - if stID == "" { +func (g *Game) canDoItem(p *player.Player, item *object.ItemDef, skill player.SkillName) bool { + m := item.FindCraft(func(c object.CraftDef) bool { + if p.Level(skill) < c.Level { return false } - } - if r.Tool != "" && !g.hasToolType(p, r.Tool) { - return false - } - return true + if !craftHasAllItemsQty(&c, p.CountItem) { + return false + } + if len(c.Station) > 0 { + stID, _ := g.findStation(p.RoomID, c.Station) + if stID == "" { + return false + } + } + if c.Tool != "" && !g.hasToolType(p, c.Tool) { + return false + } + return true + }) + return m != nil } -func (g *Game) availableRecipes(p *player.Player, allRecipes []action.RecipeDef) []action.RecipeDef { - var result []action.RecipeDef - for _, r := range allRecipes { - if r.HasAllItemsQty(p.CountItem) { - result = append(result, r) +func (g *Game) availableItems(p *player.Player, allItems []*object.ItemDef) []*object.ItemDef { + var result []*object.ItemDef + for _, item := range allItems { + m := item.FindCraft(func(c object.CraftDef) bool { + return craftHasAllItemsQty(&c, p.CountItem) + }) + if m != nil { + result = append(result, item) } } return result } -func (g *Game) startRecipe(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int, flagKey string) { +func (g *Game) startItem(sess *net.Session, p *player.Player, item *object.ItemDef, count int, flagKey string) { p.EnsureFlags() - p.Flags[flagKey] = recipe.ID + p.Flags[flagKey] = item.ID g.AccountStore.SaveCharacter(p) - g.startProductionFromRecipe(sess, p, recipe, count) + g.startProductionFromItem(sess, p, item, count) } -func (g *Game) findCraftRecipes(p *player.Player, itemAID, itemBID string) []action.RecipeDef { - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - return nil +func (g *Game) hasGrimyHerbs(p *player.Player) bool { + items := g.CraftIndex.ByType("clean") + for _, item := range items { + if item.FindCraft(func(c object.CraftDef) bool { + return craftHasAllItems(&c, p.HasItem) + }) != nil { + return true + } } + return false +} + +func (g *Game) findCraftItems(p *player.Player, itemAID, itemBID string) []*object.ItemDef { + items := g.CraftIndex.ByType("crafting") toolTypeA := "" toolTypeB := "" @@ -185,19 +193,22 @@ func (g *Game) findCraftRecipes(p *player.Player, itemAID, itemBID string) []act toolTypeB = defB.ToolType } - var matched []action.RecipeDef - for _, r := range allRecipes { - if r.Type != "crafting" { - continue - } - if r.Tool == "" { - continue - } - - if r.Tool == toolTypeA && r.MatchesEntry(itemBID) { - matched = append(matched, r) - } else if r.Tool == toolTypeB && r.MatchesEntry(itemAID) { - matched = append(matched, r) + var matched []*object.ItemDef + for _, item := range items { + match := item.FindCraft(func(c object.CraftDef) bool { + if c.Tool == "" { + return false + } + if c.Tool == toolTypeA && craftMatchesEntry(&c, itemBID) { + return true + } + if c.Tool == toolTypeB && craftMatchesEntry(&c, itemAID) { + return true + } + return false + }) + if match != nil { + matched = append(matched, item) } } return matched diff --git a/internal/game/cmd_fletch.go b/internal/game/cmd_fletch.go index 5a4bc19..088ce63 100644 --- a/internal/game/cmd_fletch.go +++ b/internal/game/cmd_fletch.go @@ -3,9 +3,9 @@ package game import ( "strings" - "thehouseoficarus/internal/action" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -23,34 +23,38 @@ var fletchGemItems = map[string]bool{ "uncut_ruby": true, "uncut_diamond": true, } -func fletchNeedsKnife(r *action.RecipeDef) bool { - if r.Tool == "knife" { - return true - } - for _, e := range r.Consume { - for _, id := range e.Items { - if fletchLogItems[id] { - if strings.Contains(r.Output, "shaft") || strings.Contains(r.Output, "bow") { - return true +func fletchNeedsKnife(item *object.ItemDef) bool { + return item.FindCraft(func(c object.CraftDef) bool { + if c.Tool == "knife" { + return true + } + for _, e := range c.Consume { + for _, id := range e.Items { + if fletchLogItems[id] { + if strings.Contains(item.ID, "shaft") || strings.Contains(item.ID, "bow") { + return true + } } } } - } - return false + return false + }) != nil } -func fletchNeedsChisel(r *action.RecipeDef) bool { - if r.Tool == "chisel" { - return true - } - for _, e := range r.Consume { - for _, id := range e.Items { - if fletchGemItems[id] { - return true +func fletchNeedsChisel(item *object.ItemDef) bool { + return item.FindCraft(func(c object.CraftDef) bool { + if c.Tool == "chisel" { + return true + } + for _, e := range c.Consume { + for _, id := range e.Items { + if fletchGemItems[id] { + return true + } } } - } - return false + return false + }) != nil } func (g *Game) doFletch(sess *net.Session, input string) { @@ -66,32 +70,19 @@ func (g *Game) doFletch(sess *net.Session, input string) { p.ActionState = nil } - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - sess.WriteLine("Error loading recipes.") - return - } - - var fletchRecipes []action.RecipeDef - for _, r := range allRecipes { - if r.Type != "fletching" { - continue - } - fletchRecipes = append(fletchRecipes, r) - } + items := g.CraftIndex.ByType("fletching") if input != "" { - g.doFletchWithInput(sess, p, fletchRecipes, input) + g.doFletchWithInput(sess, p, items, input) return } - lastRecipeID, _ := p.Flags["last_fletch"].(string) - if lastRecipeID != "" { - for i := range fletchRecipes { - if fletchRecipes[i].ID == lastRecipeID { - r := &fletchRecipes[i] - if g.canDoFletchRecipe(p, r) { - g.startFletchAction(sess, p, r, 0) + lastItemID, _ := p.Flags["last_fletch"].(string) + if lastItemID != "" { + for _, item := range items { + if item.ID == lastItemID { + if g.canDoFletchItem(p, item) { + g.startFletchAction(sess, p, item, 0) return } break @@ -99,33 +90,28 @@ func (g *Game) doFletch(sess *net.Session, input string) { } } - available := g.availableFletchRecipes(p, fletchRecipes) + available := g.availableFletchItems(p, items) if len(available) == 0 { sess.WriteLine("You don't have any materials to fletch.") return } if p.OptionBool("fletch_all") && len(available) == 1 { - g.startFletchAction(sess, p, &available[0], 0) + g.startFletchAction(sess, p, available[0], 0) return } g.showProductionTable(sess, p, available, "Fletching", "fletching", "last_fletch", "Fletch", true) } -func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, fletchRecipes []action.RecipeDef, input string) { +func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, items []*object.ItemDef, input string) { qty, productName := parseQty(input) productName = strings.TrimSpace(productName) - var matched []action.RecipeDef - for _, r := range fletchRecipes { - outDef, _ := g.ItemStore.Load(r.Output) - name := r.Output - if outDef != nil { - name = outDef.Name - } - if action.WordPrefixMatch(productName, name) { - matched = append(matched, r) + var matched []*object.ItemDef + for _, item := range items { + if object.WordPrefixMatch(productName, item.Name) { + matched = append(matched, item) } } @@ -134,10 +120,10 @@ func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, fletchReci return } - var available []action.RecipeDef - for _, r := range matched { - if g.canDoFletchRecipe(p, &r) { - available = append(available, r) + var available []*object.ItemDef + for _, item := range matched { + if g.canDoFletchItem(p, item) { + available = append(available, item) } } @@ -147,48 +133,49 @@ func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, fletchReci } if len(available) == 1 { - count := qty - g.startFletchAction(sess, p, &available[0], count) + g.startFletchAction(sess, p, available[0], qty) return } g.showProductionTable(sess, p, available, "Fletching", "fletching", "last_fletch", "Fletch", true) } -func (g *Game) hasFletchTools(p *player.Player, r *action.RecipeDef) bool { - if r.Tool != "" { - return g.hasToolType(p, r.Tool) +func (g *Game) hasFletchTools(p *player.Player, item *object.ItemDef) bool { + c := item.FirstCraft() + if c.Tool != "" { + return g.hasToolType(p, c.Tool) } - if fletchNeedsKnife(r) && !g.hasToolType(p, "knife") { + if fletchNeedsKnife(item) && !g.hasToolType(p, "knife") { return false } - if fletchNeedsChisel(r) && !g.hasToolType(p, "chisel") { + if fletchNeedsChisel(item) && !g.hasToolType(p, "chisel") { return false } return true } -func (g *Game) canDoFletchRecipe(p *player.Player, r *action.RecipeDef) bool { - return p.Level(player.Fletching) >= r.Level && - r.HasAllItemsQty(p.CountItem) && - g.hasFletchTools(p, r) +func (g *Game) canDoFletchItem(p *player.Player, item *object.ItemDef) bool { + return item.FindCraft(func(c object.CraftDef) bool { + return p.Level(player.Fletching) >= c.Level && + craftHasAllItemsQty(&c, p.CountItem) + }) != nil && g.hasFletchTools(p, item) } -func (g *Game) availableFletchRecipes(p *player.Player, allFletch []action.RecipeDef) []action.RecipeDef { - var result []action.RecipeDef - for _, r := range allFletch { - if r.HasAllItemsQty(p.CountItem) && g.hasFletchTools(p, &r) { - result = append(result, r) +func (g *Game) availableFletchItems(p *player.Player, allItems []*object.ItemDef) []*object.ItemDef { + var result []*object.ItemDef + for _, item := range allItems { + match := item.FindCraft(func(c object.CraftDef) bool { + return craftHasAllItemsQty(&c, p.CountItem) + }) + if match != nil && g.hasFletchTools(p, item) { + result = append(result, item) } } return result } -func (g *Game) findFletchRecipes(p *player.Player, itemAID, itemBID string) []action.RecipeDef { - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - return nil - } +func (g *Game) findFletchItems(p *player.Player, itemAID, itemBID string) []*object.ItemDef { + items := g.CraftIndex.ByType("fletching") toolTypeA := "" toolTypeB := "" @@ -201,26 +188,24 @@ func (g *Game) findFletchRecipes(p *player.Player, itemAID, itemBID string) []ac isToolA := toolTypeA == "knife" || toolTypeA == "chisel" isToolB := toolTypeB == "knife" || toolTypeB == "chisel" - var matched []action.RecipeDef - for _, r := range allRecipes { - if r.Type != "fletching" { - continue - } + var matched []*object.ItemDef + for _, item := range items { + c := item.FirstCraft() - if r.Tool != "" { - if r.Tool == toolTypeA && r.MatchesEntry(itemBID) { - matched = append(matched, r) + if c.Tool != "" { + if c.Tool == toolTypeA && craftMatchesEntry(c, itemBID) { + matched = append(matched, item) continue } - if r.Tool == toolTypeB && r.MatchesEntry(itemAID) { - matched = append(matched, r) + if c.Tool == toolTypeB && craftMatchesEntry(c, itemAID) { + matched = append(matched, item) continue } } aMatch := false bMatch := false - for _, e := range r.Consume { + for _, e := range c.Consume { for _, id := range e.Items { if id == itemAID { aMatch = true @@ -232,21 +217,21 @@ func (g *Game) findFletchRecipes(p *player.Player, itemAID, itemBID string) []ac } if aMatch && bMatch { - matched = append(matched, r) + matched = append(matched, item) continue } if isToolA && bMatch { - if (fletchNeedsKnife(&r) && toolTypeA == "knife") || - (fletchNeedsChisel(&r) && toolTypeA == "chisel") { - matched = append(matched, r) + if (fletchNeedsKnife(item) && toolTypeA == "knife") || + (fletchNeedsChisel(item) && toolTypeA == "chisel") { + matched = append(matched, item) continue } } if isToolB && aMatch { - if (fletchNeedsKnife(&r) && toolTypeB == "knife") || - (fletchNeedsChisel(&r) && toolTypeB == "chisel") { - matched = append(matched, r) + if (fletchNeedsKnife(item) && toolTypeB == "knife") || + (fletchNeedsChisel(item) && toolTypeB == "chisel") { + matched = append(matched, item) continue } } diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go index 2742151..c6b7831 100644 --- a/internal/game/cmd_shop.go +++ b/internal/game/cmd_shop.go @@ -158,7 +158,7 @@ func (g *Game) findShopMatches(input string, cfg *action.ShopConfig) []shopMatch if def != nil { itemName = def.Name } - if action.WordPrefixMatch(input, itemName) { + if object.WordPrefixMatch(input, itemName) { matches = append(matches, shopMatch{si: si, name: itemName, def: def}) } } diff --git a/internal/game/cmd_smelt.go b/internal/game/cmd_smelt.go index f1ff4e2..8ad2391 100644 --- a/internal/game/cmd_smelt.go +++ b/internal/game/cmd_smelt.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -23,14 +23,10 @@ func (g *Game) doSmelt(sess *net.Session, input string) { return } - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - sess.WriteLine("Error loading recipes.") - return - } + items := g.CraftIndex.ByType("smelting") if input == "" { - g.showSmeltMenu(sess, p, allRecipes, stationDefID, stationName) + g.showSmeltMenu(sess, p, items, stationDefID, stationName) return } @@ -48,33 +44,35 @@ func (g *Game) doSmelt(sess *net.Session, input string) { itemID := matches[0].ID - var recipes []action.RecipeDef - for _, r := range allRecipes { - if r.Type != "smelting" || !stationMatch(stationDefID, r.Station) || !r.MatchesEntry(itemID) { + var recipes []*object.ItemDef + for _, item := range items { + c := item.FirstCraft() + if !stationMatch(stationDefID, c.Station) || !craftMatchesEntry(c, itemID) { continue } - if !r.HasAllItemsQty(p.CountItem) || p.Level(player.SkillName(r.EffectiveSkill())) < r.Level { + if !craftHasAllItemsQty(c, p.CountItem) || p.Level(player.SkillName(c.EffectiveSkill())) < c.Level { continue } - recipes = append(recipes, r) + recipes = append(recipes, item) } g.showRecipeChoice(sess, p, recipes, "You can't smelt that.", "What would you like to smelt?", "") } -func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationDefID, stationName string) { +func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, items []*object.ItemDef, stationDefID, stationName string) { seen := make(map[string]bool) - var recipes []action.RecipeDef + var recipes []*object.ItemDef - for _, r := range allRecipes { - if r.Type != "smelting" || !stationMatch(stationDefID, r.Station) { + for _, item := range items { + c := item.FirstCraft() + if !stationMatch(stationDefID, c.Station) { continue } - if seen[r.ID] || !r.HasAllItemsQty(p.CountItem) || p.Level(player.SkillName(r.EffectiveSkill())) < r.Level { + if seen[item.ID] || !craftHasAllItemsQty(c, p.CountItem) || p.Level(player.SkillName(c.EffectiveSkill())) < c.Level { continue } - seen[r.ID] = true - recipes = append(recipes, r) + seen[item.ID] = true + recipes = append(recipes, item) } g.showRecipeChoice(sess, p, recipes, "You don't have anything you can smelt.", "What would you like to smelt?", "smelt_all") diff --git a/internal/game/cmd_smith.go b/internal/game/cmd_smith.go index c7a32cd..712c03c 100644 --- a/internal/game/cmd_smith.go +++ b/internal/game/cmd_smith.go @@ -5,8 +5,8 @@ import ( "sort" "strings" - "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -29,11 +29,7 @@ func (g *Game) doSmith(sess *net.Session, input string) { return } - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - sess.WriteLine("Error loading recipes.") - return - } + items := g.CraftIndex.ByType("smithing") if input != "" { matches := g.findInventoryMatches(input, p) @@ -47,15 +43,15 @@ func (g *Game) doSmith(sess *net.Session, input string) { return } barID := matches[0].ID - if !hasSmithingRecipes(allRecipes, barID) { + if !hasSmithingItems(items, barID) { sess.WriteLine("You can't smith that.") return } - g.showSmithTable(sess, p, barID, allRecipes) + g.showSmithTable(sess, p, barID) return } - barTypes := findSmithableBars(allRecipes, p) + barTypes := findSmithableBars(items, p) if len(barTypes) == 0 { sess.WriteLine("You don't have any bars to smith.") return @@ -63,12 +59,12 @@ func (g *Game) doSmith(sess *net.Session, input string) { if len(barTypes) == 1 { if p.OptionBool("smith_all") { - if recipe := g.findSmithRecipe(p, barTypes[0], allRecipes); recipe != nil { - g.startProductionFromRecipe(sess, p, recipe, 0) + if item := g.findSmithItem(p, barTypes[0], items); item != nil { + g.startProductionFromItem(sess, p, item, 0) return } } - g.showSmithTable(sess, p, barTypes[0], allRecipes) + g.showSmithTable(sess, p, barTypes[0]) return } @@ -86,17 +82,18 @@ func (g *Game) doSmith(sess *net.Session, input string) { g.showMenuTable(sess, "Which bars would you like to smith?", names) } -func (g *Game) showSmithTable(sess *net.Session, p *player.Player, barID string, allRecipes []action.RecipeDef) { +func (g *Game) showSmithTable(sess *net.Session, p *player.Player, barID string) { barDef, _ := g.ItemStore.Load(barID) barName := barID if barDef != nil { barName = barDef.Name } - var recipes []action.RecipeDef - for _, r := range allRecipes { - if r.Type == "smithing" && r.MatchesEntry(barID) { - recipes = append(recipes, r) + allItems := g.CraftIndex.ByType("smithing") + var recipes []*object.ItemDef + for _, item := range allItems { + if craftMatchesEntry(item.FirstCraft(), barID) { + recipes = append(recipes, item) } } @@ -109,22 +106,19 @@ func (g *Game) showSmithTable(sess *net.Session, p *player.Player, barID string, g.showProductionTable(sess, p, recipes, titleCase(barName)+" Smithing", "smithing", lastFlag, "Produce", false) } -func hasSmithingRecipes(allRecipes []action.RecipeDef, barID string) bool { - for _, r := range allRecipes { - if r.Type == "smithing" && r.MatchesEntry(barID) { +func hasSmithingItems(items []*object.ItemDef, barID string) bool { + for _, item := range items { + if craftMatchesEntry(item.FirstCraft(), barID) { return true } } return false } -func findSmithableBars(allRecipes []action.RecipeDef, p *player.Player) []string { +func findSmithableBars(items []*object.ItemDef, p *player.Player) []string { barSet := make(map[string]bool) - for _, r := range allRecipes { - if r.Type != "smithing" { - continue - } - for _, e := range r.Consume { + for _, item := range items { + for _, e := range item.FirstCraft().Consume { for _, id := range e.Items { if p.HasItem(id) { barSet[id] = true @@ -148,15 +142,16 @@ func barMenuData(barIDs []string) []map[string]string { return out } -func (g *Game) findSmithRecipe(p *player.Player, barID string, allRecipes []action.RecipeDef) *action.RecipeDef { - var makeable []*action.RecipeDef +func (g *Game) findSmithItem(p *player.Player, barID string, items []*object.ItemDef) *object.ItemDef { + var makeable []*object.ItemDef skillLevel := p.Level(player.Smithing) - for i, r := range allRecipes { - if r.Type != "smithing" || !r.MatchesEntry(barID) { + for _, item := range items { + c := item.FirstCraft() + if !craftMatchesEntry(c, barID) { continue } - if skillLevel >= r.Level && r.HasAllItemsQty(p.CountItem) { - makeable = append(makeable, &allRecipes[i]) + if skillLevel >= c.Level && craftHasAllItemsQty(c, p.CountItem) { + makeable = append(makeable, item) } } if len(makeable) == 1 { diff --git a/internal/game/cmd_trigger_utility.go b/internal/game/cmd_trigger_utility.go index ce59975..69abc34 100644 --- a/internal/game/cmd_trigger_utility.go +++ b/internal/game/cmd_trigger_utility.go @@ -6,6 +6,7 @@ import ( "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" ) @@ -169,18 +170,25 @@ func (g *Game) triggerSuperheat(sess *net.Session, p *player.Player, mod *ModDef return } - recipe := g.RecipeStore.FindByInput("smelt", inv.ItemID) - if recipe == nil { + var match *object.ItemDef + for _, item := range g.CraftIndex.ByType("smelting") { + if craftMatchesEntry(item.FirstCraft(), inv.ItemID) { + match = item + break + } + } + if match == nil { sess.WriteLine("You can't superheat that.") return } + c := match.FirstCraft() - if recipe.Level > 0 && p.Level(player.SkillName(recipe.Skill)) < recipe.Level { - sess.WriteLine(fmt.Sprintf("You need level %d %s to smelt that.", recipe.Level, recipe.Skill)) + if c.Level > 0 && p.Level(player.SkillName(c.Skill)) < c.Level { + sess.WriteLine(fmt.Sprintf("You need level %d %s to smelt that.", c.Level, c.Skill)) return } - for _, e := range recipe.Consume { + for _, e := range c.Consume { for _, itemID := range e.Items { qty := e.Quantity if qty <= 0 { @@ -202,24 +210,16 @@ func (g *Game) triggerSuperheat(sess *net.Session, p *player.Player, mod *ModDef g.cancelAction(p) } - for _, e := range recipe.Consume { - for _, itemID := range e.Items { - qty := e.Quantity - if qty <= 0 { - qty = 1 - } - p.RemoveItem(itemID, qty) - } - } + craftConsumeAll(c, p.HasItem, p.RemoveItem) - outputQty := recipe.OutputQty + outputQty := c.OutputQty if outputQty <= 0 { outputQty = 1 } placed := false for i := 0; i < 28; i++ { slot := p.InvSlot(i) - if slot != nil && slot.ItemID == recipe.Output { + if slot != nil && slot.ItemID == match.ID { slot.Quantity += outputQty placed = true break @@ -227,20 +227,16 @@ func (g *Game) triggerSuperheat(sess *net.Session, p *player.Player, mod *ModDef } if !placed { freeSlot := p.FirstFreeSlot() - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: outputQty}) + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: match.ID, Quantity: outputQty}) } var extraGains []xpGain - if recipe.XP > 0 { - extraGains = []xpGain{{recipe.Skill, recipe.XP}} + if c.XP > 0 { + extraGains = []xpGain{{c.EffectiveSkill(), c.XP}} } g.triggerModReward(sess, p, mod, 1.0, extraGains...) - outputDef, _ := g.ItemStore.Load(recipe.Output) - outputName := recipe.Output - if outputDef != nil { - outputName = outputDef.Name - } + outputName := match.Name inputDef, _ := g.ItemStore.Load(inv.ItemID) inputName := inv.ItemID if inputDef != nil { diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go index e653c93..640e28b 100644 --- a/internal/game/cmd_use.go +++ b/internal/game/cmd_use.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" @@ -116,57 +115,42 @@ func (g *Game) useRoomObject(sess *net.Session, p *player.Player, input string) return true } -func (g *Game) stationRecipes(p *player.Player, stationDefID string) []action.RecipeDef { - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - return nil - } - var results []action.RecipeDef - for _, r := range allRecipes { - if len(r.Station) == 0 { - continue - } - stationMatch := false - for _, sid := range r.Station { - if sid == stationDefID { - stationMatch = true - break - } - } - if !stationMatch { +func (g *Game) stationRecipes(p *player.Player, stationDefID string) []*object.ItemDef { + items := g.CraftIndex.ByStation(stationDefID) + var results []*object.ItemDef + for _, item := range items { + c := item.FirstCraft() + if !craftHasAllItemsQty(c, p.CountItem) { continue } - if !r.HasAllItemsQty(p.CountItem) { + skillName := player.SkillName(c.EffectiveSkill()) + if p.Level(skillName) < c.Level { continue } - skillName := player.SkillName(r.EffectiveSkill()) - if p.Level(skillName) < r.Level { - continue - } - if r.Tool != "" && !g.hasToolType(p, r.Tool) { + if c.Tool != "" && !g.hasToolType(p, c.Tool) { continue } if stationDefID == "anvil" && !g.hasToolType(p, "hammer") { continue } - results = append(results, r) + results = append(results, item) } return results } -func (g *Game) showRecipeMenu(sess *net.Session, p *player.Player, recipes []action.RecipeDef, stationName string) { - if len(recipes) == 0 { +func (g *Game) showRecipeMenu(sess *net.Session, p *player.Player, items []*object.ItemDef, stationName string) { + if len(items) == 0 { sess.WriteLine(fmt.Sprintf("You don't have anything you can make at the %s.", stationName)) return } - if len(recipes) == 1 { - g.promptHowMany(sess, recipes[0].ID) + if len(items) == 1 { + g.promptHowMany(sess, items[0].ID) return } needTypes := false - for i := range recipes { - for j := i + 1; j < len(recipes); j++ { - if recipes[i].Type != recipes[j].Type { + for i := range items { + for j := i + 1; j < len(items); j++ { + if len(items[i].Craft) > 0 && len(items[j].Craft) > 0 && items[i].FirstCraft().Type != items[j].FirstCraft().Type { needTypes = true break } @@ -175,17 +159,23 @@ func (g *Game) showRecipeMenu(sess *net.Session, p *player.Player, recipes []act break } } - menu := recipeMenuData(recipeDefIDs(recipes)) + menu := itemMenuData(itemIDs(items)) sess.PendingMenu = menu sess.State = net.StateRecipeChoice var names []string - for _, r := range recipes { - name := g.recipeName(sess, &r) + for _, item := range items { + cType := "" + if len(item.Craft) > 0 { + cType = item.FirstCraft().Type + } + if cType == "" { + cType = "combine" + } + typeLabel := strings.ToUpper(cType[:1]) + cType[1:] if needTypes { - typeLabel := strings.ToUpper(r.Type[:1]) + r.Type[1:] - names = append(names, fmt.Sprintf("%s (%s)", name, typeLabel)) + names = append(names, fmt.Sprintf("%s (%s)", g.itemName(sess, item), typeLabel)) } else { - names = append(names, name) + names = append(names, g.itemName(sess, item)) } } g.showMenuTable(sess, fmt.Sprintf("What would you like to make at the %s?", stationName), names) @@ -217,23 +207,28 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, return } - stationIDs := []string{def.ID} - recipes, err := g.RecipeStore.FindByStation(itemAID, stationIDs, "") - if err == nil && len(recipes) > 0 { - if recipes[0].Type == "smelting" { - filtered := recipes[:0:0] - for _, r := range recipes { - if r.Type != "smelting" { + recipes := g.CraftIndex.FindByStationInput(def.ID, itemAID) + + if len(recipes) > 0 { + cType := "" + if len(recipes[0].Craft) > 0 { + cType = recipes[0].FirstCraft().Type + } + if cType == "smelting" { + var filtered []*object.ItemDef + for _, item := range recipes { + c := item.FirstCraft() + if c.Type != "smelting" { continue } - if !r.HasAllItemsQty(p.CountItem) { + if !craftHasAllItemsQty(c, p.CountItem) { continue } - skillLevel := p.Level(player.SkillName(r.EffectiveSkill())) - if skillLevel < r.Level { + skillLevel := p.Level(player.SkillName(c.EffectiveSkill())) + if skillLevel < c.Level { continue } - filtered = append(filtered, r) + filtered = append(filtered, item) } if len(filtered) == 0 { sess.WriteLine("You can't smelt that here.") @@ -243,36 +238,35 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, g.promptHowMany(sess, filtered[0].ID) return } - sess.PendingMenu = recipeMenuData(recipeDefIDs(filtered)) - sess.State = net.StateRecipeChoice - var names []string - for _, r := range filtered { - names = append(names, g.recipeName(sess, &r)) - } - g.showMenuTable(sess, "What would you like to smelt?", names) - return + sess.PendingMenu = itemMenuData(itemIDs(filtered)) + sess.State = net.StateRecipeChoice + var names []string + for _, item := range filtered { + names = append(names, g.itemName(sess, item)) + } + g.showMenuTable(sess, "What would you like to smelt?", names) + return } - if recipes[0].Type == "smithing" { + if cType == "smithing" { if !g.hasToolType(p, "hammer") { sess.WriteLine("You need a hammer to smith.") return } - allRecipes, _ := g.RecipeStore.LoadAll() - g.showSmithTable(sess, p, itemAID, allRecipes) + g.showSmithTable(sess, p, itemAID) return } if len(recipes) == 1 { g.promptHowMany(sess, recipes[0].ID) return } - sess.PendingMenu = recipeMenuData(recipeDefIDs(recipes)) - sess.State = net.StateRecipeChoice - var names []string - for _, r := range recipes { - names = append(names, g.recipeName(sess, &r)) - } - g.showMenuTable(sess, fmt.Sprintf("What would you like to make with %s on the %s?", itemAName, def.Name), names) - return + sess.PendingMenu = itemMenuData(itemIDs(recipes)) + sess.State = net.StateRecipeChoice + var names []string + for _, item := range recipes { + names = append(names, g.itemName(sess, item)) + } + g.showMenuTable(sess, fmt.Sprintf("What would you like to make with %s on the %s?", itemAName, def.Name), names) + return } for _, ui := range def.UseInteractions { @@ -314,29 +308,19 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, itemBID := matchesB[0].ID - craftRecipes := g.findCraftRecipes(p, itemAID, itemBID) - fletchRecipes := g.findFletchRecipes(p, itemAID, itemBID) + craftItems := g.findCraftItems(p, itemAID, itemBID) + fletchItems := g.findFletchItems(p, itemAID, itemBID) - if len(craftRecipes) > 0 && len(fletchRecipes) > 0 { + if len(craftItems) > 0 && len(fletchItems) > 0 { var menu []map[string]string var names []string - for _, r := range craftRecipes { - menu = append(menu, map[string]string{"recipe_id": r.ID}) - outDef, _ := g.ItemStore.Load(r.Output) - name := r.Output - if outDef != nil { - name = outDef.Name - } - names = append(names, fmt.Sprintf("%s (Crafting)", name)) + for _, item := range craftItems { + menu = append(menu, map[string]string{"item_id": item.ID}) + names = append(names, fmt.Sprintf("%s (Crafting)", item.Name)) } - for _, r := range fletchRecipes { - menu = append(menu, map[string]string{"fletch_recipe_id": r.ID}) - outDef, _ := g.ItemStore.Load(r.Output) - name := r.Output - if outDef != nil { - name = outDef.Name - } - names = append(names, fmt.Sprintf("%s (Fletching)", name)) + for _, item := range fletchItems { + menu = append(menu, map[string]string{"item_id": item.ID}) + names = append(names, fmt.Sprintf("%s (Fletching)", item.Name)) } sess.PendingMenu = menu sess.State = net.StateRecipeChoice @@ -344,15 +328,15 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, return } - if len(craftRecipes) > 0 { - var available []action.RecipeDef - for _, r := range craftRecipes { - if g.canDoRecipe(p, &r, player.Crafting) { - available = append(available, r) + if len(craftItems) > 0 { + var available []*object.ItemDef + for _, item := range craftItems { + if g.canDoItem(p, item, player.Crafting) { + available = append(available, item) } } if len(available) == 1 { - g.startRecipe(sess, p, &available[0], 0, "last_craft") + g.startItem(sess, p, available[0], 0, "last_craft") return } if len(available) > 1 { @@ -361,15 +345,15 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, } } - if len(fletchRecipes) > 0 { - var available []action.RecipeDef - for _, r := range fletchRecipes { - if g.canDoFletchRecipe(p, &r) { - available = append(available, r) + if len(fletchItems) > 0 { + var available []*object.ItemDef + for _, item := range fletchItems { + if g.canDoFletchItem(p, item) { + available = append(available, item) } } if len(available) == 1 { - g.startFletchAction(sess, p, &available[0], 0) + g.startFletchAction(sess, p, available[0], 0) return } if len(available) > 1 { @@ -378,22 +362,33 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, } } - matched := g.findCombineResults(sess, p, itemAID, itemBID) - if len(matched) == 1 { - g.promptHowMany(sess, "combine_"+matched[0].ID) + matched := g.CraftIndex.FindByTwoInputs(itemAID, itemBID) + var matchable []*object.ItemDef + for _, item := range matched { + if craftHasAllItemsQty(item.FirstCraft(), p.CountItem) { + matchable = append(matchable, item) + } + } + if len(matchable) == 1 { + g.promptHowMany(sess, matchable[0].ID) return } - if len(matched) > 1 { - sess.PendingMenu = combineMenuData(matched) + if len(matchable) > 1 { + sess.PendingMenu = itemMenuData(itemIDs(matchable)) sess.State = net.StateRecipeChoice var names []string - for _, def := range matched { - names = append(names, g.itemColorize(sess, def, def.Name)) + for _, item := range matchable { + names = append(names, g.itemName(sess, item)) } g.showMenuTable(sess, "What would you like to make?", names) return } + if g.isGrimyHerb(itemAID) && itemBID == "vial_of_water" { + sess.WriteLine("Don't put a grimy herb in there! Clean it first!") + return + } + sess.WriteLine("You can't combine those items.") return } @@ -401,55 +396,16 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, sess.WriteLine(fmt.Sprintf("There's no '%s' here to use that on.", itemBName)) } -func (g *Game) findCombineResults(sess *net.Session, p *player.Player, itemAID, itemBID string) []*object.ItemDef { - items, err := g.ItemStore.LoadAll() - if err != nil { - return nil - } - var matched []*object.ItemDef - for _, def := range items { - if len(def.MadeFrom) == 0 { - continue - } - aEntry := -1 - bEntry := -1 - for ei, entry := range def.MadeFrom { - for _, id := range entry.Items { - if id == itemAID && aEntry < 0 { - aEntry = ei - } - if id == itemBID && bEntry < 0 { - bEntry = ei +func (g *Game) isGrimyHerb(itemID string) bool { + items := g.CraftIndex.ByType("clean") + for _, item := range items { + for _, e := range item.FirstCraft().Consume { + for _, id := range e.Items { + if id == itemID { + return true } } } - if aEntry >= 0 && bEntry >= 0 && aEntry != bEntry && madeFromAvailable(p, def.MadeFrom) { - matched = append(matched, def) - } } - return matched -} - -func combineMenuData(defs []*object.ItemDef) []map[string]string { - out := make([]map[string]string, len(defs)) - for i, d := range defs { - out[i] = map[string]string{"combine_item": d.ID} - } - return out -} - -func madeFromAvailable(p *player.Player, madeFrom []object.MadeFromEntry) bool { - for _, entry := range madeFrom { - found := false - for _, id := range entry.Items { - if p.HasItem(id) { - found = true - break - } - } - if !found { - return false - } - } - return true + return false } diff --git a/internal/game/cmd_verbs.go b/internal/game/cmd_verbs.go index 7801903..f66dc74 100644 --- a/internal/game/cmd_verbs.go +++ b/internal/game/cmd_verbs.go @@ -88,12 +88,12 @@ func (g *Game) executeVerbs(sess *net.Session, args []string, rawInput string) { if len(recipes) > 0 { addUnique(§ionObjs, &seen, "use "+name) for _, r := range recipes { - if info, ok := productionTypes[r.Type]; ok { + if info, ok := productionTypes[r.FirstCraft().Type]; ok { if info.ActionType != "combine" && info.ActionType != "fletch" && info.ActionType != "mix" { addUnique(§ionObjs, &seen, info.ActionType) } } - for _, c := range r.Consume { + for _, c := range r.FirstCraft().Consume { for _, itemID := range c.Items { if itemDef, err := g.ItemStore.Load(itemID); err == nil { addUnique(§ionObjs, &seen, "use "+itemDef.Name+" on "+name) diff --git a/internal/game/core_production.go b/internal/game/core_production.go index 59517d5..4aecc54 100644 --- a/internal/game/core_production.go +++ b/internal/game/core_production.go @@ -13,47 +13,61 @@ import ( "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" - ) -func (g *Game) startProduction(sess *net.Session, p *player.Player, recipe *action.RecipeDef, actionType, displayVerb, startMsg, endMsg string, count int) { +func craftFirstItemName(c *object.CraftDef, load func(string) (string, bool)) string { + if c == nil { + return "" + } + for _, e := range c.Consume { + for _, id := range e.Items { + if name, ok := load(id); ok { + return name + } + return id + } + } + return "" +} + +func (g *Game) startProduction(sess *net.Session, p *player.Player, item *object.ItemDef, craft *object.CraftDef, actionType, displayVerb, startMsg, endMsg string, count int) { g.cancelAction(p) g.cancelBgAction(p) - skill := recipe.EffectiveSkill() + skill := craft.EffectiveSkill() if skill != "" { skillLevel := p.Level(player.SkillName(skill)) - if skillLevel < recipe.Level { - sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", recipe.Level, skill)) + if skillLevel < craft.Level { + sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", craft.Level, skill)) g.reprompt(sess) return } } - if !recipe.HasAllItemsQty(p.CountItem) { + if !craftHasAllItemsQty(craft, p.CountItem) { sess.WriteLine("You don't have the required materials.") g.reprompt(sess) return } - outDef, _ := g.ItemStore.Load(recipe.Output) + outDef, _ := g.ItemStore.Load(item.ID) - wait := recipe.Wait + wait := craft.Wait if wait <= 0 { wait = 4 } - outputName := recipe.Output + outputName := item.ID if outDef != nil { outputName = outDef.Name } p.Action = &action.Action{ Type: actionType, - TargetID: recipe.ID, + TargetID: item.ID, TargetName: outputName, Data: map[string]any{ - "recipe_id": recipe.ID, + "item_id": item.ID, "phase": 0, "wait": wait, "start_msg": startMsg, @@ -68,13 +82,18 @@ func (g *Game) startProduction(sess *net.Session, p *player.Player, recipe *acti g.broadcastAction(sess, "\n%s starts %s.", p.Name, displayVerb) } -func (g *Game) startProductionFromRecipe(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) { - _, stationName := g.findStation(p.RoomID, recipe.Station) +func (g *Game) startProductionFromItem(sess *net.Session, p *player.Player, item *object.ItemDef, count int) { + craft := item.FirstCraft() + if craft == nil { + return + } + + _, stationName := g.findStation(p.RoomID, craft.Station) if stationName == "" { stationName = "inventory" } - firstItem := recipe.FirstItemName(func(id string) (string, bool) { + firstItem := craftFirstItemName(craft, func(id string) (string, bool) { def, err := g.ItemStore.Load(id) if err != nil { return id, false @@ -82,9 +101,9 @@ func (g *Game) startProductionFromRecipe(sess *net.Session, p *player.Player, re return def.Name, true }) - info, ok := productionTypes[recipe.Type] + info, ok := productionTypes[craft.Type] if !ok { - info = productionTypeInfo{recipe.Type, recipe.Type} + info = productionTypeInfo{craft.Type, craft.Type} } actionType := info.ActionType displayVerb := info.DisplayVerb @@ -97,67 +116,79 @@ func (g *Game) startProductionFromRecipe(sess *net.Session, p *player.Player, re } endMsg = fmt.Sprintf("You've finished %s.", displayVerb) - g.startProduction(sess, p, recipe, actionType, displayVerb, startMsg, endMsg, count) + g.startProduction(sess, p, item, craft, actionType, displayVerb, startMsg, endMsg, count) } -func (g *Game) loadRecipe(recipeID string) *action.RecipeDef { - if r := g.RecipeStore.GetByID(recipeID); r != nil { - return r +func (g *Game) loadCraftItem(itemID string) *object.ItemDef { + item, err := g.ItemStore.Load(itemID) + if err != nil || len(item.Craft) == 0 { + return nil } - if strings.HasPrefix(recipeID, "combine_") { - itemID := strings.TrimPrefix(recipeID, "combine_") - if itemDef, err := g.ItemStore.Load(itemID); err == nil && len(itemDef.MadeFrom) > 0 { - return g.buildCombineRecipe(itemDef) - } - } - return nil + return item } func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { - recipeID := p.Action.Data["recipe_id"].(string) + itemID := p.Action.Data["item_id"].(string) phase := p.Action.Data["phase"].(int) wait := p.Action.Data["wait"].(float64) startMsg, _ := p.Action.Data["start_msg"].(string) endMsg, _ := p.Action.Data["end_msg"].(string) remaining, _ := p.Action.Data["remaining"].(int) - recipe := g.loadRecipe(recipeID) - if recipe == nil { + item := g.loadCraftItem(itemID) + if item == nil { g.cancelAction(p) return false } + craft := item.FirstCraft() if phase == 0 { if startMsg != "" { sess.WriteLine(fmt.Sprintf("\n%s", startMsg)) } + if len(craft.Steps) > 0 { + p.Action.Data["next_step_index"] = 0 + p.Action.Data["steps_accumulated_ticks"] = float64(0) + } p.Action.Data["phase"] = 1 p.Action.WaitLeft = engine.ToTicks(wait) return true } - skill := recipe.EffectiveSkill() + if stepsIdx, ok := p.Action.Data["next_step_index"].(int); ok && stepsIdx < len(craft.Steps) { + accTicks, _ := p.Action.Data["steps_accumulated_ticks"].(float64) + accTicks += wait + p.Action.Data["steps_accumulated_ticks"] = accTicks + for i := stepsIdx; i < len(craft.Steps); i++ { + if accTicks >= craft.Steps[i].Tick { + sess.WriteLine(craft.Steps[i].Message) + p.Action.Data["next_step_index"] = i + 1 + } + } + } + + skill := craft.EffectiveSkill() skillLevel := p.Level(player.SkillName(skill)) chance := 1.0 - if recipe.Success != nil { - chance = action.SuccessChance(*recipe.Success, skillLevel, recipe.Level) + if craft.Success != nil { + chance = action.SuccessChance(action.SuccessFormula(*craft.Success), skillLevel, craft.Level) } if rand.Float64() < chance { - outputQty := recipe.OutputQty + outputQty := craft.OutputQty if outputQty <= 0 { outputQty = 1 } - byproducts := g.collectByproducts(p, recipe) + byproducts := g.collectByproducts(p, item) placed := false - outDef, _ := g.ItemStore.Load(recipe.Output) + 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 == recipe.Output { - recipe.ConsumeAll(p.HasItem, p.RemoveItem) + if slot != nil && slot.ItemID == item.ID { + craftConsumeAll(craft, p.HasItem, p.RemoveItem) slot.Quantity += outputQty placed = true break @@ -165,14 +196,14 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { } } if !placed { - recipe.ConsumeAll(p.HasItem, p.RemoveItem) + craftConsumeAll(craft, p.HasItem, 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: recipe.Output, Quantity: outputQty}) + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: item.ID, Quantity: outputQty}) } for _, bp := range byproducts { @@ -181,38 +212,38 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { } } - if recipe.XP > 0 { - if newLevel := p.AddSkillXP(player.SkillName(skill), recipe.XP); newLevel > 0 { + if craft.XP > 0 { + if newLevel := p.AddSkillXP(player.SkillName(skill), craft.XP); newLevel > 0 { sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, skill))) } } g.AccountStore.SaveCharacter(p) - msg := recipe.Message + msg := craft.Message if msg == "" { - outputName := recipe.Output + outputName := item.ID if outDef != nil { outputName = outDef.Name } msg = fmt.Sprintf("You produce %s.", outputName) } - if p.OptionBool("xp_drops") && recipe.XP > 0 { + if p.OptionBool("xp_drops") && craft.XP > 0 { abbr := player.SkillAbbr[player.SkillName(skill)] - msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr)) + msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", craft.XP, abbr)) } sess.WriteLine(msg) } else { - recipe.ConsumeAll(p.HasItem, p.RemoveItem) + craftConsumeAll(craft, p.HasItem, p.RemoveItem) - if recipe.Fail != "" { + if craft.Fail != "" { freeSlot := p.FirstFreeSlot() if freeSlot >= 0 { - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Fail, Quantity: 1}) + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: craft.Fail, Quantity: 1}) } } g.AccountStore.SaveCharacter(p) - msg := recipe.FailMessage + msg := craft.FailMessage if msg == "" { msg = "You fail and the materials are lost." } @@ -231,7 +262,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { } } - if g.canContinueProduction(p, recipe) { + if g.canContinueProduction(p, item) { p.Action.WaitLeft = engine.ToTicks(wait) return true } @@ -243,9 +274,13 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { return false } -func (g *Game) collectByproducts(p *player.Player, recipe *action.RecipeDef) []string { +func (g *Game) collectByproducts(p *player.Player, item *object.ItemDef) []string { + craft := item.FirstCraft() + if craft == nil { + return nil + } var byproducts []string - for _, e := range recipe.Consume { + for _, e := range craft.Consume { if len(e.Byproducts) == 0 { continue } @@ -259,19 +294,23 @@ func (g *Game) collectByproducts(p *player.Player, recipe *action.RecipeDef) []s return byproducts } -func (g *Game) canContinueProduction(p *player.Player, recipe *action.RecipeDef) bool { - if !recipe.HasAllItemsQty(p.CountItem) { +func (g *Game) canContinueProduction(p *player.Player, item *object.ItemDef) bool { + craft := item.FirstCraft() + if craft == nil { return false } - outputQty := recipe.OutputQty + if !craftHasAllItemsQty(craft, p.CountItem) { + return false + } + outputQty := craft.OutputQty if outputQty <= 0 { outputQty = 1 } - outDef, _ := g.ItemStore.Load(recipe.Output) + 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 == recipe.Output { + if slot != nil && slot.ItemID == item.ID { return true } } @@ -316,7 +355,7 @@ func (g *Game) handleRecipeChoice(sess *net.Session, input string) { if name == "" { continue } - if strings.ToLower(name) == lower || action.WordPrefixMatch(input, name) { + if strings.ToLower(name) == lower || object.WordPrefixMatch(input, name) { if matchIdx >= 0 { sess.WriteLine("That's ambiguous.") return @@ -338,21 +377,11 @@ func (g *Game) handleRecipeChoice(sess *net.Session, input string) { } func (g *Game) menuEntryName(entry map[string]string) string { - if rid, ok := entry["recipe_id"]; ok { - if r := g.RecipeStore.GetByID(rid); r != nil { - if def, err := g.ItemStore.Load(r.Output); err == nil { - return def.Name - } - return r.Output - } - } - if rid, ok := entry["fletch_recipe_id"]; ok { - if r := g.RecipeStore.GetByID(rid); r != nil { - if def, err := g.ItemStore.Load(r.Output); err == nil { - return def.Name - } - return r.Output + if iid, ok := entry["item_id"]; ok { + if def, _ := g.ItemStore.Load(iid); def != nil { + return def.Name } + return iid } if barID, ok := entry["bar_id"]; ok { if def, _ := g.ItemStore.Load(barID); def != nil { @@ -360,51 +389,34 @@ func (g *Game) menuEntryName(entry map[string]string) string { } return barID } - if cid, ok := entry["combine_item"]; ok { - if def, _ := g.ItemStore.Load(cid); def != nil { - return def.Name - } - return cid - } return "" } func (g *Game) dispatchMenuEntry(sess *net.Session, entry map[string]string) { - if cid, ok := entry["combine_item"]; ok { - g.promptHowMany(sess, "combine_"+cid) + if iid, ok := entry["item_id"]; ok { + g.promptHowMany(sess, iid) return } if barID, ok := entry["bar_id"]; ok { - p := sess.Player - allRecipes, _ := g.RecipeStore.LoadAll() - g.showSmithTable(sess, p, barID, allRecipes) - return - } - if rid, ok := entry["fletch_recipe_id"]; ok { - p := sess.Player - if r := g.RecipeStore.GetByID(rid); r != nil { - g.startFletchAction(sess, p, r, 0) - return - } - g.reprompt(sess) - return - } - if rid, ok := entry["recipe_id"]; ok { - g.promptHowMany(sess, rid) + g.showSmithTable(sess, sess.Player, barID) return } g.reprompt(sess) } -func (g *Game) formatMaterials(r *action.RecipeDef) string { +func (g *Game) formatMaterials(item *object.ItemDef) string { + craft := item.FirstCraft() + if craft == nil { + return "" + } var parts []string - for _, e := range r.Consume { + for _, e := range craft.Consume { itemName := e.Items[0] if def, err := g.ItemStore.Load(e.Items[0]); err == nil { itemName = def.Name } - if e.Quantity > 1 { - parts = append(parts, fmt.Sprintf("%s x%d", itemName, e.Quantity)) + if e.Quantity > 1 { + parts = append(parts, fmt.Sprintf("%s x%d", itemName, e.Quantity)) } else { parts = append(parts, itemName) } @@ -412,9 +424,9 @@ func (g *Game) formatMaterials(r *action.RecipeDef) string { return strings.Join(parts, ", ") } -func (g *Game) showProductionTable(sess *net.Session, p *player.Player, recipes []action.RecipeDef, title, skill, lastFlagKey, promptVerb string, background bool) { - sort.Slice(recipes, func(i, j int) bool { - return recipes[i].Level < recipes[j].Level +func (g *Game) showProductionTable(sess *net.Session, p *player.Player, items []*object.ItemDef, title, skill, lastFlagKey, promptVerb string, background bool) { + sort.Slice(items, func(i, j int) bool { + return items[i].FirstCraft().Level < items[j].FirstCraft().Level }) mode := g.colorMode(sess) @@ -426,14 +438,15 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, recipes Columns: []string{"#", "Product", "Materials", "Level"}, } - for i, r := range recipes { - outDef, _ := g.ItemStore.Load(r.Output) - productName := r.Output + for i, item := range items { + craft := item.FirstCraft() + outDef, _ := g.ItemStore.Load(item.ID) + productName := item.ID if outDef != nil { productName = outDef.Name } - outputQty := r.OutputQty + outputQty := craft.OutputQty if outputQty <= 0 { outputQty = 1 } @@ -441,11 +454,11 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, recipes productName = fmt.Sprintf("%s x%d", productName, outputQty) } - matStr := g.formatMaterials(&r) - levelStr := fmt.Sprint(r.Level) + matStr := g.formatMaterials(item) + levelStr := fmt.Sprint(craft.Level) numStr := fmt.Sprintf("%d", i+1) - canMake := skillLevel >= r.Level && r.HasAllItemsQty(p.CountItem) + canMake := skillLevel >= craft.Level && craftHasAllItemsQty(craft, p.CountItem) if canMake { productName = g.itemColorize(sess, outDef, productName) } else { @@ -464,12 +477,12 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, recipes sess.WriteLine(line) } - lastRecipeID, _ := p.Flags[lastFlagKey].(string) + lastItemID, _ := p.Flags[lastFlagKey].(string) hint := "" - if lastRecipeID != "" { - for _, r := range recipes { - if r.ID == lastRecipeID { - if outDef, err := g.ItemStore.Load(r.Output); err == nil { + if lastItemID != "" { + for _, item := range items { + if item.ID == lastItemID { + if outDef, err := g.ItemStore.Load(item.ID); err == nil { hint = outDef.Name } break @@ -483,7 +496,7 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, recipes sess.Write(fmt.Sprintf("%s what: ", promptVerb)) } - sess.PendingMenu = recipeMenuData(recipeDefIDs(recipes)) + sess.PendingMenu = itemMenuData(itemIDs(items)) sess.PendingSkill = skill sess.PendingLastFlag = lastFlagKey sess.PendingBackground = background @@ -504,26 +517,26 @@ func (g *Game) handleProductChoice(sess *net.Session, input string) { input = strings.TrimSpace(input) - var recipes []action.RecipeDef + var items []*object.ItemDef for _, entry := range menuData { - rid := entry["recipe_id"] - if r := g.RecipeStore.GetByID(rid); r != nil { - recipes = append(recipes, *r) + iid := entry["item_id"] + if item := g.loadCraftItem(iid); item != nil { + items = append(items, item) } } - if len(recipes) == 0 { + if len(items) == 0 { g.reprompt(sess) return } - sort.Slice(recipes, func(i, j int) bool { - return recipes[i].Level < recipes[j].Level + sort.Slice(items, func(i, j int) bool { + return items[i].FirstCraft().Level < items[j].FirstCraft().Level }) - lastRecipeID, _ := p.Flags[lastFlagKey].(string) + lastItemID, _ := p.Flags[lastFlagKey].(string) - sel, errMsg := g.resolveRecipeByInput(input, recipes, lastRecipeID) + sel, errMsg := g.resolveCraftByInput(input, items, lastItemID) switch errMsg { case "ambiguous": sess.WriteLine("That's ambiguous.") @@ -535,26 +548,27 @@ func (g *Game) handleProductChoice(sess *net.Session, input string) { return } + craft := sel.Item.FirstCraft() skillLevel := p.Level(player.SkillName(skill)) - if skillLevel < sel.Recipe.Level { - sess.WriteLine(fmt.Sprintf("You need level %d %s to make that.", sel.Recipe.Level, skill)) + if skillLevel < craft.Level { + sess.WriteLine(fmt.Sprintf("You need level %d %s to make that.", craft.Level, skill)) g.reprompt(sess) return } - if !sel.Recipe.HasAllItemsQty(p.CountItem) { + if !craftHasAllItemsQty(craft, p.CountItem) { sess.WriteLine("You don't have the materials for that.") g.reprompt(sess) return } p.EnsureFlags() - p.Flags[lastFlagKey] = sel.Recipe.ID + p.Flags[lastFlagKey] = sel.Item.ID g.AccountStore.SaveCharacter(p) if background { - g.startFletchAction(sess, p, sel.Recipe, sel.Count) + g.startFletchAction(sess, p, sel.Item, sel.Count) } else { - g.startProductionFromRecipe(sess, p, sel.Recipe, sel.Count) + g.startProductionFromItem(sess, p, sel.Item, sel.Count) } } @@ -591,10 +605,6 @@ func (g *Game) hasToolType(p *player.Player, toolType string) bool { _, _, found := g.findTool(p, []string{toolType}) return found } -type recipeEntry struct { - ItemName string - Recipe action.RecipeDef -} type productionTypeInfo struct { ActionType string @@ -621,15 +631,15 @@ func init() { } } -func recipeMenuData(recipeIDs []string) []map[string]string { - out := make([]map[string]string, len(recipeIDs)) - for i, id := range recipeIDs { - out[i] = map[string]string{"recipe_id": id} +func itemMenuData(itemIDs []string) []map[string]string { + out := make([]map[string]string, len(itemIDs)) + for i, id := range itemIDs { + out[i] = map[string]string{"item_id": id} } return out } -func recipeDefIDs(defs []action.RecipeDef) []string { +func itemIDs(defs []*object.ItemDef) []string { ids := make([]string, len(defs)) for i, d := range defs { ids[i] = d.ID @@ -637,33 +647,25 @@ func recipeDefIDs(defs []action.RecipeDef) []string { return ids } -func entryMenuData(entries []recipeEntry) []map[string]string { - out := make([]map[string]string, len(entries)) - for i, e := range entries { - out[i] = map[string]string{"recipe_id": e.Recipe.ID} - } - return out -} - -func (g *Game) promptHowMany(sess *net.Session, recipeID string) { - sess.PendingRecipeID = recipeID +func (g *Game) promptHowMany(sess *net.Session, itemID string) { + sess.PendingItemID = itemID sess.State = net.StateHowMany sess.Write("How many (return for all)?: ") } -type recipeSelection struct { - Recipe *action.RecipeDef - Count int +type craftSelection struct { + Item *object.ItemDef + Count int } -func (g *Game) resolveRecipeByInput(input string, recipes []action.RecipeDef, lastRecipeID string) (sel recipeSelection, errMsg string) { +func (g *Game) resolveCraftByInput(input string, items []*object.ItemDef, lastItemID string) (sel craftSelection, errMsg string) { if input == "" { - if lastRecipeID == "" { + if lastItemID == "" { return sel, "never_mind" } - for i := range recipes { - if recipes[i].ID == lastRecipeID { - sel.Recipe = &recipes[i] + for i := range items { + if items[i].ID == lastItemID { + sel.Item = items[i] return sel, "" } } @@ -673,22 +675,22 @@ func (g *Game) resolveRecipeByInput(input string, recipes []action.RecipeDef, la qty, productName := parseQty(input) productName = strings.TrimSpace(productName) - if idx, err := strconv.Atoi(productName); err == nil && idx > 0 && idx <= len(recipes) { - sel.Recipe = &recipes[idx-1] + if idx, err := strconv.Atoi(productName); err == nil && idx > 0 && idx <= len(items) { + sel.Item = items[idx-1] sel.Count = qty return sel, "" } matchCount := 0 - for i := range recipes { - outDef, _ := g.ItemStore.Load(recipes[i].Output) - name := recipes[i].Output + for i := range items { + outDef, _ := g.ItemStore.Load(items[i].ID) + name := items[i].ID if outDef != nil { name = outDef.Name } - if action.WordPrefixMatch(productName, name) { + if object.WordPrefixMatch(productName, name) { if matchCount == 0 { - sel.Recipe = &recipes[i] + sel.Item = items[i] sel.Count = qty } matchCount++ @@ -697,7 +699,7 @@ func (g *Game) resolveRecipeByInput(input string, recipes []action.RecipeDef, la if matchCount > 1 { return sel, "ambiguous" } - if sel.Recipe == nil { + if sel.Item == nil { return sel, "never_mind" } return sel, "" @@ -716,41 +718,42 @@ func (g *Game) showMenuTable(sess *net.Session, title string, names []string) { } } -func (g *Game) recipeName(sess *net.Session, r *action.RecipeDef) string { - if r.Output != "" { - if def, err := g.ItemStore.Load(r.Output); err == nil { +func (g *Game) itemName(sess *net.Session, item *object.ItemDef) string { + if item != nil { + if def, err := g.ItemStore.Load(item.ID); err == nil { return g.itemColorize(sess, def, def.Name) } + return item.ID } - return r.DisplayName() + return "" } -func (g *Game) showRecipeChoice(sess *net.Session, p *player.Player, recipes []action.RecipeDef, emptyMsg, title string, autoOption string) { - if len(recipes) == 0 { +func (g *Game) showRecipeChoice(sess *net.Session, p *player.Player, items []*object.ItemDef, emptyMsg, title string, autoOption string) { + if len(items) == 0 { sess.WriteLine(emptyMsg) return } - if len(recipes) == 1 { + if len(items) == 1 { if autoOption != "" && p.OptionBool(autoOption) { - g.startProductionFromRecipe(sess, p, &recipes[0], 0) + g.startProductionFromItem(sess, p, items[0], 0) return } - g.promptHowMany(sess, recipes[0].ID) + g.promptHowMany(sess, items[0].ID) return } sess.State = net.StateRecipeChoice - sess.PendingMenu = recipeMenuData(recipeDefIDs(recipes)) + sess.PendingMenu = itemMenuData(itemIDs(items)) var names []string - for i := range recipes { - names = append(names, g.recipeName(sess, &recipes[i])) + for _, item := range items { + names = append(names, g.itemName(sess, item)) } g.showMenuTable(sess, title, names) } func (g *Game) handleHowMany(sess *net.Session, input string) { p := sess.Player - recipeID := sess.PendingRecipeID - sess.PendingRecipeID = "" + itemID := sess.PendingItemID + sess.PendingItemID = "" sess.State = net.StateGame input = strings.TrimSpace(input) @@ -766,46 +769,11 @@ func (g *Game) handleHowMany(sess *net.Session, input string) { return } - var recipe *action.RecipeDef - if strings.HasPrefix(recipeID, "combine_") { - itemID := strings.TrimPrefix(recipeID, "combine_") - itemDef, err := g.ItemStore.Load(itemID) - if err != nil || len(itemDef.MadeFrom) == 0 { - g.reprompt(sess) - return - } - recipe = g.buildCombineRecipe(itemDef) - } else { - recipe = g.RecipeStore.GetByID(recipeID) - } - - if recipe == nil { + item := g.loadCraftItem(itemID) + if item == nil { g.reprompt(sess) return } - g.startProductionFromRecipe(sess, p, recipe, count) -} - -func (g *Game) buildCombineRecipe(def *object.ItemDef) *action.RecipeDef { - var consume []action.ConsumeEntry - for _, mf := range def.MadeFrom { - consume = append(consume, action.ConsumeEntry{ - Items: mf.Items, - Quantity: mf.Quantity, - Byproducts: mf.Byproducts, - }) - } - wait := def.Ticks - if wait <= 0 { - wait = 2 - } - return &action.RecipeDef{ - ID: "combine_" + def.ID, - Type: "combine", - Wait: wait, - Consume: consume, - Output: def.ID, - Message: fmt.Sprintf("You create %s.", def.Name), - } + g.startProductionFromItem(sess, p, item, count) } diff --git a/internal/game/core_startup.go b/internal/game/core_startup.go index ef33ff7..4bdb50d 100644 --- a/internal/game/core_startup.go +++ b/internal/game/core_startup.go @@ -25,7 +25,6 @@ func (g *Game) ValidateStartup() []StartupIssue { issues = append(issues, validateIDs(g.DataDir, "mobs", "mob")...) issues = append(issues, validateIDs(g.DataDir, "objects", "object")...) issues = append(issues, validateIDs(g.DataDir, "drops", "drop table")...) - issues = append(issues, validateIDs(g.DataDir, "recipes", "recipe")...) issues = append(issues, validateIDs(g.DataDir, "courses", "course")...) issues = append(issues, validateIDs(g.DataDir, "modules", "module")...) issues = append(issues, validateIDs(g.DataDir, "help", "help topic")...) @@ -34,7 +33,7 @@ func (g *Game) ValidateStartup() []StartupIssue { issues = append(issues, validateMobs(g)...) issues = append(issues, validateObjects(g)...) issues = append(issues, validateItems(g)...) - issues = append(issues, validateRecipes(g)...) + issues = append(issues, validateCraftBlocks(g)...) issues = append(issues, validateDropTables(g)...) issues = append(issues, validateCourses(g)...) issues = append(issues, validateRoomWiring(g)...) @@ -368,29 +367,6 @@ func validateItems(g *Game) []StartupIssue { }) } - for _, mf := range def.MadeFrom { - for _, item := range mf.Items { - if item != "" && !itemIDs[item] { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Item %q: made_from item %q does not exist", - id, item), - }) - } - } - for _, bp := range mf.Byproducts { - if bp != "" && !itemIDs[bp] { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Item %q: made_from byproduct %q does not exist", - id, bp), - }) - } - } - } - if def.FarmProduct != "" && !itemIDs[def.FarmProduct] { issues = append(issues, StartupIssue{ Level: "ERROR", @@ -404,64 +380,61 @@ func validateItems(g *Game) []StartupIssue { return issues } -func validateRecipes(g *Game) []StartupIssue { +func validateCraftBlocks(g *Game) []StartupIssue { var issues []StartupIssue itemIDs := g.ItemStore.IDSet() + objIDs := g.ObjectStore.IDSet() - allRecipes, err := g.RecipeStore.LoadAll() + allItems, err := g.ItemStore.LoadAll() if err != nil { issues = append(issues, StartupIssue{ Level: "ERROR", Type: "reference", - Message: fmt.Sprintf("Failed to load recipes: %v", err), + Message: fmt.Sprintf("Failed to load items: %v", err), }) return issues } - recipeIDs := make(map[string]bool) - for _, r := range allRecipes { - if r.ID != "" { - if recipeIDs[r.ID] { + for _, item := range allItems { + if len(item.Craft) == 0 { + continue + } + for ci := range item.Craft { + c := &item.Craft[ci] + + for _, e := range c.Consume { + for _, consumeItem := range e.Items { + if consumeItem != "" && !itemIDs[consumeItem] { + issues = append(issues, StartupIssue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Item %q (craft: %s): consumes nonexistent item %q", + item.ID, c.Type, consumeItem), + }) + } + } + } + + if c.Fail != "" && !itemIDs[c.Fail] { issues = append(issues, StartupIssue{ Level: "ERROR", - Type: "duplicate", - Message: fmt.Sprintf("Recipe %q: duplicate ID field (two recipe files with same ID value)", - r.ID), + Type: "reference", + Message: fmt.Sprintf("Item %q (craft: %s): fail product %q does not exist", + item.ID, c.Type, c.Fail), }) } - recipeIDs[r.ID] = true - } - for _, e := range r.Consume { - for _, item := range e.Items { - if item != "" && !itemIDs[item] { + for _, sid := range c.Station { + if sid != "" && !objIDs[sid] { issues = append(issues, StartupIssue{ Level: "ERROR", Type: "reference", - Message: fmt.Sprintf("Recipe %q (%s): consumes nonexistent item %q", - r.ID, r.Type, item), + Message: fmt.Sprintf("Item %q (craft: %s): station object %q does not exist", + item.ID, c.Type, sid), }) } } } - - if r.Output != "" && !itemIDs[r.Output] { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Recipe %q (%s): output item %q does not exist", - r.ID, r.Type, r.Output), - }) - } - - if r.Fail != "" && !itemIDs[r.Fail] { - issues = append(issues, StartupIssue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Recipe %q (%s): fail product %q does not exist", - r.ID, r.Type, r.Fail), - }) - } } return issues @@ -778,4 +751,4 @@ func LogStartupIssues(issues []StartupIssue) { if errors > 0 { fmt.Fprintf(os.Stderr, "*** %d startup errors detected. The server may behave unexpectedly. ***\n", errors) } -}
\ No newline at end of file +} diff --git a/internal/game/craft_index.go b/internal/game/craft_index.go new file mode 100644 index 0000000..078ec3c --- /dev/null +++ b/internal/game/craft_index.go @@ -0,0 +1,234 @@ +package game + +import ( + "sync" + + "thehouseoficarus/internal/object" +) + +type CraftIndex struct { + mu sync.RWMutex + byType map[string][]*object.ItemDef + byInput map[string][]*object.ItemDef + byStation map[string][]*object.ItemDef +} + +func NewCraftIndex() *CraftIndex { + return &CraftIndex{ + byType: make(map[string][]*object.ItemDef), + byInput: make(map[string][]*object.ItemDef), + byStation: make(map[string][]*object.ItemDef), + } +} + +func (idx *CraftIndex) Build(items []*object.ItemDef) { + idx.mu.Lock() + defer idx.mu.Unlock() + + idx.byType = make(map[string][]*object.ItemDef) + idx.byInput = make(map[string][]*object.ItemDef) + idx.byStation = make(map[string][]*object.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) + + for _, e := range craft.Consume { + 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 []*object.ItemDef, item *object.ItemDef) []*object.ItemDef { + for _, existing := range items { + if existing == item { + return items + } + } + return append(items, item) +} + +func (idx *CraftIndex) ByType(craftType string) []*object.ItemDef { + idx.mu.RLock() + defer idx.mu.RUnlock() + items := idx.byType[craftType] + out := make([]*object.ItemDef, len(items)) + copy(out, items) + return out +} + +func (idx *CraftIndex) ByStation(stationDefID string) []*object.ItemDef { + idx.mu.RLock() + defer idx.mu.RUnlock() + items := idx.byStation[stationDefID] + out := make([]*object.ItemDef, len(items)) + copy(out, items) + return out +} + +func (idx *CraftIndex) ByInput(itemID string) []*object.ItemDef { + idx.mu.RLock() + defer idx.mu.RUnlock() + items := idx.byInput[itemID] + out := make([]*object.ItemDef, len(items)) + copy(out, items) + return out +} + +func (idx *CraftIndex) FindByTwoInputs(itemA, itemB string) []*object.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 []*object.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) []*object.ItemDef { + idx.mu.RLock() + defer idx.mu.RUnlock() + + var results []*object.ItemDef + for _, def := range idx.byStation[stationDefID] { + for _, craft := range def.Craft { + if craftMatchesEntry(&craft, itemID) { + results = append(results, def) + break + } + } + } + return results +} + +func craftEntryFor(def *object.ItemDef, itemID string) int { + for _, craft := range def.Craft { + if ei := craftEntry(craft, itemID); ei >= 0 { + return ei + } + } + return -1 +} + +func craftEntry(craft object.CraftDef, itemID string) int { + for ei, e := range craft.Consume { + for _, id := range e.Items { + if id == itemID { + return ei + } + } + } + return -1 +} + +func craftMatchesEntry(c *object.CraftDef, itemID string) bool { + if c == nil { + return false + } + for _, e := range c.Consume { + for _, id := range e.Items { + if id == itemID { + return true + } + } + } + return false +} + +func craftHasAllItems(c *object.CraftDef, hasItem func(string) bool) bool { + if c == nil { + return false + } + for _, e := range c.Consume { + found := false + for _, id := range e.Items { + if hasItem(id) { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +func craftHasAllItemsQty(c *object.CraftDef, countItem func(string) int) bool { + if c == nil { + return false + } + for _, e := range c.Consume { + found := false + for _, id := range e.Items { + qty := e.Quantity + if qty <= 0 { + qty = 1 + } + if countItem(id) >= qty { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +func craftConsumeAll(c *object.CraftDef, hasItem func(string) bool, removeItem func(string, int) bool) bool { + if c == nil { + return false + } + for _, e := range c.Consume { + found := false + for _, id := range e.Items { + if hasItem(id) { + removeItem(id, e.Quantity) + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/internal/game/game.go b/internal/game/game.go index ac8622e..0dd5bdc 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -7,7 +7,6 @@ import ( "sync" "time" - "thehouseoficarus/internal/action" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/config" "thehouseoficarus/internal/engine" @@ -40,7 +39,7 @@ type Game struct { ItemStore *object.ItemStore AccountStore *player.AccountStore MobStore *world.MobStore - RecipeStore *action.RecipeStore + CraftIndex *CraftIndex CourseStore *CourseStore Hub *net.Hub Ticks *engine.Engine @@ -67,7 +66,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.Vali ItemStore: object.NewItemStore(dataDir), AccountStore: player.NewAccountStore(dataDir), MobStore: world.NewMobStore(dataDir), - RecipeStore: action.NewRecipeStore(dataDir), + CraftIndex: NewCraftIndex(), CourseStore: NewCourseStore(dataDir), Ticks: engine.New(), WorldFlags: make(map[string]any), @@ -85,6 +84,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.Vali } g.CourseStore.LoadAll() g.LoadMods() + g.buildCraftIndex() g.ValidateAndLog() return g } @@ -331,6 +331,14 @@ func (g *Game) ProcessQueuedCommands() { g.flushPendingDepletions() } +func (g *Game) buildCraftIndex() { + items, err := g.ItemStore.LoadAll() + if err != nil { + return + } + g.CraftIndex.Build(items) +} + type pendingDepletion struct { instanceKey string objDefID string |
