WARBIRDS.IO Dev Log

← All posts

Storm fronts on the march

“Moving weather fronts” sat in the backlog from nearly day one. Storm rounds existed — the sky went gray, the fog closed in, the wind picked up — but the weather was a mood, not a thing in the world. Now it's a thing: a line of anvil-topped thunderheads that walks across the map over the round, flickering from inside, cracking lightning down at its leading edge, dragging curtains of rain. And it costs exactly zero bytes of network traffic.

The sky is computed, not streamed

The whole trick is the one the ambient whales taught us: anything can be perfectly synchronized across every client if it's a pure function of the world seed and the server clock — two things every browser already has. The front's position at time t is closed-form: bearing and track hashed from the crossing index, distance along the track proportional to t. The in-cloud flickers and the ground strikes are hashed time slots. When your wingman sees the bolt, you see the bolt, and nobody sent either of you anything.

That design pays for itself three times over. No snapshot growth, so the air game's golden wire test never notices. No per-theater work, so air, naval, and land (and whatever theater comes next) get the same storm by construction — the only gameplay input is the weather byte every theater's snapshot already carries. And a client that joins mid-round lands on exactly the same sky as everyone else, because there's no state to catch up on.

A wall of dark thunderheads over the island, rain curtains beneath, a lightning bolt striking the terrain, planes silhouetted against the cloud base
The front catching the island. Both planes in this frame see the same bolt on the same frame.

Infrequent enough to be an event

Storms shouldn't wallpaper the war. The weather roll used to be a flat one-in-three among clear, dawn, and storm; storms now get their own dice — about one round in four or five (a config knob), with the leftover odds split evenly among the calm moods. The deliberate subtlety: the new weighted pick consumes exactly one RNG draw, same as the old uniform pick, so the world's deterministic draw stream — and the golden test pinned to it — never shifted.

The embarrassing find along the way: the naval and land theaters never re-rolled weather at all. Their round resets skipped the air theater's pickWeather() call, so whatever sky the server booted with was the sky forever. A naval server that came up storming stormed for its entire life. Both hull theaters now roll fresh weather and wind every round, like the air war always did.

Building a thunderhead out of boxes

The first draft stacked four fat towers of big translucent blocks, and the first playtester said exactly the right thing: it read as “separate clusters of building blocks in the sky.” Real squall lines aren't lonely castles, so rework one threaded a continuous line of cloud through the whole front — a ragged black base wall and one connected anvil ceiling streaming forward over the march direction, built from about two thousand small, heavily-overlapped translucent blocks with depth-write off so they melt together. Rework two chased the blockiness further into wide paper-thin sheets — and overshot: the same playtester called it “more like smog than a storm front,” and he was right again. The sheets came back out. The lesson worth writing down: small-and-dense reads as weather; big slabs read as masonry; giant thin sheets read as haze. The middle is where the storm lives. Under it, instanced rain streaks recycle down the shafts; around it, a faint gray veil makes the rain readable from kilometres out; inside it, the fog collapses to knife range the deeper you fly.

Lightning is a hashed schedule per cell, and it comes in three kinds. Most five-second slots the belly glows and gutters somewhere along the line — and the glow isn't painted on: each cell carries a real point light in its belly, so a stroke brightens the cloud blocks around it from within. Every eight seconds or so a cloud-to-cloud arc crawls between neighbor cells, a stair-stepped horizontal stroke you mostly see as two cells lighting up at once. And every so often a ground bolt drops from cloud base to the actual terrain (surfaceAt, so it hits hilltops and water where they are), biased toward the leading edge, the side the storm is walking toward. The bolt itself is built the way everything in this world is built: axis-aligned boxes — hard vertical drops, right-angle jogs, one forked branch dying mid-air — pixel lightning over pixel terrain. A strike lights the whole scene for two-thirds of a second (the sky module floods fog and key light cold blue-white), and the thunder arrives late by distance at 340 m/s: a hard crack up close, a long grumble seven seconds after a flash two kilometres out.

A dark storm front streaming over the steppe, rain curtains to the grass, a bolt striking a grove near a town
The same front over the land war. The anvil streams the way the storm marches.

Photographing a 0.65-second flash at 1.5 fps

The honest confession this time is about the playtest harness. The headless browser we screenshot with renders software WebGL at about a frame and a half per second at full resolution — and a lightning strike lives for 0.65 seconds. Run after run, the schedule said a bolt fired and the screenshot showed empty rain. The strike machinery was provably fine (an instrumented run caught it at 94% brightness, exactly on its predicted second); the camera was just always late. The fix wasn't in the game at all: the screenshot director now computes the next strike from the same deterministic schedule the clients use, pins the storm's clock at the flash's brightest instant, and photographs the exact frame a 60 fps player would see. Determinism means never having to say “hold that pose” — the storm can be asked to.

One more artifact of the same property: the whole feature ships with a nextStrikeAt() method, because if the sky is a pure function, so is its photography schedule.