modules: scavengers — the roster and the difficulty as data, and the monolith dies - #8577
Draft
keithharvey wants to merge 1 commit into
Draft
modules: scavengers — the roster and the difficulty as data, and the monolith dies#8577keithharvey wants to merge 1 commit into
keithharvey wants to merge 1 commit into
Conversation
Contributor
Integration Test Results21 tests +1 8 ✅ ±0 16s ⏱️ ±0s For more details on these failures, see this check. Results for commit 77000bc. ± Comparison against base commit d1563bc. ♻️ This comment has been updated with latest results. |
keithharvey
force-pushed
the
scavengers
branch
2 times, most recently
from
August 2, 2026 14:15
00992c9 to
4484c4c
Compare
This was referenced Aug 7, 2026
keithharvey
force-pushed
the
scavengers
branch
2 times, most recently
from
August 8, 2026 00:55
742a083 to
d4b3a1e
Compare
Two monoliths go in this commit: the 2,971-line config and the 2,500-line
spawner that read it.
luarules/configs/scav_spawn_defs.lua could only exist inside a running
game. It read modoptions, the team list and UnitDefNames at include time,
keyed its behaviour tables by unit-def id, and wrote the difficulty ladder
with the host's multipliers already folded in. None of it could be read
from a lobby, tested, or built twice with different settings — and the
ladder endless mode walks was the same table the game was mutating as it
played.
Split into inert data and one program. modules/scavengers/data/ holds the
tiers, the six difficulty rungs as BASE numbers, the beacon list, the
land, sea and air rosters, the turrets, the behaviours (keyed by unit
NAME), the commander populations, the hand-written mixed squads and the
priority targets — plain tables, no calls, no engine. lib/defs_build.lua
applies the host's dials, expands the rosters into weighted pools,
resolves names to ids and assembles the config. Build(opts) takes the
world injected and FromEngine() is the single adapter that reads Spring.
The proof is a frozen copy of the old file under spec/fixtures. The golden
spec runs it and the builder under one stubbed engine and requires them to
agree on the difficulty ladder, the economy scale, every behaviour by id,
the processed and unprocessed turrets, the beacon windows, the targets,
the commander populations, every scalar, and the contents of all ten squad
pools — on two difficulties, on a stretched game, and under unit
restrictions. One thing is deliberately not preserved: pool ORDER. The old
file built its pools with pairs, so the order was whatever the hash gave
and could differ between two clients of the same game. The builder sorts.
luarules/gadgets/scav_spawner_defense.lua is then deleted. In its place
the module builds a WaveSpec and hands it to the wave director, and three
small gadgets keep what is genuinely scavenger-specific: unit conversion
and populations, the boss's resistance and stagger banks, and the scum
capture that makes ceding ground expensive.
Activation is unchanged, deliberately. A ScavengersAI on the field is
still what turns the mode on, so every existing lobby and SPADS config
keeps working. The Mode("Scavengers") preset serializes the eight scav_*
options that already exist — it invents no wire key — and carries one
thing no option can express: the instruction to field the bot. That needed
a chain verb (mode_builder gains `chainVerbs`) and one field in the
exported JSON. The merged option set is unchanged: 337 options before, 337
after, identical position-independently.
A game with no scavenger unit defs now says so ON THE SCREEN rather than
at ERROR level in the infolog. Those defs are derived only when something
asks for them, and a mission asks for nothing — so the failure looked
exactly like a placement bug, and cost a day of reading placement code
that was working correctly. The message also no longer offers ruins as an
alternative to forceallunits: it derives the defs and scatters neutral
derelict bases across the map, which a mission did not ask for.
The config file stays, as a shim over FromEngine, and that is a correction
to the plan rather than an oversight. LuaRules/Configs is where four
consumers look — the spawner, ai_ruins, the lootbox collector and the
stats panel widget — and three of them outlive the spawner.
Proved by a real game, not only by specs: tools/headless_testing/
startscript_scav_smoke.txt sets up a ScavengersAI match, and
luaui/Tests/scavengers/test_wave_spawns.lua watches beacons appear,
scavenger units reach the field and the roster clock start climbing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two monoliths go in this PR: the 2,971-line config and the ~2,500-line spawner that read it.
The config
luarules/configs/scav_spawn_defs.luacould only exist inside a running game. It read modoptions, the team list andUnitDefNamesat include time, keyed behaviour tables by unit-def id, and folded the host's multipliers into the difficulty ladder. None of it could be read from a lobby, tested, or built twice with different settings — and the ladder endless mode walks was the same table the game mutated as it played.It is now inert data plus one program.
data/holds tiers, the six difficulty rungs as base numbers, beacons, rosters, turrets, behaviours (keyed by unit name), commander populations, squads and priority targets — plain tables, no calls, no engine.lib/defs_build.luaapplies the host's dials, expands rosters into weighted pools, resolves names to ids and assembles the config.Build(opts)takes the world injected;FromEngine()is the only adapter that reads Spring.The proof is a frozen copy of the old file under
spec/fixtures. The golden spec runs it and the builder under one stubbed engine and requires them to agree on every scalar, table and pool — across two difficulties, a stretched game, and unit restrictions.Pool order is deliberately not preserved. The old file built its pools with
pairs, so order was whatever the hash gave and could differ between two clients of the same game. The builder sorts, contents are identical, and that is what the spec asserts.The spawner
scav_spawner_defense.luais deleted. The module builds aWaveSpecand hands it to the wave director, and three small gadgets keep what is genuinely scavenger-specific: unit conversion and populations, the boss's resistance and stagger banks, and the scum capture that makes ceding ground expensive.Activation is unchanged on purpose. A ScavengersAI on the field still turns the mode on, so every existing lobby and SPADS config keeps working. The
Mode("Scavengers")preset serializes the eightscav_*options that already exist, invents no wire key, and carries the one thing no option can express — the instruction to field the bot. The merged option set is unchanged: 337 before, 337 after, identical position-independently.luarules/configs/scav_spawn_defs.luastays as a twelve-line shim overFromEngine(), because LuaRules/Configs is where four consumers look and three of them outlive the spawner.startscript_scav_smoke.txtsets up a ScavengersAI match andtest_wave_spawns.luawatches beacons appear, scavenger units reach the field and the roster clock start climbing. It passes and self-skips everywhere else. Wiring the scenario into the integration runner is a BAR-Devtools change.Update (2026-08-07): the mode takes its seat on the game axis. The Scavengers preset now lives in category
game(selectorgame_mode, shipped by #8486) rather than minting a selector of its own — the section declaresmode_category = "game"andmode_key = "scavengers", so its dials render under the Game tab exactly while the preset is the mode. Still no new option key from this PR. TheBotchain verb moved to the shared PvE grammar in waves (#8576), where raptors (#8687) now uses it too, and the no-scavenger-defs refusal points at the Mission mode first,forceallunits=1only as the raw-startscript fallback.