PinballDirector
The rules of a game of pinball: three balls, ball save, a bonus that pays when the ball drains, and a jackpot.
What it does
Every pinball machine since the 1930s runs the same loop. You get a set number of balls. The score only goes up. A bonus builds while the ball is alive and pays out, times a multiplier, when it drains. This component is that loop.
It never touches the ball or the table. It listens to what the parts say, and asks other parts to act:
- A new game clears the score and stands the drop targets up, then asks the
Plungerto serve ball 1 (plunger.serve). plunger.launchedputs the ball in play, and ball save starts counting down.plunger.drainedinside ball save serves the same ball again: BALL SAVED.plunger.drainedafter ball save pays the bonus and serves the next ball. After the last ball it says GAME OVER and asks the plunger to park the ball (plunger.park).- Bumpers, slingshots, spinner turns, lanes and drop targets each add
bonusPerSwitchto the bonus. - A made ramp adds
bonusPerRamp, and everyrampsPerMultiplierramps the multiplier goes up one. So does a finished set of lanes. - A finished bank of drop targets lights the jackpot. The saucer (
MouseHole) collects it.
Here is one ball with the default rules. Twelve switches and a spinner that turns 5 times make 17 × 10 = 170. Two ramps add 1,000 and make it 2X. A lane set makes it 3X. The ball drains after ball save, and the bonus pays 1,170 × 3 = 3,510.
It says the whole game on pinball.game.changed every time anything moves, so a HUD only draws what it is told. Short news goes out on pinball.game.message: BALL SAVED, BONUS 3X, JACKPOT 25,000, GAME OVER.
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 PinballDirector
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.
Set the Plunger's autoLaunch to false, so it waits for the director to serve each ball. Set jackpot to 0 on a table with no saucer, so the jackpot never lights.
Props
balls(number, default3). Balls per game.ballSave(number, default8). Seconds after a launch when a drain gives the ball back. 0 is off.ownerEntity(string, default"Player"). TheScorebucket it reads and pays into.bonusPerSwitch(number, default10). Bonus for each switch hit.bonusPerRamp(number, default500). Bonus for each made ramp.rampsPerMultiplier(number, default2). Ramps it takes to raise the multiplier by one.maxMultiplier(number, default5). The highest the multiplier goes.jackpot(number, default25000). What the saucer pays while the jackpot is lit. 0 is no jackpot.startKey(string, default"Enter"). Key code that starts a game between games.autoStart(boolean, defaulttrue). Start the first game without waiting for the key.
Events
Emits:
pinball.game.changed. The whole game:phase(attract,ready,playingorover),ball,balls,score,bonus,multiplier,ramps,saveLeft,jackpotLit,best.pinball.game.message,{ text, kind }.kindisinfo,awardorwarn.plunger.serveandplunger.park. To the plunger.score.add. The bonus and the jackpot.score.resetRequestanddropTarget.resetRequest. At the start of each game.
Listens to:
pinball.game.startandkeyboard.keydown. Start a game.plunger.launchedandplunger.drained.score.changedandscore.reset. The score it shows, and the best.bumper.hit,slingshot.kicked,spinner.spun,rollover.changed,dropTarget.down. Bonus.pinballRamp.madeandrollover.complete. Multiplier.dropTarget.bankCompleteandmouseHole.captured. Jackpot.
Dependencies
None in code. It talks to the other parts only through events, so a table can leave any of them out. It needs a Plunger with autoLaunch off and a Score to be a game.
Notes
- A lit jackpot does not carry over to the next ball.
bestis the highest score since the page loaded. It is not saved.



































