diff --git a/src/editor/engine/AchievementsManager.js b/src/editor/engine/AchievementsManager.js index 3944e9c..5f49b0b 100644 --- a/src/editor/engine/AchievementsManager.js +++ b/src/editor/engine/AchievementsManager.js @@ -49,13 +49,26 @@ export class AchievementsManager { } _loadSaved() { + // Резервная локальная копия (мгновенно, до ответа БД). try { const raw = localStorage.getItem(this._projectKey); if (raw) for (const id of JSON.parse(raw)) this._unlocked.add(id); } catch (e) { /* ignore */ } } + /** Загрузить разблокированные достижения из БД (по игроку). Вызывать при Play. */ + loadFromDB() { + const rt = this.s?.gameRuntime; + if (!rt || !rt.loadProgress) return; + rt.loadProgress('_achievements', (data) => { + if (Array.isArray(data)) { + for (const id of data) this._unlocked.add(id); + } + }); + } _persist() { + // 1) локально (быстрый кэш), 2) в БД (между сессиями, любое устройство). try { localStorage.setItem(this._projectKey, JSON.stringify([...this._unlocked])); } catch (e) {} + try { this.s?.gameRuntime?.saveProgress?.('_achievements', [...this._unlocked]); } catch (e) {} } unlock(id, _playerId) { diff --git a/src/editor/engine/BabylonScene.js b/src/editor/engine/BabylonScene.js index 6e46fc5..6ace30e 100644 --- a/src/editor/engine/BabylonScene.js +++ b/src/editor/engine/BabylonScene.js @@ -6203,6 +6203,13 @@ export class BabylonScene { // Старт через requestAnimationFrame — даём Babylon собрать сцену requestAnimationFrame(() => { if (this._isPlaying) this.gameRuntime?.start(this._scripts || []); + // Задача 20: подгрузить сохранённый прогресс игрока из БД ПОСЛЕ того, + // как скрипты вызвали define() (даём им 200мс на регистрацию статов). + setTimeout(() => { + if (!this._isPlaying) return; + try { this.achievements?.loadFromDB?.(); } catch (e) {} + try { this.leaderstats?.loadFromDB?.(); } catch (e) {} + }, 250); }); // === Оружие === diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js index 8499cab..869032c 100644 --- a/src/editor/engine/GameRuntime.js +++ b/src/editor/engine/GameRuntime.js @@ -4435,6 +4435,27 @@ export class GameRuntime { .then(j => this._saveReply(scriptId, reqId, j.namespaces || {})) .catch(() => this._saveReply(scriptId, reqId, {})); } + /** Публичный helper для движковых менеджеров (leaderstats/achievements): + * сохранить прогресс текущего игрока в БД (storys savegame). */ + saveProgress(namespace, data) { + const url = this._saveBaseUrl(namespace); + if (!url) return; + try { + fetch(url, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data }), + }).catch(() => {}); + } catch (e) { /* ignore */ } + } + /** Загрузить прогресс из БД (cb(data|null)). */ + loadProgress(namespace, cb) { + const url = this._saveBaseUrl(namespace); + if (!url) { cb && cb(null); return; } + fetch(url).then(r => r.json()) + .then(j => cb && cb(j.data ?? null)) + .catch(() => cb && cb(null)); + } + _saveSet(payload) { const url = this._saveBaseUrl(payload?.namespace); if (!url) return; diff --git a/src/editor/engine/LeaderstatsManager.js b/src/editor/engine/LeaderstatsManager.js index f3883a8..706bc78 100644 --- a/src/editor/engine/LeaderstatsManager.js +++ b/src/editor/engine/LeaderstatsManager.js @@ -108,6 +108,41 @@ export class LeaderstatsManager { for (const fn of this._onChange) { try { fn(pid, name, nv, old); } catch (e) { /* ignore */ } } + // Сохраняем статы текущего игрока в БД (дебаунс 1с) — между сессиями. + if (pid === this._resolveMe()) this._scheduleSave(); + } + + _scheduleSave() { + if (this._saveTimer) clearTimeout(this._saveTimer); + this._saveTimer = setTimeout(() => { + this._saveTimer = null; + try { + const me = this._resolveMe(); + const m = this._stats.get(me); + if (!m) return; + const obj = {}; for (const [k, v] of m) obj[k] = v; + this.s?.gameRuntime?.saveProgress?.('_leaderstats', obj); + } catch (e) { /* ignore */ } + }, 1000); + } + + /** Загрузить статы текущего игрока из БД (вызывать при Play, после define). */ + loadFromDB() { + const rt = this.s?.gameRuntime; + if (!rt || !rt.loadProgress) return; + rt.loadProgress('_leaderstats', (data) => { + if (data && typeof data === 'object') { + const me = this._resolveMe(); + for (const name of Object.keys(data)) { + // Применяем только к зарегистрированным статам, без повторного сейва. + if (this._defs.some(d => d.name === name)) { + this._ensure(me, name); + this._stats.get(me).set(name, Number(data[name]) || 0); + } + } + this._dirty = true; + } + }); } add(playerId, name, delta) {