Skip to content

Merchant

Jeremyzihanwu edited this page Oct 18, 2026 · 1 revision

Overview

The Travelling Merchant sells consumables, charms and weapon upgrades for Gold. The shop also offers Coin Flip and Item Draw. All purchases and rewards use the player's existing inventory and Gold balance.

Usage

  1. Approach the Travelling Merchant and press E to talk.
  2. Complete the greeting dialogue to open the shop.
  3. Check an item's description, price and Owned quantity, then select BUY. Unaffordable purchases are disabled.
  4. Select WEAPONS for weapon upgrades or CASINO for Coin Flip and Item Draw. Back to Shop returns to supplies.
  5. Select Leave (Esc) or press Escape to close the shop.

The shop freezes world entity updates and locks player movement and combat while open. The shop interface continues updating. Closing releases the shop's own freeze and control-lock requests.

Supplies

Each purchase adds one item to the inventory. Shop stock is unlimited.

Item Price (Gold) Effect
Small Health Potion 10 Restores up to 25 health when used.
Medium Health Potion 20 Restores up to 50 health when used.
Large Health Potion 35 Restores up to 100 health when used.
Shield 15 Provides temporary protection.
Speed Potion 15 Temporarily increases movement speed.
Strength Potion 20 Temporarily increases attack strength.
Freeze Bomb 20 Freezes enemies currently on screen.
Burn Vial 20 Sets enemies currently on screen alight.
Magnet Potion 20 Pulls nearby Gold and consumables towards the player.
Strength Charm 40 Adds 10 attack while equipped.
Speed Charm 40 Adds 1 movement speed while equipped.
Attack Speed Charm 40 Adds 1 attack speed while equipped.

Purchasing a consumable increases its quantity without using it. A newly purchased type is not automatically assigned to a consumable-bar slot; equip it through the inventory book if needed. An already-equipped type uses the updated quantity.

Each purchased charm is a separate, initially unequipped item. Buying two copies creates two independent charms. Bonuses apply only when equipped through the existing inventory system.

Weapon upgrades

The WEAPONS page offers one upgrade per weapon for 60 Gold each.

Shop entry Upgrade
Sword Adds 20% light-attack damage and unlocks a heavy sweep on K.
Knife Adds 20% light-attack damage and unlocks a three-strike heavy attack on K.
Throwing Knife Adds 20% light-attack damage and unlocks a spread attack on K.

Selecting a weapon shows its current/upgraded appearance and benefit summary; selection does not equip it. An already-purchased upgrade cannot be charged again.

The current shop labels its third entry Throwing Knife. Its internal product ID is bow, and it uses BowWeaponComponent. The purchase uses the existing WeaponUpgradeComponent; see Upgrading Weapons for weapon behaviour.

Casino

Both games use the same Gold balance as purchases. A valid play is settled when accepted. The animation presents that result and does not award another payout; closing during the animation does not undo the settled result.

Coin Flip

Coin Flip has a 50/50 chance of winning or losing the selected stake.

  • A win adds the stake to the balance; a loss subtracts it. A 10-Gold stake from a 50-Gold balance ends at 60 or 40 Gold.
  • The −10 / +10 buttons adjust the stake within the available balance. A player with fewer than 10 Gold can stake their remaining Gold.
  • A flip is rejected if the stake is zero, funds are insufficient or a winning payout would exceed the supported balance.
  • Flip controls are disabled during the animation.

Item Draw

Each draw costs 10 Gold and selects one outcome:

Outcome Reward Probability
Small Health Potion 1 item 1/6
Shield 1 item 1/6
Speed Potion 1 item 1/6
Strength Potion 1 item 1/6
Freeze Bomb 1 item 1/6
No prize No item 1/6

The no-prize outcome still costs 10 Gold. Rewards go into inventory and follow the same consumable-slot behaviour as purchases. Insufficient funds, an invalid reward or a quantity overflow rejects settlement without charging Gold.

The draw pool is configured separately from supplies. Selling twelve products does not make all twelve available as draw rewards.

Shop creation

ShopFactory builds the shop entity. It loads the supply and upgrade catalogs, creates their purchase services, and attaches ShopDisplay, ShopSessionComponent and ShopInputComponent.

Usage

The existing MainGameScreen registers the shop during UI setup:

ServiceLocator.getEntityService()
    .register(ShopFactory.createShop(player, terminal));

The factory uses the player's InventoryComponent and existing WeaponUpgradeComponent. Registering the returned entity starts its components and event listeners. The entity is configured with setUpdatesWhilePaused(true) so its UI remains active during the world freeze.

The overload ShopFactory.createShop(Entity player) creates the same shop without registering terminal commands. The overload accepting Terminal also provides shop open and shop close for testing. Both commands use the same shop session as NPC interaction.

Shop session

ShopSessionComponent controls when the shop is open. It connects the NPC interaction to the view and owns the shop's world freeze and player control lock.

NPC interaction and events

The session listens on the player entity's event handler:

Event Parameters Session behaviour
NpcInteractionEvents.INTERACTION_FINISHED (npcInteractionFinished) String npcId, Entity npc Opens only when npcId equals "merchant".
goldChanged Integer gold Refreshes the open shop.
consumableInventoryChanged String id, Integer count Refreshes the open shop.
weaponUpgraded Class<? extends WeaponComponent> weapon Refreshes the open shop.
entityDied None Closes the shop.

The existing NPC system sends the interaction-finished event after the greeting. The shop does not manage the dialogue itself. See Interactable NPCs for that system.

Charm purchases also emit charmAdded from inventory, but the session does not subscribe to it. A charm purchase updates Gold, which triggers a refresh, and the purchase button refreshes the view after processing the result.

How it works internally

open() ignores duplicate opens and requests after disposal. It then:

  1. Marks the session open.
  2. Calls EntityService.setFrozen(this, true) and PlayerActions.setControlsLocked(this, true).
  3. Clears the interaction prompt and shows the view with a close callback.
  4. Refreshes the displayed inventory state.

close() hides the view and releases both requests using the same owner. Other systems' locks remain intact. Player death and component disposal also close the shop. If showing the view fails, the session closes and releases its requests.

The session uses the ShopView interface:

public interface ShopView {
  void show(Runnable onClose);
  void close();
  void refresh();
}

ShopDisplay implements the view, while ShopInputComponent handles modal input and Escape. This keeps session behaviour separate from the Scene2D layout.

The world freeze does not stop GameTime.getTime(). Consumable effects using elapsed-time deadlines can lose remaining duration while shopping.

Purchasing

ShopService resolves offers and dispatches purchases. The display supplies an offer ID; the service obtains the product ID and price from its catalog.

Usage

The public purchase method is ShopPurchaseResult purchase(String offerId). For example, with the supply service created by ShopFactory:

ShopPurchaseResult result = service.purchase("HEALTH_POTION");

How it works internally

  1. Find the offer in ShopCatalog. An unknown ID returns INVALID_OFFER.
  2. Find the registered handler for its product kind. A missing handler returns UNSUPPORTED_PRODUCT.
  3. Pass inventory, product ID and the configured price to the handler.
  4. The handler uses the corresponding inventory transaction and returns a ShopPurchaseResult for the UI.
Product kind Handler Inventory transaction
CONSUMABLE ConsumablePurchaseEffect tryPurchaseConsumable(itemId, goldPrice)
CHARM CharmPurchaseEffect tryPurchaseCharm(itemId, goldPrice)
WEAPON_UPGRADE WeaponUpgradePurchaseEffect tryPurchaseWeaponUpgrade(upgrades, weaponClass, goldPrice)

Handlers implement the existing extension point:

public interface ShopPurchaseEffect {
  ShopPurchaseResult purchase(
      InventoryComponent inventory, String productId, int goldPrice);
}

Results are SUCCESS, INSUFFICIENT_GOLD, ALREADY_UPGRADED, INVALID_OFFER, UNSUPPORTED_PRODUCT or QUANTITY_LIMIT, depending on the product and failure.

Expected validation failures preserve Gold and ownership. Successful consumable and charm transactions update ownership and payment before notifying observers. Weapon transactions use the existing upgrade component and reject repeat purchases. This validation behaviour is not a rollback guarantee for arbitrary exceptions thrown by event listeners.

Casino settlement

CoinFlipGame.flip() changes the inventory balance once and returns the outcome. CoinFlipPanel animates that result.

GambleService.spin() checks funds, selects a weighted entry and asks InventoryGambleWallet to settle it. An item reward uses tryPurchaseConsumable(itemId, cost, quantity); a no-prize result deducts the draw cost without adding an item. ItemDrawPanel displays the settled result.

Configuration

Paths below are relative to source/core/assets:

Configuration Purpose
configs/shops/merchant.json Supply offers, product kinds and prices.
configs/shops/merchant-upgrades.json Weapon-upgrade offers and prices.
configs/shops/merchant-gambling.json Item Draw cost, rewards, quantities and weights.

A supply offer uses this format:

{
  "offerId": "HEALTH_POTION",
  "kind": "CONSUMABLE",
  "productId": "HEALTH_POTION",
  "goldPrice": 10
}

offerId must be unique within the catalog. productId must resolve through ItemCatalog to the declared consumable or charm type, and goldPrice must be positive. To sell another registered consumable or charm, add a valid offer to the supply configuration. A new product category also needs a purchase handler registered in ShopFactory.

For Item Draw, each reward has a positive weight. Its probability is its weight divided by the total weight. The current six entries all have weight 1. Changing supply offers does not update this reward pool automatically.

Testing

The merchant implementation includes the following test classes:

  • ShopCatalogTest
  • ShopServiceTest
  • CharmPurchaseEffectTest
  • ShopSessionComponentTest
  • ShopInputComponentTest
  • WeaponUpgradeCatalogTest
  • WeaponUpgradePurchaseEffectTest
  • WeaponUpgradePurchaseTest
  • ShopWeaponCombatIntegrationTest
  • CoinFlipGameTest
  • ItemGambleConfigTest
  • GambleServiceTest
  • ShopFactoryTest

Source and related documentation

Table of Contents

Home

Test Plan

Team 2

Features

Components
Player
Items and Inventory
Enemies
Interactable NPCs
Cutscenes and Dialogue
Music
Bosses
Rooms
Weapons
Abilities & Spells
Game Screens and Areas
Dungeons

Game Engine

Getting Started

Entities and Components

Service Locator

Loading Resources

Logging

Unit Testing

Debug Terminal

Input Handling

UI

Animations

Audio

AI

Physics

Terrain

Concurrency & Threading

Settings

Troubleshooting

MacOS Setup Guide

Clone this wiki locally