mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
Adventure — Masterwork Rework: - Expand from 3 generic items to 15 tiered masterwork items across mining (weapon), fishing (armor), and foraging (boots) with per-tier drop rates (5%/4%/3%/2%/1.5%) and location gating - Skill-specific cross-skill bonuses replace flat masterwork check - Auto-equip if better, silent discard for duplicates, inventory for rest - Add !adventure equip command for manual masterwork equipping - Tiered room announcements: T1-2 DM only, T3 quiet, T4-5 full - First-drop detection with special message - Character sheet shows ⭐/⚔️ markers for masterwork/arena gear - Sell protection for special gear in shop Adventure — Arena Combat Log: - Turn-based Dragon Quest style narrative for arena fights - Outcome-first design: roll determines win/loss, log assembled backward - No-repeat flavor text via actionPicker with per-pool tracking - 60 participation XP on arena loss Adventure — Death's Reprieve Fix: - All equipment set to 1 condition instead of death-level degradation Wordle: - Refactored word sourcing and expanded fallback word list - Simplified Wordnik integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
365 lines
12 KiB
Go
365 lines
12 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
// ── Shop Listings ────────────────────────────────────────────────────────────
|
|
|
|
// slotEmoji returns a display emoji for a slot category.
|
|
func slotEmoji(slot EquipmentSlot) string {
|
|
switch slot {
|
|
case SlotWeapon:
|
|
return "⚔️"
|
|
case SlotArmor:
|
|
return "🛡️"
|
|
case SlotHelmet:
|
|
return "🪖"
|
|
case SlotBoots:
|
|
return "👢"
|
|
case SlotTool:
|
|
return "⛏️"
|
|
default:
|
|
return "📦"
|
|
}
|
|
}
|
|
|
|
// slotTitle returns a capitalized display name for a slot.
|
|
func slotTitle(slot EquipmentSlot) string {
|
|
s := string(slot)
|
|
if len(s) == 0 {
|
|
return s
|
|
}
|
|
return strings.ToUpper(s[:1]) + s[1:]
|
|
}
|
|
|
|
// advShopOverview shows a compact category menu with current equipment and next upgrade.
|
|
func advShopOverview(equip map[EquipmentSlot]*AdvEquipment, balance float64) string {
|
|
var sb strings.Builder
|
|
sb.WriteString("🛒 **Equipment Shop**\n")
|
|
sb.WriteString(fmt.Sprintf("💰 Balance: €%.0f\n\n", balance))
|
|
|
|
for _, slot := range allSlots {
|
|
current := equip[slot]
|
|
currentTier := 0
|
|
currentName := "None"
|
|
if current != nil {
|
|
currentTier = current.Tier
|
|
currentName = current.Name
|
|
}
|
|
|
|
emoji := slotEmoji(slot)
|
|
title := slotTitle(slot)
|
|
|
|
// Find next upgrade.
|
|
defs := equipmentTiers[slot]
|
|
nextUpgrade := ""
|
|
for _, def := range defs {
|
|
if def.Tier > currentTier && def.Price > 0 {
|
|
nextUpgrade = fmt.Sprintf("Next: %s — €%.0f", def.Name, def.Price)
|
|
break
|
|
}
|
|
}
|
|
|
|
sb.WriteString(fmt.Sprintf("%s **%s** — %s (T%d)\n", emoji, title, currentName, currentTier))
|
|
if nextUpgrade != "" {
|
|
sb.WriteString(fmt.Sprintf(" %s\n", nextUpgrade))
|
|
} else {
|
|
sb.WriteString(" ✨ Maxed out!\n")
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
sb.WriteString("Browse a category: `!adventure shop <category>`\n")
|
|
sb.WriteString("Categories: `weapon` · `armor` · `helmet` · `boots` · `tool`")
|
|
return sb.String()
|
|
}
|
|
|
|
var shopCategoryAliases = map[string]EquipmentSlot{
|
|
"weapon": SlotWeapon, "weapons": SlotWeapon, "sword": SlotWeapon, "swords": SlotWeapon,
|
|
"armor": SlotArmor, "armour": SlotArmor,
|
|
"helmet": SlotHelmet, "helm": SlotHelmet, "helmets": SlotHelmet,
|
|
"boots": SlotBoots, "boot": SlotBoots,
|
|
"tool": SlotTool, "tools": SlotTool, "pickaxe": SlotTool,
|
|
}
|
|
|
|
// advParseShopCategory maps user input to an EquipmentSlot.
|
|
func advParseShopCategory(input string) EquipmentSlot {
|
|
return shopCategoryAliases[strings.ToLower(strings.TrimSpace(input))]
|
|
}
|
|
|
|
// advShopCategory shows detailed listings for a single equipment slot.
|
|
func advShopCategory(slot EquipmentSlot, equip map[EquipmentSlot]*AdvEquipment, balance float64) string {
|
|
var sb strings.Builder
|
|
|
|
current := equip[slot]
|
|
currentTier := 0
|
|
currentName := "None"
|
|
if current != nil {
|
|
currentTier = current.Tier
|
|
currentName = current.Name
|
|
}
|
|
|
|
emoji := slotEmoji(slot)
|
|
title := slotTitle(slot)
|
|
|
|
sb.WriteString(fmt.Sprintf("%s **%s Shop**\n", emoji, title))
|
|
sb.WriteString(fmt.Sprintf("💰 Balance: €%.0f\n", balance))
|
|
sb.WriteString(fmt.Sprintf("Equipped: **%s** (Tier %d)\n\n", currentName, currentTier))
|
|
|
|
defs := equipmentTiers[slot]
|
|
hasUpgrades := false
|
|
for _, def := range defs {
|
|
if def.Tier <= currentTier || def.Price == 0 {
|
|
continue
|
|
}
|
|
hasUpgrades = true
|
|
|
|
priceTag := fmt.Sprintf("€%.0f", def.Price)
|
|
if balance < def.Price {
|
|
priceTag += " 💸 can't afford"
|
|
} else {
|
|
priceTag += " ✅ affordable"
|
|
}
|
|
|
|
sb.WriteString(fmt.Sprintf("**Tier %d: %s** — %s\n", def.Tier, def.Name, priceTag))
|
|
sb.WriteString(fmt.Sprintf("%s\n\n", def.Description))
|
|
}
|
|
|
|
if !hasUpgrades {
|
|
sb.WriteString("✨ You've reached max tier! Nothing left to buy here.\n\n")
|
|
}
|
|
|
|
sb.WriteString("To buy: `!adventure buy <item name>` or `!adventure buy <tier> <category>`\n")
|
|
sb.WriteString("Example: `!adventure buy Enchanted Blade` or `!adventure buy 4 sword`\n")
|
|
sb.WriteString("Back to overview: `!adventure shop`")
|
|
return sb.String()
|
|
}
|
|
|
|
// ── Find Shop Item ───────────────────────────────────────────────────────────
|
|
|
|
// normalizeQuotes replaces common Unicode quotes/apostrophes with ASCII equivalents.
|
|
var quoteReplacer = strings.NewReplacer(
|
|
"\u2018", "'", "\u2019", "'", // left/right single curly quotes
|
|
"\u201C", "\"", "\u201D", "\"", // left/right double curly quotes
|
|
"\u2032", "'", // prime
|
|
)
|
|
|
|
func normalizeQuotes(s string) string {
|
|
return quoteReplacer.Replace(s)
|
|
}
|
|
|
|
func advFindShopItem(name string) (EquipmentSlot, *EquipmentDef, bool) {
|
|
name = normalizeQuotes(strings.TrimSpace(name))
|
|
|
|
// Support tier+category shorthand: "3 sword", "tier 3 weapon", "t3 boots", etc.
|
|
if slot, def, ok := advFindByTierShorthand(name); ok {
|
|
return slot, def, true
|
|
}
|
|
|
|
for _, slot := range allSlots {
|
|
for i := range equipmentTiers[slot] {
|
|
def := &equipmentTiers[slot][i]
|
|
if def.Price == 0 {
|
|
continue // can't buy tier 0
|
|
}
|
|
if strings.EqualFold(def.Name, name) || containsFold(normalizeQuotes(def.Name), name) {
|
|
return slot, def, true
|
|
}
|
|
}
|
|
}
|
|
return "", nil, false
|
|
}
|
|
|
|
// advFindByTierShorthand matches patterns like "3 sword", "tier 3 weapon", "t3 boots".
|
|
func advFindByTierShorthand(input string) (EquipmentSlot, *EquipmentDef, bool) {
|
|
lower := strings.ToLower(input)
|
|
|
|
// Strip optional "tier " or "t" prefix.
|
|
lower = strings.TrimPrefix(lower, "tier ")
|
|
lower = strings.TrimPrefix(lower, "t")
|
|
|
|
// Expect "<number> <category>" or "<number><category>".
|
|
parts := strings.SplitN(strings.TrimSpace(lower), " ", 2)
|
|
if len(parts) < 2 {
|
|
return "", nil, false
|
|
}
|
|
|
|
tierStr := strings.TrimSpace(parts[0])
|
|
category := strings.TrimSpace(parts[1])
|
|
|
|
tier := 0
|
|
for _, c := range tierStr {
|
|
if c < '0' || c > '9' {
|
|
return "", nil, false
|
|
}
|
|
tier = tier*10 + int(c-'0')
|
|
}
|
|
|
|
slot := advParseShopCategory(category)
|
|
if slot == "" {
|
|
return "", nil, false
|
|
}
|
|
|
|
defs := equipmentTiers[slot]
|
|
for i := range defs {
|
|
if defs[i].Tier == tier && defs[i].Price > 0 {
|
|
return slot, &defs[i], true
|
|
}
|
|
}
|
|
return "", nil, false
|
|
}
|
|
|
|
// ── Buy Equipment ────────────────────────────────────────────────────────────
|
|
|
|
func (p *AdventurePlugin) advBuyEquipment(userID id.UserID, slot EquipmentSlot, def *EquipmentDef, equip map[EquipmentSlot]*AdvEquipment) string {
|
|
current := equip[slot]
|
|
if current != nil && current.Tier >= def.Tier {
|
|
return fmt.Sprintf("You already have %s (Tier %d). %s is Tier %d. That's not an upgrade, that's a lateral move at best.",
|
|
current.Name, current.Tier, def.Name, def.Tier)
|
|
}
|
|
// Block shop purchases that would overwrite arena or masterwork gear
|
|
if current != nil && (current.ArenaTier > 0 || current.Masterwork) {
|
|
label := "Masterwork"
|
|
if current.ArenaTier > 0 {
|
|
label = "Arena"
|
|
}
|
|
return fmt.Sprintf("You already have **%s** (%s gear). A shop item cannot replace this. "+
|
|
"You earned that. Don't throw it away.",
|
|
current.Name, label)
|
|
}
|
|
|
|
balance := p.euro.GetBalance(userID)
|
|
if balance < def.Price {
|
|
return fmt.Sprintf("You cannot afford %s. You have €%.0f. %s costs €%.0f. "+
|
|
"The gap between these numbers is both mathematical and deeply personal. "+
|
|
"Go do something about it. Not gambling. You know what I mean.",
|
|
def.Name, balance, def.Name, def.Price)
|
|
}
|
|
|
|
if !p.euro.Debit(userID, def.Price, "adventure_shop_"+string(slot)) {
|
|
return "Transaction failed. The economy is having a moment."
|
|
}
|
|
|
|
// Update equipment
|
|
eq := &AdvEquipment{
|
|
Slot: slot,
|
|
Tier: def.Tier,
|
|
Condition: 100,
|
|
Name: def.Name,
|
|
ActionsUsed: 0,
|
|
}
|
|
if err := saveAdvEquipment(userID, eq); err != nil {
|
|
// Refund on DB error
|
|
p.euro.Credit(userID, def.Price, "adventure_shop_refund")
|
|
return "Something went wrong saving your equipment. You've been refunded."
|
|
}
|
|
equip[slot] = eq
|
|
|
|
return fmt.Sprintf("You have purchased **%s** for €%.0f.\n\n_%s_\n\n"+
|
|
"This is an upgrade over what you had. That bar was underground, but you cleared it. "+
|
|
"Equip it now before you do something stupid with the money instead.",
|
|
def.Name, def.Price, def.Description)
|
|
}
|
|
|
|
// ── Sell Items ────────────────────────────────────────────────────────────────
|
|
|
|
func (p *AdventurePlugin) advSellAll(userID id.UserID) string {
|
|
items, err := loadAdvInventory(userID)
|
|
if err != nil {
|
|
return "Failed to access your inventory. Try again."
|
|
}
|
|
if len(items) == 0 {
|
|
return "Your inventory is empty. There is nothing to sell. This is a metaphor for something but now is not the time."
|
|
}
|
|
|
|
var total float64
|
|
var sold int
|
|
var keptSpecial int
|
|
for _, item := range items {
|
|
if item.Type == "MasterworkGear" || item.Type == "ArenaGear" {
|
|
keptSpecial++
|
|
continue
|
|
}
|
|
if err := removeAdvInventoryItem(item.ID); err != nil {
|
|
continue
|
|
}
|
|
total += float64(item.Value)
|
|
sold++
|
|
}
|
|
|
|
if sold == 0 {
|
|
if keptSpecial > 0 {
|
|
return "Your inventory only contains Masterwork and Arena gear. The merchant doesn't deal in that. Use `!adventure equip` instead."
|
|
}
|
|
return "Your inventory is empty. There is nothing to sell. This is a metaphor for something but now is not the time."
|
|
}
|
|
|
|
p.euro.Credit(userID, total, "adventure_sell_all")
|
|
|
|
msg := fmt.Sprintf("Sold %d items for **€%.0f**.\n\nThe merchant took everything without comment. "+
|
|
"This is the most respect anyone has shown your loot collection. Take the money.",
|
|
sold, total)
|
|
if keptSpecial > 0 {
|
|
msg += fmt.Sprintf("\n\n(%d special gear items kept — the merchant knows better than to touch those.)", keptSpecial)
|
|
}
|
|
return msg
|
|
}
|
|
|
|
func (p *AdventurePlugin) advSellItem(userID id.UserID, itemName string) string {
|
|
items, err := loadAdvInventory(userID)
|
|
if err != nil {
|
|
return "Failed to access your inventory."
|
|
}
|
|
|
|
// Fuzzy match
|
|
for _, item := range items {
|
|
if containsFold(item.Name, itemName) {
|
|
if item.Type == "MasterworkGear" || item.Type == "ArenaGear" {
|
|
return fmt.Sprintf("**%s** is special gear. The merchant won't touch it. Use `!adventure equip` to equip it.", item.Name)
|
|
}
|
|
if err := removeAdvInventoryItem(item.ID); err != nil {
|
|
return "Failed to sell that item."
|
|
}
|
|
p.euro.Credit(userID, float64(item.Value), "adventure_sell_"+item.Name)
|
|
return fmt.Sprintf("Sold **%s** for **€%d**. The merchant nodded. That's it. That's the transaction.", item.Name, item.Value)
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf("No item matching '%s' found in your inventory.", itemName)
|
|
}
|
|
|
|
// ── Inventory Display ────────────────────────────────────────────────────────
|
|
|
|
func advInventoryDisplay(userID id.UserID) string {
|
|
items, err := loadAdvInventory(userID)
|
|
if err != nil {
|
|
return "Failed to load inventory."
|
|
}
|
|
if len(items) == 0 {
|
|
return "🎒 Your inventory is empty. Go adventure."
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString("🎒 **Inventory**\n\n")
|
|
|
|
var total int64
|
|
for _, item := range items {
|
|
if item.Type == "MasterworkGear" {
|
|
sb.WriteString(fmt.Sprintf(" ⭐ %s (T%d Masterwork %s)\n", item.Name, item.Tier, slotTitle(item.Slot)))
|
|
} else if item.Type == "ArenaGear" {
|
|
sb.WriteString(fmt.Sprintf(" ⚔️ %s (T%d Arena %s)\n", item.Name, item.Tier, slotTitle(item.Slot)))
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf(" %s (T%d %s) — €%d\n", item.Name, item.Tier, item.Type, item.Value))
|
|
total += item.Value
|
|
}
|
|
}
|
|
sb.WriteString(fmt.Sprintf("\n%d items — sellable value ~€%d", len(items), total))
|
|
sb.WriteString("\n\nTo sell: `!adventure sell all` or `!adventure sell <item>`")
|
|
sb.WriteString("\nTo equip Masterwork gear: `!adventure equip`")
|
|
return sb.String()
|
|
}
|