Wire the Workshop: registry boot, course.tramp slot, editor in the production build
- src/main.ts: await initAssets() before the world is built (createBlob captures blob.mesh by reference). Safe: preload races each url against a deadline with allSettled, so a stalled asset is a slow boot on built-ins, never a hang. - src/game.ts: course.tramp hook (hides the MATERIAL — the custom model is a child, so hiding the mesh would hide it too). Collider untouched. - vite.config.ts: editor.html as a build input. Without it the Workshop existed only under vite dev — invisible in the production deploy it is built for. - editor cannon.barrel: anchor the preview at the cannon group's origin (where the runtime attaches it) while still hiding the pivot's tube. Preview was 0.45m above where the game renders it — WYSIWYG off by a measurable amount. - manifest-io.test: drop the stale NOT_YET_IN_THE_RUNTIME guard now that the runtime adopted all four ids; the check is strict both ways, which is what stops the two halves drifting again. Verified: 10/10 test suites (360 assertions), sacred-parity fingerprint 49df4f20 unchanged from pre-workshop main, dist/editor.html + dist/assets/ manifest.json ship. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d318b4f031
commit
2823c93382
@ -40,30 +40,24 @@ function eq(a: unknown, b: unknown, msg: string): void {
|
||||
// such object (nothing calls createFan or createSeeSaw). The editor still
|
||||
// LISTS these, marked unavailable, rather than pretending the game has no
|
||||
// fan slot at all. `stage.slots.has(id)` is what the UI tests at run time.
|
||||
// NOT_YET_IN_THE_RUNTIME — the plan's table has these and the stage already
|
||||
// builds them, but src/assets/slots.ts has not adopted the ids yet, so the
|
||||
// runtime would discard the manifest entry. The editor must NOT offer them
|
||||
// until it does; the registrations cost nothing and light up on the day.
|
||||
// The runtime has since adopted cannon.base / course.tramp / course.tunnel /
|
||||
// course.finish, so the editor may offer every id it registers: the check below
|
||||
// is now strict in both directions. It is the guard that keeps the two halves
|
||||
// from drifting again — a slot the stage previews but the runtime would discard
|
||||
// is exactly the bug that silently ate a reskin before the vocabularies merged.
|
||||
{
|
||||
const stageSource = readFileSync(new URL('./stage.ts', import.meta.url), 'utf8')
|
||||
const registered = new Set(
|
||||
[...stageSource.matchAll(/register\('([^']+)'/g)].map((m) => m[1]!))
|
||||
const knownIds = new Set<string>(SLOT_IDS)
|
||||
const NOT_IN_THIS_COURSE = new Set(['machine.fan', 'machine.seesaw'])
|
||||
const NOT_YET_IN_THE_RUNTIME = new Set([
|
||||
'cannon.base', 'course.tramp', 'course.tunnel', 'course.finish',
|
||||
])
|
||||
for (const id of SLOT_IDS) {
|
||||
ok(registered.has(id) || NOT_IN_THIS_COURSE.has(id),
|
||||
`the stage builds something for the "${id}" slot, or it is a known course gap`)
|
||||
}
|
||||
for (const id of registered) {
|
||||
ok(knownIds.has(id) || NOT_YET_IN_THE_RUNTIME.has(id),
|
||||
`stage.ts registers "${id}", which the runtime knows about (or is queued for it)`)
|
||||
}
|
||||
for (const id of NOT_YET_IN_THE_RUNTIME) {
|
||||
ok(!knownIds.has(id),
|
||||
`"${id}" is still absent from the runtime — move it out of this list once added`)
|
||||
ok(knownIds.has(id),
|
||||
`stage.ts registers "${id}", which the runtime knows about`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -77,6 +77,18 @@ export function dressAsset(object: THREE.Object3D): void {
|
||||
export interface RegisterOptions {
|
||||
/** Per-object extra scale (puddle footprints). Index-aligned with `objects`. */
|
||||
extraScale?: [number, number, number][]
|
||||
/**
|
||||
* Override the resting pose a fit node is anchored at, when the object being
|
||||
* HIDDEN is not the object the runtime anchors to. Index-aligned with
|
||||
* `objects`; a null entry keeps the object's own pose.
|
||||
*
|
||||
* Only cannon.barrel needs this today: the runtime attaches the barrel model
|
||||
* to the whole cannon group (so a custom barrel does not inherit the aim
|
||||
* spin) while hiding the tube inside the aiming pivot. Anchoring the preview
|
||||
* on the pivot instead put every barrel 0.45m too high in the editor and
|
||||
* 0.45m lower in the game — WYSIWYG broke by a measurable amount.
|
||||
*/
|
||||
anchors?: (BaseTransform | null)[]
|
||||
}
|
||||
|
||||
export class SlotSwap {
|
||||
@ -98,7 +110,7 @@ export class SlotSwap {
|
||||
): void {
|
||||
const first = objects[0]
|
||||
if (!first) return
|
||||
const bases = objects.map(readBase)
|
||||
const bases = objects.map((o, i) => opts.anchors?.[i] ?? readBase(o))
|
||||
this.slots.set(id, {
|
||||
id,
|
||||
procedural: objects,
|
||||
|
||||
@ -27,6 +27,7 @@ import { identityTransform } from './manifest-io'
|
||||
import type { SlotEntry } from './manifest-io'
|
||||
import { SlotSwap } from './slot-swap'
|
||||
import type { SlotStageEntry } from './slot-swap'
|
||||
import type { BaseTransform } from './manifest-io'
|
||||
import { SLOTS } from './slots'
|
||||
|
||||
// Some ids below (`cannon.base`, `course.tramp`, `course.tunnel`,
|
||||
@ -67,7 +68,9 @@ export async function createEditorStage(container: HTMLElement): Promise<EditorS
|
||||
const slots = swap.slots
|
||||
const register = (
|
||||
id: string, objects: THREE.Object3D[], extraScale?: [number, number, number][],
|
||||
): void => swap.register(id, objects, world.scene, extraScale ? { extraScale } : {})
|
||||
anchors?: (BaseTransform | null)[],
|
||||
): void => swap.register(id, objects, world.scene,
|
||||
{ ...(extraScale ? { extraScale } : {}), ...(anchors ? { anchors } : {}) })
|
||||
|
||||
/** Everything a builder adds straight to the scene, recovered by diffing. */
|
||||
const capture = <T>(fn: () => T): { result: T; added: THREE.Object3D[] } => {
|
||||
@ -190,7 +193,11 @@ export async function createEditorStage(container: HTMLElement): Promise<EditorS
|
||||
.map((b) => b.children.find((c) => c.name === 'pivot'))
|
||||
.filter(Boolean) as THREE.Object3D[]
|
||||
if (bases.length) register('cannon.base', bases)
|
||||
if (pivots.length) register('cannon.barrel', pivots)
|
||||
// Hide the pivot (it holds the procedural tube the runtime hides), but anchor
|
||||
// at the cannon group's own origin — src/paint/cannon.ts attaches the barrel
|
||||
// model to the whole group, deliberately, so it does not inherit the aim spin.
|
||||
// Without the identity anchor the preview sat 0.45m above where the game puts it.
|
||||
if (pivots.length) register('cannon.barrel', pivots, undefined, pivots.map(identityTransform))
|
||||
|
||||
// ---- gap launcher --------------------------------------------------------
|
||||
// game.ts builds this inline and game.ts is frozen, so the stage mirrors its
|
||||
|
||||
@ -13,6 +13,7 @@ import { createFollowCamera } from './blob/camera'
|
||||
import { PaintSkin, PaintCannon, BuffSystem, PaintHUD, installPaint } from './paint/index'
|
||||
import { createBucketDump, createBubbleArch, createConveyorBelt } from './machine/index'
|
||||
import { installZones } from './course/zones'
|
||||
import { assets } from './assets/registry'
|
||||
import { installAudio } from './audio/index'
|
||||
import { installGhost } from './ghost/index'
|
||||
import { installTitle } from './ui/title'
|
||||
@ -96,6 +97,12 @@ export function installGame(world: World) {
|
||||
world.scene.add(tramp)
|
||||
world.physics.createCollider(
|
||||
world.rapier.ColliderDesc.cuboid(6, 0.25, 2).setTranslation(0, 0.25, 0))
|
||||
// course.tramp slot: hide the MATERIAL, not the mesh — the custom model is
|
||||
// parented as a child, so hiding the mesh would hide it too. The collider
|
||||
// above is untouched, so the bounce is identical whatever the pad looks like.
|
||||
assets().attachSlot('course.tramp', tramp, {
|
||||
onSwap: () => { (tramp.material as THREE.Material).visible = false },
|
||||
})
|
||||
// Fill BOTH under-shelf volumes solid: the front-face plinth stopped
|
||||
// overshooters, but lane-huggers rolled in from the open x±7 sides and
|
||||
// wedged (observed at (-4.1,-7.9)). Under-shelf space serves no gameplay —
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
import { createWorld } from './world'
|
||||
import { installGame } from './game'
|
||||
import { initAssets } from './assets/registry'
|
||||
|
||||
// Custom assets (the Workshop) must be resolved before anything is built —
|
||||
// createBlob captures blob.mesh by reference. Safe to await: preload races each
|
||||
// url against a deadline with allSettled, so a stalled asset costs a slow boot
|
||||
// on built-in props, never a hang.
|
||||
await initAssets()
|
||||
|
||||
const world = await createWorld(document.getElementById('app')!)
|
||||
installGame(world)
|
||||
|
||||
@ -8,6 +8,7 @@ export default defineConfig({
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, 'index.html'),
|
||||
editor: resolve(__dirname, 'editor.html'), // the Workshop — without this it exists only under `vite dev`
|
||||
'lane-a': resolve(__dirname, 'demos/lane-a.html'),
|
||||
'lane-b': resolve(__dirname, 'demos/lane-b.html'),
|
||||
'lane-c': resolve(__dirname, 'demos/lane-c.html'),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user