# preseed.tides.state — the TIDES shared contract
Everything that crosses a lane boundary in part 9 ("THE FARJAN"), as **pure
functions of part-local `t`** and nothing else. No state, no side effects, no
entity lookups, no `ctx`.
```lua
local State = require("preseed.tides.state")
State.ship(t) --> { pos = {x,y,z}, forward = {x,0,z}, speed, headingDeg }
State.sea(t) --> { windSpeed, windDirection, choppiness, amplitude,
standingWaveRatio, foamDecay, significantHeight }
State.weather(t) --> { squall01, coverage, cloudBase, cloudTop, wetness, rain01 }
State.light(t) --> { sunElevDeg, sunAzimuthDeg, timeOfDay, sunColor, sunIntensity,
ambientColor, ambientIntensity, keyOcclusion01, exposure }
State.palette(t) --> { skyHorizon, skyZenith, skyGround, cloudLit, cloudShadow,
waterBody, foam, fogColor, hullRed, upperworksWhite,
antifoul, bootTop, cabinEmissive }
```
## Why pure
Hot-reload rebinds a module's function bodies without re-running its
module-level chunk. Module-level mutable state in a shared file therefore keeps
the *old* build's values while running the *new* build's code, and the bug looks
like it is in whichever lane read it last. A file of pure functions cannot do
that.
It is also what makes the wake derivable. Because `ship(t)` is pure, the wake at
time `t` is exactly the locus `{ ship(t - a) : a in [0, 26] }` — sample it and a
scrub, a seek or a hot-reload all produce the identical wake. An accumulating
wake is empty after every seek, and this demo is scrubbed constantly.
**No lane may add module-level mutable state to this file.**
## Ownership
| Function | Owner (may retune the numbers) | Readers |
|---|---|---|
| `ship` | SEA (track & speed) | CAMERA, SEA, FERRY |
| `sea` | SEA | SKY, CAMERA |
| `weather` | SKY | LIGHT, SEA, FERRY |
| `light` | LIGHT | SKY, SEA, FERRY |
| `palette` | LIGHT | everyone |
A lane needing a new field **appends** it. Nobody renames or removes one.
## Fixed facts
- **Axes:** `+X` = ahead (bow), `-X` = astern, `+Y` = up, `-Z` = port,
`+Z` = starboard. The sun sits just off her starboard bow.
- **Music:** 192 BPM, 4/4 — `BAR` = 1.25 s. Thirty seconds is exactly 24 bars.
Nothing changes on a number that is not a multiple of 1.25.
- **`pos.y` is the calm-water datum**, not her true height. `preseed.Buoyant`
owns her vertical; read the entity for her real `y`.
- **`ProceduralSky` owns the sun's direction** via `timeOfDay`. `sunAzimuthDeg`
is the intended bearing the clouds are lit against, not an instruction to
write a direction onto the `Light`.
# preseed/tides/sky
The SKY lane of THE FARJAN (part 9, TIDES): the weather, the sky, the
volumetric squall, the horizon and the rain veil.
## What it owns
* **The scene's ProceduralSky** — gradient, hour, sun disc and disc bearing,
driven from `preseed.tides.state`. It drives the SCENE's sky entity and puts
back what it found in `M.exit`; a part that spawns its OWN ProceduralSky
beside it does not get a second sky, it gets a fight, because `SkyConfig` is
one global record every Sky component pushes into.
* **The volumetric squall** — a cellular cumulus field, raymarched,
self-shadowed, with the sun attenuating through it.
* **The horizon** — warm low height-fog that closes the sea/sky join and hides
the ocean clipmap's far edge.
* **The rain veil** from t = 25.
## The pipeline is this part's own
`tid_cloud_fill` writes the density volume, `tid_cloud_march` marches it into
`tid_cloud_rt`, and `tid_cloud_composite` resolves and blends it. None of the
three is shared with any other part, and `M.exit` releases all of them.
It does not go through `preseed.volume`, and the reason is written at the top
of `init.luau`: that module marches through `preseed.emission_raymarch`, which
builds its ray from `unproject(ndc, 0) -> unproject(ndc, 1)` and tests
`depth < 1.0` for "a surface is here" — both correct before the engine's
projection was reversed, and both wrong after it. Its target is also a single
fixed 1600x900 buffer shared with every volume in the demo.
## The numbers that are load-bearing
* **Every frequency in the fill is bounded by the VOXEL PITCH** — 98 m
horizontally, 63 m vertically at 224 x 72 x 224 over an 22 km box. A feature
shorter than about three voxels does not resolve, it aliases, and because the
cell map and the warp are functions of x and z only, that aliasing is
constant down a column and reads as vertical fluting. Detail finer than that
is added at MARCH time, where there is no grid.
* **The sun's extinction taps are a jittered cone, not a fixed ladder.** Four
fixed offsets comb-filter the medium along the sun's axis; with the sun near
horizontal that axis is horizontal too, and the ripple lies in planes of
constant X.
* **The march jitters WITHIN each step**, not at the ray's start. The step
grows with distance, so a start offset bounded by the near step leaves most
of the quantisation coherent between neighbours out at the deck.
## Contract
Reads `State.weather`, `State.light`, `State.palette`, `State.sea`
(windDirection and windSpeed — the wind that moves the cloud is the wind that
raises the sea) and `State.sunDirection`. Owns `State.weather`.
`M.update` must be called LAST in the part's update, after whatever moved the
camera: the march builds its ray basis from the camera pose.
# preseed/tides/light.module — THE FARJAN, LIGHT
Part 9's sun, fill, shadows, exposure and grade, under the SKY lane's weather.
## What it owns
* the scene's directional key and ambient fill, driven every frame from
`preseed.tides.state`, and **restored** on the way out;
* the key's **occlusion**, measured through the part's own cloud volume rather
than scheduled;
* the tonemap's exposure ramp and the bloom drive;
* the **print stage** (`preseed.shaders.tid_print`) — chromatic aberration, the
print black and the output dither;
* the reflection probe's recapture schedule.
## Three things that are not obvious
**It drives the SCENE's `sun` and `ambient`, not lights of its own.**
`/runtime/lighting` resolves exactly one directional and one ambient, and in
this scene they are the persistent `sun` and `ambient` entities. A part that
spawns its own pair authors something nothing reads: measured mid-part with
`tid_sun` at 3.0 and `tid_ambient` at 2.0 on their own entities, the resolved
config was the scene sun at 0.12 and the scene ambient at 0.14. So this lane
drives the scene's, remembers what it found, and puts it back in `M.exit`.
**It WRITES the sun's direction, which reads backwards until you know why.**
`src/engine/systems/sky.rs` refuses to sync the sun onto the lighting config
while a directional `Light` entity exists — the Light is "the sole authoritative
writer" — so a Light left alone keeps its default straight-down direction while
the visible disc goes wherever the sky puts it. `State.sunDirection(t)` is the
vector toward the disc; a Light's direction is the way light TRAVELS, so the
value written is its negation.
**The key is measured, not scheduled.** `tid_key_probe.computeShader` marches
the SKY lane's own cloud volume from the ship toward the sun every frame and
reports the mean transmittance over eight rays, and the key is
`keyOpen(t) * (1 - KEY_DEPTH * occlusion)`. `State.light(t).keyOcclusion01` is
only a FLOOR under that measurement, and it is zero until t = 24 — so for
twenty-four seconds the key is the weather's answer and nothing else. The one
exception is the slot at bar 22, which is authored and says so: the wedge of
clear air is carved at the horizon, beyond 2.2 km, and the ship stands inside
that radius under the rain veil, so the probe marching outward never sees the
hole.
## The order in `P.update` is load-bearing
CAMERA → **LIGHT** → SKY. LIGHT must come before SKY because it probes the
volume SKY filled; SKY must be last because its raymarch builds its ray basis
from the camera pose.
## The print stage
`Fx.grade` matches its table against `PP.PRESETS`, which has no
`chromatic_aberration` entry, so the brief's 0.0018 was read and dropped in
silence. The shared `fx.module` is read by twelve other parts and is not this
lane's to change, so the pass is added here from a tides-owned shader at
priority 75 — after tonemap (60) and vignette (70), before the letterbox and
fade (200). It also carries the part's **print black** and its **dither**, both
driven per frame from `printGrade(t)`: measured, round 1 put 21-57% of every
frame at exactly (0,0,0) with the 25th percentile on zero in all twelve, which
the brief forbids by name.
## Budgets published for the FERRY lane
`M.PRACTICAL_BUDGET = { points = 18, spots = 4, shadowCasting = 2 }`.
`MAX_POINT_LIGHTS` is 64 for the whole scene and over-cap lights are dropped
silently rather than merged — and this part is not alone in it.