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:
1 parent
110ae9d0ed
commit
4378385b37
7 files changed
+1365
-165
No files matched your search
@@ -54,10 +54,13 @@ namespace
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
// 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.
|
||||
// selected site's deterministic feature core/ledge-side pose.
|
||||
return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
return FMath::Max(Definition.FloatingIslandParams.IslandSpacing, 1.0f);
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater:
|
||||
return FMath::Max(Definition.GenerationParams.RoomSpacing, 1.0f);
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
@@ -85,6 +88,8 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
|
||||
int32 NumInterStratePassages = 0;
|
||||
int32 NumChecked = 0;
|
||||
int32 NumUpperChecked = 0;
|
||||
int32 NumUpperQueryFalse = 0;
|
||||
int32 NumQueryFalse = 0;
|
||||
int32 NumUnsupported = 0;
|
||||
int32 NumSupportedWithoutPoint = 0;
|
||||
@@ -144,6 +149,74 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
continue;
|
||||
}
|
||||
|
||||
// The upper mouth has its own source query too. This is deliberately evaluated from the
|
||||
// upper strate's definition, seed, bounds, and requested XY; it must never inherit the
|
||||
// lower mouth's answer or become an implicit passage chain.
|
||||
const float SourceTopZ = (float)(Source.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
const float SourceBottomZ = (float)Source.BottomChunkZ * CHUNK_SIZE;
|
||||
const int32 SourceQuerySeed = OpenPointSeedFor(
|
||||
*SourceDefinition, Source, WorldSeed);
|
||||
const float SourceMaxLateralSnap = MaxLateralSnapFor(*SourceDefinition);
|
||||
FVector SuggestedUpperPoint = FVector::ZeroVector;
|
||||
const bool bCanAnswerUpper = VF_SuggestLandingPoint(
|
||||
SourceDefinition->GeneratorType,
|
||||
SourceDefinition->GenerationParams,
|
||||
SourceDefinition->SlabParams,
|
||||
SourceDefinition->MazeParams,
|
||||
SourceDefinition->VerticalShaftParams,
|
||||
SourceDefinition->FloatingIslandParams,
|
||||
SourceQuerySeed,
|
||||
SourceTopZ,
|
||||
SourceBottomZ,
|
||||
Passage.RequestedUpperPoint.X,
|
||||
Passage.RequestedUpperPoint.Y,
|
||||
SourceMaxLateralSnap,
|
||||
SuggestedUpperPoint);
|
||||
if (!bCanAnswerUpper)
|
||||
{
|
||||
++NumUpperQueryFalse;
|
||||
FalseArchetypes.Add(static_cast<uint8>(SourceDefinition->GeneratorType));
|
||||
}
|
||||
else
|
||||
{
|
||||
++NumUpperChecked;
|
||||
const float UpperSnapDX = Passage.UpperPoint.X
|
||||
- Passage.RequestedUpperPoint.X;
|
||||
const float UpperSnapDY = Passage.UpperPoint.Y
|
||||
- Passage.RequestedUpperPoint.Y;
|
||||
const float UpperSnapDistance = FMath::Sqrt(
|
||||
UpperSnapDX * UpperSnapDX + UpperSnapDY * UpperSnapDY);
|
||||
if (!FMath::IsNearlyEqual(Passage.UpperPoint.X, SuggestedUpperPoint.X, 0.01f)
|
||||
|| !FMath::IsNearlyEqual(Passage.UpperPoint.Y, SuggestedUpperPoint.Y, 0.01f)
|
||||
|| !FMath::IsNearlyEqual(Passage.UpperPoint.Z, SuggestedUpperPoint.Z, 0.01f))
|
||||
{
|
||||
bAllAnswerableEndpointsMatchQuery = false;
|
||||
AddError(FString::Printf(
|
||||
TEXT("Passage from strate %d (%s) missed its upper player-fit landing point: endpoint (%.9g, %.9g, %.9g), query (%.9g, %.9g, %.9g)."),
|
||||
Source.StrateIndex,
|
||||
*ArchetypeName(SourceDefinition->GeneratorType),
|
||||
Passage.UpperPoint.X,
|
||||
Passage.UpperPoint.Y,
|
||||
Passage.UpperPoint.Z,
|
||||
SuggestedUpperPoint.X,
|
||||
SuggestedUpperPoint.Y,
|
||||
SuggestedUpperPoint.Z));
|
||||
}
|
||||
if (!FMath::IsFinite(UpperSnapDistance)
|
||||
|| UpperSnapDistance > SourceMaxLateralSnap + 0.01f)
|
||||
{
|
||||
bAllAnswerableEndpointsMatchQuery = false;
|
||||
AddError(FString::Printf(
|
||||
TEXT("Passage from strate %d (%s) exceeded its upper lateral snap bound: %.6f > %.6f from requested (%.3f, %.3f)."),
|
||||
Source.StrateIndex,
|
||||
*ArchetypeName(SourceDefinition->GeneratorType),
|
||||
UpperSnapDistance,
|
||||
SourceMaxLateralSnap,
|
||||
Passage.RequestedUpperPoint.X,
|
||||
Passage.RequestedUpperPoint.Y));
|
||||
}
|
||||
}
|
||||
|
||||
const FStratePassageConfig& PassageConfig = SourceDefinition->PassageConfig;
|
||||
if (!FMath::IsFinite(PassageConfig.MouthRadius) || PassageConfig.MouthRadius <= 0.0f)
|
||||
{
|
||||
@@ -377,8 +450,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Passage open-space check: %d inter-strate passages, %d checked, %d footing checks, %d/%d room/slab ring samples air, %d query-false (%d unique archetypes; %d unsupported, %d supported-but-no-point)."),
|
||||
TEXT("Passage player-fit check: %d inter-strate passages, upper %d checked (%d false), lower %d checked (%d footing checks), %d/%d room/slab ring samples air, lower %d query-false (%d unique archetypes; %d unsupported, %d supported-but-no-point)."),
|
||||
NumInterStratePassages,
|
||||
NumUpperChecked,
|
||||
NumUpperQueryFalse,
|
||||
NumChecked,
|
||||
NumFootingChecked,
|
||||
NumRingAirSamples,
|
||||
@@ -398,6 +473,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
AddError(TEXT("VACUOUS: zero answerable passage mouths were checked; the passage invariant was not exercised."));
|
||||
}
|
||||
if (NumUpperChecked == 0)
|
||||
{
|
||||
AddError(TEXT("VACUOUS: zero answerable upper passage mouths were checked; independent upper-mouth aiming was not exercised."));
|
||||
}
|
||||
if (NumFootingChecked == 0)
|
||||
{
|
||||
AddError(TEXT("VACUOUS: no Maze/VerticalShafts/FloatingIslands footing was checked."));
|
||||
@@ -461,6 +540,7 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
|
||||
return NumInterStratePassages > 0
|
||||
&& NumChecked > 0
|
||||
&& NumUpperChecked > 0
|
||||
&& NumFootingChecked > 0
|
||||
&& bAllNewFootingArchetypesAnswered
|
||||
// (fixture coverage of the room/slab archetypes is reported, not asserted — see above)
|
||||
|
||||
Reference in new issue
Block a user