package plugin import ( "fmt" "strings" "maunium.net/go/mautrix/id" ) // N4/E1 โ€” the Tier-4 "Established" home unlocks a vault: a fixed pool of slots // that shelters items from `!sell all`, crafting, and combat consumption. It is // pure storage โ€” a vaulted item drops out of every play surface (loadAdvInventory // filters it) until it is taken back โ€” and the plumbing E2 gifting builds on. const ( vaultCapacity = 10 // slots an Established home provides vaultMinHouseTier = 4 // Tier 4 "Established" gates the vault ) // vaultUnlocked reports whether the character's house tier grants a vault, and a // player-facing refusal line when it does not. func vaultUnlocked(char *AdventureCharacter) (bool, string) { if char.HouseTier >= vaultMinHouseTier { return true, "" } return false, "๐Ÿ”’ A vault comes with an **Established** home (Tier 4). Upgrade your house at `!thom` to unlock " + fmt.Sprintf("%d slots of protected storage โ€” vaulted items are safe from `!sell all` and never spent in combat.", vaultCapacity) } // handleVaultCmd routes `!adventure vault [store|take] `. The reply-building // core is client-free (renderVault / vaultStoreItem / vaultTakeItem) so it is // unit-testable without a Matrix stub; the handler only loads the character and // sends the result. func (p *AdventurePlugin) handleVaultCmd(ctx MessageContext, args string) error { char, _, err := p.ensureCharacter(ctx.Sender) if err != nil { return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to load your character. Try `!adventure` to create one first.") } if !char.Alive { return p.SendDM(ctx.Sender, "You're dead. The vault stays locked.") } if ok, refusal := vaultUnlocked(char); !ok { return p.SendDM(ctx.Sender, refusal) } lower := strings.ToLower(strings.TrimSpace(args)) var reply string switch { case lower == "" || lower == "list": reply = renderVault(ctx.Sender) case strings.HasPrefix(lower, "store "): // Serialize a player's own store/take so two near-simultaneous stores // can't both pass the capacity check and overfill the vault. userMu := p.advUserLock(ctx.Sender) userMu.Lock() reply = vaultStoreItem(ctx.Sender, strings.TrimSpace(args[len("store "):])) userMu.Unlock() case strings.HasPrefix(lower, "take "): userMu := p.advUserLock(ctx.Sender) userMu.Lock() reply = vaultTakeItem(ctx.Sender, strings.TrimSpace(args[len("take "):])) userMu.Unlock() default: reply = "Usage: `!adventure vault` to view ยท `!adventure vault store ` ยท `!adventure vault take `." } return p.SendDM(ctx.Sender, reply) } // renderVault shows what is stowed and how many slots remain. func renderVault(userID id.UserID) string { items, err := loadAdvVault(userID) if err != nil { return "The vault door won't budge. Try again in a moment." } var sb strings.Builder sb.WriteString(fmt.Sprintf("๐Ÿ›๏ธ **Your Vault** โ€” %d / %d slots\n\n", len(items), vaultCapacity)) if len(items) == 0 { sb.WriteString("Empty. `!adventure vault store ` to shelter something from sale or use.") return sb.String() } for _, it := range items { name := it.Name if it.Temper > 0 { name = fmt.Sprintf("%s +%d", name, it.Temper) } sb.WriteString(fmt.Sprintf("โ€ข %s โ€” โ‚ฌ%d\n", name, it.Value)) } sb.WriteString("\n`!adventure vault take ` to bring one back into play.") return sb.String() } // vaultStoreItem moves one matching inventory item into the vault. Guards the // capacity and reports the fresh slot count so the player never has to re-list. func vaultStoreItem(userID id.UserID, query string) string { if query == "" { return "Store what? `!adventure vault store `." } if advVaultCount(userID) >= vaultCapacity { return fmt.Sprintf("The vault is full (%d / %d). Take something out first with `!adventure vault take `.", vaultCapacity, vaultCapacity) } items, err := loadAdvInventory(userID) if err != nil { return "Couldn't reach your inventory. Try again in a moment." } match := findInventoryMatch(items, query) if match == nil { return fmt.Sprintf("No inventory item matches %q. Check `!adventure inventory`.", query) } moved, err := setItemVaulted(userID, match.ID, true) if err != nil || !moved { return "Couldn't stow that item. Try again in a moment." } return fmt.Sprintf("๐Ÿ›๏ธ Stowed **%s** in the vault. %d / %d slots used.", match.Name, advVaultCount(userID), vaultCapacity) } // vaultTakeItem returns one matching vaulted item to the active inventory. func vaultTakeItem(userID id.UserID, query string) string { if query == "" { return "Take what? `!adventure vault take `." } items, err := loadAdvVault(userID) if err != nil { return "The vault door won't budge. Try again in a moment." } match := findInventoryMatch(items, query) if match == nil { return fmt.Sprintf("Nothing in the vault matches %q. Check `!adventure vault`.", query) } moved, err := setItemVaulted(userID, match.ID, false) if err != nil || !moved { return "Couldn't retrieve that item. Try again in a moment." } return fmt.Sprintf("Took **%s** out of the vault and back into your pack. %d / %d slots used.", match.Name, advVaultCount(userID), vaultCapacity) }