Shops
概览
Shops 让玩家通过你在 TypeWriter 网页面板中构建的物品栏菜单买卖物品。一个商店就是一个 Shop Definition 条目:它包含在售物品列表、价格、货币、限制、消息以及菜单布局。玩家可通过 /tw shop <id> 打开它,也可以通过由任务、NPC 或其他菜单的按钮触发的 Open Shop 动作打开。
每件物品的定价方式有三种:固定价格、随库存变化的动态价格,或者以物易物,即用其他物品而非金钱来支付。购买和出售可通过 Vault、PlaceholderAPI 经济或内置的点数余额进行。
菜单本身由 GUI 扩展绘制。你在布局中放置用于显示商品的标记(SHOP_ITEM),以及用于翻页、关闭和选择数量的带标签按钮,Shops 会为每位玩家填充它们。
功能
- 固定价格、动态价格和物品价格 — 每件的统一价格、随库存减少而上涨的价格,或需要交出的物品列表。
- 以物易物 —
priceMode为ITEMS时,以一种或多种物品交换一件商品,支持全店默认价格和{item_price}lore 令牌。 - 三种货币 —
VAULT、PLACEHOLDER(从 PlaceholderAPI 占位符读取余额,并通过控制台命令修改)以及内置的POINTS余额。 - 点击动作与数量 — 买卖 1 个、一组、最大数量、全部、通过对话框输入的自定义数量,或者在子菜单中买卖固定数量(例如 10 个)。
- 库存、限制与重置 — 每件物品的库存、按玩家和全局的限购、全店买卖上限,以及按 cron、间隔、每日、每周或每月计划进行的重置。
- 税费与促销 — 全局税率、按物品覆盖的税率,以及限时的百分比折扣。
- 面板中的菜单 — 布局池、子菜单、填充物品、价格 lore,以及默认的名称和 lore 格式。
- 共享菜单的标签页 — 通过
baseMenuId,商店可以成为更大的 GUI 底盘的一个视图。 - 管理员命令 — 列出、查看、打开、重置、编辑库存以及读取交易记录。
- Discord 公告 — 交易会作为事件公布,由 Discord 扩展转发到 webhook。
安装
- 在 Paper 或 Folia 服务器上安装 GUI 扩展(0.17 或更新版本)和 TypeWriter。
- 可选,用于
VAULT货币:安装 Vault 以及一个向其注册的经济插件。 - 可选,用于标题和 lore 中的占位符,以及
PLACEHOLDER货币:安装 PlaceholderAPI。 - 从 GitHub Releases(上方摘要面板中的按钮)下载 Shops 的 JAR,放入
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中,添加一个id为main的simple布局,并在mainLayoutId中填入main。向其中添加一个物品,buttonPrefix为shop_button:,buttonType为SHOP_ITEM,x为 1,y为 1,count为 7,direction为right。这一行标记就是商品出现的位置。 - 再向布局中添加三个物品,
buttonPrefix为shop_button:,buttonType分别设为PREV_PAGE、NEXT_PAGE和CLOSE,放在最下面一行。为每个物品设置item和displayName。 - 将
fillerItem设为灰色染色玻璃板,以填充未使用的槽位。 - 发布,然后在游戏中运行
/tw shop admin list查看商店 id,并用/tw shop <id>打开它。若想改为从任务或 NPC 打开,请添加一个指向该商店的 Open Shop 动作。
左键购买一把剑,右键出售一把,如果你配置了购买子菜单,Shift + 左键会打开它。
商店每页显示的商品数量等于它拥有的 SHOP_ITEM 标记槽位数,其余商品通过翻页按钮浏览。如果不存在任何标记,菜单打开时不会有任何商品。
工作原理
打开时,商店会检查其访问 criteria,应用待处理的重置,然后为该玩家构建菜单。点击会把动作发送给交易处理器,由它检查限制、结合促销和税费计算价格、扣除付款、交付物品,并记录库存、限购计数和历史记录。
菜单
布局与按钮
商店菜单是 Shop Definition 的 layoutPool 中的一个 simple 布局,由 mainLayoutId 选定。布局中的物品带有 shop_button: 的 buttonPrefix 和一个 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 布局,把数量按钮放进去,并在 subMenuLayoutId 中填入它的 id。标题来自 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 之后。对于没有自己的 name 或 lore 的物品,由 defaultNameFormat 和 defaultLoreFormat 补全。在这些字段以及物品的 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 移动,步长为 stockMax 的 step,受 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 | 列出已加载的商店及其 id、名称、物品数量和池数量。 |
/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> 是条目 id,可用 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} Lore 令牌中的一行,对应以物品计价所要求的每个物品。{amount} 为数量,{name} 为物品名称。 |
unknownItemName | Shop Definition | unknown item | 无法读取自身名称的价格物品的名称,用于 {item_price} 和缺少物品列表。 |
missingItemFormat | Shop Definition | {amount} × {name} | cannotAffordItemsMessage 的 {missing} 列表中的一项。{amount} 为缺少的数量。 |
missingItemSeparator | Shop Definition | , | {missing} 列表中两项之间的内容。 |
defaultItemName | Shop Definition | Item | 对于没有 name 的物品,subMenuTitle 中的 {item_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} | 池中限量物品的最后一行 Lore。{limit} 为可用数量。 |
unlimitedLore | Shop Pool | <yellow>Unlimited | 池中无限制物品的最后一行 Lore。 |
在物品的 Lore 中使用 %typewriter_shop:<shop>:<n>:trend_icon% 即可显示图标。%typewriter_shop:<shop>:<n>:trend% 返回 up、down、stable 或 unknown,这四个词是固定的,因为条件会与它们比较。
/tw shop 命令的回复(包括 /tw shop admin 命令)是 TypeWriter 的 snippet。打开 plugins/Typewriter/snippets.yml,找到 content.shop 下的键,修改值并运行 /tw reload。扩展在启动时会以默认文本注册其所有 snippet,因此首次启动后它们会出现在该文件中。保持默认值则不会有任何变化。
回复使用 MiniMessage,发送时不带 Typewriter 前缀,因此回复的颜色属于其文本的一部分。文本中的 {name} 会被表中所列的值替换;不存在的占位符会原样保留,值中不会再次查找占位符。来自你页面的值(商店名称、玩家名称、背包标题)按原样显示,其标签不会被解析。服务器控制台日志保持英文。
Snippet(位于 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> 是商店条目的名称或 id,<n> 是物品在 items 中的位置,从 0 开始。
| 占位符 | 返回 |
|---|---|
%typewriter_shop:<shop>:name% | 条目的名称。 |
%typewriter_shop:<shop>:entryid% | 条目的 id。 |
%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 与 layoutPool 中任何布局的 id 都不匹配,或者该布局不是 simple 布局。商品只会出现在标记槽位上。
某个按钮没有任何作用。 它的 buttonPrefix 不是 shop_button:,或者它的 buttonType 拼写有误(类型为大写)。翻页或关闭按钮同样需要该前缀;只有 SHOP_ITEM 允许前缀为空。
商店提示 “No economy available”。 使用 VAULT 时,缺少 Vault 或其经济提供者。使用 PLACEHOLDER 时,balancePlaceholder、addCommand 或 removeCommand 之一为空。对于以物易物的物品,当没有设置 itemPrice 和 defaultItemPrice 时也会出现该消息,并且控制台会记录商店 id 和物品编号。
重启后库存和限制丢失,或从未被保存。 必须存在一个 id 或名称为 shop_artifact 的 Shop Artifact 条目。
某个物品无法出售。 它的卖出价为零(以物易物的物品始终如此),sellEnabled 已关闭,或者玩家持有的匹配物品不足。只有物品类型和数据完全一致的物品才会被计入。
控制台警告某个物品无法显示或已被移动。 带有 guiSlot 的物品会占用一个 SHOP_ITEM 标记。当被固定的物品占满所有标记时,其余物品会放到内容区域的空闲格子上;如果没有空闲格子,该物品将不会显示。请添加 SHOP_ITEM 标记,或空出一个格子。
itemPools 中的物品显示出来了但无法购买。 在此版本中,买卖动作只处理 items 中的条目;池中的物品显示在它们之后。
限制始终不会重置。 reset 为 none。请选择一个策略,或使用 /tw shop admin reset <shop>。
/tw shop <id> 提示该商店未知。 该命令接受的是条目 id,而不是标题。请使用 /tw shop admin list。
相关扩展
- GUI — 商店菜单所使用的布局、会话和标记机制。
- Discord — 通过 webhook 公布商店交易。
- Quest Codex — 在任务菜单中使用相同的底盘理念。