1325 lines
58 KiB
TypeScript
1325 lines
58 KiB
TypeScript
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|