/** 只负责领取反馈、轨道移动和新卡出现动画。 */ export default class JungleAnimator { constructor( private host: cc.Component, private contentNode: cc.Node, private cardFrame: cc.SpriteFrame, ) { } public playClaimEffect(card: cc.Node, onComplete: () => void): void { const button = card.getChildByName("button"); const buttonLabelNode = button && button.getChildByName("button_label"); const lock = button && button.getChildByName("lock"); if (button) { button.getComponent(cc.Button).interactable = false; cc.tween(button) .to(0.08, { scale: 1.08 }, { easing: "quadOut" }) .to(0.1, { scale: 1 }) .start(); } if (buttonLabelNode) { buttonLabelNode.opacity = 0; } if (lock) { lock.active = false; } const flash = this.createSpriteNode("claim_flash", this.cardFrame); flash.parent = card; flash.zIndex = 8; flash.color = cc.color(255, 244, 160); flash.opacity = 0; cc.tween(flash) .to(0.1, { opacity: 150 }) .to(0.18, { opacity: 0 }) .call(() => flash.destroy()) .start(); const check = new cc.Node("claim_check"); check.parent = button; check.zIndex = 20; check.setContentSize(84, 84); check.scale = 0; const checkGraphics = check.addComponent(cc.Graphics); checkGraphics.fillColor = cc.color(93, 205, 52, 255); checkGraphics.strokeColor = cc.color(255, 255, 255, 255); checkGraphics.lineWidth = 7; checkGraphics.circle(0, 0, 35); checkGraphics.fill(); checkGraphics.stroke(); checkGraphics.lineWidth = 9; checkGraphics.moveTo(-19, 1); checkGraphics.lineTo(-5, -14); checkGraphics.lineTo(23, 18); checkGraphics.stroke(); cc.tween(check) .to(0.16, { scale: 1.15 }, { easing: "backOut" }) .to(0.08, { scale: 1 }) .delay(0.18) .to(0.1, { opacity: 0 }) .call(() => check.destroy()) .start(); const rewardsNode = card.getChildByName("rewards"); const rewardGroups = rewardsNode ? rewardsNode.children.slice() : []; rewardGroups.forEach((group, index) => { const flyNode = cc.instantiate(group); const worldPosition = group.convertToWorldSpaceAR(cc.Vec2.ZERO); flyNode.parent = this.contentNode; flyNode.setPosition(this.contentNode.convertToNodeSpaceAR(worldPosition)); flyNode.zIndex = 30; const horizontalOffset = (index - (rewardGroups.length - 1) * 0.5) * 42; cc.tween(flyNode) .delay(index * 0.035) .parallel( cc.tween().by(0.42, { x: horizontalOffset, y: 190 + index % 2 * 24 }, { easing: "quadOut" }), cc.tween().to(0.42, { opacity: 0, scale: 1.12 }), ) .call(() => flyNode.destroy()) .start(); }); this.host.scheduleOnce(onComplete, 0.5); } public playCardExit(card: cc.Node, onComplete: () => void): void { cc.tween(card) .parallel( cc.tween().to(0.2, { scale: 0.78, opacity: 0 }), cc.tween().by(0.2, { y: 55 }), ) .call(onComplete) .start(); } public moveCardsWithOverlap( cards: cc.Node[], slotPositions: cc.Vec2[], cardIndex: number, onNewCardReady: () => void, ): void { if (cardIndex >= cards.length) { onNewCardReady(); return; } const movingCard = cards[cardIndex]; if (!movingCard.active) { this.moveCardsWithOverlap(cards, slotPositions, cardIndex + 1, onNewCardReady); return; } let nextCardIndex = cardIndex + 1; while (nextCardIndex < cards.length && !cards[nextCardIndex].active) { nextCardIndex++; } const targetPosition = slotPositions[cardIndex - 1]; const startPosition = cc.v2(movingCard.x, movingCard.y); const brakePosition = startPosition.lerp(targetPosition, 0.75); cc.tween(movingCard) .to(0.13, { x: brakePosition.x, y: brakePosition.y }, { easing: "linear" }) .call(() => { if (nextCardIndex < cards.length) { this.moveCardsWithOverlap(cards, slotPositions, nextCardIndex, onNewCardReady); } else { onNewCardReady(); } }) .to(0.5, { x: targetPosition.x, y: targetPosition.y }, { easing: "cubicOut" }) .start(); } public showNewCard(card: cc.Node, targetCenter: cc.Vec2, onComplete: () => void): void { card.setAnchorPoint(0.5, 0.5); card.scale = 0.06; card.setPosition(targetCenter); card.opacity = 0; card.active = true; cc.tween(card) .parallel( cc.tween().to(0.2, { opacity: 255 }, { easing: "sineOut" }), cc.tween() .to(0.28, { scale: 1.07 }, { easing: "cubicOut" }) .to(0.1, { scale: 0.97 }, { easing: "sineOut" }) .to(0.09, { scale: 1.02 }, { easing: "sineOut" }) .to(0.08, { scale: 1 }, { easing: "sineOut" }), ) .call(onComplete) .start(); } public playLockedFeedback(card: cc.Node): void { card.stopAllActions(); const x = card.x; cc.tween(card) .to(0.05, { x: x - 9 }) .to(0.05, { x: x + 9 }) .to(0.05, { x: x - 5 }) .to(0.05, { x }) .start(); } private createSpriteNode(name: string, frame: cc.SpriteFrame): cc.Node { const node = new cc.Node(name); const sprite = node.addComponent(cc.Sprite); sprite.sizeMode = cc.Sprite.SizeMode.RAW; sprite.spriteFrame = frame; if (frame) { node.setContentSize(frame.getOriginalSize()); } return node; } }