fix(passages): land mouths where a BODY fits -- and the cause is now cornered

VF_SuggestLandingPoint aimed at open AIR. Air is not where a body fits: a
mouth could sit in a perfectly open pocket no capsule can stand in. It now
requires a legal capsule pose -- clearance (r 1.36, half-height 3.52),
support within a 1.8-voxel / 45 cm step height, a walkable support PATCH
(4 of 5 columns, centre required, slope <= 44.8 deg), positive density
throughout the capsule, strict 2x seal interior, bounded lateral snap.

It stays a PURE free function, which was the hard constraint --
GeneratePassages runs inside Initialize while the layout is half-built. So
it cannot flood-fill, and Codex correctly did not pretend otherwise: it
aims at deterministic guaranteed features instead (room centre/floor, slab
floor, horizontal maze edge, shaft feature-core ledge pose, island top).
Both mouths query their own strate independently; no passage chaining, so
LayoutOrderIndependence stays green.

Placement measurably improved:
  Maze            comps 309 -> 82,  largest 176 -> 1217, gap 206 -> 122
  VerticalShafts  comps 112 -> 36,  largest  96 ->  619
  CrystalChamber  gap 47.5 -> 19.1, departure mouth now in a 717-cell comp
  Underwater      comps  86 -> 13

⚠️ AND THE LAW IS STILL 1/8. Reported honestly rather than tuned. Mouths
now land on legal body-sized poses; the poses just are not connected to
each other.

⇒ BY ELIMINATION, THE CAUSE IS GENERATION. Four candidates, three dead:
  size        DEAD -- Jahni walked it; bore measures 3.62 m vs 3.677 m spec
  the stencil FIXED -- fragmentation was 3x overstated by an exact-Z-1 floor
  placement   FIXED -- mouths now sit on legal poses, several in large comps
  GENERATION  STANDS -- body-sized space exists in big components that do
              not reach each other

⭐ AND THE MAZE TIES TWO OF JAHNI'S COMPLAINTS TOGETHER. Its arrival mouth
lands in a 30-cell pocket while the main network holds 1217. Edge selection
is INDEPENDENT HASH GATING per +X/+Y/+Z edge
(VoxelDensityOpStack.cpp:582/584/586) -- so the corridor graph is a random
subgraph with no connectivity guarantee, exactly the percolation failure
VerticalShafts had before its drainage tree. That is the same root cause as
"mostly a cube full of straight pathway along the edges": a spanning tree
would make it BOTH feel like a maze (dead ends, uneven runs) AND be
connected by construction. One fix, two complaints.

Codex found a real error in my spec: an infinite shaft cylinder has no
support surface AT its axis, so the old exact-axis assertion was
unsatisfiable. It now requires a deterministic feature core and audits tree
connectivity from the resolved axis.

Build clean, 29 succeeded / 0 failed / 2 known deliberate warnings.
Box audits 0 violations. Shaft refinement 16/16. Order-independence green
across 10 slots, 31 passages, 3 pool orders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-06 08:08:38 +02:00
1 parent 110ae9d0ed
commit 4378385b37
7 files changed
+1365 -165

No files matched your search

@@ -603,8 +603,8 @@ void UVoxelStrateManager::GeneratePassages()
constexpr uint32 PassageSaltBendFreq = 0xA110000Au;
int32 TotalPassages = 0;
int32 NumAimedAtUpperOpenPoint = 0;
int32 NumAimedAtLowerOpenPoint = 0;
int32 NumAimedAtUpperPlayerFit = 0;
int32 NumAimedAtLowerPlayerFit = 0;
TSet<FString> NoQueryArchetypes;
const auto ArchetypeName = [](ECaveGeneratorType Archetype)
@@ -629,8 +629,9 @@ void UVoxelStrateManager::GeneratePassages()
case ECaveGeneratorType::VerticalShafts:
// 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.
// remaining a bounded local query; the landing query still returns a pose inside
// that shaft's deterministic feature core, so this cannot create a non-topological
// mouth.
return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
case ECaveGeneratorType::FloatingIslands:
@@ -749,20 +750,18 @@ void UVoxelStrateManager::GeneratePassages()
// their source. The two queries are deliberately independent: a passage never reads
// another passage's endpoint or any live layout state.
FVector SuggestedUpperPoint = FVector::ZeroVector;
bool bAimedUpperAtOpenPoint = false;
bool bAimedUpperAtPlayerFitPoint = false;
if (UpperDef)
{
const bool bUsesRoomSeed =
UpperDef->GeneratorType == ECaveGeneratorType::TunnelNetwork
|| UpperDef->GeneratorType == ECaveGeneratorType::Underwater;
const int32 UpperQuerySeed = SeasonStrates.IsValidIndex(i)
? SeasonStrates[i].Seed
: (bUsesRoomSeed
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
static_cast<uint32>(CachedSeed), Upper.StrateIndex))
: CachedSeed);
const int32 UpperQuerySeed = bUsesRoomSeed
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
static_cast<uint32>(CachedSeed), Upper.StrateIndex))
: CachedSeed;
bAimedUpperAtOpenPoint = VF_SuggestLandingPoint(
bAimedUpperAtPlayerFitPoint = VF_SuggestLandingPoint(
UpperDef->GeneratorType,
UpperDef->GenerationParams,
UpperDef->SlabParams,
@@ -777,9 +776,9 @@ void UVoxelStrateManager::GeneratePassages()
MaxLateralSnapFor(*UpperDef),
SuggestedUpperPoint);
if (bAimedUpperAtOpenPoint)
if (bAimedUpperAtPlayerFitPoint)
{
++NumAimedAtUpperOpenPoint;
++NumAimedAtUpperPlayerFit;
}
else
{
@@ -788,22 +787,20 @@ void UVoxelStrateManager::GeneratePassages()
}
FVector SuggestedLowerPoint = FVector::ZeroVector;
bool bAimedLowerAtOpenPoint = false;
bool bAimedLowerAtPlayerFitPoint = false;
const UVoxelStrateDefinition* LowerDef = Lower.Definition;
if (LowerDef)
{
const bool bUsesRoomSeed =
LowerDef->GeneratorType == ECaveGeneratorType::TunnelNetwork
|| LowerDef->GeneratorType == ECaveGeneratorType::Underwater;
const int32 LowerQuerySeed = SeasonStrates.IsValidIndex(i + 1)
? SeasonStrates[i + 1].Seed
: (bUsesRoomSeed
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
static_cast<uint32>(CachedSeed), Lower.StrateIndex))
: CachedSeed);
const int32 LowerQuerySeed = bUsesRoomSeed
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
static_cast<uint32>(CachedSeed), Lower.StrateIndex))
: CachedSeed;
const float MaxLateralSnap = MaxLateralSnapFor(*LowerDef);
bAimedLowerAtOpenPoint = VF_SuggestLandingPoint(
bAimedLowerAtPlayerFitPoint = VF_SuggestLandingPoint(
LowerDef->GeneratorType,
LowerDef->GenerationParams,
LowerDef->SlabParams,
@@ -818,9 +815,9 @@ void UVoxelStrateManager::GeneratePassages()
MaxLateralSnap,
SuggestedLowerPoint);
if (bAimedLowerAtOpenPoint)
if (bAimedLowerAtPlayerFitPoint)
{
++NumAimedAtLowerOpenPoint;
++NumAimedAtLowerPlayerFit;
}
else
{
@@ -842,7 +839,7 @@ void UVoxelStrateManager::GeneratePassages()
Passage.RequestedUpperPoint = FVector(PX, PY, TopZ);
Passage.RequestedLowerPoint = FVector(PX, PY, BottomZ);
if (bAimedUpperAtOpenPoint)
if (bAimedUpperAtPlayerFitPoint)
{
const float UpperSealThickness = BoundarySealThicknessFor(*UpperDef);
const float InnerBottomZ = UpperBottomZ + UpperSealThickness;
@@ -868,7 +865,7 @@ void UVoxelStrateManager::GeneratePassages()
}
}
if (bAimedLowerAtOpenPoint)
if (bAimedLowerAtPlayerFitPoint)
{
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
const float LowerBottomZ = (float)(Lower.BottomChunkZ) * CHUNK_SIZE;
@@ -1031,12 +1028,12 @@ void UVoxelStrateManager::GeneratePassages()
}
UE_LOG(LogTemp, Log,
TEXT("[StrateManager] Passage landing sites: upper %d/%d and lower %d/%d aimed at generator-confirmed open space, %d/%d mouth queries fell back to random reach (archetypes with no query: %s)."),
NumAimedAtUpperOpenPoint,
TEXT("[StrateManager] Passage landing sites: upper %d/%d and lower %d/%d aimed at player-fit source space, %d/%d mouth queries fell back to random reach (archetypes with no query: %s)."),
NumAimedAtUpperPlayerFit,
TotalPassages,
NumAimedAtLowerOpenPoint,
NumAimedAtLowerPlayerFit,
TotalPassages,
(TotalPassages * 2) - NumAimedAtUpperOpenPoint - NumAimedAtLowerOpenPoint,
(TotalPassages * 2) - NumAimedAtUpperPlayerFit - NumAimedAtLowerPlayerFit,
TotalPassages * 2,
*NoQueryList);