refactor: enhance GoFish ask animation with fishing line effect
Refactor the ask sequence in GoFishGame.js to introduce a visual fishing line animation between the asker and target before revealing the result. Key changes: - Extract target slot/layout calculations outside conditional blocks for clarity. - Add `animateFishingLine` method to draw a dynamic hook arc with barb that extends from asker to target over 1 second. - Reorganize animation timing: reveal asker's card first, then cast fishing line, then handle catch/fail outcomes. - Simplify catch/fail logic by unifying post-line animation flows. - Improve visual feedback with staggered card appearances and smoother transitions.
This commit is contained in:
parent
55ff4a3e00
commit
b63bf1b651
|
|
@ -705,6 +705,9 @@ export default class GoFishGame extends Phaser.Scene {
|
|||
const askerSlot = this.slotForSeat[askerSeat];
|
||||
const askerLayout = slotLayout(askerSlot);
|
||||
const askerShow = this.slotShowPos(askerSlot, askerLayout);
|
||||
const targetSlot = this.slotForSeat[targetSeat];
|
||||
const targetLayout = slotLayout(targetSlot);
|
||||
const targetShow = this.slotShowPos(targetSlot, targetLayout);
|
||||
|
||||
const askerCard = beforeState.players[askerSeat].hand.find(c => c.rank === rank);
|
||||
if (!askerCard) { onComplete(); return; }
|
||||
|
|
@ -715,6 +718,7 @@ export default class GoFishGame extends Phaser.Scene {
|
|||
askerSprite.setDepth(D.banner - 4);
|
||||
this.transientObjs.push(askerSprite);
|
||||
|
||||
// Move asker card to show zone then flip it face-up.
|
||||
this.tweens.add({
|
||||
targets: askerSprite,
|
||||
x: askerShow.x, y: askerShow.y,
|
||||
|
|
@ -723,72 +727,67 @@ export default class GoFishGame extends Phaser.Scene {
|
|||
onComplete: () => this.flipCardFaceUp(askerSprite, askerCard),
|
||||
});
|
||||
|
||||
if (last.result === 'catch') {
|
||||
const targetSlot = this.slotForSeat[targetSeat];
|
||||
const targetLayout = slotLayout(targetSlot);
|
||||
const targetShow = this.slotShowPos(targetSlot, targetLayout);
|
||||
const n = last.cardsTransferred.length;
|
||||
|
||||
this.time.delayedCall(640, () => {
|
||||
const isTargetVert = targetSlot === 'left' || targetSlot === 'right';
|
||||
const targetSprites = last.cardsTransferred.map((card, i) => {
|
||||
const tx = isTargetVert ? targetShow.x
|
||||
: targetShow.x + (i - (n - 1) / 2) * (CARD_W + 8);
|
||||
const ty = isTargetVert ? targetShow.y + (i - (n - 1) / 2) * (CARD_H + 8)
|
||||
: targetShow.y;
|
||||
|
||||
const sprite = this.makeCardSprite(card, targetLayout.handCenter.x, targetLayout.handCenter.y, {
|
||||
faceUp: false, rotation: 0,
|
||||
});
|
||||
sprite.setDepth(D.banner - 4);
|
||||
this.transientObjs.push(sprite);
|
||||
|
||||
this.tweens.add({
|
||||
targets: sprite,
|
||||
x: tx, y: ty,
|
||||
duration: 250,
|
||||
delay: i * 80,
|
||||
ease: 'Cubic.easeOut',
|
||||
onComplete: () => this.flipCardFaceUp(sprite, card),
|
||||
});
|
||||
|
||||
return { sprite, tx, ty };
|
||||
});
|
||||
|
||||
const waitMs = 250 + (n - 1) * 80 + 380;
|
||||
this.time.delayedCall(waitMs, () => {
|
||||
if (askerSprite.active) {
|
||||
this.tweens.add({
|
||||
targets: askerSprite, alpha: 0, duration: 200,
|
||||
onComplete: () => { if (askerSprite.active) askerSprite.destroy(); },
|
||||
// After card is revealed, cast fishing line toward the target, then show result.
|
||||
this.time.delayedCall(640, () => {
|
||||
this.animateFishingLine(askerShow.x, askerShow.y, targetShow.x, targetShow.y, () => {
|
||||
if (last.result === 'catch') {
|
||||
const n = last.cardsTransferred.length;
|
||||
const isTargetVert = targetSlot === 'left' || targetSlot === 'right';
|
||||
const targetSprites = last.cardsTransferred.map((card, i) => {
|
||||
const tx = isTargetVert ? targetShow.x
|
||||
: targetShow.x + (i - (n - 1) / 2) * (CARD_W + 8);
|
||||
const ty = isTargetVert ? targetShow.y + (i - (n - 1) / 2) * (CARD_H + 8)
|
||||
: targetShow.y;
|
||||
const sprite = this.makeCardSprite(card, targetLayout.handCenter.x, targetLayout.handCenter.y, {
|
||||
faceUp: false, rotation: 0,
|
||||
});
|
||||
}
|
||||
for (const { sprite } of targetSprites) {
|
||||
if (!sprite.active) continue;
|
||||
sprite.setDepth(D.banner - 4);
|
||||
this.transientObjs.push(sprite);
|
||||
this.tweens.add({
|
||||
targets: sprite,
|
||||
x: askerLayout.handCenter.x, y: askerLayout.handCenter.y,
|
||||
alpha: 0, duration: 380, ease: 'Cubic.easeIn',
|
||||
onComplete: () => { if (sprite.active) sprite.destroy(); },
|
||||
x: tx, y: ty,
|
||||
duration: 250,
|
||||
delay: i * 80,
|
||||
ease: 'Cubic.easeOut',
|
||||
onComplete: () => this.flipCardFaceUp(sprite, card),
|
||||
});
|
||||
}
|
||||
this.time.delayedCall(420, onComplete);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.time.delayedCall(880, () => {
|
||||
this.flipCardFaceDown(askerSprite);
|
||||
this.time.delayedCall(340, () => {
|
||||
this.tweens.add({
|
||||
targets: askerSprite,
|
||||
x: askerLayout.handCenter.x, y: askerLayout.handCenter.y,
|
||||
duration: 280, ease: 'Cubic.easeIn',
|
||||
onComplete: () => { if (askerSprite.active) askerSprite.destroy(); },
|
||||
return { sprite, tx, ty };
|
||||
});
|
||||
this.time.delayedCall(320, onComplete);
|
||||
});
|
||||
|
||||
const waitMs = 250 + (n - 1) * 80 + 380;
|
||||
this.time.delayedCall(waitMs, () => {
|
||||
if (askerSprite.active) {
|
||||
this.tweens.add({
|
||||
targets: askerSprite, alpha: 0, duration: 200,
|
||||
onComplete: () => { if (askerSprite.active) askerSprite.destroy(); },
|
||||
});
|
||||
}
|
||||
for (const { sprite } of targetSprites) {
|
||||
if (!sprite.active) continue;
|
||||
this.tweens.add({
|
||||
targets: sprite,
|
||||
x: askerLayout.handCenter.x, y: askerLayout.handCenter.y,
|
||||
alpha: 0, duration: 380, ease: 'Cubic.easeIn',
|
||||
onComplete: () => { if (sprite.active) sprite.destroy(); },
|
||||
});
|
||||
}
|
||||
this.time.delayedCall(420, onComplete);
|
||||
});
|
||||
} else {
|
||||
// Fish — flip card back face-down and return it to hand.
|
||||
this.flipCardFaceDown(askerSprite);
|
||||
this.time.delayedCall(340, () => {
|
||||
this.tweens.add({
|
||||
targets: askerSprite,
|
||||
x: askerLayout.handCenter.x, y: askerLayout.handCenter.y,
|
||||
duration: 280, ease: 'Cubic.easeIn',
|
||||
onComplete: () => { if (askerSprite.active) askerSprite.destroy(); },
|
||||
});
|
||||
this.time.delayedCall(320, onComplete);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
slotShowPos(slot, layout) {
|
||||
|
|
@ -1013,6 +1012,67 @@ export default class GoFishGame extends Phaser.Scene {
|
|||
}
|
||||
}
|
||||
|
||||
animateFishingLine(fromX, fromY, toX, toY, onComplete) {
|
||||
const gfx = this.add.graphics().setDepth(D.banner - 3);
|
||||
const totalDx = toX - fromX, totalDy = toY - fromY;
|
||||
const totalLen = Math.sqrt(totalDx * totalDx + totalDy * totalDy) || 1;
|
||||
const ndx = totalDx / totalLen, ndy = totalDy / totalLen;
|
||||
const px = -ndy, py = ndx; // left-perpendicular to line direction
|
||||
const HOOK_R = 10;
|
||||
const BARB_LEN = 7;
|
||||
const SWEEP = (4 * Math.PI) / 3; // 240°
|
||||
|
||||
const drawFrame = (t) => {
|
||||
gfx.clear();
|
||||
gfx.lineStyle(2, 0xffe066, 1);
|
||||
const tipX = fromX + t * totalDx;
|
||||
const tipY = fromY + t * totalDy;
|
||||
|
||||
gfx.beginPath();
|
||||
gfx.moveTo(fromX, fromY);
|
||||
gfx.lineTo(tipX, tipY);
|
||||
gfx.strokePath();
|
||||
|
||||
// Hook arc offset perpendicular from tip
|
||||
const hookCx = tipX + px * HOOK_R;
|
||||
const hookCy = tipY + py * HOOK_R;
|
||||
const startAngle = Math.atan2(tipY - hookCy, tipX - hookCx);
|
||||
const endAngle = startAngle + SWEEP;
|
||||
gfx.beginPath();
|
||||
gfx.arc(hookCx, hookCy, HOOK_R, startAngle, endAngle, false);
|
||||
gfx.strokePath();
|
||||
|
||||
// Barb at hook tip
|
||||
const barbX = hookCx + HOOK_R * Math.cos(endAngle);
|
||||
const barbY = hookCy + HOOK_R * Math.sin(endAngle);
|
||||
gfx.beginPath();
|
||||
gfx.moveTo(barbX, barbY);
|
||||
gfx.lineTo(barbX - Math.sin(endAngle) * BARB_LEN, barbY + Math.cos(endAngle) * BARB_LEN);
|
||||
gfx.strokePath();
|
||||
};
|
||||
|
||||
const progress = { t: 0 };
|
||||
this.tweens.add({
|
||||
targets: progress,
|
||||
t: 1,
|
||||
duration: 1000,
|
||||
ease: 'Cubic.easeOut',
|
||||
onUpdate: () => drawFrame(progress.t),
|
||||
onComplete: () => {
|
||||
drawFrame(1);
|
||||
this.time.delayedCall(150, () => {
|
||||
this.tweens.add({
|
||||
targets: gfx,
|
||||
alpha: 0,
|
||||
duration: 250,
|
||||
ease: 'Linear',
|
||||
onComplete: () => { gfx.destroy(); onComplete(); },
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
formatAskBanner(last) {
|
||||
const asker = this.opponentName(last.askerSeat);
|
||||
const target = this.opponentName(last.targetSeat);
|
||||
|
|
|
|||
Loading…
Reference in New Issue