Repository navigation
Merchant
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.
- Approach the Travelling Merchant and press E to talk.
- Complete the greeting dialogue to open the shop.
- Check an item's description, price and Owned quantity, then select BUY. Unaffordable purchases are disabled.
- Select WEAPONS for weapon upgrades or CASINO for Coin Flip and Item Draw. Back to Shop returns to supplies.
- 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.
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.
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.
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 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.
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.
ShopFactory builds the shop entity. It loads the supply and upgrade catalogs, creates their purchase services, and attaches ShopDisplay, ShopSessionComponent and ShopInputComponent.
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.
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.
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.
open() ignores duplicate opens and requests after disposal. It then:
- Marks the session open.
- Calls
EntityService.setFrozen(this, true)andPlayerActions.setControlsLocked(this, true). - Clears the interaction prompt and shows the view with a close callback.
- 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.
ShopService resolves offers and dispatches purchases. The display supplies an offer ID; the service obtains the product ID and price from its catalog.
The public purchase method is ShopPurchaseResult purchase(String offerId). For example, with the supply service created by ShopFactory:
ShopPurchaseResult result = service.purchase("HEALTH_POTION");- Find the offer in
ShopCatalog. An unknown ID returnsINVALID_OFFER. - Find the registered handler for its product kind. A missing handler returns
UNSUPPORTED_PRODUCT. - Pass inventory, product ID and the configured price to the handler.
- The handler uses the corresponding inventory transaction and returns a
ShopPurchaseResultfor 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.
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.
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.
The merchant implementation includes the following test classes:
ShopCatalogTestShopServiceTestCharmPurchaseEffectTestShopSessionComponentTestShopInputComponentTestWeaponUpgradeCatalogTestWeaponUpgradePurchaseEffectTestWeaponUpgradePurchaseTestShopWeaponCombatIntegrationTestCoinFlipGameTestItemGambleConfigTestGambleServiceTestShopFactoryTest