Skip to content

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
): Entity

Spawn 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): void

Read 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.Element

The component needs to:

  1. Query the player entity and read their Gold trait
  2. Render sections for weapons, armor, and consumables (sourced from the shared GEAR and CONSUMABLES arrays)
  3. Show each item's name, stat bonuses, and cost
  4. Disable the buy button when the player can't afford the item
  5. Call a buyItem action 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): void

This 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 DataGEAR and CONSUMABLES arrays with item definitions and prices
  • Inventory and GoldbuyItem action 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.