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.
582 lines
19 KiB
582 lines
19 KiB
|
3 weeks ago
|
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?.();
|
||
|
|
}
|
||
|
|
}
|