A campfire mechanic built around drift rather than a single timed press: doneness climbs fastest dead-center over the flame and falls off with distance, and the flame doesn’t sit still — gusts nudge it sideways every couple of seconds, so holding a good spot for one instant isn’t enough, you have to keep re-finding it. Cross the ignite threshold and the marshmallow is genuinely on fire: doneness now climbs fast no matter where you hold it, so pulling immediately still salvages something, but waiting costs it entirely.
Timed to August 25 — the National Park Service’s founding anniversary.
Reuse
The mechanic lives in src/marshmallow-roast.js as a framework-free ES
module:
createRoast(opts)— returns a state object; callupdate(dt)each frame,begin()to skewer up and start roasting,setPosition(x, y)continuously while roasting, andpull()to end the roast and lock in a score.- Coordinates are normalized and relative to the flame’s fixed pit position:
yis height above the flame tip (0= right in it, larger = farther and cooler),xis sideways offset.state.flame.xis in the same space — compare it againstmarshmallow.x/yto know how close you are. - State exposes
phase(ready|roasting|result),marshmallow(x,y,doneness0..1,ignited),flame(x,flicker— the flicker phase is cosmetic, drift is what matters),lastOutcome(raw|soft|golden|toasty|flaming|lost),lastPoints, andscore(attempts/golden/lost/streak/bestStreak/total). Rendering, input, and the flame/marshmallow visuals are entirely the caller’s job — the demo drives it with mouse-hold-and-drag (hold to skewer, move to chase the flame, release to pull).
Port notes for DragonRuby: update() and the heat math have no DOM
dependencies; flame.x, marshmallow.doneness, and marshmallow.ignited
are all the renderer needs to draw the scene, so update(dt) translates
directly into a tick method.
Gotchas
setPosition()andpull()are no-ops outsidephase === 'roasting'; the module returns toreadyon its ownRESULT_HOLDseconds after a result, no caller-side bookkeeping needed.- Once
marshmallow.ignitedis true, position stops mattering — doneness climbs at a flat rate until eitherpull()or it hits1and the module auto-resolves aslost. Chasing the flame after ignition is cosmetic only. - The demo in
demo/bundles its own copy of the module (self-contained by contract). If you touchsrc/, re-copy it intodemo/.