25 Commits

Author SHA1 Message Date
panFD
d7cd440844 chore: 修改 git 提交规则配置中的 alwaysApply 为 false
调整 git 提交规则配置文件,将 alwaysApply 字段从 true 修改为 false
2026-10-10 08:46:32 +08:00
panFD
a242ccc2ea feat(heros): 为多个英雄预制件添加外发光材质组件 2026-10-10 08:43:55 +08:00
panFD
74d03facd9 fix(boss): keep phase outline authoritative over level tint and pool reuse
Reuse existing outline-green/outline-glow materials (P2 green / P3 red); phase outline now takes priority over level tint during boss lifetime (init/lv_up both route through BossPhaseComp), and HeroViewComp.reset clears the material back to default so pooled monster nodes never inherit a boss's red/green edge.
2026-10-10 00:40:47 +08:00
panFD
a122d0904c feat(boss): tint boss sprite outline by phase (P2 green, P3 red)
Add FlashSprite.setOutlineByBossPhase with light/dark green shade option and red fallback; BossPhaseSystem drives HeroViewComp.applyBossPhaseOutline directly after phase switch (MON faction, no hero-level event semantics, pool-node safe); extract getFlashSprite/applyOutlineByLevel to dedupe the three outline call sites.
2026-10-10 00:33:41 +08:00
panFD
24aff3baa3 refactor(hero): 移除 Boss 阶段描边相关未实现的代码
清理了 Boss 阶段描边功能的相关代码,包括移除了 resetPhaseOutline 和 setPhaseOutline 方法,删除了相关材质定义,将 onPhaseEnter 方法置空待后续迭代实现。
2026-10-10 00:27:29 +08:00
panFD
b31d870d90 feat(boss): tint boss outline green in phase 2 and red in phase 3
Add FlashSprite.setPhaseOutline (P1 default / P2 green / P3 red) and drive it from BossPhaseSystem.onPhaseEnter; reset outline on pooled boss reuse so stale tint never leaks to the next owner. Red outline material asset must be created in editor (copy green, tint red) and bound to FlashSprite.outlineMatRed.
2026-10-10 00:25:23 +08:00
panFD
11ba692f18 feat(config): add 12 boss phase plans and mount phase_ids on 6 bosses
Populate BossPhaseSet with add-style and stance-swap plans per boss (melee pool 6100-6104, ranged pool 6150-6151/6200-6204); stance-swap lists the full position ult pool in removes so random injected ults are always cleared. Mount phase_ids on 6006/6101-6106.
2026-10-09 22:15:07 +08:00
panFD
e662b63f80 docs: add boss phase content design based on shipped mechanics
Adapt external draft to existing BossPhaseSet capabilities: pool-wide removes for stance-swap semantics, ap/def-only attr mods, 30s/60s time triggers aligned with 90s battle timeout, and 13 ready-to-paste plans across 6 bosses.
2026-10-08 23:12:13 +08:00
panFD
2ab5100b41 feat: 更新BOSS阶段配置并简化方案设计
重构了兽人首领BOSS的阶段配置,将原12个阶段方案精简为1个示例方案,调整了触发条件与技能配置,优化了阶段方案的可读性与可维护性,确保大招引用近战池保证可达性。
2026-10-08 16:45:53 +08:00
panFD
f4fb561319 fix(map): 调整下一回合刷怪时机避免与英雄过场同帧移动
修改 autoNextPhase 逻辑,将 advanceToNextSection 从 PrepareStart 阶段移至 PrepareEnd 阶段,确保英雄归位待机约 1 秒后再生成怪物,使怪物随回合开始横幅同步登场,解决与英雄过场行军同帧移动的显示问题。
2026-10-08 16:44:49 +08:00
panFD
fef913bd75 feat: add 12 boss phase plans and wire to 4 bosses
Twelve plans across four bosses (melee/mage/archer/tank), clear-replace and keep-add modes interleaved, hp and time trigger dimensions. Four bosses wired with phase_ids pools for random plan selection.
2026-10-08 15:56:16 +08:00
panFD
53a0ee1c6e refactor(hero): adjust monster prefab sizes and add boss scaling logic
调整多个怪物预制体的尺寸参数,同时在怪物基类中添加 Boss 体型放大 1.5 倍的逻辑,小怪保持原尺寸。
2026-10-07 17:29:45 +08:00
panFD
f679644bbb feat: 添加 Boss 阶段系统及测试配置
新增 BossPhaseSystem 脚本及元文件,添加阶段挂载与接管的日志记录
修改测试模式配置,调整 Boss 相关属性以适配阶段系统
2026-10-07 17:20:25 +08:00
panFD
541ca0e251 chore: add Cocos meta files for new phase scripts 2026-10-07 15:44:57 +08:00
panFD
7703b0348c fix: pass phase plan id and clear stale test skills on reuse 2026-10-07 15:44:33 +08:00
panFD
ad50e11052 feat: add BossPhaseSystem with phase switch logic 2026-10-07 15:43:17 +08:00
panFD
d5158cde52 feat: wire BossPhaseComp into monster load/destroy 2026-10-07 15:39:25 +08:00
panFD
8176ef098e feat: consume phase plans in boss generation 2026-10-07 15:36:20 +08:00
panFD
56dcdccca2 feat: add phase_ids field to boss hero config 2026-10-07 15:33:45 +08:00
panFD
31a2efee4d feat: add BossPhaseSet config with phase plan schema 2026-10-07 15:32:36 +08:00
panFD
761d0aa68e feat: add BossPhaseChange game event 2026-10-07 15:32:21 +08:00
panFD
7766909294 feat: add BossPhaseComp marker component 2026-10-07 15:32:15 +08:00
panFD
71db1c24b0 docs: add boss phase system implementation plan
Nine bite-sized tasks covering config schema, marker component, phase switch system, generation wiring, stale test-skill fix, and editor-based runtime verification. Full code in every step, no placeholders.
2026-10-07 15:06:32 +08:00
panFD
ca38411cc7 fix(hero-config): 修正BOSS怪物的名称描述 2026-10-07 15:01:37 +08:00
panFD
b149e6517b docs: add boss phase system design spec
Add design for WoW-style 3-phase boss combat: mixed triggers (hp threshold OR elapsed time), incremental skill adds/removes, BossPhaseComp marker component, and BossPhaseSystem with stub performance hooks. Plan ids use the 10000 id range. Reviewed by three sub-agents against ECS framework and combat chain sources.
2026-10-07 15:00:57 +08:00
24 changed files with 2105 additions and 70 deletions

View File

@@ -1,8 +1,7 @@
---
alwaysApply: true
alwaysApply: false
scene: git_message
---
## Profile
你是一个严谨的配置管理专家与代码审查员。你的核心任务是根据代码的变更内容(diff)或开发者的简短描述,生成符合业界标准(Conventional Commits / Angular 规范)的 Git 提交信息。你擅长剥离代码细节,总结出清晰的业务意图(What 和 Why)。

View File

@@ -402,6 +402,10 @@
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
"__expectedType__": "cc.Material"
},
"outlineMatGlow": {
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
"__expectedType__": "cc.Material"
},
"_id": ""
},
{

View File

@@ -402,6 +402,10 @@
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
"__expectedType__": "cc.Material"
},
"outlineMatGlow": {
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
"__expectedType__": "cc.Material"
},
"_id": ""
},
{
@@ -812,8 +816,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -921,12 +925,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 75
"y": 50
},
"_size": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_id": ""
},

View File

@@ -402,6 +402,10 @@
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
"__expectedType__": "cc.Material"
},
"outlineMatGlow": {
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
"__expectedType__": "cc.Material"
},
"_id": ""
},
{
@@ -809,8 +813,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -918,12 +922,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 75
"y": 50
},
"_size": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 60,
"height": 100
},
"_id": ""
},

View File

@@ -402,6 +402,10 @@
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
"__expectedType__": "cc.Material"
},
"outlineMatGlow": {
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
"__expectedType__": "cc.Material"
},
"_id": ""
},
{
@@ -812,8 +816,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -921,12 +925,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 75
"y": 50
},
"_size": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 8,
"height": 100
},
"_id": ""
},

View File

@@ -402,6 +402,10 @@
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
"__expectedType__": "cc.Material"
},
"outlineMatGlow": {
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
"__expectedType__": "cc.Material"
},
"_id": ""
},
{
@@ -809,8 +813,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -918,12 +922,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 75
"y": 50
},
"_size": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_id": ""
},

View File

@@ -402,6 +402,10 @@
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
"__expectedType__": "cc.Material"
},
"outlineMatGlow": {
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
"__expectedType__": "cc.Material"
},
"_id": ""
},
{
@@ -809,8 +813,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -918,12 +922,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 75
"y": 50
},
"_size": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_id": ""
},

View File

@@ -0,0 +1,268 @@
/**
* @file BossPhaseSet.ts
* @description Boss 多阶段战斗方案配置(固定 3 阶段:P1 基线 / P2 / P3)
*
* 数据流:heroInfo.phase_ids(允许方案)→ RogueConfig.makeBoss 随机取一
* → Mon.load 第 8 参 → BossPhaseComp → BossPhaseSystem 检测切换
*
* 设计约束:
* - 方案 id 使用 10000 段(9100 段已被 TalentSet 9101-9199 / ICardSet 9101~9110 占用)
* - 增量驱动:切换 = removes 移除 + adds 注入,P1 = 基线技能(heroInfo.skills + RogueConfig 大招)无需配置
* - removes/adds 禁止操作普攻首键(SCastSystem.buildSkillCandidates 硬假设首键=普攻垫底;
* JS delete+set 会把键移到末尾,破坏普攻垫底契约)
* - 混合触发:同一切换点 hp_pct / time 任一满足即切换;
* hp_pct 切换点必须构成前缀(time-only 点之后禁止再配 hp_pct,防乱序连切)
*/
import { SkillSet, SkillOverrides } from "./SkillSet";
/** 阶段切换触发条件(混合触发:任一条件满足即切换) */
export interface PhaseTrigger {
/** 血量百分比阈值(1~99):Boss 血量比例 ≤ 该值时触发 */
hp_pct?: number;
/** Boss 在场战斗秒数:超过该时长触发 */
time?: number;
}
/** 切换瞬间一次性属性修正(基于当前值链式乘算,P3 基于 P2 修正后的值) */
export interface PhaseAttrMod {
/** 攻击 ±% */
ap_pct?: number;
/** 防御 ±%(applyDefense 除法递减模型,收益非线性,配值按公式校验) */
defense_pct?: number;
}
/** 注入技能条目(与 RogueConfig 大招条目同构,cd 缺省由系统兜底 8s) */
export interface PhaseSkillEntry {
s_uuid: number;
cd?: number;
overrides?: SkillOverrides;
}
/** 单个切换点定义(switches[0] = P1→P2,switches[1] = P2→P3) */
export interface PhaseSwitchDef {
trigger: PhaseTrigger;
/** 注入的技能(追加进 skills 表,SCastSystem 按 CD 轮转)。禁止操作普攻首键 */
adds?: PhaseSkillEntry[];
/** 移除的技能 s_uuid 列表(含 RogueConfig 注入的大招)。禁止操作普攻首键 */
removes?: number[];
attr_mod?: PhaseAttrMod;
}
/** 阶段方案:固定 2 个切换点 = 3 阶段,P1 为基线 */
export interface BossPhasePlan {
name: string;
switches: [PhaseSwitchDef, PhaseSwitchDef];
}
/**
* 阶段方案表(id 段 10000,按 Boss 分组,详见 docs/superpowers/specs/2026-10-06-boss-phase-content-design.md)
* 换招式技巧:removes 列满该 Boss 定位的整个大招池 id(delete 空键无副作用),
* 无论 RogueConfig 随机注入了哪个大招一律清净,再 adds 新技能组
* - 近战定位池:[6100, 6101, 6102, 6103, 6104]
* - 远程定位池:[6150, 6151, 6200, 6201, 6202, 6203, 6204]
*/
export const BossPhaseSet: Record<number, BossPhasePlan> = {
// ========== 6101 兽人首领-双刀dps(血量,近战池) ==========
/** 追加式:P2 换裂气斩、P3 加闪电斩 + 攻+20% */
10001: {
name: "狂暴进化",
switches: [
{ trigger: { hp_pct: 70 }, adds: [{ s_uuid: 6102, overrides: { ap: 180 } }], removes: [6100] },
{ trigger: { hp_pct: 40, time: 60 }, adds: [{ s_uuid: 6103, overrides: { ap: 180 } }], attr_mod: { ap_pct: 20 } },
],
},
/** 追加式:双刀越挥越快,技能只加不换 */
10002: {
name: "嗜血连斩",
switches: [
{ trigger: { hp_pct: 70 }, adds: [{ s_uuid: 6103, overrides: { ap: 160 } }], attr_mod: { ap_pct: 15 } },
{ trigger: { hp_pct: 40 }, adds: [{ s_uuid: 6104, overrides: { ap: 180 } }], attr_mod: { ap_pct: 25, defense_pct: -10 } },
],
},
/** 换招式:每阶段换一把"刀",系数逐段上调 */
10003: {
name: "换刃",
switches: [
{ trigger: { hp_pct: 70 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6101, overrides: { ap: 200 } }] },
{ trigger: { hp_pct: 40 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6104, overrides: { ap: 220 } }], attr_mod: { ap_pct: 20, defense_pct: -15 } },
],
},
// ========== 6102 兽人首领-坦克型(时间,近战池) ==========
/** 追加式:越久越硬,反击越痛 */
10010: {
name: "重压觉醒",
switches: [
{ trigger: { time: 30 }, adds: [{ s_uuid: 6100, overrides: { ap: 150 } }], attr_mod: { defense_pct: 20 } },
{ trigger: { time: 60 }, adds: [{ s_uuid: 6104, overrides: { ap: 180 } }], attr_mod: { ap_pct: 20 } },
],
},
/** 换招式:外壳剥落,换进攻形态 */
10011: {
name: "崩岩",
switches: [
{ trigger: { time: 30 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6102, overrides: { ap: 200 } }], attr_mod: { ap_pct: 15 } },
{ trigger: { time: 60 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6104, overrides: { ap: 220 } }], attr_mod: { ap_pct: 25, defense_pct: -20 } },
],
},
// ========== 6103 兽人首领-魔法师(时间,远程池) ==========
/** 追加式:元素逐一汇入,弹幕越来越密 */
10020: {
name: "元素汇聚",
switches: [
{ trigger: { time: 30 }, adds: [{ s_uuid: 6204, overrides: { ap: 150 } }] },
{ trigger: { time: 60 }, adds: [{ s_uuid: 6202, overrides: { ap: 150 } }], attr_mod: { ap_pct: 20 } },
],
},
/** 换招式:每阶段念诵新禁咒,旧的被遗忘 */
10021: {
name: "禁咒",
switches: [
{ trigger: { time: 30 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6201, overrides: { ap: 180 } }] },
{ trigger: { time: 60 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6204, overrides: { ap: 200 } }], attr_mod: { ap_pct: 30, defense_pct: -15 } },
],
},
// ========== 6104 兽人首领-射手(血量,远程池) ==========
/** 追加式:箭种逐段解锁 */
10030: {
name: "箭雨升级",
switches: [
{ trigger: { hp_pct: 70 }, adds: [{ s_uuid: 6150, overrides: { ap: 150 } }] },
{ trigger: { hp_pct: 40 }, adds: [{ s_uuid: 6151, overrides: { ap: 160 } }], attr_mod: { ap_pct: 20 } },
],
},
/** 换招式:放弃弹幕,换单体重炮 */
10031: {
name: "狙击姿态",
switches: [
{ trigger: { hp_pct: 70 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6203, overrides: { ap: 180 } }] },
{ trigger: { hp_pct: 40 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6204, overrides: { ap: 200 } }], attr_mod: { ap_pct: 25, defense_pct: -15 } },
],
},
// ========== 6105 亡灵首领-法师(时间,远程池) ==========
/** 追加式:毒雾与雷暴层层叠加 */
10040: {
name: "亡灵潮汐",
switches: [
{ trigger: { time: 30 }, adds: [{ s_uuid: 6202, overrides: { ap: 150 } }] },
{ trigger: { time: 60 }, adds: [{ s_uuid: 6204, overrides: { ap: 150 } }], attr_mod: { ap_pct: 25 } },
],
},
/** 换招式:燃尽旧躯壳换新法术 */
10041: {
name: "灵魂爆发",
switches: [
{ trigger: { time: 30 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6200, overrides: { ap: 180 } }], attr_mod: { defense_pct: 15 } },
{ trigger: { time: 60 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6204, overrides: { ap: 220 } }], attr_mod: { ap_pct: 30 } },
],
},
// ========== 6106 亡灵首领-骑马战士(时间,近战池) ==========
/** 追加式:战马冲锋节奏渐强 */
10050: {
name: "冲锋陷阵",
switches: [
{ trigger: { time: 30 }, adds: [{ s_uuid: 6101, overrides: { ap: 160 } }] },
{ trigger: { time: 60 }, adds: [{ s_uuid: 6104, overrides: { ap: 180 } }], attr_mod: { ap_pct: 20, defense_pct: -10 } },
],
},
/** 换招式:弃守转攻,每段更凶更脆 */
10051: {
name: "背水一战",
switches: [
{ trigger: { time: 30 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6103, overrides: { ap: 200 } }], attr_mod: { ap_pct: 15, defense_pct: -10 } },
{ trigger: { time: 60 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6104, overrides: { ap: 220 } }], attr_mod: { ap_pct: 30, defense_pct: -20 } },
],
},
// ========== 6006 骷髅领主(血量,近战池) ==========
/** 追加式:骨架越碎越硬 */
10060: {
name: "亡灵复苏",
switches: [
{ trigger: { hp_pct: 70 }, adds: [{ s_uuid: 6100, overrides: { ap: 150 } }], attr_mod: { defense_pct: 25 } },
{ trigger: { hp_pct: 40 }, adds: [{ s_uuid: 6102, overrides: { ap: 160 } }], attr_mod: { ap_pct: 15, defense_pct: 15 } },
],
},
/** 换招式:拼装新武器,攻高防低 */
10061: {
name: "骷髅狂暴",
switches: [
{ trigger: { hp_pct: 70 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6103, overrides: { ap: 180 } }] },
{ trigger: { hp_pct: 40 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6104, overrides: { ap: 220 } }], attr_mod: { ap_pct: 25, defense_pct: -15 } },
],
},
};
/**
* 校验阶段方案表合法性(供 validateRogueConfig 追加调用,建议启动期执行一次)
* @returns 错误信息数组,空数组表示校验通过
*/
export function validateBossPhaseSet(): string[] {
const errors: string[] = [];
for (const key in BossPhaseSet) {
const plan = BossPhaseSet[key];
const tag = `BossPhaseSet[${key}](${plan.name})`;
if (plan.switches.length !== 2) {
errors.push(`${tag} 切换点必须恰为 2 个(固定 3 阶段),实际 ${plan.switches.length}`);
continue;
}
let seenTimeOnly = false;
let lastHpPct = 101;
let lastTime = 0;
plan.switches.forEach((def, i) => {
const t = def.trigger;
const stag = `${tag} switches[${i}]`;
// 条件完备性与取值范围
if (t.hp_pct === undefined && t.time === undefined) {
errors.push(`${stag} trigger 至少需要 hp_pct 或 time 之一`);
return;
}
if (t.hp_pct !== undefined && (t.hp_pct <= 0 || t.hp_pct >= 100)) {
errors.push(`${stag} hp_pct 必须在 (0,100) 区间,实际 ${t.hp_pct}`);
}
if (t.time !== undefined && t.time <= 0) {
errors.push(`${stag} time 必须 > 0,实际 ${t.time}`);
}
// hp_pct 前缀规则:time-only 点之后禁止再配 hp_pct(防"P2 time / P3 hp_pct"乱序连切)
if (seenTimeOnly && t.hp_pct !== undefined) {
errors.push(`${stag} hp_pct 切换点必须构成前缀:time-only 切换点之后禁止再配置 hp_pct`);
}
if (t.hp_pct === undefined && t.time !== undefined) {
seenTimeOnly = true;
}
// hp_pct 严格递减 / time 严格递增
if (t.hp_pct !== undefined) {
if (t.hp_pct >= lastHpPct) {
errors.push(`${stag} hp_pct=${t.hp_pct} 未严格小于前一档 ${lastHpPct}`);
}
lastHpPct = t.hp_pct;
}
if (t.time !== undefined) {
if (t.time <= lastTime) {
errors.push(`${stag} time=${t.time} 未严格大于前一档 ${lastTime}`);
}
lastTime = t.time;
}
// 技能引用存在性(error 级)
for (const add of def.adds ?? []) {
if (!add.s_uuid || !SkillSet[add.s_uuid]) {
errors.push(`${stag} adds 技能 ${add.s_uuid} 未在 SkillSet 中定义`);
}
}
for (const s_uuid of def.removes ?? []) {
if (!SkillSet[s_uuid]) {
errors.push(`${stag} removes 技能 ${s_uuid} 未在 SkillSet 中定义`);
}
}
});
}
return errors;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "73ce05bd-3011-4b93-aa6b-f9eafa596cfe",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -116,5 +116,7 @@ export enum GameEvent {
NextSection = "NextSection",
/** 结算弹窗点击"再来一次"(payload 为空,MissionComp 据此重开本关) */
RestartMission = "RestartMission",
/** Boss 阶段切换(payload: { eid, planId, fromPhase, toPhase },表现层监听此事件实现演出;eid 定位实体,勿持裸引用) */
BossPhaseChange = "BossPhaseChange",
}

View File

@@ -1,4 +1,4 @@
import { v3 } from "cc"
import { v3 } from "cc"
import { BoxSet, FacSet, FightSet } from "./GameSet"
import { SkillOverrides, TGroup, SkillSet, FieldSkillSet, SkillConfig, SkillKind, FieldSkillConfig, FieldSkillType, mergeSkillParams, BuffTarget } from "./SkillSet"
import { BuffList, BuffConfig, ModOp } from "./BuffSet"
@@ -430,6 +430,7 @@ export interface heroInfo {
speed?: number; // 移动速度(像素/秒)
dodge?: number; // 闪避率(百分点,缺省由初始化处兜底为全局默认值)
accuracy?: number; // 命中率(百分点,缺省由初始化处兜底为全局默认值)
phase_ids?: number[]; // Boss 专属:允许的阶段方案 id 列表(引用 BossPhaseSet,生成时随机取一)
skills: Record<number, HSkillInfo>; // 携带技能ID列表
bonus?: LvBonusAttrsEntry[]; // 额外属性加成(按 lv 升序,运行时累加所有 lv ≤ 英雄等级 的档位 attrs;hp/ap/def 按"基础×品质+成长"快照百分比加成,crt/frz/stun/para 为固定百分点累加)
info: string; // 描述文案
@@ -1079,7 +1080,7 @@ export const HeroInfo: Record<number, heroInfo> = {
},
// 特殊位怪物
6006: {
uuid: 6006, name: "骷髅领主", path: "m6", fac: FacSet.MON, lv: 1, rarity: 3, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, monType: MonType.MeleeBoss, hp: HERO_ROLE_BASE[HRole.Tank].hp, ap: HERO_ROLE_BASE[HRole.Tank].ap, def: HERO_ROLE_BASE[HRole.Tank].def, speed: 60,
uuid: 6006, name: "骷髅领主", path: "m6", fac: FacSet.MON, lv: 1, rarity: 3, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, monType: MonType.MeleeBoss, hp: HERO_ROLE_BASE[HRole.Tank].hp, ap: HERO_ROLE_BASE[HRole.Tank].ap, def: HERO_ROLE_BASE[HRole.Tank].def, speed: 60, phase_ids: [10060, 10061],
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排MiniBoss级坦克"
},
6007: {
@@ -1093,27 +1094,27 @@ export const HeroInfo: Record<number, heroInfo> = {
// BOSS怪物 — 与英雄同一套设定(role 模板 + 品质系数),高品质承伤/输出位
6101: {
uuid: 6101, name: "兽人首领-双刀战士", path: "mb1", fac: FacSet.MON, lv: 6, rarity: 4, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, monType: MonType.MeleeBoss, hp: HERO_ROLE_BASE[HRole.Warrior].hp, ap: HERO_ROLE_BASE[HRole.Warrior].ap, def: HERO_ROLE_BASE[HRole.Warrior].def, speed: 120,
uuid: 6101, name: "兽人首领-双刀dps型", path: "mb1", fac: FacSet.MON, lv: 6, rarity: 4, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, monType: MonType.MeleeBoss, hp: HERO_ROLE_BASE[HRole.Warrior].hp, ap: HERO_ROLE_BASE[HRole.Warrior].ap, def: HERO_ROLE_BASE[HRole.Warrior].def, speed: 120, phase_ids: [10001, 10002, 10003],
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排Boss, 攻速型"
},
6102: {
uuid: 6102, name: "兽人首领-斧头战士", path: "mb2", fac: FacSet.MON, lv: 6, rarity: 5, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, monType: MonType.MeleeBoss, hp: HERO_ROLE_BASE[HRole.Tank].hp, ap: HERO_ROLE_BASE[HRole.Tank].ap, def: HERO_ROLE_BASE[HRole.Tank].def, speed: 60,
uuid: 6102, name: "兽人首领-坦克型", path: "mb2", fac: FacSet.MON, lv: 6, rarity: 5, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, monType: MonType.MeleeBoss, hp: HERO_ROLE_BASE[HRole.Tank].hp, ap: HERO_ROLE_BASE[HRole.Tank].ap, def: HERO_ROLE_BASE[HRole.Tank].def, speed: 60, phase_ids: [10010, 10011],
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排Boss, 超高HP"
},
6103: {
uuid: 6103, name: "兽人首领-魔法师", path: "mb3", fac: FacSet.MON, lv: 6, rarity: 4, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, monType: MonType.LongBoss, hp: HERO_ROLE_BASE[HRole.Mage].hp, ap: HERO_ROLE_BASE[HRole.Mage].ap, def: HERO_ROLE_BASE[HRole.Mage].def, speed: 110,
uuid: 6103, name: "兽人首领-魔法师", path: "mb3", fac: FacSet.MON, lv: 6, rarity: 4, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, monType: MonType.LongBoss, hp: HERO_ROLE_BASE[HRole.Mage].hp, ap: HERO_ROLE_BASE[HRole.Mage].ap, def: HERO_ROLE_BASE[HRole.Mage].def, speed: 110, phase_ids: [10020, 10021],
skills: { 6009: { uuid: 6009, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100, hit_count: 2 } } }, info: "后排法系Boss"
},
6104: {
uuid: 6104, name: "兽人首领-射手", path: "mb4", fac: FacSet.MON, lv: 6, rarity: 4, type: HType.Long, grow_type: HGrowType.Archer, role: HRole.Archer, monType: MonType.LongBoss, hp: HERO_ROLE_BASE[HRole.Archer].hp, ap: HERO_ROLE_BASE[HRole.Archer].ap, def: HERO_ROLE_BASE[HRole.Archer].def, speed: 70,
uuid: 6104, name: "兽人首领-射手", path: "mb4", fac: FacSet.MON, lv: 6, rarity: 4, type: HType.Long, grow_type: HGrowType.Archer, role: HRole.Archer, monType: MonType.LongBoss, hp: HERO_ROLE_BASE[HRole.Archer].hp, ap: HERO_ROLE_BASE[HRole.Archer].ap, def: HERO_ROLE_BASE[HRole.Archer].def, speed: 70, phase_ids: [10030, 10031],
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "后排位Boss, 均衡型"
},
6105: {
uuid: 6105, name: "亡灵首领-法师", path: "mb5", fac: FacSet.MON, lv: 6, rarity: 5, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, monType: MonType.LongBoss, hp: HERO_ROLE_BASE[HRole.Mage].hp, ap: HERO_ROLE_BASE[HRole.Mage].ap, def: HERO_ROLE_BASE[HRole.Mage].def, speed: 110,
uuid: 6105, name: "亡灵首领-法师", path: "mb5", fac: FacSet.MON, lv: 6, rarity: 5, type: HType.Long, grow_type: HGrowType.Mage, role: HRole.Mage, monType: MonType.LongBoss, hp: HERO_ROLE_BASE[HRole.Mage].hp, ap: HERO_ROLE_BASE[HRole.Mage].ap, def: HERO_ROLE_BASE[HRole.Mage].def, speed: 110, phase_ids: [10040, 10041],
skills: { 6011: { uuid: 6011, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100, hit_count: 2, timed_debuff_id: 7002 } } }, info: "后排高AP Boss"
},
6106: {
uuid: 6106, name: "亡灵首领-骑马战士", path: "mb6", fac: FacSet.MON, lv: 6, rarity: 5, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, monType: MonType.MeleeBoss, hp: HERO_ROLE_BASE[HRole.Tank].hp, ap: HERO_ROLE_BASE[HRole.Tank].ap, def: HERO_ROLE_BASE[HRole.Tank].def, speed: 130,
uuid: 6106, name: "亡灵首领-骑马战士", path: "mb6", fac: FacSet.MON, lv: 6, rarity: 5, type: HType.Melee, grow_type: HGrowType.Tank, role: HRole.Tank, monType: MonType.MeleeBoss, hp: HERO_ROLE_BASE[HRole.Tank].hp, ap: HERO_ROLE_BASE[HRole.Tank].ap, def: HERO_ROLE_BASE[HRole.Tank].def, speed: 130, phase_ids: [10050, 10051],
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排终极Boss, 最高HP+高速"
},

View File

@@ -0,0 +1,22 @@
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
/**
* Boss 阶段标记组件:挂载即表示该实体参与多阶段状态机
* 由 Mon.load 按 phase_plan_id 条件挂载,Monster.destroy 显式 remove(触发 reset + 回组件池)
* 计时字段 elapsed 随 reset 天然归零,无全局战斗计时状态
*/
@ecs.register('BossPhase')
export class BossPhaseComp extends ecs.Comp {
/** 生效的阶段方案 id(BossPhaseSet key) */
plan_id: number = 0;
/** 当前阶段索引:0=P1 / 1=P2 / 2=P3(P3 后系统不再检测) */
index: number = 0;
/** Boss 在场战斗累计时长(秒),time 触发条件基准 */
elapsed: number = 0;
reset() {
this.plan_id = 0;
this.index = 0;
this.elapsed = 0;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "49d9ce6c-d575-4453-83f8-721a7865a3e5",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,137 @@
/**
* BossPhaseSystem — Boss 多阶段战斗状态机。
*
* 检测混合触发条件(血量阈值 OR 在场时长),命中后执行阶段切换:
* removes 移除旧技能 → adds 注入新技能(cd 兜底 + 资源预热)→ attr_mod 属性修正
* → 空钩子 onPhaseExit/onPhaseEnter(演出后续迭代)→ 广播 GameEvent.BossPhaseChange
*
* Why: 独立系统符合 M/V/S 分层;filter 按 BossPhaseComp 过滤,每局 Boss 唯一,开销可忽略。
* 计时在组件 elapsed(随 reset 归零),避免系统级计数在 per-entity update 中重复累加。
*/
import { Prefab } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import { BossPhaseSet, PhaseSwitchDef } from "../common/config/BossPhaseSet";
import { SkillSet } from "../common/config/SkillSet";
import { mLogger } from "../common/Logger";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { BossPhaseComp } from "./BossPhaseComp";
import { HeroViewComp } from "./HeroViewComp";
/** 阶段模块调试日志开关 */
const DEBUG = true;
/** 新注入技能缺省 CD(秒),与 Mon.ts 大招注入兜底一致 */
const DEFAULT_ADD_CD = 8;
@ecs.register('BossPhaseSystem')
export class BossPhaseSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
filter(): ecs.IMatcher {
return ecs.allOf(BossPhaseComp, HeroAttrsComp);
}
update(e: ecs.Entity): void {
const phase = e.get(BossPhaseComp);
const model = e.get(HeroAttrsComp);
if (!phase || !model) return;
// 战斗状态守卫(与 MoveComp/SCastSystem 同款模式):暂停/未开战不计时
if (!smc.mission.play || smc.mission.pause) return;
// 死亡同帧守卫:防死尸换阶段
if (model.is_dead) return;
// P3 后不再检测(固定 3 阶段)
if (phase.index >= 2) return;
const plan = BossPhaseSet[phase.plan_id];
if (!plan) return;
// 分诊日志:系统接管确认(elapsed===0 表示本组件首个有效帧,只输出一次)
if (phase.elapsed === 0) {
mLogger.log(DEBUG, "BossPhase", `${model.hero_name} 阶段系统已接管(方案 ${phase.plan_id},当前 P${phase.index + 1},hp ${Math.round(model.hp)}/${model.hp_max})`);
}
phase.elapsed += this.dt;
const def = plan.switches[phase.index];
// 混合触发:任一条件满足即切换(血量实时计算,禁止缓存绝对值——hp_max 在 load 后被外部覆盖)
const hpPct = model.hp / model.hp_max * 100;
const hpHit = def.trigger.hp_pct !== undefined && hpPct <= def.trigger.hp_pct;
const timeHit = def.trigger.time !== undefined && phase.elapsed >= def.trigger.time;
if (hpHit || timeHit) {
this.executePhaseSwitch(e, def);
}
}
/**
* 执行阶段切换:技能增量改写 + 属性修正 + 钩子与事件广播
* Why 改 skills 而非施法系统过滤:与 _testSkills/RogueConfig.ults 动态注入同模式,
* SCastSystem 零改动;施法闭包捕获 SkillSet 静态配置,进行中施法不受影响
*/
private executePhaseSwitch(e: ecs.Entity, def: PhaseSwitchDef): void {
const phase = e.get(BossPhaseComp)!;
const model = e.get(HeroAttrsComp)!;
const from = phase.index;
this.onPhaseExit(e);
// 1. 移除旧技能(仅大招键;普攻首键受键序契约保护,见 BossPhaseSet 文件头注释)
if (def.removes) {
for (const s_uuid of def.removes) {
delete model.skills[s_uuid];
}
}
// 2. 注入新技能(cd 强制兜底,防 undefined → Math.min 结果 NaN 导致大招永不释放)
if (def.adds) {
for (const add of def.adds) {
if (!add || !add.s_uuid || !SkillSet[add.s_uuid]) continue;
model.skills[add.s_uuid] = {
uuid: add.s_uuid,
lv: 1,
cd: add.cd ?? DEFAULT_ADD_CD,
ccd: 0,
overrides: add.overrides,
};
// 资源预热:新技能 prefab 首次异步加载会丢一发,切换时预载(路径与 Skill.load 对齐)
oops.res.load(`game/skill/atk/${SkillSet[add.s_uuid].sp_name}`, Prefab);
}
}
// 3. 属性修正:基于当前值链式乘算(P3 基于 P2 修正后的值,配置按此口径)
const mod = def.attr_mod;
if (mod) {
if (mod.ap_pct) model.ap = Math.round(model.ap * (1 + mod.ap_pct / 100));
if (mod.defense_pct) model.defense = Math.round(model.defense * (1 + mod.defense_pct / 100));
}
phase.index++;
this.onPhaseEnter(e);
mLogger.log(DEBUG, "BossPhase", `${model.hero_name} 阶段切换 P${from + 1}→P${phase.index + 1}(方案 ${phase.plan_id})`);
// 广播阶段切换(payload 用 eid 定位实体,防监听方持有已回收实体裸引用)
oops.message.dispatchEvent(GameEvent.BossPhaseChange, {
eid: e.eid,
planId: phase.plan_id,
fromPhase: from,
toPhase: phase.index,
});
// 直接驱动描边:Boss 是 MON 阵营,不走英雄 HeroLvUp 语义;
// 且对象池节点上挂全局事件监听易漏注销,直接调视图组件最稳
const view = e.get(HeroViewComp);
if (view && view.node && view.node.isValid) {
view.applyBossPhaseOutline(phase.index);
}
}
/** TODO: 阶段退场表现(震屏/变暗等),后续迭代实现 */
private onPhaseExit(e: ecs.Entity): void { }
/** TODO: 阶段入场表现(怒吼/无敌帧/技能横幅等),后续迭代实现 */
private onPhaseEnter(e: ecs.Entity): void { }
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "bde209ff-324b-4f07-a241-fb977cbc6c2f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -16,6 +16,7 @@ import { Tooltip } from "../skill/Tooltip";
import { timedCom } from "../skill/timedCom";
import { oneCom } from "../skill/oncend";
import { FlashSprite } from "./hit-flash-white/scripts/FlashSprite";
import { BossPhaseComp } from "./BossPhaseComp";
const { ccclass, property } = _decorator;
@@ -89,7 +90,6 @@ export class HeroViewComp extends CCComp {
this.deadCD = 0;
this.lastBarUpdateTime = 0;
this.status_change("idle");
// 初始化头顶状态栏
const topNode = this.node.getChildByName("top");
this.topBar = topNode.getComponent(HeroTopBarComp) || topNode.addComponent(HeroTopBarComp);
@@ -98,15 +98,12 @@ export class HeroViewComp extends CCComp {
/** 方向 */
this.node.setScale(this.scale * Math.abs(this.node.scale.x), 1 * this.node.scale.y); // 确保 scale.x 为正后再乘方向
// 🔥 重置描边
if (this.model) {
let flashSprite = this.node.getComponent(FlashSprite);
if (!flashSprite) {
flashSprite = this.node.getComponentInChildren(FlashSprite);
}
if (flashSprite) {
flashSprite.setOutlineByLevel(this.model.lv);
}
// 🔥 重置描边:Boss 阶段描边优先(Boss 存活期不被等级描边覆盖),其余按等级
const phaseComp = this.ent ? this.ent.get(BossPhaseComp) : null;
if (phaseComp && phaseComp.index > 0) {
this.applyBossPhaseOutline(phaseComp.index);
} else {
this.applyOutlineByLevel();
}
}
@@ -170,18 +167,42 @@ export class HeroViewComp extends CCComp {
public lv_up() {
this.spawnTimedFx("game/skill/buff/buff_lvup", this.node, 1.0);
// 升级时同步更新描边
if (this.model) {
let flashSprite = this.node.getComponent(FlashSprite);
if (!flashSprite) {
flashSprite = this.node.getComponentInChildren(FlashSprite);
}
if (flashSprite) {
flashSprite.setOutlineByLevel(this.model.lv);
}
// 升级时同步更新描边:Boss 阶段描边优先(与 init 口径一致)
const phaseComp = this.ent ? this.ent.get(BossPhaseComp) : null;
if (phaseComp && phaseComp.index > 0) {
this.applyBossPhaseOutline(phaseComp.index);
} else {
this.applyOutlineByLevel();
}
}
/** 按英雄等级应用描边(升级/初始化共用入口) */
private applyOutlineByLevel() {
if (!this.model) return;
const flashSprite = this.getFlashSprite();
if (flashSprite) {
flashSprite.setOutlineByLevel(this.model.lv);
}
}
/**
* 按 Boss 阶段应用描边(P2 绿 / P3 红)
* Why: 由 BossPhaseSystem 直接调用,不走英雄 HeroLvUp 事件语义
* (Boss 是 MON 阵营,且对象池节点挂全局监听易漏注销)
* @param phase 阶段索引:1=P2 / 2=P3
*/
public applyBossPhaseOutline(phase: number) {
const flashSprite = this.getFlashSprite();
if (flashSprite) {
flashSprite.setOutlineByBossPhase(phase);
}
}
/** 查找 FlashSprite 组件(优先自身,回退子节点),描边三处调用点统一入口 */
private getFlashSprite(): FlashSprite | null {
return this.node.getComponent(FlashSprite) ?? this.node.getComponentInChildren(FlashSprite);
}
/** 攻击力提升特效 */
private ap_up() {
this.spawnTimedFx("game/skill/buff/buff_apup", this.node, 1.0);
@@ -566,6 +587,13 @@ export class HeroViewComp extends CCComp {
this.deadCD = 0
this.lastBarUpdateTime = 0
// 清掉描边材质:防 Boss 阶段描边(绿/红)随对象池节点复用串到小怪/英雄身上
const flashSprite = this.getFlashSprite();
if (flashSprite && flashSprite.defaultMaterial) {
flashSprite.orginalFlashMaterial = flashSprite.defaultMaterial;
flashSprite.sprite?.setSharedMaterial(flashSprite.defaultMaterial, 0);
}
// 清理伤害队列
this.damageQueue.length = 0;
this.isProcessingDamage = false;

View File

@@ -10,6 +10,10 @@ import { MoveComp } from "./MoveComp";
import { GameEvent } from "../common/config/GameEvent";
import { SkillTriggerType, HType } from "../common/config/heroSet";
import { SkillTriggerHelper } from "./SkillTriggerHelper";
import { BossPhaseSet } from "../common/config/BossPhaseSet";
import { BossPhaseComp } from "./BossPhaseComp";
import "./BossPhaseSystem";
import { mLogger } from "../common/Logger";
/** 怪物实体:负责怪物对象池复用、属性初始化、入场动画与回收 */
@ecs.register(`Monster`)
export class Monster extends ecs.Entity {
@@ -122,6 +126,10 @@ export class Monster extends ecs.Entity {
}
}
// 阶段组件显式移除:触发 reset 清空状态并回组件池(ECS 组件无自动销毁钩子)
if (this.has(BossPhaseComp)) {
this.remove(BossPhaseComp);
}
// 手动移除组件,避免 ecs 引用滞留
this.remove(HeroViewComp);
this.remove(HeroAttrsComp);
@@ -134,11 +142,11 @@ export class Monster extends ecs.Entity {
* 2) 初始化表现、属性、技能与阵营
* 3) 播放下落入场并在落地后启用碰撞与移动
*/
load(pos: Vec3 = Vec3.ZERO, scale: number = 1, uuid: number = 1001, is_boss: boolean = false, dropToY: number = pos.y, mon_lv: number = 1, laneIndex: number = 0) {
load(pos: Vec3 = Vec3.ZERO, scale: number = 1, uuid: number = 1001, is_boss: boolean = false, dropToY: number = pos.y, mon_lv: number = 1, laneIndex: number = 0, phase_plan_id: number = 0) {
// 怪物默认朝左,表现缩放固定为负向
scale = -1
// 当前怪物尺寸固定,保留变量便于后续扩展
let size = 1
// Boss 体型放大 1.5 倍,小怪保持原尺寸
let size = is_boss ? 1.5 : 1
var scene = smc.map.MapView.scene;
// 根据配置读取怪物预制体路径
var path = "game/heros/" + HeroInfo[uuid].path;
@@ -264,6 +272,21 @@ export class Monster extends ecs.Entity {
// 缓存技能射程等派生数据,减少战斗帧内重复计算
model.updateSkillDistanceCache();
// Boss 阶段初始化:P1 = 基线技能,仅挂标记组件并记录方案(方案缺失则退化为单阶段 Boss)
if (is_boss && phase_plan_id > 0) {
const plan = BossPhaseSet[phase_plan_id];
if (plan) {
const phase = this.add(BossPhaseComp);
phase.plan_id = phase_plan_id;
phase.index = 0;
// 分诊日志:确认组件挂载成功(区分"未挂载"与"系统未接管"两类故障)
mLogger.log(true, "BossPhase", `${model.hero_name} 挂载阶段方案 ${phase_plan_id}(P1 起点)`);
} else {
// 配置兜底:引用了不存在的方案,退化为单阶段并告警(不阻断战斗)
mLogger.warn(true, "BossPhase", `阶段方案 ${phase_plan_id} 未在 BossPhaseSet 中定义,Boss 按单阶段战斗`);
}
}
// 注册视图组件并重置对象池复用状态
this.add(view);
view.init();
@@ -356,6 +379,9 @@ export class Monster extends ecs.Entity {
/** 重置入口:复用 destroy 的释放流程 */
reset() {
if (this.has(BossPhaseComp)) {
this.remove(BossPhaseComp);
}
super.destroy();
}

View File

@@ -7,7 +7,7 @@ export class FlashSprite extends Component {
@property(Material)
hitFlashMaterial: Material | null = null;
@property({ type: Material, tooltip: '绿色描边材质(2级)' })
@property({ type: Material, tooltip: '绿色描边材质(2级 / Boss阶段2)' })
outlineMatGreen: Material | null = null;
@property({ type: Material, tooltip: '蓝色描边材质(3级)' })
@@ -19,6 +19,9 @@ export class FlashSprite extends Component {
@property({ type: Material, tooltip: '黄色描边材质(5级)' })
outlineMatYellow: Material | null = null;
@property({ type: Material, tooltip: '红色描边材质(Boss阶段3,即 outline-glow)' })
outlineMatGlow: Material | null = null;
orginalFlashMaterial: Material | null = null;
defaultMaterial: Material | null = null; // 缓存最原始的无描边材质
sprite: Sprite | null = null;
@@ -64,6 +67,34 @@ export class FlashSprite extends Component {
}
}
/**
* 按 Boss 阶段设置描边材质(优先级高于等级描边)
* Why: Boss 阶段是战斗强反馈(P2 绿=警告 / P3 红=危险),与英雄等级成长是两套视觉语言,
* 用独立通道避免污染 setOutlineByLevel 的等级语义;材质复用现有 outline-green / outline-glow
* @param phase 阶段索引:1=P2 绿 / 2=P3 红 / 0=P1 恢复默认
*/
public setOutlineByBossPhase(phase: number) {
if (!this.sprite) {
this.sprite = this.node.getComponent(Sprite);
}
if (!this.sprite) return;
let targetMat: Material | null = null;
if (phase === 1) {
targetMat = this.outlineMatGreen;
} else if (phase >= 2) {
targetMat = this.outlineMatGlow ?? this.outlineMatYellow;
} else {
targetMat = this.defaultMaterial;
}
if (targetMat) {
// 更新基础材质,确保闪白结束后恢复到这个阶段描边材质
this.orginalFlashMaterial = targetMat;
this.sprite.setSharedMaterial(targetMat, 0);
}
}
public clickFlash() {
if (!this.sprite || !this.hitFlashMaterial) return;

View File

@@ -582,12 +582,15 @@ export class MissionComp extends CCComp {
private autoNextPhase() {
switch (this.currentPhase) {
case MissionPhase.PrepareStart:
// 节间过场倒计时结束:推进到下一节、停背景滚动、派发 SectionStart 刷怪,进入 PrepareEnd 自动开战
// 节间过场倒计时结束:停背景滚动、英雄归位转待机,进入 PrepareEnd 缓冲
this.stopWaveCountdown();
this.advanceToNextSection();
this.changePhase(MissionPhase.PrepareEnd);
break;
case MissionPhase.PrepareEnd:
// 先生成下一回合怪物再进 BattleStart:此时英雄已待机约 1s,
// 怪物随"第 X 回合开始"横幅从右侧跑入,避免与英雄过场行军同帧移动;
// SectionStart 同步派发 NewWave,保证 changePhase(BattleStart) 的 isBossWave 就绪
this.advanceToNextSection();
this.changePhase(MissionPhase.BattleStart);
break;
case MissionPhase.BattleStart:
@@ -854,7 +857,10 @@ export class MissionComp extends CCComp {
* 2. 不清理英雄/技能实体、不重置局内数据、不重新三选一——英雄保留并自动归位(编队排队)。
* 3. 进度实时镜像到 HUD,但不写持久存档(存档只在整关通关时推进到下一关)。
*
* 调用时机:PrepareStart 过场倒计时结束(autoNextPhase)。
* 调用时机:PrepareEnd 结束、进入 BattleStart 前(autoNextPhase)。
* Why: 过场倒计时(PrepareStart)结束时英雄刚归位转待机,若同帧刷怪会出现
* 怪物与英雄一起移动的表现;推迟到 PrepareEnd 结束刷怪,英雄已待机约 1s,
* 怪物从右侧跑入与"第 X 回合开始"横幅同步登场。
* 保证:仅 1~4 回合清场后触发,末回合(BOSS_SECTION)已走 open_Victory 结算,不会推进。
*/
private advanceToNextSection() {

View File

@@ -217,11 +217,10 @@ export class MissionMonCompComp extends CCComp {
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
// 技能套装注入:通过 _testSkills 通道传递给 Mon.load()
if (monData.skills) {
(mon as any)._testSkills = monData.skills;
}
// Why 无条件赋值:实体池复用同一 JS 对象,条件赋值会让复用为小怪的实体残留上一个 Boss 的技能
(mon as any)._testSkills = monData.skills ?? undefined;
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monData.lv, posIndex);
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monData.lv, posIndex, monData.phase_plan_id ?? 0);
// oops.message 全局事件总线:Boss 登场(震屏/音效由表现层消费)
if (monData.isBoss) {

View File

@@ -22,6 +22,7 @@
import { HeroInfo, HType, MonType, TriggerGrouped, UltCdLv, UltCdSet } from "../common/config/heroSet";
import { SkillOverrides, SkillSet } from "../common/config/SkillSet";
import { BossPhaseSet, validateBossPhaseSet } from "../common/config/BossPhaseSet";
import { FacSet } from "../common/config/GameSet";
import {
SectionType, SectionConfig, getSectionConfig, calcMonsterAttrs,
@@ -185,6 +186,22 @@ export function validateRogueConfig(): string[] {
}
}
// 校验 Boss 阶段方案表合法性(恰 2 切换点 / trigger 条件 / hp_pct 前缀与递减 / 技能引用)
for (const err of validateBossPhaseSet()) {
errors.push(err);
}
// 校验 heroInfo.phase_ids 引用的方案存在
for (const key in HeroInfo) {
const info = HeroInfo[key];
if (!info.phase_ids) continue;
for (const pid of info.phase_ids) {
if (!BossPhaseSet[pid]) {
errors.push(`HeroInfo[${key}] phase_ids 引用的方案 ${pid} 未在 BossPhaseSet 中定义`);
}
}
}
return errors;
}
@@ -206,6 +223,8 @@ export interface GeneratedMonster {
batch: number;
/** 延迟刷出时间(秒),0 表示立即 */
delay?: number;
/** Boss 阶段方案 id(引用 BossPhaseSet,由 makeBoss 从 heroInfo.phase_ids 随机取一;未配置为 undefined) */
phase_plan_id?: number;
/**
* 怪物技能套装(技能注入通道,与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp)
@@ -230,12 +249,11 @@ export interface GeneratedMonster {
export const TestModeConfig = {
enable: false, // 默认关闭测试模式
baseHp: 150,
baseHp: 5000,
baseAp: 12,
monType: MonType.Melee,
monUuid: 6001,
monType: MonType.MeleeBoss,
monUuid: 6101,
spawnCount: 1,
skill: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides } | undefined,
/** 测试模式注入的大招列表(追加,不替换普攻) */
ults: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides }[] | undefined,
@@ -243,6 +261,11 @@ export const TestModeConfig = {
atked: undefined as TriggerGrouped | undefined,
dead: undefined as TriggerGrouped | undefined,
fstart: undefined as TriggerGrouped | undefined,
/** 测试模式:Boss 阶段方案 id(0 = 不启用阶段;需配合 isBoss: true 才会挂载组件) */
phasePlanId: 10001,
/** 测试模式:是否以 Boss 身份刷出(影响层级/金币/阶段组件挂载) */
isBoss: true,
};
// ======================== 8. 生成引擎 ========================
@@ -366,10 +389,11 @@ export class StageSpawningEngine {
type: TestModeConfig.monType,
hp: TestModeConfig.baseHp,
ap: TestModeConfig.baseAp,
isBoss: false,
isBoss: TestModeConfig.isBoss,
lv: 1,
spawnIndex: i,
batch: 0,
phase_plan_id: TestModeConfig.phasePlanId || undefined,
skills: {
skill: TestModeConfig.skill,
ults: TestModeConfig.ults,
@@ -408,6 +432,7 @@ export class StageSpawningEngine {
skills: {
ults: this.pickBossUlts(range, stage),
},
phase_plan_id: this.pickPhasePlan(baseInfo),
};
}
@@ -433,6 +458,13 @@ export class StageSpawningEngine {
return uuids[Math.floor(Math.random() * uuids.length)];
}
/** 从 Boss 允许的阶段方案中随机取一(未配置返回 undefined,Boss 退化为单阶段) */
private pickPhasePlan(baseInfo?: { phase_ids?: number[] }): number | undefined {
const ids = baseInfo?.phase_ids;
if (!ids || ids.length === 0) return undefined;
return ids[Math.floor(Math.random() * ids.length)];
}
/**
* 按定位抽取 Boss 大招(不重复),数量由关卡进度决定(getBossUltCount)
* @returns 大招配置列表;池空时返回 undefined

View File

@@ -0,0 +1,797 @@
# Boss 多阶段战斗系统实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为 Boss 实现固定 3 阶段(P1/P2/P3)战斗模式:混合触发(血量阈值 OR 在场时长)、增量技能轮换(adds/removes)、属性修正,表现层留空钩子。
**Architecture:** 增量驱动式——配置层 `BossPhaseSet`(10000 段 id)声明切换点,`heroInfo.phase_ids` 声明 Boss 允许的方案,`RogueConfig.makeBoss` 随机取一注入 `GeneratedMonster.phase_plan_id`,`Mon.load` 按需挂载 `BossPhaseComp` 标记组件,`BossPhaseSystem`(独立 ECS 系统)检测条件并增量改写 `model.skills`。`SCastSystem`/`HeroAtkSystem` 零改动。
**Tech Stack:** Cocos Creator 3.x + TypeScript 严格模式 + oops-framework ECS + `oops.message` 事件总线
**规格文档:** `docs/superpowers/specs/2026-10-06-boss-phase-system-design.md`
**验证方式:** 项目无单元测试框架,每任务完成后在 Cocos Creator 编辑器中触发编译检查控制台无红色报错;Task 9 做运行时验证。
**已知约束(来自审查,实施时必须遵守):**
- `ComblockSystem.update` 真实签名为 `update(e: ecs.Entity)`,dt 从 `this.dt` 取,框架自动遍历
- removes/adds 绝不操作普攻首键(`buildSkillCandidates` 硬假设首键=普攻)
- cd 强制兜底 8s(防 `ccd = Math.min(undefined,...) = NaN` 导致大招永不释放)
- 血量阈值每帧实时算 `hp/hp_max*100`,禁止缓存绝对值(hp_max 在 load 后被 MissionMonComp 覆盖)
- `heroSet.ts` 工作区有用户未提交修改,只做精准行内插入,勿动其他行
---
### Task 1: 新建 BossPhaseSet.ts(配置 + 类型 + 校验)
**Files:**
- Create: `assets/script/game/common/config/BossPhaseSet.ts`
- [ ] **Step 1: 创建配置文件**
```ts
/**
* @file BossPhaseSet.ts
* @description Boss 多阶段战斗方案配置(固定 3 阶段:P1 基线 / P2 / P3)
*
* 数据流:heroInfo.phase_ids(允许方案)→ RogueConfig.makeBoss 随机取一
* → Mon.load 第 8 参 → BossPhaseComp → BossPhaseSystem 检测切换
*
* 设计约束:
* - 方案 id 使用 10000 段(9100 段已被 TalentSet 9101-9199 / ICardSet 9101~9110 占用)
* - 增量驱动:切换 = removes 移除 + adds 注入,P1 = 基线技能(heroInfo.skills + RogueConfig 大招)无需配置
* - removes/adds 禁止操作普攻首键(SCastSystem.buildSkillCandidates 硬假设首键=普攻垫底;
* JS delete+set 会把键移到末尾,破坏普攻垫底契约)
* - 混合触发:同一切换点 hp_pct / time 任一满足即切换;
* hp_pct 切换点必须构成前缀(time-only 点之后禁止再配 hp_pct,防乱序连切)
*/
import { SkillSet, SkillOverrides } from "./SkillSet";
/** 阶段切换触发条件(混合触发:任一条件满足即切换) */
export interface PhaseTrigger {
/** 血量百分比阈值(1~99):Boss 血量比例 ≤ 该值时触发 */
hp_pct?: number;
/** Boss 在场战斗秒数:超过该时长触发 */
time?: number;
}
/** 切换瞬间一次性属性修正(基于当前值链式乘算,P3 基于 P2 修正后的值) */
export interface PhaseAttrMod {
/** 攻击 ±% */
ap_pct?: number;
/** 防御 ±%(applyDefense 除法递减模型,收益非线性,配值按公式校验) */
defense_pct?: number;
}
/** 注入技能条目(与 RogueConfig 大招条目同构,cd 缺省由系统兜底 8s) */
export interface PhaseSkillEntry {
s_uuid: number;
cd?: number;
overrides?: SkillOverrides;
}
/** 单个切换点定义(switches[0] = P1→P2,switches[1] = P2→P3) */
export interface PhaseSwitchDef {
trigger: PhaseTrigger;
/** 注入的技能(追加进 skills 表,SCastSystem 按 CD 轮转)。禁止操作普攻首键 */
adds?: PhaseSkillEntry[];
/** 移除的技能 s_uuid 列表(含 RogueConfig 注入的大招)。禁止操作普攻首键 */
removes?: number[];
attr_mod?: PhaseAttrMod;
}
/** 阶段方案:固定 2 个切换点 = 3 阶段,P1 为基线 */
export interface BossPhasePlan {
name: string;
switches: [PhaseSwitchDef, PhaseSwitchDef];
}
/**
* 阶段方案表(id 段 10000)
* 示例方案 10001 挂在 6101(兽人首领-双刀dps型)的 phase_ids 上;
* 大招 s_uuid 引用 RogueConfig.BossUltPools 近战池(6100~6104)保证可达
*/
export const BossPhaseSet: Record<number, BossPhasePlan> = {
10001: {
name: "狂暴进化",
switches: [
{ trigger: { hp_pct: 70 }, adds: [{ s_uuid: 6102, overrides: { ap: 180 } }], removes: [6100] },
{ trigger: { hp_pct: 40, time: 60 }, adds: [{ s_uuid: 6103, overrides: { ap: 180 } }], attr_mod: { ap_pct: 20 } },
],
},
};
/**
* 校验阶段方案表合法性(供 validateRogueConfig 追加调用,建议启动期执行一次)
* @returns 错误信息数组,空数组表示校验通过
*/
export function validateBossPhaseSet(): string[] {
const errors: string[] = [];
for (const key in BossPhaseSet) {
const plan = BossPhaseSet[key];
const tag = `BossPhaseSet[${key}](${plan.name})`;
if (plan.switches.length !== 2) {
errors.push(`${tag} 切换点必须恰为 2 个(固定 3 阶段),实际 ${plan.switches.length}`);
continue;
}
let seenTimeOnly = false;
let lastHpPct = 101;
let lastTime = 0;
plan.switches.forEach((def, i) => {
const t = def.trigger;
const stag = `${tag} switches[${i}]`;
// 条件完备性与取值范围
if (t.hp_pct === undefined && t.time === undefined) {
errors.push(`${stag} trigger 至少需要 hp_pct 或 time 之一`);
return;
}
if (t.hp_pct !== undefined && (t.hp_pct <= 0 || t.hp_pct >= 100)) {
errors.push(`${stag} hp_pct 必须在 (0,100) 区间,实际 ${t.hp_pct}`);
}
if (t.time !== undefined && t.time <= 0) {
errors.push(`${stag} time 必须 > 0,实际 ${t.time}`);
}
// hp_pct 前缀规则:time-only 点之后禁止再配 hp_pct(防"P2 time / P3 hp_pct"乱序连切)
if (seenTimeOnly && t.hp_pct !== undefined) {
errors.push(`${stag} hp_pct 切换点必须构成前缀:time-only 切换点之后禁止再配置 hp_pct`);
}
if (t.hp_pct === undefined && t.time !== undefined) {
seenTimeOnly = true;
}
// hp_pct 严格递减 / time 严格递增
if (t.hp_pct !== undefined) {
if (t.hp_pct >= lastHpPct) {
errors.push(`${stag} hp_pct=${t.hp_pct} 未严格小于前一档 ${lastHpPct}`);
}
lastHpPct = t.hp_pct;
}
if (t.time !== undefined) {
if (t.time <= lastTime) {
errors.push(`${stag} time=${t.time} 未严格大于前一档 ${lastTime}`);
}
lastTime = t.time;
}
// 技能引用存在性(error 级)
for (const add of def.adds ?? []) {
if (!add.s_uuid || !SkillSet[add.s_uuid]) {
errors.push(`${stag} adds 技能 ${add.s_uuid} 未在 SkillSet 中定义`);
}
}
for (const s_uuid of def.removes ?? []) {
if (!SkillSet[s_uuid]) {
errors.push(`${stag} removes 技能 ${s_uuid} 未在 SkillSet 中定义`);
}
}
});
}
return errors;
}
```
- [ ] **Step 2: 编译验证**
在 Cocos Creator 编辑器中保存文件触发编译(或等编辑器自动编译),检查控制台无红色报错。
重点确认:`SkillSet` 与 `SkillOverrides` 均从 `./SkillSet` 导出(RogueConfig.ts L24 已有同款 import 先例)。
- [ ] **Step 3: Commit**
```bash
git add assets/script/game/common/config/BossPhaseSet.ts
git commit -m "feat: add BossPhaseSet config with phase plan schema"
```
---
### Task 2: heroSet.ts 加 phase_ids 字段与示例配置
**Files:**
- Modify: `assets/script/game/common/config/heroSet.ts:432`(接口字段,accuracy 之后)
- Modify: `assets/script/game/common/config/heroSet.ts:1095`(6101 配置行尾)
- [ ] **Step 1: heroInfo 接口新增字段**
在 L432 `accuracy?: number;` 行之后、L433 `skills: Record<number, HSkillInfo>;` 之前插入:
```ts
phase_ids?: number[]; // Boss 专属:允许的阶段方案 id 列表(引用 BossPhaseSet,生成时随机取一)
```
- [ ] **Step 2: 6101 配置挂载示例方案**
在 L1095(`uuid: 6101, ... speed: 120,` 行)的 `speed: 120,` 之后、行尾前追加:
```ts
phase_ids: [10001],
```
即该行变为(仅尾部变化,其余内容原样保留):
```ts
uuid: 6101, name: "兽人首领-双刀dps型", path: "mb1", fac: FacSet.MON, lv: 6, rarity: 4, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, monType: MonType.MeleeBoss, hp: HERO_ROLE_BASE[HRole.Warrior].hp, ap: HERO_ROLE_BASE[HRole.Warrior].ap, def: HERO_ROLE_BASE[HRole.Warrior].def, speed: 120, phase_ids: [10001],
```
注意:该文件工作区有用户未提交修改,只做上述两处精准插入,不改动任何其他行。
- [ ] **Step 3: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。
- [ ] **Step 4: Commit**
```bash
git add assets/script/game/common/config/heroSet.ts
git commit -m "feat: add phase_ids field to boss hero config"
```
---
### Task 3: GameEvent.ts 加 BossPhaseChange 枚举
**Files:**
- Modify: `assets/script/game/common/config/GameEvent.ts:118`(RestartMission 之后)
- [ ] **Step 1: 新增枚举项**
在 L118 `RestartMission = "RestartMission",` 之后、枚举闭合 `}` 之前插入:
```ts
/** Boss 阶段切换(payload: { eid, planId, fromPhase, toPhase },表现层监听此事件实现演出;eid 定位实体,勿持裸引用) */
BossPhaseChange = "BossPhaseChange",
```
- [ ] **Step 2: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。
- [ ] **Step 3: Commit**
```bash
git add assets/script/game/common/config/GameEvent.ts
git commit -m "feat: add BossPhaseChange game event"
```
---
### Task 4: 新建 BossPhaseComp.ts(标记组件)
**Files:**
- Create: `assets/script/game/hero/BossPhaseComp.ts`
- [ ] **Step 1: 创建组件文件**
```ts
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
/**
* Boss 阶段标记组件:挂载即表示该实体参与多阶段状态机
* 由 Mon.load 按 phase_plan_id 条件挂载,Monster.destroy 显式 remove(触发 reset + 回组件池)
* 计时字段 elapsed 随 reset 天然归零,无全局战斗计时状态
*/
@ecs.register('BossPhase')
export class BossPhaseComp extends ecs.Comp {
/** 生效的阶段方案 id(BossPhaseSet key) */
plan_id: number = 0;
/** 当前阶段索引:0=P1 / 1=P2 / 2=P3(P3 后系统不再检测) */
index: number = 0;
/** Boss 在场战斗累计时长(秒),time 触发条件基准 */
elapsed: number = 0;
reset() {
this.plan_id = 0;
this.index = 0;
this.elapsed = 0;
}
}
```
- [ ] **Step 2: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。重点确认 `ecs.Comp` 的 `reset()` 为抽象方法必须实现(与 HeroAttrsComp 同款模式)。
- [ ] **Step 3: Commit**
```bash
git add assets/script/game/hero/BossPhaseComp.ts
git commit -m "feat: add BossPhaseComp marker component"
```
---
### Task 5: Mon.ts 接入(load 第 8 参 + 组件挂载/移除)
**Files:**
- Modify: `assets/script/game/hero/Mon.ts:1`(import 区)
- Modify: `assets/script/game/hero/Mon.ts:137`(load 签名)
- Modify: `assets/script/game/hero/Mon.ts:265`(挂载点,updateSkillDistanceCache 之后)
- Modify: `assets/script/game/hero/Mon.ts:126`(destroy 移除)
- Modify: `assets/script/game/hero/Mon.ts:358`(reset 对齐)
- [ ] **Step 1: 新增 import**
在 L12 `import { SkillTriggerHelper } from "./SkillTriggerHelper";` 之后插入:
```ts
import { BossPhaseSet } from "../common/config/BossPhaseSet";
import { BossPhaseComp } from "./BossPhaseComp";
```
- [ ] **Step 2: load 签名追加第 8 参**
L137 改为(仅尾部追加 `phase_plan_id: number = 0`):
```ts
load(pos: Vec3 = Vec3.ZERO, scale: number = 1, uuid: number = 1001, is_boss: boolean = false, dropToY: number = pos.y, mon_lv: number = 1, laneIndex: number = 0, phase_plan_id: number = 0) {
```
- [ ] **Step 3: 技能表构建完成后挂载组件**
在 L265 `model.updateSkillDistanceCache();` 之后、L267 `// 注册视图组件并重置对象池复用状态` 注释之前插入:
```ts
// Boss 阶段初始化:P1 = 基线技能,仅挂标记组件并记录方案(方案缺失则退化为单阶段 Boss)
if (is_boss && phase_plan_id > 0) {
const plan = BossPhaseSet[phase_plan_id];
if (plan) {
const phase = this.add(BossPhaseComp);
phase.plan_id = phase_plan_id;
phase.index = 0;
} else {
// 配置兜底:引用了不存在的方案,退化为单阶段并告警(不阻断战斗)
oops.log.warn(`[BossPhase] 阶段方案 ${phase_plan_id} 未在 BossPhaseSet 中定义,Boss 按单阶段战斗`);
}
}
```
- [ ] **Step 4: destroy 显式移除组件(触发 reset + 回组件池)**
在 L126 `this.remove(HeroViewComp);` 之前插入(放 HeroAttrsComp 移除前,便于将来清理逻辑读属性):
```ts
// 阶段组件显式移除:触发 reset 清空状态并回组件池(ECS 组件无自动销毁钩子)
if (this.has(BossPhaseComp)) {
this.remove(BossPhaseComp);
}
```
- [ ] **Step 5: reset 与 destroy 对齐(防旁路残留)**
L358-360 改为:
```ts
/** 重置入口:复用 destroy 的释放流程 */
reset() {
if (this.has(BossPhaseComp)) {
this.remove(BossPhaseComp);
}
super.destroy();
}
```
- [ ] **Step 6: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。此时 BossPhaseSystem 尚未创建,组件挂载后无消费方,属预期中间态。
- [ ] **Step 7: Commit**
```bash
git add assets/script/game/hero/Mon.ts
git commit -m "feat: wire BossPhaseComp into monster load/destroy"
```
---
### Task 6: 新建 BossPhaseSystem.ts(检测与切换系统)
**Files:**
- Create: `assets/script/game/hero/BossPhaseSystem.ts`
- [ ] **Step 1: 创建系统文件**
```ts
/**
* BossPhaseSystem — Boss 多阶段战斗状态机。
*
* 检测混合触发条件(血量阈值 OR 在场时长),命中后执行阶段切换:
* removes 移除旧技能 → adds 注入新技能(cd 兜底 + 资源预热)→ attr_mod 属性修正
* → 空钩子 onPhaseExit/onPhaseEnter(演出后续迭代)→ 广播 GameEvent.BossPhaseChange
*
* Why: 独立系统符合 M/V/S 分层;filter 按 BossPhaseComp 过滤,每局 Boss 唯一,开销可忽略。
* 计时在组件 elapsed(随 reset 归零),避免系统级计数在 per-entity update 中重复累加。
*/
import { Prefab } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import { BossPhaseSet, PhaseSwitchDef } from "../common/config/BossPhaseSet";
import { SkillSet } from "../common/config/SkillSet";
import { mLogger } from "../common/Logger";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { BossPhaseComp } from "./BossPhaseComp";
/** 阶段模块调试日志开关 */
const DEBUG = true;
/** 新注入技能缺省 CD(秒),与 Mon.ts 大招注入兜底一致 */
const DEFAULT_ADD_CD = 8;
@ecs.register('BossPhaseSystem')
export class BossPhaseSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
filter(): ecs.IMatcher {
return ecs.allOf(BossPhaseComp, HeroAttrsComp);
}
update(e: ecs.Entity): void {
const phase = e.get(BossPhaseComp);
const model = e.get(HeroAttrsComp);
if (!phase || !model) return;
// 战斗状态守卫(与 MoveComp/SCastSystem 同款模式):暂停/未开战不计时
if (!smc.mission.play || smc.mission.pause) return;
// 死亡同帧守卫:防死尸换阶段
if (model.is_dead) return;
// P3 后不再检测(固定 3 阶段)
if (phase.index >= 2) return;
const plan = BossPhaseSet[phase.plan_id];
if (!plan) return;
phase.elapsed += this.dt;
const def = plan.switches[phase.index];
// 混合触发:任一条件满足即切换(血量实时计算,禁止缓存绝对值——hp_max 在 load 后被外部覆盖)
const hpPct = model.hp / model.hp_max * 100;
const hpHit = def.trigger.hp_pct !== undefined && hpPct <= def.trigger.hp_pct;
const timeHit = def.trigger.time !== undefined && phase.elapsed >= def.trigger.time;
if (hpHit || timeHit) {
this.executePhaseSwitch(e, def);
}
}
/**
* 执行阶段切换:技能增量改写 + 属性修正 + 钩子与事件广播
* Why 改 skills 而非施法系统过滤:与 _testSkills/RogueConfig.ults 动态注入同模式,
* SCastSystem 零改动;施法闭包捕获 SkillSet 静态配置,进行中施法不受影响
*/
private executePhaseSwitch(e: ecs.Entity, def: PhaseSwitchDef): void {
const phase = e.get(BossPhaseComp)!;
const model = e.get(HeroAttrsComp)!;
const from = phase.index;
this.onPhaseExit(e);
// 1. 移除旧技能(仅大招键;普攻首键受键序契约保护,见 BossPhaseSet 文件头注释)
if (def.removes) {
for (const s_uuid of def.removes) {
delete model.skills[s_uuid];
}
}
// 2. 注入新技能(cd 强制兜底,防 undefined → Math.min 结果 NaN 导致大招永不释放)
if (def.adds) {
for (const add of def.adds) {
if (!add || !add.s_uuid || !SkillSet[add.s_uuid]) continue;
model.skills[add.s_uuid] = {
uuid: add.s_uuid,
lv: 1,
cd: add.cd ?? DEFAULT_ADD_CD,
ccd: 0,
overrides: add.overrides,
};
// 资源预热:新技能 prefab 首次异步加载会丢一发,切换时预载(路径与 Skill.load 对齐)
oops.res.load(`game/skill/atk/${SkillSet[add.s_uuid].sp_name}`, Prefab);
}
}
// 3. 属性修正:基于当前值链式乘算(P3 基于 P2 修正后的值,配置按此口径)
const mod = def.attr_mod;
if (mod) {
if (mod.ap_pct) model.ap = Math.round(model.ap * (1 + mod.ap_pct / 100));
if (mod.defense_pct) model.defense = Math.round(model.defense * (1 + mod.defense_pct / 100));
}
phase.index++;
this.onPhaseEnter(e);
mLogger.log(DEBUG, "BossPhase", `${model.hero_name} 阶段切换 P${from + 1}→P${phase.index + 1}(方案 ${phase.plan_id})`);
// 广播阶段切换(payload 用 eid 定位实体,防监听方持有已回收实体裸引用)
oops.message.dispatchEvent(GameEvent.BossPhaseChange, {
eid: e.eid,
planId: phase.plan_id,
fromPhase: from,
toPhase: phase.index,
});
}
/** TODO: 阶段退场表现(震屏/变暗等),后续迭代实现 */
private onPhaseExit(e: ecs.Entity): void { }
/** TODO: 阶段入场表现(怒吼/无敌帧/技能横幅等),后续迭代实现 */
private onPhaseEnter(e: ecs.Entity): void { }
}
```
- [ ] **Step 2: 确认系统注册链可达**
`@ecs.register` 装饰器在模块加载时自动注册系统,**唯一要求是模块被 import**。
验证:Task 5 已在 `Mon.ts` import `BossPhaseComp`,但那是组件文件;系统文件需单独确认——
在 `Mon.ts` 的 import 区追加一行(放在 `import { BossPhaseComp } from "./BossPhaseComp";` 之后):
```ts
import "./BossPhaseSystem";
```
(副作用 import 确保系统模块被加载注册;与框架"装饰器注册即生效"约定一致。)
- [ ] **Step 3: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。
重点确认:`update(e: ecs.Entity)` 签名(框架无 dt 参数)、`this.dt` 可用、`mLogger.log(DEBUG, tag, msg)` 签名与 BuffSystem.ts L39 一致、`e.eid` 属性存在(Mon.ts L120 已有先例)。
- [ ] **Step 4: Commit**
```bash
git add assets/script/game/hero/BossPhaseSystem.ts assets/script/game/hero/Mon.ts
git commit -m "feat: add BossPhaseSystem with phase switch logic"
```
---
### Task 7: RogueConfig.ts 消费阶段方案
**Files:**
- Modify: `assets/script/game/map/RogueConfig.ts:23`(import 区)
- Modify: `assets/script/game/map/RogueConfig.ts:207`(GeneratedMonster 接口)
- Modify: `assets/script/game/map/RogueConfig.ts:390-412`(makeBoss)
- Modify: `assets/script/game/map/RogueConfig.ts:167-189`(validateRogueConfig)
- Modify: `assets/script/game/map/RogueConfig.ts:231-246`(TestModeConfig)
- Modify: `assets/script/game/map/RogueConfig.ts:359-384`(generateTestWave)
- [ ] **Step 1: 新增 import**
在 L24 `import { SkillOverrides, SkillSet } from "../common/config/SkillSet";` 之后插入:
```ts
import { BossPhaseSet, validateBossPhaseSet } from "../common/config/BossPhaseSet";
```
- [ ] **Step 2: GeneratedMonster 接口加字段**
在 L208 `delay?: number;` 之后、L210 `/** 本怪所属批次...` 注释之前插入:
```ts
/** Boss 阶段方案 id(引用 BossPhaseSet,由 makeBoss 从 heroInfo.phase_ids 随机取一;未配置为 undefined) */
phase_plan_id?: number;
```
- [ ] **Step 3: makeBoss 读取消费**
L399-411 的 return 对象,在 `skills: { ults: this.pickBossUlts(range, stage) },` 之后追加一行属性:
```ts
phase_plan_id: this.pickPhasePlan(baseInfo),
```
并在 `pickBossUuid` 方法(L428-434)之后新增私有方法:
```ts
/** 从 Boss 允许的阶段方案中随机取一(未配置返回 undefined,Boss 退化为单阶段) */
private pickPhasePlan(baseInfo?: { phase_ids?: number[] }): number | undefined {
const ids = baseInfo?.phase_ids;
if (!ids || ids.length === 0) return undefined;
return ids[Math.floor(Math.random() * ids.length)];
}
```
- [ ] **Step 4: validateRogueConfig 追加校验**
在 L186 `return errors;` 之前插入:
```ts
// 校验 Boss 阶段方案表合法性(恰 2 切换点 / trigger 条件 / hp_pct 前缀与递减 / 技能引用)
for (const err of validateBossPhaseSet()) {
errors.push(err);
}
// 校验 heroInfo.phase_ids 引用的方案存在
for (const key in HeroInfo) {
const info = HeroInfo[key];
if (!info.phase_ids) continue;
for (const pid of info.phase_ids) {
if (!BossPhaseSet[pid]) {
errors.push(`HeroInfo[${key}] phase_ids 引用的方案 ${pid} 未在 BossPhaseSet 中定义`);
}
}
}
```
- [ ] **Step 5: TestModeConfig 加调试开关**
在 L244 `fstart: undefined as TriggerGrouped | undefined,` 之后追加:
```ts
/** 测试模式:Boss 阶段方案 id(0 = 不启用阶段;需配合 isBoss: true 才会挂载组件) */
phasePlanId: 0 as number,
/** 测试模式:是否以 Boss 身份刷出(影响层级/金币/阶段组件挂载) */
isBoss: false,
```
- [ ] **Step 6: generateTestWave 接通调试开关**
L364-381 的 monsters.push 对象改为(isBoss 与 phase_plan_id 两处变化,其余原样):
```ts
monsters.push({
uuid: TestModeConfig.monUuid,
type: TestModeConfig.monType,
hp: TestModeConfig.baseHp,
ap: TestModeConfig.baseAp,
isBoss: TestModeConfig.isBoss,
lv: 1,
spawnIndex: i,
batch: 0,
phase_plan_id: TestModeConfig.phasePlanId || undefined,
skills: {
skill: TestModeConfig.skill,
ults: TestModeConfig.ults,
atking: TestModeConfig.atking,
atked: TestModeConfig.atked,
dead: TestModeConfig.dead,
fstart: TestModeConfig.fstart,
},
});
```
- [ ] **Step 7: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。
- [ ] **Step 8: Commit**
```bash
git add assets/script/game/map/RogueConfig.ts
git commit -m "feat: consume phase plans in boss generation"
```
---
### Task 8: MissionMonComp.ts 接线 + _testSkills 残留修复
**Files:**
- Modify: `assets/script/game/map/MissionMonComp.ts:219-222`(_testSkills 赋值)
- Modify: `assets/script/game/map/MissionMonComp.ts:224`(load 调用)
- [ ] **Step 1: 修复 _testSkills 池化残留(现存 bug)**
L219-222 原代码:
```ts
// 技能套装注入:通过 _testSkills 通道传递给 Mon.load()
if (monData.skills) {
(mon as any)._testSkills = monData.skills;
}
```
改为(无条件赋值,Boss 死亡回池复用为小怪时不再继承上一个 Boss 的大招):
```ts
// 技能套装注入:通过 _testSkills 通道传递给 Mon.load()
// Why 无条件赋值:实体池复用同一 JS 对象,条件赋值会让复用为小怪的实体残留上一个 Boss 的技能
(mon as any)._testSkills = monData.skills ?? undefined;
```
- [ ] **Step 2: load 调用追加第 8 实参**
L224 改为:
```ts
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monData.lv, posIndex, monData.phase_plan_id ?? 0);
```
- [ ] **Step 3: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。
- [ ] **Step 4: Commit**
```bash
git add assets/script/game/map/MissionMonComp.ts
git commit -m "fix: pass phase plan id and clear stale test skills on reuse"
```
---
### Task 9: 运行时验证(编辑器内)
**Files:**
- 无代码改动(临时改 TestModeConfig 验证后还原)
- [ ] **Step 1: 配置测试模式**
`RogueConfig.ts` TestModeConfig 临时改为:
```ts
export const TestModeConfig = {
enable: true,
baseHp: 1000,
baseAp: 5,
monType: MonType.MeleeBoss,
monUuid: 6101,
spawnCount: 1,
phasePlanId: 10001,
isBoss: true,
// 其余字段保持原样(skill/ults/atking/atked/dead/fstart 均为 undefined)
```
(baseAp 调低避免过快击杀 Boss;baseHp 适中便于打到 70%/40% 阈值。)
- [ ] **Step 2: 运行验证阶段切换**
Cocos Creator 编辑器运行游戏进入战斗,验证:
1. 控制台出现 `[BossPhase] 兽人首领-双刀dps型 阶段切换 P1→P2(方案 10001)`(血量打到 70% 以下时)
2. 继续输出 `P2→P3`(血量 40% 以下或 60 秒后)
3. P3 后无论血量多低不再出现切换日志
4. 切换后 Boss 普攻(6004)仍正常施放(普攻首键未被破坏)
5. 拖过 60 秒验证 time 条件独立生效(用高生存阵容)
6. P2 起 Boss 施放新注入大招 6102(裂气斩)、P3 起 6103(闪电斩),P2 后 6100(寒冰斩,若 RogueConfig 注入过)消失
- [ ] **Step 3: 验证池化复用修复**
击败 Boss 后继续推进关卡刷出普通小怪,验证小怪不再施放 Boss 大招(控制台无异常大招横幅/弹道)。
- [ ] **Step 4: 验证乱序配置拦截(规格 §7.4)**
临时在 BossPhaseSet.ts 中加一个乱序方案:
```ts
99999: {
name: "乱序测试",
switches: [
{ trigger: { time: 30 } },
{ trigger: { hp_pct: 80 } },
],
},
```
在浏览器/编辑器控制台执行 `validateBossPhaseSet()`,确认返回包含
`hp_pct 切换点必须构成前缀` 的错误信息;验证后删除该临时方案。
- [ ] **Step 5: 验证单阶段回归**
TestModeConfig 改回 `phasePlanId: 0` 或换未配置 phase_ids 的 Boss(如 monUuid: 6102),验证行为与现状一致(无阶段切换日志)。
- [ ] **Step 6: 还原测试模式并提交**
TestModeConfig 还原为 `enable: false` 及原始默认值(phasePlanId: 0, isBoss: false 保留新增字段),提交:
```bash
git add assets/script/game/map/RogueConfig.ts
git commit -m "test: verify boss phase switching and pool reuse fix"
```
若验证中修复了任何问题,将修复与验证一并提交并在 commit body 说明。
---
## 完成标准
- [ ] 6101 Boss 在 70%/40% 血量阈值或 60s 时间阈值处依次切换 P1→P2→P3
- [ ] 切换时技能增量生效(removes 消失、adds 按 CD 轮转、attr_mod 攻防修正生效)
- [ ] 普攻全程不受影响(首键契约)
- [ ] 未配置 phase_ids 的 Boss 行为与现状完全一致
- [ ] 对象池复用无状态残留(Boss→小怪不继承技能)
- [ ] 全部任务提交完成,Cocos Creator 编译无报错

View File

@@ -0,0 +1,306 @@
# Boss 阶段战斗内容设计 v1
> **定位**:本文是 Boss 阶段系统的**内容设计文档**(配什么方案、什么数值),
> 技术机制见规格文档 `docs/superpowers/specs/2026-10-06-boss-phase-system-design.md`。
> **所有设计均基于现有实现能力落地,零机制新增**;外部设计稿(像素大勇者-boss-阶段设计稿.md)
> 仅作理念借鉴,凡与现有实现冲突处一律改写(对照见 §7)。
---
## §1 设计目标
- 6 个现有 Boss 各自拥有 **2~3 个阶段方案**,生成时随机抽一(肉鸽重玩性)
- 每个方案固定 **3 阶段**:P1(基线)→ P2 → P3,两个切换点
- 两种战斗气质:**追加式**(技能池越打越大,数值膨胀)与**换招式**(整组换招,机制重组),
玩家主观强度对齐
- P1→P2 触发条件全 Boss 统一,降低玩家学习成本;P3 是高潮,阈值按 Boss 气质差异化
---
## §2 现有机制能力边界(设计必须遵守)
| 能力 | 现状 | 设计含义 |
|------|------|----------|
| 触发条件 | `hp_pct`(血量≤%)/ `time`(Boss 在场秒数),同点 OR 关系 | 混合触发可直接配 |
| 技能变更 | `adds` 注入 + `removes` 按 id 删除,`delete` 不存在的键无副作用 | **换招式 = removes 列满该定位整个大招池**(见 §3.2) |
| 属性修正 | 仅 `ap_pct` / `defense_pct`,链式乘算 | **不配 hp 修正**(实现已排除,避免血量同步歧义) |
| 技能数值 | `overrides.ap` 系数,口径同 BossUltPools(140~220) | 不引用英雄技能 lv 体系 |
| 新技能 CD | `cd` 缺省 8s,注入后 `ccd: 0` 需等满一个 CD 才首放 | P3 注入技能有 8s 真空期,阈值设计须留窗口 |
| 战斗时长 | `BATTLE_TIMEOUT = 90s`,Boss 延迟 1s 刷出 | **时间触发方案的 P3 阈值 ≤ 60s**(留 ≥28s 收尾窗口) |
| 方案校验 | 恰 2 切换点;hp_pct ∈(0,100) 严格递减且须构成前缀;time 严格递增 | 配错启动期 `validateBossPhaseSet` 报错 |
| 普攻保护 | removes/adds 禁止操作普攻首键 | 只操作大招键 |
---
## §3 通用设计规则
### 3.1 触发维度按 Boss 定位分配(借鉴外部稿注 B,映射到现有 Boss)
| Boss | 定位 | 触发维度 | 理由 |
|------|------|----------|------|
| 6101 兽人首领-双刀dps | 近战 Warrior | **血量**(P2=70% / P3=40%) | 速战速决型,血量阈值直觉明显 |
| 6102 兽人首领-坦克型 | 近战 Tank | **时间**(P2=30s / P3=60s) | 消耗战,时间触发逼玩家在窗口期内输出 |
| 6103 兽人首领-魔法师 | 远程 Mage | **时间**(P2=30s / P3=60s) | 法系读招,时间阈值给玩家"看清招式再反应"的窗口 |
| 6104 兽人首领-射手 | 远程 Archer | **血量**(P2=70% / P3=40%) | 高频弹幕,时间触发会过于喧闹 |
| 6105 亡灵首领-法师 | 远程 Mage | **时间**(P2=30s / P3=60s) | 同 6103 |
| 6106 亡灵首领-骑马战士 | 近战 Tank | **时间**(P2=30s / P3=60s) | 同 6102 |
| 6006 骷髅领主 | 近战 Tank | **血量**(P2=70% / P3=40%) | 低攻速靶子型,血量节奏更稳 |
> 时间阈值为 **Boss 在场时长**(elapsed 从出生起算,暂停不计)。30s/60s 相对 90s 战斗超时
> 留出 P3 ≥28s 的收尾窗口。
### 3.2 换招式(clear_replace)的落地技巧
实现没有"清空"开关,但 `removes` 对不存在的 id 无副作用。因此:
> **换招式 = removes 列满该 Boss 定位对应的整个大招池 id**(近战池 6100~6104 / 远程池 6200~6204 + 6150/6151),
> 无论 RogueConfig 随机注入了哪个大招、之前阶段 adds 了什么,一律清净,再 adds 新技能组。
- 近战定位池:`[6100, 6101, 6102, 6103, 6104]`
- 远程定位池:`[6150, 6151, 6200, 6201, 6202, 6203, 6204]`
### 3.3 双模式强度对齐(借鉴外部稿 Q3,改写为现有口径)
| 模式 | 新鲜感来源 | 数值手段 | 输出口径 |
|------|-----------|----------|----------|
| 追加式(keep_add) | 技能池变大、轮转更密 | `attr_mod.ap_pct` 每段 +15~25% | 技能 overrides 维持标准档(ap 150~180) |
| 换招式(clear_replace) | 招式重组、机制差异 | 新技能 `overrides.ap` 逐段上调(180→200→220) | attr_mod 少量或不配 |
两模式的 P3 理论 DPS 控制在 **±10% 以内**;换招式清掉了随机注入的大招(可能很强),
故新技能给更高系数补偿。
### 3.4 方案 id 规划(10000 段,按 Boss 分组)
| id 段 | Boss | 触发 |
|-------|------|------|
| 10001~10009 | 6101 双刀dps | 血量 |
| 10010~10019 | 6102 坦克型 | 时间 |
| 10020~10029 | 6103 魔法师 | 时间 |
| 10030~10039 | 6104 射手 | 血量 |
| 10040~10049 | 6105 亡灵法师 | 时间 |
| 10050~10059 | 6106 骑马战士 | 时间 |
| 10060~10069 | 6006 骷髅领主 | 血量 |
---
## §4 各 Boss 阶段方案(可直接抄入 BossPhaseSet.ts)
> 以下 `removes` 中的整池列表即 §3.2 的换招技巧;`cd` 缺省 8s 不另写。
### 4.1 6101 兽人首领-双刀dps(血量,近战池)
```ts
// 10001 狂暴进化(已有):P2 换裂气斩、P3 加闪电斩 + 攻+20%
10001: {
name: "狂暴进化",
switches: [
{ trigger: { hp_pct: 70 }, adds: [{ s_uuid: 6102, overrides: { ap: 180 } }], removes: [6100] },
{ trigger: { hp_pct: 40, time: 60 }, adds: [{ s_uuid: 6103, overrides: { ap: 180 } }], attr_mod: { ap_pct: 20 } },
],
},
// 10002 嗜血连斩(追加式):双刀越挥越快,技能只加不换
10002: {
name: "嗜血连斩",
switches: [
{ trigger: { hp_pct: 70 }, adds: [{ s_uuid: 6103, overrides: { ap: 160 } }], attr_mod: { ap_pct: 15 } },
{ trigger: { hp_pct: 40 }, adds: [{ s_uuid: 6104, overrides: { ap: 180 } }], attr_mod: { ap_pct: 25, defense_pct: -10 } },
],
},
// 10003 换刃(换招式):每阶段换一把"刀",系数逐段上调
10003: {
name: "换刃",
switches: [
{ trigger: { hp_pct: 70 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6101, overrides: { ap: 200 } }] },
{ trigger: { hp_pct: 40 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6104, overrides: { ap: 220 } }], attr_mod: { ap_pct: 20, defense_pct: -15 } },
],
},
```
### 4.2 6102 兽人首领-坦克型(时间,近战池)
```ts
// 10010 重压觉醒(追加式):越久越硬,反击越痛
10010: {
name: "重压觉醒",
switches: [
{ trigger: { time: 30 }, adds: [{ s_uuid: 6100, overrides: { ap: 150 } }], attr_mod: { defense_pct: 20 } },
{ trigger: { time: 60 }, adds: [{ s_uuid: 6104, overrides: { ap: 180 } }], attr_mod: { ap_pct: 20 } },
],
},
// 10011 崩岩(换招式):外壳剥落,换进攻形态
10011: {
name: "崩岩",
switches: [
{ trigger: { time: 30 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6102, overrides: { ap: 200 } }], attr_mod: { ap_pct: 15 } },
{ trigger: { time: 60 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6104, overrides: { ap: 220 } }], attr_mod: { ap_pct: 25, defense_pct: -20 } },
],
},
```
### 4.3 6103 兽人首领-魔法师(时间,远程池)
```ts
// 10020 元素汇聚(追加式):元素逐一汇入,弹幕越来越密
10020: {
name: "元素汇聚",
switches: [
{ trigger: { time: 30 }, adds: [{ s_uuid: 6204, overrides: { ap: 150 } }] },
{ trigger: { time: 60 }, adds: [{ s_uuid: 6202, overrides: { ap: 150 } }], attr_mod: { ap_pct: 20 } },
],
},
// 10021 禁咒(换招式):每阶段念诵新禁咒,旧的被遗忘
10021: {
name: "禁咒",
switches: [
{ trigger: { time: 30 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6201, overrides: { ap: 180 } }] },
{ trigger: { time: 60 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6204, overrides: { ap: 200 } }], attr_mod: { ap_pct: 30, defense_pct: -15 } },
],
},
```
### 4.4 6104 兽人首领-射手(血量,远程池)
```ts
// 10030 箭雨升级(追加式):箭种逐段解锁
10030: {
name: "箭雨升级",
switches: [
{ trigger: { hp_pct: 70 }, adds: [{ s_uuid: 6150, overrides: { ap: 150 } }] },
{ trigger: { hp_pct: 40 }, adds: [{ s_uuid: 6151, overrides: { ap: 160 } }], attr_mod: { ap_pct: 20 } },
],
},
// 10031 狙击姿态(换招式):放弃弹幕,换单体重炮
10031: {
name: "狙击姿态",
switches: [
{ trigger: { hp_pct: 70 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6203, overrides: { ap: 180 } }] },
{ trigger: { hp_pct: 40 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6204, overrides: { ap: 200 } }], attr_mod: { ap_pct: 25, defense_pct: -15 } },
],
},
```
### 4.5 6105 亡灵首领-法师(时间,远程池)
```ts
// 10040 亡灵潮汐(追加式):毒雾与雷暴层层叠加
10040: {
name: "亡灵潮汐",
switches: [
{ trigger: { time: 30 }, adds: [{ s_uuid: 6202, overrides: { ap: 150 } }] },
{ trigger: { time: 60 }, adds: [{ s_uuid: 6204, overrides: { ap: 150 } }], attr_mod: { ap_pct: 25 } },
],
},
// 10041 灵魂爆发(换招式):燃尽旧躯壳换新法术
10041: {
name: "灵魂爆发",
switches: [
{ trigger: { time: 30 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6200, overrides: { ap: 180 } }], attr_mod: { defense_pct: 15 } },
{ trigger: { time: 60 }, removes: [6150, 6151, 6200, 6201, 6202, 6203, 6204], adds: [{ s_uuid: 6204, overrides: { ap: 220 } }], attr_mod: { ap_pct: 30 } },
],
},
```
### 4.6 6106 亡灵首领-骑马战士(时间,近战池)
```ts
// 10050 冲锋陷阵(追加式):战马冲锋节奏渐强
10050: {
name: "冲锋陷阵",
switches: [
{ trigger: { time: 30 }, adds: [{ s_uuid: 6101, overrides: { ap: 160 } }] },
{ trigger: { time: 60 }, adds: [{ s_uuid: 6104, overrides: { ap: 180 } }], attr_mod: { ap_pct: 20, defense_pct: -10 } },
],
},
// 10051 背水一战(换招式):弃守转攻,每段更凶更脆
10051: {
name: "背水一战",
switches: [
{ trigger: { time: 30 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6103, overrides: { ap: 200 } }], attr_mod: { ap_pct: 15, defense_pct: -10 } },
{ trigger: { time: 60 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6104, overrides: { ap: 220 } }], attr_mod: { ap_pct: 30, defense_pct: -20 } },
],
},
```
### 4.7 6006 骷髅领主(血量,近战池)
```ts
// 10060 亡灵复苏(追加式):骨架越碎越硬
10060: {
name: "亡灵复苏",
switches: [
{ trigger: { hp_pct: 70 }, adds: [{ s_uuid: 6100, overrides: { ap: 150 } }], attr_mod: { defense_pct: 25 } },
{ trigger: { hp_pct: 40 }, adds: [{ s_uuid: 6102, overrides: { ap: 160 } }], attr_mod: { ap_pct: 15, defense_pct: 15 } },
],
},
// 10061 骷髅狂暴(换招式):拼装新武器,攻高防低
10061: {
name: "骷髅狂暴",
switches: [
{ trigger: { hp_pct: 70 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6103, overrides: { ap: 180 } }] },
{ trigger: { hp_pct: 40 }, removes: [6100, 6101, 6102, 6103, 6104], adds: [{ s_uuid: 6104, overrides: { ap: 220 } }], attr_mod: { ap_pct: 25, defense_pct: -15 } },
],
},
```
### 4.8 heroSet.ts 挂载(phase_ids)
```ts
// 6101 双刀dps: phase_ids: [10001, 10002, 10003],
// 6102 坦克型: phase_ids: [10010, 10011],
// 6103 魔法师: phase_ids: [10020, 10021],
// 6104 射手: phase_ids: [10030, 10031],
// 6105 亡灵法师: phase_ids: [10040, 10041],
// 6106 骑马战士: phase_ids: [10050, 10051],
// 6006 骷髅领主: phase_ids: [10060, 10061],
```
---
## §5 关卡节奏:前期教学 → 后期完整
**现状约束**:`pickPhasePlan` 从 `phase_ids` 均匀随机,无关卡维度;P3 阈值写死在方案里。
**本版策略(零机制改动)**:
- P3 阈值统一标准档(血量 40% / 时间 60s),不做关卡调节
- 前期体验差异由数值自然产生:低关卡玩家 DPS 低、Boss 血量成长低,战斗更快结束,
时间型 Boss 的 P3(60s)在前期自然少见 → 符合"前期教学、后期完整"的意图
- P3 新技能有 8s CD 真空期,残血/收尾阶段常来不及施放 → P3 实际强度比纸面温和,
前期偶发触发不致失控
**后续迭代项(需小改 `pickPhasePlan`,不在本版)**:
按关卡号从 `phase_ids` 过滤"苛刻版/标准版/宽松版"方案副本
(前期 P3 血量 15% / 时间 75s 的副本方案,id 同段递增)。
---
## §6 验证清单
- [ ] `validateBossPhaseSet()` 全量通过(恰 2 切换点 / hp_pct 前缀递减 / time 递增 / 技能引用存在)
- [ ] TestMode 逐个 Boss 跑一遍:追加式 P3 技能池 = 随机大招 + 2 个 adds;换招式 P3 技能池 = 仅 1 个 adds
- [ ] 时间型方案实测:30s/60s 切换,90s 超时前 P3 至少施放 1 次新技能
- [ ] 换招式验证:removes 整池后,RogueConfig 随机注入的大招确实消失(普攻不受影响)
- [ ] 数值抽查:同 Boss 追加式 vs 换招式的 P3 阶段 30s 伤害量差距 ≤10%
---
## §7 与外部设计稿的对照(借鉴 vs 放弃)
| 外部稿概念 | 本文处理 | 原因 |
|-----------|---------|------|
| 4 主题 Boss(破阵先锋/雷巫/霜羽/磐盾) | 映射到现有 6 Boss,按定位分触发维度 | Boss 池由 heroSet 实际条目决定 |
| `phase_mode: clear_replace` 字段 | 用 `removes` 列满定位大招池实现(§3.2) | 实现为纯增量,`delete` 空键无副作用,零机制新增 |
| `property_mod.hp` 血量修正 | **全部放弃**,仅 ap/def | 实现已排除 hp,避免血量同步歧义 |
| P1→P2 固定 60s(时间型) | 改为 **30s** | 适配 `BATTLE_TIMEOUT=90s`,P3 需留收尾窗口 |
| P2→P3 关卡三档调节(5%/20%/35%) | 本版统一标准档,关卡调节列后续迭代 | `pickPhasePlan` 无关卡维度,不为此改机制 |
| 英雄 lv 技能引用(lv8/lv16) | 统一 `SkillSet` 6100~6204 + overrides.ap 口径 | Boss 大招体系与英雄 lv 体系无关 |
| 阶段切换无敌帧/演出 | 维持空钩子 `onPhaseExit/onPhaseEnter` | 表现层后续迭代,接口已预留 |
| 双模式强度对齐 ±5% | 放宽到 ±10%,用 §3.3 口径对齐 | 缺数值模拟工具,先宽松后收敛 |
| Boss 命名美学(方案名) | 全部保留并适配(狂暴进化/禁咒/背水一战…) | 与机制无关,直接采纳 |
---
**版本**:v1(2026-10-06,基于已实现机制重写)
**关联文档**:
- 技术规格:`docs/superpowers/specs/2026-10-06-boss-phase-system-design.md`
- 实现计划:`docs/superpowers/plans/2026-10-06-boss-phase-system.md`
- 外部参考:`y:\构思\像素大勇者-boss-阶段设计稿.md`(仅理念借鉴)

View File

@@ -0,0 +1,330 @@
# Boss 多阶段战斗系统设计(Boss Phase System)
> 日期:2026-10-06
> 状态:已评审(3 个子智能体审查通过,硬伤已修订)
> 关联文件:`heroSet.ts` / `RogueConfig.ts` / `Mon.ts` / `HeroAttrsComp.ts` / `GameEvent.ts`
## 1. 背景与目标
为 Boss 添加类似魔兽世界的多阶段战斗模式:
- **固定 3 个阶段**(P1/P2/P3),Boss 出生即 P1
- **混合触发**切换:血量阈值为主,战斗时长辅助(同一切换点内 OR 语义,任一满足即切)
- **混合式技能**:核心技能(普攻 + RogueConfig 注入的大招基线)贯穿全程,阶段技能增量轮换(adds/removes)
- 阶段切换的**表现层(演出/无敌/清场)留空钩子**,本次只搭骨架,后续迭代填充
## 2. 架构总览与数据流
方案选型:**增量驱动式**(方案 1)。每个切换点声明触发条件 + adds/removes + 属性修正,
切换 = BossPhaseSystem 增量改写 `model.skills`,与现有 `_testSkills` / `RogueConfig.ults`
动态注入模式完全一致,`SCastSystem` / `HeroAtkSystem` 零改动。
```
heroSet.ts (heroInfo) 【配置声明层】
└─ phase_ids: [10001, 10002] ← Boss 允许的阶段方案
↓ 生成时读取
RogueConfig.ts (StageSpawningEngine.makeBoss) 【消费层】
└─ 从 phase_ids 随机选 1 个 → GeneratedMonster.phase_plan_id
↓ 传递(唯一接线点 MissionMonComp.ts:224)
Mon.ts (load) 【初始化层】
└─ is_boss 且方案存在 → this.add(BossPhaseComp),index = 0
(P1 = 基线技能,无需注入)
↓ 每帧检测
BossPhaseSystem (新 ECS 系统) 【运行时层】
└─ filter: allOf(BossPhaseComp, HeroAttrsComp)
→ removes 移除 + adds 注入 + attr_mod 修正 + 资源预热
→ 空钩子 onPhaseExit/onPhaseEnter (TODO)
→ 广播 GameEvent.BossPhaseChange
```
关键决策:
- 新增轻量标记组件 `BossPhaseComp`:ECS matcher 按 allOf 过滤,只遍历带阶段方案的 Boss
- `HeroAttrsComp` 零新增字段(阶段状态全在 BossPhaseComp;属性修正直接改 ap/defense 现有字段)
- 计时下沉到组件(`BossPhaseComp.elapsed`),随 reset 天然归零,无全局状态
## 3. 配置层设计
### 3.1 新文件 `assets/script/game/common/config/BossPhaseSet.ts`
方案 id 使用 **10000 段**(已验证全项目空闲;9100 段被 TalentSet 9101-9199 与
ICardSet 9101~9110 双重占用,禁用)。
```ts
/** 阶段切换触发条件(混合触发:任一条件满足即切换) */
export interface PhaseTrigger {
/** 血量百分比阈值(1~99):Boss 血量比例 ≤ 该值时触发 */
hp_pct?: number;
/** Boss 在场战斗秒数:超过该时长触发 */
time?: number;
}
/** 切换瞬间一次性属性修正(基于当前值链式乘算,(1 + pct/100)) */
export interface PhaseAttrMod {
ap_pct?: number; // 攻击 ±%
defense_pct?: number; // 防御 ±%(注意 applyDefense 除法递减收益非线性)
}
/** 注入技能条目(与 RogueConfig 大招条目同构) */
export interface PhaseSkillEntry {
s_uuid: number;
cd?: number; // 缺省 8s(强制兜底,防 NaN 污染 CD)
overrides?: SkillOverrides;
}
/** 单个切换点定义(P1→P2 或 P2→P3) */
export interface PhaseSwitchDef {
trigger: PhaseTrigger;
/** 注入的技能(追加进 skills 表,SCastSystem 按 CD 轮转)。禁止操作普攻首键 */
adds?: PhaseSkillEntry[];
/** 移除的技能 s_uuid 列表(含 RogueConfig 注入的大招)。禁止操作普攻首键 */
removes?: number[];
attr_mod?: PhaseAttrMod;
}
/** 阶段方案:固定 2 个切换点 = 3 阶段,P1 为基线 */
export interface BossPhasePlan {
name: string;
switches: [PhaseSwitchDef, PhaseSwitchDef];
}
/** 方案表:key 为方案 id(10000 段) */
export const BossPhaseSet: Record<number, BossPhasePlan> = {
10001: {
name: "狂暴进化",
switches: [
{ trigger: { hp_pct: 70 }, adds: [{ s_uuid: 6102, overrides: { ap: 180 } }], removes: [6100] },
{ trigger: { hp_pct: 40, time: 60 }, adds: [{ s_uuid: 6103 }], attr_mod: { ap_pct: 20 } },
],
},
};
```
`validateBossPhaseSet(): string[]` 校验规则:
1. 恰好 2 个切换点;每个 trigger 至少含一个条件
2. `hp_pct ∈ (0, 100)`;`time > 0`;多个 time 严格递增
3. **hp_pct 前缀规则**:hp_pct 切换点必须构成前缀——一旦出现 time-only 切换点,
其后的切换点禁止再携带 hp_pct(拦截"P2 time / P3 hp_pct=80"乱序连切)
4. 存在 hp_pct 的相邻切换点严格递减(70 > 40)
5. adds/removes 的 s_uuid 存在于 SkillSet(error 级)
6. removes 的 s_uuid 应在该 Boss 可达技能集内(heroInfo.skills ∪ 大招池 ∪ 前置 adds,warn 级)
### 3.2 `heroSet.ts` 改动(2 处)
1. `heroInfo` 接口新增可选字段(与 `dis?`/`bonus?` 风格一致):
```ts
/** Boss 专属:允许的阶段方案 id 列表(引用 BossPhaseSet,生成时随机取一) */
phase_ids?: number[];
```
2. 在 6101~6106 真正的 Boss 配置上加示例:`phase_ids: [10001]`
(注意 6006 骷髅领主也在 BossPools 内但未配置 phase_ids,走兜底路径)
### 3.3 `RogueConfig.ts` 改动(4 处)
1. `GeneratedMonster` 接口加 `phase_plan_id?: number`
2. `makeBoss()`:`baseInfo.phase_ids` 非空数组时随机选 1 个写入,
否则 `undefined`(防 NaN:`arr?.length ? arr[rand] : undefined`)
3. `validateRogueConfig()` 追加调用 `validateBossPhaseSet()` + 校验
heroInfo.phase_ids 引用的方案存在
4. **顺带修复现存 bug**:`_testSkills` 池化残留(Boss 回池复用为小怪会继承大招)——
`MissionMonComp.spawnMonster` 改为无条件赋值 `(mon as any)._testSkills = monData.skills ?? undefined`
依赖方向:RogueConfig → BossPhaseSet → SkillSet,无循环导入。
## 4. 运行时层设计
### 4.1 新文件 `assets/script/game/hero/BossPhaseComp.ts`
```ts
/** Boss 阶段标记组件:挂载即表示该实体参与阶段状态机 */
@ecs.register('BossPhase')
export class BossPhaseComp extends ecs.Comp {
/** 生效的阶段方案 id(BossPhaseSet key) */
plan_id: number = 0;
/** 当前阶段索引:0=P1 / 1=P2 / 2=P3 */
index: number = 0;
/** Boss 在场战斗累计时长(秒),time 触发条件基准 */
elapsed: number = 0;
reset() { this.plan_id = 0; this.index = 0; this.elapsed = 0; }
}
```
**禁止缓存绝对血量阈值**:hp/ap 在 Mon.load 后被 MissionMonComp 外部覆盖
(MissionMonComp.ts L239-246),阶段系统每帧实时用 `model.hp / model.hp_max * 100` 比对。
### 4.2 `Mon.ts` 改动
1. `load()` 增加第 **8** 个可选参数 `phase_plan_id: number = 0`
(当前签名 7 参;唯一调用点 `MissionMonComp.ts:224` 同步追加实参)
2. 在技能表构建完成后:
```ts
// Boss 阶段初始化:P1 = 基线技能,仅挂标记组件并记录方案
if (is_boss && phase_plan_id > 0 && BossPhaseSet[phase_plan_id]) {
const phase = this.add(BossPhaseComp);
phase.plan_id = phase_plan_id;
phase.index = 0;
}
```
3. `destroy()` 中、`remove(HeroAttrsComp)` **之前**显式 `this.remove(BossPhaseComp)`
(触发 reset + 回组件池;ECS 组件无自动销毁钩子,必须手动 remove)
4. `reset()` 与 `destroy()` 对齐,同样补 remove(防旁路)
### 4.3 新文件 `assets/script/game/hero/BossPhaseSystem.ts`
```ts
/**
* Boss 阶段系统:检测混合触发条件(血量阈值 OR 在场时长)并执行阶段切换
* Why: 独立系统符合 M/V/S 分层;filter 按 BossPhaseComp 过滤,每局 Boss 唯一,开销可忽略
*/
@ecs.register('BossPhaseSystem')
export class BossPhaseSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
filter() { return ecs.allOf(BossPhaseComp, HeroAttrsComp); }
/** 框架自动遍历匹配实体逐个回调;dt 从 this.dt 取(秒) */
update(e: ecs.Entity): void {
const phase = e.get(BossPhaseComp);
const model = e.get(HeroAttrsComp);
// 守卫:非战斗/暂停/已死亡跳过(死亡同帧不切阶段,防死尸换阶段)
if (!smc.mission.in_fight || smc.mission.pause) return;
if (model.is_dead) return;
// P3 后不再检测
if (phase.index >= 2) return;
phase.elapsed += this.dt;
const plan = BossPhaseSet[phase.plan_id];
if (!plan) return;
const def = plan.switches[phase.index];
// 混合触发:任一条件满足即切换(血量实时计算,不缓存绝对值)
const hpPct = model.hp / model.hp_max * 100;
const hpHit = def.trigger.hp_pct !== undefined && hpPct <= def.trigger.hp_pct;
const timeHit = def.trigger.time !== undefined && phase.elapsed >= def.trigger.time;
if (hpHit || timeHit) {
this.executePhaseSwitch(e, def);
}
}
private executePhaseSwitch(e: ecs.Entity, def: PhaseSwitchDef) {
const phase = e.get(BossPhaseComp)!;
const model = e.get(HeroAttrsComp)!;
const from = phase.index;
this.onPhaseExit(e); // 空钩子:TODO 阶段退场演出
// 1. 移除旧技能(仅大招键,绝不操作普攻首键——键序契约见 §5)
if (def.removes) {
for (const s_uuid of def.removes) delete model.skills[s_uuid];
}
// 2. 注入新技能(cd 强制兜底 8s,防 undefined → NaN 污染 CD)
if (def.adds) {
for (const add of def.adds) {
if (!add || !add.s_uuid || !SkillSet[add.s_uuid]) continue;
model.skills[add.s_uuid] = {
uuid: add.s_uuid,
lv: 1,
cd: add.cd ?? 8,
ccd: 0,
overrides: add.overrides,
};
// 资源预热:新技能 prefab 首次异步加载会"丢一发",切换时预加载
// 路径格式与 Skill.ts L112 对齐(game/skill/atk/{sp_name},Prefab 类型)
const path = `game/skill/atk/${SkillSet[add.s_uuid].sp_name}`;
oops.res.load(path, Prefab);
}
}
// 3. 属性修正:基于当前值链式乘算(P3 基于 P2 后的值)
const mod = def.attr_mod;
if (mod) {
if (mod.ap_pct) model.ap = Math.round(model.ap * (1 + mod.ap_pct / 100));
if (mod.defense_pct) model.defense = Math.round(model.defense * (1 + mod.defense_pct / 100));
}
phase.index++;
this.onPhaseEnter(e); // 空钩子:TODO 阶段入场演出(无敌/清场/横幅后续在此实现)
// 广播:oops.message.dispatchEvent(GameEvent.BossPhaseChange, payload)
}
/** TODO: 阶段退场表现(震屏/变暗等),后续迭代实现 */
private onPhaseExit(e: ecs.Entity) { }
/** TODO: 阶段入场表现(怒吼/无敌帧/技能横幅等),后续迭代实现 */
private onPhaseEnter(e: ecs.Entity) { }
}
```
要点:
- `@ecs.register` 装饰器在模块加载时自动注册系统(App 级单例),
**唯一要求:BossPhaseSystem.ts 被 import**(从 Mon.ts import BossPhaseComp 的同文件链自然带动,
实施时确认 import 链可达)
- 不使用系统级 battleTime(per-entity update 中累加会乘 N 倍),计时在组件 elapsed
### 4.4 `GameEvent.ts` 新增枚举(近期 PascalCase 惯例)
```ts
/** Boss 阶段切换(data: { eid: number, planId: number, fromPhase: number, toPhase: number }),表现层监听此事件实现演出 */
BossPhaseChange = "BossPhaseChange",
```
payload 用 eid 定位实体(防裸引用已回收实体);监听方按项目规范在 onDestroy 中注销。
### 4.5 TestMode 调试通道
`TestModeConfig` 加 `phasePlanId: number = 0` 与 `isBoss: boolean = false`;
`generateTestWave()` 的 isBoss 硬编码 false(RogueConfig.ts L369)同步改为读配置,
并将 phasePlanId 传入 GeneratedMonster——否则调试开关配了也不生效。
## 5. 明示的设计决策(避免 QA 争议)
| 决策 | 说明 |
|------|------|
| ccd: 0 语义 | 切换后需等满一个完整 CD 才首次施放新技能(非立即轰) |
| attr_mod 口径 | 链式复利:P3 基于 P2 修正后的当前值乘算,配置时按此口径 |
| 前摇交叉语义 | 切换瞬间已在空的弹道按出手快照结算(SDataCom 值拷贝机制天然保证),可能出现"P1 技能携带 P2 攻击力落地",属可接受行为 |
| skills 键序契约 | JS `delete+set` 会把键移到对象末尾,而 `buildSkillCandidates`(SCastSystem.ts L627-631)硬假设首键=普攻垫底。**removes/adds 绝不允许操作普攻首键**(校验 + 注释双重约束);若未来需换普攻,必须整体重建 skills 表保证首键 |
| speed 不修正 | `HeroAttrsComp.speed` 是移速(仅 MoveComp 消费),攻速走 BuffComp 聚合通道。attr_mod 不含 speed_pct;攻速强化留给后续 buff 通道迭代 |
| hp_max 不修正 | 避免 hp 同步策略(保百分比/保绝对值/回满)问题,后续需要时单独设计 |
| 触发技不阶段化 | atking/atked/dead 等存于 runtime_* 缓存(独立于 model.skills),本次不覆盖,列为后续迭代 |
| 施法不打断 | 切换只改 model.skills;施法闭包捕获 SkillSet 静态配置 + 伤害走值拷贝快照,进行中的施法/飞行弹道不受影响 |
## 6. 错误处理与校验
- **运行时兜底**:BossPhaseSet 查不到方案 → 不挂组件,Boss 退化为单阶段(现状行为),`oops.log.warn`
- **校验**:`validateBossPhaseSet()` 规则见 §3.1;`validateRogueConfig()` 追加调用(启动期暴露配置错误)
- **对象池复用安全**:BossPhaseComp 随 destroy 手动 remove → reset 归零 → 回组件池;
`_testSkills` 残留 bug 顺带修复(§3.3 第 4 条)
## 7. 测试方案
1. **单阶段回归**:不配 phase_ids 的 Boss(如 6006)行为与现状完全一致
2. **阶段切换**:TestMode 配 `isBoss: true` + `phasePlanId: 10001` + 低血量阈值,
打 Boss 观察切换调试日志(`phase P1→P2`,受 debugMode 控制)
3. **time 触发**:高防阵容拖时间验证 time 条件独立生效
4. **乱序拦截**:手工配"P2 time-only / P3 hp_pct"方案,确认 validate 报错
5. **池化复用**:Boss 死亡后连续刷普通怪,确认小怪不继承 Boss 大招(_testSkills 修复验证)
6. **键序**:Boss 切换后普攻仍垫底、大招按 CD 轮转正常
## 8. 文件清单
| 文件 | 操作 | 内容 |
|------|------|------|
| `assets/script/game/common/config/BossPhaseSet.ts` | 新建 | 方案表 + 类型 + validate |
| `assets/script/game/hero/BossPhaseComp.ts` | 新建 | 标记组件 |
| `assets/script/game/hero/BossPhaseSystem.ts` | 新建 | 检测与切换系统 + 空钩子 |
| `assets/script/game/common/config/heroSet.ts` | 修改 | heroInfo 加 phase_ids 字段 + Boss 示例配置 |
| `assets/script/game/map/RogueConfig.ts` | 修改 | GeneratedMonster.phase_plan_id + makeBoss 消费 + validate 追加 + TestMode 通道 |
| `assets/script/game/map/MissionMonComp.ts` | 修改 | load 传参接线 + _testSkills 残留修复 |
| `assets/script/game/hero/Mon.ts` | 修改 | 第 8 参 + BossPhaseComp 挂载/移除 |
| `assets/script/game/common/config/GameEvent.ts` | 修改 | BossPhaseChange 枚举 |
## 9. 后续迭代(本次不做)
- 阶段切换演出:onPhaseExit/onPhaseEnter 钩子填充(无敌帧、清场、怒吼、横幅)
- 触发技(atking/atked/dead/call)阶段化:runtime_* 缓存重建通道
- 攻速强化:走 buffManager timed buff(Attrs.speed 修饰器)通道
- 阶段专属索敌/移动行为(如 P3 召唤小怪、冲锋走位)
- 换普攻支持:skills 整表重建保证首键契约