From 0bebcb56cd1874f144cb2495e363ff75bd88bb2c Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:23:53 -0700 Subject: [PATCH] =?UTF-8?q?Adventure:=20!adventure=20recipes=20=E2=80=94?= =?UTF-8?q?=20list=20known=20crafting=20recipes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Players had no way to see what they could craft without grinding ingredients and watching combat narrative. New command lists every recipe unlocked at the player's current Foraging level, grouped by tier, with: - Result name + ingredient list (so players know what to gather) - Per-recipe success rate at the player's current level - Locked-recipe count + next-unlock threshold - Max auto-crafts per combat (1 + (level-10)/10) Sub-Foraging-10 players see only the unlock hint. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/plugin/adventure.go | 11 +++++ internal/plugin/adventure_consumables.go | 57 ++++++++++++++++++++++++ 2 files changed, 68 insertions(+) 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 —