NPC 战斗
目标意图、动作选择、攻击时序、武器近战、伤害限制与战斗状态。
NPC 战斗由 entity.CombatNpcEntity 与内部的 internal.ai.action 实现负责。这些类型供项目内部扩展,不是公开战斗门面。本页说明自主行动的 NPC;玩家控制的变身仍使用玩家的战斗输入。
它做什么
战斗 NPC 通过威胁传感器选择目标。NpcCombatController 调度法术与支援动作,NpcWeaponMeleeController 则在 supportsNpcCombat() 与 NpcConfig.normalMeleeAttacks() 允许时持续驱动武器近战。普通近战没有动作索引、编写的执行时序或额外冷却;特殊动作冷却期间仍可挥击。
Better Combat 与 Better Mob Combat 是外部依赖。本模组调用它们的武器注册表、就绪检查、攻击范围与起手接口,不在这里定义其武器 JSON 默认值。本地兼容层还会将 BaseNpcEntity 和 SimpleCloneEntity 的 Better Mob Combat 生物额外冷却替换为 0。
运行时模型
目标意图与保护
NpcThreatSensor 在传感器回调中,以 20 tick 为快速校验间隔、40 tick 为完整选择间隔。这些是内部扫描门槛,不代表大脑每 tick 都调用此传感器。近期攻击者记忆持续 600 tick(30 秒),最多保存 6 个 UUID;超量时按插入顺序移除首项。刷新已有项只延长有效期,不改变插入位置。未加载、无效或已不允许反击的实体会被移除。
| 意图 | 优先级 | 支援追击距离限制 |
|---|---|---|
HOSTILE | 1 | 无 |
FAVOR_HIT | 2 | 有 |
PROTECT | 3 | 有 |
REVENGE | 4 | 无 |
COMMAND | 5 | 无 |
高优先级意图取代低优先级意图。同级候选通常必须满足 newDistanceSquared + 1 < oldDistanceSquared,比较的是距离平方。新的 FAVOR_HIT 候选可直接替换另一个协助攻击目标,不要求上述距离优势。指令目标始终通过保护模式的意图检查,但仍须满足目标有效性和伤害限制。
跟随时,ASSIST 接受全部普通意图,DEFEND 排除 FAVOR_HIT,PASSIVE 只接受 REVENGE。非跟随模式不受这些模式限制。跟随者的常规自卫扫描不会主动选择周围尚未交战的敌对生物;正在攻击 NPC 的生物仍可入选。主动敌对关系由阵营规则决定,NPC 的目标访问器会排除友好阵营实体。
保护对象包括 protectRange 内可见的友好玩家与阵营盟友,以及 max(protectRange, followRange) 内仍存活的跟随主人;主人无需出现在可见列表中。威胁必须同时接近保护对象和 NPC。令 A = max(protectRange, aggroRange),威胁距保护对象不得超过 A,距 NPC 不得超过 max(followRange, protectRange, aggroRange) + A。持续支援时仍受这个 NPC 距离限制。
| 保护评分输入 | 数值 |
|---|---|
| 阵营盟友 / 友好玩家 / 跟随主人优先级 | 1 / 2 / 3 |
| 优先级乘数 | 1000000 |
| 正在以保护对象为目标的生物 | 10000 |
| 保护对象记录的攻击者 | 9000 |
| 记录的攻击者也正在瞄准保护对象 | 额外 80 |
| 距离扣分 | 减去威胁与保护对象的距离平方 |
FAVOR_HIT 要求被跟随或受信任玩家的最近攻击时间有效,且距今不超过 100 tick(5 秒)。它取玩家攻击的实体,使用相同的距离范围,并按 priority × 1000000 + 9000 - targetToPlayerSquared - npcToTargetSquared × 0.25 评分。
当前目标必须存活、同世界、可被选中且符合意图;创造模式和旁观模式玩家被排除。只要目标可见且仍有效,普通仇恨距离检查不会将其清除。不可见的复仇目标可保留至攻击者记忆到期;其他不可见目标必须仍在 aggroRange 内。目标变化或可见时更新最后已知位置。追踪不可见目标时使用当前维度内记录的方块位置,没有记录则停留。
动作选择与时序
动作集仅根据子类注册内容延迟构建,空动作集也能进行纯武器近战。空白或重复 ID 抛出异常,不再设默认动作索引。客户端与服务端必须按相同顺序注册相同动作。
每次控制器 tick 先递减各动作冷却与正值全局冷却。NPC 死亡、睡眠、使用物品、忙于 POI 或播放表情动作时,会停止导航,并以 BLOCKED 结束当前动作。目标效果接管大脑时改为调用 interrupt(),不执行控制器 tick,因此该路径不会推进这些冷却。
空闲且全局冷却结束时,控制器逐个评估未冷却动作:满足最低已完成挥击数,选择同世界的存活目标,调用 isInRange、canStart,并要求评分为有限正数。先比较优先级,再比较评分;完全相同则保留先注册的动作。rangeOverride() 为正时替代 attackRange,否则使用配置范围。选中的动作预留下一个允许衔接的近战时点:等待期间不发起新挥击,每 tick 重新检查动作资格。
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
castTicks | integer | 必填 | CASTING 阶段的非负控制器 tick 数。为零则直接进入 EXECUTING。 |
executeTicks | integer | 必填 | 非负;控制器至少执行 1 tick。 |
triggerTick | integer | 必填 | 从 0 开始的执行 tick,范围为 0 至 max(0, executeTicks - 1);越界抛出异常。 |
NpcActionTiming.instant() 为 (0, 1, 0)。开始动作时立即调用 onStart,执行阶段的处理要等后续控制器 tick。施法的最后一个 tick 切换阶段后直接返回,不在同 tick 触发执行。执行期间,onTrigger 在指定 tick 仅调用一次;ATTACK 还会向情绪管理器报告攻击。动作冷却在触发后开始,全局冷却在动作完成或停止时开始。触发前停止也会施加动作冷却,避免被取消的施法每 tick 重试。
控制器保存选定目标的 UUID 与引用。动作进行中检查目标存活、移除状态及世界,以 45 度转向限值注视目标并推进时序。它不会逐 tick 重新执行当前动作的 selectTarget、canStart、距离、可见性或评分检查。当前动作的 interruptible() 为 true 且全局冷却已结束时,更高优先级的合格动作可以在其允许的近战衔接时点抢占;同优先级不能抢占。阻塞、显式中断和目标丢失仍可停止不可抢占动作。
导航锁在开始及施法、执行阶段生效。移动锁只在执行阶段将前进和侧向输入清零,不会重置速度。停止原因为 FINISHED、TARGET_LOST、INTERRUPTED、BLOCKED。onStop 收到保存的目标(可能为空),随后清除跟踪状态。触发前停止会消耗编写的动作冷却;已施加的动作冷却保留。这些动作计时器不会延迟基础武器攻击。
没有动作启动时,接近逻辑处理敌对目标。武器命中框已能触及目标时,即使特殊动作仍在冷却也会停留;针对该敌人的 ATTACK、DEBUFF 已入选且正在等待近战衔接时点时也会停留,否则继续接近。HEAL、BUFF、CLEANSE、SHIELD 不影响攻击接近停距。可见目标路径的目标容差为零,每 4–6 个控制器 tick 刷新一次,期间继续更新导航速度;法术的长射程不会让近战者停在武器范围外。
支援动作
NpcSupportSensor 请求 20 tick 的感知间隔。可见生物缓存不存在或为空时,会先清空支援列表,不再处理主人。否则,protectRange 内存活且受信任的跟随主人排在首位;随后加入范围内可见、友好且非创造/旁观模式的玩家,以及受阵营保护的存活实体,主人不重复添加。
NpcActions.builder 默认提供以下目标策略:
| 类型 | 选择方式 |
|---|---|
ATTACK、DEBUFF | 当前敌对目标 |
HEAL | 自身与支援候选中,受伤存活实体里生命比例最低者;全部满血则无目标 |
SHIELD | 同样优先选择受伤者,没有时选自身 |
BUFF | 随机支援候选;列表为空则选自身 |
CLEANSE | 自身与支援候选中首个存活且带有有害状态效果者 |
控制器校验存活、世界与距离;构造器的默认条件不增加可见性或阵营限制。默认锁定导航但不锁定移动输入,不允许抢占,评分为 1 + random × 0.001。生命周期回调默认不执行操作,显式传入的回调函数不得为空。通过 target、可组合的 condition 与 score 可替换选取逻辑、添加战术条件并表达紧急程度;触发时的命中有效性由动作自行校验。
持续武器近战
NpcCombatAttributes统一定义适配武器的攻击速度基值4.0,所有NPC属性构造器均继承它。服务端生成初始化和NBT加载在原版读完保存属性后恢复此基值,不清除装备、服装或效果修饰符。现有原型NPC加载时自动使用正确基准,无须重新生成或执行属性命令;伤害、生命与其他保存属性保留原值。
个体攻击节奏通过属性修饰符调整。直接修改攻击速度基值会在下次生成初始化或加载时被覆盖。这是当前统一的属性规则,不按版本迁移,也不回退到旧数值。
武器就绪时,驱动器像分身近战一样直接调用 bettermobcombat$startUpswing。攻击蓄力、连击进度、命中框、动画数据包和武器恢复时间由 Better Mob Combat 负责。此路径不调用延迟起手,不占用调度器执行窗口,也不添加动作结束后的停顿。编写的动作结束后,只要武器恢复时间允许,同一个控制器 tick 即可恢复近战。
启动要求目标存活、可见、同世界且通过 canAttack,主手武器具有非空注册攻击列表,并存在当前攻击。NpcWeaponMeleeController.isInRange 委托 MobCombatHelper.isWithinAttackRange,不会用中心距离排除已相交的命中框。NpcCombatFacing 按分身攻击目标的方式对齐朝向与俯仰,俯仰限制为 -75–75 度。实际攻击速度必须为有限正数,最终就绪检查仍由依赖负责。
BetterMobCombatNpcSwingMixin 在推进待命中的挥击前检查:自主 NPC 未阻塞、不在编写的动作中、不受目标效果接管大脑,且仍在攻击同一个有效目标。取消会清除待命中计时、延迟任务和回调,停止武器动画,但保留已有武器恢复时间。此限定范围的桥接修正了 Better Mob Combat 1.3.1 取消后仍保留命中倒计时的行为;桥接通过合并后生成的 @Accessor 方法访问依赖字段,并调用公开的 MobAttackWindup、PlayerAttackProperties 接口。它不对其他 mixin 注入的成员声明 shadow,因为 shadow 校验早于这些成员的合并。分身及玩家控制的变身实体不进入该 NPC 桥接。伤害回调还会拒绝无效攻击者、跨世界或死亡/移除的受击者,以及过期的取消令牌。玩家须通过 canApplyOutgoingDamageToPlayer,其他存活实体须通过 canAttack,随后调用 tryAttack。
动作穿插策略
每个编写的动作通过 meleePolicy() 指定策略。NpcMeleePolicy.DEFAULT 等于 afterSwings(0):不要求挥击次数,但等待当前命中时点后再进入动作。
| 策略字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
transition | enum | AFTER_IMPACT | AFTER_IMPACT 等待待执行的命中;AFTER_RECOVERY 还等待武器恢复;INTERRUPT 取消未完成的挥击。三者都不会清除武器恢复时间。 |
minimumSwings | integer | 0 | 自上一个编写的动作开始以来,已提交的挥击次数。必须非负,负值抛出异常。 |
NpcMeleePolicy.afterSwings(2) 要求完成两次挥击后才允许动作。挥空也算完成攻击;一次挥击不论命中多少目标都只计一次,被取消的起手不计数。任何编写的动作开始时都会消耗这份共享计数。敌对目标变化时重置本次交战计数,并使排队中的旧目标伤害失效。状态按 NPC 独立保存,仅在运行时存在,不新增网络或 NBT 字段。
正数门槛明确要求先近战,不应套在必须随时可用的纯远程法术或紧急治疗上;这类动作应设零门槛,并配合优先级、生命或距离条件。中断策略可取消武器起手,但若要替换正在施放的法术,还要求该法术允许 interruptible()。所有编写的动作都会在自身持续期间暂停新的基础挥击。快捷物品自动使用会等待命中时点,并要求没有编写的动作正在执行。
伤害许可与闪避
玩家敌意按攻击尝试累计,而非只按生命损失累计。普通伤害路径先拒绝无敌、非正伤害和友军伤害,再记录攻击尝试,之后才检查无敌帧与闪避。弹射物穿透回避路径仅在成功回避时记录尝试。
对 CombatNpcEntity,来自存活、非创造/旁观模式且可被选中玩家的尝试会刷新 600 tick(30 秒)记忆。当前传入伤害决定本次用于比较累计次数的门槛:
| 当前潜在伤害 | 所需尝试次数 |
|---|---|
| 不超过 2 | 5 |
| 大于 2 且不超过 7 | 2 |
| 大于 7 | 1 |
一旦解锁复仇,状态保留至该玩家记忆到期。不同伤害档位没有独立计数器。敌意表不持久化;NPC 的普通目标和攻击者访问器会隐藏受信任玩家。
对玩家造成的伤害仅允许命中保存的战斗目标,或与该目标玩家同阵营的玩家。没有保存目标时,普通目标必须为玩家,再采用同样规则。创造模式、旁观模式、死亡及其他不可被选中的玩家均被拒绝。NpcTargetDamageGuard 会同时检查攻击者与直接来源所归属的 NPC;此限制独立于队伍友军伤害保护。
闪避是通过 setDodgeableNpc 明确启用的实体能力。默认闪避要求实体存活、清醒、未使用物品、未骑乘,伤害来源可回避,并符合依赖的空中翻滚许可。它朝远离来源的方向移动,无法确定时向后,叠加翻滚速度并授予翻滚回避窗口。成功闪避设置 dodgeCooldownTick,再用刚授予的窗口检查当前伤害。
| 本地闪避计算 | 数值或行为 |
|---|---|
| 基础翻滚距离 | 3 加 Combat Roll 配置增量,最小 1 |
| 水平速度 | 距离 × 0.475 |
| 水中乘数 | min(waterHeight, 1) × 0.5 |
| 熔岩乘数 | 0.3 |
| 比草方块更滑的地面 | 乘以草方块与当前地面滑度比的平方 |
| 窗口时长 | Combat Roll 时长 × 速度比,向上取整,最少 1 tick |
| 速度比限制 | 0.5–2.5,基准为 3 × 0.475 |
| 获取依赖时长失败 | 回退为 8 tick |
本地窗口在到期 tick 仍然有效。绕过无敌或效果的伤害,以及虚空、溺水、饥饿、摔落、窒息和火焰伤害,不能通过此工具回避;可回避伤害还必须有攻击者或直接来源。表中描述的是本地适配,不是外部 Combat Roll 的配置默认值。
约定
配置与持久化
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
normalMeleeAttacks | boolean | true | 仅对具备战斗能力的 NPC 开启持续武器近战。 |
attackRange | double | 2.4 | 默认动作范围,单位为方块;构造器不限制。 |
protectRange | double | 12 | 保护和支援半径,单位为方块;构造器不限制。 |
aggroRange | double | 12 | 普通敌对及不可见目标范围,单位为方块;构造器不限制。 |
dodgeCooldownTick | integer | 30 | 成功闪避后的冷却;限制为非负。 |
这些范围是配置输入,不是经过安全校验的半径;平方计算不会拒绝负值。表中列出 NpcConfig 默认值,具体 NPC 可以覆盖。
战斗冷却字段直接存入实体 NBT:
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
CombatGlobalCD | integer | 0 | 剩余特殊动作冷却;加载的负值归 0,不阻止近战。 |
CombatActionCD | list of compounds | 空 | 已知动作 ID 的正值冷却;忽略未知 ID 和非正 tick。 |
CombatActionCD[].ActionId | string | 空 | 精确匹配当前动作集 ID;稳定 ID 可在动作重排后保留冷却对应关系。 |
CombatActionCD[].Ticks | integer | 0 | 剩余正值 tick;重复的有效 ID 后项覆盖前项。 |
控制器不保存动作阶段、已执行时长、捕获目标及是否已触发标记,加载不会续播中断攻击。跟踪器同步动作索引、阶段、序号、目标 UUID、施法开始时间和时长;阶段变化会递增序号。因此两端必须以确定且一致的顺序注册动作。
动画归属
普通动作默认使用 charge_<id>、接续的 charge_loop_<id> 和 release_<id>,显式动画键按对应循环或保持标记播放。usesExternalAnimation() 可抑制编写动作的 Gecko 动画。持续近战本身不持有 Gecko 动作动画,而由 Better Mob Combat 播放;编写的动作开始时会停止残余武器收势动画,但不会缩短服务端的武器恢复计时。
模型姿态处理末尾、体型塑形之前,HumanoidPlayerPoseBridge 先检查已采样的变身宿主姿态。否则,自主 NPC 路径要求 Player Animator 正在播放,普通近战已启用且 Better Mob Combat 战斗动画活跃;屏幕内姿态是例外。躯干、头、四肢分别映射到 torso、head、right_arm、left_arm、right_leg、left_leg,缺少的骨骼被跳过。自主动画通道以已求得的 Gecko 姿态回退,并将弯曲近似为刚体旋转。
手持物品偏移通过矩阵施加:位置除以 16,再按 Z/Y/X 顺序旋转,不累积到物品骨骼上。变身的 body 通道使用独立外层矩阵;应用宿主各部位前,桥接器会恢复 Gecko body 的绑定变换。扩展渲染器时,应分别处理动画归属与服务器命中权限。
扩展方式
在 NPC 子类覆盖 registerNpcActions(NpcActionSet.Builder),保持 ID 稳定、唯一,注册顺序确定。实现 NpcAction,或在目标策略合适时使用 NpcActions.builder。
| 成员组 | 职责 |
|---|---|
id、type、priority、score | 标识、动作类别及空闲选择顺序 |
selectTarget、canStart、rangeOverride | 启动许可;命中阶段特有条件须在触发时重查 |
timing、cooldownTicks、globalCooldownTicks | 时序与两种冷却生命周期 |
locksNavigation、locksMovement、interruptible | 导航与移动锁,以及是否允许更高优先级动作抢占 |
castAnimationKey、executeAnimationKey、loopCastAnimation、holdExecuteAnimation、usesExternalAnimation | 动画归属与播放 |
meleePolicy | 所需挥击数与近战衔接方式 |
visualSpell | 可选法术表现引用 |
onStart、onTrigger、onStop | 服务器回调;所有停止原因都应释放外部资源 |
动作上下文提供服务器世界、NPC、当前敌对目标、可见生物及支援候选。回调同步执行,控制器不捕获异常进行恢复,应在服务器 tick 路径调用。不要将 canStart 通过等同于稍后的命中仍然合法。
构造器生成不可变定义。默认时序为 instant,优先级 0,两种特殊动作冷却均为 0,范围覆盖为 0,无视觉法术、无显式动画键,施法循环为 false,执行保持为 true,外部动画为 false。设置方法拒绝负冷却,以及非有限或负的范围覆盖。重复调用 condition 会按短路 AND 组合;再次修改构造器不会改变已生成的动作。
以下示例供已开启普通近战、并具备对应 Gecko 蓄力与释放动画的战斗 NPC 使用:至少两次武器挥击后允许穿插现有雷电法术;冷却尚未结束时会继续挥击。示例动作 ID 需要在该 NPC 上提供对应动画。
builder.add(NpcActions.builder("short_cast_lightning", NpcActionType.ATTACK)
.timing(NpcActionTiming.of(20, 8, 7))
.cooldownTicks(22).globalCooldownTicks(22)
.meleePolicy(NpcMeleePolicy.afterSwings(2))
.visualSpell(LightningStrikeSpell.INSTANCE)
.onTrigger((ctx, target) -> LightningStrikeSpell.execute(ctx.npc(), target, 12.0F))
.build());
伊芙莉娜的定义已采用此构造器,保留原有法术时序、伤害以及禁用普通近战的设定。新原型自行选择循环策略,构造器不会自动将施法者变成近战者。
验证
运行./gradlew.bat compileJava。在多人游戏中验证属性存档加载、持久修饰符、武器切换、动作取消和各 NPC 状态独立。
- 给战斗 NPC 装备已注册近战武器且不注册特殊动作,检查连续挥击节奏;再加入两次挥击后施法的动作,分别检查命中后、恢复后及中断衔接策略。
- 检查评分完全相同、冷却到期和零施法时长。确认只触发一次,提前取消会消耗动作冷却,特殊动作冷却期间仍继续近战。
- 起手期间杀死或移除目标、开始使用物品、睡眠、表情动作及目标效果接管,检查各停止路径且无残留依赖起手。
- 启动自定义动作后,让目标离开范围或移到墙后,确认触发回调执行预期的命中规则。
- 测试协助、防御、被动模式,以及受信任玩家、阵营盟友和进入挥击范围的旁观者;攻击怪物时不能伤害无关玩家。
- 测试低、中、高伤害尝试,包含被闪避攻击和记忆到期;被拒绝的友军伤害不得增加敌意次数。
- 冷却中保存,保留 ID 但重排动作后加载,确认冷却正确对应,且进行中的动作不会续播。
- 从另一客户端观察近战的基础及替换服装,检查手臂、物品和身体变换没有重复 Gecko 攻击动画;另外验证玩家变身仍由玩家输入控制。