Add player settings overlay
This commit is contained in:
parent
8b4bc846a5
commit
dee38cf602
372
src/player-settings.js
Normal file
372
src/player-settings.js
Normal file
@ -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 = `
|
||||
<div class="player-settings-backdrop"></div>
|
||||
<section class="player-settings-shell" role="dialog" aria-modal="true" aria-label="Player settings">
|
||||
<header class="player-settings-header">
|
||||
<div>
|
||||
<span class="player-settings-kicker">CAPTAIN</span>
|
||||
<h1>Player Settings</h1>
|
||||
<p>Identity, shared skies and flight preferences.</p>
|
||||
</div>
|
||||
<button class="player-settings-close" type="button" aria-label="Close settings">CLOSE</button>
|
||||
</header>
|
||||
|
||||
<main class="player-settings-content">
|
||||
<section class="player-settings-card player-settings-profile">
|
||||
<div class="player-avatar" aria-hidden="true">⚓</div>
|
||||
<div class="player-profile-copy">
|
||||
<span class="player-settings-label">CAPTAIN</span>
|
||||
<strong data-player-settings="name">Solo Captain</strong>
|
||||
<span data-player-settings="session">Not signed in</span>
|
||||
</div>
|
||||
<div class="player-stat-grid">
|
||||
<div><span>WORLD</span><strong data-player-settings="world">SKYWARD-001</strong></div>
|
||||
<div><span>CAPTAINS</span><strong data-player-settings="players">1</strong></div>
|
||||
<div><span>K / D</span><strong data-player-settings="kd">0 / 0</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="player-settings-card">
|
||||
<div class="player-settings-section-head">
|
||||
<div><strong>SHARED SKIES</strong><span>Drop-in multiplayer without a lobby.</span></div>
|
||||
<span class="player-connection-chip" data-player-settings="connection">SOLO</span>
|
||||
</div>
|
||||
<label class="player-setting-row">
|
||||
<span><strong>Multiplayer</strong><small>Join the shared authoritative world when signed in.</small></span>
|
||||
<input type="checkbox" data-player-setting="multiplayerEnabled" />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="player-settings-card">
|
||||
<div class="player-settings-section-head">
|
||||
<div><strong>FLIGHT DISPLAY</strong><span>Local preferences stored on this device.</span></div>
|
||||
</div>
|
||||
<label class="player-setting-row">
|
||||
<span><strong>Captain labels</strong><small>Show names, hull and K/D above other player ships.</small></span>
|
||||
<input type="checkbox" data-player-setting="showPlayerLabels" />
|
||||
</label>
|
||||
<label class="player-setting-row">
|
||||
<span><strong>Screen shake</strong><small>Camera impact when your Sparrow takes damage.</small></span>
|
||||
<input type="checkbox" data-player-setting="screenShake" />
|
||||
</label>
|
||||
<label class="player-setting-row">
|
||||
<span><strong>Reduced motion</strong><small>Reduce non-essential UI and transition animation.</small></span>
|
||||
<input type="checkbox" data-player-setting="reducedMotion" />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="player-settings-card player-settings-account">
|
||||
<div>
|
||||
<span class="player-settings-label">ACCOUNT</span>
|
||||
<strong data-player-settings="account-summary">Play solo or sign in with Arrowstack.</strong>
|
||||
</div>
|
||||
<div class="player-settings-actions">
|
||||
<button type="button" data-player-settings-action="account">MANAGE ACCOUNT</button>
|
||||
<button type="button" data-player-settings-action="auth">SIGN IN</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</section>
|
||||
`;
|
||||
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();
|
||||
Loading…
x
Reference in New Issue
Block a user