feat(blokus): animate AI piece placement from portrait to board
Introduce a visual transition where AI pieces fly from their opponent portrait to the board upon placement. Human player placements remain instant with a flash effect. Implemented animateAIPiece to handle the tweening and temporary graphics rendering.
This commit is contained in:
parent
5c88ab7986
commit
2f7b2e183e
|
|
@ -392,7 +392,7 @@ export default class BlokusGame extends Phaser.Scene {
|
||||||
if (this.gameOver) return;
|
if (this.gameOver) return;
|
||||||
const mv = chooseMove(this.gs, seat, this.skillForSeat(seat));
|
const mv = chooseMove(this.gs, seat, this.skillForSeat(seat));
|
||||||
if (!mv) { this.gs = passTurn(this.gs, seat); this.animating = false; this.driveTurn(); return; }
|
if (!mv) { this.gs = passTurn(this.gs, seat); this.animating = false; this.driveTurn(); return; }
|
||||||
this.placePiece(seat, mv, () => { this.animating = false; this.driveTurn(); });
|
this.placePiece(seat, mv, () => { this.animating = false; this.driveTurn(); }, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
humanPass() {
|
humanPass() {
|
||||||
|
|
@ -402,23 +402,80 @@ export default class BlokusGame extends Phaser.Scene {
|
||||||
this.driveTurn();
|
this.driveTurn();
|
||||||
}
|
}
|
||||||
|
|
||||||
placePiece(seat, move, done) {
|
placePiece(seat, move, done, animateFromPortrait = false) {
|
||||||
const next = applyPlacement(this.gs, seat, move.pieceId, move.oriIdx, move.anchorR, move.anchorC);
|
const next = applyPlacement(this.gs, seat, move.pieceId, move.oriIdx, move.anchorR, move.anchorC);
|
||||||
if (next === this.gs) { done(); return; } // illegal — should not happen
|
if (next === this.gs) { done(); return; } // illegal — should not happen
|
||||||
this.gs = next;
|
this.gs = next;
|
||||||
playSound(this, SFX.CARD_PLACE);
|
playSound(this, SFX.CARD_PLACE);
|
||||||
|
|
||||||
|
if (animateFromPortrait && seat > 0) {
|
||||||
|
this.animateAIPiece(seat, move, done);
|
||||||
|
} else {
|
||||||
this.renderPieces();
|
this.renderPieces();
|
||||||
this.renderTray();
|
this.renderTray();
|
||||||
this.renderScores();
|
this.renderScores();
|
||||||
|
|
||||||
// Brief pop on the freshly placed cells.
|
// Brief pop on the freshly placed cells.
|
||||||
const cells = placementCells(move.pieceId, move.oriIdx, move.anchorR, move.anchorC);
|
const cells = placementCells(move.pieceId, move.oriIdx, move.anchorR, move.anchorC);
|
||||||
const flash = this.add.graphics().setDepth(DEPTH.ghost);
|
const flash = this.add.graphics().setDepth(DEPTH.ghost);
|
||||||
for (const [r, c] of cells) this.fillCell(flash, r, c, 0xffffff, 0.5);
|
for (const [r, c] of cells) this.fillCell(flash, r, c, 0xffffff, 0.5);
|
||||||
this.tweens.add({ targets: flash, alpha: 0, duration: 280, ease: 'Quad.easeOut', onComplete: () => flash.destroy() });
|
this.tweens.add({ targets: flash, alpha: 0, duration: 280, ease: 'Quad.easeOut', onComplete: () => flash.destroy() });
|
||||||
|
|
||||||
this.time.delayedCall(180, done);
|
this.time.delayedCall(180, done);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animateAIPiece(seat, move, done) {
|
||||||
|
const cells = placementCells(move.pieceId, move.oriIdx, move.anchorR, move.anchorC);
|
||||||
|
const col = COLOR_PALETTE[seat];
|
||||||
|
|
||||||
|
// Calculate board center of the piece
|
||||||
|
let avgR = 0, avgC = 0;
|
||||||
|
for (const [r, c] of cells) { avgR += r; avgC += c; }
|
||||||
|
avgR /= cells.length;
|
||||||
|
avgC /= cells.length;
|
||||||
|
const destX = GRID_X + avgC * CELL;
|
||||||
|
const destY = GRID_Y + avgR * CELL;
|
||||||
|
|
||||||
|
// Portrait position for this opponent
|
||||||
|
const panelH = 192;
|
||||||
|
const portraitYOffset = -panelH / 2 + 60 + 14;
|
||||||
|
const panelY = 221 + (seat - 1) * (panelH + 10);
|
||||||
|
const startX = SIDE_X;
|
||||||
|
const startY = panelY + portraitYOffset + 200;
|
||||||
|
|
||||||
|
// Create flying piece graphics — draw centered at (0,0) so we can tween position
|
||||||
|
const flyGfx = this.add.graphics().setDepth(DEPTH.piece + 1);
|
||||||
|
flyGfx.fillStyle(col.hex, 1);
|
||||||
|
flyGfx.lineStyle(2, col.dark, 1);
|
||||||
|
for (const [r, c] of cells) {
|
||||||
|
const cx = (c - avgC) * CELL;
|
||||||
|
const cy = (r - avgR) * CELL;
|
||||||
|
flyGfx.fillRoundedRect(cx - CELL / 2 + 2, cy - CELL / 2 + 2, CELL - 4, CELL - 4, 6);
|
||||||
|
flyGfx.strokeRoundedRect(cx - CELL / 2 + 2, cy - CELL / 2 + 2, CELL - 4, CELL - 4, 6);
|
||||||
|
}
|
||||||
|
flyGfx.x = startX;
|
||||||
|
flyGfx.y = startY;
|
||||||
|
|
||||||
|
// Animate from portrait to board
|
||||||
|
const duration = 400;
|
||||||
|
this.tweens.add({
|
||||||
|
targets: flyGfx,
|
||||||
|
x: destX,
|
||||||
|
y: destY,
|
||||||
|
duration,
|
||||||
|
ease: 'Quad.easeInOut',
|
||||||
|
onComplete: () => {
|
||||||
|
flyGfx.destroy();
|
||||||
|
this.renderPieces();
|
||||||
|
this.renderTray();
|
||||||
|
this.renderScores();
|
||||||
|
// Brief pop on the freshly placed cells.
|
||||||
|
const flash = this.add.graphics().setDepth(DEPTH.ghost);
|
||||||
|
for (const [r, c] of cells) this.fillCell(flash, r, c, 0xffffff, 0.5);
|
||||||
|
this.tweens.add({ targets: flash, alpha: 0, duration: 280, ease: 'Quad.easeOut', onComplete: () => flash.destroy() });
|
||||||
|
this.time.delayedCall(180, done);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateButtons() {
|
updateButtons() {
|
||||||
const human = this.isHumanTurn();
|
const human = this.isHumanTurn();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue