What it does
A wave plan is a list of waves, each a list of spawn entries: a type, how many, how far into the wave the first one
comes and how often the rest follow, with any data you want passed back, such as a lane or a formation. You step the
plan with your frame time, and it calls your onSpawn for each arrival as it falls due. A wave can stay open until
your clear() check says the field is empty, the plan can loop with a counter or stop at the end, and the loop count
is the number a per-loop speed-up reads. The plan never makes anything itself, so it is the same in 2D and 3D.
A difficulty curve declares each number once as a range, such as speed from 80 to 260 over twelve levels, linear, eased in or stepped to whole levels, and holds at the end. You set the curve's level as the game advances and read every value at once. It also records the level each kind of enemy first appears, so a wave can draw from the pool that is unlocked so far.
Goals are objectives with a label, a progress from 0 to 1, and done and failed flags: clear the field, reach a count,
survive a time, protect what is left, collect, reach a place, escape before the time runs out, land hits or run laps.
They combine with goals.all and goals.any, so "clear the waves while keeping a city standing" is one goal. A
catalogue is the collect-one-of-each objective, telling you whether each capture was new and what is still missing.
Features
- Wave plans —
createWaveswith spawn entries by type, count, start time and interval, and a callback for every spawn. - Waves that wait — a
clear()check holds a wave open until the field is empty; without one it ends with its last spawn. - Loops — plans loop with a counter for per-loop speed-ups, or end and report
finished;start,nextandresetmove between waves. - Difficulty curves —
createCurvewith named ranges over a number of levels, easedlinear,easeorstep. - Unlocks — the level each kind first appears, with
unlockedandavailablefor the pool a wave draws from. - Goals —
clear,count,collect,hits,laps,survive,protect,reachandescape, each with a label and progress for the HUD. - Combined goals —
goals.allandgoals.anystep their members and are done or failed as a group. - Catalogues —
createCataloguefor one of each, usable as a goal of its own.
Good for
- Shooters with attack waves, where each wave sends scouts, then escorts round a mothership, and the loop counter speeds the next pass up.
- Tower defence rounds, with spawns along lanes passed through each entry's data and a protect goal for the base.
- Survival modes, with a survive goal and a curve that raises speed and shortens gaps level by level.
- Escapes against the clock, where the goal fails if the player is not out in time.
- Collect-one-of-each levels, where a catalogue counts the kinds captured and says which are missing.
- Level files, since waves, ranges and goals are plain data a level can carry.
Make it yours
Everything is the data you pass. A plan is your waves and spawn entries, whether it loops, and the callbacks for each spawn, each new wave and each loop. A curve is your named ranges, how many levels they span, the easing and the unlock table. A goal is a builder and the callbacks that read your counters, with a label of your own, and combined goals nest.
Two different games: a looping shooter whose waves end when the field is clear, whose loop counter sets the curve's level so enemies fly faster on every pass, and whose goal is simply to clear; and a timed escape level with no waves at all, where a curve sets the hazard speed, a catalogue asks for one of each relic, and the win is all of collecting them and escaping before the clock runs out.
A quick look
A plan that waits for the field to clear, and a goal that is lost with the last city:
const plan = createWaves({
waves: [{ name: 'scouts', spawn: [{ type: 'scout', count: 6, every: 0.4 }], clear: () => enemies.count === 0 }],
onSpawn: (e, i) => spawnEnemy(e.type, i, e.data),
onLoop: n => { curve.level = n; },
});
const curve = createCurve({ speed: [80, 260], gap: [1.2, 0.35] }, { over: 12, ease: 'ease', unlock: { tank: 3 } });
const win = goals.all([goals.clear(() => enemies.count, 'WAVES'), goals.protect(() => cities.count, 1, 'CITIES')]);
plan.update(dt); win.update(dt);
if (win.failed) flow.go('over');
Works with
scoreboard— score, lives, extra lives and the high-score table.fsm— the game's own flow of attract, play, between waves and game over.flight-formation— for enemy formations that fly in and dive.encounter-director— for encounter pacing with warnings and safe lanes.
Good to know
- It schedules and counts: it never spawns, moves or draws anything, and what a spawn means is up to
onSpawn. - Nothing advances on its own; call
update(dt)on plans and goals once a frame. A long frame fires every spawn that fell due, in order. - A curve runs between two numbers per name; another shape is several ranges or your own function of the level.
- Goals read your counters through callbacks, so a wrong count in the game is a wrong count in the goal.






