feat: enhance Monopoly dice animations and player panel layout
- Add realistic dice throw animation with quadratic bezier arcs - Implement perspective scaling and spin during dice animation - Add impact bounce effect when dice land - Randomize dice landing positions and angles for natural appearance - Increase player portrait size and adjust panel layout for better readability - Improve cash display font size and spacing in player panels - Track dice positions and angles for consistent rendering between turns
This commit is contained in:
parent
4fbc868305
commit
ae2f3246dc
|
|
@ -30,7 +30,7 @@ const RP_X = BL + BS + 50; // 920
|
||||||
const RP_W = GAME_WIDTH - RP_X - 20; // ~980
|
const RP_W = GAME_WIDTH - RP_X - 20; // ~980
|
||||||
|
|
||||||
// Depth
|
// Depth
|
||||||
const DEPTH = { bg:0, board:5, band:6, text:7, houses:10, pawns:15, ui:25, popup:50, banner:90 };
|
const DEPTH = { bg:0, board:5, band:6, text:7, houses:10, pawns:15, ui:25, dice:40, popup:50, banner:90 };
|
||||||
|
|
||||||
// Center deck offset (must match drawCenterDecks constant)
|
// Center deck offset (must match drawCenterDecks constant)
|
||||||
const DECK_D = 130;
|
const DECK_D = 130;
|
||||||
|
|
@ -68,8 +68,10 @@ export default class MonopolyGame extends Phaser.Scene {
|
||||||
this.dyn = [];
|
this.dyn = [];
|
||||||
this.portraits = [];
|
this.portraits = [];
|
||||||
this.pawns = {}; // seat → image/circle
|
this.pawns = {}; // seat → image/circle
|
||||||
this.dieGfx = []; // [die1Graphics, die2Graphics]
|
this.dieGfx = []; // [die1Graphics, die2Graphics]
|
||||||
this.dieVals = [1,1];
|
this.dieVals = [1,1];
|
||||||
|
this.dicePositions = []; // [{cx,cy,angle}×2] — updated on each throw landing
|
||||||
|
this.diceAnimating = false;
|
||||||
this.cardPopup = null; // popup container
|
this.cardPopup = null; // popup container
|
||||||
this.bidInput = 0; // human bid amount for auction
|
this.bidInput = 0; // human bid amount for auction
|
||||||
// Property purchase modal (managed outside dyn)
|
// Property purchase modal (managed outside dyn)
|
||||||
|
|
@ -401,9 +403,13 @@ export default class MonopolyGame extends Phaser.Scene {
|
||||||
const dx = RP_X + RP_W/2 - 55;
|
const dx = RP_X + RP_W/2 - 55;
|
||||||
const dy = BT + this.playerPanelTotalH() + 30;
|
const dy = BT + this.playerPanelTotalH() + 30;
|
||||||
this.diceY = dy;
|
this.diceY = dy;
|
||||||
|
this.dicePositions = [
|
||||||
|
{ cx: dx, cy: dy, angle: 0 },
|
||||||
|
{ cx: dx + 84, cy: dy, angle: 0 },
|
||||||
|
];
|
||||||
this.dieGfx = [
|
this.dieGfx = [
|
||||||
this.add.graphics().setDepth(DEPTH.ui),
|
this.add.graphics().setDepth(DEPTH.dice),
|
||||||
this.add.graphics().setDepth(DEPTH.ui),
|
this.add.graphics().setDepth(DEPTH.dice),
|
||||||
];
|
];
|
||||||
this.drawDie(0, dx, dy, 1);
|
this.drawDie(0, dx, dy, 1);
|
||||||
this.drawDie(1, dx + 84, dy, 1);
|
this.drawDie(1, dx + 84, dy, 1);
|
||||||
|
|
@ -415,27 +421,25 @@ export default class MonopolyGame extends Phaser.Scene {
|
||||||
return rows * 190 + (rows - 1) * 12 + 20;
|
return rows * 190 + (rows - 1) * 12 + 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawDie(idx, cx, cy, value) {
|
drawDie(idx, cx, cy, value, angle = 0) {
|
||||||
const g = this.dieGfx[idx];
|
const g = this.dieGfx[idx];
|
||||||
const size = 66;
|
const size = 66;
|
||||||
const half = size / 2;
|
const half = size / 2;
|
||||||
g.clear();
|
g.clear();
|
||||||
|
g.setPosition(cx, cy);
|
||||||
|
g.setAngle(angle);
|
||||||
g.fillStyle(0xFFF8E7, 1);
|
g.fillStyle(0xFFF8E7, 1);
|
||||||
g.fillRoundedRect(cx - half, cy - half, size, size, 10);
|
g.fillRoundedRect(-half, -half, size, size, 10);
|
||||||
g.lineStyle(2, 0x4A3728, 1);
|
g.lineStyle(2, 0x4A3728, 1);
|
||||||
g.strokeRoundedRect(cx - half, cy - half, size, size, 10);
|
g.strokeRoundedRect(-half, -half, size, size, 10);
|
||||||
// Pips
|
// Pips drawn in local space (centered at origin)
|
||||||
g.fillStyle(0x1a1208, 1);
|
g.fillStyle(0x1a1208, 1);
|
||||||
const pipR = 5;
|
const pipR = 5;
|
||||||
const step = 18;
|
const step = 18;
|
||||||
const pips = PIPS[value] ?? PIPS[1];
|
const pips = PIPS[value] ?? PIPS[1];
|
||||||
for (const [px, py] of pips) {
|
for (const [px, py] of pips) {
|
||||||
g.fillCircle(cx + px * step, cy + py * step, pipR);
|
g.fillCircle(px * step, py * step, pipR);
|
||||||
}
|
}
|
||||||
this.dieGfx[idx] = g;
|
|
||||||
// Store die positions for later re-draw
|
|
||||||
if (!this.diePositions) this.diePositions = [];
|
|
||||||
this.diePositions[idx] = { cx, cy };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Portraits ──────────────────────────────────────────────────────────────
|
// ── Portraits ──────────────────────────────────────────────────────────────
|
||||||
|
|
@ -443,7 +447,7 @@ export default class MonopolyGame extends Phaser.Scene {
|
||||||
const n = this.gs.playerCount;
|
const n = this.gs.playerCount;
|
||||||
for (let seat = 0; seat < n; seat++) {
|
for (let seat = 0; seat < n; seat++) {
|
||||||
const { px, py } = this.panelPos(seat);
|
const { px, py } = this.panelPos(seat);
|
||||||
const portraitR = 28;
|
const portraitR = 40;
|
||||||
if (seat === this.humanSeat) {
|
if (seat === this.humanSeat) {
|
||||||
this.portraits[seat] = createPlayerPortrait(this, px + 16 + portraitR, py + 16 + portraitR, portraitR, DEPTH.ui+1, 'MonopolyGame');
|
this.portraits[seat] = createPlayerPortrait(this, px + 16 + portraitR, py + 16 + portraitR, portraitR, DEPTH.ui+1, 'MonopolyGame');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -636,37 +640,37 @@ export default class MonopolyGame extends Phaser.Scene {
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
const nameColor = isCurrent ? COLORS.goldHex : COLORS.textHex;
|
const nameColor = isCurrent ? COLORS.goldHex : COLORS.textHex;
|
||||||
this.reg(this.add.text(px + 72, py + 14, p.name, {
|
this.reg(this.add.text(px + 96, py + 14, p.name, {
|
||||||
fontFamily:'Righteous', fontSize:'17px', color: nameColor,
|
fontFamily:'Righteous', fontSize:'17px', color: nameColor,
|
||||||
}).setOrigin(0, 0).setDepth(DEPTH.ui+1));
|
}).setOrigin(0, 0).setDepth(DEPTH.ui+1));
|
||||||
|
|
||||||
// Cash
|
// Cash
|
||||||
this.reg(this.add.text(px + 72, py + 36, `$${p.cash.toLocaleString()}`, {
|
this.reg(this.add.text(px + 96, py + 36, `$${p.cash.toLocaleString()}`, {
|
||||||
fontFamily:'"Julius Sans One"', fontSize:'15px', color:'#7fb87f',
|
fontFamily:'"Julius Sans One"', fontSize:'26px', color:'#7fb87f',
|
||||||
}).setOrigin(0, 0).setDepth(DEPTH.ui+1));
|
}).setOrigin(0, 0).setDepth(DEPTH.ui+1));
|
||||||
|
|
||||||
// Net worth
|
// Net worth
|
||||||
const nw = netWorth(this.gs, seat);
|
const nw = netWorth(this.gs, seat);
|
||||||
this.reg(this.add.text(px + 72, py + 56, `Net: $${nw.toLocaleString()}`, {
|
this.reg(this.add.text(px + 96, py + 72, `Net: $${nw.toLocaleString()}`, {
|
||||||
fontFamily:'"Julius Sans One"', fontSize:'13px', color:COLORS.mutedHex,
|
fontFamily:'"Julius Sans One"', fontSize:'13px', color:COLORS.mutedHex,
|
||||||
}).setOrigin(0, 0).setDepth(DEPTH.ui+1));
|
}).setOrigin(0, 0).setDepth(DEPTH.ui+1));
|
||||||
|
|
||||||
// Jail indicator
|
// Jail indicator
|
||||||
if (p.jailed) {
|
if (p.jailed) {
|
||||||
this.reg(this.add.text(px + 72, py + 74, '🔒 In Jail', {
|
this.reg(this.add.text(px + 96, py + 90, '🔒 In Jail', {
|
||||||
fontFamily:'"Julius Sans One"', fontSize:'12px', color:COLORS.dangerHex,
|
fontFamily:'"Julius Sans One"', fontSize:'12px', color:COLORS.dangerHex,
|
||||||
}).setOrigin(0, 0).setDepth(DEPTH.ui+1));
|
}).setOrigin(0, 0).setDepth(DEPTH.ui+1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// GOOJF card indicator
|
// GOOJF card indicator
|
||||||
if (p.getOutOfJailFree > 0) {
|
if (p.getOutOfJailFree > 0) {
|
||||||
this.reg(this.add.text(px + 72, py + (p.jailed ? 90 : 74), `🎴 ×${p.getOutOfJailFree}`, {
|
this.reg(this.add.text(px + 96, py + (p.jailed ? 106 : 90), `🎴 ×${p.getOutOfJailFree}`, {
|
||||||
fontFamily:'"Julius Sans One"', fontSize:'11px', color:'#aaccaa',
|
fontFamily:'"Julius Sans One"', fontSize:'11px', color:'#aaccaa',
|
||||||
}).setOrigin(0, 0).setDepth(DEPTH.ui+1));
|
}).setOrigin(0, 0).setDepth(DEPTH.ui+1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Property color swatches
|
// Property color swatches
|
||||||
let sx = px + 72, sy = py + panelH - 26;
|
let sx = px + 96, sy = py + panelH - 26;
|
||||||
for (const [group, idxArr] of Object.entries(GROUPS)) {
|
for (const [group, idxArr] of Object.entries(GROUPS)) {
|
||||||
const owned = idxArr.filter(i => this.gs.board[i]?.owner === seat).length;
|
const owned = idxArr.filter(i => this.gs.board[i]?.owner === seat).length;
|
||||||
if (owned === 0) continue;
|
if (owned === 0) continue;
|
||||||
|
|
@ -692,11 +696,11 @@ export default class MonopolyGame extends Phaser.Scene {
|
||||||
const gs = this.gs;
|
const gs = this.gs;
|
||||||
if (gs.phase === 'gameover') return;
|
if (gs.phase === 'gameover') return;
|
||||||
|
|
||||||
// Dice values display (update)
|
// Dice values display — use stored landing positions/angles; skip during throw animation
|
||||||
const diceX = RP_X + RP_W/2 - 55;
|
if (gs.diceRoll && !this.diceAnimating) {
|
||||||
if (gs.diceRoll) {
|
const [dp0, dp1] = this.dicePositions;
|
||||||
this.drawDie(0, diceX, this.diceY, gs.diceRoll[0]);
|
this.drawDie(0, dp0.cx, dp0.cy, gs.diceRoll[0], dp0.angle);
|
||||||
this.drawDie(1, diceX + 84, this.diceY, gs.diceRoll[1]);
|
this.drawDie(1, dp1.cx, dp1.cy, gs.diceRoll[1], dp1.angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buttons only for human's turn
|
// Buttons only for human's turn
|
||||||
|
|
@ -1538,19 +1542,101 @@ export default class MonopolyGame extends Phaser.Scene {
|
||||||
// ── Animations ─────────────────────────────────────────────────────────────
|
// ── Animations ─────────────────────────────────────────────────────────────
|
||||||
animateDice(d1, d2) {
|
animateDice(d1, d2) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
let count = 0;
|
const defaultX = RP_X + RP_W / 2 - 55;
|
||||||
const total = 12;
|
const defaultY = this.diceY;
|
||||||
const diceX = RP_X + RP_W/2 - 55;
|
|
||||||
const ev = this.time.addEvent({
|
// Landing positions — scattered near default spots, random angle
|
||||||
delay: 70,
|
const land0 = {
|
||||||
repeat: total - 1,
|
x: defaultX + Phaser.Math.Between(-20, 20),
|
||||||
|
y: defaultY + Phaser.Math.Between(-14, 14),
|
||||||
|
angle: Phaser.Math.Between(-30, 30),
|
||||||
|
};
|
||||||
|
const land1 = {
|
||||||
|
x: defaultX + 84 + Phaser.Math.Between(-20, 20),
|
||||||
|
y: defaultY + Phaser.Math.Between(-14, 14),
|
||||||
|
angle: Phaser.Math.Between(-30, 30),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Throw origin — below the landing area (like a hand tossing upward)
|
||||||
|
const throwX = RP_X + RP_W / 2;
|
||||||
|
const throwY = GAME_HEIGHT - 40;
|
||||||
|
|
||||||
|
// Arch control points — above the landing area so dice overshoot upward then fall back down
|
||||||
|
const ctrl0 = { x: defaultX - 60, y: defaultY - 380 };
|
||||||
|
const ctrl1 = { x: defaultX + 80, y: defaultY - 350 };
|
||||||
|
|
||||||
|
// Total spin per die: 2–3 full rotations ending at the landing angle
|
||||||
|
const dir0 = Math.random() < 0.5 ? 1 : -1;
|
||||||
|
const dir1 = Math.random() < 0.5 ? 1 : -1;
|
||||||
|
const spin0 = dir0 * (Phaser.Math.Between(2, 3) * 360 + land0.angle * dir0);
|
||||||
|
const spin1 = dir1 * (Phaser.Math.Between(2, 3) * 360 + land1.angle * dir1);
|
||||||
|
|
||||||
|
const THROW_MS = 820;
|
||||||
|
let face0 = Phaser.Math.Between(1, 6);
|
||||||
|
let face1 = Phaser.Math.Between(1, 6);
|
||||||
|
|
||||||
|
// Randomize pip faces during flight
|
||||||
|
const faceTimer = this.time.addEvent({
|
||||||
|
delay: 90,
|
||||||
|
repeat: Math.ceil(THROW_MS / 90),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
count++;
|
face0 = Phaser.Math.Between(1, 6);
|
||||||
const r1 = count < total ? Math.floor(Math.random()*6)+1 : d1;
|
face1 = Phaser.Math.Between(1, 6);
|
||||||
const r2 = count < total ? Math.floor(Math.random()*6)+1 : d2;
|
},
|
||||||
this.drawDie(0, diceX, this.diceY, r1);
|
});
|
||||||
this.drawDie(1, diceX + 84, this.diceY, r2);
|
|
||||||
if (count >= total) resolve();
|
this.diceAnimating = true;
|
||||||
|
|
||||||
|
const proxy = { t: 0 };
|
||||||
|
this.tweens.add({
|
||||||
|
targets: proxy,
|
||||||
|
t: 1,
|
||||||
|
duration: THROW_MS,
|
||||||
|
ease: 'Sine.easeIn',
|
||||||
|
onUpdate: () => {
|
||||||
|
const t = proxy.t;
|
||||||
|
const inv = 1 - t;
|
||||||
|
|
||||||
|
// Quadratic bezier: start → control → land
|
||||||
|
const x0 = inv*inv*throwX + 2*inv*t*ctrl0.x + t*t*land0.x;
|
||||||
|
const y0 = inv*inv*throwY + 2*inv*t*ctrl0.y + t*t*land0.y;
|
||||||
|
const x1 = inv*inv*throwX + 2*inv*t*ctrl1.x + t*t*land1.x;
|
||||||
|
const y1 = inv*inv*throwY + 2*inv*t*ctrl1.y + t*t*land1.y;
|
||||||
|
|
||||||
|
// Scale up as dice approach (perspective/depth effect)
|
||||||
|
const scale = 0.45 + t * 0.55;
|
||||||
|
this.dieGfx[0].setScale(scale);
|
||||||
|
this.dieGfx[1].setScale(scale);
|
||||||
|
|
||||||
|
this.drawDie(0, x0, y0, face0, spin0 * t);
|
||||||
|
this.drawDie(1, x1, y1, face1, spin1 * t);
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
faceTimer.remove();
|
||||||
|
|
||||||
|
// Snap to final values at landing positions
|
||||||
|
this.dieGfx[0].setScale(1);
|
||||||
|
this.dieGfx[1].setScale(1);
|
||||||
|
this.drawDie(0, land0.x, land0.y, d1, land0.angle);
|
||||||
|
this.drawDie(1, land1.x, land1.y, d2, land1.angle);
|
||||||
|
|
||||||
|
// Store landing state for subsequent renders
|
||||||
|
this.dicePositions[0] = { cx: land0.x, cy: land0.y, angle: land0.angle };
|
||||||
|
this.dicePositions[1] = { cx: land1.x, cy: land1.y, angle: land1.angle };
|
||||||
|
|
||||||
|
// Impact bounce: scale 1 → 1.18 → 1
|
||||||
|
this.dieGfx[0].setScale(1.18);
|
||||||
|
this.dieGfx[1].setScale(1.18);
|
||||||
|
this.tweens.add({
|
||||||
|
targets: [this.dieGfx[0], this.dieGfx[1]],
|
||||||
|
scaleX: 1, scaleY: 1,
|
||||||
|
duration: 200,
|
||||||
|
ease: 'Back.easeOut',
|
||||||
|
onComplete: () => {
|
||||||
|
this.diceAnimating = false;
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue