feat(gameplay): add crazy dave casino and bank, and zombie mutation lab
This commit is contained in:
+238
-20
@@ -131,6 +131,52 @@ window.PvZAuth = (function () {
|
||||
try { return JSON.parse(localStorage.getItem(LOCAL_DATA_KEY) || 'null'); } catch (_) { return null; }
|
||||
}
|
||||
|
||||
/* ─── Profile change event listeners ─── */
|
||||
const _profileListeners = new Set();
|
||||
function onProfileChange(fn) {
|
||||
if (typeof fn === 'function') _profileListeners.add(fn);
|
||||
}
|
||||
function offProfileChange(fn) {
|
||||
_profileListeners.delete(fn);
|
||||
}
|
||||
function notifyProfileChange(data) {
|
||||
_profileListeners.forEach(fn => {
|
||||
try { fn(data); } catch (e) { console.error('Profile listener error:', e); }
|
||||
});
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('pvz:profile-updated', { detail: data }));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/* ─── Non-blocking GET server call with Bearer Token Authorization ─── */
|
||||
async function apiGet(endpoint) {
|
||||
try {
|
||||
const headers = {};
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const resp = await fetch(`${API_BASE}/${endpoint}`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
if (resp.status === 401) {
|
||||
clearToken();
|
||||
const json = await resp.json().catch(() => ({}));
|
||||
return { success: false, status: 401, message: json.message || 'Сессия истекла или недействительна' };
|
||||
}
|
||||
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
} catch (e) {
|
||||
return { success: false, message: 'Сервер авторизации недоступен' };
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Non-blocking server call with Bearer Token Authorization ─── */
|
||||
async function apiPost(endpoint, body = {}, keepalive = false) {
|
||||
try {
|
||||
@@ -172,41 +218,90 @@ window.PvZAuth = (function () {
|
||||
|
||||
/* ─── Debounced save (prevents save storms on rapid updates) ─── */
|
||||
let _saveTimer = null;
|
||||
let _hasSyncedFromServer = false;
|
||||
|
||||
function scheduleSave() {
|
||||
if (_saveTimer) clearTimeout(_saveTimer);
|
||||
_saveTimer = setTimeout(() => {
|
||||
_saveTimer = setTimeout(async () => {
|
||||
const user = localStorage.getItem(CURRENT_USER_KEY);
|
||||
if (!user || user === 'Гость' || !_cachedData) return;
|
||||
if (!_hasSyncedFromServer && getToken()) {
|
||||
await authInstance.fetchProfile();
|
||||
}
|
||||
apiPost('save', { username: user, data: _cachedData });
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function mergeUserData(serverData) {
|
||||
function mergeUserData(serverData, localData = null) {
|
||||
const def = getDefaultData();
|
||||
if (!serverData) return def;
|
||||
if (!serverData && !localData) return def;
|
||||
const base = serverData || localData || def;
|
||||
const loc = localData || {};
|
||||
|
||||
// Combine unlocked plants uniquely
|
||||
const unlocked = Array.from(new Set([
|
||||
...def.unlockedPlants,
|
||||
...(loc.unlockedPlants || []),
|
||||
...(base.unlockedPlants || [])
|
||||
]));
|
||||
|
||||
// Combine selected deck
|
||||
const selDeck = (base.selectedDeck && base.selectedDeck.length > 0)
|
||||
? base.selectedDeck
|
||||
: ((loc.selectedDeck && loc.selectedDeck.length > 0) ? loc.selectedDeck : def.selectedDeck);
|
||||
|
||||
return {
|
||||
...def,
|
||||
...serverData,
|
||||
...loc,
|
||||
...base,
|
||||
coins: base.coins !== undefined ? base.coins : (loc.coins !== undefined ? loc.coins : def.coins),
|
||||
level: Math.max(base.level || 1, loc.level || 1),
|
||||
maxUnlockedLevel: Math.max(base.maxUnlockedLevel || 1, loc.maxUnlockedLevel || 1, base.level || 1, loc.level || 1),
|
||||
highScore: Math.max(base.highScore || 0, loc.highScore || 0),
|
||||
theme: base.theme || loc.theme || def.theme,
|
||||
stats: {
|
||||
...def.stats,
|
||||
...(serverData.stats || {})
|
||||
...(loc.stats || {}),
|
||||
...(base.stats || {})
|
||||
},
|
||||
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
|
||||
upgrades: {
|
||||
...def.upgrades,
|
||||
...(loc.upgrades || {}),
|
||||
...(base.upgrades || {}),
|
||||
unlockedPlants: unlocked,
|
||||
selectedDeck: selDeck
|
||||
},
|
||||
unlockedPlants: unlocked,
|
||||
selectedDeck: selDeck,
|
||||
materials: {
|
||||
...def.materials,
|
||||
...(loc.materials || {}),
|
||||
...(base.materials || {})
|
||||
},
|
||||
plantLevels: {
|
||||
...def.plantLevels,
|
||||
...(loc.plantLevels || {}),
|
||||
...(base.plantLevels || {})
|
||||
},
|
||||
perks: {
|
||||
...def.perks,
|
||||
...(loc.perks || {}),
|
||||
...(base.perks || {})
|
||||
},
|
||||
zenGarden: base.zenGarden
|
||||
? {
|
||||
...def.zenGarden,
|
||||
...serverData.zenGarden,
|
||||
pots: serverData.zenGarden.pots || def.zenGarden.pots,
|
||||
snail: { ...def.zenGarden.snail, ...(serverData.zenGarden.snail || {}) }
|
||||
...(loc.zenGarden || {}),
|
||||
...base.zenGarden,
|
||||
pots: base.zenGarden.pots || (loc.zenGarden && loc.zenGarden.pots) || def.zenGarden.pots,
|
||||
snail: { ...def.zenGarden.snail, ...((loc.zenGarden && loc.zenGarden.snail) || {}), ...(base.zenGarden.snail || {}) }
|
||||
}
|
||||
: def.zenGarden,
|
||||
treeOfWisdom: serverData.treeOfWisdom || def.treeOfWisdom,
|
||||
achievements: serverData.achievements || {}
|
||||
: (loc.zenGarden || def.zenGarden),
|
||||
treeOfWisdom: base.treeOfWisdom || loc.treeOfWisdom || def.treeOfWisdom,
|
||||
achievements: {
|
||||
...(loc.achievements || {}),
|
||||
...(base.achievements || {})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -228,9 +323,13 @@ window.PvZAuth = (function () {
|
||||
getDefaultData,
|
||||
validateUsername,
|
||||
validatePassword,
|
||||
apiGet,
|
||||
apiPost,
|
||||
scheduleSave,
|
||||
showNotification,
|
||||
onProfileChange,
|
||||
offProfileChange,
|
||||
notifyProfileChange,
|
||||
|
||||
/**
|
||||
* User Login
|
||||
@@ -263,7 +362,11 @@ window.PvZAuth = (function () {
|
||||
|
||||
if (window.GameEngine && window._pvzEngine) {
|
||||
window._pvzEngine.loadUserData();
|
||||
if (typeof window._pvzEngine.updateProfileUI === 'function') window._pvzEngine.updateProfileUI();
|
||||
if (typeof window._pvzEngine.updateRecordDisplay === 'function') window._pvzEngine.updateRecordDisplay();
|
||||
if (typeof window._pvzEngine.updateShopUI === 'function') window._pvzEngine.updateShopUI();
|
||||
}
|
||||
notifyProfileChange(_cachedData);
|
||||
if (typeof callback === 'function') callback(true, _cachedData, null);
|
||||
return { success: true, username: cleanUser, token: res.token, data: _cachedData };
|
||||
} else {
|
||||
@@ -305,7 +408,11 @@ window.PvZAuth = (function () {
|
||||
|
||||
if (window.GameEngine && window._pvzEngine) {
|
||||
window._pvzEngine.loadUserData();
|
||||
if (typeof window._pvzEngine.updateProfileUI === 'function') window._pvzEngine.updateProfileUI();
|
||||
if (typeof window._pvzEngine.updateRecordDisplay === 'function') window._pvzEngine.updateRecordDisplay();
|
||||
if (typeof window._pvzEngine.updateShopUI === 'function') window._pvzEngine.updateShopUI();
|
||||
}
|
||||
notifyProfileChange(_cachedData);
|
||||
if (typeof callback === 'function') callback(true, _cachedData, null);
|
||||
return { success: true, username: cleanUser, token: res.token, data: _cachedData };
|
||||
} else {
|
||||
@@ -318,6 +425,7 @@ window.PvZAuth = (function () {
|
||||
loginAsGuest() {
|
||||
clearToken();
|
||||
localStorage.setItem(CURRENT_USER_KEY, 'Гость');
|
||||
notifyProfileChange(this.getUserData());
|
||||
return { success: true, user: 'Гость' };
|
||||
},
|
||||
|
||||
@@ -334,16 +442,118 @@ window.PvZAuth = (function () {
|
||||
this.loginAsGuest();
|
||||
},
|
||||
|
||||
/**
|
||||
* Strictly Server-Authoritative Profile Sync:
|
||||
* When user is logged in, fetches /api/auth/profile (with fallback to /api/users)
|
||||
* and applies it directly to the game state.
|
||||
*/
|
||||
async fetchProfile() {
|
||||
const token = getToken();
|
||||
const user = this.getCurrentUser();
|
||||
if (!token || user === 'Гость') return null;
|
||||
|
||||
try {
|
||||
let profileData = null;
|
||||
|
||||
// 1. Try dedicated profile endpoint
|
||||
const res = await apiGet('profile');
|
||||
if (res && res.success && res.data) {
|
||||
profileData = res.data;
|
||||
} else if (res && res.status === 401) {
|
||||
clearToken();
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. Fallback to /api/users which is universally supported across backend versions
|
||||
if (!profileData) {
|
||||
try {
|
||||
const usersResp = await fetch('/api/users');
|
||||
if (usersResp.ok) {
|
||||
const usersJson = await usersResp.json();
|
||||
if (usersJson && usersJson.success && Array.isArray(usersJson.users)) {
|
||||
const matched = usersJson.users.find(u => u.username && u.username.toLowerCase() === user.toLowerCase());
|
||||
if (matched) {
|
||||
profileData = matched;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (profileData) {
|
||||
_hasSyncedFromServer = true;
|
||||
_cachedData = mergeUserData(profileData);
|
||||
localSave(_cachedData);
|
||||
|
||||
if (window._pvzEngine) {
|
||||
window._pvzEngine.loadUserData();
|
||||
if (typeof window._pvzEngine.updateProfileUI === 'function') window._pvzEngine.updateProfileUI();
|
||||
if (typeof window._pvzEngine.updateRecordDisplay === 'function') window._pvzEngine.updateRecordDisplay();
|
||||
if (typeof window._pvzEngine.updateShopUI === 'function') window._pvzEngine.updateShopUI();
|
||||
}
|
||||
notifyProfileChange(_cachedData);
|
||||
return _cachedData;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Profile fetch from server error:', e);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
async syncFromServer() {
|
||||
return this.fetchProfile();
|
||||
},
|
||||
|
||||
/**
|
||||
* Active Device Sessions Management
|
||||
*/
|
||||
async fetchSessions() {
|
||||
const token = getToken();
|
||||
if (!token || this.isGuest()) return [];
|
||||
try {
|
||||
const res = await apiGet('sessions');
|
||||
if (res && res.success && Array.isArray(res.sessions)) {
|
||||
return res.sessions.map(s => ({
|
||||
token: s.token || '',
|
||||
deviceName: s.deviceName || s.device_name || 'Браузер / Компьютер',
|
||||
ipAddress: s.ipAddress || s.ip_address || '127.0.0.1',
|
||||
createdAt: s.createdAt || s.created_at || '',
|
||||
lastActive: s.lastActive || s.last_active || s.createdAt || '',
|
||||
isCurrent: Boolean(s.isCurrent || s.is_current || (token && s.token === token))
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fetch sessions error:', e);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
async terminateSession(sessionToken) {
|
||||
if (!sessionToken) return { success: false, message: 'Не указан токен сессии' };
|
||||
const res = await apiPost('sessions/terminate', { sessionToken, token: sessionToken });
|
||||
return res || { success: false, message: 'Ошибка сети' };
|
||||
},
|
||||
|
||||
async terminateOtherSessions() {
|
||||
const res = await apiPost('sessions/terminate', { allOthers: true, all_others: true });
|
||||
return res || { success: false, message: 'Ошибка сети' };
|
||||
},
|
||||
|
||||
getUserData() {
|
||||
if (_cachedData) return _cachedData;
|
||||
|
||||
const local = localLoad();
|
||||
if (local) {
|
||||
_cachedData = mergeUserData(local);
|
||||
return _cachedData;
|
||||
} else {
|
||||
_cachedData = getDefaultData();
|
||||
}
|
||||
|
||||
// Background refresh from server if logged in
|
||||
if (!this.isGuest() && getToken()) {
|
||||
this.fetchProfile();
|
||||
}
|
||||
|
||||
_cachedData = getDefaultData();
|
||||
return _cachedData;
|
||||
},
|
||||
|
||||
@@ -362,6 +572,7 @@ window.PvZAuth = (function () {
|
||||
/** Force immediate server save (call on game-over / victory / visibility hidden / unload) */
|
||||
flushSave(keepalive = false) {
|
||||
if (this.isGuest() || !_cachedData) return;
|
||||
if (!_hasSyncedFromServer && getToken()) return; // Don't flush stale unsynced local cache over server
|
||||
if (_saveTimer) { clearTimeout(_saveTimer); _saveTimer = null; }
|
||||
apiPost('save', { username: this.getCurrentUser(), data: _cachedData }, keepalive);
|
||||
},
|
||||
@@ -527,6 +738,13 @@ window.PvZAuth = (function () {
|
||||
}
|
||||
authInstance.flushSave(true);
|
||||
});
|
||||
|
||||
// Auto-sync user data on startup if logged in
|
||||
setTimeout(() => {
|
||||
if (!authInstance.isGuest() && authInstance.getToken()) {
|
||||
authInstance.syncFromServer();
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
return authInstance;
|
||||
|
||||
Reference in New Issue
Block a user