mirror of
https://github.com/isledecomp/isle.pizza.git
synced 2026-05-02 02:33:57 +00:00
* 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
146 lines
4.9 KiB
JavaScript
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
|
|
});
|