Compare commits
25 Commits
fbb6cc72cc
...
new2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7cd440844 | ||
|
|
a242ccc2ea | ||
|
|
74d03facd9 | ||
|
|
a122d0904c | ||
|
|
24aff3baa3 | ||
|
|
b31d870d90 | ||
|
|
11ba692f18 | ||
|
|
e662b63f80 | ||
|
|
2ab5100b41 | ||
|
|
f4fb561319 | ||
|
|
fef913bd75 | ||
|
|
53a0ee1c6e | ||
|
|
f679644bbb | ||
|
|
541ca0e251 | ||
|
|
7703b0348c | ||
|
|
ad50e11052 | ||
|
|
d5158cde52 | ||
|
|
8176ef098e | ||
|
|
56dcdccca2 | ||
|
|
31a2efee4d | ||
|
|
761d0aa68e | ||
|
|
7766909294 | ||
|
|
71db1c24b0 | ||
|
|
ca38411cc7 | ||
|
|
b149e6517b |
@@ -1,8 +1,7 @@
|
|||||||
---
|
---
|
||||||
alwaysApply: true
|
alwaysApply: false
|
||||||
scene: git_message
|
scene: git_message
|
||||||
---
|
---
|
||||||
|
|
||||||
## Profile
|
## Profile
|
||||||
你是一个严谨的配置管理专家与代码审查员。你的核心任务是根据代码的变更内容(diff)或开发者的简短描述,生成符合业界标准(Conventional Commits / Angular 规范)的 Git 提交信息。你擅长剥离代码细节,总结出清晰的业务意图(What 和 Why)。
|
你是一个严谨的配置管理专家与代码审查员。你的核心任务是根据代码的变更内容(diff)或开发者的简短描述,生成符合业界标准(Conventional Commits / Angular 规范)的 Git 提交信息。你擅长剥离代码细节,总结出清晰的业务意图(What 和 Why)。
|
||||||
|
|
||||||
|
|||||||
@@ -402,6 +402,10 @@
|
|||||||
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
||||||
"__expectedType__": "cc.Material"
|
"__expectedType__": "cc.Material"
|
||||||
},
|
},
|
||||||
|
"outlineMatGlow": {
|
||||||
|
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
|
||||||
|
"__expectedType__": "cc.Material"
|
||||||
|
},
|
||||||
"_id": ""
|
"_id": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -402,6 +402,10 @@
|
|||||||
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
||||||
"__expectedType__": "cc.Material"
|
"__expectedType__": "cc.Material"
|
||||||
},
|
},
|
||||||
|
"outlineMatGlow": {
|
||||||
|
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
|
||||||
|
"__expectedType__": "cc.Material"
|
||||||
|
},
|
||||||
"_id": ""
|
"_id": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -812,8 +816,8 @@
|
|||||||
},
|
},
|
||||||
"_contentSize": {
|
"_contentSize": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 150,
|
"width": 80,
|
||||||
"height": 150
|
"height": 100
|
||||||
},
|
},
|
||||||
"_anchorPoint": {
|
"_anchorPoint": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
@@ -921,12 +925,12 @@
|
|||||||
"_offset": {
|
"_offset": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 75
|
"y": 50
|
||||||
},
|
},
|
||||||
"_size": {
|
"_size": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 150,
|
"width": 80,
|
||||||
"height": 150
|
"height": 100
|
||||||
},
|
},
|
||||||
"_id": ""
|
"_id": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -402,6 +402,10 @@
|
|||||||
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
||||||
"__expectedType__": "cc.Material"
|
"__expectedType__": "cc.Material"
|
||||||
},
|
},
|
||||||
|
"outlineMatGlow": {
|
||||||
|
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
|
||||||
|
"__expectedType__": "cc.Material"
|
||||||
|
},
|
||||||
"_id": ""
|
"_id": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -809,8 +813,8 @@
|
|||||||
},
|
},
|
||||||
"_contentSize": {
|
"_contentSize": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 150,
|
"width": 80,
|
||||||
"height": 150
|
"height": 100
|
||||||
},
|
},
|
||||||
"_anchorPoint": {
|
"_anchorPoint": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
@@ -918,12 +922,12 @@
|
|||||||
"_offset": {
|
"_offset": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 75
|
"y": 50
|
||||||
},
|
},
|
||||||
"_size": {
|
"_size": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 150,
|
"width": 60,
|
||||||
"height": 150
|
"height": 100
|
||||||
},
|
},
|
||||||
"_id": ""
|
"_id": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -402,6 +402,10 @@
|
|||||||
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
||||||
"__expectedType__": "cc.Material"
|
"__expectedType__": "cc.Material"
|
||||||
},
|
},
|
||||||
|
"outlineMatGlow": {
|
||||||
|
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
|
||||||
|
"__expectedType__": "cc.Material"
|
||||||
|
},
|
||||||
"_id": ""
|
"_id": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -812,8 +816,8 @@
|
|||||||
},
|
},
|
||||||
"_contentSize": {
|
"_contentSize": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 150,
|
"width": 80,
|
||||||
"height": 150
|
"height": 100
|
||||||
},
|
},
|
||||||
"_anchorPoint": {
|
"_anchorPoint": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
@@ -921,12 +925,12 @@
|
|||||||
"_offset": {
|
"_offset": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 75
|
"y": 50
|
||||||
},
|
},
|
||||||
"_size": {
|
"_size": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 150,
|
"width": 8,
|
||||||
"height": 150
|
"height": 100
|
||||||
},
|
},
|
||||||
"_id": ""
|
"_id": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -402,6 +402,10 @@
|
|||||||
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
||||||
"__expectedType__": "cc.Material"
|
"__expectedType__": "cc.Material"
|
||||||
},
|
},
|
||||||
|
"outlineMatGlow": {
|
||||||
|
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
|
||||||
|
"__expectedType__": "cc.Material"
|
||||||
|
},
|
||||||
"_id": ""
|
"_id": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -809,8 +813,8 @@
|
|||||||
},
|
},
|
||||||
"_contentSize": {
|
"_contentSize": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 150,
|
"width": 80,
|
||||||
"height": 150
|
"height": 100
|
||||||
},
|
},
|
||||||
"_anchorPoint": {
|
"_anchorPoint": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
@@ -918,12 +922,12 @@
|
|||||||
"_offset": {
|
"_offset": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 75
|
"y": 50
|
||||||
},
|
},
|
||||||
"_size": {
|
"_size": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 150,
|
"width": 80,
|
||||||
"height": 150
|
"height": 100
|
||||||
},
|
},
|
||||||
"_id": ""
|
"_id": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -402,6 +402,10 @@
|
|||||||
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
"__uuid__": "2b8a37ee-732c-4c5b-b6a4-136f3e581cde",
|
||||||
"__expectedType__": "cc.Material"
|
"__expectedType__": "cc.Material"
|
||||||
},
|
},
|
||||||
|
"outlineMatGlow": {
|
||||||
|
"__uuid__": "2fcd55a9-38ca-45aa-9164-68e48aaf51ce",
|
||||||
|
"__expectedType__": "cc.Material"
|
||||||
|
},
|
||||||
"_id": ""
|
"_id": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -809,8 +813,8 @@
|
|||||||
},
|
},
|
||||||
"_contentSize": {
|
"_contentSize": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 150,
|
"width": 80,
|
||||||
"height": 150
|
"height": 100
|
||||||
},
|
},
|
||||||
"_anchorPoint": {
|
"_anchorPoint": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
@@ -918,12 +922,12 @@
|
|||||||
"_offset": {
|
"_offset": {
|
||||||
"__type__": "cc.Vec2",
|
"__type__": "cc.Vec2",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 75
|
"y": 50
|
||||||
},
|
},
|
||||||
"_size": {
|
"_size": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 150,
|
"width": 80,
|
||||||
"height": 150
|
"height": 100
|
||||||
},
|
},
|
||||||
"_id": ""
|
"_id": ""
|
||||||
},
|
},
|
||||||
|
|||||||
268
assets/script/game/common/config/BossPhaseSet.ts
Normal file
268
assets/script/game/common/config/BossPhaseSet.ts
Normal 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;
|
||||||
|
}
|
||||||
9
assets/script/game/common/config/BossPhaseSet.ts.meta
Normal file
9
assets/script/game/common/config/BossPhaseSet.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.24",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "73ce05bd-3011-4b93-aa6b-f9eafa596cfe",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -116,5 +116,7 @@ export enum GameEvent {
|
|||||||
NextSection = "NextSection",
|
NextSection = "NextSection",
|
||||||
/** 结算弹窗点击"再来一次"(payload 为空,MissionComp 据此重开本关) */
|
/** 结算弹窗点击"再来一次"(payload 为空,MissionComp 据此重开本关) */
|
||||||
RestartMission = "RestartMission",
|
RestartMission = "RestartMission",
|
||||||
|
/** Boss 阶段切换(payload: { eid, planId, fromPhase, toPhase },表现层监听此事件实现演出;eid 定位实体,勿持裸引用) */
|
||||||
|
BossPhaseChange = "BossPhaseChange",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { v3 } from "cc"
|
import { v3 } from "cc"
|
||||||
import { BoxSet, FacSet, FightSet } from "./GameSet"
|
import { BoxSet, FacSet, FightSet } from "./GameSet"
|
||||||
import { SkillOverrides, TGroup, SkillSet, FieldSkillSet, SkillConfig, SkillKind, FieldSkillConfig, FieldSkillType, mergeSkillParams, BuffTarget } from "./SkillSet"
|
import { SkillOverrides, TGroup, SkillSet, FieldSkillSet, SkillConfig, SkillKind, FieldSkillConfig, FieldSkillType, mergeSkillParams, BuffTarget } from "./SkillSet"
|
||||||
import { BuffList, BuffConfig, ModOp } from "./BuffSet"
|
import { BuffList, BuffConfig, ModOp } from "./BuffSet"
|
||||||
@@ -430,6 +430,7 @@ export interface heroInfo {
|
|||||||
speed?: number; // 移动速度(像素/秒)
|
speed?: number; // 移动速度(像素/秒)
|
||||||
dodge?: number; // 闪避率(百分点,缺省由初始化处兜底为全局默认值)
|
dodge?: number; // 闪避率(百分点,缺省由初始化处兜底为全局默认值)
|
||||||
accuracy?: number; // 命中率(百分点,缺省由初始化处兜底为全局默认值)
|
accuracy?: number; // 命中率(百分点,缺省由初始化处兜底为全局默认值)
|
||||||
|
phase_ids?: number[]; // Boss 专属:允许的阶段方案 id 列表(引用 BossPhaseSet,生成时随机取一)
|
||||||
skills: Record<number, HSkillInfo>; // 携带技能ID列表
|
skills: Record<number, HSkillInfo>; // 携带技能ID列表
|
||||||
bonus?: LvBonusAttrsEntry[]; // 额外属性加成(按 lv 升序,运行时累加所有 lv ≤ 英雄等级 的档位 attrs;hp/ap/def 按"基础×品质+成长"快照百分比加成,crt/frz/stun/para 为固定百分点累加)
|
bonus?: LvBonusAttrsEntry[]; // 额外属性加成(按 lv 升序,运行时累加所有 lv ≤ 英雄等级 的档位 attrs;hp/ap/def 按"基础×品质+成长"快照百分比加成,crt/frz/stun/para 为固定百分点累加)
|
||||||
info: string; // 描述文案
|
info: string; // 描述文案
|
||||||
@@ -1079,7 +1080,7 @@ export const HeroInfo: Record<number, heroInfo> = {
|
|||||||
},
|
},
|
||||||
// 特殊位怪物
|
// 特殊位怪物
|
||||||
6006: {
|
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级坦克"
|
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排MiniBoss级坦克"
|
||||||
},
|
},
|
||||||
6007: {
|
6007: {
|
||||||
@@ -1093,27 +1094,27 @@ export const HeroInfo: Record<number, heroInfo> = {
|
|||||||
|
|
||||||
// BOSS怪物 — 与英雄同一套设定(role 模板 + 品质系数),高品质承伤/输出位
|
// BOSS怪物 — 与英雄同一套设定(role 模板 + 品质系数),高品质承伤/输出位
|
||||||
6101: {
|
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, 攻速型"
|
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排Boss, 攻速型"
|
||||||
},
|
},
|
||||||
6102: {
|
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"
|
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排Boss, 超高HP"
|
||||||
},
|
},
|
||||||
6103: {
|
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"
|
skills: { 6009: { uuid: 6009, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100, hit_count: 2 } } }, info: "后排法系Boss"
|
||||||
},
|
},
|
||||||
6104: {
|
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, 均衡型"
|
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "后排位Boss, 均衡型"
|
||||||
},
|
},
|
||||||
6105: {
|
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"
|
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: {
|
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+高速"
|
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.VerySlow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排终极Boss, 最高HP+高速"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
22
assets/script/game/hero/BossPhaseComp.ts
Normal file
22
assets/script/game/hero/BossPhaseComp.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
assets/script/game/hero/BossPhaseComp.ts.meta
Normal file
9
assets/script/game/hero/BossPhaseComp.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.24",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "49d9ce6c-d575-4453-83f8-721a7865a3e5",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
137
assets/script/game/hero/BossPhaseSystem.ts
Normal file
137
assets/script/game/hero/BossPhaseSystem.ts
Normal 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 { }
|
||||||
|
}
|
||||||
9
assets/script/game/hero/BossPhaseSystem.ts.meta
Normal file
9
assets/script/game/hero/BossPhaseSystem.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.24",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "bde209ff-324b-4f07-a241-fb977cbc6c2f",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import { Tooltip } from "../skill/Tooltip";
|
|||||||
import { timedCom } from "../skill/timedCom";
|
import { timedCom } from "../skill/timedCom";
|
||||||
import { oneCom } from "../skill/oncend";
|
import { oneCom } from "../skill/oncend";
|
||||||
import { FlashSprite } from "./hit-flash-white/scripts/FlashSprite";
|
import { FlashSprite } from "./hit-flash-white/scripts/FlashSprite";
|
||||||
|
import { BossPhaseComp } from "./BossPhaseComp";
|
||||||
|
|
||||||
|
|
||||||
const { ccclass, property } = _decorator;
|
const { ccclass, property } = _decorator;
|
||||||
@@ -89,7 +90,6 @@ export class HeroViewComp extends CCComp {
|
|||||||
this.deadCD = 0;
|
this.deadCD = 0;
|
||||||
this.lastBarUpdateTime = 0;
|
this.lastBarUpdateTime = 0;
|
||||||
this.status_change("idle");
|
this.status_change("idle");
|
||||||
|
|
||||||
// 初始化头顶状态栏
|
// 初始化头顶状态栏
|
||||||
const topNode = this.node.getChildByName("top");
|
const topNode = this.node.getChildByName("top");
|
||||||
this.topBar = topNode.getComponent(HeroTopBarComp) || topNode.addComponent(HeroTopBarComp);
|
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 为正后再乘方向
|
this.node.setScale(this.scale * Math.abs(this.node.scale.x), 1 * this.node.scale.y); // 确保 scale.x 为正后再乘方向
|
||||||
|
|
||||||
// 🔥 重置描边
|
// 🔥 重置描边:Boss 阶段描边优先(Boss 存活期不被等级描边覆盖),其余按等级
|
||||||
if (this.model) {
|
const phaseComp = this.ent ? this.ent.get(BossPhaseComp) : null;
|
||||||
let flashSprite = this.node.getComponent(FlashSprite);
|
if (phaseComp && phaseComp.index > 0) {
|
||||||
if (!flashSprite) {
|
this.applyBossPhaseOutline(phaseComp.index);
|
||||||
flashSprite = this.node.getComponentInChildren(FlashSprite);
|
} else {
|
||||||
}
|
this.applyOutlineByLevel();
|
||||||
if (flashSprite) {
|
|
||||||
flashSprite.setOutlineByLevel(this.model.lv);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,16 +167,40 @@ export class HeroViewComp extends CCComp {
|
|||||||
public lv_up() {
|
public lv_up() {
|
||||||
this.spawnTimedFx("game/skill/buff/buff_lvup", this.node, 1.0);
|
this.spawnTimedFx("game/skill/buff/buff_lvup", this.node, 1.0);
|
||||||
|
|
||||||
// 升级时同步更新描边
|
// 升级时同步更新描边:Boss 阶段描边优先(与 init 口径一致)
|
||||||
if (this.model) {
|
const phaseComp = this.ent ? this.ent.get(BossPhaseComp) : null;
|
||||||
let flashSprite = this.node.getComponent(FlashSprite);
|
if (phaseComp && phaseComp.index > 0) {
|
||||||
if (!flashSprite) {
|
this.applyBossPhaseOutline(phaseComp.index);
|
||||||
flashSprite = this.node.getComponentInChildren(FlashSprite);
|
} else {
|
||||||
|
this.applyOutlineByLevel();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按英雄等级应用描边(升级/初始化共用入口) */
|
||||||
|
private applyOutlineByLevel() {
|
||||||
|
if (!this.model) return;
|
||||||
|
const flashSprite = this.getFlashSprite();
|
||||||
if (flashSprite) {
|
if (flashSprite) {
|
||||||
flashSprite.setOutlineByLevel(this.model.lv);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 攻击力提升特效 */
|
/** 攻击力提升特效 */
|
||||||
@@ -566,6 +587,13 @@ export class HeroViewComp extends CCComp {
|
|||||||
this.deadCD = 0
|
this.deadCD = 0
|
||||||
this.lastBarUpdateTime = 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.damageQueue.length = 0;
|
||||||
this.isProcessingDamage = false;
|
this.isProcessingDamage = false;
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import { MoveComp } from "./MoveComp";
|
|||||||
import { GameEvent } from "../common/config/GameEvent";
|
import { GameEvent } from "../common/config/GameEvent";
|
||||||
import { SkillTriggerType, HType } from "../common/config/heroSet";
|
import { SkillTriggerType, HType } from "../common/config/heroSet";
|
||||||
import { SkillTriggerHelper } from "./SkillTriggerHelper";
|
import { SkillTriggerHelper } from "./SkillTriggerHelper";
|
||||||
|
import { BossPhaseSet } from "../common/config/BossPhaseSet";
|
||||||
|
import { BossPhaseComp } from "./BossPhaseComp";
|
||||||
|
import "./BossPhaseSystem";
|
||||||
|
import { mLogger } from "../common/Logger";
|
||||||
/** 怪物实体:负责怪物对象池复用、属性初始化、入场动画与回收 */
|
/** 怪物实体:负责怪物对象池复用、属性初始化、入场动画与回收 */
|
||||||
@ecs.register(`Monster`)
|
@ecs.register(`Monster`)
|
||||||
export class Monster extends ecs.Entity {
|
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 引用滞留
|
// 手动移除组件,避免 ecs 引用滞留
|
||||||
this.remove(HeroViewComp);
|
this.remove(HeroViewComp);
|
||||||
this.remove(HeroAttrsComp);
|
this.remove(HeroAttrsComp);
|
||||||
@@ -134,11 +142,11 @@ export class Monster extends ecs.Entity {
|
|||||||
* 2) 初始化表现、属性、技能与阵营
|
* 2) 初始化表现、属性、技能与阵营
|
||||||
* 3) 播放下落入场并在落地后启用碰撞与移动
|
* 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
|
scale = -1
|
||||||
// 当前怪物尺寸固定,保留变量便于后续扩展
|
// Boss 体型放大 1.5 倍,小怪保持原尺寸
|
||||||
let size = 1
|
let size = is_boss ? 1.5 : 1
|
||||||
var scene = smc.map.MapView.scene;
|
var scene = smc.map.MapView.scene;
|
||||||
// 根据配置读取怪物预制体路径
|
// 根据配置读取怪物预制体路径
|
||||||
var path = "game/heros/" + HeroInfo[uuid].path;
|
var path = "game/heros/" + HeroInfo[uuid].path;
|
||||||
@@ -264,6 +272,21 @@ export class Monster extends ecs.Entity {
|
|||||||
// 缓存技能射程等派生数据,减少战斗帧内重复计算
|
// 缓存技能射程等派生数据,减少战斗帧内重复计算
|
||||||
model.updateSkillDistanceCache();
|
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);
|
this.add(view);
|
||||||
view.init();
|
view.init();
|
||||||
@@ -356,6 +379,9 @@ export class Monster extends ecs.Entity {
|
|||||||
|
|
||||||
/** 重置入口:复用 destroy 的释放流程 */
|
/** 重置入口:复用 destroy 的释放流程 */
|
||||||
reset() {
|
reset() {
|
||||||
|
if (this.has(BossPhaseComp)) {
|
||||||
|
this.remove(BossPhaseComp);
|
||||||
|
}
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export class FlashSprite extends Component {
|
|||||||
@property(Material)
|
@property(Material)
|
||||||
hitFlashMaterial: Material | null = null;
|
hitFlashMaterial: Material | null = null;
|
||||||
|
|
||||||
@property({ type: Material, tooltip: '绿色描边材质(2级)' })
|
@property({ type: Material, tooltip: '绿色描边材质(2级 / Boss阶段2)' })
|
||||||
outlineMatGreen: Material | null = null;
|
outlineMatGreen: Material | null = null;
|
||||||
|
|
||||||
@property({ type: Material, tooltip: '蓝色描边材质(3级)' })
|
@property({ type: Material, tooltip: '蓝色描边材质(3级)' })
|
||||||
@@ -19,6 +19,9 @@ export class FlashSprite extends Component {
|
|||||||
@property({ type: Material, tooltip: '黄色描边材质(5级)' })
|
@property({ type: Material, tooltip: '黄色描边材质(5级)' })
|
||||||
outlineMatYellow: Material | null = null;
|
outlineMatYellow: Material | null = null;
|
||||||
|
|
||||||
|
@property({ type: Material, tooltip: '红色描边材质(Boss阶段3,即 outline-glow)' })
|
||||||
|
outlineMatGlow: Material | null = null;
|
||||||
|
|
||||||
orginalFlashMaterial: Material | null = null;
|
orginalFlashMaterial: Material | null = null;
|
||||||
defaultMaterial: Material | null = null; // 缓存最原始的无描边材质
|
defaultMaterial: Material | null = null; // 缓存最原始的无描边材质
|
||||||
sprite: Sprite | 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() {
|
public clickFlash() {
|
||||||
if (!this.sprite || !this.hitFlashMaterial) return;
|
if (!this.sprite || !this.hitFlashMaterial) return;
|
||||||
|
|
||||||
|
|||||||
@@ -582,12 +582,15 @@ export class MissionComp extends CCComp {
|
|||||||
private autoNextPhase() {
|
private autoNextPhase() {
|
||||||
switch (this.currentPhase) {
|
switch (this.currentPhase) {
|
||||||
case MissionPhase.PrepareStart:
|
case MissionPhase.PrepareStart:
|
||||||
// 节间过场倒计时结束:推进到下一节、停背景滚动、派发 SectionStart 刷怪,进入 PrepareEnd 自动开战
|
// 节间过场倒计时结束:停背景滚动、英雄归位转待机,进入 PrepareEnd 缓冲
|
||||||
this.stopWaveCountdown();
|
this.stopWaveCountdown();
|
||||||
this.advanceToNextSection();
|
|
||||||
this.changePhase(MissionPhase.PrepareEnd);
|
this.changePhase(MissionPhase.PrepareEnd);
|
||||||
break;
|
break;
|
||||||
case MissionPhase.PrepareEnd:
|
case MissionPhase.PrepareEnd:
|
||||||
|
// 先生成下一回合怪物再进 BattleStart:此时英雄已待机约 1s,
|
||||||
|
// 怪物随"第 X 回合开始"横幅从右侧跑入,避免与英雄过场行军同帧移动;
|
||||||
|
// SectionStart 同步派发 NewWave,保证 changePhase(BattleStart) 的 isBossWave 就绪
|
||||||
|
this.advanceToNextSection();
|
||||||
this.changePhase(MissionPhase.BattleStart);
|
this.changePhase(MissionPhase.BattleStart);
|
||||||
break;
|
break;
|
||||||
case MissionPhase.BattleStart:
|
case MissionPhase.BattleStart:
|
||||||
@@ -854,7 +857,10 @@ export class MissionComp extends CCComp {
|
|||||||
* 2. 不清理英雄/技能实体、不重置局内数据、不重新三选一——英雄保留并自动归位(编队排队)。
|
* 2. 不清理英雄/技能实体、不重置局内数据、不重新三选一——英雄保留并自动归位(编队排队)。
|
||||||
* 3. 进度实时镜像到 HUD,但不写持久存档(存档只在整关通关时推进到下一关)。
|
* 3. 进度实时镜像到 HUD,但不写持久存档(存档只在整关通关时推进到下一关)。
|
||||||
*
|
*
|
||||||
* 调用时机:PrepareStart 过场倒计时结束(autoNextPhase)。
|
* 调用时机:PrepareEnd 结束、进入 BattleStart 前(autoNextPhase)。
|
||||||
|
* Why: 过场倒计时(PrepareStart)结束时英雄刚归位转待机,若同帧刷怪会出现
|
||||||
|
* 怪物与英雄一起移动的表现;推迟到 PrepareEnd 结束刷怪,英雄已待机约 1s,
|
||||||
|
* 怪物从右侧跑入与"第 X 回合开始"横幅同步登场。
|
||||||
* 保证:仅 1~4 回合清场后触发,末回合(BOSS_SECTION)已走 open_Victory 结算,不会推进。
|
* 保证:仅 1~4 回合清场后触发,末回合(BOSS_SECTION)已走 open_Victory 结算,不会推进。
|
||||||
*/
|
*/
|
||||||
private advanceToNextSection() {
|
private advanceToNextSection() {
|
||||||
|
|||||||
@@ -217,11 +217,10 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
|
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
|
||||||
|
|
||||||
// 技能套装注入:通过 _testSkills 通道传递给 Mon.load()
|
// 技能套装注入:通过 _testSkills 通道传递给 Mon.load()
|
||||||
if (monData.skills) {
|
// Why 无条件赋值:实体池复用同一 JS 对象,条件赋值会让复用为小怪的实体残留上一个 Boss 的技能
|
||||||
(mon as any)._testSkills = monData.skills;
|
(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 登场(震屏/音效由表现层消费)
|
// oops.message 全局事件总线:Boss 登场(震屏/音效由表现层消费)
|
||||||
if (monData.isBoss) {
|
if (monData.isBoss) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
import { HeroInfo, HType, MonType, TriggerGrouped, UltCdLv, UltCdSet } from "../common/config/heroSet";
|
import { HeroInfo, HType, MonType, TriggerGrouped, UltCdLv, UltCdSet } from "../common/config/heroSet";
|
||||||
import { SkillOverrides, SkillSet } from "../common/config/SkillSet";
|
import { SkillOverrides, SkillSet } from "../common/config/SkillSet";
|
||||||
|
import { BossPhaseSet, validateBossPhaseSet } from "../common/config/BossPhaseSet";
|
||||||
import { FacSet } from "../common/config/GameSet";
|
import { FacSet } from "../common/config/GameSet";
|
||||||
import {
|
import {
|
||||||
SectionType, SectionConfig, getSectionConfig, calcMonsterAttrs,
|
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;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +223,8 @@ export interface GeneratedMonster {
|
|||||||
batch: number;
|
batch: number;
|
||||||
/** 延迟刷出时间(秒),0 表示立即 */
|
/** 延迟刷出时间(秒),0 表示立即 */
|
||||||
delay?: number;
|
delay?: number;
|
||||||
|
/** Boss 阶段方案 id(引用 BossPhaseSet,由 makeBoss 从 heroInfo.phase_ids 随机取一;未配置为 undefined) */
|
||||||
|
phase_plan_id?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 怪物技能套装(技能注入通道,与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp)
|
* 怪物技能套装(技能注入通道,与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp)
|
||||||
@@ -230,12 +249,11 @@ export interface GeneratedMonster {
|
|||||||
|
|
||||||
export const TestModeConfig = {
|
export const TestModeConfig = {
|
||||||
enable: false, // 默认关闭测试模式
|
enable: false, // 默认关闭测试模式
|
||||||
baseHp: 150,
|
baseHp: 5000,
|
||||||
baseAp: 12,
|
baseAp: 12,
|
||||||
monType: MonType.Melee,
|
monType: MonType.MeleeBoss,
|
||||||
monUuid: 6001,
|
monUuid: 6101,
|
||||||
spawnCount: 1,
|
spawnCount: 1,
|
||||||
|
|
||||||
skill: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides } | undefined,
|
skill: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides } | undefined,
|
||||||
/** 测试模式注入的大招列表(追加,不替换普攻) */
|
/** 测试模式注入的大招列表(追加,不替换普攻) */
|
||||||
ults: 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,
|
atked: undefined as TriggerGrouped | undefined,
|
||||||
dead: undefined as TriggerGrouped | undefined,
|
dead: undefined as TriggerGrouped | undefined,
|
||||||
fstart: undefined as TriggerGrouped | undefined,
|
fstart: undefined as TriggerGrouped | undefined,
|
||||||
|
|
||||||
|
/** 测试模式:Boss 阶段方案 id(0 = 不启用阶段;需配合 isBoss: true 才会挂载组件) */
|
||||||
|
phasePlanId: 10001,
|
||||||
|
/** 测试模式:是否以 Boss 身份刷出(影响层级/金币/阶段组件挂载) */
|
||||||
|
isBoss: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ======================== 8. 生成引擎 ========================
|
// ======================== 8. 生成引擎 ========================
|
||||||
@@ -366,10 +389,11 @@ export class StageSpawningEngine {
|
|||||||
type: TestModeConfig.monType,
|
type: TestModeConfig.monType,
|
||||||
hp: TestModeConfig.baseHp,
|
hp: TestModeConfig.baseHp,
|
||||||
ap: TestModeConfig.baseAp,
|
ap: TestModeConfig.baseAp,
|
||||||
isBoss: false,
|
isBoss: TestModeConfig.isBoss,
|
||||||
lv: 1,
|
lv: 1,
|
||||||
spawnIndex: i,
|
spawnIndex: i,
|
||||||
batch: 0,
|
batch: 0,
|
||||||
|
phase_plan_id: TestModeConfig.phasePlanId || undefined,
|
||||||
skills: {
|
skills: {
|
||||||
skill: TestModeConfig.skill,
|
skill: TestModeConfig.skill,
|
||||||
ults: TestModeConfig.ults,
|
ults: TestModeConfig.ults,
|
||||||
@@ -408,6 +432,7 @@ export class StageSpawningEngine {
|
|||||||
skills: {
|
skills: {
|
||||||
ults: this.pickBossUlts(range, stage),
|
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)];
|
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)
|
* 按定位抽取 Boss 大招(不重复),数量由关卡进度决定(getBossUltCount)
|
||||||
* @returns 大招配置列表;池空时返回 undefined
|
* @returns 大招配置列表;池空时返回 undefined
|
||||||
|
|||||||
797
docs/superpowers/plans/2026-10-06-boss-phase-system.md
Normal file
797
docs/superpowers/plans/2026-10-06-boss-phase-system.md
Normal 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 编译无报错
|
||||||
306
docs/superpowers/specs/2026-10-06-boss-phase-content-design.md
Normal file
306
docs/superpowers/specs/2026-10-06-boss-phase-content-design.md
Normal 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`(仅理念借鉴)
|
||||||
330
docs/superpowers/specs/2026-10-06-boss-phase-system-design.md
Normal file
330
docs/superpowers/specs/2026-10-06-boss-phase-system-design.md
Normal 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 整表重建保证首键契约
|
||||||
Reference in New Issue
Block a user