Files
gogobee/internal/plugin/adventure_shop_test.go
prosolis 68b2f8b7a5 Add chat level perks: XP bonus, shop flavor, rare drop nudge, death pardon (Adventure 2.5 steps 3-6)
- XP bonus: +5% per 10 chat levels (cap +25% at 50+), applies to all
  adventure and arena XP
- Shop flavor: shopkeeper greeting changes at chat levels 10/30/50,
  replacing random flavor for recognized players
- Rare drop nudge: +0.5% per 10 chat levels (cap +2.5%) applied
  additively to treasure and masterwork drop rates
- Death pardon: chat level 20+ gets 33% chance to survive death once
  per 7 days (converts to bad-failure, quiet DM). Does not apply in
  arena. Fires before Sovereign Death's Reprieve check.
- New adventure_chat_perks.go with perk calculation helpers
- LastPardonUsed field + migration on adventure_characters

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 15:50:28 -07:00

354 lines
11 KiB
Go
Raw Blame History

package plugin
import (
"strings"
"testing"
)
// ── Display Tests ───────────────────────────────────────────────────────────
func TestLuigiShopGreeting_Basic(t *testing.T) {
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 2, Condition: 100},
}
text := luigiShopGreeting("@user:test", equip, 5000, false, 0)
if !strings.Contains(text, "Luigi's") {
t.Error("greeting should contain Luigi's")
}
if !strings.Contains(text, "€5000") {
t.Error("greeting should show balance")
}
if !strings.Contains(text, "Weapons") {
t.Error("greeting should show category grid")
}
if !strings.Contains(text, "Reply with a category") {
t.Error("greeting should prompt for category")
}
}
func TestLuigiShopGreeting_MaxedOut(t *testing.T) {
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 5, Condition: 100},
SlotArmor: {Tier: 5, Condition: 100},
SlotHelmet: {Tier: 5, Condition: 100},
SlotBoots: {Tier: 5, Condition: 100},
SlotTool: {Tier: 5, Condition: 100},
}
text := luigiShopGreeting("@user:test", equip, 99999, false, 0)
// Should NOT show category grid.
if strings.Contains(text, "Reply with a category") {
t.Error("maxed-out greeting should not show category prompt")
}
// Should use maxed-out flavor.
if !strings.Contains(text, "fully kitted") {
t.Error("maxed-out greeting should mention being fully kitted")
}
}
func TestLuigiShopGreeting_ShowAll(t *testing.T) {
equip := map[EquipmentSlot]*AdvEquipment{}
text := luigiShopGreeting("@user:test", equip, 1000, true, 0)
// Should contain a show-all comment.
if !strings.Contains(text, "judgment") && !strings.Contains(text, "thoroughness") {
t.Error("show all greeting should contain a show-all comment")
}
}
func TestLuigiCategoryView_UpgradeIndicators(t *testing.T) {
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 2, Condition: 100, Name: "Dull Steel Sword of Mediocrity"},
}
text := luigiCategoryView("@user:test", SlotWeapon, equip, 50000, false)
// Should show current equipped.
if !strings.Contains(text, "🟢") {
t.Error("should show 🟢 for currently equipped item")
}
// Should show upgrade indicators.
if !strings.Contains(text, "⬆️") {
t.Error("should show ⬆️ for items above current tier")
}
// Should NOT show T1 (below current tier) by default.
if strings.Contains(text, "Sad Iron Sword") {
t.Error("should not show items below current tier when showAll=false")
}
}
func TestLuigiCategoryView_ShowAll(t *testing.T) {
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 3, Condition: 100, Name: "Sword (It's Fine)"},
}
text := luigiCategoryView("@user:test", SlotWeapon, equip, 50000, true)
// Should show items below current tier.
if !strings.Contains(text, "Sad Iron Sword") {
t.Error("show all should include items below current tier")
}
// Below-tier items should show ⬇️.
if !strings.Contains(text, "⬇️") {
t.Error("show all should show ⬇️ for items below current tier")
}
}
func TestLuigiCategoryView_Affordability(t *testing.T) {
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 0, Condition: 100, Name: "Basic Ass Sword"},
}
// Can afford T1 (€100) but not T2 (€450).
text := luigiCategoryView("@user:test", SlotWeapon, equip, 200, false)
if !strings.Contains(text, "✅") {
t.Error("should show ✅ for affordable items")
}
if !strings.Contains(text, "💸") {
t.Error("should show 💸 for unaffordable items")
}
}
func TestLuigiCategoryView_MasterworkAck(t *testing.T) {
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 3, Condition: 100, Name: "MW Blade", Masterwork: true},
}
text := luigiCategoryView("@user:test", SlotWeapon, equip, 50000, false)
if !strings.Contains(text, "⭐") {
t.Error("should show ⭐ for masterwork gear")
}
// The masterwork acknowledgement pool has varied text — just check it's referenced.
hasMWAck := strings.Contains(text, "Masterwork") || strings.Contains(text, "Arena gear") || strings.Contains(text, "can't beat that")
if !hasMWAck {
t.Error("should acknowledge masterwork/arena gear")
}
}
func TestLuigiCategoryView_MaxedSlot(t *testing.T) {
equip := map[EquipmentSlot]*AdvEquipment{
SlotWeapon: {Tier: 5, Condition: 100, Name: "Vorpal Sword"},
}
text := luigiCategoryView("@user:test", SlotWeapon, equip, 50000, false)
if !strings.Contains(text, "max tier") {
t.Error("should show max-tier message when nothing to buy")
}
}
// ── Item Confirm Tests ──────────────────────────────────────────────<E29480><E29480><EFBFBD>───────
func TestLuigiItemConfirm_Upgrade(t *testing.T) {
def := &EquipmentDef{Name: "Silver Blade", Tier: 3, Price: 1500}
current := &AdvEquipment{Tier: 2, Name: "Dull Steel Sword"}
text := luigiItemConfirm("@user:test", SlotWeapon, def, current, 5000)
if !strings.Contains(text, "⬆️") {
t.Error("should show upgrade indicator")
}
if !strings.Contains(text, "upgrade from") {
t.Error("should mention it's an upgrade")
}
if !strings.Contains(text, "€5000") {
t.Error("should show current balance")
}
if !strings.Contains(text, "€3500") {
t.Error("should show balance after purchase")
}
if !strings.Contains(text, "Confirm?") {
t.Error("should prompt for confirmation")
}
}
func TestLuigiItemConfirm_NoCurrent(t *testing.T) {
def := &EquipmentDef{Name: "Sad Iron Sword", Tier: 1, Price: 100}
text := luigiItemConfirm("@user:test", SlotWeapon, def, nil, 1000)
if !strings.Contains(text, "upgrade from") {
t.Error("should say upgrade from None")
}
}
// ── Item Lookup Tests ──────────────────────────────────────────────<E29480><E29480>────────
func TestAdvFindShopItemInSlot_ExactMatch(t *testing.T) {
slot, def, ok := advFindShopItemInSlot("Enchanted Plate", SlotArmor)
if !ok {
t.Fatal("should find Enchanted Plate in armor slot")
}
if slot != SlotArmor {
t.Errorf("slot should be armor, got %s", slot)
}
if def.Tier != 4 {
t.Errorf("Enchanted Plate should be tier 4, got %d", def.Tier)
}
}
func TestAdvFindShopItemInSlot_WrongSlot(t *testing.T) {
_, _, ok := advFindShopItemInSlot("Dragonscale", SlotWeapon)
if ok {
t.Error("should not find armor item in weapon slot")
}
}
func TestAdvFindShopItemInSlot_PartialMatch(t *testing.T) {
slot, def, ok := advFindShopItemInSlot("Enchanted Blade", SlotWeapon)
if !ok {
t.Fatal("should find Enchanted Blade by partial match")
}
if slot != SlotWeapon {
t.Errorf("slot should be weapon, got %s", slot)
}
if def.Tier != 4 {
t.Errorf("Enchanted Blade should be tier 4, got %d", def.Tier)
}
}
func TestAdvFindShopItemInSlot_CaseInsensitive(t *testing.T) {
_, _, ok := advFindShopItemInSlot("enchanted blade", SlotWeapon)
if !ok {
t.Error("should find item case-insensitively")
}
}
func TestAdvFindShopItemInSlot_NoTier0(t *testing.T) {
_, _, ok := advFindShopItemInSlot("Basic Ass Sword", SlotWeapon)
if ok {
t.Error("should not find tier 0 items (price 0)")
}
}
func TestAdvFindShopItem_TierShorthand(t *testing.T) {
slot, def, ok := advFindShopItem("3 sword")
if !ok {
t.Fatal("should find via tier shorthand")
}
if slot != SlotWeapon {
t.Errorf("slot should be weapon, got %s", slot)
}
if def.Tier != 3 {
t.Errorf("tier should be 3, got %d", def.Tier)
}
}
func TestAdvFindShopItem_TierPrefix(t *testing.T) {
_, def, ok := advFindShopItem("tier 5 boots")
if !ok {
t.Fatal("should find via 'tier 5 boots'")
}
if def.Tier != 5 {
t.Errorf("tier should be 5, got %d", def.Tier)
}
}
func TestAdvFindShopItem_TShorthand(t *testing.T) {
_, def, ok := advFindShopItem("t4 armor")
if !ok {
t.Fatal("should find via 't4 armor'")
}
if def.Tier != 4 {
t.Errorf("tier should be 4, got %d", def.Tier)
}
}
// ── Flavor Coverage Tests ───────────────────────────────────────────────────
func TestLuigiItemDescriptions_Coverage(t *testing.T) {
for _, slot := range allSlots {
defs := equipmentTiers[slot]
for _, def := range defs {
if def.Price == 0 {
continue // skip tier 0
}
key := luigiItemKey{slot, def.Tier}
if _, ok := luigiItemDescriptions[key]; !ok {
t.Errorf("missing Luigi item description for %s tier %d (%s)", slot, def.Tier, def.Name)
}
if _, ok := luigiItemOneLiners[key]; !ok {
t.Errorf("missing Luigi one-liner for %s tier %d (%s)", slot, def.Tier, def.Name)
}
}
}
}
func TestLuigiCategoryIntros_Coverage(t *testing.T) {
for _, slot := range allSlots {
intros, ok := luigiCategoryIntros[slot]
if !ok || len(intros) == 0 {
t.Errorf("missing Luigi category intros for slot %s", slot)
}
}
}
func TestLuigiFlavorPools_NonEmpty(t *testing.T) {
pools := map[string][]string{
"luigiGreetings": luigiGreetings,
"luigiPurchaseConfirm": luigiPurchaseConfirm,
"luigiTier5Confirm": luigiTier5Confirm,
"luigiComboConfirm": luigiComboConfirm,
"luigiInsufficientFunds": luigiInsufficientFunds,
"luigiBrowseTimeout": luigiBrowseTimeout,
"luigiMaxedOut": luigiMaxedOut,
"luigiMasterworkAck": luigiMasterworkAck,
"luigiShowAllComment": luigiShowAllComment,
"luigiCommentary": luigiCommentary,
"luigiCancellation": luigiCancellation,
}
for name, pool := range pools {
if len(pool) == 0 {
t.Errorf("flavor pool %s is empty", name)
}
}
}
// ── Format String Safety ────────────────────────────────────────────────────
func TestLuigiPurchaseConfirm_FormatStrings(t *testing.T) {
for i, tmpl := range luigiPurchaseConfirm {
// Should have %s then %d
result := strings.Contains(tmpl, "%s") && strings.Contains(tmpl, "%d")
if !result {
t.Errorf("luigiPurchaseConfirm[%d] missing %%s or %%d placeholder", i)
}
}
}
func TestLuigiTier5Confirm_FormatStrings(t *testing.T) {
for i, tmpl := range luigiTier5Confirm {
result := strings.Contains(tmpl, "%s") && strings.Contains(tmpl, "%d")
if !result {
t.Errorf("luigiTier5Confirm[%d] missing %%s or %%d placeholder", i)
}
}
}
func TestLuigiComboConfirm_FormatStrings(t *testing.T) {
for i, tmpl := range luigiComboConfirm {
result := strings.Contains(tmpl, "%s") && strings.Contains(tmpl, "%d")
if !result {
t.Errorf("luigiComboConfirm[%d] missing %%s or %%d placeholder", i)
}
}
}
// ── Tier Bounds Safety ──────────────────────────────────────────────────────
func TestEquipmentTiers_ConsistentIndexing(t *testing.T) {
for _, slot := range allSlots {
defs := equipmentTiers[slot]
for i, def := range defs {
if def.Tier != i {
t.Errorf("slot %s: tier %d at index %d (should match for safe direct indexing)", slot, def.Tier, i)
}
}
}
}
func TestEquipmentTiers_SixTiersPerSlot(t *testing.T) {
for _, slot := range allSlots {
defs := equipmentTiers[slot]
if len(defs) != 6 {
t.Errorf("slot %s: expected 6 tiers (0-5), got %d", slot, len(defs))
}
}
}