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