diff --git a/src/NullDepth/Pages/Home.razor b/src/NullDepth/Pages/Home.razor index e3690a0..9e56816 100644 --- a/src/NullDepth/Pages/Home.razor +++ b/src/NullDepth/Pages/Home.razor @@ -9,7 +9,14 @@
@if (!_ready) { -
NULL DEPTH // BOOTING
+
+
+
+
+ +
+ NULL DEPTH // BOOTING +
} else if (_screen == Screen.Title) { @@ -93,9 +100,21 @@ @if (_interaction is not null) { + var availableChoices = _interaction.Choices.Where(c => Game.HasAll(c.RequiresFlags)).ToList(); } @@ -162,6 +181,7 @@ private bool _savePulse; private int _savePulseVersion; private int _interactionIndex; + private int _choiceIndex; private string? _titleNotice; private SaveLoadResult? _saveError; private Confirmation? _confirmation; @@ -213,7 +233,7 @@ _showInteractionPicker = true; _notice = null; } - private void OpenInteraction(InteractionDefinition interaction) { _showInteractionPicker = false; _interaction = interaction; _notice = null; } + private void OpenInteraction(InteractionDefinition interaction) { _showInteractionPicker = false; _interaction = interaction; _choiceIndex = 0; _notice = null; } private void SelectInteraction(InteractionDefinition interaction) => OpenInteraction(interaction); private async Task ResolveInteraction() { @@ -288,31 +308,82 @@ private async Task HandleKey(KeyboardEventArgs args) { if (!_ready) return; - if (_confirmation is not null || _saveError is not null) { if (args.Key == "Escape") { _confirmation = null; _saveError = null; } return; } + if (_confirmation is not null || _saveError is not null) + { + if (args.Key == "Escape") { _confirmation = null; _saveError = null; } + else if (args.Key is "Enter" or " ") + { + if (_confirmation is not null) await ConfirmPendingAction(); + else if (_saveError is not null) await DeleteBrokenSave(); + } + return; + } if (_showSettings) { if (args.Key == "Escape") await CloseSettings(); return; } + + var keyLower = args.Key.ToLowerInvariant(); + if (_showInteractionPicker) { var actions = AvailableActions; if (args.Key == "Escape") CloseOverlays(); - else if (args.Key is "ArrowDown" or "ArrowUp") { var delta = args.Key == "ArrowDown" ? 1 : -1; _interactionIndex = (_interactionIndex + delta + actions.Count) % actions.Count; } - else if (args.Key == "Enter" && actions.Count > 0) OpenInteraction(actions[_interactionIndex]); + else if (args.Key is "ArrowDown" or "Down" || keyLower == "s") { _interactionIndex = (_interactionIndex + 1) % actions.Count; } + else if (args.Key is "ArrowUp" or "Up" || keyLower == "w") { _interactionIndex = (_interactionIndex - 1 + actions.Count) % actions.Count; } + else if (args.Key is "Enter" or " " && actions.Count > 0) OpenInteraction(actions[_interactionIndex]); else if (int.TryParse(args.Key, out var number) && number > 0 && number <= actions.Count) OpenInteraction(actions[number - 1]); return; } - if (_interaction is not null || _showMap || _showInventory || _showArchive) { if (args.Key == "Escape") CloseOverlays(); return; } - if (_showPause) { if (args.Key == "Escape") CloseOverlays(); return; } - if (_screen == Screen.Title) + + if (_interaction is not null) { - if (args.Key is "ArrowDown" or "ArrowUp") { var delta = args.Key == "ArrowDown" ? 1 : -1; do { _menuIndex = (_menuIndex + delta + 3) % 3; } while (_menuIndex == 1 && !Game.HasSave); } - else if (args.Key == "Enter") { if (_menuIndex == 0) await StartNewGame(); else if (_menuIndex == 1) await ContinueGame(); else await OpenSettings(); } + var choices = _interaction.Choices.Where(c => Game.HasAll(c.RequiresFlags)).ToList(); + if (args.Key == "Escape") { CloseOverlays(); return; } + if (choices.Count > 0) + { + if (args.Key is "ArrowDown" or "Down" || keyLower == "s") { _choiceIndex = (_choiceIndex + 1) % choices.Count; return; } + if (args.Key is "ArrowUp" or "Up" || keyLower == "w") { _choiceIndex = (_choiceIndex - 1 + choices.Count) % choices.Count; return; } + if (args.Key is "Enter" or " ") { await ResolveChoice(choices[_choiceIndex]); return; } + if (int.TryParse(args.Key, out var number) && number > 0 && number <= choices.Count) { await ResolveChoice(choices[number - 1]); return; } + } + else + { + if (args.Key is "Enter" or " " || keyLower == "e") { await ResolveInteraction(); return; } + } return; } - if (_screen == Screen.Briefing && args.Key == "Enter") { await BeginPlay(); return; } - if (_screen != Screen.Game) return; - switch (args.Key.ToLowerInvariant()) + + if (_showMap || _showInventory || _showArchive) { if (args.Key == "Escape") CloseOverlays(); return; } + if (_showPause) { if (args.Key == "Escape") CloseOverlays(); return; } + + if (_screen == Screen.Title) { - case "arrowup": await Move("north"); break; case "arrowdown": await Move("south"); break; case "arrowleft": await Move("west"); break; case "arrowright": await Move("east"); break; - case "e": OpenInteractionSelector(); break; case "m": _showMap = true; break; case "i": _showInventory = true; break; case "l": _showArchive = true; break; case "escape": _showPause = true; break; + if (args.Key is "ArrowDown" or "Down" || keyLower == "s") { do { _menuIndex = (_menuIndex + 1) % 3; } while (_menuIndex == 1 && !Game.HasSave); } + else if (args.Key is "ArrowUp" or "Up" || keyLower == "w") { do { _menuIndex = (_menuIndex - 1 + 3) % 3; } while (_menuIndex == 1 && !Game.HasSave); } + else if (args.Key is "Enter" or " ") { if (_menuIndex == 0) await StartNewGame(); else if (_menuIndex == 1) await ContinueGame(); else await OpenSettings(); } + else if (int.TryParse(args.Key, out var num)) + { + if (num == 1) await StartNewGame(); + else if (num == 2 && Game.HasSave) await ContinueGame(); + else if (num == 3) await OpenSettings(); + } + return; + } + + if (_screen == Screen.Briefing && args.Key is "Enter" or " ") { await BeginPlay(); return; } + if (_screen == Screen.Death && (args.Key is "Enter" or " " || keyLower == "r")) { await RestoreCheckpoint(); return; } + if (_screen == Screen.Ending && args.Key is "Enter" or "Escape" or " ") { await ReturnToTitle(); return; } + if (_screen != Screen.Game) return; + + switch (keyLower) + { + case "w" or "arrowup": await Move("north"); break; + case "s" or "arrowdown": await Move("south"); break; + case "a" or "arrowleft": await Move("west"); break; + case "d" or "arrowright": await Move("east"); break; + case "e": OpenInteractionSelector(); break; + case "m": _showMap = true; break; + case "i": _showInventory = true; break; + case "l": _showArchive = true; break; + case "escape": _showPause = true; break; } } public void Dispose() => Game.Saved -= OnSaved; diff --git a/src/NullDepth/wwwroot/assets/images/logo.png b/src/NullDepth/wwwroot/assets/images/logo.png new file mode 100644 index 0000000..be04421 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/logo.png differ diff --git a/src/NullDepth/wwwroot/assets/images/logo_512.png b/src/NullDepth/wwwroot/assets/images/logo_512.png new file mode 100644 index 0000000..2352bd0 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/logo_512.png differ diff --git a/src/NullDepth/wwwroot/css/app.css b/src/NullDepth/wwwroot/css/app.css index f5f6e8d..c02e0e3 100644 --- a/src/NullDepth/wwwroot/css/app.css +++ b/src/NullDepth/wwwroot/css/app.css @@ -19,7 +19,7 @@ kbd { color:var(--cyan); background:none; padding:0; } @keyframes system-flash { 0%,100% { opacity:0; } 15%,45% { opacity:1; } 30%,65% { opacity:.18; } } .screen-image { position:absolute; inset:0; min-height:100%; background-position:center; background-size:cover; isolation:isolate; } .vignette { position:absolute; inset:0; z-index:-1; background:linear-gradient(90deg,rgba(0,4,6,.87) 0%,rgba(0,4,6,.28) 40%,rgba(0,4,6,.15) 65%,rgba(0,4,6,.64) 100%),linear-gradient(0deg,rgba(0,3,5,.82) 0%,transparent 40%,rgba(0,3,5,.3) 100%); } -.loading-screen { height:100%; display:grid; place-items:center; color:var(--cyan); font:500 .8rem 'IBM Plex Mono'; letter-spacing:.35em; animation:pulse 1.6s infinite; } +.loading-screen { height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; color:var(--cyan); font:500 .8rem 'IBM Plex Mono'; letter-spacing:.35em; animation:pulse 1.6s infinite; } @keyframes pulse { 50% { opacity:.35; } } .title-card { position:absolute; left:6vw; top:9vh; } .title-card h1 { margin:.25rem 0; font-size:clamp(3rem,7vw,7rem); font-weight:300; letter-spacing:.19em; line-height:.95; text-shadow:0 2px 32px #000; } @@ -53,7 +53,7 @@ kbd { color:var(--cyan); background:none; padding:0; } .interaction-modal, .overlay-card { width:min(43rem,92vw); max-height:86vh; overflow:auto; padding:2rem; }.terminal-copy { white-space:pre-line; color:#bfe6ea; line-height:1.75; } .close-button { position:absolute; top:.65rem; right:.65rem; border:0; font-size:1.3rem; padding:.35rem .6rem; } .choice-list { display:flex; flex-direction:column; gap:.6rem; margin-top:1.5rem; }.choice-list button { text-align:left; } -.action-picker small { display:block; margin-top:1.25rem; color:var(--muted); letter-spacing:.12em; } +.action-picker small, .interaction-modal small { display:block; margin-top:1.25rem; color:var(--muted); letter-spacing:.12em; } .map-card { width:min(62rem,94vw); }.map-grid { display:grid; grid-template-columns:repeat(9,minmax(2.5rem,1fr)); grid-template-rows:repeat(4,3.6rem); gap:.35rem; margin:2rem 0; } .map-cell { display:grid; place-items:center; border:1px solid rgba(158,239,255,.1); color:#44565a; font:1rem 'IBM Plex Mono'; }.map-cell.visited { color:var(--cyan); background:rgba(65,213,240,.06); border-color:var(--line); }.map-cell.current { color:#021014; background:var(--cyan); box-shadow:0 0 24px rgba(66,216,244,.4); } .inventory-card ul { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.65rem; list-style:none; margin:1.5rem 0 0; padding:0; }.inventory-card li { padding:1rem; border:1px solid var(--line); }.inventory-card li small { display:block; color:var(--muted); font-size:.58rem; letter-spacing:.16em; }.inventory-card li strong { display:block; margin:.3rem 0; color:var(--cyan); font-weight:500; letter-spacing:.06em; }.inventory-card li p { margin:.25rem 0 0; color:#b8c8cb; font-size:.78rem; line-height:1.5; } diff --git a/src/NullDepth/wwwroot/index.html b/src/NullDepth/wwwroot/index.html index 811823e..6ce7a92 100644 --- a/src/NullDepth/wwwroot/index.html +++ b/src/NullDepth/wwwroot/index.html @@ -4,7 +4,7 @@ - + NullDepth @@ -15,15 +15,66 @@ +
- - - - -
+
+
+
+
+
+
+ +
+
+

FACILITY 03 // SUBLEVEL 03

+

NULL DEPTH

+

INITIALIZING SUBSYSTEM MATRIX

+
+
+
+ SYSTEM INTEGRITY + +
+
+
+
+
+
+ [NET_ID: ISOLATED] + [SECURITY: ALPHA] + [TERMINAL: BOOT] +
+
+
diff --git a/src/NullDepth/wwwroot/localization/en.json b/src/NullDepth/wwwroot/localization/en.json index c6f24ff..7ada187 100644 --- a/src/NullDepth/wwwroot/localization/en.json +++ b/src/NullDepth/wwwroot/localization/en.json @@ -1,5 +1,5 @@ { - "menu.newGame": "NEW GAME", "menu.continue": "CONTINUE", "menu.settings": "SETTINGS", "menu.controlsHint": "↑ ↓ SELECT ENTER CONFIRM", + "menu.newGame": "NEW GAME", "menu.continue": "CONTINUE", "menu.settings": "SETTINGS", "menu.controlsHint": "↑ ↓ / W S SELECT ENTER CONFIRM", "common.continue": "ENTER — CONTINUE", "common.confirm": "CONFIRM", "common.cancel": "CANCEL", "common.back": "BACK", "common.mainMenu": "MAIN MENU", "briefing.header": "CONTRACT // 7841-B", "briefing.clientLabel": "CLIENT", "briefing.siteLabel": "SITE", "briefing.assignmentLabel": "ASSIGNMENT", "briefing.assignment": "CENTRAL CORE DIAGNOSTICS", "briefing.compensationLabel": "COMPENSATION", "briefing.infoLabel": "ADDITIONAL INFORMATION", "briefing.none": "NONE", "briefing.story": "Elias Venn accepted the contract at 04:17. Five hours later, he is standing at the entrance to Facility 03. No further message has arrived from the client.", "game.autosave": "AUTOSAVE // ONLINE", "game.autosaveSaving": "AUTOSAVE // SAVING", "game.autosaveSaved": "AUTOSAVE // SAVED", "game.interact": "INTERACT", "game.map": "MAP", "game.inventory": "INVENTORY", "game.archive": "ARCHIVE", "game.pause": "PAUSE", "game.exit.locked": "Access is blocked.", diff --git a/src/NullDepth/wwwroot/localization/sk.json b/src/NullDepth/wwwroot/localization/sk.json index 61423b0..7097182 100644 --- a/src/NullDepth/wwwroot/localization/sk.json +++ b/src/NullDepth/wwwroot/localization/sk.json @@ -1,5 +1,5 @@ { - "menu.newGame": "NOVÁ HRA", "menu.continue": "POKRAČOVAŤ", "menu.settings": "NASTAVENIA", "menu.controlsHint": "↑ ↓ VÝBER ENTER POTVRDIŤ", + "menu.newGame": "NOVÁ HRA", "menu.continue": "POKRAČOVAŤ", "menu.settings": "NASTAVENIA", "menu.controlsHint": "↑ ↓ / W S VÝBER ENTER POTVRDIŤ", "common.continue": "ENTER — POKRAČOVAŤ", "common.confirm": "POTVRDIŤ", "common.cancel": "ZRUŠIŤ", "common.back": "SPÄŤ", "common.mainMenu": "HLAVNÉ MENU", "briefing.header": "ZMLUVA // 7841-B", "briefing.clientLabel": "KLIENT", "briefing.siteLabel": "LOKALITA", "briefing.assignmentLabel": "ÚLOHA", "briefing.assignment": "DIAGNOSTIKA CENTRÁLNEHO JADRA", "briefing.compensationLabel": "ODMENA", "briefing.infoLabel": "DODATOČNÉ INFORMÁCIE", "briefing.none": "ŽIADNE", "briefing.story": "Elias Venn prijal zákazku o 04:17. O päť hodín neskôr stojí pri vstupe do Facility 03. Od klienta odvtedy neprišla žiadna ďalšia správa.", "game.autosave": "AUTOSAVE // ONLINE", "game.autosaveSaving": "AUTOSAVE // UKLADÁM", "game.autosaveSaved": "AUTOSAVE // ULOŽENÉ", "game.interact": "INTERAKCIA", "game.map": "MAPA", "game.inventory": "INVENTÁR", "game.archive": "ARCHÍV", "game.pause": "PAUZA", "game.exit.locked": "Prístup je zablokovaný.", diff --git a/src/NullDepth/wwwroot/logo.png b/src/NullDepth/wwwroot/logo.png new file mode 100644 index 0000000..2352bd0 Binary files /dev/null and b/src/NullDepth/wwwroot/logo.png differ