isle.pizza/src/stores.js
foxtacles 93a0c46b46
Add multiplayer frontend updates (#30)
* Add multiplayer, cloud sync, crash reporting, scene player, and memories features

* Fix multiplayer overlay showing "Waiting for ..." with no names

* Fix OGL link in README

* Update README with architecture, backend setup, environment variables, and CI docs

* Fix save editor showing wrong name for orphaned save slots

Players.gsi could fall out of sync with save files during cloud sync
because the saveSlotWritten event only tracked the slot file and
History.gsi for incremental upload, not Players.gsi. This caused
slots without a matching Players.gsi entry to display the first
player's name due to a fallback to index 0.

- Track Players.gsi in saveSlotWritten handler for incremental uploads
- Remove broken fallback to player index 0 in name resolution
- Hide save slots with no Players.gsi entry from the save editor UI
2026-04-05 17:13:23 +02:00

146 lines
4.9 KiB
JavaScript

import { writable } from 'svelte/store';
import { validateRoomName } from './core/room-names.js';
import { fromUrlSafeBase64 } from './core/base64.js';
const PAGE_MAP = {
'#read-me': 'read-me',
'#configure': 'configure',
'#free-stuff': 'free-stuff',
'#save-editor': 'save-editor',
'#multiplayer': 'multiplayer',
'#memories': 'memories'
};
// Parse a hash string into { page, room, invalidRoom }
function parseHash(hash) {
if (hash.startsWith('#r/')) {
const room = hash.slice(3);
if (validateRoomName(room)) {
return { page: 'multiplayer', room };
}
return { page: 'multiplayer', room: null, invalidRoom: true };
}
return { page: PAGE_MAP[hash] || 'main', room: null };
}
// Match a pathname against /memory/:id or /scene/:encoded path routes.
// Returns { eventId } or { sceneData } on match, null otherwise.
export function matchPathRoute(path) {
const memoryMatch = path.match(/^\/memory\/([A-Za-z0-9_-]+)$/);
if (memoryMatch) return { eventId: memoryMatch[1] };
const sceneMatch = path.match(/^\/scene\/([A-Za-z0-9_-]+)$/);
if (sceneMatch) return { sceneData: sceneMatch[1] };
return null;
}
// Parse the full URL (pathname + hash) into route state
export function parseRoute() {
if (typeof window === 'undefined') return { page: 'main', room: null };
const match = matchPathRoute(window.location.pathname);
if (match) return { page: 'scene-player', room: null, ...match };
return parseHash(window.location.hash);
}
// Page navigation - initialize from URL to prevent flicker on reload
function getInitialState() {
if (typeof window === 'undefined') return { page: 'main', room: null };
return parseRoute();
}
const initial = getInitialState();
export const currentPage = writable(initial.page);
export const multiplayerRoom = writable(initial.room);
// Set on startup if the initial URL had an invalid room
export const initialInvalidRoom = initial.invalidRoom || false;
// Initialize scene player stores from URL if applicable
const _initialEventId = initial.eventId || null;
const _initialSceneData = initial.sceneData || null;
export function tryDecodeSceneData(encoded) {
if (!encoded) return null;
try { return JSON.parse(fromUrlSafeBase64(encoded)); } catch { return null; }
}
// Debug mode
export const debugEnabled = writable(false);
// Sound state
export const soundEnabled = writable(false);
// Popup visibility
export const showUpdatePopup = writable(false);
export const showGoodbyePopup = writable(false);
export const goodbyeProgress = writable(0);
// Install state
export const installState = writable({
installed: false,
installing: false,
progress: 0,
missingFiles: []
});
// Config toast
export const configToastVisible = writable(false);
export const configToastMessage = writable('Settings saved');
// Debug UI visible (set when game reaches intro animation)
export const debugUIVisible = writable(false);
// Game running state
export const gameRunning = writable(false);
// Multiplayer player count (push-based from C++ via CustomEvent)
export const multiplayerPlayerCount = writable(null);
// Third-person camera state (push-based from C++ via CustomEvent)
export const thirdPersonEnabled = writable(true);
// Name bubbles visibility (push-based from C++ via CustomEvent)
export const showNameBubbles = writable(true);
// Allow customization setting (push-based from C++ via CustomEvent)
export const allowCustomize = writable(true);
// Connection status (push-based from C++ via CustomEvent)
// Values: null (no session), 'connected', 'reconnecting', 'failed'
export const connectionStatus = writable(null);
// Service worker registration
export const swRegistration = writable(null);
// OPFS availability
export const opfsDisabled = writable(false);
// Bumped when cloud sync writes config/saves from the server
export const configVersion = writable(0);
export const savesVersion = writable(0);
// Animation state pushed from C++ backend (reactive, always current)
// { location, state, currentAnimIndex, animations[] }
export const animationState = writable(null);
// Set of animIndex values the player has completed at least once (from IndexedDB)
export const memoryUnlocks = writable(new Set());
// All completion records from IndexedDB (null = not loaded yet)
export const memoryCompletions = writable(null);
// Scene player state (set when navigating to /memory/ or /scene/ URLs)
// Initialize from URL so they're available before onMount runs
export const scenePlayerEventId = writable(_initialEventId);
export const scenePlayerData = writable(tryDecodeSceneData(_initialSceneData));
// Crash state — set when game aborts/crashes
export const gameCrashed = writable(null);
// Save editor state
export const saveEditorState = writable({
slots: [], // Array of SaveSlot objects
selectedSlot: null, // Currently selected slot number
loading: true,
error: null
});