Files
pixelheros/assets/script/game/map/RogueConfig.ts

474 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 关起无上限,每关 5 回合(一关一局,回合间自动推进)。
* - 第 1~4 回合普通怪(3 小怪立即全刷),属性逐回合轻量增强(SECTION_RAMP_STEP)。
* - 第 5 回合单 Boss 登场(无护卫,延迟 1s 刷出),超厚血量(BOSS_HP_MUL)。
* - Boss 加载时按攻击定位(近战/中程/远程)随机取型,并随机携带大招:
* 前期 1 个,随关卡解锁扩展至多个(BOSS_ULT_UNLOCK_EVERY / BOSS_ULT_MAX_COUNT)。
*
* 设计层次:
* 1. StageSet - 关卡数值公式(等级/成长/回合增强/Boss 血攻倍率)
* 2. BossPools - Boss 池(按攻击定位分组,动态从 HeroInfo 提取)
* 3. BossUltPools - Boss 大招池(按定位分组,引用 SkillSet 6100~6204 群体大招段)
* 4. StageSpawningEngine - 生成引擎(按回合配置生成怪物列表)
*
* 核心公式:
* 小怪属性 = HeroInfo 基础属性 × MON_START_COEF × (1 + MON_GROW_SLOPE×(lv-1)) × 回合增强
* Boss 属性 = 同上 × BOSS_HP_MUL(血)/ BOSS_AP_MUL(攻),等级 = 关卡号无限成长
*/
import { HeroInfo, HType, MonType, TriggerGrouped, UltCdLv, UltCdSet } from "../common/config/heroSet";
import { SkillOverrides, SkillSet } from "../common/config/SkillSet";
import { FacSet } from "../common/config/GameSet";
import {
SectionType, SectionConfig, getSectionConfig, calcMonsterAttrs,
NORMAL_MON_COUNT, NORMAL_FIRST_SPAWN, BOSS_DELAY,
MON_START_COEF,
} 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. Boss 池(按攻击定位分组) ========================
/**
* Boss 池:按攻击定位(近战/中程/远程)分组,动态从 HeroInfo 提取。
* 加载时随机一个定位 → 从对应池随机 Boss;定位池为空时自动回退(如暂无中程 Boss 配置)。
*/
export const BossPools: Record<HType, number[]> = {
[HType.Melee]: [],
[HType.Mid]: [],
[HType.Long]: [],
};
for (const key in HeroInfo) {
const info = HeroInfo[key];
if (info.fac !== FacSet.MON) continue;
if (info.monType !== MonType.MeleeBoss && info.monType !== MonType.LongBoss) continue;
const pool = BossPools[info.type as HType];
if (pool) pool.push(info.uuid);
}
// ======================== 4. Boss 大招池 ========================
/** 每解锁 1 个额外大招收紧的关卡间隔(每 5 关 +1 个大招位) */
export const BOSS_ULT_UNLOCK_EVERY = 5;
/** Boss 同时携带大招数量上限 */
export const BOSS_ULT_MAX_COUNT = 3;
/**
* 获取指定关卡的 Boss 携带大招数量(1 个起步,随关卡解锁扩展)
* @param stage 关卡号(1~∞)
* @returns 大招数量(1~BOSS_ULT_MAX_COUNT)
*/
export function getBossUltCount(stage: number): number {
const st = Math.max(1, Math.floor(stage));
return Math.min(BOSS_ULT_MAX_COUNT, 1 + Math.floor((st - 1) / BOSS_ULT_UNLOCK_EVERY));
}
/** Boss 大招条目(s_uuid 引用 SkillSet 6100~6204 群体大招段,按定位分组) */
export interface BossUltEntry {
s_uuid: number;
name: string;
/** 抽取权重 */
weight: number;
/** 技能参数覆盖(伤害系数等) */
overrides?: SkillOverrides;
}
/**
* Boss 大招池(按攻击定位分组)
* 大招注入 Boss 的 skills 表后由 SCastSystem 按 CD 自动施放(含镜头聚焦 + 技能名横幅)。
*/
export const BossUltPools: Record<HType, BossUltEntry[]> = {
// 近战系:战士群体斩击(6100~6104 段)
[HType.Melee]: [
{ s_uuid: 6100, name: "寒冰斩", weight: 10, overrides: { ap: 180 } },
{ s_uuid: 6101, name: "裂气斩", weight: 10, overrides: { ap: 180 } },
{ s_uuid: 6102, name: "火焰斩", weight: 10, overrides: { ap: 180 } },
{ s_uuid: 6103, name: "闪电斩", weight: 8, overrides: { ap: 180 } },
{ s_uuid: 6104, name: "开天斩", weight: 6, overrides: { ap: 220 } },
],
// 中程系:射手箭雨 + 混合法术(6150~6151 / 6203~6204 段)
[HType.Mid]: [
{ s_uuid: 6150, name: "万箭齐发", weight: 10, overrides: { ap: 150 } },
{ s_uuid: 6151, name: "火箭齐发", weight: 10, overrides: { ap: 150 } },
{ s_uuid: 6203, name: "能量波", weight: 8, overrides: { ap: 150 } },
{ s_uuid: 6204, name: "雷暴", weight: 8, overrides: { ap: 150 } },
],
// 远程系:法师群体法术(6200~6204 段)
[HType.Long]: [
{ s_uuid: 6200, name: "火球术", weight: 10, overrides: { ap: 160 } },
{ s_uuid: 6201, name: "飓风", weight: 10, overrides: { ap: 140 } },
{ s_uuid: 6202, name: "毒气弹", weight: 8, overrides: { ap: 150 } },
{ s_uuid: 6203, name: "能量波", weight: 8, overrides: { ap: 150 } },
{ s_uuid: 6204, name: "雷暴", weight: 8, overrides: { ap: 150 } },
],
};
// ======================== 5. 配置校验 ========================
/**
* 校验 Boss 池 / Boss 大招池引用一致性
* 建议在游戏启动时调用一次,便于发现配置错误
* @returns 错误信息数组,空数组表示校验通过
*/
export function validateRogueConfig(): string[] {
const errors: string[] = [];
// 校验至少一个定位有可用 Boss
const hasBoss = (Object.keys(BossPools) as unknown as HType[]).some(t => BossPools[t].length > 0);
if (!hasBoss) {
errors.push("BossPools 中无任何可用 Boss uuid(MeleeBoss/LongBoss 均缺失)");
}
// 校验大招池条目均存在于 SkillSet
for (const key in BossUltPools) {
const pool = BossPools[key as unknown as HType];
const ults = BossUltPools[key as unknown as HType];
if (!pool || pool.length === 0) continue; // 空定位池跳过(大招池无消费方)
for (const ult of ults) {
if (!SkillSet[ult.s_uuid]) {
errors.push(`BossUltPools[${key}] 大招 ${ult.s_uuid}(${ult.name}) 未在 SkillSet 中定义`);
}
}
}
return errors;
}
// ======================== 6. 生成结果接口 ========================
/** 生成结果(传递给 MissionMonComp 用于实例化怪物) */
export interface GeneratedMonster {
uuid: number;
type: MonType;
hp: number;
ap: number;
isBoss: boolean;
/** 是否为精英(对象池复用实体需显式重置的标记位) */
isElite?: boolean;
/** 怪物等级(驱动技能等级增量,Mon.load 第 6 参) */
lv: number;
spawnIndex: number;
/** 本怪所属批次(0=首批立即刷,1=后续定时刷) */
batch: number;
/** 延迟刷出时间(秒),0 表示立即 */
delay?: number;
/**
* 怪物技能套装(技能注入通道,与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp)
* - skill:替换普攻(会清空原技能表)
* - ults:追加 Boss 大招(保留普攻,多个大招共存,SCastSystem 按 CD 轮转施放)
* - 其余为触发技能组覆盖
*/
skills?: {
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
/** Boss 大招列表(追加注入,不替换普攻) */
ults?: { s_uuid: number; cd?: number; overrides?: SkillOverrides }[];
call?: TriggerGrouped;
atking?: TriggerGrouped;
atked?: TriggerGrouped;
dead?: TriggerGrouped;
fstart?: TriggerGrouped;
revive?: TriggerGrouped;
};
}
// ======================== 7. 测试模式配置 ========================
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,
/** 测试模式注入的大招列表(追加,不替换普攻) */
ults: 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,
};
// ======================== 8. 生成引擎 ========================
/**
* 无限关卡刷怪生成引擎
* 按关-回合配置生成怪物列表,属性走 StageSet 数值公式
*/
export class StageSpawningEngine {
/**
* 生成指定回合的怪物列表
* @param chapter 章节号(无限关模式已废弃章节概念,兼容参数恒忽略)
* @param stage 关卡号(1~∞)
* @param section 回合号(1~SECTIONS_PER_STAGE)
* @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();
}
return cfg.type === SectionType.Boss
? this.generateBossSection(cfg, stage)
: this.generateNormalSection(cfg);
}
/** 重置引擎状态(每局开始时调用,预留) */
reset(): void {
}
/**
* 普通回合(1~4):NORMAL_MON_COUNT 只小怪全部立即刷出(batch=0),
* 属性乘回合增强系数(cfg.ramp)实现逐回合轻量增强
*/
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;
// 回合内轻量增强:第 n 回合 × (1 + SECTION_RAMP_STEP×(n-1))
mon.hp = Math.max(1, Math.round(mon.hp * cfg.ramp));
mon.ap = Math.max(1, Math.round(mon.ap * cfg.ramp));
monsters.push(mon);
}
return monsters;
}
/**
* Boss 回合(5):单 Boss 登场(无护卫)
* Boss 延迟 BOSS_DELAY 秒刷出,属性由 makeBoss 按血/攻分离倍率计算(超厚血量)
*/
private generateBossSection(cfg: SectionConfig, stage: number): GeneratedMonster[] {
const boss = this.makeBoss(cfg.monLv, stage, 0);
boss.batch = 0;
boss.delay = BOSS_DELAY;
return [boss];
}
/** 生成普通怪物(属性按 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);
return {
uuid,
type,
hp: attrs.hp,
ap: attrs.ap,
isBoss: false,
lv: monLv,
spawnIndex,
batch: 0,
};
}
/** 配置缺失兜底: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,
ults: TestModeConfig.ults,
atking: TestModeConfig.atking,
atked: TestModeConfig.atked,
dead: TestModeConfig.dead,
fstart: TestModeConfig.fstart,
},
});
}
return monsters;
}
/**
* 生成 Boss:加载时随机攻击定位 → 从对应池随机 Boss 个体 → 随机携带大招
* 属性走 calcMonsterAttrs(isBoss=true):HP × BOSS_HP_MUL(超厚血)/ AP × BOSS_AP_MUL
*/
private makeBoss(monLv: number, stage: number, spawnIndex: number): GeneratedMonster {
const range = this.pickBossRange();
const uuid = this.pickBossUuid(range);
const baseInfo = HeroInfo[uuid];
const baseHp = baseInfo ? baseInfo.hp : 600;
const baseAp = baseInfo ? baseInfo.ap : 30;
const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, true);
return {
uuid,
type: baseInfo?.monType ?? MonType.MeleeBoss,
hp: attrs.hp,
ap: attrs.ap,
isBoss: true,
lv: monLv,
spawnIndex,
batch: 0,
skills: {
ults: this.pickBossUlts(range, stage),
},
};
}
/**
* 随机 Boss 攻击定位(近战/中程/远程)
* 空池自动回退:中程暂无 Boss 配置时落到远程/近战,后续补配即自动生效
*/
private pickBossRange(): HType {
const all: HType[] = [HType.Melee, HType.Mid, HType.Long];
const first = all[Math.floor(Math.random() * all.length)];
if (BossPools[first].length > 0) return first;
// 回退链:随机定位空池时按剩余定位找非空池
const fallback = all.find(t => BossPools[t].length > 0);
return fallback ?? HType.Melee;
}
/** 从指定定位池随机取 Boss uuid(含全局兜底) */
private pickBossUuid(range: HType): number {
let uuids = BossPools[range];
if (!uuids || uuids.length === 0) {
uuids = MonList[MonType.MeleeBoss] || MonList[MonType.Melee] || [6001];
}
return uuids[Math.floor(Math.random() * uuids.length)];
}
/**
* 按定位抽取 Boss 大招(不重复),数量由关卡进度决定(getBossUltCount)
* @returns 大招配置列表;池空时返回 undefined
*/
private pickBossUlts(range: HType, stage: number): GeneratedMonster["skills"]["ults"] {
const pool = [...(BossUltPools[range] ?? [])];
const count = Math.min(getBossUltCount(stage), pool.length);
if (count <= 0) return undefined;
const ults: { s_uuid: number; cd?: number; overrides?: SkillOverrides }[] = [];
for (let i = 0; i < count; i++) {
const picked = this.pickWeighted(pool);
if (!picked) break;
pool.splice(pool.indexOf(picked), 1);
// 大招 CD 取英雄同档标准档(8s),与英雄大招节奏对齐
ults.push({ s_uuid: picked.s_uuid, cd: UltCdSet[UltCdLv.Normal].cd, overrides: picked.overrides });
}
return ults.length > 0 ? ults : undefined;
}
/** 通用加权随机抽取 */
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];
}
}
// ======================== 9. 全局单例 ========================
export const spawningEngine = new StageSpawningEngine();