11 Commits

Author SHA1 Message Date
panFD
53a0ee1c6e refactor(hero): adjust monster prefab sizes and add boss scaling logic
调整多个怪物预制体的尺寸参数,同时在怪物基类中添加 Boss 体型放大 1.5 倍的逻辑,小怪保持原尺寸。
2026-10-07 17:29:45 +08:00
panFD
f679644bbb feat: 添加 Boss 阶段系统及测试配置
新增 BossPhaseSystem 脚本及元文件,添加阶段挂载与接管的日志记录
修改测试模式配置,调整 Boss 相关属性以适配阶段系统
2026-10-07 17:20:25 +08:00
panFD
541ca0e251 chore: add Cocos meta files for new phase scripts 2026-10-07 15:44:57 +08:00
panFD
7703b0348c fix: pass phase plan id and clear stale test skills on reuse 2026-10-07 15:44:33 +08:00
panFD
ad50e11052 feat: add BossPhaseSystem with phase switch logic 2026-10-07 15:43:17 +08:00
panFD
d5158cde52 feat: wire BossPhaseComp into monster load/destroy 2026-10-07 15:39:25 +08:00
panFD
8176ef098e feat: consume phase plans in boss generation 2026-10-07 15:36:20 +08:00
panFD
56dcdccca2 feat: add phase_ids field to boss hero config 2026-10-07 15:33:45 +08:00
panFD
31a2efee4d feat: add BossPhaseSet config with phase plan schema 2026-10-07 15:32:36 +08:00
panFD
761d0aa68e feat: add BossPhaseChange game event 2026-10-07 15:32:21 +08:00
panFD
7766909294 feat: add BossPhaseComp marker component 2026-10-07 15:32:15 +08:00
16 changed files with 423 additions and 39 deletions

View File

@@ -812,8 +812,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -921,12 +921,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 75
"y": 50
},
"_size": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_id": ""
},

View File

@@ -809,8 +809,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -918,12 +918,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 75
"y": 50
},
"_size": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 60,
"height": 100
},
"_id": ""
},

View File

@@ -812,8 +812,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -921,12 +921,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 75
"y": 50
},
"_size": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 8,
"height": 100
},
"_id": ""
},

View File

@@ -809,8 +809,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -918,12 +918,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 75
"y": 50
},
"_size": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_id": ""
},

View File

@@ -809,8 +809,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -918,12 +918,12 @@
"_offset": {
"__type__": "cc.Vec2",
"x": 0,
"y": 75
"y": 50
},
"_size": {
"__type__": "cc.Size",
"width": 150,
"height": 150
"width": 80,
"height": 100
},
"_id": ""
},

View File

@@ -0,0 +1,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<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;
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { v3 } from "cc"
import { v3 } from "cc"
import { BoxSet, FacSet, FightSet } from "./GameSet"
import { SkillOverrides, TGroup, SkillSet, FieldSkillSet, SkillConfig, SkillKind, FieldSkillConfig, FieldSkillType, mergeSkillParams, BuffTarget } from "./SkillSet"
import { BuffList, BuffConfig, ModOp } from "./BuffSet"
@@ -430,6 +430,7 @@ export interface heroInfo {
speed?: number; // 移动速度(像素/秒)
dodge?: number; // 闪避率(百分点,缺省由初始化处兜底为全局默认值)
accuracy?: number; // 命中率(百分点,缺省由初始化处兜底为全局默认值)
phase_ids?: number[]; // Boss 专属:允许的阶段方案 id 列表(引用 BossPhaseSet,生成时随机取一)
skills: Record<number, HSkillInfo>; // 携带技能ID列表
bonus?: LvBonusAttrsEntry[]; // 额外属性加成(按 lv 升序,运行时累加所有 lv ≤ 英雄等级 的档位 attrs;hp/ap/def 按"基础×品质+成长"快照百分比加成,crt/frz/stun/para 为固定百分点累加)
info: string; // 描述文案
@@ -1093,7 +1094,7 @@ export const HeroInfo: Record<number, heroInfo> = {
// BOSS怪物 — 与英雄同一套设定(role 模板 + 品质系数),高品质承伤/输出位
6101: {
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,
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],
skills: { 6004: { uuid: 6004, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow].cd, ccd: 0, overrides: { ap: 100 } } }, info: "前排Boss, 攻速型"
},
6102: {

View File

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

View File

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

View File

@@ -0,0 +1,129 @@
/**
* 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;
// 分诊日志:系统接管确认(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,
});
}
/** TODO: 阶段退场表现(震屏/变暗等),后续迭代实现 */
private onPhaseExit(e: ecs.Entity): void { }
/** TODO: 阶段入场表现(怒吼/无敌帧/技能横幅等),后续迭代实现 */
private onPhaseEnter(e: ecs.Entity): void { }
}

View File

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

View File

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

View File

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

View File

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