612 lines
22 KiB
TypeScript
612 lines
22 KiB
TypeScript
/**
|
||
* @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();
|