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:
Antigravity committed 2026-09-17 09:30:52 +00:00
commit 6457a8904e
50 files changed
+14486

No files matched your search

+209
View File
@@ -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 });
}
};
})();