diff --git a/src/AcDream.App/Rendering/Walk/WalkBuilding.cs b/src/AcDream.App/Rendering/Walk/WalkBuilding.cs
index 6a4d278b..b15a18d8 100644
--- a/src/AcDream.App/Rendering/Walk/WalkBuilding.cs
+++ b/src/AcDream.App/Rendering/Walk/WalkBuilding.cs
@@ -38,8 +38,9 @@ public sealed class WalkBspNode
}
/// One degrade-ladder level: the drawing BSP of that level's
-/// GfxObj (portal-only view) and the level's far distance bound.
-public readonly record struct WalkBuildingDegradeLevel(float MaxDist, WalkBspNode? DrawingBsp);
+/// GfxObj (portal-only view) and the level's authored distance bands.
+public readonly record struct WalkBuildingDegradeLevel(
+ float MinDist, float IdealDist, float MaxDist, WalkBspNode? DrawingBsp);
/// The walk's building model (retail CBuildingObj +
/// BuildInfo as the frame walk consumes them).
@@ -66,19 +67,36 @@ public sealed class WalkBuilding
/// skips the whole building AFTER publishing the portal list.
public bool HasGeometry = true;
- /// Level selection by viewer distance. Simplified band pick
- /// (first level whose MaxDist covers the distance) — the faithful
- /// hysteresis of CPhysicsPart::UpdateViewerDistance is a port
- /// TODO; still fixtures are insensitive to hysteresis.
- public WalkBspNode? SelectDrawingBsp(float viewerDistance)
+ /// The base GfxObj's sort center — retail measures the viewer
+ /// distance to the part's SCALED sort center, not the position origin
+ /// (CPhysicsPart::UpdateViewerDistance @0x0050e030).
+ public Vector3 SortCenter;
+
+ /// Retail Render::s_rDegradeDistance — a subtractive
+ /// slack before the ladder applies (live-dumped 100 on the capture
+ /// client; registry-configurable).
+ public const float DefaultDegradeDistance = 100f;
+
+ ///
+ /// GfxObjDegradeInfo::get_degrade @0x0051e4b0 (the live client's
+ /// arm: auto_update_deg_mul with a non-positive multiplier →
+ /// thresholds are the levels' IDEAL distances): effective =
+ /// max(0, distance − degradeDistance); the FIRST level with
+ /// effective < IdealDist wins; no match → the LAST level. The
+ /// positive-bias arm (threshold = ideal − (ideal−max)·mul) is a port
+ /// follow-up if a fixture ever pins a positive bias.
+ ///
+ public WalkBspNode? SelectDrawingBsp(
+ float viewerDistance, float degradeDistance = DefaultDegradeDistance)
{
if (DegradeLevels.Length == 0) return DrawingBsp;
+ float effective = MathF.Max(0f, MathF.Abs(viewerDistance) - degradeDistance);
foreach (WalkBuildingDegradeLevel level in DegradeLevels)
{
- if (viewerDistance <= level.MaxDist)
+ if (effective < level.IdealDist)
return level.DrawingBsp;
}
- return null; // degraded out entirely
+ return DegradeLevels[^1].DrawingBsp;
}
}
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs
index 61ed50cb..2cc18a8b 100644
--- a/tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs
@@ -24,12 +24,14 @@ public sealed class WalkLookInGateSweepTests
public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent);
}
- [Fact(Skip = "FW1 adjudication driver (2026-08-30 state): the degrade-level "
- + "BSP gate killed every wrong punch — remaining deltas are the near "
- + "buildings 001a/0022 (50/28 m center distance vs the 48 m level-0 "
- + "band: port UpdateViewerDistance's sphere-adjusted distance + "
- + "hysteresis) and the ring-1 frustum boundary pair "
- + "(aab50002 extra / a9b3003c missing). Re-enable to re-adjudicate.")]
+ [Fact(Skip = "FW1 adjudication driver (2026-08-30 final state): with the "
+ + "exact get_degrade rule (slack 100, ideal thresholds, sort-center "
+ + "distance) the level-0 set is correct — the residual divergence is "
+ + "the per-portal side/clip gate: my arms punch {0017,001e,0026,0036}, "
+ + "retail punched exactly {001a,0022} — a near-complement suggesting "
+ + "one more polarity at the building-portal gate. Next instrument: "
+ + "per-portal dump for 001a (retail punches) vs 001e (retail doesn't): "
+ + "side values, plane orientation, eye side, clip result per arm.")]
public void Sweep_the_lookin_gate_decodes_against_the_street_fixture()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs
index a0821a2b..0b7f3300 100644
--- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs
@@ -114,7 +114,8 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
public float ViewerDistanceTo(WalkBuilding building)
=> Vector3.Distance(
- WorldViewpoint, Buildings[building].WorldTransform.Translation);
+ WorldViewpoint,
+ Vector3.Transform(building.SortCenter, Buildings[building].WorldTransform));
public int ClipBuildingPolygon(
WalkBuilding building, WalkPolygon polygon, int side, Span output)
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs
index fcd097e8..469b349c 100644
--- a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs
@@ -148,10 +148,12 @@ public static class WalkWorldDatAdapter
}
WalkBspNode? bsp = null;
+ Vector3 sortCenter = Vector3.Zero;
var degradeLevels = new List();
if (dats.Get(buildingInfo.ModelId) is GfxObj gfxObj)
{
bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root);
+ sortCenter = new Vector3(gfxObj.SortCenter.X, gfxObj.SortCenter.Y, gfxObj.SortCenter.Z);
if (gfxObj.DIDDegrade != 0
&& dats.Get(gfxObj.DIDDegrade)
is GfxObjDegradeInfo degradeInfo)
@@ -164,7 +166,7 @@ public static class WalkWorldDatAdapter
{
levelBsp = ConvertDrawingBsp(levelGfx, levelGfx.DrawingBSP?.Root);
}
- degradeLevels.Add(new WalkBuildingDegradeLevel(level.MaxDist, levelBsp));
+ degradeLevels.Add(new WalkBuildingDegradeLevel(level.MinDist, level.IdealDist, level.MaxDist, levelBsp));
}
}
}
@@ -180,6 +182,7 @@ public static class WalkWorldDatAdapter
Portals = portals,
DrawingBsp = bsp,
DegradeLevels = degradeLevels.ToArray(),
+ SortCenter = sortCenter,
},
worldTransform,
inverse));