diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/act_combine.go | 11 | ||||
| -rw-r--r-- | internal/game/act_fletch.go | 52 | ||||
| -rw-r--r-- | internal/game/cmd_fletch.go | 78 | ||||
| -rw-r--r-- | internal/game/cmd_room_remove.go | 59 | ||||
| -rw-r--r-- | internal/game/correct_audit_fixes_test.go | 20 | ||||
| -rw-r--r-- | internal/game/correct_combine_skill_test.go | 2 | ||||
| -rw-r--r-- | internal/game/hardening_craft_combine_fletch_test.go | 4 | ||||
| -rw-r--r-- | internal/game/production_combine_regression_test.go | 2 | ||||
| -rw-r--r-- | internal/game/production_engine.go | 58 | ||||
| -rw-r--r-- | internal/game/production_ingredients_test.go | 4 | ||||
| -rw-r--r-- | internal/game/production_menu.go | 10 |
11 files changed, 171 insertions, 129 deletions
diff --git a/internal/game/act_combine.go b/internal/game/act_combine.go index 701aaec..d7aed04 100644 --- a/internal/game/act_combine.go +++ b/internal/game/act_combine.go @@ -8,7 +8,7 @@ import ( "thehouseoficarus/internal/player" ) -func (g *Game) startCombine(sess *net.Session, p *player.Player, item *item.ItemDef, craft *item.CraftDef, count int) { +func (g *Game) startCombine(sess *net.Session, p *player.Player, item *item.ItemDef, craft *item.CraftDef, count int, craftIndex int) { g.cancelAction(p) g.cancelBgAction(p) @@ -33,6 +33,7 @@ func (g *Game) startCombine(sess *net.Session, p *player.Player, item *item.Item TargetName: outputName, Data: &behavior.CombineData{ ItemID: item.ID, + CraftIndex: craftIndex, Phase: 0, StepIndex: 0, Remaining: count, @@ -72,7 +73,11 @@ func (g *Game) advanceCombine(sess *net.Session, p *player.Player) { g.cancelAction(p) return } - craft := item.FirstCraft() + idx := d.CraftIndex + if idx < 0 || idx >= len(item.Craft) { + idx = 0 + } + craft := &item.Craft[idx] switch d.Phase { case 0: @@ -127,7 +132,7 @@ func (g *Game) advanceCombine(sess *net.Session, p *player.Player) { // Byproducts must be collected BEFORE consumption: the resolver maps // each ingredient entry to the alternative actually being consumed // (bucket vs jug of water), which requires the items still present. - byproducts := g.collectByproducts(p, item) + byproducts := g.collectByproducts(p, craft) hasItem := craftConsumedHasItem(craft, p.CountItem, p.HasItem) if !consumeIngredients(craft, p.CountItem, p.RemoveItem) { diff --git a/internal/game/act_fletch.go b/internal/game/act_fletch.go index d4adff0..ba55516 100644 --- a/internal/game/act_fletch.go +++ b/internal/game/act_fletch.go @@ -15,16 +15,41 @@ func (g *Game) startFletchAction(sess *net.Session, p *player.Player, def *item. g.setPlayerFlag(p, "last_fletch", def.ID) g.saveCharacter(p) - if def.FirstCraft().TicksPerCycle <= 0 { - g.doInstantFletch(sess, p, def, count) + idx := g.usableFletchCraftIndex(p, def) + if idx < 0 { + sess.WriteLine("You don't have the materials for that.") + return + } + if def.Craft[idx].TicksPerCycle <= 0 { + g.doInstantFletch(sess, p, def, idx, count) return } - g.startTimedFletch(sess, p, def, count) + g.startTimedFletch(sess, p, def, idx, count) } -func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, def *item.ItemDef, count int) { - c := def.FirstCraft() +// usableFletchCraftIndex selects the first fletching recipe the player both +// satisfies and has the tools for. Availability scans every recipe, so +// execution must use the same one instead of always Craft[0]. +func (g *Game) usableFletchCraftIndex(p *player.Player, def *item.ItemDef) int { + for i := range def.Craft { + c := &def.Craft[i] + if p.Level(player.Fletching) < c.Level { + continue + } + if !craftHasAllItemsQty(c, p.CountItem) { + continue + } + if !g.hasFletchTools(p, def) { + continue + } + return i + } + return -1 +} + +func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, def *item.ItemDef, craftIdx, count int) { + c := &def.Craft[craftIdx] outputQty := c.OutputQty if outputQty <= 0 { outputQty = 1 @@ -47,7 +72,7 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, def *item.It break } - byproducts := g.collectByproducts(p, def) + byproducts := g.collectByproducts(p, c) if !consumeIngredients(c, p.CountItem, p.RemoveItem) { break @@ -101,8 +126,8 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, def *item.It sess.WriteLine(msg) } -func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, def *item.ItemDef, count int) { - craft := def.FirstCraft() +func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, def *item.ItemDef, craftIdx, count int) { + craft := &def.Craft[craftIdx] startMsg := g.resolveCraftMessage(sess, craft.StartMessage, "You begin fletching %i1.", craft, def.ID, nil, p.HasItem) @@ -121,6 +146,7 @@ func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, def *item.I TargetName: def.Name, Data: &behavior.FletchData{ ItemID: def.ID, + CraftIndex: craftIdx, Phase: 0, TicksPerCycle: craft.TicksPerCycle, Remaining: count, @@ -145,7 +171,11 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { g.cancelBgAction(p) return } - c := def.FirstCraft() + idx := data.CraftIndex + if idx < 0 || idx >= len(def.Craft) { + idx = 0 + } + c := &def.Craft[idx] if phase == 0 { data.Phase = 1 @@ -174,7 +204,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { } skill := player.SkillName(c.EffectiveSkill()) - byproducts := g.collectByproducts(p, def) + byproducts := g.collectByproducts(p, c) hasItem := craftConsumedHasItem(c, p.CountItem, p.HasItem) if !consumeIngredients(c, p.CountItem, p.RemoveItem) { @@ -222,7 +252,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { } } - if !g.canContinueProduction(p, def) { + if !g.canContinueProduction(p, def, c) { sess.WriteLine("You've run out of fletching materials.") g.cancelBgAction(p) return diff --git a/internal/game/cmd_fletch.go b/internal/game/cmd_fletch.go index af27897..9c147c3 100644 --- a/internal/game/cmd_fletch.go +++ b/internal/game/cmd_fletch.go @@ -189,51 +189,57 @@ func (g *Game) findFletchItems(itemAID, itemBID string) []*item.ItemDef { var matched []*item.ItemDef for _, item := range items { - c := item.FirstCraft() - - if c.Tool != "" { - if c.Tool == toolTypeA && craftMatchesEntry(c, itemBID) { - matched = append(matched, item) - continue - } - if c.Tool == toolTypeB && craftMatchesEntry(c, itemAID) { + for i := range item.Craft { + if fletchCraftMatchesInputs(&item.Craft[i], item, itemAID, itemBID, toolTypeA, toolTypeB, isToolA, isToolB) { matched = append(matched, item) - continue + break } } + } + return matched +} - aMatch := false - bMatch := false - for _, e := range c.Ingredients { - for _, id := range e.Items { - if id == itemAID { - aMatch = true - } - if id == itemBID { - bMatch = true - } - } +// fletchCraftMatchesInputs reports whether one fletching recipe consumes the +// given two inputs (or a tool plus one input). findFletchItems must scan every +// recipe, not just the first, or multi-recipe items are unreachable. +func fletchCraftMatchesInputs(c *item.CraftDef, item *item.ItemDef, itemAID, itemBID, toolTypeA, toolTypeB string, isToolA, isToolB bool) bool { + if c.Tool != "" { + if c.Tool == toolTypeA && craftMatchesEntry(c, itemBID) { + return true } - - if aMatch && bMatch { - matched = append(matched, item) - continue + if c.Tool == toolTypeB && craftMatchesEntry(c, itemAID) { + return true } + } - if isToolA && bMatch { - if (fletchNeedsKnife(item) && toolTypeA == "knife") || - (fletchNeedsChisel(item) && toolTypeA == "chisel") { - matched = append(matched, item) - continue + aMatch := false + bMatch := false + for _, e := range c.Ingredients { + for _, id := range e.Items { + if id == itemAID { + aMatch = true } - } - if isToolB && aMatch { - if (fletchNeedsKnife(item) && toolTypeB == "knife") || - (fletchNeedsChisel(item) && toolTypeB == "chisel") { - matched = append(matched, item) - continue + if id == itemBID { + bMatch = true } } } - return matched + + if aMatch && bMatch { + return true + } + + if isToolA && bMatch { + if (fletchNeedsKnife(item) && toolTypeA == "knife") || + (fletchNeedsChisel(item) && toolTypeA == "chisel") { + return true + } + } + if isToolB && aMatch { + if (fletchNeedsKnife(item) && toolTypeB == "knife") || + (fletchNeedsChisel(item) && toolTypeB == "chisel") { + return true + } + } + return false } diff --git a/internal/game/cmd_room_remove.go b/internal/game/cmd_room_remove.go index 79b84d8..c84e747 100644 --- a/internal/game/cmd_room_remove.go +++ b/internal/game/cmd_room_remove.go @@ -2,11 +2,6 @@ package game import ( "fmt" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" "gopkg.in/yaml.v3" "thehouseoficarus/internal/net" @@ -125,7 +120,7 @@ func (g *Game) roomRemove(sess *net.Session, args []string) { // Guard 5: no other room points into B. The only allowed inbound edges are // A→dir→B and C→oppositeDir→B. - if extra := g.countExtraInbound(bID, aID, cID, dir, oppositeDir); extra > 0 { + if extra := world.CountExtraInbound(g.DataDir, bID, aID, cID, dir, oppositeDir); extra > 0 { sess.WriteLine(fmt.Sprintf( "Cannot remove: %d other exit(s) point into %s (it was modified since insertion).", extra, bName)) @@ -244,53 +239,5 @@ func (g *Game) roomRemove(sess *net.Session, args []string) { g.doLook(sess) } -// countExtraInbound returns the number of exits in rooms OTHER than A and C -// that point into B, plus any A/C exits to B other than the expected -// (A→dir→B, C→oppositeDir→B). A nonzero result means B has been modified since -// it was inserted and the remove would silently orphan edges. It walks -// data/rooms/ and matches \b<id>\b against raw file contents to skip rooms -// that don't mention bID at all, then unmarshals the matching files. -func (g *Game) countExtraInbound(bID, aID, cID int, dir, oppositeDir world.ExitDir) int { - roomsDir := filepath.Join(g.DataDir, "rooms") - re := regexp.MustCompile(fmt.Sprintf(`\b%d\b`, bID)) - var extra int - _ = filepath.WalkDir(roomsDir, func(path string, d os.DirEntry, err error) error { - if err != nil || d.IsDir() || filepath.Ext(path) != ".yaml" { - return nil - } - data, rerr := os.ReadFile(path) - if rerr != nil { - return nil - } - if !re.MatchString(string(data)) { - return nil - } - idStr := strings.TrimSuffix(filepath.Base(path), ".yaml") - rid, serr := strconv.Atoi(idStr) - if serr != nil || rid == bID { - return nil - } - var room world.Room - if yerr := yaml.Unmarshal(data, &room); yerr != nil { - return nil - } - if room.Exits == nil { - return nil - } - for ed, exitDef := range room.Exits { - if exitDef.Room != bID { - continue - } - // Allowed: A→dir→B and C→oppositeDir→B. - if rid == aID && ed == dir { - continue - } - if rid == cID && ed == oppositeDir { - continue - } - extra++ - } - return nil - }) - return extra -} +// countExtraInbound was moved to world.CountExtraInbound so the in-game and +// admin removal paths share one implementation. diff --git a/internal/game/correct_audit_fixes_test.go b/internal/game/correct_audit_fixes_test.go index 681843c..6177eac 100644 --- a/internal/game/correct_audit_fixes_test.go +++ b/internal/game/correct_audit_fixes_test.go @@ -7,6 +7,7 @@ import ( "thehouseoficarus/internal/casino" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" "thehouseoficarus/internal/prediction" ) @@ -106,6 +107,25 @@ func TestPickupCreditsHonorsRequestedQuantity(t *testing.T) { // Regression: the player-file classifier must canonicalize repeated "/" and // "data/" prefixes so a raw-editor path cannot dodge the online check. +// Regression: availability scans every recipe, but execution used Craft[0], so +// a player satisfying a later recipe (e.g. oak arrow shafts) got a start-then- +// abort. usableCraftIndex must pick the satisfied recipe. +func TestUsableCraftIndexSelectsSatisfiedRecipe(t *testing.T) { + g := &Game{} + p := player.New("alice") + def := &item.ItemDef{ + ID: "arrow_shaft", + Craft: item.CraftList{ + {Type: "fletching", Level: 1, Ingredients: []item.IngredientEntry{{Items: []string{"logs"}, Quantity: 1}}}, + {Type: "fletching", Level: 1, Ingredients: []item.IngredientEntry{{Items: []string{"oak_logs"}, Quantity: 1}}}, + }, + } + p.SetInvSlot(0, &player.InventorySlot{ItemID: "oak_logs", Quantity: 5}) + if idx := g.usableCraftIndex(p, def); idx != 1 { + t.Fatalf("usableCraftIndex = %d, want 1 (the satisfied oak recipe)", idx) + } +} + func TestPlayerDataFileNameCanonicalizesPrefixes(t *testing.T) { for _, in := range []string{ "players/characters/alice.yaml", diff --git a/internal/game/correct_combine_skill_test.go b/internal/game/correct_combine_skill_test.go index d6de229..7d9c725 100644 --- a/internal/game/correct_combine_skill_test.go +++ b/internal/game/correct_combine_skill_test.go @@ -68,7 +68,7 @@ func TestCombineXPGoesToCraftingNotAPhantomSkill(t *testing.T) { h.g.ItemStore.Reload(h.dir) p.SetInvSlot(0, &player.InventorySlot{ItemID: "correct_combine_part", Quantity: 1}) def := mustLoadItem(t, h.g, "correct_combine_out") - h.g.startCombine(sess, p, def, def.FirstCraft(), 1) + h.g.startCombine(sess, p, def, def.FirstCraft(), 1, 0) if p.Action == nil { t.Fatal("combine did not start") } diff --git a/internal/game/hardening_craft_combine_fletch_test.go b/internal/game/hardening_craft_combine_fletch_test.go index f3f6832..5b5d1c1 100644 --- a/internal/game/hardening_craft_combine_fletch_test.go +++ b/internal/game/hardening_craft_combine_fletch_test.go @@ -124,7 +124,7 @@ func TestCombineHonorsSuccessXPAndByproducts(t *testing.T) { sess.SetState(net.StateGame) def := mustLoadItem(t, g, "test_combine_out") - g.startCombine(sess, p, def, def.FirstCraft(), 1) + g.startCombine(sess, p, def, def.FirstCraft(), 1, 0) if p.Action == nil { t.Fatal("combine did not start") } @@ -165,7 +165,7 @@ func TestCombineCreatesFailOnFailure(t *testing.T) { sess.SetState(net.StateGame) def := mustLoadItem(t, g, "test_combine_fail_out") - g.startCombine(sess, p, def, def.FirstCraft(), 1) + g.startCombine(sess, p, def, def.FirstCraft(), 1, 0) g.advanceCombine(sess, p) g.advanceCombine(sess, p) g.advanceCombine(sess, p) diff --git a/internal/game/production_combine_regression_test.go b/internal/game/production_combine_regression_test.go index c617565..80e0651 100644 --- a/internal/game/production_combine_regression_test.go +++ b/internal/game/production_combine_regression_test.go @@ -92,7 +92,7 @@ func TestCombineAlternativesConsumeVerifiedSource(t *testing.T) { sess := &net.Session{Conn: conn, Player: p} sess.SetState(net.StateGame) - h.g.startCombine(sess, p, dough, dough.FirstCraft(), 1) + h.g.startCombine(sess, p, dough, dough.FirstCraft(), 1, 0) if p.Action == nil { t.Error("combine action did not start") return diff --git a/internal/game/production_engine.go b/internal/game/production_engine.go index f80daa2..4717fbf 100644 --- a/internal/game/production_engine.go +++ b/internal/game/production_engine.go @@ -72,7 +72,7 @@ func (g *Game) resolveCraftMessage(sess *net.Session, itemMsg, defaultMsg string return g.resolveCraftVar(sess, msg, craft, outputID, byproducts, hasItem) } -func (g *Game) startProduction(sess *net.Session, p *player.Player, item *item.ItemDef, craft *item.CraftDef, actionType behavior.ActionType, displayVerb, startMsg, endMsg string, count int) { +func (g *Game) startProduction(sess *net.Session, p *player.Player, item *item.ItemDef, craft *item.CraftDef, actionType behavior.ActionType, displayVerb, startMsg, endMsg string, count int, craftIndex int) { g.cancelAction(p) g.cancelBgAction(p) @@ -110,6 +110,7 @@ func (g *Game) startProduction(sess *net.Session, p *player.Player, item *item.I TargetName: outputName, Data: &behavior.ProductionData{ ItemID: item.ID, + CraftIndex: craftIndex, Phase: 0, TicksPerCycle: wait, StartMessage: startMsg, @@ -122,11 +123,42 @@ func (g *Game) startProduction(sess *net.Session, p *player.Player, item *item.I g.broadcastAction(sess, "%s starts %s.", p.Name, displayVerb) } +// usableCraftIndex returns the index of the first recipe of item the player +// satisfies (level and ingredients), or -1. Availability checks scan every +// recipe, so execution must select the same one instead of always Craft[0]. +func (g *Game) usableCraftIndex(p *player.Player, item *item.ItemDef) int { + for i := range item.Craft { + c := &item.Craft[i] + skill := c.EffectiveSkill() + if skill != "" && p.Level(player.SkillName(skill)) < c.Level { + continue + } + if !craftHasAllItemsQty(c, p.CountItem) { + continue + } + return i + } + return -1 +} + +// usableCraft is usableCraftIndex with a first-recipe fallback for callers +// that only need a display recipe. +func (g *Game) usableCraft(p *player.Player, item *item.ItemDef) *item.CraftDef { + if idx := g.usableCraftIndex(p, item); idx >= 0 { + return &item.Craft[idx] + } + return item.FirstCraft() +} + func (g *Game) startProductionFromItem(sess *net.Session, p *player.Player, item *item.ItemDef, count int) { - craft := item.FirstCraft() - if craft == nil { + idx := g.usableCraftIndex(p, item) + if idx < 0 { + idx = 0 + } + if idx >= len(item.Craft) { return } + craft := &item.Craft[idx] // Normalize the empty craft type to "combine" here as well as in the // craft index: an omitted `type:` is combine, but the index's normalized @@ -137,7 +169,7 @@ func (g *Game) startProductionFromItem(sess *net.Session, p *player.Player, item craftType = "combine" } if craftType == "combine" { - g.startCombine(sess, p, item, craft, count) + g.startCombine(sess, p, item, craft, count, idx) return } @@ -172,7 +204,7 @@ func (g *Game) startProductionFromItem(sess *net.Session, p *player.Player, item endMsg := g.resolveCraftMessage(sess, craft.EndMessage, info.EndMessage, craft, item.ID, nil, p.HasItem) - g.startProduction(sess, p, item, craft, info.ActionType, info.DisplayVerb, startMsg, endMsg, count) + g.startProduction(sess, p, item, craft, info.ActionType, info.DisplayVerb, startMsg, endMsg, count, idx) } func (g *Game) loadCraftItem(itemID string) *item.ItemDef { @@ -201,7 +233,11 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) { g.cancelAction(p) return } - craft := item.FirstCraft() + idx := d.CraftIndex + if idx < 0 || idx >= len(item.Craft) { + idx = 0 + } + craft := &item.Craft[idx] if phase == 0 { if startMsg != "" { @@ -250,7 +286,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) { // Byproducts must be collected BEFORE consumption: the resolver // maps each ingredient entry to the alternative actually being // consumed, which requires the items still present. - byproducts := g.collectByproducts(p, item) + byproducts := g.collectByproducts(p, craft) if !consumeIngredients(craft, p.CountItem, p.RemoveItem) { // Cannot happen after the availability gate above, but never @@ -323,7 +359,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) { } } - if g.canContinueProduction(p, item) { + if g.canContinueProduction(p, item, craft) { p.Action.WaitLeft = engine.ToTicks(wait) return } @@ -334,8 +370,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) { g.cancelAction(p) } -func (g *Game) collectByproducts(p *player.Player, item *item.ItemDef) []string { - craft := item.FirstCraft() +func (g *Game) collectByproducts(p *player.Player, craft *item.CraftDef) []string { if craft == nil { return nil } @@ -433,8 +468,7 @@ func (g *Game) placeCraftByproducts(p *player.Player, byproducts []string) { } } -func (g *Game) canContinueProduction(p *player.Player, item *item.ItemDef) bool { - craft := item.FirstCraft() +func (g *Game) canContinueProduction(p *player.Player, item *item.ItemDef, craft *item.CraftDef) bool { if craft == nil { return false } diff --git a/internal/game/production_ingredients_test.go b/internal/game/production_ingredients_test.go index d799df8..5206346 100644 --- a/internal/game/production_ingredients_test.go +++ b/internal/game/production_ingredients_test.go @@ -111,14 +111,14 @@ func TestCollectByproductsMatchesConsumedAlternative(t *testing.T) { // Only the jug is present: the empty jug returns. p := invOf(t, map[string]int{"jug_of_water": 1}) - if got := g.collectByproducts(p, &item.ItemDef{Craft: item.CraftList{*craft}}); len(got) != 1 || got[0] != "empty_jug" { + if got := g.collectByproducts(p, craft); len(got) != 1 || got[0] != "empty_jug" { t.Errorf("byproducts = %v, want [empty_jug]", got) } // Both present: consumption picks the FIRST alternative (the bucket), // so the byproduct must be the empty bucket. p = invOf(t, map[string]int{"bucket_of_water": 1, "jug_of_water": 1}) - if got := g.collectByproducts(p, &item.ItemDef{Craft: item.CraftList{*craft}}); len(got) != 1 || got[0] != "empty_bucket" { + if got := g.collectByproducts(p, craft); len(got) != 1 || got[0] != "empty_bucket" { t.Errorf("byproducts = %v, want [empty_bucket]", got) } } diff --git a/internal/game/production_menu.go b/internal/game/production_menu.go index 9b9f8e6..5d712da 100644 --- a/internal/game/production_menu.go +++ b/internal/game/production_menu.go @@ -101,7 +101,7 @@ func (g *Game) dispatchMenuEntry(sess *net.Session, entry map[string]string) { } func (g *Game) formatMaterials(sess *net.Session, item *item.ItemDef) string { - craft := item.FirstCraft() + craft := g.usableCraft(sess.Player, item) if craft == nil { return "" } @@ -125,7 +125,7 @@ func (g *Game) formatMaterials(sess *net.Session, item *item.ItemDef) string { func (g *Game) showProductionTable(sess *net.Session, p *player.Player, items []*item.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 + return g.usableCraft(p, items[i]).Level < g.usableCraft(p, items[j]).Level }) mode := g.colorMode(sess) @@ -138,7 +138,7 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, items [] } for i, item := range items { - craft := item.FirstCraft() + craft := g.usableCraft(p, item) outDef, _ := g.ItemStore.Load(item.ID) productName := item.ID if outDef != nil { @@ -230,7 +230,7 @@ func (g *Game) handleProductChoice(sess *net.Session, input string) { } sort.Slice(items, func(i, j int) bool { - return items[i].FirstCraft().Level < items[j].FirstCraft().Level + return g.usableCraft(p, items[i]).Level < g.usableCraft(p, items[j]).Level }) lastItemID, _ := p.Flags[lastFlagKey].(string) @@ -247,7 +247,7 @@ func (g *Game) handleProductChoice(sess *net.Session, input string) { return } - craft := sel.Item.FirstCraft() + craft := g.usableCraft(p, sel.Item) skillLevel := p.Level(player.SkillName(skill)) if skillLevel < craft.Level { sess.WriteLine(fmt.Sprintf("You need level %d %s to make that.", craft.Level, skill)) |
