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.
239 lines
7.7 KiB
239 lines
7.7 KiB
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<Vec3>): 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<typeof setTimeout> | 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);
|
|
}
|
|
|