From d6588405a48943563ed25bc3ad4da3f89153092e Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Tue, 19 May 2026 20:01:22 -0600 Subject: [PATCH] enhance uno game animations and emotional feedback - Add opponent emotion reactions (upset/happy) for specific game events like skips, draws, and wild card plays - Differentiate animation durations and effects between local player and opponents - Implement showcase animation for opponent's played cards with a grow/shrink effect - Adjust card fly and stagger timings for opponent animations to improve visual clarity --- public/src/games/uno/UnoGame.js | 42 ++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/public/src/games/uno/UnoGame.js b/public/src/games/uno/UnoGame.js index e9e17dc..2ada24b 100644 --- a/public/src/games/uno/UnoGame.js +++ b/public/src/games/uno/UnoGame.js @@ -1057,6 +1057,7 @@ export default class UnoGame extends Phaser.Scene { const banner = this.formatChallengeBanner(last); this.gs = after; this.renderAll(); + this.handleLogEffects(before, after); if (banner) { this.showBanner(banner); this.time.delayedCall(1300, () => this.hideBanner()); @@ -1086,14 +1087,23 @@ export default class UnoGame extends Phaser.Scene { for (let i = oldLen; i < after.log.length; i++) { const e = after.log[i]; if (e.kind === 'skip') { + this.opponentPortraits[e.seat]?.playEmotion('upset'); this.showBanner(`${this.opponentName(e.seat)} is skipped!`); this.time.delayedCall(900, () => this.hideBanner()); } else if (e.kind === 'reverse') { this.showBanner('Direction reversed.'); this.time.delayedCall(900, () => this.hideBanner()); } else if (e.kind === 'draw2') { + this.opponentPortraits[e.seat]?.playEmotion('upset'); this.showBanner(`${this.opponentName(e.seat)} draws 2 and is skipped.`); this.time.delayedCall(1100, () => this.hideBanner()); + } else if (e.kind === 'wild4Accept' || e.kind === 'wild4ChallengeLose' || e.kind === 'wild4ChallengeWin') { + this.opponentPortraits[e.seat]?.playEmotion('upset'); + } else if (e.kind === 'play') { + const playedCard = before.players[e.seat]?.hand?.find((c) => c.id === e.cardId); + if (playedCard?.kind === 'wild4') { + this.opponentPortraits[e.seat]?.playEmotion('happy'); + } } else if (e.kind === 'win') { // handled by endGame } @@ -1105,6 +1115,7 @@ export default class UnoGame extends Phaser.Scene { animatePlayCard(seat, card, onComplete) { const layout = slotLayout(this.slotForSeat[seat], this.gs.players.length); const handFaceUp = layout.handFaceUp; + const isOpponent = seat !== 0; const fromSprite = this.cardObjs.get(`hand-${seat}-${card.id}`); let sprite; if (fromSprite) { @@ -1124,16 +1135,25 @@ export default class UnoGame extends Phaser.Scene { targets: sprite, x: DISCARD_POS.x, y: DISCARD_POS.y, rotation: 0, - duration: 360, + duration: isOpponent ? 600 : 360, ease: 'Cubic.easeOut', onComplete: () => { if (!handFaceUp) this.renderUnoCardFace(sprite, card, true); sprite.setDepth(D.card); - // Small bounce. - this.tweens.add({ - targets: sprite, scaleX: 1.08, scaleY: 1.08, yoyo: true, duration: 90, - onComplete: () => onComplete && onComplete(), - }); + if (isOpponent) { + // Showcase: grow then shrink over 1.5 s to spotlight the played card. + this.tweens.add({ + targets: sprite, scaleX: 1.3, scaleY: 1.3, + duration: 750, ease: 'Sine.easeInOut', yoyo: true, + onComplete: () => onComplete && onComplete(), + }); + } else { + // Small bounce for local player. + this.tweens.add({ + targets: sprite, scaleX: 1.08, scaleY: 1.08, yoyo: true, duration: 90, + onComplete: () => onComplete && onComplete(), + }); + } }, }); } @@ -1157,7 +1177,7 @@ export default class UnoGame extends Phaser.Scene { targets: sprite, x: dest.x, y: dest.y, rotation: 0, - duration: 400, + duration: seat !== 0 ? 650 : 400, ease: 'Cubic.easeIn', onComplete: () => onComplete && onComplete(), }); @@ -1179,6 +1199,9 @@ export default class UnoGame extends Phaser.Scene { animateBatchDraw(seat, count, onComplete) { if (count <= 0) { onComplete && onComplete(); return; } const layout = slotLayout(this.slotForSeat[seat], this.gs.players.length); + const isOpponent = seat !== 0; + const flyDuration = isOpponent ? 480 : 320; + const stagger = isOpponent ? 170 : 110; let remaining = count; const fire = (i) => { const sprite = this.makeUnoCardSprite(null, DRAW_POS.x, DRAW_POS.y, { @@ -1190,7 +1213,7 @@ export default class UnoGame extends Phaser.Scene { this.tweens.add({ targets: sprite, x: layout.handCenter.x, y: layout.handCenter.y, - duration: 320, + duration: flyDuration, ease: 'Cubic.easeIn', onComplete: () => { remaining -= 1; @@ -1199,7 +1222,7 @@ export default class UnoGame extends Phaser.Scene { }); }; for (let i = 0; i < count; i++) { - this.time.delayedCall(i * 110, () => fire(i)); + this.time.delayedCall(i * stagger, () => fire(i)); } } @@ -1236,6 +1259,7 @@ export default class UnoGame extends Phaser.Scene { }).setOrigin(0.5).setDepth(D.banner + 2).setScale(0.2); this.transientObjs.push(t); playSound(this, SFX.CASINO_WIN); + this.opponentPortraits[seat]?.playEmotion('happy'); this.tweens.add({ targets: t, scaleX: 1, scaleY: 1, duration: 280, ease: 'Back.easeOut', onComplete: () => {