diff --git a/NullDepth.slnx b/NullDepth.slnx new file mode 100644 index 0000000..dab69e9 --- /dev/null +++ b/NullDepth.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d0a542 --- /dev/null +++ b/README.md @@ -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`. diff --git a/specs/NULL_DEPTH_ASSET_GUIDE.md b/specs/NULL_DEPTH_ASSET_GUIDE.md new file mode 100644 index 0000000..09e0efd --- /dev/null +++ b/specs/NULL_DEPTH_ASSET_GUIDE.md @@ -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`. diff --git a/specs/NULL_DEPTH_GAME_DESIGN.md b/specs/NULL_DEPTH_GAME_DESIGN.md new file mode 100644 index 0000000..2ed49c6 --- /dev/null +++ b/specs/NULL_DEPTH_GAME_DESIGN.md @@ -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“. diff --git a/specs/NULL_DEPTH_LEVEL_MAP.md b/specs/NULL_DEPTH_LEVEL_MAP.md new file mode 100644 index 0000000..7f41713 --- /dev/null +++ b/specs/NULL_DEPTH_LEVEL_MAP.md @@ -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`. diff --git a/specs/NULL_DEPTH_TECHNICAL_ARCHITECTURE.md b/specs/NULL_DEPTH_TECHNICAL_ARCHITECTURE.md new file mode 100644 index 0000000..8f07dde --- /dev/null +++ b/specs/NULL_DEPTH_TECHNICAL_ARCHITECTURE.md @@ -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ý. diff --git a/specs/NULL_DEPTH_WORKFLOW_SETTINGS.md b/specs/NULL_DEPTH_WORKFLOW_SETTINGS.md new file mode 100644 index 0000000..0e3caea --- /dev/null +++ b/specs/NULL_DEPTH_WORKFLOW_SETTINGS.md @@ -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. diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 0000000..dfbda00 --- /dev/null +++ b/specs/README.md @@ -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. diff --git a/specs/source_assets/favicon_null_depth_source.png b/specs/source_assets/favicon_null_depth_source.png new file mode 100644 index 0000000..79f1403 Binary files /dev/null and b/specs/source_assets/favicon_null_depth_source.png differ diff --git a/specs/source_assets/title_null_depth_original.png b/specs/source_assets/title_null_depth_original.png new file mode 100644 index 0000000..6d270bb Binary files /dev/null and b/specs/source_assets/title_null_depth_original.png differ diff --git a/src/NullDepth/App.razor b/src/NullDepth/App.razor new file mode 100644 index 0000000..a8a79e5 --- /dev/null +++ b/src/NullDepth/App.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/NullDepth/Game/GameService.cs b/src/NullDepth/Game/GameService.cs new file mode 100644 index 0000000..0d7ad58 --- /dev/null +++ b/src/NullDepth/Game/GameService.cs @@ -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 _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("data/game.json", _json) ?? new GameData(); + Settings = await LoadValueAsync(SettingsKey) ?? new PlayerSettings + { + Language = await js.InvokeAsync("nullDepth.browserLanguage") + }; + await SetLanguageAsync(Settings.Language, persist: false); + HasSave = !string.IsNullOrWhiteSpace(await js.InvokeAsync("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 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>($"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 ContinueAsync() + { + var loaded = await LoadValueAsync(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 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 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 flags, IEnumerable 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 LoadValueAsync(string key) + { + var raw = await js.InvokeAsync("localStorage.getItem", key); + if (string.IsNullOrWhiteSpace(raw)) return default; + try { return JsonSerializer.Deserialize(raw, _json); } + catch { return default; } + } + + private async Task StoreValueAsync(string key, T value) => + await js.InvokeVoidAsync("localStorage.setItem", key, JsonSerializer.Serialize(value, _json)); +} diff --git a/src/NullDepth/Layout/MainLayout.razor b/src/NullDepth/Layout/MainLayout.razor new file mode 100644 index 0000000..4f3f76d --- /dev/null +++ b/src/NullDepth/Layout/MainLayout.razor @@ -0,0 +1,3 @@ +@inherits LayoutComponentBase + +@Body diff --git a/src/NullDepth/Models/GameModels.cs b/src/NullDepth/Models/GameModels.cs new file mode 100644 index 0000000..ae268d9 --- /dev/null +++ b/src/NullDepth/Models/GameModels.cs @@ -0,0 +1,79 @@ +namespace NullDepth.Models; + +public sealed class GameData +{ + public string StartRoomId { get; set; } = "loc_01_exterior_access"; + public List 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 Exits { get; set; } = new(StringComparer.OrdinalIgnoreCase); + public List Interactions { get; set; } = []; +} + +public sealed class ExitDefinition +{ + public string Target { get; set; } = ""; + public List 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 RequiresFlags { get; set; } = []; + public List ForbiddenFlags { get; set; } = []; + public List SetFlags { get; set; } = []; + public List AddItems { get; set; } = []; + public bool Checkpoint { get; set; } + public List Choices { get; set; } = []; +} + +public sealed class ChoiceDefinition +{ + public string LabelKey { get; set; } = ""; + public string? ResultKey { get; set; } + public List RequiresFlags { get; set; } = []; + public List SetFlags { get; set; } = []; + public List 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 VisitedRooms { get; set; } = []; + public HashSet Flags { get; set; } = []; + public List 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 VisitedRooms { get; set; } = []; + public HashSet Flags { get; set; } = []; + public List Inventory { get; set; } = []; +} + +public sealed class PlayerSettings +{ + public string Language { get; set; } = "en"; + public int MasterVolume { get; set; } = 70; +} diff --git a/src/NullDepth/NullDepth.csproj b/src/NullDepth/NullDepth.csproj new file mode 100644 index 0000000..e1c3f50 --- /dev/null +++ b/src/NullDepth/NullDepth.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + enable + enable + true + + + + + + + + diff --git a/src/NullDepth/Pages/Home.razor b/src/NullDepth/Pages/Home.razor new file mode 100644 index 0000000..de3d6d7 --- /dev/null +++ b/src/NullDepth/Pages/Home.razor @@ -0,0 +1,167 @@ +@page "/" +@inject GameService Game + +NULL DEPTH + +
+ @if (!_ready) + { +
NULL DEPTH // BOOTING
+ } + else if (_screen == Screen.Title) + { +
+
+

FACILITY 03

NULL DEPTH

SUBLEVEL // 03

+ +
+ } + else if (_screen == Screen.Briefing) + { +
+
+
+

@Game.T("briefing.header")

+
+
@Game.T("briefing.clientLabel")
VANTAGE SYSTEMS
+
@Game.T("briefing.siteLabel")
FACILITY 03
+
@Game.T("briefing.assignmentLabel")
@Game.T("briefing.assignment")
+
@Game.T("briefing.compensationLabel")
18,000
+
@Game.T("briefing.infoLabel")
@Game.T("briefing.none")
+
+

@Game.T("briefing.story")

+ +
+
+ } + else if (_screen == Screen.Game) + { + var room = Game.CurrentRoom; +
+
+
NULL DEPTH@Game.T("game.autosave")
+
+

@room.Id.Replace("_", " // ").ToUpperInvariant()

@Game.T(room.NameKey)

@Game.T(room.DescriptionKey)

+ @if (!string.IsNullOrWhiteSpace(_notice)) {

@_notice

} +
@foreach (var action in Game.AvailableInteractions()) { }
+
+ +
+ + +
+
+ } + else if (_screen == Screen.Death) + { +

FACILITY EVENT // FATAL

@Game.T("death.title")

@Game.T("death.body")

+ } + else if (_screen == Screen.Ending) + { +

ENDING // @_ending?.ToUpperInvariant()

@Game.T($"ending.{_ending}.title")

@Game.T($"ending.{_ending}.body")

+ } + + @if (_interaction is not null) + { + + } + @if (_showMap) + { + + } + @if (_showInventory) + { + + } + @if (_showPause) + { + + } + @if (_showSettings) + { + + } +
+ +@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 } +} diff --git a/src/NullDepth/Pages/NotFound.razor b/src/NullDepth/Pages/NotFound.razor new file mode 100644 index 0000000..917ada1 --- /dev/null +++ b/src/NullDepth/Pages/NotFound.razor @@ -0,0 +1,5 @@ +@page "/not-found" +@layout MainLayout + +

Not Found

+

Sorry, the content you are looking for does not exist.

\ No newline at end of file diff --git a/src/NullDepth/Program.cs b/src/NullDepth/Program.cs new file mode 100644 index 0000000..1aa9536 --- /dev/null +++ b/src/NullDepth/Program.cs @@ -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"); +builder.RootComponents.Add("head::after"); + +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +builder.Services.AddScoped(); + +await builder.Build().RunAsync(); diff --git a/src/NullDepth/Properties/launchSettings.json b/src/NullDepth/Properties/launchSettings.json new file mode 100644 index 0000000..09e9cda --- /dev/null +++ b/src/NullDepth/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/src/NullDepth/README.md b/src/NullDepth/README.md new file mode 100644 index 0000000..f4b2ab9 --- /dev/null +++ b/src/NullDepth/README.md @@ -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é diff --git a/src/NullDepth/_Imports.razor b/src/NullDepth/_Imports.razor new file mode 100644 index 0000000..25e4a16 --- /dev/null +++ b/src/NullDepth/_Imports.razor @@ -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 diff --git a/src/NullDepth/tools/generate_placeholders.py b/src/NullDepth/tools/generate_placeholders.py new file mode 100644 index 0000000..f0e8b72 --- /dev/null +++ b/src/NullDepth/tools/generate_placeholders.py @@ -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() diff --git a/src/NullDepth/tools/process_favicon.py b/src/NullDepth/tools/process_favicon.py new file mode 100644 index 0000000..48168c8 --- /dev/null +++ b/src/NullDepth/tools/process_favicon.py @@ -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() diff --git a/src/NullDepth/wwwroot/assets/audio/README.md b/src/NullDepth/wwwroot/assets/audio/README.md new file mode 100644 index 0000000..058376c --- /dev/null +++ b/src/NullDepth/wwwroot/assets/audio/README.md @@ -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. diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_01_exterior_access.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_01_exterior_access.webp new file mode 100644 index 0000000..2916a91 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_01_exterior_access.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_02_security_lobby.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_02_security_lobby.webp new file mode 100644 index 0000000..2eac049 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_02_security_lobby.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_03_main_corridor.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_03_main_corridor.webp new file mode 100644 index 0000000..e63ea6d Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_03_main_corridor.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_03_main_corridor_alarm.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_03_main_corridor_alarm.webp new file mode 100644 index 0000000..715dd84 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_03_main_corridor_alarm.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_04_maintenance_control.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_04_maintenance_control.webp new file mode 100644 index 0000000..177ffb7 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_04_maintenance_control.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_05_staff_access.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_05_staff_access.webp new file mode 100644 index 0000000..d3cc039 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_05_staff_access.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_06_operations_room.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_06_operations_room.webp new file mode 100644 index 0000000..79b498e Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_06_operations_room.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_07_service_elevator.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_07_service_elevator.webp new file mode 100644 index 0000000..229b722 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_07_service_elevator.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_07_service_elevator_stalled.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_07_service_elevator_stalled.webp new file mode 100644 index 0000000..7528687 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_07_service_elevator_stalled.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_08_lower_landing.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_08_lower_landing.webp new file mode 100644 index 0000000..c864f5e Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_08_lower_landing.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_09_cooling_junction.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_09_cooling_junction.webp new file mode 100644 index 0000000..7991b2b Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_09_cooling_junction.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_10_cooling_control.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_10_cooling_control.webp new file mode 100644 index 0000000..2b3306e Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_10_cooling_control.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_11_equipment_storage.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_11_equipment_storage.webp new file mode 100644 index 0000000..84907a1 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_11_equipment_storage.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_12_service_passage.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_12_service_passage.webp new file mode 100644 index 0000000..ec2fa3c Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_12_service_passage.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_13_observation_room.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_13_observation_room.webp new file mode 100644 index 0000000..4aa7007 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_13_observation_room.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_14_core_access.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_14_core_access.webp new file mode 100644 index 0000000..d590983 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_14_core_access.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_15_server_core.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_15_server_core.webp new file mode 100644 index 0000000..b2307dc Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_15_server_core.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/locations/loc_15_server_core_shutdown.webp b/src/NullDepth/wwwroot/assets/images/locations/loc_15_server_core_shutdown.webp new file mode 100644 index 0000000..0951ebd Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/locations/loc_15_server_core_shutdown.webp differ diff --git a/src/NullDepth/wwwroot/assets/images/title/title_null_depth.webp b/src/NullDepth/wwwroot/assets/images/title/title_null_depth.webp new file mode 100644 index 0000000..0291618 Binary files /dev/null and b/src/NullDepth/wwwroot/assets/images/title/title_null_depth.webp differ diff --git a/src/NullDepth/wwwroot/css/app.css b/src/NullDepth/wwwroot/css/app.css new file mode 100644 index 0000000..754cfc6 --- /dev/null +++ b/src/NullDepth/wwwroot/css/app.css @@ -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; } } diff --git a/src/NullDepth/wwwroot/data/game.json b/src/NullDepth/wwwroot/data/game.json new file mode 100644 index 0000000..7210c27 --- /dev/null +++ b/src/NullDepth/wwwroot/data/game.json @@ -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" } + ] } + ] + } + ] +} diff --git a/src/NullDepth/wwwroot/favicon.ico b/src/NullDepth/wwwroot/favicon.ico new file mode 100644 index 0000000..536a3c9 Binary files /dev/null and b/src/NullDepth/wwwroot/favicon.ico differ diff --git a/src/NullDepth/wwwroot/favicon.png b/src/NullDepth/wwwroot/favicon.png new file mode 100644 index 0000000..4017a9f Binary files /dev/null and b/src/NullDepth/wwwroot/favicon.png differ diff --git a/src/NullDepth/wwwroot/icon-192.png b/src/NullDepth/wwwroot/icon-192.png new file mode 100644 index 0000000..8c70cfe Binary files /dev/null and b/src/NullDepth/wwwroot/icon-192.png differ diff --git a/src/NullDepth/wwwroot/icon-512.png b/src/NullDepth/wwwroot/icon-512.png new file mode 100644 index 0000000..06e5daa Binary files /dev/null and b/src/NullDepth/wwwroot/icon-512.png differ diff --git a/src/NullDepth/wwwroot/index.html b/src/NullDepth/wwwroot/index.html new file mode 100644 index 0000000..336a5e4 --- /dev/null +++ b/src/NullDepth/wwwroot/index.html @@ -0,0 +1,42 @@ + + + + + + + + NullDepth + + + + + + + + + + + + +
+ + + + +
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + diff --git a/src/NullDepth/wwwroot/localization/en.json b/src/NullDepth/wwwroot/localization/en.json new file mode 100644 index 0000000..e4a2191 --- /dev/null +++ b/src/NullDepth/wwwroot/localization/en.json @@ -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" +} diff --git a/src/NullDepth/wwwroot/localization/sk.json b/src/NullDepth/wwwroot/localization/sk.json new file mode 100644 index 0000000..cd13c21 --- /dev/null +++ b/src/NullDepth/wwwroot/localization/sk.json @@ -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" +} diff --git a/src/NullDepth/wwwroot/manifest.webmanifest b/src/NullDepth/wwwroot/manifest.webmanifest new file mode 100644 index 0000000..048a3ea --- /dev/null +++ b/src/NullDepth/wwwroot/manifest.webmanifest @@ -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" + } + ] +}