# schedule
The storm's clock. Every strike is a pure function of the storm phase, so the storm graph (as
expressions over its own time) and the director (as Luau over the generator's clock) agree exactly,
and the whole storm loops every `PERIOD` seconds.
Two assets require it: the `storm` graph beside it (`storm.procGraph`, which builds its flash, bolt and burst
expressions from `STRIKES`, `flashExpr`, `envelopeExpr` and `windowOne`) and the `StormDirector`
component of the Procgen Effects Showcase world (lights, materials and thunder from `phase`, `envelope`, `afterglow` and `STRIKES`).
## Exports
### Values
- `M.PERIOD`: `13.6`, the loop length in seconds.
- `M.OFFSET`: `5.006`, the phase the storm is in at graph time 0. A strike has just landed there, so
a paused frame is a hero frame.
- `M.STRIKES`: the four strikes, each `{ t, kind, target, top, variant, weight }`. `t` is the phase
the strike lands at, `target` where it lands, `top` where the bolt starts, and `weight` its
brightness weight. `kind` is `"ground"` (throws sparks and smoke where it hits) or `"tower"` (hits
the finial). `target` and `top` are in the storm graph's own space, where the tower stands at the
origin; in the world they stand where the entity playing the graph does.
| `t` | `kind` | `target` | `top` | `variant` | `weight` |
|-----|--------|----------|-------|-----------|----------|
| 1.6 | ground | `{ 15, 0, -8 }` | `{ 3, 135, -2 }` | 1 | 1.0 |
| 5.0 | tower | `{ 0.05, 48.6, 0.1 }` | `{ -7, 135, 5 }` | 2 | 1.2 |
| 8.6 | ground | `{ -19, 0, 7 }` | `{ -30, 135, 12 }` | 3 | 1.0 |
| 11.0 | tower | `{ 0.05, 48.6, 0.1 }` | `{ 9, 135, 6 }` | 4 | 1.3 |
- `M.PULSES`: the return strokes of a strike, each `{ delay after the leader, strength, decay rate
per second }`: `{ 0.00, 1.00, 30 }`, `{ 0.11, 0.80, 26 }`, `{ 0.24, 0.62, 20 }`,
`{ 0.43, 0.36, 11 }`.
### Functions
- `M.phase(t)`: the storm phase at graph time `t`, `(t + OFFSET) % PERIOD`.
- `M.envelope(ph, s)`: how bright strike `s` is at phase `ph`: 0 before the strike, about 1 at the
return stroke. It sums every pulse whose delay has passed, each decaying exponentially at its rate.
- `M.afterglow(ph, s)`: the cloud glow that lingers after strike `s`, at phase `ph`:
`0.55 * exp(-d * 1.7) * min(d * 20, 1)` for `d` seconds after the strike, 0 before it.
- `M.envelopeExpr(s)`: the same envelope as `envelope`, written as an anim expression string over
`t`.
- `M.afterglowExpr(s)`: the same afterglow as `afterglow`, as an expression string over `t`.
- `M.flashExpr()`: the scene-wide flash as an expression string: the sum of every strike's
envelope times its weight, capped at 3.
- `M.windowExpr(only, lag, rise, fall)`: a burst of activity `lag` seconds after each strike, as a
bump expression in [0, 1]: it rises with `min(d * rise, 1)` and falls with `exp(-d * fall)`.
`only` limits it to strikes of one kind (`"ground"` or `"tower"`); `nil` includes every strike.
- `M.windowOne(s, lag, rise, fall)`: the same bump for strike `s` alone.
## Usage
```luau
local S = require("~.storm.schedule")
local generator = entity.find("storm").component("Generator")
local ph = S.phase(generator:getTime())
local flash = 0
for _, strike in ipairs(S.STRIKES) do
flash += S.envelope(ph, strike) * strike.weight
end
-- the same flash, as an anim expression for a procGraph
local flashExpr = proc.anim.expr(S.flashExpr())
-- a burst 0.15 s after each ground strike
local bursts = S.windowExpr("ground", 0.15, 12, 1.0)
```
# storm_sky
Sky material for the lightning storm in the `fx_storm` scene. Loaded on its own, the scene's
entrypoint makes it the sky (a temporary `StormSky` entity with a `Skybox`); as an overlay on the main
scene, the main scene's sky stands. The `StormDirector` component on the `storm` entity resolves it by
name and, on each frame where the strike light changes, writes `flash` (the summed strike flash times
1.1 plus the afterglow times 0.6) and `bolt_dir` (the direction from the camera drawing the viewport
toward the top of the strongest current strike, with that top's height scaled by 0.6).
Shader: `storm_sky` (`storm/shaders/storm_sky.shader`), a sky-domain shader. Its header: a boiling
storm deck, with two layers of domain-warped cloud drifting with the wind over a bruised, near-black
gradient and a cold haze on the horizon. Every lightning strike lights the deck from inside: `flash`
is the strike brightness and `bolt_dir` the direction the bolt leaves the cloud, so the cloud glows
brightest around the fork, silvers the thin edges and silhouettes the thick masses in front of it.
Render state (the `render` block of mat.yaml): `cull: none`, `depth_write: false`.
## Properties
mat.yaml sets only the shader and render state. Every property takes the shader default, and the
director writes two of them at runtime:
- `flash` 0.0 (range 0 to 20): brightness of the light inside the deck, written by `StormDirector`.
- `bolt_dir` (0, 1, 0, 0): direction of the bolt from the viewer, written by `StormDirector`; the
glow centres on it.
- `wind` 1.0: multiplier on the speed the cloud layers drift.
- `exposure` 1.0: multiplier on the final sky colour.
# storm_ground
Ground material for the lightning storm scene `fx_storm`. The scene's `ground` entity (a built-in
cube scaled 90 x 0.2 x 90 at (-100, -0.1, 220)) wears it. The `StormDirector` component on the
`storm` entity resolves it by name and, on each frame where the strike light changes, writes `flash`
and `bolt_pos` for the strongest current strike.
Shader: `storm_ground` (`storm/shaders/storm_ground.shader`). Its README: sodden ground, with
churned earth, rock, puddles with animated rain rings, and the lightning bolt's reflection in the
pools. From its header: world-space and texture-free; puddles fill the hollows of a procedural
height field and mirror the sky, rain rings spread on every wet patch, the strike's reflection is
traced from the reflected view ray to the bolt's column, and a distance haze matched to the sky's
horizon blends the far ground away.
Render state: the defaults (opaque blend, back faces culled, depth write on).
## Properties
mat.yaml sets `center_x` and `center_z`, every other property takes the shader default, and the
director writes two of them at runtime:
- `wet` 0.95: darkens the albedo, lowers roughness and fills more of the hollows with water.
- `puddles` 1.0: multiplier on puddle coverage.
- `flash` 0.0 (range 0 to 20): strike brightness, written by `StormDirector`; it scales the bolt's
reflection in the pools and brightens the distance haze.
- `bolt_pos` (0, 0, 0, 0): position of the strike's target, written by `StormDirector`; the shader
uses its x and z as the bolt's column.
- `fog` 0.011: density of the distance haze, applied as `1 - exp(-distance * fog)`.
- `center_x` -100, `center_z` 220: where the keep stands; the ground within 9 units of it stays a
drier apron and the puddles fill in from 9 to 16 units out.
# effectShowcase
The procedural effects of the Procgen Effects Showcase world. Each one is a `.procGraph` that plays
through a `Generator`, and every shader, material and module a graph draws with sits beside it in
this package, so the package carries everything its graphs need. Each graph's README says what it
makes, how to place it and what each of its inputs changes.
## Contents
| Folder | Graphs | What they make |
|---|---|---|
| `combat/` | `impact`, `muzzleFlash`, `tracer` | A round striking a hard surface, the bloom at a barrel as a weapon fires, and the glowing streak of a round in flight. They draw with `fxFlash`, `fxSmoke` and `fxSpark` in `combat/shaders/`. |
| `blast/` | `wall_blast` | A charge blowing a breach through a stone block wall, on a 9.6 s loop. |
| `storm/` | `storm`, `tower` | A thunderstorm around a lone tower: wind-driven rain, lightning striking on the schedule `schedule.module` keeps, and sparks and smoke where strikes land. `tower` is the stone keep it stands around. |
| `rift/` | `arcane_rift` | A rift tearing open in the ground, its motes and rubble moved by the compute shaders in `rift/compute/`. |
| `dragon/` | `dragon_breath` | A dragon's fire breath sweeping a courtyard: a flame jet, rolling smoke and drifting embers. |
| `launch/` | `launch` | A heavy-lift rocket liftoff on one looping timeline, with the heat-haze material `rk_haze_mat`. |
| `waterfall/` | `wf_env`, `wf_water`, `wf_spray`, `wf_foam`, `wf_mist` | A waterfall built in parts: the cliff, basin and boulders, the falling and still water, the droplets, the foam where the falls land, and the mist with its rainbow. Its water materials are in `waterfall/materials/`. |
| `shaders/` | | `fx_noise`, the noise functions the waterfall and wall blast shaders include. |
## Playing one
A graph plays on a `Generator` component bound to it:
```lua
-- `graph` is the graph's reference: the AssetRef `asset.resolve` or a search returns for it.
local e = entity.spawn("impact", { position = { 0, 1, 0 } })
e.component.add("Generator", { graph = graph, playOn = "load" })
```
Set a graph's inputs with the Generator's `params`, e.g. `params = { size = 3 }`.