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 askerSlot = this.slotForSeat[askerSeat];
|
||||||
const askerLayout = slotLayout(askerSlot);
|
const askerLayout = slotLayout(askerSlot);
|
||||||
const askerShow = this.slotShowPos(askerSlot, askerLayout);
|
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);
|
const askerCard = beforeState.players[askerSeat].hand.find(c => c.rank === rank);
|
||||||
if (!askerCard) { onComplete(); return; }
|
if (!askerCard) { onComplete(); return; }
|
||||||
|
|
@ -715,6 +718,7 @@ export default class GoFishGame extends Phaser.Scene {
|
||||||
askerSprite.setDepth(D.banner - 4);
|
askerSprite.setDepth(D.banner - 4);
|
||||||
this.transientObjs.push(askerSprite);
|
this.transientObjs.push(askerSprite);
|
||||||
|
|
||||||
|
// Move asker card to show zone then flip it face-up.
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: askerSprite,
|
targets: askerSprite,
|
||||||
x: askerShow.x, y: askerShow.y,
|
x: askerShow.x, y: askerShow.y,
|
||||||
|
|
@ -723,72 +727,67 @@ export default class GoFishGame extends Phaser.Scene {
|
||||||
onComplete: () => this.flipCardFaceUp(askerSprite, askerCard),
|
onComplete: () => this.flipCardFaceUp(askerSprite, askerCard),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (last.result === 'catch') {
|
// After card is revealed, cast fishing line toward the target, then show result.
|
||||||
const targetSlot = this.slotForSeat[targetSeat];
|
this.time.delayedCall(640, () => {
|
||||||
const targetLayout = slotLayout(targetSlot);
|
this.animateFishingLine(askerShow.x, askerShow.y, targetShow.x, targetShow.y, () => {
|
||||||
const targetShow = this.slotShowPos(targetSlot, targetLayout);
|
if (last.result === 'catch') {
|
||||||
const n = last.cardsTransferred.length;
|
const n = last.cardsTransferred.length;
|
||||||
|
const isTargetVert = targetSlot === 'left' || targetSlot === 'right';
|
||||||
this.time.delayedCall(640, () => {
|
const targetSprites = last.cardsTransferred.map((card, i) => {
|
||||||
const isTargetVert = targetSlot === 'left' || targetSlot === 'right';
|
const tx = isTargetVert ? targetShow.x
|
||||||
const targetSprites = last.cardsTransferred.map((card, i) => {
|
: targetShow.x + (i - (n - 1) / 2) * (CARD_W + 8);
|
||||||
const tx = isTargetVert ? targetShow.x
|
const ty = isTargetVert ? targetShow.y + (i - (n - 1) / 2) * (CARD_H + 8)
|
||||||
: targetShow.x + (i - (n - 1) / 2) * (CARD_W + 8);
|
: targetShow.y;
|
||||||
const ty = isTargetVert ? targetShow.y + (i - (n - 1) / 2) * (CARD_H + 8)
|
const sprite = this.makeCardSprite(card, targetLayout.handCenter.x, targetLayout.handCenter.y, {
|
||||||
: targetShow.y;
|
faceUp: false, rotation: 0,
|
||||||
|
|
||||||
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(); },
|
|
||||||
});
|
});
|
||||||
}
|
sprite.setDepth(D.banner - 4);
|
||||||
for (const { sprite } of targetSprites) {
|
this.transientObjs.push(sprite);
|
||||||
if (!sprite.active) continue;
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: sprite,
|
targets: sprite,
|
||||||
x: askerLayout.handCenter.x, y: askerLayout.handCenter.y,
|
x: tx, y: ty,
|
||||||
alpha: 0, duration: 380, ease: 'Cubic.easeIn',
|
duration: 250,
|
||||||
onComplete: () => { if (sprite.active) sprite.destroy(); },
|
delay: i * 80,
|
||||||
|
ease: 'Cubic.easeOut',
|
||||||
|
onComplete: () => this.flipCardFaceUp(sprite, card),
|
||||||
});
|
});
|
||||||
}
|
return { sprite, tx, ty };
|
||||||
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(); },
|
|
||||||
});
|
});
|
||||||
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) {
|
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) {
|
formatAskBanner(last) {
|
||||||
const asker = this.opponentName(last.askerSeat);
|
const asker = this.opponentName(last.askerSeat);
|
||||||
const target = this.opponentName(last.targetSeat);
|
const target = this.opponentName(last.targetSeat);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue