Shops
Обзор
Shops позволяет игрокам покупать и продавать предметы через меню-инвентарь, которое вы создаёте в веб-панели TypeWriter. Магазин — это одна запись Shop Definition: она содержит список предметов на продажу, их цены, валюту, лимиты, сообщения и раскладку меню. Игрок открывает его командой /tw shop <id> или через действие Open Shop, запущенное квестом, NPC или кнопкой другого меню.
У каждого предмета цена задаётся одним из трёх способов: фиксированная цена, динамическая цена, зависящая от запаса, или бартер, при котором предмет оплачивается другими предметами вместо денег. Покупки и продажи проходят через Vault, экономику PlaceholderAPI или встроенный баланс очков.
Само меню рисуется с помощью расширения GUI. Вы размещаете в раскладке маркеры, где появляются товары (SHOP_ITEM), и помеченные кнопки для переключения страниц, закрытия и выбора количества, а Shops заполняет их для каждого игрока.
Возможности
- Фиксированные, динамические цены и цены в предметах — единая цена за штуку, цена, растущая по мере уменьшения запаса, или список предметов, которые нужно отдать.
- Бартер —
priceModeITEMSпродаёт товар за один или несколько предметов, с ценой по умолчанию для всего магазина и токеном описания{item_price}. - Три валюты —
VAULT,PLACEHOLDER(баланс считывается из плейсхолдера PlaceholderAPI, изменяется консольными командами) и встроенный балансPOINTS. - Действия по клику и количества — купить или продать 1, стопку, максимум, всё, произвольное количество из диалога или фиксированное количество, например 10, в подменю.
- Запас, лимиты и сбросы — запас для каждого предмета, лимиты для игрока и глобальные, общие для магазина ограничения на покупку и продажу, сбросы по cron, интервалу, ежедневному, еженедельному или ежемесячному расписанию.
- Налоги и акции — глобальный налог, переопределение налога для отдельных предметов и процентные скидки на ограниченное время.
- Меню в панели — пул раскладок, подменю, предмет-заполнитель, описание с ценами и форматы названия и описания по умолчанию.
- Вкладка общего меню — магазин может стать представлением более крупного шасси GUI через
baseMenuId. - Админ-команды — список, просмотр, открытие, сброс, изменение запаса и чтение истории транзакций.
- Уведомления в Discord — транзакции объявляются как событие, которое расширение Discord передаёт в вебхук.
Установка
- Установите расширение GUI (версии 0.17 или новее) и TypeWriter на сервер Paper или Folia.
- По желанию, для валюты
VAULT: установите Vault и плагин экономики, регистрирующийся в нём. - По желанию, для плейсхолдеров в заголовках и описаниях и для валюты
PLACEHOLDER: установите PlaceholderAPI. - Скачайте JAR-файл Shops из релизов GitHub (кнопка в сводной панели выше), поместите его в
plugins/Typewriter/extensions/и перезапустите сервер. - В веб-панели создайте одну запись Shop Artifact и назовите её
shop_artifact. Shops читает и записывает запасы, лимиты и очки через эту запись.
Начало работы
Создайте небольшой оружейный магазин с оплатой деньгами Vault:
- На static-странице добавьте запись Shop Artifact с именем
shop_artifact. - Добавьте запись Shop Definition. Задайте
titleравным<gold>Weapons,rowsравным3иcurrencyравнымVAULT. - В
itemsдобавьте предмет. Выберите алмазный меч вitem, задайтеnameравным<aqua>Sword,priceModeравнымFIXED,fixedBuyPriceравным500иfixedSellPriceравным100. ОставьтеprimaryActionравнымBUY_1, аsecondaryActionравнымSELL_1. - В
layoutPoolдобавьте раскладкуsimpleсidmainи запишитеmainвmainLayoutId. Добавьте в неё предмет сbuttonPrefixshop_button:,buttonTypeSHOP_ITEM,x1,y1,count7 иdirectionright. Этот ряд маркеров — место, где появляются товары. - Добавьте в раскладку ещё три предмета с
buttonPrefixshop_button:иbuttonType, равнымPREV_PAGE,NEXT_PAGEиCLOSE, разместив их в нижнем ряду. Задайте каждомуitemиdisplayName. - Задайте
fillerItemравным серой окрашенной стеклянной панели, чтобы заполнить неиспользуемые слоты. - Опубликуйте (Publish), затем выполните в игре
/tw shop admin list, чтобы узнать идентификатор магазина, и откройте его командой/tw shop <id>. Чтобы вместо этого открывать его из квеста или от NPC, добавьте действие Open Shop, указывающее на магазин.
Левый клик покупает один меч, правый клик продаёт один, а Shift + левый клик открывает подменю покупки, если вы его настроили.
Магазин показывает на странице столько товаров, сколько у него слотов-маркеров SHOP_ITEM, а кнопки переключения страниц листают остальные. Если маркера нет, меню открывается совсем без товаров.
Как это работает
При открытии магазин проверяет свои criteria доступа, применяет ожидающий сброс, а затем строит меню для этого игрока. Клик отправляет действие обработчику транзакций, который проверяет лимиты, вычисляет цену с учётом акций и налога, списывает оплату, выдаёт предмет и записывает запас, счётчики лимитов и историю.
Меню
Раскладка и кнопки
Меню магазина — это раскладка simple в layoutPool у Shop Definition, выбираемая через mainLayoutId. У предметов раскладки задаются buttonPrefix shop_button: и buttonType:
buttonType | Назначение |
|---|---|
SHOP_ITEM | Слот, в который помещается товар. Повторяйте его с помощью count, direction, gap и repeatY. Для этого маркера префикс можно оставить пустым. |
NEXT_PAGE, PREV_PAGE | Переход по страницам товаров. |
INFO | Отправляет строки из infoMessage. |
CLOSE | Закрывает меню. |
BACK | Возвращает в предыдущее меню, например из подменю. |
BUY_1, SELL_1 | Купить или продать одну штуку. В подменю BUY_1:10 и SELL_1:10 торгуют 10 штуками; подойдёт любое число. |
BUY_STACK, SELL_STACK | Торговля полной стопкой предмета. |
BUY_MAX, SELL_ALL | Купить как можно больше или продать всё подходящее. |
BUY_CUSTOM, SELL_CUSTOM | Запросить у игрока количество в диалоге. |
BUY_SUBMENU, SELL_SUBMENU | Открыть подменю для предмета. |
Кнопки, кроме SHOP_ITEM, NEXT_PAGE, PREV_PAGE, INFO, CLOSE и BACK, действуют на предмет, выбранный игроком, поэтому их место в раскладке подменю. Всегда задавайте buttonPrefix равным shop_button:: префикс по умолчанию принадлежит другому расширению, и кнопка останется неактивной.
Подменю
Создайте в пуле вторую раскладку simple, поместите в неё кнопки количества и запишите её id в subMenuLayoutId. Заголовок берётся из subMenuTitle, где {item_name} — это name предмета. Предметы открывают его действиями BUY_SUBMENU и SELL_SUBMENU, которые по умолчанию назначены на Shift-клик.
Действия по клику
У каждого предмета три слота действий:
| Поле | По умолчанию | Клик по умолчанию |
|---|---|---|
primaryAction | BUY_1 | Левый клик |
secondaryAction | SELL_1 | Правый клик |
shiftAction | BUY_SUBMENU | Shift + левый клик |
Возможные действия: NONE, BUY_1, SELL_1, BUY_STACK, SELL_STACK, BUY_MAX, SELL_ALL, BUY_SUBMENU, SELL_SUBMENU, BUY_CUSTOM и SELL_CUSTOM. Общий для магазина interactionConfig определяет, какой клик запускает каждое действие (купить 1, продать 1, купить стопку, продать всё, подменю, произвольное количество, навигация). Каждый тип клика может запускать только одно действие на предмет, поэтому при конфигурации по умолчанию несколько действий, назначенных на левый клик, заменяют друг друга: измените interactionConfig, если используете больше трёх действий.
buyMaxEnabled отключает BUY_MAX для предмета.
Диалог выбора количества
BUY_CUSTOM и SELL_CUSTOM открывают диалог с одним текстовым полем. Настройте его с помощью buyAmountPrompt, sellAmountPrompt, amountConfirmButton, amountInputLabel, amountInputPlaceholder, amountInputWidth и amountInputMaxLength. Ответ должен быть положительным целым числом; иначе отправляется invalidAmountMessage. Обычные проверки цены, лимита и баланса выполняются при подтверждении количества.
Отображение предметов
Строки priceLore добавляются к каждому товару после собственного описания (lore) предмета и его lore. defaultNameFormat и defaultLoreFormat подставляются для предметов без собственных name или lore. В этих полях, а также в name и lore предмета заменяются следующие токены; строка, запрашивающая несуществующую цену, отбрасывается.
| Токен | Заменяется на |
|---|---|
{item} | Собственное название предмета. |
{buy}, {sell} | Отформатированная цена за штуку. Отбрасывается, если предмет нельзя купить или продать, если цена равна нулю и для бартерных предметов. |
{item_price} | Цена в предметах, по одной строке на каждый требуемый предмет, например - 12 × Copper Ingot (формулировка задаётся в itemPriceLineFormat). Отбрасывается для предметов, не оплачиваемых предметами. |
{stock}, {stock_max} | Текущий и максимальный запас. |
{currency} | currencySymbol. |
priceFormat задаёт вид отображаемой цены: {amount} — число с двумя десятичными знаками, а {currency} — символ.
Цены и экономика
Режимы цены. FIXED использует fixedBuyPrice и fixedSellPrice. DYNAMIC использует dynamicPricing: цена начинается с price (и sellPrice для продаж) при полном запасе и движется к max по мере уменьшения запаса шагами step от stockMax, ограничиваясь min и max и округляясь до rounding. ITEMS — это бартер.
Бартер. При priceMode ITEMS перечислите стоимость в itemPrice предмета или один раз для всего магазина в defaultItemPrice. Каждая строка — это предмет и amount на каждую покупаемую штуку. Все строки требуются одновременно, а при покупке нескольких штук каждая строка умножается. Собственный список товара заменяет список магазина и никогда не добавляется к нему. Предмет, для которого нигде не задана цена, отклоняется и записывается в лог, а не отдаётся даром. Оплата проверяется по всему инвентарю до того, как что-либо будет удалено, сначала проверяется место для покупаемого предмета, а предметы каталога сопоставляются по идентичности, а не только по материалу. BUY_MAX покупает столько штук, сколько инвентарь способен оплатить. Бартерный предмет нельзя продать обратно за деньги. Недостающие предметы сообщаются через cannotAffordItemsMessage, где {missing} перечисляет их, а {amount} — количество.
Валюты.
currency | Поведение |
|---|---|
VAULT | Использует экономику, зарегистрированную в Vault. Нужны плагин Vault и провайдер. |
PLACEHOLDER | Считывает баланс из balancePlaceholder и изменяет его, выполняя addCommand и removeCommand из консоли, где {player} и {amount} заменяются. Должны быть заданы все три. Понимаются такие числа, как 1,250.5 и 12.5k. |
POINTS | Баланс, который Shops хранит в своём артефакте. Он начинается с 0 и растёт, когда игроки продают предметы; в этом релизе нет команды для выдачи очков. |
Налоги. taxRate — это процент, добавляемый к покупкам и вычитаемый из продаж. При taxMode PER_ITEM значение taxRateOverride предмета (0 или больше) заменяет его для этого предмета; -1 сохраняет ставку магазина.
Акции. Shop Promotion уменьшает цену покупки и награду за продажу в магазине на процент, для всех предметов или для перечисленных targetItemIndices (позиция в items, начиная с 0).
Запас, лимиты и сбросы
dynamicPricing.stockMaxзадаёт запас предмета. Он начинается полным, уменьшается, когда игроки покупают, и растёт, когда продают.0означает, что запас не отслеживается. При включённомperPlayerу каждого игрока свой запас каждого предмета; сброс пополняет запас всех игроков. Пока магазин открыт,defaultRefreshTicks(иrefreshTicksпредмета) заставляют его перерисовываться при изменении показанного запаса, поэтому видны покупки других игроков.guiSlotпредмета закрепляет его в заданном слоте, аsellAllEnabledотключает его действие «продать всё». Подробности в Shop Definition.playerLimitиglobalLimitограничивают, сколько штук одного предмета может купить один игрок или все вместе за цикл сброса.globalPlayerLimit,globalBuyLimitиglobalSellLimitограничивают весь магазин;0означает без ограничений.limitReachedMessageотправляется при достижении ограничения.resetвосстанавливает запас и очищает счётчики лимитов. Политики:none,cron(выражение вида0 0 * * *),interval(секунды),daily,weekly(день 1 — понедельник) иmonthly. Время использует часовой пояс сервера. Первая проверка цикла происходит при открытии магазина, а также проверка выполняется каждую минуту.criteriaограничивает доступ в магазин и отправляетcriteriaFailMessage, когда они не выполняются. Предмет, собственныеcriteriaкоторого не выполняются, показывается какlockedItemи не может быть куплен.
Команды
Подкоманды /typewriter, обычно вводимые как /tw.
| Команда | Описание |
|---|---|
/tw shop <shop> | Открыть магазин для себя. Требуется typewriter.shop.open. |
/tw shop admin | Перечислить админ-команды. Требуется typewriter.shop.admin. |
/tw shop admin list | Перечислить загруженные магазины с их идентификатором, названием, числом предметов и числом пулов. |
/tw shop admin info <shop> | Показать определение магазина и его предметы с их индексом. |
/tw shop admin open <shop> [target] | Открыть магазин для игрока. |
/tw shop admin refresh [target] | Перерисовать открытый магазин игрока. |
/tw shop admin close [target] | Закрыть открытый магазин игрока. |
/tw shop admin reset <shop> | Немедленно сбросить запас и лимиты. |
/tw shop admin stock <shop> <item> get | Показать запас предмета с этим индексом. |
/tw shop admin stock <shop> <item> set <amount> | Задать запас. Также add и remove. |
/tw shop admin history [target] | Показать последние 10 транзакций игрока с последнего перезапуска. |
<shop> — это идентификатор записи, дополняемый клавишей Tab. <item> — это позиция предмета в items магазина, начиная с 0.
Редактируемые тексты
Ни один текст, показываемый в игре, не зашит в код. Три вида текстов можно изменить без пересборки.
То, что игрок читает во время торговли, — это поля Shop Definition: purchaseMessage, sellMessage, cannotAffordMessage, limitReachedMessage, priceLore и остальные, описанные выше.
Формулировки цен, значки тренда и строки пула — это поля той записи, к которой они относятся. Значение по умолчанию — текст, который расширение показывало всегда.
| Поле | Запись | По умолчанию | Назначение |
|---|---|---|---|
trendUpIcon | Shop Definition | <red>▲</red> | Плейсхолдер trend_icon предмета, цена которого выросла с последней покупки или продажи. |
trendDownIcon | Shop Definition | <green>▼</green> | Тот же плейсхолдер, когда цена упала. |
trendStableIcon | Shop Definition | <gray>●</gray> | Тот же плейсхолдер, когда цена не изменилась или предыдущая цена неизвестна. |
itemPriceLineFormat | Shop Definition | - {amount} × {name} | Одна строка токена лора {item_price} для каждого предмета, который требует цена в предметах. {amount} — количество, {name} — название предмета. |
unknownItemName | Shop Definition | unknown item | Название предмета цены, собственное название которого прочитать нельзя, — в {item_price} и в списке недостающих предметов. |
missingItemFormat | Shop Definition | {amount} × {name} | Один элемент списка {missing} в cannotAffordItemsMessage. {amount} — недостающее количество. |
missingItemSeparator | Shop Definition | , | Что ставится между двумя элементами списка {missing}. |
defaultItemName | Shop Definition | Item | {item_name} в subMenuTitle для предмета без name. |
noPriceText | Shop Definition | — | Показывается вместо числа там, где у цены в предметах его нет: плейсхолдеры buy_price и sell_price, а также {price} в сообщении о полном инвентаре для такого предмета. |
cooldownFormat | Shop Definition | {d}d:{h}h:{m}m:{s}s | Шаблон плейсхолдера cooldown, когда сам плейсхолдер его не задаёт. {d} — дни, {h}, {m}, {s} дополняются до 2 цифр. |
limitedLore | Shop Pool | <yellow>Limited: <red>{limit} | Последняя строка лора ограниченного предмета пула. {limit} — доступное количество. |
unlimitedLore | Shop Pool | <yellow>Unlimited | Последняя строка лора предмета пула без ограничения. |
Используйте %typewriter_shop:<shop>:<n>:trend_icon% в лоре предмета, чтобы показать значок. %typewriter_shop:<shop>:<n>:trend% возвращает up, down, stable или unknown; эти четыре слова фиксированы, потому что условия сравнивают с ними.
Ответы команд /tw shop, включая команды /tw shop admin, — это сниппеты TypeWriter. Откройте plugins/Typewriter/snippets.yml, найдите ключ в content.shop, измените значение и выполните /tw reload. Расширение регистрирует все свои сниппеты с текстом по умолчанию при запуске, поэтому после первого запуска они появляются в файле. Оставьте значения по умолчанию, и ничего не изменится.
Ответы написаны в MiniMessage и отправляются без префикса Typewriter, поэтому цвет ответа — часть его текста. {name} в тексте заменяется значением, указанным в таблице; несуществующий плейсхолдер остаётся как написан, а в значении плейсхолдеры повторно не ищутся. Значение, взятое из ваших страниц (название магазина, имя игрока, заголовок инвентаря), показывается как написано: его теги никогда не интерпретируются. Журнал консоли сервера остаётся на английском.
Сниппет (в content.shop.) | По умолчанию | Плейсхолдеры |
|---|---|---|
help | <gold>Shop commands:</gold> <yellow>/tw shop <id></yellow> to open a shop, <yellow>/tw shop admin</yellow> for operator tools. | нет |
unknown_shop_error | Unknown shop: {shop} (обычный текст) | {shop} (введённое значение) |
admin.help | список административных команд, по одной в строке | нет |
admin.unknown_shop | <red>Unknown shop: {shop} | {shop} |
admin.list_empty | <yellow>No shop definitions are currently loaded. | нет |
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. | нет |
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> (обычный текст) | нет; это {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 — ошибка команды, которую игра показывает красным во время ввода: это обычный текст, без MiniMessage.
Права
| Право | Что даёт |
|---|---|
typewriter.shop.open | /tw shop <shop>. По умолчанию выдано всем. |
typewriter.shop.admin | Все команды /tw shop admin. По умолчанию только операторам. |
Плейсхолдеры
Плейсхолдеры PlaceholderAPI с расширением typewriter. <shop> — это название или идентификатор записи магазина, а <n> — позиция предмета в items, начиная с 0.
| Плейсхолдер | Возвращает |
|---|---|
%typewriter_shop:<shop>:name% | Название записи. |
%typewriter_shop:<shop>:entryid% | Идентификатор записи. |
%typewriter_shop:<shop>:cooldown% | Время до следующего сброса, по умолчанию в виде 0d:00h:00m:00s (поле cooldownFormat магазина; в прежних версиях для дней использовалось j). Добавьте формат после подчёркивания, например cooldown_{h}h{m}m, используя {d}, {h}, {m} и {s}. |
%typewriter_shop:<shop>:<n>:buy_price% | Цена покупки предмета; для предметов обмена — noPriceText магазина (по умолчанию —). |
%typewriter_shop:<shop>:<n>:sell_price% | Цена продажи предмета; для предметов обмена — noPriceText магазина (по умолчанию —). |
%typewriter_shop:<shop>:<n>:stock% | Текущий запас. |
%typewriter_shop:<shop>:<n>:stock_max% | Максимальный запас. |
%typewriter_shop:<shop>:<n>:has_promotion% | true, когда к предмету применяется акция. |
Интеграция
- GUI — Shops открывает свои меню через сервис сессий GUI, поэтому
gui:back, звуки, расширенные шасси иbaseMenuIdведут себя так же, как в любом другом меню. ЗадайтеbaseMenuIdравным общей оболочке Open GUI, добавьтеviews, и магазин станет вкладкой:contentX,contentY,contentColumnsиcontentRowsразмещают товары внутри оболочки. - Discord — после каждой покупки или продажи Shops запускает
ShopTransactionEvent. Если установлено расширение Discord и в магазине заданnotificationWebhookId, запись Shop Notification форматирует уведомление. - Звуки —
buySound,sellSoundиdeniedSoundвоспроизводятся при соответствующем исходе.
Записи
Записей: 6, сгенерированы из исходного кода расширения.
Действия1
- Open ShopOpen a shop for a player
Артефакты1
- Shop ArtifactRuntime storage for shops
Манифесты4
- Item PoolA weighted pool of items for shops
- Shop CategoryA category within a shop
- Shop DefinitionDefines a shop
- Shop PromotionA shop discount promotion
Устранение неполадок
Меню открывается, но товаров нет. В раскладке нет маркера SHOP_ITEM, mainLayoutId не соответствует id раскладки в layoutPool, либо раскладка не является раскладкой simple. Товары появляются только на слотах-маркерах.
Кнопка ничего не делает. Её buttonPrefix не равен shop_button:, либо в её buttonType опечатка (типы пишутся в верхнем регистре). Кнопке переключения страниц или закрытия тоже нужен префикс; пустой префикс допускает только SHOP_ITEM.
Магазин сообщает «No economy available». При VAULT отсутствует Vault или его провайдер экономики. При PLACEHOLDER пусто одно из balancePlaceholder, addCommand или removeCommand. Для бартерного предмета сообщение также появляется, когда не задан ни itemPrice, ни defaultItemPrice, а консоль записывает идентификатор магазина и номер предмета.
Запасы и лимиты теряются при перезапуске или никогда не сохраняются. Должна быть запись Shop Artifact, у которой идентификатор или название — shop_artifact.
Предмет нельзя продать. Его цена продажи равна нулю (у бартерных предметов она всегда такая), sellEnabled выключен либо у игрока недостаточно подходящих предметов. Учитываются только предметы с идентичным типом и данными.
Консоль предупреждает, что предмет нельзя показать или он был перемещён. Предмет с guiSlot занимает маркер SHOP_ITEM. Если закреплённые предметы занимают все маркеры, остальные предметы размещаются в свободных ячейках области содержимого; если свободных ячеек не осталось, предмет не показывается. Добавьте маркеры SHOP_ITEM или освободите слот.
Предметы из itemPools отображаются, но их нельзя купить. В этом релизе действия покупки и продажи обрабатывают только записи items; предметы из пулов отображаются после них.
Лимиты никогда не сбрасываются. reset равен none. Выберите политику или используйте /tw shop admin reset <shop>.
/tw shop <id> сообщает, что магазин неизвестен. Команда принимает идентификатор записи, а не заголовок. Используйте /tw shop admin list.
Связанные расширения
- GUI — раскладки, сессии и механизм маркеров, используемый меню магазинов.
- Discord — объявляет транзакции магазина через вебхук.
- Quest Codex — использует ту же идею шасси для меню квестов.