feat: add piece rotation/flip animations and UI sound feedback
Enhance Genius Square and Katamino with: - Smooth piece rotation and flip animations (scale + rotate tweens) - Pulsing outline glow on ghost piece preview - New UI sound effects: pick, activate, flip, place, chime - Music player support in both games - Win chime with delayed overlay for better feedback - Move Reset/Leave buttons to bottom in Katamino
This commit is contained in:
parent
762792f9f5
commit
b7821c3bd8
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 266 KiB |
Binary file not shown.
|
|
@ -84,6 +84,9 @@ export default class GeniusSquareGame extends Phaser.Scene {
|
|||
|
||||
this.humanGridGfx = null;
|
||||
this.aiGridGfx = null;
|
||||
this._pulseGfx = null;
|
||||
this._ghostAnimContainer = null;
|
||||
this._ghostAnim = null;
|
||||
this.traySlots = []; // [{ container, pieceId }]
|
||||
this.opponentPortrait = null;
|
||||
}
|
||||
|
|
@ -124,6 +127,43 @@ export default class GeniusSquareGame extends Phaser.Scene {
|
|||
this._startCountdown();
|
||||
}
|
||||
|
||||
update(time) {
|
||||
if (!this._pulseGfx || !this.ghostCells || !this.selectedPiece) {
|
||||
this._pulseGfx?.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const cells = this.ghostCells;
|
||||
const pieceId = this.selectedPiece.pieceId;
|
||||
const alpha = 0.35 + 0.65 * Math.abs(Math.sin(time * 0.004));
|
||||
const allValid = cells.every(([r, c]) =>
|
||||
r >= 0 && r < 6 && c >= 0 && c < 6 && this.humanBoard[r * 6 + c] === null
|
||||
);
|
||||
const color = allValid ? (PIECE_COLORS[pieceId] ?? 0xffffff) : 0xff4444;
|
||||
const ghostSet = new Set(cells.map(([r, c]) => `${r},${c}`));
|
||||
const OFF = 4;
|
||||
|
||||
this._pulseGfx.clear();
|
||||
this._pulseGfx.lineStyle(5, color, alpha);
|
||||
|
||||
for (const [r, c] of cells) {
|
||||
const x0 = HGX + c * CELL, y0 = HGY + r * CELL;
|
||||
const x1 = x0 + CELL, y1 = y0 + CELL;
|
||||
if (!ghostSet.has(`${r - 1},${c}`)) {
|
||||
this._pulseGfx.beginPath(); this._pulseGfx.moveTo(x0 - OFF, y0 - OFF); this._pulseGfx.lineTo(x1 + OFF, y0 - OFF); this._pulseGfx.strokePath();
|
||||
}
|
||||
if (!ghostSet.has(`${r + 1},${c}`)) {
|
||||
this._pulseGfx.beginPath(); this._pulseGfx.moveTo(x0 - OFF, y1 + OFF); this._pulseGfx.lineTo(x1 + OFF, y1 + OFF); this._pulseGfx.strokePath();
|
||||
}
|
||||
if (!ghostSet.has(`${r},${c - 1}`)) {
|
||||
this._pulseGfx.beginPath(); this._pulseGfx.moveTo(x0 - OFF, y0 - OFF); this._pulseGfx.lineTo(x0 - OFF, y1 + OFF); this._pulseGfx.strokePath();
|
||||
}
|
||||
if (!ghostSet.has(`${r},${c + 1}`)) {
|
||||
this._pulseGfx.beginPath(); this._pulseGfx.moveTo(x1 + OFF, y0 - OFF); this._pulseGfx.lineTo(x1 + OFF, y1 + OFF); this._pulseGfx.strokePath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scene construction ────────────────────────────────────────────────────────
|
||||
|
||||
_buildBackground() {
|
||||
|
|
@ -165,6 +205,7 @@ export default class GeniusSquareGame extends Phaser.Scene {
|
|||
_buildGridGraphics() {
|
||||
this.humanGridGfx = this.add.graphics().setDepth(D.grid);
|
||||
this.aiGridGfx = this.add.graphics().setDepth(D.grid);
|
||||
this._pulseGfx = this.add.graphics().setDepth(D.grid + 1);
|
||||
}
|
||||
|
||||
_buildLabels() {
|
||||
|
|
@ -358,6 +399,7 @@ export default class GeniusSquareGame extends Phaser.Scene {
|
|||
}
|
||||
this.selectedPiece = { pieceId, oriIdx: 0 };
|
||||
this.ghostCells = null;
|
||||
playSound(this, SFX.UI_PICK);
|
||||
this._renderTray();
|
||||
this._renderHumanGrid();
|
||||
}
|
||||
|
|
@ -373,16 +415,130 @@ export default class GeniusSquareGame extends Phaser.Scene {
|
|||
|
||||
_rotate() {
|
||||
if (!this.selectedPiece || this.gameOver) return;
|
||||
playSound(this, SFX.UI_ACTIVATE);
|
||||
this._cancelGhostAnim();
|
||||
if (!this.ghostCells) {
|
||||
this.selectedPiece.oriIdx = rotateOri(this.selectedPiece.pieceId, this.selectedPiece.oriIdx);
|
||||
this._updateGhostAt(this._lastHoverR, this._lastHoverC);
|
||||
this._renderHumanGrid();
|
||||
return;
|
||||
}
|
||||
this._startGhostAnim('rotate');
|
||||
}
|
||||
|
||||
_flip() {
|
||||
if (!this.selectedPiece || this.gameOver) return;
|
||||
playSound(this, SFX.UI_FLIP);
|
||||
this._cancelGhostAnim();
|
||||
if (!this.ghostCells) {
|
||||
this.selectedPiece.oriIdx = flipOri(this.selectedPiece.pieceId, this.selectedPiece.oriIdx);
|
||||
this._updateGhostAt(this._lastHoverR, this._lastHoverC);
|
||||
this._renderHumanGrid();
|
||||
return;
|
||||
}
|
||||
this._startGhostAnim('flip');
|
||||
}
|
||||
|
||||
_cancelGhostAnim() {
|
||||
if (!this._ghostAnimContainer) return;
|
||||
this.tweens.killTweensOf(this._ghostAnimContainer);
|
||||
this._ghostAnimContainer.destroy(true);
|
||||
this._ghostAnimContainer = null;
|
||||
this._ghostAnim = null;
|
||||
this._updateGhostAt(this._lastHoverR, this._lastHoverC);
|
||||
}
|
||||
|
||||
_startGhostAnim(type) {
|
||||
const cells = this.ghostCells;
|
||||
const pieceId = this.selectedPiece.pieceId;
|
||||
const hoverR = this._lastHoverR;
|
||||
const hoverC = this._lastHoverC;
|
||||
const n = cells.length;
|
||||
const cr = cells.reduce((s, [r]) => s + r, 0) / n;
|
||||
const cc = cells.reduce((s, [, c]) => s + c, 0) / n;
|
||||
|
||||
const gfx = this.add.graphics();
|
||||
this._fillGhostLocal(gfx, cells, cr, cc);
|
||||
|
||||
const pivotX = HGX + (cc + 0.5) * CELL;
|
||||
const pivotY = HGY + (cr + 0.5) * CELL;
|
||||
const container = this.add.container(pivotX, pivotY, [gfx]).setDepth(D.piece + 1);
|
||||
this._ghostAnimContainer = container;
|
||||
|
||||
// Suppress normal ghost & pulse while animating
|
||||
this.ghostCells = null;
|
||||
this._pulseGfx?.clear();
|
||||
this._renderHumanGrid();
|
||||
|
||||
const HALF = 80;
|
||||
|
||||
const finish = () => {
|
||||
container.destroy(true);
|
||||
this._ghostAnimContainer = null;
|
||||
this._ghostAnim = null;
|
||||
this._updateGhostAt(this._lastHoverR, this._lastHoverC);
|
||||
this._renderHumanGrid();
|
||||
};
|
||||
|
||||
const phase2 = () => {
|
||||
const oriCells = ORIENTATIONS[this.selectedPiece.pieceId][this.selectedPiece.oriIdx];
|
||||
const [pinDr, pinDc] = _centerPin(oriCells);
|
||||
const newCells = (hoverR >= 0 && hoverC >= 0)
|
||||
? absoluteCells(oriCells, hoverR - pinDr, hoverC - pinDc)
|
||||
: null;
|
||||
if (!newCells) { finish(); return; }
|
||||
|
||||
const nn = newCells.length;
|
||||
const ncr = newCells.reduce((s, [r]) => s + r, 0) / nn;
|
||||
const ncc = newCells.reduce((s, [, c]) => s + c, 0) / nn;
|
||||
gfx.clear();
|
||||
this._fillGhostLocal(gfx, newCells, ncr, ncc);
|
||||
container.setPosition(HGX + (ncc + 0.5) * CELL, HGY + (ncr + 0.5) * CELL);
|
||||
if (type === 'rotate') container.angle = -90;
|
||||
|
||||
this._ghostAnim = this.tweens.add({
|
||||
targets: container,
|
||||
angle: 0, scaleX: 1, scaleY: 1,
|
||||
duration: HALF,
|
||||
ease: 'Power2.easeOut',
|
||||
onComplete: finish,
|
||||
});
|
||||
};
|
||||
|
||||
const midpoint = () => {
|
||||
if (type === 'rotate') {
|
||||
this.selectedPiece.oriIdx = rotateOri(pieceId, this.selectedPiece.oriIdx);
|
||||
} else {
|
||||
this.selectedPiece.oriIdx = flipOri(pieceId, this.selectedPiece.oriIdx);
|
||||
}
|
||||
phase2();
|
||||
};
|
||||
|
||||
const phase1Props = type === 'rotate'
|
||||
? { angle: 90, scaleX: 0, scaleY: 0 }
|
||||
: { scaleX: 0 };
|
||||
|
||||
this._ghostAnim = this.tweens.add({
|
||||
targets: container,
|
||||
...phase1Props,
|
||||
duration: HALF,
|
||||
ease: 'Power2.easeIn',
|
||||
onComplete: midpoint,
|
||||
});
|
||||
}
|
||||
|
||||
_fillGhostLocal(gfx, cells, cr, cc) {
|
||||
const pieceId = this.selectedPiece?.pieceId;
|
||||
const color = PIECE_COLORS[pieceId] ?? 0xffffff;
|
||||
const allValid = cells.every(([r, c]) =>
|
||||
r >= 0 && r < 6 && c >= 0 && c < 6 && this.humanBoard[r * 6 + c] === null
|
||||
);
|
||||
gfx.fillStyle(color, allValid ? 0.55 : 0.2);
|
||||
for (const [r, c] of cells) {
|
||||
const lx = (c - cc) * CELL - CELL / 2;
|
||||
const ly = (r - cr) * CELL - CELL / 2;
|
||||
gfx.fillRoundedRect(lx + 3, ly + 3, CELL - 6, CELL - 6, 7);
|
||||
}
|
||||
}
|
||||
|
||||
_updateGhostAt(r, c) {
|
||||
|
|
@ -398,6 +554,7 @@ export default class GeniusSquareGame extends Phaser.Scene {
|
|||
const c = Math.floor((ptr.worldX - HGX) / CELL);
|
||||
this._lastHoverR = r;
|
||||
this._lastHoverC = c;
|
||||
if (this._ghostAnimContainer) return; // let animation finish
|
||||
this._updateGhostAt(r, c);
|
||||
this._renderHumanGrid();
|
||||
}
|
||||
|
|
@ -413,6 +570,7 @@ export default class GeniusSquareGame extends Phaser.Scene {
|
|||
|
||||
// Click on a placed piece → lift it back to the tray
|
||||
if (cellVal && cellVal !== 'blocked') {
|
||||
this._cancelGhostAnim();
|
||||
this._liftPiece(cellVal);
|
||||
return;
|
||||
}
|
||||
|
|
@ -426,7 +584,7 @@ export default class GeniusSquareGame extends Phaser.Scene {
|
|||
const { pieceId } = this.selectedPiece;
|
||||
this.humanBoard = placePiece(this.humanBoard, this.ghostCells, pieceId);
|
||||
this.placedPieces.add(pieceId);
|
||||
playSound(this, SFX.CARD_PLACE);
|
||||
playSound(this, SFX.UI_PLACE);
|
||||
this._clearSelection();
|
||||
this._renderHumanGrid();
|
||||
this._renderTray();
|
||||
|
|
@ -557,6 +715,7 @@ export default class GeniusSquareGame extends Phaser.Scene {
|
|||
this.gameOver = true;
|
||||
this._stopAI();
|
||||
this.opponentPortrait?.playEmotion('upset');
|
||||
playSound(this, SFX.UI_CHIME);
|
||||
playSound(this, SFX.VICTORY_SHORT);
|
||||
this._recordResult('win');
|
||||
this._showOverlay(true);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as Phaser from 'phaser';
|
||||
import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
import {
|
||||
ROWS, PIECES, PIECE_COLORS, ORIENTATIONS,
|
||||
|
|
@ -75,13 +76,21 @@ export default class KataminoGame extends Phaser.Scene {
|
|||
this._screenContainer = null;
|
||||
this._maskGfx = null;
|
||||
this._boardGfx = null;
|
||||
this._pulseGfx = null;
|
||||
this._ghostAnimContainer = null;
|
||||
this._ghostAnim = null;
|
||||
this._traySlots = [];
|
||||
|
||||
// Input handler refs for cleanup
|
||||
this._inputHandlers = [];
|
||||
}
|
||||
|
||||
create() {
|
||||
async create() {
|
||||
try {
|
||||
const music = this.cache.json.get('music');
|
||||
if (music?.tracks) new MusicPlayer(this, music.tracks);
|
||||
} catch (_) { /* optional */ }
|
||||
|
||||
this._bank = this.cache.json.get('katamino');
|
||||
|
||||
this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x0f0d0a).setDepth(D.bg);
|
||||
|
|
@ -94,6 +103,44 @@ export default class KataminoGame extends Phaser.Scene {
|
|||
this._showPentaSelect();
|
||||
}
|
||||
|
||||
update(time) {
|
||||
if (!this._pulseGfx || this._screen !== 'play' || !this._ghostCells || !this._selectedPiece) {
|
||||
this._pulseGfx?.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const cells = this._ghostCells;
|
||||
const pieceId = this._selectedPiece.pieceId;
|
||||
const alpha = 0.35 + 0.65 * Math.abs(Math.sin(time * 0.004));
|
||||
const allValid = cells.every(([r, c]) =>
|
||||
r >= 0 && r < ROWS && c >= 0 && c < this._cols && this._board[r * this._cols + c] === null
|
||||
);
|
||||
const color = allValid ? (PIECE_COLORS[pieceId] ?? 0xffffff) : 0xff4444;
|
||||
const ghostSet = new Set(cells.map(([r, c]) => `${r},${c}`));
|
||||
const OFF = 4;
|
||||
const bx = this._boardX;
|
||||
|
||||
this._pulseGfx.clear();
|
||||
this._pulseGfx.lineStyle(5, color, alpha);
|
||||
|
||||
for (const [r, c] of cells) {
|
||||
const x0 = bx + c * CELL, y0 = BOARD_Y + r * CELL;
|
||||
const x1 = x0 + CELL, y1 = y0 + CELL;
|
||||
if (!ghostSet.has(`${r - 1},${c}`)) {
|
||||
this._pulseGfx.beginPath(); this._pulseGfx.moveTo(x0 - OFF, y0 - OFF); this._pulseGfx.lineTo(x1 + OFF, y0 - OFF); this._pulseGfx.strokePath();
|
||||
}
|
||||
if (!ghostSet.has(`${r + 1},${c}`)) {
|
||||
this._pulseGfx.beginPath(); this._pulseGfx.moveTo(x0 - OFF, y1 + OFF); this._pulseGfx.lineTo(x1 + OFF, y1 + OFF); this._pulseGfx.strokePath();
|
||||
}
|
||||
if (!ghostSet.has(`${r},${c - 1}`)) {
|
||||
this._pulseGfx.beginPath(); this._pulseGfx.moveTo(x0 - OFF, y0 - OFF); this._pulseGfx.lineTo(x0 - OFF, y1 + OFF); this._pulseGfx.strokePath();
|
||||
}
|
||||
if (!ghostSet.has(`${r},${c + 1}`)) {
|
||||
this._pulseGfx.beginPath(); this._pulseGfx.moveTo(x1 + OFF, y0 - OFF); this._pulseGfx.lineTo(x1 + OFF, y1 + OFF); this._pulseGfx.strokePath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Screen management ──────────────────────────────────────────────────────
|
||||
|
||||
_clearScreen() {
|
||||
|
|
@ -102,6 +149,14 @@ export default class KataminoGame extends Phaser.Scene {
|
|||
for (const { ev, fn } of this._inputHandlers) this.input.off(ev, fn);
|
||||
this._inputHandlers = [];
|
||||
|
||||
// Kill any in-progress piece animation
|
||||
if (this._ghostAnimContainer) {
|
||||
this.tweens.killTweensOf(this._ghostAnimContainer);
|
||||
this._ghostAnimContainer.destroy(true);
|
||||
this._ghostAnimContainer = null;
|
||||
}
|
||||
this._ghostAnim = null;
|
||||
|
||||
// Reset play state
|
||||
this._board = null;
|
||||
this._cols = null;
|
||||
|
|
@ -111,6 +166,7 @@ export default class KataminoGame extends Phaser.Scene {
|
|||
this._lastHoverR = -1;
|
||||
this._lastHoverC = -1;
|
||||
this._boardGfx = null;
|
||||
this._pulseGfx = null;
|
||||
this._traySlots = [];
|
||||
}
|
||||
|
||||
|
|
@ -372,12 +428,12 @@ export default class KataminoGame extends Phaser.Scene {
|
|||
}).setOrigin(0.5);
|
||||
sc.add([backBtn, titleTxt]);
|
||||
|
||||
// Reset and Leave buttons
|
||||
const resetBtn = new Button(this, GAME_WIDTH - 310, 46, 'Reset',
|
||||
// Reset and Leave buttons (lower right)
|
||||
const resetBtn = new Button(this, GAME_WIDTH - 310, GAME_HEIGHT - 40, 'Reset',
|
||||
() => this._resetBoard(),
|
||||
{ variant: 'ghost', width: 140, height: 44, fontSize: 18 }
|
||||
).setDepth(D.ui + 1);
|
||||
const leaveBtn = new Button(this, GAME_WIDTH - 150, 46, 'Leave',
|
||||
const leaveBtn = new Button(this, GAME_WIDTH - 150, GAME_HEIGHT - 40, 'Leave',
|
||||
() => this.scene.start('GameMenu'),
|
||||
{ variant: 'ghost', width: 140, height: 44, fontSize: 18 }
|
||||
).setDepth(D.ui + 1);
|
||||
|
|
@ -387,6 +443,10 @@ export default class KataminoGame extends Phaser.Scene {
|
|||
this._boardGfx = this.add.graphics().setDepth(D.board);
|
||||
sc.add(this._boardGfx);
|
||||
|
||||
// Pulse outline (above board fill, below tray)
|
||||
this._pulseGfx = this.add.graphics().setDepth(D.board + 1);
|
||||
sc.add(this._pulseGfx);
|
||||
|
||||
// Board input zone (use setOrigin(0) so x,y = top-left)
|
||||
const zone = this.add.zone(this._boardX, BOARD_Y, boardW, ROWS * CELL)
|
||||
.setOrigin(0)
|
||||
|
|
@ -533,6 +593,7 @@ export default class KataminoGame extends Phaser.Scene {
|
|||
if (this._selectedPiece?.pieceId === pieceId) { this._clearSelection(); return; }
|
||||
this._selectedPiece = { pieceId, oriIdx: 0 };
|
||||
this._ghostCells = null;
|
||||
playSound(this, SFX.UI_PICK);
|
||||
this._renderTray();
|
||||
this._renderBoard();
|
||||
}
|
||||
|
|
@ -548,16 +609,130 @@ export default class KataminoGame extends Phaser.Scene {
|
|||
|
||||
_rotateSelected() {
|
||||
if (this._screen !== 'play' || !this._selectedPiece) return;
|
||||
playSound(this, SFX.UI_ACTIVATE);
|
||||
this._cancelGhostAnim();
|
||||
if (!this._ghostCells) {
|
||||
this._selectedPiece.oriIdx = rotateOri(this._selectedPiece.pieceId, this._selectedPiece.oriIdx);
|
||||
this._updateGhostAt(this._lastHoverR, this._lastHoverC);
|
||||
this._renderBoard();
|
||||
return;
|
||||
}
|
||||
this._startGhostAnim('rotate');
|
||||
}
|
||||
|
||||
_flipSelected() {
|
||||
if (this._screen !== 'play' || !this._selectedPiece) return;
|
||||
playSound(this, SFX.UI_FLIP);
|
||||
this._cancelGhostAnim();
|
||||
if (!this._ghostCells) {
|
||||
this._selectedPiece.oriIdx = flipOri(this._selectedPiece.pieceId, this._selectedPiece.oriIdx);
|
||||
this._updateGhostAt(this._lastHoverR, this._lastHoverC);
|
||||
this._renderBoard();
|
||||
return;
|
||||
}
|
||||
this._startGhostAnim('flip');
|
||||
}
|
||||
|
||||
_cancelGhostAnim() {
|
||||
if (!this._ghostAnimContainer) return;
|
||||
this.tweens.killTweensOf(this._ghostAnimContainer);
|
||||
this._ghostAnimContainer.destroy(true);
|
||||
this._ghostAnimContainer = null;
|
||||
this._ghostAnim = null;
|
||||
this._updateGhostAt(this._lastHoverR, this._lastHoverC);
|
||||
}
|
||||
|
||||
_startGhostAnim(type) {
|
||||
const cells = this._ghostCells;
|
||||
const pieceId = this._selectedPiece.pieceId;
|
||||
const hoverR = this._lastHoverR;
|
||||
const hoverC = this._lastHoverC;
|
||||
const n = cells.length;
|
||||
const cr = cells.reduce((s, [r]) => s + r, 0) / n;
|
||||
const cc = cells.reduce((s, [, c]) => s + c, 0) / n;
|
||||
|
||||
const gfx = this.add.graphics();
|
||||
this._fillGhostLocal(gfx, cells, cr, cc);
|
||||
|
||||
const pivotX = this._boardX + (cc + 0.5) * CELL;
|
||||
const pivotY = BOARD_Y + (cr + 0.5) * CELL;
|
||||
const container = this.add.container(pivotX, pivotY, [gfx]).setDepth(D.piece + 1);
|
||||
this._ghostAnimContainer = container;
|
||||
|
||||
// Suppress normal ghost & pulse while animating
|
||||
this._ghostCells = null;
|
||||
this._pulseGfx?.clear();
|
||||
this._renderBoard();
|
||||
|
||||
const HALF = 80;
|
||||
|
||||
const finish = () => {
|
||||
container.destroy(true);
|
||||
this._ghostAnimContainer = null;
|
||||
this._ghostAnim = null;
|
||||
this._updateGhostAt(this._lastHoverR, this._lastHoverC);
|
||||
this._renderBoard();
|
||||
};
|
||||
|
||||
const phase2 = () => {
|
||||
const oriCells = ORIENTATIONS[this._selectedPiece.pieceId][this._selectedPiece.oriIdx];
|
||||
const [pinDr, pinDc] = _centerPin(oriCells);
|
||||
const newCells = (hoverR >= 0 && hoverC >= 0)
|
||||
? absoluteCells(oriCells, hoverR - pinDr, hoverC - pinDc)
|
||||
: null;
|
||||
if (!newCells) { finish(); return; }
|
||||
|
||||
const nn = newCells.length;
|
||||
const ncr = newCells.reduce((s, [r]) => s + r, 0) / nn;
|
||||
const ncc = newCells.reduce((s, [, c]) => s + c, 0) / nn;
|
||||
gfx.clear();
|
||||
this._fillGhostLocal(gfx, newCells, ncr, ncc);
|
||||
container.setPosition(this._boardX + (ncc + 0.5) * CELL, BOARD_Y + (ncr + 0.5) * CELL);
|
||||
if (type === 'rotate') container.angle = -90;
|
||||
|
||||
this._ghostAnim = this.tweens.add({
|
||||
targets: container,
|
||||
angle: 0, scaleX: 1, scaleY: 1,
|
||||
duration: HALF,
|
||||
ease: 'Power2.easeOut',
|
||||
onComplete: finish,
|
||||
});
|
||||
};
|
||||
|
||||
const midpoint = () => {
|
||||
if (type === 'rotate') {
|
||||
this._selectedPiece.oriIdx = rotateOri(pieceId, this._selectedPiece.oriIdx);
|
||||
} else {
|
||||
this._selectedPiece.oriIdx = flipOri(pieceId, this._selectedPiece.oriIdx);
|
||||
}
|
||||
phase2();
|
||||
};
|
||||
|
||||
const phase1Props = type === 'rotate'
|
||||
? { angle: 90, scaleX: 0, scaleY: 0 }
|
||||
: { scaleX: 0 };
|
||||
|
||||
this._ghostAnim = this.tweens.add({
|
||||
targets: container,
|
||||
...phase1Props,
|
||||
duration: HALF,
|
||||
ease: 'Power2.easeIn',
|
||||
onComplete: midpoint,
|
||||
});
|
||||
}
|
||||
|
||||
_fillGhostLocal(gfx, cells, cr, cc) {
|
||||
const pieceId = this._selectedPiece?.pieceId;
|
||||
const color = PIECE_COLORS[pieceId] ?? 0xffffff;
|
||||
const allValid = cells.every(([r, c]) =>
|
||||
r >= 0 && r < ROWS && c >= 0 && c < this._cols && this._board[r * this._cols + c] === null
|
||||
);
|
||||
gfx.fillStyle(color, allValid ? 0.55 : 0.2);
|
||||
for (const [r, c] of cells) {
|
||||
const lx = (c - cc) * CELL - CELL / 2;
|
||||
const ly = (r - cr) * CELL - CELL / 2;
|
||||
gfx.fillRoundedRect(lx + 3, ly + 3, CELL - 6, CELL - 6, 9);
|
||||
}
|
||||
}
|
||||
|
||||
_updateGhostAt(r, c) {
|
||||
|
|
@ -573,6 +748,7 @@ export default class KataminoGame extends Phaser.Scene {
|
|||
const c = Math.floor((ptr.worldX - this._boardX) / CELL);
|
||||
this._lastHoverR = r;
|
||||
this._lastHoverC = c;
|
||||
if (this._ghostAnimContainer) return; // let animation finish
|
||||
this._updateGhostAt(r, c);
|
||||
this._renderBoard();
|
||||
}
|
||||
|
|
@ -584,7 +760,7 @@ export default class KataminoGame extends Phaser.Scene {
|
|||
if (r < 0 || r >= ROWS || c < 0 || c >= this._cols) return;
|
||||
|
||||
const cellVal = this._board[r * this._cols + c];
|
||||
if (cellVal !== null) { this._liftPiece(cellVal); return; }
|
||||
if (cellVal !== null) { this._cancelGhostAnim(); this._liftPiece(cellVal); return; }
|
||||
|
||||
if (!this._selectedPiece || !this._ghostCells) return;
|
||||
if (!canPlace(this._board, this._cols, this._ghostCells)) {
|
||||
|
|
@ -595,12 +771,15 @@ export default class KataminoGame extends Phaser.Scene {
|
|||
const { pieceId } = this._selectedPiece;
|
||||
this._board = placePiece(this._board, this._cols, this._ghostCells, pieceId);
|
||||
this._placedPieces.add(pieceId);
|
||||
playSound(this, SFX.CARD_PLACE);
|
||||
playSound(this, SFX.UI_PLACE);
|
||||
this._clearSelection();
|
||||
this._renderBoard();
|
||||
this._renderTray();
|
||||
|
||||
if (isSolved(this._board)) this._showWinOverlay();
|
||||
if (isSolved(this._board)) {
|
||||
playSound(this, SFX.UI_CHIME);
|
||||
this.time.delayedCall(1000, () => this._showWinOverlay());
|
||||
}
|
||||
}
|
||||
|
||||
_liftPiece(pieceId) {
|
||||
|
|
|
|||
|
|
@ -125,6 +125,11 @@ export default class PreloadScene extends Phaser.Scene {
|
|||
this.load.audio('sfx-woosh', '/assets/fx/woosh.mp3');
|
||||
this.load.spritesheet('jello-items', '/assets/images/jello-items.png', { frameWidth: 132, frameHeight: 132 });
|
||||
this.load.audio('sfx-jello-monsters','/assets/fx/jello-monsters.mp3');
|
||||
this.load.audio('sfx-ui-pick', '/assets/fx/ui-pick.mp3');
|
||||
this.load.audio('sfx-ui-activate', '/assets/fx/ui-activate.mp3');
|
||||
this.load.audio('sfx-ui-flip', '/assets/fx/ui-flip.mp3');
|
||||
this.load.audio('sfx-ui-place', '/assets/fx/ui-place.mp3');
|
||||
this.load.audio('sfx-ui-chime', '/assets/fx/ui-chime.mp3');
|
||||
|
||||
this.load.spritesheet('catan-special-cards', '/assets/images/catan-special-cards.png', { frameWidth: 270, frameHeight: 390 });
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@ export const SFX = {
|
|||
SQUASH: 'sfx-squash',
|
||||
WOOSH: 'sfx-woosh',
|
||||
JELLO_MONSTERS: 'sfx-jello-monsters',
|
||||
UI_PICK: 'sfx-ui-pick',
|
||||
UI_ACTIVATE: 'sfx-ui-activate',
|
||||
UI_FLIP: 'sfx-ui-flip',
|
||||
UI_PLACE: 'sfx-ui-place',
|
||||
UI_CHIME: 'sfx-ui-chime',
|
||||
};
|
||||
|
||||
export function playSound(scene, key) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue