- 在 SCastSystem 中增加战斗状态检查,防止非战斗时误触发技能 - 同步 mission.in_fight 状态到 vmdata.mission_data.in_fight 以保持数据一致性 - 调整 MissionCardComp 在波次开始时正确布局卡牌槽位并分发卡牌 - 优化游戏地平线位置和 UI 布局参数
459 lines
17 KiB
TypeScript
459 lines
17 KiB
TypeScript
import { _decorator, Vec3,Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label } from "cc";
|
||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||
import { smc } from "../common/SingletonModuleComp";
|
||
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
|
||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||
import { GameEvent } from "../common/config/GameEvent";
|
||
import { HeroViewComp } from "../hero/HeroViewComp";
|
||
import { UIID } from "../common/config/GameUIConfig";
|
||
import { SkillView } from "../skill/SkillView";
|
||
import { FacSet, FightSet } from "../common/config/GameSet";
|
||
import { mLogger } from "../common/Logger";
|
||
import { Monster } from "../hero/Mon";
|
||
import { Skill } from "../skill/Skill";
|
||
import { Tooltip } from "../skill/Tooltip";
|
||
const { ccclass, property } = _decorator;
|
||
|
||
|
||
//@todo 需要关注 当boss死亡的时候的动画播放完成后,需要触发事件,通知 MissionComp 进行奖励处理
|
||
|
||
/** 视图层对象 */
|
||
@ccclass('MissionComp')
|
||
@ecs.register('MissionComp', false)
|
||
export class MissionComp extends CCComp {
|
||
@property({ tooltip: "是否启用调试日志" })
|
||
private debugMode: boolean = false;
|
||
@property({ tooltip: "是否显示战斗内存观测面板" })
|
||
private showMemoryPanel: boolean = false;
|
||
@property({ tooltip: "场上怪物上限" })
|
||
private maxMonsterCount: number = 5;
|
||
@property({ tooltip: "恢复刷怪阈值" })
|
||
private resumeMonsterCount: number = 3;
|
||
@property({ tooltip: "准备阶段基础金币奖励" })
|
||
private prepareBaseCoinReward: number = 10;
|
||
@property({ tooltip: "每波准备阶段额外金币" })
|
||
private prepareCoinWaveGrow: number = 1;
|
||
@property({ tooltip: "准备阶段金币奖励上限" })
|
||
private prepareCoinRewardCap: number = 20;
|
||
|
||
// VictoryComp:any = null;
|
||
// reward:number = 0;
|
||
// reward_num:number = 0;
|
||
@property(Node)
|
||
start_btn:Node = null!
|
||
@property(Node)
|
||
time_node:Node = null!
|
||
|
||
FightTime:number = FightSet.FiIGHT_TIME
|
||
/** 剩余复活次数 */
|
||
revive_times: number = 1;
|
||
rewards:any[]=[]
|
||
game_data:any={
|
||
exp:0,
|
||
gold:0,
|
||
diamond:0
|
||
}
|
||
|
||
private lastTimeStr: string = "";
|
||
private lastTimeSecond: number = -1;
|
||
private memoryLabel: Label | null = null;
|
||
private memoryRefreshTimer: number = 0;
|
||
private lastMemoryText: string = "";
|
||
private perfDtAcc: number = 0;
|
||
private perfFrameCount: number = 0;
|
||
private heapBaseMB: number = -1;
|
||
private heapPeakMB: number = 0;
|
||
private heapTrendPerMinMB: number = 0;
|
||
private heapTrendTimer: number = 0;
|
||
private heapTrendBaseMB: number = -1;
|
||
private monsterCountSyncTimer: number = 0;
|
||
private currentWave: number = 0;
|
||
private lastPrepareCoinWave: number = 0;
|
||
private readonly heroViewMatcher = ecs.allOf(HeroViewComp);
|
||
private readonly skillViewMatcher = ecs.allOf(SkillView);
|
||
private readonly heroAttrsMatcher = ecs.allOf(HeroAttrsComp);
|
||
|
||
// 记录已触发的特殊刷怪索引
|
||
|
||
onLoad(){
|
||
this.showMemoryPanel = false
|
||
this.on(GameEvent.MissionStart,this.mission_start,this)
|
||
// this.on(GameEvent.HeroDead,this.do_hero_dead,this)
|
||
// this.on(GameEvent.FightEnd,this.fight_end,this)
|
||
this.on(GameEvent.MissionEnd,this.mission_end,this)
|
||
this.on(GameEvent.NewWave,this.onNewWave,this)
|
||
this.on(GameEvent.DO_AD_BACK,this.do_ad,this)
|
||
this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this)
|
||
this.removeMemoryPanel()
|
||
}
|
||
onDestroy(){
|
||
this.start_btn?.off(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this)
|
||
}
|
||
protected update(dt: number): void {
|
||
if(!smc.mission.play) return
|
||
if(smc.mission.pause) return
|
||
if(smc.mission.in_fight){
|
||
this.syncMonsterSpawnState(dt)
|
||
if(smc.mission.stop_mon_action) return
|
||
smc.vmdata.mission_data.fight_time+=dt
|
||
this.FightTime-=dt
|
||
// 检查特殊刷怪时间
|
||
this.update_time();
|
||
}
|
||
}
|
||
update_time(){
|
||
const time = Math.max(0, this.FightTime);
|
||
const remainSecond = Math.floor(time);
|
||
if (remainSecond === this.lastTimeSecond) return;
|
||
this.lastTimeSecond = remainSecond;
|
||
let m = Math.floor(remainSecond / 60);
|
||
let s = remainSecond % 60;
|
||
const wave = Math.max(1, this.currentWave || smc.vmdata.mission_data.level || 1);
|
||
let str = `W${wave.toString().padStart(2, '0')} ${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||
if(str != this.lastTimeStr){
|
||
this.time_node.getChildByName("time").getComponent(Label).string = str;
|
||
this.lastTimeStr = str;
|
||
}
|
||
}
|
||
|
||
|
||
//奖励发放
|
||
do_reward(){
|
||
// 奖励发放
|
||
}
|
||
|
||
do_ad(){
|
||
if(this.ad_back()){
|
||
oops.message.dispatchEvent(GameEvent.AD_BACK_TRUE)
|
||
smc.vmdata.mission_data.refresh_count+=FightSet.MORE_RC
|
||
}else{
|
||
oops.message.dispatchEvent(GameEvent.AD_BACK_FALSE)
|
||
}
|
||
}
|
||
ad_back(){
|
||
return true
|
||
}
|
||
|
||
|
||
async mission_start(){
|
||
// 防止上一局的 fight_end 延迟回调干扰新局
|
||
this.unscheduleAllCallbacks();
|
||
// 确保清理上一局的残留实体
|
||
this.cleanComponents();
|
||
this.node.active=true
|
||
this.data_init()
|
||
oops.message.dispatchEvent(GameEvent.FightReady)
|
||
this.enterPreparePhase()
|
||
let loading=this.node.parent.getChildByName("loading")
|
||
loading.active=true
|
||
this.scheduleOnce(()=>{
|
||
loading.active=false
|
||
},0.5)
|
||
|
||
}
|
||
|
||
to_fight(){
|
||
smc.mission.stop_spawn_mon = false;
|
||
smc.mission.in_fight=true
|
||
smc.vmdata.mission_data.in_fight = true
|
||
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = false;
|
||
oops.message.dispatchEvent(GameEvent.FightStart) //GameSetMonComp 监听刷怪
|
||
}
|
||
|
||
private enterPreparePhase() {
|
||
smc.mission.in_fight = false;
|
||
smc.vmdata.mission_data.in_fight = false
|
||
smc.mission.stop_spawn_mon = true;
|
||
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = true;
|
||
}
|
||
|
||
private onStartFightBtnClick() {
|
||
if (!smc.mission.play) return;
|
||
if (smc.mission.pause) return;
|
||
if (smc.mission.in_fight) return;
|
||
this.to_fight();
|
||
}
|
||
|
||
|
||
open_Victory(e:any,is_hero_dead: boolean = false){
|
||
// 暂停游戏循环和怪物行为
|
||
// smc.mission.play = false;
|
||
smc.mission.pause = true;
|
||
// oops.message.dispatchEvent(GameEvent.FightEnd,{victory:false})
|
||
mLogger.log(this.debugMode, 'MissionComp', " open_Victory",is_hero_dead,this.revive_times)
|
||
oops.gui.open(UIID.Victory,{
|
||
victory:false,
|
||
rewards:this.rewards,
|
||
game_data:this.game_data,
|
||
can_revive: is_hero_dead && this.revive_times > 0
|
||
})
|
||
}
|
||
|
||
|
||
|
||
fight_end(){
|
||
// mLogger.log(this.debugMode, 'MissionComp', "任务结束")
|
||
// 延迟0.5秒后执行任务结束逻辑
|
||
this.scheduleOnce(() => {
|
||
smc.mission.play=false
|
||
this.cleanComponents()
|
||
this.clearBattlePools()
|
||
}, 0.5)
|
||
}
|
||
|
||
mission_end(){
|
||
// mLogger.log(this.debugMode, 'MissionComp', " mission_end")
|
||
// 合并 FightEnd 逻辑:清理组件、停止游戏循环
|
||
this.unscheduleAllCallbacks();
|
||
smc.mission.play=false
|
||
smc.mission.pause = false;
|
||
smc.mission.in_fight = false;
|
||
smc.vmdata.mission_data.in_fight = false
|
||
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = false;
|
||
this.cleanComponents()
|
||
this.clearBattlePools()
|
||
this.node.active=false
|
||
}
|
||
|
||
data_init(){
|
||
//局内数据初始化 smc 数据初始化
|
||
smc.mission.play = true;
|
||
smc.mission.pause = false;
|
||
smc.mission.stop_mon_action = false;
|
||
smc.mission.stop_spawn_mon = false;
|
||
smc.vmdata.mission_data.in_fight=false
|
||
smc.vmdata.mission_data.fight_time=0
|
||
smc.vmdata.mission_data.mon_num=0
|
||
smc.vmdata.mission_data.level=0
|
||
smc.vmdata.mission_data.mon_max = Math.max(1, Math.floor(this.maxMonsterCount))
|
||
this.currentWave = 0;
|
||
this.FightTime=FightSet.FiIGHT_TIME
|
||
this.rewards=[] // 改为数组,用于存储掉落物品列表
|
||
this.revive_times = 1; // 每次任务开始重置复活次数
|
||
this.lastTimeStr = "";
|
||
this.lastTimeSecond = -1;
|
||
this.memoryRefreshTimer = 0;
|
||
this.lastMemoryText = "";
|
||
this.perfDtAcc = 0;
|
||
this.perfFrameCount = 0;
|
||
this.heapBaseMB = -1;
|
||
this.heapPeakMB = 0;
|
||
this.heapTrendPerMinMB = 0;
|
||
this.heapTrendTimer = 0;
|
||
this.heapTrendBaseMB = -1;
|
||
this.monsterCountSyncTimer = 0;
|
||
this.lastPrepareCoinWave = 0;
|
||
smc.vmdata.mission_data.coin = 0;
|
||
|
||
// 重置全局属性加成和主角引用 (确保新一局数据干净)
|
||
// smc.role = null;
|
||
|
||
// 重置英雄数据,确保新一局是初始状态
|
||
|
||
// mLogger.log(this.debugMode, 'MissionComp', "局内数据初始化",smc.vmdata.mission_data)
|
||
}
|
||
|
||
private onNewWave(event: string, data: any) {
|
||
const wave = Number(data?.wave ?? 0);
|
||
if (wave <= 0) return;
|
||
this.enterPreparePhase();
|
||
this.currentWave = wave;
|
||
smc.vmdata.mission_data.level = wave;
|
||
this.grantPrepareCoinByWave(wave);
|
||
this.lastTimeSecond = -1;
|
||
this.update_time();
|
||
}
|
||
|
||
private grantPrepareCoinByWave(wave: number) {
|
||
if (wave <= 0) return;
|
||
if (wave <= this.lastPrepareCoinWave) return;
|
||
const base = Math.max(0, Math.floor(this.prepareBaseCoinReward));
|
||
const grow = Math.max(0, Math.floor(this.prepareCoinWaveGrow));
|
||
const cap = Math.max(0, Math.floor(this.prepareCoinRewardCap));
|
||
const reward = Math.min(cap, base + (wave - 1) * grow);
|
||
if (reward <= 0) {
|
||
this.lastPrepareCoinWave = wave;
|
||
return;
|
||
}
|
||
smc.vmdata.mission_data.coin = Math.max(0, Math.floor((smc.vmdata.mission_data.coin ?? 0) + reward));
|
||
this.lastPrepareCoinWave = wave;
|
||
oops.message.dispatchEvent(GameEvent.CoinAdd, { delta: reward, syncOnly: true });
|
||
mLogger.log(this.debugMode, 'MissionComp', "prepare coin reward", { wave, reward, coin: smc.vmdata.mission_data.coin });
|
||
}
|
||
|
||
private getMonsterThresholds(): { max: number; resume: number } {
|
||
const max = Math.max(1, Math.floor(this.maxMonsterCount));
|
||
const resume = Math.min(max - 1, Math.max(0, Math.floor(this.resumeMonsterCount)));
|
||
return { max, resume };
|
||
}
|
||
|
||
private syncMonsterSpawnState(dt: number) {
|
||
this.monsterCountSyncTimer += dt;
|
||
if (dt > 0 && this.monsterCountSyncTimer < 0.2) return;
|
||
this.monsterCountSyncTimer = 0;
|
||
let monsterCount = 0;
|
||
ecs.query(this.heroAttrsMatcher).forEach(entity => {
|
||
const attrs = entity.get(HeroAttrsComp);
|
||
if (!attrs || attrs.fac !== FacSet.MON || attrs.is_dead) return;
|
||
monsterCount += 1;
|
||
});
|
||
smc.vmdata.mission_data.mon_num = monsterCount;
|
||
const { max, resume } = this.getMonsterThresholds();
|
||
smc.vmdata.mission_data.mon_max = max;
|
||
const stopSpawn = !!smc.mission.stop_spawn_mon;
|
||
if (stopSpawn) {
|
||
if (monsterCount <= resume) smc.mission.stop_spawn_mon = false;
|
||
return;
|
||
}
|
||
if (monsterCount >= max) smc.mission.stop_spawn_mon = true;
|
||
}
|
||
|
||
private cleanComponents() {
|
||
const heroEntities: ecs.Entity[] = [];
|
||
ecs.query(this.heroViewMatcher).forEach(entity => {
|
||
heroEntities.push(entity);
|
||
});
|
||
heroEntities.forEach(entity => {
|
||
entity.destroy();
|
||
});
|
||
const skillEntities: ecs.Entity[] = [];
|
||
ecs.query(this.skillViewMatcher).forEach(entity => {
|
||
skillEntities.push(entity);
|
||
});
|
||
skillEntities.forEach(entity => {
|
||
entity.destroy();
|
||
});
|
||
}
|
||
|
||
private clearBattlePools() {
|
||
Monster.clearPools();
|
||
Skill.clearPools();
|
||
Tooltip.clearPool();
|
||
this.clearBattleSceneNodes();
|
||
}
|
||
|
||
private clearBattleSceneNodes() {
|
||
const scene = smc.map?.MapView?.scene;
|
||
const layer = scene?.entityLayer?.node;
|
||
if (!layer) return;
|
||
const heroRoot = layer.getChildByName("HERO");
|
||
const skillRoot = layer.getChildByName("SKILL");
|
||
if (heroRoot) {
|
||
for (let i = heroRoot.children.length - 1; i >= 0; i--) {
|
||
heroRoot.children[i].destroy();
|
||
}
|
||
}
|
||
if (skillRoot) {
|
||
for (let i = skillRoot.children.length - 1; i >= 0; i--) {
|
||
skillRoot.children[i].destroy();
|
||
}
|
||
}
|
||
}
|
||
|
||
private getBattleLayerNodeCount() {
|
||
const scene = smc.map?.MapView?.scene;
|
||
const layer = scene?.entityLayer?.node;
|
||
if (!layer) return { heroNodes: 0, skillNodes: 0 };
|
||
const heroRoot = layer.getChildByName("HERO");
|
||
const skillRoot = layer.getChildByName("SKILL");
|
||
return {
|
||
heroNodes: heroRoot?.children.length || 0,
|
||
skillNodes: skillRoot?.children.length || 0
|
||
};
|
||
}
|
||
|
||
/** 性能监控相关代码 */
|
||
|
||
private initMemoryPanel() {
|
||
if (!this.showMemoryPanel || !this.time_node) return;
|
||
let panel = this.time_node.getChildByName("mem_panel");
|
||
if (!panel) {
|
||
panel = new Node("mem_panel");
|
||
panel.parent = this.time_node;
|
||
panel.setPosition(0, -32, 0);
|
||
}
|
||
let label = panel.getComponent(Label);
|
||
if (!label) {
|
||
label = panel.addComponent(Label);
|
||
}
|
||
label.fontSize = 16;
|
||
label.lineHeight = 20;
|
||
this.memoryLabel = label;
|
||
}
|
||
|
||
private removeMemoryPanel() {
|
||
const panel = this.time_node?.getChildByName("mem_panel");
|
||
if (panel) {
|
||
panel.destroy();
|
||
}
|
||
this.memoryLabel = null;
|
||
this.lastMemoryText = "";
|
||
}
|
||
|
||
|
||
private updateMemoryPanel(dt: number) {
|
||
if (!this.showMemoryPanel || !this.memoryLabel) return;
|
||
this.perfDtAcc += dt;
|
||
this.perfFrameCount += 1;
|
||
this.memoryRefreshTimer += dt;
|
||
if (this.memoryRefreshTimer < 0.5) return;
|
||
this.memoryRefreshTimer = 0;
|
||
let heroCount = 0;
|
||
ecs.query(this.heroViewMatcher).forEach(() => {
|
||
heroCount++;
|
||
});
|
||
let skillCount = 0;
|
||
ecs.query(this.skillViewMatcher).forEach(() => {
|
||
skillCount++;
|
||
});
|
||
const monPool = Monster.getPoolStats();
|
||
const skillPool = Skill.getPoolStats();
|
||
const tooltipPool = Tooltip.getPoolStats();
|
||
const layerNodes = this.getBattleLayerNodeCount();
|
||
const perf = (globalThis as any).performance;
|
||
const heapBytes = perf && perf.memory ? perf.memory.usedJSHeapSize : 0;
|
||
let heapMB = heapBytes > 0 ? heapBytes / 1024 / 1024 : -1;
|
||
if (heapMB > 0 && this.heapBaseMB < 0) {
|
||
this.heapBaseMB = heapMB;
|
||
this.heapPeakMB = heapMB;
|
||
this.heapTrendBaseMB = heapMB;
|
||
this.heapTrendTimer = 0;
|
||
}
|
||
if (heapMB > this.heapPeakMB) {
|
||
this.heapPeakMB = heapMB;
|
||
}
|
||
this.heapTrendTimer += 0.5;
|
||
if (heapMB > 0 && this.heapTrendBaseMB > 0 && this.heapTrendTimer >= 10) {
|
||
const deltaMB = heapMB - this.heapTrendBaseMB;
|
||
this.heapTrendPerMinMB = (deltaMB / this.heapTrendTimer) * 60;
|
||
this.heapTrendBaseMB = heapMB;
|
||
this.heapTrendTimer = 0;
|
||
}
|
||
const heapText = heapMB > 0 ? heapMB.toFixed(1) : "N/A";
|
||
const heapDeltaText = this.heapBaseMB > 0 && heapMB > 0 ? (heapMB - this.heapBaseMB).toFixed(1) : "N/A";
|
||
const heapPeakText = this.heapPeakMB > 0 ? this.heapPeakMB.toFixed(1) : "N/A";
|
||
const avgDt = this.perfFrameCount > 0 ? this.perfDtAcc / this.perfFrameCount : 0;
|
||
const fps = avgDt > 0 ? 1 / avgDt : 0;
|
||
this.perfDtAcc = 0;
|
||
this.perfFrameCount = 0;
|
||
const text =
|
||
`Heap:${heapText}MB Δ:${heapDeltaText} Peak:${heapPeakText}\n` +
|
||
`Trend:${this.heapTrendPerMinMB.toFixed(2)}MB/min\n` +
|
||
`Perf dt:${(avgDt * 1000).toFixed(1)}ms fps:${fps.toFixed(1)}\n` +
|
||
`Ent H:${heroCount} S:${skillCount} N:${layerNodes.heroNodes}/${layerNodes.skillNodes}\n` +
|
||
`Pool M:${monPool.total}(${monPool.paths}) K:${skillPool.total}(${skillPool.paths}) T:${tooltipPool.total}`;
|
||
if (text === this.lastMemoryText) return;
|
||
this.lastMemoryText = text;
|
||
this.memoryLabel.string = text;
|
||
}
|
||
|
||
|
||
|
||
|
||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||
reset() {
|
||
this.node.destroy();
|
||
}
|
||
}
|