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: