// 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'; /* ─── Password hashing (djb2 for lightweight fast client hashing) ─── */ function hashPassword(str) { if (!str) return ''; let h = 0; for (let i = 0; i < str.length; i++) { h = ((h << 5) - h) + str.charCodeAt(i); h |= 0; } return h.toString(16); } /* ─── Validation Helpers ─── */ function validateUsername(username) { const u = (username || '').trim(); if (!u) return { valid: false, message: 'Введите имя пользователя' }; if (u.length < 3) return { valid: false, message: 'Логин должен содержать минимум 3 символа' }; if (u.length > 20) return { valid: false, message: 'Логин не должен превышать 20 символов' }; if (!/^[a-zA-Zа-яА-ЯёЁ0-9_-]+$/.test(u)) { return { valid: false, message: 'Логин может содержать только буквы, цифры, "-" и "_"' }; } const reserved = ['гость', 'guest', 'admin', 'null', 'undefined', 'system', 'anonymous']; if (reserved.includes(u.toLowerCase())) { return { valid: false, message: 'Это имя пользователя зарезервировано' }; } return { valid: true, username: u }; } function validatePassword(password) { const p = password || ''; if (!p) return { valid: false, message: 'Введите пароль' }; if (p.length < 4) return { valid: false, message: 'Пароль должен содержать не менее 4 символов' }; return { valid: true, password: p }; } /* ─── Default blank profile ─── */ function getDefaultData() { return { coins: 150, level: 1, maxUnlockedLevel: 1, highScore: 0, theme: 'classic', upgrades: { extraSlot: false, hasRake: false }, unlockedPlants: ['sunflower', 'peashooter', 'wallnut'], selectedDeck: ['sunflower', 'peashooter', 'wallnut'], 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) }); const json = await resp.json(); return 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); } function mergeUserData(serverData) { const def = getDefaultData(); if (!serverData) return def; return { ...def, ...serverData, upgrades: { ...def.upgrades, ...(serverData.upgrades || {}) }, unlockedPlants: serverData.unlockedPlants || def.unlockedPlants, selectedDeck: serverData.selectedDeck || def.selectedDeck, materials: { ...def.materials, ...(serverData.materials || {}) }, plantLevels: { ...def.plantLevels, ...(serverData.plantLevels || {}) }, perks: { ...def.perks, ...(serverData.perks || {}) }, zenGarden: serverData.zenGarden ? { ...def.zenGarden, ...serverData.zenGarden, pots: serverData.zenGarden.pots || def.zenGarden.pots, snail: { ...def.zenGarden.snail, ...(serverData.zenGarden.snail || {}) } } : def.zenGarden, treeOfWisdom: serverData.treeOfWisdom || def.treeOfWisdom, achievements: serverData.achievements || {} }; } return { /* ── Public API ── */ getCurrentUser() { return localStorage.getItem(CURRENT_USER_KEY) || 'Гость'; }, isGuest() { const u = localStorage.getItem(CURRENT_USER_KEY); return !u || u === 'Гость'; }, getDefaultData, validateUsername, validatePassword, apiPost, scheduleSave, /** * User Login */ async login(username, password, callback = null) { const userVal = validateUsername(username); if (!userVal.valid) { if (typeof callback === 'function') callback(false, null, userVal.message); return { success: false, message: userVal.message }; } const passVal = validatePassword(password); if (!passVal.valid) { if (typeof callback === 'function') callback(false, null, passVal.message); return { success: false, message: passVal.message }; } const cleanUser = userVal.username; const pwdHash = hashPassword(passVal.password); const res = await apiPost('login', { username: cleanUser, passwordHash: pwdHash }); if (res && res.success && res.data) { localStorage.setItem(CURRENT_USER_KEY, cleanUser); _cachedData = mergeUserData(res.data); localSave(_cachedData); if (window.GameEngine && window._pvzEngine) { window._pvzEngine.loadUserData(); } if (typeof callback === 'function') callback(true, _cachedData, null); return { success: true, username: cleanUser, data: _cachedData }; } else { const errorMsg = (res && res.message) ? res.message : 'Неверный логин или пароль'; if (typeof callback === 'function') callback(false, null, errorMsg); return { success: false, message: errorMsg }; } }, /** * User Registration */ async register(username, password, callback = null) { const userVal = validateUsername(username); if (!userVal.valid) { if (typeof callback === 'function') callback(false, null, userVal.message); return { success: false, message: userVal.message }; } const passVal = validatePassword(password); if (!passVal.valid) { if (typeof callback === 'function') callback(false, null, passVal.message); return { success: false, message: passVal.message }; } const cleanUser = userVal.username; const pwdHash = hashPassword(passVal.password); const initData = getDefaultData(); const res = await apiPost('register', { username: cleanUser, passwordHash: pwdHash, initialData: initData }); if (res && res.success) { localStorage.setItem(CURRENT_USER_KEY, cleanUser); _cachedData = mergeUserData(res.data || initData); localSave(_cachedData); if (window.GameEngine && window._pvzEngine) { window._pvzEngine.loadUserData(); } if (typeof callback === 'function') callback(true, _cachedData, null); return { success: true, username: cleanUser, data: _cachedData }; } else { const errorMsg = (res && res.message) ? res.message : 'Ошибка при регистрации'; if (typeof callback === 'function') callback(false, null, errorMsg); return { success: false, message: errorMsg }; } }, loginAsGuest() { localStorage.setItem(CURRENT_USER_KEY, 'Гость'); _cachedData = null; localSave(null); return { success: true, user: 'Гость' }; }, logout() { 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; 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 }); } }; })();