· 7 min read · Game Dev · WebGL · Rust · Product
Impact Frame: A Browser Brawler Where the Hit Is the Whole Game
Impact Frame is a free stickman brawler that runs in your browser. Every hit is told as loudly as it landed, on a fixed 120 Hz simulation the server can replay. How the hit, the print look, the sound, the enemies and the leaderboard were built.
Most brawlers tell every hit the same way. A jab and a finisher get the same sound, the same shake, the same little spark, and after ten minutes you stop feeling any of them. I wanted a game that tells each hit as loudly as it actually landed.
Impact Frame is that game. It’s a stickman brawler that runs in your browser, on a phone as well, with nothing to download. You play Teo, who is in love with the daughter of the city’s oldest crime family. Her father keeps sending men to deal with him, and Teo can fight. It’s free, and you can play it now.
What you get
- The Gauntlet: endless waves in four acts, three lives, and a world leaderboard. Before every wave Nina texts you who is coming and how to beat them.
- Sparring: pick the number of waves, nothing is scored, and it’s where you learn the seven named strings and hunt for the four secret ones.
- Fourteen kinds of enemy, and every one winds up before it strikes. The wind-up is your warning.
- Keyboard, gamepad or touch, with every key rebindable and the pad rumbling on the big hits.
- Gear that only changes how a hit looks and sounds. A new ink re-prints the whole game, but nothing you can buy makes you stronger.
One number decides the hit
The rule the whole game hangs on is small. Every hit works out one number, the speed it launches the body. That speed, not the button you pressed, picks the tier:
- tak: a jab or a glancing kick. The fight hardly pauses.
- THUD: a clean hit. The fight freezes for a beat and the camera leans in.
- KRAK!: a longer freeze, a hard shake and a ring of force.
- BOOM: everything stops, the frame flips to negative, and the body goes through whatever is behind it.
The freeze runs from 50 ms for a light hit to about 170 ms for a lethal one, and a full charge that finishes a grunt holds for over 300. That pause is most of what makes a hit feel heavy. It’s the same trick manga uses with an impact frame: one inverted, hand-inked frame at the moment of contact, which is where the name comes from.
Because the tier comes from the outcome, the jab that finishes a battered enemy can be a BOOM, and a full-charge kick on a fresh one might only reach KRAK. A BOOM stays rare, so it stays loud.
Printed, not rendered
The look is a two-colour riso zine, and it isn’t a filter bolted on at the end. The game draws its ink into three channels of an offscreen buffer: one for black ink, one for tone, one for spot red. A single WebGL2 pass then prints them: halftone dots, screentone, overprint, plates drifting out of register, shockwave warp, paper grain, and the inverted frame on a lethal hit. Red bleeding in from the edges of the screen marks danger and weight: getting hit, a heavy impact, a full charge, low health.
One rule keeps the spectacle honest: the effects layer has its own random numbers and only decides how loudly a result is told. Turn all of it off and the fight plays out exactly the same.
Owning the clock
The simulation runs at a fixed 120 Hz, with a seeded random generator and its own trigonometry in place of the browser’s, so every browser and every machine gets the same answer from the same inputs. Hit-stop and slow motion are counted in ticks, not milliseconds. That one decision pays for a lot: replays, tests, and a server that doesn’t have to believe anything a player’s browser says.
The tests lean on it too. They play the real game headlessly, hash every draw call, HUD write, sound call and the simulation state every 300 frames, and fail when any hash moves. Changing what the game does is an explicit act, and the diff shows exactly what changed.
The client asks, the server decides
A leaderboard in a browser game is easy to cheat unless the server does the checking. So the game records a run’s inputs and sends them in, and the server doesn’t take the score on trust. A worker replays the inputs on the same simulation and compares what it gets with what was claimed. Only a run that matches counts, and only then does it earn anything or reach the board.
The backend is a Rust API (axum, Postgres, Redis) with a small Node worker that re-runs the game. It’s the part of the project that shows least, and the part that lets “world leaderboard” mean something.
Enemies that read you, and never cheat
Spam one move and someone starts waiting for it. A “?” rises over one enemy’s head, it holds for a beat, then it backdashes, hops, braces or jabs. Every answer leaves a gap you can hit. From wave 4 the mix of men also leans against how you fight, and the knock-out screen tells you what they saw.
The enemies only get smarter. Their health, speed and damage never change, so the only way through is to fight better. My first idea was to have an AI service read a player’s style from their numbers. I measured it against a few plain rolling rates (how much of your fighting is in the air, how often you dash, how often you charge, how much you mash one button), and the plain rates won on runs they hadn’t seen. They also cost no latency and give the same answer in the browser and on the server, which the replay check needs.
A sound for every hit, with no recordings
Every sound in the game is made live in Web Audio, music included. A hit is a chord of layers (whoosh, transient, body, sub, crack and tail) that changes with the tier. The whole mix is voiced for where you’re listening, because a phone speaker and a pair of headphones need different mixes. Songs are written as data and played by a step sequencer, and a conductor moves each fight’s piece between sections as the fight changes: more enemies on the floor, a higher style rank, the last man standing, a cleared wave.
The trailer is the game
I made the 30-second launch trailer the same way I made the xposed film: from code. A script stages real fights on a 150 bpm beat sheet in headless Chromium, prints every frame through the game’s own shader with the titles set in the same plates, and plays a soundtrack on the game’s own audio engine, offline. One command renders both cuts, 16:9 and 9:16, at 60 fps, in about twelve minutes. Nothing in it is screen-recorded.
Limits
- Only the rooftop arena exists so far. The docks, the club and the estate are told in Nina’s messages and the story pages, and the arenas are still to come.
- It’s a brawler where the men who come for Teo don’t get back up. Gore can be turned off, and the story is the same either way.
- Lethal hits flash the screen for a split second. Reduce flashing in Settings tones that down, and screen shake can go to half or off.
- The repository is private, so there’s no GitHub link here. The game is the thing to look at.
Twelve days
The first commit is from 7 October. By the 18th there were over 200 commits, more than 100 merged pull requests and 55 written-down design decisions. The method that kept that from turning into a mess was dull: every change starts as an issue, every decision is written down before it’s built, one branch makes one pull request, and a piece of work isn’t done until I’ve run the real thing, by playing it, driving the API, or running the end-to-end suite on throwaway containers. I don’t write unit tests for new work; I test what I built by using it.
The game itself has no runtime dependencies at all: about 21,000 lines of plain JavaScript modules. A frame full of effects costs about 5 ms at 1080p, and the resolution steps down on its own if frames keep missing the display’s refresh, which is what keeps it playable on a phone.
Try it
Open impactframe.app/play. A and D move,
W jumps, J punches, K kicks and L dashes. Hold a punch or a
kick to charge it: the whole fight slows to half speed while you hold, and the air around the enemy starts to boil.
On a phone, turn it sideways and the controls come up under your thumbs.