diff --git a/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts b/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts index 51cc54f7f..d1c4e5975 100644 --- a/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts +++ b/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts @@ -347,6 +347,7 @@ export type AetheriaMenuSurfaceRequest = { surfaceId?: string; inGame?: boolean; canOpenRuntimeInputScreen?: boolean; + panelOnly?: boolean; }; export type AetheriaEveSurfaceRequest = { diff --git a/Aetheria.Rts.Web/Client/app.ts b/Aetheria.Rts.Web/Client/app.ts index 56387fee1..1930b7c43 100644 --- a/Aetheria.Rts.Web/Client/app.ts +++ b/Aetheria.Rts.Web/Client/app.ts @@ -126,6 +126,7 @@ function renderMainMenuSurface(surface: AetheriaMenuSurfaceDocument): void { assetUrlResolver: resolveAetheriaAssetUrl, clientId: "aetheria.rts.electron", commandSink: intent => handleMenuCommand(intent.command, surface), + documentResolver: resolveEveDocument, source: "Aetheria Electron", statusElement: statusEl, }); @@ -181,7 +182,21 @@ async function showDaemonEveSurface(): Promise { async function resolveEveDocument( request: EveEmbeddedDocumentRequest, component: EveSurfaceComponent, -): Promise<{ document: unknown; documentId: string; schemaId: string } | undefined> { +): Promise<{ document?: unknown; documentId: string; schemaId: string; surface?: AetheriaMenuSurfaceDocument["surface"] } | undefined> { + if (request.schemaId === "gamecult.eve.surface.v1" || request.slotId === "mainMenuPanel") { + const surface = await window.aetheriaRts.mainMenuSurface({ + surfaceId: request.documentId, + inGame: false, + canOpenRuntimeInputScreen: false, + panelOnly: true, + }); + return { + documentId: request.documentId, + schemaId: "gamecult.eve.surface.v1", + surface: surface.surface, + }; + } + const viewport = viewportFromComponent(component); if (request.schemaId === "gamecult.aetheria.render_splats_viewport.v1" || request.slotId === "renderSplats") { return { @@ -278,6 +293,12 @@ function handleMenuCommand(commandOrIntent: string | EveCommandIntent, surface?: case "aetheria.main_menu.root.show_settings": void showMainMenu("aetheria.main_menu.settings"); return; + case "aetheria.main_menu.settings.show_player_settings": + void showMainMenu("aetheria.main_menu.player_settings"); + return; + case "aetheria.main_menu.settings.show_verse_settings": + void showMainMenu("aetheria.main_menu.verse_settings"); + return; case "aetheria.main_menu.settings.show_input_settings": void showMainMenu("aetheria.main_menu.input_settings"); return; diff --git a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts index de7f16b5f..bece47737 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts @@ -100,6 +100,7 @@ export type AetheriaMenuSurfaceRequest = { surfaceId?: string; inGame?: boolean; canOpenRuntimeInputScreen?: boolean; + panelOnly?: boolean; }; export type AetheriaEveSurfaceRequest = { @@ -336,7 +337,8 @@ export class AetheriaCultMeshClient { return buildMainMenuSurface( request.surfaceId || "aetheria.main_menu.root", request.canOpenRuntimeInputScreen === true, - request.inGame === true); + request.inGame === true, + request.panelOnly === true); } public async eveSurface(request: AetheriaEveSurfaceRequest = {}): Promise { @@ -698,8 +700,42 @@ function buildMainMenuSurface( surfaceId: string, canOpenRuntimeInputScreen: boolean, inGame: boolean, + 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)" }), + ); +} + +function buildMainMenuPanelSurface( + surfaceId: string, + canOpenRuntimeInputScreen: boolean, + inGame: boolean, + updatedAtUtc: string, +): AetheriaMenuSurfaceDocument { if (surfaceId === "aetheria.main_menu.settings") { return surfaceDocument( surfaceId, @@ -711,20 +747,25 @@ function buildMainMenuSurface( command("aetheria.main_menu.settings.show_input_settings", "Input"), command("aetheria.main_menu.settings.back_to_main", "Back"), ], - text("aetheria.mainMenu.settings.title", "SETTINGS", "text.title"), - buttonColumn( - "aetheria.mainMenu.settings.actions", - button("aetheria.mainMenu.settings.playerSettings", "Player Settings", "aetheria.main_menu.settings.show_player_settings"), - button("aetheria.mainMenu.settings.verse", "Verse", "aetheria.main_menu.settings.show_verse_settings"), - button("aetheria.mainMenu.settings.input", "Input", "aetheria.main_menu.settings.show_input_settings"), - button("aetheria.mainMenu.settings.back", "Back", "aetheria.main_menu.settings.back_to_main"), + menuPanel( + surfaceId, + [ + text("aetheria.mainMenu.settings.title", "SETTINGS", "text.title", { margin: "0 0 2.4rem 0" }, mainMenuTitleStyle("4.4rem")), + buttonColumn( + "aetheria.mainMenu.settings.actions", + button("aetheria.mainMenu.settings.playerSettings", "Player Settings", "aetheria.main_menu.settings.show_player_settings"), + button("aetheria.mainMenu.settings.verse", "Verse", "aetheria.main_menu.settings.show_verse_settings"), + button("aetheria.mainMenu.settings.input", "Input", "aetheria.main_menu.settings.show_input_settings"), + button("aetheria.mainMenu.settings.back", "Back", "aetheria.main_menu.settings.back_to_main"), + ), + ], ), ); } if (surfaceId === "aetheria.main_menu.input_settings") { const children = [ - text("aetheria.mainMenu.input.title", "INPUT", "text.title"), + text("aetheria.mainMenu.input.title", "INPUT", "text.title", { margin: "0 0 2.2rem 0" }, mainMenuTitleStyle("4.4rem")), metric("aetheria.mainMenu.input.bindingOverrides", "Binding Overrides", "0"), metric("aetheria.mainMenu.input.actionBarInputs", "Action-Bar Inputs", "0"), text( @@ -742,7 +783,47 @@ function buildMainMenuSurface( "Aetheria Input Settings", updatedAtUtc, [command("aetheria.main_menu.settings.back_to_settings", "Back")], - ...children); + menuPanel(surfaceId, children)); + } + + if (surfaceId === "aetheria.main_menu.player_settings") { + return surfaceDocument( + surfaceId, + "Aetheria Player Settings", + updatedAtUtc, + [command("aetheria.main_menu.settings.back_to_settings", "Back")], + menuPanel( + surfaceId, + [ + text("aetheria.mainMenu.player.title", "PLAYER", "text.title", { margin: "0 0 2.2rem 0" }, mainMenuTitleStyle("4.4rem")), + metric("aetheria.mainMenu.player.temperatureUnit", "Temperature Unit", "Celsius"), + metric("aetheria.mainMenu.player.significantDigits", "Significant Digits", "3"), + metric("aetheria.mainMenu.player.shutdown", "Default Shutdown", "25%"), + buttonColumn( + "aetheria.mainMenu.player.actions", + button("aetheria.mainMenu.player.back", "Back", "aetheria.main_menu.settings.back_to_settings"), + ), + ])); + } + + if (surfaceId === "aetheria.main_menu.verse_settings") { + return surfaceDocument( + surfaceId, + "Aetheria Verse Settings", + updatedAtUtc, + [command("aetheria.main_menu.settings.back_to_settings", "Back")], + menuPanel( + surfaceId, + [ + text("aetheria.mainMenu.verse.title", "VERSE", "text.title", { margin: "0 0 2.2rem 0" }, mainMenuTitleStyle("4.4rem")), + metric("aetheria.mainMenu.verse.name", "Name", "Local Aetheria"), + metric("aetheria.mainMenu.verse.id", "Verse", "aetheria.local"), + metric("aetheria.mainMenu.verse.visibility", "Visibility", "local"), + buttonColumn( + "aetheria.mainMenu.verse.actions", + button("aetheria.mainMenu.verse.back", "Back", "aetheria.main_menu.settings.back_to_settings"), + ), + ])); } const actionButtons = [ @@ -761,36 +842,50 @@ function buildMainMenuSurface( command("aetheria.main_menu.root.show_settings", "Settings"), command("aetheria.main_menu.root.quit", "Quit"), ], - gravitySurface("aetheria.main_menu.root.gravity"), - node( - "aetheria.main_menu.root.menu", - "column", - {}, - { - position: "relative", - padding: "7.25rem 0 0 6.75rem", - gap: "1.1rem", - width: "44rem", - maxWidth: "calc(100vw - 3rem)", - minHeight: "100vh", - alignItems: "flex-start", - }, - { color: "#e9fbff" }, - text("aetheria.main_menu.root.title", "AETHERIA", "text.title", { margin: "0 0 -1.6rem 0" }, { - font: "100 5.9rem/0.98 Montserrat, sans-serif", - color: "rgba(232, 250, 255, 0.94)", - whiteSpace: "nowrap", - }), - text("aetheria.main_menu.root.subtitle", "STARBRIDGE", "text.subtitle", { margin: "0 0 0.35rem 16.8rem" }, { - font: "100 2.6rem/1 Montserrat, sans-serif", - color: "rgba(232, 250, 255, 0.9)", - whiteSpace: "nowrap", - }), - buttonColumn("aetheria.main_menu.root.actions", ...actionButtons), - ), + menuPanel( + "aetheria.main_menu.root", + [ + text("aetheria.main_menu.root.title", "AETHERIA", "text.title", { margin: "0 0 -1.6rem 0" }, mainMenuTitleStyle("5.9rem")), + text("aetheria.main_menu.root.subtitle", "STARBRIDGE", "text.subtitle", { margin: "0 0 0.35rem 16.8rem" }, { + font: "100 2.6rem/1 Montserrat, sans-serif", + color: "rgba(232, 250, 255, 0.9)", + whiteSpace: "nowrap", + }), + buttonColumn("aetheria.main_menu.root.actions", ...actionButtons), + ]), ); } +function normalizeMainMenuSurfaceId(surfaceId: string): string { + switch (surfaceId) { + case "aetheria.main_menu.shell": + case "": + return "aetheria.main_menu.root"; + case "aetheria.main_menu.root": + case "aetheria.main_menu.settings": + case "aetheria.main_menu.input_settings": + case "aetheria.main_menu.player_settings": + case "aetheria.main_menu.verse_settings": + return surfaceId; + default: + return "aetheria.main_menu.root"; + } +} + +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, @@ -852,6 +947,26 @@ 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, @@ -866,18 +981,44 @@ function metric(id: string, label: string, value: string): AetheriaMenuSurfaceCo return node(id, "metric", { label, value }); } +function menuPanel(id: string, children: AetheriaMenuSurfaceComponent[]): AetheriaMenuSurfaceComponent { + return node( + `${id}.menu`, + "column", + {}, + { + position: "relative", + padding: "7.25rem 0 0 6.75rem", + gap: "1.1rem", + width: "44rem", + maxWidth: "calc(100vw - 3rem)", + minHeight: "100vh", + alignItems: "flex-start", + }, + { color: "#e9fbff", background: "rgba(0,0,0,0)" }, + ...children); +} + +function mainMenuTitleStyle(fontSize: string): Record { + return { + font: `100 ${fontSize}/0.98 Montserrat, sans-serif`, + color: "rgba(232, 250, 255, 0.94)", + whiteSpace: "nowrap", + }; +} + function button(id: string, label: string, commandId: string): AetheriaMenuSurfaceComponent { return node( id, "control.button", { label, command: commandId }, - { minWidth: "0", padding: "0.04rem 0" }, + { minWidth: "0", width: "220px", height: "32px", padding: "0" }, { background: "rgba(0, 0, 0, 0)", borderWidth: "0", borderStyle: "solid", boxShadow: "none", - font: "400 1.55rem/1.2 Ubuntu, sans-serif", + font: "400 1.55rem/32px Ubuntu, sans-serif", color: "#e8fbff", textAlign: "left", }); diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts index 183247c93..2ec8c7244 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts @@ -1916,6 +1916,7 @@ export type AetheriaMenuSurfaceRequest = { surfaceId?: string; inGame?: boolean; canOpenRuntimeInputScreen?: boolean; + panelOnly?: boolean; }; export type AetheriaEveSurfaceRequest = { diff --git a/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs b/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs index ebb1df63e..3d02eaa8b 100644 --- a/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs +++ b/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs @@ -485,6 +485,7 @@ export type AetheriaMenuSurfaceRequest = { surfaceId?: string; inGame?: boolean; canOpenRuntimeInputScreen?: boolean; + panelOnly?: boolean; }; export type AetheriaEveSurfaceRequest = { @@ -1154,6 +1155,7 @@ export type AetheriaMenuSurfaceRequest = { surfaceId?: string; inGame?: boolean; canOpenRuntimeInputScreen?: boolean; + panelOnly?: boolean; }; export type AetheriaEveSurfaceRequest = { diff --git a/Aetheria.Rts.Web/wwwroot/styles.css b/Aetheria.Rts.Web/wwwroot/styles.css index 878fc3a82..61d893687 100644 --- a/Aetheria.Rts.Web/wwwroot/styles.css +++ b/Aetheria.Rts.Web/wwwroot/styles.css @@ -25,11 +25,7 @@ body { body.main-menu-mode { -webkit-app-region: drag; - background: - radial-gradient(circle at 76% 66%, rgba(255, 213, 112, 0.74) 0, rgba(247, 121, 88, 0.36) 13%, transparent 31%), - radial-gradient(circle at 61% 37%, rgba(240, 45, 201, 0.54) 0, rgba(176, 38, 174, 0.28) 19%, transparent 45%), - radial-gradient(circle at 40% 45%, rgba(145, 21, 122, 0.42) 0, rgba(54, 8, 41, 0.4) 34%, transparent 62%), - #050407; + background: #020606; } body.main-menu-mode .shell { @@ -100,22 +96,15 @@ body.eve-game-mode .window-control-close:focus-visible { body.main-menu-mode .main-menu-host { -webkit-app-region: no-drag; - display: flex; - align-items: flex-start; - justify-content: flex-start; + display: block; min-height: 100%; - padding: 128px 96px; - background: - radial-gradient(ellipse at 78% 64%, rgba(255, 194, 95, 0.36) 0, rgba(255, 148, 92, 0.18) 16%, transparent 35%), - radial-gradient(ellipse at 59% 38%, rgba(238, 56, 216, 0.28) 0, rgba(164, 37, 160, 0.2) 22%, transparent 52%), - linear-gradient(90deg, rgba(0, 0, 0, 0.5), transparent 48%); + padding: 0; + background: transparent; } body.main-menu-mode .eve-surface { - width: 720px; - display: flex; - flex-direction: column; - align-items: flex-start; + width: 100%; + min-height: 100vh; } body.main-menu-mode .eve-text-title,