商店模式
Malkuth 当前支持 9 种商店模式。你可以通过 shop/*.yml 顶层的 mode 字段选择模式;mode 缺省或留空时按 normal 处理,非空但无法识别时会跳过加载该商店配置。
模式一览
| 模式值 | 页面 | currency | goods | 适合场景 |
|---|---|---|---|---|
normal | 普通商店 | 必填 | 必填 | 固定商品商城、礼包、权限/脚本商品、物物交换 |
limit_time / limit-time | 限时商店 | 必填 | 必填 | 限时活动、节日特卖、限时抢购 |
player | 玩家自制商店 | 必填 | 不读取 | 玩家自己上架商品、摆摊、玩家商店方块 |
recycle | 回收商店 | 可省略 | 必填 | 服务器回收物品、按规则发放奖励 |
recycle_shop / recycle-shop | 回收兑换商店 | 必填 | 必填 | 复用回收会话但要求货币上下文的回收入口 |
auction | 拍卖行 | 必填 | 不读取 | 玩家竞价拍卖、出价记录、拍品结算 |
global_market / global-market | 全球市场 | 必填 | 不读取 | 全服寄售市场、搜索、排序、筛选、均价展示 |
global_request / global-request / request | 全球求购 | 必填 | 不读取 | 玩家发布求购、供货者提交物品、求购者审核成交 |
decompose | 分解商店 | 必填 | 必填 | 拖入物品后按回收规则分解成奖励 |
通用配置
以下字段是商店配置的通用入口,具体是否生效要看对应模式页面:
| 字段 | 说明 |
|---|---|
mode | 商店模式,支持上表中的小写值、短横线别名,以及同名枚举写法如 LIMIT_TIME |
enable | 商店开关,支持 Kether 表达式,例如 true、false、perm vip.shop |
display | 商店显示名称,用于搜索、比价、市场等展示场景;不配置时通常显示配置文件名 |
item_cache.enabled | 是否在重载商店时预构建无玩家上下文的商品快照,用来减少大型配置商品商店打开时的首屏空白 |
on_disable | 商店关闭或 enable 不通过时执行的 Kether 动作 |
currency | 商店货币配置;除 recycle 外,其余模式都必须能解析出货币 |
title | 商店标题;普通分页标题常用 {page} / {max-page},限时商店还可用 {countdown} |
layout | GUI 字符布局;g 通常表示商品槽位,其他字符对应 icons |
icons | 布局中非 g 字符的图标与动作定义 |
confirm.open | 普通购买链路的确认界面开关,可被商品级 goods.*.confirm.open 覆盖 |
item_count_limit | 购买链路的商店级商品数量限制 |
trade_amount_limit | 购买链路的商店级交易金额限制 |
target_inventory | 购买发货或回收扣物的目标背包源;常用值:vanilla / legendwarehouse / soulringx |
refresh | 随机商品刷新配置,最常用于 normal / limit_time 这类读取 goods 的购买型商店 |
schedule | 定时活动折扣配置;命中窗口后会给当前商店价格乘上 discount |
decompose | decompose 模式专用配置,其他模式不会读取这部分 |
商品展示缓存
item_cache.enabled 主要适合固定配置商品较多的 normal、limit_time 商店。开启后,插件会在加载或重载 shop/ 时构建一份不依赖具体玩家的商品 ItemStack 快照;玩家打开普通 / 限时商店时会先用这份快照占位,再异步回填限购、余额、权限、PAPI 等玩家态展示。
item_cache:
enabled: false
当前源码不会给 player、auction、global_market、global_request、decompose 这几类数据库或交互型商店构建商品缓存;recycle / recycle_shop 虽然没有被缓存构建排除,但当前回收 UI 不读取 ShopItemCache 首屏缓存,所以不要把它当作回收性能开关使用。
商品展示变量
普通购买型商店的 template 与商品展示通常可以使用:
| 变量 | 说明 |
|---|---|
{name} | 商品名称 |
{lore} | 商品 lore(全部) |
{lore_N} | 商品第 N 组 lore |
{price} | 商品价格;纯物品购买模式下会显示所需物品描述 |
{has_currency} | 玩家持有货币数量;纯物品购买模式下会显示已满足的物品数量 |
{limit} / {limit_remaining} | 当前商品可购买剩余次数 |
{amount} | 本次购买数量 |
{buy_mode} | 当前购买模式文本(货币 / 物品 / 混合) |
{buy_items} / {buy_items_N} | 当前商品需要的物品描述;N 从 1 开始 |
{currency_type} | 当前生效货币标识;商品级货币覆盖后也会跟着变化 |
{condition_lore} | 条件展示列表;仅 normal / limit_time 的商品模板支持,需配合 template.condition_lore 配置 |
{shop_player_current} / {shop_player_max} / {shop_player_remaining} | 商店级个人限制状态 |
{shop_global_current} / {shop_global_max} / {shop_global_remaining} | 商店级全局限制状态 |
{goods_player_current} / {goods_player_max} / {goods_player_remaining} | 商品级个人限制状态 |
{goods_global_current} / {goods_global_max} / {goods_global_remaining} | 商品级全局限制状态 |
回收模式还有额外的回收占位符,详见 回收商店 与 回收系统。
条件 Lore 展示
如果你已经用 goods.*.condition 做 VIP 折扣、权限价或条件展示覆写,可以在普通 / 限时商店的 template.lore 中单独写一行 {condition_lore},让玩家直接看到每条条件是否满足。这个功能只改变商品 Lore 展示,不改变真实购买判定;真实价格、展示覆写和购买覆写仍按 condition.priority 取最高优先级命中的规则。
下面节选自默认 shop/normal_full.yml:
goods:
1:
id: diamond_sword_basic
amount: 1
price: 500
condition:
- id: vip-discount
priority: 100
when: 'perm vip'
text: '拥有 VIP 权限,享受 420 金币折扣价'
buy:
price: 420
template:
lore:
- '{lore}'
- '&e价格: &f{price} 金币'
- '{condition_lore}'
condition_lore:
source: goods_condition
pass: '&d条件 &a[满足] &f{text}'
fail: '&d条件 &c[未满足] &f{text}'
loading: '&d条件 &7[检查中] &f{text}'
hide_passed: false
hide_failed: false
empty: ''
| 字段 | 说明 |
|---|---|
template.condition_lore.source | inline 只读取本节点 rules,goods_condition 读取 goods.*.condition,both 合并两者 |
pass / fail / loading | 条件满足、未满足、异步检查中时的单行格式;可用 {id}、{text}、{status}、{priority} 和普通商品模板变量 |
hide_passed / hide_failed | 是否隐藏已满足或未满足的条件行 |
empty | 没有可展示条件时的兜底行;留空则不显示 |
all_passed.enabled / all_passed.text | 所有条件满足时额外显示的汇总行 |
rules | source: inline 或 both 时读取,支持 map 或 list 写法;每条规则需要 when / if |
当 source: goods_condition 时,条件文案按 text、lore_text、condition_lore 的顺序读取;如果都没写,会退回显示规则 id。
随机商品
随机商品不是单独的商店模式,而是给读取 goods 的商店槽位追加随机结果。默认示例在 shop/random_daily.yml 与 shop/random_weekly_cron.yml。
# shop/random_daily.yml
mode: NORMAL
currency: vault
title: '&6&l每日随机商店 &7{page}/{max-page}'
refresh:
window: DAILY
goods:
random_weapons:
random_pool:
count: 7
unique: true
items:
- id: 'diamond_sword_basic'
weight: 30
- id: 'diamond_sword_enhanced'
weight: 15
- id: 'mm_legendary_sword'
weight: 5
random_price:
min: 100
max: 500
amount: 1
| 字段 | 说明 |
|---|---|
refresh.window | 刷新窗口:DAILY / WEEKLY / MONTHLY / PERMANENT / CUSTOM / CRON |
refresh.custom_hours | CUSTOM 窗口的自定义小时数 |
refresh.cron_expression | CRON 窗口的 Cron 表达式 |
random_pool.count | 从候选池中抽取的数量 |
random_pool.unique | 是否去重,默认 true |
random_pool.items[].id | 候选商品 ID,对应 goods/*.yml |
random_pool.items[].weight | 权重,越大越容易被抽中 |
random_pool.items[].random_price | 候选商品级随机价格,优先于槽位级价格 |
random_price.min / random_price.max | 槽位级随机价格范围 |
random_price.decimal | 是否允许小数,默认 false |
管理员可用 /malkuth random refresh <商店ID> [玩家] 手动刷新随机结果,用 /malkuth random info <商店ID> <玩家> 查看指定玩家的随机结果。
random_id 简写
如果不需要从池中抽取多个商品,只想让一个槽位随机显示一个商品,可以省略 count / unique 包装,直接写 random_id:
# shop/random_weekly_cron.yml
refresh:
window: CRON
cron_expression: "0 0 18 ? * FRI"
goods:
weekend_special:
random_id:
- id: 'diamond_sword_enhanced'
weight: 10
- id: 'mm_legendary_sword'
weight: 10
random_price:
min: 3000
max: 8000
amount: 1
random_id 与 random_pool 共用一个解析入口,区别在于:random_id 直接写列表(每个元素为 id + weight),固定只抽 1 个且无需去重参数。槽位级 random_price 与候选商品级 random_price 同样生效。
定时活动折扣
定时活动折扣不是独立商店模式,而是给任意商店追加一个按 Cron 生效的价格乘数。默认示例在 shop/schedule_example.yml。
schedule:
cron: '0 0 20 * * ?'
duration_hours: 2
discount: 0.8
| 字段 | 说明 |
|---|---|
schedule.cron | Quartz Cron 表达式;命中时段开始后商店进入活动窗口 |
schedule.duration_hours | 每次活动持续小时数;必须大于 0 |
schedule.discount | 活动窗口内的价格乘数;1.0 不打折,0.8 表示八折 |
当前实现里,discount 会在 ShopGoods.getEffectivePrice() 的最后阶段乘到有效价格上,所以它会叠加在基础价格、条件价、随机价、动态价之后统一生效。