//********************* // create by 流云 // time: Wed Sep 09 2026 // desc: Boss 出场弹窗 //********************* import { _decorator, find, Node, sp, Tween, tween, v3, Vec3, view } from 'cc'; import { Auto_BattleBoss } from './Auto_BattleBoss'; import { ChapterLevelBuilder } from '../../battle/config/ChapterLevelBuilder'; import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback'; import { GBundle } from '../../game/ConfigRes'; import { TableNames } from '../../game/ConfigTableData'; import { MonsterDataManager } from '../../manager/MonsterDataManager'; const { ccclass } = _decorator; /** BattleBoss 打开参数 */ export type BattleBossPanelData = { /** Monster.id */ monsterId: number; }; /** 背景弹窗缩放时长(秒) */ const POP_FRAME_SEC = 0.28; /** 左右滑入时长(秒) */ const SLIDE_IN_SEC = 0.45; /** 屏幕外偏移(像素) */ const SLIDE_OFFSET_X = 820; /** 弹窗出现后自动关闭延迟(秒) */ const AUTO_CLOSE_SEC = 5; @ccclass('UIBattleBoss') export class UIBattleBoss extends Auto_BattleBoss { /** ui_center/bg */ private _bgNode: Node | null = null; /** ui_left/bossPos */ private _bossPos: Node | null = null; /** 展示用怪物皮肤节点 */ private _bossSkinNode: Node | null = null; /** ui_left 目标位置 */ private readonly _uiLeftHome = new Vec3(); /** ui_right 目标位置 */ private readonly _uiRightHome = new Vec3(); /** 背景弹窗是否播完 */ private _popFrameReady = false; /** 内容是否就绪 */ private _contentReady = false; /** 关闭动效进行中,防重复触发 */ private _closing = false; onLoad(): void { super.onLoad(); this._bgNode = find('ui_center/bg', this.node); this._bossPos = find('ui_center/ui_left/bossPos', this.node); if (this.ui_left?.isValid) { this._uiLeftHome.set(this.ui_left.position); } if (this.ui_right?.isValid) { this._uiRightHome.set(this.ui_right.position); } this._ensureRightAboveLeft(); } onDisable(): void { this._cancelAutoClose(); this._stopSlideTween(); this._stopPopFrameTween(); this._recycleBossSkin(); this._popFrameReady = false; this._contentReady = false; this._closing = false; super.onDisable(); } /** 上屏前:左右面板先藏到屏外,按钮隐藏 */ preparePopOpenPose(): void { this._prepareSlidePanelsOffScreen(); if (this.btnGet?.isValid) { this.btnGet.active = false; } } onInit(): void { const data = this.prema as BattleBossPanelData | null; const monsterId = Math.floor(Number(data?.monsterId) || 0); if (!(monsterId > 0)) { console.log('[UIBattleBoss] 缺少 monsterId'); this.close(1); return; } const row = MonsterDataManager.getMonsterDataById(monsterId); if (!row) { console.log('[UIBattleBoss] Monster 缺失', monsterId); this.close(1); return; } this._refreshSkillDesc(row); this._mountBossSkin(row); this._contentReady = true; this._tryStartSlideIn(); } /** 先遮罩 + 背景弹入,再左右滑入 */ playPopFromConfig(): void { this._scheduleAutoClose(); this._prepareSlidePanelsOffScreen(); this.popMask('dimFast'); const bg = this._bgNode; if (!bg?.isValid) { this._popFrameReady = true; this._tryStartSlideIn(); return; } this._stopPopFrameTween(); bg.setScale(0.88, 0.88, 1); tween(bg) .to(POP_FRAME_SEC, { scale: v3(1, 1, 1) }, { easing: 'backOut' }) .call(() => { this._popFrameReady = true; this._tryStartSlideIn(); }) .start(); } /** 出征按钮:滑出后关闭弹窗 */ click_btnGet(): void { this._beginCloseWithSlideOut(); } /** 技能描述:Monster.skill → Skill.desc */ private _refreshSkillDesc(row: ITableMonster): void { const skillId = Math.floor(Number(row.skill) || 0); const skill = skillId > 0 ? gg.data.table.getTableData(TableNames.Skill, skillId) : null; if (skillId > 0 && !skill) { console.log('[UIBattleBoss] Skill 缺失', skillId, row.id); } if (this.lb_skillDesc?.isValid) { this.lb_skillDesc.string = skill ? gg.lang.get(skill.desc) : ''; } } /** 按怪物名加载 battleGameMonster/prefab/monster/ 预制体到 bossPos */ private _mountBossSkin(row: ITableMonster): void { this._recycleBossSkin(); const host = this._bossPos; if (!host?.isValid) { console.log('[UIBattleBoss] bossPos 缺失'); return; } const prefabName = ChapterLevelBuilder.resolveMonsterPrefabFromRow(row) ?? row.name; if (!prefabName) { console.log('[UIBattleBoss] 怪物预制体名为空', row.id); return; } const skin = gg.res.getNode('prefab/monster/', prefabName, GBundle.BattleGameMonster); if (!skin?.isValid) { console.log('[UIBattleBoss] 怪物预制体缺失', prefabName, row.id); return; } skin.setParent(host); skin.setPosition(0, 0, 0); skin.setScale(1, 1, 1); skin.active = true; const anim = skin.getChildByName('anim')?.getComponent(sp.Skeleton) ?? null; if (!anim) { console.error('[UIBattleBoss] 怪物 Spine 缺失', prefabName, row.id); } else { if (!MonsterDataManager.skipsBattleSpineSkin(row)) { const skinName = MonsterDataManager.resolveBattleSpineSkin(row, true); if (!skinName) { console.error('[UIBattleBoss] Boss 皮肤名为空', row.id); } else { anim.setSkin(skinName); } } const march = MonsterDataManager.resolveMarchAnim(row) || '走路'; if (!BattleSpinePlayback.play(anim, march, { loop: true })) { console.error('[UIBattleBoss] Boss 行进动画起播失败', row.id, march); } } this._bossSkinNode = skin; } /** 背景弹窗与内容都就绪后再滑入左右面板 */ private _tryStartSlideIn(): void { if (!this._popFrameReady || !this._contentReady) return; this._playSlideIn(); } /** ui_right 绘制层级高于 ui_left */ private _ensureRightAboveLeft(): void { const left = this.ui_left; const right = this.ui_right; if (!left?.isValid || !right?.isValid) return; const leftIdx = left.getSiblingIndex(); const rightIdx = right.getSiblingIndex(); if (rightIdx <= leftIdx) { right.setSiblingIndex(leftIdx + 1); } } /** 左右面板先放到屏外(隐藏) */ private _prepareSlidePanelsOffScreen(): void { const left = this.ui_left; const right = this.ui_right; if (!left?.isValid || !right?.isValid) return; this._stopSlideTween(); const offset = Math.max(SLIDE_OFFSET_X, view.getVisibleSize().width * 0.55); left.setPosition(this._uiLeftHome.x - offset, this._uiLeftHome.y, this._uiLeftHome.z); right.setPosition(this._uiRightHome.x + offset, this._uiRightHome.y, this._uiRightHome.z); left.active = false; right.active = false; } /** ui_left / ui_right 带缓动从屏幕外滑入 */ private _playSlideIn(): void { const left = this.ui_left; const right = this.ui_right; if (!left?.isValid || !right?.isValid) { console.log('[UIBattleBoss] ui_left/ui_right 缺失'); this._showBtnGet(); this._finishOpenAnim(); return; } this._stopSlideTween(); const offset = Math.max(SLIDE_OFFSET_X, view.getVisibleSize().width * 0.55); left.active = true; right.active = true; this._ensureRightAboveLeft(); left.setPosition(this._uiLeftHome.x - offset, this._uiLeftHome.y, this._uiLeftHome.z); right.setPosition(this._uiRightHome.x + offset, this._uiRightHome.y, this._uiRightHome.z); let pending = 2; const onDone = (): void => { pending -= 1; if (pending <= 0) { this._showBtnGet(); this._finishOpenAnim(); } }; tween(left) .to(SLIDE_IN_SEC, { position: v3(this._uiLeftHome.x, this._uiLeftHome.y, this._uiLeftHome.z) }, { easing: 'quadOut' }) .call(onDone) .start(); tween(right) .to(SLIDE_IN_SEC, { position: v3(this._uiRightHome.x, this._uiRightHome.y, this._uiRightHome.z) }, { easing: 'quadOut' }) .call(onDone) .start(); } /** 出征按钮在左右滑入完成后显示 */ private _showBtnGet(): void { if (this.btnGet?.isValid) { this.btnGet.active = true; } } /** 入场动效结束:通知 UI 层 */ private _finishOpenAnim(): void { this.notifyOpenAnimEnd(); } /** 弹窗完全出现后 AUTO_CLOSE_SEC 秒自动关闭 */ private _scheduleAutoClose(): void { this._cancelAutoClose(); this.scheduleOnce(this._onAutoClose, AUTO_CLOSE_SEC); } /** 取消自动关闭计时 */ private _cancelAutoClose(): void { this.unschedule(this._onAutoClose); } /** 自动关闭回调 */ private _onAutoClose(): void { this._beginCloseWithSlideOut(); } /** 隐藏 ui_bg / btnGet,左右面板反向滑出后关页 */ private _beginCloseWithSlideOut(): void { if (this._closing) return; this._closing = true; this._cancelAutoClose(); this._hideCloseChrome(); let maskDone = false; let slideDone = false; const tryFinish = (): void => { if (!maskDone || !slideDone) return; void this.close(1); }; if (!this.closeMaskEff(() => { maskDone = true; tryFinish(); })) { maskDone = true; } this._playSlideOut(() => { slideDone = true; tryFinish(); }); } /** 关闭时立即隐藏背景与出征按钮 */ private _hideCloseChrome(): void { if (this.ui_bg?.isValid) { this.ui_bg.active = false; } if (this.btnGet?.isValid) { this.btnGet.active = false; } } /** ui_left / ui_right 滑回屏外(出现动画反向) */ private _playSlideOut(onDone: () => void): void { const left = this.ui_left; const right = this.ui_right; if (!left?.isValid || !right?.isValid) { onDone(); return; } if (!left.active && !right.active) { onDone(); return; } this._stopSlideTween(); const offset = Math.max(SLIDE_OFFSET_X, view.getVisibleSize().width * 0.55); let pending = 2; const onSlideDone = (): void => { pending -= 1; if (pending > 0) return; if (left.isValid) left.active = false; if (right.isValid) right.active = false; onDone(); }; tween(left) .to(SLIDE_IN_SEC, { position: v3(this._uiLeftHome.x - offset, this._uiLeftHome.y, this._uiLeftHome.z), }, { easing: 'quadIn' }) .call(onSlideDone) .start(); tween(right) .to(SLIDE_IN_SEC, { position: v3(this._uiRightHome.x + offset, this._uiRightHome.y, this._uiRightHome.z), }, { easing: 'quadIn' }) .call(onSlideDone) .start(); } /** 停止滑入 tween */ private _stopSlideTween(): void { if (this.ui_left?.isValid) { Tween.stopAllByTarget(this.ui_left); } if (this.ui_right?.isValid) { Tween.stopAllByTarget(this.ui_right); } } /** 停止背景弹窗 tween */ private _stopPopFrameTween(): void { if (this._bgNode?.isValid) { Tween.stopAllByTarget(this._bgNode); } } /** 回收 bossPos 下展示用皮肤 */ private _recycleBossSkin(): void { const skin = this._bossSkinNode; this._bossSkinNode = null; if (!skin?.isValid) return; if (skin['resKey']) { gg.res.putNode(skin); return; } skin.destroy(); } }