From 6a02b0e9aedad4bd5f0b64e1f694332325c4f84a Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 21 Sep 2026 02:09:09 +0000 Subject: [PATCH] Sync nautical startup loading screen --- index.html | 239 +++++++++++++++++++++++++++++++++++++++++- src/game-v4.js | 28 +++++ src/multiplayer.js | 12 +++ tests/wiring.test.mjs | 19 ++++ 4 files changed, 297 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index a0fc39d..5c995be 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,178 @@ overscroll-behavior: none; } + body.airship-loading #mobile-controls { + visibility: hidden; + } + + #airship-loading-screen { + position: fixed; + inset: 0; + z-index: 2000; + display: grid; + place-items: center; + box-sizing: border-box; + padding: + max(24px, env(safe-area-inset-top)) + max(22px, env(safe-area-inset-right)) + max(28px, env(safe-area-inset-bottom)) + max(22px, env(safe-area-inset-left)); + background: + radial-gradient(circle at 50% 38%, rgba(15, 70, 77, .38), transparent 33%), + linear-gradient(180deg, #132837 0%, #0a1a25 52%, #07151e 100%); + color: #f7e3ad; + opacity: 1; + visibility: visible; + transition: opacity 360ms ease, visibility 360ms ease; + pointer-events: auto; + } + + #airship-loading-screen::before { + content: ''; + position: absolute; + inset: 0; + opacity: .22; + background-image: + linear-gradient(rgba(224, 177, 87, .08) 1px, transparent 1px), + linear-gradient(90deg, rgba(224, 177, 87, .06) 1px, transparent 1px); + background-size: 42px 42px; + mask-image: linear-gradient(to bottom, transparent, black 22%, black 78%, transparent); + pointer-events: none; + } + + #airship-loading-screen.is-ready { + opacity: 0; + visibility: hidden; + pointer-events: none; + } + + .airship-loader { + position: relative; + width: min(420px, 88vw); + display: grid; + justify-items: center; + gap: 18px; + text-align: center; + } + + .airship-loader-mark { + position: relative; + width: clamp(108px, 27vw, 150px); + aspect-ratio: 1; + border: 4px solid #8e5c22; + border-radius: 50%; + background: + radial-gradient(circle, #0c4b50 0 42%, #082f35 43% 57%, #071c25 58%); + box-shadow: + 0 0 0 3px #d1a24e, + 0 0 0 7px #4e3217, + inset 0 0 28px rgba(0,0,0,.55), + 0 14px 38px rgba(0,0,0,.42); + } + + .airship-loader-mark::before, + .airship-loader-mark::after { + content: ''; + position: absolute; + left: 50%; + top: 50%; + width: 72%; + height: 8px; + margin: -4px 0 0 -36%; + border-radius: 999px; + background: linear-gradient(90deg, #8d5d26, #f0cf77 46%, #8d5d26); + transform-origin: center; + animation: airship-loader-spin 2.8s linear infinite; + box-shadow: 0 1px 2px rgba(0,0,0,.5); + } + + .airship-loader-mark::after { + animation-direction: reverse; + animation-duration: 4.2s; + transform: rotate(90deg); + } + + .airship-loader-hub { + position: absolute; + inset: 36%; + z-index: 2; + border: 3px solid #61401d; + border-radius: 50%; + background: radial-gradient(circle at 38% 35%, #f0d179, #a66c29 64%, #573516 100%); + box-shadow: 0 0 0 2px #e0b65d, 0 3px 8px rgba(0,0,0,.45); + } + + @keyframes airship-loader-spin { + to { transform: rotate(360deg); } + } + + .airship-loader-kicker { + margin: 4px 0 -8px; + color: #a8c6c9; + font: 700 11px/1.2 ui-sans-serif, system-ui, sans-serif; + letter-spacing: .34em; + } + + .airship-loader h1 { + margin: 0; + font: 700 clamp(28px, 8vw, 48px)/.95 Georgia, 'Times New Roman', serif; + letter-spacing: .02em; + text-shadow: 0 3px 0 rgba(74, 44, 15, .75); + } + + .airship-loader-phase { + min-height: 20px; + margin: 0; + color: #f5dda0; + font: 700 13px/1.3 ui-sans-serif, system-ui, sans-serif; + letter-spacing: .04em; + } + + .airship-loader-track { + position: relative; + width: 100%; + height: 12px; + padding: 2px; + box-sizing: border-box; + border: 1px solid #8a5b27; + border-radius: 999px; + background: #06161d; + box-shadow: inset 0 2px 5px rgba(0,0,0,.65), 0 0 0 1px rgba(230,183,91,.16); + overflow: hidden; + } + + .airship-loader-fill { + width: 6%; + height: 100%; + border-radius: inherit; + background: + linear-gradient(90deg, #18666b, #3f9a8d 62%, #e3bd62 100%); + box-shadow: 0 0 12px rgba(86, 191, 167, .5); + transition: width 180ms ease-out; + } + + .airship-loader-meta { + width: 100%; + display: flex; + justify-content: space-between; + gap: 12px; + color: #85a8ad; + font: 600 10px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; + text-transform: uppercase; + letter-spacing: .08em; + } + + .airship-loader-meta span:last-child { + text-align: right; + } + + @media (prefers-reduced-motion: reduce) { + .airship-loader-mark::before, + .airship-loader-mark::after { animation: none; } + #airship-loading-screen, + .airship-loader-fill { transition-duration: 1ms; } + } + #mobile-controls { position: fixed; inset: 0; @@ -121,7 +293,72 @@ } - + +
+
+ +

ARROWSTACK AIRSHIP

+

Preparing the Sparrow

+

Warming the engines…

+ +
+ 6% + Checking captain credentials… +
+
+
+ + +
diff --git a/src/game-v4.js b/src/game-v4.js index 979317e..b067c6d 100644 --- a/src/game-v4.js +++ b/src/game-v4.js @@ -160,6 +160,23 @@ class GameScene extends Phaser.Scene { } preload() { + window.dispatchEvent(new CustomEvent('airship:loading-progress', { + detail: { progress: 0.16, phase: 'Loading ship systems…' }, + })); + this.load.on('progress', (value) => { + window.dispatchEvent(new CustomEvent('airship:loading-progress', { + detail: { + progress: 0.16 + value * 0.54, + phase: 'Loading airship assets…', + }, + })); + }); + this.load.once('complete', () => { + window.dispatchEvent(new CustomEvent('airship:loading-progress', { + detail: { progress: 0.72, phase: 'Charting the skies…' }, + })); + }); + this.load.spritesheet('airships', airshipSheetUrl, { frameWidth: 64, frameHeight: 64, @@ -176,6 +193,9 @@ class GameScene extends Phaser.Scene { } create() { + window.dispatchEvent(new CustomEvent('airship:loading-progress', { + detail: { progress: 0.78, phase: 'Generating trade routes and skyports…' }, + })); this.physics.world.setBounds(0, 0, this.world.width, this.world.height); this.drawWorld(); this.createRollingClouds(); @@ -295,6 +315,10 @@ class GameScene extends Phaser.Scene { this.createHud(); + window.dispatchEvent(new CustomEvent('airship:loading-progress', { + detail: { progress: 0.94, phase: 'Bringing the Sparrow online…' }, + })); + this.scale.on('resize', this.layoutHud, this); this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { this.scale.off('resize', this.layoutHud, this); @@ -313,6 +337,10 @@ class GameScene extends Phaser.Scene { }); this.layoutHud({ width: this.scale.width, height: this.scale.height }); this.spawnWave(); + window.dispatchEvent(new CustomEvent('airship:loading-progress', { + detail: { progress: 1, phase: 'Ready to sail' }, + })); + window.dispatchEvent(new CustomEvent('airship:game-ready')); } diff --git a/src/multiplayer.js b/src/multiplayer.js index 06ae6af..14dd2a6 100644 --- a/src/multiplayer.js +++ b/src/multiplayer.js @@ -11,6 +11,7 @@ const KEYCLOAK_CONFIG = { const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); const keycloak = new Keycloak(KEYCLOAK_CONFIG); +window.airshipLoading?.setNetwork?.('Checking captain credentials…'); let socket = null; let selfId = null; let reconnectTimer = null; @@ -54,6 +55,17 @@ document.head.appendChild(style); const setStatus = (state, label) => { statusButton.dataset.state = state; statusButton.textContent = label; + + const loadingLabel = state === 'online' + ? `Shared skies online${playerCount > 1 ? ` • ${playerCount} captains` : ''}` + : state === 'connecting' + ? 'Joining shared skies…' + : state === 'error' + ? 'Multiplayer unavailable • solo ready' + : authenticated + ? 'Multiplayer ready' + : 'Solo flight ready'; + window.airshipLoading?.setNetwork?.(loadingLabel); }; const emitConnection = (state, extra = {}) => { diff --git a/tests/wiring.test.mjs b/tests/wiring.test.mjs index 1a7a2fa..b08aa76 100644 --- a/tests/wiring.test.mjs +++ b/tests/wiring.test.mjs @@ -4,6 +4,25 @@ import { readFile } from 'node:fs/promises'; const read = (path) => readFile(new URL(`../${path}`, import.meta.url), 'utf8'); +test('startup shows a real progress loading screen until the Phaser scene is ready', async () => { + const [index, game, multiplayer] = await Promise.all([ + read('index.html'), + read('src/game-v4.js'), + read('src/multiplayer.js'), + ]); + + assert.match(index, /id="airship-loading-screen"/); + assert.match(index, /Preparing the Sparrow/); + assert.match(index, /body class="airship-loading"/); + assert.match(index, /window\.airshipLoading = \{ setProgress, setNetwork, finish \}/); + assert.match(index, /airship:game-ready/); + assert.match(game, /this\.load\.on\('progress'/); + assert.match(game, /airship:loading-progress/); + assert.match(game, /airship:game-ready/); + assert.match(multiplayer, /window\.airshipLoading\?\.setNetwork/); + assert.match(index, /body\.airship-loading #mobile-controls/); +}); + test('mobile HUD uses split helm frame and knob assets', async () => { const source = await read('src/mobile-hud.js');