Compare commits
4 Commits
1b1244c33c
...
95969781fb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95969781fb | ||
|
|
4938e8468e | ||
|
|
153e0e9a1f | ||
|
|
988c669bba |
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,8 @@ export enum GameEvent {
|
|||||||
MonDead = "MonDead",
|
MonDead = "MonDead",
|
||||||
HeroDead = "HeroDead",
|
HeroDead = "HeroDead",
|
||||||
HeroSell = "HeroSell",
|
HeroSell = "HeroSell",
|
||||||
|
/** 英雄面板升级按钮点击事件(payload: { eid },由 MissionCardComp 执行升级) */
|
||||||
|
HeroUpgrade = "HeroUpgrade",
|
||||||
GOLD_UPDATE = "GOLD_UPDATE",
|
GOLD_UPDATE = "GOLD_UPDATE",
|
||||||
DIAMOND_UPDATE = "DIAMOND_UPDATE",
|
DIAMOND_UPDATE = "DIAMOND_UPDATE",
|
||||||
MEAT_UPDATE = "MEAT_UPDATE",
|
MEAT_UPDATE = "MEAT_UPDATE",
|
||||||
|
|||||||
@@ -15,13 +15,23 @@ import { EquipBoxComp } from "../map/EquipBoxComp";
|
|||||||
export class FieldSkillHelper {
|
export class FieldSkillHelper {
|
||||||
/** 获取指定驻场技能类型的总加成值(计算存活的友方英雄 + 场上的驻场技能卡) */
|
/** 获取指定驻场技能类型的总加成值(计算存活的友方英雄 + 场上的驻场技能卡) */
|
||||||
public static getFieldSkillTotalValue(type: FieldSkillType): number {
|
public static getFieldSkillTotalValue(type: FieldSkillType): number {
|
||||||
|
return FieldSkillHelper.getFieldSkillTotalValueForFac(type, FacSet.HERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定阵营的驻场技能总加成值
|
||||||
|
* @param type 驻场技能类型
|
||||||
|
* @param fac 阵营(FacSet.HERO / FacSet.MON),默认 HERO
|
||||||
|
* @returns 总加成值
|
||||||
|
*/
|
||||||
|
public static getFieldSkillTotalValueForFac(type: FieldSkillType, fac: number = FacSet.HERO): number {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|
||||||
// 1. 统计英雄带来的驻场技能加成
|
// 1. 统计英雄/怪物带来的驻场技能加成
|
||||||
// 读 model.runtime_field(已按当前等级 resolve 的 uuid 列表)
|
// 读 model.runtime_field(已按当前等级 resolve 的 uuid 列表)
|
||||||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
|
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
|
||||||
const model = entity.get(HeroAttrsComp);
|
const model = entity.get(HeroAttrsComp);
|
||||||
if (!model || model.is_dead || model.fac !== FacSet.HERO) return;
|
if (!model || model.is_dead || model.fac !== fac) return;
|
||||||
const fields = model.runtime_field;
|
const fields = model.runtime_field;
|
||||||
if (fields) {
|
if (fields) {
|
||||||
for (const skillUuid of fields) {
|
for (const skillUuid of fields) {
|
||||||
@@ -33,31 +43,33 @@ export class FieldSkillHelper {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 统计技能盒子(技能卡)带来的驻场技能加成
|
// 2. 统计技能盒子(技能卡)带来的驻场技能加成(仅英雄阵营)
|
||||||
ecs.query(ecs.allOf(SkillBoxComp)).forEach((entity: ecs.Entity) => {
|
if (fac === FacSet.HERO) {
|
||||||
const skillBox = entity.get(SkillBoxComp);
|
ecs.query(ecs.allOf(SkillBoxComp)).forEach((entity: ecs.Entity) => {
|
||||||
if (!skillBox || !skillBox.field || skillBox.field.length === 0) return;
|
const skillBox = entity.get(SkillBoxComp);
|
||||||
|
if (!skillBox || !skillBox.field || skillBox.field.length === 0) return;
|
||||||
|
|
||||||
for (const skillUuid of skillBox.field) {
|
for (const skillUuid of skillBox.field) {
|
||||||
const skillConfig = FieldSkillSet[skillUuid];
|
const skillConfig = FieldSkillSet[skillUuid];
|
||||||
if (skillConfig && skillConfig.type === type) {
|
if (skillConfig && skillConfig.type === type) {
|
||||||
total += skillConfig.value;
|
total += skillConfig.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// 3. 统计装备盒(装备卡)带来的驻场技能加成
|
// 3. 统计装备盒(装备卡)带来的驻场技能加成(仅英雄阵营)
|
||||||
ecs.query(ecs.allOf(EquipBoxComp)).forEach((entity: ecs.Entity) => {
|
ecs.query(ecs.allOf(EquipBoxComp)).forEach((entity: ecs.Entity) => {
|
||||||
const equipBox = entity.get(EquipBoxComp);
|
const equipBox = entity.get(EquipBoxComp);
|
||||||
if (!equipBox || !equipBox.field || equipBox.field.length === 0) return;
|
if (!equipBox || !equipBox.field || equipBox.field.length === 0) return;
|
||||||
|
|
||||||
for (const skillUuid of equipBox.field) {
|
for (const skillUuid of equipBox.field) {
|
||||||
const skillConfig = FieldSkillSet[skillUuid];
|
const skillConfig = FieldSkillSet[skillUuid];
|
||||||
if (skillConfig && skillConfig.type === type) {
|
if (skillConfig && skillConfig.type === type) {
|
||||||
total += skillConfig.value;
|
total += skillConfig.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,6 +225,8 @@ export class Monster extends ecs.Entity {
|
|||||||
if (testSkills.dead !== undefined) model.dead = testSkills.dead;
|
if (testSkills.dead !== undefined) model.dead = testSkills.dead;
|
||||||
if (testSkills.fstart !== undefined) model.fstart = testSkills.fstart;
|
if (testSkills.fstart !== undefined) model.fstart = testSkills.fstart;
|
||||||
if (testSkills.fend !== undefined) model.fend = testSkills.fend;
|
if (testSkills.fend !== undefined) model.fend = testSkills.fend;
|
||||||
|
if (testSkills.call !== undefined) model.call = testSkills.call;
|
||||||
|
if (testSkills.revive !== undefined) model.revive = testSkills.revive;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按怪物等级 resolve 触发技能/复活到运行时缓存(怪物无 lv 成长,恒用 mon_lv)
|
// 按怪物等级 resolve 触发技能/复活到运行时缓存(怪物无 lv 成长,恒用 mon_lv)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* - HeroInfo(heroSet)—— 英雄静态配置
|
* - HeroInfo(heroSet)—— 英雄静态配置
|
||||||
*/
|
*/
|
||||||
import { mLogger } from "../common/Logger";
|
import { mLogger } from "../common/Logger";
|
||||||
import { _decorator, Node, Sprite, Label, RichText, resources, AnimationClip, SpriteFrame, NodeEventType, Tween, tween, Vec3 } from "cc";
|
import { _decorator, Node, Sprite, Label, RichText, resources, AnimationClip, SpriteFrame, NodeEventType, Tween, tween, Vec3, Color } from "cc";
|
||||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||||
import { oops } from "db://oops-framework/core/Oops";
|
import { oops } from "db://oops-framework/core/Oops";
|
||||||
@@ -20,8 +20,8 @@ import { GameEvent } from "../common/config/GameEvent";
|
|||||||
import { HeroInfo } from "../common/config/heroSet";
|
import { HeroInfo } from "../common/config/heroSet";
|
||||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||||
import { Hero } from "../hero/Hero";
|
import { Hero } from "../hero/Hero";
|
||||||
import { FacSet, getLvColor } from "../common/config/GameSet";
|
import { FacSet, FightSet, getLvColor } from "../common/config/GameSet";
|
||||||
import { buildSkillDescRich } from "../common/config/HeroSkillDesc";
|
import { buildSkillDesc, buildSkillDescRich, ISkillDescSource } from "../common/config/HeroSkillDesc";
|
||||||
import { MissionEconomy } from "./MissionEconomy";
|
import { MissionEconomy } from "./MissionEconomy";
|
||||||
import { UIID } from "../common/config/GameUIConfig";
|
import { UIID } from "../common/config/GameUIConfig";
|
||||||
|
|
||||||
@@ -37,6 +37,9 @@ const { ccclass, property } = _decorator;
|
|||||||
export class HeroBoxComp extends CCComp {
|
export class HeroBoxComp extends CCComp {
|
||||||
private debugMode: boolean = false;
|
private debugMode: boolean = false;
|
||||||
|
|
||||||
|
/** 金币不足时升级费用标红色(暗红,区别于正常白色) */
|
||||||
|
private static readonly INSUFFICIENT_COLOR: Color = new Color(231, 76, 60);
|
||||||
|
|
||||||
// ======================== 编辑器绑定节点 ========================
|
// ======================== 编辑器绑定节点 ========================
|
||||||
|
|
||||||
/** 英雄图标节点 */
|
/** 英雄图标节点 */
|
||||||
@@ -60,10 +63,18 @@ export class HeroBoxComp extends CCComp {
|
|||||||
@property(Node)
|
@property(Node)
|
||||||
private sell_btn: Node = null;
|
private sell_btn: Node = null;
|
||||||
|
|
||||||
|
/** 升级按钮(绑定英雄时激活,点击弹出升级预览确认框) */
|
||||||
|
@property(Node)
|
||||||
|
private upgrade_btn: Node = null;
|
||||||
|
|
||||||
/** 出售价格标签(按英雄等级显示当前卖价,含驻场加成) */
|
/** 出售价格标签(按英雄等级显示当前卖价,含驻场加成) */
|
||||||
@property(Label)
|
@property(Label)
|
||||||
private sell_price_label: Label = null;
|
private sell_price_label: Label = null;
|
||||||
|
|
||||||
|
/** 升级费用标签(按当前等级显示升级消耗金币,满级时隐藏) */
|
||||||
|
@property(Label)
|
||||||
|
private upgrade_price_label: Label = null;
|
||||||
|
|
||||||
/** 技能描述富文本(多行,展示当前等级触发技能,数值高亮) */
|
/** 技能描述富文本(多行,展示当前等级触发技能,数值高亮) */
|
||||||
@property(RichText)
|
@property(RichText)
|
||||||
private skill_label: RichText = null;
|
private skill_label: RichText = null;
|
||||||
@@ -167,6 +178,8 @@ export class HeroBoxComp extends CCComp {
|
|||||||
this.noHero?.on(NodeEventType.TOUCH_END, this.onNoHeroClick, this);
|
this.noHero?.on(NodeEventType.TOUCH_END, this.onNoHeroClick, this);
|
||||||
// 出售按钮点击 → 出售当前绑定英雄
|
// 出售按钮点击 → 出售当前绑定英雄
|
||||||
this.sell_btn?.on(NodeEventType.TOUCH_END, this.onSellHeroClick, this);
|
this.sell_btn?.on(NodeEventType.TOUCH_END, this.onSellHeroClick, this);
|
||||||
|
// 升级按钮点击 → 弹出升级预览确认框
|
||||||
|
this.upgrade_btn?.on(NodeEventType.TOUCH_END, this.onUpgradeHeroClick, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy() {
|
onDestroy() {
|
||||||
@@ -181,6 +194,101 @@ export class HeroBoxComp extends CCComp {
|
|||||||
if (this.sell_btn && this.sell_btn.isValid) {
|
if (this.sell_btn && this.sell_btn.isValid) {
|
||||||
this.sell_btn.off(NodeEventType.TOUCH_END, this.onSellHeroClick, this);
|
this.sell_btn.off(NodeEventType.TOUCH_END, this.onSellHeroClick, this);
|
||||||
}
|
}
|
||||||
|
if (this.upgrade_btn && this.upgrade_btn.isValid) {
|
||||||
|
this.upgrade_btn.off(NodeEventType.TOUCH_END, this.onUpgradeHeroClick, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 升级按钮点击:构建升级后新能力预览,弹出公共确认框。
|
||||||
|
* Why: 升级消耗金币不可逆,需展示收益并二次确认。
|
||||||
|
*/
|
||||||
|
private onUpgradeHeroClick() {
|
||||||
|
if (!this.eid || !this.model) return;
|
||||||
|
const heroLv = Math.max(1, Math.floor(this.model.lv ?? 1));
|
||||||
|
// 已满级:气泡提示,复用英雄栏 smalltip 挂载点
|
||||||
|
if (heroLv >= FightSet.HERO_MAX_LV) {
|
||||||
|
oops.message.dispatchEvent(GameEvent.ShowSmallTip, { type: "hero_full", text: "已达最高等级" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
oops.audio.playEffect("music/button");
|
||||||
|
|
||||||
|
const heroName = this.model.hero_name ?? "";
|
||||||
|
const nextLv = heroLv + 1;
|
||||||
|
const cost = MissionEconomy.getUpgradeCost(heroLv);
|
||||||
|
// 金币不足:smalltip 气泡提示(挂载金币节点),不弹确认框
|
||||||
|
if (MissionEconomy.getCoin() < cost) {
|
||||||
|
oops.message.dispatchEvent(GameEvent.ShowSmallTip, { type: "buy_coin", text: "金币不足" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const preview = this.buildUpgradePreview(nextLv);
|
||||||
|
|
||||||
|
// oops.gui 公共确认窗口(自定义 CommonPrompt:内容支持富文本高亮)
|
||||||
|
oops.gui.open(UIID.Window, {
|
||||||
|
title: `升级英雄`,
|
||||||
|
content: `<color=#C0392B>${heroName}</color> Lv.${heroLv} → <color=#1E8449>Lv.${nextLv}</color>\n消耗 <color=#E67E22>${cost}</color> 金币\n\n<b>升级后获得:</b>\n${preview}`,
|
||||||
|
okWord: "升级",
|
||||||
|
cancelWord: "取消",
|
||||||
|
needCancel: true,
|
||||||
|
okFunc: () => this.executeUpgradeHero()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行升级:校验状态后派发 HeroUpgrade 事件,由 MissionCardComp 扣费并应用等级。
|
||||||
|
* Why: 扣费/属性重算统一走 MissionCardComp(与升级卡共用 tryUpgradeHeroByEid),
|
||||||
|
* 本组件仅负责 UI 交互与预览,保证单一数据源。
|
||||||
|
*/
|
||||||
|
private executeUpgradeHero() {
|
||||||
|
// 确认窗口打开期间英雄可能已死亡/被移除,需二次校验
|
||||||
|
if (!this.eid || !this.model) return;
|
||||||
|
oops.message.dispatchEvent(GameEvent.HeroUpgrade, { eid: this.eid });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建升级后新能力预览富文本。
|
||||||
|
* 数据源取 HeroInfo 静态配置(lv 置为目标等级),由描述标准层统一渲染,
|
||||||
|
* 与面板技能描述同一套文案规则,杜绝数值漂移。
|
||||||
|
*
|
||||||
|
* @param nextLv 目标等级
|
||||||
|
* @returns 多行富文本:属性成长 + 下一档触发技能/光环/复活/加成
|
||||||
|
*/
|
||||||
|
private buildUpgradePreview(nextLv: number): string {
|
||||||
|
const hero = HeroInfo[this.model?.hero_uuid ?? 0];
|
||||||
|
if (!hero) return "";
|
||||||
|
|
||||||
|
// ---- 属性成长(按 HERO_LV_MULTIPLIER 幂成长 + 等级额外加成) ----
|
||||||
|
const mult = Math.pow(FightSet.HERO_LV_MULTIPLIER, nextLv - 1);
|
||||||
|
const nextAp = Math.floor(hero.ap * mult + HeroBoxComp.sumBonus(hero.ap_bonus, nextLv));
|
||||||
|
const nextHp = Math.floor(hero.hp * mult + HeroBoxComp.sumBonus(hero.hp_bonus, nextLv));
|
||||||
|
const lines: string[] = [
|
||||||
|
`攻击 <color=#27AE60>${nextAp}</color> 生命 <color=#27AE60>${nextHp}</color>`
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---- 新能力(触发技能 / 驻场光环 / 复活 / 额外加成,取目标等级生效档) ----
|
||||||
|
const source: ISkillDescSource = {
|
||||||
|
lv: nextLv,
|
||||||
|
call: hero.call,
|
||||||
|
dead: hero.dead,
|
||||||
|
fstart: hero.fstart,
|
||||||
|
fend: hero.fend,
|
||||||
|
atking: hero.atking,
|
||||||
|
atked: hero.atked,
|
||||||
|
field: hero.field,
|
||||||
|
revive: hero.revive,
|
||||||
|
// 预览仅展示增量,额外加成由上方属性成长体现,避免重复
|
||||||
|
};
|
||||||
|
const skillDesc = buildSkillDesc(source);
|
||||||
|
if (skillDesc) lines.push(skillDesc);
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 累加等级额外加成(≤目标等级的所有档位) */
|
||||||
|
private static sumBonus(entries: { lv: number; value: number }[] | undefined, lv: number): number {
|
||||||
|
if (!entries) return 0;
|
||||||
|
let total = 0;
|
||||||
|
for (const e of entries) { if (e.lv <= lv) total += e.value; }
|
||||||
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -320,6 +428,9 @@ export class HeroBoxComp extends CCComp {
|
|||||||
if (this.sell_btn && this.sell_btn.isValid) {
|
if (this.sell_btn && this.sell_btn.isValid) {
|
||||||
this.sell_btn.active = true;
|
this.sell_btn.active = true;
|
||||||
}
|
}
|
||||||
|
if (this.upgrade_btn && this.upgrade_btn.isValid) {
|
||||||
|
this.upgrade_btn.active = true;
|
||||||
|
}
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,9 +448,15 @@ export class HeroBoxComp extends CCComp {
|
|||||||
if (this.sell_btn && this.sell_btn.isValid) {
|
if (this.sell_btn && this.sell_btn.isValid) {
|
||||||
this.sell_btn.active = false;
|
this.sell_btn.active = false;
|
||||||
}
|
}
|
||||||
|
if (this.upgrade_btn && this.upgrade_btn.isValid) {
|
||||||
|
this.upgrade_btn.active = false;
|
||||||
|
}
|
||||||
if (this.sell_price_label && this.sell_price_label.isValid) {
|
if (this.sell_price_label && this.sell_price_label.isValid) {
|
||||||
this.sell_price_label.string = "";
|
this.sell_price_label.string = "";
|
||||||
}
|
}
|
||||||
|
if (this.upgrade_price_label && this.upgrade_price_label.isValid) {
|
||||||
|
this.upgrade_price_label.string = "";
|
||||||
|
}
|
||||||
|
|
||||||
if (this.name_label && this.name_label.isValid) {
|
if (this.name_label && this.name_label.isValid) {
|
||||||
this.name_label.string = "";
|
this.name_label.string = "";
|
||||||
@@ -398,6 +515,19 @@ export class HeroBoxComp extends CCComp {
|
|||||||
this.sell_price_label.string = `${MissionEconomy.getSellGold(lv)}`;
|
this.sell_price_label.string = `${MissionEconomy.getSellGold(lv)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 升级费用(满级隐藏;金币不足时标红提示) ----
|
||||||
|
if (this.upgrade_price_label && this.upgrade_price_label.isValid) {
|
||||||
|
const lv = Math.max(1, Math.floor(this.model.lv ?? 1));
|
||||||
|
if (lv >= FightSet.HERO_MAX_LV) {
|
||||||
|
this.upgrade_price_label.string = "";
|
||||||
|
} else {
|
||||||
|
const cost = MissionEconomy.getUpgradeCost(lv);
|
||||||
|
this.upgrade_price_label.string = `${cost}`;
|
||||||
|
// 金币不足标红,足够恢复默认色,给玩家直观的可升级反馈
|
||||||
|
this.upgrade_price_label.color = MissionEconomy.getCoin() >= cost ? Color.WHITE : HeroBoxComp.INSUFFICIENT_COLOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- AP / HP ----
|
// ---- AP / HP ----
|
||||||
const finalAp = Math.max(0, Math.floor(this.model.getFinalAp()));
|
const finalAp = Math.max(0, Math.floor(this.model.getFinalAp()));
|
||||||
const baseAp = Math.max(0, Math.floor(this.model.base_ap ?? 0));
|
const baseAp = Math.max(0, Math.floor(this.model.base_ap ?? 0));
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ export class MissionCardComp extends CCComp {
|
|||||||
oops.message.on(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
|
oops.message.on(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
|
||||||
oops.message.on(GameEvent.CardUsed, this.onCardUsed, this);
|
oops.message.on(GameEvent.CardUsed, this.onCardUsed, this);
|
||||||
oops.message.on(GameEvent.HeroBoxEmptyClick, this.onHeroBoxEmptyClick, this);
|
oops.message.on(GameEvent.HeroBoxEmptyClick, this.onHeroBoxEmptyClick, this);
|
||||||
|
oops.message.on(GameEvent.HeroUpgrade, this.onHeroUpgrade, this);
|
||||||
|
|
||||||
/** 按钮触控事件:抽卡 */
|
/** 按钮触控事件:抽卡 */
|
||||||
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
|
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
|
||||||
@@ -614,6 +615,7 @@ export class MissionCardComp extends CCComp {
|
|||||||
oops.message.off(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
|
oops.message.off(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
|
||||||
oops.message.off(GameEvent.CardUsed, this.onCardUsed, this);
|
oops.message.off(GameEvent.CardUsed, this.onCardUsed, this);
|
||||||
oops.message.off(GameEvent.HeroBoxEmptyClick, this.onHeroBoxEmptyClick, this);
|
oops.message.off(GameEvent.HeroBoxEmptyClick, this.onHeroBoxEmptyClick, this);
|
||||||
|
oops.message.off(GameEvent.HeroUpgrade, this.onHeroUpgrade, this);
|
||||||
if (this.cards_chou && this.cards_chou.isValid) {
|
if (this.cards_chou && this.cards_chou.isValid) {
|
||||||
this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
|
this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
|
||||||
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
|
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
|
||||||
@@ -694,6 +696,28 @@ export class MissionCardComp extends CCComp {
|
|||||||
this.refreshHeroBoxSlots();
|
this.refreshHeroBoxSlots();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 英雄面板升级请求回调(由 HeroBoxComp 确认弹窗后派发)。
|
||||||
|
* 扣费成功后按 eid 精确升级;金币不足或不可升级时 toast 提示。
|
||||||
|
*/
|
||||||
|
private onHeroUpgrade(event: string, args: any) {
|
||||||
|
const payload = args ?? event;
|
||||||
|
const eid: number = payload?.eid ?? 0;
|
||||||
|
if (!eid) return;
|
||||||
|
const actor = this.queryAliveHeroActors().find(item => item.eid === eid);
|
||||||
|
if (!actor) return;
|
||||||
|
if (actor.model.lv >= FightSet.HERO_MAX_LV) {
|
||||||
|
oops.gui.toast(`已达最高等级`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 统一经济管理入口:按当前等级扣费
|
||||||
|
if (!MissionEconomy.executeUpgradeCost(actor.model.lv)) {
|
||||||
|
oops.gui.toast(`金币不足`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.tryUpgradeHeroByEid(eid);
|
||||||
|
}
|
||||||
|
|
||||||
/** 英雄升级事件回调:刷新英雄信息盒显示 */
|
/** 英雄升级事件回调:刷新英雄信息盒显示 */
|
||||||
private onHeroLvUp() {
|
private onHeroLvUp() {
|
||||||
this.refreshHeroBoxSlots();
|
this.refreshHeroBoxSlots();
|
||||||
@@ -1818,7 +1842,7 @@ export class MissionCardComp extends CCComp {
|
|||||||
* @param heroEid 要升级的英雄实体 eid
|
* @param heroEid 要升级的英雄实体 eid
|
||||||
* @returns true = 升级成功
|
* @returns true = 升级成功
|
||||||
*/
|
*/
|
||||||
private tryUpgradeHeroByEid(heroEid: number): boolean {
|
public tryUpgradeHeroByEid(heroEid: number): boolean {
|
||||||
if (!heroEid) return false;
|
if (!heroEid) return false;
|
||||||
const actor = this.queryAliveHeroActors().find(item => item.eid === heroEid);
|
const actor = this.queryAliveHeroActors().find(item => item.eid === heroEid);
|
||||||
if (!actor) return false;
|
if (!actor) return false;
|
||||||
@@ -1835,7 +1859,7 @@ export class MissionCardComp extends CCComp {
|
|||||||
* 应用英雄等级变化(属性按 HERO_LV_MULTIPLIER 重新计算)。
|
* 应用英雄等级变化(属性按 HERO_LV_MULTIPLIER 重新计算)。
|
||||||
* 沿用旧版本 SkillLvUp 的技能等级映射规则。
|
* 沿用旧版本 SkillLvUp 的技能等级映射规则。
|
||||||
*/
|
*/
|
||||||
private applyHeroLevel(model: HeroAttrsComp, targetLv: number) {
|
public applyHeroLevel(model: HeroAttrsComp, targetLv: number) {
|
||||||
const hero = HeroInfo[model.hero_uuid];
|
const hero = HeroInfo[model.hero_uuid];
|
||||||
if (!hero) return;
|
if (!hero) return;
|
||||||
const nextLv = Math.max(1, Math.min(FightSet.HERO_MAX_LV, Math.floor(targetLv)));
|
const nextLv = Math.max(1, Math.min(FightSet.HERO_MAX_LV, Math.floor(targetLv)));
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import { Tooltip } from "../skill/Tooltip";
|
|||||||
import { Timer } from "db://oops-framework/core/common/timer/Timer";
|
import { Timer } from "db://oops-framework/core/common/timer/Timer";
|
||||||
import { FieldSkillType } from "../common/config/SkillSet";
|
import { FieldSkillType } from "../common/config/SkillSet";
|
||||||
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
|
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
|
||||||
import { spawningEngine } from "./RogueConfig";
|
import { spawningEngine, MAX_WAVE, DynamicTuner } from "./RogueConfig";
|
||||||
const { ccclass, property } = _decorator;
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
/** 任务(关卡)生命周期阶段 */
|
/** 任务(关卡)生命周期阶段 */
|
||||||
@@ -524,20 +524,28 @@ export class MissionComp extends CCComp {
|
|||||||
|
|
||||||
let allAlive = true;
|
let allAlive = true;
|
||||||
let hasHero = false;
|
let hasHero = false;
|
||||||
|
let heroDeathCount = 0;
|
||||||
ecs.query(this.heroAttrsMatcher).forEach(entity => {
|
ecs.query(this.heroAttrsMatcher).forEach(entity => {
|
||||||
const attrs = entity.get(HeroAttrsComp);
|
const attrs = entity.get(HeroAttrsComp);
|
||||||
if (attrs && attrs.fac === FacSet.HERO) {
|
if (attrs && attrs.fac === FacSet.HERO) {
|
||||||
hasHero = true;
|
hasHero = true;
|
||||||
if (attrs.is_dead) allAlive = false;
|
if (attrs.is_dead) {
|
||||||
|
allAlive = false;
|
||||||
|
heroDeathCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 【动态难度调节】根据本波战况自动放水 / 加压
|
||||||
|
DynamicTuner.adjust(this.clearTime, heroDeathCount);
|
||||||
|
mLogger.log(this.debugMode, 'MissionComp', `[DynamicTuner] wave=${this.currentWave} clearTime=${this.clearTime.toFixed(1)}s deaths=${heroDeathCount} factor=${DynamicTuner.factor.toFixed(2)}`);
|
||||||
// 【评分系统 - 战绩分】记录全员存活的胜利回合数(额外加分)
|
// 【评分系统 - 战绩分】记录全员存活的胜利回合数(额外加分)
|
||||||
if (hasHero && allAlive) {
|
if (hasHero && allAlive) {
|
||||||
smc.vmdata.scores.wave_all_alive_count++;
|
smc.vmdata.scores.wave_all_alive_count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 【评分系统 - 战绩分】判断是否通过最后一关(第15回合)
|
// 【评分系统 - 战绩分】判断是否通过最后一关(第20回合)
|
||||||
if (this.currentWave === 15) {
|
if (this.currentWave === MAX_WAVE) {
|
||||||
smc.vmdata.scores.passed_wave_20 = true;
|
smc.vmdata.scores.passed_wave_20 = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -889,8 +897,8 @@ export class MissionComp extends CCComp {
|
|||||||
// 怪物全灭检测:如果战斗阶段场上没有任何活着的怪物,且待刷新的怪物队列也为空,直接结束战斗进入下一波的准备阶段
|
// 怪物全灭检测:如果战斗阶段场上没有任何活着的怪物,且待刷新的怪物队列也为空,直接结束战斗进入下一波的准备阶段
|
||||||
const pendingCount = smc.vmdata.mission_data.pending_mon_num || 0;
|
const pendingCount = smc.vmdata.mission_data.pending_mon_num || 0;
|
||||||
if (monsterCount === 0 && pendingCount === 0 && smc.mission.play && !smc.mission.pause && this.currentPhase === MissionPhase.Battle) {
|
if (monsterCount === 0 && pendingCount === 0 && smc.mission.play && !smc.mission.pause && this.currentPhase === MissionPhase.Battle) {
|
||||||
if (this.currentWave >= 15) {
|
if (this.currentWave >= MAX_WAVE) {
|
||||||
// 15 波通关
|
// 20 波通关
|
||||||
this.open_Victory(null, false);
|
this.open_Victory(null, false);
|
||||||
} else {
|
} else {
|
||||||
oops.message.dispatchEvent("TimeUpAdvanceWave");
|
oops.message.dispatchEvent("TimeUpAdvanceWave");
|
||||||
|
|||||||
@@ -109,6 +109,24 @@ export class MissionEconomy {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算英雄升级金币消耗(按当前等级线性缩放:Lv×5)
|
||||||
|
* @param heroLevel 当前英雄等级(升级前)
|
||||||
|
*/
|
||||||
|
static getUpgradeCost(heroLevel: number = 1): number {
|
||||||
|
const lv = Math.max(1, Math.floor(heroLevel));
|
||||||
|
return lv * 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行英雄升级扣费
|
||||||
|
* @param heroLevel 当前英雄等级(升级前)
|
||||||
|
* @returns true = 扣费成功(金币足够)
|
||||||
|
*/
|
||||||
|
static executeUpgradeCost(heroLevel: number = 1): boolean {
|
||||||
|
return this.spendCoin(this.getUpgradeCost(heroLevel));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算英雄出售金币(按英雄等级缩放)
|
* 计算英雄出售金币(按英雄等级缩放)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
*
|
*
|
||||||
* 职责:
|
* 职责:
|
||||||
* 1. 管理每一波怪物的生成计划:根据 RogueConfig 生成怪物。
|
* 1. 管理每一波怪物的生成计划:根据 RogueConfig 生成怪物。
|
||||||
* 2. 自动推进波次:在准备阶段结束时(PhasePrepareEnd)把怪物转入逐个刷出队列。
|
* 2. 自动推进波次:在准备阶段结束时(PhasePrepareEnd)启动分批释放。
|
||||||
|
* 3. 分批刷怪:每波固定 3 批,每 10 秒释放一批,批内按 MON_SPAWN_INTERVAL 逐个刷出。
|
||||||
*
|
*
|
||||||
* 关键设计:
|
* 关键设计:
|
||||||
* - 所有怪物统一从右侧 X=400 出生点逐个刷出(MON_SPAWN_INTERVAL 节奏控制)。
|
* - 所有怪物统一从右侧 X=400 出生点逐个刷出(MON_SPAWN_INTERVAL 节奏控制)。
|
||||||
@@ -21,7 +22,7 @@ import { Monster } from "../hero/Mon";
|
|||||||
import { smc } from "../common/SingletonModuleComp";
|
import { smc } from "../common/SingletonModuleComp";
|
||||||
import { GameEvent } from "../common/config/GameEvent";
|
import { GameEvent } from "../common/config/GameEvent";
|
||||||
import { BoxSet, FacSet } from "../common/config/GameSet";
|
import { BoxSet, FacSet } from "../common/config/GameSet";
|
||||||
import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig";
|
import { spawningEngine, GeneratedMonster, TestModeConfig, MAX_MONSTERS, BATCH_COUNT, BATCH_INTERVAL } from "./RogueConfig";
|
||||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||||
import { MonMoveComp } from "../hero/MonMoveComp";
|
import { MonMoveComp } from "../hero/MonMoveComp";
|
||||||
|
|
||||||
@@ -32,8 +33,6 @@ const { ccclass, property } = _decorator;
|
|||||||
export class MissionMonCompComp extends CCComp {
|
export class MissionMonCompComp extends CCComp {
|
||||||
// ======================== 常量 ========================
|
// ======================== 常量 ========================
|
||||||
|
|
||||||
/** 怪物最多 12 个 */
|
|
||||||
private static readonly MAX_MONSTERS = 12;
|
|
||||||
/** 怪物出生掉落高度 */
|
/** 怪物出生掉落高度 */
|
||||||
private static readonly MON_DROP_HEIGHT = 0;
|
private static readonly MON_DROP_HEIGHT = 0;
|
||||||
/** 怪物统一从右侧 X=400 出生点逐个刷出,随后向左推进 */
|
/** 怪物统一从右侧 X=400 出生点逐个刷出,随后向左推进 */
|
||||||
@@ -42,9 +41,10 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
private static readonly MON_SPAWN_INTERVAL = 0.3;
|
private static readonly MON_SPAWN_INTERVAL = 0.3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 12 个怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标,
|
* 怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标,
|
||||||
* 实际阵型由 MonMoveComp 在战斗中向左推进时自然拉开。
|
* 实际阵型由 MonMoveComp 在战斗中向左推进时自然拉开。
|
||||||
* 槽位索引仍保留 3 行 × 4 列结构,供 monGrid 寻路与 SCastSystem 索敌使用。
|
* 槽位索引仍保留 3 行 × 4 列结构,供 monGrid 寻路与 SCastSystem 索敌使用。
|
||||||
|
* 超出 12 个槽位的怪物(放松波)按 spawnIndex % 12 复用槽位。
|
||||||
*/
|
*/
|
||||||
public static readonly MON_POSITIONS: Vec3[] = [
|
public static readonly MON_POSITIONS: Vec3[] = [
|
||||||
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 0: Col1-Top
|
v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 0: Col1-Top
|
||||||
@@ -65,7 +65,7 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
|
|
||||||
@property({ tooltip: "是否启用调试日志" })
|
@property({ tooltip: "是否启用调试日志" })
|
||||||
private debugMode: boolean = false;
|
private debugMode: boolean = false;
|
||||||
|
|
||||||
// ======================== 运行时状态 ========================
|
// ======================== 运行时状态 ========================
|
||||||
|
|
||||||
/** 全局生成顺序计数器(用于渲染层级排序) */
|
/** 全局生成顺序计数器(用于渲染层级排序) */
|
||||||
@@ -76,9 +76,15 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
private waveTargetCount: number = 0;
|
private waveTargetCount: number = 0;
|
||||||
/** 当前波已生成的怪物数量 */
|
/** 当前波已生成的怪物数量 */
|
||||||
private waveSpawnedCount: number = 0;
|
private waveSpawnedCount: number = 0;
|
||||||
/** 等待生成的怪物队列(波次总池) */
|
/** 等待生成的怪物队列(按批次分组,batch 0~2) */
|
||||||
private pendingMonsters: GeneratedMonster[] = [];
|
private pendingBatches: GeneratedMonster[][] = [[], [], []];
|
||||||
/** 逐个刷怪队列:从 pendingMonsters 转入,按节奏在 update 中释放 */
|
/** 当前正在释放的批次索引 */
|
||||||
|
private currentBatch: number = 0;
|
||||||
|
/** 批次释放计时器(秒) */
|
||||||
|
private batchTimer: number = 0;
|
||||||
|
/** 是否正在分批释放中 */
|
||||||
|
private isReleasing: boolean = false;
|
||||||
|
/** 逐个刷怪队列:从当前批次转入,按节奏在 update 中释放 */
|
||||||
private spawnQueue: GeneratedMonster[] = [];
|
private spawnQueue: GeneratedMonster[] = [];
|
||||||
/** 逐个刷怪累计计时(秒) */
|
/** 逐个刷怪累计计时(秒) */
|
||||||
private spawnTimer: number = 0;
|
private spawnTimer: number = 0;
|
||||||
@@ -92,8 +98,21 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected update(dt: number): void {
|
protected update(dt: number): void {
|
||||||
// pending_mon_num 同时统计未刷出的总池与正在释放的队列,确保 UI 与全灭判定一致
|
// 统计待刷出的怪物总数(未释放的批次 + 正在释放的队列)
|
||||||
smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length + this.spawnQueue.length;
|
let pendingCount = this.spawnQueue.length;
|
||||||
|
for (let i = this.currentBatch; i < BATCH_COUNT; i++) {
|
||||||
|
pendingCount += this.pendingBatches[i].length;
|
||||||
|
}
|
||||||
|
smc.vmdata.mission_data.pending_mon_num = pendingCount;
|
||||||
|
|
||||||
|
// 分批释放:按 BATCH_INTERVAL 节奏推进批次
|
||||||
|
if (this.isReleasing) {
|
||||||
|
this.batchTimer += dt;
|
||||||
|
if (this.batchTimer >= BATCH_INTERVAL && this.currentBatch < BATCH_COUNT - 1) {
|
||||||
|
this.batchTimer = 0;
|
||||||
|
this.advanceBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 逐个刷怪:按 MON_SPAWN_INTERVAL 节奏从队列释放
|
// 逐个刷怪:按 MON_SPAWN_INTERVAL 节奏从队列释放
|
||||||
if (this.spawnQueue.length > 0) {
|
if (this.spawnQueue.length > 0) {
|
||||||
@@ -101,7 +120,7 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
if (this.spawnTimer >= MissionMonCompComp.MON_SPAWN_INTERVAL) {
|
if (this.spawnTimer >= MissionMonCompComp.MON_SPAWN_INTERVAL) {
|
||||||
this.spawnTimer = 0;
|
this.spawnTimer = 0;
|
||||||
const monData = this.spawnQueue.shift()!;
|
const monData = this.spawnQueue.shift()!;
|
||||||
const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MAX_MONSTERS;
|
const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MON_POSITIONS.length;
|
||||||
this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave);
|
this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave);
|
||||||
this.waveSpawnedCount++;
|
this.waveSpawnedCount++;
|
||||||
}
|
}
|
||||||
@@ -111,10 +130,16 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
start() {}
|
start() {}
|
||||||
|
|
||||||
private setupWaveData(monsters: GeneratedMonster[]) {
|
private setupWaveData(monsters: GeneratedMonster[]) {
|
||||||
this.pendingMonsters = monsters.slice(0, MissionMonCompComp.MAX_MONSTERS);
|
// 按批次分组
|
||||||
smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length;
|
this.pendingBatches = [[], [], []];
|
||||||
this.waveTargetCount = this.pendingMonsters.length;
|
for (const m of monsters) {
|
||||||
|
const batch = Math.min(m.batch, BATCH_COUNT - 1);
|
||||||
|
this.pendingBatches[batch].push(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.waveTargetCount = monsters.length;
|
||||||
|
smc.vmdata.mission_data.pending_mon_num = this.waveTargetCount;
|
||||||
|
|
||||||
let hasBoss = monsters.some(m => m.isBoss);
|
let hasBoss = monsters.some(m => m.isBoss);
|
||||||
|
|
||||||
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 波次 ${this.currentWave} 生成怪物总数: ${this.waveTargetCount}`);
|
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 波次 ${this.currentWave} 生成怪物总数: ${this.waveTargetCount}`);
|
||||||
@@ -136,10 +161,13 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
this.currentWave = 1;
|
this.currentWave = 1;
|
||||||
this.waveTargetCount = 0;
|
this.waveTargetCount = 0;
|
||||||
this.waveSpawnedCount = 0;
|
this.waveSpawnedCount = 0;
|
||||||
this.pendingMonsters = [];
|
this.pendingBatches = [[], [], []];
|
||||||
|
this.currentBatch = 0;
|
||||||
|
this.batchTimer = 0;
|
||||||
|
this.isReleasing = false;
|
||||||
this.spawnQueue = [];
|
this.spawnQueue = [];
|
||||||
this.spawnTimer = 0;
|
this.spawnTimer = 0;
|
||||||
|
|
||||||
// 预生成第一波数据以获取数量和 Boss 信息
|
// 预生成第一波数据以获取数量和 Boss 信息
|
||||||
const monsters = spawningEngine.generateWave(this.currentWave);
|
const monsters = spawningEngine.generateWave(this.currentWave);
|
||||||
this.setupWaveData(monsters);
|
this.setupWaveData(monsters);
|
||||||
@@ -159,7 +187,7 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
private onTimeUpAdvanceWave() {
|
private onTimeUpAdvanceWave() {
|
||||||
this.currentWave += 1;
|
this.currentWave += 1;
|
||||||
smc.vmdata.mission_data.level = this.currentWave;
|
smc.vmdata.mission_data.level = this.currentWave;
|
||||||
|
|
||||||
const monsters = spawningEngine.generateWave(this.currentWave);
|
const monsters = spawningEngine.generateWave(this.currentWave);
|
||||||
this.setupWaveData(monsters);
|
this.setupWaveData(monsters);
|
||||||
}
|
}
|
||||||
@@ -167,17 +195,53 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
private onPhasePrepareEnd() {
|
private onPhasePrepareEnd() {
|
||||||
this.resetSlotSpawnData();
|
this.resetSlotSpawnData();
|
||||||
|
|
||||||
// 准备结束阶段:把本波待刷怪物转入逐个释放队列,
|
// 准备结束阶段:启动分批释放,
|
||||||
// 实际生成时机由 update() 按 MON_SPAWN_INTERVAL 节奏触发,
|
// 第一批立即转入 spawnQueue,后续批次由 update 按 BATCH_INTERVAL 推进。
|
||||||
// 让怪物从 X=400 出生点排成纵队依次向左推进。
|
this.startBatchRelease();
|
||||||
if (this.pendingMonsters.length > 0) {
|
}
|
||||||
const count = Math.min(this.pendingMonsters.length, MissionMonCompComp.MAX_MONSTERS);
|
|
||||||
for (let i = 0; i < count; i++) {
|
// ======================== 分批释放 ========================
|
||||||
this.spawnQueue.push(this.pendingMonsters.shift()!);
|
|
||||||
}
|
/** 启动分批释放:立即释放第一批,启动批次计时器 */
|
||||||
// 让首个怪物在下一帧立即刷出,避免额外延迟
|
private startBatchRelease() {
|
||||||
this.spawnTimer = MissionMonCompComp.MON_SPAWN_INTERVAL;
|
this.currentBatch = 0;
|
||||||
|
this.batchTimer = 0;
|
||||||
|
this.isReleasing = true;
|
||||||
|
this.releaseCurrentBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 推进到下一批次 */
|
||||||
|
private advanceBatch() {
|
||||||
|
if (this.currentBatch >= BATCH_COUNT - 1) {
|
||||||
|
this.isReleasing = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this.currentBatch++;
|
||||||
|
this.releaseCurrentBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将当前批次的怪物转入 spawnQueue,等待逐个刷出 */
|
||||||
|
private releaseCurrentBatch() {
|
||||||
|
const batch = this.pendingBatches[this.currentBatch];
|
||||||
|
if (batch.length === 0) {
|
||||||
|
// 当前批次为空,尝试推进到下一批
|
||||||
|
if (this.currentBatch < BATCH_COUNT - 1) {
|
||||||
|
this.currentBatch++;
|
||||||
|
this.releaseCurrentBatch();
|
||||||
|
} else {
|
||||||
|
this.isReleasing = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将批次怪物转入逐个刷怪队列
|
||||||
|
for (const m of batch) {
|
||||||
|
this.spawnQueue.push(m);
|
||||||
|
}
|
||||||
|
batch.length = 0;
|
||||||
|
|
||||||
|
// 让首个怪物在下一帧立即刷出,避免额外延迟
|
||||||
|
this.spawnTimer = MissionMonCompComp.MON_SPAWN_INTERVAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 槽位管理 ========================
|
// ======================== 槽位管理 ========================
|
||||||
@@ -197,6 +261,9 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
// 同步丢弃上一波未释放的队列,避免与新一波混合
|
// 同步丢弃上一波未释放的队列,避免与新一波混合
|
||||||
this.spawnQueue = [];
|
this.spawnQueue = [];
|
||||||
this.spawnTimer = 0;
|
this.spawnTimer = 0;
|
||||||
|
this.isReleasing = false;
|
||||||
|
this.currentBatch = 0;
|
||||||
|
this.batchTimer = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 怪物生成 ========================
|
// ======================== 怪物生成 ========================
|
||||||
@@ -211,19 +278,20 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
) {
|
) {
|
||||||
let mon = ecs.getEntity<Monster>(Monster);
|
let mon = ecs.getEntity<Monster>(Monster);
|
||||||
let scale = -1;
|
let scale = -1;
|
||||||
|
|
||||||
const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length];
|
const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length];
|
||||||
const spawnX = basePos.x;
|
const spawnX = basePos.x;
|
||||||
const landingY = basePos.y + (monData.isBoss ? 6 : 0);
|
const landingY = basePos.y + (monData.isBoss ? 6 : 0);
|
||||||
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
|
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
|
||||||
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
|
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
|
||||||
|
|
||||||
if (monData.testSkills) {
|
// 技能套装注入:通过 _testSkills 通道传递给 Mon.load()
|
||||||
(mon as any)._testSkills = monData.testSkills;
|
if (monData.skills) {
|
||||||
|
(mon as any)._testSkills = monData.skills;
|
||||||
}
|
}
|
||||||
|
|
||||||
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, posIndex);
|
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, posIndex);
|
||||||
|
|
||||||
const move = mon.get(MonMoveComp);
|
const move = mon.get(MonMoveComp);
|
||||||
if (move) {
|
if (move) {
|
||||||
move.spawnOrder = this.globalSpawnOrder;
|
move.spawnOrder = this.globalSpawnOrder;
|
||||||
|
|||||||
@@ -1,68 +1,133 @@
|
|||||||
/**
|
/**
|
||||||
* @file RogueConfig.ts
|
* @file RogueConfig.ts
|
||||||
* @description 肉鸽刷怪系统(基于硬编码规则 + 模块化随机)
|
* @description 肉鸽刷怪系统(基于英雄强度的动态难度 + 心流循环)
|
||||||
*
|
*
|
||||||
* 设计层次:
|
* 设计层次:
|
||||||
* 1. MonsterElite - 个体强化(5 种)
|
* 1. WaveType - 波型(普通 / 压力 / 放松),5 波一个心流循环
|
||||||
* 2. WaveEnchant - 波次级强化(环境修饰)
|
* 2. WaveConfig - 每波硬编码(数量 + 小队池 + HP/AP 倍率 + 强度微调)
|
||||||
* 3. SquadConfig - 小队模板(3-4 只怪的组合单元)
|
* 3. DynamicTuner - 动态难度调节器(系统根据战况放水 / 加压)
|
||||||
* 4. WaveConfig - 每波硬编码(基础数 + 小队池 + 强化池)
|
* 4. MonSkillSet - 怪物技能池(atking / atked / dead 等全触发类型)
|
||||||
* 5. RogueSpawningEngine - 生成引擎(按规则组合上述配置)
|
* 5. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度)
|
||||||
*
|
*
|
||||||
* 详细设计见 docs/superpowers/specs/2026-07-05-rogue-config-refactor-design.md
|
* 核心公式:
|
||||||
|
* heroPower = Σ calcHeroPower(HeroInfo[uuid], lv) (场上存活英雄)
|
||||||
|
* targetPower = heroPower × 波型系数 × wave.power_adjust × DynamicTuner.factor
|
||||||
|
* scale = targetPower ÷ Σ 怪物基础强度
|
||||||
|
* 每只怪: hp ×= scale, ap ×= scale
|
||||||
|
*
|
||||||
|
* 波次节奏:
|
||||||
|
* - 最大 20 波,第 20 波通关
|
||||||
|
* - 每波 30 秒,固定分 3 批,每 10 秒释放一批
|
||||||
|
* - 普通波 18~36 只,放松波 × 1.5 = 27~54 只
|
||||||
|
* - wave % 5 === 0 → 压力波(必带 Boss,强度高、数量少)
|
||||||
|
* - wave % 5 === 1 → 放松波(数量 × 1.5,强度低,爽快清屏)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HeroInfo, MonType, MonTypeName } from "../common/config/heroSet";
|
import { HeroInfo, MonType, MonTypeName, calcHeroPower, TriggerGrouped, LvReviveEntry, heroInfo } from "../common/config/heroSet";
|
||||||
|
import { SkillOverrides } from "../common/config/SkillSet";
|
||||||
import { FacSet } from "../common/config/GameSet";
|
import { FacSet } from "../common/config/GameSet";
|
||||||
|
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||||
|
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||||
|
|
||||||
// ======================== 1. 枚举与基础类型 ========================
|
// ======================== 1. 波型与节奏常量 ========================
|
||||||
|
|
||||||
/**
|
/** 波型枚举 */
|
||||||
* 个体强化类型(精选 5 种)
|
export enum WaveType {
|
||||||
* 设计权衡:HeroAttrsComp 暂无 atk_cd / regen 字段,因此 Swift/Regen 简化为数值差异化。
|
Normal = 0, // 普通波
|
||||||
* 待 HeroAttrsComp 升级后可启用 cd_mul / regen 扩展点。
|
Pressure = 1, // 压力波(wave % 5 === 0,必带 Boss)
|
||||||
*/
|
Relax = 2, // 放松波(wave % 5 === 1,量大强度低)
|
||||||
export enum MonsterElite {
|
|
||||||
Elite = 0, // 精英:全面强化
|
|
||||||
Berserk = 1, // 狂暴:高攻低血
|
|
||||||
Shield = 2, // 护盾:坦克
|
|
||||||
Regen = 3, // 再生:高血偏弱攻
|
|
||||||
Swift = 4, // 疾风:平衡偏输出
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 个体强化效果定义 */
|
/** 波型名称 */
|
||||||
export const MonsterEliteSet: Record<MonsterElite, {
|
export const WaveTypeName: Record<WaveType, string> = {
|
||||||
name: string;
|
[WaveType.Normal]: "普通",
|
||||||
hp_mul: number;
|
[WaveType.Pressure]: "压力",
|
||||||
ap_mul: number;
|
[WaveType.Relax]: "放松",
|
||||||
}> = {
|
|
||||||
[MonsterElite.Elite]: { name: "精英", hp_mul: 1.5, ap_mul: 1.30 },
|
|
||||||
[MonsterElite.Berserk]: { name: "狂暴", hp_mul: 0.9, ap_mul: 1.60 },
|
|
||||||
[MonsterElite.Shield]: { name: "护盾", hp_mul: 1.8, ap_mul: 0.80 },
|
|
||||||
[MonsterElite.Regen]: { name: "再生", hp_mul: 1.3, ap_mul: 0.95 },
|
|
||||||
[MonsterElite.Swift]: { name: "疾风", hp_mul: 1.0, ap_mul: 1.15 },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 波型强度系数(硬编码) */
|
||||||
|
export const WAVE_TYPE_POWER_RATIO: Record<WaveType, number> = {
|
||||||
|
[WaveType.Normal]: 0.9, // 普通波:标准强度
|
||||||
|
[WaveType.Pressure]: 1.2, // 压力波:强度高、数量少
|
||||||
|
[WaveType.Relax]: 0.6, // 放松波:量大、强度低
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 放松波数量倍率(相对普通波) */
|
||||||
|
export const RELAX_COUNT_MUL = 1.5;
|
||||||
|
|
||||||
|
/** 最大波次(第 20 波通关) */
|
||||||
|
export const MAX_WAVE = 20;
|
||||||
|
|
||||||
|
/** 每波时长(秒) */
|
||||||
|
export const WAVE_DURATION = 30;
|
||||||
|
|
||||||
|
/** 每波分批次数 */
|
||||||
|
export const BATCH_COUNT = 3;
|
||||||
|
|
||||||
|
/** 每批间隔(秒):30 秒 / 3 批 = 10 秒 */
|
||||||
|
export const BATCH_INTERVAL = WAVE_DURATION / BATCH_COUNT;
|
||||||
|
|
||||||
|
/** 每波怪物硬上限(放松波 36 × 1.5 = 54) */
|
||||||
|
export const MAX_MONSTERS = 54;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 旧版词缀类型枚举(已废弃)
|
* 获取指定波次的波型
|
||||||
* @deprecated 已被 MonsterElite 取代,仅保留导出避免破坏外部引用。
|
* @param wave 波次(1 起)
|
||||||
* 新代码请使用 MonsterElite。
|
* @returns WaveType
|
||||||
*/
|
*/
|
||||||
export enum AffixType {
|
export function getWaveType(wave: number): WaveType {
|
||||||
Elite = 0,
|
if (wave % 5 === 0) return WaveType.Pressure;
|
||||||
Berserk = 1,
|
if (wave % 5 === 1) return WaveType.Relax;
|
||||||
Shield = 2,
|
return WaveType.Normal;
|
||||||
Regen = 3,
|
|
||||||
Swift = 4,
|
|
||||||
Giant = 5,
|
|
||||||
Chain = 6,
|
|
||||||
SummonerA = 7,
|
|
||||||
CritRes = 8,
|
|
||||||
FreezeRes = 9,
|
|
||||||
KnockbackRes = 10,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 2. 怪物 UUID 池 ========================
|
// ======================== 2. 动态难度调节器 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态难度调节器(系统控制放水 / 加压)
|
||||||
|
* 与硬编码系数并存,用于根据战况实时微调难度。
|
||||||
|
*
|
||||||
|
* 用法示例(MissionComp 每波结束时调用):
|
||||||
|
* DynamicTuner.adjust(clearTime, heroDeathCount);
|
||||||
|
*
|
||||||
|
* 调节规则(内部硬编码):
|
||||||
|
* - 清场时间 < 20s 且无英雄死亡 → factor += 0.05(加压)
|
||||||
|
* - 清场时间 > 28s 或有英雄死亡 → factor -= 0.05(放水)
|
||||||
|
* - factor 范围钳制 [0.5, 2.0]
|
||||||
|
*/
|
||||||
|
export const DynamicTuner = {
|
||||||
|
/** 当前难度系数(默认 1.0,>1 加压,<1 放水) */
|
||||||
|
factor: 1.0,
|
||||||
|
|
||||||
|
/** 系数下限(最多放水到 50%) */
|
||||||
|
MIN_FACTOR: 0.5,
|
||||||
|
/** 系数上限(最多加压到 200%) */
|
||||||
|
MAX_FACTOR: 2.0,
|
||||||
|
/** 单次调节步长 */
|
||||||
|
STEP: 0.05,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据上一波战况自动调节难度
|
||||||
|
* @param clearTime 清场耗时(秒)
|
||||||
|
* @param heroDeathCount 英雄死亡数
|
||||||
|
*/
|
||||||
|
adjust(clearTime: number, heroDeathCount: number): void {
|
||||||
|
if (heroDeathCount > 0 || clearTime > WAVE_DURATION * 0.95) {
|
||||||
|
// 有英雄死亡或清场过慢 → 放水
|
||||||
|
this.factor = Math.max(this.MIN_FACTOR, this.factor - this.STEP);
|
||||||
|
} else if (clearTime < WAVE_DURATION * 0.65 && heroDeathCount === 0) {
|
||||||
|
// 清场过快且无死亡 → 加压
|
||||||
|
this.factor = Math.min(this.MAX_FACTOR, this.factor + this.STEP);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 重置调节器(每局开始时调用) */
|
||||||
|
reset(): void {
|
||||||
|
this.factor = 1.0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ======================== 3. 怪物 UUID 池 ========================
|
||||||
|
|
||||||
/** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */
|
/** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */
|
||||||
export const MonList: Record<number, number[]> = {};
|
export const MonList: Record<number, number[]> = {};
|
||||||
@@ -76,16 +141,12 @@ for (const key in HeroInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 2.5 怪物金币掉落配置 ========================
|
// ======================== 3.5 怪物金币掉落配置 ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 怪物金币掉落配置(按 MonType 分类)
|
* 怪物金币掉落配置(按 MonType 分类)
|
||||||
* - base: 普通怪物死亡时掉落的固定金币数
|
* - base: 普通怪物死亡时掉落的固定金币数
|
||||||
* - boss: Boss 怪物死亡时掉落的固定金币数(仅对 Boss 类型生效)
|
* - boss: Boss 怪物死亡时掉落的固定金币数(仅对 Boss 类型生效)
|
||||||
*
|
|
||||||
* 设计说明:
|
|
||||||
* 金币不再按波次固定发放,改为每只怪物死亡时掉落。
|
|
||||||
* Boss 提供高额固定金币奖励,作为战斗收益的核心来源。
|
|
||||||
*/
|
*/
|
||||||
export const MonsterGoldSet: Record<number, { base: number; boss: number }> = {
|
export const MonsterGoldSet: Record<number, { base: number; boss: number }> = {
|
||||||
[MonType.Melee]: { base: 1, boss: 0 },
|
[MonType.Melee]: { base: 1, boss: 0 },
|
||||||
@@ -110,32 +171,6 @@ export function getMonsterGoldDrop(monType: number, isBoss: boolean): number {
|
|||||||
return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base));
|
return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 3. 波次级强化库 ========================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 波次级强化(环境修饰符)
|
|
||||||
* 作用于整波怪物,与 MonsterElite(个体)解耦。
|
|
||||||
*/
|
|
||||||
export interface WaveEnchant {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
desc: string;
|
|
||||||
hp_mul?: number; // 全员 HP 乘子
|
|
||||||
ap_mul?: number; // 全员 AP 乘子
|
|
||||||
cd_mul?: number; // 全员攻击间隔乘子(后续扩展点)
|
|
||||||
elite_rate_mul?: number; // 该波个体强化出现概率乘子
|
|
||||||
weight: number; // 在强化池中的抽取权重
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 波次强化库(硬编码) */
|
|
||||||
export const WaveEnchantLibrary: Record<string, WaveEnchant> = {
|
|
||||||
frenzy: { id: "frenzy", name: "狂热浪潮", desc: "全员攻击力+25%", ap_mul: 1.25, weight: 10 },
|
|
||||||
ironhide: { id: "ironhide", name: "铁皮大军", desc: "全员生命+40%", hp_mul: 1.40, weight: 10 },
|
|
||||||
swift: { id: "swift", name: "疾风突袭", desc: "全员攻速+20%", cd_mul: 0.80, weight: 8 },
|
|
||||||
nightmare: { id: "nightmare", name: "梦魇来袭", desc: "个体强化率×2,HP+15%", elite_rate_mul: 2.0, hp_mul: 1.15, weight: 5 },
|
|
||||||
fortress: { id: "fortress", name: "钢铁堡垒", desc: "全员 HP+60% AP-15%", hp_mul: 1.60, ap_mul: 0.85, weight: 6 },
|
|
||||||
};
|
|
||||||
|
|
||||||
// ======================== 4. 小队模板库 ========================
|
// ======================== 4. 小队模板库 ========================
|
||||||
|
|
||||||
/** 小队内单种怪物的槽位定义 */
|
/** 小队内单种怪物的槽位定义 */
|
||||||
@@ -162,90 +197,183 @@ export const SquadLibrary: Record<string, SquadConfig> = {
|
|||||||
summoner_cult: { id: "summoner_cult", name: "召唤教派组", weight: 4, slots: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }] },
|
summoner_cult: { id: "summoner_cult", name: "召唤教派组", weight: 4, slots: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }] },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ======================== 5. 波次配置表 ========================
|
// ======================== 5. 怪物技能池 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 怪物技能套装(一套完整的触发技能配置)
|
||||||
|
* 覆盖触发时机: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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 怪物技能池(硬编码)
|
||||||
|
* 压力波 / Boss 可配置专属技能,普通波随机挂载增加变数。
|
||||||
|
*
|
||||||
|
* 技能 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 专属技能池(压力波 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 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ======================== 6. 波次配置表 ========================
|
||||||
|
|
||||||
/** 单波次完整配置 */
|
/** 单波次完整配置 */
|
||||||
export interface WaveConfig {
|
export interface WaveConfig {
|
||||||
/** 基础怪物数(1~12) */
|
/** 基础怪物总数(普通波 36 为上限,放松波自动 × 1.5 = 54) */
|
||||||
base_count: number;
|
base_count: number;
|
||||||
/** 可选小队 id 池,引擎按权重抽取拼装到 base_count */
|
/** 可选小队 id 池,引擎按权重抽取拼装到 base_count */
|
||||||
squad_pool: string[];
|
squad_pool: string[];
|
||||||
/** 可选波次强化 id 池,按权重抽 0~2 个(可空表示无强化) */
|
/** HP 强化倍率(硬编码,逐波递进) */
|
||||||
enchant_pool?: string[];
|
hp_mul: number;
|
||||||
/** 是否 Boss 波(首位放 Boss) */
|
/** AP 强化倍率(硬编码,逐波递进) */
|
||||||
|
ap_mul: number;
|
||||||
|
/** 强度微调(放水 / 加压,默认 1.0) */
|
||||||
|
power_adjust?: number;
|
||||||
|
/** 是否 Boss 波(压力波必为 true) */
|
||||||
boss_wave?: boolean;
|
boss_wave?: boolean;
|
||||||
/** 该波个体强化基础概率(默认按波次递增) */
|
/** 普通怪技能池 id(可选,随机挂载) */
|
||||||
elite_base_rate?: number;
|
skill_pool?: string[];
|
||||||
|
/** Boss 技能池 id(Boss 波专用,随机挂载) */
|
||||||
|
boss_skill_pool?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 波次配置表(硬编码 wave 1~30)
|
* 波次配置表(硬编码 wave 1~20)
|
||||||
* 节奏曲线:教学期(1-4) → 第一Boss(5) → 引入强化(6-9) → 第二Boss(10) →
|
*
|
||||||
* 组合多样化(11-14) → 第三Boss(15) → 高压阶段(16-24) →
|
* 心流循环(5 波一循环):
|
||||||
* 第四Boss(25) → 终极阶段(26-29) → 最终Boss(30)
|
* wave % 5 === 0 → 压力波(必带 Boss,强度高、数量少)
|
||||||
|
* wave % 5 === 1 → 放松波(数量 × 1.5,强度低)
|
||||||
|
* 其余 → 普通波(标准强度)
|
||||||
|
*
|
||||||
|
* 强度递进:hp_mul / ap_mul 每 5 波一档,压力波额外提升。
|
||||||
*/
|
*/
|
||||||
export const WaveConfigs: Record<number, WaveConfig> = {
|
export const WaveConfigs: Record<number, WaveConfig> = {
|
||||||
// ===== 教学期 =====
|
// ===== 第一循环:教学期 =====
|
||||||
1: { base_count: 3, squad_pool: ["melee_grunt"] },
|
1: { base_count: 18, squad_pool: ["melee_grunt"], hp_mul: 1.00, ap_mul: 1.00 },
|
||||||
2: { base_count: 4, squad_pool: ["melee_grunt", "mixed_balanced"] },
|
2: { base_count: 21, squad_pool: ["melee_grunt", "mixed_balanced"], hp_mul: 1.00, ap_mul: 1.00 },
|
||||||
3: { base_count: 5, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"] },
|
3: { base_count: 24, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.05, ap_mul: 1.00 },
|
||||||
4: { base_count: 6, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"] },
|
4: { base_count: 27, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"], hp_mul: 1.10, ap_mul: 1.05 },
|
||||||
// ===== 第一 Boss =====
|
// 压力波:第一 Boss
|
||||||
5: { base_count: 7, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], boss_wave: true },
|
5: { base_count: 21, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], hp_mul: 1.15, ap_mul: 1.10, boss_wave: true, boss_skill_pool: ["boss_rage"] },
|
||||||
// ===== 引入波次强化 =====
|
|
||||||
6: { base_count: 7, squad_pool: ["assassin_squad", "long_line", "mixed_balanced"], enchant_pool: ["frenzy"], elite_base_rate: 0.10 },
|
// ===== 第二循环:引入技能怪 =====
|
||||||
7: { base_count: 8, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.12 },
|
// 放松波:量大好清
|
||||||
8: { base_count: 8, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], enchant_pool: ["frenzy", "swift"], elite_base_rate: 0.14 },
|
6: { base_count: 30, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.10, ap_mul: 1.05 },
|
||||||
9: { base_count: 9, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"], elite_base_rate: 0.16 },
|
7: { base_count: 30, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], hp_mul: 1.15, ap_mul: 1.10, skill_pool: ["tough"] },
|
||||||
// ===== 第二 Boss =====
|
8: { base_count: 33, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.20, ap_mul: 1.15, skill_pool: ["berserk"] },
|
||||||
10: { base_count: 10, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide"], boss_wave: true, elite_base_rate: 0.18 },
|
9: { base_count: 33, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["berserk", "tough"] },
|
||||||
// ===== 组合多样化 =====
|
// 压力波:第二 Boss
|
||||||
11: { base_count: 10, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], enchant_pool: ["swift", "frenzy"], elite_base_rate: 0.18 },
|
10: { base_count: 24, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], hp_mul: 1.30, ap_mul: 1.25, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron"] },
|
||||||
12: { base_count: 10, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.20 },
|
|
||||||
13: { base_count: 11, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.22 },
|
// ===== 第三循环:组合多样化 =====
|
||||||
14: { base_count: 11, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], enchant_pool: ["ironhide", "swift"], elite_base_rate: 0.24 },
|
// 放松波
|
||||||
// ===== 第三 Boss(中期高潮) =====
|
11: { base_count: 33, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["leech"] },
|
||||||
15: { base_count: 11, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], enchant_pool: ["nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.25 },
|
12: { base_count: 33, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.30, ap_mul: 1.25, skill_pool: ["tough", "warcry"] },
|
||||||
// ===== 高压阶段 =====
|
13: { base_count: 36, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], hp_mul: 1.35, ap_mul: 1.30, skill_pool: ["berserk", "leech"] },
|
||||||
16: { base_count: 11, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], enchant_pool: ["frenzy", "swift"], elite_base_rate: 0.26 },
|
14: { base_count: 36, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], hp_mul: 1.40, ap_mul: 1.35, skill_pool: ["berserk", "tough", "legacy"] },
|
||||||
17: { base_count: 11, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"], elite_base_rate: 0.27 },
|
// 压力波:第三 Boss(中期高潮)
|
||||||
18: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], enchant_pool: ["nightmare", "swift"], elite_base_rate: 0.28 },
|
15: { base_count: 27, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.50, ap_mul: 1.40, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron", "boss_doom"] },
|
||||||
19: { base_count: 12, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], enchant_pool: ["fortress"], elite_base_rate: 0.28 },
|
|
||||||
20: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], enchant_pool: ["frenzy", "ironhide"], boss_wave: true, elite_base_rate: 0.30 },
|
// ===== 第四循环:终极阶段 =====
|
||||||
21: { base_count: 12, squad_pool: ["melee_grunt", "assassin_squad", "summoner_cult"], enchant_pool: ["swift", "nightmare"], elite_base_rate: 0.30 },
|
// 放松波
|
||||||
22: { base_count: 12, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "fortress"], elite_base_rate: 0.30 },
|
16: { base_count: 36, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], hp_mul: 1.45, ap_mul: 1.40, skill_pool: ["leech", "warcry"] },
|
||||||
23: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "melee_grunt"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.32 },
|
17: { base_count: 36, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], hp_mul: 1.50, ap_mul: 1.45, skill_pool: ["berserk", "legacy"] },
|
||||||
24: { base_count: 12, squad_pool: ["mixed_balanced", "heavy_shield", "long_line"], enchant_pool: ["fortress", "swift"], elite_base_rate: 0.32 },
|
18: { base_count: 36, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], hp_mul: 1.55, ap_mul: 1.50, skill_pool: ["berserk", "tough", "leech"] },
|
||||||
// ===== 第四 Boss =====
|
19: { base_count: 36, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], hp_mul: 1.60, ap_mul: 1.55, skill_pool: ["berserk", "tough", "legacy", "warcry"] },
|
||||||
25: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "heavy_shield"], enchant_pool: ["nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.35 },
|
// 压力波:最终 Boss
|
||||||
// ===== 终极阶段 =====
|
20: { base_count: 30, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], hp_mul: 1.80, ap_mul: 1.70, boss_wave: true, boss_skill_pool: ["boss_doom", "boss_rage"] },
|
||||||
26: { base_count: 12, squad_pool: ["melee_grunt", "assassin_squad", "long_line", "summoner_cult"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.35 },
|
|
||||||
27: { base_count: 12, squad_pool: ["heavy_shield", "mixed_balanced", "summoner_cult"], enchant_pool: ["ironhide", "fortress"], elite_base_rate: 0.38 },
|
|
||||||
28: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "melee_grunt"], enchant_pool: ["swift", "nightmare"], elite_base_rate: 0.40 },
|
|
||||||
29: { base_count: 12, squad_pool: ["mixed_balanced", "heavy_shield", "summoner_cult"], enchant_pool: ["frenzy", "ironhide", "fortress"], elite_base_rate: 0.42 },
|
|
||||||
// ===== 最终 Boss =====
|
|
||||||
30: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide", "swift", "nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.45 },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ======================== 6. 全局难度缩放 ========================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据波次返回全局 HP/AP 乘子,每 5 波递进一档
|
|
||||||
* @param wave 当前波次
|
|
||||||
* @returns hp_mul / ap_mul
|
|
||||||
*/
|
|
||||||
export function getGlobalScale(wave: number): { hp_mul: number; ap_mul: number } {
|
|
||||||
const step = Math.floor((wave - 1) / 5);
|
|
||||||
return {
|
|
||||||
hp_mul: 1 + 0.15 * step, // wave 1=1.0, 6=1.15, 11=1.30, 16=1.45, 21=1.60, 26=1.75
|
|
||||||
ap_mul: 1 + 0.08 * step, // wave 1=1.0, 6=1.08, 11=1.16, 16=1.24, 21=1.32, 26=1.40
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 7. 配置校验 ========================
|
// ======================== 7. 配置校验 ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验 WaveConfigs / SquadLibrary / WaveEnchantLibrary 引用一致性
|
* 校验 WaveConfigs / SquadLibrary / MonSkillPool 引用一致性
|
||||||
* 建议在游戏启动时调用一次,便于发现配置错误
|
* 建议在游戏启动时调用一次,便于发现配置错误
|
||||||
* @returns 错误信息数组,空数组表示校验通过
|
* @returns 错误信息数组,空数组表示校验通过
|
||||||
*/
|
*/
|
||||||
@@ -260,15 +388,22 @@ export function validateRogueConfig(): string[] {
|
|||||||
errors.push(`Wave ${wave} 引用了不存在的小队: ${squadId}`);
|
errors.push(`Wave ${wave} 引用了不存在的小队: ${squadId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cfg.enchant_pool) {
|
if (cfg.skill_pool) {
|
||||||
for (const enchId of cfg.enchant_pool) {
|
for (const skillId of cfg.skill_pool) {
|
||||||
if (!WaveEnchantLibrary[enchId]) {
|
if (!MonSkillPool[skillId]) {
|
||||||
errors.push(`Wave ${wave} 引用了不存在的强化: ${enchId}`);
|
errors.push(`Wave ${wave} 引用了不存在的技能: ${skillId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cfg.base_count < 1 || cfg.base_count > 12) {
|
if (cfg.boss_skill_pool) {
|
||||||
errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~12)`);
|
for (const skillId of cfg.boss_skill_pool) {
|
||||||
|
if (!BossSkillPool[skillId]) {
|
||||||
|
errors.push(`Wave ${wave} 引用了不存在的 Boss 技能: ${skillId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cfg.base_count < 1 || cfg.base_count > MAX_MONSTERS) {
|
||||||
|
errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~${MAX_MONSTERS})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,23 +430,22 @@ export interface GeneratedMonster {
|
|||||||
ap: number;
|
ap: number;
|
||||||
isBoss: boolean;
|
isBoss: boolean;
|
||||||
spawnIndex: number;
|
spawnIndex: number;
|
||||||
|
/** 本怪所属批次(0~2,由 MissionMonComp 按 BATCH_INTERVAL 释放) */
|
||||||
|
batch: number;
|
||||||
|
|
||||||
/** @deprecated 已被 elite 取代,始终返回 [],仅向后兼容 */
|
/**
|
||||||
affixes: AffixType[];
|
* 怪物技能套装(覆盖全部触发时机)
|
||||||
|
* 注入方式与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp
|
||||||
/** 个体强化(无则 undefined) */
|
*/
|
||||||
elite?: MonsterElite;
|
skills?: {
|
||||||
/** 该波触发的波次强化 id 列表 */
|
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
|
||||||
wave_enchants: string[];
|
call?: TriggerGrouped;
|
||||||
|
atking?: TriggerGrouped;
|
||||||
/** 测试模式专用技能覆盖(按 s_uuid 分组 + lv 数组,与 heroInfo 结构一致) */
|
atked?: TriggerGrouped;
|
||||||
testSkills?: {
|
dead?: TriggerGrouped;
|
||||||
skill?: { s_uuid: number; cd?: number; overrides?: any };
|
fstart?: TriggerGrouped;
|
||||||
atking?: Record<number, { lv: number; t_num: number; overrides?: any }[]>;
|
fend?: TriggerGrouped;
|
||||||
atked?: Record<number, { lv: number; t_num: number; overrides?: any }[]>;
|
revive?: LvReviveEntry[];
|
||||||
dead?: Record<number, { lv: number; t_num: number; overrides?: any }[]>;
|
|
||||||
fstart?: Record<number, { lv: number; t_num: number; overrides?: any }[]>;
|
|
||||||
fend?: Record<number, { lv: number; t_num: number; overrides?: any }[]>;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,35 +458,28 @@ export const TestModeConfig = {
|
|||||||
growthRatePerWave: 0.2,
|
growthRatePerWave: 0.2,
|
||||||
monType: MonType.Melee,
|
monType: MonType.Melee,
|
||||||
monUuid: 6001,
|
monUuid: 6001,
|
||||||
/** @deprecated 已被 testElite 取代 */
|
|
||||||
affixes: [] as AffixType[],
|
|
||||||
spawnCount: 1,
|
spawnCount: 1,
|
||||||
|
|
||||||
/** 测试个体强化 */
|
skill: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides } | undefined,
|
||||||
testElite: undefined as MonsterElite | undefined,
|
atking: undefined as TriggerGrouped | undefined,
|
||||||
/** 测试波次强化 id */
|
atked: undefined as TriggerGrouped | undefined,
|
||||||
testWaveEnchant: undefined as string | undefined,
|
dead: undefined as TriggerGrouped | undefined,
|
||||||
|
fstart: undefined as TriggerGrouped | undefined,
|
||||||
skill: undefined as { s_uuid: number; cd?: number; overrides?: any } | undefined,
|
fend: undefined as TriggerGrouped | undefined,
|
||||||
atking: undefined as Record<number, { lv: number; t_num: number; overrides?: any }[]> | undefined,
|
|
||||||
atked: undefined as Record<number, { lv: number; t_num: number; overrides?: any }[]> | undefined,
|
|
||||||
dead: undefined as Record<number, { lv: number; t_num: number; overrides?: any }[]> | undefined,
|
|
||||||
fstart: undefined as Record<number, { lv: number; t_num: number; overrides?: any }[]> | undefined,
|
|
||||||
fend: undefined as Record<number, { lv: number; t_num: number; overrides?: any }[]> | undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ======================== 10. 生成引擎 ========================
|
// ======================== 10. 生成引擎 ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 肉鸽刷怪生成引擎
|
* 肉鸽刷怪生成引擎
|
||||||
* 按硬编码 WaveConfig 规则,组合小队模板与双层强化系统生成怪物列表
|
* 按英雄强度反推怪物强度,结合波型系数与动态调节器生成怪物列表
|
||||||
*/
|
*/
|
||||||
export class RogueSpawningEngine {
|
export class RogueSpawningEngine {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成指定波次的怪物列表
|
* 生成指定波次的怪物列表
|
||||||
* @param waveNumber 波次(1 起,>30 时复用 wave 30 配置)
|
* @param waveNumber 波次(1 起,>MAX_WAVE 时复用 wave MAX_WAVE 配置)
|
||||||
* @returns 怪物列表,长度 ≤ 12
|
* @returns 怪物列表,长度 ≤ MAX_MONSTERS
|
||||||
*/
|
*/
|
||||||
generateWave(waveNumber: number): GeneratedMonster[] {
|
generateWave(waveNumber: number): GeneratedMonster[] {
|
||||||
if (waveNumber < 1) return [];
|
if (waveNumber < 1) return [];
|
||||||
@@ -362,97 +489,120 @@ export class RogueSpawningEngine {
|
|||||||
return this.generateTestWave(waveNumber);
|
return this.generateTestWave(waveNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 取硬编码 WaveConfig(>30 时复用 wave 30 配置)
|
const wave = Math.min(waveNumber, MAX_WAVE);
|
||||||
const cfg = WaveConfigs[Math.min(waveNumber, 30)];
|
const cfg = WaveConfigs[wave];
|
||||||
|
const waveType = getWaveType(wave);
|
||||||
|
const typeRatio = WAVE_TYPE_POWER_RATIO[waveType];
|
||||||
|
|
||||||
// 2. 抽取波次级强化(按权重从 enchant_pool 抽 0~2 个)
|
// 1. 计算目标强度
|
||||||
const enchants = this.pickEnchants(cfg.enchant_pool);
|
const heroPower = this.getCurrentHeroPower();
|
||||||
|
const powerAdjust = cfg.power_adjust ?? 1.0;
|
||||||
|
const targetPower = heroPower * typeRatio * powerAdjust * DynamicTuner.factor;
|
||||||
|
|
||||||
// 3. 计算波次最终属性乘子
|
// 2. 确定怪物总数(放松波 × 1.5)
|
||||||
const globalScale = getGlobalScale(waveNumber);
|
let totalCount = cfg.base_count;
|
||||||
const enchantHpMul = enchants.reduce((m, e) => m * (e.hp_mul ?? 1), 1);
|
if (waveType === WaveType.Relax) {
|
||||||
const enchantApMul = enchants.reduce((m, e) => m * (e.ap_mul ?? 1), 1);
|
totalCount = Math.round(totalCount * RELAX_COUNT_MUL);
|
||||||
const eliteRateMul = enchants.reduce((m, e) => m * (e.elite_rate_mul ?? 1), 1);
|
}
|
||||||
|
totalCount = Math.min(totalCount, MAX_MONSTERS);
|
||||||
|
|
||||||
// 4. Boss 位(Boss 波首位占 1 个)
|
// 3. Boss 位(压力波必带 Boss,占 1 个名额)
|
||||||
const monsters: GeneratedMonster[] = [];
|
const monsters: GeneratedMonster[] = [];
|
||||||
let remaining = cfg.base_count;
|
let remaining = totalCount;
|
||||||
if (cfg.boss_wave) {
|
if (cfg.boss_wave) {
|
||||||
monsters.push(this.makeBoss(waveNumber));
|
monsters.push(this.makeBoss(wave, cfg));
|
||||||
remaining -= 1;
|
remaining -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining
|
// 4. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining
|
||||||
const squadMonsters = this.assembleSquads(cfg.squad_pool, remaining, waveNumber);
|
const squadMonsters = this.assembleSquads(cfg.squad_pool, remaining, wave);
|
||||||
monsters.push(...squadMonsters);
|
monsters.push(...squadMonsters);
|
||||||
|
|
||||||
// 6. 应用全局 & 波次 Enchant 乘子
|
// 5. 应用硬编码 HP/AP 倍率
|
||||||
const waveEnchantIds = enchants.map(e => e.id);
|
|
||||||
for (const m of monsters) {
|
for (const m of monsters) {
|
||||||
m.hp = Math.max(1, Math.round(m.hp * globalScale.hp_mul * enchantHpMul));
|
m.hp = Math.max(1, Math.round(m.hp * cfg.hp_mul));
|
||||||
m.ap = Math.max(1, Math.round(m.ap * globalScale.ap_mul * enchantApMul));
|
m.ap = Math.max(1, Math.round(m.ap * cfg.ap_mul));
|
||||||
m.wave_enchants = waveEnchantIds.slice();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 应用个体 Elite(非 Boss 怪,每只最多 1 个)
|
// 6. 按英雄强度反推缩放系数
|
||||||
const eliteBaseRate = cfg.elite_base_rate ?? Math.min(0.05 + waveNumber * 0.01, 0.30);
|
const totalBasePower = monsters.reduce((sum, m) => {
|
||||||
for (const m of monsters) {
|
const info = HeroInfo[m.uuid];
|
||||||
if (!m.isBoss && Math.random() < eliteBaseRate * eliteRateMul) {
|
return sum + (info ? calcHeroPower(info, 1) : m.hp + m.ap);
|
||||||
const elite = this.pickElite();
|
}, 0);
|
||||||
this.applyElite(m, elite);
|
|
||||||
|
if (totalBasePower > 0 && targetPower > 0) {
|
||||||
|
const scale = targetPower / totalBasePower;
|
||||||
|
for (const m of monsters) {
|
||||||
|
m.hp = Math.max(1, Math.round(m.hp * scale));
|
||||||
|
m.ap = Math.max(1, Math.round(m.ap * scale));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. 硬上限保护(理论不会触发,但防止配置错误)
|
// 7. 挂载技能(普通怪随机技能池,Boss 专属技能池)
|
||||||
return monsters.slice(0, 12);
|
for (const m of monsters) {
|
||||||
|
if (m.isBoss && cfg.boss_skill_pool) {
|
||||||
|
m.skills = this.pickSkillSet(cfg.boss_skill_pool, BossSkillPool);
|
||||||
|
} else if (!m.isBoss && cfg.skill_pool) {
|
||||||
|
m.skills = this.pickSkillSet(cfg.skill_pool, MonSkillPool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 分配批次(0~2,均匀分布)
|
||||||
|
for (let i = 0; i < monsters.length; i++) {
|
||||||
|
monsters[i].batch = i % BATCH_COUNT;
|
||||||
|
monsters[i].spawnIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 硬上限保护
|
||||||
|
return monsters.slice(0, MAX_MONSTERS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置引擎状态(保留接口兼容,当前无内部状态) */
|
/** 重置引擎状态(每局开始时调用) */
|
||||||
reset(): void {
|
reset(): void {
|
||||||
// 无可变状态
|
DynamicTuner.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定波次的怪物总数
|
* 获取指定波次的怪物总数
|
||||||
* @param waveNumber 目标波数
|
* @param waveNumber 目标波数
|
||||||
* @returns 预计生成的怪物总数(base_count,受 12 上限约束)
|
* @returns 预计生成的怪物总数
|
||||||
*/
|
*/
|
||||||
getWaveMonsterCount(waveNumber: number): number {
|
getWaveMonsterCount(waveNumber: number): number {
|
||||||
if (waveNumber < 1) return 0;
|
if (waveNumber < 1) return 0;
|
||||||
if (TestModeConfig.enable) {
|
if (TestModeConfig.enable) {
|
||||||
return Math.max(1, TestModeConfig.spawnCount || 1);
|
return Math.max(1, TestModeConfig.spawnCount || 1);
|
||||||
}
|
}
|
||||||
const cfg = WaveConfigs[Math.min(waveNumber, 30)];
|
const wave = Math.min(waveNumber, MAX_WAVE);
|
||||||
return Math.min(cfg.base_count, 12);
|
const cfg = WaveConfigs[wave];
|
||||||
}
|
const waveType = getWaveType(wave);
|
||||||
|
let count = cfg.base_count;
|
||||||
/**
|
if (waveType === WaveType.Relax) {
|
||||||
* 获取波次槽位配置(向后兼容接口)
|
count = Math.round(count * RELAX_COUNT_MUL);
|
||||||
* 内部从 generateWave 反推,仅用于历史调用方
|
|
||||||
* @param waveNumber 目标波数
|
|
||||||
*/
|
|
||||||
getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
|
|
||||||
const generated = this.generateWave(waveNumber);
|
|
||||||
const slotMap = new Map<number, { count: number; affixes: AffixType[] }>();
|
|
||||||
|
|
||||||
for (const m of generated) {
|
|
||||||
const existing = slotMap.get(m.type);
|
|
||||||
if (existing) {
|
|
||||||
existing.count++;
|
|
||||||
} else {
|
|
||||||
slotMap.set(m.type, { count: 1, affixes: [] });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return Math.min(count, MAX_MONSTERS);
|
||||||
return Array.from(slotMap.entries()).map(([type, data]) => ({
|
|
||||||
type,
|
|
||||||
count: data.count,
|
|
||||||
...(data.affixes.length > 0 ? { affixes: data.affixes } : {}),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 私有生成子算法 ========================
|
// ======================== 私有生成子算法 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算场上存活英雄的总强度
|
||||||
|
* 通过 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);
|
||||||
|
}
|
||||||
|
|
||||||
/** 测试模式:完全绕过引擎逻辑 */
|
/** 测试模式:完全绕过引擎逻辑 */
|
||||||
private generateTestWave(waveNumber: number): GeneratedMonster[] {
|
private generateTestWave(waveNumber: number): GeneratedMonster[] {
|
||||||
const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave;
|
const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave;
|
||||||
@@ -465,12 +615,10 @@ export class RogueSpawningEngine {
|
|||||||
type: TestModeConfig.monType,
|
type: TestModeConfig.monType,
|
||||||
hp: Math.round(TestModeConfig.baseHp * growth),
|
hp: Math.round(TestModeConfig.baseHp * growth),
|
||||||
ap: Math.round(TestModeConfig.baseAp * growth),
|
ap: Math.round(TestModeConfig.baseAp * growth),
|
||||||
affixes: [...TestModeConfig.affixes],
|
|
||||||
isBoss: false,
|
isBoss: false,
|
||||||
spawnIndex: i,
|
spawnIndex: i,
|
||||||
elite: TestModeConfig.testElite,
|
batch: i % BATCH_COUNT,
|
||||||
wave_enchants: TestModeConfig.testWaveEnchant ? [TestModeConfig.testWaveEnchant] : [],
|
skills: {
|
||||||
testSkills: {
|
|
||||||
skill: TestModeConfig.skill,
|
skill: TestModeConfig.skill,
|
||||||
atking: TestModeConfig.atking,
|
atking: TestModeConfig.atking,
|
||||||
atked: TestModeConfig.atked,
|
atked: TestModeConfig.atked,
|
||||||
@@ -509,7 +657,7 @@ export class RogueSpawningEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 生成 Boss(首位) */
|
/** 生成 Boss(首位) */
|
||||||
private makeBoss(wave: number): GeneratedMonster {
|
private makeBoss(wave: number, cfg: WaveConfig): GeneratedMonster {
|
||||||
const isMeleeBoss = Math.random() < 0.5;
|
const isMeleeBoss = Math.random() < 0.5;
|
||||||
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
|
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
|
||||||
|
|
||||||
@@ -532,10 +680,9 @@ export class RogueSpawningEngine {
|
|||||||
type,
|
type,
|
||||||
hp: Math.round(baseHp * bossBonusHpMul),
|
hp: Math.round(baseHp * bossBonusHpMul),
|
||||||
ap: baseAp,
|
ap: baseAp,
|
||||||
affixes: [], // 兼容字段
|
|
||||||
isBoss: true,
|
isBoss: true,
|
||||||
spawnIndex: 0,
|
spawnIndex: 0,
|
||||||
wave_enchants: [], // 后续统一填充
|
batch: 0, // Boss 固定第一批
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,52 +704,28 @@ export class RogueSpawningEngine {
|
|||||||
type,
|
type,
|
||||||
hp: baseHp,
|
hp: baseHp,
|
||||||
ap: baseAp,
|
ap: baseAp,
|
||||||
affixes: [], // 兼容字段
|
|
||||||
isBoss: false,
|
isBoss: false,
|
||||||
spawnIndex,
|
spawnIndex,
|
||||||
wave_enchants: [], // 后续统一填充
|
batch: 0, // 后续统一分配
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 抽取波次级强化(按权重抽 0~2 个,不重复) */
|
/** 从技能池中按权重抽取一套技能 */
|
||||||
private pickEnchants(pool?: string[]): WaveEnchant[] {
|
private pickSkillSet(pool: string[], library: Record<string, MonSkillSet>): GeneratedMonster["skills"] | undefined {
|
||||||
if (!pool || pool.length === 0) return [];
|
const valid = pool.map(id => library[id]).filter(s => s);
|
||||||
const enchants: WaveEnchant[] = [];
|
if (valid.length === 0) return undefined;
|
||||||
|
const picked = this.pickWeighted(valid);
|
||||||
// 第一个 Enchant: 70% 概率抽 1 个
|
if (!picked) return undefined;
|
||||||
if (Math.random() < 0.7) {
|
return {
|
||||||
const first = this.pickWeightedEnchant(pool);
|
skill: picked.skill,
|
||||||
if (first) enchants.push(first);
|
call: picked.call,
|
||||||
}
|
atking: picked.atking,
|
||||||
|
atked: picked.atked,
|
||||||
// 第二个 Enchant: 30% 概率再抽 1 个(不重复)
|
dead: picked.dead,
|
||||||
if (Math.random() < 0.3 && pool.length > 1) {
|
fstart: picked.fstart,
|
||||||
const remaining = pool.filter(id => !enchants.some(e => e.id === id));
|
fend: picked.fend,
|
||||||
const second = this.pickWeightedEnchant(remaining);
|
revive: picked.revive,
|
||||||
if (second) enchants.push(second);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return enchants;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 抽取个体强化(等概率随机 1 种) */
|
|
||||||
private pickElite(): MonsterElite {
|
|
||||||
const candidates: MonsterElite[] = [
|
|
||||||
MonsterElite.Elite,
|
|
||||||
MonsterElite.Berserk,
|
|
||||||
MonsterElite.Shield,
|
|
||||||
MonsterElite.Swift,
|
|
||||||
MonsterElite.Regen,
|
|
||||||
];
|
|
||||||
return candidates[Math.floor(Math.random() * candidates.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 应用个体强化到怪物 */
|
|
||||||
private applyElite(m: GeneratedMonster, elite: MonsterElite): void {
|
|
||||||
const def = MonsterEliteSet[elite];
|
|
||||||
m.hp = Math.max(1, Math.round(m.hp * def.hp_mul));
|
|
||||||
m.ap = Math.max(1, Math.round(m.ap * def.ap_mul));
|
|
||||||
m.elite = elite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按权重从小队 id 池抽 1 个小队 */
|
/** 按权重从小队 id 池抽 1 个小队 */
|
||||||
@@ -612,13 +735,6 @@ export class RogueSpawningEngine {
|
|||||||
return this.pickWeighted(valid);
|
return this.pickWeighted(valid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按权重从强化 id 池抽 1 个强化 */
|
|
||||||
private pickWeightedEnchant(pool: string[]): WaveEnchant | null {
|
|
||||||
const valid = pool.map(id => WaveEnchantLibrary[id]).filter(e => e);
|
|
||||||
if (valid.length === 0) return null;
|
|
||||||
return this.pickWeighted(valid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 通用加权随机抽取 */
|
/** 通用加权随机抽取 */
|
||||||
private pickWeighted<T extends { weight: number }>(items: T[]): T | null {
|
private pickWeighted<T extends { weight: number }>(items: T[]): T | null {
|
||||||
if (items.length === 0) return null;
|
if (items.length === 0) return null;
|
||||||
@@ -641,8 +757,6 @@ export const spawningEngine = new RogueSpawningEngine();
|
|||||||
export interface IWaveSlot {
|
export interface IWaveSlot {
|
||||||
type: number;
|
type: number;
|
||||||
count: number;
|
count: number;
|
||||||
/** @deprecated 已废弃,新接口不再使用 */
|
|
||||||
affixes?: AffixType[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -653,9 +767,16 @@ export function getWaveMonsterCount(waveNumber: number): number {
|
|||||||
return spawningEngine.getWaveMonsterCount(waveNumber);
|
return spawningEngine.getWaveMonsterCount(waveNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取波次槽位配置(向后兼容) */
|
/** 获取波次槽位配置(向后兼容,从 generateWave 反推) */
|
||||||
export function getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
|
export function getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
|
||||||
return spawningEngine.getWaveSlotConfig(waveNumber);
|
const generated = spawningEngine.generateWave(waveNumber);
|
||||||
|
const slotMap = new Map<number, number>();
|
||||||
|
|
||||||
|
for (const m of generated) {
|
||||||
|
slotMap.set(m.type, (slotMap.get(m.type) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(slotMap.entries()).map(([type, count]) => ({ type, count }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultWaveSlot: IWaveSlot[] = [
|
export const DefaultWaveSlot: IWaveSlot[] = [
|
||||||
@@ -671,7 +792,7 @@ export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = new Proxy(
|
|||||||
get(_target, prop: string) {
|
get(_target, prop: string) {
|
||||||
const wave = parseInt(prop, 10);
|
const wave = parseInt(prop, 10);
|
||||||
if (!isNaN(wave) && wave >= 1) {
|
if (!isNaN(wave) && wave >= 1) {
|
||||||
return spawningEngine.getWaveSlotConfig(wave);
|
return getWaveSlotConfig(wave);
|
||||||
}
|
}
|
||||||
if (prop === "toJSON") return () => ({});
|
if (prop === "toJSON") return () => ({});
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user