acdream/tests/AcDream.Core.Tests/Lighting/LightManagerTests.cs
Erik a80061b0c2 fix(G.3 A7): dungeon lighting — select 8 NEAREST lights, not viewer-in-range (#133)
The active-light selection dropped any point light whose range didn't reach the
VIEWER (DistSq > Range^2*slack -> skip). Retail's D3D-style fixed pipeline picks
the 8 NEAREST lights and applies the hard range cutoff PER SURFACE in the shader
(mesh_modern.frag: if (d < range)). The viewer-range candidacy filter suppressed
a torch whenever the player stood outside its range, so a dungeon room with 2227
registered torches lit only the ~1 the player was standing in (activeLights ~= 1,
rest of the room at flat 0.2 ambient = the "lighting off" report). Drop the filter;
take the nearest 8 regardless of viewer range. Removed the now-unused RangeSlack
const; updated the two tests that codified the old filter. Core lighting suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:35:01 +02:00

147 lines
4.5 KiB
C#

using System.Numerics;
using AcDream.Core.Lighting;
using Xunit;
namespace AcDream.Core.Tests.Lighting;
public sealed class LightManagerTests
{
private static LightSource MakePoint(Vector3 pos, float range, uint ownerId = 0, bool lit = true)
=> new LightSource
{
Kind = LightKind.Point,
WorldPosition = pos,
Range = range,
IsLit = lit,
OwnerId = ownerId,
};
[Fact]
public void Register_Unregister_TracksList()
{
var mgr = new LightManager();
var a = MakePoint(Vector3.Zero, 5f);
var b = MakePoint(new Vector3(10, 0, 0), 5f);
mgr.Register(a);
mgr.Register(b);
Assert.Equal(2, mgr.RegisteredCount);
mgr.Unregister(a);
Assert.Equal(1, mgr.RegisteredCount);
}
[Fact]
public void Register_DuplicateInstance_Idempotent()
{
var mgr = new LightManager();
var light = MakePoint(Vector3.Zero, 5f);
mgr.Register(light);
mgr.Register(light);
Assert.Equal(1, mgr.RegisteredCount);
}
[Fact]
public void Tick_SelectsByDistance_Top8()
{
var mgr = new LightManager();
// 12 lights at varying distances, all with range 100 so none filter out.
for (int i = 0; i < 12; i++)
mgr.Register(MakePoint(new Vector3(i, 0, 0), 100f));
mgr.Tick(viewerWorldPos: Vector3.Zero);
Assert.Equal(8, mgr.ActiveCount);
// Top 8 should be the closest (i=0..7).
foreach (var l in mgr.Active)
{
Assert.NotNull(l);
Assert.True(l!.WorldPosition.X <= 7f);
}
}
[Fact]
public void Tick_SelectsByDistance_RegardlessOfViewerRange()
{
// Retail D3D-style: candidacy is distance-only (the nearest 8). A torch
// lights its OWN surfaces — the shader applies the hard `d < range` cutoff
// PER FRAGMENT (mesh_modern.frag) — so a torch the VIEWER is standing
// outside the range of is still selected; it lights the wall it sits on.
// Replaces the old viewer-range candidacy filter that suppressed it, which
// left dungeon rooms (2227 registered torches) at activeLights≈1 / flat 0.2
// ambient — the "dungeon lighting off" report (#133 A7).
var mgr = new LightManager();
mgr.Register(MakePoint(new Vector3(20, 0, 0), range: 5f)); // viewer outside the torch's range
mgr.Tick(viewerWorldPos: Vector3.Zero);
Assert.Equal(1, mgr.ActiveCount); // selected by distance; the shader culls per-surface
}
[Fact]
public void Tick_IncludesNearbyLight()
{
var mgr = new LightManager();
// A nearby point light is selected (distance-only candidacy; the shader
// applies the per-fragment range cutoff).
mgr.Register(MakePoint(new Vector3(5, 0, 0), range: 5f));
mgr.Tick(viewerWorldPos: Vector3.Zero);
Assert.Equal(1, mgr.ActiveCount);
}
[Fact]
public void Tick_SunSlot0_PreservedAcrossTicks()
{
var mgr = new LightManager();
var sun = new LightSource { Kind = LightKind.Directional, WorldForward = -Vector3.UnitZ };
mgr.Sun = sun;
mgr.Register(MakePoint(Vector3.Zero, 100f));
mgr.Tick(Vector3.Zero);
Assert.Equal(2, mgr.ActiveCount);
Assert.Same(sun, mgr.Active[0]);
}
[Fact]
public void Tick_UnlitLight_Excluded()
{
var mgr = new LightManager();
var light = MakePoint(Vector3.Zero, 100f, lit: false);
mgr.Register(light);
mgr.Tick(Vector3.Zero);
Assert.Equal(0, mgr.ActiveCount);
// Toggle lit: should now appear.
light.IsLit = true;
mgr.Tick(Vector3.Zero);
Assert.Equal(1, mgr.ActiveCount);
}
[Fact]
public void UnregisterByOwner_RemovesAttachedLights()
{
var mgr = new LightManager();
mgr.Register(MakePoint(Vector3.Zero, 5f, ownerId: 42));
mgr.Register(MakePoint(new Vector3(1, 0, 0), 5f, ownerId: 42));
mgr.Register(MakePoint(new Vector3(2, 0, 0), 5f, ownerId: 99));
mgr.UnregisterByOwner(42);
Assert.Equal(1, mgr.RegisteredCount);
}
[Fact]
public void DistSq_UpdatedEachTick()
{
var mgr = new LightManager();
var light = MakePoint(new Vector3(3, 0, 4), 10f); // dist 5
mgr.Register(light);
mgr.Tick(Vector3.Zero);
Assert.Equal(25f, light.DistSq, 2);
mgr.Tick(new Vector3(3, 0, 0)); // same x, same y, z diff 4
Assert.Equal(16f, light.DistSq, 2);
}
}