- 5 Biomes: Day, Night, Pool, Fog, Roof (25 Campaign levels) - Minigames: Wall-nut Bowling, I Zombie, Roguelike, Boss Rush & Dr. Zomboss - Systems: Zen Garden, Workshop Crafting, Almanac, Tree of Wisdom, Achievements - Backend: REST API + SQLite Accounts & Save Synchronization - Infrastructure: Nginx HTTPS & Docker deployment
210 lines
7.5 KiB
JavaScript
210 lines
7.5 KiB
JavaScript
// 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 });
|
|
}
|
|
};
|
|
})();
|