The counter read $0046.03. The sign read $7.87 a gallon. A red banner across the top of the phone said IT IS STILL PUMPING, the readout underneath said ON THE GROUND 0.1 gal and climbing, and the screen was empty. A 390 by 844 frame of a gas station, a loose nozzle lying somewhere in it, and no fuel anywhere.
That frame is the entire game. TOP OFF is a pump where the price per gallon is the hose pressure, and the second the nozzle tears out of a man's hands and starts hosing the forecourt is the second the thing exists for. It was happening. It was billing. There was nothing to look at.

The title event of the game, mid-breakaway at the ceiling price. The counter is spinning, the bill is real, and the only fuel on screen is a sputter about a hand's width wide.
It was not the renderer, which is exactly where I went first
Earlier that night I had fixed how the fuel looks. The drops were rendering as fat camera-facing discs and reading as falling leaves, so I stretched them along their velocity into streaks, and a pour turned into a rope. So when the breakaway came back empty, my hand went straight back to the same drawer: alpha, blending, culling, the camera.
None of it. The instrumentation from that run says the particle pool peaked at 1087 live drops and the ledger billed 8.19 gallons onto the lot across three breakaways. The fuel existed, it was allocated, and it was on the invoice. It simply never travelled far enough from the muzzle to occupy a pixel.
Which meant the bug was not in the fuel at all. It was in the thing holding the fuel.
Measure the nozzle, not the picture
I stopped taking screenshots and wrote a probe instead: a headless browser that opens the game, drags the nozzle into the filler neck the honest way, pins the price to a value I choose, keeps the man down so the state cannot end, and then records one row per frame for eight seconds.
const n = t.hose.nozzle
const tipY = n.p.y + n.dir.y * n.length
w.__rec.push([+s.t.toFixed(2), s.nozzle, +n.p.y.toFixed(3), +tipY.toFixed(3), +n.v.length().toFixed(2),
+n.dir.x.toFixed(2), +n.dir.y.toFixed(2), +n.dir.z.toFixed(2), +t.hose.thrust.toFixed(1), +n.p.x.toFixed(2), +n.p.z.toFixed(2), t.fluid.live])
tipY is the spout, not the grip: the nozzle's position plus its own length along its facing. That distinction is the whole measurement, because a nozzle lying on its side with the spout aimed into the slab and a nozzle standing on its grip with the spout up are the same height and opposite events.
Mean spout height across the entire wild state, at $7.87 a gallon, the most violent setting in the game: five centimetres. Percentage of wild frames with the spout above a metre: zero.
The fuel was not invisible. The nozzle was face down on the concrete and every drop was dying underground in the frame it was born.
The rope was eating the jet
The hose is a 22-node verlet rope solved at a fixed 120 Hz, with the nozzle riding the last node as a rigid body that integrates its own spin. The jet's reaction is applied to that last node, which is the obviously correct thing to do:
// Fuel leaves along +dir, so the nozzle is shoved back along -dir.
_d.copy(h.nozzle.dir).multiplyScalar(-1)
And dir came from the rope tangent. A nozzle in flight leads with its spout, so the spout points where the last segment points, which is tail.p minus the node before it. Perfectly reasonable, and it means the reaction vector was, every single frame, lying exactly along the last segment of the rope.
Here is that segment's constraint, nine iterations a step at full quality:
for (let i = 0; i < last; i++) {
const a = nodes[i]!
const b = nodes[i + 1]!
_a.subVectors(b.p, a.p)
const d = _a.length()
if (d < 1e-6) continue
const diff = (d - seg) / d
const wa = a.invMass
const wb = b.invMass
const wsum = wa + wb
if (wsum <= 0) continue
_a.multiplyScalar(diff)
if (wa > 0) a.p.addScaledVector(_a, wa / wsum)
if (wb > 0) b.p.addScaledVector(_a, -wb / wsum)
}
That loop has one job: delete any change in the distance between two neighbours. The correction it applies is parallel to b.p - a.p, because that is the gradient of the constraint it is enforcing. Which is another way of saying it is blind to anything perpendicular and merciless about anything parallel.
So the thrust and the solver were aimed at the same axis. I was pushing the nozzle along the one direction the next nine passes were built to undo. A displacement perpendicular to the segment would have sailed through untouched. Mine had no perpendicular component at all, by construction, because I had defined the push direction in terms of the rope.
A force that lives in the row space of the constraints is not a force. It is a proposal, and the solver votes it down nine times a frame.
And the nozzle weighed the same as a foot of rubber
That alone would have been survivable, because position solvers leak. A little of the push always propagates up the chain and the tail creeps. Except the second bug made the leak arbitrarily small.
Every node on the rope was born identical:
nodes.push({ p, q: p.clone(), invMass: i === 0 ? 0 : 1 })
Node zero is pinned at the pump, so there are 21 free nodes, all with inverse mass 1. In the constraint above that makes wa / wsum exactly 0.5: the tail gives up half of every correction to its neighbour, that neighbour gives up half to its neighbour, and over nine iterations the chain converges on the answer where everybody moves together. The effective mass at the tip is not the nozzle. It is the whole rope.
The numbers on that are brutal. Thrust at the ceiling price is 62 N and the nozzle is 1.35 kg:
const THRUST_MIN = 2.0
const THRUST_MAX = 62.0
const NOZZLE_M = 1.35
62 over 1.35 is 45.9 m/s² written onto the tail. Split across 21 equally weighted nodes of dead rubber it comes out the other side as about 2.2. Gravity is 9.81. The most violent object in the game was accelerating upward at roughly a fifth of a g, in the one direction the constraint was going to cancel anyway, and then falling over.
Two bugs, and this is the part worth keeping: either one on its own would have produced something. A cancelled direction with a heavy tip gives a nozzle that jitters and strains. A good direction shared across 21 nodes gives a nozzle that drifts slowly upward. Together they produce nothing happens, which is the worst symptom there is, because it has no shape and points at no subsystem. It looked like a missing feature. It read, to me, like an art problem.
The fix is two lines, and one of them is a lie
The mass one is honest. A wild nozzle under pressure is the heavy end of the system: the jet moves it and the rubber gets dragged along behind. So while it is loose, the tail stops being rope:
const WILD_TAIL_INVMASS = 0.13
...
tail.invMass = state === 'wild' ? WILD_TAIL_INVMASS : 1
Now wb / wsum is 0.13 over 1.13, about 11.5%. The tail keeps roughly seven eighths of whatever it was given and the rubber absorbs the correction instead. Seated or held, it goes back to 1, because a nozzle in a filler neck genuinely should be pushed around by its own hose.
The direction one is a lie, deliberately. I kept half the tidy -dir reaction and spent the rest on a lift and a lateral term that have no derivation behind them:
_d.multiplyScalar(0.5)
_d.y += WILD_LIFT * (0.45 + 0.55 * pressure) * surge * (1 - 0.72 * taut) * (1 - 0.9 * high) * (1 - 0.5 * tamed)
_d.y += WILD_FLOOR_LIFT * low
_d.x += nVal[5]! * 0.55 - _aim.x * 0.4
_d.z += nVal[4]! * 0.55 - _aim.z * 0.4
The defence is that the clean model was the wrong model in the first place. A tidy -dir reaction describes a rocket in a vacuum. A live nozzle on a forecourt is a ragged thing that slaps the concrete, folds back on its own stream, catches the deck and skips. There is no single clean thrust axis on that object, and pretending there was is what put the title event of the game on the floor. WILD_FLOOR_LIFT fires below 0.7 m and high bleeds the leap away above 1.5 m, so the hose cycles: leap, arc, fall, slap, leap. It never hangs at the ceiling and it never lies down.
Same probe, after:
- mean spout height at $7.87: 2.4 m, up from 0.05 m
- wild frames with the spout above a metre: 100%, up from 0%
And the thing I was not expecting. The design became visible in the object instead of only on the sign. Mean tip height at $3.09 is 0.40 m. At $5.94 it is 1.45 m. At $7.87 it is 2.38 m. The premise of the entire game is that the number is the force, and until this fix that sentence was true only in a comment.

Same code path, same state, same banner. The nozzle is off the deck and the jet is a rope across the lot.
The drops still needed somewhere to go
One more layer, and it is the one that generalises least but bit hardest. Even with the nozzle airborne most of the time, it still spends part of every breakaway skittering across the slab with the spout aimed down. A drop born a hand's width off the concrete, aimed at the concrete, has a flight time of a few milliseconds. It spawns, it solves its parabola, it hits, it dies, all inside one frame. Emitted, billed, never seen.
Real jets do not do that. At contact range a pressurised stream does not make a stream, it makes a sheet that blows out sideways along the ground. So the emitter now checks for it at spawn:
const vy0 = DIRV.y * sp
const deck = deckUnder(TIP.x, TIP.z)
const clear = TIP.y - deck
const tHit =
clear <= 0
? 0
: (vy0 + Math.sqrt(vy0 * vy0 + 2 * GRAV * clear)) / GRAV
if (tHit < DEFLECT_T) {
DEFLECT_T is 0.085 s, about a metre of flight at the ceiling muzzle speed. Under that threshold the drop is never born at the muzzle at all. It is born at the impingement point, already travelling outward along the deck, with a skirt that opens from ±35° for grazing fire to ±150° when the spout is pointed at its own feet, and a lift floor so the sheet stays up long enough to cross a metre of concrete instead of re-entering two frames later.
The muzzle speed floor moved too, for a reason worth writing down because it is a pixel argument rather than a physics one. A wild nozzle is met mostly between $4 and $6, and at the old floor of 3.4 that is 5.9 m/s. A jet launched a metre up at 5.9 m/s is on the ground 0.45 s and 2.5 m later. At this camera, 2.5 m of arc is about 140 px on a 390 px phone: a smear, not a golden rope across a station. The ceiling was never the problem. The floor was. It went to 7.4 and the ceiling kept its 1.8x over it, and because gallons are metered by flow rate rather than by particle, the bill did not move by a cent. Only how far the same fuel travels before it lands.
What I will check first next time
Three things I would have got to in ten minutes instead of two hours.
Decompose the force against the constraint gradient before tuning its magnitude. If a push is parallel to a distance constraint, a rope segment, a hinge axis, a contact normal, then its magnitude is irrelevant, because the solver is going to remove all of it. Cranking the number is the natural first move and it is precisely the move that cannot work. The tell in my code was that the push direction was defined from the rope, so its perpendicular component was zero by construction and no amount of newtons was ever going to produce a metre.
The effective mass of the point being pushed is not that point's mass. It is the mass of everything the solver has rigidly bound to it along that axis. A 22-node chain of unit masses is a 21-unit object, and the tip of it feels like one. Any time a body in a constraint system responds a factor of N too weakly, N is worth counting against the number of things it is attached to.
Force the state, log the object, do not look at the picture. Every one of these numbers came from a probe that drives the live page, holds the interesting state open, and prints the spout's height every frame. It took about twenty minutes to write. The screenshot said "there is no fuel", which is true and useless. tipY mean=0.05 said the nozzle is lying on the floor, which is the actual bug, and it said it in one line.
Nothing happens is the most expensive symptom in the book. It is also the only one that can be turned into a number before it is understood.