battery
The hero subject of *Fire For Effect*: four heavy howitzers dug into the snow saddle, with no crews. Each one is articulated — carriage, splayed trails and spades, splinter shield, cradle, recuperator rods that telescope out as the gun recoils, a breech ring whose interrupted-scr…
ffe.battery — the guns
The hero subject of Fire For Effect: four heavy howitzers dug into the snow saddle, with no crews. Each one is articulated — carriage, splayed trails and spades, splinter shield, cradle, recuperator rods that telescope out as the gun recoils, a breech ring whose interrupted-screw block unscrews and swings clear on its arm, and a loading tray that rises and rams. The guns work themselves off film time: one registration round at 30.5 s, the correction at 46 s, ripple fire from 50 s to 88 s, cease fire at 90 s.
Serves shots 3, 4, 5, 6, 7, 8, 9, 11, 13 and 15.
Everything it animates is a pure function of the film time tick is handed,
so the film runs twice and comes out the same. It reads no wall clock and no
engine time.
Exports
The slice contract:
M.build(root)— re-entrant. Spawns the rig underrootand QUEUES the geometry (see Notes).M.tick(t, dt)— one rendered frame of film, in film seconds.M.reset()— rewind the per-round latches.
What the other slices can read. All of them are pure functions of film time and none of them need the geometry to exist yet:
M.muzzleWorld(n, t) -> {x,y,z}, {dx,dy,dz}— gunn's muzzle point and bore axis, recoil included. Where a muzzle flash belongs.M.recoil(n, t) -> metres— how far gunnis into its recoil stroke.M.fireTimes(n) -> { seconds }— every round gunnsends.M.shellRide(t) -> pos, dir— the registration shell's position and unit velocity, ornilwhen it is not in the air. This is shot 6's camera path: stand 3 m behind and abovepos, look downdir.M.deckPunches() -> { { t, x, z } }— where and when each round holes the cloud deck at y = −140.M.impacts() -> { { t, x, y, z } }— where and when each round bursts on the valley floor.M.buildProgress() -> 0..1,M.finishBuild()— see Notes.
Named entities placed for other slices to hang work on:
| Entity | What it is |
|---|---|
battery_Muzzle1..4 | an empty at the muzzle, child of the recoiling slide |
battery_Breech1..4 | an empty at the breech face — gun-gas, ejection |
battery_RideShell | the registration shell itself, for shot 6 |
bat<n>_Gun | one gun's root; isolate on it to read the gun alone |
Usage
local Battery = require("ffe.battery")
local p, d = Battery.muzzleWorld(2, 30.5) -- where the registration flash goes
local pos, dir = Battery.shellRide(35.3) -- shot 6's camera, mid-deck
for _, punch in ipairs(Battery.deckPunches()) do
local t, x, z = punch[1], punch[2], punch[3] -- tear the deck here, then
end
Notes
- The geometry is built across frames. Spawning an entity that carries a
Modelcosts ~2.9 ms in this engine on every route measured (components =in the spawn,component.add, theaddComponentstool), and the battery is ~450 of them — twice the script watchdog's budget in one call. Sobuildspawns every pivot immediately (a bare entity is ~0.2 µs) and queues the meshes;tickdrains 48 per frame. It costs the film nothing: the clock advances one fixed 1/24 step per RENDERED frame, so a heavy frame is not a slow frame in the picture. The rig is complete after ~15 frames, and Gun Two is built first. A caller that wants to park on a frame and capture rather than run the film callsM.finishBuild()— but note that a spawn queued in one script has not reached the scene by the time a capture in that SAME script renders, so build in one call and capture in the next. - The materials live in
materials/inside this folder, not beside it: a sibling directoryffe/battery/makesrequire("ffe.battery")ambiguous and the LSP then reports the module unresolvable in every file that requires it. Nested here they take the folder's namespace, so their identity isffe.battery.materials.<name>and NOT the bare nameasset.listreports (#8695). - Soot rides
Film.ruin(t)onto the guns through theModeltint lanes, spread over frames. The bulk paths (entity.batchWrite,Entity.patch) are silent no-ops on theModelcomponent in this engine (#10515), so it walks component proxies instead. - Every entity this slice spawns is
temporary = true, so none of it lands in the scene as a pending edit.
Scoped to this part · feeds back into the world's score.