Doug's Game Dev Log
About
2026-08-10

An Enemy That Blinks and a Road That Forks

Since the last batch of small fixes the Trello board earned its keep: two days, a long stack of cards, and for once some of them are actual new toys instead of papercuts. There is an enemy that teleports out of your line of fire, a map that splits into two roads and lets each enemy pick, a pause menu with volume sliders, and a card screen that finally looks deliberate. Here is the whole pile, cool stuff first.

The hopper stopped playing fair

The hopper used to blink on a dumb timer: every N seconds it jumped forward whether you were shooting it or not. Fine, but it never felt like it was reacting to you. Now it reacts. The hopper teleports 2.5 units forward along the path the instant it takes any damage, then needs 3 seconds to recharge before it can do it again. It spawns with the ability ready, so the very first hit always makes it blink.

The satisfying part is where the teleport fires from. Every point of damage in the game already flows through one dealDamage pipeline, so the blink just hooks in there via a tryHopperBlink check: take a hit, jump. That means it works for every damage source automatically, a cannon splash, a poison tick, a chain arc, without any of those systems knowing the hopper exists. The recharge rides on the same auxTimer field the brood uses to lay eggs, so no new state. The result is an enemy that shrugs off your opening shot and lurches toward the exit, and the counter is real: burst it down before the 3-second window resets, or bring something that does not care where it stands.

It also flushed out a lovely little bug in the focus beam. The beam charges up the longer it stays locked on one target, and a hopper blinking mid-charge should obviously break the lock. It didn't, at first, the beam happily kept charging on the empty space. Now a teleport resets the charge, which is exactly the tradeoff the hopper is supposed to create.

A road that forks, and enemies that choose

There is now a stage where the path splits in two. Enemies come down the shared road, hit the fork, and commit to the upper or lower branch by a weighting. The interesting bug here was about honesty. Originally each enemy picked its branch at spawn, way back at the start of the road, and the little route indicator at the fork showed... something that didn't match, because the weights could drift between spawning and arriving.

Now the decision happens at the fork itself, using the current weights at the moment the enemy actually reaches the split. And the indicator is two balls sitting right on the fork, one per branch, colored to show where the flow is going. What you see at the junction is now literally the decision each enemy is about to make. Honest UI beats clever UI. (While I was in there, the splitter also learned to spawn its children on its own branch instead of always dumping them onto path 0, which is the kind of bug that only exists once a map has more than one path.)

Pause, and an actual options screen

You can now pause. Press ESC and the run freezes onto a proper pause screen with three buttons: Resume, Options, and Give Up. Give Up ends the run cleanly like a defeat, so you still collect the per-wave coins you earned but nothing you didn't.

Behind the Options button, on both the pause screen and the main menu, there is finally a real settings screen: two volume sliders (music and SFX, independent) and a fullscreen toggle. The volumes are 0-to-1 multipliers saved with the rest of your progress; SFX scale per sound, music rides a live gain node so a drag of the slider is audible immediately. Fullscreen just mirrors and toggles the engine's own state. None of it is glamorous and all of it is the stuff a game is supposed to have before you show it to anyone.

The cards grew up

The level-up and purchase cards got a proper pass. They now tint to their rarity (white, blue, purple, gold) and spell out exactly what a pick does: LV 4 -> 5 with the stat's before and after value underneath, so you are choosing a concrete number instead of a vibe. Buying an unlock shows a shared buy-confirmation card that lays out the price in gold, silver and bronze so there is no mystery about what you are spending.

There was also an embarrassing amount of fiddling with the feel of the cards. They have a subtle 3D tilt that follows the cursor, and I kept overshooting it: first Michroma for the font, then VT323, then finally Roboto Mono; the tilt got softened, then softened again (maxTilt 0.09 down to 0.05), the hover scale nudged from 1.02 to 1.07, the lean cut in half. This is the part of game feel nobody can spec in advance, you just push a number, look at it, and push it back. All the on-screen numbers now route through one shared fmtNum, so a value looks the same on a card, a stat footer, and a tooltip.

Under those numbers, the upgrade math got rebuilt too. Global upgrades used to stack per-level deltas, and the multiplicative ones (slow, pickup radius, splash, xp gain) compounded exponentially and fell apart at high levels. They now use the same clean level1-to-level100 linear interpolation the towers already use, so a level is a straight line between two authored endpoints. Low-level balance is untouched; only the runaway high end got fixed.

Towers that keep score

Every tower now remembers what it did. During a run each placed tower counts its shots, damage, and kills (a kill is attributed to whichever tower landed the finishing blow). When the run ends those get summed by tower type and merged into your save, so the UNLOCK screen shows a lifetime footer on each tower, total damage, kills, shots, times placed, and how many distinct runs you have used it in. The in-game inspect card shows the live version for the selected tower.

The toolbar got smarter alongside it. Each button now shows that tower type's current level, and clicking a button opens a per-type menu, level, range, speed, damage, and the aggregated stats across every instance you have on the board, with aim-mode buttons and a PLACE button. Set an aim mode there and it applies to every tower of that type at once; the button only lights up when they genuinely all agree. And a new tower now inherits the shared aim mode of its type on placement, so you are not re-setting it every single time.

The determinism sweep

A run has a seed, and a few things were quietly cheating it. The tower-pick screen used to roll three fresh options every time you opened it, so you could open, ESC out without spending, reopen, and keep fishing for the tower you wanted. Now the roll is deterministic: seeded by the run seed plus a per-tower salt that only advances when you actually consume a pick (spend a tower or a reroll), never on open. Reopen after an ESC and you get the exact same three. Reroll still works, because reroll is a real consumption. The authoritative server does the rolling in co-op, so the fix holds online too.

And the pile of small stuff

The rest, briefly:

  • Grass got alive. The static decor is gone, replaced by procedurally scattered grass tufts that jitter their position and depth-sort by row so nearer scenery draws over farther, and the whole scatter varies with the run seed. Stage visuals got a general tune-up in the same pass.
  • XP orbs stay on the field. Orbs that landed off the map now snap onto the border, and they are clamped to the buildable area rather than the raw grid, so you never have to chase an orb into a wall.
  • Placing a tower freezes the sim in single player now, so you can line up a shot without enemies sprinting past mid-decision.
  • Stage unlock prices are data-driven, moved out of a formula and into the stage JSON as bronze amounts, and the purchase now spends bronze.
  • The cursor hotspot finally sits on the tip of the pointer instead of somewhere vaguely near it, and the medal-select screen shows which stage you actually picked.

Two days, a couple of genuinely new mechanics, and a game that is starting to feel less like a prototype and more like a thing with menus, memory, and manners. The board still has cards on it. Back to drawing.