// office geometry + canvas floor rendering. 640x480 internal (UI_UX.md). // COZY 16-BIT DEPTH PASS (user reference): every piece of furniture has a // dark outline, a lit top surface and a shaded front face; walls have // height; objects cast contact shadows. Geometry/colliders unchanged. // The static scene is pre-rendered once to an offscreen canvas; only // animated props (clock, rain, ringing phone, door glow…) draw per frame. export const W = 640, H = 480, WALL = 10; export const ROOMS = { boardroom: { x: WALL, y: WALL, w: 220, h: 160 }, // upper left breakroom: { x: 450, y: WALL, w: 180, h: 140 }, // upper right }; export const SPOTS = { playerDesk: { x: 150, y: 255 }, inbox: { x: 205, y: 252 }, coffee: { x: 590, y: 64 }, plant: { x: 612, y: 446 }, printer: { x: 420, y: 436 }, clock: { x: 330, y: 40 }, window: { x: 4, y: 250 }, phone: { x: 185, y: 250 }, // interact spot beside the desk, open approach boardroomDoor: { x: 236, y: 120 }, newspaperLanding: { x: 330, y: 290 }, reception: { x: 320, y: 446 }, playerStart: { x: 320, y: 300 }, }; export const NPC_SPOTS = { brad: { x: 310, y: 205 }, stacey: { x: 470, y: 235 }, kevin: { x: 560, y: 335 }, janet: { x: 360, y: 385 }, derek: { x: 175, y: 385 }, }; function deskCollider(s) { return { x: s.x - 26, y: s.y + 6, w: 52, h: 34 }; } export const COLLIDERS = [ // outer walls (top wall is deep: it has a visible face) { x: 0, y: 0, w: W, h: 26 }, { x: 0, y: H - WALL, w: W, h: WALL }, { x: 0, y: 0, w: WALL, h: H }, { x: W - WALL, y: 0, w: WALL, h: H }, // boardroom is a solid block in office view — entered via the door scene { x: ROOMS.boardroom.x, y: ROOMS.boardroom.y, w: ROOMS.boardroom.w, h: ROOMS.boardroom.h }, // break room walls with an opening at the bottom (x 500..560) { x: 450, y: WALL, w: 6, h: 140 }, // left wall { x: 450, y: 144, w: 50, h: 6 }, // bottom left segment { x: 560, y: 144, w: 70, h: 6 }, // bottom right segment // coffee machine + props { x: SPOTS.coffee.x - 12, y: SPOTS.coffee.y - 10, w: 26, h: 24 }, { x: SPOTS.plant.x - 8, y: SPOTS.plant.y - 6, w: 18, h: 20 }, { x: SPOTS.printer.x - 14, y: SPOTS.printer.y - 8, w: 30, h: 22 }, { x: SPOTS.reception.x - 30, y: SPOTS.reception.y - 6, w: 60, h: 18 }, // desks deskCollider(SPOTS.playerDesk), ...Object.values(NPC_SPOTS).map(deskCollider), ]; export function collides(px, py) { // player feet box: 16 wide, 10 tall, anchored at sprite bottom-center const box = { x: px - 8, y: py - 4, w: 16, h: 10 }; return COLLIDERS.some((c) => box.x < c.x + c.w && box.x + box.w > c.x && box.y < c.y + c.h && box.y + box.h > c.y); } /* ---------------------------------------------------------------- palette */ // aligned to the Design System warmlight theme (office34.js THEMES.warmlight) const P = { ink: "#2e2a22", // the 16-bit outline — warm dark ink tileA: "#8d8b82", tileB: "#878579", tileSeam: "#76746b", wallCap: "#ece8dc", wallFace: "#d8d4c8", wallShade: "#bcb7a8", baseboard: "#b4af9f", glass: "rgba(140,200,235,.32)", glassFrame: "#8e94a6", glassLine: "rgba(255,255,255,.65)", sky: "#aedcf2", skyHi: "#cdeaf8", bldg: "#9fb2c4", bldgLit: "#f4f9fd", bldgLit2: "#ffe9a8", woodFloorA: "#b58a55", woodFloorB: "#a87c45", woodSeam: "#8a6230", meetFloorA: "#878579", meetFloorB: "#807e72", woodTop: "#c3a169", woodTopHi: "#dcbb82", woodFace: "#997b46", deskTop: "#e9e4d3", deskTopHi: "#f4f0e2", deskFace: "#c6c2b6", deskFaceLo: "#b3ab95", partTop: "#ece8dc", partFace: "#bdb8a6", partCloth: "#a7a08c", monitor: "#1c3a5a", monitorFrame: "#2b2e3a", monitorSide: "#23242c", screenGlow: "#7fb2ec", chairTop: "#3b3e46", chairHi: "#4a4e58", chairFace: "#2f323a", paper: "#faf7ee", paperShadow: "#c7c0aa", metalTop: "#cfccc4", metalHi: "#e6e2d6", metalFace: "#a8a392", shadow: "rgba(46,42,34,.30)", shadowSoft: "rgba(46,42,34,.16)", plantPot: "#b35c3a", plantPotHi: "#cf7450", leaf: "#3f9e52", leafHi: "#5cc46e", leafLo: "#2c7a3d", mug: "#e25b4a", book: "#8e6fd8", folder: "#3aa3e0", binder2: "#e6b33c", binder3: "#d9534f", lamp: "#5b5e6b", lampGlow: "#ffd98c", case: "#2e2f37", caseHi: "#46474f", label: "#6e6e7e", }; let staticScene = null; // offscreen canvas, built once let rainDrops = []; // labels are baked into the static scene — rebuild once the pixel font lands if (typeof document !== "undefined" && document.fonts) document.fonts.ready.then(() => { staticScene = null; }); /* ------------------------------------------------------------ tiny helpers */ function r(c, x, y, w, h, color) { c.fillStyle = color; c.fillRect(x, y, w, h); } function shade(c, x, y, w, soft) { // contact shadow: a wide low rect hugging the object's south edge r(c, x + 1, y, w - 2, 4, soft ? P.shadowSoft : P.shadow); r(c, x + 3, y + 4, w - 6, 2, P.shadowSoft); } /* the core 16-bit primitive: outlined block with lit top + shaded front face */ function box3d(c, x, y, w, topH, faceH, top, face, hi, outline = P.ink) { r(c, x - 1, y - 1, w + 2, topH + faceH + 2, outline); r(c, x, y, w, topH, top); if (hi) r(c, x, y, w, 2, hi); r(c, x, y + topH, w, faceH, face); r(c, x, y + topH, w, 1, "rgba(0,0,0,.14)"); // crease under the lip } function plant(c, x, y, big) { const s = big ? 1.4 : 1; shade(c, x - 8 * s, y + 9 * s, 17 * s, true); box3d(c, x - 6 * s, y, 12 * s, 3 * s, 8 * s, P.plantPotHi, P.plantPot); r(c, x - 8 * s, y - 9 * s, 6 * s, 10 * s, P.leaf); r(c, x + 1 * s, y - 12 * s, 6 * s, 13 * s, P.leafHi); r(c, x - 3 * s, y - 7 * s, 5 * s, 8 * s, P.leafLo); r(c, x - 1 * s, y - 15 * s, 3 * s, 7 * s, P.leaf); r(c, x - 2 * s, y - 16 * s, 1, 1, "#7fe08f"); } function chair(c, x, y) { shade(c, x - 7, y + 7, 15, true); box3d(c, x - 5, y - 9, 11, 6, 3, P.chairTop, P.chairFace, P.chairHi); // back box3d(c, x - 6, y - 1, 13, 5, 4, P.chairTop, P.chairFace, P.chairHi); // seat } function monitor(c, x, y, lit = true) { r(c, x - 9, y - 7, 19, 15, P.ink); r(c, x - 8, y - 6, 17, 13, P.monitorFrame); r(c, x + 7, y - 6, 2, 13, P.monitorSide); // CRT side depth r(c, x - 6, y - 4, 12, 8, P.monitor); if (lit) { r(c, x - 5, y - 3, 8, 1, P.screenGlow); r(c, x - 5, y - 1, 5, 1, "#4d7fc0"); r(c, x - 5, y + 1, 7, 1, "#4d7fc0"); } r(c, x - 2, y + 7, 5, 2, P.ink); // stand } function papers(c, x, y) { r(c, x, y, 8, 6, P.paperShadow); r(c, x - 1, y - 1, 8, 6, P.paper); r(c, x - 1, y - 1, 8, 1, "#f0f0f4"); r(c, x + 1, y + 1, 5, 1, P.paperShadow); } function keyboard(c, x, y) { r(c, x - 1, y - 1, 14, 7, P.ink); r(c, x, y, 12, 5, "#a4a7b4"); r(c, x + 1, y + 1, 10, 1, "#c9ccd6"); } function deskLamp(c, x, y) { r(c, x, y + 4, 6, 2, P.lamp); // base r(c, x + 2, y - 2, 2, 6, P.lamp); // arm r(c, x, y - 5, 7, 4, P.ink); // head r(c, x + 1, y - 4, 5, 2, P.lamp); r(c, x + 1, y - 1, 5, 1, P.lampGlow); // warm light } function landline(c, x, y) { r(c, x - 1, y - 1, 12, 9, P.ink); r(c, x, y, 10, 7, "#b9bcc8"); r(c, x, y, 10, 2, "#d4d6de"); r(c, x + 1, y - 3, 8, 3, "#8e919e"); // handset on top r(c, x + 6, y + 3, 3, 3, "#7d8290"); // keypad } function briefcase(c, x, y) { shade(c, x - 1, y + 9, 14, true); r(c, x - 1, y - 1, 14, 11, P.ink); r(c, x, y, 12, 9, P.case); r(c, x, y, 12, 2, P.caseHi); r(c, x + 4, y - 3, 4, 3, P.ink); // handle r(c, x + 5, y - 2, 2, 1, P.case); r(c, x + 5, y + 4, 2, 2, "#8c8f9b"); // clasp } function shelfUnit(c, x, y) { box3d(c, x, y, 40, 16, 7, "#e8e8ed", "#c2c4ce", "#f6f6f9"); r(c, x + 1, y + 7, 38, 1, "#b6b6c1"); [[3, P.folder], [9, P.binder2], [15, P.binder3], [21, P.book], [27, P.folder]].forEach(([off, col]) => { r(c, x + off, y + 2, 5, 8, col); r(c, x + off, y + 2, 5, 1, "rgba(255,255,255,.4)"); }); papers(c, x + 33, y + 3); } /* a full cubicle: outlined partitions with faces, white desk with front */ function cubicle(c, s, accent, opts = {}) { const dx = s.x - 26, dy = s.y + 6; // matches deskCollider const py = s.y - 34; // back partition: cap, cloth panel, face — fully outlined r(c, dx - 5, py - 1, 62, 30, P.ink); r(c, dx - 4, py, 60, 4, P.partTop); r(c, dx - 4, py + 4, 60, 18, P.partFace); r(c, dx - 2, py + 6, 56, 14, P.partCloth); r(c, dx - 2, py + 6, 56, 1, "rgba(255,255,255,.35)"); r(c, dx - 4, py + 22, 60, 6, "#9da4b6"); // partition front face // pinned note + identity accent stripe r(c, dx + 4, py + 8, 6, 5, P.paper); r(c, dx + 44, py + 8, 7, 5, accent); r(c, dx + 44, py + 8, 7, 1, "rgba(255,255,255,.45)"); // side wings (outlined) r(c, dx - 9, py + 3, 7, 58, P.ink); r(c, dx - 8, py + 4, 5, 56, P.partFace); r(c, dx - 8, py + 4, 5, 2, P.partTop); r(c, dx + 54, py + 3, 7, 58, P.ink); r(c, dx + 55, py + 4, 5, 56, P.partFace); r(c, dx + 55, py + 4, 5, 2, P.partTop); // desk: lit top + front face + outline + contact shadow shade(c, dx - 2, dy + 31, 58); box3d(c, dx, dy, 52, 20, 10, P.deskTop, P.deskFace, P.deskTopHi); r(c, dx, dy + 26, 52, 2, P.deskFaceLo); // desk gear monitor(c, s.x, dy + 5, !opts.darkMonitor); keyboard(c, s.x - 6, dy + 15); papers(c, dx + 5, dy + 13); deskLamp(c, dx + 41, dy + 6); r(c, dx + 44, dy + 16, 5, 5, P.mug); r(c, dx + 44, dy + 16, 5, 1, "#f2917f"); if (opts.phone) landline(c, dx + 3, dy + 3); if (opts.books) { r(c, dx + 3, dy + 14, 4, 7, P.book); r(c, dx + 8, dy + 15, 4, 6, P.folder); } } /* --------------------------------------------------------- the static scene */ function buildStatic() { const cv = document.createElement("canvas"); cv.width = W; cv.height = H; const c = cv.getContext("2d"); // ---- light tile floor with thin grid seams for (let ty = 0; ty < H / 32; ty++) for (let tx = 0; tx < W / 32; tx++) r(c, tx * 32, ty * 32, 32, 32, (tx + ty) % 2 ? P.tileA : P.tileB); c.globalAlpha = 0.85; for (let x = 0; x <= W; x += 32) r(c, x, 0, 1, H, P.tileSeam); for (let y = 0; y <= H; y += 32) r(c, 0, y, W, 1, P.tileSeam); c.globalAlpha = 1; // sunlight patches under the corridor windows c.globalAlpha = 0.10; r(c, 244, 30, 92, 66, "#fff3c4"); r(c, 340, 30, 92, 66, "#fff3c4"); c.globalAlpha = 1; // ---- boardroom interior — warm meeting room const br = ROOMS.boardroom; for (let ty = 0; ty < br.h / 16; ty++) for (let tx = 0; tx < br.w / 16; tx++) r(c, br.x + tx * 16, br.y + ty * 16, 16, 16, (tx + ty) % 2 ? P.meetFloorA : P.meetFloorB); // north wall with real height (cap + face + outline) r(c, br.x, br.y, br.w, 4, P.wallCap); r(c, br.x, br.y + 4, br.w, 16, P.wallFace); r(c, br.x, br.y + 18, br.w, 2, P.wallShade); r(c, br.x, br.y + 20, br.w, 1, P.ink); // area rug r(c, br.x + 38, br.y + 52, 146, 74, "#b6543e"); r(c, br.x + 42, br.y + 56, 138, 66, "#c4664e"); // projector screen mounted on the wall face r(c, br.x + 63, br.y + 5, 92, 27, P.ink); r(c, br.x + 64, br.y + 6, 90, 25, "#5d5d68"); r(c, br.x + 67, br.y + 9, 84, 19, "#fdfdfe"); r(c, br.x + 67, br.y + 9, 84, 4, "#e8e8ee"); // big table: wood top + front face + outline shade(c, br.x + 46, br.y + 119, 130); box3d(c, br.x + 46, br.y + 56, 128, 50, 12, P.woodTop, P.woodFace, P.woodTopHi); papers(c, br.x + 70, br.y + 74); papers(c, br.x + 130, br.y + 86); r(c, br.x + 101, br.y + 78, 16, 11, P.ink); r(c, br.x + 102, br.y + 79, 14, 9, P.metalTop); // speakerphone r(c, br.x + 107, br.y + 82, 4, 3, "#6e7180"); [[64, 44], [104, 44], [144, 44], [64, 128], [104, 128], [144, 128]] .forEach(([x, y]) => chair(c, br.x + x, br.y + y)); plant(c, br.x + 16, br.y + 140); briefcase(c, br.x + 196, br.y + 132); // boardroom side walls + south wall with outside face r(c, br.x, br.y, 6, br.h, P.wallCap); r(c, br.x + 5, br.y, 1, br.h, P.ink); r(c, br.x, br.y + br.h - 6, br.w, 6, P.wallCap); r(c, br.x, br.y + br.h, br.w - 6, 10, P.wallFace); // face seen from hall r(c, br.x, br.y + br.h + 8, br.w - 6, 2, P.wallShade); r(c, br.x, br.y + br.h + 10, br.w - 6, 1, P.ink); // right wall: glass top section + door gap (90..140) + cap below r(c, br.x + br.w - 6, br.y, 6, 14, P.wallCap); r(c, br.x + br.w - 6, br.y, 6, 1, P.ink); r(c, br.x + br.w - 7, br.y + 14, 8, 70, P.glassFrame); r(c, br.x + br.w - 5, br.y + 16, 4, 66, P.glass); for (let i = 0; i < 4; i++) r(c, br.x + br.w - 5, br.y + 18 + i * 17, 4, 1, P.glassLine); r(c, br.x + br.w - 6, br.y + 84, 6, 6, P.wallCap); r(c, br.x + br.w - 6, br.y + 140, 6, 30, P.wallCap); r(c, br.x + br.w - 1, br.y, 1, br.h, P.ink); // door frame r(c, br.x + br.w - 7, br.y + 88, 8, 3, P.ink); r(c, br.x + br.w - 7, br.y + 139, 8, 3, P.ink); // BOARDROOM plaque on the wall face r(c, br.x + 55, br.y + 5, 82, 13, P.ink); r(c, br.x + 56, br.y + 6, 80, 11, "#fdfdfe"); c.font = "7px 'Press Start 2P'"; c.fillStyle = "#34343f"; c.fillText("BOARDROOM", br.x + 61, br.y + 15); // ---- break room interior — warm wood planks const bk = ROOMS.breakroom; for (let py = 0; py < bk.h; py += 10) { r(c, bk.x, bk.y + py, bk.w, 10, (py / 10) % 2 ? P.woodFloorA : P.woodFloorB); r(c, bk.x, bk.y + py, bk.w, 1, P.woodSeam); } // north wall face inside the break room r(c, bk.x, bk.y, bk.w, 4, P.wallCap); r(c, bk.x, bk.y + 4, bk.w, 14, P.wallFace); r(c, bk.x, bk.y + 16, bk.w, 2, P.wallShade); r(c, bk.x, bk.y + 18, bk.w, 1, P.ink); // counter: lit top + face + outline shade(c, bk.x + 6, bk.y + 39, 124); box3d(c, bk.x + 6, bk.y + 10, 124, 18, 10, P.deskTop, P.deskFace, P.deskTopHi); r(c, bk.x + 16, bk.y + 14, 16, 10, P.ink); r(c, bk.x + 17, bk.y + 15, 14, 8, "#a9adba"); // sink r(c, bk.x + 19, bk.y + 17, 10, 4, "#8a8e9c"); r(c, bk.x + 44, bk.y + 14, 10, 8, P.mug); // mugs r(c, bk.x + 44, bk.y + 14, 10, 2, "#f2917f"); r(c, bk.x + 58, bk.y + 16, 8, 6, P.folder); papers(c, bk.x + 76, bk.y + 15); // fridge: tall outlined block shade(c, bk.x + 140, bk.y + 53, 28); r(c, bk.x + 139, bk.y + 7, 30, 48, P.ink); r(c, bk.x + 140, bk.y + 8, 28, 46, "#eceef2"); r(c, bk.x + 140, bk.y + 8, 28, 3, "#fafbfd"); r(c, bk.x + 140, bk.y + 26, 28, 2, "#c3c6cf"); r(c, bk.x + 162, bk.y + 14, 3, 8, "#9da0ab"); r(c, bk.x + 140, bk.y + 48, 28, 6, "#d4d6de"); // fridge base face // vending machine: outlined, lit window shade(c, bk.x + 112, bk.y + 99, 22); r(c, bk.x + 111, bk.y + 63, 24, 38, P.ink); r(c, bk.x + 112, bk.y + 64, 22, 36, "#d9534f"); r(c, bk.x + 112, bk.y + 64, 22, 3, "#e8736f"); r(c, bk.x + 115, bk.y + 70, 12, 20, "#23232e"); [[0, P.binder2], [6, P.folder], [12, "#5cc46e"]].forEach(([off, col]) => { r(c, bk.x + 116, bk.y + 72 + off, 10, 3, col); }); r(c, bk.x + 129, bk.y + 72, 3, 10, "#f2f2f5"); r(c, bk.x + 112, bk.y + 94, 22, 6, "#b23f3c"); // machine base face // coffee machine on the counter end — the shrine const cf = SPOTS.coffee; shade(c, cf.x - 11, cf.y + 11, 24); r(c, cf.x - 11, cf.y - 11, 24, 24, P.ink); r(c, cf.x - 10, cf.y - 10, 22, 22, "#3a3a45"); r(c, cf.x - 10, cf.y - 10, 22, 3, "#52525e"); r(c, cf.x - 6, cf.y - 5, 14, 7, "#1d1d26"); r(c, cf.x - 4, cf.y + 4, 6, 5, P.mug); r(c, cf.x + 5, cf.y - 3, 3, 2, "#7ddb6f"); // small table + chairs shade(c, bk.x + 32, bk.y + 103, 48); box3d(c, bk.x + 32, bk.y + 76, 48, 20, 8, P.woodTop, P.woodFace, P.woodTopHi); chair(c, bk.x + 20, bk.y + 84); chair(c, bk.x + 92, bk.y + 84); r(c, bk.x + 48, bk.y + 82, 6, 6, P.mug); // water cooler near the entrance gap shade(c, bk.x + 56, bk.y + 132, 16, true); r(c, bk.x + 56, bk.y + 111, 14, 24, P.ink); r(c, bk.x + 57, bk.y + 116, 12, 15, "#dfe7f2"); r(c, bk.x + 59, bk.y + 112, 8, 6, "#bcd8f0"); r(c, bk.x + 57, bk.y + 128, 12, 3, P.metalFace); // walls r(c, bk.x, bk.y, 6, bk.h, P.wallCap); r(c, bk.x + 5, bk.y, 1, bk.h, P.ink); r(c, bk.x, bk.y + bk.h - 6, 50, 6, P.wallCap); r(c, bk.x, bk.y + bk.h, 50, 10, P.wallFace); // south face, left segment r(c, bk.x, bk.y + bk.h + 9, 50, 1, P.ink); r(c, bk.x + 110, bk.y + bk.h - 6, 70, 6, P.wallCap); r(c, bk.x + 110, bk.y + bk.h, 70, 10, P.wallFace); r(c, bk.x + 110, bk.y + bk.h + 9, 70, 1, P.ink); // BREAK RM plaque on the wall face r(c, bk.x + 29, bk.y + 3, 74, 13, P.ink); r(c, bk.x + 30, bk.y + 4, 72, 11, "#fdfdfe"); c.fillStyle = "#34343f"; c.fillText("BREAK RM", bk.x + 35, bk.y + 13); // ---- outer walls: white face with cap + ink line + daylight windows r(c, 0, 0, W, 4, P.wallCap); r(c, 0, 4, W, 18, P.wallFace); r(c, 0, 4, W, 2, "#ffffff"); r(c, 0, 20, W, 3, P.wallShade); r(c, 0, 23, W, 1, P.ink); r(c, 0, 24, W, 3, P.baseboard); // corridor windows: daytime sky, soft clouds, light skyline [[250, 76], [346, 76]].forEach(([wx, ww]) => { r(c, wx - 4, 2, ww + 8, 24, P.ink); r(c, wx - 3, 3, ww + 6, 22, P.glassFrame); r(c, wx, 5, ww, 18, P.sky); r(c, wx, 5, ww, 6, P.skyHi); r(c, wx + 8, 8, 14, 3, "#ffffff"); r(c, wx + 40, 11, 18, 3, "#ffffff"); for (let i = 0; i < ww / 12; i++) { const bh = 6 + ((i * 37) % 7); r(c, wx + i * 12 + 1, 23 - bh, 9, bh, P.bldg); if (i % 2 === 0) r(c, wx + i * 12 + 3, 25 - bh + 2, 2, 2, P.bldgLit); if (i % 3 === 0) r(c, wx + i * 12 + 6, 25 - bh + 4, 2, 2, P.bldgLit2); } r(c, wx + ww / 2 - 1, 5, 2, 18, P.glassFrame); // mullion }); // AC unit on the wall face r(c, 25, 5, 36, 16, P.ink); r(c, 26, 6, 34, 14, "#e6e6eb"); r(c, 26, 6, 34, 3, "#f6f6f9"); r(c, 30, 14, 26, 2, "#b6b6c1"); r(c, 30, 17, 26, 1, "#b6b6c1"); // shelf with binders on the corridor wall shelfUnit(c, 380, 28); // whiteboard with line graph on the corridor wall (reference back wall) r(c, 564, 4, 60, 21, P.ink); r(c, 565, 5, 58, 19, "#ffffff"); r(c, 565, 5, 58, 2, "#e8e8ee"); c.strokeStyle = "#d9534f"; c.lineWidth = 1; c.beginPath(); c.moveTo(570, 19); c.lineTo(580, 12); c.lineTo(590, 16); c.lineTo(602, 8); c.lineTo(616, 11); c.stroke(); c.strokeStyle = "#3aa3e0"; c.beginPath(); c.moveTo(570, 21); c.lineTo(585, 18); c.lineTo(600, 20); c.lineTo(616, 15); c.stroke(); // side + bottom walls r(c, 0, H - WALL, W, WALL, P.wallFace); r(c, 0, H - WALL, W, 2, P.ink); r(c, 0, 0, WALL, H, P.wallFace); r(c, WALL - 1, 0, 1, H, P.ink); r(c, W - WALL, 0, WALL, H, P.wallFace); r(c, W - WALL, 0, 1, H, P.ink); // window on the left wall (rain overlay is dynamic) r(c, 0, 226, WALL, 78, P.ink); r(c, 0, 228, WALL - 1, 74, P.glassFrame); r(c, 1, 231, WALL - 3, 68, P.sky); r(c, 1, 262, WALL - 3, 2, P.glassFrame); // entrance: double door at the bottom r(c, 299, H - WALL - 1, 62, WALL + 1, P.ink); r(c, 300, H - WALL, 60, WALL, "#c2996a"); r(c, 300, H - WALL, 60, 2, "#dab987"); r(c, 329, H - WALL, 2, WALL, P.ink); r(c, 306, H - 6, 4, 2, "#6b4d2a"); r(c, 350, H - 6, 4, 2, "#6b4d2a"); // EXIT sign r(c, 313, H - 25, 34, 12, P.ink); r(c, 314, H - 24, 32, 10, "#1f7a37"); c.fillStyle = "#d8ffe2"; c.font = "6px 'Press Start 2P'"; c.fillText("EXIT", 318, H - 16); // welcome mat r(c, 302, H - 34, 56, 14, "#b6543e"); r(c, 302, H - 34, 56, 1, "#8a3f2e"); r(c, 302, H - 21, 56, 1, "#8a3f2e"); // ---- cubicles cubicle(c, SPOTS.playerDesk, "#5b5b68", { books: true }); cubicle(c, NPC_SPOTS.brad, "#ff5a3c", { phone: true }); cubicle(c, NPC_SPOTS.stacey, "#1fd9c4", { books: true }); cubicle(c, NPC_SPOTS.kevin, "#ffd23f"); cubicle(c, NPC_SPOTS.janet, "#b95cff", { books: true }); cubicle(c, NPC_SPOTS.derek, "#8fd4ff", { darkMonitor: true, phone: true }); // "YOU" tag under the player cubicle c.fillStyle = P.label; c.font = "6px 'Press Start 2P'"; c.fillText("YOU", SPOTS.playerDesk.x - 9, SPOTS.playerDesk.y + 52); // janet's mood board on her partition const j = NPC_SPOTS.janet; r(c, j.x - 19, j.y - 31, 38, 18, P.ink); r(c, j.x - 18, j.y - 30, 36, 16, "#ffffff"); [["#ff36c0", 0], ["#3aa3e0", 9], ["#5cc46e", 18], ["#e6b33c", 27]] .forEach(([col, off]) => r(c, j.x - 15 + off, j.y - 27, 6, 6, col)); r(c, j.x - 15, j.y - 19, 26, 2, P.paperShadow); // kevin's whiteboard (right wall beside his cubicle) r(c, 605, 311, 26, 50, P.ink); r(c, 606, 312, 24, 48, "#9a9aa8"); r(c, 609, 315, 18, 42, "#ffffff"); c.strokeStyle = "#d9534f"; c.lineWidth = 1; c.beginPath(); c.moveTo(612, 350); c.lineTo(617, 326); c.lineTo(621, 340); c.lineTo(625, 320); c.stroke(); c.fillStyle = "#3aa3e0"; c.fillText("?", 612, 324); // derek: ancient beige tower + briefcase he has had since 2009 const d = NPC_SPOTS.derek; r(c, d.x - 25, d.y + 9, 12, 14, P.ink); r(c, d.x - 24, d.y + 10, 10, 12, "#d9d2b8"); r(c, d.x - 22, d.y + 12, 6, 2, "#bfb89e"); briefcase(c, d.x + 38, d.y + 24); // ---- props along walls // filing cabinets (left wall): outlined with drawer faces [320, 360].forEach((y) => { shade(c, WALL + 2, y + 31, 26); r(c, WALL + 1, y - 1, 26, 34, P.ink); r(c, WALL + 2, y, 24, 32, P.metalTop); r(c, WALL + 2, y, 24, 3, P.metalHi); r(c, WALL + 2, y + 26, 24, 6, P.metalFace); r(c, WALL + 4, y + 7, 20, 2, P.metalFace); r(c, WALL + 4, y + 17, 20, 2, P.metalFace); r(c, WALL + 12, y + 4, 5, 2, P.metalHi); r(c, WALL + 12, y + 14, 5, 2, P.metalHi); }); papers(c, WALL + 8, 308); briefcase(c, WALL + 30, 344); // colorful poster on the left wall r(c, WALL + 3, 179, 28, 22, P.ink); r(c, WALL + 4, 180, 26, 20, "#ffffff"); r(c, WALL + 6, 182, 22, 16, "#2c2c38"); [["#ff36c0", 0, 0], ["#3aa3e0", 11, 0], ["#5cc46e", 0, 8], ["#e6b33c", 11, 8]].forEach(([col, ox, oy]) => { r(c, WALL + 7 + ox, 183 + oy, 9, 7, col); }); // copier / printer near reception: outlined block with tray const pr = SPOTS.printer; shade(c, pr.x - 13, pr.y + 9, 30); r(c, pr.x - 14, pr.y - 9, 30, 20, P.ink); r(c, pr.x - 13, pr.y - 8, 28, 14, "#e6e6eb"); r(c, pr.x - 13, pr.y - 8, 28, 3, "#f6f6f9"); r(c, pr.x - 13, pr.y + 6, 28, 4, "#c5c7d1"); // front face r(c, pr.x - 8, pr.y - 12, 18, 5, "#cfcfd8"); r(c, pr.x - 8, pr.y - 12, 18, 1, P.ink); r(c, pr.x - 5, pr.y - 2, 12, 4, "#3a3a45"); r(c, pr.x + 9, pr.y - 5, 3, 2, "#5cc46e"); // reception desk: wood with front face shade(c, SPOTS.reception.x - 30, SPOTS.reception.y + 9, 60); box3d(c, SPOTS.reception.x - 30, SPOTS.reception.y - 8, 60, 14, 8, P.woodTop, P.woodFace, P.woodTopHi); monitor(c, SPOTS.reception.x - 14, SPOTS.reception.y - 2); landline(c, SPOTS.reception.x + 6, SPOTS.reception.y - 4); r(c, SPOTS.reception.x + 20, SPOTS.reception.y - 4, 8, 6, P.leaf); // big plant in the corner plant(c, SPOTS.plant.x, SPOTS.plant.y, true); plant(c, 248, 188); plant(c, 432, 188); // wall art between windows r(c, 319, 6, 24, 18, P.ink); r(c, 320, 7, 22, 16, "#ffffff"); r(c, 322, 9, 18, 12, "#aedcf2"); r(c, 324, 15, 6, 3, "#5cc46e"); r(c, 332, 11, 5, 5, "#e6b33c"); // scattered papers on the floor near brad (of course near brad) [[352, 250, -20], [368, 262, 35], [340, 270, 10]].forEach(([x, y, a]) => { c.save(); c.translate(x, y); c.rotate((a * Math.PI) / 180); r(c, -4, -3, 8, 6, P.paper); r(c, -4, 2, 8, 1, P.paperShadow); c.restore(); }); // inbox tray: outlined with paper inside const ib = SPOTS.inbox; r(c, ib.x - 10, ib.y - 6, 22, 13, P.ink); r(c, ib.x - 9, ib.y - 5, 20, 11, P.metalFace); r(c, ib.x - 8, ib.y - 4, 18, 9, P.metalTop); r(c, ib.x - 6, ib.y - 2, 14, 5, "#8a8e9c"); r(c, ib.x - 6, ib.y - 2, 14, 1, P.paper); return cv; } /* --------------------------------------------------------- per-frame layer */ const css = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim(); export function drawFloor(ctx, opts = {}) { if (!staticScene) staticScene = buildStatic(); ctx.drawImage(staticScene, 0, 0); if (opts.gloomy) { r(ctx, 0, 0, W, H, "rgba(54,60,92,.30)"); } const br = ROOMS.boardroom; // boardroom door (dynamic glow during presentation trigger) if (opts.doorGlow) { const pulse = Math.floor(Date.now() / 280) % 2; r(ctx, br.x + br.w - 6, br.y + 90, 6, 50, pulse ? "#ffc73b" : "#d9a82f"); ctx.globalAlpha = pulse ? 0.35 : 0.2; r(ctx, br.x + br.w - 16, br.y + 82, 26, 66, "#ffc73b"); ctx.globalAlpha = 1; } else { r(ctx, br.x + br.w - 6, br.y + 90, 6, 50, "#c2996a"); r(ctx, br.x + br.w - 6, br.y + 90, 2, 50, "#dab987"); r(ctx, br.x + br.w - 4, br.y + 112, 2, 6, "#6b4d2a"); // handle } // left-wall window rain (morale < 20 manifests as weather. nobody asks why) if (opts.gloomy) { if (rainDrops.length === 0) rainDrops = Array.from({ length: 7 }, () => ({ x: 1 + Math.random() * 6, y: 232 + Math.random() * 64 })); ctx.fillStyle = "#5a8ec4"; rainDrops.forEach((d) => { ctx.fillRect(d.x, d.y, 1, 4); d.y += 2; if (d.y > 296) d.y = 232; }); // rain on the skyline windows too ctx.globalAlpha = 0.5; for (let i = 0; i < 10; i++) { const rx = 252 + ((i * 53 + Math.floor(Date.now() / 90) * 7) % 168); ctx.fillRect(rx, 6 + (i * 31) % 12, 1, 4); } ctx.globalAlpha = 1; } // wall clock with moving hands const ck = SPOTS.clock; r(ctx, ck.x - 9, ck.y - 9, 18, 18, "#3a3d49"); r(ctx, ck.x - 7, ck.y - 7, 14, 14, "#ffffff"); r(ctx, ck.x - 7, ck.y - 7, 14, 2, "#e8e8ee"); ctx.fillStyle = "#2c2c38"; const sec = Math.floor(Date.now() / 1000) % 60; const ang = (sec / 60) * Math.PI * 2 - Math.PI / 2; ctx.fillRect(ck.x, ck.y, Math.max(1, Math.cos(ang) * 4), 1); ctx.fillRect(ck.x, ck.y - 3, 1, 3); // monitors flicker faintly (alive, barely) if (Math.floor(Date.now() / 800) % 5 === 0) { ctx.globalAlpha = 0.15; for (const s of [SPOTS.playerDesk, ...Object.values(NPC_SPOTS)]) r(ctx, s.x - 6, s.y + 7, 12, 8, "#7fb2ec"); ctx.globalAlpha = 1; } // printer shake during jam events if (opts.printerShake) { const pj = Math.sin(Date.now() / 30) * 2; const pr = SPOTS.printer; r(ctx, pr.x - 13 + pj, pr.y - 8, 28, 17, "#e6e6eb"); r(ctx, pr.x - 8 + pj, pr.y - 12, 18, 5, "#cfcfd8"); r(ctx, pr.x - 2 + pj, pr.y - 14, 10, 6, "#ffffff"); } // desk phone on the desk surface (vibrates for client emergencies) const pd = SPOTS.playerDesk; const ph = opts.phoneRing ? Math.sin(Date.now() / 25) * 2 : 0; r(ctx, pd.x + 9 + ph, pd.y + 12, 16, 11, "#3a3d49"); r(ctx, pd.x + 10 + ph, pd.y + 13, 14, 9, "#b9bcc8"); r(ctx, pd.x + 11 + ph, pd.y + 14, 12, 2, "#d4d6de"); r(ctx, pd.x + 12 + ph, pd.y + 18, 8, 2, "#7d8290"); if (opts.phoneRing && Math.floor(Date.now() / 200) % 2) r(ctx, pd.x + 14, pd.y + 6, 6, 4, css("--bds-neon-cyan") || "#2ff0ff"); // inbox tray lights up for inbox/HR events if (opts.inboxLit) { const ib = SPOTS.inbox; const lit = Math.floor(Date.now() / 300) % 2; r(ctx, ib.x - 6, ib.y - 2, 14, 5, lit ? "#8edb3a" : "#6fae2c"); ctx.globalAlpha = 0.3; r(ctx, ib.x - 12, ib.y - 8, 26, 17, "#8edb3a"); ctx.globalAlpha = 1; } }