Found while porting the Nauvis tile catalog to Rust (#226). Not fixed in that port, per the standing rule: a unilateral fix on the Rust side reads as a port bug in tier 2.
What is wrong
TileResolverParams in src/noise/tiles/resolve.ts has no waterLevel field, so makeTileResolver builds its elevation tree with the default:
const elevationAt = makeElevationNauvis({ seed0, segmentationMultiplier, startingPositions });
makeElevationNauvis does accept waterLevel (elevationNauvis.ts:19, used at line 146 as max(nauvisMain - waterLevel * 2, startingIsland)). It just never gets one from here.
The call site confirms it is not supplied further up either. elevationRenderRequest.ts:570 builds renderTerrain's ctx from eight request fields and waterLevel is not among them - and it could not be, because RenderTerrainOptions.ctx is typed Omit<TileResolverParams, "seed0">. renderTerrain then builds its own early-out elevationAt the same way, so both halves are consistently at water level 0.
renderElevation on the same dispatch does receive waterLevel (elevationRenderRequest.ts:695), so the elevation view responds to the slider and the terrain view does not.
How much it costs
Measured at seed 123456 over a 2,401-point grid (x, y in -600..600 step 25), comparing waterLevel 0 against 5:
| quantity |
differs at |
elevation_nauvis |
1,144 of 2,401 |
| resolved tile |
322 of 2,401 (13.4%) |
So this is not a rounding-scale difference. Moving the water-coverage slider should redraw the coastline and, on the Nauvis terrain view, it does nothing.
Why no test caught it
test/resolveTile.spec.ts grades against oracle-tile-names, which is captured on the DEFAULT preset - water level 0 - so the parameter it fails to thread is at its default in every fixture. Its Task 12b block does test that climate params thread through, and waterLevel is simply not in the list.
Scope of the fix
Add waterLevel to TileResolverParams, pass it into makeElevationNauvis in both resolve.ts and renderTerrain.ts's early-out tree, and add it to the ctx block at elevationRenderRequest.ts:570. Worth checking the other three makeTileResolver callers at the same time - renderEnemies.ts:259, renderRocks.ts:189 and renderResources.ts:247 - since a rock or enemy placement gated on a tile would inherit the same blind spot.
A fix will move rendered pixels, so it needs re-scoring rather than a green gate: test/previewAgreement.spec.ts and the tier-3 Nauvis comparisons should be read before and after.
Rust side
crates/fmw-noise carries water_level correctly through NauvisCtx into elevation_nauvis, so the port has no equivalent gap. Tier 2 (test/wasmNauvisParity.spec.ts) builds its tile env from the shipped expression trees rather than from makeTileResolver, precisely so it grades the tile FORMULAS and leaves this plumbing gap visible here instead of hiding it in a checksum. One of its three cases runs at waterLevel = 5.
🤖 Generated with Claude Code
https://claude.ai/code/session_017hVzDgUq6g7LATfczGxUrE
Found while porting the Nauvis tile catalog to Rust (#226). Not fixed in that port, per the standing rule: a unilateral fix on the Rust side reads as a port bug in tier 2.
What is wrong
TileResolverParamsinsrc/noise/tiles/resolve.tshas nowaterLevelfield, somakeTileResolverbuilds its elevation tree with the default:makeElevationNauvisdoes acceptwaterLevel(elevationNauvis.ts:19, used at line 146 asmax(nauvisMain - waterLevel * 2, startingIsland)). It just never gets one from here.The call site confirms it is not supplied further up either.
elevationRenderRequest.ts:570buildsrenderTerrain'sctxfrom eight request fields andwaterLevelis not among them - and it could not be, becauseRenderTerrainOptions.ctxis typedOmit<TileResolverParams, "seed0">.renderTerrainthen builds its own early-outelevationAtthe same way, so both halves are consistently at water level 0.renderElevationon the same dispatch does receivewaterLevel(elevationRenderRequest.ts:695), so the elevation view responds to the slider and the terrain view does not.How much it costs
Measured at seed 123456 over a 2,401-point grid (
x, yin-600..600step 25), comparingwaterLevel0 against 5:elevation_nauvisSo this is not a rounding-scale difference. Moving the water-coverage slider should redraw the coastline and, on the Nauvis terrain view, it does nothing.
Why no test caught it
test/resolveTile.spec.tsgrades againstoracle-tile-names, which is captured on the DEFAULT preset - water level 0 - so the parameter it fails to thread is at its default in every fixture. Its Task 12b block does test that climate params thread through, andwaterLevelis simply not in the list.Scope of the fix
Add
waterLeveltoTileResolverParams, pass it intomakeElevationNauvisin bothresolve.tsandrenderTerrain.ts's early-out tree, and add it to thectxblock atelevationRenderRequest.ts:570. Worth checking the other threemakeTileResolvercallers at the same time -renderEnemies.ts:259,renderRocks.ts:189andrenderResources.ts:247- since a rock or enemy placement gated on a tile would inherit the same blind spot.A fix will move rendered pixels, so it needs re-scoring rather than a green gate:
test/previewAgreement.spec.tsand the tier-3 Nauvis comparisons should be read before and after.Rust side
crates/fmw-noisecarrieswater_levelcorrectly throughNauvisCtxintoelevation_nauvis, so the port has no equivalent gap. Tier 2 (test/wasmNauvisParity.spec.ts) builds its tile env from the shipped expression trees rather than frommakeTileResolver, precisely so it grades the tile FORMULAS and leaves this plumbing gap visible here instead of hiding it in a checksum. One of its three cases runs atwaterLevel = 5.🤖 Generated with Claude Code
https://claude.ai/code/session_017hVzDgUq6g7LATfczGxUrE