diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bb723c8..7774760 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -11,6 +11,50 @@ A large A-to-Z expansion. The world is a stack of strates the player descends through; each strate can be a fundamentally different *archetype*, connected at (0,0). +### 8.0 Human-scale generation contract (2026-09-05) + +All authored lengths are voxel-space values, converted at the boundary by `0.25 m/voxel`. The body +reference is a **0.34 m radius, 1.76 m tall** capsule (`1.36 × 7.04` voxels). The default strate +height is now **8 chunks = 256 voxels = 64 m**, which is intentionally larger than the 3-4 chunk +boss-arena anchor so an archetype can reserve seals, vertical play, and a high ceiling together. + +The default size families are deliberately independent: + +| Feature | Default metre derivation | Coupling kept sound | +|---------|--------------------------|---------------------| +| Room network | radius 4-10 m (8-20 m diameters), origin radius 12 m (24 m hub), cell spacing 32 m | `MaxRoomRadius 40 < RoomSpacing/2 64`; a 6 m radial margin remains around the largest room | +| Round passages | 1.5-2 m radius (3-4 m bores); maze corridor radius 2 m (4 m bore) | a 1.76 m body fits vertically with headroom; Maze `8 < CellSize/2 32` | +| Slab | floor at 16 m, ceiling at 38.4 m, 22.4 m open span | floor/ceiling noise and 4-8 m column diameters remain inside the open span | +| SurfaceWorld | 20 m terrain relief, 12 m warp, 60.8 m sky cap | terrain features are bounded below the 64 m strate envelope | +| VerticalShafts | 4-7 m shaft diameters, 3 m connector bore, 8 m ledge interval | shaft spacing is 20 m; connector/ledge values are derived from the roughness envelope | +| FloatingIslands | 12-24 m island diameters, 28 m cell spacing | the largest island leaves a 2 m centre-to-edge gap; thickness is separately derived | +| Inter-strate passages | 3-4 m mouth bore, 3 m mid bore, 8-24 m reach, 16-48 m offset | width, reach, and wander are separate body/placement decisions | + +For wall detail, `VOXEL_NOISE_SCALE=1.25` and the proven `sup|fBM|=1.5` give a roughness reach of +`1.875 × SurfaceRoughness` voxels. The default ratios are **0.625** against the minimum tunnel +radius, **0.469** against the Maze corridor radius, **0.469/0.625** against shaft/connector radii, +**0.234** against the minimum island radius (**0.484** including its 6-voxel blend), and +**0.625/0.938** against slab column radius for floor/ceiling roughness. SurfaceWorld is a +heightfield, so its **0.047** ratio is against its 80-voxel relief scale, not a fictitious radial +feature. FlatPlain and CrystalChamber intentionally share `GetSlabDensity` and the same slab +parameter block; they cannot acquire separate scale geometry without a generator change. + +This scale contract is informed by [Epic's Unreal level-blockout guidance](https://dev.epicgames.com/documentation/en-us/unreal-engine/designer-01-project-setup-and-level-blockout-in-unreal-engine), which recommends a player-sized reference and gives 2-3 m hall / 3-4 m height starting guidance. The +[2010 ADA Standards](https://www.ada.gov/law-and-regs/design-standards/2010-stds/) are a lower-bound +accessibility reference (915 mm clear walking width and larger passing/turning spaces), while +[NPS Lehman Cave dimensions](https://www.nps.gov/grba/learn/nature/lehman-caves-dimensions.htm) support +keeping natural chambers heterogeneous and allowing 20 m-plus cathedral volumes. + +Validation is deliberately split by cost: bounded regression fixtures remain 4 chunks, while the +production definition, season defaults, and owner-facing showcase use 8 chunks. The final focused +showcase (`WorldScaleShowcaseProductionFinal`) exhausted 256 rolls, measured 82 eligible single-region +rolls, skipped 174 multi-region rolls while lateral regions remain gated, and found **0/8** complete +hard-gate survivor set at fine step 1. It still produced diagnostic cards (7 fine renders, 1 refusal, +1 blank). The full `VoxelForge` run (`WorldScaleFullFinal3`) recorded **28 successes, 2 warning-only, +0 failures, 0 not-run**; all box-verdict violation counts were **0**, `WorldRadiusVoxels` stayed **0**, +and operator-stack equivalence paths remained bit-identical. The two authored-corpus relation findings +(2/12) are conservative experiment-envelope findings, not box-verdict violations. + ### 8.1 Archetypes (`ECaveGeneratorType`, VoxelStrateTypes.h) Each archetype has its own param `USTRUCT` (on `UVoxelStrateDefinition`, EditCondition-gated by `GeneratorType`) and its own density function in `VoxelGenerator.cpp`, dispatched by the @@ -421,7 +465,8 @@ TunnelNetwork and Underwater keep the nearest hash-room vertical placement at th `MakeStrateSeed(world-seed, strate-index)` matching the room graph's existing identity; FlatPlain and CrystalChamber recompute their slab void band and reject column-overlap points. Maze snaps to the nearest roughness-safe horizontal lattice corridor within one cell, VerticalShafts to the exact -axis of a roughness-safe shaft on the drainage tree within one shaft spacing, and FloatingIslands +axis of a roughness-safe shaft on the drainage tree within two shaft spacings at the default site +density, and FloatingIslands to a validated blob top within one island spacing. Those three return false when no footing exists inside that explicit lateral budget. GeneratePassages interpolates the snapped XY through the control-point chain and recomputes its conservative bound; diff --git a/CODEMAP.md b/CODEMAP.md index ec658eb..da92525 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -73,6 +73,32 @@ what generator type, and is there a passage/elevator SDF near here?" Paths relative to `Source/VoxelForge/`. `Public/` = headers, `Private/` = impl. +### 3.0 Human-scale generation contract (2026-09-05) +`VoxelStrateTypes.h` is now authored against the confirmed player capsule, not against naked voxel +counts: one voxel is **0.25 m**, the capsule is **1.36 voxels / 0.34 m radius** and **7.04 voxels / +1.76 m tall**, and the default strate envelope is **8 chunks / 256 voxels / 64 m**. Core defaults are +therefore: room diameters **8-20 m**, origin hub **24 m**, room cells **32 m**, round tunnel bores +**3-4 m**, maze cells **16 m** with a **4 m** bore, slab open span **22.4 m**, shaft diameters +**4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. These are +different derivations, not a global multiplier: passage widths follow the 2.5-4 m walking/fight +target, room/cell spacing preserves a rock web, and the 64 m vertical envelope reserves space for +tall rooms, surface sky, and vertical traversal. + +The reachable roughness envelope is `roughness * VOXEL_NOISE_SCALE * 1.5`, or `roughness * 1.875` +voxels. The new defaults keep it below the relevant feature radius: **0.625** of the minimum +TunnelNetwork tunnel radius, **0.469** of Maze corridor radius, **0.469/0.625** of shaft/connector +radii, **0.234** of the minimum island radius (**0.484** including the 6-voxel island blend), and +**0.625/0.938** of slab column radius for floor/ceiling roughness. SurfaceWorld has no radial cave +feature, so its ratio is reported against the 80-voxel terrain-relief scale (**0.047**). + +The complete generated `name | voxels | METRES | body/level meaning` audit is emitted by +`VoxelForge.Composer.Showcase` and includes optional transport fields. The official Unreal blockout +guidance ([Unreal level blockout guidance](https://dev.epicgames.com/documentation/en-us/unreal-engine/designer-01-project-setup-and-level-blockout-in-unreal-engine)) uses a player-sized reference and treats 2-3 m halls / 3-4 m heights as useful starting +guidelines; the [2010 ADA Standards](https://www.ada.gov/law-and-regs/design-standards/2010-stds/) +provide a 915 mm clear walking-width floor, not a game-space target; and [NPS Lehman Cave +dimensions](https://www.nps.gov/grba/learn/nature/lehman-caves-dimensions.htm) show why natural +chambers need a wide range rather than one universal volume ratio. + ### 3.1 Module & build | File | Role | |------|------| @@ -383,9 +409,9 @@ Header is rich with inline docs. Two namespaces + a per-chunk cache system. `VF_SuggestLandingPoint` (h:~550, implemented in `VoxelCaveMorphology.cpp`) is the pure landing-site query: nearest hash-room height at the requested XY for TunnelNetwork/Underwater, slab void midpoint for FlatPlain/CrystalChamber, and bounded lateral searches for a horizontal lattice corridor (one -Maze cell), the exact axis of a lower-seal/ledge-safe VerticalShafts tree shaft (one ShaftSpacing), -or a validated blob top (one IslandSpacing). The VerticalShafts result is therefore on the drainage -tree, not merely in an open roughness disk. +Maze cell), the exact axis of a lower-seal/ledge-safe VerticalShafts tree shaft (up to two local +shaft spacings at the 0.6 site density), or a validated blob top (one IslandSpacing). The +VerticalShafts result is therefore on the drainage tree, not merely in an open roughness disk. It returns `false` when those feature-bearing conditions cannot be proven inside the supplied budget; SurfaceWorld remains refused because its production height can depend on manager-owned biome/per-column context. It never touches a generator, operator stack, manager, cache, or mutable state. @@ -537,7 +563,7 @@ renderer and self-contained contact sheet. It consumes `FVoxelStrateSampleGrid` pass, emits deterministic centre-Y XZ and data-selected XY slices via `IImageWrapper`, and shows filled-cell and scalar density=0 contour views side by side without blurring the sampled raster. Each image is capped at 512 pixels (including its scale footer). `FVoxelStrateFinePreviewSettings` is a separate caller-selected -fine pass: the composer selects only survivors at step 1, radius 64 (a 128×128 XY ROI), and `MaxCells=2,000,000`; +fine pass: the composer selects only survivors at step 1, radius 64 (a 128×128 XY ROI), and `MaxCells=4,000,000`; the caller centres it on each coarse survivor's `LargestComponentPoint` and the writer labels it “largest open space” with its exact ROI and step; cap refusals and blank fine ROIs are recorded explicitly instead of showing empty evidence. The ordinary 64-card sheet remains sorted by survivor status then @@ -560,8 +586,9 @@ region manifests remain rejected because their cross-seam law is only 9/16. The season test currently reports **24 generated / 19 hard-gate survivors / 6 selected / 18 unselected or rejected**, with **4 vacuous**, **1 primordial-law budget**, **13 policy-not-selected**, and **393,216** round-trip density samples bit-identical. The specified UE 5.7 editor build succeeded and the full -`VoxelForge` namespace passed **28/28 tests** after the original seam was added; current results must be -re-recorded after the owner build. +`VoxelForge` namespace report `WorldScaleFullFinal3` recorded **28 succeeded, 2 warning-only, +0 failed, 0 not run** in **1,704.010 s**. The later focused production-height showcase was rerun +after the final audit-label correction and is recorded separately below. **`Public/VoxelSeasonAsset.h` + `Private/VoxelSeasonAsset.cpp`** — `UVoxelSeasonAsset : UPrimaryDataAsset`, the cookable carrier. Its editor button imports the reviewable JSON, stores the exact @@ -617,7 +644,7 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor | File | Test name | What it proves | |------|-----------|----------------| -| `VoxelForgeTestFixture.h` | — | `FTestWorld`: a headless world (transient strate definitions → `UVoxelSettings` → a real `UVoxelStrateManager::Initialize`) so tests hit `GetDensityAt`, where the thread_local caches live. One strate per archetype, **pinned via `FixedStrates`** so slot index → archetype is stable across seeds (`SlotSurfaceWorld` etc.). | +| `VoxelForgeTestFixture.h` | — | `FTestWorld`: a headless world (transient strate definitions → `UVoxelSettings` → a real `UVoxelStrateManager::Initialize`) so tests hit `GetDensityAt`, where the thread_local caches live. One strate per archetype, **pinned via `FixedStrates`** so slot index → archetype is stable across seeds (`SlotSurfaceWorld` etc.). Bounded regression fixtures default to 4 chunks; the production/default definition and the owner-facing showcase explicitly use 8 chunks. | | `VoxelForgeDensityPurityTest.cpp` | `VoxelForge.Determinism.DensityPurity` | 10k points re-sampled in shuffled order, same thread **and** on N workers, asserting BIT equality. `ValidateDeterminism` is game-thread only and cannot see worker-cache divergence. Includes a flat-field canary (AUDIT C1) and a diff-layer pass. | | ″ | `VoxelForge.Determinism.LiveEditInvalidation` | AUDIT C2 regression: triple the heightfield params, `Initialize` again, require the density to MOVE. The edit does not move the strate, so only `LayoutVersion` changes. | | `VoxelForgeClassifyTileTest.cpp` | `VoxelForge.Determinism.ClassifyTileSoundness` | Scans for a non-`Mixed` verdict, then brute-forces the exact mesher lattice (`g ∈ [-1, Cells+1]`). **A false verdict is an invisible, collisionless hole** — T1.d v1 was reverted for exactly this. Errors out rather than passing if it found nothing to check. | @@ -631,16 +658,16 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor | `VoxelForgeOpStackIslandTest.cpp` | `VoxelForge.OpStack.FloatingIslandEquivalence` | The port that runs the stack **backwards** — void + fill vs rock + carve, same classes with the opposite sign. Counts interior-solid and open-void samples separately (on this archetype an aggregate "N solid" is dominated by the seal bands and says nothing about the islands). Counts `AllSolid` and `AllAir` verdicts **separately** too: `AllAir` is the one no cave archetype could ever prove, and it is the entire perf argument here. | | `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | **Phase 1's load-bearing test.** The 7-op Maze stack vs `GetMazeDensity` over 20k points (aiming for bit-identity; a side-of-iso disagreement is the hard fail), plus purity across workers and brute force on every box verdict the stack emits. Reports how many tiles the stack can prove uniform — today's `ClassifyTile` proves **zero** for any cave archetype. | | `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). | -| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. Bootstrap follow-up: **1,422 applications / 168 distinct fields**; **38/64 survival**, feature scale **0→376**, walkable **0.000000→0.286766** (no broadening beyond the prior run); **1,356 Mixed + 608 AllSolid + 596 AllAir = 1,204 proved boxes**, **1,602,524 lattice voxels checked, 0 violations**, 146.250 s total in the final full-namespace run. | +| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. World-scale run: **1,422 applications / 168 distinct fields**; **36/64 survival**, 60 non-vacuous, 58 largest-component, 40 exact-law passes; **1,411 Mixed + 731 AllSolid + 418 AllAir = 1,149 proved boxes**, **1,529,319 lattice voxels checked, 0 violations**, 148.387 s. | | ″ | `VoxelForge.Composer.TerrainDetailLiveness` | Fixed 4,096-point `GetDensityAt` lattice, legacy and operator-stack paths; changes one terrain-detail group at a time with an empty terrain-op pool. Proves 9 live groups / 23 fields and 3 dead groups / 11 fields; all 24 rows match. | -| `VoxelForgeComposerPromotionTest.cpp` | `VoxelForge.Composer.Promotion` | Re-measures the 12 project/default members, simulates five deterministic 24-candidate seasons with normalized measured-metric novelty (`<0.20`), cap 6, cumulative JSON promotion, provenance counts, corpus-hash checks, fresh-load gate verification, spread/survival reporting, and deliberate stale-metric corruption. Final run: **9 promoted**, corpus **4/8/9**, survival **20.8/54.2/54.2/45.8/50.0%**, spread **0.869214→0.916302**, **63.556 s**, 0 failures. | -| `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, captures the same grid for the deterministic filled/contour XZ/XY preview, runs a separate step-1 radius-64 ROI pass for the 42 survivors centred on `LargestComponentPoint`, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Final run: **42/64 survival (65.6%)**, **64 distinct recipes**, **0 invalid recipes**, **64 coarse filled + 64 coarse contour pairs**, **42 fine filled + 42 fine contour pairs**, blank plan/card **6/42→3/42**, **243.649 s**, **0 refusals**. | -| `VoxelForgeComposerShowcaseTest.cpp` | `VoxelForge.Composer.Showcase` | Exhausts the bounded parameter-roll set (seeds **0, 7331**, indices **0–63**), excludes multi-region rolls while the lateral gate is off, and measures every missing-family candidate in all six interior target slots at step 4 / radius 256 / `MaxCells=8,000,000`. Hard gates are non-vacuous, largest air share ≥ **0.50**, and exact unsnapped arrival→departure connectivity; selection score is floor-area fraction + clearance tie-break + projected-surface tie-break. It asserts roll/manifest determinism, density sign, zero `WorldRadiusVoxels`, bit-identical metric reruns, and writes one alphabetized card per archetype to `Saved/VoxelForge/Showcase/index.html`, with step-1 radius-64 filled/contour plan + vertical ROI images centred on `LargestComponentPoint`. | +| `VoxelForgeComposerPromotionTest.cpp` | `VoxelForge.Composer.Promotion` | Re-measures the 12 project/default members, simulates five deterministic 24-candidate seasons with normalized measured-metric novelty (`<0.20`), cap 6, cumulative JSON promotion, provenance counts, corpus-hash checks, fresh-load gate verification, spread/survival reporting, and deliberate stale-metric corruption. World-scale run: **16 promoted**, corpus **4/8/16**, survival **45.8/66.7/75.0/66.7/58.3%**, spread **0.677151→0.867010**, **92.390 s**, 0 failures. | +| `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, captures the same grid for the deterministic filled/contour XZ/XY preview, runs a separate step-1 radius-64 ROI pass for the 33 survivors centred on `LargestComponentPoint`, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. World-scale run: **33/64 survival (51.6%)**, **64 distinct recipes**, **0 invalid recipes**, **715 proved custom boxes / 951,665 lattice voxels / 0 violations**, blank plan/card **4/33→5/33**, **174.854 s**, **0 refusals**. | +| `VoxelForgeComposerShowcaseTest.cpp` | `VoxelForge.Composer.Showcase` | Exhausts the bounded parameter-roll set (seeds **0, 7331**, indices **0–63**), excludes multi-region rolls while the lateral gate is off, and measures every missing-family candidate in all six interior target slots at step 4 / radius 256 / `MaxCells=8,000,000`. Hard gates are non-vacuous, largest air share ≥ **0.50**, and exact unsnapped arrival→departure connectivity; selection score is floor-area fraction + clearance tie-break + projected-surface tie-break. It asserts roll/manifest determinism, density sign, zero `WorldRadiusVoxels`, bit-identical metric reruns, and writes one alphabetized card per archetype to `Saved/VoxelForge/Showcase/index.html`, with step-1 radius-64 filled/contour plan + vertical ROI images centred on `LargestComponentPoint`. The final production-height run uses **H=8 chunks / 64 m**, emits the full voxel→metre audit, and produced **7 fine renders, 1 refusal, 1 blank**; its hard-gate survivor set was **0/8**. | | `VoxelForgeComposerSeasonTest.cpp` | `VoxelForge.Composer.Season` | Existing compose/review/393,216-sample round trip plus schema hash tamper rejection, two independent runtime manager/generator instances, season-authoritative seed/spine/radius, unset-season regression, and brute-force verification of every sampled non-Mixed recipe tile verdict (up to 64). | -| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm run uses **16 candidates per arm** (256×3 and 64×3 were stopped before aggregate output for runtime), step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **13/16, 7/16, 7/16** survival; survivor walkable means **0.066071, 0.012566, 0.012851** and feature-scale means **92.307693, 64.571426, 31.428572**. Box checks: **196/260,876**, **310/412,610**, **262/348,722** proved/voxels, **0 violations** in every arm. | +| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm world-scale run uses **16 candidates per arm**, step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **10/16, 9/16, 8/16** survival; survivor feature-scale ranges are **8..268**, **8..60**, **8..92** voxels. Box checks: **185/246,235**, **260/346,060**, **299/397,969** proved/voxels, **0 violations** in every arm; constraint parameter and box-stop violations were also 0. | | `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. | | `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check every generated inter-strate passage whose destination query answers: a 16-point ring outside the mouth's carve/blend band has at least half its samples in destination air, and the endpoint matches the pure open-point result within the mouth's float envelope. This is a connectivity proxy, not a flood-fill proof. Reports checked passages and false/unanswerable archetypes; fails if it inspects zero passages. | -| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test fits the measurement AABB to each arrival/departure mouth pair, reports exact before/after seed-6 cell counts, reruns every negative with doubled margin, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts axis landings, checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. | +| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test uses the bounded 4-chunk density fixture, fits each measurement AABB to the arrival/departure mouth pair plus three shaft spacings (the prescribed local tree fallback), and caps the fitted control at 120,000,000 cells. It reports exact before/after seed-6 cell counts, reruns every negative with the fitted margin doubled, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts axis landings, checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. | | `VoxelForgeWorldEdgeSealTest.cpp` | `VoxelForge.Generation.WorldEdgeSeal` | Positive samples outside the radius across every fixture strate, bit-identical interior negative control against radius 0, monotonic smooth-ramp check, radius-0 no-op, radial `ClassifyBox`/T1.d proofs brute-forced for every reported voxel, and synthetic plus generated near-rim passage coverage. Emits one aggregate summary with proved-box and voxel counts. | ## 4. The density pipeline (most-edited hot path) diff --git a/COMPOSER-NOTES.md b/COMPOSER-NOTES.md index e481edb..cb5c663 100644 --- a/COMPOSER-NOTES.md +++ b/COMPOSER-NOTES.md @@ -381,6 +381,44 @@ small commandlet walking the struct by reflection. Clamp coverage is partial. A tunables at all (`StrateTopWorldZ` / `StrateBottomWorldZ` are runtime Z bounds) and need an explicit exclusion list. Bools cannot be blended — roll them by probability or inherit from the dominant parent. +#### Human-scale audit and the killed tunnel-only hypothesis (2026-09-05) + +The generation defaults are now derived from the body reference rather than chosen as voxel counts: +**1 voxel = 0.25 m**, the player capsule is **1.36 voxels / 0.34 m radius** and **7.04 voxels / +1.76 m tall**, and the default strate is **8 chunks / 256 voxels / 64 m**. The complete generated +`name | voxels | METRES | body/level meaning` table is emitted by `VoxelForge.Composer.Showcase` and +covers each archetype plus the shared passage/disturbance and optional transport fields. + +The requested maze test was run first on the same seed/index and a mouth-fitted step-1 ROI. The +radius is the tunnel radius; the bore is twice the radius: + +| Corridor radius | Bore | Player-fit fraction | Traversable share | Minimum clearance | Arrival→departure | +|----------------:|-----:|--------------------:|------------------:|------------------:|-------------------| +| 4 vox / 1.00 m | 2.00 m | 0.009007 | 0.078358 | 1 voxel / 0.25 m | NotConnected | +| 6 vox / 1.50 m | 3.00 m | 0.014545 | 0.165932 | 1 voxel / 0.25 m | NotConnected | +| 8 vox / 2.00 m | 4.00 m | 0.012156 | 0.126354 | 1 voxel / 0.25 m | NotConnected | +| 10 vox / 2.50 m | 5.00 m | 0.009814 | 0.105611 | 1 voxel / 0.25 m | NotConnected | +| 12 vox / 3.00 m | 6.00 m | 0.008697 | 0.185039 | 1 voxel / 0.25 m | NotConnected | + +The hypothesis is **killed as a single root cause**. There is no sharp or monotonic fit rise, and +the law stays disconnected at every bore. The unchanged one-voxel minimum also shows that the +failure is not explained by the nominal tunnel diameter alone: the generated field, route mouths, +floor/cap interaction, and/or resolution still need separate diagnosis. + +The new defaults use separate derivations: rooms are 8-20 m diameters with a 24 m origin hub and +32 m cells; round passages are 3-4 m bores; Maze is a 16 m cell with a 4 m bore; slabs reserve a +22.4 m floor-to-ceiling span; shaft diameters are 4-7 m with 3 m connectors; islands are 12-24 m +diameters on 28 m cells; ordinary passages are 3-4 m bores and reach 8-24 m. `RoomSpacing` keeps +the largest room 6 m radially clear of the next cell, `CorridorRadius` is 8/32 = 0.25 of Maze half-cell, +and `StrateHeightInChunks` grows from 4 to 8 because 17 m tall rooms, 20 m terrain relief, +vertical shafts, and a high cap do not share a comfortable 32 m envelope. No global multiplier was +used. The roughness reach is `1.875 × roughness` voxels: the new default ratios are +**0.625** (minimum tunnel), **0.469** (Maze), **0.469/0.625** (shaft/connector), **0.234** island +radius (**0.484** including blend), **0.625/0.938** slab column for floor/ceiling, and **0.047** +against SurfaceWorld's 80-voxel relief scale. + +The architecture sanity check uses [Epic's Unreal level-blockout guidance](https://dev.epicgames.com/documentation/en-us/unreal-engine/designer-01-project-setup-and-level-blockout-in-unreal-engine) (player-sized reference, 2-3 m halls and 3-4 m heights as starting guidance), the [2010 ADA Standards](https://www.ada.gov/law-and-regs/design-standards/2010-stds/) (915 mm clear walking width and larger passing/turning spaces), and [NPS Lehman Cave dimensions](https://www.nps.gov/grba/learn/nature/lehman-caves-dimensions.htm) (real chambers ranging from passages to 20 m-plus vertical volumes). These are sanity anchors, not claims that one universal ratio makes a good cave. + ### ✅ Tier 5 status — promotion is built and measured (2026-09-05) Promotion is now a real corpus input, not a claim about a future approval UI. A surviving editor @@ -817,9 +855,9 @@ pass, to: `Saved/ComposerPreview/structure_seed_0_corpus_9dbcea85_step_4_radius_256_fine_step_1_fine_radius_64/` -The fine pass is explicit and bounded: the current composer selects the 42 coarse survivors, samples -step 1 in a radius-64 (128×128 XY) ROI, and refuses before allocation when `MaxCells=2,000,000` would be -exceeded. The default is radius 64 because it stays around the observed 1.8M-cell budget while exposing +The fine pass is explicit and bounded: the historical composer selected the 42 coarse survivors, sampled +step 1 in a radius-64 (128×128 XY) ROI, and refused before allocation when `MaxCells=2,000,000` would be +exceeded. The default is radius 64 because it stays around the observed multi-million-cell budget while exposing voxel-scale detail; radius 128 is already roughly 7.1–7.5M cells and radius 256 exceeds the 8M coarse cap. The public settings let another editor/automation caller choose step, radius, centre, margin, and cap, or the writer can be called for an explicitly selected list. Fine filenames, captions, and each @@ -859,8 +897,10 @@ the editor automation test VoxelForge.Composer.Showcase; opening it is offline a generation hook. It contains one alphabetized card for each native archetype, with the exact four PIE hand-off values, recipe, coarse step-4 filled/contour XZ+XY views, and a separate step-1 filled/contour ROI centred on that card's coarse LargestComponentPoint. The fine ROI is bounded at radius 64 and -MaxCells=2,000,000; a refusal or blank ROI is written as text rather than represented by an empty -image. All eight cards in the final run rendered both fine views; refusals **0**, blank cards **0**. +MaxCells=4,000,000; a refusal or blank ROI is written as text rather than represented by an empty +image. The earlier bounded four-chunk run rendered all eight fine views; the final production-height +run is recorded below because its larger window produced **7** fine renders, **1** refusal, and **1** +blank diagnostic card. The Part A table adds three measurements to the original volumetric walkability ratio: @@ -881,7 +921,9 @@ those survivors the deterministic score is floor_area + 0.25*clamp(clearance/64, The fixture and PIE hand-off use the project generator seed **0**, bComposerRollStructure=false, and leave the existing slot layout, passage endpoints, placement, and WorldRadiusVoxels=0 intact. -Selected-card measurements (each card prints its own exact derived-margin Z window; common XY window +The following is the **pre-scale baseline** captured before the 2026-09-05 human-scale defaults +(the post-scale diagnostic table is below). Each card prints its own exact derived-margin Z window; +common XY window is [-256,256) × [-256,256) voxels, step **4**, HeadroomCells=2, MaxCells=8,000,000): | Archetype | walkable fraction | floor area fraction | median clearance | largest surface share | @@ -921,6 +963,126 @@ cave design (https://store.steampowered.com/news/posts/?appgroupname=Deep+Rock+Galactic&appids=548430&enddate=1729500950&feed=steam_community_announcements), and Team Cherry map design (https://www.pcgamer.com/how-to-design-a-great-metroidvania-map/). +#### Final post-scale measurement (2026-09-05) + +The new defaults are applied in `VoxelStrateTypes.h`; the production definition, season defaults, and +the owner-facing showcase use `StrateHeightInChunks=8` (64 m). The bounded test fixture remains at +four chunks so the broad regression suite stays finite; this is a test-budget distinction, not a +second production default. The final focused run is +`Saved/AutomationReports/WorldScaleShowcaseProductionFinal/index.json`, with 256 roll evaluations, +82 eligible single-region evaluations, 174 lateral-region evaluations skipped while that gate is off, +and a **0/8** complete hard-gate survivor set. It still wrote fresh diagnostic cards: 7 fine renders, +1 refusal, and 1 blank card. + +The before/after values below are deliberately labelled as candidate measurements, not paired +experiments: the before rows are the selected pre-scale candidates from the baseline report, while +the after rows are the best measured post-scale candidates from the production-height search. Floor +area is the coarse 128 m × 128 m projected-footprint proxy (16,384 m² maximum); the ratio is the +candidate's reported roughness-to-feature ratio. A zero or unresolved player law is a result, not a +substituted value. + +| Archetype | Before: fit / traversable / law | Before: floor m² / median clearance m / roughness ratio | After: fit / traversable / law | After: floor m² / median clearance m / roughness ratio | +|---|---|---|---|---| +| CrystalChamber | 0.011745 / 0.810378 / NotConnectedAtThisResolution | 16,382 / 17 / 1.813 floor, 1.796 ceiling | 0.002699 / 0.017288 / NotConnectedAtThisResolution | 16,384 / 35 / 0.847 | +| FlatPlain | 0.003938 / 0.053571 / NotConnectedAtThisResolution | 16,382 / 18 / 1.840 floor, 2.308 ceiling | 0.000000 / 0.000000 / unresolved | 16,343 / 43 / 1.092 | +| FloatingIslands | 0.000000 / 0.000000 / StartCellNotPlayerFit | 6,556 / 9 / 0.025 | 0.000000 / 0.000000 / StartCellNotPlayerFit | 4,577 / 29 / 0.283 | +| Maze | 0.010523 / 0.136646 / NotConnectedAtThisResolution | 4,759 / 2 / 0.938 | 0.016851 / 0.058757 / NotConnectedAtThisResolution | 6,115 / 3 / 0.423 | +| SurfaceWorld | 0.001027 / 0.029412 / NotConnectedAtThisResolution | 15,359 / 16 / 0.032 | 0.000627 / 0.008415 / NotConnectedAtThisResolution | 16,384 / 42 / 0.038 | +| TunnelNetwork | 0.004439 / 0.145833 / NotConnectedAtThisResolution | 3,414 / 5 / 1.436 | 0.001197 / 0.612245 / NotConnectedAtThisResolution | 6,153 / 9 / 0.383 | +| Underwater | 0.005251 / 0.160000 / NotConnectedAtThisResolution | 3,376 / 3 / 2.212 | 0.000000 / 0.000000 / OutOfWindow | 21 / 17 / 0.683 | +| VerticalShafts | 0.007458 / 0.233577 / NotConnectedAtThisResolution | 1,864 / 3 / 0.731 shaft, 1.302 connector | 0.009883 / 0.083045 / NotConnectedAtThisResolution | 2,718 / 4 / 0.622 | + +The interpreted outcome is mixed. Nominal dimensions and median clearances grew where they should, +and TunnelNetwork's best measured candidate's traversable share rose substantially, but the global law +did not become connected and broad rooms can still have very low player-fit fractions. Maze improved +only modestly and remains disconnected. This is consistent with the radius sweep being a killed +single-cause hypothesis: width alone did not cure the route/mouth/resolution failures. + +The direct default roughness arithmetic is stable and independent of which rolled candidate won: + +| Archetype / feature | Arithmetic | Ratio | +|---|---|---:| +| TunnelNetwork, Underwater tunnel | reach `= 2 × 1.25 × 1.5 = 3.75 vox`; `3.75 / 6` minimum radius | 0.625 | +| Maze corridor | `3.75 / 8` | 0.469 | +| FlatPlain, CrystalChamber floor / ceiling columns | `7.50 / 12` and `11.25 / 12` using mean 12-voxel column radius | 0.625 / 0.938 | +| VerticalShafts shaft / connector | `3.75 / 8` and `3.75 / 6` | 0.469 / 0.625 | +| FloatingIslands | `5.625 / 24`; including 6-voxel SDF blend, `(5.625+6)/24` | 0.234 / 0.484 | +| SurfaceWorld relief | `3.75 / 80` terrain-relief scale | 0.047 | + +The default derivations are not a global multiplier: + +* Body and target anchors: `1 voxel = 0.25 m`; the capsule is `1.36 vox` radius and `7.04 vox` + tall. A 2.5 m no-duck corridor is 10 voxels; the 3-4 m fight target is 12-16 voxels. An 8-15 m + room is 32-60 voxels across, and a 20 m cathedral anchor is 80 voxels across. Epic's official + Unreal blockout guidance uses a player-sized reference and gives roughly 2-3 m halls and 3-4 m + heights as starting guidance; the [2010 ADA Standards](https://www.ada.gov/law-and-regs/design-standards/2010-stds/) + provide a 915 mm accessible clear-width lower bound, not a combat-space target. +* TunnelNetwork/Underwater: room radii `16..40 vox = 4..10 m`, so diameters are 8-20 m; the + origin radius `48 vox = 12 m` gives a 24 m, three-chunk hub. `RoomSpacing=128 vox = 32 m` leaves + `64-40=24 vox = 6 m` radial margin around the largest room. `TunnelMin/MaxRadius=6..8` gives + 3-4 m bores. `RoomHeightRatio=0.85` makes the largest room's vertical diameter + `2×40×0.85×0.25=17 m`; `H=8×32=256 vox=64 m` leaves enough vertical budget for seals, + passages, and a high room. `MaxTunnelLength=360 vox=90 m` exceeds the worst jittered neighbour + distance with margin. +* FlatPlain/CrystalChamber: floor `0.25×256=64 vox=16 m`, ceiling `0.60×256=153.6 vox=38.4 m`, + leaving `22.4 m` before roughness. Columns are `8..16 vox` radii (4-8 m diameters) on 96-voxel + (24 m) cells. The two names intentionally retain the same slab generator and parameter block. +* Maze: `CellSize=64 vox=16 m`; `CorridorRadius=8 vox=2 m`, hence a 4 m bore; the corridor radius + is `8/32=0.25` of the half-cell, leaving a 6 m centre-to-wall radial margin before other geometry. +* SurfaceWorld: `ElevationRange=80 vox=20 m`, base ground `0.25×256=16 m`, warp `48 vox=12 m`, + sky cap `0.95×256=243.2 vox=60.8 m`; terrace/layer/beach/overhang values are separately + 2 m / 2 m / 3 m / 4 m reach + 6 m height optional features. +* VerticalShafts: spacing `80 vox=20 m`, shaft radii `8..14 vox` (4-7 m diameters), connector + radius `6 vox` (3 m bore), ledge spacing `32 vox=8 m`, ledge depth `4 vox=1 m`. +* FloatingIslands: spacing `112 vox=28 m`, radii `24..48 vox` (12-24 m diameters); the largest + leaves `56-48=8 vox=2 m` centre-to-edge radial margin. Thickness is independent: + `48×0.60=28.8 vox=7.2 m` below the centre and `48×0.20=9.6 vox=2.4 m` above it. +* Shared passages and disturbances: mouth/mid radii `8/6 vox` (4/3 m bores), reach `32..96 vox` + (8-24 m), spine distance `64..192 vox` (16-48 m), wander/spiral `24 vox=6 m`, cascade ledge + `8 vox=2 m`; chasm spacing/radius `192/24 vox` (48 m / 6 m), bridge spacing/radius `128/8` + (32 m / 2 m), and ridge spacing/height/thickness `128/32/8` (32 m / 8 m / 2 m). Optional + transport fields are also audited: arch radius `6..12` (1.5-3 m), column `4..8` (1-2 m), pit + `8..16` plus depth `32` (2-4 m plus 8 m), chimney `4..8` plus height `32` (1-2 m plus 8 m), + dome `16..32` (4-8 m), pinch length `24` (6 m), overhang depth `8` (2 m), and the inactive + layer/rib depths `0.3/0.4` (7.5/10 cm). The exact generated row-by-row audit includes nominal + periods for every frequency field. + +The 64 m height is deliberate rather than accidental: the GDD boss-space anchor is approximately +3-4 chunks (24-32 m), so the extra vertical envelope is needed to combine that room scale with +seals and vertical traversal. The NPS [Lehman Cave dimensions](https://www.nps.gov/grba/learn/nature/lehman-caves-dimensions.htm) +also support heterogeneous natural volumes rather than one fixed room ratio. + +#### Equivalence, box safety, and cost after the default move + +The full report `Saved/AutomationReports/WorldScaleFullFinal3/index.json` recorded 28 successes, +2 warning-only tests, 0 failures, and 0 not-run tests in 1,704.010 s. The warnings are known scope +findings: lateral regions remain gated at 9/16 cross-region law, and TunnelNetwork op-stack +sampling has only 240/6,000 samples outside its gate. Neither is a box-soundness violation. + +All legacy/stack equivalence tests remained bit-identical to their shared reference paths, including +Maze, both slab archetypes, SurfaceWorld, TunnelNetwork stage A+B, VerticalShafts, and FloatingIslands. +`WorldRadiusVoxels` remained 0, lateral regions stayed gated, density sign remained correct, and every +box brute-force audit reported **0 violations**. Current representative box counts are: ParameterRoll +1,149 proved / 1,529,319 lattice voxels; StructureRoll 715 / 951,665; corpus-free arms 185 / 246,235, +260 / 346,060, and 299 / 397,969; TunnelNetwork production op-stack 7/40 proved (7 AllSolid), +Underwater 34/40, Maze 127/256 plus 250/256 novel lattice boxes, FlatPlain 27/60, CrystalChamber +39/60, tuned CrystalChamber 29/60, VerticalShafts 31/60, and FloatingIslands 15/60 AllAir. +Every listed count has zero brute-force violations. + +The pristine pre-scale player-fit showcase did not execute the op-stack classification suite, so a +pure old-default tile-count delta does not exist. The nearest instrumented pre-final checkpoint had +the operator-stack 600-tile scan at **521 Mixed / 23 AllSolid / 56 AllAir**; the final world-scale +scan is **518 / 71 / 11**. Uniform proofs therefore did not collapse (79 → 82), although the +AllAir/AllSolid mix moved. The final non-stack scan is **522 / 22 / 56**. This is a real measured +classification result, but it must not be misrepresented as a pristine causal A/B run. The +production-height showcase itself cost about **1,709.7 s** and produced 7 fine renders, one refusal, +and one blank diagnostic card; its artifact is `Saved/VoxelForge/Showcase/index.html`. + +The corrected owner-facing artifact is therefore at `Saved/VoxelForge/Showcase/index.html` (fresh +report: `Saved/AutomationReports/WorldScaleShowcaseProductionFinal/index.json`). It contains the new +metre audit and the rejected-but-useful diagnostic cards; the zero hard-gate survivor set is +intentionally visible rather than hidden by a fallback candidate. + ### 3.5 Validation in three layers, and how "good" is ever judged 1. **Construct by design** so the law cannot break — build the connections FIRST and generate around @@ -1092,8 +1254,10 @@ a passage and arrive in a sealed pocket.** `SuggestLandingPoint(DesiredX, DesiredY, MaxLateralSnap) -> optional FVector`. Every source knows where its own air is: `FRoomGraphSource` keeps the nearest room's vertical placement at the requested XY, `MakeSlabVoidSource` answers in the void band, `FIslandBlobSource` finds a blob top, and the lattice/ -shaft sources search their placement grid. Sparse sources may move laterally, but only within one -source spacing/cell; if that bounded search has no footing, falling back is the honest answer. +shaft sources search their placement grid. Sparse sources may move laterally, but only within a +bounded local source neighbourhood (one cell for Maze, two shaft spacings at the default +VerticalShafts density, one spacing for islands); if that search has no footing, falling back is the +honest answer. Passage placement remains deterministic and cheap, and the slanted control-point chain is bounded too. Then the measurement pass flood fill is the net: does the upper mouth's air component reach the lower diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp index 3df93a9..3cefe83 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp @@ -656,12 +656,12 @@ bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters) const double MeasurementSeconds = FPlatformTime::Seconds() - MeasurementStartSeconds; // Audit the same declared relations against the 12 current authored corpus vectors. Project - // assets carry their own height; the eight C++ defaults use the fixture's 4-chunk height. + // assets carry their own height; the eight C++ defaults use the fixture's 8-chunk height. int32 AuthoredConstraintViolations = 0; FString AuthoredViolationList; for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries()) { - float EntryHeight = 4.0f * (float)CHUNK_SIZE; + float EntryHeight = 8.0f * (float)CHUNK_SIZE; if (!Entry.SourcePath.Contains(TEXT("/VoxelForge/ComposerDefaults/"))) { UVoxelStrateDefinition* Definition = LoadObject( diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerSeasonTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerSeasonTest.cpp index 42a4236..2206689 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeComposerSeasonTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerSeasonTest.cpp @@ -49,7 +49,7 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters) Settings.Corpus = &Corpus; Settings.CandidateCount = 24; Settings.SelectedStrateCount = 6; - Settings.StrateHeightInChunks = 4; + Settings.StrateHeightInChunks = 8; Settings.InterStrateGapChunks = 0; Settings.OriginSpineRadius = 14.0f; Settings.WorldRadiusVoxels = 0.0f; diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp index aa1c49c..be7f2a6 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp @@ -27,7 +27,7 @@ namespace constexpr float ClearanceNormalizationVoxels = 64.0f; constexpr int32 FinePreviewSampleStep = 1; constexpr int32 FinePreviewRadiusVoxels = 64; - constexpr int32 FinePreviewMaxCells = 2000000; + constexpr int32 FinePreviewMaxCells = 4000000; const ECaveGeneratorType GShowcaseArchetypes[] = { ECaveGeneratorType::CrystalChamber, @@ -125,23 +125,48 @@ namespace int32 EvaluatedCandidates = 0; TArray Survivors; TOptional Best; + // Best coarse-measured candidate, retained even when a hard gate rejects it. This keeps + // the scale audit useful when the player-fit law correctly finds no survivor. + TOptional BestMeasured; + int32 MeasuredCandidates = 0; FString LastFailure; }; + bool VF_IsBetterShowcaseSample(const FShowcaseSample& Sample, + const TOptional& Current) + { + return !Current.IsSet() + || Sample.SelectionScore > Current->SelectionScore + || (Sample.SelectionScore == Current->SelectionScore + && (Sample.Candidate.Seed < Current->Candidate.Seed + || (Sample.Candidate.Seed == Current->Candidate.Seed + && Sample.Candidate.Index < Current->Candidate.Index))); + } + void VF_AddMetricSample(FShowcaseStats& Stats, const FShowcaseSample& Sample) { Stats.Survivors.Add(Sample); - if (!Stats.Best.IsSet() - || Sample.SelectionScore > Stats.Best->SelectionScore - || (Sample.SelectionScore == Stats.Best->SelectionScore - && (Sample.Candidate.Seed < Stats.Best->Candidate.Seed - || (Sample.Candidate.Seed == Stats.Best->Candidate.Seed - && Sample.Candidate.Index < Stats.Best->Candidate.Index)))) + if (VF_IsBetterShowcaseSample(Sample, Stats.Best)) { Stats.Best = Sample; } } + void VF_AddMeasuredSample(FShowcaseStats& Stats, const FShowcaseSample& Sample) + { + // NumSampled is the direct evidence that the coarse grid was built. Keep this diagnostic + // path independent of the legacy gate's bool return and of any future metric-validity + // policy changes: a rejected candidate can still be the most useful scale evidence. + if (Sample.Candidate.bValid && Sample.Metrics.NumSampled > 0) + { + ++Stats.MeasuredCandidates; + if (VF_IsBetterShowcaseSample(Sample, Stats.BestMeasured)) + { + Stats.BestMeasured = Sample; + } + } + } + bool VF_BuildShowcaseStack( const VoxelForgeTest::FTestWorld& World, const FVoxelStrateComposerCandidate& Candidate, @@ -569,6 +594,396 @@ namespace return Report; } + FString VF_FormatWorldScaleAudit() + { + constexpr float VoxelMetres = 0.25f; + constexpr float StrateHeightVoxels = 8.0f * static_cast(CHUNK_SIZE); + FString Report = TEXT( + "WORLD SCALE AUDIT (all distances are authored in voxels; METRES = voxels * 0.25; " + "player radius=1.36 vox/0.34 m, height=7.04 vox/1.76 m; frequency rows show nominal " + "period=1/frequency)\n" + "archetype | name | voxels | METRES | body/level meaning\n"); + auto Add = [&Report](const TCHAR* Archetype, const TCHAR* Name, + float Voxels, const TCHAR* Meaning) + { + Report += FString::Printf( + TEXT("%s | %s | %.2f | %.2f | %s\n"), Archetype, Name, + Voxels, Voxels * 0.25f, Meaning); + }; + auto AddRange = [&Report](const TCHAR* Archetype, const TCHAR* Name, + float MinVoxels, float MaxVoxels, + const TCHAR* Meaning) + { + Report += FString::Printf( + TEXT("%s | %s | %.2f..%.2f | %.2f..%.2f | %s\n"), Archetype, Name, + MinVoxels, MaxVoxels, MinVoxels * 0.25f, MaxVoxels * 0.25f, Meaning); + }; + const TCHAR* TunnelNames[] = { TEXT("TunnelNetwork"), TEXT("Underwater") }; + for (const TCHAR* Archetype : TunnelNames) + { + Add(Archetype, TEXT("StrateHeight"), StrateHeightVoxels, + TEXT("8 chunks: a 64 m vertical envelope for tall rooms and passages")); + Add(Archetype, TEXT("BoundarySealThickness"), 4.0f, + TEXT("1 m solid geological boundary; not player space")); + Add(Archetype, TEXT("WormNetworkRange"), 64.0f, + TEXT("16 m side-passage reach around the room/tunnel skeleton")); + Add(Archetype, TEXT("WormFrequency (nominal period)"), 1.0f / 0.015f, + TEXT("16.7 m noise period: about two chunks for broad bends")); + Add(Archetype, TEXT("RoomSpacing"), 128.0f, + TEXT("32 m room-cell spacing; leaves a 6 m radial margin around a 20 m room")); + AddRange(Archetype, TEXT("RoomRadius"), 16.0f, 40.0f, + TEXT("8 m minimum room to 20 m large chamber diameter")); + Add(Archetype, TEXT("OriginRoomRadius"), 48.0f, + TEXT("24 m hub diameter: a three-chunk boss-space anchor")); + AddRange(Archetype, TEXT("TunnelRadius"), 6.0f, 8.0f, + TEXT("3-4 m bores: body height plus generous headroom and fight width")); + Add(Archetype, TEXT("MaxTunnelLength"), 360.0f, + TEXT("90 m: reaches the worst 32 m cell-jitter neighbour with margin")); + Add(Archetype, TEXT("TunnelWarpStrength"), 24.0f, + TEXT("6 m lateral bend, visibly organic but smaller than the 90 m reach")); + Add(Archetype, TEXT("SDFBlendRadius"), 4.0f, + TEXT("1 m junction rounding, below the 1.5 m minimum tunnel radius")); + Add(Archetype, TEXT("CaveWarpStrength"), 16.0f, + TEXT("4 m skeleton distortion")); + Add(Archetype, TEXT("SurfaceRoughness"), 2.0f, + TEXT("0.5 m wall texture; bounded reach is 0.94 m")); + Add(Archetype, TEXT("FloorReliefStrength"), 4.0f, + TEXT("1 m floor undulation, small relative to a 3 m bore")); + Add(Archetype, TEXT("FloorReliefFrequency (nominal period)"), 1.0f / 0.015f, + TEXT("16.7 m floor-undulation period: broad enough to walk as a slope")); + Add(Archetype, TEXT("RoughnessFrequency (nominal period)"), 1.0f / 0.1f, + TEXT("2.5 m wall-detail period: a few body lengths, not voxel-grain tunnel")); + Add(Archetype, TEXT("CaveWarpFrequency (nominal period)"), 1.0f / 0.015f, + TEXT("16.7 m skeleton-warp period: broad landform-scale bend")); + Add(Archetype, TEXT("DomainWarpFrequency (nominal period)"), 1.0f / 0.03f, + TEXT("8.3 m optional roughness-warp period")); + Add(Archetype, TEXT("CliffSampleDist (transport)"), 2.0f, + TEXT("0.5 m optional local slope sample distance")); + Add(Archetype, TEXT("FloorBias"), 4.0f, + TEXT("1 m floor stabilisation applied below room centre")); + Add(Archetype, TEXT("DomainWarpStrength"), 0.0f, + TEXT("0 m optional roughness-coordinate warp; disabled by default")); + Add(Archetype, TEXT("OverhangFrequency (nominal period)"), 1.0f / 0.06f, + TEXT("4.2 m optional shelf-shape period")); + Add(Archetype, TEXT("ScallopFrequency (nominal period)"), 1.0f / 0.1f, + TEXT("2.5 m optional erosion-bowl period")); + Add(Archetype, TEXT("TerraceStepHeight (transport)"), 0.0f, + TEXT("0 m optional step; disabled by default")); + Add(Archetype, TEXT("LayerLineSpacing (transport)"), 0.0f, + TEXT("0 m optional geological band period; disabled by default")); + Add(Archetype, TEXT("LayerLineDepth (transport)"), 0.3f, + TEXT("0.075 m optional groove depth; inactive while spacing is zero")); + Add(Archetype, TEXT("RibbingSpacing (transport)"), 0.0f, + TEXT("0 m optional rib period; disabled by default")); + Add(Archetype, TEXT("RibbingDepth (transport)"), 0.4f, + TEXT("0.1 m optional rib protrusion; inactive while spacing is zero")); + Add(Archetype, TEXT("OverhangDepth (transport)"), 8.0f, + TEXT("2 m optional shelf; terrain-op transport is reset when disabled")); + AddRange(Archetype, TEXT("ArchRadius (transport)"), 6.0f, 12.0f, + TEXT("1.5-3 m optional arch feature")); + AddRange(Archetype, TEXT("ColumnRadius (transport)"), 4.0f, 8.0f, + TEXT("1-2 m optional pillar radius")); + AddRange(Archetype, TEXT("PitRadius (transport)"), 8.0f, 16.0f, + TEXT("2-4 m optional vertical opening radius")); + Add(Archetype, TEXT("PitDepth (transport)"), 32.0f, + TEXT("8 m optional vertical drop")); + AddRange(Archetype, TEXT("ChimneyRadius (transport)"), 4.0f, 8.0f, + TEXT("1-2 m optional upward shaft radius")); + Add(Archetype, TEXT("ChimneyHeight (transport)"), 32.0f, + TEXT("8 m optional upward shaft height")); + AddRange(Archetype, TEXT("DomeRadius (transport)"), 16.0f, 32.0f, + TEXT("4-8 m optional ceiling dome radius")); + Add(Archetype, TEXT("PinchLength (transport)"), 24.0f, + TEXT("6 m optional bottleneck length; default operation is disabled")); + } + + for (const TCHAR* Archetype : { TEXT("FlatPlain"), TEXT("CrystalChamber") }) + { + Add(Archetype, TEXT("StrateHeight"), StrateHeightVoxels, + TEXT("8 chunks / 64 m: open vertical chamber envelope")); + Add(Archetype, TEXT("Floor height"), StrateHeightVoxels * 0.25f, + TEXT("16 m above the strate bottom")); + Add(Archetype, TEXT("Ceiling height"), StrateHeightVoxels * 0.60f, + TEXT("38.4 m above the bottom; 22.4 m clear span before roughness")); + Add(Archetype, TEXT("FloorRoughness"), 4.0f, + TEXT("1 m rolling floor displacement")); + Add(Archetype, TEXT("CeilingRoughness"), 6.0f, + TEXT("1.5 m downward formation reach")); + Add(Archetype, TEXT("FloorRoughnessFrequency (nominal period)"), 1.0f / 0.04f, + TEXT("6.25 m floor-detail period: broad rolling ground")); + Add(Archetype, TEXT("CeilingRoughnessFrequency (nominal period)"), 1.0f / 0.04f, + TEXT("6.25 m ceiling-formation period")); + AddRange(Archetype, TEXT("ColumnRadius"), 8.0f, 16.0f, + TEXT("4-8 m pillar diameters; large enough to read without erasing a fight lane")); + Add(Archetype, TEXT("ColumnSpacing"), 96.0f, + TEXT("24 m pillar-cell spacing")); + Add(Archetype, TEXT("BoundarySealThickness"), 4.0f, + TEXT("1 m solid boundary")); + } + + Add(TEXT("Common layout"), TEXT("OriginSpineRadius"), 14.0f, + TEXT("3.5 m radius / 7 m diameter central descent spine")); + Add(TEXT("Common layout"), TEXT("InterStrateGap"), 0.0f, + TEXT("0 m bedrock gap by default; configured as whole 8 m chunks")); + Add(TEXT("Common layout"), TEXT("WorldRadiusVoxels"), 0.0f, + TEXT("0 m lateral bound: radial regions remain gated off")); + + Add(TEXT("Maze"), TEXT("StrateHeight"), StrateHeightVoxels, + TEXT("8 chunks / 64 m: supports several 16 m maze levels")); + Add(TEXT("Maze"), TEXT("CellSize"), 64.0f, + TEXT("16 m lattice cells, matching ordinary fight-space scale")); + Add(TEXT("Maze"), TEXT("CorridorRadius"), 8.0f, + TEXT("2 m radius / 4 m circular bore: upper fight-space target")); + Add(TEXT("Maze"), TEXT("SurfaceRoughness"), 2.0f, + TEXT("0.5 m texture; bounded reach is 0.94 m, below the 2 m radius")); + Add(TEXT("Maze"), TEXT("BoundarySealThickness"), 4.0f, + TEXT("1 m solid boundary")); + + Add(TEXT("SurfaceWorld"), TEXT("StrateHeight"), StrateHeightVoxels, + TEXT("8 chunks / 64 m: high sky-cap envelope")); + Add(TEXT("SurfaceWorld"), TEXT("ElevationRange"), 80.0f, + TEXT("20 m terrain relief: human-scale hills, not a miniature room")); + Add(TEXT("SurfaceWorld"), TEXT("Base ground height"), StrateHeightVoxels * 0.25f, + TEXT("16 m mean ground elevation above the strate bottom")); + Add(TEXT("SurfaceWorld"), TEXT("HeightWarpStrength"), 48.0f, + TEXT("12 m horizontal landform distortion")); + Add(TEXT("SurfaceWorld"), TEXT("SurfaceRoughness"), 2.0f, + TEXT("0.5 m ground detail; bounded reach is 0.94 m")); + Add(TEXT("SurfaceWorld"), TEXT("ContinentFrequency (nominal period)"), 1.0f / 0.006f, + TEXT("41.7 m broad landmass period")); + Add(TEXT("SurfaceWorld"), TEXT("MountainFrequency (nominal period)"), 1.0f / 0.012f, + TEXT("20.8 m ridge period")); + Add(TEXT("SurfaceWorld"), TEXT("DetailFrequency (nominal period)"), 1.0f / 0.04f, + TEXT("6.25 m fine terrain period")); + Add(TEXT("SurfaceWorld"), TEXT("TerraceHeight"), 8.0f, + TEXT("2 m optional step; operation remains disabled by strength 0")); + Add(TEXT("SurfaceWorld"), TEXT("LayerLineSpacing"), 8.0f, + TEXT("2 m optional geological band spacing")); + Add(TEXT("SurfaceWorld"), TEXT("LayerLineDepth"), 0.0f, + TEXT("0 m default band displacement: optional operation disabled")); + Add(TEXT("SurfaceWorld"), TEXT("CliffSampleDist"), 2.0f, + TEXT("0.5 m slope sample scale for optional cliff operation")); + Add(TEXT("SurfaceWorld"), TEXT("OverhangReach"), 16.0f, + TEXT("4 m optional shelf reach")); + Add(TEXT("SurfaceWorld"), TEXT("OverhangHeight"), 24.0f, + TEXT("6 m optional shelf zone")); + Add(TEXT("SurfaceWorld"), TEXT("BeachWidth"), 12.0f, + TEXT("3 m shore band")); + Add(TEXT("SurfaceWorld"), TEXT("Sky cap height"), StrateHeightVoxels * 0.95f, + TEXT("60.8 m above the strate bottom; leaves a high open sky")); + Add(TEXT("SurfaceWorld"), TEXT("CeilingRoughness"), 6.0f, + TEXT("1.5 m optional downward cap detail")); + Add(TEXT("SurfaceWorld"), TEXT("CeilingUndulation"), 0.0f, + TEXT("0 m broad cap swell; disabled by default")); + Add(TEXT("SurfaceWorld"), TEXT("CeilingRidgeStrength"), 0.0f, + TEXT("0 m hanging ridge height; disabled by default")); + Add(TEXT("SurfaceWorld"), TEXT("CeilingWarpStrength"), 0.0f, + TEXT("0 m cap-coordinate warp; disabled by default")); + Add(TEXT("SurfaceWorld"), TEXT("BoundarySealThickness"), 4.0f, + TEXT("1 m solid boundary")); + + Add(TEXT("SurfaceWorld"), TEXT("HeightWarpFrequency (nominal period)"), 1.0f / 0.008f, + TEXT("31.25 m landform-warp period")); + Add(TEXT("SurfaceWorld"), TEXT("ReliefFrequency (nominal period)"), 1.0f / 0.0015f, + TEXT("166.7 m macro-region period")); + Add(TEXT("SurfaceWorld"), TEXT("OverhangFrequency (nominal period)"), 1.0f / 0.02f, + TEXT("12.5 m optional shelf-shape period")); + Add(TEXT("SurfaceWorld"), TEXT("CeilingRoughnessFrequency (nominal period)"), 1.0f / 0.04f, + TEXT("6.25 m optional cap-detail period")); + Add(TEXT("SurfaceWorld"), TEXT("CeilingUndulationFrequency (nominal period)"), 1.0f / 0.004f, + TEXT("62.5 m optional cap-swell period")); + Add(TEXT("SurfaceWorld"), TEXT("CeilingRidgeFrequency (nominal period)"), 1.0f / 0.02f, + TEXT("12.5 m optional hanging-ridge period")); + Add(TEXT("SurfaceWorld"), TEXT("CeilingWarpFrequency (nominal period)"), 1.0f / 0.01f, + TEXT("25 m optional cap-warp period")); + + Add(TEXT("VerticalShafts"), TEXT("StrateHeight"), StrateHeightVoxels, + TEXT("8 chunks / 64 m: a full vertical traversal envelope")); + Add(TEXT("VerticalShafts"), TEXT("ShaftSpacing"), 80.0f, + TEXT("20 m shaft-cell spacing")); + AddRange(TEXT("VerticalShafts"), TEXT("ShaftRadius"), 8.0f, 14.0f, + TEXT("4-7 m shaft diameters")); + Add(TEXT("VerticalShafts"), TEXT("ConnectorRadius"), 6.0f, + TEXT("1.5 m radius / 3 m horizontal connector bore")); + Add(TEXT("VerticalShafts"), TEXT("LedgeSpacing"), 32.0f, + TEXT("8 m vertical landing interval")); + Add(TEXT("VerticalShafts"), TEXT("LedgeDepth"), 4.0f, + TEXT("1 m landing intrusion")); + Add(TEXT("VerticalShafts"), TEXT("SurfaceRoughness"), 2.0f, + TEXT("0.5 m wall texture; bounded reach is 0.94 m")); + Add(TEXT("VerticalShafts"), TEXT("BoundarySealThickness"), 4.0f, + TEXT("1 m solid boundary")); + + Add(TEXT("FloatingIslands"), TEXT("StrateHeight"), StrateHeightVoxels, + TEXT("8 chunks / 64 m: vertical sky volume")); + Add(TEXT("FloatingIslands"), TEXT("IslandSpacing"), 112.0f, + TEXT("28 m island-cell spacing; max 24 m diameter leaves a 2 m centre-to-edge gap")); + AddRange(TEXT("FloatingIslands"), TEXT("IslandRadius"), 24.0f, 48.0f, + TEXT("12-24 m landmass diameters: room to cathedral scale")); + Add(TEXT("FloatingIslands"), TEXT("Underside half-depth"), 48.0f * 0.60f, + TEXT("7.2 m below the centre for the largest island; top half is 2.4 m")); + Add(TEXT("FloatingIslands"), TEXT("SurfaceRoughness"), 3.0f, + TEXT("0.75 m island texture; bounded reach is 1.41 m")); + Add(TEXT("FloatingIslands"), TEXT("SDFBlendRadius"), 6.0f, + TEXT("1.5 m island edge blend")); + Add(TEXT("FloatingIslands"), TEXT("BoundarySealThickness"), 4.0f, + TEXT("1 m solid boundary")); + + Add(TEXT("Common passage"), TEXT("MouthRadius"), 8.0f, + TEXT("2 m radius / 4 m bore at each strate mouth")); + Add(TEXT("Common passage"), TEXT("MidRadius"), 6.0f, + TEXT("1.5 m radius / 3 m minimum bore between mouths")); + AddRange(TEXT("Common passage"), TEXT("Reach"), 32.0f, 96.0f, + TEXT("8-24 m reach into each strate")); + AddRange(TEXT("Common passage"), TEXT("Distance from spine"), 64.0f, 192.0f, + TEXT("16-48 m placement range")); + Add(TEXT("Common passage"), TEXT("Wander"), 24.0f, + TEXT("6 m sideways meander")); + Add(TEXT("Common passage"), TEXT("VerticalWobble"), 0.0f, + TEXT("0 m vertical wobble by default; descent remains controlled")); + Add(TEXT("Common passage"), TEXT("SpiralRadius"), 24.0f, + TEXT("6 m corkscrew radius")); + Add(TEXT("Common passage"), TEXT("CascadeLedge"), 8.0f, + TEXT("2 m ledge run per cascade step")); + + Add(TEXT("Common disturbance"), TEXT("ChasmSpacing"), 192.0f, + TEXT("48 m chasm-cell spacing")); + Add(TEXT("Common disturbance"), TEXT("ChasmRadius"), 24.0f, + TEXT("6 m radius / 12 m opening when enabled")); + Add(TEXT("Common disturbance"), TEXT("BridgeSpacing"), 128.0f, + TEXT("32 m bridge-cell spacing")); + Add(TEXT("Common disturbance"), TEXT("BridgeRadius"), 8.0f, + TEXT("2 m radius / 4 m bridge width")); + Add(TEXT("Common disturbance"), TEXT("RidgeSpacing"), 128.0f, + TEXT("32 m ridge-cell spacing")); + Add(TEXT("Common disturbance"), TEXT("RidgeHeight"), 32.0f, + TEXT("8 m optional blade height")); + Add(TEXT("Common disturbance"), TEXT("RidgeThickness"), 8.0f, + TEXT("2 m optional blade half-width")); + Add(TEXT("Common disturbance"), TEXT("BoundarySealThickness"), 4.0f, + TEXT("1 m solid boundary shared with the active archetype")); + + Report += FString::Printf( + TEXT("Audit anchors: 1 voxel=%.2f m; 1 chunk=8.00 m; 2.5 m walk corridor=10 vox; " + "3-4 m fight corridor=12-16 vox; 8-15 m room=32-60 vox; 20 m cathedral=80 vox.\n"), + VoxelMetres); + return Report; + } + + FString VF_FormatDefaultRoughnessRatios() + { + const float TunnelReach = VF_RoughnessReach(2.0f); + const float SlabFloorReach = VF_RoughnessReach(4.0f); + const float SlabCeilingReach = VF_RoughnessReach(6.0f); + const float MazeReach = VF_RoughnessReach(2.0f); + const float ShaftReach = VF_RoughnessReach(2.0f); + const float IslandReach = VF_RoughnessReach(3.0f); + return FString::Printf( + TEXT("Default roughness-to-feature ratios (reach = roughness * 1.25 * 1.5; " + "all values are dimensionless): TunnelNetwork=%.3f reach/%.1f-voxel " + "minimum tunnel radius (room mean %.3f); Underwater=same; FlatPlain=" + "%.3f floor reach/mean column radius and %.3f ceiling reach/mean column radius; " + "CrystalChamber=same slab engine; Maze=%.3f reach/%.1f-voxel corridor radius; " + "SurfaceWorld=%.3f reach/80-voxel terrain relief; VerticalShafts=%.3f shaft-min " + "and %.3f connector; FloatingIslands=%.3f reach/min island radius " + "(%.3f including 6-voxel blend).\n"), + TunnelReach / 6.0f, 6.0f, TunnelReach / 28.0f, + SlabFloorReach / 12.0f, SlabCeilingReach / 12.0f, + MazeReach / 8.0f, 8.0f, TunnelReach / 80.0f, + ShaftReach / 8.0f, ShaftReach / 6.0f, + IslandReach / 24.0f, (IslandReach + 6.0f) / 24.0f); + } + + float VF_ShowcaseRoughnessFeatureRatio( + const FVoxelStrateComposerCandidate& Candidate) + { + switch (Candidate.Archetype) + { + case ECaveGeneratorType::TunnelNetwork: + case ECaveGeneratorType::Underwater: + return VF_RoughnessReach( + Candidate.ArchetypeParams.TunnelNetworkParams.SurfaceRoughness) + / FMath::Max( + Candidate.ArchetypeParams.TunnelNetworkParams.TunnelMinRadius, + KINDA_SMALL_NUMBER); + case ECaveGeneratorType::FlatPlain: + case ECaveGeneratorType::CrystalChamber: + { + const FSlabGenerationParams& P = Candidate.ArchetypeParams.SlabParams; + const float ColumnRadius = 0.5f * (P.ColumnMinRadius + P.ColumnMaxRadius); + return FMath::Max( + VF_RoughnessReach(P.FloorRoughness), + VF_RoughnessReach(P.CeilingRoughness)) + / FMath::Max(ColumnRadius, KINDA_SMALL_NUMBER); + } + case ECaveGeneratorType::Maze: + return VF_RoughnessReach(Candidate.ArchetypeParams.MazeParams.SurfaceRoughness) + / FMath::Max(Candidate.ArchetypeParams.MazeParams.CorridorRadius, + KINDA_SMALL_NUMBER); + case ECaveGeneratorType::SurfaceWorld: + return VF_RoughnessReach(Candidate.ArchetypeParams.SurfaceParams.SurfaceRoughness) + / FMath::Max(Candidate.ArchetypeParams.SurfaceParams.ElevationRange, + KINDA_SMALL_NUMBER); + case ECaveGeneratorType::VerticalShafts: + return VF_RoughnessReach( + Candidate.ArchetypeParams.VerticalShaftParams.SurfaceRoughness) + / FMath::Max( + Candidate.ArchetypeParams.VerticalShaftParams.ShaftMinRadius, + KINDA_SMALL_NUMBER); + case ECaveGeneratorType::FloatingIslands: + { + const FFloatingIslandParams& P = Candidate.ArchetypeParams.FloatingIslandParams; + return (VF_RoughnessReach(P.SurfaceRoughness) + P.SDFBlendRadius) + / FMath::Max(P.IslandMinRadius, KINDA_SMALL_NUMBER); + } + default: + return 0.0f; + } + } + + FString VF_FormatPlayerFitSummary( + const TArray& Samples, + const TCHAR* Label) + { + FString Report = FString::Printf( + TEXT("%s (walkable floor area is the coarse 128 m x 128 m footprint proxy)\n" + "archetype | PlayerFitFraction | TraversableComponentShare | " + "player-fit arrival->departure | walkable floor area m2 | " + "median vertical clearance m | roughness-to-feature ratio\n"), Label); + for (const ECaveGeneratorType Archetype : GShowcaseArchetypes) + { + const FShowcaseSample* Found = Samples.FindByPredicate( + [Archetype](const FShowcaseSample& Sample) + { + return Sample.Candidate.Archetype == Archetype; + }); + if (Found == nullptr) + { + Report += FString::Printf( + TEXT("%s | no measured sample | no measured sample | no measured sample | " + "n/a | n/a | n/a\n"), + VF_GetStrateArchetypeName(Archetype)); + continue; + } + const FVoxelStrateMetrics& M = Found->FineMetrics; + const FString Law = M.bPlayerFitResolved + ? VF_ShowcaseConnectivityName(Found->PlayerFitLaw.Result) + : TEXT("unresolved"); + const float FloorAreaM2 = Found->Metrics.WalkableFloorAreaFraction + * 128.0f * 128.0f; + Report += FString::Printf( + TEXT("%s | %.6f | %.6f | %s | %.1f (%.6f footprint) | %.2f | %.3f\n"), + VF_GetStrateArchetypeName(Archetype), + M.bPlayerFitResolved ? M.PlayerFitFraction : 0.0f, + M.bPlayerFitResolved ? M.TraversableComponentShare : 0.0f, + *Law, FloorAreaM2, + Found->Metrics.WalkableFloorAreaFraction, + Found->Metrics.MedianVerticalClearance * 0.25f, + VF_ShowcaseRoughnessFeatureRatio(Found->Candidate)); + } + return Report; + } + bool VF_RunMazeRoughnessExperiment( const VoxelForgeTest::FTestWorld& World, const FShowcaseSample& MazeSample, @@ -636,6 +1051,80 @@ namespace return true; } + bool VF_RunMazeCorridorRadiusSweep( + const VoxelForgeTest::FTestWorld& World, + const FShowcaseSample& MazeSample, + const FVoxelStrateMeasureSettings& BaseSettings, + const FVoxelStrateFinePreviewSettings& FinePreviewSettings, + FString& OutReport) + { + if (MazeSample.Candidate.Archetype != ECaveGeneratorType::Maze) + { + OutReport = TEXT("Part A Maze corridor-radius sweep unavailable: previous Maze roll was not found."); + return false; + } + + FVector ArrivalPoint = FVector::ZeroVector; + FVector DeparturePoint = FVector::ZeroVector; + if (!VF_GetShowcaseMouthPair( + World, MazeSample.TargetStrateIndex, ArrivalPoint, DeparturePoint)) + { + OutReport = TEXT("Part A Maze corridor-radius sweep unavailable: mouth pair was not found."); + return false; + } + + int32 TopVoxelZ = 0; + int32 BottomVoxelZ = 0; + if (!World.GetSlotVoxelZRange(MazeSample.TargetStrateIndex, TopVoxelZ, BottomVoxelZ)) + { + OutReport = TEXT("Part A Maze corridor-radius sweep unavailable: Z range was not found."); + return false; + } + + const FVoxelStrateMeasureSettings FineSettings = VF_MakeFinePlayerFitSettings( + BaseSettings, FinePreviewSettings, ArrivalPoint, DeparturePoint); + const float RadiusSweep[] = { 4.0f, 6.0f, 8.0f, 10.0f, 12.0f }; + OutReport = TEXT( + "Part A Maze corridor-radius sweep (same seed/index, fitted step-1 ROI; " + "radius voxels -> bore metres):\n"); + for (const float CorridorRadius : RadiusSweep) + { + FVoxelStrateComposerCandidate Candidate = MazeSample.Candidate; + Candidate.ArchetypeParams.MazeParams.CorridorRadius = CorridorRadius; + + FVoxelOpStack Stack; + FVoxelOpContext Context; + FString BuildError; + if (!VF_BuildShowcaseStack( + World, Candidate, TopVoxelZ, BottomVoxelZ, Stack, Context, BuildError)) + { + OutReport += FString::Printf( + TEXT(" radius=%.0f bore=%.2f m: build failed (%s)\n"), + CorridorRadius, CorridorRadius * 2.0f * VOXEL_SIZE / 100.0f, *BuildError); + continue; + } + + Stack.PrepareChunk(Context); + FShowcaseStackSampler Sampler(Stack); + FVoxelStrateMetrics Metrics; + const FVoxelConnectivityDiagnostics Law = + VF_DiagnosePlayerFitConnectivityWithSampler( + Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness, + ArrivalPoint, DeparturePoint, FineSettings, &Metrics); + OutReport += FString::Printf( + TEXT(" radius=%.0f (%.2f m) bore=%.2f m: fit=%.6f traversable=%.6f " + "fit_cells=%lld min_clearance=%.2f voxels (%.2f m) law=%s\n"), + CorridorRadius, CorridorRadius * VOXEL_SIZE / 100.0f, + CorridorRadius * 2.0f * VOXEL_SIZE / 100.0f, + Metrics.PlayerFitFraction, Metrics.TraversableComponentShare, + static_cast(Metrics.NumPlayerFitCells), + Metrics.MinimumPlayerClearanceVoxels, + Metrics.MinimumPlayerClearanceVoxels * VOXEL_SIZE / 100.0f, + VF_ShowcaseConnectivityName(Law.Result)); + } + return true; + } + struct FIslandVerticalAudit { int32 NumIslands = 0; @@ -1073,7 +1562,9 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) } FTestWorld World; - World.Build(BaseComposerSeed, 2, true); + // The shared fixture defaults to a bounded 4-chunk volume for fast density tests. The + // owner-facing scale audit must exercise the production/default 8-chunk envelope. + World.Build(BaseComposerSeed, 2, true, 8); if (!World.IsValid()) { AddError(World.WhyInvalid()); @@ -1188,6 +1679,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) { Stats[ArchetypeIndex].LastFailure = MoveTemp(FailureReason); } + VF_AddMeasuredSample(Stats[ArchetypeIndex], Sample); } } @@ -1248,6 +1740,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) { Stats[ArchetypeIndex].LastFailure = MoveTemp(FailureReason); } + VF_AddMeasuredSample(Stats[ArchetypeIndex], Sample); } } @@ -1270,13 +1763,76 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) ++ArchetypeIndex) { AddInfo(FString::Printf( - TEXT("Showcase search detail %s: eligible=%d hard_gate_survivors=%d last_failure=%s"), + TEXT("Showcase search detail %s: eligible=%d measured=%d hard_gate_survivors=%d last_failure=%s"), VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex]), Stats[ArchetypeIndex].EvaluatedCandidates, + Stats[ArchetypeIndex].MeasuredCandidates, Stats[ArchetypeIndex].Survivors.Num(), - Stats[ArchetypeIndex].LastFailure.IsEmpty() + Stats[ArchetypeIndex].LastFailure.IsEmpty() ? TEXT("none") : *Stats[ArchetypeIndex].LastFailure)); } + TArray FreshSurvivorSamples; + for (const FShowcaseStats& ArchetypeStats : Stats) + { + if (ArchetypeStats.Best.IsSet()) + { + FreshSurvivorSamples.Add(ArchetypeStats.Best.GetValue()); + } + } + AddInfo(VF_FormatPlayerFitSummary( + FreshSurvivorSamples, TEXT("AFTER defaults: fresh hard-gate survivors"))); + + TArray FreshDiagnosticSamples; + FreshDiagnosticSamples.Reserve(UE_ARRAY_COUNT(GShowcaseArchetypes)); + for (const FShowcaseStats& ArchetypeStats : Stats) + { + if (!ArchetypeStats.BestMeasured.IsSet()) + { + continue; + } + const FShowcaseSample Measured = ArchetypeStats.BestMeasured.GetValue(); + const FVoxelStrateComposerCandidate DiagnosticCandidate = Measured.Candidate; + FShowcaseSample Diagnostic; + FString DiagnosticFailure; + // Re-run the selected coarse measurement with the player-fit pass enabled even when a + // legacy gate fails. This is diagnostic only: it never enters Survivors or changes card + // selection, but it gives every archetype an honest before/after scale row. + VF_EvaluateShowcaseCandidate( + World, DiagnosticCandidate, Measured.TargetStrateIndex, MeasureSettings, + Diagnostic, &DiagnosticFailure, true); + if (Diagnostic.Candidate.bValid && Diagnostic.Metrics.NumSampled > 0) + { + FreshDiagnosticSamples.Add(MoveTemp(Diagnostic)); + } + } + AddInfo(VF_FormatPlayerFitSummary( + FreshDiagnosticSamples, + TEXT("AFTER defaults: best measured candidate per archetype (diagnostic; hard gates separate)"))); + + TArray PreviewSelections; + PreviewSelections.Reserve(UE_ARRAY_COUNT(GShowcaseArchetypes)); + bool bHaveAllPreviewSelections = true; + for (int32 ArchetypeIndex = 0; + ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes); + ++ArchetypeIndex) + { + if (Stats[ArchetypeIndex].Best.IsSet()) + { + PreviewSelections.Add(Stats[ArchetypeIndex].Best.GetValue()); + continue; + } + const FShowcaseSample* Diagnostic = FreshDiagnosticSamples.FindByPredicate( + [Archetype = GShowcaseArchetypes[ArchetypeIndex]](const FShowcaseSample& Sample) + { + return Sample.Candidate.Archetype == Archetype; + }); + if (Diagnostic == nullptr) + { + bHaveAllPreviewSelections = false; + break; + } + PreviewSelections.Add(*Diagnostic); + } struct FPreviousShowcaseSelection { @@ -1358,6 +1914,10 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) AddInfo(FString::Printf( TEXT("Previous eight showcase candidates rejected by the new player-fit gate: %d/8."), NumPreviousShowcaseRejected)); + AddInfo(VF_FormatPlayerFitSummary( + PreviousSamples, TEXT("AFTER defaults: historical candidate re-measurement"))); + AddInfo(VF_FormatWorldScaleAudit()); + AddInfo(VF_FormatDefaultRoughnessRatios()); AddInfo(VF_FormatRoughnessRadiusReport(PreviousSamples)); FString MazeRoughnessReport; if (const FShowcaseSample* MazeSample = PreviousSamples.FindByPredicate( @@ -1368,6 +1928,10 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) { VF_RunMazeRoughnessExperiment( World, *MazeSample, MeasureSettings, FinePreviewSettings, MazeRoughnessReport); + FString MazeRadiusReport; + VF_RunMazeCorridorRadiusSweep( + World, *MazeSample, MeasureSettings, FinePreviewSettings, MazeRadiusReport); + AddInfo(MazeRadiusReport); } else { @@ -1392,8 +1956,13 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) } if (!bHaveAllArchetypes) { - AddInfo(TEXT("Showcase cards are not written because the new hard gate found no complete " - "eight-archetype survivor set; the diagnostic report above is the result.")); + AddInfo(TEXT("No complete eight-archetype hard-gate survivor set was found; the fresh " + "preview cards below are rejected diagnostic candidates for scale review.")); + } + if (!bHaveAllPreviewSelections) + { + AddInfo(TEXT("Fresh diagnostic previews are incomplete because at least one archetype had " + "no measurable candidate.")); return true; } @@ -1429,8 +1998,9 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes); ++ArchetypeIndex) { - const FShowcaseSample& Selected = Stats[ArchetypeIndex].Best.GetValue(); + const FShowcaseSample& Selected = PreviewSelections[ArchetypeIndex]; const FVoxelStrateComposerCandidate& Candidate = Selected.Candidate; + const bool bDiagnosticCard = !bHaveAllArchetypes; // The output filename namespace is local to this page. Keep it distinct even if a future // search selects equal candidate indices from two different composer seeds; the PIE fields @@ -1484,7 +2054,11 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) ShowcaseDirectory, RenderIndex, VF_FormatNativeParameterRecipe(Candidate), SampleGrid, Metrics, MeasureSettings.HeadroomCells, - Selected.SelectionScore, Selected.LawText, false, TEXT(""), + Selected.SelectionScore, Selected.LawText, bDiagnosticCard, + bDiagnosticCard + ? TEXT("Diagnostic only: no complete hard-gate survivor set; this card is retained " + "to inspect the generated scale and exact player-fit law.") + : TEXT(""), PreviewCandidate, PreviewError); TestTrue(FString::Printf(TEXT("selected %s coarse preview writes"), VF_GetStrateArchetypeName(Candidate.Archetype)), @@ -1516,6 +2090,11 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) const FVoxelStrateMetrics FineMetrics = VF_MeasureStrateWithSampler( Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1, Context.EdgeSealThickness, FineMeasureSettings, &FineGrid); + FVoxelStrateMetrics FinePlayerMetrics; + VF_DiagnosePlayerFitConnectivityWithSampler( + Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1, + Context.EdgeSealThickness, FineArrivalPoint, FineDeparturePoint, + FineMeasureSettings, &FinePlayerMetrics); const auto IsInsideFineXY = [&FineGrid](const FVector& Point) { return Point.X >= FineGrid.MinX && Point.X < FineGrid.MaxX @@ -1531,7 +2110,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) !FineMetrics.bValid || FineGrid.CellCount <= FinePreviewMaxCells); TestTrue(FString::Printf(TEXT("selected %s fine player-fit metrics rerun identically"), VF_GetStrateArchetypeName(Candidate.Archetype)), - VF_ShowcaseMetricsEqual(Selected.FineMetrics, FineMetrics)); + VF_ShowcaseMetricsEqual(Selected.FineMetrics, FinePlayerMetrics)); const bool bFineWritten = VF_WriteStratePreviewFineCandidate( ShowcaseDirectory, RenderIndex, FineGrid, FineMeasureSettings.HeadroomCells, @@ -1546,17 +2125,17 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) VF_GetStrateArchetypeName(Candidate.Archetype), *PreviewError)); return false; } - PreviewCandidate.bPlayerFitResolved = FineMetrics.bPlayerFitResolved; - PreviewCandidate.PlayerFitRefusalReason = FineMetrics.PlayerFitRefusalReason; - PreviewCandidate.NumPlayerFitCells = FineMetrics.NumPlayerFitCells; - PreviewCandidate.PlayerFitFraction = FineMetrics.PlayerFitFraction; - PreviewCandidate.NumTraversableComponents = FineMetrics.NumTraversableComponents; + PreviewCandidate.bPlayerFitResolved = FinePlayerMetrics.bPlayerFitResolved; + PreviewCandidate.PlayerFitRefusalReason = FinePlayerMetrics.PlayerFitRefusalReason; + PreviewCandidate.NumPlayerFitCells = FinePlayerMetrics.NumPlayerFitCells; + PreviewCandidate.PlayerFitFraction = FinePlayerMetrics.PlayerFitFraction; + PreviewCandidate.NumTraversableComponents = FinePlayerMetrics.NumTraversableComponents; PreviewCandidate.LargestTraversableComponentCells = - FineMetrics.LargestTraversableComponentCells; - PreviewCandidate.TraversableComponentShare = FineMetrics.TraversableComponentShare; + FinePlayerMetrics.LargestTraversableComponentCells; + PreviewCandidate.TraversableComponentShare = FinePlayerMetrics.TraversableComponentShare; PreviewCandidate.MinimumPlayerClearanceVoxels = - FineMetrics.MinimumPlayerClearanceVoxels; - if (FineMetrics.bValid) + FinePlayerMetrics.MinimumPlayerClearanceVoxels; + if (FinePlayerMetrics.bValid) { TestTrue(FString::Printf(TEXT("selected %s fine preview has filled and contour views"), VF_GetStrateArchetypeName(Candidate.Archetype)), @@ -1581,17 +2160,28 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) PreviewCandidate.ComposerSeed = Candidate.Seed; PreviewCandidate.ComposerTargetStrateIndex = Selected.TargetStrateIndex; PreviewCandidate.bComposerRollStructure = false; - PreviewCandidate.SelectionReason = FString::Printf( - TEXT("Hard gates passed: non-vacuous, largest air component >= %.2f, and " - "player-fit arrival → departure connectivity (fine step=1; " - "fit cells=%lld). Selected by legacy score " - "floor_area + 0.25*clamp(clearance/%d,0,1) + 0.10*largest_surface = %.6f; " - "floor area is the primary walking signal. Applied to existing target slot %d."), - LargestComponentSurvivalThreshold, - static_cast(FineMetrics.NumPlayerFitCells), - static_cast(ClearanceNormalizationVoxels), - Selected.SelectionScore, - Selected.TargetStrateIndex); + PreviewCandidate.SelectionReason = bDiagnosticCard + ? FString::Printf( + TEXT("Diagnostic only: the complete hard-gate set was not found. Legacy law=%s; " + "player-fit law=%s; selected by the same score " + "floor_area + 0.25*clamp(clearance/%d,0,1) + 0.10*largest_surface = %.6f; " + "applied to existing target slot %d."), + VF_ShowcaseConnectivityName(Selected.LegacyLaw.Result), + VF_ShowcaseConnectivityName(Selected.PlayerFitLaw.Result), + static_cast(ClearanceNormalizationVoxels), + Selected.SelectionScore, + Selected.TargetStrateIndex) + : FString::Printf( + TEXT("Hard gates passed: non-vacuous, largest air component >= %.2f, and " + "player-fit arrival → departure connectivity (fine step=1; " + "fit cells=%lld). Selected by legacy score " + "floor_area + 0.25*clamp(clearance/%d,0,1) + 0.10*largest_surface = %.6f; " + "floor area is the primary walking signal. Applied to existing target slot %d."), + LargestComponentSurvivalThreshold, + static_cast(FineMetrics.NumPlayerFitCells), + static_cast(ClearanceNormalizationVoxels), + Selected.SelectionScore, + Selected.TargetStrateIndex); PreviewCandidates.Add(MoveTemp(PreviewCandidate)); TestTrue(FString::Printf(TEXT("selected %s retains a valid exact coarse window"), @@ -1603,7 +2193,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes); ++ArchetypeIndex) { - const FShowcaseSample& Selected = Stats[ArchetypeIndex].Best.GetValue(); + const FShowcaseSample& Selected = PreviewSelections[ArchetypeIndex]; Summaries[ArchetypeIndex].Window = PreviewCandidates[ArchetypeIndex].Window; Summaries[ArchetypeIndex].WindowSummary = VF_FormatShowcaseSummaryWindow( Stats[ArchetypeIndex]); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackTunnelTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackTunnelTest.cpp index 4b76c2e..10de554 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackTunnelTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackTunnelTest.cpp @@ -104,7 +104,7 @@ namespace // retourne ce garde-fou : il EXIGE maintenant des colonnes. /** - * ⚠️ DENSIFIÉ après le premier run vert. Aux défauts (`RoomSpacing = 80`, `RoomDensity = 0.35`) + * ⚠️ DENSIFIÉ après le premier run vert. Aux anciens défauts (`RoomSpacing = 80`, `RoomDensity = 0.35`) * le premier passage a rendu **65 échantillons en grotte sur 6000, soit 1,1 %** : bit-identique, * oui, mais en comparant surtout du roc plein à du roc plein, là où le carve et les vers ne * s'exécutent même pas. Le compteur avait été écrit exactement pour dire ça, et il l'a dit ; @@ -125,14 +125,14 @@ namespace */ void EnableTunnelFeatures(FStrateGenerationParams& P) { - P.RoomSpacing = 42.0f; // 80 → 42 : des salles à portée de chaque chunk échantillonné - P.RoomDensity = 0.85f; // 0.35 → 0.85 + P.RoomSpacing = 42.0f; // current production scale → 42: rooms reach sampled chunks + P.RoomDensity = 0.85f; // production .35 → .85 P.VerticalScale = 1.35f; // ≠ 1 ⇒ le Z « effectif » diverge du Z monde partout // ── ÉTAPE B1 : rugosité de paroi (4b) ──────────────────────────────────────────────── // Écrits EXPLICITEMENT, pas laissés au défaut : un test qui dépend d'un défaut se casse en - // silence le jour où le défaut change. `SurfaceRoughness` était le seul de ces champs non nul - // par défaut (5.0), et l'étape A le remettait à zéro — c'est ce zéro qui disparaît ici. + // silence le jour où le défaut change. The production default is now 2.0, but this fixture + // deliberately uses 5.0 so the full roughness branch is exercised. P.SurfaceRoughness = 5.0f; P.RoughnessFrequency = 0.1f; P.RoughnessNoiseType = EVoxelNoiseType::FBM; // les 4 types sont balayés au contrôle 1c @@ -450,7 +450,9 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters) } const int32 MidChunkZ = ((TopVoxelZ + BottomVoxelZ) / 2) / CHUNK_SIZE; - FStrateGenerationParams P = World.StrateManager->GetGenerationParams(FIntVector(0, 0, MidChunkZ)); + const FStrateGenerationParams ProductionP = + World.StrateManager->GetGenerationParams(FIntVector(0, 0, MidChunkZ)); + FStrateGenerationParams P = ProductionP; if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f) { @@ -1115,8 +1117,8 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters) float WorstViolation = 0.0f; TMap SolidKillerCounts; int32 NumRoomKilled = 0, NumTilesAwayFromSpine = 0; - // ±320 voxels = 4 x RoomSpacing. Hors de la boucle : la ligne de rapport en a besoin, et - // une étendue d'échantillonnage qu'on ne peut pas citer est une étendue qu'on ne surveille pas. + // Fixed ±320-voxel probe envelope. The report computes its ratio to the active + // RoomSpacing; keeping the envelope fixed makes before/after tile counts comparable. const int32 SpanCells = 40; const int32 SpanVoxelsReported = SpanCells * 8; // Extent = Step * Cells = 1 * 8 int32 TilesHitByRooms = 0, TilesHitByTunnels = 0, TilesHitByPits = 0, TilesHitByChimneys = 0; @@ -1133,9 +1135,9 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters) // ⚠️ L'ÉTENDUE XY ÉTAIT ±32 VOXELS, ET C'EST CE QUI RENDAIT CE BLOC INEXPLOITABLE. // `RandRange(-4, 4) * 8` échantillonnait 40 tuiles dans un cube de ±32 voxels autour de // (0,0) — c'est-à-dire l'endroit le PLUS creusé du monde entier, et de loin : - // • `RoomSpacing = 80`, donc ±32 ne couvre même pas la moitié d'UNE cellule de salle ; - // • `OriginRoomRadius = 20` garantit une grosse salle exactement à (0,0), de rayon de - // cull `max(20·1.5, 8) + 3·4 = 42` — qui avale la quasi-totalité de la fenêtre ; + // • the old `RoomSpacing = 80` meant ±32 did not cover half of ONE room cell ; + // • the old `OriginRoomRadius = 20` guaranteed a large room at (0,0), whose cull + // radius `max(20·1.5, 8) + 3·4 = 42` swallowed almost the whole window ; // • la spine (0,0) descend précisément là. // La mesure « 4.9 salles sur 7.2 atteignent la boîte » ne décrivait donc pas la densité // de grottes du monde, elle décrivait le hub de la spine. Aucune conclusion sur la @@ -1146,9 +1148,8 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters) // produirait un verdict FAUX échoue exactement comme avant. On corrige ce que la mesure // REGARDE, pas ce qu'elle exige. // - // The XY extent was ±32 voxels around (0,0) -- with RoomSpacing = 80 and a guaranteed - // OriginRoomRadius = 20 room at the origin, that samples the single most cave-dense spot - // in the world and says nothing about deep rock. Widening changes what the measurement + // The old XY extent was ±32 voxels around (0,0) -- inside the guaranteed origin room. + // Widening changes what the measurement // LOOKS AT, not what it demands: every verdict is still brute-forced below. const FIntVector Origin( Rng.RandRange(-SpanCells, SpanCells) * Extent, @@ -1300,7 +1301,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters) // A warning that fires every run and always means "this is fine" is noise that // trains the reader to ignore warnings. Zero proved is the only possible answer on // the dense fixture (info); on production defaults it would be a real regression - // from 11 (warning). + // from the previous measured count (warning). const FString ZeroMsg = FString::Printf( TEXT("[%s] No tile was proved, so the brute force verified nothing -- it has no ") TEXT("verdict to contradict. Do NOT re-derive the cause: read the two lines above, ") @@ -1326,15 +1327,14 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters) // LES DEUX MONDES, ET POURQUOI IL EN FAUT DEUX //------------------------------------------------------------------------- // ⚠️ LA FIXTURE REND CE VERDICT STRUCTURELLEMENT IMPOSSIBLE, ET CE N'EST PAS UN DÉFAUT DE LA - // FIXTURE. `EnableTunnelFeatures` densifie délibérément (`RoomSpacing` 80 → 42, - // `RoomDensity` 0.35 → 0.85) parce qu'aux défauts le premier run n'avait que 1,1 % des + // FIXTURE. `EnableTunnelFeatures` densifie délibérément (production spacing → 42, + // `RoomDensity` 0.35 → 0.85) because the first run had only 1.1% of samples in open cave, // échantillons en grotte — l'équivalence comparait du roc plein à du roc plein. Cette // densification est ce qui rend le contrôle 1 SIGNIFIANT. // // Mais elle est exactement ANTAGONISTE de la prouvabilité, et l'arithmétique le dit sans // ambiguïté : le rayon de cull d'une salle vaut `max(R·1.5, R·HeightRatio) + 3·SDFBlendRadius`, - // soit `1.5R + 12` ⇒ entre 27 et 57 pour `R ∈ [10, 30]`, moyenne ≈ 42 — c'est-à-dire - // **exactement le pas du réseau**, à 85 % d'occupation. Des sphères de cull de rayon égal au pas + // soit `1.5R + 12`; at the dense fixture's 85% occupancy those cull spheres overlap the lattice // du réseau recouvrent l'espace ~3,6 fois. **Aucune boîte de ce monde ne peut être hors de // toutes les sphères de cull.** Le « 6.3 salles sur 8.3 atteignent la boîte » mesuré est // exactement ça, et élargir l'échantillonneur n'y a rien changé (39 tuiles sur 40 étaient déjà @@ -1353,9 +1353,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters) RunTileScan(Stack, P, Ctx, TEXT("dense fixture"), /*bZeroProvedIsExpected*/ true); { - FStrateGenerationParams SparseP = P; - SparseP.RoomSpacing = 80.0f; // le défaut d'`UVoxelStrateDefinition` - SparseP.RoomDensity = 0.35f; // idem — voir `EnableTunnelFeatures` + FStrateGenerationParams SparseP = ProductionP; FVoxelOpStack SparseStack; VoxelDensityOps::BuildTunnelNetworkStack(SparseStack, SparseP, World.Settings->Seed, diff --git a/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp index f759bf5..392d208 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp @@ -52,7 +52,10 @@ namespace case ECaveGeneratorType::Maze: return FMath::Max(Definition.MazeParams.CellSize, 1.0f); case ECaveGeneratorType::VerticalShafts: - return FMath::Max(Definition.VerticalShaftParams.ShaftSpacing, 1.0f); + // A 0.6 shaft density does not guarantee an occupied immediate cell. The production + // query therefore permits a bounded two-cell local search and still returns the + // selected site's exact axis. + return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f); case ECaveGeneratorType::FloatingIslands: return FMath::Max(Definition.FloatingIslandParams.IslandSpacing, 1.0f); default: diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp index e0bf4cf..e8dae96 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp @@ -2107,7 +2107,9 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet FVoxelStrateMeasureSettings BaseSettings; BaseSettings.CenterXY = FVector2D::ZeroVector; - BaseSettings.MaxCells = 8000000; + // The centered control stays bounded after the 4 m vertical-volume fixture is retained; + // 12 M also leaves room for the scale-aware 320-voxel control below. + BaseSettings.MaxCells = 12000000; BaseSettings.HeadroomCells = 2; BaseSettings.InteriorMarginVoxels = -1; @@ -2121,8 +2123,8 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet {4, 256}, {4, 192}, {2, 192}, - {4, 128}, - {1, 128}, + {4, 160}, + {1, 160}, }; bool bAllChecksPassed = true; @@ -2458,7 +2460,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet } Summary += TEXT( "Controlled comparisons hold radius constant: (step 4, radius 192) vs (step 2, " - "radius 192), and (step 4, radius 128) vs (step 1, radius 128).\n"); + "radius 192), and (step 4, radius 160) vs (step 1, radius 160).\n"); Summary += FString::Printf( TEXT("VerticalShafts refinement sweep wall-clock: %.3f seconds.\n"), SweepSeconds); @@ -3236,10 +3238,17 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet // PART A/B — the production acceptance window follows the actual two mouths. The older // centered-window diagnostics above remain useful for the origin-spine invariant; they are // deliberately not reused here because a mouth-fitted box is not expected to contain (0,0). - constexpr int32 FittedMaxCells = 20000000; + // The tree's prescribed neighbour fallback reaches three shaft-grid cells. A mouth window + // must therefore cover three spacings beyond the endpoints before it can be called a fitted + // diagnostic box. This grows with the body-scale shaft spacing rather than preserving the + // old 48-voxel window by habit. + const FVerticalShaftParams& FittedShaftParams = + Layout[VerticalShaftsIndex].Definition->VerticalShaftParams; + const float FittedMargin = FMath::Max( + 48.0f, 3.0f * FMath::Max(FittedShaftParams.ShaftSpacing, 1.0f)); + const float DoubledFittedMargin = 2.0f * FittedMargin; + constexpr int32 FittedMaxCells = 120000000; constexpr int32 BeforeChangeMaxCells = 12000000; - constexpr float FittedMargin = 48.0f; - constexpr float DoubledFittedMargin = 96.0f; auto MakeFittedSettings = [&](int32 SampleStep, const FVector& A, @@ -3297,8 +3306,8 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet }; Summary += TEXT( - "PART A — fitted VerticalShafts mouth window (AABB of the two XY mouths + 48 voxels; " - "step 1/2; fitted MaxCells=20000000):\n"); + "PART A — fitted VerticalShafts mouth window (AABB of the two XY mouths + three shaft " + "spacings; step 1/2; doubled-margin control; fitted MaxCells=120000000):\n"); Summary += TEXT( " seed | step | default margin verdict | fitted dimensions/cells | doubled-margin check | " "interpretation | arrival component share | departure component share\n"); @@ -3341,12 +3350,21 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet { AddError(FString::Printf( TEXT("HARD FAILURE: fitted-window seed %d has a VerticalShafts mouth not on a " - "shaft axis (arrival axis=%s, departure axis=%s)."), + "shaft axis (arrival axis=%s, departure axis=%s; arrival=(%.3f,%.3f,%.3f); " + "departure=(%.3f,%.3f,%.3f); spacing=%.3f; density=%.3f)."), Seed, IsVerticalShaftAxis(ShaftParams, Seed, ResolutionArrivalPoint) ? TEXT("yes") : TEXT("no"), IsVerticalShaftAxis(ShaftParams, Seed, ResolutionDeparturePoint) - ? TEXT("yes") : TEXT("no"))); + ? TEXT("yes") : TEXT("no"), + ResolutionArrivalPoint.X, + ResolutionArrivalPoint.Y, + ResolutionArrivalPoint.Z, + ResolutionDeparturePoint.X, + ResolutionDeparturePoint.Y, + ResolutionDeparturePoint.Z, + ShaftParams.ShaftSpacing, + ShaftParams.ShaftDensity)); bPartAFittedChecksPassed = false; } @@ -3491,13 +3509,13 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet } Summary += TEXT( - "PART B — fitted 16-seed audit (step 2, margin 48): strict-local-minimum shafts are the " + "PART B — fitted 16-seed audit (step 2, margin=three shaft spacings): strict-local-minimum shafts are the " "pre-fix orphan candidates; post-fix parent paths are checked with the same fixed ±2/±3 " "resolver over a padded [-20,20] cell audit domain. Small-component threshold is one " "mean-radius shaft volume.\n"); Summary += TEXT( " seed | fitted verdict | cells | components | small non-largest/total | strict minima/shafts " - "| neighbour fallback | path orphans | axis mouths | doubled margin 96 verdict/cells | " + "| neighbour fallback | path orphans | axis mouths | doubled fitted-margin verdict/cells | " "tree path XY bounds\n"); static constexpr int32 FittedSeedCases[] = { @@ -3859,7 +3877,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet SmallComponentThresholdMin == INT32_MAX ? 0 : SmallComponentThresholdMin, SmallComponentThresholdMax); Summary += FString::Printf( - TEXT(" fitted acceptance arrival->departure: %s (%d/%d effective pass; direct margin48=" + TEXT(" fitted acceptance arrival->departure: %s (%d/%d effective pass; direct fitted margin=" "%d/%d, %d fail; binding negatives=%d, trusted negatives=%d, measurement limits=%d); " "sweep wall-clock=%.3f seconds.\n"), SweepPasses == UE_ARRAY_COUNT(FittedSeedCases) && SweepFailures == 0 diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h b/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h index 129997b..65b9295 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h +++ b/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h @@ -73,7 +73,8 @@ namespace VoxelForgeTest * dangerous — and `VoxelForge.Determinism.LargeSeedSurvives` deliberately passes big ones * (up to 2e9) to prove it stays that way. */ - void Build(int32 InSeed = 1337, int32 InGapChunks = 2, bool bUseOperatorStack = false) + void Build(int32 InSeed = 1337, int32 InGapChunks = 2, bool bUseOperatorStack = false, + int32 InStrateHeightInChunks = 4) { Settings = TStrongObjectPtr( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); @@ -105,7 +106,11 @@ namespace VoxelForgeTest UVoxelStrateDefinition* Def = NewObject( GetTransientPackage(), NAME_None, RF_Transient); Def->GeneratorType = Archetypes[i]; - Def->StrateHeightInChunks = 4; + // Keep the shared density fixture at the historical 4-chunk test volume unless + // a caller explicitly asks for the production/default 8-chunk envelope. The + // refinement and roll tests use the smaller synthetic volume as a bounded memory + // control; the owner-facing showcase opts into 8 chunks below. + Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks); // L'OPT-IN de la pile d'opérateurs. Faux par défaut : les treize tests existants // doivent continuer à exercer le `switch`, qui reste le comportement de référence. // Seul le test de solidité de ClassifyTie côté pile le passe à vrai. diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index ddb678c..f1cc5f9 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -634,6 +634,13 @@ namespace // than a point on the smoothed ledge edge. float LandingFromBottom = FMath::Max( Params.BoundarySealThickness, Params.LedgeDepth) + FloorClearance; + // The measurement API derives its default interior margin as two seal thicknesses. + // Keep a source-level landing strictly inside that same measured window; otherwise a + // lower-roughness default can produce a perfectly valid shaft landing just below the + // diagnostic grid's lower edge and turn a real mouth query into OUT_OF_WINDOW. + LandingFromBottom = FMath::Max( + LandingFromBottom, + 2.0f * Params.BoundarySealThickness + 0.01f); if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f) { const float SafeBand = Params.LedgeDepth + FloorClearance; diff --git a/Source/VoxelForge/Private/VoxelStrateComposer.cpp b/Source/VoxelForge/Private/VoxelStrateComposer.cpp index 82fa6b1..3f11d12 100644 --- a/Source/VoxelForge/Private/VoxelStrateComposer.cpp +++ b/Source/VoxelForge/Private/VoxelStrateComposer.cpp @@ -1119,34 +1119,34 @@ namespace if (FieldName == TEXT("WormThreshold")) return Set(0.01f, 0.50f); if (FieldName == TEXT("WormStrength")) return Set(1.0f, 64.0f); if (FieldName == TEXT("WormNetworkRange")) return Set(0.0f, H * 1.5f); - if (FieldName == TEXT("RoomSpacing")) return Set(16.0f, H * 2.5f); + if (FieldName == TEXT("RoomSpacing")) return Set(64.0f, FMath::Max(96.0f, H * 0.75f)); if (FieldName == TEXT("RoomDensity")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("MinRoomRadius")) return Set(1.0f, HHalf); - if (FieldName == TEXT("MaxRoomRadius")) return Set(1.0f, H); + if (FieldName == TEXT("MinRoomRadius")) return Set(16.0f, FMath::Max(32.0f, HHalf)); + if (FieldName == TEXT("MaxRoomRadius")) return Set(16.0f, FMath::Max(48.0f, H * 0.75f)); if (FieldName == TEXT("RoomHeightRatio")) return Set(0.10f, 1.0f); if (FieldName == TEXT("RoomShapeVariety")) return Set(0.0f, 1.0f); if (FieldName == TEXT("RoomFloorCutMin")) return Set(0.0f, 1.0f); if (FieldName == TEXT("RoomFloorCutMax")) return Set(0.0f, 1.0f); if (FieldName == TEXT("FloorReliefStrength")) return Set(0.0f, HQuarter); if (FieldName == TEXT("FloorReliefFrequency")) return Set(0.001f, 0.10f); - if (FieldName == TEXT("OriginRoomRadius")) return Set(0.0f, H * 0.75f); - if (FieldName == TEXT("TunnelMinRadius")) return Set(1.0f, HQuarter); - if (FieldName == TEXT("TunnelMaxRadius")) return Set(1.0f, HHalf); + if (FieldName == TEXT("OriginRoomRadius")) return Set(24.0f, FMath::Max(64.0f, H * 0.75f)); + if (FieldName == TEXT("TunnelMinRadius")) return Set(6.0f, FMath::Max(8.0f, HQuarter)); + if (FieldName == TEXT("TunnelMaxRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.25f)); if (FieldName == TEXT("TunnelDensity")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("MaxTunnelLength")) return Set(1.0f, H * 4.0f); + if (FieldName == TEXT("MaxTunnelLength")) return Set(64.0f, H * 4.0f); if (FieldName == TEXT("TunnelWarpStrength")) return Set(0.0f, H); if (FieldName == TEXT("TunnelHorizontalBias")) return Set(0.0f, 1.0f); if (FieldName == TEXT("TunnelEndpointZOffset")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("SDFBlendRadius")) return Set(0.0f, H * 0.15f); + if (FieldName == TEXT("SDFBlendRadius")) return Set(2.0f, FMath::Max(4.0f, H * 0.15f)); if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f); if (FieldName == TEXT("CaveWarpStrength")) return Set(0.0f, HHalf); if (FieldName == TEXT("CaveWarpFrequency")) return Set(0.001f, 0.10f); - if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(4.0f, H * 0.04f)); if (FieldName == TEXT("RoughnessFrequency")) return Set(0.001f, 0.20f); if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter); if (FieldName == TEXT("DomainWarpStrength")) return Set(0.0f, HHalf); if (FieldName == TEXT("DomainWarpFrequency")) return Set(0.001f, 0.20f); - if (FieldName == TEXT("FloorBias")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("FloorBias")) return Set(0.0f, FMath::Max(4.0f, H * 0.10f)); if (FieldName == TEXT("TerraceStepHeight")) return Set(0.0f, HQuarter); if (FieldName == TEXT("TerraceHardness")) return Set(0.0f, 1.0f); if (FieldName == TEXT("TerraceNoiseDisplacement")) return Set(0.0f, 1.0f); @@ -1161,22 +1161,22 @@ namespace if (FieldName == TEXT("ScallopStrength")) return Set(0.0f, 1.0f); if (FieldName == TEXT("ScallopFrequency")) return Set(0.001f, 0.20f); if (FieldName == TEXT("ArchDensity")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("ArchMinRadius")) return Set(1.0f, H * 0.20f); - if (FieldName == TEXT("ArchMaxRadius")) return Set(1.0f, H * 0.20f); + if (FieldName == TEXT("ArchMinRadius")) return Set(6.0f, FMath::Max(12.0f, H * 0.20f)); + if (FieldName == TEXT("ArchMaxRadius")) return Set(12.0f, FMath::Max(24.0f, H * 0.30f)); if (FieldName == TEXT("ColumnDensity")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("ColumnMinRadius")) return Set(1.0f, H * 0.20f); - if (FieldName == TEXT("ColumnMaxRadius")) return Set(1.0f, H * 0.40f); + if (FieldName == TEXT("ColumnMinRadius")) return Set(4.0f, FMath::Max(8.0f, H * 0.20f)); + if (FieldName == TEXT("ColumnMaxRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.40f)); if (FieldName == TEXT("PitDensity")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("PitMinRadius")) return Set(1.0f, H * 0.20f); - if (FieldName == TEXT("PitMaxRadius")) return Set(1.0f, H * 0.40f); + if (FieldName == TEXT("PitMinRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.20f)); + if (FieldName == TEXT("PitMaxRadius")) return Set(16.0f, FMath::Max(32.0f, H * 0.40f)); if (FieldName == TEXT("PitDepth")) return Set(0.0f, HHalf); if (FieldName == TEXT("ChimneyDensity")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("ChimneyMinRadius")) return Set(1.0f, H * 0.20f); - if (FieldName == TEXT("ChimneyMaxRadius")) return Set(1.0f, H * 0.40f); + if (FieldName == TEXT("ChimneyMinRadius")) return Set(4.0f, FMath::Max(8.0f, H * 0.20f)); + if (FieldName == TEXT("ChimneyMaxRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.40f)); if (FieldName == TEXT("ChimneyHeight")) return Set(0.0f, HHalf); if (FieldName == TEXT("DomeDensity")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("DomeMinRadius")) return Set(1.0f, H * 0.30f); - if (FieldName == TEXT("DomeMaxRadius")) return Set(1.0f, H * 0.50f); + if (FieldName == TEXT("DomeMinRadius")) return Set(16.0f, FMath::Max(32.0f, H * 0.30f)); + if (FieldName == TEXT("DomeMaxRadius")) return Set(32.0f, FMath::Max(48.0f, H * 0.50f)); if (FieldName == TEXT("DomeHeightRatio")) return Set(0.0f, 1.0f); if (FieldName == TEXT("PinchDensity")) return Set(0.0f, 1.0f); if (FieldName == TEXT("PinchStrength")) return Set(0.0f, H); @@ -1190,58 +1190,58 @@ namespace case ECaveGeneratorType::CrystalChamber: if (FieldName == TEXT("FloorRelativeHeight")) return Set(0.0f, 0.95f); if (FieldName == TEXT("CeilingRelativeHeight")) return Set(0.05f, 1.0f); - if (FieldName == TEXT("FloorRoughness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("FloorRoughness")) return Set(0.5f, FMath::Max(4.0f, H * 0.04f)); if (FieldName == TEXT("FloorRoughnessFrequency")) return Set(0.001f, 0.15f); - if (FieldName == TEXT("CeilingRoughness")) return Set(0.0f, HHalf); + if (FieldName == TEXT("CeilingRoughness")) return Set(1.0f, FMath::Max(16.0f, H * 0.12f)); if (FieldName == TEXT("CeilingRoughnessFrequency")) return Set(0.001f, 0.15f); if (FieldName == TEXT("ColumnDensity")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("ColumnMinRadius")) return Set(1.0f, H * 0.20f); - if (FieldName == TEXT("ColumnMaxRadius")) return Set(1.0f, H * 0.40f); - if (FieldName == TEXT("ColumnSpacing")) return Set(10.0f, H * 1.5f); + if (FieldName == TEXT("ColumnMinRadius")) return Set(8.0f, FMath::Max(12.0f, H * 0.20f)); + if (FieldName == TEXT("ColumnMaxRadius")) return Set(16.0f, FMath::Max(24.0f, H * 0.40f)); + if (FieldName == TEXT("ColumnSpacing")) return Set(64.0f, FMath::Max(96.0f, H * 0.75f)); if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter); if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f); return false; case ECaveGeneratorType::Maze: - if (FieldName == TEXT("CellSize")) return Set(8.0f, H * 2.0f); - if (FieldName == TEXT("CorridorRadius")) return Set(1.0f, HQuarter); + if (FieldName == TEXT("CellSize")) return Set(48.0f, FMath::Max(64.0f, FMath::Min(96.0f, H * 0.75f))); + if (FieldName == TEXT("CorridorRadius")) return Set(6.0f, 8.0f); if (FieldName == TEXT("BranchProbability")) return Set(0.05f, 1.0f); if (FieldName == TEXT("Verticality")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(3.0f, H * 0.02f)); if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter); if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f); return false; case ECaveGeneratorType::SurfaceWorld: if (FieldName == TEXT("BaseGroundRelative")) return Set(0.05f, 0.90f); - if (FieldName == TEXT("ElevationRange")) return Set(0.0f, H * 2.0f); + if (FieldName == TEXT("ElevationRange")) return Set(24.0f, FMath::Max(80.0f, H * 0.50f)); if (FieldName == TEXT("ContinentFrequency")) return Set(0.0005f, 0.10f); if (FieldName == TEXT("MountainStrength")) return Set(0.0f, 1.0f); if (FieldName == TEXT("MountainFrequency")) return Set(0.0005f, 0.10f); if (FieldName == TEXT("DetailFrequency")) return Set(0.0005f, 0.20f); - if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter); - if (FieldName == TEXT("HeightWarpStrength")) return Set(0.0f, H); + if (FieldName == TEXT("SurfaceRoughness")) return Set(1.0f, FMath::Max(3.0f, H * 0.02f)); + if (FieldName == TEXT("HeightWarpStrength")) return Set(0.0f, FMath::Max(48.0f, H * 0.25f)); if (FieldName == TEXT("HeightWarpFrequency")) return Set(0.0005f, 0.10f); if (FieldName == TEXT("ReliefFrequency")) return Set(0.0002f, 0.02f); if (FieldName == TEXT("ReliefStrength")) return Set(0.0f, 1.0f); if (FieldName == TEXT("ReliefContrast")) return Set(0.25f, 4.0f); if (FieldName == TEXT("TerraceStrength")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("TerraceHeight")) return Set(1.0f, HHalf); + if (FieldName == TEXT("TerraceHeight")) return Set(2.0f, FMath::Max(8.0f, H * 0.08f)); if (FieldName == TEXT("TerraceHardness")) return Set(0.0f, 1.0f); if (FieldName == TEXT("LayerLineDepth")) return Set(0.0f, HQuarter); - if (FieldName == TEXT("LayerLineSpacing")) return Set(1.0f, H); + if (FieldName == TEXT("LayerLineSpacing")) return Set(2.0f, FMath::Max(8.0f, H * 0.25f)); if (FieldName == TEXT("CliffStrength")) return Set(0.0f, 1.0f); if (FieldName == TEXT("CliffSlopeThreshold")) return Set(0.05f, 2.0f); if (FieldName == TEXT("CliffSharpness")) return Set(0.0f, 4.0f); if (FieldName == TEXT("CliffSampleDist")) return Set(0.5f, HQuarter); if (FieldName == TEXT("OverhangStrength")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("OverhangReach")) return Set(0.0f, HHalf); - if (FieldName == TEXT("OverhangHeight")) return Set(0.0f, HHalf); + if (FieldName == TEXT("OverhangReach")) return Set(4.0f, FMath::Max(16.0f, H * 0.15f)); + if (FieldName == TEXT("OverhangHeight")) return Set(4.0f, FMath::Max(24.0f, H * 0.25f)); if (FieldName == TEXT("OverhangFrequency")) return Set(0.0005f, 0.20f); if (FieldName == TEXT("OverhangZScale")) return Set(0.0f, 1.0f); if (FieldName == TEXT("OverhangSlopeThreshold")) return Set(0.05f, 2.0f); if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("BeachWidth")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("BeachWidth")) return Set(4.0f, FMath::Max(12.0f, H * 0.10f)); if (FieldName == TEXT("CeilingRelative")) return Set(0.30f, 1.0f); if (FieldName == TEXT("CeilingRoughness")) return Set(0.0f, HHalf); if (FieldName == TEXT("CeilingRoughnessFrequency")) return Set(0.0005f, 0.20f); @@ -1256,29 +1256,29 @@ namespace return false; case ECaveGeneratorType::VerticalShafts: - if (FieldName == TEXT("ShaftSpacing")) return Set(10.0f, H * 2.0f); + if (FieldName == TEXT("ShaftSpacing")) return Set(64.0f, FMath::Max(80.0f, FMath::Min(112.0f, H * 0.75f))); if (FieldName == TEXT("ShaftDensity")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("ShaftMinRadius")) return Set(1.0f, H * 0.20f); - if (FieldName == TEXT("ShaftMaxRadius")) return Set(1.0f, H * 0.40f); + if (FieldName == TEXT("ShaftMinRadius")) return Set(8.0f, 16.0f); + if (FieldName == TEXT("ShaftMaxRadius")) return Set(12.0f, 24.0f); if (FieldName == TEXT("CrossConnectChance")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("ConnectorRadius")) return Set(1.0f, H * 0.20f); - if (FieldName == TEXT("LedgeSpacing")) return Set(0.0f, H); - if (FieldName == TEXT("LedgeDepth")) return Set(0.0f, HQuarter); - if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("ConnectorRadius")) return Set(6.0f, 12.0f); + if (FieldName == TEXT("LedgeSpacing")) return Set(8.0f, FMath::Max(32.0f, H * 0.25f)); + if (FieldName == TEXT("LedgeDepth")) return Set(2.0f, FMath::Max(4.0f, H * 0.08f)); + if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(3.0f, H * 0.02f)); if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter); if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f); return false; case ECaveGeneratorType::FloatingIslands: - if (FieldName == TEXT("IslandSpacing")) return Set(20.0f, H * 3.0f); + if (FieldName == TEXT("IslandSpacing")) return Set(96.0f, FMath::Max(112.0f, FMath::Min(160.0f, H * 0.75f))); if (FieldName == TEXT("IslandDensity")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("IslandMinRadius")) return Set(2.0f, H * 0.30f); - if (FieldName == TEXT("IslandMaxRadius")) return Set(2.0f, H * 0.75f); + if (FieldName == TEXT("IslandMinRadius")) return Set(24.0f, FMath::Max(32.0f, H * 0.30f)); + if (FieldName == TEXT("IslandMaxRadius")) return Set(48.0f, FMath::Max(64.0f, H * 0.50f)); if (FieldName == TEXT("ThicknessRatio")) return Set(0.10f, 1.5f); if (FieldName == TEXT("VerticalJitter")) return Set(0.0f, 1.0f); if (FieldName == TEXT("TopFlatten")) return Set(0.0f, 1.0f); - if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter); - if (FieldName == TEXT("SDFBlendRadius")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(3.0f, H * 0.02f)); + if (FieldName == TEXT("SDFBlendRadius")) return Set(2.0f, FMath::Max(6.0f, H * 0.10f)); if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter); if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f); return false; @@ -1533,35 +1533,47 @@ namespace float StrateHeightInVoxels) { const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); - const float SpacingMin = FMath::Min(48.0f, H * 0.35f); - const float SpacingMax = FMath::Max(SpacingMin + 8.0f, FMath::Min(160.0f, H * 0.94f)); + // Human-scale derivation: an 8 m room is the smallest room-like volume (radius 16), + // a 20 m chamber is the large-room target (radius 40), and a 24 m hub is the three- + // chunk boss-space target. Keep the room-cell margin independent of those radii. + const float SpacingMin = FMath::Max(96.0f, H * 0.35f); + const float SpacingMax = FMath::Max(SpacingMin + 32.0f, FMath::Min(160.0f, H * 0.94f)); const float Coverage = VF_CorpusFreeU(Rng, 0.08f, 0.18f); P.BoundarySealThickness = VF_CorpusFreeU( Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); P.RoomSpacing = VF_CorpusFreeU(Rng, SpacingMin, SpacingMax); - P.RoomDensity = VF_CorpusFreeU(Rng, 0.25f, 0.75f); + P.RoomDensity = VF_CorpusFreeU(Rng, 0.20f, 0.45f); const float RadiusRatio = FMath::Sqrt( Coverage / (PI * FMath::Max(P.RoomDensity, 0.001f))); - P.MaxRoomRadius = P.RoomSpacing * RadiusRatio; - P.MinRoomRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.35f, 0.75f); - P.RoomHeightRatio = VF_CorpusFreeU(Rng, 0.25f, 0.85f); - P.OriginRoomRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.60f, 0.95f); + P.MaxRoomRadius = FMath::Min( + FMath::Max(40.0f, P.RoomSpacing * RadiusRatio), + FMath::Min(P.RoomSpacing * 0.45f, H * 0.30f)); + P.MinRoomRadius = FMath::Max( + 16.0f, P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.45f, 0.70f)); + P.MinRoomRadius = FMath::Min(P.MinRoomRadius, P.MaxRoomRadius); + P.RoomHeightRatio = VF_CorpusFreeU(Rng, 0.70f, 0.95f); + const float MaxRoomVerticalRadius = FMath::Max( + 8.0f, H * 0.50f - P.BoundarySealThickness - 1.0f); + P.OriginRoomRadius = FMath::Min( + P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.90f, 1.15f), + MaxRoomVerticalRadius / FMath::Max(P.RoomHeightRatio, 0.1f)); + P.OriginRoomRadius = FMath::Max(P.OriginRoomRadius, P.MinRoomRadius); - P.RoomFloorCutMin = VF_CorpusFreeU(Rng, 0.35f, 0.85f); + P.RoomFloorCutMin = VF_CorpusFreeU(Rng, 0.55f, 0.75f); P.RoomFloorCutMax = P.RoomFloorCutMin + VF_CorpusFreeU( - Rng, 0.0f, 1.0f - P.RoomFloorCutMin); + Rng, 0.0f, FMath::Min(0.25f, 1.0f - P.RoomFloorCutMin)); P.FloorReliefStrength = VF_CorpusFreeU( Rng, 0.0f, FMath::Min(12.0f, P.MaxRoomRadius * P.RoomHeightRatio * 0.25f)); - const float TunnelMinCap = FMath::Max(1.5f, P.MinRoomRadius * 0.45f); + const float TunnelMinCap = FMath::Max(6.0f, FMath::Min(8.0f, P.MinRoomRadius * 0.45f)); P.TunnelMinRadius = FMath::Max( - 1.5f, P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.25f, 0.45f)); + 6.0f, P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.25f, 0.40f)); P.TunnelMinRadius = FMath::Min(P.TunnelMinRadius, TunnelMinCap); P.TunnelMaxRadius = FMath::Max( P.TunnelMinRadius, - P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.55f, 0.90f)); + FMath::Min(8.0f, P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.50f, 0.75f))); // Adjacent cells can differ in both X and Y. The placement jitter is [0.15, 0.85] // cell, so the worst local diagonal separation is 1.7 * S * sqrt(2), not the @@ -1570,17 +1582,17 @@ namespace const float VerticalReach = 2.0f * P.MaxRoomRadius * P.RoomHeightRatio; const float NeighborReach = FMath::Sqrt( HorizontalReach * HorizontalReach + VerticalReach * VerticalReach); - P.MaxTunnelLength = NeighborReach * VF_CorpusFreeU(Rng, 1.0f, 1.40f); - P.TunnelWarpStrength = P.MaxTunnelLength * VF_CorpusFreeU(Rng, 0.02f, 0.12f); - P.WormStrength = P.BaseDensity * VF_CorpusFreeU(Rng, 1.10f, 1.80f); - P.WormNetworkRange = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.20f, 0.60f); - P.CaveWarpStrength = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.02f, 0.12f); + P.MaxTunnelLength = NeighborReach * VF_CorpusFreeU(Rng, 1.05f, 1.30f); + P.TunnelWarpStrength = P.MaxTunnelLength * VF_CorpusFreeU(Rng, 0.03f, 0.08f); + P.WormStrength = P.BaseDensity * VF_CorpusFreeU(Rng, 1.35f, 1.80f); + P.WormNetworkRange = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.25f, 0.50f); + P.CaveWarpStrength = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.03f, 0.10f); P.SDFBlendRadius = VF_CorpusFreeU( - Rng, 0.5f, FMath::Max(0.5f, FMath::Min(4.0f, P.TunnelMinRadius * 0.35f))); + Rng, 2.0f, FMath::Max(2.0f, FMath::Min(4.0f, P.TunnelMinRadius * 0.50f))); const float RoughnessMax = FMath::Max( 0.0f, (P.TunnelMinRadius - P.SDFBlendRadius - 0.25f) / VOXEL_NOISE_SCALE); - P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessMax * 0.70f); + P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.5f, RoughnessMax * 0.60f); P.FloorBias = VF_CorpusFreeU( Rng, 0.0f, P.MaxRoomRadius * P.RoomHeightRatio * 0.25f); @@ -1597,7 +1609,12 @@ namespace P.ArchMaxRadius = FMath::Max( P.ArchMinRadius, P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.20f, 0.45f)); - P.DomeMaxRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.30f, 0.60f); + // The public envelope has a 32-voxel minimum dome radius. Apply that envelope before + // deriving the height ratio; otherwise a small sampled dome would be clamped larger + // after this equation and could violate the room-height relation. + P.DomeMaxRadius = FMath::Min( + P.MaxRoomRadius, + FMath::Max(32.0f, P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.30f, 0.60f))); P.DomeMinRadius = P.DomeMaxRadius * VF_CorpusFreeU(Rng, 0.35f, 0.75f); P.DomeHeightRatio = FMath::Min( 1.0f, @@ -1615,23 +1632,27 @@ namespace const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); P.BoundarySealThickness = VF_CorpusFreeU( Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); - P.FloorRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.04f); - P.CeilingRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.08f); + // A slab is an open fight/chamber volume, not a one-voxel shell. At the new + // eight-chunk default this leaves roughly 20-50 m between the two surfaces after + // reserving the 2.5-4 m feature envelopes below. + P.FloorRoughness = VF_CorpusFreeU(Rng, 1.0f, FMath::Max(4.0f, H * 0.04f)); + P.CeilingRoughness = VF_CorpusFreeU(Rng, 2.0f, FMath::Max(8.0f, H * 0.06f)); const float FloorNoise = VOXEL_NOISE_SCALE * P.FloorRoughness; const float CeilingNoise = VOXEL_NOISE_SCALE * P.CeilingRoughness; - const float FloorClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.16f); - const float CeilingClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.16f); + const float FloorClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.12f); + const float CeilingClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.12f); P.FloorRelativeHeight = (P.BoundarySealThickness + FloorNoise + FloorClearance) / H; P.CeilingRelativeHeight = 1.0f - (P.BoundarySealThickness + CeilingNoise + CeilingClearance) / H; P.ColumnSpacing = VF_CorpusFreeU( - Rng, FMath::Max(24.0f, H * 0.30f), FMath::Max(32.0f, H * 0.75f)); - P.ColumnMinRadius = VF_CorpusFreeU(Rng, 1.0f, P.ColumnSpacing * 0.08f); + Rng, FMath::Max(96.0f, H * 0.375f), + FMath::Max(128.0f, FMath::Min(160.0f, H * 0.75f))); + P.ColumnMinRadius = VF_CorpusFreeU(Rng, 8.0f, 12.0f); P.ColumnMaxRadius = FMath::Max( P.ColumnMinRadius, - FMath::Min(P.ColumnSpacing * 0.40f, H * 0.38f)); + FMath::Min(P.ColumnSpacing * 0.40f, P.ColumnMinRadius + 8.0f)); return true; } @@ -1640,12 +1661,16 @@ namespace float StrateHeightInVoxels) { const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); + // Body-derived lattice: the fight corridor is 3-4 m wide (radius 6-8 voxels), + // while the lattice cell is 12-16 m (48-64 voxels). Thus a corridor radius is + // about one eighth of a cell, far below CellSize/2, leaving a real rock wall. P.CellSize = VF_CorpusFreeU( - Rng, FMath::Max(24.0f, H * 0.25f), FMath::Min(96.0f, H * 0.75f)); - P.CorridorRadius = P.CellSize * VF_CorpusFreeU(Rng, 0.12f, 0.24f); + Rng, 48.0f, FMath::Max(64.0f, FMath::Min(96.0f, H * 0.75f))); + P.CorridorRadius = VF_CorpusFreeU( + Rng, 6.0f, FMath::Min(8.0f, P.CellSize * 0.20f)); const float RoughnessCap = FMath::Max( 0.0f, (P.CorridorRadius + 2.0f) / (VOXEL_NOISE_SCALE * 1.25f)); - P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessCap * 0.70f); + P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.5f, RoughnessCap * 0.60f); P.BoundarySealThickness = VF_CorpusFreeU( Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); return true; @@ -1658,9 +1683,11 @@ namespace const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); P.BoundarySealThickness = VF_CorpusFreeU( Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); - P.ElevationRange = VF_CorpusFreeU(Rng, H * 0.06f, H * 0.16f); + // Outdoor relief uses 6-20 m as its authored human-scale band; the absolute + // height is still tied to H so the sky cap has room above the terrain. + P.ElevationRange = VF_CorpusFreeU(Rng, H * 0.10f, H * 0.25f); P.MountainStrength = VF_CorpusFreeU(Rng, 0.20f, 0.80f); - P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.02f); + P.SurfaceRoughness = VF_CorpusFreeU(Rng, 1.0f, FMath::Max(3.0f, H * 0.02f)); const float GroundLowerDeviation = P.ElevationRange * 0.50f + P.SurfaceRoughness * VOXEL_NOISE_SCALE; @@ -1743,18 +1770,21 @@ namespace const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); P.BoundarySealThickness = VF_CorpusFreeU( Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); + // A shaft is a 4-7 m diameter vertical room; connectors are 3-6 m bores. + // The 20-28 m site spacing leaves a substantial rock web between shafts. P.ShaftSpacing = VF_CorpusFreeU( - Rng, FMath::Max(32.0f, H * 0.30f), - FMath::Max(32.0f, FMath::Min(112.0f, H * 0.90f))); - P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.04f); + Rng, 80.0f, FMath::Max(96.0f, FMath::Min(112.0f, H * 0.90f))); + P.SurfaceRoughness = VF_CorpusFreeU(Rng, 1.0f, FMath::Max(3.0f, H * 0.02f)); const float RoughnessReach = P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f; - P.ShaftMinRadius = RoughnessReach + 0.5f + VF_CorpusFreeU(Rng, 1.0f, 4.0f); + P.ShaftMinRadius = FMath::Max( + 8.0f, RoughnessReach + 0.5f + VF_CorpusFreeU(Rng, 1.0f, 3.0f)); P.ShaftMaxRadius = FMath::Max( - P.ShaftMinRadius + 0.5f, - FMath::Min(P.ShaftSpacing * 0.40f, P.ShaftMinRadius + 16.0f)); - P.ConnectorRadius = RoughnessReach + 1.0f + VF_CorpusFreeU(Rng, 0.0f, 3.0f); + P.ShaftMinRadius + 4.0f, + FMath::Min(P.ShaftSpacing * 0.40f, P.ShaftMinRadius + 8.0f)); + P.ConnectorRadius = FMath::Max( + 6.0f, RoughnessReach + 1.0f + VF_CorpusFreeU(Rng, 0.0f, 2.0f)); P.LedgeDepth = VF_CorpusFreeU( - Rng, 0.5f, FMath::Max(0.5f, FMath::Min(4.0f, P.ShaftSpacing * 0.08f))); + Rng, 2.0f, FMath::Max(4.0f, FMath::Min(6.0f, P.ShaftSpacing * 0.08f))); const float FloorClearance = FMath::Max( 1.0f, P.SurfaceRoughness * VOXEL_NOISE_SCALE + 1.0f); P.LedgeSpacing = 2.0f * (P.LedgeDepth + FloorClearance) @@ -1770,21 +1800,23 @@ namespace const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); P.BoundarySealThickness = VF_CorpusFreeU( Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); + // Island radii are deliberately larger than the roughness envelope: 12-24 m + // diameters read as land masses, while 28-40 m site spacing prevents mush. P.IslandSpacing = VF_CorpusFreeU( - Rng, FMath::Max(40.0f, H * 0.30f), - FMath::Max(40.0f, FMath::Min(120.0f, H * 0.75f))); - P.ThicknessRatio = VF_CorpusFreeU(Rng, 0.30f, 0.90f); + Rng, 112.0f, FMath::Max(112.0f, FMath::Min(160.0f, H * 0.75f))); + P.ThicknessRatio = VF_CorpusFreeU(Rng, 0.45f, 0.75f); const float MaxByHeight = (H * 0.50f - P.BoundarySealThickness) / FMath::Max(P.ThicknessRatio, 0.25f); const float MaxRadius = FMath::Max( - 2.0f, FMath::Min(P.IslandSpacing * 0.45f, MaxByHeight * 0.85f)); + 48.0f, FMath::Min(P.IslandSpacing * 0.45f, MaxByHeight * 0.85f)); P.IslandMaxRadius = MaxRadius; - P.IslandMinRadius = P.IslandMaxRadius * VF_CorpusFreeU(Rng, 0.45f, 0.80f); + P.IslandMinRadius = FMath::Max( + 24.0f, P.IslandMaxRadius * VF_CorpusFreeU(Rng, 0.50f, 0.80f)); P.SDFBlendRadius = VF_CorpusFreeU( - Rng, 0.5f, FMath::Min(6.0f, P.IslandMinRadius * 0.20f)); + Rng, 2.0f, FMath::Min(6.0f, P.IslandMinRadius * 0.20f)); const float RoughnessMax = FMath::Max( 0.0f, (P.IslandMinRadius - P.SDFBlendRadius) / VOXEL_NOISE_SCALE); - P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessMax * 0.70f); + P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.5f, RoughnessMax * 0.60f); return true; } @@ -1860,8 +1892,39 @@ namespace } if (bValid && bConstraintSampled) { - bValid = VF_ApplyCorpusFreeConstraints(Archetype, Params, ScalarRng, - StrateHeightInVoxels); + // Dependent constraints are authored in terms of the final vector, while the + // reflected envelope clamp below can move an independently sampled field (for + // example SurfaceWorld elevation) after that derivation. Treat the fully clamped + // vector as the candidate and deterministically resample until the declared + // relations hold; never return a vector whose coupling was only true pre-clamp. + constexpr int32 MaxConstraintAttempts = 64; + FString ConstraintViolation; + bool bConstraintValid = false; + for (int32 Attempt = 0; Attempt < MaxConstraintAttempts; ++Attempt) + { + bConstraintValid = VF_ApplyCorpusFreeConstraints( + Archetype, Params, ScalarRng, StrateHeightInVoxels); + if (!bConstraintValid) + { + break; + } + VF_ClampCorpusFreeFields(Params, Archetype, StrateHeightInVoxels); + VF_ResetExcludedFields(Params, Archetype); + if (VF_ValidateStrateCorpusFreeConstraints( + Archetype, Params, StrateHeightInVoxels, ConstraintViolation)) + { + break; + } + bConstraintValid = false; + } + if (!bConstraintValid) + { + Result.FailureReason = ConstraintViolation.IsEmpty() + ? TEXT("Constraint sampler could not produce a clamped legal vector.") + : FString::Printf(TEXT("Constraint sampler exhausted legal retries: %s"), + *ConstraintViolation); + return Result; + } } if (!bValid) { @@ -1869,10 +1932,12 @@ namespace return Result; } - // Last safety net only: all dependent values above are derived before this envelope - // clamp, and no corpus/default value is consulted. - VF_ClampCorpusFreeFields(Params, Archetype, StrateHeightInVoxels); - VF_ResetExcludedFields(Params, Archetype); + if (!bConstraintSampled) + { + // Last safety net for the independent arm; no corpus/default value is consulted. + VF_ClampCorpusFreeFields(Params, Archetype, StrateHeightInVoxels); + VF_ResetExcludedFields(Params, Archetype); + } Result.ArchetypeParams = Params; Result.Params = Params.TunnelNetworkParams; Result.bValid = true; diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index a1d85a5..069c14a 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -627,9 +627,11 @@ void UVoxelStrateManager::GeneratePassages() return FMath::Max(Definition.MazeParams.CellSize, 1.0f); case ECaveGeneratorType::VerticalShafts: - // One shaft grid spacing is the natural nearest-site budget. A sparser layout may - // legitimately decline instead of pulling the passage away from the spine. - return FMath::Max(Definition.VerticalShaftParams.ShaftSpacing, 1.0f); + // A density of 0.6 does not guarantee an occupied cell in the immediate grid + // neighbourhood. Allow the nearest occupied shaft to be two grid cells away while + // remaining a bounded local query; the landing query still returns that shaft's + // exact axis, so this cannot create a non-topological mouth. + return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f); case ECaveGeneratorType::FloatingIslands: // One island grid spacing protects the intentional radial placement while still diff --git a/Source/VoxelForge/Public/VoxelSeasonManifest.h b/Source/VoxelForge/Public/VoxelSeasonManifest.h index 46a682f..99d728c 100644 --- a/Source/VoxelForge/Public/VoxelSeasonManifest.h +++ b/Source/VoxelForge/Public/VoxelSeasonManifest.h @@ -57,7 +57,7 @@ struct VOXELFORGE_API FVoxelSeasonSelectionPolicy struct VOXELFORGE_API FVoxelSeasonFixedStrate { int32 Seed = 0; - int32 HeightInChunks = 4; + int32 HeightInChunks = 8; ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork; FVoxelStrateArchetypeParams Params; FVoxelOpStackRecipe Recipe; @@ -85,7 +85,7 @@ struct VOXELFORGE_API FVoxelSeasonCompositionSettings // Generated candidates use one stable height. Fixed authored slots may provide their own // height. World layout bounds are written into every selected parameter family below. - int32 StrateHeightInChunks = 4; + int32 StrateHeightInChunks = 8; int32 InterStrateGapChunks = 0; float OriginSpineRadius = 14.0f; float WorldRadiusVoxels = 0.0f; @@ -125,7 +125,9 @@ struct VOXELFORGE_API FVoxelSeasonCompositionSettings PlayerFitMeasureSettings.SampleStep = 1; PlayerFitMeasureSettings.RadiusInVoxels = 64; - PlayerFitMeasureSettings.MaxCells = 2000000; + // A fitted mouth-to-mouth XY window across an 8-chunk (256-voxel) strate can approach + // four million cells. Keep the cap finite and below the old multi-gigabyte retry cost. + PlayerFitMeasureSettings.MaxCells = 4000000; PlayerFitMeasureSettings.MaxRouteRetries = 16; PlayerFitMeasureSettings.HeadroomCells = 2; PlayerFitMeasureSettings.InteriorMarginVoxels = -1; diff --git a/Source/VoxelForge/Public/VoxelStrateDefinition.h b/Source/VoxelForge/Public/VoxelStrateDefinition.h index 68fb512..ef3a1b2 100644 --- a/Source/VoxelForge/Public/VoxelStrateDefinition.h +++ b/Source/VoxelForge/Public/VoxelStrateDefinition.h @@ -66,11 +66,11 @@ public: // How many chunks tall this strate is. // Each strate can have a different height! - // Big open caverns = 6-8 chunks, tight tunnels = 3, vertical shaft = 12+ - // Surface/sky worlds want a high sky-cap ceiling, so they go much taller. - // 1 chunk = 32 voxels x 25cm = 8 m, so 256 chunks ~= 2 km of vertical strate. + // Human-scale defaults use 8 chunks = 64 m. This gives a 20 m+ vertical budget for + // cathedral rooms, tall shafts, and the surface sky cap without changing the lateral gate. + // 1 chunk = 32 voxels x 25cm = 8 m, so the maximum 256 chunks is ~= 2 km of vertical strate. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (ClampMin = "1", ClampMax = "256")) - int32 StrateHeightInChunks = 4; + int32 StrateHeightInChunks = 8; //========================================================================= // BOUNDARY TRANSITION diff --git a/Source/VoxelForge/Public/VoxelStratePreview.h b/Source/VoxelForge/Public/VoxelStratePreview.h index 0d5a51e..5362378 100644 --- a/Source/VoxelForge/Public/VoxelStratePreview.h +++ b/Source/VoxelForge/Public/VoxelStratePreview.h @@ -38,11 +38,11 @@ struct VOXELFORGE_API FVoxelStrateFinePreviewSettings { // The default is deliberately one voxel: the pass exists to expose detail that step 4 cannot. int32 SampleStep = 1; - // radius=64 gives a 128x128 XY ROI and stays below the default two-million-cell cap for the - // four-chunk fixture. The caller may re-centre it on the deterministic largest open space + // radius=64 gives a 128x128 XY ROI and stays below the default four-million-cell cap for the + // eight-chunk fixture. The caller may re-centre it on the deterministic largest open space // point found by the coarse air flood fill; it is not inherently the strate midpoint. int32 RadiusInVoxels = 64; - int32 MaxCells = 2000000; + int32 MaxCells = 4000000; FVector2D CenterXY = FVector2D::ZeroVector; int32 InteriorMarginVoxels = -1; diff --git a/Source/VoxelForge/Public/VoxelStrateTypes.h b/Source/VoxelForge/Public/VoxelStrateTypes.h index c43ee8b..9eab7d0 100644 --- a/Source/VoxelForge/Public/VoxelStrateTypes.h +++ b/Source/VoxelForge/Public/VoxelStrateTypes.h @@ -446,10 +446,10 @@ struct VOXELFORGE_API FStrateGenerationParams // the cave system instead of spraying disconnected noise pockets through the whole // strate (the far-field "confetti"). 0 = unlimited (legacy unmasked behaviour). // 16 → tight braiding right along rooms/tunnels - // 24 → braids + short noodle shortcuts (good default) - // 48+ → loose, wandering side-passages + // 64 → 16 m braids + side passages (human-scale default) + // 96+ → loose, wandering side-passages UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Worm Tunnels", meta = (ClampMin = "0.0")) - float WormNetworkRange = 24.0f; + float WormNetworkRange = 64.0f; // ===== CAVE MORPHOLOGY (room-and-corridor) ===== // @@ -465,12 +465,12 @@ struct VOXELFORGE_API FStrateGenerationParams // Distance between room grid cells (in voxels). // This controls room SPACING, not size. Larger = rooms further apart. - // 40 → dense room network, rooms nearly touching - // 80 → moderate density, corridors between rooms (good default) - // 120 → sparse rooms, long corridors + // 64 → dense room network + // 128 → 32 m cell spacing (human-scale default) + // 160 → sparse rooms, long corridors // 200+→ isolated chambers with long tunnel treks UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms") - float RoomSpacing = 80.0f; + float RoomSpacing = 128.0f; // Probability of a room existing in each grid cell (0-1). // Not every cell gets a room — this controls how many are filled. @@ -483,28 +483,29 @@ struct VOXELFORGE_API FStrateGenerationParams // Smallest possible room radius (in voxels). // Small rooms feel like alcoves or nooks. - // 5-8 → small alcoves - // 10-15→ natural small chambers (good default) - // 20+ → even "small" rooms are spacious + // 8-12 → alcoves and small rooms + // 16 → 8 m diameter room (human-scale minimum) + // 24+ → spacious small rooms UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms") - float MinRoomRadius = 10.0f; + float MinRoomRadius = 16.0f; // Largest possible room radius (in voxels). // Large rooms are cathedral chambers. - // 15-20→ moderate rooms - // 25-35→ large caverns (good default) - // 50+ → massive cathedral spaces + // 24-32→ moderate rooms + // 40 → 20 m diameter chamber (large-room default) + // 80+ → massive cathedral spaces UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms") - float MaxRoomRadius = 30.0f; + float MaxRoomRadius = 40.0f; // How vertically squished rooms are. // 1.0 = perfect sphere (tall as wide). // Lower = flatter, more horizontal chambers. // 0.2-0.3 → very flat caverns (strate-like, wide and low) - // 0.4-0.5 → natural cave chambers (good default) + // 0.4-0.5 → natural cave chambers + // 0.85 → tall rooms (17 m diameter-height envelope at radius 40) // 0.7-1.0 → tall, cathedral-like rooms UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms", meta = (ClampMin = "0.1", ClampMax = "1.0")) - float RoomHeightRatio = 0.4f; + float RoomHeightRatio = 0.85f; // Per-room floor flatness range. Each room hash-rolls a value in [Min, Max]. // FloorCutZ = RoomCenter.Z - RoomRadiusZ * roll @@ -521,11 +522,11 @@ struct VOXELFORGE_API FStrateGenerationParams // with navigable flat floors. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms", meta = (ClampMin = "0.0", ClampMax = "1.0")) - float RoomFloorCutMin = 1.0f; + float RoomFloorCutMin = 0.70f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms", meta = (ClampMin = "0.0", ClampMax = "1.0")) - float RoomFloorCutMax = 1.0f; + float RoomFloorCutMax = 0.90f; // Large-scale floor undulation — how many voxels the floor plane rises and falls. // This is NOT surface roughness (which adds rock texture). This shifts the entire @@ -535,13 +536,14 @@ struct VOXELFORGE_API FStrateGenerationParams // // Only meaningful when RoomFloorCutMin < 1.0 (flat floor mode active). // - // 0 → perfectly flat floor (default — use surface roughness for texture) + // 0 → perfectly flat floor + // 4 → 1 m variation (human-scale default; subtle beside a room) // 2-4 → subtle floor variation, barely perceptible gradient // 5-10 → clear hills and basins — interesting to walk across // 15+ → dramatic terrain — slopes and drops within the room UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms", meta = (ClampMin = "0.0", EditCondition = "RoomFloorCutMin < 1.0")) - float FloorReliefStrength = 0.0f; + float FloorReliefStrength = 4.0f; // How wide the floor hills are. Lower = broader, gentler undulations. // 0.005 → very broad (one hill spans the whole room) @@ -567,10 +569,11 @@ struct VOXELFORGE_API FStrateGenerationParams // making it the natural hub of the strate's cave network. // 0 → disabled (no guaranteed origin room) // 15 → moderate hub room - // 20 → large central chamber (good default) - // 30+ → massive starting cavern + // 32 → large central chamber + // 48 → 24 m hub (three-chunk boss-space default) + // 64+ → massive starting cavern UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms") - float OriginRoomRadius = 20.0f; + float OriginRoomRadius = 48.0f; // Maximum number of backbone tunnels that can force-connect to the origin room. // Without a limit, every room in the search area that picks origin as its nearest @@ -588,18 +591,18 @@ struct VOXELFORGE_API FStrateGenerationParams // ===== TUNNELS (connecting corridors between rooms) ===== // Smallest tunnel radius (in voxels). Creates tight squeeze corridors. - // 2-3 → claustrophobic crawlspaces - // 3-4 → tight but passable (good default) - // 5+ → even "narrow" tunnels are comfortable + // 4-5 → marginal passage + // 6 → 3 m bore (minimum comfortable round passage) + // 8 → 4 m bore (human-scale wide passage) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels") - float TunnelMinRadius = 3.0f; + float TunnelMinRadius = 6.0f; // Largest tunnel radius (in voxels). Creates wide passages. - // 5-6 → comfortable walking tunnels - // 7-8 → wide passages (good default) + // 6 → 3 m bore (comfortable walking tunnel) + // 8 → 4 m bore (fight-space passage) // 10+ → practically small rooms UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels") - float TunnelMaxRadius = 7.0f; + float TunnelMaxRadius = 8.0f; // Probability that two nearby rooms are connected by a tunnel (0-1). // 0.2 → sparse connections, mostly dead-end rooms @@ -610,10 +613,11 @@ struct VOXELFORGE_API FStrateGenerationParams // Maximum tunnel length (in voxels). Rooms further apart won't connect. // 100 → only close neighbors - // 200 → moderate reach (good default) + // 200 → moderate reach + // 360 → 90 m, reaches the worst jittered neighbour at 32 m cell spacing // 400+→ long-range connections possible UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels") - float MaxTunnelLength = 200.0f; + float MaxTunnelLength = 360.0f; // How much tunnel paths curve (in voxels of sideways displacement). // Without this, tunnels are straight lines between rooms. @@ -622,10 +626,10 @@ struct VOXELFORGE_API FStrateGenerationParams // Capped at 25% of tunnel length to prevent kinky short tunnels. // 0 → perfectly straight tunnels (artificial look) // 8-12→ gentle natural curves - // 15 → clearly winding passages (good default) + // 24 → 6 m lateral bend (human-scale default) // 25+ → very curvy, meandering corridors UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels") - float TunnelWarpStrength = 15.0f; + float TunnelWarpStrength = 24.0f; // Preference for horizontal connections over vertical (0-1). // Real caves are mostly horizontal — vertical connections are rarer. @@ -652,17 +656,17 @@ struct VOXELFORGE_API FStrateGenerationParams // Combined with terrain ops (cliffs, terraces), this creates multi-level rooms // where one tunnel exits at the upper ledge and another at the lower floor. // 0.0 → all tunnels connect at room center height - // 0.5 → endpoints range from -50% to +50% of room height (good default) + // 0.25 → endpoints use a restrained quarter-height offset (default) // 1.0 → full range — tunnels can enter near floor or ceiling UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels", meta = (ClampMin = "0.0", ClampMax = "1.0")) - float TunnelEndpointZOffset = 0.5f; + float TunnelEndpointZOffset = 0.25f; // ===== SDF BLEND (junction smoothness) ===== // Smooth union blend radius. Controls how rounded the junctions // are where rooms meet tunnels (or rooms meet rooms). // 1-2 → sharp, angular junctions - // 3-5 → natural rounded junctions (good default) + // 4 → 1 m rounded junction (default) // 8+ → very blobby, organic blending UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Blend") float SDFBlendRadius = 4.0f; @@ -685,10 +689,10 @@ struct VOXELFORGE_API FStrateGenerationParams // This controls the overall organic distortion of the cave skeleton. // 0 → disabled — pristine geometric rooms and straight tunnels // 4-6 → subtle natural variation (rooms slightly irregular) - // 8-12→ clearly organic caves, tunnels visibly curve (good default) + // 16 → 4 m skeleton distortion (human-scale default) // 16+ → heavily distorted, surreal cave shapes UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Cave Warp") - float CaveWarpStrength = 8.0f; + float CaveWarpStrength = 16.0f; // Frequency of the warp noise. Lower = broader, smoother bends. // This controls the SCALE of the distortion. @@ -708,10 +712,11 @@ struct VOXELFORGE_API FStrateGenerationParams // How strong the roughness is (in voxels of displacement). // 0 → perfectly smooth SDF shapes // 2-3 → subtle rocky texture - // 4-6 → natural rocky cave surfaces (good default) + // 2 → 0.5 m wall texture (human-scale default; bounded reach 0.94 m) + // 4-6 → stronger natural rocky cave surfaces // 8+ → very rough, jagged rock UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Roughness") - float SurfaceRoughness = 5.0f; + float SurfaceRoughness = 2.0f; // Frequency of the roughness noise. Higher = finer details. // 0.05 → large rocky features (boulders, ledges) @@ -758,13 +763,14 @@ struct VOXELFORGE_API FStrateGenerationParams // Only applied below room center Z (fades to 0 at center, strongest at floor). // Does NOT affect the open space above room center or walls/ceiling. // - // 0 → disabled — full roughness on all surfaces (default) + // 0 → disabled — full roughness on all surfaces // 2-3 → subtle flattening, floor still has character + // 4 → 1 m of floor bias (human-scale default) // 4-6 → noticeably flatter floor while keeping wall/ceiling rough // 8+ → near-flat floor (good for navigable caves) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Roughness", meta = (ClampMin = "0.0")) - float FloorBias = 0.0f; + float FloorBias = 4.0f; // ===== TERRAIN OPERATIONS ===== // @@ -837,8 +843,9 @@ struct VOXELFORGE_API FStrateGenerationParams float OverhangStrength = 0.0f; // How far overhangs protrude from walls (in voxels of density added). - // 2-3 → small, 5-8 → natural, 10+ → dramatic - float OverhangDepth = 5.0f; + // 8 → 2 m optional shelf (human-scale default) + // 16+ → dramatic + float OverhangDepth = 8.0f; // Frequency of the overhang noise. Z freq is auto 5x lower for horizontal features. // 0.03 → large rare, 0.06 → moderate, 0.1+ → many small @@ -919,8 +926,9 @@ struct VOXELFORGE_API FStrateGenerationParams // 0.06 → moderate (good for cathedral strates) // 0.1 → frequent bridges float ArchDensity = 0.0f; - float ArchMinRadius = 3.0f; - float ArchMaxRadius = 6.0f; + // Radius of an optional bridge tube: 1.5-3 m at the human-scale defaults. + float ArchMinRadius = 6.0f; + float ArchMaxRadius = 12.0f; // ===== COLUMNS / PILLARS ===== // @@ -939,8 +947,8 @@ struct VOXELFORGE_API FStrateGenerationParams // 0.15 → moderate (good for cathedral strates) // 0.3 → dense forest of pillars float ColumnDensity = 0.0f; - float ColumnMinRadius = 2.0f; - float ColumnMaxRadius = 5.0f; + float ColumnMinRadius = 4.0f; + float ColumnMaxRadius = 8.0f; // ===== PITS / VERTICAL SHAFTS ===== // @@ -960,24 +968,25 @@ struct VOXELFORGE_API FStrateGenerationParams // 0.08 → moderate (good for deep strates with vertical gameplay) // 0.15 → frequent pits (chaotic, maze-like) float PitDensity = 0.0f; - float PitMinRadius = 4.0f; - float PitMaxRadius = 10.0f; - float PitDepth = 25.0f; + float PitMinRadius = 8.0f; + float PitMaxRadius = 16.0f; + float PitDepth = 32.0f; // ----- Chimney / Shaft (upward voids — inverse of pits) ----- // Chimneys: narrow vertical tubes piercing upward from cave ceilings. float ChimneyDensity = 0.0f; - float ChimneyMinRadius = 2.0f; - float ChimneyMaxRadius = 5.0f; - float ChimneyHeight = 20.0f; + float ChimneyMinRadius = 4.0f; + float ChimneyMaxRadius = 8.0f; + float ChimneyHeight = 32.0f; // ----- Dome (hemispherical chamber ceilings) ----- - // Domes: hemispherical chamber ceilings for cathedral-like feel. + // Domes: hemispherical chamber ceilings for cathedral-like feel. The default 16-32 voxel + // radius is a 4-8 m optional ceiling feature; the surrounding room remains the main volume. float DomeDensity = 0.0f; - float DomeMinRadius = 8.0f; - float DomeMaxRadius = 15.0f; + float DomeMinRadius = 16.0f; + float DomeMaxRadius = 32.0f; float DomeHeightRatio = 0.8f; // ----- Pinch / Bottleneck (passage narrowing) ----- @@ -985,7 +994,7 @@ struct VOXELFORGE_API FStrateGenerationParams // Pinch: passage narrowing for bottlenecks/chokepoints. float PinchDensity = 0.0f; float PinchStrength = 5.0f; - float PinchLength = 12.0f; + float PinchLength = 24.0f; // ----- Strate boundary sealing ----- @@ -1129,7 +1138,7 @@ struct VOXELFORGE_API FSlabGenerationParams // 0.0 = at the very bottom of the strate, 1.0 = at the very top. // Keep this well below CeilingRelativeHeight or you get no open space. // 0.1-0.2 → floor is near the bottom (lots of headroom) - // 0.25 → floor at 25% height (good default) + // 0.25 → floor at 16 m in a 64 m strate (human-scale default) // 0.4+ → very low ceiling; claustrophobic plains UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Shape", meta = (ClampMin = "0.0", ClampMax = "0.95")) @@ -1138,12 +1147,12 @@ struct VOXELFORGE_API FSlabGenerationParams // Where the ceiling surface sits, relative to this strate's height. // Must be above FloorRelativeHeight. The difference determines how tall // the open void is. - // 0.6 → moderate ceiling height (tight plains) - // 0.8 → tall open space (good default for FlatPlain) + // 0.6 → ceiling at 38.4 m in a 64 m strate (22.4 m open span) + // 0.8 → tall open space // 0.9+ → nearly the full strate height is open (cavernous plains) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Shape", meta = (ClampMin = "0.05", ClampMax = "1.0")) - float CeilingRelativeHeight = 0.80f; + float CeilingRelativeHeight = 0.60f; // ===== FLOOR ROUGHNESS ===== // Perlin-based displacement of the floor surface up/down. @@ -1152,7 +1161,8 @@ struct VOXELFORGE_API FSlabGenerationParams // How many voxels the floor surface can shift up or down. // 0 → perfectly flat floor (artificial, but dramatic) - // 3-5 → subtle rolling ground (good for FlatPlain) + // 4 → 1 m rolling ground displacement (human-scale default) + // 3-5 → subtle rolling ground // 8+ → significant hills, deep valleys UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Floor") float FloorRoughness = 4.0f; @@ -1172,8 +1182,8 @@ struct VOXELFORGE_API FSlabGenerationParams // How far ceiling formations hang down into the void (in voxels). // 0 → flat ceiling (no formations) - // 4-6 → subtle bumps (FlatPlain default) - // 10-15 → significant formations (CrystalChamber default) + // 6 → 1.5 m downward formations (FlatPlain/Crystal default) + // 10-15 → significant formations // 20+ → dramatic columns reaching toward the floor UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Ceiling") float CeilingRoughness = 6.0f; @@ -1205,22 +1215,22 @@ struct VOXELFORGE_API FSlabGenerationParams // Smallest column radius (in voxels). // 1-2 → slender needle-like pillars - // 3-4 → narrow columns (good default) + // 8 → 2 m radius / 4 m diameter (human-scale minimum) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Columns") - float ColumnMinRadius = 2.0f; + float ColumnMinRadius = 8.0f; // Largest column radius (in voxels). - // 6-8 → natural stone pillars (good default) - // 12-15 → massive trunks + // 8-16 → 2-4 m optional pillars + // 24+ → massive trunks UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Columns") - float ColumnMaxRadius = 7.0f; + float ColumnMaxRadius = 16.0f; // Hash grid cell size (higher = columns further apart). // 30 → dense grid, many candidate slots (most empty due to ColumnDensity) - // 60 → moderate spacing (good default) + // 96 → 24 m spacing (human-scale default) // 100+ → only occasional columns UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Columns") - float ColumnSpacing = 60.0f; + float ColumnSpacing = 96.0f; // ===== BOUNDARY & DENSITY ===== @@ -1265,14 +1275,14 @@ struct VOXELFORGE_API FMazeGenerationParams GENERATED_BODY() // Lattice cell size in voxels. Smaller = tighter, more claustrophobic maze. - // 25-35 → very tight warren · 40 → classic maze (default) · 60+ → roomy + // 32 → compact maze cell · 64 → 16 m fight-space cell (default) · 96+ → roomy UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Maze", meta = (ClampMin = "8.0")) - float CellSize = 40.0f; + float CellSize = 64.0f; // Corridor tube radius in voxels. Keep well below CellSize/2 to leave walls. - // 3 → crawlspace · 4 → walkable (default) · 6 → wide halls + // 4-5 → marginal passage · 8 → 2 m radius / 4 m bore (default) · 10+ → broad hall UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Maze", meta = (ClampMin = "1.0")) - float CorridorRadius = 4.0f; + float CorridorRadius = 8.0f; // Probability that a horizontal edge between adjacent cells is open (0-1). // Lower = more dead ends and a more maze-like feel; higher = more open/connected. @@ -1321,12 +1331,12 @@ struct VOXELFORGE_API FSurfaceGenerationParams // Mean ground height as a fraction of the strate height (0 = bottom, 1 = top). // The terrain heightfield varies around this baseline by ElevationRange. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape", meta = (ClampMin = "0.05", ClampMax = "0.9")) - float BaseGroundRelative = 0.30f; + float BaseGroundRelative = 0.25f; // Total vertical amplitude of the terrain heightfield in voxels (peak-to-trough-ish). - // 20 → gentle plains · 60 → hills + valleys (default) · 120+ → tall mountains + // 20 → gentle plains · 80 → 20 m hills + valleys (default) · 120+ → tall mountains UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape", meta = (ClampMin = "0.0")) - float ElevationRange = 60.0f; + float ElevationRange = 80.0f; // Low-frequency "continent" noise — broad landmasses and basins. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape") @@ -1345,9 +1355,10 @@ struct VOXELFORGE_API FSurfaceGenerationParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape") float DetailFrequency = 0.04f; - // Small-scale surface roughness in voxels (rocks, bumps). + // Small-scale surface roughness in voxels (rocks, bumps). The default 2 voxels is 0.5 m; + // its proved 0.94 m noise reach remains small beside the 20 m terrain relief. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape", meta = (ClampMin = "0.0")) - float SurfaceRoughness = 3.0f; + float SurfaceRoughness = 2.0f; // ----- Macro relief & landforms ----- @@ -1355,7 +1366,7 @@ struct VOXELFORGE_API FSurfaceGenerationParams // sampling continents/mountains. Bends straight coastlines and ridgelines into winding, // organic landforms. 0 = no warp (axis-aligned blobby noise, the old look). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "0.0")) - float HeightWarpStrength = 35.0f; + float HeightWarpStrength = 48.0f; // Frequency of the domain-warp noise. Lower = broader, sweeping bends. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro") @@ -1382,9 +1393,10 @@ struct VOXELFORGE_API FSurfaceGenerationParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "0.0", ClampMax = "1.0")) float TerraceStrength = 0.0f; - // Height of each terrace step in voxels (when TerraceStrength > 0). + // Height of each terrace step in voxels (when TerraceStrength > 0). The 8-voxel default is + // a 2 m optional landform step; TerraceStrength remains off by default. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "1.0")) - float TerraceHeight = 12.0f; + float TerraceHeight = 8.0f; // Terrace edge sharpness (0-1). 0 = soft rounded steps; 1 = crisp flat mesas with near- // vertical risers. Only matters when TerraceStrength > 0. (F20 — the plateau tops flatten @@ -1405,9 +1417,9 @@ struct VOXELFORGE_API FSurfaceGenerationParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0")) float LayerLineDepth = 0.0f; - // Vertical spacing between layer lines in voxels (band period). + // Vertical spacing between layer lines in voxels (band period). The 8-voxel default is 2 m. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "1.0")) - float LayerLineSpacing = 4.0f; + float LayerLineSpacing = 8.0f; // Cliff STEEPENING (0-1 master): where the surface is already STEEP (slope > threshold), // push the height away from the local mean so gentle slopes become sheer walls / canyon @@ -1448,13 +1460,13 @@ struct VOXELFORGE_API FSurfaceGenerationParams // at which the terrain gradient is measured (so a spot over the void can "see" the cliff). Bigger = // deeper overhangs reaching further out (and a bit more cost). ~8-20. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0")) - float OverhangReach = 12.0f; + float OverhangReach = 16.0f; // Vertical HEIGHT (voxels) of the overhang zone above the local ground — where the shelf sits above // the ground/void directly under it, AND the band ClassifyTile treats as ambiguous (so it never holes // a trivially-skipped tile). Larger = taller/higher shelves but more woken air tiles near cliffs. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0")) - float OverhangHeight = 20.0f; + float OverhangHeight = 24.0f; // Horizontal frequency of the shelf-shape noise (breaks the reach up so shelves are ragged, not a // uniform lip). Lower = broader, smoother shelves. @@ -1480,9 +1492,9 @@ struct VOXELFORGE_API FSurfaceGenerationParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Water", meta = (ClampMin = "0.0", ClampMax = "1.0")) float WaterLevelRelative = 0.27f; - // Width (voxels) of the flattened beach/shore band around the water line. + // Width (voxels) of the flattened beach/shore band around the water line. The default is 3 m. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Water", meta = (ClampMin = "0.0")) - float BeachWidth = 8.0f; + float BeachWidth = 12.0f; // ----- Sky cap & boundary ----- @@ -1491,7 +1503,7 @@ struct VOXELFORGE_API FSurfaceGenerationParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Sky", meta = (ClampMin = "0.3", ClampMax = "1.0")) float CeilingRelative = 0.95f; - // Downward bumpiness of the sky-cap ceiling (voxels). 0 = flat ceiling. + // Downward bumpiness of the sky-cap ceiling (voxels). 0 = flat ceiling; 6 = 1.5 m detail. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Sky", meta = (ClampMin = "0.0")) float CeilingRoughness = 6.0f; @@ -1564,39 +1576,41 @@ struct VOXELFORGE_API FVerticalShaftParams GENERATED_BODY() // Hash-grid cell size for shaft XY placement (voxels). Larger = shafts further apart. + // 80 voxels = 20 m, leaving room for a 4-7 m diameter shaft and a fight lane. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "10.0")) - float ShaftSpacing = 55.0f; + float ShaftSpacing = 80.0f; // Probability a grid cell contains a shaft (0-1). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0", ClampMax = "1.0")) float ShaftDensity = 0.6f; - // Shaft radius range (voxels). + // Shaft radius range (voxels): 4-7 m diameter at the human-scale default. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "1.0")) - float ShaftMinRadius = 5.0f; + float ShaftMinRadius = 8.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "1.0")) - float ShaftMaxRadius = 11.0f; + float ShaftMaxRadius = 14.0f; // Chance an adjacent pair of shafts is joined by a horizontal connector tunnel (0-1). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0", ClampMax = "1.0")) float CrossConnectChance = 0.35f; - // Radius of horizontal connector tunnels (voxels). + // Radius of horizontal connector tunnels (voxels): 1.5 m radius / 3 m bore by default. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "1.0")) - float ConnectorRadius = 4.0f; + float ConnectorRadius = 6.0f; - // Vertical spacing of ledges inside shafts (voxels). 0 = no ledges (sheer drops). + // Vertical spacing of ledges inside shafts (voxels). 32 = 8 m landing interval; + // 0 = no ledges (sheer drops). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0")) - float LedgeSpacing = 24.0f; + float LedgeSpacing = 32.0f; - // How far ledges intrude into the shaft (voxels of solid added). + // How far ledges intrude into the shaft (voxels of solid added). 4 = 1 m by default. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0")) - float LedgeDepth = 3.0f; + float LedgeDepth = 4.0f; // Small-scale wall roughness (voxels). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0")) - float SurfaceRoughness = 3.0f; + float SurfaceRoughness = 2.0f; // Solid shell thickness at strate top/bottom (voxels). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0")) @@ -1626,25 +1640,26 @@ struct VOXELFORGE_API FFloatingIslandParams { GENERATED_BODY() - // Hash-grid cell size for island placement (voxels). Larger = islands further apart. + // Hash-grid cell size for island placement (voxels). 112 = 28 m, while the largest default + // island is 24 m across, leaving a 2 m centre-to-edge gap. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "20.0")) - float IslandSpacing = 95.0f; + float IslandSpacing = 112.0f; // Probability a grid cell contains an island (0-1). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0", ClampMax = "1.0")) float IslandDensity = 0.5f; - // Island horizontal radius range (voxels). + // Island horizontal radius range (voxels): 12-24 m landmass diameters by default. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "2.0")) - float IslandMinRadius = 18.0f; + float IslandMinRadius = 24.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "2.0")) - float IslandMaxRadius = 42.0f; + float IslandMaxRadius = 48.0f; // Island vertical thickness as a fraction of its horizontal radius. - // 0.4 → thin plates · 0.7 → chunky (default) · 1.0 → near-spherical + // 0.4 → thin plates · 0.6 → substantial landmass (default) · 1.0 → near-spherical UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.1", ClampMax = "1.5")) - float ThicknessRatio = 0.7f; + float ThicknessRatio = 0.6f; // How much islands scatter vertically within the void (0 = all mid-height, 1 = full spread). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0", ClampMax = "1.0")) @@ -1656,11 +1671,11 @@ struct VOXELFORGE_API FFloatingIslandParams // Surface roughness on island shells (voxels) — craggy undersides, bumpy tops. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0")) - float SurfaceRoughness = 4.0f; + float SurfaceRoughness = 3.0f; - // SmoothMin blend radius for merging overlapping islands (voxels). + // SmoothMin blend radius for merging overlapping islands (voxels): 1.5 m by default. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0")) - float SDFBlendRadius = 5.0f; + float SDFBlendRadius = 6.0f; // Solid shell thickness at strate top/bottom (voxels). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0")) @@ -1696,27 +1711,30 @@ struct VOXELFORGE_API FStrateDisturbanceParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "0.0", ClampMax = "1.0")) float ChasmDensity = 0.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "20.0")) - float ChasmSpacing = 170.0f; + // 192 voxels = 48 m between optional chasm sites. + float ChasmSpacing = 192.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "1.0")) - float ChasmRadius = 14.0f; + float ChasmRadius = 24.0f; // --- BRIDGES: horizontal solid spans across open space --- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Bridges", meta = (ClampMin = "0.0", ClampMax = "1.0")) float BridgeDensity = 0.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Bridges", meta = (ClampMin = "20.0")) - float BridgeSpacing = 120.0f; + // 128 voxels = 32 m between optional bridge sites. + float BridgeSpacing = 128.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Bridges", meta = (ClampMin = "1.0")) - float BridgeRadius = 5.0f; + float BridgeRadius = 8.0f; // --- RIDGES: thin solid blades rising from the floor --- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Ridges", meta = (ClampMin = "0.0", ClampMax = "1.0")) float RidgeDensity = 0.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Ridges", meta = (ClampMin = "20.0")) - float RidgeSpacing = 110.0f; + // 128 voxels = 32 m between optional ridge sites. + float RidgeSpacing = 128.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Ridges", meta = (ClampMin = "0.0")) - float RidgeHeight = 30.0f; + float RidgeHeight = 32.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Ridges", meta = (ClampMin = "1.0")) - float RidgeThickness = 6.0f; + float RidgeThickness = 8.0f; // Rock solidity used to scale carve/fill strength. Match the strate's BaseDensity. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance") @@ -1768,33 +1786,36 @@ struct VOXELFORGE_API FStratePassageConfig // ----- WIDTH (tapers along the length) ----- // Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube; + // The defaults are 2 m radius / 4 m bore at mouths and 1.5 m radius / 3 m bore in the middle. // Mouth > Mid = chambers at the ends with a squeeze between; Mid > Mouth = a bulge. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0")) - float MouthRadius = 6.0f; + float MouthRadius = 8.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0")) - float MidRadius = 4.0f; + float MidRadius = 6.0f; // ----- LENGTH ----- - // How far the tunnel reaches INTO each strate (voxels). Auto-capped to the interior. + // How far the tunnel reaches INTO each strate (voxels). Auto-capped to the interior; + // the defaults span 8-24 m. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Length", meta = (ClampMin = "8.0")) - float ReachMin = 40.0f; + float ReachMin = 32.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Length", meta = (ClampMin = "8.0")) - float ReachMax = 90.0f; + float ReachMax = 96.0f; // ----- PLACEMENT ----- - // Horizontal distance range from the (0,0) spine (voxels) where tunnels may appear. + // Horizontal distance range from the (0,0) spine (voxels) where tunnels may appear; + // the defaults are 16-48 m. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Placement", meta = (ClampMin = "0.0")) - float DistanceMin = 60.0f; + float DistanceMin = 64.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Placement", meta = (ClampMin = "0.0")) - float DistanceMax = 200.0f; + float DistanceMax = 192.0f; // ----- SHAPE detail ----- - // Worm: max sideways excursion from the axis (voxels). 0 = straight even in Worm style. + // Worm: max sideways excursion from the axis (voxels). 24 = 6 m; 0 = straight even in Worm style. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0")) - float Wander = 15.0f; + float Wander = 24.0f; // Path resolution (control points). Higher = smoother curves; lower = more faceted / // cheaper. The worm makes several bends, so keep this reasonably high for smoothness. @@ -1805,19 +1826,21 @@ struct VOXELFORGE_API FStratePassageConfig UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0")) float VerticalWobble = 0.0f; - // Spiral style: helix radius (voxels) and number of full turns over the descent. + // Spiral style: helix radius (voxels) and number of full turns over the descent. The default + // radius is 6 m. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Spiral", meta = (ClampMin = "1.0")) - float SpiralRadius = 16.0f; + float SpiralRadius = 24.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Spiral", meta = (ClampMin = "0.25")) float SpiralTurns = 2.0f; - // Cascading style: number of ledge+drop steps, and how far each ledge runs (voxels). + // Cascading style: number of ledge+drop steps, and how far each ledge runs (voxels). The + // default ledge run is 2 m. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Cascade", meta = (ClampMin = "1", ClampMax = "16")) int32 CascadeSteps = 4; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Cascade", meta = (ClampMin = "0.0")) - float CascadeLedge = 14.0f; + float CascadeLedge = 8.0f; }; //=============================================================================