A day split between art and a repo-wide sweep for the kind of bug that does not announce itself. Four mini-bosses got real sprites. The sweep found a crash that had shipped, a hitbox that compounded, and a final stage with nothing in it.
Four mini-bosses were literally boxes
The report was blunt: a mini-boss looked like a box. It was not one level. Four of the eight mini-bosses were the same nine rectangle calls with the palette swapped: an outer box, an inner box, a horizontal bar, a vertical bar and two coloured dots. No silhouette at all.
All four now have authored sheets at 176x128, drawn at scale 0.5. The old art was 64x48 at scale 1.35, so the footprint on screen was already right. What was wrong was the resolution behind it. The game renders at 2x, so art authored at twice world size and drawn at half lands 1:1: same size on screen, four times the pixels.
Each one is a different kind of thing:
- A ray, with fins that bank on a travelling wave and a tail that hooks down and forward so the barb sits below the body.
- A folding siege machine whose clamshell chest opens across the fire frames to expose a reactor.
- A hovering cowl, the one soft vertical shape in a desert of hard horizontal shells. A void where a face should be, a torn hem, ribbons each on their own phase.
- A six-petal rosette that rotates. Nothing else in the game turns.
Drawn from the cast palette, not the terrain palette
This is not a detail. One stage's caverns are built to exclude chitin and green so the enemies can use them. Another stage's dunes are capped at a brightness ceiling so the cast can live above it. A third inverts the rule, holding terrain under luma 47.6 because its enemies are the brightest in the game. A mini-boss drawn in its own terrain's values sinks into the background it flies past, which is a bug one stage's mid layer had already shipped once.
The helper was erasing the detail
The shared sprite helper ran its build callback and then ran the shading pass. The shading pass repaints every opaque pixel from a colour ramp by its position in the run. It does not blend. It erases. So everything painted inside the callback was destroyed before the sheet was written: eyes, gill slits, hull bolts, louvre slats, copper strakes, petal veins.
The sprites came out as smooth, untextured hull, and no amount of extra detail could have fixed that, because the detail was being deleted. The order has to be shape, then shade, then detail, then edge.
This was live in the siege machine as shipped the day before. Its review note, "the copper trim barely shows at this size", was this bug and not a scale problem. Once fixed, its louvres, strakes, sensor and reactor core all render. That correction did more for the sprite than any of the geometry.
Rejected cuts, and what they taught
Every rejected version passed the automated art QA gate. The gate catches dead frames and flat fills. It does not catch bad drawing. Everything below was caught by looking at built contact sheets frame by frame.
- The ray's head read as a detached, eyeless pebble, twice, for two different reasons.
- Its barb was drawn from coordinates the tail loop had already walked past, so it floated free as the single brightest object on the sprite.
- Its pores were seven identical circles at an even 8px pitch. That is a decal, not a surface.
- The rosette's first cut was a side-on bud that never visibly opened (13px of travel across the fire frames).
- The rosette then converged with the ray. Both landed on a grey-violet armoured shape with a bright slot amidships. Two mini-bosses sharing a silhouette is worse than the box was, because the box at least never pretended to be a different enemy. It was rebuilt face-on.
- Then five of its six petals vanished. The rotate call returns a new frame instead of changing the old one, and the result was being thrown away.
All of this was reviewed from contact sheets, not in a running game. That is what caught every fault above, and it is still not the same thing as watching it play.
The sweep, and what it found
A ten-dimension audit went over the whole repo: draw order, scale and units, silent fallbacks, stage data, audio, docs against code, tooling, dead content, balance invariants, runtime logic. It returned 103 findings. These are the ones worth writing down.
Clearing Level 3 crashed Level 4
Campaign-blocking, and it had shipped. The street-traffic setup returns early unless the stage theme matches, without clearing its field. The engine reuses the scene across stages, so the old traffic group survived into Level 4 as a handle to a group the previous display list had already destroyed. The guard if (!group) return passes a destroyed group, because the reference is still truthy, and the next call throws. Every frame, for the rest of the run.
Walking the campaign the way the stage-complete scene does: stages 1, 2 and 3 clean, stage 4 throws on its first update. Fifteen sibling handles had the same shape and are now cleared too. After the fix, stages 1 through 8 all run clean.
Hitboxes grew every time an enemy respawned
The scaling code set the sprite scale and then wrote the hitbox size as body width times scale. Body width is already the world width, which the physics engine derives from source width times scale, so the scale was applied twice. Enemies come from a pool, so every reuse multiplied it again.
Measured on Level 3, tracking individual pooled objects:
| respawn | hitbox |
|---|---|
| first | 43x28 |
| second | 54x35 |
| third | 67x44 |
| fourth | 85x55 |
| fifth | 106x69 |
| sixth | 133x86 |
A cublet draws at 15x15. Late in the stage one was carrying a hitbox seven times its own width, so the player died to something they never touched. Different objects sat on different rungs depending on recycling order, which is why it would never have reproduced consistently enough to report.
The fix was deleting the second line. Setting the scale is enough, because the engine applies it to the hitbox for free. Ten consecutive pooled cublets now report an identical, stable body.
A parameter the language ignored
The shading function destructures { left, right }. All four new mini-bosses called it with { from, to }, at ten call sites. JavaScript does not care. The keys were dropped and every call ran at the default, so the two-band depth split, named as a headline device in three of those files, did nothing. Nothing failed. The art gate passed 71 of 71 and the sprites were merely flatter than their own comments claimed.
Turning it on then pushed the far bands below their own stage's terrain ceiling, which is exactly the cast-sinks-into-the-background failure each terrain module exists to prevent. Each was raised per stage. One stage caps terrain at luma 47 with the cast floored at 78, so a far band of 2..0 ramps 78 down to 16.
Two more from earlier the same day
- A subset art build gutted the sprite manifest. Building one sprite regenerated the manifest from only that sprite, leaving 70 unloaded and the game silently falling back to placeholders. Nothing threw and the check script stayed green. It surfaced as a Level 2 mini-boss spawning as the old box. The manifest is now generated from the merged sheet data, so a filtered build cannot drop a sprite it did not touch.
- Every authored mini-boss hitbox was half size. The physics engine multiplies the box by sprite scale, so numbers written in texture space at scale 0.5 became a quarter of the intended area. They are now stored in world pixels and divided by the draw scale.
The final stage had no waves
The last stage shipped with an empty wave array. It is built around bosses, so the fights themselves were never missing, but nothing filled the run-in, and the campaign's last level opened with 28 seconds of flying through an empty screen.
| before | after | |
|---|---|---|
| screen showing nothing | 36.9% | 2.0% |
| longest empty stretch | 27.5s | 1.0s |
| max enemies on screen | 1 | 21 |
After the fix the mean is 5.9 enemies on screen. The approach is twelve waves drawn from the two previous stages' rosters, since the finale has none of its own, trimmed from 103 to 90 enemies per minute so it is a clear step up from Level 5 and not a wall.
Levels 6 and 7 had the opposite problem. When Levels 1 to 5 moved to the roughly three-minute layout, 6 and 7 stayed on the old 90-second one, so the campaign got shorter and thinner exactly where it should peak. Both now run the extended layout. Five bridging waves per stage close a 24-second hole the first draft of the back half opened, the same dead-air failure Levels 4 and 5 had shipped once before. Longest gap on both is now 6.0 seconds, against 8 to 9 on Levels 1 to 5.
Tests that guard all of it
Nothing caught any of the above. The wave lists were well-formed, every wave sat inside its stage, and the totals looked healthy. Only the gaps and the layout were wrong. One test had the defect written down as an expectation: an assertion that the finale has zero waves passed happily while the last level of the game was empty.
- No stage leaves more than nine seconds between waves. Nine is the widest any shipped stage actually has.
- Every stage before the finale is on the long layout: 180s duration, boss at 165, second checkpoint at 120, last wave before the boss.
- No wave is scheduled at or past its stage's duration, since a wave that never spawns is silently lost content.
- The finale has an approach, and it ends before the boss.
The suite goes from 204 tests to 208. Two existing tests that asserted the old 90-second layout were themselves wrong and are fixed.
Difficulty
- The boss HP ladder was not monotonic. The Level 2 boss was weaker than the Level 1 boss, which is exactly what had been asked not to happen. The first boss is up 25% and the ladder now strictly increases across all eight.
- The first mini-boss fight was reported as a bullet-hell mess, and it was not the mini-boss. The escort waves around it were what killed runs. Verified without god mode, then thinned on every level, not just the one reported.
- Regular enemies are capped at one shot per second. The whole enemy table was swept for the same behaviour, rather than patching the one jellyfish that prompted it.
Three set-pieces
The feedback was that one stage had something memorable, a gunship you strip the guns off and watch fall away exploding, and the rest did not. Three set-pieces came out of that. In one the terrain itself closes on you. In another there are boulders that take 20 hits, blink as they take damage, then burst into seven fragments that kill enemies on contact. In a third there are five waterfalls of deliberately unequal widths that push the ship down as it passes through, with an underwater look inside and a splash on entry and exit.
