From 21609a7cd7e1059fcf55adf49ce0490055605375 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 16:37:49 +0200 Subject: [PATCH] =?UTF-8?q?test(physics):=20Phase=20W=20triage=20=E2=80=94?= =?UTF-8?q?=20fix=20stale=20GetMaxSpeed=20tests;=20file=20#104=20(particle?= =?UTF-8?q?=20cell-clip=20deferral)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/ISSUES.md | 31 +++++++++++++ .../Physics/MotionInterpreterTests.cs | 45 +++++++------------ 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 54fbc6b..feaaeac 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -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? visibleCellIds` into `ParticleRenderer.BuildDrawList`, +and skips emitters whose `OwnerCellId` ∉ the visible set. That touches `IParticleSystem.SpawnEmitter`, +`ParticleSystem`, `ParticleHookSink`, and the `SpawnEmitter` call sites (~6–8 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 diff --git a/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs b/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs index 1b7d05e..251b057 100644 --- a/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs +++ b/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs @@ -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]