JUICE.md — the game-feel cookbook

The correctness spine proves a game is not broken. This is the other half: how to make it feel professional — and how to make that feel checkable, not a vibe.

The engine ships a full juice/art kit; the failure mode this doc exists to fix is that examples historically left it on the shelf (an audit found the art toolkit used in 0 of 26 games). Every technique below is cosmetic by construction — its own Rng, out of world.hash() — so you can lean into it with zero risk to the sim.

Reference instance: the precision platformer examples/small-flame leans on this kit and passes its feel gates. Copy from it; read here for the why.


Part 1 — The juice kit (all cosmetic)

wantuseone line
impact dust, bursts, sparklesParticles + PARTICLE_PRESETSparticles.burst(8, feet, PARTICLE_PRESETS.dust())
screen shakeShaker (as the camera’s parent)shaker.addTrauma(0.12) on land, 0.5 on death
weather / atmosphereAmbientField + AMBIENT_PRESETSnew AmbientField({ width, height, style: AMBIENT_PRESETS.snow() })
depthParallaxLayernew ParallaxLayer({ factor: 0.25 }) — 0 = far/pinned, 1 = foreground
pop numbers / pickupsFloatingText + FLOAT_PRESETSpops.pop('✦', at, FLOAT_PRESETS.heal())
terrain silhouetteautotileToCommands / contourToCommandsseamless inked mass from a boolean grid
procedural SFXaudio.tone/blip/play/successaudio.tone({ freq: 180, duration: 0.07, type: 'sine' })
screen-space HUDText parented to the Camera2Dchildren of the camera ride the screen

The cosmetic rule. Emitters carry their own Rng and never enter the hash. Wire them to sim events, not to state you read back — then a replay reproduces the exact same show, and deleting the whole view changes not one game bit. Beat/impact timing is sim time (world.time, the fixed dt), never wall-clock.

Part 2 — Feel is a curve, not a constant

Juice without restraint is noise. The envelopes that read as professional:

Part 3 — Make the feel checkable (Channel 4 gates)

Declare the feel, then prove it. See VERIFICATION.md Channel 4 for the full list; the shape:

// Declare a feedback contract next to the sim…
export const FEEDBACK: FeedbackContract = {
  land: { channels: ['audio', 'visual', 'haptic'], shake: 0.12 },
  death:{ channels: ['audio', 'visual', 'haptic'], shake: 0.5, hitstopFrames: 6 },
  // …
};

// …and gate it, plus the other three feel fundamentals, in verify.ts:
feedbackIssues(FEEDBACK, ['land','death', /*…*/]).length === 0;          // ≥2 senses, bounded juice
forgivenessIssues(CONFIG).length === 0;                                   // coyote/buffer/corner specced
salienceIssues(world.render(), avatarFill, background).length === 0;      // avatar out-contrasts scenery
cameraIssues(camSamples).length === 0;                                    // no snaps / jerk

A gate that fails is a design regression that used to ship silently. That is the whole point: “green tests, dead game” is the trap — Channel 4 turns more of feel green so a game can’t pass every proof and still play like a physics demo.

Part 4 — Wiring order gotchas (paid for in a platformer build)

This module rendersthe repo's markdowndirectly — edit it there, it changes here.