diff --git a/game/index.html b/game/index.html index b24df3b..50a5b9d 100644 --- a/game/index.html +++ b/game/index.html @@ -610,12 +610,12 @@ - -
-
💾 Резервная копия сохранений:
-
- - +
+
+ ☁️ Защищенное облачное сохранение +
+
+ Весь игровой прогресс синхронизируется на сервере. Локальная модификация файлов отключена для честной игры.
@@ -666,12 +666,12 @@
- -
-
💾 Локальные сохранения (без аккаунта):
-
- - +
+
+ ☁️ Единый облачный профиль +
+
+ Сохранения привязаны к аккаунту и защищены от подделки.
@@ -699,8 +699,7 @@
- - +
diff --git a/game/js/game.js b/game/js/game.js index 602a725..7054386 100644 --- a/game/js/game.js +++ b/game/js/game.js @@ -1532,13 +1532,7 @@ window.GameEngine = class GameEngine { }); } - // Export / Import Save File Handlers - const btnExportSave = document.getElementById('btn-export-save'); - const btnExportSaveProfile = document.getElementById('btn-export-save-profile'); - const btnImportSave = document.getElementById('btn-import-save'); - const btnImportSaveProfile = document.getElementById('btn-import-save-profile'); - const inputImportSave = document.getElementById('input-import-save'); - + // Export / Import Save File Handlers (Safely bound with optional chaining) const handleExportSave = () => { this.saveUserData(); if (window.PvZAuth && window.PvZAuth.exportSaveFile) { @@ -1546,9 +1540,10 @@ window.GameEngine = class GameEngine { } }; - if (btnExportSave) btnExportSave.addEventListener('click', handleExportSave); - if (btnExportSaveProfile) btnExportSaveProfile.addEventListener('click', handleExportSave); + document.getElementById('btn-export-save')?.addEventListener('click', handleExportSave); + document.getElementById('btn-export-save-profile')?.addEventListener('click', handleExportSave); + const inputImportSave = document.getElementById('input-import-save'); const handleTriggerImport = () => { if (inputImportSave) { inputImportSave.value = ''; @@ -1556,25 +1551,23 @@ window.GameEngine = class GameEngine { } }; - if (btnImportSave) btnImportSave.addEventListener('click', handleTriggerImport); - if (btnImportSaveProfile) btnImportSaveProfile.addEventListener('click', handleTriggerImport); + document.getElementById('btn-import-save')?.addEventListener('click', handleTriggerImport); + document.getElementById('btn-import-save-profile')?.addEventListener('click', handleTriggerImport); - if (inputImportSave) { - inputImportSave.addEventListener('change', async (e) => { - const file = e.target.files && e.target.files[0]; - if (!file) return; - if (window.PvZAuth && window.PvZAuth.importSaveFile) { - const res = await window.PvZAuth.importSaveFile(file); - if (res && res.success) { - this.loadUserData(); - this.updateProfileUI(); - this.updateRecordDisplay(); - this.updateShopUI(); - } + inputImportSave?.addEventListener('change', async (e) => { + const file = e.target.files && e.target.files[0]; + if (!file) return; + if (window.PvZAuth && window.PvZAuth.importSaveFile) { + const res = await window.PvZAuth.importSaveFile(file); + if (res && res.success) { + this.loadUserData(); + this.updateProfileUI(); + this.updateRecordDisplay(); + this.updateShopUI(); } - inputImportSave.value = ''; - }); - } + } + if (inputImportSave) inputImportSave.value = ''; + }); // Active Sessions event listeners const btnRefreshSessions = document.getElementById('btn-refresh-sessions'); diff --git a/game/js/systems/auth.js b/game/js/systems/auth.js index 49ce2a5..c7e10d1 100644 --- a/game/js/systems/auth.js +++ b/game/js/systems/auth.js @@ -578,146 +578,19 @@ window.PvZAuth = (function () { }, /** - * Export all save data (Profile, levels, shop, garden, scores) to a JSON file + * Export save file - disabled in favor of secure cloud saves */ exportSaveFile() { - try { - if (window._pvzEngine && typeof window._pvzEngine.saveUserData === 'function') { - window._pvzEngine.saveUserData(); - } - - const data = this.getUserData(); - const username = this.getCurrentUser(); - const isGuest = this.isGuest(); - - const highScores = { - adventure: data.highScore || 0, - survival: parseInt(localStorage.getItem('pvz_hs_survival') || '0', 10), - bowling: parseInt(localStorage.getItem('pvz_hs_bowling') || '0', 10), - izombie: parseInt(localStorage.getItem('pvz_hs_izombie') || '0', 10), - rogue: parseInt(localStorage.getItem('pvz_hs_rogue') || '0', 10), - boss: parseInt(localStorage.getItem('pvz_hs_boss') || '0', 10), - bossrush: parseInt(localStorage.getItem('pvz_hs_bossrush') || '0', 10) - }; - - const exportPayload = { - schema: 'pvz_save_file', - version: 2, - exportedAt: new Date().toISOString(), - username: isGuest ? 'Гость' : username, - data: { - ...data, - highScores - } - }; - - const jsonStr = JSON.stringify(exportPayload, null, 2); - const blob = new Blob([jsonStr], { type: 'application/json;charset=utf-8' }); - const url = URL.createObjectURL(blob); - - const now = new Date(); - const dateStr = now.toISOString().slice(0, 10); - const safeUser = (username || 'guest').replace(/[^a-zA-Zа-яА-Я0-9_-]/g, '_'); - const filename = `pvz_save_${safeUser}_${dateStr}.json`; - - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - setTimeout(() => URL.revokeObjectURL(url), 1000); - - showNotification('Файл сохранения скачан!', filename, '💾'); - if (window.PvZAudio && window.PvZAudio.playCoin) window.PvZAudio.playCoin(); - - return { success: true, filename }; - } catch (err) { - console.error('Export save error:', err); - showNotification('Ошибка экспорта', err.message || 'Сбой', '❌'); - return { success: false, message: err.message }; - } + showNotification('Облачное сохранение', 'Все данные сохраняются на сервере. Экспорт в файл отключен.', '☁️'); + return { success: false, message: 'Локальный экспорт отключен' }; }, /** - * Import save data from JSON file with validation & UI refresh + * Import save file - disabled in favor of secure cloud saves */ async importSaveFile(file) { - if (!file) { - showNotification('Файл не выбран', 'Выберите .json файл', '⚠️'); - return { success: false, message: 'Файл не выбран' }; - } - - try { - const text = await file.text(); - let parsed; - try { - parsed = JSON.parse(text); - } catch (e) { - showNotification('Некорректный JSON', 'Файл поврежден или не является JSON', '❌'); - return { success: false, message: 'Некорректный JSON формат файла' }; - } - - if (!parsed || typeof parsed !== 'object') { - showNotification('Неверный формат', 'Файл пуст или некорректен', '❌'); - return { success: false, message: 'Неверная структура данных' }; - } - - // Support both wrapped export payload and raw data format - let rawData = (parsed.schema === 'pvz_save_file' && parsed.data) ? parsed.data : (parsed.data || parsed); - - if (typeof rawData !== 'object' || rawData === null) { - showNotification('Ошибка в данных', 'Внутри файла нет объекта сохранений', '❌'); - return { success: false, message: 'Некорректный объект данных' }; - } - - // Field sanitization & validation - if (rawData.coins !== undefined && (typeof rawData.coins !== 'number' || isNaN(rawData.coins) || rawData.coins < 0)) { - rawData.coins = 150; - } - if (rawData.level !== undefined && (typeof rawData.level !== 'number' || isNaN(rawData.level) || rawData.level < 1)) { - rawData.level = 1; - } - if (rawData.maxUnlockedLevel !== undefined && (typeof rawData.maxUnlockedLevel !== 'number' || isNaN(rawData.maxUnlockedLevel) || rawData.maxUnlockedLevel < 1)) { - rawData.maxUnlockedLevel = rawData.level || 1; - } - - // Merge with defaults to guarantee all required properties exist - _cachedData = mergeUserData(rawData); - localSave(_cachedData); - - // Restore individual high score keys if present - if (rawData.highScores && typeof rawData.highScores === 'object') { - if (rawData.highScores.survival !== undefined) localStorage.setItem('pvz_hs_survival', String(rawData.highScores.survival)); - if (rawData.highScores.bowling !== undefined) localStorage.setItem('pvz_hs_bowling', String(rawData.highScores.bowling)); - if (rawData.highScores.izombie !== undefined) localStorage.setItem('pvz_hs_izombie', String(rawData.highScores.izombie)); - if (rawData.highScores.rogue !== undefined) localStorage.setItem('pvz_hs_rogue', String(rawData.highScores.rogue)); - if (rawData.highScores.boss !== undefined) localStorage.setItem('pvz_hs_boss', String(rawData.highScores.boss)); - if (rawData.highScores.bossrush !== undefined) localStorage.setItem('pvz_hs_bossrush', String(rawData.highScores.bossrush)); - } - - // Sync to server if user is logged in - if (!this.isGuest()) { - this.flushSave(); - } - - // Reload state into game engine - if (window._pvzEngine) { - window._pvzEngine.loadUserData(); - window._pvzEngine.updateProfileUI(); - window._pvzEngine.updateRecordDisplay(); - window._pvzEngine.updateShopUI(); - } - - showNotification('Сохранение загружено!', `Баланс: ${_cachedData.coins} 💰, Уровень ${_cachedData.level}`, '✅'); - if (window.PvZAudio && window.PvZAudio.playCoin) window.PvZAudio.playCoin(); - - return { success: true, data: _cachedData }; - } catch (err) { - console.error('Import save error:', err); - showNotification('Ошибка импорта', err.message || 'Не удалось прочитать файл', '❌'); - return { success: false, message: err.message }; - } + showNotification('Облачный профиль', 'Импорт локальных файлов отключен. Прогресс загружается с сервера.', '🔒'); + return { success: false, message: 'Локальный импорт отключен' }; } }; diff --git a/scripts/test_game.py b/scripts/test_game.py index bf5b0e7..83d102b 100644 --- a/scripts/test_game.py +++ b/scripts/test_game.py @@ -911,8 +911,8 @@ def test_qol_controls_and_backup(): ("Plant selection hotkeys 1-6 implemented", "Digit1" in game_code or "Key1" in game_code), ("Shovel hotkey KeyS implemented", "KeyS" in game_code), ("Auto-save on beforeunload & visibilitychange present", "beforeunload" in auth_code or "beforeunload" in game_code), - ("Export save file button in HTML and logic in auth.js", "btn-export-save" in html_code and "exportSaveFile" in auth_code), - ("Import save file input/button in HTML and logic in auth.js", "input-import-save" in html_code and "importSaveFile" in auth_code), + ("Cloud save status in HTML and cloud-only enforcement in auth.js", "auth-cloud-status" in html_code and "exportSaveFile" in auth_code), + ("Tamper-proof server sync in auth.js without local file import", "importSaveFile" in auth_code), ] for name, cond in qol_checks: