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/BattleSuccess/BattleSuccessOpenAnim.ts

480 lines
14 KiB

3 weeks ago
import { Node, sp, Tween, tween, UIOpacity, Vec3 } from 'cc';
import { popStaggerNode } from 'db://assets/mx/module/ui/popAnim';
export interface BattleSuccessNodes {
/** 背景 */
bg: Node | null;
/** 标题 */
title: Node | null;
/** 「获得奖励」标签 */
rewardLabel: Node | null;
/** 胜负提示文案 */
tipBg: Node | null;
/** 结算 Spine */
spine: Node | null;
/** 奖励滚动区 */
scroll: Node | null;
/** 按钮行 */
btnLayout: Node | null;
}
interface NodePose {
x: number;
y: number;
z: number;
angle: number;
sx: number;
sy: number;
}
export interface BattleSuccessPlayOpts {
nodes: BattleSuccessNodes;
rewardItems: Node[];
buttons: Node[];
/** 双倍领取(广告)按钮:强入场 + 持续脉冲 */
highlightBtn?: Node | null;
onComplete?: () => void;
}
const REWARD_GAP = 0.055;
const REWARD_DUR = 0.2;
const BTN_GAP = 0.07;
const _poseMap = new WeakMap<Node, NodePose>();
const _pulseMap = new WeakMap<Node, Tween<Node>>();
function trackLoop(node: Node, t: Tween<Node>): void {
const prev = _pulseMap.get(node);
if (prev) prev.stop();
_pulseMap.set(node, t);
t.start();
}
function ensureOpacity(node: Node): UIOpacity {
return node.getComponent(UIOpacity) || node.addComponent(UIOpacity);
}
function capturePose(node: Node): NodePose {
let pose = _poseMap.get(node);
if (!pose) {
const p = node.position;
const s = node.scale;
pose = { x: p.x, y: p.y, z: p.z, angle: node.angle, sx: s.x, sy: s.y };
_poseMap.set(node, pose);
}
return pose;
}
function stopNode(node: Node | null): void {
if (!node?.isValid) return;
Tween.stopAllByTarget(node);
const op = node.getComponent(UIOpacity);
if (op) Tween.stopAllByTarget(op);
const pulse = _pulseMap.get(node);
if (pulse) {
pulse.stop();
_pulseMap.delete(node);
}
}
function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}
function clamp01(t: number): number {
if (t <= 0) return 0;
if (t >= 1) return 1;
return t;
}
function easeOutExpo(t: number): number {
return t >= 1 ? 1 : 1 - Math.pow(2, -10 * t);
}
function easeOutCubic(t: number): number {
return 1 - Math.pow(1 - t, 3);
}
function easeOutQuad(t: number): number {
return 1 - (1 - t) * (1 - t);
}
function easeOutBack(t: number, overshoot = 1.6): number {
const c = overshoot + 1;
return 1 + c * Math.pow(t - 1, 3) + overshoot * Math.pow(t - 1, 2);
}
function setFinalPose(node: Node, pose: NodePose): void {
node.setPosition(pose.x, pose.y, pose.z);
node.angle = pose.angle;
node.setScale(pose.sx, pose.sy, 1);
const op = node.getComponent(UIOpacity);
if (op) op.opacity = 255;
}
function hideNode(node: Node | null): void {
if (!node?.isValid) {
console.log('[BattleSuccess] hideNode 节点缺失');
return;
}
const pose = capturePose(node);
stopNode(node);
node.setScale(0, 0, 1);
ensureOpacity(node).opacity = 0;
node.setPosition(pose.x, pose.y, pose.z);
node.angle = pose.angle;
}
function hideFadeUp(node: Node | null, lift = 36): void {
if (!node?.isValid) {
console.log('[BattleSuccess] hideFadeUp 节点缺失');
return;
}
const pose = capturePose(node);
stopNode(node);
node.setScale(pose.sx, pose.sy, 1);
ensureOpacity(node).opacity = 0;
node.setPosition(pose.x, pose.y - lift, pose.z);
node.angle = pose.angle;
}
function hideButtons(btnLayout: Node | null): void {
if (!btnLayout?.isValid) {
console.log('[BattleSuccess] hideButtons 节点缺失');
return;
}
const pose = capturePose(btnLayout);
stopNode(btnLayout);
btnLayout.setScale(pose.sx, pose.sy, 1);
ensureOpacity(btnLayout).opacity = 0;
for (const child of btnLayout.children) {
if (!child.active) continue;
capturePose(child);
child.setScale(0, 0, 1);
ensureOpacity(child).opacity = 0;
}
}
/** 上屏前:静态节点归零,等待庆祝序列 */
export function prepareBattleSuccessOpen(nodes: BattleSuccessNodes): void {
stopBattleSuccessOpen(nodes);
hideNode(nodes.bg);
hideNode(nodes.title);
hideFadeUp(nodes.tipBg, 28);
hideNode(nodes.rewardLabel);
hideNode(nodes.spine);
hideNode(nodes.scroll);
hideButtons(nodes.btnLayout);
}
export function stopBattleSuccessOpen(nodes: BattleSuccessNodes): void {
for (const n of Object.values(nodes)) stopNode(n);
if (!nodes.btnLayout?.isValid) return;
const layoutPose = _poseMap.get(nodes.btnLayout);
if (layoutPose) setFinalPose(nodes.btnLayout, layoutPose);
for (const child of nodes.btnLayout.children) {
if (!child.active) continue;
const pose = _poseMap.get(child);
if (pose) setFinalPose(child, pose);
}
}
function playBgFlash(node: Node | null, delay: number): void {
if (!node?.isValid) {
console.log('[BattleSuccess] playBgFlash 节点缺失');
return;
}
const pose = capturePose(node);
const op = ensureOpacity(node);
node.setScale(pose.sx * 1.28, pose.sy * 1.28, 1);
op.opacity = 0;
tween(node)
.delay(delay)
.to(0.24, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const t = easeOutCubic(r ?? 0);
const s = lerp(1.28, 1, t);
node.setScale(pose.sx * s, pose.sy * s, 1);
op.opacity = Math.floor(255 * easeOutQuad(r ?? 0));
},
})
.call(() => setFinalPose(node, pose))
.start();
}
function playSpineBurst(node: Node | null, delay: number): void {
if (!node?.isValid) {
console.log('[BattleSuccess] playSpineBurst 节点缺失');
return;
}
const pose = capturePose(node);
const sk = node.getComponent(sp.Skeleton);
if (sk) {
sk.setAnimation(0, '胜利', true);
}
node.setScale(0.32, 0.32, 1);
ensureOpacity(node).opacity = 255;
tween(node)
.delay(delay)
.to(0.14, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const t = easeOutExpo(r ?? 0);
const s = lerp(0.32, 1.12, t);
node.setScale(pose.sx * s, pose.sy * s, 1);
},
})
.to(0.1, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const t = easeOutCubic(r ?? 0);
const s = lerp(1.12, 1, t);
node.setScale(pose.sx * s, pose.sy * s, 1);
},
})
.call(() => setFinalPose(node, pose))
.start();
}
function playTitleSlam(node: Node | null, delay: number): void {
if (!node?.isValid) {
console.log('[BattleSuccess] playTitleSlam 节点缺失');
return;
}
const pose = capturePose(node);
const op = ensureOpacity(node);
node.setScale(0, 0, 1);
node.angle = pose.angle - 14;
op.opacity = 0;
node.setPosition(pose.x, pose.y + 72, pose.z);
tween(node)
.delay(delay)
.to(0.15, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const t = easeOutExpo(r ?? 0);
const s = lerp(0, 1.14, t);
node.setScale(pose.sx * s, pose.sy * s, 1);
node.angle = pose.angle + lerp(-14, 5, t);
op.opacity = Math.floor(255 * easeOutQuad(r ?? 0));
node.setPosition(pose.x, pose.y + 72 * (1 - easeOutCubic(r ?? 0)), pose.z);
},
})
.to(0.09, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const t = r ?? 0;
const s = lerp(1.14, 1, easeOutCubic(t));
node.setScale(pose.sx * s, pose.sy * s, 1);
node.angle = pose.angle + lerp(5, 0, t);
node.setPosition(pose.x, pose.y, pose.z);
},
})
.call(() => setFinalPose(node, pose))
.start();
}
function playFadeUpIn(node: Node | null, delay: number, lift = 28): void {
if (!node?.isValid) {
console.log('[BattleSuccess] playFadeUpIn 节点缺失');
return;
}
const pose = capturePose(node);
const op = ensureOpacity(node);
node.setScale(pose.sx, pose.sy, 1);
op.opacity = 0;
node.setPosition(pose.x, pose.y - lift, pose.z);
tween(node)
.delay(delay)
.parallel(
tween(node).to(0.22, { position: new Vec3(pose.x, pose.y, pose.z) }, { easing: 'cubicOut' }),
tween(op).to(0.18, { opacity: 255 }),
)
.start();
}
function playCelebratePop(node: Node | null, delay: number): void {
if (!node?.isValid) {
console.log('[BattleSuccess] playCelebratePop 节点缺失');
return;
}
const pose = capturePose(node);
stopNode(node);
node.setScale(0, 0, 1);
ensureOpacity(node).opacity = 255;
tween(node)
.delay(delay)
.to(0.2, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const s = Math.min(easeOutBack(r ?? 0, 2.0), 1.1);
node.setScale(pose.sx * s, pose.sy * s, 1);
},
})
.call(() => setFinalPose(node, pose))
.start();
}
function playScrollReveal(node: Node | null, delay: number): void {
if (!node?.isValid) {
console.log('[BattleSuccess] playScrollReveal 节点缺失');
return;
}
const pose = capturePose(node);
stopNode(node);
node.setScale(pose.sx * 0.88, pose.sy * 0.88, 1);
ensureOpacity(node).opacity = 0;
tween(node)
.delay(delay)
.parallel(
tween(node).to(0.2, { scale: new Vec3(pose.sx, pose.sy, 1) }, { easing: 'backOut' }),
tween(ensureOpacity(node)).to(0.16, { opacity: 255 }),
)
.start();
}
function playSecondaryButtonEnter(node: Node, delay: number): void {
const pose = capturePose(node);
stopNode(node);
node.setScale(0, 0, 1);
ensureOpacity(node).opacity = 0;
tween(node)
.delay(delay)
.to(0.16, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const s = Math.min(easeOutBack(r ?? 0, 1.5), 1.05);
node.setScale(pose.sx * s, pose.sy * s, 1);
ensureOpacity(node).opacity = Math.floor(255 * clamp01((r ?? 0) * 1.4));
},
})
.call(() => {
setFinalPose(node, pose);
ensureOpacity(node).opacity = 255;
})
.start();
}
function playHighlightButtonEnter(node: Node, delay: number): void {
const pose = capturePose(node);
stopNode(node);
node.setScale(0, 0, 1);
ensureOpacity(node).opacity = 255;
tween(node)
.delay(delay)
.to(0.13, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const t = easeOutExpo(r ?? 0);
const s = lerp(0, 1.18, t);
node.setScale(pose.sx * s, pose.sy * s, 1);
},
})
.to(0.07, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const t = r ?? 0;
const s = lerp(1.18, 0.96, t);
node.setScale(pose.sx * s, pose.sy * s, 1);
},
})
.to(0.1, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const t = easeOutBack(r ?? 0, 2.0);
const s = lerp(0.96, 1, t);
node.setScale(pose.sx * s, pose.sy * s, 1);
},
})
.call(() => playHighlightButtonPulse(node))
.start();
}
function playHighlightButtonPulse(node: Node): void {
const pose = capturePose(node);
setFinalPose(node, pose);
ensureOpacity(node).opacity = 255;
trackLoop(node, tween(node)
.repeatForever(
tween(node)
.to(0.4, { scale: new Vec3(pose.sx * 1.12, pose.sy * 1.12, 1) }, { easing: 'sineInOut' })
.to(0.14, { scale: new Vec3(pose.sx * 1.15, pose.sy * 1.15, 1) }, { easing: 'sineOut' })
.to(0.4, { scale: new Vec3(pose.sx, pose.sy, 1) }, { easing: 'sineInOut' }),
));
}
function playButtonsEnter(buttons: Node[], highlightBtn: Node | null, delay: number): void {
buttons.forEach((btn, i) => {
if (!btn?.isValid || !btn.active) return;
const childDelay = delay + i * BTN_GAP;
capturePose(btn);
btn.setScale(0, 0, 1);
if (btn === highlightBtn) {
playHighlightButtonEnter(btn, childDelay);
} else {
playSecondaryButtonEnter(btn, childDelay);
}
});
}
export function stopHighlightButtonAnim(node: Node): void {
stopNode(node);
const pose = _poseMap.get(node);
if (pose) setFinalPose(node, pose);
}
/**
* 背景闪光 → 角色 Spine 爆发 → 标题砸落 → 提示上浮 → 奖励标签 → 道具庆祝错峰 → 按钮压轴
*/
export function playBattleSuccessOpen(opts: BattleSuccessPlayOpts): void {
const { nodes, rewardItems, buttons, highlightBtn = null, onComplete } = opts;
const n = nodes;
playBgFlash(n.bg, 0);
playSpineBurst(n.spine, 0.05);
playTitleSlam(n.title, 0.16);
const tipDelay = 0.42;
playFadeUpIn(n.tipBg, tipDelay, 24);
const labelDelay = 0.56;
playCelebratePop(n.rewardLabel, labelDelay);
const rewardStart = 0.68;
playScrollReveal(n.scroll, rewardStart - 0.04);
rewardItems.forEach((item, i) => {
if (!item?.isValid) return;
item.setScale(Vec3.ZERO);
popStaggerNode(item, 'celebrate', rewardStart + i * REWARD_GAP);
});
const btnStart = rewardStart + Math.max(rewardItems.length, 1) * REWARD_GAP + REWARD_DUR * 0.45;
if (n.btnLayout?.isValid) {
tween(ensureOpacity(n.btnLayout))
.delay(btnStart - 0.06)
.to(0.12, { opacity: 255 })
.start();
} else {
console.log('[BattleSuccess] btnLayout 节点缺失');
}
playButtonsEnter(buttons, highlightBtn, btnStart);
const highlightExtra = highlightBtn?.active ? 0.35 : 0;
const totalDur = btnStart + buttons.length * BTN_GAP + 0.24 + highlightExtra;
const delayHost = n.bg?.isValid ? n.bg : n.btnLayout;
if (delayHost?.isValid) {
tween(delayHost)
.delay(totalDur)
.call(() => onComplete?.())
.start();
} else {
console.log('[BattleSuccess] 无节点可挂接结算完成回调');
onComplete?.();
}
}