qFelix's picture
Brad Did Something - Gradio/FastAPI Space on HF
78c0f6e
Raw
History Blame Contribute Delete
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;
}
}