/** * Lane I — the slot inventory. * * A "slot" is a named place in the scene where a custom GLB may replace the * procedural mesh the game builds today. Slot ids are a closed set on purpose: * the editor (lane J) lists them, the manifest keys on them, and an unknown key * in a hand-edited manifest should be reported rather than silently ignored. * * Kept free of `three` imports so the validator can be node-tested headlessly. */ export const SLOT_IDS = [ 'blob.body', 'blob.face', 'ghost.body', 'cannon.base', 'cannon.barrel', 'machine.plate', 'machine.boot', 'machine.bucket', 'machine.arch', 'machine.belt', 'machine.fan', 'machine.seesaw', 'course.scenery.cereal', 'course.scenery.block', 'course.finish', 'course.tunnel', 'course.tramp', 'fx.puddle', ] as const export type SlotId = (typeof SLOT_IDS)[number] const SLOT_SET: ReadonlySet = new Set(SLOT_IDS) export function isSlotId(v: unknown): v is SlotId { return typeof v === 'string' && SLOT_SET.has(v) } /** Plain-language labels for the editor UI (lane J reads this). */ export const SLOT_LABELS: Record = { 'blob.body': 'Blobbo body (paintable)', 'blob.face': 'Blobbo eyes', 'ghost.body': 'Ghost racer', 'cannon.base': 'Paint cannon base', 'cannon.barrel': 'Paint cannon barrel', 'machine.plate': 'Pressure plate', 'machine.boot': 'Spring boot', 'machine.bucket': 'Paint bucket', 'machine.arch': 'Bubble arch', 'machine.belt': 'Conveyor belt', 'machine.fan': 'Fan', 'machine.seesaw': 'See-saw plank', 'course.scenery.cereal': 'Giant cereal box', 'course.scenery.block': 'Purple block', 'course.finish': 'Finish pad', 'course.tunnel': 'MINI tunnel roof', 'course.tramp': 'Trampoline / gap launcher', 'fx.puddle': 'Paint puddle', } /** * Slots whose replacement is constrained beyond "looks different". Surfaced in * the editor so John sees the rule at the moment he drops a file on the slot. */ export const SLOT_NOTES: Partial> = { 'blob.body': 'Must be ONE mesh with ONE material and clean 0..1 UVs — the paint is stamped ' + 'through those UVs. Auto-resized to the 1.0u body; manifest scale is ignored here.', 'ghost.body': 'Copied from blob.body automatically unless you set this slot; materials are ' + 'forced translucent.', 'machine.boot': 'Model it standing on the floor, origin at the base — it shakes about its origin.', 'machine.bucket': 'Origin at the TIPPING EDGE, pours toward +X, up axis +Y.', 'machine.belt': 'Model the belt slab only; the moving chevrons stay procedural.', 'machine.fan': 'Face +Z. The blades stay procedural so they keep spinning.', 'machine.seesaw': 'Plank only, long axis along X, origin at the plank centre.', 'machine.plate': 'Frame only — the pressed pad stays procedural so it can light up.', 'cannon.base': 'The stand only; the barrel is a separate slot because it aims every step.', 'cannon.barrel': 'The tube only, pointing +Z — the aiming pivot stays procedural.', 'course.finish': 'Fill the 18 x 1.2 x 6 pad volume — its collider never moves.', 'course.tunnel': 'Roof slab only (7 x 0.3 x 14). Keep the underside FLAT at the model\'s bottom: ' + 'clearance under it is what a MINI blob squeezes through, and the collider does not change.', 'course.tramp': 'Fill the pad volume; the launch impulse is unaffected by the model.', } /** * Slots the game builds ONCE. Everything else may be instanced many times over * (nine puddles, six cannons, every machine) and each instance gets its own * clone of the asset at its own pose — see docs/ASSET-SLOTS.md. */ export const SINGLE_INSTANCE_SLOTS: ReadonlySet = new Set([ 'blob.body', 'blob.face', 'ghost.body', 'course.finish', 'course.tunnel', 'course.scenery.cereal', 'course.scenery.block', ])