diff --git a/src/game-entry.js b/src/game-entry.js
index 2b1ea33..922ec69 100644
--- a/src/game-entry.js
+++ b/src/game-entry.js
@@ -1,4 +1,5 @@
import './multiplayer.js';
+import './player-settings.js';
import './game-v4.js';
import './mobile-hud.js';
import './dock-ui.js';
diff --git a/src/game-v4.js b/src/game-v4.js
index b067c6d..81f3816 100644
--- a/src/game-v4.js
+++ b/src/game-v4.js
@@ -1614,6 +1614,7 @@ class GameScene extends Phaser.Scene {
remote.label.setText(remote.alive
? `${state.name || 'CAPTAIN'} ${Math.round(remote.hull)}HP K${state.kills || 0}/D${state.deaths || 0}`
: `${state.name || 'CAPTAIN'} • DOWN`);
+ remote.label.setVisible(window.airshipSettings?.get?.().showPlayerLabels !== false);
}
for (const [id, remote] of this.remotePlayers) {
@@ -1716,6 +1717,7 @@ class GameScene extends Phaser.Scene {
sprite.y = Phaser.Math.Linear(sprite.y, remote.targetY, 0.32);
sprite.angle += signedAngleDelta(remote.targetAngle, sprite.angle) * 0.32;
remote.label.setPosition(sprite.x, sprite.y - 70);
+ remote.label.setVisible(window.airshipSettings?.get?.().showPlayerLabels !== false);
}
}
@@ -2160,7 +2162,9 @@ class GameScene extends Phaser.Scene {
if (this.player?.active && this.gameState === 'playing') this.player.clearTint();
});
- this.cameras.main.shake(120, clamp(0.0035 + damage * 0.00055, 0.004, 0.012));
+ if (window.airshipSettings?.get?.().screenShake !== false) {
+ this.cameras.main.shake(120, clamp(0.0035 + damage * 0.00055, 0.004, 0.012));
+ }
}
validateLivePlayerState() {
diff --git a/src/multiplayer.js b/src/multiplayer.js
index 14dd2a6..e885bd6 100644
--- a/src/multiplayer.js
+++ b/src/multiplayer.js
@@ -9,6 +9,7 @@ const KEYCLOAK_CONFIG = {
};
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
+const MULTIPLAYER_ENABLED_KEY = 'airship.multiplayer.enabled';
const keycloak = new Keycloak(KEYCLOAK_CONFIG);
window.airshipLoading?.setNetwork?.('Checking captain credentials…');
@@ -18,12 +19,16 @@ let reconnectTimer = null;
let authenticated = false;
let playerCount = 0;
let inputSequence = 0;
+let profile = null;
+let selfSnapshot = null;
+let worldSeed = 'SKYWARD-001';
+let multiplayerEnabled = localStorage.getItem(MULTIPLAYER_ENABLED_KEY) !== 'false';
const statusButton = document.createElement('button');
statusButton.id = 'multiplayer-status';
statusButton.type = 'button';
-statusButton.textContent = 'MULTIPLAYER';
-statusButton.setAttribute('aria-label', 'Multiplayer login');
+statusButton.textContent = '⚙ SOLO';
+statusButton.setAttribute('aria-label', 'Open player settings');
document.body.appendChild(statusButton);
const style = document.createElement('style');
@@ -34,7 +39,7 @@ style.textContent = `
left: max(10px, env(safe-area-inset-left));
z-index: 410;
min-height: 38px;
- max-width: 190px;
+ max-width: 150px;
padding: 7px 11px;
border: 1px solid rgba(220, 168, 79, .72);
border-radius: 999px;
@@ -52,9 +57,17 @@ style.textContent = `
`;
document.head.appendChild(style);
-const setStatus = (state, label) => {
+const setStatus = (state) => {
statusButton.dataset.state = state;
- statusButton.textContent = label;
+ statusButton.textContent = state === 'online'
+ ? `⚙ ONLINE ${Math.max(1, playerCount)}`
+ : state === 'connecting'
+ ? '⚙ CONNECTING'
+ : state === 'disabled'
+ ? '⚙ OFFLINE'
+ : state === 'error'
+ ? '⚙ SOLO'
+ : '⚙ SOLO';
const loadingLabel = state === 'online'
? `Shared skies online${playerCount > 1 ? ` • ${playerCount} captains` : ''}`
@@ -62,15 +75,28 @@ const setStatus = (state, label) => {
? 'Joining shared skies…'
: state === 'error'
? 'Multiplayer unavailable • solo ready'
- : authenticated
- ? 'Multiplayer ready'
- : 'Solo flight ready';
+ : state === 'disabled'
+ ? 'Shared skies disabled • solo ready'
+ : authenticated
+ ? 'Multiplayer ready'
+ : 'Solo flight ready';
window.airshipLoading?.setNetwork?.(loadingLabel);
-};
+}
+
+const getState = () => ({
+ authenticated,
+ connected: socket?.readyState === WebSocket.OPEN,
+ enabled: multiplayerEnabled,
+ playerCount,
+ selfId,
+ profile,
+ self: selfSnapshot,
+ worldSeed,
+});
const emitConnection = (state, extra = {}) => {
window.dispatchEvent(new CustomEvent('airship:multiplayer-state', {
- detail: { state, authenticated, playerCount, selfId, ...extra },
+ detail: { state, ...getState(), ...extra },
}));
};
@@ -86,6 +112,15 @@ const authFetch = async (path, options = {}) => {
});
};
+const loadProfile = async () => {
+ const response = await authFetch('/api/me');
+ if (!response.ok) throw new Error(`profile ${response.status}`);
+ profile = await response.json();
+ worldSeed = profile.worldSeed || worldSeed;
+ emitConnection(socket?.readyState === WebSocket.OPEN ? 'online' : 'authenticated');
+ return profile;
+};
+
const requestSocketTicket = async () => {
const response = await authFetch('/api/ws-ticket', { method: 'POST' });
if (!response.ok) throw new Error(`ticket ${response.status}`);
@@ -93,7 +128,7 @@ const requestSocketTicket = async () => {
};
const scheduleReconnect = () => {
- if (!authenticated || reconnectTimer) return;
+ if (!authenticated || !multiplayerEnabled || reconnectTimer) return;
reconnectTimer = window.setTimeout(() => {
reconnectTimer = null;
connect().catch(() => scheduleReconnect());
@@ -101,17 +136,17 @@ const scheduleReconnect = () => {
};
const connect = async () => {
- if (!authenticated) return;
+ if (!authenticated || !multiplayerEnabled) return;
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) return;
- setStatus('connecting', 'MULTIPLAYER • CONNECTING');
+ setStatus('connecting');
const { ticket } = await requestSocketTicket();
const ws = new WebSocket(`${WS_BASE}/ws?ticket=${encodeURIComponent(ticket)}`);
socket = ws;
ws.addEventListener('open', () => {
inputSequence = 0;
- setStatus('online', 'MULTIPLAYER • ONLINE');
+ setStatus('online');
emitConnection('online');
});
@@ -121,7 +156,8 @@ const connect = async () => {
if (message.type === 'welcome') {
selfId = message.playerId || selfId;
- emitConnection('online', { worldSeed: message.worldSeed });
+ worldSeed = message.worldSeed || worldSeed;
+ emitConnection('online');
return;
}
@@ -135,8 +171,8 @@ const connect = async () => {
? message.projectiles.filter((shot) => shot?.id)
: [];
playerCount = allPlayers.length;
- const score = self ? ` • ${self.kills || 0}/${self.deaths || 0}` : '';
- setStatus('online', `MULTIPLAYER • ${playerCount}${score}`);
+ selfSnapshot = self;
+ setStatus('online');
window.dispatchEvent(new CustomEvent('airship:multiplayer-snapshot', {
detail: {
self,
@@ -151,13 +187,19 @@ const connect = async () => {
ws.addEventListener('close', () => {
if (socket === ws) socket = null;
playerCount = 0;
- setStatus(authenticated ? 'connecting' : 'offline', authenticated ? 'MULTIPLAYER • RECONNECT' : 'MULTIPLAYER');
- emitConnection('offline');
- scheduleReconnect();
+ selfSnapshot = null;
+ if (authenticated && multiplayerEnabled) {
+ setStatus('connecting');
+ emitConnection('offline');
+ scheduleReconnect();
+ } else {
+ setStatus(authenticated ? 'disabled' : 'offline');
+ emitConnection(authenticated ? 'disabled' : 'offline');
+ }
});
ws.addEventListener('error', () => {
- setStatus('error', 'MULTIPLAYER • RETRY');
+ setStatus('error');
});
};
@@ -185,12 +227,40 @@ const fire = (side) => {
const login = () => keycloak.login({ redirectUri: window.location.href });
const logout = () => keycloak.logout({ redirectUri: `${window.location.origin}/` });
-statusButton.addEventListener('click', () => {
- if (!authenticated) {
- login();
+const setEnabled = (enabled) => {
+ multiplayerEnabled = Boolean(enabled);
+ localStorage.setItem(MULTIPLAYER_ENABLED_KEY, String(multiplayerEnabled));
+
+ if (!multiplayerEnabled) {
+ if (reconnectTimer) {
+ clearTimeout(reconnectTimer);
+ reconnectTimer = null;
+ }
+ if (socket) {
+ const current = socket;
+ socket = null;
+ current.close();
+ }
+ playerCount = 0;
+ selfSnapshot = null;
+ setStatus(authenticated ? 'disabled' : 'offline');
+ emitConnection(authenticated ? 'disabled' : 'signed-out');
return;
}
- if (socket?.readyState !== WebSocket.OPEN) connect().catch(() => setStatus('error', 'MULTIPLAYER • RETRY'));
+
+ if (authenticated) {
+ setStatus('connecting');
+ emitConnection('connecting');
+ connect().catch(() => {
+ setStatus('error');
+ emitConnection('error');
+ scheduleReconnect();
+ });
+ }
+};
+
+statusButton.addEventListener('click', () => {
+ window.dispatchEvent(new CustomEvent('airship:settings-open'));
});
window.airshipMultiplayer = {
@@ -199,6 +269,8 @@ window.airshipMultiplayer = {
login,
logout,
connect,
+ setEnabled,
+ getState,
getSelfId: () => selfId,
isAuthenticated: () => authenticated,
isConnected: () => socket?.readyState === WebSocket.OPEN,
@@ -208,7 +280,7 @@ keycloak.onTokenExpired = () => {
keycloak.updateToken(30).catch(() => {
authenticated = false;
if (socket) socket.close();
- setStatus('offline', 'MULTIPLAYER');
+ setStatus('offline');
});
};
@@ -217,19 +289,37 @@ keycloak.init({
pkceMethod: 'S256',
checkLoginIframe: false,
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
-}).then((ok) => {
+}).then(async (ok) => {
authenticated = Boolean(ok);
if (!authenticated) {
- setStatus('offline', 'MULTIPLAYER');
+ profile = null;
+ setStatus('offline');
emitConnection('signed-out');
return;
}
- setStatus('connecting', 'MULTIPLAYER • CONNECTING');
+
+ try {
+ await loadProfile();
+ } catch {
+ profile = { id: selfId, name: keycloak.tokenParsed?.preferred_username || keycloak.tokenParsed?.name || 'Captain' };
+ }
+
+ if (!multiplayerEnabled) {
+ setStatus('disabled');
+ emitConnection('disabled');
+ return;
+ }
+
+ setStatus('connecting');
+ emitConnection('connecting');
connect().catch(() => {
- setStatus('error', 'MULTIPLAYER • RETRY');
+ setStatus('error');
+ emitConnection('error');
scheduleReconnect();
});
}).catch(() => {
authenticated = false;
- setStatus('error', 'MULTIPLAYER • LOGIN');
+ profile = null;
+ setStatus('error');
+ emitConnection('error');
});
diff --git a/src/player-settings.js b/src/player-settings.js
new file mode 100644
index 0000000..6052bcc
--- /dev/null
+++ b/src/player-settings.js
@@ -0,0 +1,372 @@
+const STORAGE_KEY = 'airship.player-settings.v1';
+
+const DEFAULTS = {
+ showPlayerLabels: true,
+ screenShake: true,
+ reducedMotion: false,
+};
+
+const loadPreferences = () => {
+ try {
+ const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
+ return { ...DEFAULTS, ...saved };
+ } catch {
+ return { ...DEFAULTS };
+ }
+};
+
+let preferences = loadPreferences();
+let multiplayerState = window.airshipMultiplayer?.getState?.() || {};
+let latestSelf = null;
+
+const overlay = document.createElement('div');
+overlay.id = 'player-settings-overlay';
+overlay.hidden = true;
+overlay.innerHTML = `
+
+
+
+
+
+
+ ⚓
+
+ CAPTAIN
+ Solo Captain
+ Not signed in
+
+
+
WORLDSKYWARD-001
+
CAPTAINS1
+
K / D0 / 0
+
+
+
+
+
+
SHARED SKIESDrop-in multiplayer without a lobby.
+
SOLO
+
+
+
+
+
+
+
+
+ ACCOUNT
+ Play solo or sign in with Arrowstack.
+
+
+
+
+
+
+
+
+`;
+document.body.appendChild(overlay);
+
+const style = document.createElement('style');
+style.textContent = `
+ #player-settings-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 520;
+ color: #f0d9a0;
+ font-family: Georgia, 'Times New Roman', serif;
+ }
+ #player-settings-overlay[hidden] { display: none; }
+ .player-settings-backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(2, 10, 15, .9);
+ backdrop-filter: blur(9px);
+ -webkit-backdrop-filter: blur(9px);
+ }
+ .player-settings-shell {
+ position: relative;
+ width: min(760px, calc(100vw - 18px));
+ max-height: calc(100dvh - 18px);
+ margin: max(9px, env(safe-area-inset-top)) auto;
+ padding: 14px;
+ overflow: auto;
+ box-sizing: border-box;
+ border: 1px solid rgba(196, 140, 54, .64);
+ border-radius: 14px;
+ background: radial-gradient(circle at 50% 0%, rgba(20, 72, 84, .34), transparent 38%), #06161e;
+ box-shadow: 0 18px 60px rgba(0,0,0,.62);
+ overscroll-behavior: contain;
+ }
+ .player-settings-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 8px 10px 14px;
+ border-bottom: 1px solid rgba(196, 140, 54, .34);
+ }
+ .player-settings-kicker,
+ .player-settings-label {
+ color: #adcfca;
+ font: 700 10px/1 system-ui, sans-serif;
+ letter-spacing: .18em;
+ }
+ .player-settings-header h1 {
+ margin: 4px 0 3px;
+ color: #fff0bd;
+ font-size: clamp(28px, 7vw, 46px);
+ line-height: .95;
+ }
+ .player-settings-header p {
+ margin: 0;
+ color: #9eb2b9;
+ font: 600 11px/1.3 system-ui, sans-serif;
+ }
+ .player-settings-close,
+ .player-settings-actions button {
+ min-height: 44px;
+ padding: 8px 14px;
+ border: 1px solid #9a6a2b;
+ border-radius: 9px;
+ background: linear-gradient(#174a50, #0a2930);
+ color: #ffe5a7;
+ font: 700 11px/1 Georgia, serif;
+ touch-action: manipulation;
+ }
+ .player-settings-content {
+ display: grid;
+ gap: 10px;
+ padding-top: 12px;
+ }
+ .player-settings-card {
+ padding: 14px;
+ border: 1px solid rgba(194, 142, 61, .46);
+ border-radius: 12px;
+ background: linear-gradient(180deg, rgba(10, 52, 61, .88), rgba(4, 24, 31, .96));
+ }
+ .player-settings-profile {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ align-items: center;
+ gap: 12px;
+ }
+ .player-avatar {
+ width: 58px;
+ height: 58px;
+ display: grid;
+ place-items: center;
+ border: 2px solid #a06b2d;
+ border-radius: 50%;
+ background: radial-gradient(circle, #1b6062, #082b34 68%);
+ box-shadow: inset 0 0 0 2px rgba(239, 197, 104, .2);
+ font-size: 28px;
+ }
+ .player-profile-copy { display: grid; gap: 3px; min-width: 0; }
+ .player-profile-copy strong { color: #fff0bd; font-size: 21px; overflow: hidden; text-overflow: ellipsis; }
+ .player-profile-copy > span:last-child { color: #9db4ba; font: 600 11px/1.3 system-ui,sans-serif; }
+ .player-stat-grid {
+ grid-column: 1 / -1;
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 7px;
+ }
+ .player-stat-grid div {
+ display: grid;
+ gap: 3px;
+ padding: 9px;
+ border: 1px solid rgba(194, 142, 61, .3);
+ border-radius: 8px;
+ background: rgba(2, 18, 24, .72);
+ }
+ .player-stat-grid span { color: #8ca9ae; font:700 9px/1 system-ui,sans-serif; letter-spacing:.08em; }
+ .player-stat-grid strong { color:#f7dfa1; font:700 12px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace; }
+ .player-settings-section-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding-bottom: 8px;
+ }
+ .player-settings-section-head > div { display:grid; gap:3px; }
+ .player-settings-section-head strong { color:#fff0bd; font-size:15px; }
+ .player-settings-section-head span { color:#9db4ba; font:600 10px/1.3 system-ui,sans-serif; }
+ .player-connection-chip {
+ padding: 6px 9px;
+ border: 1px solid rgba(194,142,61,.42);
+ border-radius: 999px;
+ color: #f7dfa1 !important;
+ white-space: nowrap;
+ }
+ .player-setting-row {
+ min-height: 58px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+ border-top: 1px solid rgba(194,142,61,.18);
+ color: #f0d9a0;
+ }
+ .player-setting-row > span { display:grid; gap:3px; }
+ .player-setting-row strong { color:#f5dda0; font:700 13px/1.2 system-ui,sans-serif; }
+ .player-setting-row small { color:#91aab0; font:600 10px/1.3 system-ui,sans-serif; }
+ .player-setting-row input {
+ width: 46px;
+ height: 26px;
+ flex: 0 0 auto;
+ accent-color: #1b6b69;
+ }
+ .player-settings-account {
+ display:flex;
+ align-items:center;
+ justify-content:space-between;
+ gap:12px;
+ }
+ .player-settings-account > div:first-child { display:grid; gap:4px; }
+ .player-settings-account strong { color:#f5dda0; font:700 12px/1.25 system-ui,sans-serif; }
+ .player-settings-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:7px; }
+ body.player-settings-open #mobile-controls,
+ body.player-settings-open #multiplayer-status { visibility: hidden; }
+ html.airship-reduced-motion *,
+ html.airship-reduced-motion *::before,
+ html.airship-reduced-motion *::after {
+ scroll-behavior: auto !important;
+ animation-duration: .001ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: .001ms !important;
+ }
+ @media (max-width: 560px) {
+ .player-settings-shell { min-height: calc(100dvh - 18px); }
+ .player-settings-account { align-items:flex-start; flex-direction:column; }
+ .player-settings-actions { width:100%; }
+ .player-settings-actions button { flex:1 1 140px; }
+ }
+`;
+document.head.appendChild(style);
+
+const fields = Object.fromEntries(
+ [...overlay.querySelectorAll('[data-player-settings]')].map((el) => [el.dataset.playerSettings, el]),
+);
+const settingInputs = Object.fromEntries(
+ [...overlay.querySelectorAll('[data-player-setting]')].map((el) => [el.dataset.playerSetting, el]),
+);
+
+const persistPreferences = () => {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
+ document.documentElement.classList.toggle('airship-reduced-motion', Boolean(preferences.reducedMotion));
+ window.dispatchEvent(new CustomEvent('airship:settings-changed', {
+ detail: { ...preferences },
+ }));
+};
+
+const render = () => {
+ multiplayerState = window.airshipMultiplayer?.getState?.() || multiplayerState || {};
+ const profile = multiplayerState.profile || {};
+ const connected = Boolean(multiplayerState.connected);
+ const authenticated = Boolean(multiplayerState.authenticated);
+ const enabled = multiplayerState.enabled !== false;
+ const self = multiplayerState.self || latestSelf || null;
+
+ fields.name.textContent = profile.name || self?.name || (authenticated ? 'Captain' : 'Solo Captain');
+ fields.session.textContent = authenticated
+ ? connected ? 'Signed in • shared skies connected' : enabled ? 'Signed in • reconnecting' : 'Signed in • multiplayer disabled'
+ : 'Not signed in • local flight';
+ fields.world.textContent = multiplayerState.worldSeed || 'SKYWARD-001';
+ fields.players.textContent = connected ? multiplayerState.playerCount || 1 : 1;
+ fields.kd.textContent = `${self?.kills || 0} / ${self?.deaths || 0}`;
+ fields.connection.textContent = connected ? `ONLINE ${multiplayerState.playerCount || 1}` : authenticated && enabled ? 'CONNECTING' : authenticated ? 'OFFLINE' : 'SOLO';
+ fields['account-summary'].textContent = authenticated
+ ? `Arrowstack account • ${profile.name || self?.name || 'Captain'}`
+ : 'Play solo or sign in with Arrowstack.';
+
+ settingInputs.multiplayerEnabled.checked = enabled;
+ settingInputs.multiplayerEnabled.disabled = !authenticated;
+ settingInputs.showPlayerLabels.checked = Boolean(preferences.showPlayerLabels);
+ settingInputs.screenShake.checked = Boolean(preferences.screenShake);
+ settingInputs.reducedMotion.checked = Boolean(preferences.reducedMotion);
+
+ const authButton = overlay.querySelector('[data-player-settings-action="auth"]');
+ authButton.textContent = authenticated ? 'SIGN OUT' : 'SIGN IN';
+ const accountButton = overlay.querySelector('[data-player-settings-action="account"]');
+ accountButton.disabled = !authenticated;
+};
+
+const open = () => {
+ render();
+ overlay.hidden = false;
+ document.body.classList.add('player-settings-open');
+};
+const close = () => {
+ overlay.hidden = true;
+ document.body.classList.remove('player-settings-open');
+};
+
+for (const [key, input] of Object.entries(settingInputs)) {
+ input.addEventListener('change', () => {
+ if (key === 'multiplayerEnabled') {
+ window.airshipMultiplayer?.setEnabled?.(input.checked);
+ render();
+ return;
+ }
+ preferences = { ...preferences, [key]: input.checked };
+ persistPreferences();
+ render();
+ });
+}
+
+overlay.querySelector('.player-settings-close').addEventListener('click', close);
+overlay.querySelector('.player-settings-backdrop').addEventListener('click', close);
+overlay.querySelector('[data-player-settings-action="auth"]').addEventListener('click', () => {
+ if (window.airshipMultiplayer?.isAuthenticated?.()) window.airshipMultiplayer.logout();
+ else window.airshipMultiplayer?.login?.();
+});
+overlay.querySelector('[data-player-settings-action="account"]').addEventListener('click', () => {
+ if (!window.airshipMultiplayer?.isAuthenticated?.()) return;
+ window.open('https://auth.arrowstack.dev/realms/dev/account/', '_blank', 'noopener,noreferrer');
+});
+
+window.addEventListener('airship:settings-open', open);
+window.addEventListener('airship:multiplayer-state', () => {
+ if (!overlay.hidden) render();
+});
+window.addEventListener('airship:multiplayer-snapshot', (event) => {
+ latestSelf = event.detail?.self || latestSelf;
+ if (!overlay.hidden) render();
+});
+window.addEventListener('keydown', (event) => {
+ if (event.key === 'Escape' && !overlay.hidden) close();
+});
+
+window.airshipSettings = {
+ get: () => ({ ...preferences }),
+ open,
+ close,
+};
+
+persistPreferences();
diff --git a/tests/wiring.test.mjs b/tests/wiring.test.mjs
index b08aa76..346a19a 100644
--- a/tests/wiring.test.mjs
+++ b/tests/wiring.test.mjs
@@ -4,6 +4,32 @@ import { readFile } from 'node:fs/promises';
const read = (path) => readFile(new URL(`../${path}`, import.meta.url), 'utf8');
+test('player settings owns account, multiplayer and local flight preferences', async () => {
+ const [entry, settings, multiplayer, game] = await Promise.all([
+ read('src/game-entry.js'),
+ read('src/player-settings.js'),
+ read('src/multiplayer.js'),
+ read('src/game-v4.js'),
+ ]);
+
+ assert.match(entry, /import '.\/player-settings\.js'/);
+ assert.match(settings, /Player Settings/);
+ assert.match(settings, /data-player-setting="multiplayerEnabled"/);
+ assert.match(settings, /data-player-setting="showPlayerLabels"/);
+ assert.match(settings, /data-player-setting="screenShake"/);
+ assert.match(settings, /data-player-setting="reducedMotion"/);
+ assert.match(settings, /window\.airshipMultiplayer\?\.setEnabled/);
+ assert.match(settings, /window\.airshipMultiplayer\.logout\(\)/);
+ assert.match(settings, /realms\/dev\/account/);
+ assert.match(multiplayer, /airship:settings-open/);
+ assert.match(multiplayer, /const setEnabled = \(enabled\) =>/);
+ assert.match(multiplayer, /getState/);
+ assert.match(multiplayer, /\/api\/me/);
+ assert.match(multiplayer, /⚙ ONLINE/);
+ assert.match(game, /window\.airshipSettings\?\.get\?\.\(\)\.screenShake/);
+ assert.match(game, /showPlayerLabels/);
+});
+
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'),