Files
NullDepth/src/NullDepth/Pages/Home.razor
T
2026-08-27 20:42:45 +02:00

168 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/"
@inject GameService Game
<PageTitle>NULL DEPTH</PageTitle>
<main class="game-shell" tabindex="0" @ref="_shell" @onkeydown="HandleKey" @onkeydown:preventDefault>
@if (!_ready)
{
<section class="loading-screen"><span>NULL DEPTH // BOOTING</span></section>
}
else if (_screen == Screen.Title)
{
<section class="title-screen screen-image" style="background-image:url('assets/images/title/title_null_depth.webp')">
<div class="vignette"></div>
<div class="title-card"><p class="eyebrow">FACILITY 03</p><h1>NULL DEPTH</h1><p class="subtitle">SUBLEVEL // 03</p></div>
<nav class="title-menu" aria-label="Main menu">
<button class="@MenuClass(0)" @onclick="StartNewGame">@Game.T("menu.newGame")</button>
<button class="@MenuClass(1)" disabled="@(!Game.HasSave)" @onclick="ContinueGame">@Game.T("menu.continue")</button>
<button class="@MenuClass(2)" @onclick="() => _showSettings = true">@Game.T("menu.settings")</button>
<small>@Game.T("menu.controlsHint")</small>
</nav>
</section>
}
else if (_screen == Screen.Briefing)
{
<section class="briefing-screen screen-image" style="background-image:url('assets/images/title/title_null_depth.webp')">
<div class="vignette"></div>
<article class="terminal-card briefing-card">
<p class="eyebrow">@Game.T("briefing.header")</p>
<dl>
<dt>@Game.T("briefing.clientLabel")</dt><dd>VANTAGE SYSTEMS</dd>
<dt>@Game.T("briefing.siteLabel")</dt><dd>FACILITY 03</dd>
<dt>@Game.T("briefing.assignmentLabel")</dt><dd>@Game.T("briefing.assignment")</dd>
<dt>@Game.T("briefing.compensationLabel")</dt><dd>18,000</dd>
<dt>@Game.T("briefing.infoLabel")</dt><dd>@Game.T("briefing.none")</dd>
</dl>
<p>@Game.T("briefing.story")</p>
<button class="primary-action" @onclick="BeginPlay">@Game.T("common.continue")</button>
</article>
</section>
}
else if (_screen == Screen.Game)
{
var room = Game.CurrentRoom;
<section class="play-screen screen-image" style="background-image:url('@Game.RoomImage(room)')">
<div class="vignette"></div>
<header class="top-hud"><span>NULL DEPTH</span><span>@Game.T("game.autosave")</span></header>
<article class="room-panel">
<p class="eyebrow">@room.Id.Replace("_", " // ").ToUpperInvariant()</p><h2>@Game.T(room.NameKey)</h2><p>@Game.T(room.DescriptionKey)</p>
@if (!string.IsNullOrWhiteSpace(_notice)) { <p class="notice">@_notice</p> }
<div class="interaction-list">@foreach (var action in Game.AvailableInteractions()) { <button @onclick="() => OpenInteraction(action)"><kbd>E</kbd> @Game.T(action.LabelKey)</button> }</div>
</article>
<nav class="movement-pad" aria-label="Movement">
<button class="north" disabled="@(!HasNorth)" @onclick="MoveNorth">↑</button>
<button class="west" disabled="@(!HasWest)" @onclick="MoveWest">←</button><span class="position">@room.X:@room.Y</span>
<button class="east" disabled="@(!HasEast)" @onclick="MoveEast">→</button>
<button class="south" disabled="@(!HasSouth)" @onclick="MoveSouth">↓</button>
</nav>
<footer class="command-bar">
<button @onclick="OpenFirstInteraction"><kbd>E</kbd> @Game.T("game.interact")</button><button @onclick="() => _showMap = true"><kbd>M</kbd> @Game.T("game.map")</button>
<button @onclick="() => _showInventory = true"><kbd>I</kbd> @Game.T("game.inventory")</button><button @onclick="() => _showPause = true"><kbd>ESC</kbd> @Game.T("game.pause")</button>
</footer>
</section>
}
else if (_screen == Screen.Death)
{
<section class="outcome-screen death-screen"><p class="eyebrow">FACILITY EVENT // FATAL</p><h1>@Game.T("death.title")</h1><p>@Game.T("death.body")</p><button class="primary-action" @onclick="RestoreCheckpoint">@Game.T("death.restore")</button><button @onclick="ReturnToTitle">@Game.T("common.mainMenu")</button></section>
}
else if (_screen == Screen.Ending)
{
<section class="outcome-screen ending-screen"><p class="eyebrow">ENDING // @_ending?.ToUpperInvariant()</p><h1>@Game.T($"ending.{_ending}.title")</h1><p>@Game.T($"ending.{_ending}.body")</p><button class="primary-action" @onclick="ReturnToTitle">@Game.T("ending.return")</button></section>
}
@if (_interaction is not null)
{
<div class="modal-layer" role="dialog" aria-modal="true"><article class="terminal-card interaction-modal">
<button class="close-button" aria-label="Close" @onclick="CloseOverlays">×</button><p class="eyebrow">@_interaction.Type.ToUpperInvariant() // @_interaction.Id.ToUpperInvariant()</p><h2>@Game.T(_interaction.LabelKey)</h2><p class="terminal-copy">@Game.T(_interaction.ContentKey)</p>
@if (_interaction.Choices.Count > 0) { <div class="choice-list">@foreach (var choice in _interaction.Choices.Where(c => Game.HasAll(c.RequiresFlags))) { <button @onclick="() => ResolveChoice(choice)">@Game.T(choice.LabelKey)</button> }</div> }
else { <button class="primary-action" @onclick="ResolveInteraction">@Game.T("common.confirm")</button> }
</article></div>
}
@if (_showMap)
{
<div class="modal-layer" role="dialog" aria-modal="true"><article class="overlay-card map-card"><button class="close-button" aria-label="Close" @onclick="CloseOverlays">×</button><p class="eyebrow">FACILITY 03 // @Game.T("game.map")</p><h2>@Game.T("map.title")</h2>
<div class="map-grid">@foreach (var mapRoom in Game.Data.Rooms) { var visited = Game.State.VisitedRooms.Contains(mapRoom.Id); <div class="map-cell @(visited ? "visited" : "unknown") @(mapRoom.Id == Game.State.CurrentRoomId ? "current" : "")" style="grid-column:@(mapRoom.X + 1);grid-row:@(mapRoom.Y + 1)" title="@(visited ? Game.T(mapRoom.NameKey) : Game.T("map.unknown"))">@(visited ? (mapRoom.Id == Game.State.CurrentRoomId ? "●" : "□") : "?")</div> }</div><p>@Game.T("map.visited"): @Game.State.VisitedRooms.Count / @Game.Data.Rooms.Count</p>
</article></div>
}
@if (_showInventory)
{
<div class="modal-layer" role="dialog" aria-modal="true"><article class="overlay-card inventory-card"><button class="close-button" aria-label="Close" @onclick="CloseOverlays">×</button><p class="eyebrow">ELIAS VENN // LOADOUT</p><h2>@Game.T("inventory.title")</h2>@if (Game.State.Inventory.Count == 0) { <p>@Game.T("inventory.empty")</p> } else { <ul>@foreach (var item in Game.State.Inventory) { <li>@Game.T($"item.{item}")</li> }</ul> }</article></div>
}
@if (_showPause)
{
<div class="modal-layer" role="dialog" aria-modal="true"><article class="overlay-card pause-card"><p class="eyebrow">SYSTEM // PAUSED</p><h2>@Game.T("pause.title")</h2><button class="primary-action" @onclick="CloseOverlays">@Game.T("pause.resume")</button><button @onclick="() => _showSettings = true">@Game.T("menu.settings")</button><button @onclick="ReturnToTitle">@Game.T("common.mainMenu")</button></article></div>
}
@if (_showSettings)
{
<div class="modal-layer settings-layer" role="dialog" aria-modal="true"><article class="overlay-card settings-card"><button class="close-button" aria-label="Close" @onclick="CloseSettings">×</button><p class="eyebrow">SYSTEM // CONFIGURATION</p><h2>@Game.T("settings.title")</h2>
<label>@Game.T("settings.language")</label><div class="segmented-control"><button class="@(Game.Settings.Language == "sk" ? "active" : "")" @onclick="ChangeToSlovak">Slovenčina</button><button class="@(Game.Settings.Language == "en" ? "active" : "")" @onclick="ChangeToEnglish">English</button></div>
<label for="volume">@Game.T("settings.volume") — @Game.Settings.MasterVolume%</label><input id="volume" type="range" min="0" max="100" step="5" @bind="Game.Settings.MasterVolume" @bind:event="oninput" @onchange="SaveSettings" /><button class="primary-action" @onclick="CloseSettings">@Game.T("common.back")</button>
</article></div>
}
</main>
@code {
private ElementReference _shell;
private bool _ready;
private Screen _screen = Screen.Title;
private int _menuIndex;
private string? _notice;
private string? _ending;
private InteractionDefinition? _interaction;
private bool _showMap;
private bool _showInventory;
private bool _showPause;
private bool _showSettings;
protected override async Task OnInitializedAsync() { await Game.InitializeAsync(); _ready = true; }
protected override async Task OnAfterRenderAsync(bool firstRender) { if (_ready) await _shell.FocusAsync(); }
private async Task StartNewGame() { await Game.NewGameAsync(); _screen = Screen.Briefing; _notice = null; }
private async Task ContinueGame() { if (await Game.ContinueAsync()) _screen = Screen.Game; }
private void BeginPlay() => _screen = Screen.Game;
private async Task Move(string direction) { var result = await Game.MoveAsync(direction); if (result is not null) _notice = result; if (result == "") _notice = null; }
private Task MoveNorth() => Move("north");
private Task MoveSouth() => Move("south");
private Task MoveWest() => Move("west");
private Task MoveEast() => Move("east");
private bool HasNorth => Game.CurrentRoom.Exits.ContainsKey("north");
private bool HasSouth => Game.CurrentRoom.Exits.ContainsKey("south");
private bool HasWest => Game.CurrentRoom.Exits.ContainsKey("west");
private bool HasEast => Game.CurrentRoom.Exits.ContainsKey("east");
private void OpenFirstInteraction() { var first = Game.AvailableInteractions().FirstOrDefault(); if (first is not null) OpenInteraction(first); }
private void OpenInteraction(InteractionDefinition interaction) { _interaction = interaction; _notice = null; }
private async Task ResolveInteraction() { if (_interaction is null) return; await Game.ApplyInteractionAsync(_interaction); _notice = Game.T(_interaction.ContentKey); _interaction = null; }
private async Task ResolveChoice(ChoiceDefinition choice) { await Game.ApplyChoiceAsync(choice); _interaction = null; if (choice.Death) { _screen = Screen.Death; return; } if (!string.IsNullOrWhiteSpace(choice.Ending)) { _ending = choice.Ending; _screen = Screen.Ending; return; } _notice = choice.ResultKey is null ? null : Game.T(choice.ResultKey); }
private async Task RestoreCheckpoint() { await Game.RestoreCheckpointAsync(); _screen = Screen.Game; _notice = Game.T("death.restored"); }
private void ReturnToTitle() { CloseOverlays(); _showSettings = false; _screen = Screen.Title; _menuIndex = 0; }
private async Task ChangeLanguage(string language) { await Game.SetLanguageAsync(language); StateHasChanged(); }
private Task ChangeToSlovak() => ChangeLanguage("sk");
private Task ChangeToEnglish() => ChangeLanguage("en");
private async Task SaveSettings(ChangeEventArgs _) => await Game.SaveSettingsAsync();
private async Task CloseSettings() { await Game.SaveSettingsAsync(); _showSettings = false; }
private void CloseOverlays() { _interaction = null; _showMap = false; _showInventory = false; _showPause = false; }
private string MenuClass(int index) => _menuIndex == index ? "selected" : "";
private async Task HandleKey(KeyboardEventArgs args)
{
if (!_ready) return;
if (_showSettings) { if (args.Key == "Escape") await CloseSettings(); return; }
if (_interaction is not null || _showMap || _showInventory) { if (args.Key == "Escape") CloseOverlays(); return; }
if (_showPause) { if (args.Key == "Escape") CloseOverlays(); return; }
if (_screen == Screen.Title)
{
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 _showSettings = true; }
return;
}
if (_screen == Screen.Briefing && args.Key == "Enter") { BeginPlay(); return; }
if (_screen != Screen.Game) return;
switch (args.Key.ToLowerInvariant())
{
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": OpenFirstInteraction(); break; case "m": _showMap = true; break; case "i": _showInventory = true; break; case "escape": _showPause = true; break;
}
}
private enum Screen { Title, Briefing, Game, Death, Ending }
}