import Phaser from 'phaser' import { TILE_SIZE, OFFICE_COLS, OFFICE_ROWS, GAP_COLS, OUTDOOR_MARGIN_X, OUTDOOR_MARGIN_TOP, OUTDOOR_MARGIN_BOTTOM, isLocalDaytime, } from '../config' import { getOffices, buildCompositeGrid, DEFAULT_MAP_STR, DEFAULT_SEATS, type OfficeConfig } from './OfficeStore' export { DEFAULT_MAP_STR, DEFAULT_SEATS } export interface MapData { wallBodies: Phaser.Physics.Arcade.StaticGroup collisionGrid: number[][] waterfront: { dockCenterX: number dockY: number waterTopY: number waterBottomY: number } } export function parseMapStr(mapStr: string[], cols = OFFICE_COLS, rows = OFFICE_ROWS): number[][] { const grid: number[][] = [] for (let r = 0; r < rows; r++) { const row: number[] = [] const line = r < mapStr.length ? mapStr[r] : '#'.repeat(cols) for (let c = 0; c < cols; c++) { row.push(c < line.length && line[c] === '.' ? 0 : 1) } grid.push(row) } return grid } export function gridToMapStr(grid: number[][]): string[] { return grid.map(row => row.map(v => (v === 0 ? '.' : '#')).join('')) } interface ForegroundCrop { x: number y: number w: number h: number depthY: number } function cropTiles(x: number, y: number, w: number, h: number, depthY = y + h): ForegroundCrop { return { x: Math.round(x * TILE_SIZE), y: Math.round(y * TILE_SIZE), w: Math.round(w * TILE_SIZE), h: Math.round(h * TILE_SIZE), depthY: Math.round(depthY * TILE_SIZE), } } const OFFICE_FOREGROUND_CROPS: ForegroundCrop[] = [ cropTiles(8.0, 4.0, 4.3, 3.35, 7.15), cropTiles(2.3, 8.65, 5.35, 1.95, 10.55), cropTiles(11.1, 8.55, 3.1, 1.7, 10.45), cropTiles(14.15, 8.55, 4.95, 2.0, 10.95), cropTiles(2.1, 11.45, 3.05, 1.8, 12.95), cropTiles(5.25, 11.45, 3.05, 1.8, 12.95), cropTiles(8.45, 11.45, 3.05, 1.8, 12.95), cropTiles(2.1, 14.55, 3.05, 1.85, 16.1), cropTiles(5.25, 14.55, 3.05, 1.85, 16.1), cropTiles(8.45, 14.55, 3.05, 1.85, 16.1), cropTiles(14.0, 12.15, 4.85, 3.0, 15.3), cropTiles(1.0, 19.55, 3.05, 1.15, 20.7), cropTiles(4.45, 20.45, 3.35, 1.35, 21.8), cropTiles(14.1, 20.5, 3.15, 1.85, 22.3), cropTiles(8.05, 23.1, 4.2, 1.0, 24.1), cropTiles(17.0, 23.1, 2.1, 1.0, 24.1), ] export class OfficeMapBuilder { private bgSprites: Phaser.GameObjects.Image[] = [] private foregroundSprites: Phaser.GameObjects.Image[] = [] private nameLabels: Phaser.GameObjects.Text[] = [] private decor: Phaser.GameObjects.GameObject[] = [] /** Outdoor campus graphics (skyline through waterfront); replaced on day/night switch. */ private outdoorBackdrop: Phaser.GameObjects.Graphics | null = null /** Parking slabs on their own layer so asphalt always paints over full-width lawn. */ private outdoorParking: Phaser.GameObjects.Graphics | null = null private readonly tilesetFrames = { shelfA: 61, shelfB: 62, waterCooler: 105, waterCoolerAlt: 106, fridge: 108, vending: 110, plantA: 148, plantB: 150, boxA: 153, boxB: 154, boxC: 158, } private clearSceneDecor() { for (const sprite of this.bgSprites) sprite.destroy() for (const sprite of this.foregroundSprites) sprite.destroy() for (const label of this.nameLabels) label.destroy() for (const item of this.decor) item.destroy() this.bgSprites = [] this.foregroundSprites = [] this.nameLabels = [] this.decor = [] this.outdoorBackdrop = null this.outdoorParking = null } private addForegroundOccluders(scene: Phaser.Scene, office: OfficeConfig) { const officeX = office.offsetCol * TILE_SIZE for (const crop of OFFICE_FOREGROUND_CROPS) { const sprite = scene.add.image(officeX, 0, 'office-bg') sprite.setOrigin(0, 0) sprite.setCrop(crop.x, crop.y, crop.w, crop.h) sprite.setDepth(crop.depthY) this.foregroundSprites.push(sprite) } } private addPixelLabel( scene: Phaser.Scene, x: number, y: number, text: string, bgColor: string, color = '#f5efe3', size = '10px', ) { const label = scene.add.text(x, y, text, { fontSize: size, fontFamily: 'monospace', color, backgroundColor: bgColor, padding: { left: 4, right: 4, top: 2, bottom: 2 }, resolution: 2, align: 'center', }) label.setOrigin(0.5, 0.5) label.setDepth(-94) this.decor.push(label) return label } private addTilesetSprite( scene: Phaser.Scene, x: number, y: number, frame: number, scale = 1, ) { const sprite = scene.add.image(x, y, 'office-tileset-32', frame) sprite.setOrigin(0, 0) sprite.setScale(scale) sprite.setDepth(-92) this.decor.push(sprite) return sprite } private getOutdoorMetrics(totalWidth: number, totalHeight: number) { const roadY = totalHeight + TILE_SIZE * 10.1 const roadHeight = TILE_SIZE * 4.1 const waterTopY = roadY + roadHeight + TILE_SIZE * 0.15 const dockY = waterTopY + TILE_SIZE * 1.1 return { gateCenterX: totalWidth / 2, roadY, roadHeight, waterTopY, dockY, bottomY: totalHeight + OUTDOOR_MARGIN_BOTTOM, } } /** * Original top-down pixel cars: bold outline, stepped shading, glass highlights. * Tires are drawn to the sides of the body (mostly outside the paint); solid rubber, no hub in plan view. */ private paintStylizedParkingCar( g: Phaser.GameObjects.Graphics, cx: number, cy: number, mirrorX: boolean, warmBody: boolean, drawRunningLights: boolean, ) { const P = warmBody ? { outline: 0x141018, body: 0xd94a52, bodyMid: 0xc73c44, bodyDark: 0x8e2a32, cabin: 0xe0686e, cabinHi: 0xf2888c, cabinLo: 0xb0303c, glass: 0x5c80b8, glassDeep: 0x3a5c88, glassHi: 0xb8e0fc, tire: 0x1c1e24, tireOuter: 0x12141a, bonnetHi: 0xe87074, } : { outline: 0x101420, body: 0x3e7cc8, bodyMid: 0x2f64a8, bodyDark: 0x244878, cabin: 0x5c98dc, cabinHi: 0x7eb4f0, cabinLo: 0x3468a8, glass: 0x4a6fa0, glassDeep: 0x304868, glassHi: 0xa8d8ff, tire: 0x1c1e24, tireOuter: 0x12141a, bonnetHi: 0x6ca8e8, } const blot = (x0: number, y: number, w: number, h: number, col: number, a = 1) => { g.fillStyle(col, a) const sx = mirrorX ? -x0 - w : x0 g.fillRect(Math.round(cx + sx), Math.round(cy + y), w, h) } g.fillStyle(0x0f140f, 0.28) g.fillEllipse(cx, cy + 36, 36, 9) blot(-16, -38, 32, 76, P.outline) blot(-15, -37, 30, 74, P.body) blot(-14, 2, 28, 35, P.bodyDark) blot(-14, -4, 28, 10, P.bodyMid) blot(-13, 8, 26, 22, P.body) blot(-13, 8, 26, 7, P.bonnetHi, 0.92) blot(-13, -35, 26, 32, P.cabin) blot(-13, -35, 26, 4, P.cabinHi) blot(-13, -8, 26, 5, P.cabinLo) blot(-11, -7, 22, 11, P.glassDeep) blot(-11, -7, 22, 5, P.glass) blot(-10, -6, 8, 3, P.glassHi) blot(-1, -6, 6, 2, P.glassHi, 0.75) blot(-16, -2, 3, 8, P.bodyDark) blot(13, -2, 3, 8, P.bodyDark) const wheelYFront = 22 const wheelYRear = -30 const tireW = 7 const tireH = 9 for (const wy of [wheelYFront, wheelYRear]) { const leftX = -22 const rightX = 15 blot(leftX, wy, tireW, tireH, P.tire) blot(rightX, wy, tireW, tireH, P.tire) blot(leftX, wy, 1, tireH, P.tireOuter) blot(rightX + tireW - 1, wy, 1, tireH, P.tireOuter) } for (const wy of [wheelYFront, wheelYRear]) { blot(-18, wy + 1, 4, tireH - 2, P.bodyDark) blot(14, wy + 1, 4, tireH - 2, P.bodyDark) } if (drawRunningLights) { blot(-12, 33, 5, 4, 0xf4f2ea) blot(7, 33, 5, 4, 0xf4f2ea) blot(-11, 34, 2, 2, 0xfff8c8) blot(8, 34, 2, 2, 0xfff8c8) blot(-13, -38, 4, 3, 0xff5530) blot(9, -38, 4, 3, 0xc83820) blot(-18, 4, 3, 4, 0xffcc48) blot(15, 4, 3, 4, 0xffa030) } blot(-15, -37, 30, 1, P.cabinHi, 0.45) blot(-14, 28, 28, 1, 0x000000, 0.12) } private buildOutdoorBackdrop( scene: Phaser.Scene, totalWidth: number, totalHeight: number, offices: OfficeConfig[], isDay: boolean, opts?: { skipLabels?: boolean }, ) { const backdrop = scene.add.graphics() backdrop.setDepth(-320) const startX = -OUTDOOR_MARGIN_X const startY = -OUTDOOR_MARGIN_TOP const width = totalWidth + OUTDOOR_MARGIN_X * 2 const height = totalHeight + OUTDOOR_MARGIN_TOP + OUTDOOR_MARGIN_BOTTOM const endX = startX + width const bottomY = totalHeight + OUTDOOR_MARGIN_BOTTOM const horizonY = startY + TILE_SIZE * 6.2 const lawnY = totalHeight + TILE_SIZE * 0.35 const fenceY = totalHeight + TILE_SIZE * 4.25 const parkingY = totalHeight + TILE_SIZE * 5.9 const { gateCenterX, roadY, roadHeight, waterTopY, dockY } = this.getOutdoorMetrics(totalWidth, totalHeight) const pal = isDay ? { skyTop: 0x6ec4f0, skyBand: 0xa8daf8, towerFar: 0x889fb4, towerNear: 0x9cb0c4, towerRoof: 0xb4c8dc, winColor: 0xffffff, winAlpha: 0.22, facadeColor: 0xd8e4f0, facadeAlpha: 0.36, bridgeColor: 0xc0d0e0, bridgeAlpha: 0.92, grassBase: 0x3a7844, grassLawn: 0x5cb868, treeTrunk: 0x365c40, treeMid: 0x4ca868, treeHi: 0x72d884, grove0: 0x305838, grove1: 0x4e9860, grove2: 0x68c078, fence: 0x9a8a78, road: 0x3c4048, roadDash: 0xd8c890, curb: 0x363230, water: 0x3588b8, ripple: 0x5cacd4, rippleHi: 0x98d8f0, edgeGreen: 0x58a868, edgeGreenHi: 0x7ed090, logoDirt: 0xb8b898, logoDirtHi: 0xd0d0b8, } : { skyTop: 0x263345, skyBand: 0x35516a, towerFar: 0x132032, towerNear: 0x1d2b3e, towerRoof: 0x24364b, winColor: 0xf3d58c, winAlpha: 0.42, facadeColor: 0x30455c, facadeAlpha: 0.45, bridgeColor: 0x233247, bridgeAlpha: 0.95, grassBase: 0x31523b, grassLawn: 0x4f814f, treeTrunk: 0x2b442d, treeMid: 0x3f6b3f, treeHi: 0x548654, grove0: 0x284028, grove1: 0x406f40, grove2: 0x5d8c58, fence: 0x7c6a55, road: 0x30343a, roadDash: 0xd0b17a, curb: 0x2e2a27, water: 0x214a62, ripple: 0x3c88a3, rippleHi: 0x78bfd0, edgeGreen: 0x6f8b5f, edgeGreenHi: 0x8fb47a, logoDirt: 0x9ca277, logoDirtHi: 0xbbb38d, } backdrop.fillStyle(pal.skyTop, 1) backdrop.fillRect(startX, startY, width, TILE_SIZE * 8.2) backdrop.fillStyle(pal.skyBand, 1) backdrop.fillRect(startX, startY + TILE_SIZE * 6.15, width, TILE_SIZE * 2.05) if (isDay) { backdrop.fillStyle(0xfff8e8, 0.45) for (let i = 0; i < 5; i++) { const cx = startX + width * (0.12 + i * 0.19) + ((i * 17) % 40) const cy = startY + TILE_SIZE * (1.4 + (i % 3) * 0.35) backdrop.fillEllipse(cx, cy, TILE_SIZE * 2.2, TILE_SIZE * 0.95) } backdrop.fillStyle(0xfff2b0, 0.92) backdrop.fillCircle(endX - TILE_SIZE * 2.4, startY + TILE_SIZE * 1.25, TILE_SIZE * 0.85) backdrop.fillStyle(0xfffcf0, 0.35) backdrop.fillCircle(endX - TILE_SIZE * 2.4, startY + TILE_SIZE * 1.25, TILE_SIZE * 1.45) } // Distant city skyline with layered towers, roof forms, and lit windows. backdrop.fillStyle(pal.towerFar, 1) for (let x = startX - TILE_SIZE; x < endX + TILE_SIZE; x += TILE_SIZE * 3.4) { const stepIndex = Math.floor((x - startX + TILE_SIZE) / (TILE_SIZE * 3.4)) const widthSteps = [2.3, 1.7, 2.9, 2.1, 3.2, 1.9] const heightSteps = [6.8, 8.7, 7.2, 10.4, 8.1, 9.6] const towerWidth = TILE_SIZE * widthSteps[stepIndex % widthSteps.length] const towerHeight = TILE_SIZE * heightSteps[stepIndex % heightSteps.length] const towerTop = horizonY - towerHeight - TILE_SIZE * 0.9 backdrop.fillRect(x, towerTop, towerWidth, towerHeight) if (stepIndex % 3 === 0) { backdrop.fillRect(x + towerWidth * 0.22, towerTop - TILE_SIZE * 0.7, towerWidth * 0.56, TILE_SIZE * 0.7) } else if (stepIndex % 3 === 1) { backdrop.fillRect(x + towerWidth * 0.4, towerTop - TILE_SIZE * 1.0, 4, TILE_SIZE) } else { backdrop.fillRect(x + towerWidth * 0.18, towerTop - TILE_SIZE * 0.45, towerWidth * 0.64, TILE_SIZE * 0.45) } } backdrop.fillStyle(pal.towerNear, 1) for (let x = startX; x < endX; x += TILE_SIZE * 2.65) { const stepIndex = Math.floor((x - startX) / (TILE_SIZE * 2.65)) const widthSteps = [1.8, 2.4, 1.55, 2.8, 2.1, 1.7, 2.5] const heightSteps = [5.4, 7.1, 6.0, 8.6, 6.7, 7.8, 5.8] const towerWidth = TILE_SIZE * widthSteps[stepIndex % widthSteps.length] const towerHeight = TILE_SIZE * heightSteps[stepIndex % heightSteps.length] const towerTop = horizonY - towerHeight backdrop.fillRect(x, towerTop, towerWidth, towerHeight) // Rooftop shapes to avoid flat silhouettes. if (stepIndex % 4 === 0) { backdrop.fillStyle(pal.towerRoof, 1) backdrop.fillRect(x + towerWidth * 0.24, towerTop - TILE_SIZE * 0.42, towerWidth * 0.52, TILE_SIZE * 0.42) backdrop.fillStyle(pal.towerNear, 1) } else if (stepIndex % 4 === 1) { backdrop.fillStyle(pal.towerRoof, 1) backdrop.fillRect(x + towerWidth * 0.48, towerTop - TILE_SIZE * 0.8, 4, TILE_SIZE * 0.8) backdrop.fillStyle(pal.towerNear, 1) } else if (stepIndex % 4 === 2) { backdrop.fillStyle(pal.towerRoof, 1) backdrop.fillRect(x + towerWidth * 0.18, towerTop - TILE_SIZE * 0.35, towerWidth * 0.64, TILE_SIZE * 0.35) backdrop.fillStyle(pal.towerNear, 1) } // Night: warm lit windows. Day: faint glass reflections (no "hub" glow). backdrop.fillStyle(pal.winColor, pal.winAlpha) for (let wy = towerTop + TILE_SIZE * 0.7; wy < towerTop + towerHeight - TILE_SIZE * 0.5; wy += TILE_SIZE * 1.05) { if (isDay && (stepIndex + Math.floor(wy)) % 2 !== 0) continue if ((Math.floor(wy) + stepIndex) % 3 === 0) { backdrop.fillRect(x + 5, wy, 3, 3) } if ((Math.floor(wy) + stepIndex) % 2 === 0 && towerWidth > TILE_SIZE * 1.9) { backdrop.fillRect(x + towerWidth - 8, wy + 4, 3, 3) } } backdrop.fillStyle(pal.facadeColor, pal.facadeAlpha) backdrop.fillRect(x + towerWidth * 0.5, towerTop + TILE_SIZE * 0.4, 2, towerHeight - TILE_SIZE * 0.8) backdrop.fillStyle(pal.towerNear, 1) } // A few skybridges / podiums between towers to make the silhouette richer. backdrop.fillStyle(pal.bridgeColor, pal.bridgeAlpha) for (let x = startX + TILE_SIZE * 6; x < endX - TILE_SIZE * 6; x += TILE_SIZE * 11) { const bridgeY = horizonY - TILE_SIZE * (2.8 + ((x / TILE_SIZE) % 2)) backdrop.fillRect(x, bridgeY, TILE_SIZE * 2.2, TILE_SIZE * 0.35) } // Campus grass field around the offices. backdrop.fillStyle(pal.grassBase, 1) backdrop.fillRect(startX, horizonY, width, height - (horizonY - startY)) backdrop.fillStyle(pal.grassLawn, 1) backdrop.fillRect(startX, lawnY, width, roadY - lawnY) // Tree belts on both sides and behind the complex. const treeXs: number[] = [] for (let x = startX + TILE_SIZE * 0.1; x < endX; x += TILE_SIZE * 3.9) treeXs.push(x) for (const x of treeXs) { const canopyW = TILE_SIZE * (2.25 + (Math.abs(Math.floor(x / TILE_SIZE)) % 3) * 0.32) backdrop.fillStyle(pal.treeTrunk, 1) backdrop.fillRect(x + TILE_SIZE * 1.05, horizonY + TILE_SIZE * 1.2, 8, TILE_SIZE * 1.65) backdrop.fillStyle(pal.treeMid, 1) backdrop.fillRect(x, horizonY - TILE_SIZE * 1.0, canopyW, TILE_SIZE * 2.15) backdrop.fillStyle(pal.treeHi, 1) backdrop.fillRect(x + 5, horizonY - TILE_SIZE * 1.3, canopyW - 10, TILE_SIZE * 0.95) } // Side groves so the left/right margins feel intentional. for (const groveX of [startX + TILE_SIZE * 1.2, endX - TILE_SIZE * 6.5]) { for (let i = 0; i < 4; i++) { const x = groveX + i * TILE_SIZE * 1.8 const y = totalHeight - TILE_SIZE * (4.5 + (i % 2) * 0.9) backdrop.fillStyle(pal.grove0, 1) backdrop.fillRect(x + 10, y + TILE_SIZE * 1.5, 8, TILE_SIZE * 1.45) backdrop.fillStyle(pal.grove1, 1) backdrop.fillRect(x, y, TILE_SIZE * 2.35, TILE_SIZE * 1.9) backdrop.fillStyle(pal.grove2, 1) backdrop.fillRect(x + 4, y - 6, TILE_SIZE * 1.75, TILE_SIZE * 0.82) } } // Keep the forecourt green on both sides; only the central axis should read as a paved approach. // Center arrival walk from the lobby to the street. const officeDoorCenters = offices.map(office => (office.offsetCol + 2.95) * TILE_SIZE) const hallSpanX = TILE_SIZE * 0.9 const hallSpanW = totalWidth - TILE_SIZE * 1.8 const wallThickness = 6 const doorOpeningW = TILE_SIZE * 1.7 const hallY = totalHeight + TILE_SIZE * 0.08 const hallH = TILE_SIZE * 2.05 const hallBottom = hallY + hallH const lobbyWidth = TILE_SIZE * 9.4 const lobbyX = gateCenterX - lobbyWidth / 2 const lobbyY = hallBottom - 2 const lobbyH = TILE_SIZE * 3.1 const lobbyBottom = lobbyY + lobbyH const frontOpeningW = TILE_SIZE * 2.3 backdrop.fillStyle(0xbda57b, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 4.25, lobbyBottom, TILE_SIZE * 8.5, roadY - lobbyBottom) backdrop.fillStyle(0xd7c6a1, 0.95) for (let y = lobbyBottom + TILE_SIZE * 0.35; y < roadY - 6; y += TILE_SIZE * 0.8) { backdrop.fillRect(gateCenterX - TILE_SIZE * 3.35, y, TILE_SIZE * 6.7, 3) } // Outdoor arrival node and stone lawn edging so the forecourt reads as organized landscape. const forecourtNodeY = lobbyBottom + TILE_SIZE * 1.65 backdrop.fillStyle(0xcab48b, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 3.6, forecourtNodeY, TILE_SIZE * 7.2, TILE_SIZE * 2.15) backdrop.fillStyle(0xe1d1ad, 0.95) backdrop.fillRect(gateCenterX - TILE_SIZE * 2.8, forecourtNodeY + TILE_SIZE * 0.42, TILE_SIZE * 5.6, TILE_SIZE * 0.18) backdrop.fillStyle(0x756a59, 1) backdrop.fillRect(startX, roadY - TILE_SIZE * 0.35, width, 3) backdrop.fillRect(startX, lawnY + TILE_SIZE * 0.18, width, 3) for (const edgeY of [forecourtNodeY + TILE_SIZE * 0.35, forecourtNodeY + TILE_SIZE * 1.45]) { backdrop.fillStyle(pal.edgeGreen, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 5.0, edgeY, TILE_SIZE * 1.25, TILE_SIZE * 0.4) backdrop.fillRect(gateCenterX + TILE_SIZE * 3.75, edgeY, TILE_SIZE * 1.25, TILE_SIZE * 0.4) backdrop.fillStyle(pal.edgeGreenHi, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 4.78, edgeY - 2, TILE_SIZE * 0.82, 3) backdrop.fillRect(gateCenterX + TILE_SIZE * 3.96, edgeY - 2, TILE_SIZE * 0.82, 3) } // Shared indoor hall directly beneath the offices. backdrop.fillStyle(0xbfd7d6, 1) backdrop.fillRect(hallSpanX, hallY, hallSpanW, hallH) backdrop.fillStyle(0xcfe2e1, 1) for (let x = hallSpanX + TILE_SIZE * 0.2; x < hallSpanX + hallSpanW - TILE_SIZE * 0.2; x += TILE_SIZE) { backdrop.fillRect(x, hallY + TILE_SIZE * 0.42, 2, hallH - TILE_SIZE * 0.62) } for (let y = hallY + TILE_SIZE * 0.35; y < hallBottom - TILE_SIZE * 0.25; y += TILE_SIZE * 0.72) { backdrop.fillRect(hallSpanX + TILE_SIZE * 0.2, y, hallSpanW - TILE_SIZE * 0.4, 2) } // Top wall with three openings aligned to office exits. backdrop.fillStyle(0xf2ede4, 1) let cursorX = hallSpanX for (const centerX of officeDoorCenters) { const openingX = centerX - doorOpeningW / 2 if (openingX > cursorX) { backdrop.fillRect(cursorX, hallY, openingX - cursorX, wallThickness) } backdrop.fillStyle(0x80684e, 1) backdrop.fillRect(openingX - 4, hallY, 4, TILE_SIZE * 0.62) backdrop.fillRect(openingX + doorOpeningW, hallY, 4, TILE_SIZE * 0.62) backdrop.fillStyle(0xa88961, 1) backdrop.fillRect(openingX, hallY, doorOpeningW, 4) backdrop.fillStyle(0xf2ede4, 1) cursorX = openingX + doorOpeningW } if (cursorX < hallSpanX + hallSpanW) { backdrop.fillRect(cursorX, hallY, hallSpanX + hallSpanW - cursorX, wallThickness) } // Hall side walls and bottom wall segments. backdrop.fillRect(hallSpanX, hallY, wallThickness, hallH) backdrop.fillRect(hallSpanX + hallSpanW - wallThickness, hallY, wallThickness, hallH) backdrop.fillRect(hallSpanX, hallBottom - wallThickness, lobbyX - hallSpanX, wallThickness) backdrop.fillRect(lobbyX + lobbyWidth, hallBottom - wallThickness, hallSpanX + hallSpanW - (lobbyX + lobbyWidth), wallThickness) // Door thresholds and shallow wall openings from each office into the hall. for (const centerX of officeDoorCenters) { const openingX = centerX - doorOpeningW / 2 backdrop.fillStyle(0x9c835e, 1) backdrop.fillRect(openingX, hallY + TILE_SIZE * 0.02, doorOpeningW, TILE_SIZE * 0.16) backdrop.fillStyle(0xd9c79d, 1) backdrop.fillRect(openingX + TILE_SIZE * 0.12, hallY + TILE_SIZE * 0.24, doorOpeningW - TILE_SIZE * 0.24, 3) backdrop.fillStyle(0x6a5642, 0.55) backdrop.fillRect(openingX + TILE_SIZE * 0.1, hallY + TILE_SIZE * 0.34, doorOpeningW - TILE_SIZE * 0.2, TILE_SIZE * 0.26) } // Central lobby as a true top-down indoor room extending from the hall. backdrop.fillStyle(0xc6ddd8, 1) backdrop.fillRect(lobbyX, lobbyY, lobbyWidth, lobbyH) backdrop.fillStyle(0xd7e7e3, 1) for (let x = lobbyX + TILE_SIZE * 0.3; x < lobbyX + lobbyWidth - TILE_SIZE * 0.2; x += TILE_SIZE) { backdrop.fillRect(x, lobbyY + TILE_SIZE * 0.4, 2, lobbyH - TILE_SIZE * 0.6) } for (let y = lobbyY + TILE_SIZE * 0.35; y < lobbyBottom - TILE_SIZE * 0.2; y += TILE_SIZE * 0.72) { backdrop.fillRect(lobbyX + TILE_SIZE * 0.24, y, lobbyWidth - TILE_SIZE * 0.48, 2) } backdrop.fillStyle(0xf2ede4, 1) backdrop.fillRect(lobbyX, lobbyY, wallThickness, lobbyH) backdrop.fillRect(lobbyX + lobbyWidth - wallThickness, lobbyY, wallThickness, lobbyH) backdrop.fillRect(lobbyX, lobbyBottom - wallThickness, (lobbyWidth - frontOpeningW) / 2, wallThickness) backdrop.fillRect(gateCenterX + frontOpeningW / 2, lobbyBottom - wallThickness, (lobbyWidth - frontOpeningW) / 2, wallThickness) // Short returns where the hall opens into the lobby. backdrop.fillRect(lobbyX, lobbyY, TILE_SIZE * 1.1, wallThickness) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 1.1, lobbyY, TILE_SIZE * 1.1, wallThickness) backdrop.fillStyle(0x9f8566, 1) backdrop.fillRect(lobbyX + TILE_SIZE * 0.2, lobbyY + TILE_SIZE * 0.18, TILE_SIZE * 0.7, 3) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 0.9, lobbyY + TILE_SIZE * 0.18, TILE_SIZE * 0.7, 3) // Side returns and glazed side screens behind the logo wall. backdrop.fillStyle(0xe8f0ef, 0.7) backdrop.fillRect(lobbyX + TILE_SIZE * 1.15, lobbyY + TILE_SIZE * 0.38, TILE_SIZE * 1.15, TILE_SIZE * 0.9) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 2.3, lobbyY + TILE_SIZE * 0.38, TILE_SIZE * 1.15, TILE_SIZE * 0.9) backdrop.fillStyle(0x7ca0a5, 0.65) for (let glassY = lobbyY + TILE_SIZE * 0.44; glassY < lobbyY + TILE_SIZE * 1.16; glassY += 8) { backdrop.fillRect(lobbyX + TILE_SIZE * 1.38, glassY, TILE_SIZE * 0.68, 2) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 2.06, glassY, TILE_SIZE * 0.68, 2) } backdrop.fillStyle(0x465357, 0.9) backdrop.fillRect(lobbyX + TILE_SIZE * 1.1, lobbyY + TILE_SIZE * 0.34, 2, TILE_SIZE * 0.98) backdrop.fillRect(lobbyX + TILE_SIZE * 2.26, lobbyY + TILE_SIZE * 0.34, 2, TILE_SIZE * 0.98) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 2.3, lobbyY + TILE_SIZE * 0.34, 2, TILE_SIZE * 0.98) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 1.14, lobbyY + TILE_SIZE * 0.34, 2, TILE_SIZE * 0.98) // Simple lobby furnishings: rug, reception desk, logo wall, and side seating/planting. backdrop.fillStyle(0x6f8f93, 0.95) backdrop.fillRect(gateCenterX - TILE_SIZE * 1.65, lobbyY + TILE_SIZE * 0.65, TILE_SIZE * 3.3, TILE_SIZE * 1.1) backdrop.fillStyle(0x87a7ab, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 1.2, lobbyY + TILE_SIZE * 0.92, TILE_SIZE * 2.4, TILE_SIZE * 0.18) // Soft logo-wall backlight and wall washers. backdrop.fillStyle(0xb8d8d1, 0.38) backdrop.fillRect(gateCenterX - TILE_SIZE * 2.45, lobbyY + TILE_SIZE * 0.08, TILE_SIZE * 4.9, TILE_SIZE * 1.25) backdrop.fillStyle(0xe8f3ef, 0.7) backdrop.fillRect(gateCenterX - TILE_SIZE * 1.7, lobbyY + TILE_SIZE * 0.18, TILE_SIZE * 3.4, TILE_SIZE * 0.18) backdrop.fillRect(lobbyX + TILE_SIZE * 0.55, lobbyY + TILE_SIZE * 0.22, TILE_SIZE * 0.9, TILE_SIZE * 0.12) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 1.45, lobbyY + TILE_SIZE * 0.22, TILE_SIZE * 0.9, TILE_SIZE * 0.12) backdrop.fillStyle(0x4e6b68, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 1.95, lobbyY + TILE_SIZE * 0.18, TILE_SIZE * 3.9, TILE_SIZE * 0.42) backdrop.fillStyle(0xdfeee8, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 1.2, lobbyY + TILE_SIZE * 0.3, TILE_SIZE * 2.4, TILE_SIZE * 0.12) // Reception desk shadow / light falloff onto the floor. backdrop.fillStyle(0x566f76, 0.35) backdrop.fillRect(gateCenterX - TILE_SIZE * 1.65, lobbyY + TILE_SIZE * 2.05, TILE_SIZE * 3.3, TILE_SIZE * 0.32) backdrop.fillRect(gateCenterX - TILE_SIZE * 1.1, lobbyY + TILE_SIZE * 2.38, TILE_SIZE * 2.2, TILE_SIZE * 0.18) backdrop.fillStyle(0x8e8f9b, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 1.28, lobbyY + TILE_SIZE * 1.45, TILE_SIZE * 2.56, TILE_SIZE * 0.72) backdrop.fillStyle(0xe7eef0, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 1.0, lobbyY + TILE_SIZE * 1.6, TILE_SIZE * 2.0, TILE_SIZE * 0.18) backdrop.fillStyle(0x5f6472, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 0.22, lobbyY + TILE_SIZE * 1.42, TILE_SIZE * 0.44, TILE_SIZE * 0.18) backdrop.fillStyle(0xf6f7f2, 0.78) backdrop.fillRect(gateCenterX - TILE_SIZE * 0.9, lobbyY + TILE_SIZE * 1.72, TILE_SIZE * 1.8, TILE_SIZE * 0.12) backdrop.fillStyle(0xbfa67e, 1) backdrop.fillRect(lobbyX + TILE_SIZE * 0.7, lobbyY + TILE_SIZE * 1.55, TILE_SIZE * 1.2, TILE_SIZE * 0.52) backdrop.fillStyle(0xf0f0ea, 1) backdrop.fillRect(lobbyX + TILE_SIZE * 0.95, lobbyY + TILE_SIZE * 1.7, TILE_SIZE * 0.7, TILE_SIZE * 0.18) backdrop.fillStyle(0x9a9faa, 1) backdrop.fillRect(lobbyX + TILE_SIZE * 0.72, lobbyY + TILE_SIZE * 2.2, TILE_SIZE * 1.6, TILE_SIZE * 0.42) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 2.32, lobbyY + TILE_SIZE * 2.2, TILE_SIZE * 1.6, TILE_SIZE * 0.42) backdrop.fillStyle(0xc8d1d6, 1) backdrop.fillRect(lobbyX + TILE_SIZE * 0.88, lobbyY + TILE_SIZE * 2.08, TILE_SIZE * 1.28, TILE_SIZE * 0.16) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 2.16, lobbyY + TILE_SIZE * 2.08, TILE_SIZE * 1.28, TILE_SIZE * 0.16) backdrop.fillStyle(0x6a7f55, 1) backdrop.fillRect(lobbyX + TILE_SIZE * 0.38, lobbyY + TILE_SIZE * 2.0, TILE_SIZE * 0.34, TILE_SIZE * 0.62) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 0.72, lobbyY + TILE_SIZE * 2.0, TILE_SIZE * 0.34, TILE_SIZE * 0.62) backdrop.fillStyle(0x8eab74, 1) backdrop.fillRect(lobbyX + TILE_SIZE * 0.22, lobbyY + TILE_SIZE * 1.78, TILE_SIZE * 0.66, TILE_SIZE * 0.34) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 0.88, lobbyY + TILE_SIZE * 1.78, TILE_SIZE * 0.66, TILE_SIZE * 0.34) // Reception approach lines, rug edging, and side wayfinding. backdrop.fillStyle(0xe9dcc0, 0.88) backdrop.fillRect(gateCenterX - TILE_SIZE * 1.78, lobbyY + TILE_SIZE * 2.46, TILE_SIZE * 3.56, 2) backdrop.fillRect(gateCenterX - TILE_SIZE * 0.06, lobbyY + TILE_SIZE * 1.98, 2, TILE_SIZE * 1.18) backdrop.fillStyle(0x9c8866, 0.92) backdrop.strokeRect(gateCenterX - TILE_SIZE * 1.82, lobbyY + TILE_SIZE * 2.3, TILE_SIZE * 3.64, TILE_SIZE * 0.66) backdrop.fillStyle(0xb79e72, 1) backdrop.fillRect(lobbyX + TILE_SIZE * 2.42, lobbyY + TILE_SIZE * 2.03, TILE_SIZE * 0.24, TILE_SIZE * 0.78) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 2.66, lobbyY + TILE_SIZE * 2.03, TILE_SIZE * 0.24, TILE_SIZE * 0.78) backdrop.fillStyle(0xe8dfc6, 1) backdrop.fillRect(lobbyX + TILE_SIZE * 2.16, lobbyY + TILE_SIZE * 1.9, TILE_SIZE * 0.76, TILE_SIZE * 0.16) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 2.92, lobbyY + TILE_SIZE * 1.9, TILE_SIZE * 0.76, TILE_SIZE * 0.16) backdrop.fillStyle(0x5f7b69, 1) backdrop.fillRect(lobbyX + TILE_SIZE * 2.16, lobbyY + TILE_SIZE * 2.18, TILE_SIZE * 0.76, TILE_SIZE * 0.56) backdrop.fillRect(lobbyX + lobbyWidth - TILE_SIZE * 2.92, lobbyY + TILE_SIZE * 2.18, TILE_SIZE * 0.76, TILE_SIZE * 0.56) // Threshold from lobby to outside path. backdrop.fillStyle(0x8f7557, 1) backdrop.fillRect(gateCenterX - frontOpeningW / 2, lobbyBottom - wallThickness, frontOpeningW, wallThickness) backdrop.fillStyle(0xd7c79e, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 0.72, lobbyBottom + 2, TILE_SIZE * 1.44, 3) // Small buffer vestibule between indoor lobby and outdoor approach. const vestibuleW = TILE_SIZE * 4.7 const vestibuleH = TILE_SIZE * 1.2 const vestibuleY = lobbyBottom - 1 backdrop.fillStyle(0xc1d4cf, 0.98) backdrop.fillRect(gateCenterX - vestibuleW / 2, vestibuleY, vestibuleW, vestibuleH) backdrop.fillStyle(0xf2ede4, 1) backdrop.fillRect(gateCenterX - vestibuleW / 2, vestibuleY, wallThickness, vestibuleH) backdrop.fillRect(gateCenterX + vestibuleW / 2 - wallThickness, vestibuleY, wallThickness, vestibuleH) backdrop.fillStyle(0xd7e7e3, 1) backdrop.fillRect(gateCenterX - TILE_SIZE * 1.35, vestibuleY + TILE_SIZE * 0.34, TILE_SIZE * 2.7, 2) backdrop.fillStyle(0x94a7a3, 0.8) backdrop.fillRect(gateCenterX - vestibuleW / 2 + TILE_SIZE * 0.35, vestibuleY + TILE_SIZE * 0.18, TILE_SIZE * 0.55, 2) backdrop.fillRect(gateCenterX + vestibuleW / 2 - TILE_SIZE * 0.9, vestibuleY + TILE_SIZE * 0.18, TILE_SIZE * 0.55, 2) // Courtyard feature on the front lawn: a landscape logo marker. const courtyardY = hallBottom + TILE_SIZE * 1.7 const logoX = gateCenterX + TILE_SIZE * 11.2 // Landscape logo now rendered as standalone letters without a backing plinth. backdrop.fillStyle(pal.logoDirt, 0.95) backdrop.fillRect(logoX - TILE_SIZE * 4.3, courtyardY - TILE_SIZE * 1.2, TILE_SIZE * 8.6, TILE_SIZE * 2.35) backdrop.fillStyle(pal.logoDirtHi, 0.9) backdrop.fillRect(logoX - TILE_SIZE * 3.5, courtyardY - TILE_SIZE * 0.8, TILE_SIZE * 7.0, TILE_SIZE * 1.55) for (const shrubX of [logoX - TILE_SIZE * 4.9, logoX - TILE_SIZE * 3.9, logoX + TILE_SIZE * 3.9, logoX + TILE_SIZE * 4.9]) { backdrop.fillStyle(0x5a7a49, 1) backdrop.fillRect(shrubX, courtyardY - TILE_SIZE * 0.42, TILE_SIZE * 0.72, TILE_SIZE * 0.42) backdrop.fillStyle(0x7ea56a, 1) backdrop.fillRect(shrubX + 2, courtyardY - TILE_SIZE * 0.54, TILE_SIZE * 0.48, TILE_SIZE * 0.22) } // Fence line across the campus front. backdrop.fillStyle(pal.fence, 1) for (let x = startX + TILE_SIZE * 0.8; x < endX; x += TILE_SIZE * 1.25) { if (x > gateCenterX - TILE_SIZE * 5.4 && x < gateCenterX + TILE_SIZE * 5.4) continue backdrop.fillRect(x, fenceY, 3, TILE_SIZE * 0.8) } backdrop.fillRect(startX + TILE_SIZE * 0.8, fenceY + TILE_SIZE * 0.6, gateCenterX - TILE_SIZE * 5.45 - (startX + TILE_SIZE * 0.8), 3) backdrop.fillRect(gateCenterX + TILE_SIZE * 5.45, fenceY + TILE_SIZE * 0.6, endX - TILE_SIZE * 0.8 - (gateCenterX + TILE_SIZE * 5.45), 3) backdrop.fillStyle(pal.road, 1) backdrop.fillRect(startX, roadY, width, roadHeight) backdrop.fillStyle(pal.roadDash, 1) for (let x = startX + TILE_SIZE; x < endX; x += TILE_SIZE * 2.6) { backdrop.fillRect(x, roadY + TILE_SIZE * 2.0, TILE_SIZE * 1.6, 4) } for (let x = startX + TILE_SIZE * 2; x < endX; x += TILE_SIZE * 4.8) { backdrop.fillStyle(pal.curb, 1) backdrop.fillRect(x, roadY - TILE_SIZE * 1.45, 4, TILE_SIZE * 1.45) backdrop.fillStyle(0xe5c670, 0.95) backdrop.fillRect(x - 4, roadY - TILE_SIZE * 1.5, 12, 4) } // Bollards + warm glow read as "night"; daytime keeps slim posts only so the lot still feels sun-lit. for (const lampX of [gateCenterX - TILE_SIZE * 4.8, gateCenterX - TILE_SIZE * 2.4, gateCenterX + TILE_SIZE * 2.4, gateCenterX + TILE_SIZE * 4.8, logoX - TILE_SIZE * 5.7, logoX + TILE_SIZE * 5.7]) { const lampY = lampX === logoX - TILE_SIZE * 5.7 || lampX === logoX + TILE_SIZE * 5.7 ? courtyardY + TILE_SIZE * 0.75 : forecourtNodeY + TILE_SIZE * 0.85 backdrop.fillStyle(0x4d4438, 1) backdrop.fillRect(lampX, lampY, 3, TILE_SIZE * 0.72) if (!isDay) { backdrop.fillStyle(0xefcf7a, 0.95) backdrop.fillRect(lampX - 4, lampY - 4, 11, 4) backdrop.fillStyle(0xf2dd9a, 0.2) backdrop.fillRect(lampX - TILE_SIZE * 0.38, lampY + TILE_SIZE * 0.42, TILE_SIZE * 0.76, TILE_SIZE * 0.24) } } // Full-width water below the road. backdrop.fillStyle(pal.water, 1) backdrop.fillRect(startX, waterTopY, width, bottomY - waterTopY) // Sparse ripple groups so the lake reads as water, without reverting to full-width dashed lines. const rippleRows = [waterTopY + TILE_SIZE * 0.9, waterTopY + TILE_SIZE * 2.0, waterTopY + TILE_SIZE * 3.25] for (let rowIndex = 0; rowIndex < rippleRows.length; rowIndex++) { const y = rippleRows[rowIndex] const offset = rowIndex % 2 === 0 ? TILE_SIZE * 1.1 : TILE_SIZE * 2.4 backdrop.fillStyle(pal.ripple, isDay ? 0.38 : 0.42) for (let x = startX + offset; x < endX - TILE_SIZE; x += TILE_SIZE * 5.2) { backdrop.fillRect(x, y, TILE_SIZE * 1.8, 2) backdrop.fillRect(x + TILE_SIZE * 2.35, y + 3, TILE_SIZE * 0.9, 2) } } // Localized ripple ring around the dock. backdrop.fillStyle(pal.rippleHi, isDay ? 0.38 : 0.34) for (const rippleY of [waterTopY + TILE_SIZE * 1.05, waterTopY + TILE_SIZE * 1.8]) { backdrop.fillRect(gateCenterX - TILE_SIZE * 4.8, rippleY, TILE_SIZE * 2.0, 2) backdrop.fillRect(gateCenterX + TILE_SIZE * 2.8, rippleY, TILE_SIZE * 2.0, 2) } const dockWidth = TILE_SIZE * 12.5 const pierTopY = roadY + roadHeight - 2 backdrop.fillStyle(0x6f543b, 1) backdrop.fillRect(gateCenterX - dockWidth / 2, dockY, dockWidth, TILE_SIZE * 1.75) backdrop.fillRect(gateCenterX - TILE_SIZE * 0.55, pierTopY, TILE_SIZE * 1.1, dockY - pierTopY) backdrop.fillStyle(0x8d6a49, 1) for (let x = gateCenterX - dockWidth / 2 + 6; x < gateCenterX + dockWidth / 2 - 6; x += TILE_SIZE * 0.7) { backdrop.fillRect(x, dockY + TILE_SIZE * 0.35, 3, TILE_SIZE * 1.2) } // Subtle retaining edge above the water so the dock reads cleanly. backdrop.fillStyle(0x5b6a62, 1) backdrop.fillRect(startX, waterTopY - 4, width, 4) backdrop.fillStyle(0x6f8177, 1) backdrop.fillRect(startX, waterTopY - 8, width, 3) if (this.outdoorParking) { const pi = this.decor.indexOf(this.outdoorParking) if (pi >= 0) this.decor.splice(pi, 1) this.outdoorParking.destroy() this.outdoorParking = null } const parkingG = scene.add.graphics() // Between outdoor backdrop (-320) and office shell (-140); must stay above lawn so asphalt wins. parkingG.setDepth(-300) const PARKING_ASPHALT = 0x3e434c const PARKING_LINE = 0xe8e2d8 const parkingLots = [ { x: startX + TILE_SIZE * 2.2, count: 5 }, { x: endX - TILE_SIZE * 11.4, count: 5 }, ] // Draw all slabs, then all strokes, then all cars — avoids Phaser Graphics path/state quirks between lots. parkingG.lineStyle(0, 0x000000, 0) parkingG.fillStyle(PARKING_ASPHALT, 1) for (const lot of parkingLots) { parkingG.fillRect(lot.x, parkingY, TILE_SIZE * 12.8, TILE_SIZE * 3.8) } parkingG.lineStyle(1, PARKING_LINE, 0.95) for (const lot of parkingLots) { for (let i = 0; i < lot.count; i++) { const slotX = lot.x + TILE_SIZE * 0.45 + i * TILE_SIZE * 1.5 parkingG.strokeRect(slotX, parkingY + TILE_SIZE * 0.5, TILE_SIZE * 1.55, TILE_SIZE * 2.55) } } parkingG.lineStyle(0, 0x000000, 0) const slotW = TILE_SIZE * 1.55 const slotH = TILE_SIZE * 2.55 const slotPadY = TILE_SIZE * 0.5 const nightLights = !isDay for (const lot of parkingLots) { const mirrorForSide = lot.x > gateCenterX - TILE_SIZE for (let i = 0; i < lot.count - 1; i += 2) { const slotX = lot.x + TILE_SIZE * 0.45 + i * TILE_SIZE * 1.5 const cx = slotX + slotW / 2 const cy = parkingY + slotPadY + slotH / 2 const warm = i % 4 === 0 this.paintStylizedParkingCar(parkingG, cx, cy, mirrorForSide, warm, nightLights) } } if (!opts?.skipLabels) { this.addPixelLabel(scene, gateCenterX, lobbyY + TILE_SIZE * 0.6, 'OPENOPC', '#4e6b68', '#eef4ec', '10px') const lawnLogoBaseY = courtyardY - TILE_SIZE * 0.24 const letterW = TILE_SIZE * 0.88 const letterGap = TILE_SIZE * 0.28 const word = 'OPENOPC' const totalLogoWidth = word.length * letterW + (word.length - 1) * letterGap let letterX = logoX - totalLogoWidth / 2 for (const char of word) { const shadow = scene.add.text(letterX + TILE_SIZE * 0.12, lawnLogoBaseY + TILE_SIZE * 0.14, char, { fontSize: '26px', fontFamily: 'monospace', color: '#4e6b46', resolution: 2, }) shadow.setOrigin(0, 0.5) shadow.setDepth(-95) this.decor.push(shadow) const face = scene.add.text(letterX, lawnLogoBaseY, char, { fontSize: '26px', fontFamily: 'monospace', color: '#f6f6ee', stroke: '#8f8c7a', strokeThickness: 1, resolution: 2, }) face.setOrigin(0, 0.5) face.setDepth(-94) this.decor.push(face) letterX += letterW + letterGap } this.addPixelLabel(scene, gateCenterX, dockY + TILE_SIZE * 0.95, 'LAKE DOCK', '#1d3d4f', '#d7eef1', '9px') } this.decor.push(backdrop, parkingG) this.outdoorBackdrop = backdrop this.outdoorParking = parkingG } /** Redraw outdoor environment when local time crosses the day/night boundary (keeps lawn text objects). */ refreshOutdoorDayNight(scene: Phaser.Scene, isDay: boolean) { const offices = getOffices() const totalWidth = ((offices[offices.length - 1]?.offsetCol ?? 0) + OFFICE_COLS) * TILE_SIZE const totalHeight = OFFICE_ROWS * TILE_SIZE if (this.outdoorParking) { const pi = this.decor.indexOf(this.outdoorParking) if (pi >= 0) this.decor.splice(pi, 1) this.outdoorParking.destroy() this.outdoorParking = null } if (this.outdoorBackdrop) { const i = this.decor.indexOf(this.outdoorBackdrop) if (i >= 0) this.decor.splice(i, 1) this.outdoorBackdrop.destroy() this.outdoorBackdrop = null } this.buildOutdoorBackdrop(scene, totalWidth, totalHeight, offices, isDay, { skipLabels: true }) } private buildBoundaryDecor(scene: Phaser.Scene, offices: OfficeConfig[], isDay: boolean) { const base = scene.add.graphics() base.setDepth(-140) const detail = scene.add.graphics() detail.setDepth(-95) const totalWidth = ((offices[offices.length - 1]?.offsetCol ?? 0) + OFFICE_COLS) * TILE_SIZE const totalHeight = OFFICE_ROWS * TILE_SIZE const officeWidth = OFFICE_COLS * TILE_SIZE const lobbyHeight = TILE_SIZE * 2 const wainscotY = TILE_SIZE * 20 this.buildOutdoorBackdrop(scene, totalWidth, totalHeight, offices, isDay) // Base shell for the whole floor plate. base.fillStyle(0x26211c, 1) base.fillRect(-TILE_SIZE, 0, totalWidth + TILE_SIZE * 2, totalHeight) base.fillStyle(0x312922, 1) base.fillRect(0, 0, totalWidth, totalHeight) // Unified perimeter so the three offices read as one complete floor plate. base.fillStyle(0x5c4c3f, 1) base.fillRect(0, 0, totalWidth, 8) base.fillRect(0, totalHeight - 8, totalWidth, 8) base.fillRect(0, 0, 8, totalHeight) base.fillRect(totalWidth - 8, 0, 8, totalHeight) base.fillStyle(0x241e19, 1) base.fillRect(8, 8, totalWidth - 16, 3) base.fillRect(8, totalHeight - 11, totalWidth - 16, 3) // Top lobby band. base.fillStyle(0x1a1714, 1) base.fillRect(0, 0, totalWidth, lobbyHeight) base.fillStyle(0x4b4035, 1) base.fillRect(0, lobbyHeight - 5, totalWidth, 5) // Global floor base + subtle horizontal zoning. base.fillStyle(0x40362d, 1) base.fillRect(0, lobbyHeight, totalWidth, totalHeight - lobbyHeight) base.fillStyle(0x4a3e34, 1) base.fillRect(0, TILE_SIZE * 8, totalWidth, TILE_SIZE * 4) base.fillStyle(0x3a3028, 1) base.fillRect(0, wainscotY, totalWidth, totalHeight - wainscotY) // Pixel-floor grid to make the scene feel like a cohesive map instead of empty fill. base.lineStyle(1, 0x5a4b3f, 0.32) for (let x = 0; x <= totalWidth; x += TILE_SIZE) { base.beginPath() base.moveTo(x, lobbyHeight) base.lineTo(x, totalHeight) base.strokePath() } for (let y = lobbyHeight; y <= totalHeight; y += TILE_SIZE) { base.beginPath() base.moveTo(0, y) base.lineTo(totalWidth, y) base.strokePath() } // Ceiling lights along the lobby. for (let x = TILE_SIZE * 3; x < totalWidth; x += TILE_SIZE * 6) { base.fillStyle(0xe0ba6f, 0.85) base.fillRect(x, TILE_SIZE * 0.7, TILE_SIZE * 1.5, 4) base.fillStyle(0x8e6f42, 1) base.fillRect(x + TILE_SIZE * 0.2, TILE_SIZE * 0.55, TILE_SIZE * 1.1, 2) } this.addPixelLabel(scene, totalWidth / 2, TILE_SIZE * 0.95, 'MAIN HALL', '#2a221b', '#f3dcc0', '11px') for (const office of offices) { const x = office.offsetCol * TILE_SIZE const centerX = x + officeWidth / 2 const officeIndex = offices.findIndex(o => o.id === office.id) // Outer office shell. base.fillStyle(0x58483b, 1) base.fillRect(x - 10, 0, officeWidth + 20, totalHeight) // Inner trim to visually separate the room cutaway from the shared floor plate. base.fillStyle(0x231d18, 1) base.fillRect(x - 4, 0, officeWidth + 8, totalHeight) base.lineStyle(2, 0x7b6858, 0.9) base.strokeRect(x - 10, 0, officeWidth + 20, totalHeight) base.lineStyle(2, 0xa7927e, 0.34) base.strokeRect(x - 4, 0, officeWidth + 8, totalHeight) // Lobby lintel + label rail. base.fillStyle(0x181513, 1) base.fillRect(x - 4, 0, officeWidth + 8, TILE_SIZE) base.fillStyle(0x6d5b4c, 1) base.fillRect(x + TILE_SIZE * 5.5, TILE_SIZE * 0.55, TILE_SIZE * 9, 5) // Corner blocks for a more intentional pixel-architecture look. base.fillStyle(0x8a7562, 1) base.fillRect(x - 10, 0, 8, 8) base.fillRect(x + officeWidth + 2, 0, 8, 8) base.fillRect(x - 10, totalHeight - 8, 8, 8) base.fillRect(x + officeWidth + 2, totalHeight - 8, 8, 8) // Lower wall trim to visually anchor each office to the floor. base.fillStyle(0x312820, 1) base.fillRect(x - 4, wainscotY, officeWidth + 8, 6) // Fill the upper side voids with small support zones using the imported tileset. const leftNookX = x + TILE_SIZE * 0.7 const rightNookX = x + TILE_SIZE * 14.65 const nookY = TILE_SIZE * 1.05 const nookW = TILE_SIZE * 4.45 const nookH = TILE_SIZE * 6.4 const supportBandY = nookY + nookH + TILE_SIZE * 0.35 const nookDecorOffsetY = TILE_SIZE * 0.55 const leftWallpaper = officeIndex % 2 === 0 ? 0x5d5245 : 0x4d5a46 const rightWallpaper = officeIndex % 2 === 0 ? 0x465459 : 0x5b4f46 const leftFloor = officeIndex % 2 === 0 ? 0x4f6e88 : 0x6f5b74 const rightFloor = officeIndex % 2 === 0 ? 0x567867 : 0x8a724f const leftRug = officeIndex % 2 === 0 ? 0x6b7fb5 : 0x9a6a7b const rightRug = officeIndex % 2 === 0 ? 0x5e8b79 : 0xa78758 // Stronger structural band between top support rooms and the main office. base.fillStyle(0x2b241f, 1) base.fillRect(x + TILE_SIZE * 0.58, supportBandY, officeWidth - TILE_SIZE * 1.16, TILE_SIZE * 0.44) base.fillStyle(0x75624f, 1) base.fillRect(x + TILE_SIZE * 0.92, supportBandY + 2, officeWidth - TILE_SIZE * 1.84, 2) base.fillRect(x + TILE_SIZE * 4.3, supportBandY - TILE_SIZE * 0.2, 4, TILE_SIZE * 0.84) base.fillRect(x + officeWidth - TILE_SIZE * 4.3, supportBandY - TILE_SIZE * 0.2, 4, TILE_SIZE * 0.84) // Frame and floor treatment so these areas read as fitted support rooms, not empty voids. detail.fillStyle(0x1d1916, 1) detail.fillRect(leftNookX, nookY, nookW, nookH) detail.fillRect(rightNookX, nookY, nookW, nookH) detail.fillStyle(0x2f2922, 1) detail.fillRect(leftNookX + 4, nookY + 4, nookW - 8, nookH - 8) detail.fillRect(rightNookX + 4, nookY + 4, nookW - 8, nookH - 8) detail.fillStyle(leftWallpaper, 1) detail.fillRect(leftNookX + 4, nookY + 4, nookW - 8, TILE_SIZE * 0.7) detail.fillStyle(rightWallpaper, 1) detail.fillRect(rightNookX + 4, nookY + 4, nookW - 8, TILE_SIZE * 0.7) detail.fillStyle(leftFloor, 0.95) detail.fillRect(leftNookX + 4, nookY + TILE_SIZE * 1.15, nookW - 8, nookH - TILE_SIZE * 1.45) detail.fillStyle(rightFloor, 0.95) detail.fillRect(rightNookX + 4, nookY + TILE_SIZE * 1.15, nookW - 8, nookH - TILE_SIZE * 1.45) detail.fillStyle(0xe6d2a4, 0.16) detail.fillRect(leftNookX + 10, nookY + 10, nookW - 20, 2) detail.fillRect(rightNookX + 10, nookY + 10, nookW - 20, 2) detail.lineStyle(1, 0x5e5244, 0.28) for (let floorX = leftNookX + 12; floorX < leftNookX + nookW - 4; floorX += TILE_SIZE) { detail.beginPath() detail.moveTo(floorX, nookY + TILE_SIZE * 1.15) detail.lineTo(floorX, nookY + nookH - 4) detail.strokePath() } for (let floorX = rightNookX + 12; floorX < rightNookX + nookW - 4; floorX += TILE_SIZE) { detail.beginPath() detail.moveTo(floorX, nookY + TILE_SIZE * 1.15) detail.lineTo(floorX, nookY + nookH - 4) detail.strokePath() } for (let floorY = nookY + TILE_SIZE * 1.45; floorY < nookY + nookH - 4; floorY += TILE_SIZE * 0.82) { detail.beginPath() detail.moveTo(leftNookX + 4, floorY) detail.lineTo(leftNookX + nookW - 4, floorY) detail.strokePath() detail.beginPath() detail.moveTo(rightNookX + 4, floorY) detail.lineTo(rightNookX + nookW - 4, floorY) detail.strokePath() } detail.fillStyle(leftRug, 0.94) detail.fillRect(leftNookX + TILE_SIZE * 0.45, nookY + TILE_SIZE * 3.95, TILE_SIZE * 2.7, TILE_SIZE * 1.25) detail.fillStyle(0xe8ddc7, 0.34) detail.strokeRect(leftNookX + TILE_SIZE * 0.45, nookY + TILE_SIZE * 3.95, TILE_SIZE * 2.7, TILE_SIZE * 1.25) detail.fillRect(leftNookX + TILE_SIZE * 0.75, nookY + TILE_SIZE * 4.35, TILE_SIZE * 2.1, 3) detail.fillStyle(rightRug, 0.94) detail.fillRect(rightNookX + TILE_SIZE * 0.45, nookY + TILE_SIZE * 3.95, TILE_SIZE * 2.7, TILE_SIZE * 1.25) detail.fillStyle(0xe8ddc7, 0.34) detail.strokeRect(rightNookX + TILE_SIZE * 0.45, nookY + TILE_SIZE * 3.95, TILE_SIZE * 2.7, TILE_SIZE * 1.25) detail.fillRect(rightNookX + TILE_SIZE * 0.75, nookY + TILE_SIZE * 4.35, TILE_SIZE * 2.1, 3) detail.fillStyle(0xe7c97f, 0.85) detail.fillRect(leftNookX + TILE_SIZE * 1.45, nookY + 12, TILE_SIZE * 1.55, 4) detail.fillRect(rightNookX + TILE_SIZE * 1.45, nookY + 12, TILE_SIZE * 1.55, 4) detail.fillStyle(0x5e4d3f, 0.5) detail.fillRect(leftNookX + 4, nookY + nookH - 10, nookW - 8, 4) detail.fillRect(rightNookX + 4, nookY + nookH - 10, nookW - 8, 4) detail.fillStyle(0x7a6651, 1) detail.fillRect(leftNookX + TILE_SIZE * 0.22, nookY + nookH - TILE_SIZE * 0.7, nookW - TILE_SIZE * 0.44, 3) detail.fillRect(rightNookX + TILE_SIZE * 0.22, nookY + nookH - TILE_SIZE * 0.7, nookW - TILE_SIZE * 0.44, 3) this.addPixelLabel(scene, leftNookX + nookW / 2, nookY + TILE_SIZE * 0.42, officeIndex % 2 === 0 ? 'ARCHIVE' : 'UTILITY', '#4a4035', '#f1dfc2', '8px') this.addPixelLabel(scene, rightNookX + nookW / 2, nookY + TILE_SIZE * 0.42, officeIndex % 2 === 0 ? 'PANTRY' : 'SUPPLY', '#4a4035', '#f1dfc2', '8px') if (officeIndex % 2 === 0) { this.addTilesetSprite(scene, leftNookX + 12, nookY + 14 + nookDecorOffsetY, this.tilesetFrames.shelfA) this.addTilesetSprite(scene, leftNookX + 44, nookY + 14 + nookDecorOffsetY, this.tilesetFrames.shelfB) this.addTilesetSprite(scene, leftNookX + 12, nookY + 48 + nookDecorOffsetY, this.tilesetFrames.shelfA) this.addTilesetSprite(scene, leftNookX + 14, nookY + 50 + nookDecorOffsetY, this.tilesetFrames.boxC) this.addTilesetSprite(scene, leftNookX + 26, nookY + 82 + nookDecorOffsetY, this.tilesetFrames.boxA) this.addTilesetSprite(scene, leftNookX + 58, nookY + 82 + nookDecorOffsetY, this.tilesetFrames.boxB) this.addTilesetSprite(scene, leftNookX + 60, nookY + 48 + nookDecorOffsetY, this.tilesetFrames.plantA) this.addTilesetSprite(scene, rightNookX + 22, nookY + 18 + nookDecorOffsetY, this.tilesetFrames.waterCooler) this.addTilesetSprite(scene, rightNookX + 56, nookY + 18 + nookDecorOffsetY, this.tilesetFrames.plantA) this.addTilesetSprite(scene, rightNookX + 20, nookY + 50 + nookDecorOffsetY, this.tilesetFrames.shelfA) this.addTilesetSprite(scene, rightNookX + 18, nookY + 82 + nookDecorOffsetY, this.tilesetFrames.fridge) this.addTilesetSprite(scene, rightNookX + 54, nookY + 82 + nookDecorOffsetY, this.tilesetFrames.boxA) } else { this.addTilesetSprite(scene, leftNookX + 16, nookY + 18 + nookDecorOffsetY, this.tilesetFrames.waterCoolerAlt) this.addTilesetSprite(scene, leftNookX + 52, nookY + 18 + nookDecorOffsetY, this.tilesetFrames.plantB) this.addTilesetSprite(scene, leftNookX + 16, nookY + 52 + nookDecorOffsetY, this.tilesetFrames.shelfB) this.addTilesetSprite(scene, leftNookX + 24, nookY + 84 + nookDecorOffsetY, this.tilesetFrames.boxC) this.addTilesetSprite(scene, leftNookX + 58, nookY + 84 + nookDecorOffsetY, this.tilesetFrames.boxA) this.addTilesetSprite(scene, rightNookX + 10, nookY + 14 + nookDecorOffsetY, this.tilesetFrames.shelfB) this.addTilesetSprite(scene, rightNookX + 42, nookY + 14 + nookDecorOffsetY, this.tilesetFrames.shelfA) this.addTilesetSprite(scene, rightNookX + 10, nookY + 50 + nookDecorOffsetY, this.tilesetFrames.waterCooler) this.addTilesetSprite(scene, rightNookX + 26, nookY + 82 + nookDecorOffsetY, this.tilesetFrames.vending) this.addTilesetSprite(scene, rightNookX + 58, nookY + 82 + nookDecorOffsetY, this.tilesetFrames.plantB) } } for (let i = 0; i < offices.length - 1; i++) { const gapX = (offices[i].offsetCol + OFFICE_COLS) * TILE_SIZE const gapWidth = GAP_COLS * TILE_SIZE const centerX = gapX + gapWidth / 2 const isSharedGap = i === 0 const accent = isSharedGap ? 0x5f7d63 : 0x6a6f8f const zoneLabel = isSharedGap ? 'SHARE' : 'MEET' // Structural wall strip. base.fillStyle(0x201b17, 1) base.fillRect(gapX, 0, gapWidth, totalHeight) // Main corridor shaft. base.fillStyle(0x3a3028, 1) base.fillRect(gapX + 5, lobbyHeight, gapWidth - 10, totalHeight - lobbyHeight) base.fillStyle(0x4f4338, 1) for (let y = lobbyHeight + 6; y < totalHeight - 6; y += 14) { base.fillRect(gapX + 8, y, gapWidth - 16, 2) } // Structural core / service shaft treatment inside each vertical connection strip. base.fillStyle(0x27211c, 1) base.fillRect(centerX - TILE_SIZE * 0.52, TILE_SIZE * 2.4, TILE_SIZE * 1.04, totalHeight - TILE_SIZE * 4.8) base.fillStyle(0x65584c, 1) base.fillRect(centerX - 2, TILE_SIZE * 2.7, 4, totalHeight - TILE_SIZE * 5.4) base.fillStyle(0x8a7866, 0.88) base.fillRect(centerX - TILE_SIZE * 0.68, TILE_SIZE * 3.15, TILE_SIZE * 0.3, TILE_SIZE * 1.25) base.fillRect(centerX + TILE_SIZE * 0.38, TILE_SIZE * 3.15, TILE_SIZE * 0.3, TILE_SIZE * 1.25) base.fillRect(centerX - TILE_SIZE * 0.68, TILE_SIZE * 15.6, TILE_SIZE * 0.3, TILE_SIZE * 1.25) base.fillRect(centerX + TILE_SIZE * 0.38, TILE_SIZE * 15.6, TILE_SIZE * 0.3, TILE_SIZE * 1.25) // Corridor center line. base.lineStyle(2, 0x7d6a56, 0.8) base.beginPath() base.moveTo(centerX, lobbyHeight + 8) base.lineTo(centerX, totalHeight - 8) base.strokePath() // Shared area / meeting area node in each connection strip. base.fillStyle(accent, 0.95) base.fillRect(gapX + 7, TILE_SIZE * 11.5, gapWidth - 14, TILE_SIZE * 2.2) base.lineStyle(1, 0xd8d1b7, 0.18) base.strokeRect(gapX + 7, TILE_SIZE * 11.5, gapWidth - 14, TILE_SIZE * 2.2) if (isSharedGap) { // Shared zone: sofa, table, coffee stand, plant. detail.fillStyle(0x845f4f, 1) detail.fillRect(centerX - 12, TILE_SIZE * 11.95, 24, 7) detail.fillStyle(0xd8c292, 1) detail.fillRect(centerX - 5, TILE_SIZE * 12.2, 10, 4) detail.fillStyle(0x9cab77, 1) detail.fillRect(centerX - 15, TILE_SIZE * 13.1, 5, 6) detail.fillStyle(0xa37b57, 1) detail.fillRect(centerX + 8, TILE_SIZE * 13.0, 7, 7) detail.fillStyle(0xe6d8bb, 0.85) detail.fillRect(centerX + 10, TILE_SIZE * 13.25, 3, 3) } else { // Meeting zone: table, four seats, presentation board. detail.fillStyle(0x8f7657, 1) detail.fillRect(centerX - 8, TILE_SIZE * 12.1, 16, 9) detail.fillStyle(0x4c5976, 1) detail.fillRect(centerX - 13, TILE_SIZE * 12.55, 4, 4) detail.fillRect(centerX + 9, TILE_SIZE * 12.55, 4, 4) detail.fillRect(centerX - 2, TILE_SIZE * 11.7, 4, 4) detail.fillRect(centerX - 2, TILE_SIZE * 13.95, 4, 4) detail.fillStyle(0xd8e3ea, 0.85) detail.fillRect(centerX - 14, TILE_SIZE * 11.65, 6, 3) } // Corridor lights. base.fillStyle(0xe0ba6f, 0.9) base.fillRect(centerX - 5, TILE_SIZE * 4.5, 10, 3) base.fillRect(centerX - 5, TILE_SIZE * 18.5, 10, 3) this.addPixelLabel(scene, centerX, TILE_SIZE * 8.75, zoneLabel, '#2f2822', '#f1dcc0', '8px') } // Outer edges read as a full stitched floor plan. base.fillStyle(0x191614, 1) base.fillRect(0, totalHeight - 6, totalWidth, 6) base.fillRect(0, 0, 6, totalHeight) base.fillRect(totalWidth - 6, 0, 6, totalHeight) // Simple wayfinding markers along the main hall. this.addPixelLabel(scene, TILE_SIZE * 7, TILE_SIZE * 1.0, '< WEST', '#2d241c', '#e8d5b6', '8px') this.addPixelLabel(scene, totalWidth - TILE_SIZE * 7, TILE_SIZE * 1.0, 'EAST >', '#2d241c', '#e8d5b6', '8px') this.decor.push(base, detail) } buildMap(scene: Phaser.Scene, isDay = isLocalDaytime()): MapData { this.clearSceneDecor() const offices = getOffices() const totalWidth = ((offices[offices.length - 1]?.offsetCol ?? 0) + OFFICE_COLS) * TILE_SIZE const totalHeight = OFFICE_ROWS * TILE_SIZE const waterfrontMetrics = this.getOutdoorMetrics(totalWidth, totalHeight) this.buildBoundaryDecor(scene, offices, isDay) for (const office of offices) { const centerX = (office.offsetCol + OFFICE_COLS / 2) * TILE_SIZE const bg = scene.add.image(office.offsetCol * TILE_SIZE, 0, 'office-bg') bg.setOrigin(0, 0) bg.setDepth(-100) this.bgSprites.push(bg) this.addForegroundOccluders(scene, office) const label = scene.add.text( centerX, 6, office.name, { fontSize: '14px', fontFamily: 'monospace', color: '#ffffff', stroke: '#000000', strokeThickness: 3, align: 'center' }, ) label.setOrigin(0.5, 0) label.setDepth(99999) label.setData('officeId', office.id) this.nameLabels.push(label) } const grid = buildCompositeGrid(offices) const wallBodies = OfficeMapBuilder.buildWallBodies(scene, grid) console.log('[OfficeMapBuilder] Composite map ready —', offices.length, 'offices') return { wallBodies, collisionGrid: grid, waterfront: { dockCenterX: waterfrontMetrics.gateCenterX, dockY: waterfrontMetrics.dockY, waterTopY: waterfrontMetrics.waterTopY, waterBottomY: waterfrontMetrics.bottomY, }, } } getBackgroundSprites(): Phaser.GameObjects.Image[] { return this.bgSprites } getNameLabels(): Phaser.GameObjects.Text[] { return this.nameLabels } updateLabel(officeId: string, name: string) { const label = this.nameLabels.find(l => l.getData('officeId') === officeId) if (label) label.setText(name) } static buildWallBodies( scene: Phaser.Scene, grid: number[][], ): Phaser.Physics.Arcade.StaticGroup { const rows = grid.length const cols = rows > 0 ? grid[0].length : 0 const group = scene.physics.add.staticGroup() for (let r = 0; r < rows; r++) { let start = -1 for (let c = 0; c <= cols; c++) { const isWall = c < cols && grid[r][c] === 1 if (isWall && start < 0) { start = c } else if (!isWall && start >= 0) { const len = c - start const body = scene.add.rectangle( start * TILE_SIZE + (len * TILE_SIZE) / 2, r * TILE_SIZE + TILE_SIZE / 2, len * TILE_SIZE, TILE_SIZE, ) body.setVisible(false) group.add(body) start = -1 } } } return group } static buildWallBodiesForRegion( scene: Phaser.Scene, grid: number[][], existingGroup: Phaser.Physics.Arcade.StaticGroup, offsetCol: number, ) { const rows = grid.length const cols = rows > 0 ? grid[0].length : 0 for (let r = 0; r < rows; r++) { let start = -1 for (let c = 0; c <= cols; c++) { const isWall = c < cols && grid[r][c] === 1 if (isWall && start < 0) { start = c } else if (!isWall && start >= 0) { const len = c - start const body = scene.add.rectangle( (offsetCol + start) * TILE_SIZE + (len * TILE_SIZE) / 2, r * TILE_SIZE + TILE_SIZE / 2, len * TILE_SIZE, TILE_SIZE, ) body.setVisible(false) existingGroup.add(body) start = -1 } } } } }