Skip to content

jengine-rampart

Wave defence where the searchlight is the targeting - one light set read by the shader and by the game.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-rampart
Version0.1.0
Entry pointjengine_rampart::RampartPackage
Script namespaceRampart
Listed byprojects/arcade-classics

List it in the top-level "packages" array of project.jengine:

{
"packages": [
{ "name": "jengine-rampart", "source": { "type": "path", "path": "../../packages/jengine-rampart" } }
]
}

Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the editor’s Rebuild viewport banner). Native and web builds link it on their own.

A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field for a package component and keeps it as inert custom data.

A script can check that the package is linked with if Rampart == nil then return end.

Scene keyRust type
dr_fieldRampartpackage only: needs a v2 scene

Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.

CallFlat name
Rampart.start(e, which)dr_start
Rampart.quit(e)dr_quit
Rampart.aim(e, x, y)dr_aim
Rampart.turn(e, dir)dr_turn
  • projects/arcade-classics/assets/scenes/rampart.scene

From packages/jengine-rampart/src/lib.rs.

Dusk Rampart - wave defence where the searchlight is the targeting.

  • [beams]: the light set. One list, read by the shader and by the game.
  • [wall]: the rampart, its segments, and the shadow it casts.
  • [run]: the match - attackers, turrets, waves, and the reveal rule.
  • [content]: the stage and foe tables, and the two refusals.

Three engine subsystems had shipped in an Engine Test Lab scene and in no game at all: renderer materials, the shader graph, and spot lights. A feature whose only exercise is a Test Lab scene is one nobody re-checks after the commit that added it - which is the standing VectorPath had when Coral Drive found it is drawn by one runtime of four.

The design claim is Tallow Deep’s, moved to a light it did not cover: the searchlight you can see is the searchlight the game reads. A searchlight in a defence game is a cone the player watches and a rule that decides who is revealed, and written twice those drift invisibly - the screen shows an attacker in the beam while the simulation calls it hidden, and what gets reported is that the game feels unfair. So there is one [beams::Beams]: beams::Beams::light_data hands it to renderer::lighting::light_at, and sync_lights hands the same values to the renderer as components. Neither is derived from the other.

That needed engine work first. light_at was point-lights-only until Sep 2026 - a deliberate hole, on the grounds that nothing used spot lights and a wrong answer is worse than a missing one. renderer::lighting::spot_light_at is the CPU twin now, pinned to spot_light.wgsl by cargo test -p jengine-core --test cpu_light_matches_the_shader.

The second claim is the light count. MAX_LIGHTS_PER_KIND is 256 and nothing in the repo had gone near it; Tallow Deep runs a lantern and a few candles. Every shot here makes a real point-light muzzle flash, so a firing line puts dozens on screen at once.

A LightOccluder is a closed outline, so the cheapest one is four segments. A rampart is a handful of long straight boxes, and wall::Wall::occluders merges abutting segments into runs, so an unbroken wall is one box. Until Sep 2026 the renderer kept sixty-four segments for every light, and that was why the genre fitted where a cave did not. The pool is 448 edges now, and when dozens of lights share one wall the pass hands them all one list of its distinct edges (pack_light_occluders), so the wall is paid for once, not per light.

dr_field, on an entity with a transform.

"dr_field": {
"tables": { "stages": "data/dr_stages", "foes": "data/dr_foes" },
"which": 0
}
CallDoes
Rampart.start(e, which)begin (or restart) a stage
Rampart.aim(e, x, y)aim the beam at a world point, for a mouse
Rampart.turn(e, dir)turn the beam, for keys: -1, 0 or 1
Rampart.quit(e)abandon the defence

Both aiming calls are limited by the stage’s turn rate inside run::Run, so a mouse cannot teleport the beam - that limit is the only cost the game has, and a script must not be able to spend around it.