Fix arena gear lockout, extend DM window, scrub mining flavor

Arena helmet auto-equip now preserves the existing helmet to inventory
(or stashes the new drop if the existing piece is strictly higher tier),
and arena gear can be re-equipped via !adventure equip. Shop only blocks
swaps that aren't an upgrade over current arena tier. DM response window
bumped from 15m to 3h so NPC/treasure/shop prompts don't expire while
players are AFK. MiningSuccess flavor pools no longer name specific ores
in prose — uses {ore}/{location}/{tool} placeholders so the narration
matches the actual loot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-04-24 22:23:58 -07:00
parent d77c2ebbbb
commit be76973fd2
7 changed files with 135 additions and 61 deletions

View File

@@ -46,7 +46,7 @@ func (p *AdventurePlugin) advUserLock(userID id.UserID) *sync.Mutex {
return val.(*sync.Mutex) return val.(*sync.Mutex)
} }
const advDMResponseWindow = 15 * time.Minute const advDMResponseWindow = 3 * time.Hour
// advMarkMenuSent records that an actionable adventure menu was DM'd to the user. // advMarkMenuSent records that an actionable adventure menu was DM'd to the user.
// Only bare-number DM replies within this window will be treated as adventure choices. // Only bare-number DM replies within this window will be treated as adventure choices.
@@ -1108,7 +1108,7 @@ func (p *AdventurePlugin) checkTreasureDrop(userID id.UserID, char *AdventureCha
NewTreasure: drop.Def, NewTreasure: drop.Def,
Existing: existing, Existing: existing,
}, },
ExpiresAt: time.Now().Add(5 * time.Minute), ExpiresAt: time.Now().Add(advDMResponseWindow),
}) })
text := renderAdvTreasureDiscardPrompt(drop.Def, existing) text := renderAdvTreasureDiscardPrompt(drop.Def, existing)

View File

@@ -1040,6 +1040,15 @@ func arenaGearByTier(tier int) *ArenaGearSet {
return &arenaGearSets[tier-1] return &arenaGearSets[tier-1]
} }
func arenaGearByName(name string) *ArenaGearSet {
for i := range arenaGearSets {
if arenaGearSets[i].HelmetName == name {
return &arenaGearSets[i]
}
}
return nil
}
// arenaRollHelmetDrop checks if a helmet should drop and equips it if the player // arenaRollHelmetDrop checks if a helmet should drop and equips it if the player
// doesn't already have an arena helmet at this tier or higher. If they do, the // doesn't already have an arena helmet at this tier or higher. If they do, the
// drop is silently discarded (no duplicate drops). // drop is silently discarded (no duplicate drops).
@@ -1063,14 +1072,49 @@ func (p *AdventurePlugin) arenaRollHelmetDrop(userID id.UserID, tier int) *Arena
} }
helmet, hasHelmet := equip[SlotHelmet] helmet, hasHelmet := equip[SlotHelmet]
if hasHelmet && (helmet.ArenaTier >= tier || helmet.Tier > tier) { hasRealHelmet := hasHelmet && helmet.Tier > 0
// Already has same-or-better arena helmet, or a higher-tier normal helmet — discard
// Discard only if existing is a same-or-better arena helmet (duplicate suppression).
if hasRealHelmet && helmet.ArenaTier >= tier {
return nil return nil
} }
// Equip the arena helmet // If existing is a strictly better non-arena helmet, stash the drop in inventory
// so the player can equip it later via !adventure equip rather than overwriting.
if hasRealHelmet && helmet.ArenaTier == 0 && helmet.Tier > tier {
if err := addAdvInventoryItem(userID, AdvItem{
Name: gear.HelmetName,
Type: "ArenaGear",
Tier: tier,
Slot: SlotHelmet,
}); err != nil {
slog.Error("arena: failed to stash arena helmet drop", "user", userID, "err", err)
return nil
}
return gear
}
// Auto-equip: move the old helmet to inventory first (preserving its flavor)
// so the player doesn't silently lose it.
if hasRealHelmet {
oldType := "ShopGear"
if helmet.Masterwork {
oldType = "MasterworkGear"
} else if helmet.ArenaTier > 0 {
oldType = "ArenaGear"
}
if err := addAdvInventoryItem(userID, AdvItem{
Name: helmet.Name,
Type: oldType,
Tier: helmet.Tier,
Slot: SlotHelmet,
SkillSource: helmet.SkillSource,
}); err != nil {
slog.Error("arena: failed to move old helmet to inventory", "user", userID, "err", err)
}
}
if !hasHelmet { if !hasHelmet {
// Shouldn't happen (all slots created at character creation), but be safe
helmet = &AdvEquipment{Slot: SlotHelmet} helmet = &AdvEquipment{Slot: SlotHelmet}
} }
helmet.Tier = tier helmet.Tier = tier
@@ -1079,6 +1123,8 @@ func (p *AdventurePlugin) arenaRollHelmetDrop(userID id.UserID, tier int) *Arena
helmet.ActionsUsed = 0 helmet.ActionsUsed = 0
helmet.ArenaTier = tier helmet.ArenaTier = tier
helmet.ArenaSet = gear.SetKey helmet.ArenaSet = gear.SetKey
helmet.Masterwork = false
helmet.SkillSource = ""
if err := saveAdvEquipment(userID, helmet); err != nil { if err := saveAdvEquipment(userID, helmet); err != nil {
slog.Error("arena: failed to save arena helmet drop", "user", userID, "err", err) slog.Error("arena: failed to save arena helmet drop", "user", userID, "err", err)

View File

@@ -363,9 +363,9 @@ var MiningSuccess = map[int][]string{
"In and out. {ore}, €{value}, zero cave-ins, one bat that thought about it " + "In and out. {ore}, €{value}, zero cave-ins, one bat that thought about it " +
"and decided not to. Your {tool} is slightly worse. Everything else is better. " + "and decided not to. Your {tool} is slightly worse. Everything else is better. " +
"By Surface Pits standards, a professional operation.", "By {location} standards, a professional operation.",
"The copper vein ran further than the map suggested. Further and richer. " + "The seam ran further than the map suggested. Further and richer. " +
"You took {ore} worth €{value} and left more than you could carry. " + "You took {ore} worth €{value} and left more than you could carry. " +
"This is the correct response to a rich vein. Come back tomorrow. " + "This is the correct response to a rich vein. Come back tomorrow. " +
"Bring a bigger bag. Bring a better pickaxe.", "Bring a bigger bag. Bring a better pickaxe.",
@@ -380,68 +380,69 @@ var MiningSuccess = map[int][]string{
"and the rock rewarded you for it. This is the entire transaction. " + "and the rock rewarded you for it. This is the entire transaction. " +
"It is, when it works, genuinely satisfying.", "It is, when it works, genuinely satisfying.",
"The tin seam yielded more than expected and the coal beneath it " + "The upper seam yielded more than expected and the layer beneath it " +
"turned out to have a copper pocket behind it " + "turned out to have a pocket behind it " +
"that wasn't on any map because nobody had gone that far before. " + "that wasn't on any map because nobody had gone that far before. " +
"You went that far. {ore} total, €{value}. " + "You went that far. {ore} total, €{value}. " +
"The {tool} is worse. The discovery is better.", "The {tool} is worse. The discovery is better.",
}, },
2: { 2: {
"Iron Ridge delivered today. {ore} extracted, €{value} assessed, " + "{location} delivered today. {ore} extracted, €{value} assessed, " +
"cave troll acknowledged and navigated around with something approaching grace. " + "cave troll acknowledged and navigated around with something approaching grace. " +
"The {tool} performed admirably. Your back has filed a formal complaint. " + "The {tool} performed admirably. Your back has filed a formal complaint. " +
"The back's complaint is noted and will not change the outcome.", "The back's complaint is noted and will not change the outcome.",
"A good seam. A real seam — proper iron, " + "A good seam. A real seam — the kind where the rock splits cleanly " +
"the kind where the rock splits cleanly and the ore sits in it " + "and the ore sits in it like it's been waiting. " +
"like it's been waiting. {ore} worth €{value}. {xp} XP. " + "{ore} worth €{value}. {xp} XP. " +
"This is what mining is supposed to feel like. " + "This is what mining is supposed to feel like. " +
"Remember it for the days it doesn't.", "Remember it for the days it doesn't.",
"The lead pocket was deeper than the iron, which meant going deeper " + "The deeper pocket was further in than the surface yield suggested, " +
"than planned, which meant more exposure to everything Iron Ridge contains. " + "which meant going deeper than planned, " +
"which meant more exposure to everything {location} contains. " +
"You made it. {ore} worth €{value}. The depth was worth it. " + "You made it. {ore} worth €{value}. The depth was worth it. " +
"The {tool} has opinions about the depth. The opinions are structural.", "The {tool} has opinions about the depth. The opinions are structural.",
}, },
3: { 3: {
"Silver. Real silver, properly embedded, not quartz, not illusion, " + "Whatever you came down here for, you took it. No illusions, no " +
"not iron-coloured rock lying about its nature. Silver. " + "iron-coloured rock lying about its nature. " +
"{ore} worth €{value}. {xp} XP. Your {tool} is a real tool " + "{ore} worth €{value}. {xp} XP. Your {tool} is a real tool " +
"and this was a real seam and you extracted the silver. " + "and this was a real seam and you extracted what was in it. " +
"That's the whole job. Today the whole job got done.", "That's the whole job. Today the whole job got done.",
"The Silver Seam at depth, past the quartz formations, " + "{location} at depth, past the decorative formations, " +
"past the section that looks better than it is, " + "past the section that looks better than it is, " +
"down to where the silver sits in the rock like an argument for effort. " + "down to where the yield sits in the rock like an argument for effort. " +
"{ore}, €{value}, {xp} XP, home by sunset. " + "{ore}, €{value}, {xp} XP, home by sunset. " +
"A good day. In a mine. Those exist.", "A good day. In a mine. Those exist.",
}, },
4: { 4: {
"Deeprock gold. Actual gold, not pyrite, not gold-coloured copper, " + "Proper haul. No pyrite, no gold-coloured copper, " +
"not optimism in mineral form. Gold. {ore} worth €{value}. " + "no optimism in mineral form. {ore} worth €{value}. " +
"The Deeprock tried to discourage you with atmosphere and pressure. " + "{location} tried to discourage you with atmosphere and pressure. " +
"You brought a Mithril Pickaxe and ignored the atmosphere.", "You brought a {tool} and ignored the atmosphere.",
"You went to the fourth level of the Deeprock and you came back " + "You went to the fourth level of {location} and you came back " +
"with {ore} worth €{value} and {xp} XP and a story " + "with {ore} worth €{value} and {xp} XP and a story " +
"that mostly involves how far down the fourth level is. " + "that mostly involves how far down the fourth level is. " +
"It is very far down. The ore was worth being very far down. Today.", "It is very far down. The ore was worth being very far down. Today.",
}, },
5: { 5: {
"Mythril. You found mythril and extracted mythril " + "You found the seam and you extracted the seam " +
"and brought mythril home and mythril is worth €{value} per unit. " + "and brought the seam home. " +
"The {ore} you're carrying is worth €{value} total. " + "{ore} worth €{value} total. " +
"{xp} XP. The Diamond Pickaxe is a Diamond Pickaxe for a reason. " + "{xp} XP. The {tool} is a {tool} for a reason. " +
"Today you proved the reason.", "Today you proved the reason.",
"The Voidstone was present but passive today. " + "The Voidstone was present but passive today. " +
"The Dragon Crystals were warm but not hostile. " + "The Dragon Crystals were warm but not hostile. " +
"The mythril seam was open and you took {ore} worth €{value} from it. " + "The seam was open and you took {ore} worth €{value} from it. " +
"{xp} XP. The Mythril Caverns had a good day at the same time you did. " + "{xp} XP. {location} had a good day at the same time you did. " +
"This does not happen often. Don't examine it. Take the ore home.", "This does not happen often. Don't examine it. Take the ore home.",
}, },
} }

View File

@@ -217,7 +217,7 @@ func thomShopView(char *AdventureCharacter, balance float64) string {
sb.WriteString(fmt.Sprintf(" Closing costs (5%%): €%d\n", int(float64(def.BasePrice)*0.05))) sb.WriteString(fmt.Sprintf(" Closing costs (5%%): €%d\n", int(float64(def.BasePrice)*0.05)))
sb.WriteString(fmt.Sprintf(" Realtor fees (6%%): €%d\n", int(float64(def.BasePrice)*0.06))) sb.WriteString(fmt.Sprintf(" Realtor fees (6%%): €%d\n", int(float64(def.BasePrice)*0.06)))
sb.WriteString(fmt.Sprintf(" **Total: €%d**\n\n", total)) sb.WriteString(fmt.Sprintf(" **Total: €%d**\n\n", total))
sb.WriteString(fmt.Sprintf("Reply `buy` to purchase, or `buy <amount>` for a down payment.\n")) sb.WriteString(fmt.Sprintf("`!thom buy` to purchase outright, or `!thom buy <amount>` for a down payment.\n"))
} }
} }

View File

@@ -333,16 +333,16 @@ func (p *AdventurePlugin) handleEquipCmd(ctx MessageContext) error {
return p.SendDM(ctx.Sender, "Failed to load inventory.") return p.SendDM(ctx.Sender, "Failed to load inventory.")
} }
// Filter to masterwork items only // Filter to special gear (Masterwork + Arena) items only
var mwItems []AdvItem var mwItems []AdvItem
for _, it := range items { for _, it := range items {
if it.Type == "MasterworkGear" { if it.Type == "MasterworkGear" || it.Type == "ArenaGear" {
mwItems = append(mwItems, it) mwItems = append(mwItems, it)
} }
} }
if len(mwItems) == 0 { if len(mwItems) == 0 {
return p.SendDM(ctx.Sender, "You have no Masterwork gear waiting to be equipped. Go find some.") return p.SendDM(ctx.Sender, "You have no special gear waiting to be equipped. Go find some.")
} }
equip, err := loadAdvEquipment(ctx.Sender) equip, err := loadAdvEquipment(ctx.Sender)
@@ -352,7 +352,7 @@ func (p *AdventurePlugin) handleEquipCmd(ctx MessageContext) error {
// Build listing // Build listing
var sb strings.Builder var sb strings.Builder
sb.WriteString("⭐ **Your unequipped Masterwork gear:**\n\n") sb.WriteString("⭐ **Your unequipped special gear:**\n\n")
for i, it := range mwItems { for i, it := range mwItems {
current := equip[it.Slot] current := equip[it.Slot]
@@ -366,8 +366,12 @@ func (p *AdventurePlugin) handleEquipCmd(ctx MessageContext) error {
} }
currentDesc = fmt.Sprintf("%s (%s)", current.Name, tag) currentDesc = fmt.Sprintf("%s (%s)", current.Name, tag)
} }
sb.WriteString(fmt.Sprintf("%d. %s (T%d %s) — currently: %s\n", marker := "⭐"
i+1, it.Name, it.Tier, slotTitle(it.Slot), currentDesc)) if it.Type == "ArenaGear" {
marker = "⚔️"
}
sb.WriteString(fmt.Sprintf("%d. %s %s (T%d %s) — currently: %s\n",
i+1, marker, it.Name, it.Tier, slotTitle(it.Slot), currentDesc))
} }
sb.WriteString("\nReply with a number to equip, or \"cancel\".") sb.WriteString("\nReply with a number to equip, or \"cancel\".")
@@ -375,7 +379,7 @@ func (p *AdventurePlugin) handleEquipCmd(ctx MessageContext) error {
p.pending.Store(string(ctx.Sender), &advPendingInteraction{ p.pending.Store(string(ctx.Sender), &advPendingInteraction{
Type: "masterwork_equip", Type: "masterwork_equip",
Data: &advPendingMasterworkEquip{Items: mwItems}, Data: &advPendingMasterworkEquip{Items: mwItems},
ExpiresAt: time.Now().Add(5 * time.Minute), ExpiresAt: time.Now().Add(advDMResponseWindow),
}) })
return p.SendDM(ctx.Sender, sb.String()) return p.SendDM(ctx.Sender, sb.String())
@@ -430,17 +434,22 @@ func (p *AdventurePlugin) handleMasterworkEquipReply(ctx MessageContext, interac
p.pending.Store(string(ctx.Sender), &advPendingInteraction{ p.pending.Store(string(ctx.Sender), &advPendingInteraction{
Type: "masterwork_equip_confirm", Type: "masterwork_equip_confirm",
Data: &advPendingMasterworkConfirm{Item: selected}, Data: &advPendingMasterworkConfirm{Item: selected},
ExpiresAt: time.Now().Add(5 * time.Minute), ExpiresAt: time.Now().Add(advDMResponseWindow),
}) })
def := masterworkDefFor(slotToActivity(selected.Slot), selected.Tier)
bonusDesc := "" bonusDesc := ""
if def != nil { marker := "⭐"
bonusDesc = fmt.Sprintf("\n%s gives 1.25x %s effectiveness and +5%% %s success.\n", selected.Name, slotTitle(selected.Slot), def.SkillSource) if selected.Type == "ArenaGear" {
marker = "⚔️"
} else {
def := masterworkDefFor(slotToActivity(selected.Slot), selected.Tier)
if def != nil {
bonusDesc = fmt.Sprintf("\n%s gives 1.25x %s effectiveness and +5%% %s success.\n", selected.Name, slotTitle(selected.Slot), def.SkillSource)
}
} }
return p.SendDM(ctx.Sender, fmt.Sprintf("Equip **%s** (T%d %s)?%s\nThis replaces your %s. The old item goes to inventory.%s\nReply \"yes\" to confirm.", return p.SendDM(ctx.Sender, fmt.Sprintf("Equip **%s** (T%d %s %s)?%s\nThis replaces your %s. The old item goes to inventory.%s\nReply \"yes\" to confirm.",
selected.Name, selected.Tier, slotTitle(selected.Slot), warning, currentName, bonusDesc)) selected.Name, selected.Tier, marker, slotTitle(selected.Slot), warning, currentName, bonusDesc))
} }
func (p *AdventurePlugin) handleMasterworkEquipConfirm(ctx MessageContext, interaction *advPendingInteraction) error { func (p *AdventurePlugin) handleMasterworkEquipConfirm(ctx MessageContext, interaction *advPendingInteraction) error {
@@ -488,10 +497,21 @@ func (p *AdventurePlugin) handleMasterworkEquipConfirm(ctx MessageContext, inter
eq.Condition = 100 eq.Condition = 100
eq.Name = selected.Name eq.Name = selected.Name
eq.ActionsUsed = 0 eq.ActionsUsed = 0
eq.ArenaTier = 0
eq.ArenaSet = "" if selected.Type == "ArenaGear" {
eq.Masterwork = true eq.Masterwork = false
eq.SkillSource = selected.SkillSource eq.SkillSource = ""
eq.ArenaTier = selected.Tier
eq.ArenaSet = ""
if gs := arenaGearByName(selected.Name); gs != nil {
eq.ArenaSet = gs.SetKey
}
} else {
eq.ArenaTier = 0
eq.ArenaSet = ""
eq.Masterwork = true
eq.SkillSource = selected.SkillSource
}
if err := saveAdvEquipment(ctx.Sender, eq); err != nil { if err := saveAdvEquipment(ctx.Sender, eq); err != nil {
return p.SendDM(ctx.Sender, "Failed to save equipment. Try again.") return p.SendDM(ctx.Sender, "Failed to save equipment. Try again.")
@@ -502,7 +522,11 @@ func (p *AdventurePlugin) handleMasterworkEquipConfirm(ctx MessageContext, inter
slog.Error("adventure: failed to remove equipped item from inventory", "user", ctx.Sender, "err", err) slog.Error("adventure: failed to remove equipped item from inventory", "user", ctx.Sender, "err", err)
} }
return p.SendDM(ctx.Sender, fmt.Sprintf("**%s** equipped. ⭐ Masterwork %s, Tier %d.", selected.Name, slotTitle(selected.Slot), selected.Tier)) kind := "⭐ Masterwork"
if selected.Type == "ArenaGear" {
kind = "⚔️ Arena"
}
return p.SendDM(ctx.Sender, fmt.Sprintf("**%s** equipped. %s %s, Tier %d.", selected.Name, kind, slotTitle(selected.Slot), selected.Tier))
} }
// slotToActivity converts an EquipmentSlot to the activity that drops masterwork for it. // slotToActivity converts an EquipmentSlot to the activity that drops masterwork for it.

View File

@@ -158,7 +158,7 @@ func (p *AdventurePlugin) npcFireEncounter(userID id.UserID, npc string) {
p.pending.Store(string(userID), &advPendingInteraction{ p.pending.Store(string(userID), &advPendingInteraction{
Type: "npc_encounter", Type: "npc_encounter",
Data: npc, Data: npc,
ExpiresAt: time.Now().Add(5 * time.Minute), ExpiresAt: time.Now().Add(advDMResponseWindow),
}) })
if err := p.SendDM(userID, prompt); err != nil { if err := p.SendDM(userID, prompt); err != nil {

View File

@@ -417,10 +417,10 @@ func (p *AdventurePlugin) resolveShopItemChoice(ctx MessageContext, interaction
// Check Arena/Masterwork block — only block if shop item isn't clearly better. // Check Arena/Masterwork block — only block if shop item isn't clearly better.
// Arena gear always blocks (1.5x multiplier, earned through combat). // Arena gear always blocks (1.5x multiplier, earned through combat).
// Masterwork blocks only if the shop item's tier doesn't exceed the MW effective tier. // Masterwork blocks only if the shop item's tier doesn't exceed the MW effective tier.
if current != nil && current.ArenaTier > 0 { if current != nil && current.ArenaTier > 0 && def.Tier <= current.ArenaTier {
p.pending.Store(string(ctx.Sender), interaction) p.pending.Store(string(ctx.Sender), interaction)
return p.SendDM(ctx.Sender, fmt.Sprintf("You have **%s** (Arena gear) in that slot. A shop item can't replace that. You earned it.\n\nPick something else, or \"back\" to return.", return p.SendDM(ctx.Sender, fmt.Sprintf("You have **%s** (Arena gear, T%d) in that slot. That T%d shop item isn't an upgrade.\n\nPick something else, or \"back\" to return.",
current.Name)) current.Name, current.ArenaTier, def.Tier))
} }
if current != nil && current.Masterwork && float64(def.Tier) <= advEffectiveTier(current) { if current != nil && current.Masterwork && float64(def.Tier) <= advEffectiveTier(current) {
p.pending.Store(string(ctx.Sender), interaction) p.pending.Store(string(ctx.Sender), interaction)
@@ -479,8 +479,8 @@ func (p *AdventurePlugin) resolveShopConfirm(ctx MessageContext, interaction *ad
current := equip[data.Slot] current := equip[data.Slot]
// Re-check Arena/Masterwork block. // Re-check Arena/Masterwork block.
if current != nil && current.ArenaTier > 0 { if current != nil && current.ArenaTier > 0 && def.Tier <= current.ArenaTier {
return p.SendDM(ctx.Sender, fmt.Sprintf("Your **%s** is Arena gear. Luigi won't overwrite it.", current.Name)) return p.SendDM(ctx.Sender, fmt.Sprintf("Your **%s** (Arena gear T%d) is not worse than that T%d shop item.", current.Name, current.ArenaTier, def.Tier))
} }
if current != nil && current.Masterwork && float64(def.Tier) <= advEffectiveTier(current) { if current != nil && current.Masterwork && float64(def.Tier) <= advEffectiveTier(current) {
return p.SendDM(ctx.Sender, fmt.Sprintf("Your **%s** (Masterwork ⭐) is better than that T%d shop item.", current.Name, def.Tier)) return p.SendDM(ctx.Sender, fmt.Sprintf("Your **%s** (Masterwork ⭐) is better than that T%d shop item.", current.Name, def.Tier))
@@ -510,6 +510,8 @@ func (p *AdventurePlugin) resolveShopConfirm(ctx MessageContext, interaction *ad
if current.Masterwork { if current.Masterwork {
itemType = "MasterworkGear" itemType = "MasterworkGear"
// Masterwork has no resale value (it's special, not shop-priced) // Masterwork has no resale value (it's special, not shop-priced)
} else if current.ArenaTier > 0 {
itemType = "ArenaGear"
} else if current.Tier < len(equipmentTiers[data.Slot]) { } else if current.Tier < len(equipmentTiers[data.Slot]) {
resaleValue = int64(equipmentTiers[data.Slot][current.Tier].Price * 0.3) resaleValue = int64(equipmentTiers[data.Slot][current.Tier].Price * 0.3)
} }
@@ -682,10 +684,9 @@ func (p *AdventurePlugin) advBuyEquipment(userID id.UserID, slot EquipmentSlot,
} }
// Block shop purchases that would overwrite arena gear (always) or // Block shop purchases that would overwrite arena gear (always) or
// masterwork gear (only if shop item isn't clearly better). // masterwork gear (only if shop item isn't clearly better).
if current != nil && current.ArenaTier > 0 { if current != nil && current.ArenaTier > 0 && def.Tier <= current.ArenaTier {
return fmt.Sprintf("You already have **%s** (Arena gear). A shop item cannot replace this. "+ return fmt.Sprintf("You already have **%s** (Arena gear T%d). That T%d shop item isn't an upgrade.",
"You earned that. Don't throw it away.", current.Name, current.ArenaTier, def.Tier)
current.Name)
} }
if current != nil && current.Masterwork && float64(def.Tier) <= advEffectiveTier(current) { if current != nil && current.Masterwork && float64(def.Tier) <= advEffectiveTier(current) {
return fmt.Sprintf("You already have **%s** (Masterwork ⭐ T%d). That T%d shop item isn't an upgrade.", return fmt.Sprintf("You already have **%s** (Masterwork ⭐ T%d). That T%d shop item isn't an upgrade.",
@@ -714,6 +715,8 @@ func (p *AdventurePlugin) advBuyEquipment(userID id.UserID, slot EquipmentSlot,
var resaleValue int64 var resaleValue int64
if current.Masterwork { if current.Masterwork {
itemType = "MasterworkGear" itemType = "MasterworkGear"
} else if current.ArenaTier > 0 {
itemType = "ArenaGear"
} else if current.Tier < len(equipmentTiers[slot]) { } else if current.Tier < len(equipmentTiers[slot]) {
resaleValue = int64(equipmentTiers[slot][current.Tier].Price * 0.3) resaleValue = int64(equipmentTiers[slot][current.Tier].Price * 0.3)
} }