Files
pixelheros/assets/script/game/map/MissionMonComp.ts
walkpan c7cb8b3e1e fix(game): 调整怪物生成逻辑和UI尺寸,优化资源图集
- 修复怪物生成时近战/远程类型与槽位不匹配的问题,增加槽位类型限制
- 调整加载界面进度条尺寸和颜色,优化视觉表现
- 修改任务主页组件,注释掉未使用的标签切换功能
- 更新资源图集布局,修正精灵帧坐标和旋转状态
- 调整英雄界面预制件的部分UI元素尺寸
2026-04-01 22:25:07 +08:00

386 lines
15 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, HType } 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 MON_SLOT_COUNT = 6;
private static readonly MON_FRONT_SLOT_COUNT = 3;
private static readonly MON_SLOT_START_X = 30;
private static readonly MON_SLOT_X_INTERVAL = 60;
private static readonly MON_DROP_HEIGHT = 280;
@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 slotSpawnQueues: Array<Array<{
uuid: number,
isBoss: boolean,
upType: UpType,
monLv: number,
}>> = [];
private slotOccupiedEids: Array<number | null> = [];
private slotRangeTypes: Array<HType.Melee | HType.Long> = [];
/** 全局生成顺序计数器,用于层级管理(预留) */
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);
this.resetSlotSpawnData()
}
/**
* 接收特殊刷怪事件并入队
* 事件数据最小结构:{ 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.resetSlotSpawnData()
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.refreshSlotOccupancy();
this.trySpawnFromSlotQueues();
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.enqueueMonsterRequest(item.uuid, BossList.includes(item.uuid), upType, Math.max(1, Number(item.level ?? 1)), true);
}
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.enqueueMonsterRequest(bossUuid, true, bossUpType);
this.bossSpawnedInWave = true;
}
return;
}
const uuid = this.getRandomNormalMonsterUuid();
const upType = this.getRandomUpType();
this.enqueueMonsterRequest(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.bossSpawnedInWave = false;
this.waveSpawnTimer = this.waveSpawnCd;
this.confirmWaveSlotTypes();
this.primeWaveInitialBurst();
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 (this.hasPendingSlotQueue()) return;
if (this.hasActiveSlotMonster()) 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 primeWaveInitialBurst() {
const remain = this.waveTargetCount - this.waveSpawnedCount;
if (remain <= 0) return;
const burstCount = Math.min(MissionMonCompComp.MON_SLOT_COUNT, remain);
for (let i = 0; i < burstCount; i++) {
const uuid = this.getRandomNormalMonsterUuid();
const upType = this.getRandomUpType();
this.enqueueMonsterRequest(uuid, false, upType);
}
this.waveSpawnedCount += burstCount;
}
private resetSlotSpawnData() {
this.slotSpawnQueues = Array.from(
{ length: MissionMonCompComp.MON_SLOT_COUNT },
() => []
);
this.slotOccupiedEids = Array.from(
{ length: MissionMonCompComp.MON_SLOT_COUNT },
() => null
);
this.confirmWaveSlotTypes();
}
private confirmWaveSlotTypes() {
this.slotRangeTypes = Array.from(
{ length: MissionMonCompComp.MON_SLOT_COUNT },
(_, index) => index < MissionMonCompComp.MON_FRONT_SLOT_COUNT ? HType.Melee : HType.Long
);
}
private hasPendingSlotQueue() {
for (let i = 0; i < this.slotSpawnQueues.length; i++) {
if (this.slotSpawnQueues[i].length > 0) return true;
}
return false;
}
private hasActiveSlotMonster() {
for (let i = 0; i < this.slotOccupiedEids.length; i++) {
if (this.slotOccupiedEids[i]) return true;
}
return false;
}
private refreshSlotOccupancy() {
for (let i = 0; i < this.slotOccupiedEids.length; i++) {
const eid = this.slotOccupiedEids[i];
if (!eid) continue;
const entity = ecs.getEntityByEid(eid);
if (!entity) {
this.slotOccupiedEids[i] = null;
continue;
}
const attrs = entity.get(HeroAttrsComp);
if (!attrs) {
this.slotOccupiedEids[i] = null;
}
}
}
private getSlotQueueLoad(slotIndex: number): number {
const occupied = this.slotOccupiedEids[slotIndex] ? 1 : 0;
return occupied + this.slotSpawnQueues[slotIndex].length;
}
private resolveMonsterSlotRange(uuid: number): HType.Melee | HType.Long {
const type = HeroInfo[uuid]?.type;
if (type === HType.Melee) return HType.Melee;
return HType.Long;
}
private resolveSlotPriorityIndexes(uuid: number): number[] {
if (this.resolveMonsterSlotRange(uuid) === HType.Melee) {
return [0, 1, 2, 3, 4, 5];
}
return [5, 4, 3, 2, 1, 0];
}
private pickAssignSlotIndex(uuid: number): number {
const expectedRange = this.resolveMonsterSlotRange(uuid);
const slotPriority = this.resolveSlotPriorityIndexes(uuid);
let bestLoad = Number.MAX_SAFE_INTEGER;
let bestIndex = -1;
for (let i = 0; i < slotPriority.length; i++) {
const index = slotPriority[i];
if (this.slotRangeTypes[index] !== expectedRange) continue;
const load = this.getSlotQueueLoad(index);
if (load < bestLoad) {
bestLoad = load;
bestIndex = index;
}
}
if (bestIndex >= 0) return bestIndex;
for (let i = 0; i < slotPriority.length; i++) {
const index = slotPriority[i];
const load = this.getSlotQueueLoad(index);
if (load < bestLoad) {
bestLoad = load;
bestIndex = index;
}
}
if (bestIndex >= 0) return bestIndex;
return bestIndex;
}
private enqueueMonsterRequest(
uuid: number,
isBoss: boolean,
upType: UpType,
monLv: number = 1,
priority: boolean = false,
) {
const slotIndex = this.pickAssignSlotIndex(uuid);
const request = { uuid, isBoss, upType, monLv };
if (priority) {
this.slotSpawnQueues[slotIndex].unshift(request);
return;
}
this.slotSpawnQueues[slotIndex].push(request);
}
private trySpawnFromSlotQueues() {
if (smc.mission.stop_spawn_mon) return;
for (let i = 0; i < this.slotSpawnQueues.length; i++) {
if (this.slotOccupiedEids[i]) continue;
const request = this.slotSpawnQueues[i].shift();
if (!request) continue;
this.addMonsterBySlot(i, request.uuid, request.isBoss, request.upType, request.monLv);
}
}
private addMonsterBySlot(
slotIndex: number,
uuid: number = 1001,
isBoss: boolean = false,
upType: UpType = UpType.AP1_HP1,
monLv: number = 1,
) {
let mon = ecs.getEntity<Monster>(Monster);
let scale = -1;
const spawnX = MissionMonCompComp.MON_SLOT_START_X + slotIndex * MissionMonCompComp.MON_SLOT_X_INTERVAL;
const landingY = BoxSet.GAME_LINE + (isBoss ? 6 : 0);
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
mon.load(spawnPos, scale, uuid, isBoss, landingY, monLv);
this.slotOccupiedEids[slotIndex] = mon.eid;
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);
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));
model.hp = model.hp_max;
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
// this.node.destroy();
}
}