diff --git a/internal/plugin/adventure.go b/internal/plugin/adventure.go index 609334a..7184ad7 100644 --- a/internal/plugin/adventure.go +++ b/internal/plugin/adventure.go @@ -267,6 +267,8 @@ func (p *AdventurePlugin) dispatchCommand(ctx MessageContext) error { return p.handleRepairSlotCmd(ctx, strings.TrimSpace(args[7:])) case lower == "boost": return p.handleBoostCmd(ctx) + case lower == "recipes": + return p.handleRecipesCmd(ctx) } return p.SendDM(ctx.Sender, "Unknown command. Type `!adventure help` to see available commands.") @@ -290,6 +292,7 @@ const advHelpText = `**Adventure Commands** ` + "`!adventure blacksmith`" + ` — Visit the blacksmith (view repair costs) ` + "`!adventure repair all`" + ` — Repair all damaged equipment ` + "`!adventure repair `" + ` — Repair a specific slot +` + "`!adventure recipes`" + ` — List crafting recipes known at your current Foraging level ` + "`!coop`" + ` — Co-op dungeons (multi-day party runs). See ` + "`!coop help`" + `. ` + "`!hospital`" + ` — Visit St. Guildmore's Memorial Hospital (same-day revival when dead) ` + "`!thom`" + ` — Visit Thom Krooke (housing and loans) @@ -1343,6 +1346,14 @@ func advApplyBoost(result *AdvActionResult) { } } +func (p *AdventurePlugin) handleRecipesCmd(ctx MessageContext) error { + char, _, err := p.ensureCharacter(ctx.Sender) + if err != nil { + return p.SendDM(ctx.Sender, "Failed to load your character.") + } + return p.SendDM(ctx.Sender, renderRecipesKnown(char.ForagingSkill)) +} + func (p *AdventurePlugin) handleBoostCmd(ctx MessageContext) error { if !p.IsAdmin(ctx.Sender) { return nil diff --git a/internal/plugin/adventure_consumables.go b/internal/plugin/adventure_consumables.go index fe6d048..0e33213 100644 --- a/internal/plugin/adventure_consumables.go +++ b/internal/plugin/adventure_consumables.go @@ -1,8 +1,10 @@ package plugin import ( + "fmt" "log/slog" "math/rand/v2" + "strings" "maunium.net/go/mautrix/id" ) @@ -339,6 +341,61 @@ type CraftResult struct { Success bool } +// renderRecipesKnown returns a player-facing list of recipes available at +// their current foraging level, plus a teaser line for the next unlock +// threshold. Hides exact ingredient lists for recipes the player hasn't +// unlocked — only the count of locked recipes is shown. +func renderRecipesKnown(foragingLevel int) string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("🧪 **Crafting Recipes** — Foraging Lv.%d\n\n", foragingLevel)) + + if foragingLevel < 10 { + sb.WriteString("_Auto-crafting unlocks at Foraging Lv.10. Keep gathering._") + return sb.String() + } + + // Group recipes by tier, list those known. + known := map[int][]CraftingRecipe{} + totalKnown := 0 + totalLocked := 0 + nextUnlock := 0 + for _, r := range craftingRecipes { + if r.MinForaging <= foragingLevel { + known[r.Tier] = append(known[r.Tier], r) + totalKnown++ + } else { + totalLocked++ + if nextUnlock == 0 || r.MinForaging < nextUnlock { + nextUnlock = r.MinForaging + } + } + } + + for tier := 1; tier <= 5; tier++ { + recipes := known[tier] + if len(recipes) == 0 { + continue + } + sb.WriteString(fmt.Sprintf("**Tier %d** (Foraging %d+)\n", tier, recipes[0].MinForaging)) + for _, r := range recipes { + rate := craftingSuccessRate(foragingLevel, r.MinForaging) * 100 + sb.WriteString(fmt.Sprintf(" • %s — %s (%.0f%% success)\n", + r.Result, strings.Join(r.Ingredients, " + "), rate)) + } + } + + sb.WriteString(fmt.Sprintf("\nKnown: %d · ", totalKnown)) + if totalLocked == 0 { + sb.WriteString("All recipes unlocked. ⭐") + } else { + sb.WriteString(fmt.Sprintf("Locked: %d (next unlock at Foraging Lv.%d)", totalLocked, nextUnlock)) + } + + maxCrafts := 1 + (foragingLevel-10)/10 + sb.WriteString(fmt.Sprintf("\nMax auto-crafts per combat: %d.", maxCrafts)) + return sb.String() +} + // craftXPRewards: per-tier foraging XP granted on successful (and tiny on // failed) auto-crafts. Successful T1 ≈ 30% of a Foraging Success haul, scaling // up so T5 grants are meaningful. Failures get a token consolation grant —