Skip to content

feat: WP-07 site and asset pipeline - #119

Open
N1k4G wants to merge 3 commits into
mainfrom
site/wp-07-asset-pipeline
Open

feat: WP-07 site and asset pipeline#119
N1k4G wants to merge 3 commits into
mainfrom
site/wp-07-asset-pipeline

Conversation

@N1k4G

@N1k4G N1k4G commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Moves dive sites to validated, renderer-neutral resources with an asset manifest, layer factories, culling, pooling and quality tiers, per Codex_Review.md WP-07.

Both acceptance criteria have passing tests

"Invalid site data fails at build/test time." src/sites/site-resources.ts validates on import and throws, targeting the failures that are silent rather than loud:

  • lerpProfile walks points assuming ascending x. Unsorted input does not throw — it interpolates against the wrong segment and returns a plausible depth for the wrong place.
  • An inverted structure box can never be hit by solidAt, so a wall quietly stops existing.

"One asset can be replaced without changing simulation or collision data." Re-pointing an asset at a different atlas and frame leaves every gameplay value byte-identical, and the two documents are asserted structurally disjoint so an art edit cannot reach collision again.

The separation is structural, not documentary

src/sites.js mixed collision, air, spawn and current data with features, visual zones, atmosphere and decoration rules in one object — so "change how a site looks" and "change where the diver can swim" were the same edit.

The generator partitions them into two documents and refuses to run if a descriptor grows a field in neither list. A new field must be deliberately classified, never silently dropped or silently promoted into collision.

Parity is proven against the real legacy source loaded via ?raw, not a snapshot, replaying floorAt, ceilingAt, solidAt, overheadAt and badAirAt across a sampled grid of every site including beyond the profile ends where clamping applies.

The parity test earned its place immediately

The first implementation folded lerpProfile's division into its multiplication:

legacy:  t = (x - a.x) / (b.x - a.x);  return a.d + (b.d - a.d) * t
mine:    return a.d + ((b.d - a.d) * (x - a.x)) / (b.x - a.x)

Algebraically identical, numerically not. It moved results by one ULP and broke parity on two of four sites. The operation order is now preserved deliberately, with a comment saying why.

The typechecker caught a second assumption: SiteFeature was modelled as a uniform {kind, x, d}, but real features include depth spans (dTop/dBottom) and kind-specific extras.

Renderer

The Pixi renderer added children in a flat list, so draw order was an accident of call order. It now owns named layer containers and populates them from the layer factory, with pooled markers and resync skipped until the camera has actually travelled.

Two deliberate limits:

  • Markers are plain placeholder shapes. Production atlases are BLOCKED_EXTERNAL, so this draws something obviously provisional rather than art guessed at here. The manifest fixes the atlas/frame contract, so substituting real textures will not touch the renderer's structure.
  • The hand-authored hull, seabed and engine stay. They are bespoke art for one scene, not authored site content; replacing them with markers would degrade the slice to prove a point the decoration layer already proves.

Known gap, worth raising before WP-08

The visual risk in the renderer change is unguarded. compare-rendering.mjs gates the legacy client only, so nothing here would catch a Pixi visual regression. This was verified structurally — build, e2e, layer assignment — but not visually.

Extending the pixel comparison to the Pixi client is the natural next work, and I would want it before WP-08 starts moving real scenes.

Verification

npm ci, npm run lint, npm run typecheck, npm run build, and npm test all pass. No change to the legacy client, the numerical model or the golden traces.

N1k4G added 3 commits August 4, 2026 17:19
WP-07 begins by making the separation structural rather than documented. The
legacy descriptors mix collision, air, spawn and current data with features,
visual zones, atmosphere and decoration rules in one object, so "change how a
site looks" and "change where the diver can swim" were the same edit.

scripts/generate-site-resources.mjs partitions the descriptors into two
renderer-neutral documents and refuses to run if a descriptor grows a field
that is in neither list. A new field must be deliberately classified, never
silently dropped or silently promoted into collision.

src/sites/site-resources.ts validates on import and throws rather than
serving geometry that corrupts quietly:

- lerpProfile walks points assuming ascending x. Unsorted input does not
  throw; it interpolates against the wrong segment and returns a plausible
  depth for the wrong place.
- An inverted structure box can never be hit by solidAt, so a wall silently
  stops existing.

tests/parity/site-geometry.test.ts replays floorAt, ceilingAt, solidAt,
overheadAt and badAirAt against the real legacy source across a sampled grid
of every site, including beyond the profile ends where clamping applies.

That test earned its place immediately: the first implementation folded
lerpProfile's division into its multiplication, which is algebraically
identical and numerically is not, and it broke parity on two sites by one ULP.
The operation order is now preserved deliberately, with a comment saying why.
The Pixi slice built its wreck from hand-written Graphics calls and read no
site data at all, which is the scaling limit WP-07 exists to remove. This adds
the data path without yet rewiring the renderer.

The manifest maps every authored feature kind to one asset, with an atlas, a
layer and a minimum quality tier. The mapping is hand-authored because it is
art direction, but completeness is enforced by test in both directions: a new
feature kind fails until someone assigns it an asset, and an orphaned entry
fails until someone removes it. Neither can rot quietly.

The layer factory turns presentation data into an ordered, culled placement
list and builds no Pixi objects, so it is testable without a GPU and a second
renderer could consume the same output. Draw order is sorted rather than
inherited from authoring order, so the same inputs always produce the same
scene. Quality tiers drop decoration before the things a diver navigates by.

Feature shapes are modelled as authored rather than as assumed: some are
points with a `d`, others span `dTop`/`dBottom`, and kinds carry their own
extra fields. The first version assumed a uniform `{kind, x, d}` and the
typechecker rejected it against the real data.

WP-07's acceptance criterion now has a test: re-pointing an asset at a
different atlas and frame leaves every gameplay value byte-identical, and the
two documents are asserted structurally disjoint so an art edit cannot reach
collision again.
The renderer added its children in a flat list, so draw order was an accident
of call order and there was nowhere for authored content to go. It now owns
named layer containers matching the manifest, and populates them from the
layer factory.

Placement markers are pooled. Which features are visible changes many times
during a dive, and allocating a Graphics per feature per resync would churn
the heap for a scene whose contents barely change. Resync is also skipped
until the camera has actually travelled, rather than running every frame.

Markers are deliberately plain shapes. Production atlases are BLOCKED_EXTERNAL,
so this draws a placeholder rather than art guessed at here; the manifest
already fixes the atlas and frame contract they will load through, so
substituting real textures does not touch this file's structure.

The hand-authored hull, seabed and engine stay as they are. They are bespoke
art for one scene, not authored site content, and replacing them with markers
would degrade the slice to prove a point that the decoration layer already
proves.
@github-actions github-actions Bot added the release:minor Minor production release / user-visible feature label Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:minor Minor production release / user-visible feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant