diff --git a/specs/NULL_DEPTH_LEVEL_MAP.md b/specs/NULL_DEPTH_LEVEL_MAP.md index 7f41713..77779a5 100644 --- a/specs/NULL_DEPTH_LEVEL_MAP.md +++ b/specs/NULL_DEPTH_LEVEL_MAP.md @@ -1,36 +1,48 @@ # NULL DEPTH — level mapa -## Grid +## Grid (Orientácia: Zospodu nahor / Bottom-to-Top) ```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] + [15 Server Core (1,0)] + | + [14 Core Access (1,1)] + | + [13 Observation Room (1,2)] + | + [12 Service Passage (1,3)] + | + [09 Cooling Junction (1,4)]—[10 Cooling Control (2,4)] + | +[11 Storage (0,5)]—[08 Lower Landing (1,5)] + | + [07 Service Elevator (1,6)] + | +[06 Operations (0,7)]—[03 Main Corridor (1,7)]—[04 Maint (2,7)]—[05 Staff (3,7)] + | + [02 Security Lobby (1,8)] + | + [01 Exterior Access (1,9)] ``` -Súradnice v `game.json` používajú `x: 0–8`, `y: 0–3`. Minimapa odhaľuje iba navštívené miestnosti. +Súradnice v `game.json` používajú `x: 0–3`, `y: 0–9`. Postup v komplexe začína na dolnom vstupe (y=9) a smeruje na sever do hĺbky jadra (y=0). | 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 | +| loc_01 | Exterior Access | 1,9 | Identifikácia kontraktora, prvý checkpoint | +| loc_02 | Security Lobby | 1,8 | Stav personálu, bezpečnostné kamery | +| loc_03 | Main Corridor | 1,7 | Centrálny rozbočovač horného podlažia | +| loc_04 | Maintenance Control | 2,7 | Obnovenie pomocnej siete, checkpoint | +| loc_05 | Staff Access | 3,7 | Log 01, korešpondencia personálu | +| loc_06 | Operations Room | 0,7 | Záznam o containment | +| loc_07 | Service Elevator | 1,6 | Klaustrofóbia, zostup výťahom, checkpoint | +| loc_08 | Lower Landing | 1,5 | Rozbočovač dolnej úrovne | +| loc_09 | Cooling Junction | 1,4 | Log 02, tepelný zámok | +| loc_10 | Cooling Control | 2,4 | Voľba chladenie/security, checkpoint | +| loc_11 | Equipment Storage | 0,5 | Manuálny override (alebo live bus smrť) | +| loc_12 | Service Passage | 1,3 | Klaustrofobický servisný prechod | +| loc_13 | Observation Room | 1,2 | Log 03, špecifikácia architektúry jadra | +| loc_14 | Core Access | 1,1 | Rozhovor s AI, ľudská autorizácia | +| loc_15 | Server Core | 1,0 | Diagnostika a tri finálne konce | ## Kľúčové flagy diff --git a/src/NullDepth/Game/GameService.cs b/src/NullDepth/Game/GameService.cs index f11da73..bff10dc 100644 --- a/src/NullDepth/Game/GameService.cs +++ b/src/NullDepth/Game/GameService.cs @@ -10,16 +10,21 @@ public sealed class GameService(HttpClient http, IJSRuntime js) private const int CurrentSaveVersion = 3; private const string SaveKey = "null-depth.save.v1"; private const string SettingsKey = "null-depth.settings.v1"; + private const string ProfileKey = "null-depth.profile.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 PlayerProfile Profile { get; private set; } = new(); public bool HasSave { get; private set; } public SaveLoadResult LastLoadResult { get; private set; } = SaveLoadResult.Missing; public event Action? Saved; + public int CurrentBpm => CalculateBpm(); + public string StressStatusKey => CalculateStressStatusKey(); + public async Task InitializeAsync() { Data = await http.GetFromJsonAsync("data/game.json", _json) ?? new GameData(); @@ -27,6 +32,7 @@ public sealed class GameService(HttpClient http, IJSRuntime js) { Language = await js.InvokeAsync("nullDepth.browserLanguage") }; + Profile = await LoadValueAsync(ProfileKey) ?? new PlayerProfile(); await SetLanguageAsync(Settings.Language, persist: false); var rawSave = await js.InvokeAsync("localStorage.getItem", SaveKey); HasSave = !string.IsNullOrWhiteSpace(rawSave); @@ -40,6 +46,36 @@ public sealed class GameService(HttpClient http, IJSRuntime js) public ArchiveDefinition? FindArchive(string id) => Data.Archive.FirstOrDefault(a => a.Id == id); public bool HasAll(IEnumerable flags) => flags.All(State.Flags.Contains); + public int CalculateBpm() + { + if (State.Flags.Contains("elevator_stalled")) return 128; + if (State.CurrentRoomId == "loc_12_service_passage" && !State.Flags.Contains("passage_crossed")) return 118; + if (State.CurrentRoomId == "loc_11_equipment_storage" && !State.Flags.Contains("manual_override_found")) return 106; + if (State.CurrentRoomId == "loc_14_core_access") return 96; + if (State.CurrentRoomId == "loc_15_server_core") return 104; + if (State.Flags.Contains("power_restored") && (State.CurrentRoomId == "loc_03_main_corridor" || State.CurrentRoomId == "loc_04_maintenance_control")) return 88; + if (State.VisitedRooms.Count > 10) return 82; + return 74; + } + + public string CalculateStressStatusKey() + { + var bpm = CurrentBpm; + if (bpm >= 120) return "hud.stress.critical"; + if (bpm >= 95) return "hud.stress.elevated"; + return "hud.stress.nominal"; + } + + public string ScanCurrentRoom() + { + var room = CurrentRoom; + if (!string.IsNullOrWhiteSpace(room.ScanKey)) + { + return T(room.ScanKey); + } + return T("scan.default"); + } + public async Task SetLanguageAsync(string language, bool persist = true) { Settings.Language = language == "sk" ? "sk" : "en"; @@ -150,11 +186,23 @@ public sealed class GameService(HttpClient http, IJSRuntime js) public IEnumerable AvailableInteractions() => CurrentRoom.Interactions.Where(i => HasAll(i.RequiresFlags) && !i.ForbiddenFlags.Any(State.Flags.Contains)); + public IEnumerable AvailableHotspots() => CurrentRoom.Hotspots.Where(h => + HasAll(h.RequiresFlags) && !h.ForbiddenFlags.Any(State.Flags.Contains)); + + public InteractionDefinition? FindInteraction(string id) => CurrentRoom.Interactions.FirstOrDefault(i => i.Id == id); + public async Task ApplyInteractionAsync(InteractionDefinition interaction) { Apply(interaction.SetFlags, interaction.ClearFlags, interaction.AddItems); - if (!string.IsNullOrWhiteSpace(interaction.ArchiveId) && !State.Archive.Contains(interaction.ArchiveId)) - State.Archive.Add(interaction.ArchiveId); + if (!string.IsNullOrWhiteSpace(interaction.ArchiveId)) + { + if (!State.Archive.Contains(interaction.ArchiveId)) State.Archive.Add(interaction.ArchiveId); + if (!Profile.DiscoveredArchives.Contains(interaction.ArchiveId)) + { + Profile.DiscoveredArchives.Add(interaction.ArchiveId); + await SaveProfileAsync(); + } + } if (interaction.Checkpoint) SetCheckpoint(); await SaveAsync(); } @@ -162,11 +210,21 @@ public sealed class GameService(HttpClient http, IJSRuntime js) public async Task ApplyChoiceAsync(ChoiceDefinition choice) { Apply(choice.SetFlags, choice.ClearFlags, choice.AddItems); - if (!string.IsNullOrWhiteSpace(choice.Ending)) State.CompletedEnding = choice.Ending; + if (!string.IsNullOrWhiteSpace(choice.Ending)) + { + State.CompletedEnding = choice.Ending; + if (!Profile.UnlockedEndings.Contains(choice.Ending)) + { + Profile.UnlockedEndings.Add(choice.Ending); + await SaveProfileAsync(); + } + } if (choice.Checkpoint) SetCheckpoint(); await SaveAsync(); } + public async Task SaveProfileAsync() => await StoreValueAsync(ProfileKey, Profile); + public async Task RestoreCheckpointAsync() { var checkpoint = State.Checkpoint; diff --git a/src/NullDepth/Models/GameModels.cs b/src/NullDepth/Models/GameModels.cs index 76cd5e9..2af737c 100644 --- a/src/NullDepth/Models/GameModels.cs +++ b/src/NullDepth/Models/GameModels.cs @@ -2,7 +2,7 @@ namespace NullDepth.Models; public sealed class GameData { - public int Version { get; set; } = 1; + public int Version { get; set; } = 2; public string StartRoomId { get; set; } = "loc_01_exterior_access"; public List InitialItems { get; set; } = []; public List Items { get; set; } = []; @@ -15,6 +15,7 @@ public sealed class ItemDefinition public string Id { get; set; } = ""; public string NameKey { get; set; } = ""; public string DescriptionKey { get; set; } = ""; + public string? DetailedKey { get; set; } public string Category { get; set; } = "equipment"; } @@ -30,6 +31,7 @@ public sealed class RoomDefinition public string Id { get; set; } = ""; public string NameKey { get; set; } = ""; public string DescriptionKey { get; set; } = ""; + public string? ScanKey { get; set; } public string Image { get; set; } = ""; public string? Ambient { get; set; } public string? AlternateImage { get; set; } @@ -38,6 +40,20 @@ public sealed class RoomDefinition public int Y { get; set; } public Dictionary Exits { get; set; } = new(StringComparer.OrdinalIgnoreCase); public List Interactions { get; set; } = []; + public List Hotspots { get; set; } = []; +} + +public sealed class HotspotDefinition +{ + public string Id { get; set; } = ""; + public string LabelKey { get; set; } = ""; + public string? InteractionId { get; set; } + public double X { get; set; } = 50; + public double Y { get; set; } = 50; + public string Icon { get; set; } = "terminal"; + public string? TelemetryKey { get; set; } + public List RequiresFlags { get; set; } = []; + public List ForbiddenFlags { get; set; } = []; } public sealed class ExitDefinition @@ -81,7 +97,7 @@ public sealed class ChoiceDefinition public sealed class GameState { public int Version { get; set; } = 3; - public int ContentVersion { get; set; } = 1; + public int ContentVersion { get; set; } = 2; public string CurrentRoomId { get; set; } = "loc_01_exterior_access"; public HashSet VisitedRooms { get; set; } = []; public HashSet Flags { get; set; } = []; @@ -107,6 +123,12 @@ public sealed class PlayerSettings public int MasterVolume { get; set; } = 70; } +public sealed class PlayerProfile +{ + public HashSet UnlockedEndings { get; set; } = []; + public HashSet DiscoveredArchives { get; set; } = []; +} + public enum SaveLoadResult { Success, diff --git a/src/NullDepth/Pages/Home.razor b/src/NullDepth/Pages/Home.razor index cec9eae..37c39a4 100644 --- a/src/NullDepth/Pages/Home.razor +++ b/src/NullDepth/Pages/Home.razor @@ -22,7 +22,23 @@ {
-

FACILITY 03

NULL DEPTH

SUBLEVEL // 03

+
+

FACILITY 03

+

NULL DEPTH

+

SUBLEVEL // 03

+ @if (Game.Profile.UnlockedEndings.Count > 0 || Game.Profile.DiscoveredArchives.Count > 0) + { +
+ @Game.T("menu.endingsCount"): +
+ END A: RECON + END B: PURGE + END C: ISOLATE +
+ @Game.T("menu.archivesCount"): @Game.Profile.DiscoveredArchives.Count / 7 +
+ } +