mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 17:02:42 +00:00
adventure: worn magic items bond when a slot frees, and can be taken off
An attunement item equipped while at the 3-bond cap sat permanently inert: nothing re-bonded it when a slot opened, and the equip picker only lists inventory, so a slotted item could never be reached again. - reconcileMagicAttunements bonds worn-but-inert items whenever bond capacity is free (bonding is strictly beneficial; inert should only exist at the cap). Runs on equip-magic open and after any swap. - New !adventure unequip-magic picker takes a worn item off and returns it to inventory at full value, freeing its bond slot (which then heals any straggler). Destructive-op-first ordering mirrors the equip path.
This commit is contained in:
@@ -533,6 +533,8 @@ func (p *AdventurePlugin) dispatchCommand(ctx MessageContext) error {
|
|||||||
return p.handleEquipCmd(ctx)
|
return p.handleEquipCmd(ctx)
|
||||||
case lower == "equip-magic" || lower == "equipmagic" || lower == "equip magic":
|
case lower == "equip-magic" || lower == "equipmagic" || lower == "equip magic":
|
||||||
return p.handleEquipMagicCmd(ctx)
|
return p.handleEquipMagicCmd(ctx)
|
||||||
|
case lower == "unequip-magic" || lower == "unequipmagic" || lower == "unequip magic":
|
||||||
|
return p.handleUnequipMagicCmd(ctx)
|
||||||
case lower == "inventory" || lower == "inv":
|
case lower == "inventory" || lower == "inv":
|
||||||
return p.handleInventoryCmd(ctx)
|
return p.handleInventoryCmd(ctx)
|
||||||
case lower == "leaderboard" || lower == "lb":
|
case lower == "leaderboard" || lower == "lb":
|
||||||
@@ -594,6 +596,7 @@ const advHelpText = `**Adventure Commands**
|
|||||||
` + "`!adventure buy <item>`" + ` — Buy equipment (e.g. ` + "`buy Enchanted Blade`" + ` or ` + "`buy 4 sword`" + `)
|
` + "`!adventure buy <item>`" + ` — Buy equipment (e.g. ` + "`buy Enchanted Blade`" + ` or ` + "`buy 4 sword`" + `)
|
||||||
` + "`!adventure equip`" + ` — Equip Masterwork gear from inventory
|
` + "`!adventure equip`" + ` — Equip Masterwork gear from inventory
|
||||||
` + "`!adventure equip-magic`" + ` — Equip magic items (curios) into your D&D slots
|
` + "`!adventure equip-magic`" + ` — Equip magic items (curios) into your D&D slots
|
||||||
|
` + "`!adventure unequip-magic`" + ` — Take a worn magic item off and return it to inventory
|
||||||
` + "`!adventure sell <item>`" + ` — Sell an inventory item (or ` + "`sell all`" + `)
|
` + "`!adventure sell <item>`" + ` — Sell an inventory item (or ` + "`sell all`" + `)
|
||||||
` + "`!adventure inventory`" + ` — View your inventory
|
` + "`!adventure inventory`" + ` — View your inventory
|
||||||
` + "`!adventure vault`" + ` — Store items safely (Tier-4 Established home; ` + "`vault store/take <item>`" + `)
|
` + "`!adventure vault`" + ` — Store items safely (Tier-4 Established home; ` + "`vault store/take <item>`" + `)
|
||||||
@@ -940,6 +943,8 @@ func (p *AdventurePlugin) resolvePendingInteraction(ctx MessageContext, interact
|
|||||||
return p.handleMasterworkEquipReply(ctx, interaction)
|
return p.handleMasterworkEquipReply(ctx, interaction)
|
||||||
case "magic_equip":
|
case "magic_equip":
|
||||||
return p.resolveMagicEquipReply(ctx, interaction)
|
return p.resolveMagicEquipReply(ctx, interaction)
|
||||||
|
case "magic_unequip":
|
||||||
|
return p.resolveMagicUnequipReply(ctx, interaction)
|
||||||
case "masterwork_equip_confirm":
|
case "masterwork_equip_confirm":
|
||||||
return p.handleMasterworkEquipConfirm(ctx, interaction)
|
return p.handleMasterworkEquipConfirm(ctx, interaction)
|
||||||
case "rival_rps":
|
case "rival_rps":
|
||||||
|
|||||||
@@ -328,6 +328,42 @@ func countAttunedMagicItems(equipped map[DnDSlot]EquippedMagicItem) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reconcileMagicAttunements bonds any worn attunement item that is sitting
|
||||||
|
// inert while a bond slot is free. Bonding is strictly beneficial (there are no
|
||||||
|
// cursed items), so the only legitimate reason for a worn attunement item to be
|
||||||
|
// unbonded is the hard cap — the moment a slot frees, every straggler should
|
||||||
|
// light up. Without this, an item equipped while at cap stays permanently inert:
|
||||||
|
// the equip picker only lists inventory, so a slotted item can never be reached
|
||||||
|
// to re-attempt bonding. Idempotent; returns the names of items it newly bonded.
|
||||||
|
func reconcileMagicAttunements(userID id.UserID) ([]string, error) {
|
||||||
|
equipped, err := loadEquippedMagicItems(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
used := countAttunedMagicItems(equipped)
|
||||||
|
if used >= dndMagicItemAttuneLimit {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var bonded []string
|
||||||
|
for _, ds := range dndSlotOrder { // deterministic order when slots are scarce
|
||||||
|
if used >= dndMagicItemAttuneLimit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
e, ok := equipped[ds]
|
||||||
|
if !ok || e.Item.ID == "" || !e.Item.Attunement || e.Attuned {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := db.Get().Exec(
|
||||||
|
`UPDATE magic_item_equipped SET attuned = 1 WHERE user_id = ? AND slot = ?`,
|
||||||
|
string(userID), string(ds)); err != nil {
|
||||||
|
return bonded, err
|
||||||
|
}
|
||||||
|
bonded = append(bonded, e.Item.Name)
|
||||||
|
used++
|
||||||
|
}
|
||||||
|
return bonded, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ── Combat hook ─────────────────────────────────────────────────────────────
|
// ── Combat hook ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// applyMagicItemEffects layers the player's equipped magic items onto their
|
// applyMagicItemEffects layers the player's equipped magic items onto their
|
||||||
@@ -523,6 +559,12 @@ func magicItemEffectSummary(mi MagicItem) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *AdventurePlugin) handleEquipMagicCmd(ctx MessageContext) error {
|
func (p *AdventurePlugin) handleEquipMagicCmd(ctx MessageContext) error {
|
||||||
|
// Self-heal first: bond any worn item stranded inert while a slot is free
|
||||||
|
// (e.g. equipped at cap, then a bond slot opened). This is the only path a
|
||||||
|
// slotted-but-inert item can be reached from, since the picker below lists
|
||||||
|
// inventory only.
|
||||||
|
healed, _ := reconcileMagicAttunements(ctx.Sender)
|
||||||
|
|
||||||
items, err := loadAdvInventory(ctx.Sender)
|
items, err := loadAdvInventory(ctx.Sender)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p.SendDM(ctx.Sender, "Failed to access your inventory.")
|
return p.SendDM(ctx.Sender, "Failed to access your inventory.")
|
||||||
@@ -538,13 +580,23 @@ func (p *AdventurePlugin) handleEquipMagicCmd(ctx MessageContext) error {
|
|||||||
}
|
}
|
||||||
magic = append(magic, it)
|
magic = append(magic, it)
|
||||||
}
|
}
|
||||||
|
healNote := ""
|
||||||
|
if len(healed) > 0 {
|
||||||
|
healNote = fmt.Sprintf("🔗 A bond slot freed up — **%s** bonded and is now active.\n\n",
|
||||||
|
strings.Join(healed, "**, **"))
|
||||||
|
}
|
||||||
|
|
||||||
if len(magic) == 0 {
|
if len(magic) == 0 {
|
||||||
return p.SendDM(ctx.Sender,
|
msg := "No curios to equip yet — they drop from zones or Luigi's 🔮 shelf."
|
||||||
"No curios to equip yet — they drop from zones or Luigi's 🔮 shelf.")
|
if healNote != "" {
|
||||||
|
msg = strings.TrimSpace(healNote)
|
||||||
|
}
|
||||||
|
return p.SendDM(ctx.Sender, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
equipped, _ := loadEquippedMagicItems(ctx.Sender)
|
equipped, _ := loadEquippedMagicItems(ctx.Sender)
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
sb.WriteString(healNote)
|
||||||
sb.WriteString("🔮 **Equippable magic items:**\n\n")
|
sb.WriteString("🔮 **Equippable magic items:**\n\n")
|
||||||
for i, it := range magic {
|
for i, it := range magic {
|
||||||
base, _ := magicItemFromAdvItem(it)
|
base, _ := magicItemFromAdvItem(it)
|
||||||
@@ -656,8 +708,113 @@ func (p *AdventurePlugin) resolveMagicEquipReply(ctx MessageContext, interaction
|
|||||||
sb.WriteString(fmt.Sprintf("\nBonded (%d/%d bond slots used).",
|
sb.WriteString(fmt.Sprintf("\nBonded (%d/%d bond slots used).",
|
||||||
countAttunedMagicItems(equipped)+1, dndMagicItemAttuneLimit))
|
countAttunedMagicItems(equipped)+1, dndMagicItemAttuneLimit))
|
||||||
case mi.Attunement && atCap:
|
case mi.Attunement && atCap:
|
||||||
sb.WriteString(fmt.Sprintf("\n⚠️ All %d bond slots are full — it's worn but **inert** until you free one (`!adventure equip-magic` to swap).",
|
sb.WriteString(fmt.Sprintf("\n⚠️ All %d bond slots are full — it's worn but **inert** until you free one (`!adventure unequip-magic` to take a bonded item off; this one bonds automatically once a slot opens).",
|
||||||
dndMagicItemAttuneLimit))
|
dndMagicItemAttuneLimit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Swapping out the prior occupant may have freed a bond slot — light up any
|
||||||
|
// item that was stranded inert (including a previously-equipped one the
|
||||||
|
// picker could never reach).
|
||||||
|
if healed, _ := reconcileMagicAttunements(ctx.Sender); len(healed) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("\n🔗 A freed bond slot also activated **%s**.",
|
||||||
|
strings.Join(healed, "**, **")))
|
||||||
|
}
|
||||||
return p.SendDM(ctx.Sender, sb.String())
|
return p.SendDM(ctx.Sender, sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Unequip command (`!adventure unequip-magic`) ─────────────────────────────
|
||||||
|
|
||||||
|
// advPendingMagicUnequip is the pending-interaction payload for the numbered
|
||||||
|
// unequip picker. It lists slots (not inventory rows) because the item lives in
|
||||||
|
// magic_item_equipped, not adventure_inventory.
|
||||||
|
type advPendingMagicUnequip struct {
|
||||||
|
Slots []DnDSlot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AdventurePlugin) handleUnequipMagicCmd(ctx MessageContext) error {
|
||||||
|
equipped, err := loadEquippedMagicItems(ctx.Sender)
|
||||||
|
if err != nil {
|
||||||
|
return p.SendDM(ctx.Sender, "Failed to load your equipped magic items.")
|
||||||
|
}
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("🔮 **Worn magic items** — reply with a number to take one off (it returns to your inventory), or \"cancel\".\n\n")
|
||||||
|
var slots []DnDSlot
|
||||||
|
for _, ds := range dndSlotOrder { // stable numbering across repeated calls
|
||||||
|
e, ok := equipped[ds]
|
||||||
|
if !ok || e.Item.ID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slots = append(slots, ds)
|
||||||
|
mi := e.Effective()
|
||||||
|
status := ""
|
||||||
|
if mi.Attunement {
|
||||||
|
if e.Attuned {
|
||||||
|
status = " — bonded"
|
||||||
|
} else {
|
||||||
|
status = " — _(inert)_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%d. **%s** _(%s)_ → %s slot%s\n",
|
||||||
|
len(slots), mi.Name, mi.Rarity, ds, status))
|
||||||
|
}
|
||||||
|
if len(slots) == 0 {
|
||||||
|
return p.SendDM(ctx.Sender, "You aren't wearing any magic items. `!adventure equip-magic` to put one on.")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.pending.Store(string(ctx.Sender), &advPendingInteraction{
|
||||||
|
Type: "magic_unequip",
|
||||||
|
Data: &advPendingMagicUnequip{Slots: slots},
|
||||||
|
ExpiresAt: time.Now().Add(advDMResponseWindow),
|
||||||
|
})
|
||||||
|
return p.SendDM(ctx.Sender, sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AdventurePlugin) resolveMagicUnequipReply(ctx MessageContext, interaction *advPendingInteraction) error {
|
||||||
|
data := interaction.Data.(*advPendingMagicUnequip)
|
||||||
|
reply := strings.TrimSpace(ctx.Body)
|
||||||
|
if strings.EqualFold(reply, "cancel") {
|
||||||
|
return p.SendDM(ctx.Sender, "Unequip cancelled.")
|
||||||
|
}
|
||||||
|
idx, ok := parseMenuIndex(reply, len(data.Slots))
|
||||||
|
if !ok {
|
||||||
|
p.pending.Store(string(ctx.Sender), interaction)
|
||||||
|
return p.SendDM(ctx.Sender, "Invalid selection. Reply with a number from the list, or \"cancel\".")
|
||||||
|
}
|
||||||
|
|
||||||
|
slot := data.Slots[idx]
|
||||||
|
equipped, err := loadEquippedMagicItems(ctx.Sender)
|
||||||
|
if err != nil {
|
||||||
|
return p.SendDM(ctx.Sender, "Failed to load your equipped magic items.")
|
||||||
|
}
|
||||||
|
e, ok := equipped[slot]
|
||||||
|
if !ok || e.Item.ID == "" {
|
||||||
|
return p.SendDM(ctx.Sender, "That slot is already empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the slot FIRST, then return the item to inventory at full value.
|
||||||
|
// This mirrors the equip resolver's ordering (destructive op first, restore
|
||||||
|
// on failure): the other order could leave the item both worn and in
|
||||||
|
// inventory — a free duplicate — on a transient DB error.
|
||||||
|
if err := unequipMagicItem(ctx.Sender, slot); err != nil {
|
||||||
|
return p.SendDM(ctx.Sender, "Failed to take that item off.")
|
||||||
|
}
|
||||||
|
back := magicItemSellAt(e.Item, e.Temper)
|
||||||
|
back.SkillSource = "magic_item:" + e.Item.ID
|
||||||
|
if err := addAdvInventoryItem(ctx.Sender, back); err != nil {
|
||||||
|
// Roll back: re-equip exactly as it was so the item isn't lost.
|
||||||
|
if rbErr := equipMagicItem(ctx.Sender, slot, e.Item.ID, e.Attuned, e.Temper); rbErr != nil {
|
||||||
|
slog.Error("magic-item: unequip failed AND re-equip rollback failed",
|
||||||
|
"user", ctx.Sender, "item", e.Item.ID, "inv_err", err, "rollback_err", rbErr)
|
||||||
|
}
|
||||||
|
return p.SendDM(ctx.Sender, "Failed to return that item to your inventory.")
|
||||||
|
}
|
||||||
|
|
||||||
|
mi := e.Effective()
|
||||||
|
msg := fmt.Sprintf("📦 **%s** taken off your %s slot and returned to inventory.", mi.Name, slot)
|
||||||
|
// Freeing a bonded slot may let a worn-but-inert item finally bond.
|
||||||
|
if healed, _ := reconcileMagicAttunements(ctx.Sender); len(healed) > 0 {
|
||||||
|
msg += fmt.Sprintf("\n🔗 That freed a bond slot — **%s** is now active.",
|
||||||
|
strings.Join(healed, "**, **"))
|
||||||
|
}
|
||||||
|
return p.SendDM(ctx.Sender, msg)
|
||||||
|
}
|
||||||
|
|||||||
@@ -252,6 +252,152 @@ func TestSwapBackReturnsFullValue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReconcileBondsStrandedItem reproduces the player report: an attunement
|
||||||
|
// item equipped while at the bond cap sits inert, then a bond slot frees up.
|
||||||
|
// The item is worn (not in inventory) so the equip picker can never reach it —
|
||||||
|
// reconcile must be what lights it up.
|
||||||
|
func TestReconcileBondsStrandedItem(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
db.Close()
|
||||||
|
if err := db.Init(dir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(db.Close)
|
||||||
|
|
||||||
|
user := id.UserID("@stranded:test.invalid")
|
||||||
|
|
||||||
|
// Gather attunement items in distinct slots — need cap+1 of them.
|
||||||
|
seen := map[DnDSlot]bool{}
|
||||||
|
var att []MagicItem
|
||||||
|
for _, ds := range dndSlotOrder {
|
||||||
|
for _, mi := range magicItemRegistry {
|
||||||
|
if mi.Attunement && mi.Slot == ds && !seen[ds] {
|
||||||
|
att = append(att, mi)
|
||||||
|
seen[ds] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(att) < dndMagicItemAttuneLimit+1 {
|
||||||
|
t.Skipf("registry has only %d attunement slots, need %d", len(att), dndMagicItemAttuneLimit+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bond the first `limit` items, then wear one more inert (attuned=false).
|
||||||
|
for i := 0; i < dndMagicItemAttuneLimit; i++ {
|
||||||
|
if err := equipMagicItem(user, att[i].Slot, att[i].ID, true, 0); err != nil {
|
||||||
|
t.Fatalf("bond seed %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stranded := att[dndMagicItemAttuneLimit]
|
||||||
|
if err := equipMagicItem(user, stranded.Slot, stranded.ID, false, 0); err != nil {
|
||||||
|
t.Fatalf("equip stranded: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At cap, reconcile must be a no-op — the stranded item stays inert.
|
||||||
|
if healed, err := reconcileMagicAttunements(user); err != nil || len(healed) != 0 {
|
||||||
|
t.Fatalf("reconcile at cap = %v (err %v), want no change", healed, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free a slot (the player swaps out a bonded item), then reconcile.
|
||||||
|
if err := unequipMagicItem(user, att[0].Slot); err != nil {
|
||||||
|
t.Fatalf("free slot: %v", err)
|
||||||
|
}
|
||||||
|
healed, err := reconcileMagicAttunements(user)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reconcile after free: %v", err)
|
||||||
|
}
|
||||||
|
if len(healed) != 1 || healed[0] != stranded.Name {
|
||||||
|
t.Fatalf("reconcile healed %v, want [%s]", healed, stranded.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
equipped, _ := loadEquippedMagicItems(user)
|
||||||
|
if !equipped[stranded.Slot].Attuned {
|
||||||
|
t.Errorf("stranded item still inert after reconcile")
|
||||||
|
}
|
||||||
|
if got := countAttunedMagicItems(equipped); got != dndMagicItemAttuneLimit {
|
||||||
|
t.Errorf("bonded count = %d, want %d", got, dndMagicItemAttuneLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idempotent: a second pass changes nothing.
|
||||||
|
if healed, _ := reconcileMagicAttunements(user); len(healed) != 0 {
|
||||||
|
t.Errorf("second reconcile healed %v, want none", healed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnequipMagicReturnsToInventoryAndHeals drives the unequip resolver end to
|
||||||
|
// end: taking a bonded item off must return it to inventory AND free its bond
|
||||||
|
// slot so a worn-but-inert item lights up.
|
||||||
|
func TestUnequipMagicReturnsToInventoryAndHeals(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
db.Close()
|
||||||
|
if err := db.Init(dir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(db.Close)
|
||||||
|
|
||||||
|
user := id.UserID("@unequip:test.invalid")
|
||||||
|
|
||||||
|
seen := map[DnDSlot]bool{}
|
||||||
|
var att []MagicItem
|
||||||
|
for _, ds := range dndSlotOrder {
|
||||||
|
for _, mi := range magicItemRegistry {
|
||||||
|
if mi.Attunement && mi.Slot == ds && !seen[ds] {
|
||||||
|
att = append(att, mi)
|
||||||
|
seen[ds] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(att) < dndMagicItemAttuneLimit+1 {
|
||||||
|
t.Skipf("registry has only %d attunement slots, need %d", len(att), dndMagicItemAttuneLimit+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bond the cap, then wear one more inert.
|
||||||
|
for i := 0; i < dndMagicItemAttuneLimit; i++ {
|
||||||
|
if err := equipMagicItem(user, att[i].Slot, att[i].ID, true, 0); err != nil {
|
||||||
|
t.Fatalf("bond seed %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stranded := att[dndMagicItemAttuneLimit]
|
||||||
|
if err := equipMagicItem(user, stranded.Slot, stranded.ID, false, 0); err != nil {
|
||||||
|
t.Fatalf("equip stranded: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &AdventurePlugin{} // nil Sink/Client → SendDM is a no-op
|
||||||
|
interaction := &advPendingInteraction{
|
||||||
|
Type: "magic_unequip",
|
||||||
|
Data: &advPendingMagicUnequip{Slots: []DnDSlot{att[0].Slot}},
|
||||||
|
}
|
||||||
|
if err := p.resolveMagicUnequipReply(MessageContext{Sender: user, Body: "1"}, interaction); err != nil {
|
||||||
|
t.Fatalf("resolve unequip: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The unequipped item is back in inventory as a curio.
|
||||||
|
inv, err := loadAdvInventory(user)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, it := range inv {
|
||||||
|
if it.SkillSource == "magic_item:"+att[0].ID {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("unequipped item %q not returned to inventory", att[0].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Its slot is empty, and the freed bond slot bonded the stranded item.
|
||||||
|
equipped, _ := loadEquippedMagicItems(user)
|
||||||
|
if _, still := equipped[att[0].Slot]; still {
|
||||||
|
t.Errorf("slot %s still occupied after unequip", att[0].Slot)
|
||||||
|
}
|
||||||
|
if !equipped[stranded.Slot].Attuned {
|
||||||
|
t.Errorf("stranded item did not bond after a slot freed")
|
||||||
|
}
|
||||||
|
if got := countAttunedMagicItems(equipped); got != dndMagicItemAttuneLimit {
|
||||||
|
t.Errorf("bonded count = %d, want %d", got, dndMagicItemAttuneLimit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestEquippedMagicItemRoundTrip exercises the DB persistence layer: equip,
|
// TestEquippedMagicItemRoundTrip exercises the DB persistence layer: equip,
|
||||||
// load, attunement counting, unequip.
|
// load, attunement counting, unequip.
|
||||||
func TestEquippedMagicItemRoundTrip(t *testing.T) {
|
func TestEquippedMagicItemRoundTrip(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user