Skip to content

modules: waves — the PvE wave director as a library - #8576

Draft
keithharvey wants to merge 1 commit into
placementfrom
waves
Draft

modules: waves — the PvE wave director as a library#8576
keithharvey wants to merge 1 commit into
placementfrom
waves

Conversation

@keithharvey

@keithharvey keithharvey commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

The base game's PvE wave machinery is two ~2,500-line monoliths about 70% line-identical to each other, enabled only when a LuaAI team is present, with no way for anything else to ask for a wave. This is the module they should have been: a generic wave director configured with plain data.

The seam is one table, the WaveSpec. The gadget hosts 1..n directors keyed by name, so a multiplayer mode and a mission's scripted pressure are the same machinery at different intensities.

lib/ is rewritten, pure and spec'd; spring/ is ported leaf code kept close to the original so a playtester recognises its behaviour. The spec is immutable after Start, with everything the monolith mutated moved into a params copy in the director's state — the pile a savegame writes. The director returns orders rather than acting, so the whole lifecycle runs under busted against a fake map and a seeded RNG.

One deliberate behaviour change: to compose a wave the monolith re-rolled a random candidate up to a thousand times until one landed in the anger bracket. This filters first and draws once, distribution-equivalent because buckets carry an entry once per weight point.

Burrow placement goes through #8632, so a spawner lands in the same spot on every client, and a mission's named origin keeps its direction instead of widening until it covers the map.

Nothing is live yet — no spec registered, no callin hooked, scav_spawner_defense.lua still owns the scavengers game. #8577 switches it over.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Integration Test Results

20 tests   8 ✅  16s ⏱️
 1 suites  8 💤
 1 files    4 ❌

For more details on these failures, see this check.

Results for commit d1563bc.

♻️ This comment has been updated with latest results.

@keithharvey
keithharvey changed the base branch from cm8-ashfall-base to cm8-ashfall August 1, 2026 18:24
@Damgam

Damgam commented Aug 1, 2026

Copy link
Copy Markdown
Member

He's touching my babies now aaaaaaaaa

The base game's PvE wave machinery is two 2,500-line monoliths that are
~70% line-identical to each other, enabled only by a LuaAI team being
present, with no way for anything else to ask for a wave. This adds the
module they should have been: a generic wave director that a flavor pack
configures with plain data.

The seam is one table — the WaveSpec — plus optional named hooks. The
gadget hosts 1..n directors keyed by name, so the multiplayer mode and a
mission's scripted pressure are the same machinery at different
intensities.

The split is the hybrid rule, and it maps onto directories. lib/ is
rewritten, pure and spec'd: the anger clocks, the archetype wheel, squad
composition, dynamic difficulty and the endless reloop, boss lifecycle,
the cadence brain, and the director that ties them together. spring/ is
ported leaf code — placement cascades, the spawn drain, squad AI, creep
structures — kept close to the original so a playtester recognises its
behaviour, with every file-level global it used to read threaded through
explicit state instead.

Two disciplines carry through. The spec is immutable after Start:
everything the monolith mutated on its config lives in a mutable params
copy inside the director's state, which is the pile a savegame writes. And
the gadget is a dumb executor — the director returns orders, including
already-composed waves, so the whole lifecycle runs under busted against a
fake map and a seeded RNG.

Composition changes one thing on purpose: the monolith drew a random
candidate and re-rolled up to a thousand times until one landed in the
anger bracket. That is rejection sampling with a silent failure mode. This
filters first and draws once, which is distribution-equivalent because
buckets carry an entry once per weight point.

Placing a burrow is the part that decides whether a director runs at all,
and it took three fixes to get right:

A director's origin is a preference, not a cage. Placement widens its box
when the named corner has no ground to stand on — the original refused,
which is defensible on a symmetric multiplayer map and useless to a
mission that names a corner by map fraction and finds lava there.

A last resort that still refuses is not a last resort. The fallback
dropped the sight test but went on demanding a clear flat circle of 1.5
burrows, which is spacing, not fit — so on a small or crowded map every
attempt failed including the one whose job was to succeed. It now asks for
the footprint, and sweeps DETERMINISTICALLY outward from the middle of the
box through the placement module before falling back to the random probe.
The probe can miss a spot that exists; the sweep cannot, and it picks the
same spot on every client.

And a wet sample is a sample. The surface test ran once, after the probe
had already chosen, and refusing it threw the spot away and ended the
cadence — so on a map like Shallow Straits, where most shoreline is
neither land nor sea, nearly every cadence placed nothing. It is per
sample now, and the warning counts what refused rather than blaming
crowding by default.

A callin is never unhooked from inside a callin. The handler defers its
list updates while iterating but nils the method field immediately, so a
director that stopped during GameFrame took the whole gadget with it on
the next frame. The hooks install once and idle on an empty host list.

Nothing is live yet. No spec is registered, so no callin is hooked and
scav_spawner_defense.lua still owns the scavengers game.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants