// Auth & Profile Management System — Server-backed (SQLite via /api/auth/) // Falls back to localStorage if server is unavailable. window.PvZAuth = (function () { const CURRENT_USER_KEY = 'pvz_current_user'; const LOCAL_DATA_KEY = 'pvz_local_data'; // local fallback cache const API_BASE = '/api/auth'; /* ─── Simple djb2 hash (same as before, for backward-compat) ─── */ function hashPassword(str) { let h = 0; for (let i = 0; i < str.length; i++) { h = ((h << 5) - h) + str.charCodeAt(i); h |= 0; } return h.toString(16); } /* ─── Default blank profile ─── */ function getDefaultData() { return { coins: 150, level: 1, maxUnlockedLevel: 1, highScore: 0, theme: 'classic', upgrades: { extraSlot: false, hasRake: false }, materials: { bioHumus: 10, scrapMetal: 5, sunEssence: 20, crystals: 0 }, plantLevels: { peashooter: 1, sunflower: 1, wallnut: 1, potatomine: 1, repeater: 1, snowpea: 1, chomper: 1 }, perks: { magnetism: 0, startSun: 0, fastCool: 0, hardyRoots: 0, luckyDrop: 0 }, zenGarden: { pots: [ { id: 0, plantType: 'sunflower', stage: 2, thirstTimer: 0, needsFertilizer: false, happyTimer: 60, coinTimer: 5 }, { id: 1, plantType: 'peashooter', stage: 1, thirstTimer: 0, needsFertilizer: false, happyTimer: 45, coinTimer: 8 }, { id: 2, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 20 }, { id: 3, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 20 }, { id: 4, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 20 }, { id: 5, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 20 } ], fertilizerCount: 5, chocolateCount: 2, snail: { x: 100, targetX: 300, dir: 1, isAwake: true, sleepTimer: 120, boostTimer: 0 }, lastVisit: Date.now() }, treeOfWisdom: { height: 0, cheats: {} }, achievements: {} }; } /* ─── In-memory current user data ─── */ let _cachedData = null; function localSave(data) { try { localStorage.setItem(LOCAL_DATA_KEY, JSON.stringify(data)); } catch (_) {} } function localLoad() { try { return JSON.parse(localStorage.getItem(LOCAL_DATA_KEY) || 'null'); } catch (_) { return null; } } /* ─── Non-blocking server call ─── */ async function apiPost(endpoint, body) { try { const resp = await fetch(`${API_BASE}/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await resp.json(); } catch (e) { return { success: false, message: 'Сервер недоступен' }; } } /* ─── Debounced save (prevents save storms on rapid updates) ─── */ let _saveTimer = null; function scheduleSave() { if (_saveTimer) clearTimeout(_saveTimer); _saveTimer = setTimeout(() => { const user = localStorage.getItem(CURRENT_USER_KEY); if (!user || user === 'Гость' || !_cachedData) return; apiPost('save', { username: user, data: _cachedData }); }, 1500); } return { /* ── Public API ── */ getCurrentUser() { return localStorage.getItem(CURRENT_USER_KEY) || 'Гость'; }, isGuest() { const u = localStorage.getItem(CURRENT_USER_KEY); return !u || u === 'Гость'; }, getDefaultData, /** * Login or auto-register. * Synchronous return with provisional localStorage data; * server response loaded asynchronously and applied via callback. */ login(username, password, onServerResponse = null) { username = (username || '').trim(); if (!username) return { success: false, message: 'Введите имя пользователя' }; if (username.toLowerCase() === 'гость') { this.loginAsGuest(); return { success: true, user: 'Гость' }; } const pwdHash = hashPassword(password); // Optimistically set local session localStorage.setItem(CURRENT_USER_KEY, username); _cachedData = getDefaultData(); localSave(_cachedData); // Async server auth apiPost('login', { username, passwordHash: pwdHash }).then(res => { if (res && res.success && res.data) { // Merge defaults + server data const def = getDefaultData(); _cachedData = { ...def, ...res.data, upgrades: { ...def.upgrades, ...(res.data.upgrades || {}) }, materials: { ...def.materials, ...(res.data.materials || {}) }, plantLevels: { ...def.plantLevels, ...(res.data.plantLevels || {}) }, perks: { ...def.perks, ...(res.data.perks || {}) }, zenGarden: res.data.zenGarden ? { ...def.zenGarden, ...res.data.zenGarden, pots: res.data.zenGarden.pots || def.zenGarden.pots, snail: { ...def.zenGarden.snail, ...(res.data.zenGarden.snail || {}) } } : def.zenGarden, treeOfWisdom: res.data.treeOfWisdom || def.treeOfWisdom, achievements: res.data.achievements || {} }; localSave(_cachedData); if (typeof onServerResponse === 'function') onServerResponse(_cachedData); // Notify game engine to reload profile if (window.GameEngine && window._pvzEngine) { window._pvzEngine.loadUserData(); } } else if (res && !res.success) { // Wrong password — kick back to guest if (res.message && res.message.includes('пароль')) { localStorage.setItem(CURRENT_USER_KEY, 'Гость'); _cachedData = null; if (typeof onServerResponse === 'function') onServerResponse(null, res.message); } } }); return { success: true, user: username, pending: true }; }, loginAsGuest() { localStorage.setItem(CURRENT_USER_KEY, 'Гость'); _cachedData = null; localSave(null); return { success: true, user: 'Гость' }; }, logout() { // Flush any pending save first if (_saveTimer) { clearTimeout(_saveTimer); _saveTimer = null; } const user = this.getCurrentUser(); if (user !== 'Гость' && _cachedData) { apiPost('save', { username: user, data: _cachedData }); } this.loginAsGuest(); }, getUserData() { if (this.isGuest()) return getDefaultData(); if (_cachedData) return _cachedData; // Try local cache (sync fallback while server loads) const local = localLoad(); if (local) { _cachedData = local; return local; } _cachedData = getDefaultData(); return _cachedData; }, saveUserData(data) { if (this.isGuest()) return; // Guests are not persisted _cachedData = _cachedData ? { ..._cachedData, ...data } : { ...getDefaultData(), ...data }; localSave(_cachedData); // instant local write scheduleSave(); // debounced server sync }, /** Force immediate server save (call on game-over / victory) */ flushSave() { if (this.isGuest() || !_cachedData) return; if (_saveTimer) { clearTimeout(_saveTimer); _saveTimer = null; } apiPost('save', { username: this.getCurrentUser(), data: _cachedData }); } }; })();