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
This commit is contained in:
parent
85dff94613
commit
d6588405a4
|
|
@ -1057,6 +1057,7 @@ export default class UnoGame extends Phaser.Scene {
|
||||||
const banner = this.formatChallengeBanner(last);
|
const banner = this.formatChallengeBanner(last);
|
||||||
this.gs = after;
|
this.gs = after;
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
|
this.handleLogEffects(before, after);
|
||||||
if (banner) {
|
if (banner) {
|
||||||
this.showBanner(banner);
|
this.showBanner(banner);
|
||||||
this.time.delayedCall(1300, () => this.hideBanner());
|
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++) {
|
for (let i = oldLen; i < after.log.length; i++) {
|
||||||
const e = after.log[i];
|
const e = after.log[i];
|
||||||
if (e.kind === 'skip') {
|
if (e.kind === 'skip') {
|
||||||
|
this.opponentPortraits[e.seat]?.playEmotion('upset');
|
||||||
this.showBanner(`${this.opponentName(e.seat)} is skipped!`);
|
this.showBanner(`${this.opponentName(e.seat)} is skipped!`);
|
||||||
this.time.delayedCall(900, () => this.hideBanner());
|
this.time.delayedCall(900, () => this.hideBanner());
|
||||||
} else if (e.kind === 'reverse') {
|
} else if (e.kind === 'reverse') {
|
||||||
this.showBanner('Direction reversed.');
|
this.showBanner('Direction reversed.');
|
||||||
this.time.delayedCall(900, () => this.hideBanner());
|
this.time.delayedCall(900, () => this.hideBanner());
|
||||||
} else if (e.kind === 'draw2') {
|
} else if (e.kind === 'draw2') {
|
||||||
|
this.opponentPortraits[e.seat]?.playEmotion('upset');
|
||||||
this.showBanner(`${this.opponentName(e.seat)} draws 2 and is skipped.`);
|
this.showBanner(`${this.opponentName(e.seat)} draws 2 and is skipped.`);
|
||||||
this.time.delayedCall(1100, () => this.hideBanner());
|
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') {
|
} else if (e.kind === 'win') {
|
||||||
// handled by endGame
|
// handled by endGame
|
||||||
}
|
}
|
||||||
|
|
@ -1105,6 +1115,7 @@ export default class UnoGame extends Phaser.Scene {
|
||||||
animatePlayCard(seat, card, onComplete) {
|
animatePlayCard(seat, card, onComplete) {
|
||||||
const layout = slotLayout(this.slotForSeat[seat], this.gs.players.length);
|
const layout = slotLayout(this.slotForSeat[seat], this.gs.players.length);
|
||||||
const handFaceUp = layout.handFaceUp;
|
const handFaceUp = layout.handFaceUp;
|
||||||
|
const isOpponent = seat !== 0;
|
||||||
const fromSprite = this.cardObjs.get(`hand-${seat}-${card.id}`);
|
const fromSprite = this.cardObjs.get(`hand-${seat}-${card.id}`);
|
||||||
let sprite;
|
let sprite;
|
||||||
if (fromSprite) {
|
if (fromSprite) {
|
||||||
|
|
@ -1124,16 +1135,25 @@ export default class UnoGame extends Phaser.Scene {
|
||||||
targets: sprite,
|
targets: sprite,
|
||||||
x: DISCARD_POS.x, y: DISCARD_POS.y,
|
x: DISCARD_POS.x, y: DISCARD_POS.y,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
duration: 360,
|
duration: isOpponent ? 600 : 360,
|
||||||
ease: 'Cubic.easeOut',
|
ease: 'Cubic.easeOut',
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
if (!handFaceUp) this.renderUnoCardFace(sprite, card, true);
|
if (!handFaceUp) this.renderUnoCardFace(sprite, card, true);
|
||||||
sprite.setDepth(D.card);
|
sprite.setDepth(D.card);
|
||||||
// Small bounce.
|
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({
|
this.tweens.add({
|
||||||
targets: sprite, scaleX: 1.08, scaleY: 1.08, yoyo: true, duration: 90,
|
targets: sprite, scaleX: 1.08, scaleY: 1.08, yoyo: true, duration: 90,
|
||||||
onComplete: () => onComplete && onComplete(),
|
onComplete: () => onComplete && onComplete(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1157,7 +1177,7 @@ export default class UnoGame extends Phaser.Scene {
|
||||||
targets: sprite,
|
targets: sprite,
|
||||||
x: dest.x, y: dest.y,
|
x: dest.x, y: dest.y,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
duration: 400,
|
duration: seat !== 0 ? 650 : 400,
|
||||||
ease: 'Cubic.easeIn',
|
ease: 'Cubic.easeIn',
|
||||||
onComplete: () => onComplete && onComplete(),
|
onComplete: () => onComplete && onComplete(),
|
||||||
});
|
});
|
||||||
|
|
@ -1179,6 +1199,9 @@ export default class UnoGame extends Phaser.Scene {
|
||||||
animateBatchDraw(seat, count, onComplete) {
|
animateBatchDraw(seat, count, onComplete) {
|
||||||
if (count <= 0) { onComplete && onComplete(); return; }
|
if (count <= 0) { onComplete && onComplete(); return; }
|
||||||
const layout = slotLayout(this.slotForSeat[seat], this.gs.players.length);
|
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;
|
let remaining = count;
|
||||||
const fire = (i) => {
|
const fire = (i) => {
|
||||||
const sprite = this.makeUnoCardSprite(null, DRAW_POS.x, DRAW_POS.y, {
|
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({
|
this.tweens.add({
|
||||||
targets: sprite,
|
targets: sprite,
|
||||||
x: layout.handCenter.x, y: layout.handCenter.y,
|
x: layout.handCenter.x, y: layout.handCenter.y,
|
||||||
duration: 320,
|
duration: flyDuration,
|
||||||
ease: 'Cubic.easeIn',
|
ease: 'Cubic.easeIn',
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
remaining -= 1;
|
remaining -= 1;
|
||||||
|
|
@ -1199,7 +1222,7 @@ export default class UnoGame extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
for (let i = 0; i < count; i++) {
|
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);
|
}).setOrigin(0.5).setDepth(D.banner + 2).setScale(0.2);
|
||||||
this.transientObjs.push(t);
|
this.transientObjs.push(t);
|
||||||
playSound(this, SFX.CASINO_WIN);
|
playSound(this, SFX.CASINO_WIN);
|
||||||
|
this.opponentPortraits[seat]?.playEmotion('happy');
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: t, scaleX: 1, scaleY: 1, duration: 280, ease: 'Back.easeOut',
|
targets: t, scaleX: 1, scaleY: 1, duration: 280, ease: 'Back.easeOut',
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue