Smite Lab

live

Train your Smite timing down to the millisecond.

typescriptcanvaswebsocketsnodecloud-runfirestoreterraformgameleague-of-legends
Blog postThe story: why it exists, the bots, and the inbox full of alerts.

Context

League of Legends is a five-versus-five game. Each team has one jungler, and only the jungler carries Smite, a spell that does a fixed amount of damage to monsters. The big neutral monsters (Dragons, Rift Herald, Voidgrubs, Atakhan, Baron Nashor) reward the whole team, and they belong to whoever lands the killing blow, so as one gets low both junglers wait with Smite for its health to drop into range.

Too early and Smite doesn't kill it. Too late and the other jungler, or any of the champions fighting around it, takes it. Nothing on screen says when you're in range, it happens in the middle of a fight, and a real game is the only place to practice it, with no feedback on how close you were.

What it does

A Practice fight at Baron.

A fight at Baron: champions around the pit, Baron's health bar, and the Smite key with its damage

You stand at an epic objective while allied and enemy champions take its health down at their real in-game damage, with projectiles in flight and an enemy jungler bot waiting to steal it. Press Smite the instant health drops into range. The result is your timing to a tenth of a millisecond, the health you had to spare or were short by, and the whole fight's health timeline.

Modes

Mode is the first choice, and each card says what it uses.

The Play screen with the Practice, Daily challenge and Solo mode cards
  • —Practice: any objective, any champions, your jungler's abilities on auto or on your own keys, endless attempts
  • —Daily challenge: today's scenario for everyone, five fights with per-player seeds, first run counts
  • —Solo: ten standard fights against a Diamond jungler bot, three placement runs, a rating and a leaderboard
  • —Duels: a room code and a link, two players racing to the same Smite
  • —Ranked: matchmaking, ready checks and an LP ladder

Timing model

Damage is a timeline of discrete events {t, sourceId, amount, kind}, so health is a step function, and the moment a Smite would kill is the exact timestamp of the first hit that brings health to or below its damage. Nothing is rounded to a frame.

A press is read from the input event's own timeStamp (the performance.now() clock), in a capture-phase listener with auto-repeat ignored, so a result is exact to the event rather than the next animation frame. Edge cases are decided once and unit-tested: a press on the opening instant is SECURED +0.0 ms, one millisecond earlier is EARLY by 1.0 ms, and a Smite landing on the same instant as the killing hit is late.

Latency calibration is a rhythm test: tapping along with a beat every 700 ms cancels reaction time and leaves display and input latency. The offset only ever applies in Practice.

One fight, every engine

Duels and Ranked need the browser and the server to produce the same fight from the same seed, bit for bit. ECMAScript lets engines approximate Math.log, Math.cos and friends, so the simulation uses none of them: the enemy jungler's reaction is Irwin-Hall (twelve uniforms minus six, additions only), and the one logarithm is detLog, built from correctly rounded arithmetic. structuredClone is replaced too.

A golden fingerprint covers every scenario across seeds, with and without simulated ping, and npm run check:engines runs it in V8 and in JavaScriptCore and fails if they differ.

Live fights

The server holds the seed and runs every live fight. The client gets the public setup, then each champion hit's launch 700 ms ahead (who, what, when, but not the damage) so projectiles can fly, and the damage when it lands. The seed and the enemy jungler's plan are revealed at the end so the client can rebuild the fight for its results.

A press is credited at max(claimed, arrival - min(RTT + buffer + 30 ms, 400 ms)), with RTT measured by the server from WebSocket ping frames every second, so it is judged on what was on the player's screen and never earlier than their latency explains. That is fair up to about a 330 ms round trip.

In a duel, when one Smite lands, every other seat is held for its own allowance on the timeline as it would have gone without that Smite, so a response to it can never be credited before it. The team's timeline is generated only as far as the next launches need, so the stream's length never reveals how long a fight with a hidden Smite lasts; a test pins that from a 250 ms horizon. Everything online was proven with scriptable bots, which found a crash on a one-word message and a path into two Ranked matches at once.

What it runs on

TypeScript and canvas in the browser, a Node WebSocket server on Cloud Run that scales all the way from zero to one. One instance holds roughly 370 to 600 live players at about 40 ms of CPU per duel round, which is a few hundred more than currently play it. Firestore holds players and leaderboards, sign-in is Google, and everything is Terraform.

Deploys go to a revision with no traffic, get smoke-tested at their own URL, then get promoted, and a scheduled check redeploys if a push ever fails to. A first visit plus its first fight is held under 700 KB by a check in CI.

A small budget reports spend and a hard cap scales the service to zero if a month ever runs away. Alerts go to J.A.R.V.I.S., which pulls them over an outbound connection.