feat(render) Campaign FW1: port the exact retail degrade selection

UpdateViewerDistance @0x0050e030 + get_degrade @0x0051e4b0 ported with
live-pinned globals: distance measured to the part SCALED SORT CENTER,
effective = max(0, dist - s_rDegradeDistance [live 100]), level = first
with effective < IdealDist (the live auto_update_deg_mul<=0 arm), else
the last level. WalkBuildingDegradeLevel carries the full authored
bands; the adapter fills sort centers; the replay context measures to
the transformed sort center. Sweep state after the rule: the level-0
building set is now correct; the residual divergence is the per-portal
side/clip gate (my arms punch a near-complement of retail two) - the
sweep driver carries the next instrument in its Skip note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 10:55:19 +02:00
parent 844a6c097d
commit e23a589a6b
4 changed files with 41 additions and 17 deletions

View file

@ -38,8 +38,9 @@ public sealed class WalkBspNode
}
/// <summary>One degrade-ladder level: the drawing BSP of that level's
/// GfxObj (portal-only view) and the level's far distance bound.</summary>
public readonly record struct WalkBuildingDegradeLevel(float MaxDist, WalkBspNode? DrawingBsp);
/// GfxObj (portal-only view) and the level's authored distance bands.</summary>
public readonly record struct WalkBuildingDegradeLevel(
float MinDist, float IdealDist, float MaxDist, WalkBspNode? DrawingBsp);
/// <summary>The walk's building model (retail <c>CBuildingObj</c> +
/// <c>BuildInfo</c> as the frame walk consumes them).</summary>
@ -66,19 +67,36 @@ public sealed class WalkBuilding
/// skips the whole building AFTER publishing the portal list.</summary>
public bool HasGeometry = true;
/// <summary>Level selection by viewer distance. Simplified band pick
/// (first level whose MaxDist covers the distance) — the faithful
/// hysteresis of <c>CPhysicsPart::UpdateViewerDistance</c> is a port
/// TODO; still fixtures are insensitive to hysteresis.</summary>
public WalkBspNode? SelectDrawingBsp(float viewerDistance)
/// <summary>The base GfxObj's sort center — retail measures the viewer
/// distance to the part's SCALED sort center, not the position origin
/// (<c>CPhysicsPart::UpdateViewerDistance</c> @0x0050e030).</summary>
public Vector3 SortCenter;
/// <summary>Retail <c>Render::s_rDegradeDistance</c> — a subtractive
/// slack before the ladder applies (live-dumped 100 on the capture
/// client; registry-configurable).</summary>
public const float DefaultDegradeDistance = 100f;
/// <summary>
/// <c>GfxObjDegradeInfo::get_degrade</c> @0x0051e4b0 (the live client's
/// arm: <c>auto_update_deg_mul</c> with a non-positive multiplier →
/// thresholds are the levels' IDEAL distances): effective =
/// max(0, distance degradeDistance); the FIRST level with
/// effective &lt; IdealDist wins; no match → the LAST level. The
/// positive-bias arm (threshold = ideal (idealmax)·mul) is a port
/// follow-up if a fixture ever pins a positive bias.
/// </summary>
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;
}
}