Skip to content
Northforge Interactive
← Back to journal
PULSE//ZERO

Devlog #003 — The Adaptive Director

Static spawn timers make every run feel the same. PULSE//ZERO has an invisible game master instead — a deterministic, explainable pacing system that reads the run and decides who shows up, when, and how hard. Here is how it breathes.

Devlog #002 was about who the enemies are. This one is about who shows up, when, and how hard — the system we call the Adaptive Director. It’s the invisible game master behind every run, and it’s the difference between an arena that feels authored and one that feels like a spawn timer.

The problem with timers

The naïve way to pace a wave shooter is a script: at 0:30 spawn three Hunters, at 1:00 add a Charger, at 1:30 a Splitter. It works exactly once. The second run you’ve memorised it, and every run after that is identical regardless of whether you’re cruising or barely holding on. Static spawning can’t tell the difference between a player on a hot streak and a player one hit from death.

We wanted pacing that responds. But we did not want a black box that “feels dynamic” and can’t be explained or debugged. So the Director has one hard rule: every decision traces back to its inputs. It never reads the future and never cheats.

Intensity: one number that drives everything

Each tick, the Director collapses the state of the run into a single continuous intensity value between 0 and 1. Right now two things feed it:

  • Time — runs naturally ramp over their first three minutes.
  • Your combo multiplier — playing aggressively raises the heat. Chain kills and the game leans in.
var i_time := clampf(_t / config.intensity_time_ramp, 0.0, 1.0) * 0.7
var i_mult := clampf(float(mult - 1) / config.intensity_mult_cap, 0.0, 1.0) * 0.3
var target := i_time + i_mult
_intensity = lerpf(_intensity, target, ...)   # eased, never snaps

Intensity then maps to a threat tier — Low, Medium, High, Critical — which gates the spicier decisions (more on elites below). One readable number; every downstream system reads from it. That’s what keeps the whole thing explainable.

The breathing loop

A relentless arena is exhausting; a calm one is boring. So the Director cycles a small state machine that deliberately breathes:

Recovery → Escalation → Pressure → Peak → (mini-boss?) → Recovery

  • Recovery (~7s) — spawns nothing. Room to reposition, grab fragments, breathe.
  • Escalation (~12s) — a trickle of enemies begins.
  • Pressure (~16s) — the trickle thickens; this is the grind.
  • Peak (~8s) — a single dramatic formation drops all at once: a circle, a wall, a flank, a hunter-pack. This is the crescendo.

Each state changes two dials: how fast the spawn budget regenerates, and how much enemy “weight” is allowed on the field. Recovery throttles both to near zero; Peak opens them wide. And as intensity climbs, the whole cycle speeds up — late-run states run at ~55% of their early-run length, so the rhythm tightens the longer you survive.

A budget, not a body count

The Director never thinks in “number of enemies.” It thinks in cost. Every enemy from #002 carries a spawn_cost — a Swarm is cheap (0.6), a Splitter is expensive (3.0). The Director holds a regenerating spawn budget and can only spend what it has:

var def := _pick_def()
if def == null or _budget < def.spawn_cost:
    return                       # can't afford it — wait
_budget -= def.spawn_cost
_spawner.spawn(def, _arena_edge(), _roll_elites())

On top of that sits a separate population cap — a ceiling on total on-field cost that scales with intensity (≈5 early, ≈26 at full tilt). Two independent brakes: the budget controls flow (how fast new threats arrive), the cap controls density (how much can exist at once). Tuning them separately is what lets us make a moment feel fast but not crowded, or sparse but heavy.

Composition: who’s eligible

Not every enemy can appear from second one. The roster is gated by intensity, so the run has a natural difficulty arc built from the cast itself:

Enemy Unlocks at intensity Weight
Hunter 0.00 1.0
Swarm 0.15 0.9
Charger 0.30 0.7
Splitter 0.50 0.5
Orbiter 0.65 0.6

Early on you face honest Hunters. As heat rises, the pool widens and a weighted roll picks from whatever’s currently eligible. You don’t get told “difficulty increased” — you feel it, because the things attacking you genuinely changed.

Elites as a pressure valve

Once threat reaches High, the Director starts rolling for elite modifiers — the Fast / Shielded / Explosive / Regenerating remixes from #002 — at ~14%, rising to ~28% at Critical. At Critical it can even occasionally stack two modifiers on one enemy. Elites are the Director’s way of raising the stakes without breaking the population cap: same body count, more danger per body.

Mini-bosses: a teaser

After enough Peaks have passed and the run is at least two minutes deep, a Peak can resolve into a mini-boss state instead of looping back to Recovery — at which point normal spawning stops entirely and the arena focuses on one big fight. The gating (miniboss_first_time, miniboss_peak_gap) lives right here in the Director, but the encounter itself is a whole system of its own.

That’s Devlog #005. Mini-bosses — how they arrive, how they telegraph, and why beating one feels like a reward — get their own entry.

Why build it this way

The throughline across #002 and #003 is one idea: separate the data from the brain. Enemies are data (stats, behaviour, cost). The Director is the brain that spends a budget on that data according to rules you can read top to bottom. Because nothing is hard-scripted, we can retune the entire feel of a run by nudging a handful of config values — and because every decision is traceable, a debug overlay can show you, live, exactly why the Director just did what it did.

Authored-feeling pacing, no authored script. We’ll report back with the mini-bosses. As always: we build in the open.