# Merchants and shops (trade.shop) ## What it is for `trade.shop` turns the `shops` collection into merchants. A shop lists offers (item, price, stock), may buy the player's goods back at a rate, and brings sold-out offers back after a while. Buying takes money through `economy.money` and gives the item through `inventory.items`; selling does the reverse. The kit holds the rules and emits events; the counter on screen is drawn by its interface fragment (buy on the left, sell on the right, the purse). For the balance itself and the family overview, see the [money and shops guide](guide:economy). ## Turn it on ```text gripforge_gamekit_install { project_id: "gkp_…", id: "trade.shop" } gripforge_gamekit_configure { project_id: "gkp_…", id: "trade.shop", config: { "buyRate": 0.4, "restockMinutes": 5 } } ``` It requires `economy.money`, `inventory.items` and `game.engine`, so installing it brings them. Describe the merchants in the `shops` collection of the project: ```json { "shops": [ { "id": "smith", "name": "Smith", "greeting": "Welcome", "buyRate": 0.25, "offers": [{ "item": "sword", "price": 60, "stock": 1 }, { "item": "potion", "price": 12.5 }], "buys": [{ "item": "gold", "price": 40 }] } ] } ``` A merchant in the world is an entity with the component `shop { id }`; interacting with it opens that shop. ## First example With 100 in the purse, a bag of two slots and the shop above, calling the service `trade.shop`: ```text buy("smith", "potion", 2) -> { ok: true, paid: 25 } balance 75 buy("smith", "sword") -> { ok: true, paid: 60 } buy("smith", "sword") -> { ok: false, reason: "sold_out" } buy("smith", "potion", 2) -> { ok: false, reason: "too_poor" } buy("smith", "rock") -> { ok: false, reason: "unknown" } buy("grocer", "apple") -> { ok: false, reason: "no_room" } a full bag refuses before any money moves ``` Every refusal also emits `shop.refused` with the same reason. ## Settings and defaults | Setting | Type | Default | Meaning | |---|---|---|---| | `buyRate` | number | `0.5` | Share of an item's price paid when the player sells it (0 to 2). A shop can set its own `buyRate`. | | `buysAnything` | boolean | `true` | A merchant buys any item that has a price in any shop. | | `restockMinutes` | number | `0` | Minutes before a sold-out offer comes back to its stock (0 to 1440); 0: never. | | `interactLabel` | string | `Trade with {name}` | The prompt on a merchant; `{name}` is the shop name. | A shop document has `id`, `name`, `offers` (required), and `greeting`, `buyRate`, `buys`. An offer has `item` (an item of the items collection), `price` and `stock` (`-1`, the default: unlimited). `buys` lists items the merchant buys that are not in its offers: an id, or `{ item, price }` with its own base price. ## Service methods Service `trade.shop`: - `shops()`: every shop as `{ id, name }`. - `open(id)`: open a shop (true or false), `close()`, `current()`: the id of the open shop. - `offers(id)`: each offer with `item`, `price`, `stock`, `affordable` and `sellPrice`. - `priceOf(shop, item)` and `sellPriceOf(shop, item)`: the buy price of an offer, and what the merchant pays for the item. - `buy(shop, item, n)` answers `{ ok: true, paid }`; `sell(shop, item, n)` answers `{ ok: true, earned }`. A refusal is `{ ok: false, reason }`. ## Verbs, events, variables, hooks - `open_shop { shop }` and `close_shop` (actions), `shop_open { shop? }` (condition: a shop, or this one, is open). - Variable `shop.open`: the id of the open shop, empty when none. - Events: `shop.opened { shop, name, greeting }`, `shop.closed { shop }`, `shop.bought { shop, item, count, paid }`, `shop.sold { shop, item, count, earned }`, `shop.refused { shop, item, reason }`. - Refusal reasons: `too_poor`, `sold_out`, `not_held`, `unknown`, `no_room`, `not_buying`, `closed`, or the reason given by a tap that cancelled. - Hook `shop.open` `{ shop }`: cancellable; the refusal is `closed` unless the tap gives a reason. - Hook `shop.buy` `{ shop, item, count, price }`: mutable `price` (the total), cancellable. Asked after the stock check and before the balance check. - Hook `shop.sell` `{ shop, item, count, price }`: mutable `price` (the total the merchant pays), cancellable. ```json { "hook": "shop.buy", "match": { "shop": "smith" }, "mul": { "price": 0.8 } } ``` ## Hooks and modding The settings and the `shops` collection are moddable. `shops` is a registry: `{ "id": "smith_hard", "extends": "smith" }` of a project or a mod inherits an entry and changes only what it lists. A discount, a tax or a closed-at-night merchant is a hook, not a change to the kit. ## Engines The rules run on the web and in the native player, where the hooks also run. The counter is an `ui.webui` fragment shared by the page and every engine. ## When it does not do what you expect - Nothing opens when interacting: the entity has no `shop` component, or its `id` is not in `shops`; a `shop.open` tap may refuse (`closed`). - Buying says `no_room`: the inventory cannot take the units; nothing was paid. - Selling says `not_buying`: the item is not in the offers or `buys` and `buysAnything` is off, or the item has no price. - Selling pays little: `buyRate` is `0.5` by default. - A sold-out offer never returns: `restockMinutes` is `0`. Selling a unit of an offer back puts it on the shelf. ## Limits A price is the one in the shop document, changed only by the hooks `shop.buy` and `shop.sell`. The stock and the restock timers are saved with the game. ## Versions Kit version 1.2.1; requires `economy.money` 1, `inventory.items` 1 and `game.engine` 1. Hooks arrived in 1.1.0 and the inheriting `shops` registry in 1.2.0.