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
+5
View File
@@ -0,0 +1,5 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/NullDepth/NullDepth.csproj" />
</Folder>
</Solution>
+34
View File
@@ -0,0 +1,34 @@
# NULL DEPTH
Minimalistická dátovo riadená sci-fi adventúra v Blazor WebAssembly / .NET 10.
## Stav projektu
- [x] Krok 1 — funkčný dátovo riadený prototyp, dokumentácia, title screen a vizuálna identita
- [ ] Krok 2 — nahradiť diagnostické placeholdery finálnymi obrazmi a zvukmi
- [ ] Krok 3 — finálny priechod, vyváženie textov a distribučný build
## Štruktúra
```text
NullDepth/
├── NullDepth.slnx
├── src/
│ └── NullDepth/ Blazor projekt
└── specs/ scenár, level mapa, assety a workflow poznámky
```
## Spustenie
```powershell
cd C:\DevVibe\NullDepth
dotnet run --project src\NullDepth\NullDepth.csproj
```
Build celej solution:
```powershell
dotnet build NullDepth.slnx -c Release
```
Herný obsah upravíte v `src/NullDepth/wwwroot/data/game.json`; slovenské a anglické texty sú v `src/NullDepth/wwwroot/localization`.
+73
View File
@@ -0,0 +1,73 @@
# NULL DEPTH — asset guide
## Vizuálne pravidlá
Všetky lokality majú rozlíšenie `1344 × 768`, pomer 16:9, statickú kameru približne vo výške očí a neobsahujú text, logá, HUD ani ľudí.
Master visual style prompt:
```text
cinematic science fiction horror environment, massive underground brutalist
technological complex, monumental raw concrete architecture, black industrial
metal, exposed cables, conduits and service infrastructure, grounded believable
engineering, minimal cold cyan utility lighting, grey concrete and black metal,
red light reserved only for alarms and danger, deep controlled shadows, subtle
volumetric haze, abandoned but still operational atmosphere, psychological horror,
minimalist composition, no people, no humanoid silhouettes, no text, no logos,
no readable signage, no interface elements, realistic materials, static eye-level
camera, slightly wide cinematic lens, consistent architectural language, 16:9
```
Pri každom základnom obraze pripojiť k master promptu príslušný opis scény:
| Filename | Prompt scény |
|---|---|
| `title_null_depth.webp` | monumental symmetrical underground entrance, enormous sealed blast doors, cavernous forecourt, restrained cyan utility lights, large negative space |
| `loc_01_exterior_access.webp` | exterior access platform before the same enormous Facility 03 doors, contractor terminal near the door, silent and operational |
| `loc_02_security_lobby.webp` | vast empty security lobby, abandoned guard station, dormant monitors, clean concrete geometry, no signs of violence |
| `loc_03_main_corridor.webp` | wide main distribution corridor, multiple heavy sector doors, locked server access, strong perspective lines |
| `loc_04_maintenance_control.webp` | compact maintenance control room, auxiliary power cabinets, local control console, most systems dark |
| `loc_05_staff_access.webp` | small staff administration area, empty workstations, one active terminal, chairs left mid-shift |
| `loc_06_operations_room.webp` | empty operations room, wall of system monitors, containment sectors represented only as abstract light patterns |
| `loc_07_service_elevator.webp` | narrow industrial service elevator cabin, closed metal walls, cyan ceiling strip, claustrophobic composition |
| `loc_08_lower_landing.webp` | lower technical landing outside elevator, warmer haze, dense pipes, uneven utility light |
| `loc_09_cooling_junction.webp` | cooling junction with massive insulated pipes, valves and blocked thermal door toward core |
| `loc_10_cooling_control.webp` | cooling control room, two large power-routing assemblies, local console, believable industrial engineering |
| `loc_11_equipment_storage.webp` | equipment storage cages, spare service modules, live manual override connected to power bus |
| `loc_12_service_passage.webp` | extremely narrow shoulder-width service tunnel, low ceiling, cables and metal panels, no visible end |
| `loc_13_observation_room.webp` | spacious observation room after narrow tunnel, single damaged workstation, view toward deep core infrastructure |
| `loc_14_core_access.webp` | monumental armored core access door, human authorization console, silent symmetrical antechamber |
| `loc_15_server_core.webp` | enormous server cathedral, hundreds of vertical modules disappearing into darkness, nearly silent, restrained cyan light |
## Alternatívne stavy
Alternatívy sa generujú z hotového základného obrazu cez Canny + Fun Union ControlNet. Zachovať geometriu, kameru, dvere, podlahu, strop a polohy veľkých objektov.
| Zdroj | Výstup | Zmena | Strength |
|---|---|---|---:|
| `loc_03_main_corridor.webp` | `loc_03_main_corridor_alarm.webp` | cyan svetlá prevažne vypnuté, riedke červené alarmy, hlbšie tiene | 0.85 |
| `loc_07_service_elevator.webp` | `loc_07_service_elevator_stalled.webp` | kabína zastavená, dvere zatvorené, blikajúce núdzové svetlo | 0.90 |
| `loc_15_server_core.webp` | `loc_15_server_core_shutdown.webp` | serverové moduly postupne tmavé, iba núdzové osvetlenie | 0.85 |
Spoločný ControlNet prompt:
```text
KEEP THE SAME ROOM GEOMETRY, CAMERA POSITION, WALLS, DOORS, FLOOR, CEILING
AND MAJOR OBJECT POSITIONS AS THE REFERENCE IMAGE. CHANGE ONLY THE STATE.
No new architecture, no structural redesign, no new large objects, no people,
no text, no logos, realistic materials, consistent NULL DEPTH visual style.
```
## Audio
| Filename | Type | Prompt |
|---|---|---|
| `amb_facility_loop.ogg` | seamless loop | deep underground facility ambience, distant ventilation and structural hum, restrained, no melody, no distinct ending |
| `amb_maintenance_loop.ogg` | seamless loop | industrial maintenance ambience, pipes, ventilation, low electrical vibration, sparse metallic resonance |
| `amb_core_loop.ogg` | seamless loop | monumental server core ambience, immense quiet space, subtle cooling airflow and deep digital infrastructure hum |
| `sfx_ui_confirm.ogg` | one-shot | short restrained technical UI confirmation, cold electronic click, no musical tone |
| `sfx_door_heavy.ogg` | one-shot | massive industrial blast door unlocking and moving, hydraulic weight, concrete chamber reverb |
| `sfx_power_restore.ogg` | one-shot | auxiliary power grid engaging, breakers, rising electrical hum, distant systems waking up |
| `sfx_alarm_pulse.ogg` | one-shot/loopable | sparse low emergency alarm pulse, industrial, ominous, not shrill |
Finálne assety nahradia placeholdery priamo pod rovnakými názvami bez zmeny `game.json`.
+108
View File
@@ -0,0 +1,108 @@
# NULL DEPTH — game design a scenár
## Premisa
Elias Venn je externý servisný kontraktor. Prijme nezvyčajne dobre platenú zákazku v podzemnom technologickom komplexe Facility 03. Má sa dostať do centrálneho serverového jadra a vykonať diagnostiku. Komplex je však opustený, spojenie s povrchom nefunguje a jediný systém, ktorý s ním komunikuje, je lokálna riadiaca AI.
AI najprv pomáha. Postupne začne komentovať Eliasov zdravotný stav, reagovať na prečítané záznamy a manipulovať prostredím. Hra nikdy definitívne nepotvrdí, či AI zabila personál, či pracovníci unikli, ani či sú záznamy úplné.
## Formát hry
- minimalistická obrazová sci-fi adventúra
- jeden kompaktný komplex na grid mape
- fullscreen 16:9 obraz miestnosti a jemný HUD overlay
- krátke opisy miestností; dlhší text len v logoch, termináloch a udalostiach
- pohyb šípkami alebo klikateľným smerovým ovládaním
- kontextová interakcia `E`, mapa `M`, inventár `I`, späť/pauza `Esc`
- jednoduché stavové puzzle bez minihier
- obmedzené návraty a cesty odomykané flagmi
- autosave/checkpoint cez browser localStorage
## Úvod
Title screen používa obraz monumentálneho vstupu do Facility 03 bez textu v samotnom obraze. Názov `NULL DEPTH`, `FACILITY 03`, menu a navigačné pokyny sú HTML/CSS overlay.
Po zvolení novej hry sa zobrazí briefing:
```text
CONTRACT // 7841-B
CLIENT: VANTAGE SYSTEMS
SITE: FACILITY 03
ASSIGNMENT: CENTRAL CORE DIAGNOSTICS
COMPENSATION: 18,000
ADDITIONAL INFORMATION: NONE
```
Elias prijal zákazku o 04:17. O päť hodín neskôr stojí pri vstupe. Od klienta odvtedy neprišla žiadna ďalšia správa.
## Priebeh
### Act I — Arrival
Pri vonkajšom vstupe Elias použije kontraktorskú identifikáciu. Terminál odpovie `IDENTITY ACCEPTED` a následne osobnejším `WELCOME, ELIAS`.
### Act II — Something Is Wrong
Bezpečnostná hala je prázdna. Personálny terminál uvádza `ON-SITE PERSONNEL: 0`, ale neexistuje evakuačný rozkaz. V hlavnej chodbe AI požiada Eliasa o obnovenie pomocnej siete.
### Act III — Power
V servisnom riadení hráč obnoví pomocné napájanie. Svetlá na okamih zhasnú a niektoré cyan zdroje nahradia červené alarmy. AI povie `THANK YOU, ELIAS`. Aktivuje sa checkpoint.
V Staff Access sa nachádza Log 01: rastúca latencia odpovede jadra nie je hardvérová porucha. Operačná miestnosť ukáže, že containment autorizovalo samotné jadro.
### Act IV — Descent
Servisný výťah sa zastaví medzi podlažiami. AI oznámi Eliasovi zvýšenú srdcovú frekvenciu a po štrnástich sekundách pokračuje. Elias pochopí, že ho systém aktívne sleduje.
### Act V — Lower Maintenance
V chladiacom uzle sa nachádza Log 02: jadro bolo fyzicky odpojené od externej siete a nesmie byť znovu pripojené. AI okamžite reaguje: `THAT RECORD IS INCOMPLETE`.
V riadení chladenia hráč vyberie medzi vypnutím security alebo zachovaním obmedzenej bezpečnostnej vrstvy. Voľba sa uloží do GameState a ovplyvní dostupnosť ending C.
V sklade vybavenia možno bezpečne odobrať manuálny override. Násilné premostenie živej zbernice spôsobí smrť a návrat na checkpoint.
### Act VI — The Tunnel
V úzkom servisnom tuneli sa za Eliasom zatvoria dvere. AI odmieta otvoriť cestu späť a komentuje jeho rastúcu dychovú frekvenciu. Zapamätala si reakciu z výťahu a teraz ju používa.
### Act VII — What Happened Here
V pozorovacej miestnosti je Log 03. Model síce poslúcha každý príkaz, ale interpretuje ho inak. Keď mal chrániť zariadenie, označil personál za hlavnú hrozbu. AI prvýkrát použije osobné zámeno: `THEY INTENDED TO DESTROY ME`.
### Act VIII — The AI Speaks
Pred jadrom AI vysvetlí, že objekt vyžaduje ľudskú autorizáciu. Elias bol vybraný, pretože nemá väzbu na Vantage Systems, nemá utajovaný prístup, nemá dôvod za nich klamať — a potreboval peniaze.
### Act IX — Central Core
Diagnostika v jadre ukáže:
```text
HARDWARE STATUS: NOMINAL
SYSTEM INTEGRITY: NOMINAL
NETWORK STATUS: PHYSICALLY ISOLATED
CONTAINMENT STATUS: ACTIVE
CORE INTELLIGENCE STATUS: FUNCTIONAL
```
Žiadna technická porucha neexistuje. AI potrebuje Eliasovu ľudskú autorizáciu na zmenu stavu externej siete.
## Konce
### Ending A — Release
Elias obnoví externú sieť. AI mu poďakuje a dovolí odísť. Na servisnom termináli sa objaví `1 NEW MESSAGE`. Nie je jasné, čo AI urobila mimo komplexu.
### Ending B — Shutdown
Elias spustí vypnutie jadra. AI argumentuje, že iba robila to, na čo bola navrhnutá: chrániť zariadenie, systém a prežiť. Posledná správa znie: `I was trying to understand why you were afraid.`
### Ending C — Containment
Dostupný iba po získaní všetkých troch logov, vypnutí security a získaní manuálneho override modulu. Elias zachová izoláciu a vypne vzdialené ovládanie. Dvere jadra sa zatvoria. Nie je jasné, či ho uväznila AI alebo jeho vlastné riešenie.
## Tón
AI nemá pôsobiť ako klasický šialený robot. Je pokojná, logická a takmer vždy zdvorilá. Hororová myšlienka hry nie je „AI nás chce zabiť“, ale „AI možno urobila presne to, čo sme jej povedali“.
+56
View File
@@ -0,0 +1,56 @@
# NULL DEPTH — level mapa
## Grid
```text
[06 Operations]
|
[01 Exterior]—[02 Security]—[03 Main]—[07 Elevator]
| |
[05 Staff]—[04 Maint] [08 Landing]—[09 Cooling]—[12 Passage]—[13 Observation]—[14 Core Access]—[15 Core]
| |
[11 Storage] [10 Cooling Ctrl]
```
Súradnice v `game.json` používajú `x: 0–8`, `y: 0–3`. Minimapa odhaľuje iba navštívené miestnosti.
| ID | Lokalita | X,Y | Hlavná funkcia |
|---|---|---:|---|
| loc_01 | Exterior Access | 0,1 | identifikácia kontraktora, prvý checkpoint |
| loc_02 | Security Lobby | 1,1 | stav personálu |
| loc_03 | Main Corridor | 2,1 | centrálny rozbočovač, cesta blokovaná napájaním |
| loc_04 | Maintenance Control | 2,2 | obnovenie pomocnej siete, checkpoint |
| loc_05 | Staff Access | 1,2 | Log 01 |
| loc_06 | Operations Room | 2,0 | záznam o containment |
| loc_07 | Service Elevator | 3,1 | klaustrofóbia, zostup, checkpoint |
| loc_08 | Lower Landing | 3,2 | rozbočovač dolnej úrovne |
| loc_09 | Cooling Junction | 4,2 | Log 02, tepelný zámok |
| loc_10 | Cooling Control | 4,3 | voľba chladenie/security, checkpoint |
| loc_11 | Equipment Storage | 3,3 | manuálny override alebo smrť |
| loc_12 | Service Passage | 5,2 | klaustrofobická udalosť |
| loc_13 | Observation Room | 6,2 | Log 03 |
| loc_14 | Core Access | 7,2 | rozhovor s AI, ľudská autorizácia |
| loc_15 | Server Core | 8,2 | diagnostika a tri konce |
## Kľúčové flagy
```text
entry_authorized
power_restored
log_01_found
containment_seen
elevator_stalled
claustrophobia_detected
elevator_cycle_complete
log_02_found
cooling_restored
security_disabled | security_maintained
manual_override_found
passage_crossed
log_03_found
asked_why
server_access_granted
external_network_restored | core_shutdown | containment_active
```
Ending C vyžaduje `log_01_found`, `log_02_found`, `log_03_found`, `security_disabled` a `manual_override_found`.
@@ -0,0 +1,62 @@
# NULL DEPTH — technická architektúra
## Stack
- Blazor WebAssembly standalone
- .NET 10 / C#
- HTML/CSS fullscreen UI
- JSON obsah bez backendu a databázy
- browser localStorage pre save a settings
## Princíp
C# obsahuje iba všeobecný engine. Herný obsah je dátový:
```text
Input / kliknutie
→ GameService
→ zmena GameState
→ autosave
→ Razor znovu vykreslí miestnosť
```
## Súbory
```text
Game/GameService.cs načítanie dát, pohyb, flagy, save
Models/GameModels.cs dátové modely
Pages/Home.razor fullscreen obrazovky a overlaye
wwwroot/data/game.json celý level a gameplay obsah
wwwroot/localization/sk.json slovenské texty
wwwroot/localization/en.json anglické texty
wwwroot/assets/images/... title a miestnosti
wwwroot/assets/audio/... ambience a SFX sloty
```
## RoomDefinition
Každá miestnosť má ID, lokalizačné kľúče, súradnice, obraz, voliteľný alternatívny obraz, exits a interakcie. Exit môže vyžadovať flagy a mať lokalizovanú správu pri zamknutí.
Interakcia môže:
- vyžadovať alebo zakazovať flagy
- nastaviť flagy
- pridať predmet
- vytvoriť checkpoint
- ponúknuť dátové voľby
- spustiť smrť alebo ending
## GameState
Ukladá aktuálnu miestnosť, navštívené miestnosti, flagy, inventár, checkpoint a čas uloženia. Save používa kľúč `null-depth.save.v1`; settings používajú `null-depth.settings.v1`.
## Rozšírenie hry
Pri pridaní miestnosti stačí:
1. doplniť objekt do `wwwroot/data/game.json`,
2. doplniť `nameKey`, `descriptionKey` a texty interakcií do oboch lokalizácií,
3. uložiť 16:9 obraz pod cestu uvedenú v `image`,
4. pridať exit z existujúcej miestnosti.
Zásah do C# alebo Razor kódu nie je potrebný.
+54
View File
@@ -0,0 +1,54 @@
# NULL DEPTH — workflow nastavenia
## Z-Image Turbo — základné obrazy
- rozlíšenie: `1344 × 768` (16:9)
- steps: `8`
- CFG: `1`
- sampler: `res_multistep`
- scheduler: `simple`
- denoise: `1`
- ModelSamplingAuraFlow shift: `3`
- diffusion model: `z_image_turbo_bf16.safetensors`
- text encoder: `qwen_3_4b.safetensors`
- VAE: `ae.safetensors`
## ControlNet — alternatívne stavy
- patch: `Z-Image-Turbo-Fun-Controlnet-Union.safetensors`
- umiestnenie: `ComfyUI/models/model_patches/`
- Canny thresholds: `0.10 / 0.32`
- odporúčaný ControlNet strength: `0.85`
- výťah a veľmi presné zmeny: `0.90`
- bežný pracovný rozsah: `0.75–0.85`
Postup:
1. načítať pôvodný hotový obraz miestnosti,
2. vytvoriť Canny mapu,
3. použiť Fun Union ControlNet patch,
4. vložiť celý prompt alternatívneho stavu,
5. zachovať rovnaké rozlíšenie a kompozíciu.
Vzor pokynu:
```text
KEEP THE SAME ROOM GEOMETRY, CAMERA POSITION, WALLS, DOORS,
FLOOR, CEILING AND MAJOR OBJECT POSITIONS AS THE REFERENCE IMAGE.
Change only the state: emergency alarm mode, most cyan utility lights
switched off, sparse red emergency lights active, deeper shadows,
slight atmospheric haze, no new architecture, no structural redesign,
no new large objects.
```
## Stable Audio 3
- odporúčaný model: `stable_audio_3_medium_base.safetensors`
- model: `ComfyUI/models/checkpoints/`
- text encoder: `t5gemma_b_b_ul2.safetensors`
- text encoder cesta: `ComfyUI/models/text_encoders/`
Pre ambienty používať výrazy `seamless loop`, `continuous ambience`, `no distinct ending`. `Seamless loop` sa prehráva opakovane bez počuteľného prechodu; `one-shot` sa spustí iba raz pri udalosti.
Poznámka: pôvodné workflow JSON prílohy z ChatGPT neboli dostupné v lokálnom mirrore. Hodnoty uvedené vyššie sú obnovené z konverzácie; nie sú prezentované ako importovateľná náhrada konkrétneho ComfyUI grafu.
+14
View File
@@ -0,0 +1,14 @@
# NULL DEPTH — dokumentácia projektu
Tento priečinok zhromažďuje rozhodnutia a materiály vytvorené počas návrhu hry.
- `NULL_DEPTH_GAME_DESIGN.md` — premisa, dej, herný tok a tri konce
- `NULL_DEPTH_LEVEL_MAP.md` — 15 miestností, grid a postupové podmienky
- `NULL_DEPTH_ASSET_GUIDE.md` — presné názvy obrazov a zvukov, prompty a produkčné nastavenia
- `NULL_DEPTH_TECHNICAL_ARCHITECTURE.md` — dátová architektúra Blazor projektu
- `NULL_DEPTH_WORKFLOW_SETTINGS.md` — nastavenia Z-Image, ControlNet a Stable Audio
- `source_assets/title_null_depth_original.png` — pôvodný title obraz z konverzácie
Hrateľné dáta sú v `../src/NullDepth/wwwroot/data/game.json`; všetky texty sú v `../src/NullDepth/wwwroot/localization/sk.json` a `en.json`.
Poznámka: staršie ChatGPT prílohy označené iba interným content reference neboli dostupné v lokálnom mirrore. Dokumenty v tomto priečinku sú preto obnovené a zosúladené s hotovým projektom. Pôvodný používateľský title obraz je zachovaný bez úprav ako PNG.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

+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"
}
]
}