Appearance
How to Build: Vendor Shop
Gold is only interesting if you can spend it. The vendor shop is the simplest possible economy sink: an NPC who sells weapons, armor, and HP potions at fixed prices. No haggling, no buy-back, no selling your old gear. Every item is available from the start — the only gate is cost.
This is intentionally a prototype. The goal is to get a functioning economy loop (earn gold from combat, spend gold on upgrades, use upgrades to fight harder enemies) without over-engineering the shop experience. Let's build it.
NPC Traits
Before we can have a vendor, we need a way to represent NPCs in the Entity Component System. These traits will be reused for any future NPCs (quest givers, trainers, whatever):
typescript
const IsNpc = trait()
const NpcData = trait({
name: '' as string,
interactType: '' as 'shop' | 'checkpoint',
})IsNpc is a tag trait for queries. NpcData carries the NPC's name and what kind of interaction it offers. Right now we only need 'shop' and 'checkpoint', but this is easy to extend later.
Spawning the Vendor
The vendor is an entity in the world with a position and NPC data:
typescript
function spawnVendor(
world: World,
position: THREE.Vector3
): EntitySpawn it with IsNpc, Transform, and NpcData({ name: 'Vendor', interactType: 'shop' }).
For the prototype, we don't actually have an overworld hub yet. So the vendor is also accessible as a "Shop" button in the HUD menu bar — a shortcut that opens the same vendor menu without needing to walk to an NPC. When the hub gets built later, the button can be removed and the vendor becomes a proper in-world character.
Handling Interaction
When the player taps on or walks near the vendor NPC, you trigger the interaction:
typescript
function handleNpcInteraction(world: World, npc: Entity): voidRead the NpcData from the entity. If interactType is 'shop', call openMenu(world, 'vendor') to open the vendor panel. That's it — the interaction handler is a thin dispatcher.
Building the Vendor Menu
The vendor menu is a React component that lists all available items with prices and buy buttons. It reads the player's gold in real time using Koota's useTrait hook:
typescript
function VendorMenu(): JSX.ElementThe component needs to:
- Query the player entity and read their
Goldtrait - Render sections for weapons, armor, and consumables (sourced from the shared
GEARandCONSUMABLESarrays) - Show each item's name, stat bonuses, and cost
- Disable the buy button when the player can't afford the item
- Call a
buyItemaction on click
Notice how all items are always visible. There's no unlock progression in the shop itself — progression comes from earning enough gold to afford better gear. This keeps the shop dead simple while still creating a meaningful gold sink.
The Purchase Action
typescript
function buyItem(player: Entity, itemId: string): voidThis action (defined in the inventory system) checks the player has enough gold, deducts the cost, and adds the item to their inventory. The vendor menu re-renders automatically because useTrait reacts to the gold change.
Purchase Feedback
When the player buys something, they need to feel it. Keep the feedback simple for the prototype:
- The gold count updates immediately in the UI
- The item appears in the inventory list
- Optionally, a brief flash or color change on the buy button to confirm the action
No toast notifications, no particle effects, no animations. Just clear, immediate state changes that the player can see.
What This System Depends On
- Shared Game Data —
GEARandCONSUMABLESarrays with item definitions and prices - Inventory and Gold —
buyItemaction for handling purchases
We now have a functioning economy loop. The hero earns gold by fighting, spends it at the vendor, and uses the gear to fight tougher enemies. But we still need a way for the player to access the shop (and everything else) from the UI. That's the HUD.