acdream/docs/superpowers/plans/2026-05-21-indoor-walk-miss-probe.md
Erik bb1e919ef2 docs(physics): spec + plan + findings for ISSUES #83 walk-miss probe spike
Three docs from the indoor walk-miss probe spike landed in commits
27c7284..a2e7a87:

- Spec: design of the [walk-miss] + [floor-polys] diagnostic emissions
  with the H1/H2/H3 disambiguation matrix.
- Plan: 3-task TDD implementation plan (flag, aggregator, emissions).
- Findings: live-capture analysis showing H3 (walkable_hits_sphere /
  adjust_sphere_to_plane synthesis rejection) is the dominant defect.
  817 of 876 ground-contact misses (93%) cluster at dz~0.48 m, while
  the 7 HITs all sit at dz~0.46 m — a 2 cm boundary between working
  and broken that points at the sphere-overlap math, not the probe
  distance. H1 (multi-cell iteration missing) is real but only 3%
  of misses, secondary. H2 (probe distance) ruled out.

Next step: line-by-line decomp comparison of FindWalkableInternal /
walkable_hits_sphere / adjust_sphere_to_plane against retail at
acclient_2013_pseudo_c.txt:322032 / :323006 / :326793, then design
the fix in a follow-up session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:00:11 +02:00

27 KiB

Indoor walk-miss probe — implementation plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Ship a diagnostic probe set ([walk-miss] per-miss line + [floor-polys] per-cell-load dump) that disambiguates H1/H2/H3 for ISSUES #83 in a single Holtburg capture session. No physics behavior changes.

Architecture: One new env-gated flag in PhysicsDiagnostics. One new static aggregator (WalkMissDiagnostic) that's pure-function over a CellPhysics.Resolved dict. Two emission sites: Transition.FindEnvCollisions MISS branch (per-frame) and PhysicsDataCache.CacheCellStruct (one-shot per cell).

Tech Stack: C# .NET 10, xUnit, existing PhysicsDiagnostics / ResolvedPolygon types.

Spec: docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md


File Structure

File Action Responsibility
src/AcDream.Core/Physics/PhysicsDiagnostics.cs Modify Add ProbeWalkMissEnabled flag (matches existing pattern).
src/AcDream.Core/Physics/WalkMissDiagnostic.cs Create Pure aggregator: given a CellPhysics.Resolved dict + foot local position, returns the nearest walkable-poly stats. Also enumerates walkable polys for the per-cell dump.
src/AcDream.Core/Physics/TransitionTypes.cs Modify Add [walk-miss] emission inside the existing MISS branch at the [indoor-walkable] log site (~line 1538).
src/AcDream.Core/Physics/PhysicsDataCache.cs Modify Add [floor-polys] one-shot emission immediately after the existing [cell-cache] block (~line 220).
tests/AcDream.Core.Tests/Physics/WalkMissDiagnosticTests.cs Create 3 unit tests: flag roundtrip + aggregator logic.

Task 1: PhysicsDiagnostics.ProbeWalkMissEnabled flag

Files:

  • Modify: src/AcDream.Core/Physics/PhysicsDiagnostics.cs (add property after ProbeContactPlaneEnabled at line 244)

  • Create: tests/AcDream.Core.Tests/Physics/WalkMissDiagnosticTests.cs (new file, flag roundtrip test only)

  • Step 1: Write the failing test

Create tests/AcDream.Core.Tests/Physics/WalkMissDiagnosticTests.cs:

using AcDream.Core.Physics;
using DatReaderWriter.Enums;
using System.Collections.Generic;
using System.Numerics;
using Xunit;

namespace AcDream.Core.Tests.Physics;

/// <summary>
/// Tests for the ISSUES #83 H-disambiguation probe spike (spec
/// 2026-05-21-indoor-walk-miss-probe-design.md).
///
/// Covers:
/// 1. PhysicsDiagnostics.ProbeWalkMissEnabled flag get/set roundtrip.
/// 2. WalkMissDiagnostic.AggregateNearestWalkable selects the nearest
///    walkable polygon by |dz| when the foot XY lies inside a poly's
///    local XY bounding box.
/// 3. WalkMissDiagnostic.AggregateNearestWalkable falls back to the
///    nearest poly by |dz| when no walkable poly XY-contains the foot,
///    reporting ContainsFootXY=false.
/// </summary>
public class WalkMissDiagnosticTests
{
    [Fact]
    public void ProbeWalkMiss_StaticApi_Roundtrip()
    {
        bool initial = PhysicsDiagnostics.ProbeWalkMissEnabled;
        try
        {
            PhysicsDiagnostics.ProbeWalkMissEnabled = true;
            Assert.True(PhysicsDiagnostics.ProbeWalkMissEnabled);

            PhysicsDiagnostics.ProbeWalkMissEnabled = false;
            Assert.False(PhysicsDiagnostics.ProbeWalkMissEnabled);
        }
        finally
        {
            PhysicsDiagnostics.ProbeWalkMissEnabled = initial;
        }
    }
}
  • Step 2: Run test to verify it fails

Run: dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~WalkMissDiagnosticTests"

Expected: FAIL with "PhysicsDiagnostics does not contain a definition for ProbeWalkMissEnabled".

  • Step 3: Add the flag to PhysicsDiagnostics

Edit src/AcDream.Core/Physics/PhysicsDiagnostics.cs. Insert after the ProbeContactPlaneEnabled property (line 244) and before the LogCpBoolWrite helper (line 246):

    /// <summary>
    /// Indoor walking ISSUES #83 H-disambiguation spike (2026-05-21).
    /// When true, two diagnostic emissions activate:
    /// <list type="bullet">
    ///   <item><description>One <c>[walk-miss]</c> line per
    ///     <see cref="Transition.TryFindIndoorWalkablePlane"/> MISS
    ///     event, dumping foot world/local position, the nearest
    ///     walkable polygon in the cell (with XY-containment flag and
    ///     vertical gap), and whether the LandCell terrain at the same
    ///     XY would have grounded the player.</description></item>
    ///   <item><description>One <c>[floor-polys]</c> line per indoor
    ///     cell cached, enumerating each walkable-eligible polygon's
    ///     id, normal Z, local-XY bounding box, and plane Z at the
    ///     bbox center.</description></item>
    /// </list>
    /// Together these answer H1 (multi-cell iteration missing) vs H2
    /// (probe distance too short) vs H3 (poly absent /
    /// <c>walkable_hits_sphere</c> rejection) for the ISSUES #83
    /// stuck-falling bug. Spike-only — remove once the root cause is
    /// identified and the fix lands.
    ///
    /// <para>
    /// Initial state from <c>ACDREAM_PROBE_WALK_MISS=1</c>.
    /// No DebugPanel mirror — one-shot diagnostic.
    /// </para>
    ///
    /// <para>
    /// Spec: <c>docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md</c>.
    /// </para>
    /// </summary>
    public static bool ProbeWalkMissEnabled { get; set; } =
        Environment.GetEnvironmentVariable("ACDREAM_PROBE_WALK_MISS") == "1";

  • Step 4: Run test to verify it passes

Run: dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~WalkMissDiagnosticTests"

Expected: PASS — 1 test passing.

  • Step 5: Commit
git add src/AcDream.Core/Physics/PhysicsDiagnostics.cs tests/AcDream.Core.Tests/Physics/WalkMissDiagnosticTests.cs
git commit -m "$(cat <<'EOF'
feat(physics): ProbeWalkMissEnabled flag for ISSUES #83 H-disambiguation

Adds a new diagnostic flag for the indoor-walking walk-miss probe
spike per docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md.
Env var ACDREAM_PROBE_WALK_MISS=1, runtime-toggleable via property.
No DebugPanel mirror — spike-only. Following commits wire the
[walk-miss] and [floor-polys] emissions to this flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 2: WalkMissDiagnostic aggregator

Files:

  • Create: src/AcDream.Core/Physics/WalkMissDiagnostic.cs

  • Modify: tests/AcDream.Core.Tests/Physics/WalkMissDiagnosticTests.cs (add 2 aggregator tests)

  • Step 1: Write the failing tests

Append to tests/AcDream.Core.Tests/Physics/WalkMissDiagnosticTests.cs, inside the class body (after ProbeWalkMiss_StaticApi_Roundtrip):

    private static ResolvedPolygon MakeFloorPoly(
        Vector3 v00, Vector3 v10, Vector3 v11, Vector3 v01)
    {
        var verts = new[] { v00, v10, v11, v01 };
        var normal = Vector3.Normalize(Vector3.Cross(v10 - v00, v01 - v00));
        float d = -Vector3.Dot(normal, v00);
        return new ResolvedPolygon
        {
            Vertices  = verts,
            Plane     = new System.Numerics.Plane(normal, d),
            NumPoints = 4,
            SidesType = CullMode.None,
        };
    }

    /// <summary>
    /// Foot at (0,0,1). Two walkable polys: a low one at Z=0 (foot is
    /// 1 m above) and a high one at Z=0.8 (foot is 0.2 m above).
    /// Aggregator picks the high one — smaller |dz|.
    /// </summary>
    [Fact]
    public void AggregateNearestWalkable_PicksNearestByDz_WhenFootXYInsideMultiplePolys()
    {
        var lowFloor  = MakeFloorPoly(
            new Vector3(-5f, -5f, 0f),
            new Vector3( 5f, -5f, 0f),
            new Vector3( 5f,  5f, 0f),
            new Vector3(-5f,  5f, 0f));
        var highFloor = MakeFloorPoly(
            new Vector3(-2f, -2f, 0.8f),
            new Vector3( 2f, -2f, 0.8f),
            new Vector3( 2f,  2f, 0.8f),
            new Vector3(-2f,  2f, 0.8f));

        var resolved = new Dictionary<ushort, ResolvedPolygon>
        {
            [1] = lowFloor,
            [2] = highFloor,
        };

        var result = WalkMissDiagnostic.AggregateNearestWalkable(
            resolved,
            footLocal: new Vector3(0f, 0f, 1f),
            floorZ: PhysicsGlobals.FloorZ);

        Assert.True(result.Found);
        Assert.Equal((ushort)2, result.PolyId);
        Assert.True(result.ContainsFootXY);
        Assert.Equal(0.2f, result.Dz, precision: 5);
        Assert.Equal(1.0f, result.NormalZ, precision: 5);
    }

    /// <summary>
    /// Foot at (10,10,1) — outside both poly XY bboxes. Aggregator
    /// returns the poly with smallest |dz| but with ContainsFootXY=false.
    /// </summary>
    [Fact]
    public void AggregateNearestWalkable_FallsBackByDz_WhenFootXYOutsideAllBboxes()
    {
        var poly = MakeFloorPoly(
            new Vector3(-1f, -1f, 0.5f),
            new Vector3( 1f, -1f, 0.5f),
            new Vector3( 1f,  1f, 0.5f),
            new Vector3(-1f,  1f, 0.5f));

        var resolved = new Dictionary<ushort, ResolvedPolygon> { [42] = poly };

        var result = WalkMissDiagnostic.AggregateNearestWalkable(
            resolved,
            footLocal: new Vector3(10f, 10f, 1f),
            floorZ: PhysicsGlobals.FloorZ);

        Assert.True(result.Found);
        Assert.Equal((ushort)42, result.PolyId);
        Assert.False(result.ContainsFootXY);
        Assert.Equal(0.5f, result.Dz, precision: 5);
    }
  • Step 2: Run tests to verify they fail

Run: dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~WalkMissDiagnosticTests"

Expected: FAIL — 2 new tests fail with "The name 'WalkMissDiagnostic' does not exist".

  • Step 3: Create WalkMissDiagnostic.cs with the aggregator

Create src/AcDream.Core/Physics/WalkMissDiagnostic.cs:

using System.Collections.Generic;
using System.Numerics;

namespace AcDream.Core.Physics;

/// <summary>
/// ISSUES #83 H-disambiguation spike (2026-05-21). Pure-function
/// aggregator over a <see cref="CellPhysics.Resolved"/> dict — picks
/// the nearest walkable-eligible polygon to a given foot position
/// (cell-local space) and reports XY-containment + vertical gap so
/// the <c>[walk-miss]</c> emission site can disambiguate H1/H2/H3
/// without re-walking the dictionary itself.
///
/// <para>
/// Also enumerates walkable polygons for the one-shot
/// <c>[floor-polys]</c> dump at cell-cache time.
/// </para>
///
/// <para>
/// Spec: <c>docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md</c>.
/// </para>
/// </summary>
public static class WalkMissDiagnostic
{
    public readonly struct AggregateResult
    {
        public bool   Found          { get; init; }
        public ushort PolyId         { get; init; }
        public bool   ContainsFootXY { get; init; }
        public float  Dz             { get; init; }
        public float  NormalZ        { get; init; }
    }

    public readonly struct WalkableEntry
    {
        public ushort PolyId            { get; init; }
        public float  NormalZ           { get; init; }
        public Vector3 BboxMin          { get; init; }
        public Vector3 BboxMax          { get; init; }
        public float  PlaneZAtBboxCenter { get; init; }
    }

    /// <summary>
    /// Walks <paramref name="resolved"/>, considering only polygons
    /// whose plane normal Z is at least <paramref name="floorZ"/>
    /// (walkable slope). Selection rule:
    /// <list type="number">
    ///   <item><description>Polygons whose local-XY bounding box contains
    ///     <paramref name="footLocal"/>'s XY are preferred. Among them,
    ///     the one with smallest <c>|dz|</c> wins.</description></item>
    ///   <item><description>If no poly contains the foot XY, the poly
    ///     with smallest <c>|dz|</c> across all walkable polys wins,
    ///     and <see cref="AggregateResult.ContainsFootXY"/> is false.</description></item>
    /// </list>
    /// </summary>
    public static AggregateResult AggregateNearestWalkable(
        IReadOnlyDictionary<ushort, ResolvedPolygon> resolved,
        Vector3 footLocal,
        float floorZ)
    {
        bool   bestFound          = false;
        bool   bestContainsFootXY = false;
        ushort bestPolyId         = 0;
        float  bestAbsDz          = float.MaxValue;
        float  bestSignedDz       = 0f;
        float  bestNormalZ        = 0f;

        foreach (var kvp in resolved)
        {
            var poly = kvp.Value;
            if (poly.Plane.Normal.Z < floorZ) continue;
            if (poly.Vertices.Length < 3) continue;

            // Local-XY bounding box.
            float minX = float.MaxValue, minY = float.MaxValue;
            float maxX = float.MinValue, maxY = float.MinValue;
            for (int i = 0; i < poly.Vertices.Length; i++)
            {
                var v = poly.Vertices[i];
                if (v.X < minX) minX = v.X;
                if (v.Y < minY) minY = v.Y;
                if (v.X > maxX) maxX = v.X;
                if (v.Y > maxY) maxY = v.Y;
            }
            bool containsFootXY =
                footLocal.X >= minX && footLocal.X <= maxX &&
                footLocal.Y >= minY && footLocal.Y <= maxY;

            // Signed vertical gap from foot to the polygon's plane at
            // the foot's XY: plane.D + n.x*X + n.y*Y + n.z*Z = 0
            //   ⇒ planeZ = -(D + n.x*X + n.y*Y) / n.z
            //   ⇒ dz = footZ - planeZ
            float planeZ = -(poly.Plane.D
                             + poly.Plane.Normal.X * footLocal.X
                             + poly.Plane.Normal.Y * footLocal.Y)
                           / poly.Plane.Normal.Z;
            float signedDz = footLocal.Z - planeZ;
            float absDz    = System.MathF.Abs(signedDz);

            // Preference: prefer XY-containing polys. Among the
            // preferred set, smallest |dz| wins.
            bool preferOver = !bestFound
                              || (containsFootXY && !bestContainsFootXY)
                              || (containsFootXY == bestContainsFootXY && absDz < bestAbsDz);

            if (preferOver)
            {
                bestFound          = true;
                bestContainsFootXY = containsFootXY;
                bestPolyId         = kvp.Key;
                bestAbsDz          = absDz;
                bestSignedDz       = signedDz;
                bestNormalZ        = poly.Plane.Normal.Z;
            }
        }

        return new AggregateResult
        {
            Found          = bestFound,
            PolyId         = bestPolyId,
            ContainsFootXY = bestContainsFootXY,
            Dz             = bestSignedDz,
            NormalZ        = bestNormalZ,
        };
    }

    /// <summary>
    /// Enumerates walkable-eligible polygons (normal Z &gt;= floorZ)
    /// with their local-XY bounding boxes and plane Z at the bbox
    /// center. Used by the one-shot <c>[floor-polys]</c> cell-load
    /// dump.
    /// </summary>
    public static IEnumerable<WalkableEntry> EnumerateWalkable(
        IReadOnlyDictionary<ushort, ResolvedPolygon> resolved,
        float floorZ)
    {
        foreach (var kvp in resolved)
        {
            var poly = kvp.Value;
            if (poly.Plane.Normal.Z < floorZ) continue;
            if (poly.Vertices.Length < 3) continue;

            float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue;
            float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue;
            for (int i = 0; i < poly.Vertices.Length; i++)
            {
                var v = poly.Vertices[i];
                if (v.X < minX) minX = v.X;
                if (v.Y < minY) minY = v.Y;
                if (v.Z < minZ) minZ = v.Z;
                if (v.X > maxX) maxX = v.X;
                if (v.Y > maxY) maxY = v.Y;
                if (v.Z > maxZ) maxZ = v.Z;
            }

            float cx = (minX + maxX) * 0.5f;
            float cy = (minY + maxY) * 0.5f;
            float planeZAtCenter = -(poly.Plane.D
                                     + poly.Plane.Normal.X * cx
                                     + poly.Plane.Normal.Y * cy)
                                   / poly.Plane.Normal.Z;

            yield return new WalkableEntry
            {
                PolyId             = kvp.Key,
                NormalZ            = poly.Plane.Normal.Z,
                BboxMin            = new Vector3(minX, minY, minZ),
                BboxMax            = new Vector3(maxX, maxY, maxZ),
                PlaneZAtBboxCenter = planeZAtCenter,
            };
        }
    }
}
  • Step 4: Run tests to verify all 3 pass

Run: dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~WalkMissDiagnosticTests"

Expected: PASS — 3 tests passing.

  • Step 5: Run the full Core test suite for regression

Run: dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj

Expected: PASS — no new failures vs. the pre-change baseline. (The pre-existing 8-failure physics baseline mentioned in the L.2a slice 3 ship notes is the floor; we should not introduce new failures.)

  • Step 6: Commit
git add src/AcDream.Core/Physics/WalkMissDiagnostic.cs tests/AcDream.Core.Tests/Physics/WalkMissDiagnosticTests.cs
git commit -m "$(cat <<'EOF'
feat(physics): WalkMissDiagnostic aggregator for ISSUES #83 probe spike

Pure-function aggregator that, given a CellPhysics.Resolved dict and
a foot local position, picks the nearest walkable-eligible polygon
(normal Z >= FloorZ) and reports XY-containment + signed vertical gap.
Also enumerates walkable polys with local-XY bboxes for the one-shot
[floor-polys] cell-load dump.

Pure-function, no behavior change. Wiring to emission sites lands in
the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 3: Wire emission sites — [walk-miss] + [floor-polys]

Files:

  • Modify: src/AcDream.Core/Physics/TransitionTypes.cs (~line 1538, inside the existing MISS branch)
  • Modify: src/AcDream.Core/Physics/PhysicsDataCache.cs (~line 220, immediately after the existing [cell-cache] block)

No new tests — Task 2 covers the aggregator logic; the emission lines themselves are verified by the live capture per the spec's acceptance criteria.

  • Step 1: Add the [walk-miss] emission to TransitionTypes.cs

Edit src/AcDream.Core/Physics/TransitionTypes.cs. The MISS branch already emits [indoor-walkable] ... result=MISS at ~line 1538. Immediately after that Console.WriteLine (still inside the if (PhysicsDiagnostics.ProbeIndoorBspEnabled) { ... else { ... }} block but outside its enclosing scope so it doesn't depend on the ProbeIndoorBspEnabled flag), add a new block guarded by ProbeWalkMissEnabled.

Find this code at ~line 1525-1541:

                if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
                {
                    if (walkableHit)
                    {
                        // dz = signed gap between foot and synthesized plane.
                        // ...
                        float dz = footCenter.Z + indoorPlane.D / indoorPlane.Normal.Z;
                        Console.WriteLine(System.FormattableString.Invariant(
                            $"[indoor-walkable] cell=0x{sp.CheckCellId:X8} ..."));
                    }
                    else
                    {
                        Console.WriteLine(System.FormattableString.Invariant(
                            $"[indoor-walkable] cell=0x{sp.CheckCellId:X8} wpos=({footCenter.X:F3},{footCenter.Y:F3},{footCenter.Z:F3}) probe={INDOOR_WALKABLE_PROBE_DISTANCE:F2} result=MISS"));
                    }
                }

Add directly after the closing } of the outer if (PhysicsDiagnostics.ProbeIndoorBspEnabled) block (before if (walkableHit) { return ValidateWalkable(...); } at line 1543):

                if (!walkableHit && PhysicsDiagnostics.ProbeWalkMissEnabled)
                {
                    var agg = WalkMissDiagnostic.AggregateNearestWalkable(
                        cellPhysics.Resolved,
                        footLocal: localCenter,
                        floorZ: PhysicsGlobals.FloorZ);

                    // Count walkable polys for the line (cheap re-scan; the
                    // probe is opt-in so cost is bounded to MISS frames).
                    int walkableCount = 0;
                    foreach (var kvp in cellPhysics.Resolved)
                    {
                        if (kvp.Value.Plane.Normal.Z >= PhysicsGlobals.FloorZ
                            && kvp.Value.Vertices.Length >= 3)
                            walkableCount++;
                    }

                    // Outdoor terrain probe at the same world XY — the
                    // "would multi-cell iteration have grounded us?" check.
                    var terrain = engine.SampleTerrainWalkable(footCenter.X, footCenter.Y);
                    string terrainPart;
                    if (terrain is null)
                    {
                        terrainPart = "landcell.hasTerrain=false";
                    }
                    else
                    {
                        var tp = terrain.Value.Plane;
                        float terrainZ = -(tp.D + tp.Normal.X * footCenter.X
                                                + tp.Normal.Y * footCenter.Y)
                                         / tp.Normal.Z;
                        float terrainDz = footCenter.Z - terrainZ;
                        terrainPart = System.FormattableString.Invariant(
                            $"landcell.hasTerrain=true landcell.terrainZ={terrainZ:F3} landcell.dz={terrainDz:+0.000;-0.000;+0.000}");
                    }

                    string nearestPart = agg.Found
                        ? System.FormattableString.Invariant(
                            $"nearest.polyId=0x{agg.PolyId:X4} nearest.containsFootXY={agg.ContainsFootXY} nearest.dz={agg.Dz:+0.000;-0.000;+0.000} nearest.normalZ={agg.NormalZ:F3}")
                        : "nearest=none";

                    Console.WriteLine(System.FormattableString.Invariant(
                        $"[walk-miss] cell=0x{sp.CheckCellId:X8} foot.W=({footCenter.X:F3},{footCenter.Y:F3},{footCenter.Z:F3}) foot.L=({localCenter.X:F3},{localCenter.Y:F3},{localCenter.Z:F3}) floorPolyCount={walkableCount} {nearestPart} {terrainPart}"));
                }
  • Step 2: Add the [floor-polys] emission to PhysicsDataCache.cs

Edit src/AcDream.Core/Physics/PhysicsDataCache.cs. Immediately after the closing } of the existing if (PhysicsDiagnostics.ProbeCellCacheEnabled) block at ~line 220 (the line ending with worldOrigin=(...)"));), add a new block:

        if (PhysicsDiagnostics.ProbeWalkMissEnabled)
        {
            int walkableCount = 0;
            foreach (var entry in WalkMissDiagnostic.EnumerateWalkable(
                         resolved, PhysicsGlobals.FloorZ))
                walkableCount++;

            Console.Write(System.FormattableString.Invariant(
                $"[floor-polys] cellId=0x{envCellId:X8} walkableCount={walkableCount}"));
            foreach (var entry in WalkMissDiagnostic.EnumerateWalkable(
                         resolved, PhysicsGlobals.FloorZ))
            {
                Console.Write(System.FormattableString.Invariant(
                    $" [id=0x{entry.PolyId:X4} nz={entry.NormalZ:F3} bbox=({entry.BboxMin.X:F2},{entry.BboxMin.Y:F2})..({entry.BboxMax.X:F2},{entry.BboxMax.Y:F2}) planeZ@center={entry.PlaneZAtBboxCenter:F3}]"));
            }
            Console.WriteLine();
        }
  • Step 3: Build the project

Run: dotnet build

Expected: PASS — no compile errors.

  • Step 4: Run the full Core test suite for regression

Run: dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj

Expected: PASS — no new failures vs. the pre-change baseline.

  • Step 5: Smoke-check zero-cost gate

Run a brief 5-second dotnet run launch (or rely on developer eyeball) with ACDREAM_PROBE_WALK_MISS unset and confirm no [walk-miss] / [floor-polys] lines appear in stdout. Skip this step if user explicitly asks to skip to the live capture.

  • Step 6: Commit
git add src/AcDream.Core/Physics/TransitionTypes.cs src/AcDream.Core/Physics/PhysicsDataCache.cs
git commit -m "$(cat <<'EOF'
feat(physics): [walk-miss] + [floor-polys] diagnostic emissions

Wires the WalkMissDiagnostic aggregator + flag into the two emission
sites per docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md.

- [walk-miss] (per-frame, MISS branch of TryFindIndoorWalkablePlane):
  foot world+local position, nearest walkable poly with XY-containment
  flag and vertical gap, and LandCell terrain probe at the same XY.
- [floor-polys] (one-shot per cell at cache time): walkable poly id,
  normal Z, local-XY bbox, plane Z at bbox center.

Both gated on ACDREAM_PROBE_WALK_MISS=1. No physics behavior changes.
The live capture at the Holtburg cottage doorway + inn 2nd floor +
cellar descent disambiguates H1 (multi-cell iteration), H2 (probe
distance), H3 (poly absent / walkable_hits_sphere rejection) for
ISSUES #83.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

Task 4: Live capture (manual — outside the plan's automated scope)

The probe is now ready. To collect data:

$env:ACDREAM_DAT_DIR              = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE                 = "1"
$env:ACDREAM_TEST_HOST            = "127.0.0.1"
$env:ACDREAM_TEST_PORT            = "9000"
$env:ACDREAM_TEST_USER            = "testaccount"
$env:ACDREAM_TEST_PASS            = "testpassword"
$env:ACDREAM_DEVTOOLS             = "1"
$env:ACDREAM_PROBE_WALK_MISS      = "1"
$env:ACDREAM_PROBE_INDOOR_BSP     = "1"
$env:ACDREAM_PROBE_CELL_CACHE     = "1"
dotnet build -c Debug
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
    Tee-Object -FilePath "launch-walk-miss.log"

Scenarios to walk:

  1. Cottage doorway threshold (cross in, cross out).
  2. Holtburg inn ground → upper-floor stairs → upper-floor edge walk.
  3. Cellar descent (or any single-floor → lower-floor stair pair).

Then convert log to UTF-8 for grep:

Get-Content launch-walk-miss.log -Encoding Unicode |
    Out-File launch-walk-miss.utf8.log -Encoding utf8

Aggregate the [walk-miss] lines, classify per the disambiguation matrix in the spec, write up findings in docs/research/2026-05-21-walk-miss-capture-findings.md. The fix design happens in a separate follow-up session.


Self-review checklist

  • Spec coverage: each spec component (flag, aggregator, two emissions, three tests) maps to a task step.
  • No placeholders: every step has the exact file path + the exact code to insert.
  • Type consistency: AggregateResult / WalkableEntry / property names match across Task 2 definition and Task 3 usage. WalkMissDiagnostic.AggregateNearestWalkable signature stable.
  • Test names match spec: AggregateNearestWalkable_PicksNearestByDz_WhenFootXYInsideMultiplePolys covers spec test 2 (renamed for clarity); AggregateNearestWalkable_FallsBackByDz_WhenFootXYOutsideAllBboxes covers spec test 3.
  • Commits are atomic: 3 commits, each green-tests + green-build at HEAD.
  • Acceptance criteria: live capture is Task 4 (manual), spec criteria 1-2 covered by Task 3 step 3+4, criterion 4 covered by step 5.