test(physics): Phase W triage — fix stale GetMaxSpeed tests; file #104 (particle cell-clip deferral)

GetMaxSpeed deliberately does NOT branch on ForwardCommand — it returns RunAnimSpeed x run-rate as the InterpolationManager.AdjustOffset catch-up speed (doc comment + ACE MotionInterp.cs:670-678, retail-verified; the slow catch-up fixed the 1-Hz remote-blip). The 3 failing tests (WalkForward/WalkBackward/Idle) asserted a REMOVED command-branching design. Consolidated into one [Theory] pinning the no-branch contract across commands.

Also files #104 (LOW): scene VFX particles not clipped to the PView visible cell set — deferred out of the Phase W seal (entity bleed already gated by Stage 5; scene particles depth-tested; sky particles scissored). Needs OwnerCellId plumbing (~6-8 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-02 16:37:49 +02:00
parent 872dd34943
commit 21609a7cd7
2 changed files with 47 additions and 29 deletions

View file

@ -44,6 +44,37 @@ Copy this block when adding a new issue:
---
## #104 — Scene VFX particles not clipped to the PView visible cell set
**Status:** OPEN
**Severity:** LOW
**Filed:** 2026-06-02
**Component:** render, vfx
**Description:** Scene-pass VFX particles (spell effects, smoke) are drawn from their world-space
position only; they are not gated by the PView visible cell set, so a particle emitter in a
sealed (non-visible) cell can bleed past a wall edge. In practice this is mostly masked: scene
particles ARE depth-tested (walls occlude most of their geometry), the dominant indoor entity
bleed is already gated by the Phase W Stage 5 entity gate
(`WbDrawDispatcher.EntityPassesVisibleCellGate`), and Stage 4 already scissors the SKY particle
passes to the doorway. The residual is the occasional additive particle visible past a wall edge.
**Root cause / status:** Particles carry no cell id. `ParticleEmitter` (`Vfx/VfxModel.cs`) has
`AnchorPos` + `AttachedObjectId` but no owning-cell id; `Particle` has a world `Position` only. A
clean fix adds an `OwnerCellId` to `ParticleEmitter` (set at spawn from the owning entity's
`ParentCellId`), threads a `HashSet<uint>? visibleCellIds` into `ParticleRenderer.BuildDrawList`,
and skips emitters whose `OwnerCellId` ∉ the visible set. That touches `IParticleSystem.SpawnEmitter`,
`ParticleSystem`, `ParticleHookSink`, and the `SpawnEmitter` call sites (~68 files) — a plumbing
pass, deliberately deferred out of the Phase W seal (which covers sky/terrain/walls/entities).
**Files:** `src/AcDream.App/Rendering/ParticleRenderer.cs` (BuildDrawList), `src/AcDream.Core/Vfx/`
(ParticleSystem, VfxModel), `src/AcDream.App/Rendering/Vfx/ParticleHookSink.cs`.
**Acceptance:** A scene-particle emitter in a non-visible cell does not draw; outdoor particles
(null `visibleCellIds`) unaffected; no regression on fireplace/spell VFX in the visible cell.
---
## #103 — Phase A8.F portal-frame indoor rendering broken at runtime (visual-gate failure)
**Status:** SUPERSEDED 2026-05-30 by **Phase U (Unified Render Pipeline)**. The

View file

@ -838,40 +838,27 @@ public sealed class MotionInterpreterTests
Assert.Equal(MotionInterpreter.RunAnimSpeed * 1.5f, speed, precision: 4); // 6.0
}
[Fact]
public void GetMaxSpeed_WalkForward_ReturnsWalkAnimSpeed()
[Theory]
[InlineData(MotionCommand.WalkForward)]
[InlineData(MotionCommand.WalkBackward)]
[InlineData(MotionCommand.Ready)]
[InlineData(MotionCommand.RunForward)]
public void GetMaxSpeed_IgnoresForwardCommand_AlwaysReturnsRunRate(uint command)
{
// WalkForward max speed is always WalkAnimSpeed (3.12) — no run-rate scaling.
// GetMaxSpeed is the InterpolationManager.AdjustOffset catch-up speed — it deliberately
// returns RunAnimSpeed × run-rate REGARDLESS of the current ForwardCommand (see GetMaxSpeed's
// doc comment: the bare run rate × RunAnimSpeed, ACE MotionInterp.cs:670-678, retail-verified
// — the slow catch-up is intentional, it fixed the 1-Hz remote-blip). It does NOT branch
// per-command. These previously asserted a REMOVED command-branching design (WalkForward →
// WalkAnimSpeed, WalkBackward → ×0.65, Idle → 0); that contract no longer exists, so they are
// consolidated here to PIN the no-branch contract across commands (Phase W green-tests triage).
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
interp.MyRunRate = 1.75f;
interp.InterpretedState.ForwardCommand = command;
float speed = interp.GetMaxSpeed();
Assert.Equal(MotionInterpreter.WalkAnimSpeed, speed, precision: 4);
}
[Fact]
public void GetMaxSpeed_WalkBackward_ReturnsWalkAnimSpeedTimesBackwardsFactor()
{
// BackwardsFactor = 0.65, from adjust_motion @ 0x00528010 in the named retail decomp.
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = MotionCommand.WalkBackward;
float speed = interp.GetMaxSpeed();
Assert.Equal(MotionInterpreter.WalkAnimSpeed * 0.65f, speed, precision: 4);
}
[Fact]
public void GetMaxSpeed_Idle_ReturnsZero()
{
// Ready / non-locomotion commands → 0 (no movement speed).
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = MotionCommand.Ready;
float speed = interp.GetMaxSpeed();
Assert.Equal(0f, speed, precision: 4);
Assert.Equal(MotionInterpreter.RunAnimSpeed * 1.75f, speed, precision: 4);
}
[Fact]