formation(阵法)
文件位置:data/<namespace>/mxt/formation/<path>.json
用途:阵法生命周期和灵气覆写。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
structure_template | Identifier | 必填 | 原版结构模板 ID。 |
structure | List<RequiredBlock> | 见文档 | 内联结构,与 structure_template 二选一。 |
radius | NumberProvider | 必填 | 阵法影响半径。 |
activation_costs | List<ResourceCost> | [] | 激活时消耗。 |
maintenance_costs | List<ResourceCost> | [] | 每个维护周期消耗;失败时失效。 |
storage | Storage | 无(不启用) | 阵法自己的存量:capacity(Map<Holder<aura>, NumberProvider>,必填)声明每种灵气最多存多少。存下自家地脉供应的盈余,付账顺序变成 地脉 → 存量 → 阵主。通用字段,任何阵法都能写。 |
actions | List<Formation Action> | [] | 功能模块列表:mxt:attack、mxt:buff、mxt:protection、mxt:range_display、mxt:none。灵气覆写 aura_zone 与上限加成 max_bonus 属于 mxt:buff 模块的字段;把守御交给领地插件用 mxt:protection 的 delegate_to_claims;画范围轮廓用 mxt:range_display 的 particle。 |
spare_friends | boolean | false | 敌我识别开关:逐实体行为给覆盖范围内所有人,还是先过一道敌我判断(好友与认不出身份的不受影响)。它只决定"全部 / 识别",不代表阵法是攻击型——那是 actions 的事。 |
activate_action | BlockAction | mxt:no_op | 激活方块行为。 |
tick_action | BlockAction | mxt:no_op | 阵法周期方块行为。 |
deactivate_action | BlockAction | mxt:no_op | 失效方块行为。 |
entity_tick_action | EntityAction | mxt:no_op | 对半径内每个实体执行。 |
entity_enter_action | EntityAction | mxt:no_op | 实体进入半径时执行。 |
entity_exit_action | EntityAction | mxt:no_op | 实体离开半径或阵法拆除时执行。 |
结构尺寸不受结构方块的 48×48×48 限制。 那对常量(
StructureBlockEntity.MAX_SIZE_PER_AXIS/MAX_OFFSET_PER_AXIS)只用于原版结构方块读取自身 NBT 时的 clamp,是编辑器限制,不是格式或阵法限制:StructureTemplate.load读size不设上限,结构方块 LOAD 一个更大的模板时会直接采用模板尺寸,/place template也没有尺寸检查。因此手工用结构方块圈选做不了超过 48³ 的模板,但外部工具生成的.nbt可以被阵法正常引用。阵法的作用范围由radius决定,与结构包围盒大小无关。
formation 定义一座阵法:结构、半径、资源消耗、生命周期行为,以及它做什么(actions 里的功能模块)。运行时阵法还可以提供临时灵气覆写与灵气上限加成。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
structure_template | Identifier | 见下 | 结构模板;controller 即模板原点 |
structure | List<RequiredBlock> | 见下 | 内联结构;controller 即偏移原点 |
radius | NumberProvider | 必填 | 球形作用半径 |
activation_costs | List<ResourceCost> | [] | 激活消耗 |
maintenance_costs | List<ResourceCost> | [] | 每 20 tick 的维持消耗;阵法内方块供的灵气先抵扣,缺口由阵主支付,付不出即拆除(可被拦截,见下) |
storage | Storage | 无(不启用) | 阵法自己的存量:存下自家地脉供应的盈余,付账顺序变成 地脉 → 存量 → 阵主。通用字段,任何阵法都能写;不写就是不启用,见「存量」 |
actions | List<Formation Action> | [] | 这座阵法做什么:功能模块列表,见「阵法功能」 |
spare_friends | boolean | false | 敌我识别开关:逐实体行为是给覆盖范围内所有人,还是先过一道敌我判断(好友与认不出身份的都不受影响)。它只决定"全部 / 识别",不代表这座阵法是攻击型——那是 actions 的事,见「敌我判断」 |
activate_action | Block Action | mxt:no_op | 激活时 |
tick_action | Block Action | mxt:no_op | 每 20 tick,上下文是 Level |
deactivate_action | Block Action | mxt:no_op | 拆除时,上下文是 Level |
entity_tick_action | Entity Action | mxt:no_op | 每 20 tick,对半径内每个实体 |
entity_enter_action | Entity Action | mxt:no_op | 实体进入半径时 |
entity_exit_action | Entity Action | mxt:no_op | 实体离开半径、或阵法被拆除时 |
阵法功能(actions)
actions 回答"这座阵法做什么"。它是一个模块列表,每一项用 type 选中一种功能,并且只带自己需要的字段—— 结构、半径、成本、生命周期钩子留在顶层共用,功能参数放进模块里。这样加一种新阵法不必给公共字段表再添一个对 其他所有阵法都无意义的字段。
{
"structure": [ { "offset": [0, 0, 0], "state": "minecraft:gold_block" } ],
"radius": 12,
"activation_costs": [ { "id": "mxt:spirit_power", "amount": 100 } ],
"actions": [
{ "type": "mxt:attack", "damage": 8, "damage_type": "minecraft:lightning_bolt" },
{ "type": "mxt:protection", "spare_friends": true }
]
}规则:
- 同一类型可以出现多次,列表不会合并——两段数值不同的攻击模块是两段攻击。
- 模块和通用钩子可以叠加:每周期先跑模块,再跑
entity_tick_action,所以自定义钩子读到的状态是模块作用之后的。 作用于阵法自身的模块(目前只有mxt:range_display)同理,跑在tick_action之前。 - 模块只在阵法覆盖的实体上跑,而"谁被覆盖"由顶层
spare_friends决定(见「敌我判断」):写着它,通用钩子和所有模块一起只作用于非好友;不写就是覆盖范围内所有人。 type写错会让定义加载失败,不会退化成"什么都不做"——模块的字段几乎全是可选的,静默退化会把一个拼写错误变成一座站着不动的阵法。- 目前内置
mxt:attack、mxt:buff、mxt:protection、mxt:range_display,以及mxt:none(空模块,同时是注册表的默认项)。
mxt:attack:攻击型
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
damage | NumberProvider | 0 | 每周期对每个受影响实体的伤害 |
damage_type | damage_type | 无 | 伤害类型;不写就走原版「阵主打了他」的语义 |
attribute_to_owner | boolean | true | 把阵主记成加害者。这决定击杀归属、怪物仇恨、以及所有读取攻击者的条件;设成 false 才是无主的"环境伤害" |
effects | List<ApplyEffect> | [] | 每周期施加的状态效果,字段与 mxt:apply_effect 完全一致(effect / duration_ticks / amplifier) |
target_condition | Entity Condition | 恒真 | 对目标实体的额外筛选(只打亡灵、只打玩家之类),在敌我判断之后求值 |
这个模块只说明"打什么",不说明"打谁"。 它打的是阵法覆盖到的每一个实体;要不要放过阵主和好友,是顶层 spare_friends 说了算(不写就是连阵主一起打,见「敌我判断」)。认不出敌友时声明了 spare_friends 的阵法停火,理由见下一节。
打击的每次伤害都过统一伤害管线:阵主(attribute_to_owner 为真时)作为攻击者参与第一层的元素克制, 受击者按自己的元素适应做第二层减免。也就是说"阵主是火灵根、打的是水灵根目标"会让这座阵法的伤害更高, 而目标若适应火则扣得更少——这些倍率写在 mxt:element 定义里,不在这里。
mxt:buff:增益型
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
abilities | List<ability> | [] | 授予范围内实体。source 自动取 mxt:formation/<命名空间>/<路径>,离开半径、阵法拆除、或不再被选中时自动撤销 |
target | all / allies / owner | all | 谁吃到这份增益。allies 走好友判定,认不出就不给 |
aura_zone | aura_zone | 无 | 高优先级运行时灵气覆写(见「灵气覆写」) |
max_bonus | Map<aura, NumberProvider> | {} | 范围内区块的灵气上限加成(重叠取最高) |
三档 target 是同一条规则的不同宽度:all 不筛;allies 要求好友判定给出 true,而阵主在好友判定里算自己的好友, 所以 allies 包含阵主;owner 只认 UUID,是同样效果的更窄写法。
属性加成不在这里找字段。 ability 自带 modifiers,授予一个能力就等于授予它的属性修饰符;基座不再开第二个入口, 否则同一件事会有两套规则,包括"修饰符活过了授予它的那座阵法"这类最容易出错的部分。
不再被选中的实体会被收回授予:站在阵里而好友关系消失时,这个 source 会被对账清空,不必等它走出去。
mxt:protection:守御型
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
block_break | boolean | true | 拦破坏 |
block_place | boolean | true | 拦放置 |
block_interact | boolean | true | 拦右键方块(箱子、门、工作台……),也拦"拿着物品对方块使用" |
explosions | boolean | true | 拦爆炸改地形;只剔掉方块,实体照样受伤 |
mob_griefing | boolean | true | 拦生物破坏(末影人搬方块这类) |
entity_interact | boolean | true | 拦右键实体(村民、马、盔甲架……) |
attack_entity | boolean | true | 拦近战攻击实体 |
item_use | boolean | true | 拦使用物品(对着空气或自己用桶、药水、拉弓……) |
spare_friends | boolean | true | 阵主的好友豁免 |
delegate_to_claims | boolean | false | 把守御交给领地插件(例如 FTB Chunks):在有生效的领地保护时,上面九个开关只作声明、不再执行。见下 |
九个开关全部默认 true:声明这个模块本身就是那句完整的话,只想放开某一项就显式写 false。其中三个的代价要大一些, 不需要时值得关掉:block_interact 覆盖所有带界面的方块;explosions 与 mob_griefing 在爆炸逐方块结算、生物每次破坏 这类高频路径上都要扫一遍在册阵法。
判定:动作的两端都算
只问一句:行为者,或者被作用的方块 / 实体,任意一个落在半径内,这条开关就生效。
- 站在阵外点阵内的箱子 → 拦(箱子在阵里)。
- 站在阵内点阵外的箱子 → 拦(人在阵里)。
item_use没有作用对象,只看行为者自己。- 爆炸与生物破坏没有行为者,只看位置,因此阵主自己的爆炸也会被拦 —— 要留口子就写
"explosions": false。
把"地"和"人"拆成两个模块,曾经是两半必须一起声明才能说清的一句话,而右键方块那一项还会读起来像自相矛盾。现在它就是一句话。
豁免与其它规则
- 阵主永远豁免,且不受任何配置影响——把自己锁在自家门外是 bug 不是规则。
- 好友豁免要求
spare_friends为真且服务端配置「阵法 → 敌我识别」打开。 - 认不出就照常保护:声明了
spare_friends的阵法在认不出时停火,守御阵在认不出时继续拦。猜错的方向应当指向安全的那一侧。 - 只有在册阵法生效,拆除即失效;无主阵法不豁免任何人(否则一座被遗弃的阵法会永久锁住一块地)。
- 基座不再自带任何领地保护(原宗门领地实现已随
sect注册表一并移除):需要领地时由外部领地模组提供,本层按下面的delegate_to_claims让路。
交给领地插件(delegate_to_claims)
置真且当前确实有生效的领地保护时,上面九个开关一个都不执行:真正拦人的是领地插件自己的规则。以 FTB Chunks 为例, 它本来就独立保护自己的领地,所以"交出去"不需要调用它的 API,只是让阵法不再叠加自己那一层。
判定"有没有生效的领地保护"会看一眼 FTB Chunks:装了,且它的全局 disable_protection 没被关。注意它看不见队伍级的 设置 —— 某个队伍把自己的领地四项隐私模式全设成 public 时,这里仍算作"有保护"(要把这层也重算,就等于把领地插件的判定抄第二份, 而那正是"交出去"想避开的事)。
服务端配置「兼容 → 委派需领地保护」(默认开启)决定没有领地保护时怎么办:
| 配置 | 行为 |
|---|---|
| 开(默认) | 只有真的有领地保护时才交出;否则回退到上面九个开关,并在激活时按定义打一条警告日志(warnIfDelegationFallsBack) |
| 关 | 字面读法:一律交出,没有领地保护时就真的什么都不保护 |
无论哪一支,两条代价都属于设置者:
- 交出时阵法只保护被认领的地方,自己的
radius不再参与保护判定; - 回退分支里阵法保护的是它自己半径内的东西,与领地插件的判定无关。
与领地插件的关系:三种联动(服务端配置)
上面那个开关是内容包对单座阵法的声明;服务端另有一个配置(「兼容」标签页)决定所有保护阵法与领地插件怎么相处:
服务端配置「兼容 → 领地联动」
| 取值 | 行为 |
|---|---|
| 不联动(默认) | 不联动:阵法按自己的开关在任意位置生效;两边都在时是"与"关系 —— 谁拒绝都算拒绝 |
| 只在领地内 | 只允许在领地内立保护阵法:阵心所在区块必须已被认领(任何队伍),否则激活失败并提示(item.mxt.formation_plate.failed.not_claimed)。只作用于带守御模块的阵法 |
| 领地优先 | 哪里都能立,但立在领地内时改用该领地规则:阵心所在区块一旦被认领,这座阵法的九个开关整个交出,由领地插件判定;不在领地内则仍按自己的开关 |
三条边界:
- 判定"在不在领地内"看的是阵心所在区块,不是每一个受影响的位置 —— 一座横跨领地边界的阵法要么整体归领地管,要么整体不归。
- 「只在领地内」不要求"你自己的领地":站得进去就说明房主允许(否则他早就把你拦了),这条选项管的是"别在无主之地立法阵"。
- 服务器没装领地插件时两个模式都不起作用,而不是让阵法立不起来:没有领地可要求,也没有领地规则可让。「只在领地内」的这种情况会打一条警告(整局只报一次),因为"选项没生效"和"选项生效但没人来"在游戏里一模一样。
别人领地不能开保护阵法
服务端配置「兼容 → 立阵需许可」(默认开启,与上面那个枚举相互独立)
阵心落在别人认领的区块里时,只有三种人能立保护阵法:
- 有编辑权限的人 —— 直接问领地插件"你会不会让他在这种方块"(
canPlayerUse(BLOCK_EDIT_MODE)),而不是在这里重写一遍隐私模式规则:公开领地、队内职级、盟友、被许可的假玩家到底算不算,那是它的规则,抄第二份只会抄出个会漂移的副本。 - 地主的好友 —— 走的是守御阵同一套好友系统(所以装了 FTB Teams 时,队友与盟友本来就被算作好友,自然会通过)。
- 无主之地 —— 没被认领的地方是"没有地主",谁都能立。
其余一律拒绝,包括解析不出的行为者(控制台/指令来源):没人能替地主答应。
为什么值得单开一条(而不只是靠"放不下方块"):守御阵是宣称管辖区,不只是盖房子。允许在别人地盘放方块,不等于允许在那里立法;而结构本来就立着的时候(模板匹配的是自然地形,或者别人搭的),光靠方块保护根本拦不住任何人。
无领地插件时这条规则同样惰性(没有别人的领地可判),所以对纯 Mxt 服务器不改变任何行为。 拒绝时的提示是
item.mxt.formation_plate.failed.foreign_claim,比"结构不匹配"更能说明问题。
记在别处:FTB Chunks 默认不拦物品使用(只有
ftbchunks:right_click_blacklist能拦),也不拦攻击生物。 也就是说item_use与attack_entity这两项一旦交出去就没人管了。详见research/19_FTB_Teams与FTB_Chunks接入分析.md。
已知不覆盖
弹射物。attack_entity 拦的是近战挥击(游戏在攻击者一侧派发的事件),从阵内射出的箭命中时不算"攻击者此刻在攻击", 因此不会被拦。
mxt:range_display:范围显示型
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
particle | particle | 必填 | 画什么粒子 |
shape | ring / sphere | ring | ring 是阵心同一高度的水平圆(地坪线);sphere 铺满整个球面 |
points | int(1–512) | 32 | 每周期画多少个点 |
interval_periods | int(1–1200) | 1 | 每几个阵法周期画一次(1 周期 = 20 tick) |
阵法的范围是个球,世界上没有任何东西说明它到哪里为止 —— 结构只标出阵心。这个模块把那条线画出来。
- 它只是显示:不作用于任何实体、不会让阵法变成敌对、不参与敌我判断;不写这个模块的阵法一点开销都没有。
- 只发给看得见的玩家:半径 + 32 格以内。原版的广播接口自己卡死在 32 格,所以这里按玩家逐个发送 —— 否则半径超过 32 格的阵法会被剪掉一圈轮廓。一个点一个粒子包,所以
points乘以看得见的人数就是每周期成本。 ring画在阵心方块的中心高度上;sphere用黄金角铺点,避免经纬网格把点挤到两极。- 它跑在
TickEffects之后、tick_action之前,因此取消TickEffects的周期连范围一起不画。 - 可以写多个,叠不同粒子。
结构:二选一
structure_template 与 structure 必须且只能给一个,两个都给或都不给都会让定义加载失败。
优先用 structure。 阵法要表达的通常是「阵基 + 几根阵旗」这种少量固定位置,内联写法已经够用,而且它在数据包加载时就把期望值解析完并固定下来——每次校验只是每个方块一次 getBlockState。structure_template 只在布局真的复杂到无法逐格列出时才值得用。
structure(内联,首选):无需.nbt资源,偏移量直接写在定义里。json"structure": [ { "offset": [0, 0, 0], "state": "minecraft:gold_block" }, { "offset": [0, 0, -8], "state": "mypack:wood_flag" } ]offset相对 controller。state可以直接写方块 id(取默认状态);需要指定属性时才用原版的{ "Name": "...", "Properties": { ... } }写法——能用一个 id 表达的状态在导出时也会写回 id。structure_template:引用原版结构模板(data/<命名空间>/structure/<路径>.nbt,与结构方块、/place template共用同一批文件)。适合大型或复杂布局。代价有两层:
- 每次校验都要重新序列化并解析一遍模板,另加每方块一次方块状态注册表查询——20 tick 一次、每座阵法都来一遍,所以别拿它描述几个坐标。
- 模板里所有方块都必须逐个对上,除了空气。
空气不参与校验。 用结构方块保存时,它会记录整个包围盒,空位也作为空气条目写进
.nbt;如果要求这些格子保持空气,旁边落一根火把或者掉一个方块就会把阵法废掉。所以模板里的空气条目表示"这里不关心",不是"这里必须是空气"。反过来说,模板只能约束"什么必须在",不能约束"什么必须不在"——需要"此处必须空着"的判定只能靠条件自行实现。注意这与原版放置模板的行为正好相反:
placeInWorld会把空气覆盖到世界上,因为结构复现的是整个体积;阵法只断言自己的阵旗还立着,是个更弱的问题。48×48×48 是结构方块自己的限制,不是阵法的限制。 原版
StructureBlockEntity的两个常量MAX_SIZE_PER_AXIS/MAX_OFFSET_PER_AXIS都是48,并且只用在它读取自身 NBT 时做 clamp;底层的结构模板格式(StructureTemplate.load从 NBT 读size)没有任何尺寸上限,结构方块在 LOAD 模式下加载更大的模板时会直接采用模板尺寸而不是夹到 48。所以用结构方块手工圈选做不了超过 48³ 的模板,但外部工具直接生成.nbt、再用/place template(同样没有尺寸限制)验证过的文件,阵法照常可以引用——问题只在"怎么把文件做出来",不在加载。
逐实体行为的上下文
entity_tick_action / entity_enter_action / entity_exit_action 对每个实体各执行一次,可以读到:
| 名称 | 位置 | 含义 |
|---|---|---|
formation_radius | 公式显式值 | 阵法半径 |
distance | 公式显式值 | 该实体到阵心的距离 |
formation_x / formation_y / formation_z | 公式显式值 | 阵心坐标,可直接喂给 mxt:teleport |
| 阵法携带者 | 上下文数据 | 记录"是哪座阵法、阵主是谁、阵心在哪",由 mxt:formation_owner 与 mxt:formation_ally 消费 |
tick_action / deactivate_action 的上下文是 Level,只有 zero / random 两个变量 —— 不要在它们里面写依赖实体状态的公式。
阵法所有者
mxt:formation_owner—— 该实体是当前正在评估的这座阵法的所有者。阵法之外恒为false。mxt:formation_member—— 该实体拥有本维度任意一座在册阵法。与上一条是不同的问题,不要混用。
放置者在阵盘激活时记录。有维持消耗的阵法在阵主离线时会被拆除(取不到付费者)——除非有脚本取消 UpkeepFailed 让它撑过去,或者它的 storage 存量还付得起这一期(见「存量」)。
mxt:formation_owner 只排除阵主本人。队友保护是它旁边的另一个判定(见下),不会改动它的语义——数据包因此可以分别表达 「只排除阵主」和「排除所有友方」。
敌我判断(队友保护)
顶层 spare_friends 是开关,只回答一件事:这座阵法的逐实体行为是给覆盖范围内的所有人,还是先过一道敌我判断 (好友与认不出身份的都不受影响)。它不代表这座阵法是攻击型——一座阵法是进攻还是辅助,看的是它的 actions: 带 mxt:attack 就是会打人的阵法,带 mxt:buff 就是给增益的阵法。开关与性质各管一头,互不推断。
不写(默认)=对所有人生效,阵主和好友也不例外。想让攻击阵法放过自己人,必须显式写
"spare_friends": true; 忘了写就是一拳打在阵主脸上,这是刻意的——运行时不再替数据包猜意图。写了之后:好友与阵主不受任何逐实体行为影响,陌生人照常受影响,认不出身份的也放过(见下)。
谁算队友由好友系统回答:先发
FriendEvent.Relation(其他模组可以表态,且事件带的是阵主 UUID,所以数据在服务端的来源—— 例如装了 FTB Teams 时的队伍成员与盟友——在阵主离线时也能作答),没人表态时才查阵主自己的好友名单。 阵主本人也算——好友判定里"自己算自己的好友",所以声明了开关的阵法也不会伤到立阵的人,不必再写mxt:formation_owner条件。阵主无法解析时(离线、未加载)仍然先按 id 问一遍:事件带的是阵主 UUID,所以装了 FTB Teams 之类的来源照样能答;内置好友系统也有离线镜像缓存,阵主只要登录过一次就仍有答案可用。只有没人能识别这个实体时(没有阵主记录,或既没有实体、镜像里也没见过这个玩家、又没有来源认识这对关系)声明了开关的阵法才停火:对所有实体都不生效,而不是"对所有人生效"。 名单挂在玩家实体上,"打所有人"恰好会打到这个开关本来要保护的人,而"挑一批人放过"只能靠猜 —— 认不出敌我就不开火。 停火同样意味着它授予的能力会被释放(被豁免的实体不进在场集合,见下一条)。想做"阵主不在也照打"的阵法, 有两条路:不写
spare_friends、改用mxt:formation_ally自己写判断(它在阵法之外/没有阵主时恒为false,于是走"打"的分支), 或者把服务端配置关掉。被豁免的实体不会进入阵法的在场集合,因此也拿不到
entity_enter_action/entity_exit_action。好处是阵法授予的 能力会在豁免生效时被释放;代价是好友关系变化会产生一次"离开再进入",所以entity_enter_action依然必须幂等。服务端配置「阵法 → 敌我识别」(默认开启)是总开关:关掉之后
spare_friends形同不存在, 阵法一律对所有人生效(包括阵主自己)。默认开启不会改变任何现有数据包——spare_friends默认为false,只有显式声明的阵法受影响。
数据包也可以自己判断,不必依赖自动过滤。mxt:formation_ally 是实体条件,在阵法上下文里回答"该实体是不是阵主的好友" (阵法之外、或阵法没有记录阵主时恒为 false;阵主只是离线则仍会按 UUID 问事件,和其它判断走同一条流水线)。 逐条行为各判一次,就能表达单一开关表达不了的东西 —— 例如同一座阵法伤敌而治疗友军:
"entity_tick_action": {
"type": "mxt:if_else",
"condition": { "type": "mxt:formation_ally" },
"if_action": { "type": "mxt:heal", "amount": 1 },
"else_action": { "type": "mxt:damage", "amount": 2 }
}两条路的取舍:
spare_friends(顶层开关) | mxt:formation_ally(实体条件) | |
|---|---|---|
| 粒度 | 整座阵法 | 逐条行为 |
| 由谁决定是否执行 | 服务端配置「阵法 → 敌我识别」 | 数据包自己,不受配置影响 |
| 认不出敌我时 | 停火(不打任何人) | 走 false 分支(照打) |
| 适用 | 「这座阵法的效果只给外人和认不出的人以外的人」 | 「同一次 tick 里对敌人和友军做不同的事」 |
两者不要同时用在同一座阵法上。
spare_friends: true会先由运行时把好友整个剔除,逐实体行为根本不会跑在他们身上, 上面那个"治疗友军"的分支也就永远走不到。要做分而治之,就别声明这个开关。
mxt:friend 是同一判断的双实体条件版本(actor 把 target 当自己人时为真),用在有双实体条件槽的地方, 例如技能的 target_condition:{"type": "mxt:not", "condition": {"type": "mxt:friend"}} 表示"不作用于好友"。
周期与三个事件
一切以 20 tick 为周期,一个周期会依次发三个事件(KubeJS 里同在 formation 组,用 isCancellable() / getPhase() 区分):
| 事件 | 可取消 | 含义 |
|---|---|---|
Tick | ❌ | 维持费已经结算完。观察点:需要「每个付过费的周期都发生点什么」就挂这里 |
TickEffects | ✅ | 只挡 tick_action 与逐实体行为。取消不退费 —— 阵法还立着,也还在被维持 |
UpkeepFailed | ✅ | 付不出维持费。取消 = 这一周期不付也不做事,但阵法保留;不取消才拆除 |
拆分的原因:Tick 从前是可取消的,而它触发时钱已经花了,于是「取消」在一句话里同时意味着"什么都不发生"和"照扣钱"—— 一个无条件取消的监听器就能零效果地掏空付费者。现在两个含义各有一个明确的家。
范围与时间粒度
- 范围是球,不是方盒:方盒角落处不会生效。
- 一切以 20 tick 为周期,这也是进出判定的粒度:20 tick 内穿过阵法不会被观察到。
- 区块卸载再加载会产生一次假的"离开再进入";服务器重启后阵内实体一律视为新进入。 因此
entity_enter_action必须是幂等的。
释放阵法授予的能力
mxt:grant_ability 的 source 建议取该 formation 自己的 id 对应的 mxt:formation/<命名空间>/<路径>。 基座会在实体离开、阵法拆除、以及玩家脱离阵法时对账这个 source 并撤销它的全部授予 —— 否则能力会永久留在实体身上(ability_holder 是持久附件,而 deactivate_action 的上下文看不到实体)。 不遵守这个约定不会出错,只是失去自动清理。
阵法吞噬地脉(维持费的第一个来源)
阵法半径内的方块灵气发射器不再向环境供气,改为全部供给阵法。 阵法用这些灵气支付自己的 maintenance_costs,缺口才由阵主出;如果阵法自己供的灵气就够,阵主一分钱都不用出(甚至不需要 持有那种资源)。
规则要点:
- 判定范围就是阵法自己声明的
radius球,没有第二套定义。 - 被吞的方块会从环境灵气里彻底消失 —— 它们不再参与"这里有多少灵气"的解析,也不会被其他查询共享。 所以阵法是个真正的灵气黑洞,建在灵脉上会把那片地脉抽干(地脉本身仍按
regen_per_tick恢复)。 - 按资源类型抵扣,不通用:阵法里全是火属性灵气,抵不掉一笔灵力账单。不匹配的灵气既不给环境、 也不给阵法,就是被吞掉了。
- 不设存量:本周期吞到的灵气直接抵本期维护费,超出的部分不返还、不留到下期。 要留下来就给这座阵法写一个
storage字段(见「存量」)—— 这是唯一的例外,而且开得很窄。 - 只有阵主能作为付费者(沿用现有模型)。阵主离线时,阵法仍会因为"付不出缺口"而被拆除 —— 除非缺口被自家灵气完全覆盖,那时根本不需要付费者。
storage的存量同样算付费者: 阵主不在时它照常付账,付不动才拆(见「存量」)。 - 可选:连地皮的自然灵气也一起抽。 服务端配置「阵法 → 抽取环境灵气」(默认关闭) 打开后,阵法所在位置解析出的环境灵气也会参与抵扣。注意两项:
- 只算自然环境(群系/维度/人工区域以及该处的场地灵气),阵法内发射器贡献的那一份会被扣掉, 不会重复计入;
- 它让阵法比"站在什么之上"更便宜 —— 灵气浓郁处即使不摆发射器也可能白跑,这是另一套平衡。
配套的引擎要求:阵法激活或拆除时会让半径覆盖到的区块重算方块灵气(否则被吞的方块会继续供环境, 阵法那一份就等于白拿)。重算是排队执行的,在下一个服务端配置「灵气 → 方块灵气周期」的边界发生。
想让阵法完全靠自己运转,就在半径内摆足够多的发射器(例如
mxt:spirit_stone_block)。
存量(storage)
storage 是通用字段:每座阵法都有这个属性,只是默认不启用 —— 不写它就是不存。
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
capacity | Map<aura, NumberProvider> | 必填 | 每种灵气最多存多少;没列出的灵气不存 |
{
"structure": [ { "offset": [0, 0, 0], "state": "minecraft:gold_block" } ],
"radius": 12,
"maintenance_costs": [ { "id": "mxt:common", "amount": 10 } ],
"storage": { "capacity": { "mxt:common": 500 } }
}不写 storage 的阵法是穿堂而过的:自家地脉供的灵气只抵本期维护费,多出来的当场散逸(见「阵法吞噬地脉」)。 写上它,这座阵法才开始把盈余留下来 —— 而且刻意留得很窄:存量来自地脉、只付这座阵法自己的账、随阵法一起消失。
付账顺序:本期地脉供应 → 存量 → 阵主。 先花当期的是有意的:地脉这份是本周期新给的,不花就没了; 存量是能留到下一周期的那一份,用它先付反而等于让阵主为本周期站在脚下的灵气买单。
| 场景(账单 10,容量 500) | 地脉给 | 存量 | 存量支付 | 阵主支付 | 存量入账 |
|---|---|---|---|---|---|
| 地脉足够 | 50 | 0 | 0 | 0 | 40 |
| 地脉断了 | 0 | 30 | 10 | 0 | 0 |
| 三方分摊 | 4 | 3 | 3 | 3 | 0 |
| 库快满了 | 50 | 490 | 0 | 0 | 10(只进到 500) |
| 容量里没列它 | 50 | 0 | 0 | 0 | 0 |
按资源各算各的:一库火灵气付不了灵力账单,不管它是本周期收的还是十个周期前收的。 一条规则对应一条账:某个资源的账单可以由地脉付、另一个由存量付、第三个落到阵主头上,同周期互不影响。
几条边界:
- 容量是每种资源各自的上限,不是总量:
{"mxt:common": 500}说的是mxt:common最多 500。 - 写了
storage就必须写capacity:一个什么都不存的存量声明是拼写错误,不是配置(要关掉整个存量就别写storage)。 - 没有
maintenance_costs的阵法什么都不存 —— 没有账单要抵,存了也没有出口,所以连扫描都省了。 - 拆除即散逸:存量属于这座阵法,拆掉就没了。归还阵主会把地脉灵气接成一条通向玩家背包的管道,那是另一套平衡。
- 阵主离线时存量照常付款,缺口被存量覆盖就撑过去,覆盖不了才拆。代价是这时账单按 Level 上下文求值: 常量费用不受影响,写依赖玩家状态的公式则读不到玩家。
- 存量随存档持久化,
/mxt formation info会额外显示非空的存量。 - 容量是公式,求值结果非有限或 ≤ 0 就表示这种资源不存(不夹取、不猜)。
灵气覆写
aura_zone 是 mxt:buff 模块的字段,让这座阵法替换阵地所在位置的灵气解析结果(优先级高于群系 / 维度 / 人工区域):
- 覆盖通过可取消的
AuraZoneEvent.Override发布 —— 取消它就退回静态结果,阵法仍然立着。 - 覆盖只在阵法在册期间生效,拆除后立刻退回静态解析。
- 灵气答案是按 tick 记忆化的,所以同一个 tick 内激活的阵法要到下一个 tick 才影响灵气查询。
- 重叠时取最近一座声明了
aura_zone的阵法;一座没有声明 zone 的阵法(例如地形保护阵)不会挡住它旁边那座修炼阵法的覆写。
max_bonus 同样是 mxt:buff 的字段,给灵气池加上限,但有一个必须知道的耦合:它只随着 aura_zone 的覆盖一起应用。 上限是加在「覆盖后选中的那个 zone」所提供的资源上的,因此:
- 没有
aura_zone→ 没有覆盖 →max_bonus无处可加。 - 有
aura_zone但该 zone 不提供某个灵气 → 对这个灵气加不上(不会凭空创造灵气)。
两者都已在服务端审计里断言。
激活与拆除
阵盘(mxt:formation_plate)是唯一能把阵法带进世界的物品:它的 mxt:formation_plate 组件里存着一个 formation 引用, 右键方块时由 FormationWorldService 结算。同一块阵盘就是开关 —— 对着阵心右键是激活,对着已激活的阵心右键是拆除 (需为阵主或管理员;打开服务端配置「阵法 → 队友可拆除」后,阵主的好友也能拆)。
绑定阵法:用 /mxt formation bind <formation> 写入主手的阵盘(需要 gamemaster 权限),或在物品组件里直接指定:
give @s mxt:formation_plate[mxt:formation_plate={formation:"mypack:green_shade_array"}]未绑定的阵盘会自动识别
没有绑定阵法的阵盘,右键时会认出脚下这座阵法并激活它。 识别用的就是阵心容错那 3×3×3, 候选是这块阵盘允许激活的全部阵法(allowed,空列表按服务端配置解释),按 ID 排序逐座比对结构, 离点击位置最近的一座胜出;点击位置本身能匹配时永远优先。成功后的提示带上阵法名 —— 玩家从来没说过要立哪一座,"有座阵法出现了"不算回答。
- 绑定优先:绑了阵法的阵盘只立那一座,既不会顺手认出别的,也不做这次扫描。 白名单回答"这块盘能立哪些",绑定回答"这块盘立哪一座",两者不能互相替代。
/mxt formation bind因此不再是"能玩"的前提,而变成一种限制手段:内容包发出去的阵盘可以绑好,也可以留空。- 关掉服务端配置「阵法 → 阵盘自动识别」(默认开启)就回到旧行为:未绑定的阵盘提示「阵盘尚未绑定阵法」。
- 代价是一次点击最多 27 × 白名单内阵法数次结构检查。内联
structure是每格一次方块查询, 而structure_template每次检查都要重新序列化并解析模板 —— 数据包很大、又不想让玩家点一下卡一下, 就把常用阵盘绑好(绑了就不扫描),或者直接关掉这个选项。 - 认不出任何结构时提示
item.mxt.formation_plate.no_structure,而不是「尚未绑定阵法」: 这两句话对应的下一步不一样("换地方 / 先把阵基摆出来" 对 "先用命令绑定")。
激活失败时会说明原因(结构不匹配 / 资源不足 / 位置已占用 / 领地不允许),不再只回一个内部枚举名, 并且任何失败都不消耗东西。
阵盘白名单
mxt:formation_plate 组件有两个独立字段:
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
allowed | List<ID 或 #标签> | [] | 这块阵盘允许激活哪些阵法。属性属于物品本身,同一 ID 的每一份都一样 |
formation | 阵法 ID | 无 | 这一份当前选中哪一座;必须属于 allowed 才可用 |
# 只允许两座具体阵法
give @s mxt:formation_plate[mxt:formation_plate={allowed:["mypack:thunder_array","mypack:ice_array"],formation:"mypack:thunder_array"}]
# 用一个标签把同组阵法交给多块阵盘(推荐)
give @s mxt:formation_plate[mxt:formation_plate={allowed:["#mypack:wood_arrays"],formation:"mypack:green_shade_array"}]标签文件放在 data/<命名空间>/tags/mxt/formation/<路径>.json,写法与物品标签一致:
{ "values": ["mypack:green_shade_array", "mypack:spirit_gathering_array"] }allowed 为空是歧义情况,所以做成配置项:默认「不限制」(兼容所有旧阵盘),把服务端配置「阵法 → 空白名单放行」关掉后,空 allowed 表示什么都不允许,必须显式列出。 /mxt formation bind 的 Tab 补全只列出这块阵盘允许的阵法;白名单把它们限定住,补全也就有了上界。
白名单只在写入前和激活前生效,都先于任何资源消耗:bind 拒绝不在名单里的阵法且不改动物品; 若某块阵盘的 formation 不在自己的 allowed 里(手改存档、或配置改动导致),右键会提示 「阵盘不允许激活它所绑定的阵法」而不是照常激活 —— 这是一种显式配置,不是损坏的物品。
阵心容错:点击位置周围 3×3×3 内会寻找最近一个满足结构的阵心。点歪一格不会失败;点击位置本身有效时永远优先取它, 所以贴着一个可当阵心的方块点永远不会把激活挪到别处。拆除走同一次查找。
注意这个窗口只有 3 格宽,所以它只对紧凑布局真正有用:一个位置要能当阵心,必须从它开始就能满足整份结构, 因此跨度超过 1 格时只有原阵心能命中。这正是「点方块偏一格」这个真实约束对应的范围。
诊断
/mxt formation list 列出当前维度所有已激活阵法;/mxt formation info 列出覆盖玩家所在位置的阵法; /mxt formation bind 是这棵子树里唯一的写操作。三者都有顶层别名 /formation ... (可用服务端配置「命令别名 → /formation」关掉别名,/mxt formation 始终完整)。
forging_blueprint 定义输入、锻造步骤、偏移范围、质量阈值和失败结算;forging_method 定义每次操作的消耗、条件与锻打音效。
alchemy_recipe 使用原版配方系统和炼丹环境条件,材料、灵气和结果由数据包定义。