/** * Player Storage API * * Handles loading and saving player game states from/to the backend. * This enables cross-domain synchronization between hoerdle.de and hördle.de. */ import { getOrCreatePlayerId } from './playerId'; import type { GameState, Statistics } from './gameState'; /** * Get genre key for storage * * Formats the genre/special into a consistent key format: * - Global: "global" * - Genre: "Rock" (localized name) * - Special: "special:00725" (special name) */ export function getGenreKey( genre: string | null, isSpecial: boolean, specialName?: string ): string { if (isSpecial && specialName) { return `special:${specialName}`; } return genre || 'global'; } /** * Load player state from backend * * @param genreKey - Genre key (from getGenreKey) * @returns GameState and Statistics, or null if not found */ export async function loadPlayerState( genreKey: string ): Promise<{ gameState: GameState; statistics: Statistics } | null> { try { // Use async version to enable cross-domain player ID sync const { getOrCreatePlayerIdAsync } = await import('./playerId'); const playerId = await getOrCreatePlayerIdAsync(genreKey); if (!playerId) { return null; } // Determine if it's a special or genre const isSpecial = genreKey.startsWith('special:'); const params = new URLSearchParams(); if (isSpecial) { const specialName = genreKey.replace('special:', ''); params.append('special', specialName); } else if (genreKey !== 'global') { params.append('genre', genreKey); } const response = await fetch(`/api/player-state?${params.toString()}`, { method: 'GET', headers: { 'X-Player-Id': playerId, }, }); if (!response.ok) { if (response.status === 404) { // No state found - this is normal for new players return null; } // Other errors: log and return null (will fallback to localStorage) console.warn('[playerStorage] Failed to load player state:', response.status); return null; } const data = await response.json(); if (!data || !data.gameState || !data.statistics) { return null; } return { gameState: data.gameState as GameState, statistics: data.statistics as Statistics, }; } catch (error) { // Network errors or other issues: fallback to localStorage console.warn('[playerStorage] Error loading player state:', error); return null; } } /** * Save player state to backend * * @param genreKey - Genre key (from getGenreKey) * @param gameState - Current game state * @param statistics - Current statistics */ export async function savePlayerState( genreKey: string, gameState: GameState, statistics: Statistics ): Promise { try { // Use async version to ensure device ID is included const { getOrCreatePlayerIdAsync } = await import('./playerId'); const playerId = await getOrCreatePlayerIdAsync(); if (!playerId) { console.warn('[playerStorage] No player ID available, cannot save state'); return; } const response = await fetch('/api/player-state', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Player-Id': playerId, }, body: JSON.stringify({ genreKey, gameState, statistics, }), }); if (!response.ok) { console.warn('[playerStorage] Failed to save player state:', response.status); // Don't throw - allow fallback to localStorage } } catch (error) { // Network errors: log but don't throw (will fallback to localStorage) console.warn('[playerStorage] Error saving player state:', error); } }