1 / 2 / 3: pick GUN, CANNON or TESLA click a cell: build there — click a turret again to upgrade it (4 levels) SPACE or N: send the next wave L: target leading on/off — the hit rate on the HUD resets when you toggle R: restart Hovering shows the range ring and, when there is a target in it, two lines: a pale one to where the enemy is now and a bright yellow one to where the shell will meet it. The gap between them is the whole feature. The HUD reads the live hit rate and the mean lead distance in pixels. turret: shell why it exists GUN: 420 px/s cheap and fast; leading barely matters, which is the control case CANNON: 190 px/s heavy, splashes, slows — leading matters enormously TESLA: hitscan nothing to lead at all, for contrast Enemies: grunts (42 px/s), runners (104 px/s, from wave 2), tanks (24 px/s but five times the health, from wave 4).
Waves, turrets, economy and upgrades — with turrets that solve the intercept problem before firing, so a slow shell arrives where the enemy is going to be rather than where it was. THE INTERESTING BIT What separates a tower defense that feels right from one that feels broken is not the waves or the economy, it is whether the turrets can hit anything. Fire at where the enemy is and a 190 px/s shell arrives at an empty patch of path. Every miss looks like a collision bug and none of them are. So each turret solves for the interception. Enemy at E moving at V, turret at T, shell speed s — they meet at time t where | E + V t − T | = s t Squaring, with P = E − T: (V·V − s²) t² + 2 (P·V) t + (P·P) = 0 The smallest positive root is the flight time and E + V t is the aim point. Three cases matter and two are easy to get wrong: * No real root. The target is leaving faster than the shell can close. The turret holds fire. Firing anyway is what produces a turret that sprays and never connects. * V·V = s² exactly. The t² coefficient vanishes and the quadratic formula divides by zero; the correct answer is the linear solve t = −c/b, not a skipped shot. * Root selection. When the shell is faster than the target — the normal case — the coefficient a is negative and the two roots straddle zero. You want the smallest positive root, not the algebraically smaller one, which is negative and points the turret backwards. [Measured] Two cannons, identical placement, identical enemies, run through waves 1 and 2. Only the aim point differs: | | shots | hits | misses | hit rate | mean lead | max lead | leaked | |---|---|---|---|---|---|---|---| | leading on | 18 | 17 | 1 | 94% | 19.3 px | 56.5 px | 0 | | leading off | 19 | 14 | 5 | 74% | 0 | 0 | 1 | A shell that leaves the map or times out is counted as a miss, so the rate is honest about shells that hit nothing. Sample sizes here are small — two turrets get about nine shots a wave — which is why the rate is on the HUD: press L mid-game and watch it move over a few waves. One finding that was a bug in the demonstration rather than the game. The first version used a flat 11px contact radius for shells, and toggling leading changed nothing measurable — both modes came out at 88%, byte-for-byte identical end states. The enemies are drawn at 5 to 9px radius, so an 11px hit test was quietly forgiving every lead error smaller than the sprite itself. Collision tolerance has to be the size of the thing on screen or the feature it is testing is invisible. With the radius matched per enemy type the 20-point gap above appears. The other decision worth pointing at: **the buildable cells are derived from the path, not authored.** Every cell whose centre falls within 20px of any path segment is marked unbuildable by a point-to-segment distance test at startup — 39 of 260 cells. Hand-listing them would work exactly once and then disagree with the path the moment a waypoint moved. The HUD is drawn with the pen from a stroke font (tools/mkfont.py, 49 glyphs on a 3×5 box, shipped as three text files that goboscript reads into lists at compile time). Scratch cannot draw text except as a costume, and outlining a font per string only works for fixed titles — a gold counter that changes every frame needs strokes. All original - code, art and sound. See Inside is open.