import { director, Director, Node, Sorting2D, Tween, UIOpacity, UITransform, Vec3, sp, tween } from 'cc'; import { GBundle, GPath } from '../../game/ConfigRes'; import { BattleSpinePlayback } from '../presentation/BattleSpinePlayback'; import { DeviceProfile } from './DeviceProfile'; const SPINE_NODE_NAME = 'spine'; const FALL_SPINE_NODE_NAME = 'fallSpine'; const OPEN_SPINE_NODE_NAME = 'openSpine'; const UPGRADE_ANIM = '升级效果'; /** 文字落到空格时的放置效果 */ export const FALL_ANIM = 'animation'; /** 铲开挖未开启格 */ export const OPEN_ANIM = 'animation'; /** levelUpText 预制体目录 */ const LEVEL_UP_TEXT_PATH = GPath.ObstaclePrefab(''); /** levelUpText 预制体名 */ const LEVEL_UP_TEXT_NAME = 'levelUpText'; /** 升级飘字上浮像素 */ const LEVEL_UP_TEXT_FLOAT_PX = 40; /** 升级飘字时长(秒) */ const LEVEL_UP_TEXT_DURATION_SEC = 1.0; /** 升级飘字相对锚点上移 */ const LEVEL_UP_TEXT_LIFT_PX = 20; /** 升级飘字 Sorting2D 层(monsterUp,压过英雄皮) */ const LEVEL_UP_TEXT_SORT_LAYER = 4; /** 升级飘字 Sorting2D order */ const LEVEL_UP_TEXT_SORT_ORDER = 10; /** * 英雄 / 单字升级飘字(battleMap/prefab/障碍物/levelUpText)。 */ export class LevelUpTextFloater { /** * 在飞行层按世界坐标播放「升 级」飘字。 * @param parent 挂载父节点(通常为 ui_fly) * @param worldPos 锚点世界坐标 */ static play(parent: Node | null, worldPos: Readonly): void { if (!DeviceProfile.enableLevelUpTextFloater) return; if (!parent?.isValid) { console.log('[LevelUpTextFloater] 挂载层缺失'); return; } let node: Node | null = null; try { node = gg.res.getNode(LEVEL_UP_TEXT_PATH, LEVEL_UP_TEXT_NAME, GBundle.BattleMap); } catch (e) { console.log('[LevelUpTextFloater] 预制体加载异常', String(e)); return; } if (!node?.isValid) { console.log('[LevelUpTextFloater] levelUpText 预制体缺失'); return; } node.parent = parent; node.layer = parent.layer; node.active = true; node.setSiblingIndex(parent.children.length - 1); const parentUt = parent.getComponent(UITransform); if (!parentUt) { console.log('[LevelUpTextFloater] 挂载层缺少 UITransform'); LevelUpTextFloater._release(node); return; } const local = parentUt.convertToNodeSpaceAR(new Vec3(worldPos.x, worldPos.y + LEVEL_UP_TEXT_LIFT_PX, worldPos.z)); node.setPosition(local.x, local.y, local.z); node.setScale(1, 1, 1); let sorting = node.getComponent(Sorting2D); if (!sorting) sorting = node.addComponent(Sorting2D); sorting.sortingLayer = LEVEL_UP_TEXT_SORT_LAYER; sorting.sortingOrder = LEVEL_UP_TEXT_SORT_ORDER; const opacity = node.getComponent(UIOpacity) ?? node.addComponent(UIOpacity); opacity.opacity = 255; Tween.stopAllByTarget(node); Tween.stopAllByTarget(opacity); const endY = local.y + LEVEL_UP_TEXT_FLOAT_PX; tween(node) .to(LEVEL_UP_TEXT_DURATION_SEC, { position: new Vec3(local.x, endY, local.z) }, { easing: 'cubicOut' }) .call(() => LevelUpTextFloater._release(node)) .start(); tween(opacity) .delay(LEVEL_UP_TEXT_DURATION_SEC * 0.35) .to(LEVEL_UP_TEXT_DURATION_SEC * 0.65, { opacity: 0 }) .start(); } /** 回收飘字节点 */ private static _release(node: Node | null): void { if (!node?.isValid) return; Tween.stopAllByTarget(node); const opacity = node.getComponent(UIOpacity); if (opacity) Tween.stopAllByTarget(opacity); if (node['resKey']) gg.res.putNode(node); else node.destroy(); } } function playNamedSpine( host: Node | null, spineNodeName: string, animName: string, onComplete?: () => void, ): void { if (!host?.isValid) { onComplete?.(); return; } const spineNode = host.getChildByName(spineNodeName); if (!spineNode?.isValid) { onComplete?.(); return; } const spine = spineNode.getComponent(sp.Skeleton); if (!spine?.isValid || !spine.skeletonData) { onComplete?.(); return; } spineNode.active = true; spineNode.setSiblingIndex(host.children.length - 1); spine.enabled = true; spine.paused = false; BattleSpinePlayback.resetTracks(spine); let settled = false; let hideTimer: ReturnType | null = null; const finish = (): void => { if (settled) return; settled = true; if (hideTimer != null) clearTimeout(hideTimer); if (spineNode?.isValid) spineNode.active = false; onComplete?.(); }; const ok = BattleSpinePlayback.play(spine, animName, { loop: false, onComplete: finish, }); if (ok) { hideTimer = setTimeout(finish, 2500); } else { finish(); } } function hideNamedSpine(host: Node | null, spineNodeName: string): void { if (!host?.isValid) return; const spineNode = host.getChildByName(spineNodeName); if (!spineNode?.isValid) return; const spine = spineNode.getComponent(sp.Skeleton); if (spine?.isValid) { BattleSpinePlayback.clearPendingComplete(spine); BattleSpinePlayback.resetTracks(spine); } spineNode.active = false; } /** 合成/升级时播放 mapGrid 上的 Spine 特效 */ export function playGridUpgradeSpine( host: Node | null, animName = UPGRADE_ANIM, onComplete?: () => void, ): void { if (!DeviceProfile.enableGridUpgradeSpine) { onComplete?.(); return; } playNamedSpine(host, SPINE_NODE_NAME, animName, onComplete); } /** 下一帧播放,避免 rebuild / 节点挂载同一帧内 Spine 未就绪或被覆盖 */ export function playGridUpgradeSpineDeferred( host: Node | null, animName = UPGRADE_ANIM, onComplete?: () => void, ): void { if (!host?.isValid) { onComplete?.(); return; } director.once(Director.EVENT_AFTER_UPDATE, () => { if (host?.isValid) playGridUpgradeSpine(host, animName, onComplete); else onComplete?.(); }); } export function hideGridUpgradeSpine(host: Node | null): void { hideNamedSpine(host, SPINE_NODE_NAME); } /** 文字落到空 mapGrid 时播放 fallSpine「animation」,播完自动隐藏 */ export function playGridFallSpine(host: Node | null, onComplete?: () => void): void { playNamedSpine(host, FALL_SPINE_NODE_NAME, FALL_ANIM, onComplete); } export function playGridFallSpineDeferred(host: Node | null, onComplete?: () => void): void { if (!host?.isValid) { onComplete?.(); return; } director.once(Director.EVENT_AFTER_UPDATE, () => { if (host?.isValid) playGridFallSpine(host, onComplete); else onComplete?.(); }); } export function hideGridFallSpine(host: Node | null): void { hideNamedSpine(host, FALL_SPINE_NODE_NAME); } /** 铲开挖未开启格时播放 openSpine「animation」,播完自动隐藏 */ export function playGridOpenSpine(host: Node | null, onComplete?: () => void): void { playNamedSpine(host, OPEN_SPINE_NODE_NAME, OPEN_ANIM, onComplete); } export function playGridOpenSpineDeferred(host: Node | null, onComplete?: () => void): void { if (!host?.isValid) { onComplete?.(); return; } director.once(Director.EVENT_AFTER_UPDATE, () => { if (host?.isValid) playGridOpenSpine(host, onComplete); else onComplete?.(); }); } export function hideGridOpenSpine(host: Node | null): void { hideNamedSpine(host, OPEN_SPINE_NODE_NAME); }