feat: Initial commit Plants vs Zombies Web Edition v2.4
- 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
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
// 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 });
|
||||
}
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user