MatchMaster/assets/jungle_treasure/script/JungleAnimator.ts
2026-08-07 10:59:22 +08:00

302 lines
11 KiB
TypeScript

/** 只负责领取反馈、轨道移动和新卡出现动画。 */
export default class JungleAnimator {
constructor(
private host: cc.Component,
private contentNode: cc.Node,
private cardFrame: cc.SpriteFrame,
private unlockFrame: cc.SpriteFrame,
) { }
/** 档位从锁定变为当前可领取时,播放锁旋转下坠和按钮回弹。 */
public playUnlockEffect(card: cc.Node, onComplete: () => void): void {
let button = card && card.getChildByName("button");
let buttonVisual = button && button.getChildByName("button_content");
let lock = button && button.getChildByName("lock");
if (!button || !lock) {
onComplete();
return;
}
let buttonComponent = button.getComponent(cc.Button);
let lockSprite = lock.getComponent(cc.Sprite);
let originalLockFrame = lockSprite && lockSprite.spriteFrame;
let originalLockSize = lock.getContentSize();
let buttonLocalPosition = lock.getPosition();
let originZIndex = lock.zIndex;
let lockWorldPosition = lock.convertToWorldSpaceAR(cc.Vec2.ZERO);
let originPosition = this.contentNode.convertToNodeSpaceAR(lockWorldPosition);
let screenBottomY = cc.visibleRect && cc.visibleRect.bottom
? cc.visibleRect.bottom.y
: 0;
let dropWorldPosition = cc.v2(
lockWorldPosition.x,
screenBottomY - lock.height * 0.6,
);
let dropLocalPosition = this.contentNode.convertToNodeSpaceAR(dropWorldPosition);
let originX = originPosition.x;
let originY = originPosition.y;
let dropDistance = Math.max(0, originY - dropLocalPosition.y);
let dropDuration = Math.min(0.42, Math.max(0.22, dropDistance / 2800));
button.stopAllActions();
lock.stopAllActions();
if (buttonVisual) {
buttonVisual.stopAllActions();
buttonVisual.opacity = 0;
buttonVisual.scale = 0.92;
}
if (buttonComponent) {
buttonComponent.interactable = false;
}
button.scale = 1;
lock.active = true;
lock.opacity = 255;
lock.scale = 1;
lock.angle = 0;
if (lockSprite && this.unlockFrame) {
lockSprite.spriteFrame = this.unlockFrame;
lock.setContentSize(this.unlockFrame.getOriginalSize());
}
lock.parent = this.contentNode;
lock.zIndex = 60;
lock.setPosition(originX, originY);
cc.tween(button)
.to(0.08, { scale: 1.08 }, { easing: "quadOut" })
.to(0.1, { scale: 1 }, { easing: "backOut" })
.start();
if (buttonVisual) {
cc.tween(buttonVisual)
.to(0.08, { opacity: 255, scale: 1.06 }, { easing: "backOut" })
.to(0.1, { scale: 1 }, { easing: "sineOut" })
.start();
}
cc.tween(lock)
.to(0.04, { y: originY + 7, angle: -12 }, { easing: "quadOut" })
.to(0.04, { y: originY + 9, angle: 10 }, { easing: "sineInOut" })
.to(0.03, { y: originY + 4, angle: -6 }, { easing: "sineInOut" })
.parallel(
cc.tween().to(dropDuration, {
x: originX - 28,
y: dropLocalPosition.y,
angle: 220,
}, { easing: "quadIn" }),
cc.tween().to(dropDuration, { scale: 0.82 }, { easing: "quadIn" }),
)
.call(() => {
lock.active = false;
lock.opacity = 255;
lock.scale = 1;
lock.angle = 0;
if (lockSprite && originalLockFrame) {
lockSprite.spriteFrame = originalLockFrame;
lock.setContentSize(originalLockSize);
}
lock.parent = button;
lock.zIndex = originZIndex;
lock.setPosition(buttonLocalPosition);
if (buttonComponent) {
buttonComponent.interactable = true;
}
onComplete();
})
.start();
}
public playClaimEffect(card: cc.Node, onComplete: () => void): void {
const button = card.getChildByName("button");
const buttonVisual = button && button.getChildByName("button_content");
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 (buttonVisual) {
buttonVisual.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 playCardAttention(card: cc.Node, onComplete: () => void): void {
if (!card || !cc.isValid(card) || !card.activeInHierarchy) {
onComplete();
return;
}
card.scaleX = 1;
card.scaleY = 1;
cc.tween(card)
.to(0.14, { scaleX: 1.06, scaleY: 0.95 }, { easing: "sineInOut" })
.to(0.17, { scaleX: 0.97, scaleY: 1.06 }, { easing: "sineInOut" })
.to(0.18, { scaleX: 1.025, scaleY: 0.985 }, { easing: "sineInOut" })
.to(0.2, { scaleX: 1, scaleY: 1 }, { easing: "sineOut" })
.call(onComplete)
.start();
}
public stopCardAttention(card: cc.Node): void {
if (!card || !cc.isValid(card)) {
return;
}
cc.Tween.stopAllByTarget(card);
card.scaleX = 1;
card.scaleY = 1;
}
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;
}
}