From a39ed2489fccc3298792550d2c2ec1fad145160a Mon Sep 17 00:00:00 2001 From: Michal Vrba Date: Sat, 29 Aug 2026 09:24:22 +0200 Subject: [PATCH] prechody --- src/NullDepth/Pages/Home.razor | 43 +++++++++++++++++++++++++++++-- src/NullDepth/wwwroot/css/app.css | 26 ++++++++++++++++--- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/NullDepth/Pages/Home.razor b/src/NullDepth/Pages/Home.razor index 51d7cf2..cef0cc3 100644 --- a/src/NullDepth/Pages/Home.razor +++ b/src/NullDepth/Pages/Home.razor @@ -6,7 +6,7 @@ NULL DEPTH -
+
@if (!_ready) {
@@ -438,6 +438,9 @@ } + + +
@code { @@ -457,6 +460,10 @@ private bool _scanMode; private bool _isFullscreen; private bool _isTransitioning; + private string _moveDirection = "north"; + private bool _crtSync; + private bool _rebootFlash; + private bool _deathFlash; private bool _systemFlash; private bool _savePulse; private int _savePulseVersion; @@ -529,6 +536,7 @@ await Game.NewGameAsync(); _screen = Screen.Briefing; _notice = null; + _ = TriggerCrtSyncAsync(); await Audio.PlayAmbientAsync("assets/audio/amb_facility.mp3"); } @@ -540,6 +548,7 @@ _scanMode = false; _ending = Game.State.CompletedEnding; _screen = _ending is null ? Screen.Game : Screen.Ending; + _ = TriggerCrtSyncAsync(); await Audio.PlayAmbientAsync(Game.CurrentRoom.Ambient); await Audio.SetHeartbeatBpmAsync(Game.CurrentBpm); } @@ -547,6 +556,7 @@ private async Task BeginPlay() { _screen = Screen.Game; + _ = TriggerCrtSyncAsync(); await Audio.PlayAmbientAsync(Game.CurrentRoom.Ambient); await Audio.SetHeartbeatBpmAsync(Game.CurrentBpm); } @@ -556,6 +566,7 @@ if (_isTransitioning) 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; } + _moveDirection = direction; _isTransitioning = true; await Audio.PlaySfxAsync("assets/audio/sfx_door_heavy.mp3"); await Task.Delay(150); @@ -654,7 +665,7 @@ _interaction = null; if (choice.Sound?.Contains("alarm") == true || choice.Sound?.Contains("disturbance") == true) _ = 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; } await Audio.SetHeartbeatBpmAsync(Game.CurrentBpm); await Task.Delay(180); @@ -686,6 +697,7 @@ await Game.RestoreCheckpointAsync(); _screen = Screen.Game; _notice = Game.T("death.restored"); + _ = TriggerRebootAsync(); await Audio.PlayAmbientAsync(Game.CurrentRoom.Ambient); } @@ -786,6 +798,33 @@ 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 async Task HandleKey(KeyboardEventArgs args) diff --git a/src/NullDepth/wwwroot/css/app.css b/src/NullDepth/wwwroot/css/app.css index f4e7aed..b4119d0 100644 --- a/src/NullDepth/wwwroot/css/app.css +++ b/src/NullDepth/wwwroot/css/app.css @@ -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 { color:var(--cyan); background:none; padding:0; } .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; } -.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%); } .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; } } @@ -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 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; } -.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); } -.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; } +.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; } +@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); } .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; }