Compare commits
3 Commits
6a33bf500b
...
2f7b2e183e
| Author | SHA1 | Date |
|---|---|---|
|
|
2f7b2e183e | |
|
|
5c88ab7986 | |
|
|
aa920fddfb |
|
|
@ -5,6 +5,7 @@ import { auth } from '../../services/auth.js';
|
||||||
import { api } from '../../services/api.js';
|
import { api } from '../../services/api.js';
|
||||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||||
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
||||||
|
import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js';
|
||||||
import {
|
import {
|
||||||
SIZE, CELL, BOARD_PX, GRID_X, GRID_Y, cellToWorld, worldToCell,
|
SIZE, CELL, BOARD_PX, GRID_X, GRID_Y, cellToWorld, worldToCell,
|
||||||
COLOR_PALETTE, cornersFor, ALL_PIECE_IDS, PIECE_SIZE, ORIENTATIONS,
|
COLOR_PALETTE, cornersFor, ALL_PIECE_IDS, PIECE_SIZE, ORIENTATIONS,
|
||||||
|
|
@ -44,6 +45,7 @@ export default class BlokusGame extends Phaser.Scene {
|
||||||
this.hoverMove = null;
|
this.hoverMove = null;
|
||||||
this.trayObjs = [];
|
this.trayObjs = [];
|
||||||
this.scoreObjs = [];
|
this.scoreObjs = [];
|
||||||
|
this.opponentPortraits = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -255,18 +257,31 @@ export default class BlokusGame extends Phaser.Scene {
|
||||||
// ── Score panels + controls ──────────────────────────────────────────────────
|
// ── Score panels + controls ──────────────────────────────────────────────────
|
||||||
buildScorePanels() {
|
buildScorePanels() {
|
||||||
this.scorePanels = [];
|
this.scorePanels = [];
|
||||||
|
const panelH = 192; // 3× original height
|
||||||
|
const portraitR = 60;
|
||||||
|
const portraitY = -panelH / 2 + portraitR + 14; // centered in upper space
|
||||||
for (let seat = 0; seat < this.gs.players.length; seat++) {
|
for (let seat = 0; seat < this.gs.players.length; seat++) {
|
||||||
const y = 146 + seat * 74;
|
const y = 221 + seat * (panelH + 10);
|
||||||
const container = this.add.container(SIDE_X, y).setDepth(DEPTH.hud);
|
const container = this.add.container(SIDE_X, y).setDepth(DEPTH.hud);
|
||||||
const bg = this.add.graphics();
|
const bg = this.add.graphics();
|
||||||
container.add(bg);
|
container.add(bg);
|
||||||
const swatch = this.add.graphics();
|
const swatch = this.add.graphics();
|
||||||
container.add(swatch);
|
container.add(swatch);
|
||||||
const name = this.add.text(-120, -18, '', { fontFamily: 'Righteous', fontSize: '20px', color: COLORS.textHex }).setOrigin(0, 0.5);
|
const portraitStroke = this.add.graphics().setDepth(DEPTH.hud + 1);
|
||||||
const detail = this.add.text(-120, 14, '', { fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex }).setOrigin(0, 0.5);
|
container.add(portraitStroke);
|
||||||
const score = this.add.text(150, 0, '', { fontFamily: 'Righteous', fontSize: '26px', color: COLORS.accentHex }).setOrigin(1, 0.5);
|
const name = this.add.text(-120, 60, '', { fontFamily: 'Righteous', fontSize: '20px', color: COLORS.textHex }).setOrigin(0, 0.5);
|
||||||
|
const detail = this.add.text(-120, 85, '', { fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex }).setOrigin(0, 0.5);
|
||||||
|
const score = this.add.text(150, 60, '', { fontFamily: 'Righteous', fontSize: '26px', color: COLORS.accentHex }).setOrigin(1, 0.5);
|
||||||
container.add([name, detail, score]);
|
container.add([name, detail, score]);
|
||||||
this.scorePanels.push({ container, bg, swatch, name, detail, score });
|
this.scorePanels.push({ container, bg, swatch, portraitStroke, name, detail, score, panelH });
|
||||||
|
|
||||||
|
// Create portrait
|
||||||
|
if (seat === 0) {
|
||||||
|
createPlayerPortrait(this, SIDE_X, y + portraitY, portraitR, DEPTH.hud + 2, 'BlokusGame');
|
||||||
|
} else {
|
||||||
|
const opp = this.opponents[seat - 1];
|
||||||
|
this.opponentPortraits[seat - 1] = createOpponentPortrait(this, opp, SIDE_X, y + portraitY, portraitR, DEPTH.hud + 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,12 +298,17 @@ export default class BlokusGame extends Phaser.Scene {
|
||||||
const isTurn = !this.gameOver && this.gs.current === seat;
|
const isTurn = !this.gameOver && this.gs.current === seat;
|
||||||
panel.bg.clear();
|
panel.bg.clear();
|
||||||
panel.bg.fillStyle(COLORS.panel, isTurn ? 0.95 : 0.6);
|
panel.bg.fillStyle(COLORS.panel, isTurn ? 0.95 : 0.6);
|
||||||
panel.bg.fillRoundedRect(-180, -32, 360, 64, 10);
|
panel.bg.fillRoundedRect(-180, -panel.panelH / 2, 360, panel.panelH, 10);
|
||||||
panel.bg.lineStyle(isTurn ? 3 : 1, isTurn ? COLORS.accent : col.hex, isTurn ? 1 : 0.5);
|
panel.bg.lineStyle(isTurn ? 3 : 1, isTurn ? COLORS.accent : col.hex, isTurn ? 1 : 0.5);
|
||||||
panel.bg.strokeRoundedRect(-180, -32, 360, 64, 10);
|
panel.bg.strokeRoundedRect(-180, -panel.panelH / 2, 360, panel.panelH, 10);
|
||||||
panel.swatch.clear();
|
panel.swatch.clear();
|
||||||
panel.swatch.fillStyle(col.hex, 1);
|
panel.swatch.fillStyle(col.hex, 1);
|
||||||
panel.swatch.fillRoundedRect(-172, -16, 28, 32, 5);
|
panel.swatch.fillRoundedRect(-172, panel.panelH / 2 - 44, 28, 32, 5);
|
||||||
|
// Portrait stroke — colored circle around the portrait
|
||||||
|
const portraitY = -panel.panelH / 2 + 60 + 14;
|
||||||
|
panel.portraitStroke.clear();
|
||||||
|
panel.portraitStroke.lineStyle(4, col.hex, 1);
|
||||||
|
panel.portraitStroke.strokeCircle(0, portraitY, 63);
|
||||||
panel.name.setText(this.playerName(seat));
|
panel.name.setText(this.playerName(seat));
|
||||||
panel.detail.setText(p.out ? 'done' : `${p.remaining.size} pieces left`);
|
panel.detail.setText(p.out ? 'done' : `${p.remaining.size} pieces left`);
|
||||||
panel.score.setText(`${scoreFor(p)}`);
|
panel.score.setText(`${scoreFor(p)}`);
|
||||||
|
|
@ -296,11 +316,15 @@ export default class BlokusGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildControls() {
|
buildControls() {
|
||||||
this.rotateBtn = new Button(this, SIDE_X - 95, 610, '↻ Rotate', () => this.rotateSelection(), { width: 170, height: 48, fontSize: 20 }).setDepth(DEPTH.button);
|
const trayCenter = TRAY_X + (TRAY_COLS * TRAY_SLOT_W) / 2;
|
||||||
this.flipBtn = new Button(this, SIDE_X + 95, 610, '↔ Flip', () => this.flipSelection(), { width: 170, height: 48, fontSize: 20 }).setDepth(DEPTH.button);
|
this.passBtn = new Button(this, trayCenter, 898, 'Pass', () => this.humanPass(), { variant: 'ghost', width: 360, height: 48, fontSize: 20 }).setDepth(DEPTH.button);
|
||||||
this.passBtn = new Button(this, SIDE_X, 674, 'Pass', () => this.humanPass(), { variant: 'ghost', width: 360, height: 48, fontSize: 20 }).setDepth(DEPTH.button);
|
this.rotateBtn = new Button(this, trayCenter - 95, 838, '↻ Rotate', () => this.rotateSelection(), { width: 170, height: 48, fontSize: 20 }).setDepth(DEPTH.button);
|
||||||
new Button(this, SIDE_X - 95, 750, 'New', () => this.scene.restart(), { variant: 'ghost', width: 170, height: 44, fontSize: 18 }).setDepth(DEPTH.button);
|
this.flipBtn = new Button(this, trayCenter + 95, 838, '↔ Flip', () => this.flipSelection(), { width: 170, height: 48, fontSize: 20 }).setDepth(DEPTH.button);
|
||||||
new Button(this, SIDE_X + 95, 750, 'Leave', () => this.scene.start('GameMenu'), { variant: 'ghost', width: 170, height: 44, fontSize: 18 }).setDepth(DEPTH.button);
|
const rightPad = 20;
|
||||||
|
const gap = 20;
|
||||||
|
const leaveX = GAME_WIDTH - rightPad - 85;
|
||||||
|
new Button(this, leaveX - 170 - gap, 1030, 'New', () => this.scene.restart(), { variant: 'ghost', width: 170, height: 44, fontSize: 18 }).setDepth(DEPTH.button);
|
||||||
|
new Button(this, leaveX, 1030, 'Leave', () => this.scene.start('GameMenu'), { variant: 'ghost', width: 170, height: 44, fontSize: 18 }).setDepth(DEPTH.button);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildStatus() {
|
buildStatus() {
|
||||||
|
|
@ -368,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() {
|
||||||
|
|
@ -378,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