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() {
|
_loadSaved() {
|
||||||
|
// Резервная локальная копия (мгновенно, до ответа БД).
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(this._projectKey);
|
const raw = localStorage.getItem(this._projectKey);
|
||||||
if (raw) for (const id of JSON.parse(raw)) this._unlocked.add(id);
|
if (raw) for (const id of JSON.parse(raw)) this._unlocked.add(id);
|
||||||
} catch (e) { /* ignore */ }
|
} 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() {
|
_persist() {
|
||||||
|
// 1) локально (быстрый кэш), 2) в БД (между сессиями, любое устройство).
|
||||||
try { localStorage.setItem(this._projectKey, JSON.stringify([...this._unlocked])); } catch (e) {}
|
try { localStorage.setItem(this._projectKey, JSON.stringify([...this._unlocked])); } catch (e) {}
|
||||||
|
try { this.s?.gameRuntime?.saveProgress?.('_achievements', [...this._unlocked]); } catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
unlock(id, _playerId) {
|
unlock(id, _playerId) {
|
||||||
|
|||||||
@ -6203,6 +6203,13 @@ export class BabylonScene {
|
|||||||
// Старт через requestAnimationFrame — даём Babylon собрать сцену
|
// Старт через requestAnimationFrame — даём Babylon собрать сцену
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (this._isPlaying) this.gameRuntime?.start(this._scripts || []);
|
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 || {}))
|
.then(j => this._saveReply(scriptId, reqId, j.namespaces || {}))
|
||||||
.catch(() => this._saveReply(scriptId, reqId, {}));
|
.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) {
|
_saveSet(payload) {
|
||||||
const url = this._saveBaseUrl(payload?.namespace);
|
const url = this._saveBaseUrl(payload?.namespace);
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|||||||
@ -108,6 +108,41 @@ export class LeaderstatsManager {
|
|||||||
for (const fn of this._onChange) {
|
for (const fn of this._onChange) {
|
||||||
try { fn(pid, name, nv, old); } catch (e) { /* ignore */ }
|
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) {
|
add(playerId, name, delta) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user