MatchMaster/assets/reward_claim_effect/RewardClaimEffect.ts
2026-09-22 13:41:47 +08:00

115 lines
5.9 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;
}
/** 独立领取效果包,只展示,不发奖、不创建奖励弹窗。 */
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 iconSize = 132;
let remaining = visible.length; // 多奖励全部播完后,只回调一次。
visible.forEach((item, index) => {
const node = new cc.Node("rewardClaim" + index);
layer.addChild(node);
const icon = new cc.Node("icon");
node.addChild(icon);
icon.setContentSize(iconSize, iconSize);
const sprite = icon.addComponent(cc.Sprite);
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM;
sprite.spriteFrame = item.icon;
// 飞出图标比原领取效果放大 50%,保持原图比例。
sprite.trim = false;
if (item.icon) {
const original = item.icon.getOriginalSize();
const scale = iconSize / Math.max(original.width, original.height, 1);
icon.setContentSize(original.width * scale, original.height * scale);
}
this.createAmount(node, item, numberFrames, iconSize, 40, icon);
node.setPosition(layer.convertToNodeSpaceAR(item.worldPosition));
const horizontalOffset = (index - (visible.length - 1) * 0.5) * 42;
// 从原位置错峰上浮,稍慢地散开,逐渐放大并淡出。
cc.tween(node)
.delay(index * 0.035)
.parallel(
cc.tween().by(0.6, { x: horizontalOffset, y: 190 + index % 2 * 24 }, { easing: "quadOut" }),
cc.tween().to(0.6, { opacity: 0, scale: 1.45 }),
)
.call(() => {
node.destroy();
if (--remaining === 0) { layer.destroy(); done(); }
})
.start();
});
}
// 卡片和领取效果共用图片数量;仅显示层换算分钟,不改变发奖数值。
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;
}
}