Forward-looking plan (not a living doc - a point-in-time record, like the specs
under docs/superpowers/). Written 2026-07-17, right after basis_noise and
spot_noise were fully reverse-engineered.
Generate Factorio 2.1.11 map previews in the browser, from a Preset, with no
server round-trip - to optionally replace (or front-run) the headless
preview-service/ container with instant, offline previews.
Correctness bar: visually faithful, with per-expression agreement to ~1e-6
against the game (the fastapprox noise floor - bit-exactness is the wrong goal,
see basis-noise-NOTES.md). A preview is downsampled (~1 px per N tiles), so
small numeric drift is invisible anyway.
Non-goal: reproducing the game's threading, chunk streaming, or entity placement. We evaluate noise per pixel, independently.
The two primitives the community called "black magic" are solved and verified against the game's own machine code:
basis_noise-src/noise/basisNoise.ts(basisNoise, andbasisNoiseTablesFromSeedfor the full seed -> tables derivation).docs/noise/basis-noise-NOTES.md.spot_noise- candidate RNGsrc/noise/spotCandidates.ts, selectionsrc/noise/spotSelection.ts, rendering documented.docs/noise/spot-noise-NOTES.md.
What remains is breadth, not depth: an expression evaluator, a handful of smaller primitives, and a large body of game data (the expression trees + tile autoplace) to port - all of it validatable against the oracle, none of it research-hard.
6. Render tile/resource -> color, downsample, compose image
5. Tile types elevation + climate -> winning tile (autoplace "peaks")
4. Settings control:*:frequency/size/richness/bias, property_expression_names
3. Expressions base-game trees: elevation, temperature, moisture, aux, resources
2. Evaluator the 14 NoiseOperations + arithmetic + built-in vars (x,y,distance,seed)
1. Primitives basis_noise (done), spot_noise (done), + the rest below
Everything below is built test-first against the game, exactly as the primitives were. Commit the harness so every later layer has ground truth.
- Commit a reusable headless oracle under
test/oracle/(currently rebuilt ad hoc each session): a mod that routes an arbitrary named noise expression ontoelevation, readscalculate_tile_properties({"elevation"}, positions)inon_init,write_files JSON,error("DUMPED-OK")to exit (~1.7 s/run). Recipe inbasis-noise-NOTES.md/noise-oracle-basis-measurements. It needs a local Factorio install, so gate the tests on its presence. - A
sampleExpression(exprString, positions, {seed, ...}) -> number[]helper that runs the oracle and returns game values, for use as fixtures. - Decide fixture policy: capture once, commit JSON (like the existing
basis-noise*.json), run the comparison in CI without Factorio.
The smallest recognizable preview. Proves the evaluator design before the big data port.
- Evaluator core (
src/noise/eval/): a per-pixel evaluator. Recommended shape - compile each named expression to a JS closure(ctx) => numberwherectx = { x, y, seed, controls }; the expression tree maps directly to nested calls. (The game compiles to a register program inNoiseCache; we do not need to replicate that - it is an internal batching optimization.) - Remaining primitives needed for elevation (RE against the oracle):
MultioctaveNoise/VariablePersistenceMultioctaveNoise- octave layering overbasis_noise. Already partly measured (basis summed, ~RMS-normalized, std ratio 1.077x at octaves=4). Nail persistence/lacunarity/per-octave seed and input-scale exactly.NoiseOperations::MultioctaveNoise::runat a known address (see the disassembler inscratchpad), or infer fromcore/prototypes/noise-programsLua.- Trivial math ops:
Const,Clamp,If,PowInt, arithmetic (BinaryExpressionadd/sub/mul/div/min/max,UnaryExpression). These are one-liners; confirm signatures from the binary if in doubt. DistanceFromNearestPoint(starting-area / spawn shaping) if the elevation tree uses it.
- The elevation expression: Nauvis
elevation(the default) ported 2026-07-19 asmakeElevationNauvis(src/noise/expressions/elevationNauvis.ts, merged f95594f) - a 1:1 hand-port ofelevation_nauvis_functionfromcore/prototypes/noise-programs.lua, validated against the oracle to the f32 floor (worst far-field 4.08e-3, near-spawn 2.87e-6). KEY finding: it needs NO new primitive -ridge/terrace(rated "small" below) are used by NO elevation tree (every apparent "ridge" was thebridge`` substring). The Island variant (elevation_island, a trivial `bias=-1000` lakes variant) shipped 2026-07-19 as `makeElevationIsland` (`src/noise/expressions/elevationIsland.ts`) - a thin wrapper over `makeElevationLakes` with `bias=-1000` and `segmentation_multiplier/4`, validated against the oracle to the f32 floor (worst far-field 6.66e-3, near-spawn 5.2e-7). All three base map types (Nauvis, Lakes, Island) now render client-side; they dispatch via a `mapType` selector. - Render:
elevation < 0-> water (deep vs shallow by threshold), else land; draw to a<canvas>at a chosen scale. Coastline falls out. - Validate: sample the game's
elevationat a grid for a few seeds; diff to ~1e-6; then eyeball the rendered coastline against a real--generate-map-preview.
Done = a recognizable land/water/coastline image from a Preset, matching the
game's elevation to the noise floor.
Shipped on branch feat/m2-climate-terrain (see
docs/superpowers/specs/2026-07-19-milestone2-climate-terrain-design.md and
docs/noise/terrain-render-NOTES.md). Client renders Nauvis terrain colors that
match the real game's tile placement at 100% (153/153 oracle points, 3 seeds).
- Ported
temperature_basic,moisture_nauvis,aux_nauvis(default Nauvis climate) as hand-written closures over the solved primitives, reusing the extractednauvisSharedinternals (nauvis_plateaus/hills/bridge_billows+ newforest_path_billows). Oracle-validated to the f32 floor (temperature worst 7.07e-5, aux 1.01e-5, moisture 2.76e-5).temperatureis ported but unused by tiles. KEY:trees_forest_path_cutoutis billow noise, not tree placement. - Reverse-engineered the native
expression_in_range(undocumented C++ builtin) from the oracle:min(peak_maximum, peak_multiplier * min_i(min(v_i - from_i, to_i - v_i)))- per-dim triangular peak, min across dims, no lower clamp,peak_maximummay be Infinity (sand-1). Worst residual 9.5e-7. Seedocs/noise/expression-in-range-NOTES.md. - Tile-type resolution (
src/noise/tiles/): the 21 Nauvis tiles as a catalog (catalog.ts) built onexpression_in_range_base/water_base/noise_layer_noise;resolveTilepicks the argmaxprobability_expression(tiles are a pure argmax -orderhas no effect). Tile selection is driven by aux + moisture (land) and elevation (water); temperature is unused. - Palette: each tile's
map_color, cross-checked byte-for-byte againsttiles.lua. - Validated tile assignment against the game via a new
get_tiletile-ID oracle path (chunk-generate +surface.get_tile().name,test/oracle/oracle.ts), NOT--generate-map-preview(which only sanity-checks compositing). The widened multi-seed fixture (20/21 tiles) caught a latentquickMultioctaveNoiseoctave-seed bug (invisible on even seeds) - fixed to a flatseed0 + k. - Terrain render path (
renderTerrain, water early-out with an analytically proven-safe threshold) + a Terrain/Elevation view toggle (Nauvis only). The moisture/aux sliders drive the terrain view (non-default climate is a faithful port but not oracle-validated point-by-point; only the default preset is).
Done = colored terrain (grass / sand / dirt / desert variants + water), matching the game's tile placement at the default preset.
spot_noise is fully solved; this is wiring, not research.
M3a (regular whole-map patches) - DONE (merged to main, pushed, deployed live
2026-07-20).
Spec docs/superpowers/specs/2026-07-19-milestone3-resources-design.md, plan
docs/superpowers/plans/2026-07-19-milestone3a-regular-patches.md.
-
Port the per-resource autoplace expressions (iron/copper/coal/stone/ uranium + crude-oil) from
prototypes/entity/resources.lua+resource-autoplace.lua.src/noise/resources/= catalog (6 param sets), resourceMath (distance/amplitude local functions), regularPatches (makeRegularPatches: spot field + blob term). Thecontrol:<resource>:*levers flow in viareadResourceControls. -
Wire
selectSpots+ the max-of-cones renderer into per-pixel evaluation, region-cached. The one unknown (random_penalty's batch composition inside spot-quantity selection) resolved: a batch over all skip-set accepted spots in acceptance order (quantityBatch). Oracle-validated (pure-regular fixture, iron+uranium, 2 seeds): abs error < 0.7 units everywhere after fixing the cube root to the game's fastapproxpow(fastApprox.ts,fastCbrt). -
Overlay resource patches on the terrain image (resource
map_color), order-priority winner whereprobability >= 0.5(resolveResource+renderResources); workerview:"resources"; Resources toggle on the panel. -
Validated against the pure-regular
calculate_tile_propertiesoracle and a headless full-view render (all 6 ore types as blob patches, spawn cleared). -
Ore excluded from water (2026-07-20): resources collide with water, so renderResources skips any pixel the terrain drew as deepwater/water. Reuses renderTerrain's exact water decision (no re-derivation), so the ore edge lines up with the drawn coastline.
M3b (starting patches, near-spawn guaranteed ore) - DONE (merged to main,
pushed, deployed live 2026-07-20; branch feat/m3-resources fast-forwarded in,
tip 0131aa8). Plan
docs/superpowers/plans/2026-07-20-milestone3b-starting-patches.md.
- Ported
starting_patches(src/noise/resources/startingPatches.ts) for the four solids (iron/copper/coal/stone): a spot field (region_size 450, candidate_spot_count 32, spacing 48,hard_region_target_quantity) plus the sameblobs0blob term shape as regular patches, over a deterministic favorability (lake-mask x modulation x origin-excluder x 2, minus a distance term - no random jitter).resourcePatches.tscomposesall_patches = max(starting, regular)for the four solids; oil/uranium (no starting placement) delegate to the regular field unchanged. - Version-discrepancy finding: the
~/Downloads/factorio 4/datadump used to draft the M3b plan is Factorio 2.0.77 (stale); the app targets 2.1.11.starting_patcheschanged materially between those versions (radius 120->150, region_size radius2->radius3, spacing 32->48, and the favorability term dropped itsrandom_penalty_at(0.5,1)jitter entirely in favor of a deterministicorigin_excluder). Caught mid-implementation by diffing against the oracle and the Steam app's own bundled 2.1.11 Lua - seedocs/noise/M3-session-handoff.mdand thefactorio-data-version-hazardmemory note for the full finding and the authoritative source path. - The starting favorability's lake mask couples to the map's
elevationPROPERTY, which on the default Nauvis map iselevation_nauvis(confirmed against the oracle, not theelevation_lakesliteral an earlier draft assumed).makeStartingPatcheshardcodesmakeElevationNauvis; generalizing to Lakes/Island starting elevation is deferred until the resolver needs a non-default map type for resources. -
selectSpotsgained afavorabilityBatchoption (mirrors the existingquantityBatch) for favorability expressions with arandom_penaltybatch term - unused by 2.1.11 starting patches (whose favorability turned out deterministic) but available for future primitives - plus routed the hard-targetconeScaleshrink throughfastCbrt(wasMath.cbrt). - Oracle-validated against a
has_starting_area_placement=1fixture: near-spawn points match to well under 1.0 absolute.test/resourcePatches.spec.tsuses a combinedabs<1.0 OR rel<1e-2tolerance to also accommodate the pre-existing far-fieldbasisNoisef32 floor (~5e-4 relative on ~1e4-magnitude points) without masking real near-spawn errors. - Headless full-view eyeball (Task 7): iron/copper/coal/stone patches cluster tightly around spawn, oil renders as a small dot far out (no starting placement, as expected), spawn is not bare.
M3.5 (per-tile placement stipple) - SPIKED then DEFERRED (2026-07-20). The
placement RNG was reverse-engineered (EntityMapGenerationTask::generateEntities /
generateEntityOnTile): it is taus88, seeded once per 32x32 chunk from
ChunkPosition (max(341, 0x3FBE2C + 7919*chunkX + 7907*chunkY), no map-seed), and
the roll is place if taus88()/2^32 < probability. But it is a single per-chunk
stream shared across ALL entity autoplacers (enemies, rocks, resources), consumed
in a fixed order with 2 data-dependent jitter draws per placed entity - so faithful
resource stipple would require simulating whole-chunk entity generation across
subsystems this app never ported. Multi-session + cross-subsystem; deferred
(Eric, 2026-07-20). Full RE + a cheaper cosmetic-dither alternative:
docs/noise/placement-roll-NOTES.md. Solid-footprint (opaque where prob >= 0.5)
shipped in M3a/b.
SHIPPED 2026-07-27, as a third option neither the spike nor this entry named.
Not the whole-chunk simulator, and not the cosmetic dither: a real roll against
the real probability, with the two coupling sources deliberately dropped. Dropping
the 2 jitter draws is the load-bearing move - with no data-dependent consumption a
chunk's 1024 draws are a pure function of (chunkX, chunkY, salt), which restores
per-position purity and keeps the tiled render byte-identical to the untiled one.
Cross-overlay arbitration is then simply absent, so positions are not the
game's; density is the claim, and it is validated per overlay against real
count_entities_filtered counts. All five overlays converted (Vulcanus rocks,
Nauvis rocks, Nauvis enemy bases, Vulcanus geysers, Nauvis crude oil - the four
issue #9 named, plus Vulcanus rocks). See "WHAT WAS ACTUALLY BUILT" in
placement-roll-NOTES.md.
M3a follow-ups (known, deferred by priority - 2026-07-20):
- [~] Ore excluded from cliffs - WITHDRAWN 2026-07-30 (issue #24, PR #57).
There is no exclusion rule to port.
generateCliffs()(0x1016229b4) calls exactly three functions and touches no tile, entity or resource data, so the game does not exclude ore from cliffs at generation time by any mechanism this could reuse. The observation that started this - cliffs appearing on ore far below the chance rate - also does not survive its own null: region[0,0]'s 945 ore tiles are 2 blobs, not 945 independent samples, and under a torus-shift null two of three regions were not significant at all (P = 0.51, 0.29). The effect is real pooled over nine regions (18/12,533 = 0.14%) but it is a consequence of where cliffs land, not of a rule that keeps them off ore. Do not re-open this as a port. - Oil renders as tiny dots, not patches - DONE 2026-07-27 (Task 8). Both
sub-items landed together, which is what the note predicted: (a)
renderResources.tsnow applies oil'srandom_penalty{source=1, amplitude=48}probability factor, and (b) oil places through the per-tile placement roll rather than the>= 0.5footprint. It was never really "tiny dots" - the threshold drew oil's whole patch EXTENT, 1234 tiles in[0,0]-[512,512]where the game has 8 wells. Now 7 vs 8 there, and 0 vs 0 in[4096,4096]. Therandom_penaltybatch extent, which this was expected to require, turned out to be irrelevant to density - seedocs/noise/random-penalty-NOTES.md. - 2026-07-20 finding (from~/GitHub/factorio-data@ 2.1.11,resource-autoplace.lua:103-105): forrandom_probability < 1the game appends* random_penalty{x=x, y=y, source=1, amplitude=1/random_probability}to the probability expression (and/ random_probabilityto richness - the richness half is ALREADY done inregularPatches.ts). Therandom_penaltyfactor is1 - (1/rp)*U, i.e. it is itself a per-tile ~rpBernoulli baked into the probability. Applying (a) alone, with the current>= 0.5threshold render, makes oil sparser or vanish (the factor is>= 0.5for only ~rp/2of tiles, and oil's spots are already ~1 tile), so it is NOT a standalone visual win - it only pays off combined with M3.5's roll (place if roll < probability). So (a) is really M3.5-prep, or wants a per-resource render rule (e.g. render oil where probability> 0, ~rpdensity, using only the already-validatedrandomPenaltyprimitive - a cheap approximation that skips the un-RE'd placement RNG). Decision (2026-07-20, Eric): FOLD INTO M3.5 - apply the probability factor together with the RE'd placement roll (place if roll < probability), validated tile-for-tile againstfind_entities. Oil stays as-is (tiny dots) until M3.5; no standalone change. - DONE 2026-07-27 (Task 8), and the fold was the right call - the factor and the roll landed together exactly as decided. Two corrections to the analysis above, both from measurement. The factor costs a 96x density reduction, notrp= 48x:1 - 48Uis positive only forU < 1/48and averages 1/2 there. And validation is by density, not tile-for-tile againstfind_entities- this port does not reproduce positions for any overlay. Therandom_penaltybatch extent that made this look expensive turned out to be irrelevant to density (random-penalty-NOTES.md), so the per-resource render rule was never needed.
Done = ore patches overlaid on land, responding to the frequency/size/richness sliders.
-
Cliffs (
cliffiness/ cliff elevation bands) - DONE (2026-07-20; merged to main + deployed live, main tipe783901). Done = a Nauvis-gatedview: "cliffs"footprint overlay paintingCLIFF_MAP_COLOR = [144,119,87](a 5x5 block per cell for preview legibility), driven by the two oracle-validated noise fields (cliff_elevation_nauvis,cliffiness_nauvis) plus a disasm-derived geometric placement rule (crossesCliff+ the 4-tile grid +toMaybeCliffOrientationnone/not-none predicate), levers wired (nauvis_clifffrequency/continuity +cliffSettings). Validated 100% against realfind_entities_filtered{type="cliff"}dumps (frac 1.000 at both seeds since 2026-07-30; was 0.943/0.942 while the fields were sampled half a tile off in y) and cross-checked against a realfactorio --generate-map-previewrender of the same seed/region (95.7% spatial agreement at render resolution, using the game's own emitted[144,119,87]pixels as ground truth). Full writeup:cliffs-NOTES.md. All three items previously listed here as "still deferred" are now retired, and none of them explained the ~6% Nauvis residual - which is itself now RESOLVED (2026-07-30): the fields were sampled atj*4 + 0.5because the port added the prototype'sgrid_offset(a CENTRE offset) to the sample position. Corrected, Nauvis is 1.0000 recall / 1.0000 precision / ratio 1.000 at both seeds, i.e. exact. The five earlier causes were falsified on (2026-07-28, the day five stated causes were falsified - seenotes-must-say-how-they-were-measuredandcliffs-NOTES.md):fixImpossibleCellswas ported in PR #32 (dea73ac,cliffPlacement.ts:105,test/cliffFixImpossibleCells.spec.ts) and changes zero predictions on Nauvis; the exactwouldColliderejection is moot because no Nauvis cliff touches water at all; and the ore-on-cliff exclusion was withdrawn outright (see the M3a follow-up above - there is no such rule in the game). The residual is still open and is now believed to be a rule error rather than a field error (PR #57).VoronoiNoise(layer-1 primitive table below) is confirmed unneeded for Nauvis - it appears nowhere in the cliff tree or any other Nauvis expression traced so far, only on Space-Age planets - so it remains un-ported with no open TODO against it. -
Enemy bases (enemy autoplace -
enemy-autoplace-utils.lua), starting-area clearing. Done = footprint overlay (view: "enemies") from the oracle-validatedenemy_base_probabilityfield, thresholdENEMY_FOOTPRINT_THRESHOLD = 0.05, spawners only (worms and per-nest placement deferred, seedocs/noise/enemy-bases-NOTES.md). -
Trees (tree autoplace -
base/prototypes/entity/trees.lua), decoratives ruled out. Done = a density-shadedview: "trees"overlay, all 15 Nauvis species (tree_01...tree_09_red), oracle-validated to the ~1e-3 noise floor, composited intoview: "all"between terrain and the resource/ enemy/cliff overlays, withcontrol:treesfrequency/size wired. The preview's own binary was disassembled first: it places real entities and charts them (chartEntities->Chart::drawEntity->ChartingInterface::drawRectangle), so this port renders the exact expected value of what the game's per-tile placement roll would produce rather than a guessed shading. The charted footprint (1.0x1.0 tiles, kernel[0.5,1,0.5], compounding per-tree alpha) was determined empirically against a real game render and validated on two seeds (game ink / our ink: 5.70/4.67 = 1.22x @ seed 123456, 7.89/6.89 = 1.14x @ seed 777771) - the residual is the expected-value-vs-discrete gap and needs the deferred Phase 2 stipple to close, not further tuning. Decoratives are ruled out by binary evidence: noDecorativeMapGenerationTasksymbol exists anywhere in the shipped binary, and the preview worker calls onlychartCliffs+chartEntities- the game's own preview shows no decoratives either. Full writeup, including the oracle-caught per-speciessizeOffsetbug and its methodology lesson, theBASIS_ABS_MAXearly-out measurement, and the measured render cost:docs/noise/trees-NOTES.md. Design record:docs/superpowers/specs/2026-07-21-nauvis-trees-design.md. Still deferred (do not treat as done): the per-tile placement roll (Phase 2,docs/noise/placement-roll-NOTES.md), worms/fish autoplacers, and cliff exclusion (consistent with the existing ore-on-cliffs gap). -
[~] Non-Nauvis planets (Space Age) - each is another expression set. Vulcanus TERRAIN done 2026-07-23 (V1: spawn geometry, cracks, the radial ashlands/mountains/basalts biome system, volcano spots, climate aux/moisture/ temperature, elevation, the ~24
*_rangetile-probability expressions + the 19-tile argmax, and planet-dispatched rendering). Every expression is oracle-validated against real Factorio 2.1.12 (Space Age) to the f32 floor. Spec/plan:docs/superpowers/specs/2026-07-23-vulcanus-client-preview-design.md,docs/superpowers/plans/2026-07-23-vulcanus-client-preview-v1.md; per-expression notes indocs/noise/vulcanus-*-NOTES.md. - Perf - FIXED 2026-07-24 (~50x, byte-identical). Was ~60x heavier per pixel than Nauvis (~545 us/px); now ~12 us/px (~1.4x Nauvis), i.e. interactive when tiled across the worker pool (~2s at 1024^2). The prime suspect in the earlier note - per-pixelspot_noise/region work - was wrong: a CPU profile put ~81% of the time in rawbasisNoiseeval, and a call count showedbasisNoiserunning ~12,600x/pixel against a ~200-eval floor. Root cause was the un-memoized field DAG: unlike the Nauvis resolver (which evaluates each field once per pixel into anenvscalar bag), the Vulcanus resolver passed lazy field closures into its 19 tile*_rangeexpressions and re-invoked them per access, so a shared node likemountains_raw_volcano(feeds all 3 biomes, each read ~5x by the ranges) and every noise octave beneath it recomputed ~60-100x/pixel. Fix:memoXY(src/noise/eval/memoXY.ts), a single-slot(x,y)cache wrapping each field node across the Vulcanus expression files - a render sweeps one pixel at a time, so repeat reads collapse to one eval per node per pixel while returning the identical float (byte-exact; verified by a stash-diff of a 4-window render hash and the full oracle suite still green at 984 passed). Confined to the Vulcanus path; the shared noise primitives and Nauvis are untouched.memoXYdoes NOT transfer to Nauvis - Nauvis never had the pathology (checked 2026-07-24, don't re-investigate). Profiled the same way: Nauvis terrain is 164basisNoise/px, ~8 us/px - already at the noise floor (Vulcanus's post-fix floor is ~200/px). Two reasons, both verified: (1) at the argmax leveltiles/resolve.tsalready evaluates elevation/aux/moisture once per pixel into anenvscalar bag, so the tile catalog reads scalars, not closures; (2) inside each field,elevationNauvis/aux/moistureare written straight-line - every sub-node is computed once into a local const and then combined, nothing re-read. There is no redundant work for a cache to remove, so wrapping Nauvis inmemoXYwould only add per-call branch overhead (marginally slower). The one remaining Nauvis lever is the hot leaf itself:basisNoiseis ~65% of both planets' time and is now necessary work - only SIMD/WASM on the kernel would move it, and that would help both planets equally.**Vulcanus V2 (resources) DONE 2026-07-24** - the four resource region fields (tungsten-ore, coal, calcite, sulfuric-acid geyser), their near-spawn starting spots and biome favorabilities, and a resource overlay (`view: "resources"`, three solid ores) all ported and oracle-validated. Also **restored the three resource-coupling terms** V1 had approximated away in the tile catalog (`vulcanus_metal_tile`, `vulcanus_calcite_region`, `vulcanus_sulfuric_acid_region_patchy`), raising `get_tile` agreement from **96.85% (369/381) to 98.16% (374/381)**. Full writeup, including the per-resource parameter table, the two approximations (`random_penalty -> 1`, no richness) and their measured consequences, the `sulfuricAcidPatches` residual anomaly and its resolution, and characterization of the 7 remaining `get_tile` mismatches (far-field f32 argmax flips, unrelated to the coupling restoration): `docs/noise/vulcanus-resources-NOTES.md`. - **Perf gate (Task 8, 2026-07-24; corrected in the final-fix pass): PASS.** Terrain now evaluates the resource region fields on every pixel even with the overlay off, since the tile catalog reads them. Measured 11.92 us/px (V1, worktree at `a0ea049`) vs. 13.96 us/px (V2, this branch, `view: "terrain"`) at 1024x1024/seed 123456 - a 1.17x change. **But `"terrain"` is not the view non-dev users get for Vulcanus** - Task 7 made `"resources"` the default (`effectiveView` in `ElevationPreviewPanel.vue`), and `renderVulcanusResources` builds its own independent Vulcanus field stack rather than reusing the terrain render's, so the ore DAG runs twice per pixel on that path. Measured 18.22 us/px for `view: "resources"` - a **1.53x** change against the 11.92 us/px baseline. Both are well inside the ~2x regression gate, so this is a record correction, not a regression - but 1.53x / 18.22 us/px is the number that reflects what users actually see. See `docs/noise/vulcanus-resources-NOTES.md`'s Performance section and `.superpowers/sdd/2026-07-24-vulcanus-v2-resources/task-8-report.md` for the full before/after table and methodology. **Vulcanus V3 (sulfuric-acid geyser) - DONE 2026-07-27.** Shipped 2026-07-26 as a patch blob (a fourth `VULCANUS_RESOURCE_CATALOG` entry painting `sulfuricAcidRegionPatchy > 0` in `map_color` `[199, 199, 26]`), which drew the region where the game *rolls* rather than the geysers themselves and overstated their area by 4.2x (measured). It now rolls: `placement: "roll"` on the catalog entry, the game's `vulcanus_sulfuric_acid_geyser_probability` read from source (no `random_penalty` - unlike its calcite/coal/tungsten neighbours), `makePlacementSet` with the lava tile gate and the 2.8 x 2.8 `collision_box`, and a 3x3 mark per placement. **Validated against the real game.** Oracle region 4 `[-256,-256]` has 56 geysers and the model places 56; regions 2 and 3 hold no sulfur at all and both sides are 0. n = 56 is a weak denominator (Poisson sigma ~7.5) and eight alternative salts span 46-63, so read the agreement as unbiased, not precise. See `vulcanus-resources-NOTES.md` gap #4 and `placement-roll-NOTES.md`. Nauvis crude oil followed in Task 8, which closes the last overlay issue #9 tracked. **Vulcanus cliffs - DONE 2026-07-26.** `renderVulcanusCliffs` + `cliffiness_basic`, oracle-validated to under 5e-6. Far smaller than the Nauvis cliff port: the planet overrides `cliffiness` to `cliffiness_basic` and `cliff_elevation` to its own `elevation`, so none of the Nauvis hills/ringbreak/billows chain is involved, and the placement geometry is shared. Vulcanus has **no** cliff autoplace control, so the bands are planet constants (elevation_0 70, interval 120) rather than preset fields. Not yet validated at entity level against a `find_entities` dump - see `docs/noise/vulcanus-cliffs-NOTES.md`. **Vulcanus rocks - DONE 2026-07-26.** `renderVulcanusRocks` + `vulcanus_decorative_knockout`, oracle-validated. Four rock entities share two probability expressions, and everything they read except the knockout was already ported for V1's tile catalog. Vulcanus deliberately omits the `rocks` autoplace control ("can't add the rocks control otherwise nauvis rocks spawn"), so there are no sliders. Rendered as a threshold on the probability field, which caps at 0.2 and forms a plateau - so it reads as rocky ground rather than as individual rocks, and wants an eyeball. See `docs/noise/vulcanus-rocks-NOTES.md`. Deferred: demolishers (the only `voronoi_cell_id` user - skipping it is why Vulcanus needed no `VoronoiNoise` port). Fulgora/Aquilo DO build terrain on Voronoi, so they will force that primitive.
-
A
previewMap(preset, {width, height, scale}) -> ImageDataentry point. -
Wire into the app UI next to / instead of the
preview-servicecall. Keep the server path as a fallback and as a cross-check oracle. -
Perf: evaluate off the main thread (Web Worker); tile the viewport. Done 2026-07-21 - the 1024x1024 preview is split into 64 128px tiles rendered across a pool of
hardwareConcurrency - 1workers and blitted progressively. Browser-measured on a 12-core machine (8 performance + 4 efficiency), seed 123456, median of 3:| view | single worker | tiled (11 workers) | speedup | | --------- | ------------- | ------------------ | ------- | | elevation | ~1,700 ms | 237 ms | 7.2x | | terrain | ~9,100 ms | 1,519 ms | 6.0x | | all | ~12,500 ms | 1,918 ms | 6.5x | Half the image is on screen about a second before the render finishes, so the perceived latency is lower again. Tiling is byte-identical to the single render - `test/tiledEquality.spec.ts` asserts it across all six views, and the only renderer needing a seam fix was cliffs (marks are painted around a cell centre, so each tile queries a 2-tile halo clamped to the image). Caching per-region spot lists across tiles was measured and **rejected**: rebuilding every resolver per tile costs only 1.03-1.07x total CPU, so the scene/geometry split it would have required was not worth it. See `docs/superpowers/plans/2026-07-21-region-tiling-renderer.md`. -
Perf: target interactive re-render on slider changes (currently still a manual Generate).
The binary defines exactly 14 NoiseOperations::*::run types. Status:
| op | needed for | difficulty |
|---|---|---|
BasisNoise |
everything | DONE |
SpotNoise |
resources/enemies | DONE (RNG+select+render) |
Const, Clamp, If, PowInt |
all trees | trivial (math) |
BinaryExpression/UnaryExpression (arithmetic) |
all trees | trivial |
MultioctaveNoise |
terrain | DONE (multioctaveNoise.ts) |
QuickMultioctaveNoise |
climate (temp/moisture/aux) | DONE (quickMultioctaveNoise.ts) |
VariablePersistenceMultioctaveNoise |
terrain (elevation) | DONE (variablePersistenceMultioctaveNoise.ts) |
DistanceFromNearestPoint |
spawn/starting area | DONE (distanceFromNearestPoint.ts) - min(maximum_distance, nearest euclidean dist to any point); points int/256 fixed-point. Geometry read off disasm; needs runtime point data (see below) so validates via the elevation tree, not standalone. |
Terrace |
terrain banding | small - NOT used by any elevation tree (2026-07-19) |
Ridge |
terrain | small - NOT used by any elevation tree; every "ridge" in noise-programs.lua is the bridge`` substring (2026-07-19) |
RandomPenalty |
jitter | small |
Multisampling |
AA/quality | small |
VoronoiNoise |
some terrains | confirmed unneeded for Nauvis (2026-07-20, M4 cliffs RE) - present in the binary but not referenced by the cliff tree or any other Nauvis expression traced so far; only appears on Space-Age planets. Un-ported, no open TODO. |
Method for each: disassemble NoiseOperations::<Op>::run (the capstone
disassembler scratchpad/re/fdis.py seeks by symbol address from
nm | c++filt), and/or probe it through the oracle, then validate to ~1e-6.
None are "black magic" - the two that were are already done.
Traced the Nauvis elevation tree (core/prototypes/noise-programs.lua). The
default elevation_nauvis is large (elevation_nauvis_function over nauvis_main,
nauvis_bridges, nauvis_detail, nauvis_macro, nauvis_hills_plateaus, ...);
the elevation_lakes / elevation_island variants are smaller
(finish_elevation{make_0_12like_lakes{...}}). All of them reference values that
are not pure noise:
distance- distance from the spawn point (world origin-ish).starting_lake_positions/starting_positions- runtime-generated spawn/lake points, fed todistance_from_nearest_point. These come from the game's starting-area placement, not a noise expression.water_level,segmentation_multiplier- map-gen settings (scalars the app'sPresetalready models, but the noise DSL exposes them as vars).
So an elevation render needs a decision before the evaluator work: either (a) render
far from spawn, where the distance- and starting_*-gated terms fade out, and
supply water_level/segmentation_multiplier from the preset - the cheapest path to
a recognizable coastline; or (b) additionally RE the spawn / starting-lake placement
to be faithful near the origin. (a) is the right MVP.
There is also an evaluator-shape fork: hand-port each named expression to a TS closure (roadmap's stated primary strategy) vs. write a parser for the noise DSL strings. Hand-porting is less upfront work for the MVP; a parser pays off only if the whole tree corpus gets ported.
Two strategies; use both:
- Transpile the Lua (primary). The base-game expressions live in
data/base/prototypes/**anddata/core/prototypes/**as the noise DSL (an expression-builder API). Re-express each named expression as a JS closure over the primitives. Some are generated by helper functions (autoplace utils) - port the helper, not each output. - Capture + validate (safety net). The game can serialize named noise
expressions; and
calculate_tile_propertiesgives exact values. Every ported expression is diffed against the game before it is trusted. This is what makes the port low-risk despite its size.
This layer is where the real effort and the ongoing maintenance sit: the trees
are large, interdependent, reference the control:* constants, and differ across
game versions and base-vs-Space-Age.
The app already models the levers in Preset
(autoplaceControls, property_expression_names, seed). The evaluator's ctx
needs to expose them as the control:<name>:frequency|size|richness|bias
constants and honor property_expression_names overrides (which can replace a
named expression entirely - e.g. the map-type elevation swap). seed = seed0
(random -> wire 0); each basis_noise/spot_noise call carries its own seed1.
- Maintenance surface. The expression trees track game versions and base vs
Space Age. The
preview-servicecontainer gets this for free by running the real game; a client port must be re-synced on updates. - You already have a working preview.
preview-service/(headless Factorio + Cloudflare) renders correct previews today. A client-side reimplementation is a UX/infra optimization (instant, offline, no cold starts), not a correctness need. Weigh the ongoing port cost against that benefit before committing past the MVP. - De-risking: the MVP (M1) is cheap and proves the evaluator; stop there if the value/cost tradeoff does not hold up. The oracle makes every step verifiable, so there are no unknown cliffs left after the primitives.
src/noise/
basisNoise.ts # done
spotCandidates.ts # done
spotSelection.ts # done
eval/ # M1: evaluator + primitive library + arithmetic
expressions/ # M1+: ported base-game trees (elevation, climate, ...)
tiles/ # M2: tile autoplace peak resolution + palette
preview/ # M1+: per-pixel driver, region spot cache, render to canvas
test/oracle/ # M0: committed headless harness + capture helper
Start M0 + M1: commit the oracle harness, build the evaluator core, RE
MultioctaveNoise, port Nauvis elevation, render land/water. That converts the
two solved primitives into an actual image and locks the evaluator design before
the large expression port begins.