Перейти к основному содержимому

Shops

Меню магазинов с фиксированными, динамическими или бартерными ценами, запасами, лимитами, налогами и тремя типами валюты.

Бесплатныеv1.12Экономика и крафт
ТребуетсяTypeWriter0.9GUI0.17+VaultнеобязательноPlaceholderAPIнеобязательноDiscordнеобязательно

Обзор​

Shops позволяет игрокам покупать и продавать предметы через меню-инвентарь, которое вы создаёте в веб-панели TypeWriter. Магазин — это одна запись Shop Definition: она содержит список предметов на продажу, их цены, валюту, лимиты, сообщения и раскладку меню. Игрок открывает его командой /tw shop <id> или через действие Open Shop, запущенное квестом, NPC или кнопкой другого меню.

У каждого предмета цена задаётся одним из трёх способов: фиксированная цена, динамическая цена, зависящая от запаса, или бартер, при котором предмет оплачивается другими предметами вместо денег. Покупки и продажи проходят через Vault, экономику PlaceholderAPI или встроенный баланс очков.

Само меню рисуется с помощью расширения GUI. Вы размещаете в раскладке маркеры, где появляются товары (SHOP_ITEM), и помеченные кнопки для переключения страниц, закрытия и выбора количества, а Shops заполняет их для каждого игрока.

Возможности​

  • Фиксированные, динамические цены и цены в предметах — единая цена за штуку, цена, растущая по мере уменьшения запаса, или список предметов, которые нужно отдать.
  • Бартер — priceMode ITEMS продаёт товар за один или несколько предметов, с ценой по умолчанию для всего магазина и токеном описания {item_price}.
  • Три валюты — VAULT, PLACEHOLDER (баланс считывается из плейсхолдера PlaceholderAPI, изменяется консольными командами) и встроенный баланс POINTS.
  • Действия по клику и количества — купить или продать 1, стопку, максимум, всё, произвольное количество из диалога или фиксированное количество, например 10, в подменю.
  • Запас, лимиты и сбросы — запас для каждого предмета, лимиты для игрока и глобальные, общие для магазина ограничения на покупку и продажу, сбросы по cron, интервалу, ежедневному, еженедельному или ежемесячному расписанию.
  • Налоги и акции — глобальный налог, переопределение налога для отдельных предметов и процентные скидки на ограниченное время.
  • Меню в панели — пул раскладок, подменю, предмет-заполнитель, описание с ценами и форматы названия и описания по умолчанию.
  • Вкладка общего меню — магазин может стать представлением более крупного шасси GUI через baseMenuId.
  • Админ-команды — список, просмотр, открытие, сброс, изменение запаса и чтение истории транзакций.
  • Уведомления в Discord — транзакции объявляются как событие, которое расширение Discord передаёт в вебхук.

Установка​

  1. Установите расширение GUI (версии 0.17 или новее) и TypeWriter на сервер Paper или Folia.
  2. По желанию, для валюты VAULT: установите Vault и плагин экономики, регистрирующийся в нём.
  3. По желанию, для плейсхолдеров в заголовках и описаниях и для валюты PLACEHOLDER: установите PlaceholderAPI.
  4. Скачайте JAR-файл Shops из релизов GitHub (кнопка в сводной панели выше), поместите его в plugins/Typewriter/extensions/ и перезапустите сервер.
  5. В веб-панели создайте одну запись Shop Artifact и назовите её shop_artifact. Shops читает и записывает запасы, лимиты и очки через эту запись.

Начало работы​

Создайте небольшой оружейный магазин с оплатой деньгами Vault:

  1. На static-странице добавьте запись Shop Artifact с именем shop_artifact.
  2. Добавьте запись Shop Definition. Задайте title равным <gold>Weapons, rows равным 3 и currency равным VAULT.
  3. В items добавьте предмет. Выберите алмазный меч в item, задайте name равным <aqua>Sword, priceMode равным FIXED, fixedBuyPrice равным 500 и fixedSellPrice равным 100. Оставьте primaryAction равным BUY_1, а secondaryAction равным SELL_1.
  4. В layoutPool добавьте раскладку simple с id main и запишите main в mainLayoutId. Добавьте в неё предмет с buttonPrefix shop_button:, buttonType SHOP_ITEM, x 1, y 1, count 7 и direction right. Этот ряд маркеров — место, где появляются товары.
  5. Добавьте в раскладку ещё три предмета с buttonPrefix shop_button: и buttonType, равным PREV_PAGE, NEXT_PAGE и CLOSE, разместив их в нижнем ряду. Задайте каждому item и displayName.
  6. Задайте fillerItem равным серой окрашенной стеклянной панели, чтобы заполнить неиспользуемые слоты.
  7. Опубликуйте (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-клик.

Действия по клику​

У каждого предмета три слота действий:

ПолеПо умолчаниюКлик по умолчанию
primaryActionBUY_1Левый клик
secondaryActionSELL_1Правый клик
shiftActionBUY_SUBMENUShift + левый клик

Возможные действия: 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 и остальные, описанные выше.

Формулировки цен, значки тренда и строки пула — это поля той записи, к которой они относятся. Значение по умолчанию — текст, который расширение показывало всегда.

ПолеЗаписьПо умолчаниюНазначение
trendUpIconShop Definition<red>▲</red>Плейсхолдер trend_icon предмета, цена которого выросла с последней покупки или продажи.
trendDownIconShop Definition<green>▼</green>Тот же плейсхолдер, когда цена упала.
trendStableIconShop Definition<gray>●</gray>Тот же плейсхолдер, когда цена не изменилась или предыдущая цена неизвестна.
itemPriceLineFormatShop Definition- {amount} × {name}Одна строка токена лора {item_price} для каждого предмета, который требует цена в предметах. {amount} — количество, {name} — название предмета.
unknownItemNameShop Definitionunknown itemНазвание предмета цены, собственное название которого прочитать нельзя, — в {item_price} и в списке недостающих предметов.
missingItemFormatShop Definition{amount} × {name}Один элемент списка {missing} в cannotAffordItemsMessage. {amount} — недостающее количество.
missingItemSeparatorShop Definition, Что ставится между двумя элементами списка {missing}.
defaultItemNameShop DefinitionItem{item_name} в subMenuTitle для предмета без name.
noPriceTextShop Definition—Показывается вместо числа там, где у цены в предметах его нет: плейсхолдеры buy_price и sell_price, а также {price} в сообщении о полном инвентаре для такого предмета.
cooldownFormatShop Definition{d}d:{h}h:{m}m:{s}sШаблон плейсхолдера cooldown, когда сам плейсхолдер его не задаёт. {d} — дни, {h}, {m}, {s} дополняются до 2 цифр.
limitedLoreShop Pool<yellow>Limited: <red>{limit}Последняя строка лора ограниченного предмета пула. {limit} — доступное количество.
unlimitedLoreShop 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_errorUnknown 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
Артефакты1
Манифесты4

Устранение неполадок​

Меню открывается, но товаров нет. В раскладке нет маркера 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 — использует ту же идею шасси для меню квестов.