Files
pixelheros/assets/script/game/map/MissionMonComp.ts
panw a3a19b1960 refactor(map): 重构怪物生成位置逻辑以改善分布
- 移除未使用的 MonStart 导入
- 将 spawnCount 重命名为 waveSpawnOrder 以更准确反映其用途
- 使用固定的横向间隔(WAVE_SPAWN_X_INTERVAL)和起始位置(WAVE_SPAWN_START_X)替代随机高度偏移
- 简化 addMonster 方法签名,移除冗余的 i 参数
- 确保每波开始时重置 waveSpawnOrder 计数器
2026-03-30 15:22:29 +08:00

244 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { _decorator, v3, Vec3 } from "cc";
import { mLogger } from "../common/Logger";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { Monster } from "../hero/Mon";
import { HeroInfo } from "../common/config/heroSet";
import { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import {BoxSet } from "../common/config/GameSet";
import { BossList, MonList, MonType, SpawnPowerBias, StageBossGrow, StageGrow, UpType } from "./RogueConfig";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { MoveComp } from "../hero/MoveComp";
const { ccclass, property } = _decorator;
/** 视图层对象 */
@ccclass('MissionMonCompComp')
@ecs.register('MissionMonComp', false)
export class MissionMonCompComp extends CCComp {
private static readonly BOSS_RENDER_PRIORITY = 1000000;
private static readonly WAVE_SPAWN_START_X = 400;
private static readonly WAVE_SPAWN_X_INTERVAL = 60;
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = false;
@property({ tooltip: "每波基础普通怪数量" })
private baseMonstersPerWave: number = 5;
@property({ tooltip: "每波额外增加普通怪数量" })
private waveMonsterGrowth: number = 0;
@property({ tooltip: "多少波刷新一次 Boss" })
private bossWaveInterval: number = 5;
@property({ tooltip: "同一波内刷怪间隔(秒)" })
private waveSpawnCd: number = 0.35;
// 刷怪队列(用于插队生成:比如运营活动怪、技能召唤怪、剧情强制怪)
// 约定:队列里的怪会优先于常规刷新处理
private MonQueue: Array<{
uuid: number,
level: number,
}> = [];
private waveSpawnOrder: number = 0;
/** 全局生成顺序计数器,用于层级管理(预留) */
private globalSpawnOrder: number = 0;
/** 插队刷怪处理计时器 */
private queueTimer: number = 0;
private waveSpawnTimer: number = 0;
private currentWave: number = 0;
private waveTargetCount: number = 0;
private waveSpawnedCount: number = 0;
private bossSpawnedInWave: boolean = false;
onLoad(){
this.on(GameEvent.FightReady,this.fight_ready,this)
this.on("SpawnSpecialMonster", this.onSpawnSpecialMonster, this);
}
/**
* 接收特殊刷怪事件并入队
* 事件数据最小结构:{ uuid, level }
*/
private onSpawnSpecialMonster(event: string, args: any) {
if (!args) return;
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 收到特殊刷怪指令:`, args);
this.MonQueue.push({
uuid: args.uuid,
level: args.level,
});
// 让队列在下一帧附近尽快消费,提升事件响应感
this.queueTimer = 1.0;
}
start() {
}
fight_ready(){
smc.vmdata.mission_data.mon_num=0
smc.mission.stop_spawn_mon = false
this.globalSpawnOrder = 0
this.queueTimer = 0
this.waveSpawnTimer = 0
this.currentWave = 0
this.waveTargetCount = 0
this.waveSpawnedCount = 0
this.bossSpawnedInWave = false
this.MonQueue = []
this.waveSpawnOrder = 0
this.startNextWave()
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System");
}
protected update(dt: number): void {
if(!smc.mission.play) return
if(smc.mission.pause) return
if(smc.mission.stop_mon_action) return;
if(!smc.mission.in_fight) return;
this.tryAdvanceWave();
if(!smc.mission.in_fight) return;
if(smc.mission.stop_spawn_mon) return;
this.updateSpecialQueue(dt);
this.updateWaveSpawn(dt);
}
private updateSpecialQueue(dt: number) {
if (this.MonQueue.length <= 0) return;
this.queueTimer += dt;
// 轻微节流,避免同帧内突发大量插队导致瞬间堆怪
if (this.queueTimer < 0.15) return;
this.queueTimer = 0;
const item = this.MonQueue.shift();
if (!item) return;
// 特殊怪同样走随机成长类型,保持局内随机性一致
const upType = this.getRandomUpType();
this.addMonster(item.uuid, BossList.includes(item.uuid), upType);
}
private updateWaveSpawn(dt: number) {
this.waveSpawnTimer += dt;
if (this.waveSpawnTimer < this.waveSpawnCd) return;
this.waveSpawnTimer = 0;
if (this.waveSpawnedCount >= this.waveTargetCount) {
if (this.isBossWave() && !this.bossSpawnedInWave) {
const bossUuid = this.getRandomBossUuid();
const bossUpType = this.getRandomUpType();
this.addMonster(bossUuid, true, bossUpType);
this.bossSpawnedInWave = true;
}
return;
}
const uuid = this.getRandomNormalMonsterUuid();
const upType = this.getRandomUpType();
this.addMonster(uuid, false, upType);
this.waveSpawnedCount += 1;
}
private startNextWave() {
this.currentWave += 1;
smc.vmdata.mission_data.level = this.currentWave;
this.waveTargetCount = Math.max(1, this.baseMonstersPerWave + (this.currentWave - 1) * this.waveMonsterGrowth);
this.waveSpawnedCount = 0;
this.waveSpawnOrder = 0;
this.bossSpawnedInWave = false;
this.waveSpawnTimer = this.waveSpawnCd;
oops.message.dispatchEvent(GameEvent.NewWave, {
wave: this.currentWave,
total: this.waveTargetCount,
bossWave: this.isBossWave(),
});
}
private tryAdvanceWave() {
if (this.waveSpawnedCount < this.waveTargetCount) return;
if (this.isBossWave() && !this.bossSpawnedInWave) return;
if (smc.vmdata.mission_data.mon_num > 0) return;
this.startNextWave();
}
private isBossWave() {
const interval = Math.max(1, Math.floor(this.bossWaveInterval));
return this.currentWave > 0 && this.currentWave % interval === 0;
}
private getCurrentStage(): number {
return Math.max(0, this.currentWave - 1);
}
private getRandomUpType(): UpType {
// 从 StageGrow 的 key 中采样,保证新增配置无需改逻辑
const keys = Object.keys(StageGrow).map(v => Number(v) as UpType);
const index = Math.floor(Math.random() * keys.length);
return keys[index] ?? UpType.AP1_HP1;
}
private getRandomNormalMonsterUuid(): number {
// MonType 是常量对象,这里通过值采样拿到怪物类型 id
const typeKeys = Object.keys(MonType).map(k => (MonType as any)[k]).filter(v => typeof v === "number");
const randomType = typeKeys[Math.floor(Math.random() * typeKeys.length)] as number;
// 如果某类型配置被清空,回退到 AP 类型,避免空池异常
const pool = MonList[randomType] || MonList[MonType.AP];
const index = Math.floor(Math.random() * pool.length);
return pool[index];
}
private getRandomBossUuid(): number {
// 目前 Boss 池可扩展为多个,先走随机抽取
const index = Math.floor(Math.random() * BossList.length);
return BossList[index];
}
private resolveGrowPair(upType: UpType, isBoss: boolean): [number, number] {
// 普通怪基础成长StageGrow
const grow = StageGrow[upType] || StageGrow[UpType.AP1_HP1];
if (!isBoss) return [grow[0], grow[1]];
// Boss 额外成长StageBossGrow在普通成长上叠加
const bossGrow = StageBossGrow[upType] || StageBossGrow[UpType.AP1_HP1];
return [grow[0] + bossGrow[0], grow[1] + bossGrow[1]];
}
private getSpawnPowerBias(): number {
// 动态难度偏差入口:当前固定读取配置,后续可切到玩家表现驱动
return SpawnPowerBias;
}
private addMonster(
uuid: number = 1001,
isBoss: boolean = false,
upType: UpType = UpType.AP1_HP1,
) {
// 创建 ECS 怪物实体
let mon = ecs.getEntity<Monster>(Monster);
let scale = -1;
const spawnX = MissionMonCompComp.WAVE_SPAWN_START_X + this.waveSpawnOrder * MissionMonCompComp.WAVE_SPAWN_X_INTERVAL;
const landingY = BoxSet.GAME_LINE + (isBoss ? 6 : 0);
let pos: Vec3 = v3(spawnX, landingY, 0);
this.waveSpawnOrder += 1;
// 递增全局生成顺序,做溢出保护
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
mon.load(pos, scale, uuid, isBoss, landingY);
const move = mon.get(MoveComp);
if (move) {
move.spawnOrder = isBoss
? MissionMonCompComp.BOSS_RENDER_PRIORITY + this.globalSpawnOrder
: this.globalSpawnOrder;
}
const model = mon.get(HeroAttrsComp);
const base = HeroInfo[uuid];
if (!model || !base) return;
const stage = this.getCurrentStage();
const grow = this.resolveGrowPair(upType, isBoss);
// 偏差值用于整体系数缩放1=不变,>1增强<1减弱
const bias = Math.max(0.1, this.getSpawnPowerBias());
// 最终公式:基础值 + 阶段成长,再乘偏差
model.ap = Math.max(1, Math.floor((base.ap + stage * grow[0]) * bias));
model.hp_max = Math.max(1, Math.floor((base.hp + stage * grow[1]) * bias));
// 满血登场,保证 hp/hp_max 一致
model.hp = model.hp_max;
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
// this.node.destroy();
}
}