diff --git a/public/src/games/monopoly/MonopolyGame.js b/public/src/games/monopoly/MonopolyGame.js index f619250..55b584e 100644 --- a/public/src/games/monopoly/MonopolyGame.js +++ b/public/src/games/monopoly/MonopolyGame.js @@ -30,7 +30,7 @@ const RP_X = BL + BS + 50; // 920 const RP_W = GAME_WIDTH - RP_X - 20; // ~980 // 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) const DECK_D = 130; @@ -68,8 +68,10 @@ export default class MonopolyGame extends Phaser.Scene { this.dyn = []; this.portraits = []; this.pawns = {}; // seat → image/circle - this.dieGfx = []; // [die1Graphics, die2Graphics] - this.dieVals = [1,1]; + this.dieGfx = []; // [die1Graphics, die2Graphics] + this.dieVals = [1,1]; + this.dicePositions = []; // [{cx,cy,angle}×2] — updated on each throw landing + this.diceAnimating = false; this.cardPopup = null; // popup container this.bidInput = 0; // human bid amount for auction // 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 dy = BT + this.playerPanelTotalH() + 30; this.diceY = dy; + this.dicePositions = [ + { cx: dx, cy: dy, angle: 0 }, + { cx: dx + 84, cy: dy, angle: 0 }, + ]; this.dieGfx = [ - this.add.graphics().setDepth(DEPTH.ui), - this.add.graphics().setDepth(DEPTH.ui), + this.add.graphics().setDepth(DEPTH.dice), + this.add.graphics().setDepth(DEPTH.dice), ]; this.drawDie(0, dx, 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; } - drawDie(idx, cx, cy, value) { + drawDie(idx, cx, cy, value, angle = 0) { const g = this.dieGfx[idx]; const size = 66; const half = size / 2; g.clear(); + g.setPosition(cx, cy); + g.setAngle(angle); 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.strokeRoundedRect(cx - half, cy - half, size, size, 10); - // Pips + g.strokeRoundedRect(-half, -half, size, size, 10); + // Pips drawn in local space (centered at origin) g.fillStyle(0x1a1208, 1); const pipR = 5; const step = 18; const pips = PIPS[value] ?? PIPS[1]; 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 ────────────────────────────────────────────────────────────── @@ -443,7 +447,7 @@ export default class MonopolyGame extends Phaser.Scene { const n = this.gs.playerCount; for (let seat = 0; seat < n; seat++) { const { px, py } = this.panelPos(seat); - const portraitR = 28; + const portraitR = 40; if (seat === this.humanSeat) { this.portraits[seat] = createPlayerPortrait(this, px + 16 + portraitR, py + 16 + portraitR, portraitR, DEPTH.ui+1, 'MonopolyGame'); } else { @@ -636,37 +640,37 @@ export default class MonopolyGame extends Phaser.Scene { // Name 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, }).setOrigin(0, 0).setDepth(DEPTH.ui+1)); // Cash - this.reg(this.add.text(px + 72, py + 36, `$${p.cash.toLocaleString()}`, { - fontFamily:'"Julius Sans One"', fontSize:'15px', color:'#7fb87f', + this.reg(this.add.text(px + 96, py + 36, `$${p.cash.toLocaleString()}`, { + fontFamily:'"Julius Sans One"', fontSize:'26px', color:'#7fb87f', }).setOrigin(0, 0).setDepth(DEPTH.ui+1)); // Net worth 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, }).setOrigin(0, 0).setDepth(DEPTH.ui+1)); // Jail indicator 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, }).setOrigin(0, 0).setDepth(DEPTH.ui+1)); } // GOOJF card indicator 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', }).setOrigin(0, 0).setDepth(DEPTH.ui+1)); } // 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)) { const owned = idxArr.filter(i => this.gs.board[i]?.owner === seat).length; if (owned === 0) continue; @@ -692,11 +696,11 @@ export default class MonopolyGame extends Phaser.Scene { const gs = this.gs; if (gs.phase === 'gameover') return; - // Dice values display (update) - const diceX = RP_X + RP_W/2 - 55; - if (gs.diceRoll) { - this.drawDie(0, diceX, this.diceY, gs.diceRoll[0]); - this.drawDie(1, diceX + 84, this.diceY, gs.diceRoll[1]); + // Dice values display — use stored landing positions/angles; skip during throw animation + if (gs.diceRoll && !this.diceAnimating) { + const [dp0, dp1] = this.dicePositions; + this.drawDie(0, dp0.cx, dp0.cy, gs.diceRoll[0], dp0.angle); + this.drawDie(1, dp1.cx, dp1.cy, gs.diceRoll[1], dp1.angle); } // Buttons only for human's turn @@ -1538,19 +1542,101 @@ export default class MonopolyGame extends Phaser.Scene { // ── Animations ───────────────────────────────────────────────────────────── animateDice(d1, d2) { return new Promise(resolve => { - let count = 0; - const total = 12; - const diceX = RP_X + RP_W/2 - 55; - const ev = this.time.addEvent({ - delay: 70, - repeat: total - 1, + const defaultX = RP_X + RP_W / 2 - 55; + const defaultY = this.diceY; + + // Landing positions — scattered near default spots, random angle + const land0 = { + 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: () => { - count++; - const r1 = count < total ? Math.floor(Math.random()*6)+1 : d1; - 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(); + face0 = Phaser.Math.Between(1, 6); + face1 = Phaser.Math.Between(1, 6); + }, + }); + + 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(); + }, + }); }, }); });