|
|
|
|
|
|
|
|
|
//*********************
|
|
|
|
|
// 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<ITableSkill>(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 {
|
|
|
|
|
// 本项目怪物仅 default 皮,不 setSkin
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|