Files
pixelheros/assets/script/game/map/RogueConfig.ts
panFD 1b2545deec refactor(rogue): 重构关卡刷怪与流程为一关一局模式
1.  将原10小节/关改为5小节/关,固定节类型为1-3普通节、4精英节、5Boss节
2.  调整刷怪逻辑:普通节3只小怪立即全刷,移除定时分批刷怪
3.  统一怪物入场方式为从右侧屏幕外贴地跑入,移除高空掉落动画
4.  移除小节间结算弹窗,改为自动连战,节间仅播放背景滚动过渡
5.  简化结算逻辑,仅保留通关/失败两种结算类型,移除小节胜利分支
6.  重构关卡进度存储,仅保存章节关卡,小节恒从1开始
2026-09-12 19:03:48 +08:00

514 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file RogueConfig.ts
* @description 关卡刷怪系统(章节-关-节三级推进)
*
* 游戏机制:
* - 玩家线性推进 1 章 10 关 × 每关 5 小节(一关一局,节间自动推进)。
* - 每关节点固定:1~3 节普通(3 小怪,立即全刷)、第 4 节精英(3 精英同刷)、
* 第 5 节 Boss(2 小怪先刷,Boss 延迟 1s 刷出)。
* - 怪物属性 = 基础 × 0.8 × (1 + 0.05×(lv-1)) × 类型倍率(精英1.5 / Boss3.0)。
*
* 设计层次:
* 1. StageSet - 关卡配置(章节/关/节 + 数值公式)
* 2. MonSkillPool/BossSkillPool - 怪物技能池(atking / atked / dead 等全触发类型)
* 3. StageSpawningEngine - 生成引擎(按小节配置生成怪物列表)
*
* 核心公式:
* 最终属性 = HeroInfo 基础属性 × MON_START_COEF × (1 + MON_GROW_SLOPE×(lv-1)) × 类型倍率
* 1 级小怪 ≈ 英雄 × 0.8,10 级小怪 ≈ 英雄 × 1.16,保持平缓成长。
*/
import { HeroInfo, MonType, MonTypeName, TriggerGrouped } from "../common/config/heroSet";
import { SkillOverrides } from "../common/config/SkillSet";
import { FacSet } from "../common/config/GameSet";
import {
SectionType, SectionConfig, getSectionConfig, calcMonsterAttrs,
NORMAL_MON_COUNT, NORMAL_FIRST_SPAWN, ELITE_COUNT, BOSS_ELITE_COUNT, BOSS_DELAY,
MON_START_COEF, MON_GROW_SLOPE, ELITE_MUL, BOSS_MUL,
} from "../common/config/StageSet";
// ======================== 1. 战斗常量 ========================
/** 单场战斗时长基准(秒):供清屏速度评价(BattleBannerComp 清屏奖励)等使用 */
export const WAVE_DURATION = 30;
/** 战斗超时(秒):超过后强制结算整局(残留怪销毁并扣留存分) */
export const BATTLE_TIMEOUT = 90;
// ======================== 2. 怪物 UUID 池 ========================
/** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */
export const MonList: Record<number, number[]> = {};
for (const key in HeroInfo) {
const info = HeroInfo[key];
if (info.fac === FacSet.MON && info.monType !== undefined) {
if (!MonList[info.monType]) {
MonList[info.monType] = [];
}
MonList[info.monType].push(info.uuid);
}
}
// ======================== 2.5 怪物金币掉落配置 ========================
/**
* 怪物金币掉落配置(按 MonType 分类)
* - base: 普通怪物死亡时掉落的固定金币数
* - boss: Boss 怪物死亡时掉落的固定金币数(仅对 Boss 类型生效)
*/
export const MonsterGoldSet: Record<number, { base: number; boss: number }> = {
[MonType.Melee]: { base: 1, boss: 0 },
[MonType.Heavy]: { base: 2, boss: 0 },
[MonType.Long]: { base: 2, boss: 0 },
[MonType.Support]: { base: 3, boss: 0 },
[MonType.Summoner]: { base: 3, boss: 0 },
[MonType.Assassin]: { base: 3, boss: 0 },
[MonType.MeleeBoss]: { base: 0, boss: 15 },
[MonType.LongBoss]: { base: 0, boss: 15 },
};
/**
* 获取指定怪物类型的金币掉落数量
* @param monType 怪物类型(MonType)
* @param isBoss 是否为 Boss
* @returns 掉落的金币数量(≥0)
*/
export function getMonsterGoldDrop(monType: number, isBoss: boolean): number {
const cfg = MonsterGoldSet[monType];
if (!cfg) return 0;
return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base));
}
// ======================== 3. 怪物技能池 ========================
/**
* 怪物技能套装(一套完整的触发技能配置)
* 覆盖触发时机:call / atking / atked / dead / fstart / revive
*/
export interface MonSkillSet {
id: string;
name: string;
/** 普攻技能覆盖(可选,不填则使用怪物默认普攻) */
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
/** 召唤触发 */
call?: TriggerGrouped;
/** 攻击触发 */
atking?: TriggerGrouped;
/** 受击触发 */
atked?: TriggerGrouped;
/** 死亡触发 */
dead?: TriggerGrouped;
/** 战斗开始触发 */
fstart?: TriggerGrouped;
/** 复活触发 */
revive?: TriggerGrouped;
/** 在技能池中的抽取权重 */
weight: number;
}
/**
* 怪物技能池(硬编码)
* 技能 uuid 引用 SkillSet 中的 6000~6500 段触发技能。
*/
export const MonSkillPool: Record<string, MonSkillSet> = {
/** 狂暴:攻击触发自身攻击提升 */
berserk: {
id: "berserk", name: "狂暴", weight: 10,
atking: {
6401: [{ lv: 1, t_num: 5, overrides: { ap: 1 } }],
},
},
/** 坚韧:受击获得护盾 */
tough: {
id: "tough", name: "坚韧", weight: 8,
atked: {
6301: [{ lv: 1, t_num: 3, overrides: { ap: 2 } }],
},
},
/** 遗志:死亡时全队攻击提升 */
legacy: {
id: "legacy", name: "遗志", weight: 6,
dead: {
6401: [{ lv: 1, t_num: 1, overrides: { ap: 3 } }],
},
},
/** 战吼:战斗开始时全队攻击提升 */
warcry: {
id: "warcry", name: "战吼", weight: 5,
fstart: {
6401: [{ lv: 1, t_num: 1, overrides: { ap: 2 } }],
},
},
/** 吸血:攻击恢复生命 */
leech: {
id: "leech", name: "吸血", weight: 7,
atking: {
6302: [{ lv: 1, t_num: 4, overrides: { ap: 150 } }],
},
},
};
/** Boss 专属技能池 */
export const BossSkillPool: Record<string, MonSkillSet> = {
/** 狂暴领主:攻击触发全队攻击提升 */
boss_rage: {
id: "boss_rage", name: "狂暴领主", weight: 10,
atking: {
6401: [{ lv: 1, t_num: 3, overrides: { ap: 3 } }],
},
dead: {
6401: [{ lv: 1, t_num: 1, overrides: { ap: 5 } }],
},
},
/** 铁壁领主:受击获得高额护盾 */
boss_iron: {
id: "boss_iron", name: "铁壁领主", weight: 8,
atked: {
6301: [{ lv: 1, t_num: 2, overrides: { ap: 5 } }],
},
},
/** 毁灭领主:战斗开始时全队攻击大幅提升 */
boss_doom: {
id: "boss_doom", name: "毁灭领主", weight: 6,
fstart: {
6401: [{ lv: 1, t_num: 1, overrides: { ap: 8 } }],
},
atking: {
6401: [{ lv: 1, t_num: 5, overrides: { ap: 2 } }],
},
},
};
// ======================== 4. 配置校验 ========================
/**
* 校验 MonSkillPool / BossSkillPool 引用一致性
* 建议在游戏启动时调用一次,便于发现配置错误
* @returns 错误信息数组,空数组表示校验通过
*/
export function validateRogueConfig(): string[] {
const errors: string[] = [];
// 校验 Boss 类型在 MonList 中有怪
if ((!MonList[MonType.MeleeBoss] || MonList[MonType.MeleeBoss].length === 0)
&& (!MonList[MonType.LongBoss] || MonList[MonType.LongBoss].length === 0)) {
errors.push("MonList 中无任何 Boss 类型(MeleeBoss/LongBoss)可用 uuid");
}
return errors;
}
// ======================== 5. 生成结果接口 ========================
/** 生成结果(传递给 MissionMonComp 用于实例化怪物) */
export interface GeneratedMonster {
uuid: number;
type: MonType;
hp: number;
ap: number;
isBoss: boolean;
/** 是否为精英(精英关/Boss 关的精英) */
isElite?: boolean;
/** 是否为 Boss 护卫队成员(供 UI/统计识别) */
isBossGuard?: boolean;
/** 怪物等级(驱动技能等级增量,Mon.load 第 6 参) */
lv: number;
spawnIndex: number;
/** 本怪所属批次(0=首批立即刷,1=后续定时刷) */
batch: number;
/** 延迟刷出时间(秒),0 表示立即 */
delay?: number;
/**
* 怪物技能套装(覆盖全部触发时机)
* 注入方式与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp
*/
skills?: {
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
call?: TriggerGrouped;
atking?: TriggerGrouped;
atked?: TriggerGrouped;
dead?: TriggerGrouped;
fstart?: TriggerGrouped;
revive?: TriggerGrouped;
};
}
// ======================== 6. 测试模式配置 ========================
export const TestModeConfig = {
enable: false, // 默认关闭测试模式
baseHp: 150,
baseAp: 12,
monType: MonType.Melee,
monUuid: 6001,
spawnCount: 1,
skill: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides } | undefined,
atking: undefined as TriggerGrouped | undefined,
atked: undefined as TriggerGrouped | undefined,
dead: undefined as TriggerGrouped | undefined,
fstart: undefined as TriggerGrouped | undefined,
};
// ======================== 7. 生成引擎 ========================
/**
* 关卡刷怪生成引擎
* 按章节-关-节配置生成怪物列表,属性走 StageSet 数值公式
*/
export class StageSpawningEngine {
/**
* 生成指定小节的怪物列表
* @param chapter 章节号
* @param stage 关卡号
* @param section 小节号(1~10)
* @returns 怪物列表(按刷出顺序排列)
*/
generateSectionMonsters(chapter: number, stage: number, section: number): GeneratedMonster[] {
// 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪
if (TestModeConfig.enable) {
return this.generateTestWave();
}
const cfg = getSectionConfig(chapter, stage, section);
if (!cfg) {
// 配置缺失兜底:生成 3 只 Lv1 近战小怪
return this.makeFallbackMonsters();
}
switch (cfg.type) {
case SectionType.Elite:
return this.generateEliteSection(cfg);
case SectionType.Boss:
return this.generateBossSection(cfg);
case SectionType.Normal:
default:
return this.generateNormalSection(cfg);
}
}
/** 重置引擎状态(每局开始时调用,预留) */
reset(): void {
}
/**
* 普通节:NORMAL_MON_COUNT 只小怪全部立即刷出(batch=0)
*/
private generateNormalSection(cfg: SectionConfig): GeneratedMonster[] {
const monsters: GeneratedMonster[] = [];
const types = cfg.monTypes ?? [MonType.Melee];
for (let i = 0; i < NORMAL_MON_COUNT; i++) {
const type = types[i % types.length];
const mon = this.makeMonster(type, cfg.monLv, i);
mon.batch = i < NORMAL_FIRST_SPAWN ? 0 : 1;
monsters.push(mon);
}
return monsters;
}
/**
* 精英节:3 只精英同时刷出
*/
private generateEliteSection(cfg: SectionConfig): GeneratedMonster[] {
const monsters: GeneratedMonster[] = [];
const count = cfg.eliteCount ?? ELITE_COUNT;
// 精英池:近战/重型/远程/刺客
const eliteTypes = [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Assassin];
for (let i = 0; i < count; i++) {
const type = eliteTypes[i % eliteTypes.length];
const mon = this.makeMonster(type, cfg.monLv, i);
mon.isElite = true;
mon.batch = 0;
// 精英属性 = 普通 × ELITE_MUL
mon.hp = Math.max(1, Math.round(mon.hp * ELITE_MUL));
mon.ap = Math.max(1, Math.round(mon.ap * ELITE_MUL));
monsters.push(mon);
}
return monsters;
}
/**
* Boss 节:2 小怪护卫 + 1 Boss
* 小怪先刷(batch=0),Boss 延迟刷出(batch=0, delay=BOSS_DELAY)
*/
private generateBossSection(cfg: SectionConfig): GeneratedMonster[] {
const monsters: GeneratedMonster[] = [];
const eliteCount = cfg.eliteCount ?? BOSS_ELITE_COUNT;
const eliteTypes = [MonType.Melee, MonType.Heavy];
// 先刷精英
for (let i = 0; i < eliteCount; i++) {
const type = eliteTypes[i % eliteTypes.length];
const mon = this.makeMonster(type, cfg.monLv, i);
mon.isElite = true;
mon.isBossGuard = true;
mon.batch = 0;
mon.hp = Math.max(1, Math.round(mon.hp * ELITE_MUL));
mon.ap = Math.max(1, Math.round(mon.ap * ELITE_MUL));
monsters.push(mon);
}
// Boss 延迟刷出
const boss = this.makeBoss(cfg.monLv, monsters.length);
boss.batch = 0;
boss.delay = BOSS_DELAY;
// Boss 属性 = 普通 × BOSS_MUL
boss.hp = Math.max(1, Math.round(boss.hp * BOSS_MUL));
boss.ap = Math.max(1, Math.round(boss.ap * BOSS_MUL));
monsters.push(boss);
return monsters;
}
/** 配置缺失兜底:3 只 Lv1 近战小怪 */
private makeFallbackMonsters(): GeneratedMonster[] {
const monsters: GeneratedMonster[] = [];
for (let i = 0; i < 3; i++) {
monsters.push({
uuid: 6001,
type: MonType.Melee,
hp: Math.round(560 * MON_START_COEF),
ap: Math.round(22 * MON_START_COEF),
isBoss: false,
lv: 1,
spawnIndex: i,
batch: 0,
});
}
return monsters;
}
/** 测试模式:完全绕过引擎逻辑 */
private generateTestWave(): GeneratedMonster[] {
const count = Math.max(1, TestModeConfig.spawnCount || 1);
const monsters: GeneratedMonster[] = [];
for (let i = 0; i < count; i++) {
monsters.push({
uuid: TestModeConfig.monUuid,
type: TestModeConfig.monType,
hp: TestModeConfig.baseHp,
ap: TestModeConfig.baseAp,
isBoss: false,
lv: 1,
spawnIndex: i,
batch: 0,
skills: {
skill: TestModeConfig.skill,
atking: TestModeConfig.atking,
atked: TestModeConfig.atked,
dead: TestModeConfig.dead,
fstart: TestModeConfig.fstart,
},
});
}
return monsters;
}
/** 生成 Boss(基础面板按 StageSet 公式计算) */
private makeBoss(monLv: number, spawnIndex: number): GeneratedMonster {
const isMeleeBoss = Math.random() < 0.5;
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
// Boss 类型兜底:若 MonList 缺失则退回 MeleeBoss
let uuids = MonList[type];
if (!uuids || uuids.length === 0) {
uuids = MonList[MonType.MeleeBoss] || MonList[MonType.Melee] || [6001];
}
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
// Boss 基础面板取 HeroInfo 基础值,属性由调用方乘 BOSS_MUL
const baseInfo = HeroInfo[uuid];
const baseHp = baseInfo ? baseInfo.hp : 600;
const baseAp = baseInfo ? baseInfo.ap : 30;
const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, false, false);
return {
uuid,
type,
hp: attrs.hp,
ap: attrs.ap,
isBoss: true,
lv: monLv,
spawnIndex,
batch: 0,
};
}
/** 生成普通怪物(属性按 StageSet 公式计算) */
private makeMonster(type: MonType, monLv: number, spawnIndex: number): GeneratedMonster {
let uuids = MonList[type];
if (!uuids || uuids.length === 0) {
// 兜底 Melee
uuids = MonList[MonType.Melee] || [6001];
}
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
const baseInfo = HeroInfo[uuid];
const baseHp = baseInfo ? baseInfo.hp : 100;
const baseAp = baseInfo ? baseInfo.ap : 10;
const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, false, false);
return {
uuid,
type,
hp: attrs.hp,
ap: attrs.ap,
isBoss: false,
lv: monLv,
spawnIndex,
batch: 0,
};
}
/** 从技能池中按权重抽取一套技能 */
private pickSkillSet(pool: string[], library: Record<string, MonSkillSet>): GeneratedMonster["skills"] | undefined {
const valid = pool.map(id => library[id]).filter(s => s);
if (valid.length === 0) return undefined;
const picked = this.pickWeighted(valid);
if (!picked) return undefined;
return {
skill: picked.skill,
call: picked.call,
atking: picked.atking,
atked: picked.atked,
dead: picked.dead,
fstart: picked.fstart,
revive: picked.revive,
};
}
/** 通用加权随机抽取 */
private pickWeighted<T extends { weight: number }>(items: T[]): T | null {
if (items.length === 0) return null;
const total = items.reduce((s, it) => s + it.weight, 0);
if (total <= 0) return items[Math.floor(Math.random() * items.length)];
let r = Math.random() * total;
for (const it of items) {
r -= it.weight;
if (r <= 0) return it;
}
return items[items.length - 1];
}
}
// ======================== 8. 全局单例 ========================
export const spawningEngine = new StageSpawningEngine();
// 保留旧接口兼容(逐步迁移期使用)
/** @deprecated 请使用 StageSpawningEngine.generateSectionMonsters */
export class RogueSpawningEngine extends StageSpawningEngine {
/** @deprecated 请使用 generateSectionMonsters */
generateBossCombo(): GeneratedMonster[] {
return this.generateSectionMonsters(1, 1, 1);
}
}
/** @deprecated 请使用 spawningEngine */
export const rogueSpawningEngine = new RogueSpawningEngine();