wip(assembler): round L24 -- per-strate passage styles; natural Worm default; FI anchor island
Core plans inter-strate passages from each upper strate's FStratePassageConfig for every generator: Straight / Worm (the new natural default: meander, variable radius, rough walls) / Spiral / Cascading. A naturalness audit (no >8 m flat runs, >=0.20 m wall relief) fails on L23's smooth tube. FloatingIslands selects Spiral, with a deterministic unique anchor island (ordinal 2) joined to the bedrock floor by a rock root. The SW arrival no longer stamps flat shelves. Capsule audits 0 blocked / 0 unsupported / 0 grade failures, all styles. Full-view hashes deterministic W1/W8/W12, Clang = GCC (reviewer re-run). Evaluator 10, FI geometry 5. KNOWN: the FI helix renders as a spiral ribbon in open air around the pillar instead of a tunnel enclosed in rock; fix next round. Unreal edits uncompiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
1 parent
71d09a8c52
commit
cb9236c265
17 files changed
+922
-122
No files matched your search
@@ -594,12 +594,19 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
const std::int64_t inner=cfg?std::min(cfg->distanceMinimumSubvoxels,
|
||||
cfg->distanceMaximumSubvoxels):8*qPerMetre;
|
||||
const std::int64_t placementRadius=std::max<std::int64_t>(1,outer);
|
||||
const std::int64_t mouthRadius=std::max<std::int64_t>(static_cast<std::int64_t>(std::llround(1.25*qPerMetre)),
|
||||
// Leave room for the capsule and bounded wall relief at the floor/wall seam.
|
||||
const std::int64_t mouthRadius=std::max<std::int64_t>(static_cast<std::int64_t>(std::llround(1.5*qPerMetre)),
|
||||
cfg?cfg->mouthRadiusSubvoxels:0);
|
||||
const std::int64_t midRadius=std::max<std::int64_t>(mouthRadius,
|
||||
cfg?cfg->midRadiusSubvoxels:0);
|
||||
const std::int64_t reachMinimum=cfg?std::max<std::int64_t>(0,cfg->reachMinimumSubvoxels):8*qPerMetre;
|
||||
const std::int64_t reachMaximum=cfg?std::max<std::int64_t>(reachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre;
|
||||
const std::uint32_t style=cfg?std::min<std::uint32_t>(cfg->style,3u):1u;
|
||||
const std::uint32_t segments=cfg?std::clamp<std::uint32_t>(cfg->segments,16u,48u):20u;
|
||||
const std::int64_t wander=cfg?std::max<std::int64_t>(0,cfg->wanderSubvoxels):3*qPerMetre;
|
||||
const std::int64_t spiralRadius=cfg?std::max<std::int64_t>(0,cfg->spiralRadiusSubvoxels):6*qPerMetre;
|
||||
const std::int64_t spiralTurnsQ16=cfg?std::max<std::int64_t>(16384,cfg->spiralTurnsQ16):2ll<<16;
|
||||
const std::int64_t verticalWobble=cfg?std::max<std::int64_t>(0,cfg->verticalWobbleSubvoxels):0;
|
||||
const std::uint32_t connectionCount=cfg?static_cast<std::uint32_t>(std::clamp(cfg->connections,1,12)):1;
|
||||
for(std::uint32_t connection=0;connection<connectionCount;++connection)
|
||||
{
|
||||
@@ -612,6 +619,12 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
std::array<std::int64_t,3> upperMouth{},lowerMouth{};
|
||||
const bool hasUpperRoute=connection==0&&routeAnchor(upper,true,upperMouth);
|
||||
const bool hasLowerRoute=connection==0&&routeAnchor(lower,false,lowerMouth);
|
||||
const ResolvedLayoutAnchorV2* passageAnchor=nullptr;
|
||||
if(connection==0)
|
||||
for(const auto& anchor:upper.anchors)
|
||||
if(anchor.kind=="passage_anchor"&&anchor.values.size()>=7)
|
||||
{passageAnchor=&anchor;break;}
|
||||
const bool anchoredSpiral=style==2&&passageAnchor!=nullptr;
|
||||
if(!hasUpperRoute||!hasLowerRoute)
|
||||
{
|
||||
const auto xy=PassageRandomXY(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
||||
@@ -642,8 +655,22 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
const bool hasLowerTangent=hasLowerRoute
|
||||
&&routeTangent(lower,false,lowerTangent);
|
||||
|
||||
const std::int64_t ringRadius=std::max<std::int64_t>(1,
|
||||
const std::int64_t outerRingRadius=std::max<std::int64_t>(1,
|
||||
placementRadius-mouthRadius-qPerMetre/2);
|
||||
const std::uint32_t pathStyle=anchoredSpiral?1u:style;
|
||||
std::int64_t ringRadius=outerRingRadius;
|
||||
if(pathStyle==1u)
|
||||
ringRadius=std::min<std::int64_t>(outerRingRadius,
|
||||
std::max<std::int64_t>(mouthRadius+2*qPerMetre,placementRadius*5/8));
|
||||
else if(pathStyle==2u)
|
||||
ringRadius=std::clamp(spiralRadius,
|
||||
std::min(outerRingRadius,mouthRadius+qPerMetre/2),outerRingRadius);
|
||||
else if(pathStyle==3u)
|
||||
ringRadius=std::clamp(outerRingRadius
|
||||
-std::min<std::int64_t>(outerRingRadius/3,
|
||||
std::max<std::int64_t>(0,cfg?cfg->cascadeLedgeSubvoxels:0)
|
||||
*static_cast<std::int64_t>(cfg?cfg->cascadeSteps:4)),
|
||||
mouthRadius+2*qPerMetre,outerRingRadius);
|
||||
std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
|
||||
if(hasUpperRoute)
|
||||
@@ -662,28 +689,159 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{bestAlignment=alignment;phase=candidate;}
|
||||
}
|
||||
}
|
||||
const bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
||||
bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
||||
connection,0x44495245u)&1u)!=0;
|
||||
const int direction=clockwise?1:-1;
|
||||
const auto ringPoint=[&](std::uint32_t directionIndex)
|
||||
if(anchoredSpiral&&passageAnchor&&hasUpperTangent)
|
||||
{
|
||||
const double radialX=static_cast<double>(upperMouth[0]-passageAnchor->values[0]);
|
||||
const double radialY=static_cast<double>(upperMouth[1]-passageAnchor->values[1]);
|
||||
const double clockwiseTangentX=-radialY,clockwiseTangentY=radialX;
|
||||
clockwise=clockwiseTangentX*upperTangent[0]
|
||||
+clockwiseTangentY*upperTangent[1]>=0.0;
|
||||
}
|
||||
const int direction=clockwise?1:-1;
|
||||
constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree contract.
|
||||
const std::int64_t wormRadialWobble=std::min<std::int64_t>(
|
||||
std::max<std::int64_t>(0,wander/3),2*qPerMetre);
|
||||
const std::int64_t wormCenterWobble=std::min<std::int64_t>(
|
||||
std::max<std::int64_t>(0,wander/6),qPerMetre);
|
||||
const auto ringPoint=[&](std::uint32_t directionIndex,std::uint32_t loopIndex=0)
|
||||
{
|
||||
const double angle=static_cast<double>(directionIndex)*
|
||||
(3.14159265358979323846/8.0);
|
||||
double radius=static_cast<double>(ringRadius);
|
||||
double centerX=0.0,centerY=0.0;
|
||||
if(pathStyle==1u)
|
||||
{
|
||||
const double loop=static_cast<double>(loopIndex);
|
||||
const double phaseA=loop*0.6180339887498948+0.37;
|
||||
const double phaseB=loop*0.4142135623730950+1.11;
|
||||
radius+=static_cast<double>(wormRadialWobble)
|
||||
*std::sin(angle+phaseA);
|
||||
centerX=static_cast<double>(wormCenterWobble)*std::sin(phaseB);
|
||||
centerY=static_cast<double>(wormCenterWobble)*std::cos(phaseB);
|
||||
}
|
||||
else if(pathStyle==3u)
|
||||
{
|
||||
const std::uint32_t steps=std::max<std::uint32_t>(1,
|
||||
cfg?cfg->cascadeSteps:4u);
|
||||
const double ledge=std::min<double>(ringRadius/4,
|
||||
static_cast<double>(cfg?std::max<std::int64_t>(0,
|
||||
cfg->cascadeLedgeSubvoxels):0));
|
||||
radius-=ledge*(0.5-0.5*std::cos(angle*steps));
|
||||
}
|
||||
return std::array<std::int64_t,2>{
|
||||
static_cast<std::int64_t>(ringRadius)*directions[directionIndex][0]/65536,
|
||||
static_cast<std::int64_t>(ringRadius)*directions[directionIndex][1]/65536};
|
||||
static_cast<std::int64_t>(std::llround(centerX+radius*directions[directionIndex][0]/65536.0)),
|
||||
static_cast<std::int64_t>(std::llround(centerY+radius*directions[directionIndex][1]/65536.0))};
|
||||
};
|
||||
std::vector<std::array<std::int64_t,2>> xy;
|
||||
const auto pushXY=[&](std::array<std::int64_t,2> p)
|
||||
{if(xy.empty()||xy.back()!=p)xy.push_back(p);};
|
||||
const auto firstRing=ringPoint(phase);
|
||||
std::size_t helixEndIndex=0;
|
||||
std::int64_t helixBedrockZ=0;
|
||||
bool hasAnchorHelix=false;
|
||||
pushXY({upperMouth[0],upperMouth[1]});
|
||||
if(anchoredSpiral&&passageAnchor&&upper.bedrock.presentInCurrentField
|
||||
&&spiralRadius>0)
|
||||
{
|
||||
const std::int64_t anchorX=passageAnchor->values[0];
|
||||
const std::int64_t anchorY=passageAnchor->values[1];
|
||||
const std::int64_t rootRadius=passageAnchor->values[2];
|
||||
const std::int64_t rootBottom=passageAnchor->values[5];
|
||||
helixBedrockZ=rootBottom!=0?rootBottom:contentZQ(upper.bedrock.topMicrometres);
|
||||
const std::int64_t helixRadius=spiralRadius;
|
||||
if(rootRadius<helixRadius+midRadius+qPerMetre/4
|
||||
||std::hypot(static_cast<double>(upperMouth[0]-anchorX),
|
||||
static_cast<double>(upperMouth[1]-anchorY))
|
||||
>static_cast<double>(rootRadius+qPerMetre/2)
|
||||
||upperMouth[2]<=helixBedrockZ)
|
||||
{error=passage.stableId+" anchor root cannot contain its authored spiral mouth or bedrock descent";return false;}
|
||||
const double drop=static_cast<double>(upperMouth[2]-helixBedrockZ);
|
||||
const double pointsPerTurn=static_cast<double>(segments);
|
||||
const double polygonCircumference=2.0*3.14159265358979323846
|
||||
*static_cast<double>(helixRadius)
|
||||
*(std::sin(3.14159265358979323846/pointsPerTurn)
|
||||
/(3.14159265358979323846/pointsPerTurn));
|
||||
const double horizontalPerTurn=polygonCircumference
|
||||
*static_cast<double>(TargetGradeQ16)/65536.0;
|
||||
const std::uint32_t authoredTurns=static_cast<std::uint32_t>(std::max<std::int64_t>(1,
|
||||
(spiralTurnsQ16+65535)/65536));
|
||||
const std::uint32_t gradeTurns=static_cast<std::uint32_t>(std::max(1.0,
|
||||
std::ceil(drop/std::max(1.0,horizontalPerTurn))));
|
||||
// The first coil grows from the island mouth offset to the full authored
|
||||
// radius, so reserve one extra turn for that shorter opening revolution.
|
||||
const std::uint32_t turns=std::max(authoredTurns,gradeTurns+1);
|
||||
if(turns>512)
|
||||
{error=passage.stableId+" authored spiral radius needs more than 512 turns to meet walkable grade";return false;}
|
||||
const double startAngle=std::atan2(static_cast<double>(upperMouth[1]-anchorY),
|
||||
static_cast<double>(upperMouth[0]-anchorX));
|
||||
const std::uint32_t helixPoints=turns*segments;
|
||||
for(std::uint32_t point=1;point<=helixPoints;++point)
|
||||
{
|
||||
const double t=static_cast<double>(point)/helixPoints;
|
||||
const double angle=startAngle+static_cast<double>(clockwise?1:-1)
|
||||
*2.0*3.14159265358979323846*t*turns;
|
||||
const double radialT=std::min(1.0,static_cast<double>(point)/segments);
|
||||
const double mouthOffset=std::clamp(static_cast<double>(passageAnchor->values[3]),
|
||||
0.0,static_cast<double>(helixRadius));
|
||||
const double radial=mouthOffset+(static_cast<double>(helixRadius)-mouthOffset)
|
||||
*radialT;
|
||||
const std::array<std::int64_t,2> p{
|
||||
static_cast<std::int64_t>(std::llround(anchorX
|
||||
+radial*std::cos(angle))),
|
||||
static_cast<std::int64_t>(std::llround(anchorY
|
||||
+radial*std::sin(angle)))};
|
||||
pushXY(p);
|
||||
}
|
||||
helixEndIndex=xy.size()-1;
|
||||
hasAnchorHelix=helixEndIndex>0;
|
||||
}
|
||||
const std::array<std::int64_t,2> firstRing=ringPoint(phase);
|
||||
if(hasAnchorHelix&&pathStyle==1u)
|
||||
{
|
||||
// Let the anchored helix widen into the winding inter-strate route
|
||||
// over a full, smooth coil. A direct jump from the root radius to the
|
||||
// outer worm ring creates a sharp support seam inside the rock neck.
|
||||
const double anchorX=static_cast<double>(passageAnchor->values[0]);
|
||||
const double anchorY=static_cast<double>(passageAnchor->values[1]);
|
||||
const double startAngle=std::atan2(static_cast<double>(upperMouth[1])-anchorY,
|
||||
static_cast<double>(upperMouth[0])-anchorX);
|
||||
const double targetX=static_cast<double>(firstRing[0])-anchorX;
|
||||
const double targetY=static_cast<double>(firstRing[1])-anchorY;
|
||||
const double targetAngle=std::atan2(targetY,targetX);
|
||||
const double angleDelta=static_cast<double>(direction)*2.0
|
||||
*3.14159265358979323846
|
||||
+std::remainder(targetAngle-startAngle,2.0*3.14159265358979323846);
|
||||
const double startRadius=static_cast<double>(spiralRadius);
|
||||
const std::int64_t targetDx=firstRing[0]-passageAnchor->values[0];
|
||||
const std::int64_t targetDy=firstRing[1]-passageAnchor->values[1];
|
||||
const double targetRadius=static_cast<double>(PassageIsqrt(
|
||||
static_cast<std::uint64_t>(targetDx*targetDx+targetDy*targetDy)));
|
||||
const std::uint32_t transitionSegments=static_cast<std::uint32_t>(
|
||||
std::clamp(std::ceil(std::abs(angleDelta)*(startRadius+targetRadius)
|
||||
/(2.0*0.75*qPerMetre)),static_cast<double>(segments),256.0));
|
||||
for(std::uint32_t step=1;step<=transitionSegments;++step)
|
||||
{
|
||||
const double t=static_cast<double>(step)/transitionSegments;
|
||||
const double smooth=t*t*(3.0-2.0*t);
|
||||
const double radius=startRadius+(targetRadius-startRadius)*smooth;
|
||||
const double angle=startAngle+angleDelta*t;
|
||||
const std::array<std::int64_t,2> point=step==transitionSegments?firstRing:
|
||||
std::array<std::int64_t,2>{
|
||||
static_cast<std::int64_t>(std::llround(anchorX+radius*std::cos(angle))),
|
||||
static_cast<std::int64_t>(std::llround(anchorY+radius*std::sin(angle)))};
|
||||
pushXY(point);
|
||||
}
|
||||
}
|
||||
pushXY(firstRing);
|
||||
std::uint64_t totalHorizontal=0;std::uint32_t loops=0;
|
||||
const std::int64_t verticalDistance=std::abs(lowerMouth[2]-upperMouth[2]);
|
||||
constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree contract.
|
||||
const std::int64_t verticalDistance=hasAnchorHelix
|
||||
?std::abs(lowerMouth[2]-helixBedrockZ):std::abs(lowerMouth[2]-upperMouth[2]);
|
||||
auto measurePath=[&]()
|
||||
{
|
||||
std::uint64_t total=0;
|
||||
for(std::size_t i=1;i<xy.size();++i)
|
||||
const std::size_t start=hasAnchorHelix?helixEndIndex:0;
|
||||
for(std::size_t i=start+1;i<xy.size();++i)
|
||||
total+=distanceQ(xy[i][0]-xy[i-1][0],xy[i][1]-xy[i-1][1]);
|
||||
return total;
|
||||
};
|
||||
@@ -696,7 +854,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{
|
||||
int directionIndex=static_cast<int>(phase)+direction*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex)));
|
||||
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex),
|
||||
loops+(step==16?1u:0u)));
|
||||
}
|
||||
++loops;
|
||||
}
|
||||
@@ -710,7 +869,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{
|
||||
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
||||
(hashedLowerPhase+offset)%16);
|
||||
const auto terminal=ringPoint(candidate);
|
||||
const auto terminal=ringPoint(candidate,loops);
|
||||
const std::int64_t dx=lowerMouth[0]-terminal[0];
|
||||
const std::int64_t dy=lowerMouth[1]-terminal[1];
|
||||
const std::int64_t alignment=dx*lowerTangent[0]+dy*lowerTangent[1];
|
||||
@@ -728,7 +887,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{
|
||||
int directionIndex=static_cast<int>(phase)+direction*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex)));
|
||||
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex),loops));
|
||||
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
|
||||
}
|
||||
pushXY(ringPoint(lowerPhase));
|
||||
@@ -775,16 +934,33 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
for(std::size_t i=1;i<xy.size();++i)
|
||||
cumulative[i]=cumulative[i-1]+distanceQ(xy[i][0]-xy[i-1][0],xy[i][1]-xy[i-1][1]);
|
||||
const std::uint64_t mouthHorizontal=cumulative[lowerMouthIndex];
|
||||
const std::uint64_t helixHorizontal=hasAnchorHelix
|
||||
?cumulative[helixEndIndex]:0;
|
||||
if(mouthHorizontal==0)
|
||||
{error=passage.stableId+" has no horizontal reach before its lower mouth";return false;}
|
||||
if(hasAnchorHelix&&helixHorizontal==0)
|
||||
{error=passage.stableId+" anchor spiral has no horizontal reach";return false;}
|
||||
passage.controlPoints.reserve(xy.size());
|
||||
for(std::size_t i=0;i<xy.size();++i)
|
||||
{
|
||||
const std::int64_t radius=(i<=1||i+1>=xy.size()-1)?mouthRadius:midRadius;
|
||||
const std::int64_t playerZ=i<=lowerMouthIndex
|
||||
?upperMouth[2]+PassageRoundRatio(lowerMouth[2]-upperMouth[2],
|
||||
static_cast<std::int64_t>(cumulative[i]),static_cast<std::int64_t>(mouthHorizontal))
|
||||
:lowerLandingPoints[i-lowerMouthIndex-1][2];
|
||||
const std::int64_t radius=(i<=1||i==lowerMouthIndex
|
||||
||i+1>=xy.size()-1)?mouthRadius:midRadius;
|
||||
std::int64_t playerZ=0;
|
||||
if(i>lowerMouthIndex)
|
||||
playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2];
|
||||
else if(hasAnchorHelix&&i<=helixEndIndex)
|
||||
playerZ=upperMouth[2]+PassageRoundRatio(helixBedrockZ-upperMouth[2],
|
||||
static_cast<std::int64_t>(cumulative[i]),
|
||||
static_cast<std::int64_t>(helixHorizontal));
|
||||
else
|
||||
{
|
||||
const std::uint64_t segmentStart=hasAnchorHelix?helixHorizontal:0;
|
||||
const std::int64_t startZ=hasAnchorHelix?helixBedrockZ:upperMouth[2];
|
||||
const std::uint64_t segmentLength=mouthHorizontal-segmentStart;
|
||||
playerZ=startZ+PassageRoundRatio(lowerMouth[2]-startZ,
|
||||
static_cast<std::int64_t>(cumulative[i]-segmentStart),
|
||||
static_cast<std::int64_t>(segmentLength));
|
||||
}
|
||||
ResolvedLayoutPassagePointV1 point;
|
||||
point.positionSubvoxels={xy[i][0],xy[i][1],playerZ+radius-playerCenterAboveFloorQ};
|
||||
point.radiusSubvoxels=radius;passage.controlPoints.push_back(point);
|
||||
|
||||
Reference in new issue
Block a user