feat(paigow): add animated hand-return and modal comparison sequence
- Animate player's split cards back to seat with split layout on submit - Replace showComparisons with step-by-step modal comparison overlay - Modal shows dealer cards, then each player's cards sequentially - Display high/low hand arrows (red=dealer wins, green=player wins) - Show verdict text (WIN/PUSH/LOSE) with sound effects and emotions - Animate cards and result text back to seat between players - Clear drag highlights when hiding hand-set panel - Swap chips/cards depth values in z-ordering constant
This commit is contained in:
parent
2f4a89fd3f
commit
e0b48a6285
|
|
@ -45,7 +45,7 @@ const CHIP_COLORS = { 5: 0xe05c5c, 25: 0x5cb85c, 50: 0x4a90d9, 100: 0x2c2c2c };
|
|||
const CHIP_AMOUNTS = [5, 25, 50, 100];
|
||||
const RESULT_COLORS = { win: '#5cb85c', lose: '#e05c5c', push: '#8a94a6', foul: '#e05c5c' };
|
||||
|
||||
const D = { bg: -1, table: 0, cards: 10, chips: 20, ui: 30, panel: 25, modal: 50 };
|
||||
const D = { bg: -1, table: 0, chips: 10, cards: 20, ui: 30, panel: 25, modal: 50 };
|
||||
|
||||
// ─── Hand-setting panel geometry ──────────────────────────────────────────────
|
||||
const PANEL_H = 390; // panel height
|
||||
|
|
@ -463,6 +463,7 @@ export default class PaiGowPokerGame extends Phaser.Scene {
|
|||
|
||||
hideHandSetPanel() {
|
||||
this.panelVisible = false;
|
||||
this._clearDragHighlights();
|
||||
for (const o of this.setPanelGroup) o.setVisible(false);
|
||||
for (const c of this.setCardConts) c.destroy();
|
||||
this.setCardConts = [];
|
||||
|
|
@ -758,8 +759,50 @@ export default class PaiGowPokerGame extends Phaser.Scene {
|
|||
const highHand = this.humanHighSlots.map(i => hand[i]);
|
||||
const lowHand = this.humanLowSlots.map(i => hand[i]);
|
||||
this.gs = applyHumanSplit(this.gs, highHand, lowHand);
|
||||
this.animating = true;
|
||||
this._animateHandReturnToSeat(() => {
|
||||
// Clear panel card containers before hideHandSetPanel destroys them
|
||||
for (const c of this.setCardConts) c.destroy();
|
||||
this.setCardConts = [];
|
||||
this.hideHandSetPanel();
|
||||
this.startComparingPhase();
|
||||
});
|
||||
}
|
||||
|
||||
_animateHandReturnToSeat(onComplete) {
|
||||
const pos = SEAT_POS[0];
|
||||
// Split display mirrors renderSeat's comparing layout
|
||||
const highCX = pos.x - 130;
|
||||
const lowCX = pos.x + 130;
|
||||
|
||||
const splitTarget = (cx, count, slotIdx) => {
|
||||
const totalW = (count - 1) * CARD_SPREAD_SPLIT;
|
||||
return { x: cx - totalW / 2 + slotIdx * CARD_SPREAD_SPLIT, y: pos.y };
|
||||
};
|
||||
|
||||
// Build ordered list: high hand cards first, then low hand
|
||||
const moves = [];
|
||||
this.humanHighSlots.forEach((cardIdx, slotIdx) => {
|
||||
if (cardIdx !== null) moves.push({ cont: this.setCardConts[cardIdx], ...splitTarget(highCX, 5, slotIdx) });
|
||||
});
|
||||
this.humanLowSlots.forEach((cardIdx, slotIdx) => {
|
||||
if (cardIdx !== null) moves.push({ cont: this.setCardConts[cardIdx], ...splitTarget(lowCX, 2, slotIdx) });
|
||||
});
|
||||
|
||||
if (!moves.length) { onComplete(); return; }
|
||||
|
||||
let done = 0;
|
||||
moves.forEach(({ cont, x, y }, i) => {
|
||||
cont.setDepth(D.panel + 10);
|
||||
this.tweens.add({
|
||||
targets: cont,
|
||||
x, y,
|
||||
duration: 320,
|
||||
delay: i * 45,
|
||||
ease: 'Power2.Out',
|
||||
onComplete: () => { if (++done === moves.length) onComplete(); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Chip balance ──────────────────────────────────────────────────────────
|
||||
|
|
@ -1184,6 +1227,10 @@ export default class PaiGowPokerGame extends Phaser.Scene {
|
|||
startComparingPhase() {
|
||||
this.animating = true;
|
||||
|
||||
// Advance phase so renderSeat renders everyone (including the human) in split layout,
|
||||
// preserving the animated split the player's cards just landed in.
|
||||
this.gs = { ...this.gs, phase: 'comparing' };
|
||||
|
||||
// Briefly reveal all AI splits (1.5s), then reveal dealer
|
||||
this.renderAll(); // shows AI split layouts
|
||||
|
||||
|
|
@ -1193,13 +1240,338 @@ export default class PaiGowPokerGame extends Phaser.Scene {
|
|||
// Resolve round
|
||||
this.gs = resolveRound(this.gs);
|
||||
this.renderAll();
|
||||
// Show comparison results seat by seat
|
||||
this.showComparisons(() => {
|
||||
// Modal comparison sequence, then chip animations
|
||||
this.startModalComparisons(() => {
|
||||
this.animateAllChips(() => {
|
||||
this.updateServerChips();
|
||||
this.time.delayedCall(800, () => {
|
||||
this.animating = false;
|
||||
this.showNextRoundPrompt();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Modal comparison ─────────────────────────────────────────────────────────
|
||||
|
||||
startModalComparisons(onComplete) {
|
||||
const MODAL_W = 1100;
|
||||
const MODAL_H = 530;
|
||||
const MODAL_X = CX - MODAL_W / 2; // 410
|
||||
const MODAL_TOP = 150;
|
||||
const DEALER_ROW_Y = 262;
|
||||
const PLAYER_ROW_Y = 510;
|
||||
const ARROW_Y = 393;
|
||||
const HIGH_CX = 650;
|
||||
const LOW_CX = 1270;
|
||||
const RESULT_CENTER_Y = MODAL_TOP + Math.round(MODAL_H / 2); // 415
|
||||
|
||||
// slot x for card i in a n-card spread (CARD_SPREAD_SPLIT=10)
|
||||
const slotX = (cx, count, i) => cx - ((count - 1) * CARD_SPREAD_SPLIT) / 2 + i * CARD_SPREAD_SPLIT;
|
||||
|
||||
this._modal = { MODAL_X, MODAL_TOP, MODAL_W, MODAL_H, DEALER_ROW_Y, PLAYER_ROW_Y,
|
||||
ARROW_Y, HIGH_CX, LOW_CX, RESULT_CENTER_Y, slotX };
|
||||
this._buildCompareModal();
|
||||
|
||||
// Fade in frame elements (arrows/rank text/result have their own reveal animations)
|
||||
const frameObjs = this._modal.frameObjects;
|
||||
for (const o of frameObjs) o.setAlpha(0);
|
||||
this.tweens.add({
|
||||
targets: frameObjs, alpha: 1, duration: 250, ease: 'Linear',
|
||||
onComplete: () => {
|
||||
this._animateDealerIntoModal(() => {
|
||||
const players = PLAY_ORDER
|
||||
.map(seat => this.gs.players[seat])
|
||||
.filter(p => p?.active);
|
||||
const processNext = idx => {
|
||||
if (idx >= players.length) {
|
||||
this._destroyModal();
|
||||
onComplete();
|
||||
return;
|
||||
}
|
||||
const isLast = idx === players.length - 1;
|
||||
this._comparePlayerInModal(players[idx], isLast, () => processNext(idx + 1));
|
||||
};
|
||||
processNext(0);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_buildCompareModal() {
|
||||
const { MODAL_X, MODAL_TOP, MODAL_W, MODAL_H, DEALER_ROW_Y, PLAYER_ROW_Y,
|
||||
HIGH_CX, LOW_CX, RESULT_CENTER_Y } = this._modal;
|
||||
const LABEL_STYLE = { fontFamily: '"Julius Sans One"', fontSize: '18px', color: '#d4a017' };
|
||||
const RANK_STYLE = { fontFamily: '"Julius Sans One"', fontSize: '16px', color: '#c0c0c0' };
|
||||
const HEADER_STYLE = { fontFamily: '"Julius Sans One"', fontSize: '14px', color: '#6a7484' };
|
||||
|
||||
const bg = this.add.graphics().setDepth(D.modal);
|
||||
bg.fillStyle(0x0a0f1a, 0.94);
|
||||
bg.fillRoundedRect(MODAL_X, MODAL_TOP, MODAL_W, MODAL_H, 12);
|
||||
bg.lineStyle(2, 0xd4a017, 0.8);
|
||||
bg.strokeRoundedRect(MODAL_X, MODAL_TOP, MODAL_W, MODAL_H, 12);
|
||||
|
||||
const sep = this.add.graphics().setDepth(D.modal + 1);
|
||||
sep.lineStyle(1, 0x3a4454, 0.6);
|
||||
sep.lineBetween(CX, MODAL_TOP + 20, CX, MODAL_TOP + MODAL_H - 20);
|
||||
|
||||
const dealerLabel = this.add.text(CX, MODAL_TOP + 25, 'DEALER', LABEL_STYLE)
|
||||
.setOrigin(0.5, 0).setDepth(D.modal + 1);
|
||||
|
||||
const hiLabel = this.add.text(HIGH_CX, MODAL_TOP + 28, 'HIGH HAND', HEADER_STYLE)
|
||||
.setOrigin(0.5, 0).setDepth(D.modal + 1);
|
||||
const loLabel = this.add.text(LOW_CX, MODAL_TOP + 28, 'LOW HAND', HEADER_STYLE)
|
||||
.setOrigin(0.5, 0).setDepth(D.modal + 1);
|
||||
|
||||
const dealerRankHi = this.add.text(HIGH_CX, DEALER_ROW_Y + CARD_H / 2 + 10, '', RANK_STYLE)
|
||||
.setOrigin(0.5, 0).setDepth(D.modal + 1);
|
||||
const dealerRankLo = this.add.text(LOW_CX, DEALER_ROW_Y + CARD_H / 2 + 10, '', RANK_STYLE)
|
||||
.setOrigin(0.5, 0).setDepth(D.modal + 1);
|
||||
|
||||
const playerLabel = this.add.text(CX, PLAYER_ROW_Y - CARD_H / 2 - 22, '', LABEL_STYLE)
|
||||
.setOrigin(0.5, 1).setDepth(D.modal + 1);
|
||||
const playerRankHi = this.add.text(HIGH_CX, PLAYER_ROW_Y + CARD_H / 2 + 10, '', RANK_STYLE)
|
||||
.setOrigin(0.5, 0).setDepth(D.modal + 1);
|
||||
const playerRankLo = this.add.text(LOW_CX, PLAYER_ROW_Y + CARD_H / 2 + 10, '', RANK_STYLE)
|
||||
.setOrigin(0.5, 0).setDepth(D.modal + 1);
|
||||
|
||||
const arrowHiG = this.add.graphics().setDepth(D.modal + 2).setAlpha(0);
|
||||
const arrowLoG = this.add.graphics().setDepth(D.modal + 2).setAlpha(0);
|
||||
|
||||
// resultTxt lives at modal center and flies to portrait after each player
|
||||
const resultTxt = this.add.text(CX, RESULT_CENTER_Y, '', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '72px', fontStyle: 'bold', color: '#f5d020',
|
||||
}).setOrigin(0.5).setDepth(D.modal + 3).setAlpha(0).setScale(0);
|
||||
|
||||
Object.assign(this._modal, {
|
||||
bg, sep, dealerLabel, hiLabel, loLabel,
|
||||
dealerRankHi, dealerRankLo, playerLabel, playerRankHi, playerRankLo,
|
||||
arrowHiG, arrowLoG, resultTxt,
|
||||
// frameObjects: modal chrome that hides/shows between players
|
||||
frameObjects: [bg, sep, dealerLabel, hiLabel, loLabel, dealerRankHi, dealerRankLo],
|
||||
// dynamicObjects: per-player state reset each round
|
||||
dynamicObjects: [playerLabel, playerRankHi, playerRankLo, arrowHiG, arrowLoG],
|
||||
// allObjects: everything for final destroy
|
||||
allObjects: [bg, sep, dealerLabel, hiLabel, loLabel,
|
||||
dealerRankHi, dealerRankLo, playerLabel, playerRankHi, playerRankLo,
|
||||
arrowHiG, arrowLoG, resultTxt],
|
||||
});
|
||||
}
|
||||
|
||||
_animateDealerIntoModal(onComplete) {
|
||||
const { DEALER_ROW_Y, HIGH_CX, LOW_CX, slotX } = this._modal;
|
||||
const cards = this.dealerCardGraphics;
|
||||
const dealer = this.gs.dealer;
|
||||
const hiCount = dealer.highHand.length; // 5
|
||||
const loCount = dealer.lowHand.length; // 2
|
||||
|
||||
this._dealerCardOrigins = cards.map(c => ({ x: c.x, y: c.y }));
|
||||
for (const c of cards) c.setDepth(D.modal + 5);
|
||||
|
||||
let done = 0;
|
||||
const total = cards.length;
|
||||
cards.forEach((card, i) => {
|
||||
const isHi = i < hiCount;
|
||||
const idx = isHi ? i : i - hiCount;
|
||||
const tx = isHi ? slotX(HIGH_CX, hiCount, idx) : slotX(LOW_CX, loCount, idx);
|
||||
this.tweens.add({
|
||||
targets: card, x: tx, y: DEALER_ROW_Y,
|
||||
duration: 350, delay: i * 50, ease: 'Power2.Out',
|
||||
onComplete: () => {
|
||||
done++;
|
||||
if (done === total) {
|
||||
const { dealerRankHi, dealerRankLo } = this._modal;
|
||||
dealerRankHi.setText(handName5(dealer.highEval));
|
||||
dealerRankLo.setText(dealer.lowEval?.name ?? '');
|
||||
onComplete();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_drawModalArrow(g, x, y, dealerWins) {
|
||||
g.clear();
|
||||
if (dealerWins) {
|
||||
g.fillStyle(0xff3a3a, 1); // bright red
|
||||
g.fillTriangle(x, y - 18, x - 14, y + 14, x + 14, y + 14); // apex up
|
||||
} else {
|
||||
g.fillStyle(0x00e676, 1); // bright green
|
||||
g.fillTriangle(x, y + 18, x - 14, y - 14, x + 14, y - 14); // apex down
|
||||
}
|
||||
}
|
||||
|
||||
_comparePlayerInModal(player, isLast, onComplete) {
|
||||
const { PLAYER_ROW_Y, HIGH_CX, LOW_CX, slotX,
|
||||
dealerRankHi, dealerRankLo, playerLabel, playerRankHi, playerRankLo,
|
||||
arrowHiG, arrowLoG, resultTxt, RESULT_CENTER_Y } = this._modal;
|
||||
const dealer = this.gs.dealer;
|
||||
const cards = this.seatCardGraphics[player.seat] ?? [];
|
||||
const hiCount = player.highHand.length; // 5
|
||||
const loCount = player.lowHand.length; // 2
|
||||
|
||||
// Reset per-player UI
|
||||
arrowHiG.setAlpha(0).setScale(1);
|
||||
arrowLoG.setAlpha(0).setScale(1);
|
||||
resultTxt.setAlpha(0).setScale(0).setText('').setPosition(CX, RESULT_CENTER_Y);
|
||||
playerRankHi.setText('').setColor('#c0c0c0').setAlpha(1);
|
||||
playerRankLo.setText('').setColor('#c0c0c0').setAlpha(1);
|
||||
dealerRankHi.setColor('#c0c0c0');
|
||||
dealerRankLo.setColor('#c0c0c0');
|
||||
playerLabel.setText(player.name.toUpperCase()).setColor('#d4a017').setAlpha(1);
|
||||
|
||||
this._playerCardOrigins = cards.map(c => ({ x: c.x, y: c.y }));
|
||||
for (const c of cards) c.setDepth(D.modal + 5);
|
||||
|
||||
let done = 0;
|
||||
const total = cards.length;
|
||||
if (total === 0) {
|
||||
this._playerCardOrigins = [];
|
||||
this._afterCardsIn(player, isLast, onComplete);
|
||||
return;
|
||||
}
|
||||
|
||||
cards.forEach((card, i) => {
|
||||
const isHi = i < hiCount;
|
||||
const idx = isHi ? i : i - hiCount;
|
||||
const tx = isHi ? slotX(HIGH_CX, hiCount, idx) : slotX(LOW_CX, loCount, idx);
|
||||
this.tweens.add({
|
||||
targets: card, x: tx, y: PLAYER_ROW_Y,
|
||||
duration: 280, delay: i * 45, ease: 'Power2.Out',
|
||||
onComplete: () => {
|
||||
done++;
|
||||
if (done === total) {
|
||||
playerRankHi.setText(handName5(player.highEval));
|
||||
playerRankLo.setText(player.lowEval?.name ?? '');
|
||||
this._afterCardsIn(player, isLast, onComplete);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_afterCardsIn(player, isLast, onComplete) {
|
||||
const dealer = this.gs.dealer;
|
||||
const { ARROW_Y, HIGH_CX, LOW_CX, RESULT_CENTER_Y, arrowHiG, arrowLoG, resultTxt,
|
||||
dealerRankHi, dealerRankLo, playerRankHi, playerRankLo } = this._modal;
|
||||
|
||||
const hiWinDealer = player.isFoul || compare5Card(player.highEval, dealer.highEval) <= 0;
|
||||
const loWinDealer = player.isFoul || compare2Card(player.lowEval, dealer.lowEval) <= 0;
|
||||
|
||||
// 500ms pause then HIGH arrow
|
||||
this.time.delayedCall(500, () => {
|
||||
this._drawModalArrow(arrowHiG, HIGH_CX, ARROW_Y, hiWinDealer);
|
||||
arrowHiG.setAlpha(0).setScale(0);
|
||||
this.tweens.add({ targets: arrowHiG, alpha: 1, scaleX: 1, scaleY: 1, duration: 200, ease: 'Back.easeOut' });
|
||||
if (hiWinDealer) { dealerRankHi.setColor('#ffe033'); playerRankHi.setColor('#ff3a3a'); }
|
||||
else { dealerRankHi.setColor('#ff3a3a'); playerRankHi.setColor('#ffe033'); }
|
||||
|
||||
// 1000ms pause then LOW arrow + verdict
|
||||
this.time.delayedCall(1000, () => {
|
||||
this._drawModalArrow(arrowLoG, LOW_CX, ARROW_Y, loWinDealer);
|
||||
arrowLoG.setAlpha(0).setScale(0);
|
||||
this.tweens.add({ targets: arrowLoG, alpha: 1, scaleX: 1, scaleY: 1, duration: 200, ease: 'Back.easeOut' });
|
||||
if (loWinDealer) { dealerRankLo.setColor('#ffe033'); playerRankLo.setColor('#ff3a3a'); }
|
||||
else { dealerRankLo.setColor('#ff3a3a'); playerRankLo.setColor('#ffe033'); }
|
||||
|
||||
// 500ms pause after low arrow before verdict appears
|
||||
this.time.delayedCall(500, () => {
|
||||
let verdict, color;
|
||||
if (player.result === 'win') { verdict = 'WIN'; color = '#ffe033'; }
|
||||
else if (player.result === 'push') { verdict = 'PUSH'; color = '#a0aabb'; }
|
||||
else { verdict = 'LOSE'; color = '#ff3a3a'; }
|
||||
|
||||
resultTxt.setText(verdict).setColor(color).setAlpha(0).setScale(0)
|
||||
.setPosition(CX, RESULT_CENTER_Y);
|
||||
this.tweens.add({ targets: resultTxt, alpha: 1, scaleX: 1, scaleY: 1,
|
||||
duration: 250, ease: 'Back.easeOut' });
|
||||
const sfx = player.result === 'win' ? SFX.CASINO_WIN
|
||||
: player.result === 'push' ? SFX.SQUASH
|
||||
: SFX.CASINO_LOSE;
|
||||
playSound(this, sfx);
|
||||
if (player.seat > 0) {
|
||||
const emotion = player.result === 'win' ? 'happy' : 'upset';
|
||||
this.portraits[player.seat]?.playEmotion?.(emotion);
|
||||
}
|
||||
});
|
||||
|
||||
// 500ms (low arrow) + 1200ms (verdict visible) = 1700ms before fly-back
|
||||
this.time.delayedCall(1700, () => {
|
||||
const cards = this.seatCardGraphics[player.seat] ?? [];
|
||||
const origins = this._playerCardOrigins;
|
||||
const pos = SEAT_POS[player.seat];
|
||||
|
||||
// Fade out modal frame + dynamic UI + dealer cards together (resultTxt stays)
|
||||
const fadeTargets = [
|
||||
...this._modal.frameObjects,
|
||||
...this._modal.dynamicObjects,
|
||||
...this.dealerCardGraphics,
|
||||
];
|
||||
this.tweens.add({ targets: fadeTargets, alpha: 0, duration: 250, ease: 'Linear' });
|
||||
|
||||
// Fly player cards back to seat simultaneously
|
||||
let done = 0;
|
||||
const afterFly = () => {
|
||||
// Once cards are home, fly resultTxt to player's portrait
|
||||
const destX = pos.portraitX ?? pos.x;
|
||||
const destY = pos.portraitY ?? pos.y;
|
||||
this.tweens.add({
|
||||
targets: resultTxt, x: destX, y: destY,
|
||||
scaleX: 0.45, scaleY: 0.45,
|
||||
duration: 1500, ease: 'Power2.In',
|
||||
onComplete: () => {
|
||||
this.tweens.add({ targets: resultTxt, alpha: 0, duration: 200, ease: 'Linear',
|
||||
onComplete: () => {
|
||||
if (isLast) {
|
||||
// No need to reopen — _destroyModal follows in processNext
|
||||
onComplete();
|
||||
return;
|
||||
}
|
||||
// Reopen modal frame + dealer cards for next player
|
||||
const reshow = [...this._modal.frameObjects, ...this.dealerCardGraphics];
|
||||
for (const o of reshow) o.setAlpha(0);
|
||||
this.tweens.add({ targets: reshow, alpha: 1, duration: 250, ease: 'Linear',
|
||||
onComplete: () => onComplete() });
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!cards.length) {
|
||||
afterFly();
|
||||
return;
|
||||
}
|
||||
cards.forEach((card, i) => {
|
||||
const { x, y } = origins[i] ?? { x: card.x, y: card.y };
|
||||
this.tweens.add({
|
||||
targets: card, x, y, duration: 260, delay: i * 30, ease: 'Power2.In',
|
||||
onComplete: () => {
|
||||
done++;
|
||||
if (done === cards.length) {
|
||||
for (const c of cards) c.setDepth(D.cards);
|
||||
afterFly();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_destroyModal() {
|
||||
if (!this._modal) return;
|
||||
for (const o of this._modal.allObjects) { if (o?.destroy) o.destroy(); }
|
||||
// Dealer card containers were faded out in the last player's close; destroy them so
|
||||
// renderAll can recreate them cleanly for the next round.
|
||||
for (const c of this.dealerCardGraphics) { if (c?.destroy) c.destroy(); }
|
||||
this.dealerCardGraphics = [];
|
||||
delete this._modal;
|
||||
delete this._dealerCardOrigins;
|
||||
delete this._playerCardOrigins;
|
||||
}
|
||||
|
||||
showComparisons(onComplete) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue