This commit is contained in:
Michal Vrba
2026-08-29 09:24:22 +02:00
parent 8fcca27cf8
commit a39ed2489f
2 changed files with 63 additions and 6 deletions
+41 -2
View File
@@ -6,7 +6,7 @@
<PageTitle>NULL DEPTH</PageTitle> <PageTitle>NULL DEPTH</PageTitle>
<main class="game-shell @(_isTransitioning ? "transitioning" : "") @(_systemFlash ? "system-alert" : "")" tabindex="0" @ref="_shell" @onkeydown="HandleKey"> <main class="game-shell @(_isTransitioning ? $"transitioning dir-{_moveDirection}" : "") @(_systemFlash ? "system-alert" : "")" tabindex="0" @ref="_shell" @onkeydown="HandleKey">
@if (!_ready) @if (!_ready)
{ {
<section class="loading-screen"> <section class="loading-screen">
@@ -438,6 +438,9 @@
} }
<div class="system-flash" aria-hidden="true"></div> <div class="system-flash" aria-hidden="true"></div>
<div class="screen-fade" aria-hidden="true"></div> <div class="screen-fade" aria-hidden="true"></div>
<div class="screen-glitch @(_crtSync ? "active" : "")" aria-hidden="true"></div>
<div class="screen-reboot @(_rebootFlash ? "active" : "")" aria-hidden="true"></div>
<div class="screen-death-flash @(_deathFlash ? "active" : "")" aria-hidden="true"></div>
</main> </main>
@code { @code {
@@ -457,6 +460,10 @@
private bool _scanMode; private bool _scanMode;
private bool _isFullscreen; private bool _isFullscreen;
private bool _isTransitioning; private bool _isTransitioning;
private string _moveDirection = "north";
private bool _crtSync;
private bool _rebootFlash;
private bool _deathFlash;
private bool _systemFlash; private bool _systemFlash;
private bool _savePulse; private bool _savePulse;
private int _savePulseVersion; private int _savePulseVersion;
@@ -529,6 +536,7 @@
await Game.NewGameAsync(); await Game.NewGameAsync();
_screen = Screen.Briefing; _screen = Screen.Briefing;
_notice = null; _notice = null;
_ = TriggerCrtSyncAsync();
await Audio.PlayAmbientAsync("assets/audio/amb_facility.mp3"); await Audio.PlayAmbientAsync("assets/audio/amb_facility.mp3");
} }
@@ -540,6 +548,7 @@
_scanMode = false; _scanMode = false;
_ending = Game.State.CompletedEnding; _ending = Game.State.CompletedEnding;
_screen = _ending is null ? Screen.Game : Screen.Ending; _screen = _ending is null ? Screen.Game : Screen.Ending;
_ = TriggerCrtSyncAsync();
await Audio.PlayAmbientAsync(Game.CurrentRoom.Ambient); await Audio.PlayAmbientAsync(Game.CurrentRoom.Ambient);
await Audio.SetHeartbeatBpmAsync(Game.CurrentBpm); await Audio.SetHeartbeatBpmAsync(Game.CurrentBpm);
} }
@@ -547,6 +556,7 @@
private async Task BeginPlay() private async Task BeginPlay()
{ {
_screen = Screen.Game; _screen = Screen.Game;
_ = TriggerCrtSyncAsync();
await Audio.PlayAmbientAsync(Game.CurrentRoom.Ambient); await Audio.PlayAmbientAsync(Game.CurrentRoom.Ambient);
await Audio.SetHeartbeatBpmAsync(Game.CurrentBpm); await Audio.SetHeartbeatBpmAsync(Game.CurrentBpm);
} }
@@ -556,6 +566,7 @@
if (_isTransitioning) return; if (_isTransitioning) return;
if (!Game.CurrentRoom.Exits.TryGetValue(direction, out var exit)) return; if (!Game.CurrentRoom.Exits.TryGetValue(direction, out var exit)) return;
if (!Game.HasAll(exit.RequiresFlags)) { _notice = Game.T(exit.LockedMessageKey ?? "game.exit.locked"); return; } if (!Game.HasAll(exit.RequiresFlags)) { _notice = Game.T(exit.LockedMessageKey ?? "game.exit.locked"); return; }
_moveDirection = direction;
_isTransitioning = true; _isTransitioning = true;
await Audio.PlaySfxAsync("assets/audio/sfx_door_heavy.mp3"); await Audio.PlaySfxAsync("assets/audio/sfx_door_heavy.mp3");
await Task.Delay(150); await Task.Delay(150);
@@ -654,7 +665,7 @@
_interaction = null; _interaction = null;
if (choice.Sound?.Contains("alarm") == true || choice.Sound?.Contains("disturbance") == true) if (choice.Sound?.Contains("alarm") == true || choice.Sound?.Contains("disturbance") == true)
_ = TriggerSystemFlashAsync(); _ = TriggerSystemFlashAsync();
if (choice.Death) { await Audio.SetHeartbeatBpmAsync(0); _screen = Screen.Death; return; } if (choice.Death) { await Audio.SetHeartbeatBpmAsync(0); _ = TriggerDeathFlashAsync(); _screen = Screen.Death; return; }
if (!string.IsNullOrWhiteSpace(choice.Ending)) { await Audio.SetHeartbeatBpmAsync(0); _ending = choice.Ending; _screen = Screen.Ending; return; } if (!string.IsNullOrWhiteSpace(choice.Ending)) { await Audio.SetHeartbeatBpmAsync(0); _ending = choice.Ending; _screen = Screen.Ending; return; }
await Audio.SetHeartbeatBpmAsync(Game.CurrentBpm); await Audio.SetHeartbeatBpmAsync(Game.CurrentBpm);
await Task.Delay(180); await Task.Delay(180);
@@ -686,6 +697,7 @@
await Game.RestoreCheckpointAsync(); await Game.RestoreCheckpointAsync();
_screen = Screen.Game; _screen = Screen.Game;
_notice = Game.T("death.restored"); _notice = Game.T("death.restored");
_ = TriggerRebootAsync();
await Audio.PlayAmbientAsync(Game.CurrentRoom.Ambient); await Audio.PlayAmbientAsync(Game.CurrentRoom.Ambient);
} }
@@ -786,6 +798,33 @@
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private async Task TriggerCrtSyncAsync()
{
_crtSync = true;
await InvokeAsync(StateHasChanged);
await Task.Delay(220);
_crtSync = false;
await InvokeAsync(StateHasChanged);
}
private async Task TriggerRebootAsync()
{
_rebootFlash = true;
await InvokeAsync(StateHasChanged);
await Task.Delay(480);
_rebootFlash = false;
await InvokeAsync(StateHasChanged);
}
private async Task TriggerDeathFlashAsync()
{
_deathFlash = true;
await InvokeAsync(StateHasChanged);
await Task.Delay(750);
_deathFlash = false;
await InvokeAsync(StateHasChanged);
}
private string MenuClass(int index) => _menuIndex == index ? "selected" : ""; private string MenuClass(int index) => _menuIndex == index ? "selected" : "";
private async Task HandleKey(KeyboardEventArgs args) private async Task HandleKey(KeyboardEventArgs args)
+22 -4
View File
@@ -11,9 +11,23 @@ button:disabled { opacity:.28; cursor:not-allowed; background:transparent; color
kbd, .eyebrow, .subtitle, .top-hud, .command-bar, .terminal-copy, small { font-family:'IBM Plex Mono',Consolas,monospace; } 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; } kbd { color:var(--cyan); background:none; padding:0; }
.game-shell { position:relative; width:100%; height:100%; outline:none; background:#020507; } .game-shell { position:relative; width:100%; height:100%; outline:none; background:#020507; }
.screen-fade, .system-flash { position:absolute; inset:0; z-index:50; pointer-events:none; opacity:0; } .screen-fade, .system-flash, .screen-glitch, .screen-reboot, .screen-death-flash { position:absolute; inset:0; z-index:50; pointer-events:none; opacity:0; }
.screen-fade { background:#010304; transition:opacity .15s ease; } .screen-fade { background:#010304; transition:opacity .15s ease; }
.game-shell.transitioning .screen-fade { opacity:1; transition-duration:.22s; } .game-shell.transitioning .screen-fade { opacity:1; transition-duration:.18s; }
.game-shell.transitioning.dir-north .parallax-bg { transform:translate3d(calc(var(--mouse-px, 0) * -14px), calc(var(--mouse-py, 0) * -14px - 18px), 0) scale(1.08); }
.game-shell.transitioning.dir-south .parallax-bg { transform:translate3d(calc(var(--mouse-px, 0) * -14px), calc(var(--mouse-py, 0) * -14px + 18px), 0) scale(0.98); }
.game-shell.transitioning.dir-west .parallax-bg { transform:translate3d(calc(var(--mouse-px, 0) * -14px - 22px), calc(var(--mouse-py, 0) * -14px), 0) scale(1.04); }
.game-shell.transitioning.dir-east .parallax-bg { transform:translate3d(calc(var(--mouse-px, 0) * -14px + 22px), calc(var(--mouse-py, 0) * -14px), 0) scale(1.04); }
.screen-glitch.active { opacity:1; animation:crt-glitch .22s ease-out both; }
@keyframes crt-glitch { 0% { opacity:0; transform:scale(1.02); filter:brightness(2.2) contrast(1.6); } 25% { opacity:1; transform:scale(0.99) skewX(1deg); } 65% { opacity:0.7; transform:scale(1.01) skewX(-0.5deg); } 100% { opacity:0; transform:scale(1); } }
.screen-reboot.active { opacity:1; animation:reboot-flash .5s ease-out both; }
@keyframes reboot-flash { 0% { opacity:1; } 100% { opacity:0; } }
.screen-death-flash.active { opacity:1; animation:death-flash .75s ease-out both; }
@keyframes death-flash { 0% { opacity:1; } 40% { opacity:0.9; } 100% { opacity:0; } }
.system-flash { z-index:40; background:radial-gradient(circle at 50% 45%,rgba(255,74,80,.35),rgba(125,0,8,.12) 42%,transparent 72%); } .system-flash { z-index:40; background:radial-gradient(circle at 50% 45%,rgba(255,74,80,.35),rgba(125,0,8,.12) 42%,transparent 72%); }
.game-shell.system-alert .system-flash { animation:system-flash .65s steps(2,end); } .game-shell.system-alert .system-flash { animation:system-flash .65s steps(2,end); }
@keyframes system-flash { 0%,100% { opacity:0; } 15%,45% { opacity:1; } 30%,65% { opacity:.18; } } @keyframes system-flash { 0%,100% { opacity:0; } 15%,45% { opacity:1; } 30%,65% { opacity:.18; } }
@@ -67,8 +81,12 @@ kbd { color:var(--cyan); background:none; padding:0; }
.movement-pad { position:absolute; right:2rem; bottom:6.2rem; display:grid; grid-template:repeat(3,2.7rem)/repeat(3,2.7rem); gap:.25rem; z-index:10; } .movement-pad { position:absolute; right:2rem; bottom:6.2rem; display:grid; grid-template:repeat(3,2.7rem)/repeat(3,2.7rem); gap:.25rem; z-index:10; }
.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; } .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); z-index:15; }.command-bar button { border:0; font-size:.66rem; } .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); z-index:15; }.command-bar button { border:0; font-size:.66rem; }
.modal-layer { position:absolute; inset:0; z-index:30; display:grid; place-items:center; padding:2rem; background:rgba(0,3,5,.75); backdrop-filter:blur(8px); } .modal-layer { position:absolute; inset:0; z-index:30; display:grid; place-items:center; padding:2rem; background:rgba(0,3,5,.8); backdrop-filter:blur(12px); animation:modal-backdrop-in .18s ease-out both; }
.interaction-modal, .overlay-card { width:min(44rem,92vw); max-height:86vh; overflow:auto; padding:2rem; }.terminal-copy { white-space:pre-line; color:#bfe6ea; line-height:1.75; } @keyframes modal-backdrop-in { from { opacity:0; } to { opacity:1; } }
.interaction-modal, .overlay-card, .breaker-switchboard { width:min(44rem,92vw); max-height:86vh; overflow:auto; padding:2rem; animation:modal-card-in .2s cubic-bezier(0.16, 1, 0.3, 1) both; }
@keyframes modal-card-in { from { opacity:0; transform:scale(0.96) translateY(8px); } to { opacity:1; transform:scale(1) translateY(0); } }
.terminal-copy { white-space:pre-line; color:#bfe6ea; line-height:1.75; }
.terminal-copy strong { color:var(--cyan); } .terminal-copy strong { color:var(--cyan); }
.close-button { position:absolute; top:.65rem; right:.65rem; border:0; font-size:1.3rem; padding:.35rem .6rem; } .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; line-height:1.4; padding:.8rem 1rem; } .choice-list { display:flex; flex-direction:column; gap:.6rem; margin-top:1.5rem; }.choice-list button { text-align:left; line-height:1.4; padding:.8rem 1rem; }