115 lines
5.9 KiB
TypeScript
115 lines
5.9 KiB
TypeScript
// 只传展示数据;来源使用世界坐标,兼容卡片缩放和位移。
|
||
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;
|
||
}
|
||
}
|