MatchMaster/assets/reward_claim_effect/RewardClaimEffect.ts

136 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 只传展示数据;来源使用世界坐标,兼容卡片缩放和位移。
export interface RewardClaimVisual {
icon: cc.SpriteFrame;
count: number;
prefix: "" | "×";
isTime?: boolean; // 时间原值为秒,展示时换算分钟。
worldPosition: cc.Vec2;
worldPopPosition: cc.Vec2; // 道具中心上方一个道具高度,已包含节点缩放。
}
/** 独立领取效果包,只展示,不发奖、不创建奖励弹窗。 */
export default class RewardClaimEffect {
// 统一创建奖励展示节点,全部动画结束后清理并通知调用方。
public static play(parent: cc.Node, rewards: RewardClaimVisual[], done: () => void, numberFrames: cc.SpriteFrame[]) {
const visible = rewards.filter(item => item.count > 0);
if (!visible.length) { done(); return; }
const layer = new cc.Node("rewardClaimEffects");
parent.addChild(layer);
layer.zIndex = 2000;
const growDuration = 0.12;
const returnDuration = 0.025;
const stagger = growDuration + returnDuration; // 第一次放大并回正后,下一个开始生成。
const entries = visible.map(item => ({
item,
source: layer.convertToNodeSpaceAR(item.worldPosition),
position: layer.convertToNodeSpaceAR(item.worldPopPosition),
}));
entries.sort((a, b) => a.source.x - b.source.x);
const rowCenterX = (entries[0].position.x + entries[entries.length - 1].position.x) / 2;
const rowY = Math.max(...entries.map(entry => entry.position.y)) + 24;
// 正常尺寸下图标边缘间隔 10px,整排以原道具区域居中。
const rowSpacing = 88 + 10;
let remaining = visible.length; // 多奖励全部播完后,只回调一次。
entries.forEach(({ item }, index) => {
const node = new cc.Node("rewardClaim" + index);
layer.addChild(node);
const icon = new cc.Node("icon");
node.addChild(icon);
icon.setContentSize(88, 88);
const sprite = icon.addComponent(cc.Sprite);
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM;
sprite.spriteFrame = item.icon;
// 领取图标保持原图比例,最长边不超过 88px。
sprite.trim = false;
if (item.icon) {
const original = item.icon.getOriginalSize();
const scale = 88 / Math.max(original.width, original.height, 1);
icon.setContentSize(original.width * scale, original.height * scale);
}
this.createAmount(node, item, numberFrames, 88, 30, icon);
node.setPosition(cc.v2(rowCenterX + (index - (entries.length - 1) / 2) * rowSpacing, rowY));
node.scale = 0.2;
node.opacity = 0;
// 两次弹到 1.25 倍并快速回正,中间轻微收缩;停留后再上浮渐隐。
const animation = cc.sequence(
cc.spawn(cc.fadeIn(0.06), cc.scaleTo(growDuration, 1.25).easing(cc.easeSineOut())),
cc.scaleTo(returnDuration, 1),
cc.scaleTo(0.05, 0.9).easing(cc.easeSineOut()),
cc.scaleTo(0.1, 1.25).easing(cc.easeSineOut()),
cc.scaleTo(0.025, 1),
// 首个等最后一个弹完再停 0.25 秒;各自保留同样等待,按生成顺序起飞。
cc.delayTime((entries.length - 1) * stagger + 0.25),
cc.spawn(cc.moveBy(0.6, cc.v2(0, 150)).easing(cc.easeSineOut()), cc.fadeOut(0.6)),
);
node.runAction(cc.sequence(
cc.delayTime(index * stagger),
animation,
cc.callFunc(() => {
node.destroy();
if (--remaining === 0) { layer.destroy(); done(); }
}),
));
});
}
// 卡片和领取效果共用图片数量;仅显示层换算分钟,不改变发奖数值。
public static createAmount(parent: cc.Node, item: { count: number; prefix: "" | "×"; isTime?: boolean },
frames: cc.SpriteFrame[], maxWidth: number = 88, height: number = 30, icon: cc.Node = parent) {
const amount = new cc.Node("amount");
parent.addChild(amount);
// 按贴图实际可见边缘定位,透明留白不参与数量位置计算。
const frame = icon.getComponent(cc.Sprite).spriteFrame;
const original = frame && frame.getOriginalSize();
const rect = frame && frame.getRect();
const offset = frame && frame.getOffset();
const bottom = frame ? (offset.y - rect.height / 2) * icon.height / original.height : -icon.height / 2;
const right = frame ? (offset.x + rect.width / 2) * icon.width / original.width : icon.width / 2;
amount.setPosition(0, bottom + 4);
const value = item.isTime ? Number((item.count / 60).toFixed(2)) : item.count;
const symbols = (item.prefix + value).split("");
const glyphs = symbols.map(char => frames && frames[char === "+" ? 10 : char === "×" ? 11 : Number(char)]);
if (item.isTime) glyphs.push(frames && frames[12]);
// 缺图或小数使用文字兜底,保留完整的数量和单位。
if (!glyphs.every(Boolean) || !/^×?\d+$/.test(symbols.join(""))) {
const fallback = this.addAmountLabel(amount, item.prefix + value + (item.isTime ? "分钟" : ""), height, 0);
const label = fallback.getComponent(cc.Label);
fallback.setContentSize(maxWidth, height + 6);
label.overflow = cc.Label.Overflow.SHRINK;
if (item.prefix === "×") amount.x = right + 4 - maxWidth / 2;
return;
}
const scale = height / Math.max(...glyphs.map(frame => frame.getOriginalSize().height));
const widths = glyphs.map(frame => frame.getOriginalSize().width * scale);
const totalWidth = widths.reduce((sum, width) => sum + width, 0) + glyphs.length - 1;
const fit = Math.min(1, maxWidth / totalWidth);
// 金币/时间底边居中,道具乘号数量沿右下角对齐。
if (item.prefix === "×") amount.x = right + 4 - totalWidth * fit / 2;
let x = -totalWidth / 2;
glyphs.forEach((frame, index) => {
const glyph = new cc.Node("digit" + index);
amount.addChild(glyph);
const sprite = glyph.addComponent(cc.Sprite);
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM;
sprite.trim = false;
sprite.spriteFrame = frame;
glyph.setContentSize(widths[index] * fit, frame.getOriginalSize().height * scale * fit);
glyph.setPosition((x + widths[index] / 2) * fit, 0);
x += widths[index] + 1;
});
}
private static addAmountLabel(parent: cc.Node, value: string, size: number, y: number) {
const node = new cc.Node("amountLabel");
parent.addChild(node);
node.setPosition(0, y);
const label = node.addComponent(cc.Label);
label.string = value;
label.fontSize = size;
label.lineHeight = size + 4;
const outline = node.addComponent(cc.LabelOutline);
outline.color = cc.color(151, 79, 38);
outline.width = 2;
return node;
}
}