Adventure: !adventure recipes — list known crafting recipes

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) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-04-28 23:23:53 -07:00
parent 8e3b8377c0
commit 0bebcb56cd
2 changed files with 68 additions and 0 deletions

View File

@@ -267,6 +267,8 @@ func (p *AdventurePlugin) dispatchCommand(ctx MessageContext) error {
return p.handleRepairSlotCmd(ctx, strings.TrimSpace(args[7:])) return p.handleRepairSlotCmd(ctx, strings.TrimSpace(args[7:]))
case lower == "boost": case lower == "boost":
return p.handleBoostCmd(ctx) 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.") 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 blacksmith`" + ` — Visit the blacksmith (view repair costs)
` + "`!adventure repair all`" + ` — Repair all damaged equipment ` + "`!adventure repair all`" + ` — Repair all damaged equipment
` + "`!adventure repair <slot>`" + ` — Repair a specific slot ` + "`!adventure repair <slot>`" + ` — 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`" + `. ` + "`!coop`" + ` — Co-op dungeons (multi-day party runs). See ` + "`!coop help`" + `.
` + "`!hospital`" + ` — Visit St. Guildmore's Memorial Hospital (same-day revival when dead) ` + "`!hospital`" + ` — Visit St. Guildmore's Memorial Hospital (same-day revival when dead)
` + "`!thom`" + ` — Visit Thom Krooke (housing and loans) ` + "`!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 { func (p *AdventurePlugin) handleBoostCmd(ctx MessageContext) error {
if !p.IsAdmin(ctx.Sender) { if !p.IsAdmin(ctx.Sender) {
return nil return nil

View File

@@ -1,8 +1,10 @@
package plugin package plugin
import ( import (
"fmt"
"log/slog" "log/slog"
"math/rand/v2" "math/rand/v2"
"strings"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
) )
@@ -339,6 +341,61 @@ type CraftResult struct {
Success bool 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 // craftXPRewards: per-tier foraging XP granted on successful (and tiny on
// failed) auto-crafts. Successful T1 ≈ 30% of a Foraging Success haul, scaling // failed) auto-crafts. Successful T1 ≈ 30% of a Foraging Success haul, scaling
// up so T5 grants are meaningful. Failures get a token consolation grant — // up so T5 grants are meaningful. Failures get a token consolation grant —