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:
parent
872dd34943
commit
21609a7cd7
2 changed files with 47 additions and 29 deletions
|
|
@ -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 (~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)
|
## #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
|
**Status:** SUPERSEDED 2026-05-30 by **Phase U (Unified Render Pipeline)**. The
|
||||||
|
|
|
||||||
|
|
@ -838,40 +838,27 @@ public sealed class MotionInterpreterTests
|
||||||
Assert.Equal(MotionInterpreter.RunAnimSpeed * 1.5f, speed, precision: 4); // 6.0
|
Assert.Equal(MotionInterpreter.RunAnimSpeed * 1.5f, speed, precision: 4); // 6.0
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void GetMaxSpeed_WalkForward_ReturnsWalkAnimSpeed()
|
[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();
|
var interp = MakeInterp();
|
||||||
interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
|
interp.MyRunRate = 1.75f;
|
||||||
|
interp.InterpretedState.ForwardCommand = command;
|
||||||
|
|
||||||
float speed = interp.GetMaxSpeed();
|
float speed = interp.GetMaxSpeed();
|
||||||
|
|
||||||
Assert.Equal(MotionInterpreter.WalkAnimSpeed, speed, precision: 4);
|
Assert.Equal(MotionInterpreter.RunAnimSpeed * 1.75f, 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue