SpaceGolfDirector
Runs a game of Space Golf: lays each level out of pools, keeps the tally, and moves on when a level is won or lost.
What it does
The director sits on the world entity and is the game. Every other Space Golf component does one job, and none of them knows a game is on.
Levels are pure data. To lay one out the director takes every piece from its pool and sets it up, then releases them all when the level ends, so nothing is created or destroyed in play and no level has code of its own.
Each start counts down 3, 2, 1 while the player sets up, then the chute opens. A rock is in play from launch until the plant takes it, a mass ends it, it leaves bounds, or it flies longer than maxFlightSeconds because it found an orbit. Meeting quota clears the level, with a bonus. Reaching maxMisses fails it, and a tie goes to the clear.
A failed level runs again with the player's black holes where they left them, because the retry is about adjusting the shot, not rebuilding it. A restart, a jump to another level, and each new level start from the level's own layout.
Clicking empty space adds a black hole there, up to the level's maxBlackHoles. Past the limit the director emits spacegolf.blackhole.refused, since a click that does nothing reads as broken.
Use it in a scene
A scene is just data — a list of entities, each with its components. At startup the SceneLoader turns this JSON into a live world; edit the file and reload to rebuild it.
Build it from scratch with the bjs CLI:
- 1Scaffold a project
World, renderer, and dev server — ready to run.
- 2Install dependencies
- 3Add SpaceGolfDirector
Copies its source into src/ so the scene resolves.
First time? Run bjs login once.
- 4Paste the scene into src/scenes/arcade-room.ts and run
SceneLoader builds the world from the JSON; reload to rebuild.
The real scene carries twenty levels, plus a pool blueprint for every kind of piece a level can place.
Props
levels(array, default[]) — the course, in order.startLevel(number, default0) — the first level, counted from 0.bounds([x, y, z], default[34, 16, 16]) — half-extents of the field round the origin.maxFlightSeconds(number, default16) — a rock still flying after this is lost.pointsPerRock,clearBonus,missBonus(numbers, defaults100,500,100): paid for each delivery, for clearing a level, and for each miss left unused.countdownSeconds(number, default3) — the count before the chute opens. 0 opens it on the next frame.outroSeconds(number, default2.8) — the pause after a level ends.owner(string, default"player") — the score bucket the points go to.cadenceScales(number[], default[0.75, 1, 1.5, 2, 3]). Notches for the time between rocks, as multiples of each level's own interval, sorted fastest first. The notch in use carries from level to level.cadence(number, default1) — the notch play starts on.wormholeColors([r, g, b][], default[[0.4, 0.8, 1], [1, 0.45, 0.85], [0.6, 1, 0.45]]). Given to a level's wormhole pairs in turn.pools(object) — pool names by piece, merged over the defaults. Each defaults to its key in lower case, soblackHoleis"blackhole".- Keys, as key codes:
restartKey(KeyR),addKey(KeyB, adds a hole at the centre),removeKey(Delete, removes the hovered hole; Backspace works too),prevLevelKeyandnextLevelKey(BracketLeft,BracketRight),slowerKeyandfasterKey(Minus,Equal).
Each level is an object:
name,hint(strings) — the title, and one optional line under it.quota(number) — rocks to deliver.maxMisses(number) — the number of misses that fails the level.maxBlackHoles(number) — black holes allowed out at once.previewSeconds(number, optional) — seconds of flight the aim line shows.chute—{ position, direction, speed?, interval?, spread? }. Anything left out keeps the blueprint's value.plant—{ position }.blackHoles—[{ position, mass? }], where the level's holes start.asteroids—[{ position, radius?, mass? }]. The radius also scales the rock's look.ships—[{ center, radius, speed, phase?, mass? }], each flying a flat circle at its centre's height.whiteHoles—[{ position, strength?, radius? }]. Strength is the push in black-hole mass; radius is the core a rock crashes into.wormholes—[{ a, b, radius?, color? }]: two mouths that are one place.solution—[{ position, mass? }], the holes SOLVE puts in place of the player's, at mostmaxBlackHoles. A level cleared that way pays its rocks but no level bonus.
Events
Emits:
spacegolf.level.started—{ index, total, name, quota, maxMisses, maxBlackHoles, hint, solvable }.spacegolf.countdown.changed—{ seconds }: 3, 2, 1, then 0 at GO.spacegolf.progress.changed—{ delivered, quota, missed, maxMisses, blackHoles, maxBlackHoles }.spacegolf.rock.delivered—{ entityId, points, x, y, z }.spacegolf.rock.lost—{ entityId, reason, x, y, z }, withreasonone ofcrashed,swallowed,escaped,expired.spacegolf.level.cleared—{ index, name, bonus, assisted }.spacegolf.level.failed—{ index, name }.spacegolf.game.completed— the last level was cleared.spacegolf.blackhole.refused—{ max }.spacegolf.cadence.changed—{ scale, interval, slowest, fastest }.spacegolf.level.solved—{ index, holes }.score.add—{ ownerEntity, points }for each rock and each level bonus.score.resetRequest— at the first start after the game is completed.massdriver.active.set— the chute on at GO, off when the level ends.spaceshooter.explosion— where a rock crashed; a swallow makes none.
Listens:
pool.ready: play waits for the pools.massdriver.launched,capturezone.captured,gravity.body.contact: the tally.pointerdrag.empty.clicked,pointerdrag.context,pointerdrag.hover.changed: add, remove, and the hovered hole for the remove key.keyboard.keydown: the keys above.spacegolf.blackhole.add,spacegolf.blackhole.remove,spacegolf.level.restart,spacegolf.level.goto,spacegolf.cadence.adjust,spacegolf.level.solve: the HUD's buttons. Goto takes{ index }or{ step }; cadence takes{ steps }, +1 slower.
Dependencies
MassDriver— the chute, set up for each level.TrajectoryPreview— the chute's aim line.GravityMass— the mass of each hole, asteroid and ship.MeshPrimitive— rock positions, and asteroid size.SpaceRock— an asteroid's look.OrbitMover— each ship's circle.WhiteHole— each white hole's push and core.Wormhole— each pair of mouths.
The scene also needs CaptureZone on the plant, GravityBody on the rocks and PointerDrag on the holes, which the director hears only through events.
Notes
- A delivery pays
pointsPerRock; the plant's own CaptureZonepointsis ignored. - Play waits for the
rock,blackhole,chuteandplantpools, and any other pool the first level uses. A missing or misnamed one fails quietly: the game never starts, or a later level's pieces are left out. - Restart is ignored during a cleared level's outro, so a won level cannot pay its bonus twice.
- SOLVE works during the countdown and in play. A failed retry keeps the solution's holes, and the lost bonus with them.





































