feat(studio): прогресс лидербордов и достижений сохраняется в БД (между сессиями)
GameRuntime.saveProgress/loadProgress — helper к storys savegame endpoint (/kubikon3d/savegame/<pid>/<uid>/<ns>) для движковых менеджеров. - Достижения: при unlock сохраняются в БД (namespace _achievements) + localStorage как быстрый кэш; loadFromDB при Play восстанавливает разблокированные. - Лидерстаты: статы текущего игрока сохраняются в БД (namespace _leaderstats, дебаунс 1с при set); loadFromDB при Play восстанавливает значения. - Загрузка из БД через 250мс после старта скриптов (даём define зарегистрировать). Теперь прогресс игрока подгружается при каждой сессии с любого устройства. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c9498b086e
commit
33cd435d06
@ -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) {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
// === Оружие ===
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user