You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/ui/BattleGame/BattleGameStartMonsterPrese...

748 lines
27 KiB

import { Label, Node, Tween, tween, v3, Vec3, sp } from 'cc';
import type { Cell } from 'db://assets/mx/module/battle';
import { AiHeadlessBagHost } from '../../battle/ai/runtime/AiHeadlessBagHost';
import { resolveSpawnJumpVisualPosInto } from '../../battle/bridge/CocosBattleMovementBridge';
import { applyMonsterFootOffset, BattleMapSpec } from '../../battle/map/BattleMapSpec';
import {
LOCAL_ENEMY_SPAWN_PORT,
resolvePathCellsFromSpawnPort,
} from '../../battle/map/MapPathQueries';
import type { MapManager } from '../../battle/map/legacy/MapManager';
import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback';
import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime';
import { schedulePresentation } from '../../battle/skill/BattlePresentationClock';
import { CocosBattleMonsterPresenter, SPAWN_PRESENT_SEC } from '../../battle/view/CocosBattleMonsterPresenter';
import { glyphSpineHasSkin } from '../../battle/view/GlyphSpineSkin';
import { BattleAudio } from '../../battle/audio/BattleAudio';
import { GBundle } from '../../game/ConfigRes';
import { TableNames } from '../../game/ConfigTableData';
import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch';
/** beginMonster 预制体目录 */
const BEGIN_MONSTER_PREFAB_PATH = 'prefab/beginMonster/';
/** 表演怪 anim 节点 */
const ANIM_NODE = 'anim';
/** 表演怪号角节点 */
const HORN_NODE = 'haojiao';
/** 我方开场气泡预制体(battleMap/prefab/地图/) */
const SELF_TALK_PREFAB = 'self';
/** 敌方开场气泡预制体(battleMap/prefab/地图/) */
const OTHER_TALK_PREFAB = 'other';
/** self 气泡相对出怪口偏移 */
const SELF_TALK_OFFSET = { x: -40, y: 20 };
/** other 气泡相对出怪口偏移 */
const OTHER_TALK_OFFSET = { x: 40, y: -20 };
/** 气泡内文本 label */
const LABL_NODE = 'labl';
/** 走路 */
const WALK_ANIM = '走路';
/** 待机 */
const IDLE_ANIM = '待机';
/** 拿出号角 */
const HORN_TAKE_ANIM = '拿出号角';
/** 吹号角 */
const HORN_BLOW_ANIM = '吹号角';
/** 收起号角 */
const HORN_PUT_AWAY_ANIM = '收起号角';
/** 配表:2 格移动基准 */
const MOVE_BASE_GRIDS = 2;
/** 吹号角停留(秒) */
const HORN_BLOW_HOLD_SEC = 2;
/** 开局后延迟多久再开始表演流程(秒) */
const START_FLOW_DELAY_SEC = 3;
/** 出怪口动画先播,再延迟此时间后开始跳出 */
const SPAWN_PORTAL_LEAD_SEC = 0.2;
/** 出怪跳出上升段 */
const SPAWN_JUMP_UP_SEC = 0.2;
/** 出怪跳出落地段 */
const SPAWN_JUMP_LAND_SEC = 0.28;
/** 我方出怪口 */
const ALLY_SPAWN_PORT = 1;
/** 表演怪专用层(与 monsterLayer 分离,不参与战斗 targeting) */
const PERFORM_LAYER_NAME = 'startMonsterLayer';
/** 表演节点名前缀 */
const PERFORM_NODE_NAME = 'startMonsterPerform';
/** 单侧表演上下文 */
type StartMonsterSideCtx = {
/** 出怪口 */
port: number;
/** 防守方 playerId */
defenderPlayerId: number;
/** anim 皮肤名 */
skinName: string;
/** 沿进路前缀路径 */
path: Cell[];
};
/** 单侧表演运行时 */
type StartMonsterActor = {
/** 根节点 */
node: Node;
/** 小怪 anim */
anim: sp.Skeleton;
/** 号角 anim */
horn: sp.Skeleton;
/** 出怪口挂载的开场气泡(self / other 预制体实例) */
talkBubble: Node | null;
/** 开场话候选(starttalk 按 | 拆分) */
startTalks: string[];
/** 路径 */
path: Cell[];
/** 移动距离(格) */
distance: number;
/** 两格移动时长(秒) */
moveTimeForTwoGrids: number;
};
/**
* 开局表演:双方出怪口跳出 beginMonster,沿进路前进、吹号角、再退回。
* 表演结束后再开战斗出怪。
*/
export class BattleGameStartMonsterPresenter {
private _gen = 0;
private _battle: ProjectBattleHandle | null = null;
private _map: MapManager | null = null;
private _monsterPresenter: CocosBattleMonsterPresenter | null = null;
private _performLayer: Node | null = null;
private _pendingDone: (() => void) | null = null;
private _actors: StartMonsterActor[] = [];
private _remaining = 0;
/** 开局表演是否进行中 */
isPerforming(): boolean {
return this._remaining > 0 || this._battle?.isStartMonsterPerforming?.() === true;
}
/** 中止表演并回收节点 */
unbind(): void {
this._gen++;
this._battle?.setStartMonsterPerforming(false);
this._battle = null;
this._map = null;
this._monsterPresenter = null;
this._performLayer = null;
this._pendingDone = null;
this._remaining = 0;
for (let i = 0; i < this._actors.length; i++) {
this._recycleActor(this._actors[i]);
}
this._actors.length = 0;
}
/**
* 若关卡配置了 startmonster,则播放开局表演;否则直接回调。
* @param battle 战斗句柄
* @param map 地图
* @param displayRoot 地图 UI 根(ui_mapParentNode)
* @param monsterPresenter 怪物表现(复用 shadowLayer / 阴影预制体)
* @param onComplete 表演怪回到出怪口并回收后 / 跳过后
*/
tryPlay(
battle: ProjectBattleHandle,
map: MapManager,
displayRoot: Node,
monsterPresenter: CocosBattleMonsterPresenter,
onComplete: () => void,
): void {
this.unbind();
this._battle = battle;
this._monsterPresenter = monsterPresenter;
const levelRow = gg.data.table.getTableData<ITableLevel>(TableNames.Level, battle.levelConfigId);
const prefabName = typeof levelRow?.startmonster === 'string' ? levelRow.startmonster.trim() : '';
const distance = Math.max(0, Math.floor(Number(levelRow?.startdistance) || 0));
const moveTimeForTwoGrids = Math.max(0.01, Number(levelRow?.startmovetime) || 0.01);
const startTalks = this._parseStartTalks(levelRow);
if (!prefabName || distance <= 0) {
onComplete();
return;
}
if (!map?.gridsInitialized) {
console.log('[StartMonster] 地图未就绪,跳过表演');
onComplete();
return;
}
const performLayer = this._ensurePerformLayer(displayRoot);
if (!performLayer?.isValid) {
console.log('[StartMonster] startMonsterLayer 缺失,跳过表演');
onComplete();
return;
}
this._performLayer = performLayer;
battle.setStartMonsterPerforming(true);
const sides: StartMonsterSideCtx[] = [
{
port: LOCAL_ENEMY_SPAWN_PORT,
defenderPlayerId: 0,
skinName: `${prefabName}2`,
path: resolvePathCellsFromSpawnPort(map, battle.session, LOCAL_ENEMY_SPAWN_PORT, 0, distance),
},
{
port: ALLY_SPAWN_PORT,
defenderPlayerId: AiHeadlessBagHost.AI_PLAYER_ID,
skinName: prefabName,
path: resolvePathCellsFromSpawnPort(
map,
battle.session,
ALLY_SPAWN_PORT,
AiHeadlessBagHost.AI_PLAYER_ID,
distance,
),
},
];
const gen = ++this._gen;
this._map = map;
this._pendingDone = onComplete;
this._remaining = 0;
this._actors.length = 0;
schedulePresentation(START_FLOW_DELAY_SEC, () => {
if (gen !== this._gen) return;
this._beginPerformFlow(gen, map, performLayer, prefabName, distance, moveTimeForTwoGrids, sides, startTalks);
});
}
/**
* 延迟结束后:生成表演怪并开始单侧流程。
* @param gen 世代号(unbind 后失效)
*/
private _beginPerformFlow(
gen: number,
map: MapManager,
performLayer: Node,
prefabName: string,
distance: number,
moveTimeForTwoGrids: number,
sides: readonly StartMonsterSideCtx[],
startTalks: string[],
): void {
if (gen !== this._gen) return;
const battle = this._battle;
if (!battle || !map?.gridsInitialized || !performLayer?.isValid) {
this._abortPerformWithoutActors();
return;
}
for (let i = 0; i < sides.length; i++) {
const side = sides[i];
if (side.path.length < distance + 1) {
console.log('[StartMonster] 进路不足', side.port, distance, side.path.length);
continue;
}
const actor = this._spawnActor(performLayer, prefabName, side, distance, moveTimeForTwoGrids, startTalks);
if (!actor) continue;
this._actors.push(actor);
this._remaining++;
this._playSide(gen, map, side.port, actor);
}
if (this._remaining <= 0) {
this._abortPerformWithoutActors();
}
}
/** 延迟后无法开演:解除挂起并回调 */
private _abortPerformWithoutActors(): void {
const battle = this._battle;
const done = this._pendingDone;
this._pendingDone = null;
this._map = null;
this._performLayer = null;
this._monsterPresenter = null;
this._battle = null;
battle?.setStartMonsterPerforming(false);
done?.();
}
/** 获取或创建表演专用层 */
private _ensurePerformLayer(displayRoot: Node): Node | null {
if (!displayRoot?.isValid) return null;
let layer = displayRoot.getChildByName(PERFORM_LAYER_NAME);
if (!layer) {
layer = new Node(PERFORM_LAYER_NAME);
layer.layer = displayRoot.layer;
layer.parent = displayRoot;
layer.setPosition(0, 0, 0);
}
return layer;
}
/** 单侧:出怪口特效 → 跳出 → 前进 → 号角 → 退回 */
private _playSide(gen: number, map: MapManager, port: number, actor: StartMonsterActor): void {
map.playSpawnPointEffect(port, SPAWN_PRESENT_SEC);
actor.node.opacity = 0;
this._syncActorShadow(actor);
Tween.stopAllByTarget(actor.node);
// 不用节点当 tween 目标:节点被回收时 Cocos 会停掉 tween,回调不走,startCombat 永远等不到
const lead = { t: 0 };
tween(lead)
.delay(SPAWN_PORTAL_LEAD_SEC)
.call(() => {
this._afterPortalLead(gen, map, port, actor);
})
.start();
}
/**
* 出怪口特效过后开始跳出。节点已失效则直接结束该侧,避免卡死开局出怪。
* @param gen 世代号
* @param map 地图
* @param port 出怪口
* @param actor 表演怪
*/
private _afterPortalLead(gen: number, map: MapManager, port: number, actor: StartMonsterActor): void {
if (gen !== this._gen) return;
if (!actor.node?.isValid) {
this._finishActor(actor);
return;
}
actor.talkBubble = this._attachTalkBubble(map, port, actor.startTalks);
this._showStartTalk(actor);
this._playSpawnJump(map, actor, () => {
if (gen !== this._gen) return;
this._walkPath(actor, true, () => {
if (gen !== this._gen) return;
this._playHornSequence(actor, () => {
if (gen !== this._gen) return;
this._walkPath(actor, false, () => {
if (gen !== this._gen) return;
this._snapActorToSpawn(actor);
this._finishActor(actor);
});
});
});
});
}
/** 实例化 beginMonster 并设置 anim 皮肤 */
private _spawnActor(
parent: Node,
prefabName: string,
side: StartMonsterSideCtx,
distance: number,
moveTimeForTwoGrids: number,
startTalks: string[],
): StartMonsterActor | null {
let node: Node | null = null;
try {
node = gg.res.getNode(BEGIN_MONSTER_PREFAB_PATH, prefabName, GBundle.BattleGameMonster);
} catch (err) {
console.log('[StartMonster] 预制体加载失败', prefabName, String(err));
return null;
}
if (!node?.isValid) {
console.log('[StartMonster] 预制体缺失', prefabName);
return null;
}
const animNode = node.getChildByName(ANIM_NODE);
const hornNode = node.getChildByName(HORN_NODE);
const anim = animNode?.getComponent(sp.Skeleton) ?? null;
const horn = hornNode?.getComponent(sp.Skeleton) ?? null;
if (!anim?.isValid || !horn?.isValid) {
console.log('[StartMonster] anim/haojiao 缺失', prefabName);
gg.res.putNode(node);
return null;
}
if (!this._applyAnimSkin(anim, side.skinName)) {
gg.res.putNode(node);
return null;
}
if (side.port === LOCAL_ENEMY_SPAWN_PORT) {
hornNode.setScale(-1, 1, 1);
} else {
hornNode.setScale(1, 1, 1);
}
hornNode.active = false;
BattleSpinePlayback.resetTracks(anim);
BattleSpinePlayback.resetTracks(horn);
BattleSpinePlayback.play(anim, IDLE_ANIM, { loop: true, optional: true });
node.name = PERFORM_NODE_NAME;
node.setParent(parent);
node.active = true;
node.setSiblingIndex(parent.children.length - 1);
this._monsterPresenter?.attachDecorFootShadow(node, prefabName);
return {
node,
anim,
horn,
talkBubble: null,
startTalks,
path: side.path,
distance,
moveTimeForTwoGrids,
};
}
/**
* 表演怪出来时:在出怪口挂载开场气泡预制体(随后立即显示)。
* spawnPoint_1 → self;spawnPoint_2 → other。
*/
private _attachTalkBubble(map: MapManager, port: number, startTalks: string[]): Node | null {
const spawnNode = map.spawnPointParent?.getChildByName(`spawnPoint_${port}`);
if (!spawnNode?.isValid) {
console.log('[StartMonster] 出怪口缺失,无法挂载开场气泡', port);
return null;
}
const prefabName = port === ALLY_SPAWN_PORT ? SELF_TALK_PREFAB : OTHER_TALK_PREFAB;
let bubble: Node | null = null;
try {
bubble = gg.res.getNode(BattleMapSpec.PREFAB_DIR, prefabName, GBundle.BattleMap);
} catch (err) {
console.log('[StartMonster] 开场气泡预制体加载失败', prefabName, String(err));
return null;
}
if (!bubble?.isValid) {
console.log('[StartMonster] 开场气泡预制体缺失', prefabName);
return null;
}
if (startTalks.length <= 0) {
console.log('[StartMonster] 关卡 starttalk 为空', port);
}
bubble.setParent(spawnNode);
const offset = port === ALLY_SPAWN_PORT ? SELF_TALK_OFFSET : OTHER_TALK_OFFSET;
bubble.setPosition(offset.x, offset.y, 0);
bubble.active = false;
return bubble;
}
/** 同步表演怪脚下阴影 */
private _syncActorShadow(actor: StartMonsterActor): void {
const node = actor.node;
if (!node?.isValid) return;
this._monsterPresenter?.syncDecorFootShadow(node);
}
/** 退回出怪口后对齐格心脚底 */
private _snapActorToSpawn(actor: StartMonsterActor): void {
const foot = this._cellFoot(actor.path[0]);
const node = actor.node;
if (!foot || !node?.isValid) return;
Tween.stopAllByTarget(node);
node.setWorldPosition(foot);
BattleSpinePlayback.play(actor.anim, IDLE_ANIM, { loop: true, optional: true });
actor.horn.node.active = false;
this._syncActorShadow(actor);
}
/** 写入 anim 皮肤 */
private _applyAnimSkin(anim: sp.Skeleton, skinName: string): boolean {
if (!glyphSpineHasSkin(anim, skinName)) {
console.log('[StartMonster] 皮肤不存在', skinName);
return false;
}
if (anim.skeletonData) ensureSpineSkeletonDataRuntime(anim.skeletonData);
try {
anim.setSkin(skinName);
(anim as sp.Skeleton & { defaultSkin: string }).defaultSkin = skinName;
anim.setSlotsToSetupPose();
return true;
} catch (err) {
console.log('[StartMonster] setSkin 失败', skinName, String(err));
return false;
}
}
/** 出怪口跳出:落在 path[1] */
private _playSpawnJump(map: MapManager, actor: StartMonsterActor, onLand: () => void): void {
const node = actor.node;
const spawnCell = actor.path[0];
const landCell = actor.path.length > 1 ? actor.path[1] : spawnCell;
const cellSize = map.mapGridSize ?? BattleMapSpec.CELL_SIZE;
const start = v3();
const end = v3();
const startOk = resolveSpawnJumpVisualPosInto(
map, spawnCell.row, spawnCell.col, landCell.row, landCell.col, 0, start,
);
const endOk = resolveSpawnJumpVisualPosInto(
map, spawnCell.row, spawnCell.col, landCell.row, landCell.col, 1, end,
);
if (!startOk || !endOk) {
console.log('[StartMonster] 跳出坐标无效', spawnCell, landCell);
onLand();
return;
}
const peakY = Math.max(start.y, end.y) + cellSize * 0.7;
const peak = v3((start.x + end.x) * 0.5, peakY, start.z);
const posProxy = { x: start.x, y: start.y };
const scale = 1;
const scaleProxy = { x: scale * 0.45, y: scale * 0.25 };
node.active = true;
node.setWorldPosition(start.x, start.y, start.z);
node.setScale(scaleProxy.x, scaleProxy.y, 1);
node.opacity = 255;
this._syncActorShadow(actor);
BattleSpinePlayback.play(actor.anim, WALK_ANIM, { loop: true, optional: true });
Tween.stopAllByTarget(node);
Tween.stopAllByTarget(posProxy);
Tween.stopAllByTarget(scaleProxy);
tween(scaleProxy)
.to(SPAWN_JUMP_UP_SEC, { x: scale * 0.88, y: scale * 1.22 }, {
onUpdate: () => {
if (!node.isValid) return;
node.setScale(scaleProxy.x, scaleProxy.y, 1);
},
})
.to(SPAWN_JUMP_LAND_SEC, { x: scale, y: scale }, {
onUpdate: () => {
if (!node.isValid) return;
node.setScale(scaleProxy.x, scaleProxy.y, 1);
},
})
.start();
tween(posProxy)
.to(SPAWN_JUMP_UP_SEC, { x: peak.x, y: peak.y }, {
onUpdate: () => {
if (!node.isValid) return;
node.setWorldPosition(posProxy.x, posProxy.y, start.z);
this._syncActorShadow(actor);
},
})
.to(SPAWN_JUMP_LAND_SEC, { x: end.x, y: end.y }, {
onUpdate: () => {
if (!node.isValid) return;
node.setWorldPosition(posProxy.x, posProxy.y, start.z);
this._syncActorShadow(actor);
},
})
.call(() => {
if (!node.isValid) {
onLand();
return;
}
this._syncActorShadow(actor);
onLand();
})
.start();
}
/**
* 沿 path 逐格移动。
* @param forward true=前进至 path[distance];false=退回 path[0]
*/
private _walkPath(actor: StartMonsterActor, forward: boolean, onDone: () => void): void {
const segments = this._buildWalkSegments(actor, forward);
if (segments.length <= 0) {
onDone();
return;
}
const secPerGrid = actor.moveTimeForTwoGrids / MOVE_BASE_GRIDS;
BattleSpinePlayback.play(actor.anim, WALK_ANIM, { loop: true, optional: true });
actor.horn.node.active = false;
this._walkSegment(actor, segments, 0, secPerGrid, onDone);
}
/** 构建逐格移动段 */
private _buildWalkSegments(actor: StartMonsterActor, forward: boolean): Vec3[] {
const out: Vec3[] = [];
const path = actor.path;
const dist = actor.distance;
if (forward) {
const startIdx = Math.min(1, path.length - 1);
for (let i = startIdx + 1; i <= dist; i++) {
const foot = this._cellFoot(path[i]);
if (foot) out.push(foot);
}
return out;
}
for (let i = dist - 1; i >= 0; i--) {
const foot = this._cellFoot(path[i]);
if (foot) out.push(foot);
}
return out;
}
/** 逐段 tween */
private _walkSegment(
actor: StartMonsterActor,
segments: Vec3[],
index: number,
secPerGrid: number,
onDone: () => void,
): void {
if (index >= segments.length) {
BattleSpinePlayback.play(actor.anim, IDLE_ANIM, { loop: true, optional: true });
onDone();
return;
}
const node = actor.node;
if (!node.isValid) {
onDone();
return;
}
const target = segments[index];
const fromX = node.worldPosition.x;
this._faceWalkDirection(node, fromX, target.x);
const posProxy = { x: node.worldPosition.x, y: node.worldPosition.y };
Tween.stopAllByTarget(posProxy);
tween(posProxy)
.to(secPerGrid, { x: target.x, y: target.y }, {
onUpdate: () => {
if (!node.isValid) return;
node.setWorldPosition(posProxy.x, posProxy.y, node.worldPosition.z);
this._syncActorShadow(actor);
},
})
.call(() => {
if (!node.isValid) {
onDone();
return;
}
this._syncActorShadow(actor);
this._walkSegment(actor, segments, index + 1, secPerGrid, onDone);
})
.start();
}
/** 号角:拿出 → 吹 2 秒 → 收起 */
private _playHornSequence(actor: StartMonsterActor, onDone: () => void): void {
const anim = actor.anim;
const horn = actor.horn;
const hornNode = horn.node;
if (!anim.isValid || !horn.isValid) {
this._hideStartTalk(actor);
onDone();
return;
}
BattleSpinePlayback.play(anim, IDLE_ANIM, { loop: true, optional: true });
hornNode.active = true;
BattleSpinePlayback.resetTracks(horn);
BattleSpinePlayback.play(horn, HORN_TAKE_ANIM, {
loop: false,
onComplete: () => {
if (!horn.isValid) {
this._hideStartTalk(actor);
onDone();
return;
}
BattleSpinePlayback.play(horn, HORN_BLOW_ANIM, { loop: true });
BattleAudio.playStartMonsterHorn();
schedulePresentation(HORN_BLOW_HOLD_SEC, () => {
if (!horn.isValid) {
this._hideStartTalk(actor);
onDone();
return;
}
BattleSpinePlayback.clearPendingComplete(horn);
BattleSpinePlayback.play(horn, HORN_PUT_AWAY_ANIM, {
loop: false,
onComplete: () => {
if (hornNode.isValid) hornNode.active = false;
this._hideStartTalk(actor);
onDone();
},
});
});
},
});
}
/** 关卡 starttalk 按 | 拆分为候选话 */
private _parseStartTalks(levelRow: ITableLevel | null): string[] {
const raw = typeof levelRow?.starttalk === 'string' ? levelRow.starttalk : '';
return raw.split('|').map(s => s.trim()).filter(s => s.length > 0);
}
/** 从候选话中随机取一条 */
private _pickStartTalk(talks: readonly string[]): string {
if (!talks || talks.length <= 0) return '';
return talks[Math.floor(Math.random() * talks.length)];
}
/** 表演怪出来时显示出怪口气泡并写入随机开场话 */
private _showStartTalk(actor: StartMonsterActor): void {
const bubble = actor.talkBubble;
if (!bubble?.isValid) return;
bubble.active = true;
const label = bubble.getChildByName(LABL_NODE)?.getComponent(Label) ?? null;
if (!label) {
console.log('[StartMonster] 气泡 labl 缺失');
return;
}
label.string = this._pickStartTalk(actor.startTalks);
}
/** 收起号角后隐藏出怪口开场气泡 */
private _hideStartTalk(actor: StartMonsterActor): void {
if (actor.talkBubble?.isValid) actor.talkBubble.active = false;
}
/** 格心 → 脚底世界坐标 */
private _cellFoot(cell: Cell): Vec3 | null {
const map = this._map;
if (!map) return null;
const grid = map.getMapGrid(cell.row, cell.col);
if (!grid?.node?.isValid) return null;
return applyMonsterFootOffset(grid.node.worldPosition, undefined, map.mapGridSize);
}
/** 按移动方向翻转 */
private _faceWalkDirection(node: Node, fromX: number, toX: number): void {
const sy = node.scale.y;
const absX = Math.abs(node.scale.x) || 1;
node.setScale(toX >= fromX ? absX : -absX, sy, 1);
}
/** 单侧结束并检查全局完成 */
private _finishActor(actor: StartMonsterActor): void {
this._recycleActor(actor);
for (let i = 0; i < this._actors.length; i++) {
if (this._actors[i] === actor) {
this._actors.splice(i, 1);
break;
}
}
this._remaining = Math.max(0, this._remaining - 1);
if (this._remaining > 0) return;
const battle = this._battle;
const done = this._pendingDone;
this._pendingDone = null;
this._map = null;
this._performLayer = null;
this._monsterPresenter = null;
this._battle = null;
battle?.setStartMonsterPerforming(false);
done?.();
}
/** 回收节点 */
private _recycleActor(actor: StartMonsterActor): void {
const node = actor.node;
this._recycleTalkBubble(actor);
if (!node?.isValid) return;
this._monsterPresenter?.recycleDecorFootShadow(node);
Tween.stopAllByTarget(node);
if (actor.anim?.isValid) BattleSpinePlayback.clearPendingComplete(actor.anim);
if (actor.horn?.isValid) BattleSpinePlayback.clearPendingComplete(actor.horn);
gg.res.putNode(node);
}
/** 回收出怪口开场气泡 */
private _recycleTalkBubble(actor: StartMonsterActor): void {
const bubble = actor.talkBubble;
actor.talkBubble = null;
if (!bubble?.isValid) return;
bubble.active = false;
bubble.removeFromParent();
gg.res.putNode(bubble);
}
}
let _inst: BattleGameStartMonsterPresenter | null = null;
/** 开局表演单例 */
export function getBattleGameStartMonsterPresenter(): BattleGameStartMonsterPresenter {
if (!_inst) _inst = new BattleGameStartMonsterPresenter();
return _inst;
}