diff --git a/Aetheria.Rts.Web/Client/app.ts b/Aetheria.Rts.Web/Client/app.ts index 1930b7c43..fef4f4c15 100644 --- a/Aetheria.Rts.Web/Client/app.ts +++ b/Aetheria.Rts.Web/Client/app.ts @@ -7,6 +7,7 @@ import { import type { AetheriaRtsApi, AetheriaMenuSurfaceDocument, + AetheriaMenuSurfaceComponent, AuthorityStatusDocument, AetheriaRuntimeViewportFeedSnapshot, BodyView, @@ -59,6 +60,8 @@ let refreshInFlight = false; let viewportFeedUnsubscribe: (() => void) | null = null; let eveSurfacePoll: number | null = null; let latestEveSurfaceKey = ""; +let activeMainMenuSurfaceId = "aetheria.main_menu.root"; +let renderDaemonEveSurfaceNow: (() => Promise) | null = null; const mainMenuMode = window.location.hash === "#main-menu"; const legacyViewportMode = window.location.hash === "#legacy-viewport"; @@ -99,40 +102,6 @@ function setStatus(text: string): void { statusEl.textContent = text; } -async function showMainMenu(surfaceId = "aetheria.main_menu.root"): Promise { - document.body.classList.add("main-menu-mode"); - document.body.classList.remove("eve-game-mode"); - viewportFeedUnsubscribe?.(); - stopEveSurfacePoll(); - const surface = await window.aetheriaRts.mainMenuSurface({ - surfaceId, - inGame: false, - canOpenRuntimeInputScreen: false, - }); - renderMainMenuSurface(surface); -} - -function renderMainMenuSurface(surface: AetheriaMenuSurfaceDocument): void { - let host = document.querySelector("#main-menu-host"); - if (!host) { - host = document.createElement("section"); - host.id = "main-menu-host"; - host.className = "main-menu-host"; - document.body.append(host); - } - - renderEveSurface(surface, host, { - body: document.body, - assetUrlResolver: resolveAetheriaAssetUrl, - clientId: "aetheria.rts.electron", - commandSink: intent => handleMenuCommand(intent.command, surface), - documentResolver: resolveEveDocument, - source: "Aetheria Electron", - statusElement: statusEl, - }); - wireWindowControls(); -} - async function showDaemonEveSurface(): Promise { document.body.classList.remove("main-menu-mode"); document.body.classList.add("eve-game-mode"); @@ -152,10 +121,11 @@ async function showDaemonEveSurface(): Promise { const surface = await window.aetheriaRts.eveSurface({ recordKey: "eve:surface:aetheria.daemon.game", }); - const surfaceKey = `${surface.providerId}:${surface.surface.id}:${surface.version}:${surface.updatedAtUtc}`; + const loweredSurface = withActiveMainMenuPanel(surface, activeMainMenuSurfaceId); + const surfaceKey = `${surface.providerId}:${surface.surface.id}:${surface.version}:${surface.updatedAtUtc}:${activeMainMenuSurfaceId}`; if (surfaceKey !== latestEveSurfaceKey) { latestEveSurfaceKey = surfaceKey; - renderEveSurface(surface, host, { + renderEveSurface(loweredSurface, host, { body: document.body, assetUrlResolver: resolveAetheriaAssetUrl, clientId: "aetheria.rts.electron", @@ -171,6 +141,7 @@ async function showDaemonEveSurface(): Promise { } }; + renderDaemonEveSurfaceNow = renderLatest; await renderLatest(); if (eveSurfacePoll == null) { eveSurfacePoll = window.setInterval(() => { @@ -179,6 +150,70 @@ async function showDaemonEveSurface(): Promise { } } +async function showMainMenuPanel(surfaceId: string): Promise { + activeMainMenuSurfaceId = normalizeMainMenuSurfaceId(surfaceId); + latestEveSurfaceKey = ""; + if (renderDaemonEveSurfaceNow) { + await renderDaemonEveSurfaceNow(); + return; + } + + await showDaemonEveSurface(); +} + +function normalizeMainMenuSurfaceId(surfaceId: string): string { + switch (surfaceId) { + case "aetheria.main_menu.settings": + case "aetheria.main_menu.player_settings": + case "aetheria.main_menu.verse_settings": + case "aetheria.main_menu.input_settings": + return surfaceId; + default: + return "aetheria.main_menu.root"; + } +} + +function withActiveMainMenuPanel( + document: AetheriaMenuSurfaceDocument, + panelSurfaceId: string, +): AetheriaMenuSurfaceDocument { + return { + ...document, + surface: { + ...document.surface, + root: rewriteMainMenuPanelSlot(document.surface.root, panelSurfaceId), + }, + }; +} + +type MutableEveComponent = AetheriaMenuSurfaceComponent & { + layout?: Record; + style?: Record; +}; + +function rewriteMainMenuPanelSlot( + component: AetheriaMenuSurfaceComponent, + panelSurfaceId: string, +): AetheriaMenuSurfaceComponent { + const source = component as MutableEveComponent; + const props = source.props ?? {}; + const embeddedDocuments = source.embeddedDocuments ?? []; + const isMainMenuSlot = props.slotId === "mainMenuPanel" || + embeddedDocuments.some(slot => slot.slotId === "mainMenuPanel"); + return { + ...source, + props: isMainMenuSlot + ? { ...props, documentId: panelSurfaceId } + : props, + embeddedDocuments: isMainMenuSlot + ? embeddedDocuments.map(slot => slot.slotId === "mainMenuPanel" + ? { ...slot, documentId: panelSurfaceId } + : slot) + : source.embeddedDocuments, + children: source.children.map(child => rewriteMainMenuPanelSlot(child, panelSurfaceId)), + }; +} + async function resolveEveDocument( request: EveEmbeddedDocumentRequest, component: EveSurfaceComponent, @@ -291,22 +326,22 @@ function handleMenuCommand(commandOrIntent: string | EveCommandIntent, surface?: const command = typeof commandOrIntent === "string" ? commandOrIntent : commandOrIntent.command; switch (command) { case "aetheria.main_menu.root.show_settings": - void showMainMenu("aetheria.main_menu.settings"); + void showMainMenuPanel("aetheria.main_menu.settings"); return; case "aetheria.main_menu.settings.show_player_settings": - void showMainMenu("aetheria.main_menu.player_settings"); + void showMainMenuPanel("aetheria.main_menu.player_settings"); return; case "aetheria.main_menu.settings.show_verse_settings": - void showMainMenu("aetheria.main_menu.verse_settings"); + void showMainMenuPanel("aetheria.main_menu.verse_settings"); return; case "aetheria.main_menu.settings.show_input_settings": - void showMainMenu("aetheria.main_menu.input_settings"); + void showMainMenuPanel("aetheria.main_menu.input_settings"); return; case "aetheria.main_menu.settings.back_to_settings": - void showMainMenu("aetheria.main_menu.settings"); + void showMainMenuPanel("aetheria.main_menu.settings"); return; case "aetheria.main_menu.settings.back_to_main": - void showMainMenu("aetheria.main_menu.root"); + void showMainMenuPanel("aetheria.main_menu.root"); return; default: setStatus(`${surface?.title ?? "Aetheria Starbridge"}: ${command || "no command"}`); @@ -841,7 +876,7 @@ function roleText(session: StarbridgeSessionDocument): string { } if (mainMenuMode) { - void showMainMenu(); + void showDaemonEveSurface(); } else if (legacyViewportMode) { document.body.classList.remove("main-menu-mode", "eve-game-mode"); updateZoomLabel(); diff --git a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts index a200cd745..7d796ae50 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts @@ -700,34 +700,11 @@ function buildMainMenuSurface( surfaceId: string, canOpenRuntimeInputScreen: boolean, inGame: boolean, - panelOnly = false, + _panelOnly = false, ): AetheriaMenuSurfaceDocument { const updatedAtUtc = new Date().toISOString(); const activeSurfaceId = normalizeMainMenuSurfaceId(surfaceId); - return panelOnly - ? buildMainMenuPanelSurface(activeSurfaceId, canOpenRuntimeInputScreen, inGame, updatedAtUtc) - : buildMainMenuShellSurface(activeSurfaceId, updatedAtUtc); -} - -function buildMainMenuShellSurface( - activeSurfaceId: string, - updatedAtUtc: string, -): AetheriaMenuSurfaceDocument { - return surfaceDocument( - "aetheria.main_menu.shell", - "Aetheria Starbridge", - updatedAtUtc, - mainMenuCommands(), - gravitySurface("aetheria.main_menu.shell.gravity"), - surfaceSlot( - "aetheria.main_menu.shell.panel", - "mainMenuPanel", - activeSurfaceId, - eveSurfaceSchemaId, - "menu.overlay", - { position: "absolute", top: "0", right: "0", bottom: "0", left: "0", width: "100%", height: "100%" }, - { background: "rgba(0,0,0,0)" }), - ); + return buildMainMenuPanelSurface(activeSurfaceId, canOpenRuntimeInputScreen, inGame, updatedAtUtc); } function buildMainMenuPanelSurface( @@ -858,7 +835,6 @@ function buildMainMenuPanelSurface( function normalizeMainMenuSurfaceId(surfaceId: string): string { switch (surfaceId) { - case "aetheria.main_menu.shell": case "": return "aetheria.main_menu.root"; case "aetheria.main_menu.root": @@ -872,20 +848,6 @@ function normalizeMainMenuSurfaceId(surfaceId: string): string { } } -function mainMenuCommands(): AetheriaMenuSurfaceCommand[] { - return [ - command("aetheria.main_menu.root.continue", "Continue"), - command("aetheria.main_menu.root.new_game", "New Game"), - command("aetheria.main_menu.root.show_settings", "Settings"), - command("aetheria.main_menu.root.quit", "Quit"), - command("aetheria.main_menu.settings.show_player_settings", "Player Settings"), - command("aetheria.main_menu.settings.show_verse_settings", "Verse"), - command("aetheria.main_menu.settings.show_input_settings", "Input"), - command("aetheria.main_menu.settings.back_to_main", "Back"), - command("aetheria.main_menu.settings.back_to_settings", "Back"), - ]; -} - function surfaceDocument( surfaceId: string, title: string, @@ -893,7 +855,6 @@ function surfaceDocument( commands: AetheriaMenuSurfaceCommand[], ...children: AetheriaMenuSurfaceComponent[] ): AetheriaMenuSurfaceDocument { - const isMenuShell = surfaceId === "aetheria.main_menu.shell"; return { providerId: "aetheria", providerKind: "game.menu", @@ -907,7 +868,7 @@ function surfaceDocument( "surface", {}, { position: "relative", overflow: "hidden", width: "100%", height: "100vh", minHeight: "100vh" }, - { background: isMenuShell ? "#020606" : "rgba(0,0,0,0)" }, + { background: "rgba(0,0,0,0)" }, ...children), styles: [ { name: "font.title.family", value: "Montserrat" }, @@ -948,26 +909,6 @@ function node( return { id, kind, props, layout, style, children: normalizedChildren }; } -function surfaceSlot( - id: string, - slotId: string, - documentId: string, - schemaId: string, - presentationKind: string, - layout?: Record, - style?: Record, -): AetheriaMenuSurfaceComponent { - return { - id, - kind: "surface.slot", - props: { slotId, documentId, schemaId, presentationKind }, - layout, - style, - embeddedDocuments: [{ slotId, documentId, schemaId, presentationKind }], - children: [], - }; -} - function text( id: string, value: string, @@ -1016,8 +957,10 @@ function button(id: string, label: string, commandId: string): AetheriaMenuSurfa { minWidth: "0", width: "220px", height: "32px", padding: "0" }, { background: "rgba(0, 0, 0, 0)", + border: "0", borderWidth: "0", borderStyle: "solid", + borderRadius: "0", boxShadow: "none", font: "400 1.55rem/32px Ubuntu, sans-serif", color: "#e8fbff", @@ -1029,43 +972,6 @@ function buttonColumn(id: string, ...children: AetheriaMenuSurfaceComponent[]): return node(id, "column", {}, { gap: "0.18rem", alignItems: "flex-start" }, { color: "#e8fbff" }, ...children); } -function gravitySurface(id: string): AetheriaMenuSurfaceComponent { - return node( - id, - "gravity.surface", - { - label: "Aetheria level gravity surface", - viewRadius: "1450", - terrainRadius: "1450", - terrainDepth: "-8", - terrainDepthExponent: "1.2", - terrainWaveFrequency: "0.6", - simulationTimeSeconds: "0", - samplesX: "196", - isolines: "22", - shader: "perlines.gravity-map.v1", - noiseAsset: "cultmesh://aetheria/assets/textures/perlines-nebula", - tintSplatAsset: "cultmesh://aetheria/assets/textures/tint_splat", - bodies: [ - "terminus-sun|Sun|0|0|900|-80|3|450|10|2|cultmesh://aetheria/assets/icons/star.terminus-sun|cultmesh://aetheria/assets/textures/tint_splat", - "anchor-moon|Planet|360|-180|260|-22|2.2|180|4|1.4|cultmesh://aetheria/assets/icons/body.anchor-moon|cultmesh://aetheria/assets/textures/tint_splat", - "blueglass|Planet|-540|340|340|-34|2.6|150|5|1.1|cultmesh://aetheria/assets/icons/body.blueglass|cultmesh://aetheria/assets/textures/tint_splat", - "emberhook|Planet|-680|-420|310|-28|2.1|130|4|1.3|cultmesh://aetheria/assets/icons/body.emberhook|cultmesh://aetheria/assets/textures/tint_splat", - "vesper|Gas Giant|780|420|420|-38|2.4|210|6|0.7|cultmesh://aetheria/assets/icons/body.vesper|cultmesh://aetheria/assets/textures/tint_splat", - "latch|Moon|-160|620|210|-18|2.8|95|3|1.6|cultmesh://aetheria/assets/icons/body.latch|cultmesh://aetheria/assets/textures/tint_splat", - ].join(";"), - objects: [ - "anchor-station|station|Anchor Station|-220|-90|1|0|player|1|1|cultmesh://aetheria/assets/icons/station.anchor-station", - "vanguard-one|ship|Vanguard One|-60|-40|1|0.2|player|1|1|cultmesh://aetheria/assets/icons/ship.vanguard-one", - "wing-two|ship|Wing Two|180|120|-0.4|0.9|player|1|1|cultmesh://aetheria/assets/icons/ship.wing-two", - "torch-three|ship|Torch Three|-160|210|0.6|-0.8|player|1|1|cultmesh://aetheria/assets/icons/ship.torch-three", - "ash-raider|ship|Ash Raider|420|180|-0.8|-0.1|raider|0|1|cultmesh://aetheria/assets/icons/ship.ash-raider", - "derelict-relay|station|Derelict Relay|-260|260|0|1|neutral|0|1|cultmesh://aetheria/assets/icons/station.derelict-relay", - ].join(";"), - }, - { position: "absolute", top: "0", right: "0", bottom: "0", left: "0", width: "100%", height: "100%" }); -} - function isSurfaceComponent(value: unknown): value is AetheriaMenuSurfaceComponent { return !!value && typeof value === "object" && "kind" in value && "props" in value && "children" in value; } diff --git a/Aetheria.Rts.Web/Electron/main.ts b/Aetheria.Rts.Web/Electron/main.ts index 675807a93..50bff1c45 100644 --- a/Aetheria.Rts.Web/Electron/main.ts +++ b/Aetheria.Rts.Web/Electron/main.ts @@ -96,10 +96,7 @@ app.whenReady().then(async () => { throw new Error("AETHERIA_RTS_PEER_CULTMESH_ENDPOINTS has been removed. Configure AETHERIA_RTS_CULTMESH_URI and let Odin/CultMesh discover peer endpoints."); } - if (rendererView === "main-menu") { - showStartup("Launching Aetheria Menu", "Lowering the CultUI main-menu surface."); - mkdirSync(runtimeRoot, { recursive: true }); - } else if (launchLocalDaemon) { + if (launchLocalDaemon) { await ensureDotnetBuild(); showStartup("Launching Aetheria RTS", "Starting the Aetheria daemon."); mkdirSync(runtimeRoot, { recursive: true }); @@ -139,11 +136,11 @@ app.whenReady().then(async () => { "aetheria-rts-electron", { publicationMode: launchLocalDaemon ? "local" : "remote" }); + showStartup("Launching Aetheria RTS", "Waiting for the daemon CultMesh frame."); + await rtsClient.waitForFrame(30000); if (rendererView === "main-menu") { await mainWindow.loadFile(rendererIndex, { hash: "main-menu" }); } else { - showStartup("Launching Aetheria RTS", "Waiting for the daemon CultMesh frame."); - await rtsClient.waitForFrame(30000); await mainWindow.loadFile(rendererIndex); } if (electronSmoke) { diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs index 0f0aafbee..1215b9979 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs @@ -367,55 +367,37 @@ namespace GameCult.Aetheria.State.Verse { return new AetheriaRuntimeSurfaceComponent( id, - "column", - new Dictionary(StringComparer.Ordinal), + "surface.slot", + new Dictionary(StringComparer.Ordinal) + { + ["slotId"] = "mainMenuPanel", + ["documentId"] = AetheriaRuntimeMainMenuCommands.RootSurfaceId, + ["schemaId"] = "gamecult.eve.surface.v1", + ["presentationKind"] = "menu.overlay" + }, + Array.Empty(), + AetheriaRuntimeSurfaceStateBindings.FromProps(new Dictionary(StringComparer.Ordinal)), new[] { - TextNode( - $"{id}.title", - "AETHERIA", - "text.title", - Layout(("margin", "0 0 -1.35rem 0")), - new Dictionary - { - ["font"] = "100 5.3rem/0.98 Montserrat, sans-serif", - ["color"] = "rgba(232, 250, 255, 0.94)", - ["whiteSpace"] = "nowrap" - }), - TextNode( - $"{id}.subtitle", - "STARBRIDGE", - "text.subtitle", - Layout(("margin", "0 0 1.15rem 15.2rem")), - new Dictionary - { - ["font"] = "100 2.25rem/1 Montserrat, sans-serif", - ["color"] = "rgba(232, 250, 255, 0.88)", - ["whiteSpace"] = "nowrap" - }), - Node( - $"{id}.actions", - "column", - Array.Empty<(string Key, string Value)>(), - MenuButton($"{id}.continue", "Continue", "aetheria.main_menu.root.continue"), - MenuButton($"{id}.new_game", "New Game", "aetheria.main_menu.root.new_game"), - MenuButton($"{id}.settings", "Settings", "aetheria.main_menu.root.show_settings"), - MenuButton($"{id}.quit", "Quit", "aetheria.main_menu.root.quit")) + new AetheriaRuntimeEmbeddedDocumentSlot( + "mainMenuPanel", + AetheriaRuntimeMainMenuCommands.RootSurfaceId, + "gamecult.eve.surface.v1", + "menu.overlay") }, - AetheriaRuntimeSurfaceStateBindings.FromProps(new Dictionary(StringComparer.Ordinal)), - Array.Empty(), Layout( ("position", "absolute"), - ("left", "6.75rem"), - ("top", "5.75rem"), - ("width", "34rem"), - ("gap", "1rem"), - ("alignItems", "flex-start"), + ("top", "0"), + ("right", "0"), + ("bottom", "0"), + ("left", "0"), + ("width", "100%"), + ("height", "100%"), ("zIndex", "4"), ("pointerEvents", "auto")), new Dictionary { - ["color"] = "#e9fbff" + ["background"] = "rgba(0,0,0,0)" }); }