Download static/js/map.js from build-small-hackathon/brad-did-something: direct link, hf CLI and curl.
- Browser
- Download file 27.1 kB
-
https://huggingface.co/spaces/build-small-hackathon/brad-did-something/resolve/main/static/js/map.js
- Command line
-
hf download hf://spaces/build-small-hackathon/brad-did-something/static/js/map.js
-
curl -L -o map.js https://huggingface.co/spaces/build-small-hackathon/brad-did-something/resolve/main/static/js/map.js
27.1 kB
| // 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; | |
| } | |
| } | |