Skip to content

How to Build: Inventory & Gold

The prototype needs just enough inventory to close the gameplay loop: kill enemies, earn gold, buy better gear, equip it, fight tougher enemies. No item drops, no crafting, no rarity tiers, no loot tables. Just a gold counter, two equipment slots, and a potion count.

I deliberately kept this minimal. It's tempting to build a 28-slot grid inventory with drag-and-drop and item stacking and tooltips. But for the prototype, the question is: can the player feel the progression from "Copper Dagger" to "Iron Greatsword"? If yes, the inventory system is doing its job. Everything else is polish for later.

The traits

These are defined on the player entity (from the player entity guide):

typescript
const Gold = trait({ amount: 0 })

const Equipment = trait(() => ({
  weapon: null as string | null,    // gear identifier from shared data
  armor: null as string | null,     // gear identifier from shared data
}))

const Inventory = trait(() => ({
  potions: 0,
  ownedGear: [] as string[],   // purchased but unequipped gear identifiers
}))

Equipment holds what's currently active -- one weapon slot, one armor slot. Inventory holds what's in the backpack -- potions and any gear that's been purchased but not equipped. Gold is separate because it's modified by combat (loot drops) independently from inventory actions.

The ownedGear array stores gear identifiers as strings, not full objects. When you need the stats of an owned item, look it up from the shared game data layer with getGearById(). This keeps the trait serializable and avoids duplicating data.

The four actions

All inventory mutations go through these actions:

Adding gold

typescript
function addGold(entity: Entity, amount: number): void

Reads the current gold amount, adds the specified value, writes it back. Called by the combat loot system when an enemy dies.

Buying items

typescript
function buyItem(entity: Entity, itemId: string): boolean

This handles both gear and consumables. Look up the item in shared data (try getGearById first, then getConsumableById). If the player can't afford it, return false. Otherwise, deduct the cost and add the item to the appropriate place: consumables increment the potion counter, gear gets added to ownedGear.

The boolean return value lets the vendor user interface show feedback -- "Not enough gold" versus a successful purchase animation.

Equipping gear

typescript
function equipGear(entity: Entity, gearId: string): void

Look up the gear definition to determine whether it's a weapon or armor. Check what's currently in that slot. If something is already equipped, move it back to ownedGear (you don't lose it, it just goes to inventory). Then remove the new gear from ownedGear and put it in the equipment slot.

This swap logic is the trickiest part. Make sure you handle the order carefully:

  1. Read the currently equipped item identifier for the relevant slot
  2. If there's a current item, push it to ownedGear
  3. Remove the new item from ownedGear
  4. Set the equipment slot to the new item

Getting the order wrong can duplicate or lose items. Write a few manual test cases: equip into an empty slot, swap with an occupied slot, equip the same item that's already equipped (should be a no-op or at least not break anything).

Using a potion

typescript
function usePotion(entity: Entity): boolean

Check if the player has potions. If not, return false. Otherwise, look up the potion's heal amount from shared data, apply it to the player's health (capped at max), and decrement the potion count.

The ConsumableSettings trait (from the player entity) controls automatic potion usage during combat: when enabled is true and the player's health drops below hpThreshold (default 40%), the combat system calls usePotion automatically. This is the "idle" part of the idle RPG -- the player doesn't have to manually heal during auto-battle.

The equipment menu

A React component that shows the player's current loadout:

typescript
function EquipmentMenu(): JSX.Element

It queries the player entity for Equipment and Inventory, then renders:

  • Weapon slot -- shows the equipped weapon's name and stats (or "Empty")
  • Armor slot -- shows the equipped armor's name and stats (or "Empty")
  • Owned gear list -- all items in ownedGear, each tappable to equip

When the player taps an owned item, call equipGear. The current item swaps back to the list, and the new item fills the slot. The user interface updates reactively because Koota trait changes trigger re-renders through the query hooks.

Stat comparison previews

When the player hovers over (or long-presses on mobile) an unequipped item, show the stat change that would result from equipping it. Call getEffectiveStats with the current equipment, then call it again with the proposed swap, and display the delta. Green numbers for improvements, red for downgrades. This is a small detail that makes a big difference for player decision-making.

The inventory menu

A simpler panel that shows:

  • Gold count -- just a number
  • Potion count -- number with a "Use" button (disabled if health is full)
  • All owned items -- gear list, same as equipment menu but focused on the backpack view

For the prototype, this can be a single scrollable list. No categories, no sorting, no search. Keep it minimal.

File organization

src/core/actions/inventory.ts       # addGold, buyItem, equipGear, usePotion
src/features/ui/menus/
├── EquipmentMenu.tsx               # Equipment slot panel with swap interaction
└── InventoryMenu.tsx               # Gold, potions, owned gear list

Actions live in core/ because they're called by multiple systems (combat calls addGold, vendor calls buyItem, user interface calls equipGear and usePotion). The menu components live in features/ui/ because they're purely presentational.

With inventory and gold in place, the prototype's economic loop is complete. The player earns gold from combat, spends it at the vendor, equips better gear, and takes on tougher enemies. Every system we've built so far -- shared data, player entity, skills, movement, pathfinding, spatial hash, and now inventory -- connects to form the core gameplay loop.