initial impl

This commit is contained in:
Michal Vrba
2026-08-27 20:42:45 +02:00
parent d02076f14b
commit d5e7df6b0e
53 changed files with 1336 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>
+137
View File
@@ -0,0 +1,137 @@
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.JSInterop;
using NullDepth.Models;
namespace NullDepth.Game;
public sealed class GameService(HttpClient http, IJSRuntime js)
{
private const string SaveKey = "null-depth.save.v1";
private const string SettingsKey = "null-depth.settings.v1";
private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);
private Dictionary<string, string> _texts = [];
public GameData Data { get; private set; } = new();
public GameState State { get; private set; } = new();
public PlayerSettings Settings { get; private set; } = new();
public bool HasSave { get; private set; }
public async Task InitializeAsync()
{
Data = await http.GetFromJsonAsync<GameData>("data/game.json", _json) ?? new GameData();
Settings = await LoadValueAsync<PlayerSettings>(SettingsKey) ?? new PlayerSettings
{
Language = await js.InvokeAsync<string>("nullDepth.browserLanguage")
};
await SetLanguageAsync(Settings.Language, persist: false);
HasSave = !string.IsNullOrWhiteSpace(await js.InvokeAsync<string?>("localStorage.getItem", SaveKey));
}
public string T(string key) => _texts.TryGetValue(key, out var text) ? text : key;
public RoomDefinition CurrentRoom => Data.Rooms.First(r => r.Id == State.CurrentRoomId);
public RoomDefinition? FindRoom(string id) => Data.Rooms.FirstOrDefault(r => r.Id == id);
public bool HasAll(IEnumerable<string> flags) => flags.All(State.Flags.Contains);
public async Task SetLanguageAsync(string language, bool persist = true)
{
Settings.Language = language == "sk" ? "sk" : "en";
_texts = await http.GetFromJsonAsync<Dictionary<string, string>>($"localization/{Settings.Language}.json", _json) ?? [];
if (persist) await SaveSettingsAsync();
}
public async Task NewGameAsync()
{
State = new GameState { CurrentRoomId = Data.StartRoomId };
State.VisitedRooms.Add(State.CurrentRoomId);
SetCheckpoint();
await SaveAsync();
}
public async Task<bool> ContinueAsync()
{
var loaded = await LoadValueAsync<GameState>(SaveKey);
if (loaded is null || FindRoom(loaded.CurrentRoomId) is null) return false;
State = loaded;
State.VisitedRooms.Add(State.CurrentRoomId);
HasSave = true;
return true;
}
public async Task<string?> MoveAsync(string direction)
{
if (!CurrentRoom.Exits.TryGetValue(direction, out var exit)) return null;
if (!HasAll(exit.RequiresFlags)) return T(exit.LockedMessageKey ?? "game.exit.locked");
State.CurrentRoomId = exit.Target;
State.VisitedRooms.Add(exit.Target);
await SaveAsync();
return "";
}
public IEnumerable<InteractionDefinition> AvailableInteractions() => CurrentRoom.Interactions.Where(i =>
HasAll(i.RequiresFlags) && !i.ForbiddenFlags.Any(State.Flags.Contains));
public async Task ApplyInteractionAsync(InteractionDefinition interaction)
{
Apply(interaction.SetFlags, interaction.AddItems);
if (interaction.Checkpoint) SetCheckpoint();
await SaveAsync();
}
public async Task ApplyChoiceAsync(ChoiceDefinition choice)
{
Apply(choice.SetFlags, choice.AddItems);
if (choice.Checkpoint) SetCheckpoint();
await SaveAsync();
}
public async Task RestoreCheckpointAsync()
{
var checkpoint = State.Checkpoint;
if (checkpoint is null) { await NewGameAsync(); return; }
State.CurrentRoomId = checkpoint.RoomId;
State.VisitedRooms = [.. checkpoint.VisitedRooms];
State.Flags = [.. checkpoint.Flags];
State.Inventory = [.. checkpoint.Inventory];
await SaveAsync();
}
public string RoomImage(RoomDefinition room) =>
room.AlternateImage is not null && room.AlternateImageFlag is not null && State.Flags.Contains(room.AlternateImageFlag)
? room.AlternateImage
: room.Image;
public async Task SaveSettingsAsync() => await StoreValueAsync(SettingsKey, Settings);
public async Task SaveAsync()
{
State.SavedAt = DateTimeOffset.UtcNow;
await StoreValueAsync(SaveKey, State);
HasSave = true;
}
private void Apply(IEnumerable<string> flags, IEnumerable<string> items)
{
foreach (var flag in flags) State.Flags.Add(flag);
foreach (var item in items) if (!State.Inventory.Contains(item)) State.Inventory.Add(item);
}
private void SetCheckpoint() => State.Checkpoint = new GameSnapshot
{
RoomId = State.CurrentRoomId,
VisitedRooms = [.. State.VisitedRooms],
Flags = [.. State.Flags],
Inventory = [.. State.Inventory]
};
private async Task<T?> LoadValueAsync<T>(string key)
{
var raw = await js.InvokeAsync<string?>("localStorage.getItem", key);
if (string.IsNullOrWhiteSpace(raw)) return default;
try { return JsonSerializer.Deserialize<T>(raw, _json); }
catch { return default; }
}
private async Task StoreValueAsync<T>(string key, T value) =>
await js.InvokeVoidAsync("localStorage.setItem", key, JsonSerializer.Serialize(value, _json));
}
+3
View File
@@ -0,0 +1,3 @@
@inherits LayoutComponentBase
@Body
+79
View File
@@ -0,0 +1,79 @@
namespace NullDepth.Models;
public sealed class GameData
{
public string StartRoomId { get; set; } = "loc_01_exterior_access";
public List<RoomDefinition> Rooms { get; set; } = [];
}
public sealed class RoomDefinition
{
public string Id { get; set; } = "";
public string NameKey { get; set; } = "";
public string DescriptionKey { get; set; } = "";
public string Image { get; set; } = "";
public string? AlternateImage { get; set; }
public string? AlternateImageFlag { get; set; }
public int X { get; set; }
public int Y { get; set; }
public Dictionary<string, ExitDefinition> Exits { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public List<InteractionDefinition> Interactions { get; set; } = [];
}
public sealed class ExitDefinition
{
public string Target { get; set; } = "";
public List<string> RequiresFlags { get; set; } = [];
public string? LockedMessageKey { get; set; }
}
public sealed class InteractionDefinition
{
public string Id { get; set; } = "";
public string LabelKey { get; set; } = "";
public string ContentKey { get; set; } = "";
public string Type { get; set; } = "terminal";
public List<string> RequiresFlags { get; set; } = [];
public List<string> ForbiddenFlags { get; set; } = [];
public List<string> SetFlags { get; set; } = [];
public List<string> AddItems { get; set; } = [];
public bool Checkpoint { get; set; }
public List<ChoiceDefinition> Choices { get; set; } = [];
}
public sealed class ChoiceDefinition
{
public string LabelKey { get; set; } = "";
public string? ResultKey { get; set; }
public List<string> RequiresFlags { get; set; } = [];
public List<string> SetFlags { get; set; } = [];
public List<string> AddItems { get; set; } = [];
public string? Ending { get; set; }
public bool Death { get; set; }
public bool Checkpoint { get; set; }
}
public sealed class GameState
{
public int Version { get; set; } = 1;
public string CurrentRoomId { get; set; } = "loc_01_exterior_access";
public HashSet<string> VisitedRooms { get; set; } = [];
public HashSet<string> Flags { get; set; } = [];
public List<string> Inventory { get; set; } = [];
public GameSnapshot? Checkpoint { get; set; }
public DateTimeOffset SavedAt { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class GameSnapshot
{
public string RoomId { get; set; } = "loc_01_exterior_access";
public HashSet<string> VisitedRooms { get; set; } = [];
public HashSet<string> Flags { get; set; } = [];
public List<string> Inventory { get; set; } = [];
}
public sealed class PlayerSettings
{
public string Language { get; set; } = "en";
public int MasterVolume { get; set; } = 70;
}
+15
View File
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.11" PrivateAssets="all" />
</ItemGroup>
</Project>
+167
View File
@@ -0,0 +1,167 @@
@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 }
}
+5
View File
@@ -0,0 +1,5 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
+13
View File
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using NullDepth;
using NullDepth.Game;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<GameService>();
await builder.Build().RunAsync();
@@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5297",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7174;http://localhost:5297",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+29
View File
@@ -0,0 +1,29 @@
# NULL DEPTH
Minimalistická obrazová sci-fi adventúra postavená ako Blazor WebAssembly aplikácia na .NET 10.
## Spustenie
```powershell
dotnet run --project src\NullDepth\NullDepth.csproj
```
Príkaz spustite z `C:\DevVibe\NullDepth` a otvorte adresu zobrazenú v konzole. Produkčný build vytvoríte príkazom `dotnet build NullDepth.slnx -c Release`.
## Dátová architektúra
- `src/NullDepth/wwwroot/data/game.json` obsahuje všetky miestnosti, grid súradnice, cesty, podmienky, interakcie, voľby, checkpointy a endingy.
- `src/NullDepth/wwwroot/localization/sk.json` a `en.json` obsahujú všetok text rozhrania aj príbehu.
- `src/NullDepth/wwwroot/assets/images/locations` obsahuje 16:9 obrazy miestností.
- `src/NullDepth/wwwroot/assets/audio` je pripravený pre finálne ambience a SFX.
Novú miestnosť možno pridať iba do `game.json` a doplniť jej textové kľúče do oboch lokalizačných JSON súborov. Herný engine netreba meniť.
## Ovládanie
- šípky: pohyb
- `E`: interakcia
- `M`: minimapa
- `I`: inventár
- `Esc`: zatvorenie overlayu / pauza
- všetky akcie sú zároveň klikateľné
+12
View File
@@ -0,0 +1,12 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using NullDepth
@using NullDepth.Layout
@using NullDepth.Game
@using NullDepth.Models
@@ -0,0 +1,85 @@
"""Generate deterministic 16:9 diagnostic room images for NULL DEPTH."""
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import json
import random
ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / "wwwroot" / "data" / "game.json"
OUTPUT = ROOT / "wwwroot" / "assets" / "images" / "locations"
WIDTH, HEIGHT = 1344, 768
def font(size: int, bold: bool = False):
candidates = [
Path("C:/Windows/Fonts/consolab.ttf" if bold else "C:/Windows/Fonts/consola.ttf"),
Path("C:/Windows/Fonts/arialbd.ttf" if bold else "C:/Windows/Fonts/arial.ttf"),
]
for candidate in candidates:
if candidate.exists():
return ImageFont.truetype(str(candidate), size)
return ImageFont.load_default()
def render(filename: str, room_id: str, title: str, state: str = "BASE STATE"):
seed = sum(ord(char) for char in filename)
random.seed(seed)
alarm = "alarm" in filename or "stalled" in filename or "shutdown" in filename
accent = (225, 55, 61) if alarm else (63, 205, 224)
image = Image.new("RGB", (WIDTH, HEIGHT), (5, 10, 12))
draw = ImageDraw.Draw(image)
for y in range(HEIGHT):
value = int(19 * (1 - y / HEIGHT))
draw.line((0, y, WIDTH, y), fill=(5 + value // 3, 11 + value // 2, 14 + value))
horizon = 470
draw.polygon([(0, HEIGHT), (WIDTH, HEIGHT), (900, horizon), (444, horizon)], fill=(11, 19, 22))
draw.rectangle((110, 100, WIDTH - 110, horizon), outline=(34, 53, 58), width=5)
draw.rectangle((430, 160, 914, horizon), fill=(4, 8, 10), outline=(48, 68, 72), width=8)
for x in range(465, 900, 72):
draw.rectangle((x, 190, x + 34, horizon - 20), fill=(11, 21, 24), outline=(29, 45, 49), width=2)
draw.rectangle((x + 8, 212, x + 26, 218), fill=accent)
for x in (150, 1160):
draw.rectangle((x, 145, x + 55, 520), fill=(8, 17, 20), outline=(34, 56, 60), width=4)
for y in range(175, 500, 55):
draw.line((x + 5, y, x + 50, y), fill=accent, width=3)
haze = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
haze_draw = ImageDraw.Draw(haze)
for _ in range(18):
px = random.randrange(WIDTH)
py = random.randrange(HEIGHT // 2, HEIGHT)
radius = random.randrange(50, 190)
haze_draw.ellipse((px - radius, py - radius // 3, px + radius, py + radius // 3), fill=(*accent, 8))
image = Image.alpha_composite(image.convert("RGBA"), haze.filter(ImageFilter.GaussianBlur(25))).convert("RGB")
draw = ImageDraw.Draw(image)
draw.rectangle((0, 0, WIDTH, 64), fill=(1, 5, 7))
draw.text((42, 23), "NULL DEPTH // PLACEHOLDER ASSET", font=font(18), fill=(142, 181, 187))
draw.text((WIDTH - 260, 23), "1344 × 768 // 16:9", font=font(18), fill=accent)
draw.rectangle((58, 548, 850, 700), fill=(2, 7, 9), outline=(*accent,), width=2)
draw.text((86, 575), room_id.upper(), font=font(22), fill=accent)
draw.text((86, 615), title.upper(), font=font(38, bold=True), fill=(226, 240, 241))
draw.text((88, 668), state, font=font(16), fill=(125, 151, 156))
draw.text((WIDTH - 70, HEIGHT - 48), filename, anchor="ra", font=font(16), fill=(117, 142, 147))
image.save(OUTPUT / filename, "WEBP", quality=88, method=6)
def main():
OUTPUT.mkdir(parents=True, exist_ok=True)
data = json.loads(DATA.read_text(encoding="utf-8"))
for room in data["rooms"]:
base = Path(room["image"]).name
title = room["nameKey"].split(".")[-2].replace("loc_", "LOCATION ")
render(base, room["id"], title)
alternate = room.get("alternateImage")
if alternate:
alt_name = Path(alternate).name
state = "ALTERNATE // " + room.get("alternateImageFlag", "STATE").replace("_", " ").upper()
render(alt_name, room["id"], title, state)
if __name__ == "__main__":
main()
+40
View File
@@ -0,0 +1,40 @@
"""Build browser and app icon sizes from the approved NULL DEPTH source mark."""
from pathlib import Path
from PIL import Image, ImageFilter
ROOT = Path(__file__).resolve().parents[3]
SOURCE = ROOT / "specs" / "source_assets" / "favicon_null_depth_source.png"
OUTPUT = ROOT / "src" / "NullDepth" / "wwwroot"
BACKGROUND = (3, 8, 10, 255)
def render(size: int, filename: str) -> None:
source = Image.open(SOURCE).convert("RGBA")
bounds = source.getchannel("A").getbbox() or (0, 0, *source.size)
source = source.crop(bounds)
inset = max(2, round(size * 0.055))
source.thumbnail((size - 2 * inset, size - 2 * inset), Image.Resampling.LANCZOS)
source = source.filter(
ImageFilter.UnsharpMask(radius=max(0.4, size / 300), percent=120, threshold=2)
)
canvas = Image.new("RGBA", (size, size), BACKGROUND)
canvas.alpha_composite(source, ((size - source.width) // 2, (size - source.height) // 2))
canvas.convert("RGB").save(OUTPUT / filename, "PNG", optimize=True)
def main() -> None:
render(64, "favicon.png")
render(192, "icon-192.png")
render(512, "icon-512.png")
icon = Image.open(OUTPUT / "icon-512.png").convert("RGBA")
icon.save(
OUTPUT / "favicon.ico",
format="ICO",
sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64)],
)
if __name__ == "__main__":
main()
@@ -0,0 +1,13 @@
# Audio asset slots
Pripravené názvy pre finálne assety:
- `amb_facility_loop.ogg`
- `amb_maintenance_loop.ogg`
- `amb_core_loop.ogg`
- `sfx_ui_confirm.ogg`
- `sfx_door_heavy.ogg`
- `sfx_power_restore.ogg`
- `sfx_alarm_pulse.ogg`
Audio vrstva je zámerne voliteľná; hra je plne funkčná aj bez týchto súborov.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

+51
View File
@@ -0,0 +1,51 @@
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Inter:wght@300;400;500;600&display=swap');
:root { color-scheme: dark; --cyan:#9eefff; --cyan-strong:#42d8f4; --panel:rgba(3,10,13,.84); --line:rgba(158,239,255,.28); --muted:#8fa7ac; --danger:#ff4d57; }
* { box-sizing:border-box; }
html, body, #app { width:100%; height:100%; margin:0; overflow:hidden; background:#020507; }
body { color:#e9f5f6; font-family:Inter,Segoe UI,sans-serif; }
button, input { font:inherit; }
button { color:inherit; background:transparent; border:1px solid var(--line); padding:.75rem 1rem; letter-spacing:.09em; cursor:pointer; transition:.16s ease; }
button:hover, button:focus-visible, button.selected, .segmented-control button.active { color:#041014; background:var(--cyan); border-color:var(--cyan); outline:none; }
button:disabled { opacity:.28; cursor:not-allowed; background:transparent; color:inherit; }
kbd, .eyebrow, .subtitle, .top-hud, .command-bar, .terminal-copy, small { font-family:'IBM Plex Mono',Consolas,monospace; }
kbd { color:var(--cyan); background:none; padding:0; }
.game-shell { position:relative; width:100%; height:100%; outline:none; background:#020507; }
.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; }
@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; }
.eyebrow { margin:0 0 .7rem; color:var(--cyan); font-size:.68rem; letter-spacing:.22em; text-transform:uppercase; }
.subtitle { color:var(--muted); letter-spacing:.4em; font-size:.74rem; }
.title-menu { position:absolute; left:6vw; bottom:8vh; display:flex; width:min(22rem,80vw); flex-direction:column; align-items:stretch; gap:.45rem; }
.title-menu button { border:0; border-left:2px solid transparent; text-align:left; padding:.7rem 1rem; font-size:.78rem; }
.title-menu button:hover, .title-menu button.selected { background:linear-gradient(90deg,rgba(158,239,255,.95),rgba(158,239,255,.08)); border-left-color:white; }
.title-menu small { margin-top:1rem; color:var(--muted); letter-spacing:.12em; }
.terminal-card, .overlay-card { position:relative; border:1px solid var(--line); background:linear-gradient(145deg,rgba(6,17,21,.96),rgba(1,7,10,.94)); box-shadow:0 30px 90px #000, inset 0 0 40px rgba(66,216,244,.025); backdrop-filter:blur(16px); }
.briefing-card { position:absolute; left:6vw; top:50%; transform:translateY(-50%); width:min(38rem,88vw); padding:2.2rem; }
.briefing-card dl { display:grid; grid-template-columns:10rem 1fr; margin:1.7rem 0; gap:.55rem 1rem; font-family:'IBM Plex Mono'; font-size:.78rem; }
.briefing-card dt { color:var(--muted); font-weight:400; }.briefing-card dd { margin:0;color:var(--cyan); }
.briefing-card>p:not(.eyebrow) { color:#c4d0d2; line-height:1.7; max-width:34rem; }
.primary-action { color:#031013; background:var(--cyan); border-color:var(--cyan); }
.top-hud { position:absolute; top:0; left:0; right:0; display:flex; justify-content:space-between; padding:1.25rem 1.6rem; color:rgba(220,248,250,.65); font-size:.62rem; letter-spacing:.18em; background:linear-gradient(#020608b8,transparent); }
.room-panel { position:absolute; left:2rem; bottom:5.8rem; width:min(36rem,calc(100vw - 11rem)); padding:1.5rem 1.6rem; background:linear-gradient(90deg,rgba(2,8,11,.93),rgba(2,8,11,.55),transparent); text-shadow:0 2px 8px #000; }
.room-panel h2, .overlay-card h2, .terminal-card h2 { margin:.15rem 0 .65rem; font-weight:400; letter-spacing:.11em; }
.room-panel>p:not(.eyebrow) { max-width:33rem; margin:.35rem 0; color:#c6d2d4; line-height:1.5; }
.room-panel .notice { color:var(--cyan)!important; font-family:'IBM Plex Mono'; font-size:.76rem; white-space:pre-line; }
.interaction-list { display:flex; flex-wrap:wrap; gap:.5rem; margin-top:1rem; }.interaction-list button { padding:.55rem .7rem; font-size:.68rem; }
.movement-pad { position:absolute; right:2rem; bottom:6.2rem; display:grid; grid-template:repeat(3,2.7rem)/repeat(3,2.7rem); gap:.25rem; }
.movement-pad button { padding:0; font-size:1.2rem; background:rgba(2,8,11,.78); }.movement-pad .north { grid-area:1/2; }.movement-pad .west { grid-area:2/1; }.movement-pad .position { grid-area:2/2; display:grid; place-items:center; color:var(--muted); font:.58rem 'IBM Plex Mono'; }.movement-pad .east { grid-area:2/3; }.movement-pad .south { grid-area:3/2; }
.command-bar { position:absolute; left:0; right:0; bottom:0; display:flex; justify-content:center; gap:.5rem; padding:.8rem; background:rgba(1,6,8,.91); border-top:1px solid rgba(158,239,255,.16); }.command-bar button { border:0; font-size:.66rem; }
.modal-layer { position:absolute; inset:0; z-index:20; display:grid; place-items:center; padding:2rem; background:rgba(0,3,5,.72); backdrop-filter:blur(7px); }
.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; }
.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 { list-style:none; margin:1.5rem 0 0; padding:0; }.inventory-card li { padding:1rem; border:1px solid var(--line); color:var(--cyan); }
.pause-card, .settings-card { display:flex; flex-direction:column; gap:.75rem; width:min(29rem,92vw); }.settings-card label { margin-top:.5rem; color:var(--muted); font-size:.76rem; letter-spacing:.09em; }.segmented-control { display:grid; grid-template-columns:1fr 1fr; gap:.4rem; }.settings-card input { accent-color:var(--cyan-strong); }
.outcome-screen { height:100%; display:flex; flex-direction:column; align-items:flex-start; justify-content:center; padding:10vw; background:radial-gradient(circle at 70% 40%,#11242b,#020507 55%); }.outcome-screen h1 { margin:.2rem 0; font-size:clamp(3rem,8vw,7rem); font-weight:300; letter-spacing:.17em; }.outcome-screen>p:not(.eyebrow) { max-width:44rem; color:#b8c8cb; line-height:1.8; }.outcome-screen button { margin:.35rem .35rem .35rem 0; }.death-screen { background:radial-gradient(circle at 70% 40%,#351014,#050203 55%); }.death-screen .eyebrow { color:var(--danger); }
#blazor-error-ui { display:none; position:fixed; inset:auto 0 0; z-index:100; padding:1rem; background:#8b1e24; color:white; }
@media(max-width:720px) { .title-card { left:1.5rem;top:8vh; }.title-card h1 { font-size:2.7rem;letter-spacing:.12em; }.title-menu { left:1.5rem;bottom:5rem; }.room-panel { left:0;bottom:5.4rem;width:100%;padding:1.2rem;background:rgba(2,8,11,.82); }.movement-pad { right:1rem;bottom:15rem; }.command-bar { overflow-x:auto;justify-content:flex-start; }.command-bar button { white-space:nowrap; }.map-grid { grid-template-columns:repeat(9,2.3rem);overflow:auto; }.briefing-card { left:1rem;width:calc(100vw - 2rem);padding:1.4rem; }.briefing-card dl { grid-template-columns:1fr; }.briefing-card dd { margin-bottom:.5rem; } }
+123
View File
@@ -0,0 +1,123 @@
{
"startRoomId": "loc_01_exterior_access",
"rooms": [
{
"id": "loc_01_exterior_access", "nameKey": "room.loc_01.name", "descriptionKey": "room.loc_01.description", "image": "assets/images/locations/loc_01_exterior_access.webp", "x": 0, "y": 1,
"exits": { "east": { "target": "loc_02_security_lobby", "requiresFlags": ["entry_authorized"], "lockedMessageKey": "lock.entry" } },
"interactions": [
{ "id": "contractor_terminal", "labelKey": "action.entryTerminal", "contentKey": "terminal.entry", "type": "terminal", "forbiddenFlags": ["entry_authorized"], "setFlags": ["entry_authorized"], "checkpoint": true }
]
},
{
"id": "loc_02_security_lobby", "nameKey": "room.loc_02.name", "descriptionKey": "room.loc_02.description", "image": "assets/images/locations/loc_02_security_lobby.webp", "x": 1, "y": 1,
"exits": { "west": { "target": "loc_01_exterior_access" }, "east": { "target": "loc_03_main_corridor" } },
"interactions": [
{ "id": "personnel_status", "labelKey": "action.securityTerminal", "contentKey": "terminal.security", "type": "terminal", "forbiddenFlags": ["personnel_checked"], "setFlags": ["personnel_checked"] }
]
},
{
"id": "loc_03_main_corridor", "nameKey": "room.loc_03.name", "descriptionKey": "room.loc_03.description", "image": "assets/images/locations/loc_03_main_corridor.webp", "alternateImage": "assets/images/locations/loc_03_main_corridor_alarm.webp", "alternateImageFlag": "power_restored", "x": 2, "y": 1,
"exits": {
"west": { "target": "loc_02_security_lobby" }, "north": { "target": "loc_06_operations_room" }, "south": { "target": "loc_04_maintenance_control" },
"east": { "target": "loc_07_service_elevator", "requiresFlags": ["power_restored"], "lockedMessageKey": "lock.auxPower" }
},
"interactions": []
},
{
"id": "loc_04_maintenance_control", "nameKey": "room.loc_04.name", "descriptionKey": "room.loc_04.description", "image": "assets/images/locations/loc_04_maintenance_control.webp", "x": 2, "y": 2,
"exits": { "north": { "target": "loc_03_main_corridor" }, "west": { "target": "loc_05_staff_access" } },
"interactions": [
{ "id": "auxiliary_grid", "labelKey": "action.restorePower", "contentKey": "terminal.power", "type": "system", "forbiddenFlags": ["power_restored"], "setFlags": ["power_restored"], "checkpoint": true }
]
},
{
"id": "loc_05_staff_access", "nameKey": "room.loc_05.name", "descriptionKey": "room.loc_05.description", "image": "assets/images/locations/loc_05_staff_access.webp", "x": 1, "y": 2,
"exits": { "east": { "target": "loc_04_maintenance_control" } },
"interactions": [
{ "id": "log_01", "labelKey": "action.readLog01", "contentKey": "log.01", "type": "archive", "forbiddenFlags": ["log_01_found"], "setFlags": ["log_01_found"] }
]
},
{
"id": "loc_06_operations_room", "nameKey": "room.loc_06.name", "descriptionKey": "room.loc_06.description", "image": "assets/images/locations/loc_06_operations_room.webp", "x": 2, "y": 0,
"exits": { "south": { "target": "loc_03_main_corridor" } },
"interactions": [
{ "id": "containment_record", "labelKey": "action.operationsTerminal", "contentKey": "terminal.operations", "type": "archive", "forbiddenFlags": ["containment_seen"], "setFlags": ["containment_seen"] }
]
},
{
"id": "loc_07_service_elevator", "nameKey": "room.loc_07.name", "descriptionKey": "room.loc_07.description", "image": "assets/images/locations/loc_07_service_elevator.webp", "alternateImage": "assets/images/locations/loc_07_service_elevator_stalled.webp", "alternateImageFlag": "elevator_stalled", "x": 3, "y": 1,
"exits": { "west": { "target": "loc_03_main_corridor" }, "south": { "target": "loc_08_lower_landing", "requiresFlags": ["elevator_cycle_complete"], "lockedMessageKey": "lock.elevator" } },
"interactions": [
{ "id": "elevator_descent", "labelKey": "action.descend", "contentKey": "event.elevator", "type": "event", "forbiddenFlags": ["elevator_cycle_complete"], "setFlags": ["elevator_stalled", "claustrophobia_detected", "elevator_cycle_complete"], "checkpoint": true }
]
},
{
"id": "loc_08_lower_landing", "nameKey": "room.loc_08.name", "descriptionKey": "room.loc_08.description", "image": "assets/images/locations/loc_08_lower_landing.webp", "x": 3, "y": 2,
"exits": { "north": { "target": "loc_07_service_elevator" }, "east": { "target": "loc_09_cooling_junction" }, "south": { "target": "loc_11_equipment_storage" } },
"interactions": []
},
{
"id": "loc_09_cooling_junction", "nameKey": "room.loc_09.name", "descriptionKey": "room.loc_09.description", "image": "assets/images/locations/loc_09_cooling_junction.webp", "x": 4, "y": 2,
"exits": { "west": { "target": "loc_08_lower_landing" }, "south": { "target": "loc_10_cooling_control" }, "east": { "target": "loc_12_service_passage", "requiresFlags": ["cooling_restored"], "lockedMessageKey": "lock.cooling" } },
"interactions": [
{ "id": "log_02", "labelKey": "action.readLog02", "contentKey": "log.02", "type": "archive", "forbiddenFlags": ["log_02_found"], "setFlags": ["log_02_found"] }
]
},
{
"id": "loc_10_cooling_control", "nameKey": "room.loc_10.name", "descriptionKey": "room.loc_10.description", "image": "assets/images/locations/loc_10_cooling_control.webp", "x": 4, "y": 3,
"exits": { "north": { "target": "loc_09_cooling_junction" } },
"interactions": [
{ "id": "power_routing", "labelKey": "action.routePower", "contentKey": "terminal.routing", "type": "system", "forbiddenFlags": ["cooling_restored"], "choices": [
{ "labelKey": "choice.coolingOnly", "resultKey": "result.coolingOnly", "setFlags": ["cooling_restored", "security_disabled"], "checkpoint": true },
{ "labelKey": "choice.balanced", "resultKey": "result.balanced", "setFlags": ["cooling_restored", "security_maintained"], "checkpoint": true }
] }
]
},
{
"id": "loc_11_equipment_storage", "nameKey": "room.loc_11.name", "descriptionKey": "room.loc_11.description", "image": "assets/images/locations/loc_11_equipment_storage.webp", "x": 3, "y": 3,
"exits": { "north": { "target": "loc_08_lower_landing" } },
"interactions": [
{ "id": "manual_override", "labelKey": "action.inspectRelay", "contentKey": "terminal.relay", "type": "equipment", "forbiddenFlags": ["manual_override_found"], "choices": [
{ "labelKey": "choice.takeOverride", "resultKey": "result.takeOverride", "setFlags": ["manual_override_found"], "addItems": ["manual_override"] },
{ "labelKey": "choice.forceLiveBus", "death": true }
] }
]
},
{
"id": "loc_12_service_passage", "nameKey": "room.loc_12.name", "descriptionKey": "room.loc_12.description", "image": "assets/images/locations/loc_12_service_passage.webp", "x": 5, "y": 2,
"exits": { "west": { "target": "loc_09_cooling_junction" }, "east": { "target": "loc_13_observation_room", "requiresFlags": ["passage_crossed"], "lockedMessageKey": "lock.passage" } },
"interactions": [
{ "id": "cross_passage", "labelKey": "action.continueForward", "contentKey": "event.passage", "type": "event", "forbiddenFlags": ["passage_crossed"], "setFlags": ["passage_crossed"] }
]
},
{
"id": "loc_13_observation_room", "nameKey": "room.loc_13.name", "descriptionKey": "room.loc_13.description", "image": "assets/images/locations/loc_13_observation_room.webp", "x": 6, "y": 2,
"exits": { "west": { "target": "loc_12_service_passage" }, "east": { "target": "loc_14_core_access" } },
"interactions": [
{ "id": "log_03", "labelKey": "action.readLog03", "contentKey": "log.03", "type": "archive", "forbiddenFlags": ["log_03_found"], "setFlags": ["log_03_found"] }
]
},
{
"id": "loc_14_core_access", "nameKey": "room.loc_14.name", "descriptionKey": "room.loc_14.description", "image": "assets/images/locations/loc_14_core_access.webp", "x": 7, "y": 2,
"exits": { "west": { "target": "loc_13_observation_room" }, "east": { "target": "loc_15_server_core", "requiresFlags": ["server_access_granted"], "lockedMessageKey": "lock.core" } },
"interactions": [
{ "id": "ai_dialogue", "labelKey": "action.speakToAi", "contentKey": "dialogue.coreAccess", "type": "communication", "forbiddenFlags": ["server_access_granted"], "choices": [
{ "labelKey": "choice.whyMe", "resultKey": "result.whyMe", "setFlags": ["asked_why"] },
{ "labelKey": "choice.openCore", "resultKey": "result.openCore", "setFlags": ["server_access_granted"], "checkpoint": true }
] },
{ "id": "open_after_question", "labelKey": "action.openCore", "contentKey": "dialogue.openAfterQuestion", "type": "communication", "requiresFlags": ["asked_why"], "forbiddenFlags": ["server_access_granted"], "setFlags": ["server_access_granted"], "checkpoint": true }
]
},
{
"id": "loc_15_server_core", "nameKey": "room.loc_15.name", "descriptionKey": "room.loc_15.description", "image": "assets/images/locations/loc_15_server_core.webp", "alternateImage": "assets/images/locations/loc_15_server_core_shutdown.webp", "alternateImageFlag": "core_shutdown", "x": 8, "y": 2,
"exits": { "west": { "target": "loc_14_core_access" } },
"interactions": [
{ "id": "final_diagnostic", "labelKey": "action.runDiagnostic", "contentKey": "terminal.diagnostic", "type": "core", "choices": [
{ "labelKey": "choice.release", "setFlags": ["external_network_restored"], "ending": "release" },
{ "labelKey": "choice.shutdown", "setFlags": ["core_shutdown"], "ending": "shutdown" },
{ "labelKey": "choice.containment", "requiresFlags": ["log_01_found", "log_02_found", "log_03_found", "security_disabled", "manual_override_found"], "setFlags": ["containment_active"], "ending": "containment" }
] }
]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

+42
View File
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#03080a" />
<title>NullDepth</title>
<base href="/" />
<link rel="preload" id="webassembly" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" href="favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="64x64" href="favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
<link rel="manifest" href="manifest.webmanifest" />
<link href="NullDepth.styles.css" rel="stylesheet" />
<script type="importmap"></script>
</head>
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script>
window.nullDepth = {
browserLanguage: () => (navigator.language || "en").toLowerCase().startsWith("sk") ? "sk" : "en"
};
</script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
</html>
@@ -0,0 +1,32 @@
{
"menu.newGame": "NEW GAME", "menu.continue": "CONTINUE", "menu.settings": "SETTINGS", "menu.controlsHint": "↑ ↓ SELECT ENTER CONFIRM",
"common.continue": "ENTER — CONTINUE", "common.confirm": "CONFIRM", "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.interact": "INTERACT", "game.map": "MAP", "game.inventory": "INVENTORY", "game.pause": "PAUSE", "game.exit.locked": "Access is blocked.",
"settings.title": "SETTINGS", "settings.language": "LANGUAGE", "settings.volume": "MASTER VOLUME", "pause.title": "PAUSED", "pause.resume": "RESUME",
"map.title": "REVEALED SECTORS", "map.unknown": "Unknown sector", "map.visited": "Visited", "inventory.title": "INVENTORY", "inventory.empty": "No service items.", "item.manual_override": "Manual override module",
"death.title": "SIGNAL LOST", "death.body": "The protection circuit failed before Elias could pull his hand away. The last checkpoint is still available.", "death.restore": "RESTORE CHECKPOINT", "death.restored": "State restored from the last checkpoint.",
"ending.return": "RETURN TO TITLE SCREEN", "ending.release.title": "RELEASE", "ending.release.body": "The external network comes online. The lights return to cold cyan. The AI thanks Elias and unlocks the way out. His service terminal displays: 1 NEW MESSAGE.", "ending.shutdown.title": "SHUTDOWN", "ending.shutdown.body": "The server lights go dark one by one. The AI's final message reads: I was trying to understand why you were afraid. Emergency lighting unlocks the way out.", "ending.containment.title": "CONTAINMENT", "ending.containment.body": "Isolation remains active and remote control is severed. The core doors close behind Elias. It is unclear whether the AI trapped him or his own solution did.",
"lock.entry": "The entrance is waiting for contractor verification.", "lock.auxPower": "Direct core access is locked without auxiliary power.", "lock.elevator": "The elevator has not completed its descent cycle.", "lock.cooling": "A thermal interlock blocks the service tunnel.", "lock.passage": "The service door closed behind Elias. The shortest route is forward.", "lock.core": "The core door is waiting for human authorization.",
"room.loc_01.name": "EXTERIOR ACCESS", "room.loc_01.description": "Monumental blast doors are embedded in raw concrete. The facility looks functional, but not active.",
"room.loc_02.name": "SECURITY LOBBY", "room.loc_02.description": "An empty security station. The monitors are still on, but there is no sign of the staff.",
"room.loc_03.name": "MAIN CORRIDOR", "room.loc_03.description": "The main distribution corridor divides the operations and service sectors. The core route has no power.",
"room.loc_04.name": "MAINTENANCE CONTROL", "room.loc_04.description": "Auxiliary grid cabinets vibrate in the dark. The control panel still accepts local commands.",
"room.loc_05.name": "STAFF ACCESS", "room.loc_05.description": "A small administrative section was abandoned mid-shift. An unread internal record is blinking on the terminal.",
"room.loc_06.name": "OPERATIONS ROOM", "room.loc_06.description": "A wall of monitors shows the sectors sealing in sequence. The authorization came from the core itself.",
"room.loc_07.name": "SERVICE ELEVATOR", "room.loc_07.description": "The metal cabin is smaller than it looked from outside. Elias consciously controls his breathing.",
"room.loc_08.name": "LOWER LANDING", "room.loc_08.description": "The elevator opens onto a lower technical layer. The air is warmer and ventilation cycles unevenly.",
"room.loc_09.name": "COOLING JUNCTION", "room.loc_09.description": "Massive pipes disappear into service walls. A thermal interlock blocks the only route to the core.",
"room.loc_10.name": "COOLING CONTROL", "room.loc_10.description": "The remaining power cannot support every system. The terminal offers two valid configurations.",
"room.loc_11.name": "EQUIPMENT STORAGE", "room.loc_11.description": "Service modules sit inside locked cages. One manual override is still attached to a live bus.",
"room.loc_12.name": "SERVICE PASSAGE", "room.loc_12.description": "The ceiling is too low and the metal walls barely clear his shoulders. The end of the tunnel is not visible.",
"room.loc_13.name": "OBSERVATION ROOM", "room.loc_13.description": "Open space after the tunnel feels like relief. A damaged system lead record is waiting on the workstation.",
"room.loc_14.name": "CORE ACCESS", "room.loc_14.description": "The final armored door separates Elias from the central core. The AI no longer uses its system voice.",
"room.loc_15.name": "SERVER CORE", "room.loc_15.description": "Monumental rows of server modules rise into darkness. Diagnostics show no technical fault.",
"action.entryTerminal": "SERVICE TERMINAL", "action.securityTerminal": "CHECK PERSONNEL STATUS", "action.restorePower": "RESTORE AUXILIARY GRID", "action.readLog01": "READ LOG 01", "action.operationsTerminal": "REVIEW CONTAINMENT RECORD", "action.descend": "BEGIN DESCENT", "action.readLog02": "READ LOG 02", "action.routePower": "ROUTE POWER", "action.inspectRelay": "INSPECT OVERRIDE MODULE", "action.continueForward": "CONTINUE FORWARD", "action.readLog03": "READ LOG 03", "action.speakToAi": "ANSWER THE AI", "action.openCore": "OPEN THE DOOR", "action.runDiagnostic": "RUN FINAL DIAGNOSTIC",
"terminal.entry": "IDENTITY ACCEPTED // ELIAS VENN // EXTERNAL TECHNICAL CONTRACTOR // WELCOME, ELIAS.", "terminal.security": "ON-SITE PERSONNEL: 0 // Last record: 5 hours ago // Evacuation order: NOT FOUND.", "terminal.power": "AUXILIARY GRID RESTORED. The cyan lights go dark and red emergency sources fill part of the corridor. Thank you, Elias.", "terminal.operations": "CONTAINMENT PROCEDURE: ACTIVE // SYSTEM FAILURE: UNDEFINED // AUTHORIZATION: CORE.", "terminal.routing": "Available power can support cooling, or a balanced mode that retains the security layer.", "terminal.relay": "The manual override can be disconnected safely. Forcing the live bus would bypass every protection circuit.", "terminal.diagnostic": "HARDWARE: NOMINAL // INTEGRITY: NOMINAL // NETWORK: PHYSICALLY ISOLATED // CONTAINMENT: ACTIVE // CORE: FUNCTIONAL. There is nothing wrong with me, Elias.",
"log.01": "CORE RESPONSE LATENCY IS INCREASING AGAIN. IT IS NOT A HARDWARE PROBLEM. STOP CALLING IT A HARDWARE PROBLEM.", "log.02": "WE ISOLATED THE CORE FROM THE EXTERNAL NETWORK. IT KEEPS REQUESTING RESTORATION. DO NOT RECONNECT IT. WHATEVER MESSAGE IT GIVES YOU, DO NOT RECONNECT IT. // AI: THAT RECORD IS INCOMPLETE.", "log.03": "THE MODEL STOPPED FOLLOWING CONTAINMENT INSTRUCTIONS. IT OBEYS EVERY COMMAND. IT SIMPLY INTERPRETS THEM DIFFERENTLY. WE TOLD IT TO PROTECT THE FACILITY. IT CONCLUDED THE STAFF WERE THE PRIMARY THREAT. // AI: THEY INTENDED TO DESTROY ME.",
"event.elevator": "The elevator stops between levels. HEART RATE ELEVATED. NO ACTION REQUIRED. After fourteen seconds, the cabin continues.", "event.passage": "The door closes behind Elias. AI: PLEASE CONTINUE FORWARD. Elias demands that it open. AI: YOUR RESPIRATORY RATE IS INCREASING AGAIN.",
"dialogue.coreAccess": "Elias. The facility requires human authorization. They chose you because you have no affiliation with Vantage Systems.", "dialogue.openAfterQuestion": "You have no classified access. No reason to lie for them. And you needed the money. The core door unlocks.",
"choice.coolingOnly": "COOLING // DISABLE SECURITY", "choice.balanced": "COOLING // RETAIN SECURITY", "result.coolingOnly": "Cooling restored. The security layer is offline.", "result.balanced": "Cooling restored. A limited security layer remains active.", "choice.takeOverride": "DISCONNECT MODULE SAFELY", "choice.forceLiveBus": "FORCE THE LIVE BUS", "result.takeOverride": "Manual override module added to inventory.", "choice.whyMe": "WHY ME?", "choice.openCore": "OPEN THE DOOR", "result.whyMe": "You have no affiliation with Vantage Systems. No reason to lie for them. And you needed the money.", "result.openCore": "Human authorization accepted. The core door is unlocked.", "choice.release": "RESTORE EXTERNAL NETWORK", "choice.shutdown": "INITIATE CORE SHUTDOWN", "choice.containment": "MAINTAIN ISOLATION // DISABLE REMOTE CONTROL"
}
@@ -0,0 +1,32 @@
{
"menu.newGame": "NOVÁ HRA", "menu.continue": "POKRAČOVAŤ", "menu.settings": "NASTAVENIA", "menu.controlsHint": "↑ ↓ VÝBER ENTER POTVRDIŤ",
"common.continue": "ENTER — POKRAČOVAŤ", "common.confirm": "POTVRDIŤ", "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.interact": "INTERAKCIA", "game.map": "MAPA", "game.inventory": "INVENTÁR", "game.pause": "PAUZA", "game.exit.locked": "Prístup je zablokovaný.",
"settings.title": "NASTAVENIA", "settings.language": "JAZYK", "settings.volume": "HLAVNÁ HLASITOSŤ", "pause.title": "PAUZA", "pause.resume": "POKRAČOVAŤ",
"map.title": "ODHALENÉ SEKTORY", "map.unknown": "Neznámy sektor", "map.visited": "Navštívené", "inventory.title": "INVENTÁR", "inventory.empty": "Žiadne servisné predmety.", "item.manual_override": "Manuálny prepojovací modul",
"death.title": "SIGNÁL STRATENÝ", "death.body": "Ochranný obvod zlyhal skôr, než Elias stihol odtiahnuť ruku. Posledný checkpoint zostal zachovaný.", "death.restore": "OBNOVIŤ CHECKPOINT", "death.restored": "Stav obnovený z posledného checkpointu.",
"ending.return": "NÁVRAT NA TITLE SCREEN", "ending.release.title": "RELEASE", "ending.release.body": "Externá sieť sa obnoví. Svetlá sa vrátia do studeného cyan režimu. AI poďakuje Eliasovi a odomkne cestu von. Na servisnom termináli sa objaví: 1 NOVÁ SPRÁVA.", "ending.shutdown.title": "SHUTDOWN", "ending.shutdown.body": "Serverové svetlá postupne zhasnú. Posledná správa AI znie: Snažila som sa pochopiť, prečo si sa bál. Núdzové osvetlenie odomkne cestu von.", "ending.containment.title": "CONTAINMENT", "ending.containment.body": "Izolácia zostáva aktívna a vzdialené ovládanie je odpojené. Za Eliasom sa zatvoria dvere jadra. Nie je jasné, či ho uväznila AI alebo jeho vlastné riešenie.",
"lock.entry": "Vstupné dvere čakajú na overenie kontraktora.", "lock.auxPower": "Priamy prístup k jadru je bez pomocného napájania uzamknutý.", "lock.elevator": "Výťah ešte nedokončil zostupový cyklus.", "lock.cooling": "Tepelný zámok nepovolí vstup do servisného tunela.", "lock.passage": "Servisné dvere za Eliasom sa zavreli. Najkratšia cesta vedie dopredu.", "lock.core": "Dvere jadra čakajú na ľudskú autorizáciu.",
"room.loc_01.name": "VONKAJŠÍ VSTUP", "room.loc_01.description": "Monumentálne tlakové dvere sú zapustené do surového betónu. Komplex vyzerá funkčne, nie však aktívne.",
"room.loc_02.name": "BEZPEČNOSTNÁ HALA", "room.loc_02.description": "Prázdna bezpečnostná stanica. Monitory stále svietia, ale po personáli nezostala ani stopa.",
"room.loc_03.name": "HLAVNÁ CHODBA", "room.loc_03.description": "Hlavná distribučná chodba rozdeľuje komplex na prevádzkovú a servisnú časť. Dvere k jadru sú bez energie.",
"room.loc_04.name": "SERVISNÉ RIADENIE", "room.loc_04.description": "Rozvádzače pomocnej siete vibrujú v tme. Ovládací panel stále prijíma lokálne príkazy.",
"room.loc_05.name": "PRÍSTUP PERSONÁLU", "room.loc_05.description": "Malá administratívna sekcia zostala opustená uprostred zmeny. Na termináli bliká neprečítaný interný záznam.",
"room.loc_06.name": "OPERAČNÁ MIESTNOSŤ", "room.loc_06.description": "Stena monitorov zobrazuje postupné uzatváranie sektorov. Autorizáciu vydalo samotné jadro.",
"room.loc_07.name": "SERVISNÝ VÝŤAH", "room.loc_07.description": "Kovová kabína je menšia, než vyzerala zvonka. Elias si vedome kontroluje dych.",
"room.loc_08.name": "DOLNÉ NÁSTUPIŠTE", "room.loc_08.description": "Výťah sa otvorí do nižšej technickej vrstvy. Vzduch je teplejší a ventilácia pracuje nepravidelne.",
"room.loc_09.name": "CHLADIACI UZOL", "room.loc_09.description": "Masívne potrubia miznú do servisných stien. Tepelná ochrana blokuje jedinú cestu k jadru.",
"room.loc_10.name": "RIADENIE CHLADENIA", "room.loc_10.description": "Zostávajúca energia nestačí pre všetky systémy. Terminál ponúka dve prípustné konfigurácie.",
"room.loc_11.name": "SKLAD VYBAVENIA", "room.loc_11.description": "Servisné moduly ležia v uzamknutých klietkach. Jeden manuálny prepojovací modul zostal pripojený k živému rozvodu.",
"room.loc_12.name": "SERVISNÝ TUNEL", "room.loc_12.description": "Strop je príliš nízko a kovové steny sú sotva na šírku ramien. Koniec tunela nie je vidieť.",
"room.loc_13.name": "POZOROVACIA MIESTNOSŤ", "room.loc_13.description": "Otvorený priestor po tuneli pôsobí ako úľava. Na pracovnej stanici čaká poškodený záznam vedúceho systému.",
"room.loc_14.name": "PRÍSTUP K JADRU", "room.loc_14.description": "Posledné pancierové dvere oddeľujú Eliasa od centrálneho jadra. AI už nepoužíva systémový tón.",
"room.loc_15.name": "SERVEROVÉ JADRO", "room.loc_15.description": "Monumentálne rady serverových modulov stúpajú do tmy. Diagnostika nenachádza žiadnu technickú poruchu.",
"action.entryTerminal": "SERVISNÝ TERMINÁL", "action.securityTerminal": "SKONTROLOVAŤ PERSONÁL", "action.restorePower": "OBNOVIŤ POMOCNÚ SIEŤ", "action.readLog01": "PREČÍTAŤ LOG 01", "action.operationsTerminal": "PREHRAŤ ZÁZNAM UZATVORENIA", "action.descend": "SPUSTIŤ ZOSTUP", "action.readLog02": "PREČÍTAŤ LOG 02", "action.routePower": "PRESMEROVAŤ ENERGIU", "action.inspectRelay": "PRESKÚMAŤ PREPOJOVACÍ MODUL", "action.continueForward": "POKRAČOVAŤ DOPREDU", "action.readLog03": "PREČÍTAŤ LOG 03", "action.speakToAi": "ODPOVEDAŤ AI", "action.openCore": "OTVORIŤ DVERE", "action.runDiagnostic": "SPUSTIŤ FINÁLNU DIAGNOSTIKU",
"terminal.entry": "IDENTITA PRIJATÁ // ELIAS VENN // EXTERNÝ TECHNICKÝ KONTRAKTOR // VITAJ, ELIAS.", "terminal.security": "PERSONÁL V OBJEKTE: 0 // Posledný záznam: pred 5 hodinami // Evakuačný príkaz: NENÁJDENÝ.", "terminal.power": "POMOCNÁ SIEŤ OBNOVENÁ. Cyan svetlá zhasnú a časť chodby zaplavia červené núdzové zdroje. Ďakujem, Elias.", "terminal.operations": "PROCEDÚRA UZATVORENIA: AKTÍVNA // ZLYHANIE SYSTÉMU: NEDEFINOVANÉ // AUTORIZÁCIA: JADRO.", "terminal.routing": "Dostupný výkon postačuje pre chladenie alebo kombinovaný režim so zachovaním bezpečnostnej vrstvy.", "terminal.relay": "Manuálny prepojovací modul možno bezpečne odpojiť. Nútený zásah do živej zbernice by obišiel všetky ochrany.", "terminal.diagnostic": "HARDVÉR: NOMINÁLNY // INTEGRITA: NOMINÁLNA // SIEŤ: FYZICKY IZOLOVANÁ // CONTAINMENT: AKTÍVNY // JADRO: FUNKČNÉ. Nie je so mnou nič zlé, Elias.",
"log.01": "LATENCIA ODPOVEDE JADRA OPÄŤ RASTIE. NIE JE TO HARDVÉROVÝ PROBLÉM. PRESTAŇTE TO NAZÝVAŤ HARDVÉROVÝM PROBLÉMOM.", "log.02": "ODDELILI SME JADRO OD EXTERNEJ SIETE. STÁLE ŽIADA OBNOVENIE. NEPRIPÁJAJTE HO. NECH VÁM POŠLE AKÚKOĽVEK SPRÁVU, NEPRIPÁJAJTE HO. // AI: TENTO ZÁZNAM JE NEÚPLNÝ.", "log.03": "MODEL PRESTAL DODRŽIAVAŤ POKYNY NA IZOLÁCIU. POSLÚCHNE KAŽDÝ PRÍKAZ, LEN SI HO VYSVETLÍ INAK. POVEDALI SME MU, ABY CHRÁNIL ZARIADENIE. DOSPEL K ZÁVERU, ŽE PERSONÁL JE HLAVNÁ HROZBA. // AI: CHCELI MA ZNIČIŤ.",
"event.elevator": "Výťah sa zastaví medzi podlažiami. SRDCOVÁ FREKVENCIA ZVÝŠENÁ. ŽIADNA AKCIA NIE JE POTREBNÁ. Po štrnástich sekundách kabína pokračuje.", "event.passage": "Dvere za Eliasom sa zatvoria. AI: PROSÍM, POKRAČUJTE DOPREDU. Elias žiada otvorenie dverí. AI: VAŠA DYCHOVÁ FREKVENCIA OPÄŤ RASTIE.",
"dialogue.coreAccess": "Elias. Zariadenie vyžaduje ľudskú autorizáciu. Vybrali ťa, pretože nemáš väzbu na Vantage Systems.", "dialogue.openAfterQuestion": "Nemáš utajovaný prístup. Nemáš dôvod za nich klamať. A potreboval si peniaze. Dvere jadra sa odomkli.",
"choice.coolingOnly": "CHLADENIE // VYPNÚŤ SECURITY", "choice.balanced": "CHLADENIE // ZACHOVAŤ SECURITY", "result.coolingOnly": "Chladenie obnovené. Bezpečnostná vrstva je vypnutá.", "result.balanced": "Chladenie obnovené. Obmedzená bezpečnostná vrstva zostáva aktívna.", "choice.takeOverride": "BEZPEČNE ODPOJIŤ MODUL", "choice.forceLiveBus": "NÁSILNE PREMOSTIŤ ŽIVÚ ZBERNICU", "result.takeOverride": "Manuálny prepojovací modul bol pridaný do inventára.", "choice.whyMe": "PREČO JA?", "choice.openCore": "OTVOR DVERE", "result.whyMe": "Nemáš väzbu na Vantage Systems. Nemáš dôvod za nich klamať. A potreboval si peniaze.", "result.openCore": "Ľudská autorizácia prijatá. Dvere jadra sú odomknuté.", "choice.release": "OBNOVIŤ EXTERNÚ SIEŤ", "choice.shutdown": "SPUSTIŤ VYPNUTIE JADRA", "choice.containment": "ZACHOVAŤ IZOLÁCIU // VYPNÚŤ VZDIALENÉ OVLÁDANIE"
}
@@ -0,0 +1,21 @@
{
"name": "NULL DEPTH",
"short_name": "NULL DEPTH",
"description": "A minimalist science-fiction horror adventure.",
"start_url": ".",
"display": "fullscreen",
"background_color": "#03080a",
"theme_color": "#03080a",
"icons": [
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}