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/BattleRevive/BattleReviveOpenAnim.ts

581 lines
19 KiB

import { Label, Layout, Node, sp, Tween, tween, UIOpacity, Vec3 } from 'cc';
export interface BattleReviveNodes {
bg: Node | null;
title: Node | null;
spine: Node | null;
progressBg: Node | null;
progressPrefix: Node | null;
progressLabel: Node | null;
progressSuffix: Node | null;
rewardLabel: Node | null;
rewardPanel: Node | null;
tipText: Node | null;
btnLayout: Node | null;
}
interface NodePose {
x: number;
y: number;
z: number;
angle: number;
sx: number;
sy: number;
}
export interface BattleRevivePlayOpts {
nodes: BattleReviveNodes;
progressPercent: number;
reviveBtn: Node | null;
onComplete?: () => void;
}
/** 按钮显隐变更后刷新 Layout,避免动效阶段错位 */
export function syncBtnLayout(btnLayout: Node): void {
btnLayout?.getComponent(Layout)?.updateLayout();
}
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 | null): NodePose | null {
if (!node?.isValid) return null;
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 hideNodeSafe(node: Node | null): void {
if (!node?.isValid) return;
const pose = capturePose(node);
if (!pose) return;
stopNode(node);
node.setScale(0, 0, 1);
ensureOpacity(node).opacity = 0;
node.setPosition(pose.x, pose.y, pose.z);
node.angle = pose.angle;
}
function hideFadeSafe(node: Node | null): void {
if (!node?.isValid) return;
const pose = capturePose(node);
if (!pose) return;
stopNode(node);
node.setScale(pose.sx, pose.sy, 1);
ensureOpacity(node).opacity = 0;
}
function hideSlideXSafe(node: Node | null, offsetX: number): void {
if (!node?.isValid) return;
const pose = capturePose(node);
if (!pose) return;
stopNode(node);
node.setScale(pose.sx, pose.sy, 1);
ensureOpacity(node).opacity = 0;
node.setPosition(pose.x + offsetX, pose.y, pose.z);
}
function hideFadeUpSafe(node: Node | null, lift = 20): void {
if (!node?.isValid) return;
const pose = capturePose(node);
if (!pose) return;
stopNode(node);
node.setScale(pose.sx, pose.sy, 1);
ensureOpacity(node).opacity = 0;
node.setPosition(pose.x, pose.y - lift, pose.z);
}
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 easeOutCubic(t: number): number {
return 1 - Math.pow(1 - t, 3);
}
function easeOutExpo(t: number): number {
return t >= 1 ? 1 : 1 - Math.pow(2, -10 * t);
}
function easeOutBack(t: number, overshoot = 1.5): 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): void {
hideNodeSafe(node);
}
function hideFade(node: Node): void {
hideFadeSafe(node);
}
function hideSlideX(node: Node, offsetX: number): void {
hideSlideXSafe(node, offsetX);
}
function hideFadeUp(node: Node, lift = 20): void {
hideFadeUpSafe(node, lift);
}
function hideButtons(btnLayout: Node | null): void {
if (!btnLayout?.isValid) return;
stopNode(btnLayout);
const pose = capturePose(btnLayout);
if (!pose) return;
btnLayout.setScale(pose.sx, pose.sy, 1);
btnLayout.setPosition(pose.x, pose.y, pose.z);
ensureOpacity(btnLayout).opacity = 255;
for (const child of btnLayout.children) {
if (!child.active) continue;
capturePose(child);
stopNode(child);
child.setScale(0, 0, 1);
ensureOpacity(child).opacity = 0;
}
}
export function prepareBattleReviveOpen(nodes: BattleReviveNodes): void {
stopBattleReviveOpen(nodes);
hideFadeSafe(nodes.bg);
hideNodeSafe(nodes.title);
hideNodeSafe(nodes.spine);
hideFadeSafe(nodes.progressBg);
hideSlideXSafe(nodes.progressPrefix, -48);
hideNodeSafe(nodes.progressLabel);
hideSlideXSafe(nodes.progressSuffix, 48);
hideNodeSafe(nodes.rewardLabel);
hideFadeSafe(nodes.rewardPanel);
hideFadeUpSafe(nodes.tipText, 20);
hideButtons(nodes.btnLayout);
}
export function stopBattleReviveOpen(nodes: BattleReviveNodes): void {
for (const n of Object.values(nodes)) stopNode(n);
if (!nodes.btnLayout?.isValid) return;
stopNode(nodes.progressBg);
stopNode(nodes.progressPrefix);
stopNode(nodes.progressLabel);
stopNode(nodes.progressSuffix);
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);
}
const bgPose = nodes.progressBg ? _poseMap.get(nodes.progressBg) : undefined;
if (bgPose && nodes.progressBg?.isValid) setFinalPose(nodes.progressBg, bgPose);
const prefixPose = nodes.progressPrefix ? _poseMap.get(nodes.progressPrefix) : undefined;
if (prefixPose && nodes.progressPrefix?.isValid) setFinalPose(nodes.progressPrefix, prefixPose);
const labelPose = nodes.progressLabel ? _poseMap.get(nodes.progressLabel) : undefined;
if (labelPose && nodes.progressLabel?.isValid) setFinalPose(nodes.progressLabel, labelPose);
const suffixPose = nodes.progressSuffix ? _poseMap.get(nodes.progressSuffix) : undefined;
if (suffixPose && nodes.progressSuffix?.isValid) setFinalPose(nodes.progressSuffix, suffixPose);
}
function playBgFadeIn(node: Node | null, delay: number): void {
if (!node?.isValid) return;
const pose = capturePose(node);
if (!pose) return;
const op = ensureOpacity(node);
node.setScale(pose.sx * 1.08, pose.sy * 1.08, 1);
op.opacity = 0;
tween(node)
.delay(delay)
.parallel(
tween(node).to(0.28, { scale: new Vec3(pose.sx, pose.sy, 1) }, { easing: 'cubicOut' }),
tween(op).to(0.22, { opacity: 255 }),
)
.start();
}
function playSpineRevive(node: Node | null, delay: number): void {
if (!node?.isValid) return;
node.active = true;
const pose = capturePose(node);
if (!pose) return;
const sk = node.getComponent(sp.Skeleton);
if (sk) sk.setAnimation(0, '复活继续', true);
node.setScale(pose.sx * 0.55, pose.sy * 0.55, 1);
node.setPosition(pose.x, pose.y - 40, pose.z);
ensureOpacity(node).opacity = 0;
tween(node)
.delay(delay)
.parallel(
tween(node).to(0.26, { position: new Vec3(pose.x, pose.y, pose.z) }, { easing: 'cubicOut' }),
tween(node).to(0.22, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const t = easeOutBack(r ?? 0, 1.4);
const s = lerp(0.55, 1, t);
node.setScale(pose.sx * s, pose.sy * s, 1);
},
}),
tween(ensureOpacity(node)).to(0.18, { opacity: 255 }),
)
.call(() => setFinalPose(node, pose))
.start();
}
function playTitleDrop(node: Node | null, delay: number): void {
if (!node?.isValid) return;
const pose = capturePose(node);
if (!pose) return;
const op = ensureOpacity(node);
node.setScale(pose.sx * 1.4, pose.sy * 1.4, 1);
op.opacity = 0;
node.setPosition(pose.x, pose.y + 50, pose.z);
tween(node)
.delay(delay)
.to(0.2, {}, {
easing: 'linear',
onUpdate: (_t, r) => {
const t = easeOutExpo(r ?? 0);
const s = lerp(1.4, 1, easeOutCubic(r ?? 0));
node.setScale(pose.sx * s, pose.sy * s, 1);
op.opacity = Math.floor(255 * t);
node.setPosition(pose.x, pose.y + 50 * (1 - t), pose.z);
},
})
.call(() => setFinalPose(node, pose))
.start();
}
function playProgressGroup(
bg: Node | null,
prefix: Node | null,
labelNode: Node | null,
suffix: Node | null,
label: Label | null,
targetPercent: number,
delay: number,
): void {
if (!bg?.isValid || !prefix?.isValid || !labelNode?.isValid || !suffix?.isValid || !label?.isValid) return;
const bgPose = capturePose(bg);
const prefixPose = capturePose(prefix);
const labelPose = capturePose(labelNode);
const suffixPose = capturePose(suffix);
if (!bgPose || !prefixPose || !labelPose || !suffixPose) return;
tween(ensureOpacity(bg))
.delay(delay)
.to(0.14, { opacity: 255 })
.start();
tween(prefix)
.delay(delay + 0.04)
.parallel(
tween(prefix).to(0.18, { position: new Vec3(prefixPose.x, prefixPose.y, prefixPose.z) }, { easing: 'backOut' }),
tween(ensureOpacity(prefix)).to(0.14, { opacity: 255 }),
)
.start();
tween(suffix)
.delay(delay + 0.28)
.parallel(
tween(suffix).to(0.18, { position: new Vec3(suffixPose.x, suffixPose.y, suffixPose.z) }, { easing: 'backOut' }),
tween(ensureOpacity(suffix)).to(0.14, { opacity: 255 }),
)
.start();
labelNode.setScale(0, 0, 1);
ensureOpacity(labelNode).opacity = 255;
const safeTarget = Math.max(0, Math.min(99, targetPercent));
tween({ val: 0 })
.delay(delay + 0.1)
.to(0.5, { val: safeTarget }, {
easing: 'cubicOut',
onUpdate: (obj: { val: number }) => {
label.string = `${Math.floor(obj.val)}%`;
const wave = 1 + 0.06 * Math.sin((obj.val / Math.max(safeTarget, 1)) * Math.PI);
labelNode.setScale(labelPose.sx * wave, labelPose.sy * wave, 1);
},
})
.call(() => {
label.string = `${safeTarget}%`;
tween(labelNode)
.to(0.07, { scale: new Vec3(labelPose.sx * 1.28, labelPose.sy * 1.28, 1) })
.to(0.14, { scale: new Vec3(labelPose.sx, labelPose.sy, 1) }, { easing: 'backOut' })
.call(() => playProgressIdleLoop(bg, prefix, labelNode, suffix))
.start();
})
.start();
tween(bg)
.delay(delay + 0.42)
.to(0.06, { scale: new Vec3(bgPose.sx * 1.04, bgPose.sy * 1.04, 1) })
.to(0.1, { scale: new Vec3(bgPose.sx, bgPose.sy, 1) }, { easing: 'sineOut' })
.start();
}
/** 「只差 X% 通关」持续呼吸:数字强脉冲 + 底条微扩 + 两侧向中间挤压 */
function playProgressIdleLoop(
bg: Node,
prefix: Node,
labelNode: Node,
suffix: Node,
): void {
const bgPose = capturePose(bg);
const labelPose = capturePose(labelNode);
const prefixPose = capturePose(prefix);
const suffixPose = capturePose(suffix);
trackLoop(labelNode, tween(labelNode)
.repeatForever(
tween(labelNode)
.to(0.5, { scale: new Vec3(labelPose.sx * 1.16, labelPose.sy * 1.16, 1) }, { easing: 'sineInOut' })
.to(0.5, { scale: new Vec3(labelPose.sx, labelPose.sy, 1) }, { easing: 'sineInOut' }),
));
const bgOp = ensureOpacity(bg);
trackLoop(bg, tween(bg)
.repeatForever(
tween(bg)
.parallel(
tween(bg).to(0.65, { scale: new Vec3(bgPose.sx * 1.06, bgPose.sy * 1.06, 1) }, { easing: 'sineInOut' })
.to(0.65, { scale: new Vec3(bgPose.sx, bgPose.sy, 1) }, { easing: 'sineInOut' }),
tween(bgOp).to(0.65, { opacity: 215 }).to(0.65, { opacity: 255 }),
),
));
trackLoop(prefix, tween(prefix)
.repeatForever(
tween(prefix)
.to(0.55, { position: new Vec3(prefixPose.x + 5, prefixPose.y, prefixPose.z) }, { easing: 'sineInOut' })
.to(0.55, { position: new Vec3(prefixPose.x, prefixPose.y, prefixPose.z) }, { easing: 'sineInOut' }),
));
trackLoop(suffix, tween(suffix)
.repeatForever(
tween(suffix)
.to(0.55, { position: new Vec3(suffixPose.x - 5, suffixPose.y, suffixPose.z) }, { easing: 'sineInOut' })
.to(0.55, { position: new Vec3(suffixPose.x, suffixPose.y, suffixPose.z) }, { easing: 'sineInOut' }),
));
}
function playCelebrateIn(node: Node | null, delay: number): void {
if (!node?.isValid) return;
const pose = capturePose(node);
if (!pose) return;
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, 1.8), 1.08);
node.setScale(pose.sx * s, pose.sy * s, 1);
},
})
.call(() => setFinalPose(node, pose))
.start();
}
function playRewardSection(rewardLabel: Node | null, rewardPanel: Node | null, tipText: Node | null, delay: number): void {
if (!rewardLabel?.isValid || !rewardPanel?.isValid || !tipText?.isValid) return;
playCelebrateIn(rewardLabel, delay);
const panelPose = capturePose(rewardPanel);
const tipPose = capturePose(tipText);
if (!panelPose || !tipPose) return;
ensureOpacity(rewardPanel).opacity = 0;
rewardPanel.setScale(panelPose.sx * 0.92, panelPose.sy * 0.92, 1);
tween(rewardPanel)
.delay(delay + 0.08)
.parallel(
tween(rewardPanel).to(0.2, { scale: new Vec3(panelPose.sx, panelPose.sy, 1) }, { easing: 'backOut' }),
tween(ensureOpacity(rewardPanel)).to(0.16, { opacity: 255 }),
)
.start();
tween(tipText)
.delay(delay + 0.16)
.parallel(
tween(tipText).to(0.18, { position: new Vec3(tipPose.x, tipPose.y, tipPose.z) }, { easing: 'cubicOut' }),
tween(ensureOpacity(tipText)).to(0.14, { opacity: 255 }),
)
.start();
}
function playSecondaryButtonEnter(node: Node | null, delay: number): void {
if (!node?.isValid) return;
const pose = capturePose(node);
if (!pose) return;
stopNode(node);
node.setScale(0, 0, 1);
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);
},
})
.call(() => {
setFinalPose(node, pose);
ensureOpacity(node).opacity = 255;
})
.start();
}
/** 整行按钮随 Layout 入场:只动 btnLayout 位移 + 子节点 scale,不改子节点 position/angle */
function playButtonLayoutEnter(btnLayout: Node | null, reviveBtn: Node | null, delay: number): void {
if (!btnLayout?.isValid) return;
const layoutPose = capturePose(btnLayout);
if (!layoutPose) return;
ensureOpacity(btnLayout).opacity = 255;
btnLayout.setPosition(layoutPose.x, layoutPose.y - 22, layoutPose.z);
tween(btnLayout)
.delay(delay - 0.06)
.to(0.18, { position: new Vec3(layoutPose.x, layoutPose.y, layoutPose.z) }, { easing: 'backOut' })
.start();
const activeChildren = btnLayout.children.filter(c => c.active);
activeChildren.forEach((child, i) => {
const childDelay = delay + i * 0.05;
capturePose(child);
child.setScale(0, 0, 1);
ensureOpacity(child).opacity = 0;
if (child === reviveBtn) {
playReviveButtonEnter(child, childDelay);
} else {
playSecondaryButtonEnter(child, childDelay);
}
});
}
function playReviveButtonEnter(node: Node | null, delay: number): void {
if (!node?.isValid) return;
const pose = capturePose(node);
if (!pose) return;
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(() => playReviveButtonPulse(node))
.start();
}
function playReviveButtonPulse(node: Node | null): void {
if (!node?.isValid) return;
const pose = capturePose(node);
if (!pose) return;
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' }),
));
}
/**
* 复活界面激励序列:
* 沉痛开场 → 角色挣扎 → 标题落定 → 「只差 X% 通关」数字滚动 → 奖励展示 → 复活按钮脉冲压轴
*/
export function playBattleReviveOpen(opts: BattleRevivePlayOpts): void {
const { nodes, progressPercent, reviveBtn, onComplete } = opts;
const n = nodes;
syncBtnLayout(n.btnLayout);
playBgFadeIn(n.bg, 0);
playSpineRevive(n.spine, 0.08);
playTitleDrop(n.title, 0.22);
const progressDelay = 0.42;
playProgressGroup(
n.progressBg,
n.progressPrefix,
n.progressLabel,
n.progressSuffix,
n.progressLabel?.getComponent(Label) ?? null,
progressPercent,
progressDelay,
);
const rewardDelay = 0.88;
playRewardSection(n.rewardLabel, n.rewardPanel, n.tipText, rewardDelay);
const reviveDelay = 1.18;
playButtonLayoutEnter(n.btnLayout, reviveBtn, reviveDelay);
const totalDur = reviveDelay + 0.7;
const bg = n.bg;
if (bg?.isValid) {
tween(bg)
.delay(totalDur)
.call(() => onComplete?.())
.start();
} else {
onComplete?.();
}
}