|
|
|
|
import { _decorator, Component, Node, Prefab, instantiate, director, Vec3, Label, UITransform, v3, Vec2, tween, BoxCollider2D, easing, isValid, Collider2D } from 'cc';
|
|
|
|
|
import { BattleType, ChapterDifficulty } from '../../manager/ChapterDataManager';
|
|
|
|
|
import { ConstEnum, ConstManager } from '../../manager/ConstManager';
|
|
|
|
|
import { Monster, MonsterState } from './Monster';
|
|
|
|
|
import { MapAgent } from './MapAgent';
|
|
|
|
|
import { ConstNumType } from '../../game/ConfigProjectData';
|
|
|
|
|
import { GEvent } from 'db://assets/mx/module/event/GEvent';
|
|
|
|
|
import { BattleResLoader } from '../../game/BattleResLoader';
|
|
|
|
|
const { ccclass, property } = _decorator;
|
|
|
|
|
|
|
|
|
|
interface MonsterConfig {
|
|
|
|
|
monsterId: number;
|
|
|
|
|
totalCount: number;
|
|
|
|
|
wave: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface SpawnEvent {
|
|
|
|
|
time: number;
|
|
|
|
|
monsterId: number;
|
|
|
|
|
monsterGroup: number;
|
|
|
|
|
spawnPoint: number; // 出怪口编号
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PendingSpawn {
|
|
|
|
|
monsterId: number;
|
|
|
|
|
totalCount: number;
|
|
|
|
|
monsterGroup: number;
|
|
|
|
|
spawnPointRange: [number, number];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SpecialSpawnEvent {
|
|
|
|
|
time: number;
|
|
|
|
|
spawnPoint: number;
|
|
|
|
|
monsterId: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ccclass('MonsterSpawner')
|
|
|
|
|
export class MonsterSpawner extends Component {
|
|
|
|
|
// @property({ type: [Prefab] })
|
|
|
|
|
// private monsterPrefabs: Prefab[] = []; // 怪物预制体数组
|
|
|
|
|
|
|
|
|
|
tempMonsterParent: Node = null;
|
|
|
|
|
|
|
|
|
|
@property(Label)
|
|
|
|
|
debugLabel: Label = null;
|
|
|
|
|
|
|
|
|
|
private spawnEvents: SpawnEvent[] = [];
|
|
|
|
|
|
|
|
|
|
private specialSpawnEvents: SpecialSpawnEvent[] = [];
|
|
|
|
|
private nextEventIndex: number = 0;
|
|
|
|
|
private nextSpecialEventIndex: number = 0;
|
|
|
|
|
|
|
|
|
|
//每次出怪时间间隔一秒
|
|
|
|
|
private createMonsterTimeInterval: number = 1;
|
|
|
|
|
|
|
|
|
|
/** 每帧最多生成多少只怪,防止一帧爆量卡顿 */
|
|
|
|
|
/** 每帧最多实例化怪物数,避免出怪帧尖刺(线上卡顿率 11%→14%) */
|
|
|
|
|
private spawnBudgetPerFrame: number = 4;
|
|
|
|
|
|
|
|
|
|
/** 按波暂存(monster1 / monster2_1 等会合并到同一波) */
|
|
|
|
|
private pendingByWave: PendingSpawn[][] = [];
|
|
|
|
|
|
|
|
|
|
protected onEnable(): void {
|
|
|
|
|
GEvent.Ins.on(GEvent.InfiniteModeWaveReset, this.resetInfiniteModeWave, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected onDisable(): void {
|
|
|
|
|
GEvent.Ins.off(GEvent.InfiniteModeWaveReset, this.resetInfiniteModeWave, this);
|
|
|
|
|
}
|
|
|
|
|
update(dt: number) {
|
|
|
|
|
if (!gg.game.isBattleContextActive()) return;
|
|
|
|
|
if (!gg.game.CurentBattle.canUpdateFrameMonster()) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//检查生成怪物CurentWaveTime
|
|
|
|
|
const t = gg.game.CurentBattle.CurentWaveTime;
|
|
|
|
|
let budget = this.spawnBudgetPerFrame;
|
|
|
|
|
while (
|
|
|
|
|
budget-- > 0 &&
|
|
|
|
|
this.nextEventIndex < this.spawnEvents.length &&
|
|
|
|
|
this.spawnEvents[this.nextEventIndex].time <= t
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
//console.log('生成怪物', this.spawnEvents[this.nextEventIndex])
|
|
|
|
|
const event = this.spawnEvents[this.nextEventIndex++];
|
|
|
|
|
//无限模式下,最后一波怪物生成完后,重置怪物生成事件索引
|
|
|
|
|
|
|
|
|
|
// console.log('生成怪物', event.monsterId, event.spawnPoint, event.monsterGroup)
|
|
|
|
|
|
|
|
|
|
gg.game.CurentBattle.spawnMonster(event.monsterId, event.spawnPoint, event.monsterGroup);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (director.getTotalFrames() % 20 === 0) {
|
|
|
|
|
this.sortMonstersByYDis()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 初始化怪物配置
|
|
|
|
|
*/
|
|
|
|
|
public initMonsterConfigs() {
|
|
|
|
|
this.createMonsterTimeInterval = gg.data.table.getConst(ConstNumType.DefaultMonsterInterval);
|
|
|
|
|
// 章节配置(出怪)
|
|
|
|
|
let config: ITableChapter = gg.game.CurentBattle.getMonsterSpawnChapterConfig();
|
|
|
|
|
|
|
|
|
|
if(gg.game.CurentBattle.BattleType == BattleType.OrangeWeaponMode_Ya){
|
|
|
|
|
// 解析monster1配置 (1-3出怪口随机)
|
|
|
|
|
this.parseSpawnPointConfig(config.monster1, [4, 6]);
|
|
|
|
|
// 解析monster2_1配置 (4-6出怪口)
|
|
|
|
|
this.parseSpawnPointConfig(config.monster2_1, [4, 6]);
|
|
|
|
|
}else{
|
|
|
|
|
// 解析monster1配置 (1-3出怪口随机)
|
|
|
|
|
this.parseSpawnPointConfig(config.monster1, [1, 3]);
|
|
|
|
|
// 解析monster2_1配置 (4-6出怪口)
|
|
|
|
|
this.parseSpawnPointConfig(config.monster2_1, [4, 6]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.logSpawnConfigSummary(config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 打印章节出怪 id 与总波次(调试用) */
|
|
|
|
|
private logSpawnConfigSummary(config: ITableChapter) {
|
|
|
|
|
const battle = gg.game.CurentBattle;
|
|
|
|
|
const totalWaves = battle?.TotalWaveNum ?? this.pendingByWave.length;
|
|
|
|
|
const randomChapterId = battle?.CurentConfigChapter2?.id ?? '-';
|
|
|
|
|
const waveParts: string[] = [];
|
|
|
|
|
for (let i = 0; i < totalWaves; i++) {
|
|
|
|
|
const ids = this.getMonsterIdsForWave(i);
|
|
|
|
|
waveParts.push(`波${i + 1}[${ids.length ? ids.join(',') : '-'}]`);
|
|
|
|
|
}
|
|
|
|
|
console.log(
|
|
|
|
|
`[出怪配置] 随机章节=${randomChapterId} 出怪章节=${config?.id} 总波次=${totalWaves}`,
|
|
|
|
|
waveParts.join(' ')
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 指定波次(0-based)涉及的怪物 id */
|
|
|
|
|
getMonsterIdsForWave(waveIdx: number): number[] {
|
|
|
|
|
const pendings = this.pendingByWave[waveIdx];
|
|
|
|
|
if (!pendings?.length) return [];
|
|
|
|
|
const ids = new Set<number>();
|
|
|
|
|
for (let i = 0; i < pendings.length; i++) {
|
|
|
|
|
const id = pendings[i].monsterId;
|
|
|
|
|
if (Number.isFinite(id)) ids.add(id);
|
|
|
|
|
}
|
|
|
|
|
return Array.from(ids);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _collectMonsterModels(monsterIds: number[]): string[] {
|
|
|
|
|
const models: string[] = [];
|
|
|
|
|
for (let i = 0; i < monsterIds.length; i++) {
|
|
|
|
|
const data = gg.data.project.getMonsterDataByID(monsterIds[i]);
|
|
|
|
|
const model = data?.model;
|
|
|
|
|
if (typeof model === 'string' && model.length > 0) {
|
|
|
|
|
models.push(model);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return models;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 按波次预加载怪物 prefab(增量,不重复加载) */
|
|
|
|
|
preloadWaveMonsters(waveIdx: number): void {
|
|
|
|
|
if (waveIdx < 0 || !this.pendingByWave?.length) return;
|
|
|
|
|
const monsterIds = this.getMonsterIdsForWave(waveIdx);
|
|
|
|
|
if (!monsterIds.length) return;
|
|
|
|
|
const models = this._collectMonsterModels(monsterIds);
|
|
|
|
|
if (models.length > 0) {
|
|
|
|
|
BattleResLoader.preloadMonsterModels(models);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析出怪口配置
|
|
|
|
|
parseSpawnPointConfig(configString: string, spawnPointRange: [number, number]) {
|
|
|
|
|
if (!configString) return;
|
|
|
|
|
|
|
|
|
|
const segments = configString.split('|');
|
|
|
|
|
const monsterNumScale = gg.game.monsterNumScale > 0 ? gg.game.monsterNumScale : 1;
|
|
|
|
|
|
|
|
|
|
for (let waveIdx = 0; waveIdx < segments.length; waveIdx++) {
|
|
|
|
|
const segment = segments[waveIdx];
|
|
|
|
|
if (!segment) continue;
|
|
|
|
|
|
|
|
|
|
if (!this.pendingByWave[waveIdx]) this.pendingByWave[waveIdx] = [];
|
|
|
|
|
|
|
|
|
|
const configStrings = segment.split(';');
|
|
|
|
|
for (const configStr of configStrings) {
|
|
|
|
|
if (!configStr) continue;
|
|
|
|
|
const parts = configStr.split(',').map(part => parseInt(part.trim(), 10));
|
|
|
|
|
if (parts.length !== 3) continue;
|
|
|
|
|
|
|
|
|
|
const monsterId = parts[0];
|
|
|
|
|
const totalCountRaw = parts[1];
|
|
|
|
|
let monsterGroup = parts[2];
|
|
|
|
|
if (!Number.isFinite(monsterId) || !Number.isFinite(totalCountRaw)) continue;
|
|
|
|
|
|
|
|
|
|
const totalCount = Math.max(0, Math.round(totalCountRaw * monsterNumScale));
|
|
|
|
|
if (totalCount <= 0) continue;
|
|
|
|
|
|
|
|
|
|
this.pendingByWave[waveIdx].push({
|
|
|
|
|
monsterId,
|
|
|
|
|
totalCount,
|
|
|
|
|
monsterGroup,
|
|
|
|
|
spawnPointRange,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将“按波暂存”的配置展开为按时间递增的 SpawnEvent(1 秒 1 只)
|
|
|
|
|
this.rebuildSpawnEventsFromPending();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 计算指定波次(0-based)在刷怪时间轴上的起始时间 */
|
|
|
|
|
private calcWaveStartTime(waveIdx: number): number {
|
|
|
|
|
let waveStartTime = 0;
|
|
|
|
|
const end = Math.min(waveIdx, this.pendingByWave.length);
|
|
|
|
|
for (let i = 0; i < end; i++) {
|
|
|
|
|
waveStartTime += this.calcWaveDuration(i);
|
|
|
|
|
}
|
|
|
|
|
return waveStartTime;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 单波刷怪时长(与 rebuildSpawnEventsFromPending 中 lane 并行规则一致) */
|
|
|
|
|
private calcWaveDuration(waveIdx: number): number {
|
|
|
|
|
const pendings = this.pendingByWave[waveIdx];
|
|
|
|
|
if (!pendings?.length) return 0;
|
|
|
|
|
|
|
|
|
|
const lanes = new Map<string, number>();
|
|
|
|
|
for (const p of pendings) {
|
|
|
|
|
const key = `${p.spawnPointRange[0]}-${p.spawnPointRange[1]}`;
|
|
|
|
|
lanes.set(key, (lanes.get(key) ?? 0) + p.totalCount);
|
|
|
|
|
}
|
|
|
|
|
let maxLaneLen = 0;
|
|
|
|
|
for (const len of lanes.values()) {
|
|
|
|
|
if (len > maxLaneLen) maxLaneLen = len;
|
|
|
|
|
}
|
|
|
|
|
return maxLaneLen * this.createMonsterTimeInterval;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 救救我鸭:螺丝提前消除完后,将刷怪进度跳到指定波(0-based),用于跳过中间波次直接打最后一波。
|
|
|
|
|
*/
|
|
|
|
|
seekToWave(waveIdx: number) {
|
|
|
|
|
if (waveIdx < 0) return;
|
|
|
|
|
const waveStartTime = this.calcWaveStartTime(waveIdx);
|
|
|
|
|
const battle = gg.game.CurentBattle;
|
|
|
|
|
if (battle) {
|
|
|
|
|
battle.CurentWaveTime = waveStartTime;
|
|
|
|
|
}
|
|
|
|
|
while (
|
|
|
|
|
this.nextEventIndex < this.spawnEvents.length
|
|
|
|
|
&& this.spawnEvents[this.nextEventIndex].time < waveStartTime
|
|
|
|
|
) {
|
|
|
|
|
this.nextEventIndex++;
|
|
|
|
|
}
|
|
|
|
|
console.log(
|
|
|
|
|
'[jiujiuwoya] seekToWave',
|
|
|
|
|
waveIdx + 1,
|
|
|
|
|
'time=',
|
|
|
|
|
waveStartTime,
|
|
|
|
|
'nextEventIndex=',
|
|
|
|
|
this.nextEventIndex,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private rebuildSpawnEventsFromPending() {
|
|
|
|
|
this.spawnEvents.length = 0;
|
|
|
|
|
this.nextEventIndex = 0;
|
|
|
|
|
|
|
|
|
|
// 波次时间不清零:每一波有自己的起始时间 waveStartTime。
|
|
|
|
|
// 同一波内,不同出怪口范围(monster1 / monster2_1 等)并行各自 1 秒 1 只。
|
|
|
|
|
// 下一波从上一波“最长 lane 完成的时间”开始,保证并行波次不交叉。
|
|
|
|
|
let waveStartTime = 0;
|
|
|
|
|
for (let waveIdx = 0; waveIdx < this.pendingByWave.length; waveIdx++) {
|
|
|
|
|
const pendings = this.pendingByWave[waveIdx];
|
|
|
|
|
if (!pendings || pendings.length === 0) continue;
|
|
|
|
|
|
|
|
|
|
// 按出怪口范围分 lane(1-3 一条、4-6 一条…),lane 内按顺序刷;不同 lane 并行
|
|
|
|
|
const lanes = new Map<string, { range: [number, number]; items: PendingSpawn[]; laneLen: number }>();
|
|
|
|
|
for (const p of pendings) {
|
|
|
|
|
const key = `${p.spawnPointRange[0]}-${p.spawnPointRange[1]}`;
|
|
|
|
|
let lane = lanes.get(key);
|
|
|
|
|
if (!lane) {
|
|
|
|
|
lane = { range: p.spawnPointRange, items: [], laneLen: 0 };
|
|
|
|
|
lanes.set(key, lane);
|
|
|
|
|
}
|
|
|
|
|
lane.items.push(p);
|
|
|
|
|
lane.laneLen += p.totalCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let maxLaneLen = 0;
|
|
|
|
|
for (const lane of lanes.values()) {
|
|
|
|
|
if (lane.laneLen > maxLaneLen) maxLaneLen = lane.laneLen;
|
|
|
|
|
|
|
|
|
|
let laneCursor = 0;
|
|
|
|
|
const [minSp, maxSp] = lane.range;
|
|
|
|
|
const span = Math.max(0, maxSp - minSp);
|
|
|
|
|
for (const p of lane.items) {
|
|
|
|
|
for (let i = 0; i < p.totalCount; i++) {
|
|
|
|
|
const spawnPoint = minSp + Math.floor(Math.random() * (span + 1));
|
|
|
|
|
this.spawnEvents.push({
|
|
|
|
|
time: waveStartTime + laneCursor,
|
|
|
|
|
monsterId: p.monsterId,
|
|
|
|
|
monsterGroup: p.monsterGroup,
|
|
|
|
|
spawnPoint,
|
|
|
|
|
});
|
|
|
|
|
laneCursor += this.createMonsterTimeInterval;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 同一波生成完后,将下一波起点推进到本波最长 lane 的结束时间
|
|
|
|
|
waveStartTime += maxLaneLen * this.createMonsterTimeInterval;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 多 lane 并行会产生同 time 多事件,统一按时间排序,确保 update() while 可顺序消费
|
|
|
|
|
this.spawnEvents.sort((a, b) => a.time - b.time);
|
|
|
|
|
|
|
|
|
|
// console.log('this.spawnEvents++++++++===', this.spawnEvents, this.createMonsterTimeInterval)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析特殊出怪口配置
|
|
|
|
|
parseSpecialSpawnConfig(configString: string) {
|
|
|
|
|
if (configString == '') return;
|
|
|
|
|
//|91,2,301010|101,5,301011|111,2,301012|111,1,501012|121,5,301013|131,2,301014|141,5,301015|151,2,301016|161,5,301017|171,2,301018|181,5,301019|191,2,301020|201,5,301021|211,2,301022|221,5,301023|231,6,601024
|
|
|
|
|
const events = configString.split('|');
|
|
|
|
|
|
|
|
|
|
for (const eventStr of events) {
|
|
|
|
|
const parts = eventStr.split(',').map(part => Number(part.trim()));
|
|
|
|
|
if (parts.length !== 3) continue;
|
|
|
|
|
|
|
|
|
|
const [time, spawnPoint, monsterId] = parts;
|
|
|
|
|
if (gg.game.CurentSelectBattleType == BattleType.BoxMode
|
|
|
|
|
|| gg.game.CurentSelectBattleType == BattleType.GachaMode
|
|
|
|
|
) {
|
|
|
|
|
if (monsterId == 30011 || monsterId == 40011) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.specialSpawnEvents.push({
|
|
|
|
|
time,
|
|
|
|
|
spawnPoint,
|
|
|
|
|
monsterId
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 按时间排序特殊事件
|
|
|
|
|
this.specialSpawnEvents.sort((a, b) => a.time - b.time);
|
|
|
|
|
}
|
|
|
|
|
// 生成所有怪物生成事件
|
|
|
|
|
generateSpawnEvents() {
|
|
|
|
|
// 按时间排序所有事件
|
|
|
|
|
this.spawnEvents.sort((a, b) => a.time - b.time);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 开始生成怪物
|
|
|
|
|
startSpawning() {
|
|
|
|
|
// this.currentTime = 0;
|
|
|
|
|
this.nextEventIndex = 0;
|
|
|
|
|
this.nextSpecialEventIndex = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.debugLabel) {
|
|
|
|
|
this.debugLabel.string = "开始生成怪物...";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//刷新所有怪物路径
|
|
|
|
|
refreshAllMonsterPath() {
|
|
|
|
|
// for (let value of this.node.children) {
|
|
|
|
|
// value.getComponent(MapAgent)?.updatePath()
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
/**暂停所有怪物行为 */
|
|
|
|
|
pauseAllMonsterAction() {
|
|
|
|
|
for (let value of this.node.children) {
|
|
|
|
|
let a = value.getComponent(MapAgent)
|
|
|
|
|
if (a) a.CanMove = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**恢复所有怪物行为 */
|
|
|
|
|
resumeAllMonsterAction() {
|
|
|
|
|
for (let value of this.node.children) {
|
|
|
|
|
let a = value.getComponent(MapAgent)
|
|
|
|
|
if (a) a.CanMove = (value.getComponent(Monster).Data.id != 40041) &&
|
|
|
|
|
(value.getComponent(Monster).state === MonsterState.MOVING_FORWARD || value.getComponent(Monster).state === MonsterState.PATHFINDING);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
getAllMonster() {
|
|
|
|
|
const n = this.node;
|
|
|
|
|
if (!n?.isValid) return [];
|
|
|
|
|
return n.children;
|
|
|
|
|
}
|
|
|
|
|
getAllMonsterCount() {
|
|
|
|
|
const n = this.node;
|
|
|
|
|
if (!n?.isValid) return 0;
|
|
|
|
|
return n.children.length;
|
|
|
|
|
}
|
|
|
|
|
/** 所有刷怪事件是否已处理完成 */
|
|
|
|
|
isAllSpawnFinished() {
|
|
|
|
|
return this.nextEventIndex >= this.spawnEvents.length
|
|
|
|
|
&& this.nextSpecialEventIndex >= this.specialSpawnEvents.length;
|
|
|
|
|
}
|
|
|
|
|
/** 获取当前场上仍会计入通关统计的怪物数量 */
|
|
|
|
|
getAliveCountForWin() {
|
|
|
|
|
const n = this.node;
|
|
|
|
|
if (!n?.isValid) return 0;
|
|
|
|
|
let count = 0;
|
|
|
|
|
for (const child of n.children) {
|
|
|
|
|
const m = child.getComponent(Monster);
|
|
|
|
|
if (!m || !m.Data || m.IsDead) continue;
|
|
|
|
|
// 与 BattleCore.monsterDie 统计条件保持一致
|
|
|
|
|
if (m.Data.id != 40041 && m.Data.type != 8) {
|
|
|
|
|
count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
// 打印调试信息
|
|
|
|
|
printDebugInfo() {
|
|
|
|
|
// console.log("普通生成事件列表:", this.spawnEvents);
|
|
|
|
|
// console.log("特殊生成事件列表:", this.specialSpawnEvents);
|
|
|
|
|
|
|
|
|
|
if (this.debugLabel) {
|
|
|
|
|
let debugText = "怪物生成计划:\n";
|
|
|
|
|
|
|
|
|
|
// 显示普通生成事件
|
|
|
|
|
debugText += "--- 普通生成事件 ---\n";
|
|
|
|
|
for (const event of this.spawnEvents) {
|
|
|
|
|
debugText += `时间 ${event.time.toFixed(2)}: 出怪口 ${event.spawnPoint} -> 怪物 ${event.monsterId}\n`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示特殊生成事件
|
|
|
|
|
debugText += "\n--- 特殊生成事件 ---\n";
|
|
|
|
|
for (const event of this.specialSpawnEvents) {
|
|
|
|
|
debugText += `时间 ${event.time}: 出怪口 ${event.spawnPoint} -> 怪物 ${event.monsterId}\n`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.debugLabel.string = debugText;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 重置生成系统
|
|
|
|
|
reset() {
|
|
|
|
|
// this.currentTime = 0;
|
|
|
|
|
this.nextEventIndex = 0;
|
|
|
|
|
this.nextSpecialEventIndex = 0;
|
|
|
|
|
this.spawnEvents = [];
|
|
|
|
|
this.specialSpawnEvents = [];
|
|
|
|
|
|
|
|
|
|
if (this.debugLabel) {
|
|
|
|
|
this.debugLabel.string = "系统已重置";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 获取指定区域内的所有怪物(圆形范围与怪物轴对齐包围盒有重叠即算在区域内)
|
|
|
|
|
getMonstersInArea(center: Vec3, radius: number) {
|
|
|
|
|
const cx = center.x;
|
|
|
|
|
const cy = center.y;
|
|
|
|
|
const r2 = radius * radius;
|
|
|
|
|
return this.node.children.filter(monster => {
|
|
|
|
|
const pos = monster.worldPosition;
|
|
|
|
|
const worldScale = monster.worldScale;
|
|
|
|
|
let centerX = pos.x;
|
|
|
|
|
let centerY = pos.y;
|
|
|
|
|
let halfW = 0;
|
|
|
|
|
let halfH = 0;
|
|
|
|
|
const col = monster.getComponent(BoxCollider2D);
|
|
|
|
|
if (col) {
|
|
|
|
|
const absScaleX = Math.abs(worldScale.x);
|
|
|
|
|
const absScaleY = Math.abs(worldScale.y);
|
|
|
|
|
centerX += col.offset.x * absScaleX;
|
|
|
|
|
centerY += col.offset.y * absScaleY;
|
|
|
|
|
halfW = (col.size.width * absScaleX) / 2;
|
|
|
|
|
halfH = (col.size.height * absScaleY) / 2;
|
|
|
|
|
} else {
|
|
|
|
|
// 兜底:没有碰撞器时使用 UITransform 尺寸
|
|
|
|
|
const tf = monster.getComponent(UITransform);
|
|
|
|
|
if (tf) {
|
|
|
|
|
halfW = (tf.width * Math.abs(worldScale.x)) / 2;
|
|
|
|
|
halfH = (tf.height * Math.abs(worldScale.y)) / 2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (halfW <= 0 || halfH <= 0) return false;
|
|
|
|
|
const left = centerX - halfW;
|
|
|
|
|
const right = centerX + halfW;
|
|
|
|
|
const bottom = centerY - halfH;
|
|
|
|
|
const top = centerY + halfH;
|
|
|
|
|
const closestX = Math.max(left, Math.min(cx, right));
|
|
|
|
|
const closestY = Math.max(bottom, Math.min(cy, top));
|
|
|
|
|
const dx = cx - closestX;
|
|
|
|
|
const dy = cy - closestY;
|
|
|
|
|
return dx * dx + dy * dy <= r2;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
//所有怪物回到出生点
|
|
|
|
|
setAllMonsterToBornPos() {
|
|
|
|
|
for (let value of this.node.children) {
|
|
|
|
|
value.getComponent(Monster).teleportToBornPos()
|
|
|
|
|
}
|
|
|
|
|
// //重新刷新路线
|
|
|
|
|
// this.refreshAllMonsterPath()
|
|
|
|
|
}
|
|
|
|
|
/**设置怪物透明度 */
|
|
|
|
|
setAllMonsterAlpha(alpha: number = 255) {
|
|
|
|
|
for (let value of this.node.children) {
|
|
|
|
|
value.opacity = alpha
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**设置怪物红月状态*/
|
|
|
|
|
setAllMonsterBloodMoon(show = true) {
|
|
|
|
|
for (let value of this.node.children) {
|
|
|
|
|
value.getComponent(Monster).setBloodMoon(show);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//记录摆放障碍物前的位置
|
|
|
|
|
recordAllMonsterBeforePos() {
|
|
|
|
|
for (let value of this.node.children) {
|
|
|
|
|
value.getComponent(Monster).recordSetObstacleBeforePos()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//还原到摆放障碍物前的位置
|
|
|
|
|
resetAllMonsterBeforePos() {
|
|
|
|
|
for (let value of this.node.children) {
|
|
|
|
|
value.getComponent(Monster).resetSetObstacleBeforePos()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**对所有怪物按Y轴排序 */
|
|
|
|
|
sortMonstersByYDis() {
|
|
|
|
|
this.node.children.sort((a, b) => {
|
|
|
|
|
return b.worldPosition.y - a.worldPosition.y
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
/**清空所有怪物 */
|
|
|
|
|
clearAllMonster() {
|
|
|
|
|
for (let value of this.node.children) {
|
|
|
|
|
value.getComponent(Monster).clearNode()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**获取所有怪物路径距离 */
|
|
|
|
|
getAllMonsterPathDis() {
|
|
|
|
|
let disArr = []
|
|
|
|
|
// for (let value of this.node.children) {
|
|
|
|
|
// if (value.getComponent(MapAgent)) {
|
|
|
|
|
// let dis = value.getComponent(MapAgent).getRemainPathDistance();
|
|
|
|
|
// disArr.push(dis)
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
return disArr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 取相对武器「路径距离」最近的怪物。
|
|
|
|
|
* 若场上存在 state 为 ATTACKING 的怪,只在它们之中取最近;否则再对全场怪取最近。
|
|
|
|
|
*/
|
|
|
|
|
getAllPathDisMinMonster(weaponNode: Node) {
|
|
|
|
|
if (!weaponNode || !weaponNode.isValid) return null;
|
|
|
|
|
const scored: Array<{ m: Node; dis: number; attacking: boolean }> = [];
|
|
|
|
|
const monsters = this.node.children;
|
|
|
|
|
for (let i = 0; i < monsters.length; i++) {
|
|
|
|
|
const mNode = monsters[i];
|
|
|
|
|
if (!mNode || !mNode.isValid) continue;
|
|
|
|
|
const mComp = mNode.getComponent(Monster);
|
|
|
|
|
if (!mComp || mComp.getIsDead()) continue;
|
|
|
|
|
const agent = mNode.getComponent(MapAgent);
|
|
|
|
|
if (!agent) continue;
|
|
|
|
|
const dis = agent.getNearestMonster(weaponNode);
|
|
|
|
|
if (!Number.isFinite(dis)) continue;
|
|
|
|
|
scored.push({
|
|
|
|
|
m: mNode,
|
|
|
|
|
dis,
|
|
|
|
|
attacking: mComp.state === MonsterState.ATTACKING,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (scored.length <= 0) return null;
|
|
|
|
|
const attackingPool = scored.filter((s) => s.attacking);
|
|
|
|
|
const pool = attackingPool.length > 0 ? attackingPool : scored;
|
|
|
|
|
let best = pool[0];
|
|
|
|
|
for (let j = 1; j < pool.length; j++) {
|
|
|
|
|
if (pool[j].dis < best.dis) best = pool[j];
|
|
|
|
|
}
|
|
|
|
|
return best.m;
|
|
|
|
|
}
|
|
|
|
|
/**获取所有怪物路径距离最短值 */
|
|
|
|
|
getAllMonsterMinPath() {
|
|
|
|
|
let minDis = Math.min(...this.getAllMonsterPathDis())
|
|
|
|
|
return minDis
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 获取无人机目标怪物
|
|
|
|
|
*/
|
|
|
|
|
getDroneTargetMonster(droneNode: Node, radius: number) {
|
|
|
|
|
const all = this.node.children.slice();
|
|
|
|
|
if (all.length <= 0) return null;
|
|
|
|
|
const wallNode = gg.game.CurentBattle.WallTargetNode;
|
|
|
|
|
const motoNode = wallNode?.find('摩托车');
|
|
|
|
|
const targetPos = (motoNode && motoNode.isValid) ? motoNode.worldPosition : wallNode?.worldPosition;
|
|
|
|
|
if (!targetPos) return all[0] || null;
|
|
|
|
|
let arr = [];
|
|
|
|
|
for (let monster of all) {
|
|
|
|
|
let dis = Vec3.distance(monster.worldPosition, targetPos)
|
|
|
|
|
arr.push({ m: monster, d: dis });
|
|
|
|
|
}
|
|
|
|
|
arr.sort((a, b) => {
|
|
|
|
|
return a.d - b.d;
|
|
|
|
|
})
|
|
|
|
|
if (arr.length > 0) {
|
|
|
|
|
return arr[0].m;
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 获取激光武器目标怪物
|
|
|
|
|
* @param index 第几个激光塔
|
|
|
|
|
* @param count 总数量
|
|
|
|
|
*/
|
|
|
|
|
getLaserTargetMonster(index, count) {
|
|
|
|
|
let targetArr = []
|
|
|
|
|
for (let value of this.node.children) {
|
|
|
|
|
//如果不是锁定状态且在左侧
|
|
|
|
|
// if (!value.getComponent(Monster).getIsLockByLaser(index) && value.position.y < 500) {
|
|
|
|
|
// targetArr.push(value)
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
let result = []; // 存储结果的数组
|
|
|
|
|
if (targetArr.length > 0) {
|
|
|
|
|
// targetArr = targetArr.sort((a, b) => {
|
|
|
|
|
// return Vec3.distance(a, gg.game.CurentBattle.WallTargetNode) - Vec3.distance(b, gg.game.CurentBattle.WallTargetNode)
|
|
|
|
|
// });
|
|
|
|
|
//取出前count个
|
|
|
|
|
result = targetArr.slice(0, count)
|
|
|
|
|
for (let value of result) {
|
|
|
|
|
value.getComponent(Monster).setIsLockByLaser(index, true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**获取烟花陀螺武器的怪物目标
|
|
|
|
|
* @param startPos 武器发射位置(如果为空,默认选择离目标点最短的怪物)
|
|
|
|
|
* @param count 目标数量
|
|
|
|
|
* @param isfa 是否最远目标
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
getFireworkGyroTargetMonster(startPos, count, isfa = false) {
|
|
|
|
|
if (this.node.children.length <= 0) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
//返回目标
|
|
|
|
|
let targets: Node[] = [];
|
|
|
|
|
let arr = [];
|
|
|
|
|
for (let monster of this.node.children) {
|
|
|
|
|
let dis = 0;
|
|
|
|
|
if (startPos)
|
|
|
|
|
dis = Vec3.distance(monster.worldPosition, startPos)
|
|
|
|
|
else
|
|
|
|
|
dis = monster.getComponent(MapAgent).getRemainPathDistance();
|
|
|
|
|
arr.push({ m: monster, d: dis });
|
|
|
|
|
}
|
|
|
|
|
//排序
|
|
|
|
|
arr.sort((a, b) => {
|
|
|
|
|
if (isfa)//如果选择的是最远目标,降序排序
|
|
|
|
|
return b.d - a.d;
|
|
|
|
|
else///如果选择的是最近目标,升序排序
|
|
|
|
|
return a.d - b.d;
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
//找出所有已经存在的陀螺
|
|
|
|
|
let curentAllGyro = gg.game.CurentBattle.SkillController.node.children.filter(c => c.name == "weapon22");
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
|
|
|
//数量够了结束遍历
|
|
|
|
|
if (targets.length >= count) break;
|
|
|
|
|
//跳过已经死亡的怪物
|
|
|
|
|
let n: Node = arr[i].m;
|
|
|
|
|
let m: Monster = n.getComponent(Monster);
|
|
|
|
|
if (!isValid(n) || m.IsDead) continue;
|
|
|
|
|
//跳过已经被选中的怪物
|
|
|
|
|
let curentNodes = curentAllGyro.concat(targets);
|
|
|
|
|
//如果当前所有场上已经存在的陀螺和已经选取的目标怪物距离本怪物的距离小于100,则跳过(拉开每个陀螺的间距)
|
|
|
|
|
if (curentNodes.find(c => Vec3.distance(c.worldPosition, n.worldPosition) <= 100)) continue;
|
|
|
|
|
targets.push(n);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return targets
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//获取怪物距离最近的<=n个怪物不重复,如果怪物数目不足n个,返回符合条件的怪物就行
|
|
|
|
|
getMonsterNearestCountPar(weaponNode: Node, count: number = 1): Node[] {
|
|
|
|
|
if (!weaponNode || !isValid(weaponNode) || count <= 0) return [];
|
|
|
|
|
const monsters = this.node.children;
|
|
|
|
|
if (!monsters || monsters.length <= 0) return [];
|
|
|
|
|
|
|
|
|
|
const arr: Array<{ n: Node; d: number }> = [];
|
|
|
|
|
for (let i = 0; i < monsters.length; i++) {
|
|
|
|
|
const mNode = monsters[i];
|
|
|
|
|
if (!mNode || !isValid(mNode)) continue;
|
|
|
|
|
const mComp = mNode.getComponent(Monster);
|
|
|
|
|
if (!mComp || mComp.getIsDead()) continue;
|
|
|
|
|
|
|
|
|
|
const agent = mNode.getComponent(MapAgent);
|
|
|
|
|
const dis = agent ? agent.getNearestMonster(weaponNode) : Vec3.distance(weaponNode.worldPosition, mNode.worldPosition);
|
|
|
|
|
if (!Number.isFinite(dis)) continue;
|
|
|
|
|
arr.push({ n: mNode, d: dis });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (arr.length <= 0) return [];
|
|
|
|
|
arr.sort((a, b) => a.d - b.d);
|
|
|
|
|
|
|
|
|
|
const out: Node[] = [];
|
|
|
|
|
const limit = Math.min(arr.length, Math.floor(count));
|
|
|
|
|
for (let i = 0; i < limit; i++) {
|
|
|
|
|
const n = arr[i].n;
|
|
|
|
|
if (n && isValid(n)) out.push(n);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 获取怪物抛物线落点(目标怪列表)
|
|
|
|
|
* 取相对 fromNode(武器/方块发射点)路径或直线距离最近的 <= count 个怪物,不重复;不足则全返回。
|
|
|
|
|
* 若场上存在 state 为 ATTACKING 的活怪,只在其中取最近;否则对全场活怪取最近。
|
|
|
|
|
* @param count 目标个数上限
|
|
|
|
|
* @param fromNode 武器或方块节点,用于量距;不传则退化为剩余路径长度排序
|
|
|
|
|
* @param _isWholeScene 保留参数(兼容旧调用)
|
|
|
|
|
* @returns 与历史格式一致:{ node, num, isPrimarySide }[],调用方使用 .node
|
|
|
|
|
*/
|
|
|
|
|
getMonsterParabolaPos(count: number = 0, fromNode: Node | null = null, _isWholeScene: boolean = false) {
|
|
|
|
|
if (count <= 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const monsters = this.node.children;
|
|
|
|
|
const scored: Array<{ node: Node; d: number; attacking: boolean }> = [];
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < monsters.length; i++) {
|
|
|
|
|
const mNode = monsters[i];
|
|
|
|
|
if (!mNode?.isValid) continue;
|
|
|
|
|
const mComp = mNode.getComponent(Monster);
|
|
|
|
|
if (!mComp || mComp.getIsDead()) continue;
|
|
|
|
|
|
|
|
|
|
const agent = mNode.getComponent(MapAgent);
|
|
|
|
|
let d: number;
|
|
|
|
|
if (fromNode?.isValid) {
|
|
|
|
|
d = agent ? agent.getNearestMonster(fromNode) : Vec3.distance(fromNode.worldPosition, mNode.worldPosition);
|
|
|
|
|
} else {
|
|
|
|
|
if (!agent) continue;
|
|
|
|
|
d = agent.getRemainPathDistance();
|
|
|
|
|
}
|
|
|
|
|
if (!Number.isFinite(d)) continue;
|
|
|
|
|
|
|
|
|
|
scored.push({
|
|
|
|
|
node: mNode,
|
|
|
|
|
d,
|
|
|
|
|
attacking: mComp.state === MonsterState.ATTACKING,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scored.length <= 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const attackingPool = scored.filter((s) => s.attacking);
|
|
|
|
|
const pool = attackingPool.length > 0 ? attackingPool : scored;
|
|
|
|
|
pool.sort((a, b) => a.d - b.d);
|
|
|
|
|
|
|
|
|
|
const need = Math.min(Math.floor(count), pool.length);
|
|
|
|
|
const result: Array<{ node: Node; num: number; isPrimarySide: boolean }> = [];
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < pool.length && result.length < need; i++) {
|
|
|
|
|
const { node } = pool[i];
|
|
|
|
|
if (!node?.isValid) continue;
|
|
|
|
|
const id = node.uuid;
|
|
|
|
|
if (seen.has(id)) continue;
|
|
|
|
|
seen.add(id);
|
|
|
|
|
result.push({ node, num: 0, isPrimarySide: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 获取手电筒范围内的怪物
|
|
|
|
|
* @param flashlight 手电筒节点
|
|
|
|
|
* @param scale 手电筒缩放比例
|
|
|
|
|
* @returns 在手电筒范围内的怪物数组
|
|
|
|
|
*/
|
|
|
|
|
getFlashlighttMonster(flashlight: Node, scale: number): Node[] {
|
|
|
|
|
// 竖向光束:美术尺寸为 窄边×长边 = baseWidth × baseHeight(如 120×1500),与缩放
|
|
|
|
|
// 局部坐标:长边沿 +Y 从锚点向前延伸,窄边沿 X 对称(与旧版「长边沿 +X、窄边沿 Y」对调)
|
|
|
|
|
const baseWidth = 120;
|
|
|
|
|
const baseHeight = 1500;
|
|
|
|
|
|
|
|
|
|
const beamLength = baseHeight * scale;
|
|
|
|
|
const halfThickness = (baseWidth * scale) / 2;
|
|
|
|
|
|
|
|
|
|
// 获取手电筒的世界位置和角度
|
|
|
|
|
const flashlightPos = flashlight.worldPosition;
|
|
|
|
|
const flashlightAngle = flashlight.angle;
|
|
|
|
|
|
|
|
|
|
// 存储在手电筒范围内的怪物
|
|
|
|
|
const monstersInRange: Node[] = [];
|
|
|
|
|
|
|
|
|
|
// 遍历所有怪物,检查是否在手电筒范围内
|
|
|
|
|
for (const monsterNode of this.node.children) {
|
|
|
|
|
// 获取怪物的世界位置
|
|
|
|
|
const monsterPos = monsterNode.worldPosition;
|
|
|
|
|
|
|
|
|
|
// 将怪物坐标转换为手电筒局部坐标系
|
|
|
|
|
// 计算怪物相对于手电筒的偏移量
|
|
|
|
|
const dx = monsterPos.x - flashlightPos.x;
|
|
|
|
|
const dy = monsterPos.y - flashlightPos.y;
|
|
|
|
|
|
|
|
|
|
// 旋转坐标(将手电筒旋转角度转换为弧度,注意符号)
|
|
|
|
|
// 这里使用负数是因为我们需要将怪物坐标旋转回手电筒未旋转时的坐标系
|
|
|
|
|
const radian = -flashlightAngle * Math.PI / 180;
|
|
|
|
|
|
|
|
|
|
// 应用旋转公式:x' = x*cosθ - y*sinθ, y' = x*sinθ + y*cosθ
|
|
|
|
|
const localX = dx * Math.cos(radian) - dy * Math.sin(radian);
|
|
|
|
|
const localY = dx * Math.sin(radian) + dy * Math.cos(radian);
|
|
|
|
|
|
|
|
|
|
// 竖向矩形:|localX| <= 半窄边,localY 从锚点沿光束向前 [0, beamLength]
|
|
|
|
|
// 若预制体光束沿 -Y 或中心对称,需改 localY 区间
|
|
|
|
|
if (Math.abs(localX) <= halfThickness && localY >= 0 && localY <= beamLength) {
|
|
|
|
|
monstersInRange.push(monsterNode);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return monstersInRange;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resetInfiniteModeWave() {
|
|
|
|
|
if (gg.game.CurentBattle.BattleType == BattleType.InfiniteMode) {
|
|
|
|
|
if (this.nextEventIndex == this.spawnEvents.length) {
|
|
|
|
|
console.log('无尽模式最后一波怪物生成完后,重置怪物生成事件索引', gg.game.CurentBattle.BattleTime)
|
|
|
|
|
this.nextEventIndex = 0;
|
|
|
|
|
//gg.game.CurentBattle.MonsterDeadNum = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|