An effect bank is a named visual effect — an explosion, an engine trail, a shield
impact, a burning hull. Each is built from one or more renderer layers, and
can fire a sound bank alongside them.
Banks live in World/effect-banks.xml and are edited in Hydra's
Effect editor. They are id‑keyed and module‑layered like every other
resource.
Layers make an effect
A hull detonation is not one thing. It is a fireball, a flash, debris and a light, fired
together off one clock:
<effect-bank id="Explosion" category="death" instances="8">
<description>Standard hull detonation: what a DESTROYED object throws</description>
<alias>Object</alias>
<sound bank="Explosion" />
<layer renderer="fireball" />
<layer renderer="flash" />
<layer renderer="debris" />
<layer renderer="light" curve="blast" intensity="6" />
</effect-bank>
Every layer of one effect derives its time from a single clock, so the layers of one
explosion cannot drift apart from each other.
Compare that with a glancing hit, which is deliberately one layer and no fireball:
<effect-bank id="Impact Sparks" category="weapon" instances="12">
<layer renderer="sparks" space="world" rate="240" duration="120"
life="500" speed="26" spread="1" drag="6" color="#ffd08a" />
</effect-bank>
That contrast is most of effect authoring: what an effect leaves out is what makes it read
as a different event.
The renderers
Each layer names a renderer. The base game provides fourteen:
| Renderer | Draws |
fireball | An expanding burning ball. The core of a detonation. |
flash | A brief bright pop. |
debris | Thrown fragments. |
ring | An expanding shockwave ring. |
shockwave | The front of a rolling blast: a sphere that grows at the blast's speed until it reaches its full reach. World space only. |
light | A real light that illuminates nearby geometry. |
sparks | Short‑lived hot particles. |
smoke | Rising, drifting volume. bloom below 1 makes each puff swell fast, as exhaust does leaving a nozzle; texture and blend (normal or additive) change what a puff is. |
sprite | A single textured billboard that can grow, spin and fade. |
trail | A ribbon following a moving source. |
beam | A line between two points. |
blast, blast-classic | Particle bursts. |
wormhole | A lensed throat onto another sky. |
Bank attributes
| Attribute | Meaning |
id | How content references it. |
category | A filing label for the editor — the base game uses weapon, death, damage, defense, light, trail, environment. |
instances | How many can run at once; 4 when left out. Past the cap, the oldest is torn down. Same reasoning as sound banks: shield hits want 16, a wormhole wants 2. |
persistent | An effect started on an object with an instance id is remembered on that object, so a console that reloads or joins late, and the Game Master, see it too. Engine trails and hull fires are persistent; explosions are not. Running until stopped is the renderer's part: a smoke or sparks layer with duration="0" emits until the effect is stopped. |
tier | The lowest console video tier that draws it — low, medium or high, low when left out. Set on a bank or on one layer. |
<alias> | Alternate names. As with sound banks, this is how legacy references (Object, Missile, Shield) still resolve. |
<sound bank="..."> | A sound bank to fire with the visual. |
The wrapper can carry master-scale and clip-radius. The server
reads and serves them, but nothing draws by them yet, so they do not change how big an
effect is or how far away it is drawn.
Ids are unique by replacement, silently: a second bank with the same id in one file
replaces the first, and a later module's bank replaces the whole of an earlier one. An
alias spelled like a real bank id never resolves, because ids are looked up first.
The architectural point: the server does not understand a layer
This is the thing worth knowing, because it changes what you can do without touching the
engine.
Of everything on a <layer>, the server reads only
renderer, tier, space and offset, plus
any <shader> and <bind> children. Every other
attribute is carried as opaque key/value and forwarded to the client verbatim — as
strings, unparsed.
So adding a new kind of effect is a client file plus data, with no engine
rebuild. There is no server‑side schema to extend and no C# to write.
Writing your own renderer
A renderer is a JavaScript module at /js/effects/<name>.js. Module
Html folders overlay one virtual web root by priority, so a module dropping
Html/js/effects/plasma-arc.js is served and imported exactly like a built‑in
one — and <layer renderer="plasma-arc" /> then works.
Name the file in lower case: the renderer name is lower‑cased before it is imported. The
default export is a class extending Mythrical, from
/js/three/effect-kit.js:
constructor(ctx) // build; add everything to this.Group
Update(elapsed) // ms since last frame, slow-mo aware;
// call this.Dispose() when the effect is over
Stop() // optional: a persistent effect asked to end gracefully
Dispose() // release GPU resources; detach this.Group
ctx carries the layer's authored attributes in params, live game
state in bindings, the scene, what the effect is attached to
(source, null for a world‑anchored effect), the trigger
position and radius, and clock — the one timeline
every layer of the effect shares, in milliseconds. life is only what the
trigger asked for, and 0 when it asked nothing; the built‑in renderers use a
life param if the layer sets one and clock otherwise. Parameters
that arrive with a trigger override the layer's own.
Params arrive as strings, because the server never parses them. Fold them
with the kit's FxNum and FxBool rather than parseFloat
alone: those refuse to poison a uniform with NaN, and FxBool handles the
"false" case, which is truthy as a bare string.
Use effect-kit.js rather than reimplementing its pieces. It exists because
writing a good renderer previously meant copying engine shader source — and the worst thing
to go without is the log‑depth handling, whose failure mode is not a warning: fragments
depth‑test in the wrong space and the effect hides behind the world, which presents as
"my effect does not draw". The viewer uses reversed depth where the browser supports it
and log depth elsewhere, so a shader that skips the log‑depth chunks can draw on your
machine and vanish on another.
Load textures through the kit's cached loader. Each renderer building its own loader leaked
a GPU texture per instance, so every projectile impact cost one.
Declare a static Manifest = { name, description, params, targets, spaces }
and the Effect editor can offer your renderer and edit its parameters.
spaces lists the layer spaces it handles (local,
world, emit) and is ["local"] when left out. A
renderer without a Manifest still runs in game, but the editor reports it as an error and
will not save a bank that uses it.
Effects compile while the mission loads
When a console loads, every effect its video tier can draw is built once out of sight
and its shaders compiled, behind the Initializing spinner, so the first explosion
of a fight does not stall the frame. Restarting the same mission does no work again.
A custom renderer takes part without doing anything special, provided its constructor
builds its materials from params alone. During the warm‑up it is constructed
with no source, no position and a radius of 1, and is never updated. Materials made later,
in Update or after an asynchronous load, are not warmed, and a constructor that
throws on that bare context just leaves the renderer cold: the first real play pays the
cost, and nothing breaks.
Blasts
A blast is an object with a speed. A weapon with class="Blast", or ordnance
with a <blast speed="…"> child above 0, sends a front rolling outward
instead of hitting everything in reach at once. Its effect is the Shockwave
bank unless effect names another. The front's effect is fired once, with the
full reach as its radius, the travel time as its life, and a speed
trigger param; the server does not update it afterwards. So a front bank's renderer must
grow by its own clock, and a radius‑scaled layer such as flash does not belong
in one, because it would draw the whole reach at once.
Ordnance can also name an impact-effect bank for what its round shows on
a hit.
Two things that will catch you
The file is generated, and comments do not survive a save.
effect-banks.xml round‑trips through the Effect editor, so any XML comment you
add is lost the next time it is saved. Per‑bank notes go in
<description>, which does round‑trip.
Offset is per‑layer and sits within the effect. A layer's
offset positions it relative to the effect; positioning the effect as a whole is
done by whatever placed it — a vessel class, a model, a placement, or a mission action. A
mission's Effect action can give its own offset property, in the same model
space, to put a one‑off effect at a point on a hull. Hull fires are the example: each
layer is offset to a point on the hull, and the effect rides the ship because it is in the
local frame.
Saving in the editor takes effect on the server at once, but open consoles keep
the banks they loaded. Reload a console to see a change. A hand edit to the file
is read when the server starts.
Practical advice
- Build up from one layer. Add layers until it reads, then stop. The
one‑layer spark shower is as deliberate as the five‑layer detonation.
- Add a light to anything that should feel bright. A fireball with no
light layer does not touch the ships around it, and that is most of why an
explosion can look flat.
- Set
instances honestly. Shield hits happen in volleys.
- Mark continuous effects
persistent, or a console that
reloads or joins late will not see them.
- Use
description. It survives, and it is the only note that
does.