From 31a2efee4d8433f83338baa578af206f50ab0281 Mon Sep 17 00:00:00 2001 From: panFD Date: Wed, 7 Oct 2026 15:32:36 +0800 Subject: [PATCH] feat: add BossPhaseSet config with phase plan schema --- .../script/game/common/config/BossPhaseSet.ts | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 assets/script/game/common/config/BossPhaseSet.ts diff --git a/assets/script/game/common/config/BossPhaseSet.ts b/assets/script/game/common/config/BossPhaseSet.ts new file mode 100644 index 00000000..5c5a9eea --- /dev/null +++ b/assets/script/game/common/config/BossPhaseSet.ts @@ -0,0 +1,146 @@ +/** + * @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 = { + 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; +}