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));