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/BattleBoss/UIBattleBoss.ts

396 lines
13 KiB

3 weeks ago
//*********************
// 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);
3 weeks ago
} 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);
3 weeks ago
}
}
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();
}
}