mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-18 01:42:42 +00:00
For each backpack magic item, compute the per-stat diff against whatever occupies the slot it would equip into (mi.Slot — the same slot the web Equip button targets, so the card describes the trade that actually happens). The verdict is strict dominance: all-gain is an upgrade, all-loss a downgrade, mixed a sidegrade with no winner claimed (the case the two opaque effect strings could never show). Empty slot reads 'new'; an attunement item with no free bond reads 'inert', which overrides the stat verdict because wearing it does nothing. The diff is over tempered effects on both sides and reuses the engine's own magicItemEffectFor, so nothing here re-derives power math that could drift from the game. Rides an additive Compare object on the private backpack ItemView — no migration, no endpoint, no public surface. Rings collapse to the same path: every ring equips to ring_1 (ring_2 is declared but never assigned by any live code), so a backpack ring simply compares against the ring_1 occupant.
170 lines
6.4 KiB
Go
170 lines
6.4 KiB
Go
package plugin
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gogobee/internal/peteclient"
|
|
)
|
|
|
|
// weapon/ring/wondrous helpers build a MagicItem the codified effect formula
|
|
// (magicItemEffectFor) can score without touching the DB.
|
|
func mkItem(kind MagicItemKind, rarity DnDRarity, slot DnDSlot) MagicItem {
|
|
return MagicItem{ID: string(kind) + "_" + string(rarity), Kind: kind, Rarity: rarity, Slot: slot}
|
|
}
|
|
|
|
// TestMagicItemDeltasDirection pins the "which way is better" call for each stat,
|
|
// including DamageReductMult where LOWER is the improvement.
|
|
func TestMagicItemDeltasDirection(t *testing.T) {
|
|
neutral := magicItemEffect{DamageReductMult: 1.0}
|
|
|
|
t.Run("more damage is better", func(t *testing.T) {
|
|
d := magicItemDeltas(magicItemEffect{DamageBonus: 0.15, DamageReductMult: 1.0}, magicItemEffect{DamageBonus: 0.10, DamageReductMult: 1.0})
|
|
if len(d) != 1 || d[0].Label != "damage" || !d[0].Better || d[0].Text != "+5% damage" {
|
|
t.Fatalf("got %+v", d)
|
|
}
|
|
})
|
|
|
|
t.Run("less damage taken is better", func(t *testing.T) {
|
|
// cand mult 0.90 (blocks 10%) vs worn neutral 1.0 (blocks nothing).
|
|
d := magicItemDeltas(magicItemEffect{DamageReductMult: 0.90}, neutral)
|
|
if len(d) != 1 || d[0].Label != "defense" || !d[0].Better || d[0].Text != "-10% damage taken" {
|
|
t.Fatalf("got %+v", d)
|
|
}
|
|
})
|
|
|
|
t.Run("weaker armor reads as worse", func(t *testing.T) {
|
|
// cand blocks less than worn: mult rises, damage taken goes up.
|
|
d := magicItemDeltas(magicItemEffect{DamageReductMult: 0.96}, magicItemEffect{DamageReductMult: 0.90})
|
|
if len(d) != 1 || d[0].Better || d[0].Text != "+6% damage taken" {
|
|
t.Fatalf("got %+v", d)
|
|
}
|
|
})
|
|
|
|
t.Run("hp and opening damage", func(t *testing.T) {
|
|
d := magicItemDeltas(
|
|
magicItemEffect{DamageReductMult: 1.0, MaxHP: 10, FlatDmgStart: 3},
|
|
magicItemEffect{DamageReductMult: 1.0, MaxHP: 6, FlatDmgStart: 5},
|
|
)
|
|
byLabel := map[string]itemDelta{}
|
|
for _, x := range d {
|
|
byLabel[x.Label] = itemDelta{x.Better, x.Text}
|
|
}
|
|
if v := byLabel["hp"]; v.text != "+4 HP" || !v.better {
|
|
t.Errorf("hp: %+v", v)
|
|
}
|
|
if v := byLabel["opening"]; v.text != "-2 opening damage" || v.better {
|
|
t.Errorf("opening: %+v", v)
|
|
}
|
|
})
|
|
|
|
t.Run("sub-percent change is dropped", func(t *testing.T) {
|
|
// 0.004 fraction = 0.4% → rounds to 0% → not a visible delta.
|
|
if d := magicItemDeltas(
|
|
magicItemEffect{DamageBonus: 0.104, DamageReductMult: 1.0},
|
|
magicItemEffect{DamageBonus: 0.100, DamageReductMult: 1.0},
|
|
); len(d) != 0 {
|
|
t.Fatalf("expected no visible delta, got %+v", d)
|
|
}
|
|
})
|
|
}
|
|
|
|
type itemDelta struct {
|
|
better bool
|
|
text string
|
|
}
|
|
|
|
// TestCompareVerdict pins strict-dominance classification and the overrides.
|
|
func TestCompareVerdict(t *testing.T) {
|
|
gain := []peteclient.ItemDelta{{Better: true}}
|
|
loss := []peteclient.ItemDelta{{Better: false}}
|
|
mixed := []peteclient.ItemDelta{{Better: true}, {Better: false}}
|
|
|
|
cases := []struct {
|
|
name string
|
|
deltas []peteclient.ItemDelta
|
|
empty, inrt bool
|
|
want string
|
|
}{
|
|
{"all gains", gain, false, false, "upgrade"},
|
|
{"all losses", loss, false, false, "downgrade"},
|
|
{"mixed", mixed, false, false, "sidegrade"},
|
|
{"no change", nil, false, false, "same"},
|
|
{"empty slot", gain, true, false, "new"},
|
|
{"inert overrides upgrade", gain, false, true, "inert"},
|
|
{"inert overrides new", gain, true, true, "inert"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
if got := compareVerdict(c.deltas, c.empty, c.inrt); got != c.want {
|
|
t.Errorf("compareVerdict(%s) = %q, want %q", c.name, got, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMagicItemCompareIntegration drives the whole builder against equipped maps.
|
|
func TestMagicItemCompareIntegration(t *testing.T) {
|
|
rareWpn := mkItem(MagicItemWeapon, RarityRare, DnDSlotMainHand) // +15% damage
|
|
uncWpn := mkItem(MagicItemWeapon, RarityUncommon, DnDSlotMainHand) // +10% damage
|
|
|
|
t.Run("upgrade over a weaker worn weapon", func(t *testing.T) {
|
|
eq := map[DnDSlot]EquippedMagicItem{DnDSlotMainHand: {Slot: DnDSlotMainHand, Item: uncWpn}}
|
|
c := magicItemCompare(rareWpn, 0, eq)
|
|
if c.Verdict != "upgrade" || c.VsName != uncWpn.Name || c.VsSlot != "main_hand" {
|
|
t.Fatalf("got %+v", c)
|
|
}
|
|
})
|
|
|
|
t.Run("downgrade under a stronger worn weapon", func(t *testing.T) {
|
|
eq := map[DnDSlot]EquippedMagicItem{DnDSlotMainHand: {Slot: DnDSlotMainHand, Item: rareWpn}}
|
|
if c := magicItemCompare(uncWpn, 0, eq); c.Verdict != "downgrade" {
|
|
t.Fatalf("got %+v", c)
|
|
}
|
|
})
|
|
|
|
t.Run("same as an identical worn weapon", func(t *testing.T) {
|
|
eq := map[DnDSlot]EquippedMagicItem{DnDSlotMainHand: {Slot: DnDSlotMainHand, Item: rareWpn}}
|
|
c := magicItemCompare(rareWpn, 0, eq)
|
|
if c.Verdict != "same" || len(c.Deltas) != 0 {
|
|
t.Fatalf("got %+v", c)
|
|
}
|
|
})
|
|
|
|
t.Run("new into an empty slot names no worn item", func(t *testing.T) {
|
|
c := magicItemCompare(rareWpn, 0, map[DnDSlot]EquippedMagicItem{})
|
|
if c.Verdict != "new" || c.VsName != "" || len(c.Deltas) == 0 {
|
|
t.Fatalf("got %+v", c)
|
|
}
|
|
})
|
|
|
|
t.Run("attunement item with no free bond is inert", func(t *testing.T) {
|
|
ring := mkItem(MagicItemRing, RarityRare, DnDSlotRing1)
|
|
ring.Attunement = true
|
|
// Three bonds spent elsewhere; the ring slot is empty. Wearing it does nothing.
|
|
eq := map[DnDSlot]EquippedMagicItem{
|
|
DnDSlotChest: {Slot: DnDSlotChest, Item: mkItem(MagicItemArmor, RarityRare, DnDSlotChest), Attuned: true},
|
|
DnDSlotAmulet: {Slot: DnDSlotAmulet, Item: mkItem(MagicItemWondrous, RarityRare, DnDSlotAmulet), Attuned: true},
|
|
DnDSlotCloak: {Slot: DnDSlotCloak, Item: mkItem(MagicItemWondrous, RarityRare, DnDSlotCloak), Attuned: true},
|
|
}
|
|
if c := magicItemCompare(ring, 0, eq); c.Verdict != "inert" {
|
|
t.Fatalf("got %+v", c)
|
|
}
|
|
})
|
|
|
|
t.Run("evicting the worn occupant frees its bond, so not inert", func(t *testing.T) {
|
|
ring := mkItem(MagicItemRing, RarityRare, DnDSlotRing1)
|
|
ring.Attunement = true
|
|
wornRing := mkItem(MagicItemRing, RarityUncommon, DnDSlotRing1)
|
|
wornRing.Attunement = true
|
|
// Three bonds spent — but one of them is the ring the candidate would replace.
|
|
eq := map[DnDSlot]EquippedMagicItem{
|
|
DnDSlotRing1: {Slot: DnDSlotRing1, Item: wornRing, Attuned: true},
|
|
DnDSlotChest: {Slot: DnDSlotChest, Item: mkItem(MagicItemArmor, RarityRare, DnDSlotChest), Attuned: true},
|
|
DnDSlotAmulet: {Slot: DnDSlotAmulet, Item: mkItem(MagicItemWondrous, RarityRare, DnDSlotAmulet), Attuned: true},
|
|
}
|
|
if c := magicItemCompare(ring, 0, eq); c.Verdict == "inert" {
|
|
t.Fatalf("bond freed by eviction, should not be inert: %+v", c)
|
|
}
|
|
})
|
|
}
|