diff --git a/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs b/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs
new file mode 100644
index 000000000..09941107c
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs
@@ -0,0 +1,74 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.NetCode;
+using Unity.Transforms;
+
+namespace ProjectM.Server
+{
+ ///
+ /// LANTERN "light is territory" relevancy prototype (Build Spec Phase 1 step 8): per connection, marks every
+ /// ENEMY ghost OUTSIDE that player's lamp radius IRRELEVANT — so monsters in the dark aren't replicated (the
+ /// gamma test: "nothing to see"). Players + untagged/global ghosts stay relevant for free (SetIsIrrelevant).
+ ///
+ /// SHARED-SET DISCIPLINE: is the SOLE clearer of the GhostRelevancy
+ /// set — its OnUpdate runs every tick (requires only GhostRelevancy), setting the mode + clearing +
+ /// adding its region hides. This system runs [UpdateAfter(RegionRelevancySystem)] and only ADDS light
+ /// hides (never clears) via TryAdd (defensive against a ghost hidden by BOTH region and light for one
+ /// connection — a duplicate Add would throw). Gated on so it runs ONLY in the
+ /// LANTERN gym; the legacy base/expedition game keeps region relevancy alone. Runs in
+ /// before GhostSendSystem reads the set. Per-player dim/bright (a
+ /// variable radius) is the tuning follow-up; the prototype uses .
+ ///
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(GhostSimulationSystemGroup))]
+ [UpdateAfter(typeof(RegionRelevancySystem))]
+ public partial struct LightRelevancySystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ state.RequireForUpdate(); // prototype: light-as-territory runs only in the LANTERN gym
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ // Each in-game connection's player position (per-player lamp radius = the dim/bright follow-up; fixed here).
+ var connPos = new NativeHashMap(8, Allocator.Temp);
+ foreach (var (owner, lt) in
+ SystemAPI.Query, RefRO>().WithAll())
+ connPos[owner.ValueRO.NetworkId] = lt.ValueRO.Position;
+
+ if (connPos.IsEmpty) { connPos.Dispose(); return; }
+
+ // The set is already mode=SetIsIrrelevant + cleared + region-populated THIS tick by RegionRelevancySystem.
+ ref var relevancy = ref SystemAPI.GetSingletonRW().ValueRW;
+ var set = relevancy.GhostRelevancySet;
+ var conns = connPos.GetKeyValueArrays(Allocator.Temp);
+
+ float rSq = LightRelevancyMath.LampRadiusDefault * LightRelevancyMath.LampRadiusDefault;
+ foreach (var (ghost, lt) in
+ SystemAPI.Query, RefRO>().WithAll())
+ {
+ int ghostId = ghost.ValueRO.ghostId;
+ if (ghostId == 0) continue; // ghost id not assigned yet this tick
+
+ float3 gp = lt.ValueRO.Position;
+ for (int i = 0; i < conns.Keys.Length; i++)
+ {
+ if (LightRelevancyMath.IsHidden(conns.Values[i], gp, rSq))
+ set.TryAdd(new RelevantGhostForConnection { Connection = conns.Keys[i], Ghost = ghostId }, 1);
+ }
+ }
+
+ conns.Dispose();
+ connPos.Dispose();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs.meta b/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs.meta
new file mode 100644
index 000000000..fb5ed0b7b
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/LightRelevancySystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e7c6c7ce249ae9b44821e998e0331a52
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs b/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs
new file mode 100644
index 000000000..f3cf28189
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs
@@ -0,0 +1,21 @@
+using Unity.Mathematics;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Pure helpers for the LANTERN "light is territory" relevancy prototype (Build Spec Phase 1 step 8): an entity
+ /// outside a player's lamp radius isn't replicated to that player. The decision is a planar (XZ) distance test
+ /// so it's Burst-safe + unit-testable independent of the netcode GhostRelevancy set manipulation.
+ ///
+ public static class LightRelevancyMath
+ {
+ /// Prototype lamp radius (world units). Per-player dim/bright (a variable radius = the quiet/blind
+ /// trade) is the tuning follow-up; the prototype uses one fixed radius to prove the gamma test.
+ public const float LampRadiusDefault = 25f;
+
+ /// True when the ghost at is OUTSIDE the lamp centred on
+ /// (planar XZ distance beyond the radius) — i.e. hidden from that player.
+ public static bool IsHidden(float3 playerPos, float3 ghostPos, float radiusSq)
+ => math.distancesq(playerPos.xz, ghostPos.xz) > radiusSq;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs.meta b/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs.meta
new file mode 100644
index 000000000..e3ab41f1b
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/LightRelevancyMath.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e2224a38c42888d408d54961131dfff3
\ No newline at end of file
diff --git a/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs b/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs
new file mode 100644
index 000000000..2a1c3bdf5
--- /dev/null
+++ b/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs
@@ -0,0 +1,36 @@
+using NUnit.Framework;
+using ProjectM.Simulation;
+using Unity.Mathematics;
+
+namespace ProjectM.Tests
+{
+ ///
+ /// Unit tests for the LANTERN light-as-territory relevancy DECISION (LightRelevancyMath). The full per-connection
+ /// GhostRelevancy set population is netcode-world (the L3 gamma test); this pins the pure planar-distance rule.
+ ///
+ public class LightRelevancyMathTests
+ {
+ [Test]
+ public void InsideLamp_IsVisible()
+ {
+ float r = LightRelevancyMath.LampRadiusDefault;
+ Assert.IsFalse(LightRelevancyMath.IsHidden(float3.zero, new float3(r - 1f, 0f, 0f), r * r), "just inside the radius = visible");
+ Assert.IsFalse(LightRelevancyMath.IsHidden(float3.zero, float3.zero, r * r), "at the player = visible");
+ }
+
+ [Test]
+ public void OutsideLamp_IsHidden()
+ {
+ float r = LightRelevancyMath.LampRadiusDefault;
+ Assert.IsTrue(LightRelevancyMath.IsHidden(float3.zero, new float3(r + 1f, 0f, 0f), r * r), "just beyond the radius = hidden (nothing to see)");
+ }
+
+ [Test]
+ public void Planar_IgnoresY()
+ {
+ float r = 5f;
+ // Within the planar (XZ) radius but far in Y is still VISIBLE — top-down relevancy ignores height.
+ Assert.IsFalse(LightRelevancyMath.IsHidden(float3.zero, new float3(2f, 1000f, 2f), r * r), "Y is ignored (planar XZ test)");
+ }
+ }
+}
diff --git a/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs.meta b/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs.meta
new file mode 100644
index 000000000..7e8f84c68
--- /dev/null
+++ b/Assets/_Project/Tests/EditMode/LightRelevancyMathTests.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 14e936313eb9a864181228bcee28c321
\ No newline at end of file