Bank & Storage System
Bank & Storage System
Section titled “Bank & Storage System”This document outlines the bank/storage system for Rent Earth, providing players with additional item storage beyond their inventory.
Overview
Section titled “Overview”The bank is a secure storage system accessible via Bank NPCs in towns. It provides:
- Extra storage beyond the 40-100 inventory slots
- Safe storage - items in bank are never lost on death
- Account-wide potential - can share between characters (optional)
- Expandable - purchase additional tabs with gold
Authority Model
Section titled “Authority Model”PostgreSQL (Supabase) Axum Game Server Unity Client──────────────────── ──────────────── ────────────Bank slot arrays AUTHORITATIVE Display only- character_bank table - Deposit/withdraw - Show bank UI- items table - Validate ownership - Drag & drop - Tab unlocks - Send requestsImplementation Status
Section titled “Implementation Status”| Component | Status | Location |
|---|---|---|
| BankConfig | ✅ Complete | Assets/Scripts/ScriptableObjects/BankConfig.cs |
| BankState | ✅ Complete | Assets/Scripts/State/BankState.cs |
| BankService | ✅ Complete | Assets/Scripts/Services/BankService.cs |
| MessagePipe Events | ✅ Complete | RootLifetimeScope.cs |
| BankConfig.asset | ✅ Complete | Resources/ScriptableObjects/Oracle/BankConfig.asset |
| Bank UI | 🔲 Planned | UI Toolkit implementation |
| Server Validation | 🔲 Planned | Axum game server |
| Database Schema | 🔲 Planned | Supabase migration |
| Proto Messages | 🔲 Planned | snapshot.proto extension |
Bank Structure
Section titled “Bank Structure”┌─────────────────────────────────────────────────────────────────┐│ BANK STORAGE │├─────────────────────────────────────────────────────────────────┤│ TAB 1 (Default) TAB 2 (Unlockable) TAB 3 (Unlockable) ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ 28 slots │ │ 28 slots │ │ 28 slots │ ││ │ (7x4 grid) │ │ (7x4 grid) │ │ (7x4 grid) │ ││ │ │ │ LOCKED │ │ LOCKED │ ││ └─────────────┘ └─────────────┘ └─────────────┘ ││ ││ TAB 4 (Unlockable) TAB 5 (Unlockable) ││ ┌─────────────┐ ┌─────────────┐ ││ │ 28 slots │ │ 28 slots │ Total: 140 slots ││ │ LOCKED │ │ LOCKED │ (5 tabs × 28) ││ └─────────────┘ └─────────────┘ ││ │└─────────────────────────────────────────────────────────────────┘Bank Tabs
Section titled “Bank Tabs”| Tab | Slots | Default | Unlock Cost |
|---|---|---|---|
| 1 | 28 | Yes (free) | - |
| 2 | 28 | No | 5,000 gold |
| 3 | 28 | No | 15,000 gold |
| 4 | 28 | No | 50,000 gold |
| 5 | 28 | No | 150,000 gold |
Tab Unlock Costs (Cumulative)
Section titled “Tab Unlock Costs (Cumulative)”| Tab | Cost | Cumulative |
|---|---|---|
| 2 | 5,000 | 5,000 |
| 3 | 15,000 | 20,000 |
| 4 | 50,000 | 70,000 |
| 5 | 150,000 | 220,000 |
Client Implementation
Section titled “Client Implementation”BankConfig (ScriptableObject)
Section titled “BankConfig (ScriptableObject)”Configuration asset for bank system parameters. Located at Resources/ScriptableObjects/Oracle/BankConfig.asset.
[CreateAssetMenu(fileName = "BankConfig", menuName = "RentEarth/Bank/Bank Config")]public class BankConfig : ScriptableObject{ // Bank Structure public int slotsPerTab = 28; // 7x4 grid public int maxTabs = 5; public int defaultUnlockedTabs = 1;
// Tab Unlock Costs public long tab2UnlockCost = 5000; public long tab3UnlockCost = 15000; public long tab4UnlockCost = 50000; public long tab5UnlockCost = 150000;
// Interaction Settings public float interactionRange = 5f; // meters public bool requireNpcProximity = true;
// Gold Storage public bool enableGoldStorage = true; public float depositFeePercent = 0f; public float withdrawFeePercent = 0f;
// Item Restrictions public bool allowQuestItems = false; public bool allowSoulboundItems = true; public bool requireUnequipped = true;
// UI Grid public int gridColumns = 7; public int gridRows = 4;}BankState (Reactive State)
Section titled “BankState (Reactive State)”Thread-safe reactive state for bank storage. Uses SynchronizedReactiveProperty for UI binding and ReaderWriterLockSlim for thread safety.
public class BankState : IAsyncStartable, IDisposable{ // Reactive properties for UI binding public ReadOnlyReactiveProperty<int> UnlockedTabCount { get; } public ReadOnlyReactiveProperty<long> StoredGold { get; } public ReadOnlyReactiveProperty<bool> IsBankOpen { get; }
// Thread-safe slot access public BankSlotData GetSlot(int tabIndex, int slotIndex); public BankSlotData[] GetTabSlots(int tabIndex); public (int tabIndex, int slotIndex) FindFirstEmptySlot();
// Server response handlers (for future integration) public void ApplyFullState(int tabsUnlocked, long storedGold, BankSlotData[][] tabSlots); public void ApplyDelta(int tabIndex, (int slot, BankSlotData data)[] changes, long? newStoredGold);}BankService (API Layer)
Section titled “BankService (API Layer)”Clean API for UI and game systems to interact with the bank.
public class BankService{ // Bank Access public void OpenBank(Guid npcEntityId = default); public void CloseBank(); public void ToggleBank(Guid npcEntityId = default); public bool IsBankOpen { get; }
// Item Operations public void DepositItem(int inventorySlot, int bankTab, int bankSlot = -1, int quantity = 0); public void DepositItemToFirstEmpty(int inventorySlot, int quantity = 0); public void WithdrawItem(int bankTab, int bankSlot, int inventorySlot = -1, int quantity = 0); public void MoveItem(int fromTab, int fromSlot, int toTab, int toSlot);
// Gold Operations public void DepositGold(long amount); public void DepositAllGold(); public void WithdrawGold(long amount); public void WithdrawAllGold(); public long StoredGold { get; }
// Tab Operations public bool TryUnlockNextTab(); public long GetNextUnlockCost(); public int UnlockedTabCount { get; } public bool IsTabUnlocked(int tabIndex);
// Slot Queries public BankSlotData GetSlot(int tabIndex, int slotIndex); public int TotalAvailableSlots { get; } public int TotalUsedSlots { get; } public int TotalEmptySlots { get; }}MessagePipe Events
Section titled “MessagePipe Events”Bank events registered in RootLifetimeScope:
// Bank request messages (client -> server)builder.RegisterMessageBroker<OpenBankRequest>(options);builder.RegisterMessageBroker<DepositItemRequest>(options);builder.RegisterMessageBroker<WithdrawItemRequest>(options);builder.RegisterMessageBroker<MoveBankItemRequest>(options);builder.RegisterMessageBroker<DepositGoldRequest>(options);builder.RegisterMessageBroker<WithdrawGoldRequest>(options);builder.RegisterMessageBroker<UnlockBankTabRequest>(options);
// Bank state messages (for UI updates)builder.RegisterMessageBroker<BankChangedMessage>(options);builder.RegisterMessageBroker<BankUIStateMessage>(options);Database Schema (Planned)
Section titled “Database Schema (Planned)”character_bank Table
Section titled “character_bank Table”CREATE TABLE IF NOT EXISTS rentearth.character_bank ( character_id uuid PRIMARY KEY REFERENCES rentearth.character(id) ON DELETE CASCADE,
-- Bank tabs (each tab = 28 slots) -- NULL in array = empty slot, UUID = item reference tab_1 uuid[] NOT NULL DEFAULT array_fill(NULL::uuid, ARRAY[28]), tab_2 uuid[] NOT NULL DEFAULT array_fill(NULL::uuid, ARRAY[28]), tab_3 uuid[] NOT NULL DEFAULT array_fill(NULL::uuid, ARRAY[28]), tab_4 uuid[] NOT NULL DEFAULT array_fill(NULL::uuid, ARRAY[28]), tab_5 uuid[] NOT NULL DEFAULT array_fill(NULL::uuid, ARRAY[28]),
-- Which tabs are unlocked tabs_unlocked smallint NOT NULL DEFAULT 1 CHECK (tabs_unlocked BETWEEN 1 AND 5),
-- Stored gold (separate from inventory gold) stored_gold bigint NOT NULL DEFAULT 0 CHECK (stored_gold >= 0),
updated_at timestamptz NOT NULL DEFAULT timezone('utc', now()));
-- Function to get total bank slotsCREATE OR REPLACE FUNCTION rentearth.bank_total_slots(tabs_unlocked smallint)RETURNS integer AS $$ SELECT tabs_unlocked * 28;$$ LANGUAGE sql IMMUTABLE;
-- Function to count used bank slotsCREATE OR REPLACE FUNCTION rentearth.bank_used_slots( tab_1 uuid[], tab_2 uuid[], tab_3 uuid[], tab_4 uuid[], tab_5 uuid[], tabs_unlocked smallint)RETURNS integer AS $$DECLARE total integer := 0;BEGIN SELECT count(*) INTO total FROM unnest(tab_1) AS s WHERE s IS NOT NULL; IF tabs_unlocked >= 2 THEN total := total + (SELECT count(*) FROM unnest(tab_2) AS s WHERE s IS NOT NULL); END IF; IF tabs_unlocked >= 3 THEN total := total + (SELECT count(*) FROM unnest(tab_3) AS s WHERE s IS NOT NULL); END IF; IF tabs_unlocked >= 4 THEN total := total + (SELECT count(*) FROM unnest(tab_4) AS s WHERE s IS NOT NULL); END IF; IF tabs_unlocked >= 5 THEN total := total + (SELECT count(*) FROM unnest(tab_5) AS s WHERE s IS NOT NULL); END IF; RETURN total;END;$$ LANGUAGE plpgsql IMMUTABLE;Item Definition Extension
Section titled “Item Definition Extension”-- Add bank restriction flag to item definitionsALTER TABLE rentearth.item_definitionsADD COLUMN can_bank boolean NOT NULL DEFAULT true;Proto Definitions (Planned)
Section titled “Proto Definitions (Planned)”Bank State Message
Section titled “Bank State Message”// Full bank state sent to client when opening bankmessage CharacterBankState { // Tab data (only unlocked tabs have items) repeated BankTab tabs = 1;
// How many tabs unlocked uint32 tabs_unlocked = 2;
// Gold stored in bank int64 stored_gold = 3;
// Full item data for items in bank repeated ItemInstance items = 4;}
message BankTab { uint32 tab_index = 1; // 0-4 repeated bytes slots = 2; // 28 UUIDs (empty bytes = empty slot) bool is_locked = 3;}Bank Actions
Section titled “Bank Actions”message BankAction { oneof action { OpenBankRequest open = 1; DepositItemRequest deposit = 2; WithdrawItemRequest withdraw = 3; MoveBankItemRequest move = 4; DepositGoldRequest deposit_gold = 5; WithdrawGoldRequest withdraw_gold = 6; UnlockTabRequest unlock_tab = 7; }}
// Request to open bank (must be near Bank NPC)message OpenBankRequest { bytes npc_entity_id = 1; // Bank NPC being interacted with}
// Deposit item from inventory to bankmessage DepositItemRequest { int32 inventory_slot = 1; int32 bank_tab = 2; // 0-4 int32 bank_slot = 3; // 0-27 within tab (-1 = first empty) uint32 quantity = 4; // For stacks (0 = all)}
// Withdraw item from bank to inventorymessage WithdrawItemRequest { int32 bank_tab = 1; int32 bank_slot = 2; int32 inventory_slot = 3; // -1 = first empty uint32 quantity = 4; // For stacks (0 = all)}
// Move item within bankmessage MoveBankItemRequest { int32 from_tab = 1; int32 from_slot = 2; int32 to_tab = 3; int32 to_slot = 4;}
// Deposit/withdraw goldmessage DepositGoldRequest { int64 amount = 1; // 0 = all}
message WithdrawGoldRequest { int64 amount = 1; // 0 = all}
// Unlock next bank tabmessage UnlockTabRequest { // Cost determined by server based on current tabs_unlocked}Bank Response
Section titled “Bank Response”message BankResponse { bool success = 1; string error = 2;
oneof result { CharacterBankState bank_state = 3; // Full state after open/unlock BankDelta delta = 4; // Partial update after deposit/withdraw UnlockTabResult unlock_result = 5; }}
message BankDelta { int32 tab = 1; repeated BankSlotChange changes = 2; int64 stored_gold = 3; // Updated bank gold int64 inventory_gold = 4; // Updated inventory gold}
message BankSlotChange { int32 slot = 1; bytes item_id = 2; // Empty = slot cleared}
message UnlockTabResult { uint32 new_tabs_unlocked = 1; int64 gold_spent = 2;}Bank NPC Interaction
Section titled “Bank NPC Interaction”Players must be near a Bank NPC to access their bank:
- Player interacts with Bank NPC (press E)
- Client sends
OpenBankRequestwith NPC entity ID - Server validates player is in range
- Server sends
CharacterBankState - Client opens bank UI
Range Validation (Server)
Section titled “Range Validation (Server)”const BANK_INTERACTION_RANGE: f32 = 5.0; // meters
fn validate_bank_access(player_pos: Vec3, npc_pos: Vec3) -> bool { player_pos.distance(npc_pos) <= BANK_INTERACTION_RANGE}Item Movement Flows
Section titled “Item Movement Flows”Deposit Flow
Section titled “Deposit Flow”1. Client sends DepositItemRequest2. Server validates: - Item exists in inventory - Target tab is unlocked - Target slot is empty (or can stack) - Item is not bound/quest item (optional restriction)3. Server updates: - items.container_type = 'bank' - items.container_slot = bank_slot - character_inventory.slots[inv_slot] = NULL - character_bank.tab_X[bank_slot] = item_id4. Server sends BankDelta + InventoryDeltaWithdraw Flow
Section titled “Withdraw Flow”1. Client sends WithdrawItemRequest2. Server validates: - Item exists in bank - Inventory has space3. Server updates: - items.container_type = 'inventory' - items.container_slot = inv_slot - character_bank.tab_X[bank_slot] = NULL - character_inventory.slots[inv_slot] = item_id4. Server sends BankDelta + InventoryDeltaGold Storage
Section titled “Gold Storage”Players can store gold in the bank for safekeeping:
- Bank gold is separate from inventory gold
- Bank gold is never lost on death
- No fees for deposit/withdraw (configurable)
// Example gold depositDepositGoldRequest { amount: 10000 }
// Server response includes updated balancesBankDelta { stored_gold: 15000, // Bank now has 15k inventory_gold: 5000 // Inventory now has 5k}Validation Rules
Section titled “Validation Rules”- Proximity: Must be near Bank NPC (configurable in BankConfig)
- Tab Access: Can only use unlocked tabs
- Slot Bounds: Slot index 0-27 within each tab
- Ownership: Item must belong to character
- Space Check: Target location has room
- Stack Rules: Same as inventory stacking
- Binding: Some items may be “cannot bank” (
can_bank = false)
Item Restrictions
Section titled “Item Restrictions”Items That Cannot Be Banked
Section titled “Items That Cannot Be Banked”- Quest items: Must stay in inventory (configurable)
- Soulbound: Depends on game design (configurable)
- Currently equipped: Must unequip first (configurable)
UI Considerations
Section titled “UI Considerations”Bank Window Layout
Section titled “Bank Window Layout”┌─────────────────────────────────────────────────────────────────┐│ BANK [X] Close │├─────────────────────────────────────────────────────────────────┤│ [Tab 1] [Tab 2] [Tab 3] [Tab 4] [Tab 5] ││ 🔒 🔒 🔒 🔒 │├─────────────────────────────────────────────────────────────────┤│ ┌────┬────┬────┬────┬────┬────┬────┐ ││ │ │ │ │ │ │ │ │ Row 1 ││ ├────┼────┼────┼────┼────┼────┼────┤ ││ │ │ │ │ │ │ │ │ Row 2 ││ ├────┼────┼────┼────┼────┼────┼────┤ ││ │ │ │ │ │ │ │ │ Row 3 ││ ├────┼────┼────┼────┼────┼────┼────┤ ││ │ │ │ │ │ │ │ │ Row 4 ││ └────┴────┴────┴────┴────┴────┴────┘ ││ ││ Gold Stored: 15,000 g [Deposit All] [Withdraw All] ││ ││ [Unlock Next Tab: 5,000 gold] │└─────────────────────────────────────────────────────────────────┘Integration with Auction House (Future)
Section titled “Integration with Auction House (Future)”When submitting an item to auction from bank:
- Item must be in bank
- Server moves item:
container_type = 'auction' - If auction expires/cancelled, item returns to bank (not inventory)
message SubmitToAuctionRequest { oneof source { int32 inventory_slot = 1; BankSlotRef bank_slot = 2; } int64 starting_bid = 3; int64 buyout_price = 4; int32 duration_hours = 5; // 12, 24, or 48}
message BankSlotRef { int32 tab = 1; int32 slot = 2;}Account-Wide Storage (Future)
Section titled “Account-Wide Storage (Future)”For shared storage between characters on same account:
CREATE TABLE IF NOT EXISTS rentearth.account_storage ( user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, slots uuid[] NOT NULL DEFAULT array_fill(NULL::uuid, ARRAY[28]), updated_at timestamptz NOT NULL DEFAULT timezone('utc', now()));Items in account storage would have container_type = 'account_storage'.
Implementation Phases
Section titled “Implementation Phases”Phase 1: Client Foundation (Complete)
Section titled “Phase 1: Client Foundation (Complete)”- Create
BankConfigScriptableObject - Create
BankStatewith reactive properties - Create
BankServiceAPI layer - Register MessagePipe events
- Create
BankConfig.assetwith default values
Phase 2: Bank UI
Section titled “Phase 2: Bank UI”- Create Bank UI with UI Toolkit
- Implement tab switching
- Implement slot grid display
- Add drag-and-drop support
- Add gold display and deposit/withdraw buttons
- Add unlock tab button with cost display
Phase 3: Server Integration
Section titled “Phase 3: Server Integration”- Add proto definitions to
snapshot.proto - Implement bank handlers in Axum server
- Add NPC proximity validation
- Connect client requests to server
Phase 4: Database Persistence
Section titled “Phase 4: Database Persistence”- Create
character_banktable migration - Add
can_bankcolumn toitem_definitions - Implement database operations in server
- Add bank state to character load
Phase 5: Polish
Section titled “Phase 5: Polish”- Add Bank NPC entity type
- Add bank interaction animation
- Add sound effects for deposit/withdraw
- Add toast notifications for errors
Open Questions
Section titled “Open Questions”| Question | Notes |
|---|---|
| Account-wide storage | Future feature - share between alts |
| Bank fees | Free for now, could add gold sink later |
| Remote banking | Allow bank access from anywhere? (premium feature?) |
| Guild bank | Shared storage for guilds |
Files Reference
Section titled “Files Reference”| File | Purpose |
|---|---|
Assets/Scripts/ScriptableObjects/BankConfig.cs | Bank configuration ScriptableObject |
Assets/Scripts/State/BankState.cs | Reactive bank state with thread safety |
Assets/Scripts/Services/BankService.cs | Bank API for UI and game systems |
Assets/Scripts/LifetimeScope/RootLifetimeScope.cs | MessagePipe registration |
Resources/ScriptableObjects/Oracle/BankConfig.asset | Default configuration values |
Related Documents
Section titled “Related Documents”- Game Design Document - Core game design documentation
- Combat System - Stats and entity systems
- Pool Oracle - Prefab and entity management