# ShieldFx
ShieldFx is the energy-shield dome hero effect. Tracer shots fly in from far out, low over the
horizon, singly and in volleys, and strike a dome of a given radius. Each impact throws a flare,
sparks and burning fragments that fall under gravity and bounce off the dome and the ground, flashes
a pooled impact light, and records the hit in a table the dome and ground shaders read.
Everything drawn as particles is an instance in one of four GPU-instanced draws (one draw call per
pool), whose transform and data buffers the module writes each frame. The hit table goes to two
materials as one batched property set each per step.
`ShieldFxDriver` uses it: that component starts it centred on its entity, steps it every frame and
stops it on destroy. The module resolves these materials by name, so the world holds them:
- `shield_dome_mat` and `ground_arcs_mat`: receive `center` (the dome centre's three coordinates
followed by the radius) at start, and `hit0` to `hit11` plus `strain` every step.
- `fx_streak_mat` (tracers), `fx_flare_mat` (flares), `fx_spark_mat` (sparks) and `fx_frag_mat`
(fragments): the materials of the four instanced draws.
## Exports
- `M.start(opts)`: builds a run. If one is already running it is stopped first. `opts.radius` sets
the dome radius (default 9) and `opts.center` its centre as a three-number array (default
`{ 0, 0, 0 }`).
It resets the simulation, sets `center` on the dome and ground materials, and creates the four
pools `streak` (16 instances), `flare` (12), `spark` (260) and `frag` (90). Each pool has a mesh,
two GPU substrate buffers (for the streak pool, `shieldfx.streak.xf` holding mat4 transforms and
`shieldfx.streak.dat` holding vec4 data; the other pools follow the same naming), and one instanced
draw with shadows off. Returns a token table that identifies this run.
- `M.attachLights(list)`: takes a list of `{ ent = entity, comp = lightComponent }` entries. Each
impact moves the light whose last impact is oldest to the impact point, and its intensity then
decays from `160 * strength`.
- `M.hit(dx, dy, dz, s)`: forces an impact on the dome in the direction `(dx, dy, dz)` (normalised by
the call) with strength `s` (default 1).
- `M.step(dt)`: advances a running effect by `dt`, clamped to at most 1/20. It schedules incoming
fire, moves the tracers (an impact fires when a tracer reaches the dome), sparks, fragments and
flares, ages the recorded hits (a hit clears after an age of 3.5), decays `strain` (held at most
3), writes the hit table to the materials, writes the instance buffers and drives the attached
lights.
- `M.stop(token)`: ends the running effect: drops the four instanced draws and destroys their meshes
and buffers. With a token, it acts only when the token is the current run's, so a scene unloading
after its successor already started leaves the new run alone.
- `M.stats()`: returns `{ sparks, frags, streaks, flares, strain, t }`, the live counts of each
particle kind, the current strain and the run's elapsed time.
- `M.domeGeometry(R)`: returns the dome surface geometry (`{ positions, normals, uvs, indices }`), a
hemisphere of radius `R` with 64 latitude and 160 longitude segments.
- `M.groundGeometry(R)`: returns the ground ring geometry, a flat annulus from `R - 1.6` to `R + 10.0`
at height 0.04, with 5 radial and 160 around segments.
- `M._state`: the table that holds the running effect's state.
`domeGeometry` and `groundGeometry` are the geometry of the two scene-authored surfaces; the source
states they are called once by the authoring script, and the resulting `.mesh` assets are what the
scene's Model entities reference.
### The hit table
`hit0` to `hit11` form a ring of the 12 most recent hits. Each is a vec4: the hit direction on the
dome scaled by the hit strength, and the hit's age in its fourth component (`-1` for an empty slot).
`strain` rises with every hit, and rises more for hits that land near recent hits.
## Usage
```luau
local ShieldFx = require("ShieldFx")
local token = ShieldFx.start({ center = { 0, 0, 0 }, radius = 9 })
local light = entity.findAll("ImpactLight1")[1]
ShieldFx.attachLights({ { ent = light, comp = light.component.get("Light") } })
-- every frame
ShieldFx.step(dt)
-- force an impact straight overhead at full strength
ShieldFx.hit(0, 1, 0, 1)
print(ShieldFx.stats().sparks)
-- when done
ShieldFx.stop(token)
```