diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 31118bffc..518a6c514 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -1827,7 +1827,11 @@ static void RequireDaemonRenderQueryAuthority(string root) "public static AetheriaRuntimeDaemonAsteroidInstancePose[] QueryAsteroidInstancePoses(", "public static int QueryAsteroidInstancePoses(", "TryResolveAsteroidBeltCenter(body, orbitPositions, orbits, out var center)", - "ResolveAsteroidBeltRadius(body)" + "ResolveAsteroidBeltRadius(body)", + "AetheriaRuntimeDaemonCompassMarker", + "public static AetheriaRuntimeDaemonCompassMarker[] QueryCompassMarkers(", + "public static int QueryCompassMarkers(", + "BuildEntityMap(zone)" }; var missingQuerySymbols = requiredQuerySymbols @@ -1934,6 +1938,12 @@ static void RequireDaemonRenderQueryAuthority(string root) "ZoneRenderer must reconcile rendered entities from daemon frame snapshots instead of subscribing to mirrored Unity entity collection mutations."); } + if (zoneRenderer.Contains("PerspectiveEntity.EntityInfoGathered", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "ZoneRenderer must query daemon compass markers instead of reading mirrored Unity entity visibility state."); + } + var requiredZoneRendererSymbols = new[] { "private AetheriaRuntimeZoneSnapshotCommit _daemonZoneSnapshot;", @@ -1941,6 +1951,8 @@ static void RequireDaemonRenderQueryAuthority(string root) "private readonly Dictionary _daemonBodyPosesByBodyKey", "private readonly List _daemonAsteroidBeltPoses", "private readonly Dictionary _daemonAsteroidBeltPosesByBodyKey", + "private readonly List _daemonCompassMarkers", + "private readonly Dictionary _daemonCompassMarkersByEntityIndex", "public void LoadZone(Zone zone, AetheriaRuntimeZoneSnapshotCommit daemonZone = null)", "_daemonZoneSnapshot = daemonZone;", "AetheriaRuntimeDaemonRenderQueries.EvaluateGravityTerrainHeight(", @@ -1955,6 +1967,8 @@ static void RequireDaemonRenderQueryAuthority(string root) "AetheriaRuntimeDaemonRenderQueries.QueryBodyPoses(_daemonZoneSnapshot, _daemonBodyPoses);", "AetheriaRuntimeDaemonRenderQueries.QueryAsteroidBeltPoses(_daemonZoneSnapshot, _daemonAsteroidBeltPoses);", "AetheriaRuntimeDaemonRenderQueries.QueryAsteroidInstancePoses(", + "AetheriaRuntimeDaemonRenderQueries.QueryCompassMarkers(", + "_daemonCompassMarkersByEntityIndex.TryGetValue(entityInstance.Entity.DaemonEntityIndex, out var marker)", "_daemonBodyPosesByBodyKey.TryGetValue(planet.Key, out var pose)", "_daemonAsteroidBeltPosesByBodyKey.TryGetValue(key, out var beltPose)", "pose.GravityWaveSpeed" diff --git a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs index 8a0e8a72a..bddb2ec7b 100644 --- a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs +++ b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs @@ -933,6 +933,100 @@ public class DaemonRuntimeDocumentTests Assert.AreEqual(0, poses[2].Size, 0.0001); } + [Test] + public void DaemonRenderQueriesPublishCompassMarkersFromVisibleContacts() + { + var zone = new AetheriaRuntimeZoneSnapshotCommit + { + Entities = new[] + { + new AetheriaRuntimeEntitySnapshotCommit + { + EntityIndex = 0, + PositionX = 10, + PositionZ = 20, + Contacts = new[] + { + new AetheriaRuntimeEntityContactCommit + { + TargetEntityIndex = 1, + InfoGathered = 0.8, + Visible = true, + Hostile = true + }, + new AetheriaRuntimeEntityContactCommit + { + TargetEntityIndex = 2, + InfoGathered = 0.2, + Visible = true + }, + new AetheriaRuntimeEntityContactCommit + { + TargetEntityIndex = 3, + InfoGathered = 0.9, + Visible = false + }, + new AetheriaRuntimeEntityContactCommit + { + TargetEntityIndex = 4, + InfoGathered = 0.9, + Visible = true + }, + new AetheriaRuntimeEntityContactCommit + { + TargetEntityIndex = 99, + InfoGathered = 0.9, + Visible = true + } + } + }, + new AetheriaRuntimeEntitySnapshotCommit + { + EntityIndex = 1, + PositionX = 13, + PositionZ = 24 + }, + new AetheriaRuntimeEntitySnapshotCommit + { + EntityIndex = 2, + PositionX = 100, + PositionZ = 20 + }, + new AetheriaRuntimeEntitySnapshotCommit + { + EntityIndex = 3, + PositionX = 10, + PositionZ = 200 + }, + new AetheriaRuntimeEntitySnapshotCommit + { + EntityIndex = 4, + PositionX = 11, + PositionZ = 20 + } + } + }; + + var markers = new List(); + var count = AetheriaRuntimeDaemonRenderQueries.QueryCompassMarkers( + zone, + 0, + 0.5, + 2, + markers); + + Assert.AreEqual(1, count); + Assert.AreEqual(1, markers.Count); + Assert.AreEqual(1, markers[0].TargetEntityIndex); + Assert.AreEqual(13, markers[0].PositionX, 0.0001); + Assert.AreEqual(24, markers[0].PositionZ, 0.0001); + Assert.AreEqual(3, markers[0].DeltaX, 0.0001); + Assert.AreEqual(4, markers[0].DeltaZ, 0.0001); + Assert.AreEqual(5, markers[0].Distance, 0.0001); + Assert.AreEqual(0.8, markers[0].InfoGathered, 0.0001); + Assert.IsTrue(markers[0].Hostile); + } + [Test] public void SoaViewIndexRejectsInvalidRenderGroups() { diff --git a/Assets/Scripts/Zone Display/ZoneRenderer.cs b/Assets/Scripts/Zone Display/ZoneRenderer.cs index f5c2798ed..1996780c4 100644 --- a/Assets/Scripts/Zone Display/ZoneRenderer.cs +++ b/Assets/Scripts/Zone Display/ZoneRenderer.cs @@ -94,6 +94,10 @@ public class ZoneRenderer : MonoBehaviour new Dictionary(StringComparer.Ordinal); private readonly List _visibleAsteroidInstancePoses = new List(); + private readonly List _daemonCompassMarkers = + new List(); + private readonly Dictionary _daemonCompassMarkersByEntityIndex = + new Dictionary(); public Dictionary WormholeInstances = new Dictionary(); private List _loot = new List(); @@ -471,6 +475,7 @@ public class ZoneRenderer : MonoBehaviour Shader.SetGlobalFloat("_AsteroidVerticalOffset", ActionGameManager.Instance.Settings.PlanetSettings.AsteroidVerticalOffset); RefreshDaemonBodyPoses(); RefreshDaemonAsteroidBeltPoses(); + RefreshDaemonCompassMarkers(); Plane[] planes = GeometryUtility.CalculateFrustumPlanes(MainCamera); bool isVisible(Bounds bounds) => GeometryUtility.TestPlanesAABB(planes, bounds); @@ -556,14 +561,16 @@ public class ZoneRenderer : MonoBehaviour { if(entityInstance.CompassIcon) { - var difference = entityInstance.Entity.CultPositionXZ - PerspectiveEntity.CultPositionXZ; - var distance = CultMath.math.length(difference); - - entityInstance.CompassIcon.gameObject.SetActive( - PerspectiveEntity.EntityInfoGathered.ContainsKey(entityInstance.Entity) && - PerspectiveEntity.EntityInfoGathered[entityInstance.Entity] > Settings.GameplaySettings.TargetDetectionInfoThreshold && - distance > _minimapDistance); - entityInstance.CompassIcon.transform.rotation = Quaternion.Euler(0, 0, Mathf.Atan2(difference.y, difference.x) * Mathf.Rad2Deg - 90); + var active = entityInstance.Entity != null && + _daemonCompassMarkersByEntityIndex.TryGetValue(entityInstance.Entity.DaemonEntityIndex, out var marker); + entityInstance.CompassIcon.gameObject.SetActive(active); + if (active) + { + entityInstance.CompassIcon.transform.rotation = Quaternion.Euler( + 0, + 0, + Mathf.Atan2((float)marker.DeltaZ, (float)marker.DeltaX) * Mathf.Rad2Deg - 90); + } } } @@ -616,6 +623,19 @@ public class ZoneRenderer : MonoBehaviour } } + private void RefreshDaemonCompassMarkers() + { + AetheriaRuntimeDaemonRenderQueries.QueryCompassMarkers( + _daemonZoneSnapshot, + PerspectiveEntity?.DaemonEntityIndex ?? -1, + Settings.GameplaySettings.TargetDetectionInfoThreshold, + _minimapDistance, + _daemonCompassMarkers); + _daemonCompassMarkersByEntityIndex.Clear(); + foreach (var marker in _daemonCompassMarkers) + _daemonCompassMarkersByEntityIndex[marker.TargetEntityIndex] = marker; + } + private float GetTerrainHeight(float2 position) { return (float)AetheriaRuntimeDaemonRenderQueries.EvaluateGravityTerrainHeight( diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs index 12cb4a67d..8361b9e5e 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs @@ -156,6 +156,38 @@ namespace GameCult.Aetheria.State.Verse public double Size { get; } } + public readonly struct AetheriaRuntimeDaemonCompassMarker + { + public AetheriaRuntimeDaemonCompassMarker( + int targetEntityIndex, + double positionX, + double positionZ, + double deltaX, + double deltaZ, + double distance, + double infoGathered, + bool hostile) + { + TargetEntityIndex = targetEntityIndex; + PositionX = positionX; + PositionZ = positionZ; + DeltaX = deltaX; + DeltaZ = deltaZ; + Distance = Math.Max(0, distance); + InfoGathered = infoGathered; + Hostile = hostile; + } + + public int TargetEntityIndex { get; } + public double PositionX { get; } + public double PositionZ { get; } + public double DeltaX { get; } + public double DeltaZ { get; } + public double Distance { get; } + public double InfoGathered { get; } + public bool Hostile { get; } + } + public readonly struct AetheriaRuntimeGravityTerrainBand { public AetheriaRuntimeGravityTerrainBand(double startDepth, double depthRange) @@ -358,6 +390,64 @@ namespace GameCult.Aetheria.State.Verse return poses.Count; } + public static AetheriaRuntimeDaemonCompassMarker[] QueryCompassMarkers( + AetheriaRuntimeZoneSnapshotCommit? zone, + int observerEntityIndex, + double minimumInfoGathered, + double minimumDistance) + { + var markers = new List(); + QueryCompassMarkers(zone, observerEntityIndex, minimumInfoGathered, minimumDistance, markers); + return markers.Count == 0 ? Array.Empty() : markers.ToArray(); + } + + public static int QueryCompassMarkers( + AetheriaRuntimeZoneSnapshotCommit? zone, + int observerEntityIndex, + double minimumInfoGathered, + double minimumDistance, + List markers) + { + if (markers == null) throw new ArgumentNullException(nameof(markers)); + markers.Clear(); + if (zone == null || observerEntityIndex < 0) + return 0; + + var entities = BuildEntityMap(zone); + if (!entities.TryGetValue(observerEntityIndex, out var observer)) + return 0; + + var requiredDistance = Math.Max(0, minimumDistance); + foreach (var contact in observer.Contacts ?? Array.Empty()) + { + if (contact == null || + !contact.Visible || + contact.InfoGathered <= minimumInfoGathered || + !entities.TryGetValue(contact.TargetEntityIndex, out var target)) + { + continue; + } + + var deltaX = target.PositionX - observer.PositionX; + var deltaZ = target.PositionZ - observer.PositionZ; + var distance = Math.Sqrt(deltaX * deltaX + deltaZ * deltaZ); + if (distance <= requiredDistance) + continue; + + markers.Add(new AetheriaRuntimeDaemonCompassMarker( + target.EntityIndex, + target.PositionX, + target.PositionZ, + deltaX, + deltaZ, + distance, + contact.InfoGathered, + contact.Hostile)); + } + + return markers.Count; + } + public static AetheriaRuntimeDaemonRenderGroupDocument[] QueryRenderGroups( AetheriaRuntimeDaemonSoaViewIndex? index, double minX, @@ -464,6 +554,18 @@ namespace GameCult.Aetheria.State.Verse zone.GravityTerrainDepth - startDepth + maxDepth); } + private static Dictionary BuildEntityMap(AetheriaRuntimeZoneSnapshotCommit zone) + { + var entities = new Dictionary(); + foreach (var entity in zone.Entities ?? Array.Empty()) + { + if (entity != null && entity.EntityIndex >= 0) + entities[entity.EntityIndex] = entity; + } + + return entities; + } + private static Dictionary BuildOrbitPositions(AetheriaRuntimeZoneSnapshotCommit zone) { var source = BuildOrbitMap(zone);