feat: add active SF and LA HQ environments
This commit is contained in:
+17
@@ -263,6 +263,21 @@
|
||||
.city:hover { background: rgba(40, 48, 58, 0.7); color: var(--ink); }
|
||||
}
|
||||
.city[aria-pressed="true"] { background: rgba(242, 177, 52, 0.22); color: var(--amber-ink); }
|
||||
.office-choice {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: var(--s1);
|
||||
text-align: left;
|
||||
}
|
||||
.office-choice__status {
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
.office-choice[aria-pressed="true"] .office-choice__status { color: var(--amber-ink); }
|
||||
|
||||
.enter {
|
||||
font: inherit;
|
||||
@@ -840,6 +855,8 @@
|
||||
was 17px tall. */
|
||||
#panel-toggle,
|
||||
#enter,
|
||||
#walk,
|
||||
#screens,
|
||||
.city,
|
||||
.chapter,
|
||||
.sheet-close {
|
||||
|
||||
@@ -22,8 +22,8 @@ const configured = (overrides: Partial<IceConfig> = {}): IceConfig => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const BINDING = { officeId: "lumbridge-hq", levelId: "level-1", roomId: "lobby", screenId: "lobby-monitor" } as const;
|
||||
const SECOND_BINDING = { officeId: "lumbridge-hq", levelId: "level-1", roomId: "open-floor", screenId: "open-display" } as const;
|
||||
const BINDING = { officeId: "lumbridge-hq", levelId: "level-1", roomId: "live-work", screenId: "lobby-monitor" } as const;
|
||||
const SECOND_BINDING = { officeId: "lumbridge-hq", levelId: "level-1", roomId: "live-work", screenId: "open-display" } as const;
|
||||
const PROVIDER_CREDENTIAL: ScreenShareCredential = {
|
||||
sessionId: "opaque-session", participantId: "opaque-participant", role: "presenter", grantToken: "opaque-grant-token-value",
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
} from "../../../src/media/signalingTypes.ts";
|
||||
|
||||
const BINDING: ScreenShareBinding = {
|
||||
officeId: "lumbridge-hq", levelId: "level-1", roomId: "lobby", screenId: "lobby-monitor",
|
||||
officeId: "lumbridge-hq", levelId: "level-1", roomId: "live-work", screenId: "lobby-monitor",
|
||||
};
|
||||
const create = (sequence = 1): ScreenShareCreateRequest => ({
|
||||
type: "screen-share-create-request", protocolVersion: 1, requestId: "request-create", sequence,
|
||||
@@ -193,7 +193,7 @@ describe("office media signaling service", () => {
|
||||
|
||||
it("validates the exact authored level, room and screen prop", () => {
|
||||
assert.equal(officeHasMediaBinding(LUMBRIDGE_HQ, BINDING), true);
|
||||
assert.equal(officeHasMediaBinding(LUMBRIDGE_HQ, { ...BINDING, roomId: "open-floor" }), false);
|
||||
assert.equal(officeHasMediaBinding(LUMBRIDGE_HQ, { ...BINDING, roomId: "kitchen" }), false);
|
||||
assert.equal(officeHasMediaBinding(LUMBRIDGE_HQ, { ...BINDING, screenId: "not-authored" }), false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -583,6 +583,20 @@ export function samplePresenceAt(when: Date): Presence[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Office-aware fallback for shipped environments.
|
||||
*
|
||||
* The legacy roster above is valuable as a renderer fixture, but its seat ids
|
||||
* describe the original reference tower and therefore cannot truthfully appear
|
||||
* in either active HQ. A missing presence service now means what it says: no
|
||||
* fabricated people. Keeping this resolver keyed by the stable office id gives
|
||||
* self-hosters and future sample packs an explicit seam without ever treating a
|
||||
* shiny room as evidence of employees.
|
||||
*/
|
||||
export function samplePresenceForOfficeAt(_officeId: string, _when: Date): Presence[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Minutes since local midnight, as a literal. Every window in this section is
|
||||
* written with it so that the source reads as a timetable.
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
/**
|
||||
* The small-space habitat kit.
|
||||
*
|
||||
* Offices prove the scene format with desks and meeting rooms. A studio home
|
||||
* needs a different handful of silhouettes before it reads honestly: a bed is
|
||||
* not a wide desk, a kitchen is not a row of lockers, and two lounge chairs do
|
||||
* not become a sofa because they touch. These assets stay inside the same
|
||||
* primitive grammar and material palette as the office kit, so they add no
|
||||
* binary art, texture download, or second visual language.
|
||||
*
|
||||
* As with every asset in this directory, source is Apache-2.0 and the generated
|
||||
* artistic output is dedicated under CC0-1.0 by `src/assets/LICENSE-ART`.
|
||||
*/
|
||||
|
||||
import * as THREE from "three";
|
||||
import { defineAsset } from "../kit.ts";
|
||||
import { MeshBin } from "../parts.ts";
|
||||
import { panelSlab, slab, tintable } from "./common.ts";
|
||||
|
||||
type BedParams = {
|
||||
width: number;
|
||||
length: number;
|
||||
platformHeight: number;
|
||||
headboardHeight: number;
|
||||
};
|
||||
|
||||
/** A low platform bed. The headboard is at local -Z and the foot faces +Z. */
|
||||
export const bedPlatform = defineAsset<BedParams>({
|
||||
id: "tera:bed.platform",
|
||||
label: "Platform bed",
|
||||
defaults: { width: 1.52, length: 2.03, platformHeight: 0.28, headboardHeight: 0.92 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.width, depth: p.length, height: p.headboardHeight, clearance: 0.7 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const wood = ctx.materials.get("shelf");
|
||||
const textile = tintable(ctx, "upholstery");
|
||||
const linen = ctx.materials.get("paper");
|
||||
const frameH = Math.max(0.12, p.platformHeight);
|
||||
const mattressH = 0.2;
|
||||
|
||||
// A recessed plinth gives the platform a shadow line instead of a box-on-floor read.
|
||||
bin.add(ctx.parts.box(), ctx.materials.get("chairBase"), {
|
||||
y: 0.035,
|
||||
size: [p.width - 0.22, 0.07, p.length - 0.18],
|
||||
});
|
||||
slab(bin, ctx, wood, {
|
||||
y: 0.09,
|
||||
width: p.width,
|
||||
depth: p.length,
|
||||
thickness: frameH - 0.09,
|
||||
});
|
||||
bin.add(ctx.parts.roundedBox(0.09), textile, {
|
||||
y: frameH,
|
||||
z: 0.03,
|
||||
size: [p.width - 0.12, mattressH, p.length - 0.12],
|
||||
});
|
||||
panelSlab(bin, ctx, wood, {
|
||||
y: 0,
|
||||
z: -p.length / 2 + 0.035,
|
||||
width: p.width,
|
||||
height: p.headboardHeight,
|
||||
thickness: 0.07,
|
||||
faces: "front",
|
||||
});
|
||||
|
||||
// Two pillows, separated enough that the silhouette survives the dollhouse view.
|
||||
for (const sx of [-1, 1]) {
|
||||
bin.add(ctx.parts.roundedBox(0.12), linen, {
|
||||
x: sx * p.width * 0.23,
|
||||
y: frameH + mattressH + 0.015,
|
||||
z: -p.length / 2 + 0.38,
|
||||
size: [p.width * 0.38, 0.11, 0.48],
|
||||
yaw: sx * 0.035,
|
||||
});
|
||||
}
|
||||
|
||||
return bin.build("bed.platform");
|
||||
},
|
||||
});
|
||||
|
||||
type SofaParams = {
|
||||
width: number;
|
||||
depth: number;
|
||||
seatHeight: number;
|
||||
backHeight: number;
|
||||
modules: number;
|
||||
};
|
||||
|
||||
/** A low modular sofa with individually readable seat and back cushions. */
|
||||
export const sofaModular = defineAsset<SofaParams>({
|
||||
id: "tera:sofa.modular",
|
||||
label: "Modular sofa",
|
||||
defaults: { width: 2.2, depth: 0.9, seatHeight: 0.42, backHeight: 0.82, modules: 3 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.width, depth: p.depth, height: p.backHeight, clearance: 0.7 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const P = ctx.parts;
|
||||
const base = ctx.materials.get("chairShell");
|
||||
const textile = tintable(ctx, "upholstery");
|
||||
const modules = Math.max(2, Math.min(4, Math.round(p.modules)));
|
||||
const innerW = p.width - 0.28;
|
||||
const moduleW = innerW / modules;
|
||||
|
||||
for (const sx of [-1, 1]) {
|
||||
for (const sz of [-1, 1]) {
|
||||
bin.add(P.rod(), ctx.materials.get("chairBase"), {
|
||||
x: sx * (p.width / 2 - 0.12),
|
||||
z: sz * (p.depth / 2 - 0.12),
|
||||
size: [0.04, 0.08, 0.04],
|
||||
});
|
||||
}
|
||||
}
|
||||
bin.add(P.box(), base, { y: 0.08, size: [innerW, p.seatHeight - 0.15, p.depth - 0.14] });
|
||||
|
||||
for (let i = 0; i < modules; i += 1) {
|
||||
const x = -innerW / 2 + moduleW * (i + 0.5);
|
||||
bin.add(P.roundedBox(0.07), textile, {
|
||||
x,
|
||||
y: p.seatHeight - 0.12,
|
||||
z: -0.04,
|
||||
size: [moduleW - 0.025, 0.14, p.depth - 0.25],
|
||||
});
|
||||
bin.add(P.roundedBox(0.09), textile, {
|
||||
x,
|
||||
y: p.seatHeight + 0.015,
|
||||
z: p.depth / 2 - 0.15,
|
||||
size: [moduleW - 0.035, p.backHeight - p.seatHeight, 0.17],
|
||||
pitch: 0.1,
|
||||
});
|
||||
}
|
||||
for (const sx of [-1, 1]) {
|
||||
bin.add(P.roundedBox(0.09), textile, {
|
||||
x: sx * (p.width / 2 - 0.07),
|
||||
y: 0.08,
|
||||
size: [0.14, p.seatHeight + 0.18, p.depth - 0.1],
|
||||
});
|
||||
}
|
||||
|
||||
return bin.build("sofa.modular");
|
||||
},
|
||||
});
|
||||
|
||||
type KitchenRunParams = {
|
||||
width: number;
|
||||
depth: number;
|
||||
counterHeight: number;
|
||||
height: number;
|
||||
bays: number;
|
||||
};
|
||||
|
||||
/** A complete compact kitchen wall: base units, worktop, sink, backsplash and uppers. */
|
||||
export const kitchenRun = defineAsset<KitchenRunParams>({
|
||||
id: "tera:kitchen.run",
|
||||
label: "Kitchen wall",
|
||||
defaults: { width: 3.6, depth: 0.62, counterHeight: 0.91, height: 2.16, bays: 5 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.width, depth: p.depth, height: p.height, clearance: 1 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const P = ctx.parts;
|
||||
const cabinet = tintable(ctx, "cabinet");
|
||||
const top = ctx.materials.get("tableTop");
|
||||
const metal = ctx.materials.get("metalTrim");
|
||||
const bays = Math.max(3, Math.min(7, Math.round(p.bays)));
|
||||
const bayW = p.width / bays;
|
||||
|
||||
bin.add(P.box(), ctx.materials.get("chairBase"), {
|
||||
y: 0.02,
|
||||
z: -0.035,
|
||||
size: [p.width - 0.08, 0.08, p.depth - 0.12],
|
||||
});
|
||||
for (let i = 0; i < bays; i += 1) {
|
||||
const x = -p.width / 2 + bayW * (i + 0.5);
|
||||
bin.add(P.box(), cabinet, {
|
||||
x,
|
||||
y: 0.1,
|
||||
z: -0.03,
|
||||
size: [bayW - 0.018, p.counterHeight - 0.15, p.depth - 0.06],
|
||||
});
|
||||
bin.add(P.box(), metal, {
|
||||
x: x + bayW * 0.34,
|
||||
y: p.counterHeight * 0.55,
|
||||
z: p.depth / 2 + 0.006,
|
||||
size: [0.012, 0.12, 0.012],
|
||||
});
|
||||
}
|
||||
slab(bin, ctx, top, {
|
||||
y: p.counterHeight - 0.045,
|
||||
width: p.width,
|
||||
depth: p.depth + 0.05,
|
||||
thickness: 0.045,
|
||||
});
|
||||
|
||||
// Backsplash and upper units stop short over the sink to create a focal bay.
|
||||
panelSlab(bin, ctx, ctx.materials.get("tile"), {
|
||||
y: p.counterHeight,
|
||||
z: -p.depth / 2 + 0.012,
|
||||
width: p.width,
|
||||
height: 0.62,
|
||||
thickness: 0.024,
|
||||
faces: "front",
|
||||
});
|
||||
const upperY = 1.48;
|
||||
for (let i = 0; i < bays; i += 1) {
|
||||
if (i === Math.floor(bays / 2)) continue;
|
||||
const x = -p.width / 2 + bayW * (i + 0.5);
|
||||
bin.add(P.box(), cabinet, {
|
||||
x,
|
||||
y: upperY,
|
||||
z: -p.depth / 2 + 0.17,
|
||||
size: [bayW - 0.018, p.height - upperY, 0.34],
|
||||
});
|
||||
bin.add(P.box(), metal, {
|
||||
x: x + bayW * 0.34,
|
||||
y: upperY + 0.2,
|
||||
z: -p.depth / 2 + 0.345,
|
||||
size: [0.012, 0.13, 0.012],
|
||||
});
|
||||
}
|
||||
|
||||
const sinkX = -p.width / 2 + bayW * (Math.floor(bays / 2) + 0.5);
|
||||
bin.add(P.box(), metal, {
|
||||
x: sinkX,
|
||||
y: p.counterHeight + 0.003,
|
||||
size: [bayW * 0.62, 0.018, p.depth * 0.5],
|
||||
});
|
||||
bin.add(P.rod(), metal, {
|
||||
x: sinkX,
|
||||
y: p.counterHeight + 0.02,
|
||||
z: -0.12,
|
||||
size: [0.025, 0.24, 0.025],
|
||||
});
|
||||
bin.add(P.rod(), metal, {
|
||||
x: sinkX,
|
||||
y: p.counterHeight + 0.24,
|
||||
z: -0.04,
|
||||
size: [0.025, 0.025, 0.16],
|
||||
});
|
||||
|
||||
return bin.build("kitchen.run");
|
||||
},
|
||||
});
|
||||
|
||||
type IslandParams = {
|
||||
length: number;
|
||||
depth: number;
|
||||
height: number;
|
||||
overhang: number;
|
||||
};
|
||||
|
||||
/** A storage island with a real seating overhang on its +Z side. */
|
||||
export const kitchenIsland = defineAsset<IslandParams>({
|
||||
id: "tera:kitchen.island",
|
||||
label: "Kitchen island",
|
||||
defaults: { length: 2.2, depth: 0.9, height: 0.92, overhang: 0.24 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.length, depth: p.depth, height: p.height, clearance: 0.8 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const cabinetDepth = Math.max(0.42, p.depth - p.overhang);
|
||||
bin.add(ctx.parts.box(), tintable(ctx, "cabinet"), {
|
||||
y: 0.06,
|
||||
z: -p.overhang / 2,
|
||||
size: [p.length - 0.12, p.height - 0.12, cabinetDepth],
|
||||
});
|
||||
slab(bin, ctx, ctx.materials.get("tableTop"), {
|
||||
y: p.height - 0.05,
|
||||
width: p.length,
|
||||
depth: p.depth,
|
||||
thickness: 0.05,
|
||||
});
|
||||
for (const sx of [-1, 1]) {
|
||||
bin.add(ctx.parts.rod(), ctx.materials.get("metalTrim"), {
|
||||
x: sx * (p.length / 2 - 0.12),
|
||||
y: 0.08,
|
||||
z: p.depth / 2 - 0.08,
|
||||
size: [0.035, p.height - 0.13, 0.035],
|
||||
});
|
||||
}
|
||||
return bin.build("kitchen.island");
|
||||
},
|
||||
});
|
||||
|
||||
type WardrobeParams = { width: number; depth: number; height: number; doors: number };
|
||||
|
||||
/** Full-height closed storage with a recessed plinth and quiet vertical pulls. */
|
||||
export const storageWardrobe = defineAsset<WardrobeParams>({
|
||||
id: "tera:storage.wardrobe",
|
||||
label: "Wardrobe",
|
||||
defaults: { width: 1.8, depth: 0.58, height: 2.18, doors: 3 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.width, depth: p.depth, height: p.height, clearance: 0.75 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const body = ctx.materials.get("shelf");
|
||||
const doors = tintable(ctx, "cabinet");
|
||||
const metal = ctx.materials.get("metalTrim");
|
||||
const count = Math.max(2, Math.min(5, Math.round(p.doors)));
|
||||
const doorW = (p.width - 0.05) / count;
|
||||
bin.add(ctx.parts.box(), ctx.materials.get("chairBase"), {
|
||||
x: 0,
|
||||
y: 0.02,
|
||||
z: -0.02,
|
||||
size: [p.width - 0.1, 0.08, p.depth - 0.1],
|
||||
});
|
||||
bin.add(ctx.parts.box(), body, { y: 0.08, size: [p.width, p.height - 0.08, p.depth] });
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const x = -p.width / 2 + 0.025 + doorW * (i + 0.5);
|
||||
bin.add(ctx.parts.box(), doors, {
|
||||
x,
|
||||
y: 0.11,
|
||||
z: p.depth / 2 + 0.006,
|
||||
size: [doorW - 0.014, p.height - 0.16, 0.018],
|
||||
});
|
||||
bin.add(ctx.parts.box(), metal, {
|
||||
x: x + doorW * 0.34,
|
||||
y: p.height * 0.46,
|
||||
z: p.depth / 2 + 0.02,
|
||||
size: [0.014, 0.28, 0.014],
|
||||
});
|
||||
}
|
||||
return bin.build("storage.wardrobe");
|
||||
},
|
||||
});
|
||||
|
||||
type StoolParams = { diameter: number; seatHeight: number; back: boolean };
|
||||
|
||||
/** A kitchen-height stool. It faces -Z so a seat address can share its yaw. */
|
||||
export const seatStool = defineAsset<StoolParams>({
|
||||
id: "tera:seat.stool",
|
||||
label: "Counter stool",
|
||||
defaults: { diameter: 0.4, seatHeight: 0.68, back: true },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.diameter + 0.1, depth: p.diameter + 0.12, height: p.back ? 0.96 : p.seatHeight, clearance: 0.35 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const metal = ctx.materials.get("chairBase");
|
||||
const textile = tintable(ctx, "chairFabric");
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
const yaw = Math.PI / 4 + i * Math.PI / 2;
|
||||
bin.add(ctx.parts.rod(), metal, {
|
||||
x: Math.sin(yaw) * p.diameter * 0.34,
|
||||
z: Math.cos(yaw) * p.diameter * 0.34,
|
||||
size: [0.025, p.seatHeight - 0.06, 0.025],
|
||||
});
|
||||
}
|
||||
bin.add(ctx.parts.cylinder(20), textile, {
|
||||
y: p.seatHeight - 0.06,
|
||||
size: [p.diameter, 0.1, p.diameter],
|
||||
});
|
||||
if (p.back) {
|
||||
bin.add(ctx.parts.rod(), metal, {
|
||||
x: -p.diameter * 0.34,
|
||||
y: p.seatHeight - 0.02,
|
||||
z: p.diameter * 0.34,
|
||||
size: [0.022, 0.28, 0.022],
|
||||
});
|
||||
bin.add(ctx.parts.rod(), metal, {
|
||||
x: p.diameter * 0.34,
|
||||
y: p.seatHeight - 0.02,
|
||||
z: p.diameter * 0.34,
|
||||
size: [0.022, 0.28, 0.022],
|
||||
});
|
||||
// Keep the back and circular seat in the same indexed primitive family.
|
||||
// `roundedBox` is non-indexed; mixing it with the indexed cylinder would
|
||||
// make BufferGeometryUtils reject the material bin and drop every fabric
|
||||
// part from the finished stool.
|
||||
bin.add(ctx.parts.box(), textile, {
|
||||
y: p.seatHeight + 0.18,
|
||||
z: p.diameter * 0.34,
|
||||
size: [p.diameter * 0.82, 0.2, 0.06],
|
||||
});
|
||||
}
|
||||
return bin.build("seat.stool");
|
||||
},
|
||||
});
|
||||
|
||||
type FloorLightParams = { height: number; reach: number; shadeDiameter: number };
|
||||
|
||||
/** An arced floor lamp. Geometry glows; the office lighting rig remains the only light owner. */
|
||||
export const lightFloor = defineAsset<FloorLightParams>({
|
||||
id: "tera:light.floor",
|
||||
label: "Floor lamp",
|
||||
defaults: { height: 1.55, reach: 0.42, shadeDiameter: 0.32 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: Math.max(0.42, p.shadeDiameter), depth: p.reach + p.shadeDiameter / 2, height: p.height };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const body = new MeshBin();
|
||||
const glow = new MeshBin();
|
||||
const metal = ctx.materials.get("lightHousing");
|
||||
body.add(ctx.parts.cylinder(20), ctx.materials.get("chairBase"), {
|
||||
size: [0.36, 0.035, 0.36],
|
||||
});
|
||||
body.add(ctx.parts.rod(), metal, { y: 0.035, size: [0.025, p.height - 0.18, 0.025] });
|
||||
body.add(ctx.parts.rod(), metal, {
|
||||
y: p.height - 0.18,
|
||||
z: -p.reach / 2,
|
||||
size: [0.025, 0.025, p.reach],
|
||||
});
|
||||
body.add(ctx.parts.cone(20), metal, {
|
||||
y: p.height - 0.4,
|
||||
z: -p.reach,
|
||||
size: [p.shadeDiameter, 0.24, p.shadeDiameter],
|
||||
pitch: Math.PI,
|
||||
});
|
||||
glow.add(ctx.parts.disc(20), ctx.materials.get("lightDiffuser"), {
|
||||
y: p.height - 0.395,
|
||||
z: -p.reach,
|
||||
size: [p.shadeDiameter * 0.86, 1, p.shadeDiameter * 0.86],
|
||||
pitch: Math.PI,
|
||||
});
|
||||
const group = new THREE.Group();
|
||||
group.name = "light.floor";
|
||||
group.add(body.build("light.floor:body"));
|
||||
group.add(glow.build("light.floor:glow", { castShadow: false, receiveShadow: false }));
|
||||
return group;
|
||||
},
|
||||
});
|
||||
@@ -17,7 +17,7 @@
|
||||
* somebody walks through a wall — leave the collider with no gap where the door
|
||||
* is.
|
||||
*
|
||||
* Seventeen assets is not a furniture catalogue and is not trying to be. It is
|
||||
* Twenty-five assets is not a furniture catalogue and is not trying to be. It is
|
||||
* the set that gets a real floor plate looking like an office: somewhere to
|
||||
* work, somewhere to sit, somewhere to meet, somewhere to put things, something
|
||||
* to look at, something alive, and light.
|
||||
@@ -26,6 +26,15 @@
|
||||
import { kit, type AnyAsset, type AssetRegistry } from "../kit.ts";
|
||||
import { deskPartition, deskPedestal, deskWorkstation } from "./desks.ts";
|
||||
import { plantPotted, plantTall } from "./greenery.ts";
|
||||
import {
|
||||
bedPlatform,
|
||||
kitchenIsland,
|
||||
kitchenRun,
|
||||
lightFloor,
|
||||
seatStool,
|
||||
sofaModular,
|
||||
storageWardrobe,
|
||||
} from "./habitat.ts";
|
||||
import { lightPendant, lightTroffer } from "./lighting.ts";
|
||||
import { robotOptimus } from "./optimus.ts";
|
||||
import { screenMonitor, screenWallDisplay } from "./screens.ts";
|
||||
@@ -53,6 +62,13 @@ export const OFFICE_ASSETS: readonly AnyAsset[] = [
|
||||
robotOptimus,
|
||||
rug,
|
||||
whiteboard,
|
||||
bedPlatform,
|
||||
sofaModular,
|
||||
kitchenRun,
|
||||
kitchenIsland,
|
||||
storageWardrobe,
|
||||
seatStool,
|
||||
lightFloor,
|
||||
];
|
||||
|
||||
/** Register the built-in catalogue into a registry. Defaults to the shared one. */
|
||||
@@ -81,4 +97,11 @@ export {
|
||||
tableMeeting,
|
||||
tableSide,
|
||||
whiteboard,
|
||||
bedPlatform,
|
||||
sofaModular,
|
||||
kitchenRun,
|
||||
kitchenIsland,
|
||||
storageWardrobe,
|
||||
seatStool,
|
||||
lightFloor,
|
||||
};
|
||||
|
||||
+45
-28
@@ -53,10 +53,11 @@ import {
|
||||
SAMPLE_MARKERS,
|
||||
SAMPLE_PALETTE,
|
||||
SAMPLE_PRESENCE_PALETTE,
|
||||
samplePresenceAt,
|
||||
samplePresenceForOfficeAt,
|
||||
sampleRoutesFor,
|
||||
} from "./adapters/sample.ts";
|
||||
import { OFFICE_SITES } from "./offices/sites.ts";
|
||||
import { OFFICE_SITES, type ShippedOfficeId } from "./offices/sites.ts";
|
||||
import { activityRobotsForOffice } from "./offices/runtime.ts";
|
||||
import { authFetch } from "./session.ts";
|
||||
import { capabilitiesFor, resolveAccess, type Access } from "./access.ts";
|
||||
import { createMinimap, type Minimap } from "./engine/minimap.ts";
|
||||
@@ -213,11 +214,17 @@ function journeyToCity(city: JourneyCity): void {
|
||||
* a second pack eagerly imported would put its furniture in the entry bundle
|
||||
* for every visitor who never opens it.
|
||||
*/
|
||||
const OFFICES: { id: string; label: string; load: () => Promise<{ default: Office }> }[] = [
|
||||
{ id: "lumbridge-hq", label: "Lumbridge HQ", load: () => import("./offices/lumbridge-hq.ts") },
|
||||
{ id: "frontier-valley", label: "Frontier Valley", load: () => import("./offices/frontier-valley.ts") },
|
||||
{ id: "mateo-court", label: "Mateo Court", load: () => import("./offices/mateo-court.ts") },
|
||||
];
|
||||
const OFFICE_LOADERS: Readonly<Record<ShippedOfficeId, () => Promise<{ default: Office }>>> = {
|
||||
"lumbridge-hq": () => import("./offices/lumbridge-hq.ts"),
|
||||
"frontier-valley": () => import("./offices/frontier-valley.ts"),
|
||||
"mateo-court": () => import("./offices/mateo-court.ts"),
|
||||
};
|
||||
|
||||
const OFFICES = OFFICE_SITES.map((entry) => ({
|
||||
...entry,
|
||||
label: entry.name,
|
||||
load: OFFICE_LOADERS[entry.id],
|
||||
}));
|
||||
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#scene");
|
||||
if (!canvas) throw new Error("#scene canvas missing");
|
||||
@@ -299,7 +306,7 @@ const OFFICE_MARKERS: Marker[] = OFFICE_SITES.map((entry) => ({
|
||||
id: `office:${entry.id}`,
|
||||
label: entry.name,
|
||||
colorKey: "office",
|
||||
blurb: `${entry.site.label ?? "An office"} — click to walk in`,
|
||||
blurb: `${entry.entryCopy} · ${entry.status}`,
|
||||
lat: entry.site.lat,
|
||||
lng: entry.site.lng,
|
||||
// Hand-typed from the street grid, like every other coordinate here. Not a
|
||||
@@ -450,7 +457,7 @@ let officeMaterials: MaterialRegistry | null = null;
|
||||
* memoised forever, which is the one-line difference from the arrangement that
|
||||
* could only ever hold one office.
|
||||
*/
|
||||
let officeId = OFFICES[0]?.id ?? "lumbridge-hq";
|
||||
let officeId: string = OFFICES[0]?.id ?? "lumbridge-hq";
|
||||
/**
|
||||
* The office's own sky, when its pack says where it stands.
|
||||
*
|
||||
@@ -1263,15 +1270,10 @@ async function enterOffice() {
|
||||
...(pack.site ? {} : { background: 0x11161c }),
|
||||
...(pack.site ? { lighting: officeLighting(pack.site) } : {}),
|
||||
...(pack.site ? { horizon: { drop: pack.site.elevation } } : {}),
|
||||
// Two per floor, whatever floors this pack has — so the two-storey tower
|
||||
// gets four and the single-storey hangar gets two, without either pack
|
||||
// having to know about robots. Derived from the levels rather than
|
||||
// written down, because a pack that gains a storey should not need an
|
||||
// edit here to be staffed.
|
||||
robots: pack.levels.flatMap((level) => [
|
||||
{ levelId: level.id, id: `${level.id}-a` },
|
||||
{ levelId: level.id, id: `${level.id}-b` },
|
||||
]),
|
||||
// Environment-authored activity, not inferred headcount. These stable
|
||||
// specs keep active HQs intentionally sparse and give the robot-jobs
|
||||
// runtime ids, seeds and job sets it can adopt without a migration.
|
||||
robots: activityRobotsForOffice(pack),
|
||||
depth,
|
||||
materials,
|
||||
// Ignored entirely at `"public"` depth, where no layer is built to colour.
|
||||
@@ -1527,7 +1529,7 @@ function watchOccupancy() {
|
||||
if (!scene || scene.depth !== "full") return;
|
||||
presenceWatch = tera.watchPresence(officePack?.id ?? "lumbridge-hq", (body) => {
|
||||
livePresence = body?.people ?? null;
|
||||
presenceIsSample = livePresence === null;
|
||||
presenceIsSample = false;
|
||||
// The scene may have been torn down between a request going out and coming
|
||||
// back — a city switch disposes the office — and writing people into a
|
||||
// disposed layer is a use-after-free with a friendly name. The watch's own
|
||||
@@ -1564,7 +1566,9 @@ function stopWatchingOccupancy() {
|
||||
*/
|
||||
function applyPresence() {
|
||||
if (!office || office.depth !== "full") return;
|
||||
const people = livePresence ?? samplePresenceAt(currentInstant());
|
||||
const fallback = samplePresenceForOfficeAt(officePack?.id ?? officeId, currentInstant());
|
||||
const people = livePresence ?? fallback;
|
||||
presenceIsSample = livePresence === null && fallback.length > 0;
|
||||
office.setPresence(people);
|
||||
officePlan?.setPresence(people);
|
||||
}
|
||||
@@ -1627,18 +1631,26 @@ function renderCityPicker() {
|
||||
if (!cityNav) return;
|
||||
cityNav.replaceChildren();
|
||||
const entries = inside
|
||||
? OFFICES.map((o) => ({ id: o.id, label: o.label, active: o.id === officeId }))
|
||||
: CITIES.map((c) => ({ id: c.id, label: c.label, active: c.id === cityId }));
|
||||
? OFFICES.map((o) => ({ id: o.id, label: o.label, active: o.id === officeId, status: o.status }))
|
||||
: CITIES.map((c) => ({ id: c.id, label: c.label, active: c.id === cityId, status: null }));
|
||||
|
||||
for (const entry of entries) {
|
||||
const b = document.createElement("button");
|
||||
// `aria-pressed` rather than a class, because these buttons choose exactly
|
||||
// one active board. The stylesheet keys off the attribute
|
||||
// so the visual state and the announced state cannot drift apart.
|
||||
b.className = "city";
|
||||
b.className = entry.status ? "city office-choice" : "city";
|
||||
b.type = "button";
|
||||
b.setAttribute("aria-pressed", String(entry.active));
|
||||
b.textContent = entry.label;
|
||||
const name = document.createElement("span");
|
||||
name.textContent = entry.label;
|
||||
b.append(name);
|
||||
if (entry.status) {
|
||||
const status = document.createElement("span");
|
||||
status.className = "office-choice__status";
|
||||
status.textContent = entry.status;
|
||||
b.append(status);
|
||||
}
|
||||
b.addEventListener("click", () => {
|
||||
if (inside) void switchOffice(entry.id);
|
||||
else switchCity(entry.id);
|
||||
@@ -1719,14 +1731,19 @@ function renderLegend() {
|
||||
blurb.hidden = !active?.description;
|
||||
}
|
||||
const cityLabel = CITIES.find((c) => c.id === cityId)?.city.name ?? "";
|
||||
const selectedOffice = OFFICES.find((entry) => entry.id === officeId) ?? OFFICES[0];
|
||||
if (title) title.textContent = inside ? officeName() : cityLabel;
|
||||
if (subtitle) {
|
||||
subtitle.textContent = inside ? "Spaces · a Lumbridge office" : "Tera · Lumbridge Simulate";
|
||||
subtitle.textContent = inside
|
||||
? `Tera · ${selectedOffice?.status ?? "building"} environment`
|
||||
: "Tera · Lumbridge Simulate";
|
||||
}
|
||||
if (enterButton) {
|
||||
// One label for everyone. The door is open at both tiers; what differs is
|
||||
// what is behind it, and that is the badge's job to say, not the button's.
|
||||
enterButton.textContent = inside ? "← Back to the city" : "Enter the office →";
|
||||
enterButton.textContent = inside
|
||||
? "← Back to the city"
|
||||
: selectedOffice?.status === "active"
|
||||
? `Open ${selectedOffice.label} →`
|
||||
: `Preview ${selectedOffice?.label ?? "environment"} · building →`;
|
||||
}
|
||||
const walking = inside && (office?.walker?.active() ?? false);
|
||||
const exploring = !inside && (city.actorActive() ?? false);
|
||||
|
||||
+200
-2029
File diff suppressed because it is too large
Load Diff
+85
-180
@@ -241,8 +241,11 @@ const PEDESTAL: AssetId = "tera:desk.pedestal";
|
||||
const PARTITION: AssetId = "tera:desk.partition";
|
||||
const TASK_CHAIR: AssetId = "tera:seat.task-chair";
|
||||
const LOUNGE_CHAIR: AssetId = "tera:seat.lounge";
|
||||
const STOOL: AssetId = "tera:seat.stool";
|
||||
const MEETING_TABLE: AssetId = "tera:table.meeting";
|
||||
const SIDE_TABLE: AssetId = "tera:table.side";
|
||||
const KITCHEN: AssetId = "tera:kitchen.run";
|
||||
const ISLAND: AssetId = "tera:kitchen.island";
|
||||
const SHELF: AssetId = "tera:storage.shelf";
|
||||
const LOCKER: AssetId = "tera:storage.locker";
|
||||
const MONITOR: AssetId = "tera:screen.monitor";
|
||||
@@ -392,7 +395,7 @@ const ROOMS: Room[] = [
|
||||
// -- The street range, west to east ---------------------------------------
|
||||
{
|
||||
id: "press",
|
||||
name: "The Press",
|
||||
name: "Media Studio",
|
||||
// The messy end, on the corner, with a concrete floor and its own door out
|
||||
// of the front office wide enough to carry a sheet of ply through.
|
||||
outline: rect(0, 0, PRESS_E, COURT_N),
|
||||
@@ -401,7 +404,7 @@ const ROOMS: Room[] = [
|
||||
},
|
||||
{
|
||||
id: "lobby",
|
||||
name: "The Front",
|
||||
name: "Front Door",
|
||||
// Reception, and the only room in the building with a door on the paseo and
|
||||
// a door on the yard. Somebody arriving is met here and then walked out
|
||||
// into the middle of the block, which is the introduction the plan wants to
|
||||
@@ -412,7 +415,7 @@ const ROOMS: Room[] = [
|
||||
},
|
||||
{
|
||||
id: "paseo",
|
||||
name: "The Paseo",
|
||||
name: "Entry Passage",
|
||||
// A passage cut straight through the street range: three metres wide and
|
||||
// eight and a half from the pavement to the yard, counting the brick at the
|
||||
// street end. It is the only way into this building that is not a goods
|
||||
@@ -424,14 +427,14 @@ const ROOMS: Room[] = [
|
||||
},
|
||||
{
|
||||
id: "mess",
|
||||
name: "The Mess",
|
||||
name: "Kitchen · Commons",
|
||||
outline: rect(PASEO_E, 0, MESS_E, COURT_N),
|
||||
floor: TERRAZZO,
|
||||
ceiling: null,
|
||||
},
|
||||
{
|
||||
id: "hewitt",
|
||||
name: "Hewitt",
|
||||
name: "Review Room",
|
||||
// The big meeting room, in the corner where two walls have no openings at
|
||||
// all — the party wall east and the range wall south. The meeting rooms in
|
||||
// this pack take their names from the streets of the district, the way the
|
||||
@@ -444,14 +447,14 @@ const ROOMS: Room[] = [
|
||||
// -- The west range -------------------------------------------------------
|
||||
{
|
||||
id: "willow",
|
||||
name: "Willow",
|
||||
name: "Project Room",
|
||||
outline: rect(0, COURT_N, COURT_W, WILLOW_S),
|
||||
floor: CARPET_ACCENT,
|
||||
ceiling: null,
|
||||
},
|
||||
{
|
||||
id: "violet",
|
||||
name: "Violet",
|
||||
name: "Quiet Room",
|
||||
// The quiet room. No table you can put a laptop on, on purpose.
|
||||
outline: rect(0, WILLOW_S, COURT_W, COURT_S),
|
||||
floor: CARPET_ACCENT,
|
||||
@@ -461,7 +464,7 @@ const ROOMS: Room[] = [
|
||||
// -- The east range -------------------------------------------------------
|
||||
{
|
||||
id: "shop",
|
||||
name: "The Shop",
|
||||
name: "Robotics Lab",
|
||||
// Fabrication, with a four-metre roller shutter onto the yard. That shutter
|
||||
// is the reason the yard is paved rather than planted: work comes out of
|
||||
// this room and is finished in the middle of the building, which is a thing
|
||||
@@ -474,7 +477,7 @@ const ROOMS: Room[] = [
|
||||
// -- The south range ------------------------------------------------------
|
||||
{
|
||||
id: "works",
|
||||
name: "The Works",
|
||||
name: "Agent Floor",
|
||||
// Thirty-one metres of desk floor along the back of the block, glazed onto
|
||||
// the yard on one side and onto the alley on the other. The longest room
|
||||
// here, and the only one that is more than one thing at once.
|
||||
@@ -484,7 +487,7 @@ const ROOMS: Room[] = [
|
||||
},
|
||||
{
|
||||
id: "store",
|
||||
name: "Facilities",
|
||||
name: "Equipment Storage",
|
||||
outline: rect(STORE_W, COURT_S, WIDTH, DEPTH),
|
||||
floor: CONCRETE,
|
||||
ceiling: { height: 3.0, surface: CEILING_TILE },
|
||||
@@ -504,7 +507,7 @@ const ROOMS: Room[] = [
|
||||
*/
|
||||
{
|
||||
id: "court",
|
||||
name: "The Yard",
|
||||
name: "Courtyard Yard",
|
||||
outline: [
|
||||
{ x: COURT_W, z: STAIR_S },
|
||||
{ x: COURT_W, z: COURT_S },
|
||||
@@ -755,7 +758,7 @@ const WALLS: Wall[] = [
|
||||
// ---- Desk banks -----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Twenty-eight desks in two declarations, both in the works.
|
||||
* Twelve desks in one declaration on the agent floor.
|
||||
*
|
||||
* `Plan` expands each into a desk prop, a chair prop and a seat per station,
|
||||
* with ids that are contractual: `works-a-01` … `works-a-14` counting along each
|
||||
@@ -763,9 +766,9 @@ const WALLS: Wall[] = [
|
||||
* furniture. A `Presence` binds to `works-a-04` and has no way to notice that
|
||||
* the bench moved 200 mm, which is the property the whole scheme exists for.
|
||||
*
|
||||
* A second bench is a second bank, for the reason both other packs record: one
|
||||
* `rowPitch` cannot be both the 0.85 m that makes two rows meet back to back and
|
||||
* the 1.95 m of chair, aisle and chair between one bench and the next.
|
||||
* The rest of the long south range stays open for robots, rolling prototypes
|
||||
* and review. Capacity is a plan decision here, not every square metre filled
|
||||
* with a generated workstation.
|
||||
*/
|
||||
const DESK_BANKS: DeskBank[] = [
|
||||
{
|
||||
@@ -774,35 +777,20 @@ const DESK_BANKS: DeskBank[] = [
|
||||
chair: TASK_CHAIR,
|
||||
origin: { x: 3.4, z: 19.2 },
|
||||
rotation: NORTH,
|
||||
columns: 7,
|
||||
columns: 6,
|
||||
rows: 2,
|
||||
pitch: 1.7, // 1.6 m desks with a 100 mm gap
|
||||
rowPitch: 0.85,
|
||||
facingRows: true,
|
||||
seatOffset: 0.6,
|
||||
},
|
||||
{
|
||||
id: "works-b",
|
||||
desk: DESK,
|
||||
chair: TASK_CHAIR,
|
||||
origin: { x: 3.4, z: 23.2 },
|
||||
rotation: NORTH,
|
||||
columns: 7,
|
||||
rows: 2,
|
||||
pitch: 1.7,
|
||||
rowPitch: 0.85,
|
||||
facingRows: true,
|
||||
seatOffset: 0.6,
|
||||
},
|
||||
];
|
||||
|
||||
// ---- Seats ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The forty-one seats on this level that a `DeskBank` cannot generate: chairs
|
||||
* round tables, two people waiting in reception, four standing at a pair of
|
||||
* worktables — and eight at a table in the open air, which is the entry the
|
||||
* other two packs have nothing like.
|
||||
* Nine deliberate ground-level addresses outside the generated agent bench.
|
||||
* They describe useful activity points, not a fabricated headcount.
|
||||
*
|
||||
* A seat is an address and not a chair. The prefixes here (`front`, `mess`,
|
||||
* `hewitt`, `willow`, `violet`, `shop`, `press`, `yard`) are disjoint from both
|
||||
@@ -811,70 +799,22 @@ const DESK_BANKS: DeskBank[] = [
|
||||
* the hard way, and staying disjoint costs nothing.
|
||||
*/
|
||||
const SEATS: Seat[] = [
|
||||
// Reception, and the pair waiting for somebody to come and get them.
|
||||
// One host position inside the front door.
|
||||
{ id: "front-01", position: { x: 13.6, z: 5.0 }, facing: EAST, pose: "sit" },
|
||||
{ id: "front-02", position: { x: 11.6, z: 6.6 }, facing: EAST, pose: "sit" },
|
||||
{ id: "front-03", position: { x: 13.2, z: 6.6 }, facing: WEST, pose: "sit" },
|
||||
|
||||
// The press. Four standing at two worktables, because nobody sits down to cut
|
||||
// a mount board — `pose` is the only thing that tells a consumer how tall to
|
||||
// draw an occupant.
|
||||
{ id: "press-01", position: { x: 3.8, z: 4.4 }, facing: NORTH, pose: "stand" },
|
||||
{ id: "press-02", position: { x: 5.4, z: 4.4 }, facing: NORTH, pose: "stand" },
|
||||
{ id: "press-03", position: { x: 3.8, z: 5.4 }, facing: SOUTH, pose: "stand" },
|
||||
{ id: "press-04", position: { x: 5.4, z: 5.4 }, facing: SOUTH, pose: "stand" },
|
||||
// Two standing capture/review marks in the media studio.
|
||||
{ id: "media-01", position: { x: 3.8, z: 4.4 }, facing: NORTH, pose: "stand" },
|
||||
{ id: "media-02", position: { x: 5.4, z: 4.4 }, facing: NORTH, pose: "stand" },
|
||||
|
||||
// The mess: three perched at the island, two at the table by the glass.
|
||||
{ id: "mess-01", position: { x: 22.2, z: 4.2 }, facing: NORTH, pose: "stand" },
|
||||
{ id: "mess-02", position: { x: 23.0, z: 4.2 }, facing: NORTH, pose: "stand" },
|
||||
{ id: "mess-03", position: { x: 23.8, z: 4.2 }, facing: NORTH, pose: "stand" },
|
||||
{ id: "mess-04", position: { x: 20.2, z: 6.6 }, facing: EAST, pose: "sit" },
|
||||
{ id: "mess-05", position: { x: 23.0, z: 6.6 }, facing: WEST, pose: "sit" },
|
||||
// Two stools at the kitchen island.
|
||||
{ id: "commons-01", position: { x: 22.6, z: 4.2 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "commons-02", position: { x: 23.5, z: 4.2 }, facing: NORTH, pose: "sit" },
|
||||
|
||||
// Hewitt — ten round a 4.8 m table, five a side, both ends left clear so the
|
||||
// people at them can see the display on the party wall.
|
||||
{ id: "hewitt-01", position: { x: 29.4, z: 3.2 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "hewitt-02", position: { x: 30.4, z: 3.2 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "hewitt-03", position: { x: 31.4, z: 3.2 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "hewitt-04", position: { x: 32.4, z: 3.2 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "hewitt-05", position: { x: 33.4, z: 3.2 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "hewitt-06", position: { x: 29.4, z: 5.2 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "hewitt-07", position: { x: 30.4, z: 5.2 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "hewitt-08", position: { x: 31.4, z: 5.2 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "hewitt-09", position: { x: 32.4, z: 5.2 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "hewitt-10", position: { x: 33.4, z: 5.2 }, facing: NORTH, pose: "sit" },
|
||||
|
||||
// Willow, six.
|
||||
{ id: "willow-01", position: { x: 3.4, z: 9.7 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "willow-02", position: { x: 4.4, z: 9.7 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "willow-03", position: { x: 5.4, z: 9.7 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "willow-04", position: { x: 3.4, z: 11.7 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "willow-05", position: { x: 4.4, z: 11.7 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "willow-06", position: { x: 5.4, z: 11.7 }, facing: NORTH, pose: "sit" },
|
||||
|
||||
// Violet, the quiet room: two chairs and nothing else.
|
||||
{ id: "violet-01", position: { x: 4.4, z: 14.4 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "violet-02", position: { x: 4.4, z: 16.2 }, facing: NORTH, pose: "sit" },
|
||||
|
||||
// The shop, standing at the benches.
|
||||
{ id: "shop-01", position: { x: 30.2, z: 11.4 }, facing: NORTH, pose: "stand" },
|
||||
{ id: "shop-02", position: { x: 31.8, z: 11.4 }, facing: NORTH, pose: "stand" },
|
||||
{ id: "shop-03", position: { x: 31.0, z: 14.0 }, facing: NORTH, pose: "stand" },
|
||||
|
||||
/**
|
||||
* The yard table. Eight seats outdoors, which is the pack's whole argument in
|
||||
* one declaration: a seat is a spot on the floor with a name, the format never
|
||||
* asked whether the floor had a roof over it, and a building in this climate
|
||||
* puts a third of its meeting capacity in the middle of the block.
|
||||
*/
|
||||
{ id: "yard-01", position: { x: 17.2, z: 12.4 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "yard-02", position: { x: 18.4, z: 12.4 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "yard-03", position: { x: 19.6, z: 12.4 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "yard-04", position: { x: 20.8, z: 12.4 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "yard-05", position: { x: 17.2, z: 14.4 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "yard-06", position: { x: 18.4, z: 14.4 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "yard-07", position: { x: 19.6, z: 14.4 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "yard-08", position: { x: 20.8, z: 14.4 }, facing: NORTH, pose: "sit" },
|
||||
// Four places in the review room, all with a clean line to the stable screen.
|
||||
{ id: "review-01", position: { x: 29.8, z: 3.2 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "review-02", position: { x: 32.6, z: 3.2 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "review-03", position: { x: 29.8, z: 5.2 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "review-04", position: { x: 32.6, z: 5.2 }, facing: NORTH, pose: "sit" },
|
||||
];
|
||||
|
||||
// ---- Props ----------------------------------------------------------------
|
||||
@@ -953,10 +893,9 @@ const PROPS: Prop[] = [
|
||||
...scatter("press-light", TROFFER, grid(1.6, 1.6, 3, 3, 3.2, 2.6), { elevation: GROUND }),
|
||||
|
||||
// -- The Mess -------------------------------------------------------------
|
||||
// A meeting table doing duty as an island, exactly as the reference pack's
|
||||
// kitchen does. There is no `tera:kitchen.island` and there should not be; the
|
||||
// honest way to get one is to register it in your own namespace.
|
||||
{ id: "mess-island", kind: MEETING_TABLE, position: { x: 23.0, z: 3.0 }, rotation: NORTH },
|
||||
// A complete but restrained commons: one authored run, one island, two stools.
|
||||
{ id: "mess-kitchen-run", kind: KITCHEN, position: { x: 25.7, z: 1.15 }, rotation: NORTH },
|
||||
{ id: "mess-island", kind: ISLAND, position: { x: 23.0, z: 3.0 }, rotation: NORTH },
|
||||
{
|
||||
id: "mess-island-plant",
|
||||
kind: PLANT,
|
||||
@@ -964,13 +903,7 @@ const PROPS: Prop[] = [
|
||||
rotation: NORTH,
|
||||
elevation: 0.74,
|
||||
},
|
||||
...scatter("mess-stool", TASK_CHAIR, grid(22.2, 4.2, 3, 1, 0.8, 0), { rotation: NORTH }),
|
||||
// The counters run down the wall the building's only internal door on this
|
||||
// range is in, not under the street windows: a 1.8 m locker in front of a
|
||||
// window with a 1 m sill is a wall with a window nobody can see out of.
|
||||
...scatter("mess-counter", LOCKER, grid(MESS_E - INT_FACE - 0.25, 1.4, 1, 3, 0, 1.3), {
|
||||
rotation: EAST,
|
||||
}),
|
||||
...scatter("mess-stool", STOOL, grid(22.6, 4.2, 2, 1, 0.9, 0), { rotation: NORTH }),
|
||||
{
|
||||
id: "mess-shelf",
|
||||
kind: SHELF,
|
||||
@@ -985,10 +918,9 @@ const PROPS: Prop[] = [
|
||||
...scatter("mess-light", TROFFER, grid(20.6, 2.0, 3, 2, 2.6, 3.4), { elevation: GROUND }),
|
||||
|
||||
// -- Hewitt ---------------------------------------------------------------
|
||||
{ id: "hewitt-table-w", kind: MEETING_TABLE, position: { x: 30.4, z: 4.2 }, rotation: NORTH },
|
||||
{ id: "hewitt-table-e", kind: MEETING_TABLE, position: { x: 32.8, z: 4.2 }, rotation: NORTH },
|
||||
...scatter("hewitt-chair-n", TASK_CHAIR, grid(29.4, 3.2, 5, 1, 1.0, 0), { rotation: SOUTH }),
|
||||
...scatter("hewitt-chair-s", TASK_CHAIR, grid(29.4, 5.2, 5, 1, 1.0, 0), { rotation: NORTH }),
|
||||
{ id: "hewitt-table", kind: MEETING_TABLE, position: { x: 31.2, z: 4.2 }, rotation: NORTH },
|
||||
...scatter("hewitt-chair-n", TASK_CHAIR, grid(29.8, 3.2, 2, 1, 2.8, 0), { rotation: SOUTH }),
|
||||
...scatter("hewitt-chair-s", TASK_CHAIR, grid(29.8, 5.2, 2, 1, 2.8, 0), { rotation: NORTH }),
|
||||
{
|
||||
id: "hewitt-display",
|
||||
kind: DISPLAY,
|
||||
@@ -1009,8 +941,8 @@ const PROPS: Prop[] = [
|
||||
|
||||
// -- Willow ---------------------------------------------------------------
|
||||
{ id: "willow-table", kind: MEETING_TABLE, position: { x: 4.4, z: 10.7 }, rotation: NORTH },
|
||||
...scatter("willow-chair-n", TASK_CHAIR, grid(3.4, 9.7, 3, 1, 1.0, 0), { rotation: SOUTH }),
|
||||
...scatter("willow-chair-s", TASK_CHAIR, grid(3.4, 11.7, 3, 1, 1.0, 0), { rotation: NORTH }),
|
||||
...scatter("willow-chair-n", TASK_CHAIR, grid(3.4, 9.7, 2, 1, 2.0, 0), { rotation: SOUTH }),
|
||||
...scatter("willow-chair-s", TASK_CHAIR, grid(3.4, 11.7, 2, 1, 2.0, 0), { rotation: NORTH }),
|
||||
{
|
||||
id: "willow-display",
|
||||
kind: DISPLAY,
|
||||
@@ -1063,15 +995,12 @@ const PROPS: Prop[] = [
|
||||
// -- The Works ------------------------------------------------------------
|
||||
// A screen down the spine of each bench: 1.4 m against a 1.7 m pitch leaves a
|
||||
// gap you can talk through, which is the entire argument for a 0.45 m screen.
|
||||
...scatter("works-a-screen", PARTITION, grid(3.4, 19.625, 7, 1, 1.7, 0)),
|
||||
...scatter("works-b-screen", PARTITION, grid(3.4, 23.625, 7, 1, 1.7, 0)),
|
||||
...scatter("works-a-screen", PARTITION, grid(3.4, 19.625, 6, 1, 1.7, 0)),
|
||||
// Pedestals park at the ends of the benches rather than under the desks, which
|
||||
// is where they end up in a building where people move seats.
|
||||
...scatter("works-pedestal", PEDESTAL, [
|
||||
{ x: 2.0, z: 19.6 },
|
||||
{ x: 2.0, z: 23.6 },
|
||||
{ x: 14.8, z: 19.6 },
|
||||
{ x: 14.8, z: 23.6 },
|
||||
{ x: 13.2, z: 19.6 },
|
||||
]),
|
||||
...scatter("works-locker", LOCKER, grid(1.4, COURT_S + INT_FACE + 0.25, 3, 1, 1.3, 0)),
|
||||
{
|
||||
@@ -1127,10 +1056,9 @@ const PROPS: Prop[] = [
|
||||
* light this space in the evening are on the loggia above, hung from a soffit
|
||||
* that exists.
|
||||
*/
|
||||
{ id: "yard-table-w", kind: MEETING_TABLE, position: { x: 18.0, z: 13.4 }, rotation: NORTH },
|
||||
{ id: "yard-table-e", kind: MEETING_TABLE, position: { x: 20.4, z: 13.4 }, rotation: NORTH },
|
||||
...scatter("yard-chair-n", TASK_CHAIR, grid(17.2, 12.4, 4, 1, 1.2, 0), { rotation: SOUTH }),
|
||||
...scatter("yard-chair-s", TASK_CHAIR, grid(17.2, 14.4, 4, 1, 1.2, 0), { rotation: NORTH }),
|
||||
{ id: "yard-table", kind: MEETING_TABLE, position: { x: 19.2, z: 13.4 }, rotation: NORTH },
|
||||
...scatter("yard-chair-n", TASK_CHAIR, grid(18.0, 12.4, 2, 1, 2.4, 0), { rotation: SOUTH }),
|
||||
...scatter("yard-chair-s", TASK_CHAIR, grid(18.0, 14.4, 2, 1, 2.4, 0), { rotation: NORTH }),
|
||||
...scatter("yard-tree", TREE, [
|
||||
{ x: 11.4, z: 12.6 },
|
||||
{ x: 11.4, z: 16.2 },
|
||||
@@ -1158,19 +1086,18 @@ const PROPS: Prop[] = [
|
||||
* twice.
|
||||
*/
|
||||
const ZONES: Zone[] = [
|
||||
{ id: "zone-yard", name: "The Yard", outline: rect(COURT_W, COURT_N, COURT_E, COURT_S), colorKey: "social" },
|
||||
{ id: "zone-yard", name: "Courtyard Yard", outline: rect(COURT_W, COURT_N, COURT_E, COURT_S), colorKey: "social" },
|
||||
{
|
||||
id: "zone-fabrication",
|
||||
name: "Fabrication",
|
||||
name: "Robotics Lab",
|
||||
outline: rect(COURT_E, COURT_N, WIDTH, COURT_S),
|
||||
colorKey: "focus",
|
||||
},
|
||||
{
|
||||
id: "zone-crew",
|
||||
name: "Crew",
|
||||
name: "Agent Floor",
|
||||
outline: rect(2.0, 18.4, 15.6, 24.8),
|
||||
colorKey: "team-a",
|
||||
audience: "private",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1195,7 +1122,7 @@ const ZONES: Zone[] = [
|
||||
const L2_ROOMS: Room[] = [
|
||||
{
|
||||
id: "loft",
|
||||
name: "The Loft",
|
||||
name: "Model Loft",
|
||||
// Twenty-six metres of north-lit floor over the street range, with the
|
||||
// paseo tunnelling underneath it. The one room in this building that could
|
||||
// not exist on the ground floor, because on the ground floor the way in
|
||||
@@ -1206,14 +1133,14 @@ const L2_ROOMS: Room[] = [
|
||||
},
|
||||
{
|
||||
id: "palmetto",
|
||||
name: "Palmetto",
|
||||
name: "Project Review",
|
||||
outline: rect(0, LOGGIA_N, LOGGIA_W, JESSE_N),
|
||||
floor: CARPET_ACCENT,
|
||||
ceiling: null,
|
||||
},
|
||||
{
|
||||
id: "jesse",
|
||||
name: "Jesse",
|
||||
name: "Materials Room",
|
||||
// The project room: concrete, three boards along the end wall, and nowhere
|
||||
// comfortable to sit down. Its equivalent downstairs is the press, and both
|
||||
// rooms read the same way for the same reason.
|
||||
@@ -1237,7 +1164,7 @@ const L2_ROOMS: Room[] = [
|
||||
*/
|
||||
{
|
||||
id: "loggia",
|
||||
name: "The Loggia",
|
||||
name: "Model Walk",
|
||||
outline: [
|
||||
{ x: LOGGIA_W, z: LOGGIA_N },
|
||||
{ x: LOGGIA_W, z: COURT_S },
|
||||
@@ -1367,19 +1294,17 @@ const L2_WALLS: Wall[] = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Twelve desks upstairs, in one bank. Seat ids are `loft-a-01` … `loft-a-12`,
|
||||
* restarting at 01 as every bank's do and unique building-wide because the
|
||||
* prefix is.
|
||||
* Two workstations upstairs for model and environment review.
|
||||
*/
|
||||
const L2_DESK_BANKS: DeskBank[] = [
|
||||
{
|
||||
id: "loft-a",
|
||||
desk: DESK,
|
||||
chair: TASK_CHAIR,
|
||||
origin: { x: 8.4, z: 1.9 },
|
||||
origin: { x: 8.4, z: 2.3 },
|
||||
rotation: NORTH,
|
||||
columns: 6,
|
||||
rows: 2,
|
||||
columns: 2,
|
||||
rows: 1,
|
||||
pitch: 1.7,
|
||||
rowPitch: 0.85,
|
||||
facingRows: true,
|
||||
@@ -1388,34 +1313,16 @@ const L2_DESK_BANKS: DeskBank[] = [
|
||||
];
|
||||
|
||||
const L2_SEATS: Seat[] = [
|
||||
// Palmetto, six round a 2.4 m table.
|
||||
{ id: "palmetto-01", position: { x: 2.6, z: 7.9 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "palmetto-02", position: { x: 3.6, z: 7.9 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "palmetto-03", position: { x: 4.6, z: 7.9 }, facing: SOUTH, pose: "sit" },
|
||||
{ id: "palmetto-04", position: { x: 2.6, z: 9.9 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "palmetto-05", position: { x: 3.6, z: 9.9 }, facing: NORTH, pose: "sit" },
|
||||
{ id: "palmetto-06", position: { x: 4.6, z: 9.9 }, facing: NORTH, pose: "sit" },
|
||||
|
||||
// Jesse. Standing at the boards, like the reference pack's project room.
|
||||
{ id: "jesse-01", position: { x: 1.4, z: 16.4 }, facing: SOUTH, pose: "stand" },
|
||||
{ id: "jesse-02", position: { x: 3.4, z: 16.4 }, facing: SOUTH, pose: "stand" },
|
||||
{ id: "jesse-03", position: { x: 5.4, z: 16.4 }, facing: SOUTH, pose: "stand" },
|
||||
|
||||
// The loggia, looking down into the yard. Set back from the rail, because a
|
||||
// chair against a balustrade is a chair nobody chooses.
|
||||
// One optional review perch, set back from the open edge.
|
||||
{ id: "loggia-01", position: { x: 8.2, z: 12.0 }, facing: EAST, pose: "sit" },
|
||||
{ id: "loggia-02", position: { x: 8.2, z: 13.6 }, facing: EAST, pose: "sit" },
|
||||
{ id: "loggia-03", position: { x: 12.0, z: 6.9 }, facing: SOUTH, pose: "sit" },
|
||||
];
|
||||
|
||||
const L2_PROPS: Prop[] = [
|
||||
// -- The Loft -------------------------------------------------------------
|
||||
...scatter("loft-screen", PARTITION, grid(8.4, 2.325, 6, 1, 1.7, 0)),
|
||||
...scatter("loft-screen", PARTITION, grid(8.4, 2.325, 2, 1, 1.7, 0)),
|
||||
...scatter("loft-pedestal", PEDESTAL, [
|
||||
{ x: 7.0, z: 1.4 },
|
||||
{ x: 7.0, z: 3.2 },
|
||||
{ x: 18.4, z: 1.4 },
|
||||
{ x: 18.4, z: 3.2 },
|
||||
]),
|
||||
...scatter("loft-locker", LOCKER, grid(LOFT_E - EXT_FACE - 0.25, 1.4, 1, 3, 0, 1.3), {
|
||||
rotation: EAST,
|
||||
@@ -1442,8 +1349,8 @@ const L2_PROPS: Prop[] = [
|
||||
|
||||
// -- Palmetto -------------------------------------------------------------
|
||||
{ id: "palmetto-table", kind: MEETING_TABLE, position: { x: 3.6, z: 8.9 }, rotation: NORTH },
|
||||
...scatter("palmetto-chair-n", TASK_CHAIR, grid(2.6, 7.9, 3, 1, 1.0, 0), { rotation: SOUTH }),
|
||||
...scatter("palmetto-chair-s", TASK_CHAIR, grid(2.6, 9.9, 3, 1, 1.0, 0), { rotation: NORTH }),
|
||||
...scatter("palmetto-chair-n", TASK_CHAIR, grid(2.6, 7.9, 2, 1, 2.0, 0), { rotation: SOUTH }),
|
||||
...scatter("palmetto-chair-s", TASK_CHAIR, grid(2.6, 9.9, 2, 1, 2.0, 0), { rotation: NORTH }),
|
||||
{
|
||||
id: "palmetto-display",
|
||||
kind: DISPLAY,
|
||||
@@ -1469,9 +1376,7 @@ const L2_PROPS: Prop[] = [
|
||||
// courtyard for one to hang from.
|
||||
...scatter("loggia-chair", LOUNGE_CHAIR, [
|
||||
{ x: 8.2, z: 12.0 },
|
||||
{ x: 8.2, z: 13.6 },
|
||||
], { rotation: EAST }),
|
||||
{ id: "loggia-chair-n", kind: LOUNGE_CHAIR, position: { x: 12.0, z: 6.9 }, rotation: SOUTH },
|
||||
{ id: "loggia-side", kind: SIDE_TABLE, position: { x: 8.2, z: 12.8 }, rotation: NORTH },
|
||||
...scatter("loggia-plant", PLANT, [
|
||||
{ x: 8.9, z: 9.0 },
|
||||
@@ -1486,7 +1391,7 @@ const L2_PROPS: Prop[] = [
|
||||
const L2_ZONES: Zone[] = [
|
||||
{
|
||||
id: "zone-loggia",
|
||||
name: "The Loggia",
|
||||
name: "Model Walk",
|
||||
outline: rect(LOGGIA_W, LOGGIA_N, COURT_W, COURT_S),
|
||||
colorKey: "social",
|
||||
},
|
||||
@@ -1515,68 +1420,68 @@ const VIEWPOINTS: Viewpoint[] = [
|
||||
{
|
||||
id: "paseo",
|
||||
number: "01",
|
||||
label: "The Paseo",
|
||||
shortLabel: "Paseo",
|
||||
label: "The Front Door",
|
||||
shortLabel: "Front Door",
|
||||
levelId: "level-1",
|
||||
focus: { at: { x: 18.1, z: 9.0 }, distance: 8, height: 1.7, rotation: Math.PI },
|
||||
description:
|
||||
"Standing under the street arch looking down the passage into the yard. Three metres wide and eight and a half from the pavement to the yard, and the only way into this building anybody arrives by — everything else here is on the other side of it.",
|
||||
"Arrival under the street arch, looking down the entry passage into the courtyard. The route stays clear from the front door to every ground-floor workspace.",
|
||||
},
|
||||
{
|
||||
id: "block",
|
||||
number: "02",
|
||||
label: "The Whole Block",
|
||||
shortLabel: "The Block",
|
||||
label: "The LA Office",
|
||||
shortLabel: "LA Office",
|
||||
levelId: "level-1",
|
||||
// Aimed at the middle of the yard rather than the middle of the plate,
|
||||
// because the middle of the plate is the thing that is not there.
|
||||
focus: { at: { x: 18.0, z: 13.0 }, distance: 44, height: 26, rotation: 0.5 },
|
||||
description:
|
||||
"Thirty-six metres by twenty-six with a hole in the middle of it. A ring of rooms never more than nine and a half metres deep on the long sides, two storeys on two sides and one on the other two, and the courtyard doing the job a corridor does in every other pack in this directory.",
|
||||
"The complete LA HQ: a low brick ring around an open courtyard, with the agent floor, robotics lab, media studio, review room and commons visible as one working environment.",
|
||||
},
|
||||
{
|
||||
id: "yard",
|
||||
number: "03",
|
||||
label: "The Yard",
|
||||
shortLabel: "Yard",
|
||||
label: "Courtyard Review",
|
||||
shortLabel: "Courtyard",
|
||||
levelId: "level-2",
|
||||
// From the loggia's west arm, looking east across the void at the far range.
|
||||
// On level 2, so `height` is measured from the walkway rather than the
|
||||
// pavers five metres below it.
|
||||
focus: { at: { x: 18.0, z: 12.0 }, distance: 9.6, height: 1.7, rotation: -Math.PI / 2 },
|
||||
description:
|
||||
"From the walkway, looking across the middle of the block. Sixteen metres of open air with a table in it, the shop's shutter on the far side, and nothing overhead — the one room in this office that is not a room.",
|
||||
"From the model walk, across the open-air review yard toward the robotics lab. Four loose chairs keep the courtyard useful without turning circulation into an auditorium.",
|
||||
},
|
||||
{
|
||||
id: "works",
|
||||
number: "04",
|
||||
label: "The Works",
|
||||
shortLabel: "Works",
|
||||
label: "Agent Floor",
|
||||
shortLabel: "Agent Floor",
|
||||
levelId: "level-1",
|
||||
focus: { at: { x: 8.6, z: 21.6 }, distance: 12, height: 4.0, rotation: 1.2 },
|
||||
description:
|
||||
"Twenty-eight seats in two benches along the back of the block, glazed onto the yard on one side and the alley on the other. The bench nearer the yard is works-a-01 through works-a-14.",
|
||||
"Twelve agent workstations in one focused bench, leaving the rest of the long floor clear for rolling prototypes, activity robots and changing environment work.",
|
||||
},
|
||||
{
|
||||
id: "shop",
|
||||
number: "05",
|
||||
label: "The Shop",
|
||||
shortLabel: "Shop",
|
||||
label: "Robotics Lab",
|
||||
shortLabel: "Robotics Lab",
|
||||
levelId: "level-1",
|
||||
// Standing in the yard looking east through the open shutter.
|
||||
focus: { at: { x: 31.0, z: 13.0 }, distance: 9, height: 2.6, rotation: 4.9 },
|
||||
description:
|
||||
"Four metres of roller shutter onto the courtyard, with the benches inside it and the party wall behind them. Work comes out of this room and gets finished in the middle of the building.",
|
||||
"The robotics lab opens through a four-metre shutter onto the yard, keeping benches, storage and test circulation connected without inventing a separate facility.",
|
||||
},
|
||||
{
|
||||
id: "loft",
|
||||
number: "06",
|
||||
label: "The Loft",
|
||||
shortLabel: "Loft",
|
||||
label: "Model Loft",
|
||||
shortLabel: "Model Loft",
|
||||
levelId: "level-2",
|
||||
focus: { at: { x: 12.0, z: 3.0 }, distance: 13, height: 4.0, rotation: 1.4 },
|
||||
description:
|
||||
"Twenty-six metres of unbroken floor over the street range, with the paseo tunnelling under the far end of it. Twelve seats, north light on one side and the loggia on the other.",
|
||||
"Two model-review workstations on the north-lit upper floor, beside boards, display surfaces and the open model walk above the yard.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1626,7 +1531,7 @@ const LEVEL_2: Level = {
|
||||
|
||||
export const MATEO_COURT: Office = {
|
||||
id: "mateo-court",
|
||||
name: "Mateo Court",
|
||||
name: "LA HQ · Office",
|
||||
levels: [LEVEL_1, LEVEL_2],
|
||||
viewpoints: VIEWPOINTS,
|
||||
/**
|
||||
@@ -1639,11 +1544,11 @@ export const MATEO_COURT: Office = {
|
||||
site: MATEO_COURT_SITE,
|
||||
meta: {
|
||||
description:
|
||||
"A courtyard block in the Arts District: a ring of rooms round a yard with the roof off, an outdoor stair and an open walkway over it. The third pack, and the one where the circulation is outside.",
|
||||
"Lumbridge's LA HQ office: an authored courtyard workplace with a front door, twelve-seat agent floor, robotics lab, media studio, four-seat review room, kitchen commons, yard and two-seat model loft.",
|
||||
author: "Lumbridge",
|
||||
license: "CC0-1.0",
|
||||
version: "1.0.0",
|
||||
updated: "2026-08-07",
|
||||
version: "2.0.0",
|
||||
updated: "2026-08-19",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Small, deterministic runtime facts for the shipped office environments.
|
||||
*
|
||||
* Activity robots are authored here rather than inferred from storey count. A
|
||||
* floor is geometry, not a staffing instruction: the SF studio should have one
|
||||
* useful machine, LA has two distinct work areas, and a building environment
|
||||
* still under construction should not wake a synthetic workforce merely
|
||||
* because it has levels. The extra job metadata is ignored by today's wander
|
||||
* controller but is deliberately carried beside `RobotSpec`, ready for the
|
||||
* robot-jobs runtime without changing stable robot ids.
|
||||
*/
|
||||
|
||||
import type { RobotSpec } from "../interiors/robots.ts";
|
||||
import type { Office } from "../interiors/types.ts";
|
||||
import type { ShippedOfficeId } from "./sites.ts";
|
||||
|
||||
export type OfficeRobotJobSet = "studio-support" | "office-operations" | "model-review";
|
||||
|
||||
export interface OfficeActivityRobotSpec extends RobotSpec {
|
||||
id: string;
|
||||
/** Stable per-robot seed for a future job scheduler or replay harness. */
|
||||
seed: number;
|
||||
jobSet: OfficeRobotJobSet;
|
||||
}
|
||||
|
||||
const OFFICE_ACTIVITY_ROBOTS: Readonly<Record<ShippedOfficeId, readonly OfficeActivityRobotSpec[]>> = {
|
||||
"lumbridge-hq": [
|
||||
{ id: "sf-studio-activity-01", levelId: "level-1", seed: 0x53464851, jobSet: "studio-support" },
|
||||
],
|
||||
"frontier-valley": [],
|
||||
"mateo-court": [
|
||||
{ id: "la-office-activity-01", levelId: "level-1", seed: 0x4c414851, jobSet: "office-operations" },
|
||||
{ id: "la-office-activity-02", levelId: "level-2", seed: 0x4c414852, jobSet: "model-review" },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve fresh specs and discard any whose authored level is absent.
|
||||
*
|
||||
* The level check keeps downloaded or locally modified packs safe, while fresh
|
||||
* objects stop one consumer's scheduler state from mutating the next scene or
|
||||
* determinism test.
|
||||
*/
|
||||
export function activityRobotsForOffice(
|
||||
office: Pick<Office, "id" | "levels">,
|
||||
): OfficeActivityRobotSpec[] {
|
||||
const specs = OFFICE_ACTIVITY_ROBOTS[office.id as ShippedOfficeId] ?? [];
|
||||
const levels = new Set(office.levels.map((level) => level.id));
|
||||
return specs
|
||||
.filter((spec) => levels.has(spec.levelId))
|
||||
.map((spec) => ({ ...spec }));
|
||||
}
|
||||
+47
-4
@@ -100,8 +100,51 @@ export const MATEO_COURT_SITE: OfficeSite = {
|
||||
* them and by `office.test.ts` asserting that each pack's site is the very
|
||||
* object named here — identity, not equality, so a restated coordinate fails.
|
||||
*/
|
||||
export const OFFICE_SITES: { id: string; name: string; site: OfficeSite }[] = [
|
||||
{ id: "lumbridge-hq", name: "Lumbridge HQ", site: LUMBRIDGE_HQ_SITE },
|
||||
{ id: "frontier-valley", name: "Frontier Valley", site: FRONTIER_VALLEY_SITE },
|
||||
{ id: "mateo-court", name: "Mateo Court", site: MATEO_COURT_SITE },
|
||||
export const SHIPPED_OFFICE_IDS = ["lumbridge-hq", "frontier-valley", "mateo-court"] as const;
|
||||
|
||||
export type ShippedOfficeId = (typeof SHIPPED_OFFICE_IDS)[number];
|
||||
export type OfficeStatus = "active" | "building";
|
||||
|
||||
/**
|
||||
* The public, eager metadata for one shipped environment.
|
||||
*
|
||||
* `status` is about the authored runtime, not a lease or a headcount. `active`
|
||||
* means the environment is ready to enter; it never implies that people are in
|
||||
* the building. `entryCopy` is intentionally operational and contains no
|
||||
* invented street address or staffing claim.
|
||||
*/
|
||||
export interface OfficeDirectoryEntry {
|
||||
id: ShippedOfficeId;
|
||||
name: string;
|
||||
status: OfficeStatus;
|
||||
entryCopy: string;
|
||||
site: OfficeSite;
|
||||
}
|
||||
|
||||
export const OFFICE_SITES: readonly OfficeDirectoryEntry[] = [
|
||||
{
|
||||
id: "lumbridge-hq",
|
||||
name: "SF HQ · Studio",
|
||||
status: "active",
|
||||
entryCopy: "Enter the compact live/work studio",
|
||||
site: LUMBRIDGE_HQ_SITE,
|
||||
},
|
||||
{
|
||||
id: "frontier-valley",
|
||||
name: "Frontier Valley",
|
||||
status: "building",
|
||||
entryCopy: "Preview the hangar environment in progress",
|
||||
site: FRONTIER_VALLEY_SITE,
|
||||
},
|
||||
{
|
||||
id: "mateo-court",
|
||||
name: "LA HQ · Office",
|
||||
status: "active",
|
||||
entryCopy: "Enter the courtyard office and robotics lab",
|
||||
site: MATEO_COURT_SITE,
|
||||
},
|
||||
];
|
||||
|
||||
export function officeDirectoryEntry(id: string): OfficeDirectoryEntry | undefined {
|
||||
return OFFICE_SITES.find((entry) => entry.id === id);
|
||||
}
|
||||
|
||||
+61
-66
@@ -118,25 +118,21 @@ describe("the reference pack resolves cleanly", () => {
|
||||
* and setting it to the ceiling height is the classic way to bury one storey's
|
||||
* slab inside the one below.
|
||||
*/
|
||||
it("stacks the storeys floor-to-floor, not floor-to-ceiling", () => {
|
||||
const [first, second] = plan.levels;
|
||||
assert.ok(first && second, "both storeys should resolve");
|
||||
assert.equal(first.id, "level-1");
|
||||
assert.equal(second.id, "level-2");
|
||||
assert.equal(first.floorY, 0);
|
||||
|
||||
const ceiling = LUMBRIDGE_HQ.levels[0]?.wallHeight ?? 0;
|
||||
assert.ok(
|
||||
second.floorY > ceiling,
|
||||
`level 2's slab at ${second.floorY} m is inside level 1, whose ceiling is at ${ceiling} m`,
|
||||
);
|
||||
it("is an honest twelve-by-nine metre single-level studio", () => {
|
||||
assert.deepEqual(plan.levels.map((level) => [level.id, level.floorY]), [["level-1", 0]]);
|
||||
assert.deepEqual(LUMBRIDGE_HQ.levels[0]?.floorplan.rooms[3]?.outline, [
|
||||
{ x: 0.2, z: 3.15 },
|
||||
{ x: 0.2, z: 8.8 },
|
||||
{ x: 11.8, z: 8.8 },
|
||||
{ x: 11.8, z: 3.15 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps every viewpoint on a level that exists", () => {
|
||||
// `Plan` drops a viewpoint whose level does not resolve, so this is really
|
||||
// an assertion that none were dropped — which the count makes visible in a
|
||||
// way `problems` being empty already implies but does not state.
|
||||
assert.equal(plan.viewpoints.length, 8);
|
||||
assert.equal(plan.viewpoints.length, 5);
|
||||
const levels = new Set(plan.levels.map((l) => l.id));
|
||||
for (const v of plan.viewpoints) assert.ok(levels.has(v.levelId), `${v.id} is on nothing`);
|
||||
});
|
||||
@@ -163,7 +159,8 @@ describe("seat ids", () => {
|
||||
*/
|
||||
it("expand the desk banks to the documented ids", () => {
|
||||
const ids = new Set(seats.map((s) => s.id));
|
||||
for (const id of ["eng-01", "eng-12", "studio-01", "studio-12", "studio-b-01"]) {
|
||||
assert.equal(seats.length, 5);
|
||||
for (const id of ["sf-agent-01", "sf-agent-02", "sf-island-01", "sf-island-02", "sf-lounge-01"]) {
|
||||
assert.ok(ids.has(id), `${id} is missing`);
|
||||
}
|
||||
});
|
||||
@@ -186,8 +183,7 @@ describe("every room is reachable on foot", () => {
|
||||
|
||||
/** Where somebody arrives: the entrance downstairs, the landing upstairs. */
|
||||
const ENTRY: Record<string, { x: number; z: number }> = {
|
||||
"level-1": { x: 1.2, z: 4.9 },
|
||||
"level-2": { x: 2.4, z: 4.0 },
|
||||
"level-1": { x: 1.02, z: 7.68 },
|
||||
};
|
||||
|
||||
function reachableRooms(levelId: string): Set<string> {
|
||||
@@ -257,24 +253,12 @@ describe("every room is reachable on foot", () => {
|
||||
* the only place the difference between a balustrade and a handrail-shaped prop
|
||||
* is observable. A prop would leave the edge open and nothing would look wrong.
|
||||
*/
|
||||
describe("the gallery balustrade", () => {
|
||||
const OUTSIDE = 0.6;
|
||||
|
||||
it("stops a walker at all three open edges", () => {
|
||||
const edges: [string, { x: number; z: number }, { x: number; z: number }][] = [
|
||||
// North edge, at z = 2.0: standing on the gallery, stepping north.
|
||||
["north", { x: 36.0, z: 2.6 }, { x: 36.0, z: 2.0 - OUTSIDE }],
|
||||
// East edge, at x = 38.2: stepping out over the commons.
|
||||
["east", { x: 37.6, z: 9.0 }, { x: 38.2 + OUTSIDE, z: 9.0 }],
|
||||
// South edge, at z = 16.0.
|
||||
["south", { x: 36.0, z: 15.4 }, { x: 36.0, z: 16.0 + OUTSIDE }],
|
||||
];
|
||||
for (const [name, from, to] of edges) {
|
||||
assert.ok(
|
||||
plan.blocked("level-2", from, to, 0.3),
|
||||
`the ${name} edge of the gallery lets a walker off a 16.8 m drop`,
|
||||
);
|
||||
}
|
||||
describe("the studio arrival", () => {
|
||||
it("starts inside the live/work room with collision-clear movement", () => {
|
||||
const arrival = LUMBRIDGE_HQ.viewpoints[0]?.focus.at;
|
||||
assert.ok(arrival);
|
||||
assert.equal(plan.roomAt("level-1", arrival)?.id, "live-work");
|
||||
assert.equal(plan.blocked("level-1", arrival, { x: arrival.x + 0.6, z: arrival.z }, 0.3), false);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -282,8 +266,11 @@ describe("the gallery balustrade", () => {
|
||||
* balcony you cannot fall off and also cannot reach is a balcony nobody has
|
||||
* noticed is broken.
|
||||
*/
|
||||
it("does not seal the gallery off from the studio", () => {
|
||||
assert.ok(!plan.blocked("level-2", { x: 33.0, z: 7.4 }, { x: 35.0, z: 7.4 }, 0.3));
|
||||
it("keeps all five authored views meaningful and distinct", () => {
|
||||
assert.deepEqual(
|
||||
LUMBRIDGE_HQ.viewpoints.map((view) => view.id),
|
||||
["arrival", "studio", "agent-bench", "demo-lounge", "kitchen-home"],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -293,38 +280,26 @@ describe("the gallery balustrade", () => {
|
||||
* `Wall` carries a height; get the second wrong and the void has a lid at 2.8 m
|
||||
* that you cannot see but the light rig can.
|
||||
*/
|
||||
describe("the commons is open to both storeys", () => {
|
||||
it("has no ceiling", () => {
|
||||
describe("the studio programme", () => {
|
||||
it("names every promised live/work zone without pretending it is a tower floor", () => {
|
||||
const level1 = plan.levels.find((l) => l.id === "level-1");
|
||||
const commons = level1?.rooms.find((r) => r.id === "commons");
|
||||
assert.ok(commons, "the commons is missing");
|
||||
assert.equal(commons.ceiling, null);
|
||||
assert.deepEqual(
|
||||
level1?.rooms.map((room) => room.name),
|
||||
["Kitchen", "Sleeping Alcove", "Bath / Storage", "Live / Work Studio"],
|
||||
);
|
||||
});
|
||||
|
||||
it("is enclosed to two storeys rather than one", () => {
|
||||
const [level1, level2] = plan.levels;
|
||||
assert.ok(level1 && level2);
|
||||
|
||||
// The height the void *should* be, derived rather than typed: everything up
|
||||
// to level 2's slab, plus level 2's own room. That is what "open to both
|
||||
// storeys" means, and it stays true whatever the interstitial is set to.
|
||||
const ceiling = LUMBRIDGE_HQ.levels[1]?.wallHeight ?? 0;
|
||||
const expected = level2.floorY + ceiling;
|
||||
|
||||
// `solid` only. The splitting pass emits a run per piece, so a wall with a
|
||||
// door in it also yields a `lintel` over the opening — correct, and not the
|
||||
// wall's height.
|
||||
const wingWalls = level1.runs.filter(
|
||||
(run) => run.wallId.startsWith("wing-") && run.role === "solid",
|
||||
);
|
||||
assert.ok(wingWalls.length > 0, "the wing has no walls");
|
||||
for (const run of wingWalls) {
|
||||
assert.equal(
|
||||
run.top - run.bottom,
|
||||
expected,
|
||||
`${run.wallId} is ${run.top - run.bottom} m tall, not the ${expected} m the void needs`,
|
||||
);
|
||||
}
|
||||
it("uses the complete original habitat kit", () => {
|
||||
const kinds = new Set((LUMBRIDGE_HQ.levels[0]?.floorplan.props ?? []).map((prop) => prop.kind));
|
||||
for (const id of [
|
||||
"tera:bed.platform",
|
||||
"tera:sofa.modular",
|
||||
"tera:kitchen.run",
|
||||
"tera:kitchen.island",
|
||||
"tera:storage.wardrobe",
|
||||
"tera:seat.stool",
|
||||
"tera:light.floor",
|
||||
]) assert.ok(kinds.has(id), `${id} is not placed in SF HQ`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -530,6 +505,17 @@ describe("the office site table", () => {
|
||||
assert.equal(entry.site, pack.site, `${pack.id} has a site of its own`);
|
||||
}
|
||||
});
|
||||
|
||||
it("publishes truthful environment names and runtime status", () => {
|
||||
assert.deepEqual(
|
||||
OFFICE_SITES.map(({ id, name, status }) => ({ id, name, status })),
|
||||
[
|
||||
{ id: "lumbridge-hq", name: "SF HQ · Studio", status: "active" },
|
||||
{ id: "frontier-valley", name: "Frontier Valley", status: "building" },
|
||||
{ id: "mateo-court", name: "LA HQ · Office", status: "active" },
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -571,6 +557,15 @@ describe("the Mateo Court pack", () => {
|
||||
assert.deepEqual(mine.filter((id) => others.has(id)), []);
|
||||
});
|
||||
|
||||
it("caps the authored office at twenty-four useful seat addresses", () => {
|
||||
const ids = mc.allSeats().map((seat) => seat.id);
|
||||
assert.equal(ids.length, 24);
|
||||
for (const id of ["works-a-01", "works-a-12", "loft-a-01", "loft-a-02", "review-04", "loggia-01"]) {
|
||||
assert.ok(ids.includes(id), `${id} is missing`);
|
||||
}
|
||||
assert.equal(ids.some((id) => id.startsWith("works-b-")), false);
|
||||
});
|
||||
|
||||
/**
|
||||
* **The check that matters most in this pack.**
|
||||
*
|
||||
@@ -613,7 +608,7 @@ describe("the Mateo Court pack", () => {
|
||||
});
|
||||
|
||||
it("stands its upper-floor occupants on the deck", () => {
|
||||
for (const id of ["loggia-01", "palmetto-01"]) {
|
||||
for (const id of ["loggia-01", "loft-a-01"]) {
|
||||
assert.equal(mc.seat(id)?.y, 5, `${id} is not on the deck`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import * as THREE from "three";
|
||||
import { createAssetContext, kit } from "../assets/kit.ts";
|
||||
import { MaterialRegistry } from "../assets/materials.ts";
|
||||
import "../assets/office/index.ts";
|
||||
|
||||
const HABITAT_IDS = [
|
||||
"tera:bed.platform",
|
||||
"tera:sofa.modular",
|
||||
"tera:kitchen.run",
|
||||
"tera:kitchen.island",
|
||||
"tera:storage.wardrobe",
|
||||
"tera:seat.stool",
|
||||
"tera:light.floor",
|
||||
] as const;
|
||||
|
||||
function seeded(): () => number {
|
||||
let value = 0x12345678;
|
||||
return () => {
|
||||
value = (Math.imul(value, 1664525) + 1013904223) >>> 0;
|
||||
return value / 0x1_0000_0000;
|
||||
};
|
||||
}
|
||||
|
||||
function disposeObject(object: THREE.Object3D): void {
|
||||
object.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) child.geometry.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
describe("procedural habitat assets", () => {
|
||||
it("registers finite metre-scale footprints for the complete studio kit", () => {
|
||||
for (const id of HABITAT_IDS) {
|
||||
assert.equal(kit.has(id), true, `${id} is not registered`);
|
||||
const footprint = kit.footprintOf(id);
|
||||
for (const value of [footprint.width, footprint.depth, footprint.height]) {
|
||||
assert.ok(Number.isFinite(value) && value > 0 && value < 5, `${id} has invalid footprint ${value}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("builds deterministic, non-placeholder geometry with finite bounds", () => {
|
||||
const materials = new MaterialRegistry({ quality: "low" });
|
||||
try {
|
||||
for (const id of HABITAT_IDS) {
|
||||
const first = kit.build(id, createAssetContext({ materials, rand: seeded() }));
|
||||
const second = kit.build(id, createAssetContext({ materials, rand: seeded() }));
|
||||
assert.equal(first.userData.assetId, id);
|
||||
assert.notEqual(first.userData.missing, true);
|
||||
const firstBox = new THREE.Box3().setFromObject(first);
|
||||
const secondBox = new THREE.Box3().setFromObject(second);
|
||||
const encode = (box: THREE.Box3) => [...box.min.toArray(), ...box.max.toArray()];
|
||||
assert.deepEqual(encode(firstBox), encode(secondBox), `${id} is not deterministic`);
|
||||
assert.ok(encode(firstBox).every(Number.isFinite), `${id} has non-finite bounds`);
|
||||
assert.ok(firstBox.max.y > 0, `${id} has no visible height`);
|
||||
disposeObject(first);
|
||||
disposeObject(second);
|
||||
}
|
||||
} finally {
|
||||
materials.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { samplePresenceAt, samplePresenceForOfficeAt } from "../adapters/sample.ts";
|
||||
import FRONTIER_VALLEY from "../offices/frontier-valley.ts";
|
||||
import LUMBRIDGE_HQ from "../offices/lumbridge-hq.ts";
|
||||
import MATEO_COURT from "../offices/mateo-court.ts";
|
||||
import { activityRobotsForOffice } from "../offices/runtime.ts";
|
||||
|
||||
describe("office-keyed runtime activity", () => {
|
||||
it("authors one deterministic SF robot, two LA robots, and none in a building preview", () => {
|
||||
assert.deepEqual(activityRobotsForOffice(LUMBRIDGE_HQ), [
|
||||
{ id: "sf-studio-activity-01", levelId: "level-1", seed: 0x53464851, jobSet: "studio-support" },
|
||||
]);
|
||||
assert.deepEqual(activityRobotsForOffice(MATEO_COURT), [
|
||||
{ id: "la-office-activity-01", levelId: "level-1", seed: 0x4c414851, jobSet: "office-operations" },
|
||||
{ id: "la-office-activity-02", levelId: "level-2", seed: 0x4c414852, jobSet: "model-review" },
|
||||
]);
|
||||
assert.deepEqual(activityRobotsForOffice(FRONTIER_VALLEY), []);
|
||||
});
|
||||
|
||||
it("returns defensive specs and drops a robot when a local pack removes its level", () => {
|
||||
const first = activityRobotsForOffice(MATEO_COURT);
|
||||
first[0]!.id = "mutated";
|
||||
assert.equal(activityRobotsForOffice(MATEO_COURT)[0]?.id, "la-office-activity-01");
|
||||
assert.deepEqual(activityRobotsForOffice({ id: MATEO_COURT.id, levels: [MATEO_COURT.levels[0]!] }), [
|
||||
{ id: "la-office-activity-01", levelId: "level-1", seed: 0x4c414851, jobSet: "office-operations" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("office-keyed presence fallback", () => {
|
||||
const workingHour = new Date(2026, 7, 19, 11, 0, 0);
|
||||
|
||||
it("never fabricates people at either active HQ", () => {
|
||||
assert.deepEqual(samplePresenceForOfficeAt("lumbridge-hq", workingHour), []);
|
||||
assert.deepEqual(samplePresenceForOfficeAt("mateo-court", workingHour), []);
|
||||
});
|
||||
|
||||
it("retains the legacy roster only as an explicit renderer fixture", () => {
|
||||
assert.ok(samplePresenceAt(workingHour).length > 0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user