Shops
Overview
Shops lets players buy and sell items from an inventory menu that you build in the TypeWriter web panel. A shop is one Shop Definition entry: it holds the list of items on sale, their prices, the currency, the limits, the messages and the menu layout. A player opens it with /tw shop <id> or from an Open Shop action fired by a quest, an NPC or a button of another menu.
Each item is priced in one of three ways: a fixed price, a dynamic price that follows the stock, or barter, where the item is paid for with other items instead of money. Purchases and sales go through Vault, a PlaceholderAPI economy or a built-in points balance.
The menu itself is drawn with the GUI extension. You place markers in a layout where products appear (SHOP_ITEM) and tagged buttons for paging, closing and quantities, and Shops fills them in for each player.
Features
- Fixed, dynamic and item prices — a flat price per unit, a price that rises as stock falls, or a list of items to hand over.
- Barter —
priceModeITEMSsells an article against one or several items, with a shop-wide default price and the{item_price}lore token. - Three currencies —
VAULT,PLACEHOLDER(balance read from a PlaceholderAPI placeholder, changed with console commands) and a built-inPOINTSbalance. - Click actions and quantities — buy or sell 1, a stack, the maximum, everything, a custom amount from a dialog, or a fixed amount such as 10 in a sub-menu.
- Stock, limits and resets — stock per item, per-player and global limits, shop-wide buy and sell caps, and resets on a cron, interval, daily, weekly or monthly schedule.
- Taxes and promotions — a global tax, per-item tax overrides, and timed percentage discounts.
- Menu in the panel — layout pool, sub-menu, filler item, price lore and default name and lore formats.
- Tab of a shared menu — a shop can become a view of a larger GUI chassis through
baseMenuId. - Admin commands — list, inspect, open, reset, edit stock and read transaction history.
- Discord announcements — transactions are announced as an event that the Discord extension relays to a webhook.
Installation
- Install the GUI extension (version 0.17 or newer) and TypeWriter on a Paper or Folia server.
- Optional, for the
VAULTcurrency: install Vault and an economy plugin that registers with it. - Optional, for placeholders in titles and lore, and for the
PLACEHOLDERcurrency: install PlaceholderAPI. - Download the Shops JAR from the GitHub releases (the button in the summary panel above), put it in
plugins/Typewriter/extensions/and restart the server. - In the web panel, create one Shop Artifact entry and name it
shop_artifact. Shops reads and writes its stock, limits and points through that entry.
Getting started
Build a small weapon shop paid with Vault money:
- On a static page, add a Shop Artifact entry named
shop_artifact. - Add a Shop Definition entry. Set
titleto<gold>Weapons,rowsto3andcurrencytoVAULT. - In
items, add an item. Choose a diamond sword initem, setnameto<aqua>Sword,priceModetoFIXED,fixedBuyPriceto500andfixedSellPriceto100. LeaveprimaryActiononBUY_1andsecondaryActiononSELL_1. - In
layoutPool, add asimplelayout with theidmain, and writemaininmainLayoutId. Add an item to it withbuttonPrefixshop_button:,buttonTypeSHOP_ITEM,x1,y1,count7 anddirectionright. This marker row is where products appear. - Add three more items to the layout with
buttonPrefixshop_button:andbuttonTypeset toPREV_PAGE,NEXT_PAGEandCLOSE, placed on the bottom row. Give each anitemand adisplayName. - Set
fillerItemto a gray stained glass pane so unused slots are filled. - Publish, then run
/tw shop admin listin game to read the shop id, and open it with/tw shop <id>. To open it from a quest or an NPC instead, add an Open Shop action pointing at the shop.
Left click buys one sword, right click sells one, and shift + left click opens the buy sub-menu if you configured one.
A shop shows as many products per page as it has SHOP_ITEM marker slots, and the paging buttons move through the rest. If no marker exists, the menu opens with no product at all.
How it works
On open, the shop checks its access criteria, applies a pending reset, then builds the menu for that player. A click sends the action to the transaction handler, which checks the limits, computes the price with promotions and tax, takes the payment, hands over the item and records the stock, the limit counters and the history.
Menus
Layout and buttons
A shop menu is a simple layout in the layoutPool of the Shop Definition, selected by mainLayoutId. Items of the layout carry a buttonPrefix of shop_button: and a buttonType:
buttonType | Role |
|---|---|
SHOP_ITEM | A slot where a product is placed. Repeat it with count, direction, gap and repeatY. The prefix may be left empty for this marker. |
NEXT_PAGE, PREV_PAGE | Move through the pages of products. |
INFO | Sends the lines of infoMessage. |
CLOSE | Closes the menu. |
BACK | Returns to the previous menu, for example from a sub-menu. |
BUY_1, SELL_1 | Buy or sell one unit. In a sub-menu, BUY_1:10 and SELL_1:10 trade 10 units; any number works. |
BUY_STACK, SELL_STACK | Trade a full stack of the item. |
BUY_MAX, SELL_ALL | Buy as many as possible, or sell everything matching. |
BUY_CUSTOM, SELL_CUSTOM | Ask the player for an amount in a dialog. |
BUY_SUBMENU, SELL_SUBMENU | Open the sub-menu for the item. |
Buttons other than SHOP_ITEM, NEXT_PAGE, PREV_PAGE, INFO, CLOSE and BACK act on the item the player selected, so they belong in the sub-menu layout. Always set the buttonPrefix to shop_button:: the default prefix belongs to another extension and the button would stay inert.
Sub-menu
Create a second simple layout in the pool, put the quantity buttons in it and write its id in subMenuLayoutId. The title comes from subMenuTitle, where {item_name} is the name of the item. Items open it with the BUY_SUBMENU and SELL_SUBMENU actions, which are the default shift click.
Click actions
Each item has three action slots:
| Field | Default | Default click |
|---|---|---|
primaryAction | BUY_1 | Left click |
secondaryAction | SELL_1 | Right click |
shiftAction | BUY_SUBMENU | Shift + left click |
The possible actions are NONE, BUY_1, SELL_1, BUY_STACK, SELL_STACK, BUY_MAX, SELL_ALL, BUY_SUBMENU, SELL_SUBMENU, BUY_CUSTOM and SELL_CUSTOM. The shop-wide interactionConfig decides which click triggers each action (buy 1, sell 1, buy stack, sell all, sub-menu, custom amount, navigation). Each click type can run only one action per item, so with the default configuration several actions mapped to a left click replace each other: change interactionConfig if you use more than three actions.
buyMaxEnabled switches BUY_MAX off for an item.
Amount dialog
BUY_CUSTOM and SELL_CUSTOM open a dialog with one text field. Customize it with buyAmountPrompt, sellAmountPrompt, amountConfirmButton, amountInputLabel, amountInputPlaceholder, amountInputWidth and amountInputMaxLength. The answer must be a positive whole number; otherwise invalidAmountMessage is sent. The normal price, limit and balance checks run when the amount is confirmed.
Item display
The lines of priceLore are appended to each product, after the item's own lore and its lore. defaultNameFormat and defaultLoreFormat fill in for items with no name or lore of their own. In these fields and in an item's name and lore, the following tokens are replaced; a line asking for a price that does not exist is dropped.
| Token | Replaced by |
|---|---|
{item} | The item's own name. |
{buy}, {sell} | Formatted unit price. Dropped when the item cannot be bought or sold, when the price is zero, and on barter items. |
{item_price} | The price in items, one line per requested item such as - 12 × Copper Ingot (wording set by itemPriceLineFormat). Dropped on items that are not paid in items. |
{stock}, {stock_max} | Current and maximum stock. |
{currency} | The currencySymbol. |
priceFormat shapes the displayed price: {amount} is the number with two decimals and {currency} the symbol.
Prices and economy
Price modes. FIXED uses fixedBuyPrice and fixedSellPrice. DYNAMIC uses dynamicPricing: the price starts at price (and sellPrice for sales) when the stock is full and moves toward max as stock falls, in steps of step of stockMax, clamped by min and max and rounded to rounding. ITEMS is barter.
Barter. With priceMode ITEMS, list the cost in the item's itemPrice, or once for the whole shop in defaultItemPrice. Each line is an item and an amount per unit bought. Several lines are all required at once, and buying several units multiplies every line. An article's own list replaces the shop's list, it is never added to it. An item with no price anywhere is refused and logged, never given away. The payment is checked on the whole inventory before anything is removed, space for the bought item is checked first, and catalogue items are matched by identity rather than by material only. BUY_MAX buys as many units as the inventory can pay. A barter item cannot be sold back for money. Missing items are reported with cannotAffordItemsMessage, where {missing} lists them and {amount} is the quantity.
Currencies.
currency | Behavior |
|---|---|
VAULT | Uses the economy registered with Vault. Needs the Vault plugin and a provider. |
PLACEHOLDER | Reads the balance from balancePlaceholder and changes it by running addCommand and removeCommand from the console, where {player} and {amount} are replaced. All three must be set. Numbers such as 1,250.5 and 12.5k are understood. |
POINTS | A balance kept by Shops in its artifact. It starts at 0 and grows when players sell items; the release has no command to grant points. |
Taxes. taxRate is a percentage added to purchases and subtracted from sales. With taxMode PER_ITEM, an item's taxRateOverride (0 or more) replaces it for that item; -1 keeps the shop rate.
Promotions. A Shop Promotion discounts the buy price and the sell reward of a shop by a percentage, for all items or the listed targetItemIndices (position in items, from 0).
Stock, limits and resets
dynamicPricing.stockMaxsets the stock of an item. It starts full, goes down when players buy and up when they sell.0means no stock tracking. WithperPlayeron, every player has their own stock of each item; a reset refills everyone's. While a shop is open,defaultRefreshTicks(and an item'srefreshTicks) make it redraw when the stock shown changes, so a player sees purchases made by others. An item'sguiSlotpins it at a fixed slot, andsellAllEnabledswitches off its sell-all action. Details in Shop Definition.playerLimitandglobalLimitcap how many units of one item a player, or everybody together, can buy per reset cycle.globalPlayerLimit,globalBuyLimitandglobalSellLimitcap the whole shop;0means unlimited.limitReachedMessageis sent when a cap is hit.resetrestores stock and clears the limit counters. Policies arenone,cron(an expression such as0 0 * * *),interval(seconds),daily,weekly(day 1 is Monday) andmonthly. Times use the server time zone. The first check of a cycle happens when the shop is opened, and a check also runs every minute.criteriarestricts access to the shop and sendscriteriaFailMessagewhen they do not match. An item whose owncriteriado not match is shown aslockedItemand cannot be bought.
Commands
Subcommands of /typewriter, usually typed as /tw.
| Command | Description |
|---|---|
/tw shop <shop> | Open a shop for yourself. Requires typewriter.shop.open. |
/tw shop admin | List the admin commands. Requires typewriter.shop.admin. |
/tw shop admin list | List loaded shops with their id, name, item count and pool count. |
/tw shop admin info <shop> | Show a shop definition and its items with their index. |
/tw shop admin open <shop> [target] | Open a shop for a player. |
/tw shop admin refresh [target] | Redraw the open shop of a player. |
/tw shop admin close [target] | Close the open shop of a player. |
/tw shop admin reset <shop> | Reset stock and limits now. |
/tw shop admin stock <shop> <item> get | Show the stock of the item at that index. |
/tw shop admin stock <shop> <item> set <amount> | Set the stock. Also add and remove. |
/tw shop admin history [target] | Show the last 10 transactions of a player since the last restart. |
<shop> is the entry id, completed with the Tab key. <item> is the position of the item in the shop items, starting at 0.
Editable texts
No text shown in game is fixed in the code. Three kinds of text can be changed without rebuilding anything.
What a player reads while trading is a field of the Shop Definition: purchaseMessage, sellMessage, cannotAffordMessage, limitReachedMessage, priceLore and the others described above.
Price wordings, trend icons and pool lines are fields of the entry they belong to. Their default is the text the extension always showed.
| Field | Entry | Default | Used for |
|---|---|---|---|
trendUpIcon | Shop Definition | <red>▲</red> | The trend_icon placeholder of an item whose price went up since the last purchase or sale. |
trendDownIcon | Shop Definition | <green>▼</green> | The same placeholder when the price went down. |
trendStableIcon | Shop Definition | <gray>●</gray> | The same placeholder when the price did not move, or when no earlier price is known. |
itemPriceLineFormat | Shop Definition | - {amount} × {name} | One line of the {item_price} lore token, for each item a price in items asks for. {amount} is the quantity, {name} the item name. |
unknownItemName | Shop Definition | unknown item | The name of a price item whose own name cannot be read, in {item_price} and in the missing items list. |
missingItemFormat | Shop Definition | {amount} × {name} | One entry of the {missing} list of cannotAffordItemsMessage. {amount} is the quantity that is lacking. |
missingItemSeparator | Shop Definition | , | What goes between two entries of the {missing} list. |
defaultItemName | Shop Definition | Item | The {item_name} of subMenuTitle for an item that has no name. |
noPriceText | Shop Definition | — | Shown instead of a number where a price in items has none: the buy_price and sell_price placeholders, and {price} in the inventory full message of such an item. |
cooldownFormat | Shop Definition | {d}d:{h}h:{m}m:{s}s | The pattern of the cooldown placeholder when the placeholder gives none. {d} days, {h}, {m}, {s} padded to 2 digits. |
limitedLore | Shop Pool | <yellow>Limited: <red>{limit} | The last lore line of a limited item of the pool. {limit} is the quantity available. |
unlimitedLore | Shop Pool | <yellow>Unlimited | The last lore line of an item of the pool that has no limit. |
Use %typewriter_shop:<shop>:<n>:trend_icon% in the lore of an item to show the icon. %typewriter_shop:<shop>:<n>:trend% returns up, down, stable or unknown, and these four words are fixed because conditions compare against them.
What the /tw shop commands answer, the /tw shop admin commands included, is a TypeWriter snippet. Open plugins/Typewriter/snippets.yml, find the key under content.shop, change the value and run /tw reload. The extension registers all its snippets with their default text when it starts, so they appear in the file after the first start. Keep the defaults and nothing changes.
Answers are MiniMessage and are sent without the Typewriter prefix, so the colour of an answer is part of its text. A {name} in a text is replaced by the value named in the table; a placeholder that does not exist stays as written, and a value is never searched for placeholders again. A value that comes from your pages (a shop name, a player name, an inventory title) is shown as written: its tags are never interpreted. The server console log stays in English.
Snippet (under content.shop.) | Default | Placeholders |
|---|---|---|
help | <gold>Shop commands:</gold> <yellow>/tw shop <id></yellow> to open a shop, <yellow>/tw shop admin</yellow> for operator tools. | none |
unknown_shop_error | Unknown shop: {shop} (plain text) | {shop} (what was typed) |
admin.help | the list of admin commands, one per line | none |
admin.unknown_shop | <red>Unknown shop: {shop} | {shop} |
admin.list_empty | <yellow>No shop definitions are currently loaded. | none |
admin.list_header | <gold>Loaded shops ({count}): | {count} |
admin.list_line | <white>- {id} | {name} | items={items} | pools={pools} | {id}, {name}, {items}, {pools} |
admin.opened | <green>Opened {shop} for {player}. | {shop}, {player} |
admin.refreshed | <green>Refreshed the shop session for {player}. | {player} |
admin.closed | <green>Closed the shop session for {player}. | {player} |
admin.reset | <green>Reset runtime stock and limits for {shop}. | {shop} |
admin.history_header | <gold>Recent transactions for {player} ({count}): | {player}, {count} |
admin.history_empty | <yellow>No transactions recorded in the current runtime. | none |
admin.history_line | <white>{time} | {type} | {shop}[{index}] | amount={amount} | price={price} | {time}, {type}, {shop}, {index}, {amount}, {price} |
admin.info_title | <gold>=== Shop {shop} === | {shop} |
admin.info_name | <white>Name: {name} | {name} |
admin.info_inventory_title | <white>Title: {title} | {title} |
admin.info_currency | <white>Currency: {currency} | rows={rows} | {currency}, {rows} |
admin.info_items | <white>Items: {items} direct | pools={pools} | access criteria={criteria} | {items}, {pools}, {criteria} |
admin.info_reset | <white>Reset policy: {policy} | {policy} |
admin.info_item | <white>[{index}] {name} | buy={buy} sell={sell} | playerLimit={playerLimit} globalLimit={globalLimit} | item criteria={criteria} | {index}, {name}, {buy}, {sell}, {playerLimit}, {globalLimit}, {criteria} |
admin.info_item_unnamed | <unnamed> (plain text) | none; it is the {name} of an item with no name |
admin.stock_unlimited | <yellow>{shop}[{index}] uses unlimited stock. | {shop}, {index} |
admin.stock_value | <green>{shop}[{index}] stock: {stock}/{max}. | {shop}, {index}, {stock}, {max} |
admin.stock_out_of_range | <red>Stock must stay between 0 and {max}. | {max} |
admin.stock_updated | <green>{shop}[{index}] stock set to {stock}/{max}. | {shop}, {index}, {stock}, {max} |
admin.unknown_item | <red>Unknown direct item index {index} in {shop}. | {index}, {shop} |
unknown_shop_error is a command error, shown in red by the game as the player types: it is plain text, without MiniMessage.
Permissions
| Permission | Grants |
|---|---|
typewriter.shop.open | /tw shop <shop>. Granted to everyone by default. |
typewriter.shop.admin | Every /tw shop admin command. Operators only by default. |
Placeholders
PlaceholderAPI placeholders, with the typewriter expansion. <shop> is the shop entry name or id and <n> the position of the item in items, from 0.
| Placeholder | Returns |
|---|---|
%typewriter_shop:<shop>:name% | Name of the entry. |
%typewriter_shop:<shop>:entryid% | Id of the entry. |
%typewriter_shop:<shop>:cooldown% | Time until the next reset, as 0d:00h:00m:00s by default (the cooldownFormat field of the shop; earlier releases used j for days). Add a format after an underscore, for example cooldown_{h}h{m}m, using {d}, {h}, {m} and {s}. |
%typewriter_shop:<shop>:<n>:buy_price% | Buy price of the item; the noPriceText of the shop (— by default) on barter items. |
%typewriter_shop:<shop>:<n>:sell_price% | Sell price of the item; the noPriceText of the shop (— by default) on barter items. |
%typewriter_shop:<shop>:<n>:stock% | Current stock. |
%typewriter_shop:<shop>:<n>:stock_max% | Maximum stock. |
%typewriter_shop:<shop>:<n>:has_promotion% | true when a promotion applies to the item. |
Integration
- GUI — Shops opens its menus through the GUI session service, so
gui:back, sounds, extended chassis andbaseMenuIdbehave as in any other menu. SetbaseMenuIdto a shared Open GUI shell, addviews, and the shop becomes a tab:contentX,contentY,contentColumnsandcontentRowsplace the products inside the shell. - Discord — after each purchase or sale Shops fires a
ShopTransactionEvent. When the Discord extension is installed and the shop setsnotificationWebhookId, a Shop Notification entry formats the announcement. - Sounds —
buySound,sellSoundanddeniedSoundplay on the matching outcome.
Entries
6 entries, generated from the extension source.
Actions1
- Open ShopOpen a shop for a player
Artifacts1
- Shop ArtifactRuntime storage for shops
Manifests4
- Item PoolA weighted pool of items for shops
- Shop CategoryA category within a shop
- Shop DefinitionDefines a shop
- Shop PromotionA shop discount promotion
Troubleshooting
The menu opens but shows no product. The layout has no SHOP_ITEM marker, mainLayoutId does not match a layout id in layoutPool, or the layout is not a simple layout. Products only appear on marker slots.
A button does nothing. Its buttonPrefix is not shop_button:, or its buttonType is misspelled (types are upper case). A paging or close button needs the prefix too; only SHOP_ITEM tolerates an empty prefix.
The shop says "No economy available". With VAULT, Vault or its economy provider is missing. With PLACEHOLDER, one of balancePlaceholder, addCommand or removeCommand is empty. With a barter item, the message also appears when no itemPrice and no defaultItemPrice is set, and the console logs the shop id and the item number.
Stock and limits are lost on restart, or never saved. There must be a Shop Artifact entry whose id or name is shop_artifact.
An item cannot be sold. Its sell price is zero (barter items always are), sellEnabled is off, or the player does not hold enough matching items. Only items with an identical item type and data count.
The console warns that an article cannot be shown, or was moved. An item with a guiSlot holds a SHOP_ITEM marker. When pinned items hold every marker, the other items are placed on the free cells of the content area; when there is no free cell left, the item is not shown. Add SHOP_ITEM markers or free a slot.
Items from itemPools appear but cannot be bought. Only the entries of items are handled by the buy and sell actions in this release; pool items are displayed after them.
Limits never reset. reset is none. Choose a policy or use /tw shop admin reset <shop>.
/tw shop <id> says the shop is unknown. The command takes the entry id, not the title. Use /tw shop admin list.
Related extensions
- GUI — layouts, sessions and the marker mechanism used by shop menus.
- Discord — announces shop transactions on a webhook.
- Quest Codex — uses the same chassis idea for quest menus.