Purchase property routine

This commit is contained in:
Brian Fertig 2026-06-07 15:15:16 -06:00
parent 77e334e192
commit e5c1021322
3 changed files with 325 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

View File

@ -32,6 +32,16 @@ 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 };
// Property purchase modal
const MODAL_W = 340;
const MODAL_H = 500;
const MODAL_BAND_H = 80;
const MODAL_TARGET_X = GAME_WIDTH / 2; // 960
const MODAL_TARGET_Y = GAME_HEIGHT / 2; // 540
const MODAL_AUCTION_X = 680;
const MODAL_AUCTION_Y = 500;
const MODAL_AUCTION_SCALE = 0.80;
// Pip positions for each die face (relative to die center)
const PIPS = {
1: [[0,0]],
@ -59,6 +69,13 @@ export default class MonopolyGame extends Phaser.Scene {
this.dieVals = [1,1];
this.cardPopup = null; // popup container
this.bidInput = 0; // human bid amount for auction
// Property purchase modal (managed outside dyn)
this.modalActive = false;
this.modalGfx = [];
this.modalContainer = null;
this.modalOverlay = null;
this.modalSpaceIdx = null;
this.modalOrigin = null;
}
create() {
@ -397,6 +414,7 @@ export default class MonopolyGame extends Phaser.Scene {
this.drawActionBar();
if (this.gs.pendingCard) this.drawCardPopup();
if (this.gs.phase === 'auction' && this.gs.pendingAuction) this.drawAuctionPanel();
if (this.modalActive && this.gs.phase === 'buy') this.drawModalBuyButtons();
}
drawHousesHotels() {
@ -675,12 +693,6 @@ export default class MonopolyGame extends Phaser.Scene {
}
}
if (phase === 'buy' && gs.pendingBuy) {
const sp = SPACES[gs.pendingBuy.spaceIdx];
mkBtn(`Buy ${sp.name}\n$${sp.price}`, () => this.onBuyProperty(), p.cash >= sp.price);
mkBtn('Decline (Auction)', () => this.onDeclineProperty(), true, { variant:'ghost' });
}
if (phase === 'card' && gs.pendingCard && gs.current === this.humanSeat) {
mkBtn('OK', () => this.onDismissCard());
}
@ -742,8 +754,10 @@ export default class MonopolyGame extends Phaser.Scene {
const bidderSeat = auc.bidOrder[auc.currentBidderIdx];
const isHuman = bidderSeat === this.humanSeat;
const pw = RP_W - 20, ph = 340;
const px = RP_X + 10, py = GAME_HEIGHT/2 - ph/2;
const pw = this.modalActive ? 520 : RP_W - 20;
const ph = 360;
const px = this.modalActive ? GAME_WIDTH - pw - 30 : RP_X + 10;
const py = GAME_HEIGHT/2 - ph/2;
const g = this.reg(this.add.graphics().setDepth(DEPTH.popup));
g.fillStyle(0x1e1a12, 1);
@ -1026,6 +1040,26 @@ export default class MonopolyGame extends Phaser.Scene {
const gs = this.gs;
if (gs.phase === 'gameover') { this.showGameOver(); return; }
// Guard 1: zoom property card to center when entering 'buy' phase
if (gs.phase === 'buy' && gs.pendingBuy && !this.modalActive) {
this.busy = true;
this.showPropertyModal(gs.pendingBuy.spaceIdx).then(() => {
this.busy = false;
this.advance();
});
return;
}
// Guard 2: dismiss modal when phase leaves buy/auction
if (this.modalActive && gs.phase !== 'buy' && gs.phase !== 'auction') {
this.busy = true;
this.dismissPropertyModal().then(() => {
this.busy = false;
this.time.delayedCall(0, () => this.advance());
});
return;
}
// Determine who acts next
let actingSeat = gs.current;
if (gs.phase === 'auction' && gs.pendingAuction) {
@ -1102,13 +1136,16 @@ export default class MonopolyGame extends Phaser.Scene {
break;
}
case 'buy': {
// Modal already zoomed in (advance() called showPropertyModal before doAiAction)
const buy = chooseBuy(gs, seat, skill);
await this.delay(700);
await this.delay(700); // AI "thinking" pause
if (buy) {
this.gs = buyProperty(this.gs, seat);
playSound(this, SFX.purchase);
await this.dismissPropertyModal(); // fill with owner color + zoom back
} else {
this.gs = declineProperty(this.gs, seat);
await this.shiftModalForAuction();
}
this.render();
break;
@ -1234,6 +1271,276 @@ export default class MonopolyGame extends Phaser.Scene {
});
}
// ── Property Purchase Modal ────────────────────────────────────────────────
buildPropertyCardContainer(spaceIdx) {
const sp = SPACES[spaceIdx];
const w = MODAL_W, h = MODAL_H, bh = MODAL_BAND_H;
const container = this.add.container(0, 0);
const g = this.add.graphics();
// Card background + border
g.fillStyle(0xFFF8E7, 1);
g.fillRoundedRect(-w/2, -h/2, w, h, 8);
g.lineStyle(2, 0x2c1810, 1);
g.strokeRoundedRect(-w/2, -h/2, w, h, 8);
// Top band
const bandCol = sp.group ? GROUP_COLORS[sp.group]
: sp.type === 'railroad' ? 0x1a1208
: sp.type === 'utility' && spaceIdx === 12 ? 0xFFD700
: 0x1565C0;
g.fillStyle(bandCol, 1);
g.fillRoundedRect(-w/2, -h/2, w, bh, { tl:8, tr:8, bl:0, br:0 });
// Band separator line
g.lineStyle(1, 0x2c1810, 0.5);
g.beginPath(); g.moveTo(-w/2, -h/2 + bh); g.lineTo(w/2, -h/2 + bh); g.strokePath();
container.add(g);
const bandTextCol = '#FFF8E7';
const darkTextCol = '#1a1208';
// "TITLE DEED" inside band
container.add(this.add.text(0, -h/2 + 8, 'TITLE DEED', {
fontFamily: '"Julius Sans One"', fontSize: '10px', color: bandTextCol, align: 'center',
}).setOrigin(0.5, 0));
// Property name inside band
container.add(this.add.text(0, -h/2 + 24, sp.name, {
fontFamily: 'Righteous', fontSize: '18px', color: bandTextCol,
align: 'center', wordWrap: { width: w - 20, useAdvancedWrap: true },
}).setOrigin(0.5, 0));
// --- Content area below band ---
const contentTop = -h/2 + bh + 14;
let cy = contentTop;
if (sp.type === 'property') {
const rentLabels = [
['Rent', sp.rent[0]],
['Color group', sp.rent[1]],
['1 House', sp.rent[2]],
['2 Houses', sp.rent[3]],
['3 Houses', sp.rent[4]],
['4 Houses', sp.rent[5]],
['Hotel', sp.rent[6]],
];
rentLabels.forEach(([label, val]) => {
const row = this.add.text(0, cy, `${label} $${val}`, {
fontFamily: '"Julius Sans One"', fontSize: '13px', color: darkTextCol, align: 'center',
}).setOrigin(0.5, 0);
container.add(row);
cy += 20;
});
cy += 6;
container.add(this.add.text(0, cy, `Houses / Hotels $${sp.houseCost} each`, {
fontFamily: '"Julius Sans One"', fontSize: '11px', color: '#555544', align: 'center',
}).setOrigin(0.5, 0));
cy += 16;
container.add(this.add.text(0, cy, `Mortgage value $${sp.mortgage}`, {
fontFamily: '"Julius Sans One"', fontSize: '11px', color: '#555544', align: 'center',
}).setOrigin(0.5, 0));
} else if (sp.type === 'railroad') {
[['1 Railroad', '$25'], ['2 Railroads', '$50'], ['3 Railroads', '$100'], ['4 Railroads', '$200']]
.forEach(([label, val]) => {
container.add(this.add.text(0, cy, `${label} ${val}`, {
fontFamily: '"Julius Sans One"', fontSize: '14px', color: darkTextCol, align: 'center',
}).setOrigin(0.5, 0));
cy += 24;
});
cy += 6;
container.add(this.add.text(0, cy, `Mortgage value $${sp.mortgage}`, {
fontFamily: '"Julius Sans One"', fontSize: '11px', color: '#555544', align: 'center',
}).setOrigin(0.5, 0));
} else if (sp.type === 'utility') {
['If 1 Utility owned:', '4× your dice roll', '', 'If 2 Utilities owned:', '10× your dice roll']
.forEach((line) => {
container.add(this.add.text(0, cy, line, {
fontFamily: '"Julius Sans One"', fontSize: '13px', color: darkTextCol, align: 'center',
}).setOrigin(0.5, 0));
cy += line ? 20 : 8;
});
cy += 6;
container.add(this.add.text(0, cy, `Mortgage value $${sp.mortgage}`, {
fontFamily: '"Julius Sans One"', fontSize: '11px', color: '#555544', align: 'center',
}).setOrigin(0.5, 0));
}
// Price banner near bottom of info area
container.add(this.add.text(0, h/2 - 88, `Purchase Price $${sp.price}`, {
fontFamily: 'Righteous', fontSize: '18px', color: darkTextCol, align: 'center',
}).setOrigin(0.5, 0));
// Horizontal rule above price
const ruleG = this.add.graphics();
ruleG.lineStyle(1, 0x2c1810, 0.4);
ruleG.beginPath(); ruleG.moveTo(-w/2 + 12, h/2 - 98); ruleG.lineTo(w/2 - 12, h/2 - 98); ruleG.strokePath();
container.add(ruleG);
// Player pieces currently on this space
const onSpace = this.gs.players.filter(p => p.position === spaceIdx && !p.bankrupt);
if (onSpace.length > 0) {
const spacing = 52;
const startX = -(onSpace.length - 1) * spacing / 2;
onSpace.forEach((p, i) => {
const px = startX + i * spacing;
const py = h/2 - 44;
if (this.hasPawns) {
container.add(this.add.image(px, py, 'monopoly-pawns', PAWN_FRAME(p.seat)).setDisplaySize(44, 44));
} else {
const pg = this.add.graphics();
pg.fillStyle(PLAYER_COLORS[p.seat], 1);
pg.fillCircle(px, py, 20);
pg.lineStyle(2, 0xffffff, 0.8);
pg.strokeCircle(px, py, 20);
container.add(pg);
}
});
}
return container;
}
async showPropertyModal(spaceIdx) {
const geo = spaceGeometry(spaceIdx);
const ox = BL + geo.x + geo.w / 2;
const oy = BT + geo.y + geo.h / 2;
const scaleStart = geo.w / MODAL_W;
const rotStart = -geo.rotation;
// Dim overlay
this.modalOverlay = this.add.rectangle(GAME_WIDTH/2, GAME_HEIGHT/2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0)
.setDepth(DEPTH.popup - 2).setInteractive();
this.modalGfx.push(this.modalOverlay);
this.tweens.add({ targets: this.modalOverlay, alpha: 0.68, duration: 400 });
// Property card container
const container = this.buildPropertyCardContainer(spaceIdx);
container.setPosition(ox, oy).setScale(scaleStart).setRotation(rotStart).setDepth(DEPTH.popup - 1);
this.modalGfx.push(container);
this.modalContainer = container;
this.modalSpaceIdx = spaceIdx;
this.modalOrigin = { ox, oy, scaleStart, rotStart };
this.modalActive = true;
return new Promise(resolve => {
this.tweens.add({
targets: container,
x: MODAL_TARGET_X, y: MODAL_TARGET_Y,
scaleX: 1, scaleY: 1,
rotation: 0,
duration: 700,
ease: 'Cubic.easeOut',
onComplete: resolve,
});
});
}
async animateModalFill(seat) {
if (!this.modalContainer) return;
const fillGfx = this.add.graphics();
this.modalContainer.add(fillGfx);
const proxy = { h: 0 };
return new Promise(resolve => {
this.tweens.add({
targets: proxy,
h: MODAL_H,
duration: 1200,
ease: 'Linear',
onUpdate: () => {
fillGfx.clear();
fillGfx.fillStyle(PLAYER_COLORS[seat], 0.55);
fillGfx.fillRect(-MODAL_W/2, MODAL_H/2 - proxy.h, MODAL_W, proxy.h);
},
onComplete: resolve,
});
});
}
async shiftModalForAuction() {
if (!this.modalContainer) return;
return new Promise(resolve => {
this.tweens.add({
targets: this.modalContainer,
x: MODAL_AUCTION_X, y: MODAL_AUCTION_Y,
scaleX: MODAL_AUCTION_SCALE, scaleY: MODAL_AUCTION_SCALE,
duration: 400,
ease: 'Cubic.easeInOut',
onComplete: resolve,
});
});
}
async dismissPropertyModal() {
if (!this.modalContainer) return;
// Fill with owner color if someone bought this property
const winner = this.gs.board?.[this.modalSpaceIdx]?.owner;
if (winner !== null && winner !== undefined) {
await this.animateModalFill(winner);
await this.delay(500);
}
const { ox, oy, scaleStart, rotStart } = this.modalOrigin;
// Animate card back to board position
const returnP = new Promise(resolve => {
this.tweens.add({
targets: this.modalContainer,
x: ox, y: oy,
scaleX: scaleStart, scaleY: scaleStart,
rotation: rotStart,
duration: 600,
ease: 'Cubic.easeIn',
onComplete: resolve,
});
});
// Simultaneously fade out overlay
this.tweens.add({ targets: this.modalOverlay, alpha: 0, duration: 400 });
await returnP;
// Destroy container children first (Phaser won't do it automatically)
if (this.modalContainer) {
this.modalContainer.each(child => { try { child.destroy(); } catch {} });
this.modalContainer.destroy();
}
if (this.modalOverlay) this.modalOverlay.destroy();
this.modalGfx = [];
this.modalContainer = null;
this.modalOverlay = null;
this.modalSpaceIdx = null;
this.modalOrigin = null;
this.modalActive = false;
}
drawModalBuyButtons() {
const gs = this.gs;
if (!gs.pendingBuy || gs.current !== this.humanSeat) return;
const sp = SPACES[gs.pendingBuy.spaceIdx];
const p = gs.players[this.humanSeat];
const bx = GAME_WIDTH / 2;
const by = MODAL_TARGET_Y + MODAL_H / 2 + 52;
const buyBtn = new Button(this, bx, by,
`Buy $${sp.price}`,
() => this.onBuyProperty(),
{ width: 340, height: 56, fontSize: 24, enabled: p.cash >= sp.price });
buyBtn.setDepth(DEPTH.popup + 1);
this.reg(buyBtn);
const declineBtn = new Button(this, bx, by + 66,
'Decline → Auction',
() => this.onDeclineProperty(),
{ width: 340, height: 50, fontSize: 20, variant: 'ghost' });
declineBtn.setDepth(DEPTH.popup + 1);
this.reg(declineBtn);
}
// ── Human Handlers ─────────────────────────────────────────────────────────
onRollDice() {
if (this.busy) return;
@ -1244,18 +1551,22 @@ export default class MonopolyGame extends Phaser.Scene {
});
}
onBuyProperty() {
async onBuyProperty() {
if (this.busy) return;
this.gs = buyProperty(this.gs, this.humanSeat);
this.busy = true;
this.gs = buyProperty(this.gs, this.humanSeat); // owner set → dismissPropertyModal fills
playSound(this, SFX.purchase);
this.render();
await this.dismissPropertyModal(); // fill + zoom back
this.busy = false;
this.advance();
}
onDeclineProperty() {
async onDeclineProperty() {
if (this.busy) return;
this.busy = true;
this.gs = declineProperty(this.gs, this.humanSeat);
this.render();
await this.shiftModalForAuction();
this.busy = false;
this.advance();
}