Files
pixelheros/assets/script/game/map/RogueConfig.ts
pan 9da3e2d9fd fix(map): adjust global monster power multiplier to 1
更新全局怪物强度系数配置,将原本的2/3调整为1,让怪物强度和英雄能力持平
2026-08-13 11:17:29 +08:00

612 lines
22 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 Boss 组合刷怪系统(单场 Boss 战定胜负)
*
* 游戏机制:
* - 整局只有一场战斗:开局英雄三次三选一后自动开战。
* - 怪物从 BossComboLibrary 按权重随机抽一组1 Boss + 4 小怪),
* 战斗开始瞬间一次全量登场X∈[100,320] 均匀铺开),双方立即互相攻击。
* - 怪物全灭 → 胜利结算;英雄全灭 → 失败结算;无回合递进、无分批刷怪。
*
* 设计层次:
* 1. BossComboLibrary - Boss 组合库(小怪构成 + Boss/小怪技能池 + 抽取权重)
* 2. MonSkillPool/BossSkillPool - 怪物技能池atking / atked / dead 等全触发类型)
* 3. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度)
*
* 核心公式:
* heroPower = Σ calcHeroPower(HeroInfo[uuid], lv) (场上存活英雄)
* targetPower = heroPower × BOSS_TYPE_RATIO × FINAL_HP_MUL × MON_POWER_MUL
* hpScale = targetPower ÷ Σ 怪物基础强度
* 怪物 hp = base_hp × hpScale (走总强度框架,自动匹配玩家)
*
* 怪物 ap 独立挂钩玩家输出(不走总强度,避免被 hp 挤占):
* 怪物 ap = 玩家平均攻击 × AP_GROWTH_FINAL × AP_RELATIVE × 类型系数
*/
import { HeroInfo, MonType, MonTypeName, calcHeroPower, TriggerGrouped, LvReviveEntry } from "../common/config/heroSet";
import { SkillOverrides } from "../common/config/SkillSet";
import { FacSet } from "../common/config/GameSet";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
// ======================== 1. 战斗常量 ========================
/** 单场战斗时长基准供清屏速度评价BattleBannerComp 清屏奖励)等使用 */
export const WAVE_DURATION = 30;
/** 怪物登场固定构成1 个 Boss + 4 个小怪(一次全量登场,打完即整局结束) */
export const BOSS_COUNT = 1;
export const MINION_COUNT = 4;
/** Boss 战强度系数(原压力回合系数) */
export const BOSS_TYPE_RATIO = 1.2;
/** 最终 Boss 战 HP 乘区(原 wave 20 配置) */
export const FINAL_HP_MUL = 1.8;
/** 全局怪物强度系数1 = 怪物强度与英雄能力持平,同时作用于 hp 缩放与 ap 挂钩公式) */
export const MON_POWER_MUL = 1;
/** 怪物 ap 最终递进系数(原 wave 20 满档 3.0 */
export const AP_GROWTH_FINAL = 3.0;
/** 怪物 ap 相对玩家平均攻击的基础系数(<1 保证单怪不至于一击致命,由 Boss/精英类型再上浮) */
export const AP_RELATIVE = 0.6;
/** Boss 血量加成(原 wave 20 bossBonusHpMul = 1.6 */
export const BOSS_HP_BONUS_MUL = 1.6;
/** 战斗超时(秒):超过后强制结算整局(残留怪销毁并扣留存分) */
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 组合中小怪的槽位定义type + 数量Boss 另由 BOSS_COUNT 提供) */
export interface BossComboMinionSlot {
type: MonType;
count: number;
}
/** Boss 组合配置一场战斗随机抽一组登场1 Boss + minions 小怪) */
export interface BossComboConfig {
id: string;
name: string;
/** 小怪构成(合计数量建议等于 MINION_COUNT超出截断、不足自动补近战 */
minions: BossComboMinionSlot[];
/** Boss 技能池 id引用 BossSkillPool可选 */
boss_skill_pool?: string[];
/** 小怪技能池 id引用 MonSkillPool可选 */
skill_pool?: string[];
/** 在组合库中的抽取权重 */
weight: number;
}
/** Boss 组合库(硬编码):每场战斗按权重随机抽一组 */
export const BossComboLibrary: Record<string, BossComboConfig> = {
rage_charge: {
id: "rage_charge", name: "狂暴冲锋组", weight: 10,
minions: [{ type: MonType.Melee, count: 3 }, { type: MonType.Heavy, count: 1 }],
boss_skill_pool: ["boss_rage"],
},
iron_wall: {
id: "iron_wall", name: "铁壁推进组", weight: 8,
minions: [{ type: MonType.Heavy, count: 2 }, { type: MonType.Support, count: 1 }, { type: MonType.Long, count: 1 }],
boss_skill_pool: ["boss_iron"],
skill_pool: ["tough"],
},
doom_ritual: {
id: "doom_ritual", name: "毁灭仪式组", weight: 6,
minions: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }, { type: MonType.Support, count: 1 }],
boss_skill_pool: ["boss_doom"],
skill_pool: ["warcry"],
},
assassin_raid: {
id: "assassin_raid", name: "刺客突袭组", weight: 7,
minions: [{ type: MonType.Assassin, count: 2 }, { type: MonType.Melee, count: 2 }],
boss_skill_pool: ["boss_rage", "boss_doom"],
skill_pool: ["berserk"],
},
blood_legion: {
id: "blood_legion", name: "吸血军团组", weight: 5,
minions: [{ type: MonType.Melee, count: 2 }, { type: MonType.Long, count: 1 }, { type: MonType.Summoner, count: 1 }],
boss_skill_pool: ["boss_iron", "boss_doom"],
skill_pool: ["leech"],
},
};
/**
* 按权重随机抽取一组 Boss 组合
* @returns 组合配置,库为空时返回 null
*/
export function pickBossCombo(): BossComboConfig | null {
const combos = Object.values(BossComboLibrary).filter(c => c && c.weight > 0);
if (combos.length === 0) return null;
const total = combos.reduce((s, c) => s + c.weight, 0);
let r = Math.random() * total;
for (const c of combos) {
r -= c.weight;
if (r <= 0) return c;
}
return combos[combos.length - 1];
}
// ======================== 4. 怪物技能池 ========================
/**
* 怪物技能套装(一套完整的触发技能配置)
* 覆盖触发时机call / atking / atked / dead / fstart / fend / 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;
/** 战斗结束触发 */
fend?: TriggerGrouped;
/** 复活 */
revive?: LvReviveEntry[];
/** 在技能池中的抽取权重 */
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 } }],
},
},
};
// ======================== 5. 配置校验 ========================
/**
* 校验 BossComboLibrary / MonSkillPool 引用一致性
* 建议在游戏启动时调用一次,便于发现配置错误
* @returns 错误信息数组,空数组表示校验通过
*/
export function validateRogueConfig(): string[] {
const errors: string[] = [];
// 1. 校验组合引用与数量
for (const id in BossComboLibrary) {
const combo = BossComboLibrary[id];
let minionTotal = 0;
for (const slot of combo.minions) {
minionTotal += slot.count;
if (!MonList[slot.type] || MonList[slot.type].length === 0) {
errors.push(`组合 ${id} 引用的怪物类型 ${slot.type}(${MonTypeName[slot.type] || "?"}) 在 MonList 中无可用 uuid`);
}
}
if (minionTotal !== MINION_COUNT) {
errors.push(`组合 ${id} 小怪数量=${minionTotal} 与 MINION_COUNT=${MINION_COUNT} 不一致`);
}
if (combo.skill_pool) {
for (const skillId of combo.skill_pool) {
if (!MonSkillPool[skillId]) {
errors.push(`组合 ${id} 引用了不存在的技能: ${skillId}`);
}
}
}
if (combo.boss_skill_pool) {
for (const skillId of combo.boss_skill_pool) {
if (!BossSkillPool[skillId]) {
errors.push(`组合 ${id} 引用了不存在的 Boss 技能: ${skillId}`);
}
}
}
}
// 2. 校验 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;
}
// ======================== 6. 生成结果接口 ========================
/** 生成结果(传递给 MissionMonComp 用于实例化怪物) */
export interface GeneratedMonster {
uuid: number;
type: MonType;
hp: number;
ap: number;
isBoss: boolean;
/** 是否为 Boss 护卫队成员(供 UI/统计识别) */
isBossGuard?: boolean;
spawnIndex: number;
/** 本怪所属批次(保留字段,单场全量登场恒为 0 */
batch: number;
/**
* 怪物技能套装(覆盖全部触发时机)
* 注入方式与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp
*/
skills?: {
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
call?: TriggerGrouped;
atking?: TriggerGrouped;
atked?: TriggerGrouped;
dead?: TriggerGrouped;
fstart?: TriggerGrouped;
fend?: TriggerGrouped;
revive?: LvReviveEntry[];
};
}
// ======================== 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,
atking: undefined as TriggerGrouped | undefined,
atked: undefined as TriggerGrouped | undefined,
dead: undefined as TriggerGrouped | undefined,
fstart: undefined as TriggerGrouped | undefined,
fend: undefined as TriggerGrouped | undefined,
};
// ======================== 8. 生成引擎 ========================
/**
* Boss 组合生成引擎
* 随机抽一组 Boss 组合,按英雄强度反推怪物强度(最终 Boss 战档位)
*/
export class RogueSpawningEngine {
/**
* 生成一场战斗的怪物列表:随机一组 Boss 组合1 Boss + 组合小怪)。
* Boss 压队尾(登场时由 MissionMonComp 统一布位)。
* @returns 怪物列表小怪在前、Boss 在尾)
*/
generateBossCombo(): GeneratedMonster[] {
// 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪
if (TestModeConfig.enable) {
return this.generateTestWave();
}
const combo = pickBossCombo();
const monsters: GeneratedMonster[] = [];
if (combo) {
for (const slot of combo.minions) {
for (let i = 0; i < slot.count; i++) {
monsters.push(this.makeMonster(slot.type, monsters.length));
}
}
}
// 组合库为空或数量不足兜底:补近战小怪至 MINION_COUNT
while (monsters.length < MINION_COUNT) {
monsters.push(this.makeMonster(MonType.Melee, monsters.length));
}
// Boss 压队尾
for (let i = 0; i < BOSS_COUNT; i++) {
monsters.push(this.makeBoss());
}
// 属性缩放:直接取最终 Boss 战档位系数
const heroPower = this.getCurrentHeroPower();
const targetPower = heroPower * BOSS_TYPE_RATIO * MON_POWER_MUL;
const totalBasePower = monsters.reduce((sum, m) => {
const info = HeroInfo[m.uuid];
return sum + (info ? calcHeroPower(info, 1) : m.hp + m.ap);
}, 0);
if (totalBasePower > 0 && targetPower > 0) {
const hpScale = (targetPower * FINAL_HP_MUL) / totalBasePower;
const heroAvgAp = this.getCurrentHeroAttack();
for (const m of monsters) {
m.hp = Math.max(1, Math.round(m.hp * hpScale));
// ap = 玩家平均攻击 × 最终递进 × 基础系数 × 全局强度系数 × 类型系数
m.ap = Math.max(1, Math.round(heroAvgAp * AP_GROWTH_FINAL * AP_RELATIVE * MON_POWER_MUL * this.getApTypeFactor(m)));
}
}
// 挂载组合技能Boss 专属池 / 小怪普通池)
for (const m of monsters) {
if (m.isBoss && combo?.boss_skill_pool) {
m.skills = this.pickSkillSet(combo.boss_skill_pool, BossSkillPool);
} else if (!m.isBoss && combo?.skill_pool) {
m.skills = this.pickSkillSet(combo.skill_pool, MonSkillPool);
}
m.batch = 0;
}
for (let i = 0; i < monsters.length; i++) {
monsters[i].spawnIndex = i;
}
return monsters;
}
/** 重置引擎状态(每局开始时调用,预留) */
reset(): void {
}
// ======================== 私有生成子算法 ========================
/**
* 计算场上存活英雄的总强度
* 通过 ECS 查询所有 HeroAttrsComp累加 calcHeroPower
*/
private getCurrentHeroPower(): number {
let total = 0;
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const attrs = entity.get(HeroAttrsComp);
if (!attrs || attrs.is_dead || attrs.fac !== FacSet.HERO) return;
const info = HeroInfo[attrs.hero_uuid];
if (!info) return;
// 使用英雄当前等级(升级卡驱动),保证强度评估与实战一致
const lv = Math.max(1, attrs.lv || 1);
total += calcHeroPower(info, lv);
});
// 兜底:无英雄时返回基准强度,避免除零
return Math.max(total, 100);
}
/**
* 计算场上存活英雄的输出强度(用于怪物 ap 挂钩玩家,独立于总强度框架)。
* 取玩家英雄的"实时 ap 平均",怪物 ap 以此为锚,保证玩家输出越高、被反击越痛。
*/
private getCurrentHeroAttack(): number {
let totalAp = 0;
let count = 0;
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const attrs = entity.get(HeroAttrsComp);
if (!attrs || attrs.is_dead || attrs.fac !== FacSet.HERO) return;
totalAp += Math.max(1, attrs.ap || 1);
count++;
});
// 兜底:无英雄时返回基准攻击,避免除零
if (count === 0) return 30;
return totalAp / count; // 平均单英雄攻击
}
/** 测试模式:完全绕过引擎逻辑 */
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,
spawnIndex: i,
batch: 0,
skills: {
skill: TestModeConfig.skill,
atking: TestModeConfig.atking,
atked: TestModeConfig.atked,
dead: TestModeConfig.dead,
fstart: TestModeConfig.fstart,
fend: TestModeConfig.fend,
},
});
}
return monsters;
}
/** 生成 Boss基础血量带最终战加成属性随后由总强度框架统一缩放 */
private makeBoss(): 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)];
const baseInfo = HeroInfo[uuid];
const baseHp = baseInfo ? baseInfo.hp : 500;
const baseAp = baseInfo ? baseInfo.ap : 30;
return {
uuid,
type,
hp: Math.round(baseHp * BOSS_HP_BONUS_MUL),
ap: baseAp,
isBoss: true,
spawnIndex: 0, // 占位值,由 generateBossCombo 统一分配
batch: 0,
};
}
/**
* 怪物 ap 类型系数Boss 上浮,远程/召唤稍降(避免玻璃大炮互秒),近战/重甲基准。
*/
private getApTypeFactor(m: GeneratedMonster): number {
if (m.isBoss) return 2.2; // Boss 单发重,符合"压场"定位
if (m.isBossGuard) return 1.2; // 护卫略高于杂鱼
switch (m.type) {
case MonType.Long: return 0.9;
case MonType.Summoner: return 0.7;
case MonType.Assassin: return 1.1;
default: return 1.0; // Melee / Heavy
}
}
/** 生成普通怪物 */
private makeMonster(type: MonType, 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;
return {
uuid,
type,
hp: baseHp,
ap: baseAp,
isBoss: false,
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,
fend: picked.fend,
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];
}
}
// ======================== 9. 全局单例 ========================
export const spawningEngine = new RogueSpawningEngine();