diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 59845185d..466b1a215 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -140,6 +140,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks Require(run != null && run.IsTutorial && run.ZoneKeys.Length == 64 && run.EntranceZoneIndex == run.CurrentZoneIndex && run.ExitZoneIndex == -1, "persisted New Game truth must be the complete authored tutorial graph, not the Terminus witness fixture"); + Require(run!.HomeZones.Length == 6 && run.HomeZones.All(entry => entry.ZoneIndex >= 0), + "persisted tutorial truth must retain every faction home landmark for the sector map"); Require(run!.AgentTasks.Length > 0 && run.AgentTasks.All(task => task.TaskType == "patrol" && task.Status == "assigned" && task.TargetOrbitKeys.Length == 4), "persisted tutorial agents must retain their assigned four-orbit fossil patrols"); @@ -306,6 +308,10 @@ internal sealed class AetheriaDaemonYmirSmokeChecks task.TaskType == AetheriaRuntimeAgentTaskTypes.Patrol && task.Status == AetheriaRuntimeAgentTaskStatuses.Assigned), "regular population must persist autonomous patrol inhabitants"); + Require(run.HomeZones.Length == 6 && run.BossZones.Length == 2 && + run.HomeZones.All(entry => entry.ZoneIndex >= 0) && + run.BossZones.All(entry => entry.ZoneIndex >= 0), + "regular persistence must retain every home and executive sector-map landmark"); Require(run.ZoneKeys.All(key => key.Contains(run.RunId, StringComparison.Ordinal)), "regular zone records must be namespaced by the generated run identity"); var entrance = node.MutableDocument( @@ -327,8 +333,9 @@ internal sealed class AetheriaDaemonYmirSmokeChecks var restored = reopened.MutableDocument(new CultRecordKey(runKey)) .ReadAsync().GetAwaiter().GetResult(); Require(restored != null && restored.GameMode == AetheriaGameSessionState.AetheriaMode && - restored.ZoneKeys.Length == 24 && !string.IsNullOrWhiteSpace(restored.CurrentEntityKey), - "regular sector selection and controlled identity must survive a hard CultCache reopen"); + restored.ZoneKeys.Length == 24 && !string.IsNullOrWhiteSpace(restored.CurrentEntityKey) && + restored.HomeZones.Length == 6 && restored.BossZones.Length == 2, + "regular sector selection, controlled identity, and map landmarks must survive a hard CultCache reopen"); } finally { @@ -768,6 +775,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks GeneratedZoneBodiesPreserveFossilHierarchy, TutorialWorldMaterializesEveryTopologyZone, GeneratedOrbitsAdvanceOrbitalWorldTruth, + SectorMapSurfacePublishesDiscoveredGraphAndVisualLevers, VolumeSurfaceKeepsNativeShaderAbiInAssetVariant, YmirMovesProjectileAndReportsStableContact, CompressedTerminusStopsAtAttentionBoundary, @@ -3127,6 +3135,126 @@ internal sealed class AetheriaDaemonYmirSmokeChecks "cargo transfer must preserve source instance temperature"); } + private static void SectorMapSurfacePublishesDiscoveredGraphAndVisualLevers() + { + var map = new AetheriaRuntimeSectorMapDocument + { + FrameId = 42, + RunId = "run.map", + CurrentZoneIndex = 1, + EntranceZoneIndex = 0, + ExitZoneIndex = 2, + DiscoveredZoneIndices = new[] { 0, 1 }, + Zones = new[] + { + new AetheriaRuntimeSectorMapZone + { + ZoneIndex = 0, + Name = "Gate", + X = -10, + Y = 5, + Entrance = true, + Discovered = true + }, + new AetheriaRuntimeSectorMapZone + { + ZoneIndex = 1, + Name = "Here", + X = 4, + Y = 7, + Current = true, + Discovered = true, + OwnerFactionIndex = 3, + FactionIndices = new[] { 3, 4 } + }, + new AetheriaRuntimeSectorMapZone + { + ZoneIndex = 2, + Name = "Unseen Exit", + X = 20, + Y = 12, + Exit = true + } + }, + Links = new[] + { + new AetheriaRuntimeSectorMapLink + { + FromZoneIndex = 0, + ToZoneIndex = 1, + Discovered = true + }, + new AetheriaRuntimeSectorMapLink + { + FromZoneIndex = 1, + ToZoneIndex = 2, + Discovered = false + } + }, + HomeZones = new[] + { + new AetheriaRuntimeFactionZoneCommit { FactionIndex = 3, ZoneIndex = 1 } + }, + BossZones = new[] + { + new AetheriaRuntimeFactionZoneCommit { FactionIndex = 3, ZoneIndex = 1 } + } + }; + + var surface = AetheriaRuntimeSectorMapSurfaceBuilder.Build( + map, + "2026-07-17T00:00:00.0000000Z"); + var graph = surface.Surface.Root.Children.Single(child => child.Kind == "graph"); + var legend = surface.Surface.Root.Children.Single(child => child.Kind == "graph.legend"); + var nodes = graph.Children.Where(child => child.Kind == "graph.node").ToArray(); + var edges = graph.Children.Where(child => child.Kind == "graph.edge").ToArray(); + var regions = graph.Children.Where(child => child.Kind == "graph.region").ToArray(); + + RequireEqual(AetheriaRuntimeSectorMapSurfaceBuilder.SurfaceId, surface.Surface.Id, + "the portable sector map must have one stable advertised surface identity"); + RequireEqual("graph", graph.Kind, + "the sector map must lower through Eve's generic graph component"); + RequireEqual(2, nodes.Length, + "the sector-map surface must not reveal an undiscovered zone through presentation state"); + RequireEqual(1, edges.Length, + "the sector-map surface must publish only discovered links whose endpoints are visible"); + Require(edges.Single().Props["critical"] == "true", + "links on the weighted entrance-to-Terminus route must carry the historical critical-path role"); + Require(nodes.Single(node => node.Props["zoneIndex"] == "1").Props["role"] == "current", + "the daemon current-zone fact must select the graph's current semantic role"); + Require(nodes.Single(node => node.Props["zoneIndex"] == "1").Props["landmarkAssetKeys"] + .Contains(AetheriaRuntimeSectorMapSurfaceBuilder.HomeIconAssetKey, StringComparison.Ordinal) && + nodes.Single(node => node.Props["zoneIndex"] == "1").Props["landmarkAssetKeys"] + .Contains(AetheriaRuntimeSectorMapSurfaceBuilder.ExecutiveIconAssetKey, StringComparison.Ordinal), + "home and executive landmarks must survive daemon persistence as independent map semantics"); + Require(!nodes.Any(node => node.Props["label"] == "Unseen Exit"), + "provider presentation must not leak undiscovered zone labels"); + RequireEqual(2, regions.Length, + "every visible faction must publish its own historical influence scalar field"); + Require(regions.Single(region => region.Props["factionIndex"] == "3").Children + .Single(sample => sample.Props["nodeId"].EndsWith(".zone.1", StringComparison.Ordinal)) + .Props["value"] == "10", + "an owning faction must contribute the fossil owner influence value at its zone"); + Require(legend.Children.Any(item => item.Props.TryGetValue("role", out var role) && role == "executive"), + "the portable map legend must retain the historical executive landmark semantics"); + Require(surface.Surface.Styles.Any(style => style.Name == "sectorMap.zone.current.labelColor") && + surface.Surface.Styles.Any(style => style.Name == "sectorMap.link.critical.width") && + surface.Surface.Styles.Any(style => style.Name == "sectorMap.reveal.linkDurationSeconds") && + surface.Surface.Styles.Any(style => style.Name == "sectorMap.zoom.maximum") && + surface.Surface.Styles.Any(style => style.Name == "sectorMap.faction.3.primary"), + "provider-owned map color, geometry, motion, and camera levers must travel with the surface"); + Require(surface.Surface.Styles.Single(style => style.Name == "sectorMap.link.width").Value == "0.002" && + surface.Surface.Styles.Single(style => style.Name == "sectorMap.influence.fillTiling").Value == "1024" && + graph.Props["backgroundAssetKey"] == AetheriaRuntimeSectorMapSurfaceBuilder.BackgroundAssetKey, + "the surface must preserve the authored scene and influence-shader values rather than generic substitutes"); + var assets = AetheriaRuntimeAssets.ProjectManifest(new AetheriaRuntimeCatalogSnapshot()); + Require(assets.Assets.Any(asset => asset.Ref.AssetKey == AetheriaRuntimeSectorMapSurfaceBuilder.BackgroundAssetKey) && + assets.Assets.Any(asset => asset.Ref.AssetKey == AetheriaRuntimeSectorMapSurfaceBuilder.InfluenceAssetKey) && + assets.Assets.Any(asset => asset.Ref.AssetKey == AetheriaRuntimeSectorMapSurfaceBuilder.HomeIconAssetKey) && + assets.Assets.Any(asset => asset.Ref.AssetKey == AetheriaRuntimeSectorMapSurfaceBuilder.ExecutiveIconAssetKey), + "provider asset delivery must include the historical galaxy, influence, and landmark assets"); + } + private static void VolumeSurfaceKeepsNativeShaderAbiInAssetVariant() { var player = Entity(0, 0, "player"); diff --git a/Aetheria.State.Daemon/AetheriaDaemonRegularRunWriter.cs b/Aetheria.State.Daemon/AetheriaDaemonRegularRunWriter.cs index b0d01aceb..d6b1889a6 100644 --- a/Aetheria.State.Daemon/AetheriaDaemonRegularRunWriter.cs +++ b/Aetheria.State.Daemon/AetheriaDaemonRegularRunWriter.cs @@ -85,6 +85,22 @@ public static class AetheriaDaemonRegularRunWriter Standing = 0 }) .ToArray(), + HomeZones = materialized.Topology.HomeZoneByFactionKey + .OrderBy(pair => pair.Key, StringComparer.Ordinal) + .Select(pair => new AetheriaFactionZoneState + { + FactionIndex = corporationIndices[pair.Key], + ZoneIndex = pair.Value + }) + .ToArray(), + BossZones = materialized.Topology.BossZoneByFactionKey + .OrderBy(pair => pair.Key, StringComparer.Ordinal) + .Select(pair => new AetheriaFactionZoneState + { + FactionIndex = corporationIndices[pair.Key], + ZoneIndex = pair.Value + }) + .ToArray(), GenerationSeed = generationSeed, CurrentEntityKey = playerEntityKey, LifecyclePhase = AetheriaRuntimeRunLifecycle.Active, diff --git a/Aetheria.State.Daemon/AetheriaDaemonTutorialRunWriter.cs b/Aetheria.State.Daemon/AetheriaDaemonTutorialRunWriter.cs index 2219f333c..18defa535 100644 --- a/Aetheria.State.Daemon/AetheriaDaemonTutorialRunWriter.cs +++ b/Aetheria.State.Daemon/AetheriaDaemonTutorialRunWriter.cs @@ -87,6 +87,22 @@ public static class AetheriaDaemonTutorialRunWriter Relationship = "Neutral", Standing = 0 }).ToArray(), + HomeZones = materialized.Topology.HomeZoneByFactionKey + .OrderBy(pair => pair.Key, StringComparer.Ordinal) + .Select(pair => new AetheriaFactionZoneState + { + FactionIndex = corporationIndices[pair.Key], + ZoneIndex = pair.Value + }) + .ToArray(), + BossZones = materialized.Topology.BossZoneByFactionKey + .OrderBy(pair => pair.Key, StringComparer.Ordinal) + .Select(pair => new AetheriaFactionZoneState + { + FactionIndex = corporationIndices[pair.Key], + ZoneIndex = pair.Value + }) + .ToArray(), GenerationSeed = GenerationSeed, CurrentEntityKey = playerEntityKey, LifecyclePhase = AetheriaRuntimeRunLifecycle.Active, diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index 72484a092..b211db0ab 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -1589,6 +1589,7 @@ static async Task PublishDaemonApiDocumentsAsync( await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.DaemonGameSurface) .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(gameSurface)) .ConfigureAwait(false); + await PublishDaemonSectorMapSurfaceAsync(node, result.Frame, inputCatalog).ConfigureAwait(false); if (!publishTopology) return; @@ -1616,6 +1617,27 @@ static async Task PublishDaemonApiDocumentsAsync( await PublishDaemonMenuSurfacesAsync(node, options, result.Frame).ConfigureAwait(false); } +static async Task PublishDaemonSectorMapSurfaceAsync( + AetheriaStateNode node, + AetheriaRuntimeDaemonFrameDocument frame, + AetheriaRuntimeCatalogSnapshot catalog) +{ + if (frame == null) + return; + + var updatedAtUtc = string.IsNullOrWhiteSpace(frame.PublishedAtUtc) + ? DateTimeOffset.UtcNow.ToString("O") + : frame.PublishedAtUtc; + var surface = AetheriaRuntimeSectorMapSurfaceBuilder.Build( + AetheriaRuntimeGameDocuments.SectorMap(frame), + updatedAtUtc, + frame.FrameId, + catalog); + await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.MapMenuSurface) + .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(surface)) + .ConfigureAwait(false); +} + static EveProviderAdvertisementDocument BuildCoreProviderAdvertisement( AetheriaDaemonHostOptions options, string updatedAtUtc) @@ -1630,6 +1652,16 @@ static EveProviderAdvertisementDocument BuildCoreProviderAdvertisement( AetheriaRuntimeVerseRecordKeys.EveAssetCatalog.ToString(), new[] { "unity-scene", "web-reference", "electron-shell", "tui" }, "provider-owns-world-state-command-acceptance-and-receipts"); + var mapInteraction = new EveWorldInteractionAdvertisement( + "provider-authored-map-surface", + new[] { AetheriaRuntimeDaemonSchemas.SectorMap }, + "", + "", + "", + "", + AetheriaRuntimeVerseRecordKeys.EveAssetCatalog.ToString(), + new[] { "unity-scene", "web-reference", "electron-shell" }, + "provider-owns-topology-discovery-landmarks-influence-and-assets"); return new EveProviderAdvertisementDocument( "aetheria.daemon", options.DaemonId, @@ -1664,7 +1696,15 @@ static EveProviderAdvertisementDocument BuildCoreProviderAdvertisement( "cultmesh-record", "active", "interactive-world", - interaction) + interaction), + new EveAdvertisedSurface( + AetheriaRuntimeSectorMapSurfaceBuilder.SurfaceId, + EveSurfaceDocument.SchemaId, + AetheriaRuntimeVerseRecordKeys.MapMenuSurface.ToString(), + "cultmesh-record", + "active", + "graph", + mapInteraction) }, Array.Empty(), new[] { AetheriaRuntimeDaemonSoaFramePublisher.ProducerId }); @@ -1772,12 +1812,6 @@ static async Task PublishDaemonMenuSurfacesAsync( stationRefit, dropdownRequest, updatedAtUtc); - var mapMenu = AetheriaRuntimeZoneDetailsSurfaceBuilder.BuildFromDocuments( - AetheriaRuntimeGameDocuments.ZoneDetails(frame, currentEntity.ZoneIndex), - AetheriaRuntimeGameDocuments.SectorMap(frame), - catalog, - playerSettings, - updatedAtUtc); var tradeMenu = BuildTradeMenuSurface(stationRefit, catalog, updatedAtUtc, frame.FrameId); await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.MainMenuSurface) @@ -1801,9 +1835,6 @@ static async Task PublishDaemonMenuSurfacesAsync( await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.InventoryDropdownSurface) .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(inventoryDropdown)) .ConfigureAwait(false); - await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.MapMenuSurface) - .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(mapMenu)) - .ConfigureAwait(false); await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.TradeMenuSurface) .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(tradeMenu)) .ConfigureAwait(false); @@ -2076,7 +2107,7 @@ static async Task PublishOdinSurfaceAnnouncementsAsync( ("aetheria.main_menu.verse_settings", "Main Menu Verse Settings", AetheriaRuntimeVerseRecordKeys.MainMenuVerseSettingsSurface), ("aetheria.inventory.panel", "Inventory Panel", AetheriaRuntimeVerseRecordKeys.InventoryPanelSurface), ("aetheria.inventory.panel.dropdown", "Inventory Dropdown", AetheriaRuntimeVerseRecordKeys.InventoryDropdownSurface), - ("aetheria.map.zone_details", "Map Menu", AetheriaRuntimeVerseRecordKeys.MapMenuSurface), + (AetheriaRuntimeSectorMapSurfaceBuilder.SurfaceId, "Sector Map", AetheriaRuntimeVerseRecordKeys.MapMenuSurface), ("aetheria.trade.menu", "Trade Menu", AetheriaRuntimeVerseRecordKeys.TradeMenuSurface) }; @@ -2618,6 +2649,20 @@ static async Task ReadRuntimeRunCheckpointA FactionKey = relationship.FactionKey ?? "" }) .ToArray(), + HomeZones = (run.HomeZones ?? Array.Empty()) + .Select(entry => new AetheriaRuntimeFactionZoneCommit + { + FactionIndex = entry.FactionIndex, + ZoneIndex = entry.ZoneIndex + }) + .ToArray(), + BossZones = (run.BossZones ?? Array.Empty()) + .Select(entry => new AetheriaRuntimeFactionZoneCommit + { + FactionIndex = entry.FactionIndex, + ZoneIndex = entry.ZoneIndex + }) + .ToArray(), GenerationSeed = run.GenerationSeed, CurrentEntityKey = run.CurrentEntityKey ?? "", LifecyclePhase = string.IsNullOrWhiteSpace(run.LifecyclePhase) diff --git a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs index 1c861cbd8..fa322a91e 100644 --- a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs +++ b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs @@ -256,6 +256,19 @@ public sealed class AetheriaRunState [Key(17)] public string GameMode { get; set; } = AetheriaGameSessionState.AetheriaMode; + + [Key(18)] + public AetheriaFactionZoneState[] HomeZones { get; set; } = []; + + [Key(19)] + public AetheriaFactionZoneState[] BossZones { get; set; } = []; +} + +[MessagePackObject] +public sealed class AetheriaFactionZoneState +{ + [Key(0)] public int FactionIndex { get; set; } = -1; + [Key(1)] public int ZoneIndex { get; set; } = -1; } [MessagePackObject] diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs index ff1fad48e..1cdb6398c 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs @@ -40,6 +40,7 @@ namespace GameCult.Aetheria.State.Verse Add(entries, EnvironmentStardustComputeShader()); Add(entries, EnvironmentStardustMaterial()); Add(entries, EnvironmentStardustColorTexture()); + foreach (var mapAsset in SectorMapAssets()) Add(entries, mapAsset); Add(entries, MapIcon("entity.player", "Player", "Sprites/Icons/Stroked/Ship")); Add(entries, MapIcon("entity.ship", "Ship", "Sprites/Icons/Stroked/Ship")); Add(entries, MapIcon("entity.orbital", "Orbital", "Sprites/Icons/Stroked/orbital")); @@ -535,6 +536,39 @@ namespace GameCult.Aetheria.State.Verse }; } + private static IReadOnlyList SectorMapAssets() => + new[] + { + MapPresentationAsset( + "material.sector-map.background", + AetheriaRuntimeAssetKinds.Material, + "Sector Map Galaxy", + "Assets/Materials/UI/Galaxy.mat", + "sector-map.background"), + MapPresentationAsset( + "material.sector-map.influence", + AetheriaRuntimeAssetKinds.Material, + "Sector Map Influence", + "Assets/Materials/UI/Influence Map.mat", + "sector-map.influence"), + MapIcon("sector-map.icon.start", "Start", "Sprites/Icons/enter"), + MapIcon("sector-map.icon.terminus", "Terminus", "Sprites/Icons/exit"), + MapIcon("sector-map.icon.home", "Home Zone", "Sprites/Icons/Home"), + MapIcon("sector-map.icon.executive", "Executive", "Sprites/Icons/boss") + }; + + private static AetheriaRuntimeAssetManifestEntry MapPresentationAsset( + string key, + string kind, + string label, + string unityAssetPath, + string presentationRole) + { + var entry = MapProjectAsset(key, kind, label, unityAssetPath, presentationRole); + entry.Tags = new[] { "presentation", "sector-map", label ?? "" }; + return entry; + } + private static IReadOnlyList ThermalPresentationProfiles() => new[] { diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs index 9dc202a21..adef5c240 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs @@ -125,7 +125,7 @@ namespace GameCult.Aetheria.State.Verse Surface(AetheriaRuntimeLoadoutTemplateCommands.SurfaceId, "Loadout Templates", "", "player", "Loadout template save surface.", status: "client-contextual"), Surface(AetheriaRuntimeClientTargetCommands.SurfaceId, "Client Target", "", "operator", "Local client target and Verse discovery surface.", status: "client-contextual"), Surface(AetheriaRuntimeLocalStorySurfaceBuilder.SurfaceId, "Local Story", "", "player", "Local runtime story/menu surface.", status: "client-contextual"), - Surface(AetheriaRuntimeZoneDetailsSurfaceBuilder.SurfaceId, "Map Menu", AetheriaRuntimeVerseRecordKeys.MapMenuSurface.ToString(), "player", "Current-zone map details surface.", transport: "cultmesh-record"), + Surface(AetheriaRuntimeSectorMapSurfaceBuilder.SurfaceId, "Sector Map", AetheriaRuntimeVerseRecordKeys.MapMenuSurface.ToString(), "player", "Daemon-owned discovered sector graph with provider-owned presentation tokens.", transport: "cultmesh-record"), Surface(AetheriaRuntimeInventoryDropdownSurfaceBuilder.SurfaceId, "Inventory Dropdown", AetheriaRuntimeVerseRecordKeys.InventoryDropdownSurface.ToString(), "player", "Inventory navigation dropdown surface.", transport: "cultmesh-record"), Surface(AetheriaRuntimeShipSettingsSurfaceBuilder.SurfaceId, "Current Ship Settings", "", "player", "Current ship settings surface.", status: "contextual"), Surface(AetheriaRuntimeCargoItemDetailsSurfaceBuilder.SurfaceId, "Cargo Item Details", "", "player", "Selected cargo item details surface.", status: "contextual"), diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameDocuments.cs index 6e141a8b6..3dc0ee147 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameDocuments.cs @@ -834,6 +834,12 @@ namespace GameCult.Aetheria.State.Verse FactionRelationships = (run.FactionRelationships ?? Array.Empty()) .Where(relationship => relationship != null) .ToArray(), + HomeZones = (run.HomeZones ?? Array.Empty()) + .Where(entry => entry != null) + .ToArray(), + BossZones = (run.BossZones ?? Array.Empty()) + .Where(entry => entry != null) + .ToArray(), DiscoveredZoneIndices = discovered.OrderBy(index => index).ToArray(), Zones = zones .OrderBy(zone => zone.ZoneIndex) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs index d89c3e1e5..22b04888f 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs @@ -751,6 +751,14 @@ namespace GameCult.Aetheria.State.Verse [Key(13)] public IReadOnlyList FactionRelationships { get; set; } = Array.Empty(); + + [Key(14)] + public IReadOnlyList HomeZones { get; set; } = + Array.Empty(); + + [Key(15)] + public IReadOnlyList BossZones { get; set; } = + Array.Empty(); } [MessagePackObject] diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSectorMapSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSectorMapSurfaceBuilder.cs new file mode 100644 index 000000000..186f98f88 --- /dev/null +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSectorMapSurfaceBuilder.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace GameCult.Aetheria.State.Verse +{ + /// + /// Projects daemon-owned sector truth into the historical Aetheria map + /// vocabulary without assigning gameplay authority to a renderer. + /// + public static class AetheriaRuntimeSectorMapSurfaceBuilder + { + public const string SurfaceId = "aetheria.sector_map"; + public const string BackgroundAssetKey = "material.sector-map.background"; + public const string InfluenceAssetKey = "material.sector-map.influence"; + public const string StartIconAssetKey = "map.sector-map.icon.start"; + public const string TerminusIconAssetKey = "map.sector-map.icon.terminus"; + public const string HomeIconAssetKey = "map.sector-map.icon.home"; + public const string ExecutiveIconAssetKey = "map.sector-map.icon.executive"; + + public static AetheriaRuntimeSurfaceDocument Build( + AetheriaRuntimeSectorMapDocument sectorMap, + string updatedAtUtc, + long version = 1, + AetheriaRuntimeCatalogSnapshot? catalog = null) + { + sectorMap ??= new AetheriaRuntimeSectorMapDocument(); + updatedAtUtc ??= ""; + + var discovered = new HashSet(sectorMap.DiscoveredZoneIndices ?? Array.Empty()); + foreach (var zone in sectorMap.Zones ?? Array.Empty()) + { + if (zone != null && zone.Discovered) + discovered.Add(zone.ZoneIndex); + } + if (sectorMap.CurrentZoneIndex >= 0) + discovered.Add(sectorMap.CurrentZoneIndex); + + var allZones = (sectorMap.Zones ?? Array.Empty()) + .Where(zone => zone != null) + .OrderBy(zone => zone.ZoneIndex) + .ToArray(); + var zones = allZones.Where(zone => discovered.Contains(zone.ZoneIndex)).ToArray(); + var zoneIndices = new HashSet(zones.Select(zone => zone.ZoneIndex)); + var criticalEdges = CriticalPathEdges(sectorMap, allZones); + var homeByZone = LandmarkByZone(sectorMap.HomeZones); + var bossByZone = LandmarkByZone(sectorMap.BossZones); + var factionIndices = zones + .SelectMany(zone => (zone.FactionIndices ?? Array.Empty()).Append(zone.OwnerFactionIndex)) + .Concat((sectorMap.HomeZones ?? Array.Empty()) + .Where(entry => entry != null).Select(entry => entry.FactionIndex)) + .Concat((sectorMap.BossZones ?? Array.Empty()) + .Where(entry => entry != null).Select(entry => entry.FactionIndex)) + .Where(index => index >= 0) + .Distinct() + .OrderBy(index => index) + .ToArray(); + var children = new List(); + + foreach (var factionIndex in factionIndices) + children.Add(InfluenceRegion(factionIndex, zones, sectorMap, catalog)); + + foreach (var link in (sectorMap.Links ?? Array.Empty()) + .Where(link => link != null && link.Discovered && + zoneIndices.Contains(link.FromZoneIndex) && zoneIndices.Contains(link.ToZoneIndex)) + .OrderBy(link => Math.Min(link.FromZoneIndex, link.ToZoneIndex)) + .ThenBy(link => Math.Max(link.FromZoneIndex, link.ToZoneIndex))) + { + children.Add(Node( + $"{SurfaceId}.link.{link.FromZoneIndex}.{link.ToZoneIndex}", + "graph.edge", + ("from", ZoneId(link.FromZoneIndex)), + ("to", ZoneId(link.ToZoneIndex)), + ("critical", Bool(criticalEdges.Contains(EdgeKey(link.FromZoneIndex, link.ToZoneIndex)))), + ("discovered", "true"))); + } + + foreach (var zone in zones) + { + var homes = homeByZone.TryGetValue(zone.ZoneIndex, out var homeFactions) + ? homeFactions + : Array.Empty(); + var bosses = bossByZone.TryGetValue(zone.ZoneIndex, out var bossFactions) + ? bossFactions + : Array.Empty(); + children.Add(Node( + ZoneId(zone.ZoneIndex), + "graph.node", + ("label", zone.Name ?? ""), + ("x", Format(zone.X)), + ("y", Format(zone.Y)), + ("zoneIndex", zone.ZoneIndex.ToString(CultureInfo.InvariantCulture)), + ("ownerFactionIndex", zone.OwnerFactionIndex.ToString(CultureInfo.InvariantCulture)), + ("factionIndices", Join(zone.FactionIndices)), + ("homeFactionIndices", Join(homes)), + ("executiveFactionIndices", Join(bosses)), + ("current", Bool(zone.Current || zone.ZoneIndex == sectorMap.CurrentZoneIndex)), + ("entrance", Bool(zone.Entrance || zone.ZoneIndex == sectorMap.EntranceZoneIndex)), + ("exit", Bool(zone.Exit || zone.ZoneIndex == sectorMap.ExitZoneIndex)), + ("discovered", "true"), + ("role", Role(zone, sectorMap)), + ("landmarkAssetKeys", LandmarkAssets(zone, sectorMap, homes.Count > 0, bosses.Count > 0)))); + } + + var graph = new AetheriaRuntimeSurfaceComponent( + $"{SurfaceId}.graph", + "graph", + Props( + ("label", "Aetheria Sector Map"), + ("coordinateSpace", "provider"), + ("backgroundAssetKey", BackgroundAssetKey), + ("influenceMaterialAssetKey", InfluenceAssetKey), + ("selectedNodeId", sectorMap.CurrentZoneIndex >= 0 ? ZoneId(sectorMap.CurrentZoneIndex) : ""), + ("pan", "true"), + ("zoom", "true"), + ("preserveAspect", "true")), + children); + + return new AetheriaRuntimeSurfaceDocument( + providerId: "aetheria", + providerKind: "sector.map", + title: "Sector Map", + version: Math.Max(version, sectorMap.FrameId), + updatedAtUtc: updatedAtUtc, + surface: new AetheriaRuntimeSurfaceTree( + SurfaceId, + new AetheriaRuntimeSurfaceComponent( + $"{SurfaceId}.root", + "surface.map", + Props(("title", "Sector Map"), ("runId", sectorMap.RunId ?? "")), + new[] { graph, Legend(factionIndices, catalog) }), + StyleTokens(factionIndices, sectorMap.GenerationSeed, sectorMap.IsTutorial)), + commands: Array.Empty()); + } + + public static IReadOnlyList StyleTokens( + IEnumerable? factionIndices = null, + uint generationSeed = 0, + bool tutorial = false) + { + var tokens = new List + { + Token("sectorMap.background.asset", BackgroundAssetKey), + Token("sectorMap.background.noiseAmplitude", "1"), + Token("sectorMap.background.noiseOffset", "0.3"), + Token("sectorMap.background.noiseGain", "0.7"), + Token("sectorMap.background.noiseLacunarity", "2"), + Token("sectorMap.background.noiseFrequency", tutorial ? "0.1" : "1"), + Token("sectorMap.background.noisePosition", tutorial ? "536.5106" : "91.29439"), + Token("sectorMap.background.cloudExponent", "10"), + Token("sectorMap.background.cloudAmplitude", "0.01"), + Token("sectorMap.influence.material", InfluenceAssetKey), + Token("sectorMap.influence.threshold", "1"), + Token("sectorMap.influence.fillTiling", "1024"), + Token("sectorMap.influence.fillBorderBlend", "0.05"), + Token("sectorMap.influence.fillBlend", "20"), + Token("sectorMap.influence.patternBlend", "0.25"), + Token("sectorMap.influence.patternOffset", "-0.75"), + Token("sectorMap.influence.fillAlpha", "0.4"), + Token("sectorMap.influence.stroke", "4"), + Token("sectorMap.influence.strokeBlend", "0.25"), + Token("sectorMap.influence.strokeTransitionBlend", "1"), + Token("sectorMap.faction.zonePrimaryBoost", "0.75"), + Token("sectorMap.faction.zoneSecondaryBoost", "2"), + Token("sectorMap.faction.linkBoost", "3"), + Token("sectorMap.neutral.color", "#D9D9D9"), + Token("sectorMap.zone.stroke", "#FFFFFF"), + Token("sectorMap.zone.current.labelColor", "#FF720D"), + Token("sectorMap.zone.current.scale", "1.25"), + Token("sectorMap.link.width", "0.002"), + Token("sectorMap.link.critical.width", "0.005"), + Token("sectorMap.landmark.iconDistance", "1"), + Token("sectorMap.landmark.backgroundSize", "3"), + Token("sectorMap.label.offset", "0.4"), + Token("sectorMap.reveal.linkDurationSeconds", "0.5"), + Token("sectorMap.reveal.nodeDurationSeconds", "0.25"), + Token("sectorMap.zoom.minimum", "0.1"), + Token("sectorMap.zoom.maximum", "2") + }; + + foreach (var factionIndex in (factionIndices ?? Array.Empty()) + .Where(index => index >= 0).Distinct().OrderBy(index => index)) + { + tokens.Add(Token($"sectorMap.faction.{factionIndex}.primary", + HsvHex(Fraction(factionIndex * 0.173), 0.62, 0.88))); + tokens.Add(Token($"sectorMap.faction.{factionIndex}.secondary", + HsvHex(Fraction((factionIndex + 0.37) * 0.173), 0.48, 0.72))); + tokens.Add(Token($"sectorMap.faction.{factionIndex}.fillTiltRadians", + Format(DeterministicTilt(generationSeed, factionIndex)))); + } + return tokens; + } + + private static AetheriaRuntimeSurfaceComponent InfluenceRegion( + int factionIndex, + IReadOnlyList zones, + AetheriaRuntimeSectorMapDocument map, + AetheriaRuntimeCatalogSnapshot? catalog) + { + var samples = zones + .Where(zone => (zone.FactionIndices?.Count ?? 0) > 0) + .Select(zone => Node( + $"{SurfaceId}.influence.{factionIndex}.sample.{zone.ZoneIndex}", + "graph.region.sample", + ("nodeId", ZoneId(zone.ZoneIndex)), + ("value", zone.FactionIndices.Contains(factionIndex) + ? zone.OwnerFactionIndex == factionIndex ? "10" : "5" + : "-10"))) + .ToArray(); + return new AetheriaRuntimeSurfaceComponent( + $"{SurfaceId}.influence.{factionIndex}", + "graph.region", + Props( + ("label", FactionLabel(catalog, factionIndex)), + ("factionIndex", factionIndex.ToString(CultureInfo.InvariantCulture)), + ("materialAssetKey", InfluenceAssetKey), + ("threshold", "1"), + ("fillTiltToken", $"sectorMap.faction.{factionIndex}.fillTiltRadians")), + samples); + } + + private static AetheriaRuntimeSurfaceComponent Legend( + IReadOnlyList factionIndices, + AetheriaRuntimeCatalogSnapshot? catalog) + { + var items = factionIndices.Select(index => Node( + $"{SurfaceId}.legend.faction.{index}", + "graph.legend.item", + ("label", FactionLabel(catalog, index)), + ("role", "faction"), + ("factionIndex", index.ToString(CultureInfo.InvariantCulture)))) + .Concat(new[] + { + LegendItem("start", "Start", StartIconAssetKey), + LegendItem("terminus", "Terminus", TerminusIconAssetKey), + LegendItem("home", "Home Zone", HomeIconAssetKey), + LegendItem("executive", "Executive", ExecutiveIconAssetKey) + }) + .ToArray(); + return new AetheriaRuntimeSurfaceComponent( + $"{SurfaceId}.legend", + "graph.legend", + Props(("label", "Legend")), + items); + } + + private static AetheriaRuntimeSurfaceComponent LegendItem(string id, string label, string assetKey) => + Node($"{SurfaceId}.legend.{id}", "graph.legend.item", + ("label", label), ("role", id), ("assetKey", assetKey)); + + private static Dictionary> LandmarkByZone( + IReadOnlyList entries) => + (entries ?? Array.Empty()) + .Where(entry => entry != null && entry.ZoneIndex >= 0 && entry.FactionIndex >= 0) + .GroupBy(entry => entry.ZoneIndex) + .ToDictionary(group => group.Key, + group => (IReadOnlyList)group.Select(entry => entry.FactionIndex).Distinct().OrderBy(index => index).ToArray()); + + private static string LandmarkAssets( + AetheriaRuntimeSectorMapZone zone, + AetheriaRuntimeSectorMapDocument map, + bool home, + bool boss) + { + var assets = new List(); + if (zone.Entrance || zone.ZoneIndex == map.EntranceZoneIndex) assets.Add(StartIconAssetKey); + if (zone.Exit || zone.ZoneIndex == map.ExitZoneIndex) assets.Add(TerminusIconAssetKey); + if (home) assets.Add(HomeIconAssetKey); + if (boss) assets.Add(ExecutiveIconAssetKey); + return string.Join(",", assets); + } + + private static HashSet CriticalPathEdges( + AetheriaRuntimeSectorMapDocument map, + IReadOnlyList zones) + { + var result = new HashSet(StringComparer.Ordinal); + if (map.EntranceZoneIndex < 0 || map.ExitZoneIndex < 0 || map.EntranceZoneIndex == map.ExitZoneIndex) + return result; + var byIndex = zones.ToDictionary(zone => zone.ZoneIndex); + if (!byIndex.ContainsKey(map.EntranceZoneIndex) || !byIndex.ContainsKey(map.ExitZoneIndex)) + return result; + var adjacency = byIndex.Keys.ToDictionary(index => index, _ => new List()); + foreach (var link in map.Links ?? Array.Empty()) + { + if (link == null || !adjacency.ContainsKey(link.FromZoneIndex) || !adjacency.ContainsKey(link.ToZoneIndex)) + continue; + adjacency[link.FromZoneIndex].Add(link.ToZoneIndex); + adjacency[link.ToZoneIndex].Add(link.FromZoneIndex); + } + var distance = byIndex.Keys.ToDictionary(index => index, _ => double.PositiveInfinity); + var previous = new Dictionary(); + var unvisited = new HashSet(byIndex.Keys); + distance[map.EntranceZoneIndex] = 0; + while (unvisited.Count > 0) + { + var current = unvisited.OrderBy(index => distance[index]).ThenBy(index => index).First(); + if (double.IsPositiveInfinity(distance[current]) || current == map.ExitZoneIndex) break; + unvisited.Remove(current); + foreach (var adjacent in adjacency[current].Where(unvisited.Contains).OrderBy(index => index)) + { + var dx = byIndex[current].X - byIndex[adjacent].X; + var dy = byIndex[current].Y - byIndex[adjacent].Y; + var candidate = distance[current] + dx * dx + dy * dy; + if (candidate >= distance[adjacent]) continue; + distance[adjacent] = candidate; + previous[adjacent] = current; + } + } + var cursor = map.ExitZoneIndex; + while (cursor != map.EntranceZoneIndex && previous.TryGetValue(cursor, out var parent)) + { + result.Add(EdgeKey(parent, cursor)); + cursor = parent; + } + return cursor == map.EntranceZoneIndex ? result : new HashSet(StringComparer.Ordinal); + } + + private static string FactionLabel(AetheriaRuntimeCatalogSnapshot? catalog, int factionIndex) + { + var corporations = catalog?.Corporations ?? Array.Empty(); + if (factionIndex < 0 || factionIndex >= corporations.Count || corporations[factionIndex] == null) + return $"F{factionIndex}"; + var faction = corporations[factionIndex]; + return string.IsNullOrWhiteSpace(faction.ShortName) + ? string.IsNullOrWhiteSpace(faction.Name) ? $"F{factionIndex}" : faction.Name + : faction.ShortName; + } + + private static string ZoneId(int zoneIndex) => $"{SurfaceId}.zone.{zoneIndex}"; + private static string EdgeKey(int left, int right) => + left < right ? $"{left}:{right}" : $"{right}:{left}"; + + private static string Role(AetheriaRuntimeSectorMapZone zone, AetheriaRuntimeSectorMapDocument map) + { + if (zone.Current || zone.ZoneIndex == map.CurrentZoneIndex) return "current"; + if (zone.Entrance || zone.ZoneIndex == map.EntranceZoneIndex) return "entrance"; + if (zone.Exit || zone.ZoneIndex == map.ExitZoneIndex) return "exit"; + return "zone"; + } + + private static double DeterministicTilt(uint seed, int factionIndex) + { + var value = seed ^ unchecked((uint)(factionIndex + 1) * 0x9E3779B9u); + value ^= value >> 16; + value *= 0x7FEB352Du; + value ^= value >> 15; + return value / (double)uint.MaxValue * Math.PI; + } + + private static string HsvHex(double hue, double saturation, double value) + { + var sector = hue * 6; + var index = (int)Math.Floor(sector); + var fraction = sector - index; + var p = value * (1 - saturation); + var q = value * (1 - fraction * saturation); + var t = value * (1 - (1 - fraction) * saturation); + var (red, green, blue) = (index % 6) switch + { + 0 => (value, t, p), 1 => (q, value, p), 2 => (p, value, t), + 3 => (p, q, value), 4 => (t, p, value), _ => (value, p, q) + }; + return $"#{Byte(red):X2}{Byte(green):X2}{Byte(blue):X2}"; + } + + private static int Byte(double value) => + (int)Math.Round(Math.Max(0, Math.Min(1, value)) * 255, MidpointRounding.AwayFromZero); + private static double Fraction(double value) => value - Math.Floor(value); + private static AetheriaRuntimeSurfaceStyleToken Token(string name, string value) => + new AetheriaRuntimeSurfaceStyleToken(name, value); + private static AetheriaRuntimeSurfaceComponent Node( + string id, string kind, params (string Key, string Value)[] props) => + new AetheriaRuntimeSurfaceComponent(id, kind, Props(props), Array.Empty()); + private static IReadOnlyDictionary Props(params (string Key, string Value)[] values) => + values.ToDictionary(value => value.Key, value => value.Value ?? "", StringComparer.Ordinal); + private static string Format(double value) => value.ToString("R", CultureInfo.InvariantCulture); + private static string Join(IEnumerable? values) => + string.Join(",", values ?? Array.Empty()); + private static string Bool(bool value) => value ? "true" : "false"; + } +} diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSectorMapSurfaceBuilder.cs.meta b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSectorMapSurfaceBuilder.cs.meta new file mode 100644 index 000000000..2f2e7ffe1 --- /dev/null +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSectorMapSurfaceBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 65b8583559804c7ab04ad99590de2279 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs index 89df40236..46ddc560b 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs @@ -230,6 +230,14 @@ namespace GameCult.Aetheria.State.Verse [Key(20)] public string GameMode { get; set; } = "aetheria"; + [Key(21)] + public IReadOnlyList HomeZones { get; set; } = + Array.Empty(); + + [Key(22)] + public IReadOnlyList BossZones { get; set; } = + Array.Empty(); + public AetheriaRuntimeLoadoutTemplateCommit CreateLoadoutTemplate(string entityKey) { return TryParseEntityKey(entityKey, out var zoneIndex, out var entityIndex) @@ -474,6 +482,13 @@ namespace GameCult.Aetheria.State.Verse [Key(13)] public double AuxiliaryValue { get; set; } } + [MessagePackObject] + public sealed class AetheriaRuntimeFactionZoneCommit + { + [Key(0)] public int FactionIndex { get; set; } = -1; + [Key(1)] public int ZoneIndex { get; set; } = -1; + } + public static class AetheriaRuntimeRunLifecycle { public const string Active = "active"; diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 6897dad93..beb4a3731 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -879,6 +879,40 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. connects replicas directly, proves accepted/rejected policy outcomes, and proves committed-fact convergence with production Ymir physics injected. +### Daemon Sector-Map Surface + +- Owner: the daemon run owns sector topology, discovery, current location, + faction presence, and the visibility of zone and link facts. +- Inputs: `gamecult.aetheria.sector_map.v1`, projected from the committed run. +- Outputs: the directly advertised `aetheria.sector_map` Eve surface. Its + generic graph contains discovered nodes and links, the weighted + entrance-to-Terminus route, home/executive landmarks, a legend, and one + `graph.region` scalar field per visible faction. Region samples preserve the + fossil values: owner `10`, present `5`, excluded `-10`. +- Derived state: graph drawing and selection treatment are presentation. The + provider publishes the historical galaxy and Influence materials plus the + start, Terminus, home, and executive sprites through its asset catalog. Exact + scene and shader levers travel as `sectorMap.*` tokens: background noise, + contour threshold, fill tiling/blends/alpha, hatch tilt, stroke shape, + faction color boosts, `0.002/0.005` link widths, `0.5/0.25s` reveal timing, + landmark scale, current marker treatment, label offset, and zoom bounds. + Faction hatch tilt is stable for generation seed and faction identity rather + than renderer-local randomness. +- Forbidden writers: Unity `SectorMap`, map cameras, Electron, and generic Eve + lowerers cannot reveal zones, add links, or choose the current zone. +- Shared paths: every runtime reads the same CultMesh record advertised by the + daemon. The record republishes with every daemon frame so travel and discovery + cannot remain stuck at topology-boot state. Zone details remain an inspection + surface and do not impersonate the sector map itself. +- Cut line: `MapMenuSurface` publishes the sector graph rather than a card for + only the current zone. The legacy Unity map remains a transitional lowerer, + not an alternate map authority. +- Verification layer: daemon smoke proves undiscovered labels and links are + absent; the weighted critical route is derived from daemon facts; home and + executive landmarks survive CultCache reopen; influence samples, legend, + historical values, material refs, sprites, and provider-owned visual tokens + travel with the surface. + ### Daemon Combat Lifecycle Weapon admission, pending trigger, progressive target lock, burst/reload/charge diff --git a/docs/original-game-migration-matrix.md b/docs/original-game-migration-matrix.md index 694cb1162..cc4e2abe8 100644 --- a/docs/original-game-migration-matrix.md +++ b/docs/original-game-migration-matrix.md @@ -34,7 +34,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity. | Action bar/input | input shell and rebinding UI | daemon advertises actions; client owns bindings | input capability and action descriptors | universal binding UI | contracts wired; consumable rows are catalog-filtered, expose daemon-owned cargo quantity, active-effect count, exact duration/remaining duration, and the fossil `remaining / duration` fill as `remaining-ratio.v1`. A depleted or non-stackable active consumable remains visible with unavailable command state until its effect ends, so a lowerer never queries Unity gameplay objects. Weapon groups advertise the fossil press/release lifecycle as generic `button-hold.v1` values routed to `SetWeaponGroupActive`; daemon entity state persists the active-group latch, press edges pulse instant/charged weapons once, constant weapons remain selected without client keepalives, release clears the latch, and the one-shot compatibility command is not the public input contract. Equipment actions derive from exact installed catalog payload/state pairs instead of blindly activating behavior zero: Switch and Trigger expose generic hold values, adjustable Thermotoggle exposes a canonical `scalar.v1` Kelvin target/current value, fixed/non-interactive rows are omitted, and offline rows retain identity as unavailable. Target nearest/previous/next/reticle/clear actions expose daemon-owned current selection, visible-hostile count, and availability; the default profile preserves fossil `R/T/Y/U`, restores the shared wormhole-first/then-dock interaction to `F`, and routes `view-direction.v1` world XYZ through the released generic runtime while target ranking remains daemon policy. Released EveUnity lowers provider action-bar suggestions with provider-owned icon asset keys, availability, one-shot, held, view-direction, and explicit scalar controls without importing Aetheria. Every value form translates through generic Eve requests into typed daemon commands under provider identity `aetheria` | generic slot editing/persistence, consumable quantity/progress chrome, provider styling, and live action-bar witness | | Pilot camera | ARPG follow/dock cameras | Eve view semantics + runtime | camera rig and semantic view id | EveUnity camera rig | daemon dock state selects distinct contracts instead of collapsing both fossil cameras: undocked flight advertises the entity-forward `Third Person Rig` framing (distance 30, screen 0.64/0.19, FOV 60, lens 1-4096) and the shared `aim.presentation` convergence point as its look-at; Aetheria translates the fossil Cinemachine top-origin screen Y of `0.81` to Eve's bottom-origin viewport Y of `0.19`; EveUnity solves native pose while preserving both optical aim and advertised entity framing, without provider types; the retained dock projection remains separate | reproduce the dock camera's separate follow/look-at subjects, blend timeline, noise, and reconnect state | | Render channels | Unity layers/culling masks | provider surface policy + runtime asset variant | semantic channel exclusions | EveUnity native layer mapping | wired, exclusive, and lowered: the pilot surface excludes the semantic `map` channel; the Unity asset variant maps `map` to its authored native layer; EveUnity subtracts that layer from the configured camera mask; portable state contains no Unity layer or mask; provider conversion preserves the source icon shader's unlit lighting model; the released witness proves the pilot camera excludes map glyphs while the map camera retains readable white glyphs without a scene light | preserve the channel proof while world presentation and transport continue to evolve | -| Minimap/tactical map | `ZoneRenderer`, map cameras | daemon world facts | explicit map projections | Unity and Electron | facts fragmented; generic lowerers absent | same state in both runtimes | +| Minimap/tactical map | `ZoneRenderer`, map cameras | daemon world facts | explicit map projections | Unity and Electron | the daemon directly advertises one every-frame `aetheria.sector_map` Eve surface backed by the committed sector-map projection, so travel and discovery cannot remain stuck at topology boot. The graph carries only discovered topology, current/start/Terminus roles, the fossil weighted critical route, persistent home/executive faction landmarks, and a legend. Each visible faction owns a `graph.region` scalar field with the original owner `10`, present `5`, excluded `-10` samples. The provider asset catalog carries the historical Galaxy and Influence materials plus start, Terminus, home, and executive sprites. `sectorMap.*` tokens preserve the authored background noise, contour/hatch/stroke parameters, faction boosts, deterministic per-faction hatch tilt, `0.002/0.005` link widths, `0.5/0.25s` reveal timing, marker scale, label offset, and zoom bounds without putting Aetheria styling or gameplay logic in EveUnity. Zone details remain a subordinate inspection surface; the fossil Unity map remains a transitional lowerer, not a writer | generic EveUnity and Electron graph/region lowering, provider palette recovery beyond the current stable fallback colors, node activation into zone details, and same-state visual witness in both runtimes | | Celestial world | `ZoneRenderer`, `ZoneGenerator`, `GeneratorPlanet`, body/asteroid prefab scripts | daemon render projection plus provider assets | ordinary generic Eve entity SoA rows | EveUnity generic entity/prefab lowerer | wired, exclusive, projected, and lowered for Terminus; generated-world authority now also exists in the daemon. Each tutorial topology zone deterministically evaluates the authored radius/mass/subzone curves, rosette-or-solar hierarchy, five satellite/binary passes, belt selection, mass normalization, stable orbits/bodies, gravity/wave curves, asteroid rows, and sun/gas-giant visual parameters. All 64 zones, their station/turret orbits, faction ships, and starter entry are persisted as canonical daemon state. Provider packaging extracts only each advertised visual child and strips fossil scripts/physics; released EveUnity `0.3.51` applies semantic scale around provider-authored local scale and contains no celestial or Aetheria branch. The released warm Terminus witness records eleven bodies and eighteen asteroids with provider refs and enabled renderers, including seven celestial instances intersecting the pilot frustum | witness generic lowering of the persisted tutorial run; body material/light readability, close-range composition matching the fossil, asteroid variation, and Electron lowering | | Fields/fog/gravity | zone renderer and shaders | daemon/plugin semantics | Fields plugin documents | EveUnity fields lowerer | daemon publishes canonical gravity/fog splat layers through `field.volume3d`; Eve Fields `0.2.3` owns portable `PowerPulse` and `AnimatedRadialCosine` semantics; Aetheria emits the fossil global animated simplex/cellular/ambient stack, body-owned radial waves, and separate negative gameplay `gravity.height` and positive visual `fog.surface_height`; the shared camera-relative lattice spans 1536 world units as 256 six-unit Stardust cells, rasterizes gravity at eight texels per cell into 2048 texels, and snaps the viewport by one six-unit cell; daemon entities follow fossil terrain height plus hull grid offset after Ymir XZ motion; released EveUnity `0.3.58` lowers the authored topology, one shared snapped viewport frame for fog and Stardust, provider shader ABI, pre-postprocess URP pass, provider-requested TAA, provider-material temporal coverage inputs, authoritative flow time, the historical positive-Z Unity shader camera transform, and float32 packed-output diagnostics without provider code. The provider bundle runs the actual Stardust compute shader across X, Y, and diagonal one-cell remaps and proves 705 overlapping world cells remain bit-identical after buffer reassignment; an asymmetric GPU fixture proves positive world X samples positive texture X instead of retaining the deleted fossil gravity camera's two-axis reversal; its source gate rejects replacement of temporal dithering with a fixed alpha threshold. A separate gravity-fog gate requires raymarch and temporal reprojection to share the historical positive-Z camera convention. The flight post-process bundle gate verifies serialized ACES, exposure/contrast, bloom, vignette, and film-grain components | restore readable patch structure, gravity waves, surrounding world geometry, and final luminance/composition parity; visual parity remains explicitly open | | HUD/schematic | ARPG HUD and `SchematicDisplay` | Eve composition from daemon facts | semantic status/components | EveUnity UI | provider-authored transparent cockpit is wired, projected, and lowered through generic UI Toolkit panes, metrics, and progress components for hull, shield, capacitor, temperature, cargo, weapon cooldown/groups, selected target, target lock, target shield, and target hull. The daemon now publishes the original spatial schematic semantics as a generic `ship.schematic` subtree for both self and selected target: explicit occupied hull cells with current/maximum armor, catalog-backed rotated equipment footprints, durability, thermal range/performance, online state, wear, and nested weapon ammo/cooldown/charge/reload facts. Refit, damage, and presentation share the same rotation geometry; Unity objects do not derive schematic truth | generic spatial schematic lowering, warning timelines, dynamic action state, and composited live capture |