@
feat(quantik-game): фаза 5 — авторинг игровых уровней в sim-builder + раздача Учитель собирает игровой уровень без кода: новая (аддитивная, сворачиваемая) панель в sim-builder задаёт блок goal (when/title/hint/hold/fail) + до 3 звёзд + game-мету (chapter/order/par_ms); выражения проверяются inline через SimExpr.compile (без eval). buildSpec/loadFromSim — round-trip без потерь (goal/game пишутся только при включённом слое; обычная sim не меняется). Кнопка «Играть» монтирует черновик в SimEngine-модалке (HUD цели из Ф0). QuantikLevels стал async: подмешивает custom_sims cat=game (свои+ published) в реестр (custom:<dbid>), offline-safe, строки без goal отбрасываются; deep-link /quantik?level=custom:<id> с серверной проверкой доступа (own|published → иначе 403/404), мимо геймплейного гейта unlockStars. Раздача классу — реюз share Ф6 (game-aware ссылка + durable pushNotif). Правки sim-builder строго аддитивны (параллельная сессия). npm test 259/8 baseline; quantik-authoring 6/6; lint:routes 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
@@ -961,19 +961,98 @@
|
||||
kinematics: { key: 'kinematics', title: 'Кинематика', subtitle: 'Полёт и гравитация', accent: '#22D3EE' },
|
||||
dynamics: { key: 'dynamics', title: 'Динамика', subtitle: 'Силы, пружины, орбиты', accent: '#A78BFA' },
|
||||
functions: { key: 'functions', title: 'Функции', subtitle: 'Едем по кривой y = f(x)', accent: '#67E8F9' },
|
||||
quantum: { key: 'quantum', title: 'Квантовые законы', subtitle: 'Суперпозиция · прицел · туннель', accent: '#C4B5FD' }
|
||||
quantum: { key: 'quantum', title: 'Квантовые законы', subtitle: 'Суперпозиция · прицел · туннель', accent: '#C4B5FD' },
|
||||
// Авторённые учителями уровни (custom_sims cat='game') без явной главы — сюда.
|
||||
custom: { key: 'custom', title: 'Уровни учителей', subtitle: 'Авторённые уровни сообщества', accent: '#F472B6' }
|
||||
};
|
||||
|
||||
function list() { return LEVELS.slice(); }
|
||||
/* ── Авторённые уровни (Фаза 5): custom_sims с cat='game' ───────────────────
|
||||
Реестр становится «асинхронным»: встроенные уровни доступны сразу (offline),
|
||||
а опубликованные/свои игровые спеки подмешиваются после ensureCustom().
|
||||
Запись уровня из строки custom_sims: id='custom:<dbid>', метаданные — из
|
||||
spec.game (chapter/order/par_ms/unlockStars), spec — как есть. */
|
||||
var CUSTOM = []; // смёрженные записи авторённых уровней
|
||||
var _customPromise = null; // кэш промиса загрузки (грузим один раз)
|
||||
|
||||
/* Строка из LS.customSimsList/Get -> запись реестра уровня (или null). */
|
||||
function customToLevel(row) {
|
||||
if (!row || !row.spec || typeof row.spec !== 'object') return null;
|
||||
var spec = row.spec;
|
||||
if (!spec.goal) return null; // не игровой уровень — пропускаем
|
||||
var gm = (spec.game && typeof spec.game === 'object') ? spec.game : {};
|
||||
var dbid = row.id;
|
||||
return {
|
||||
id: 'custom:' + dbid,
|
||||
dbid: dbid,
|
||||
title: row.title || (spec.meta && spec.meta.title) || 'Уровень',
|
||||
chapter: gm.chapter || 'custom',
|
||||
order: (typeof gm.order === 'number') ? gm.order : (1000 + Number(dbid)),
|
||||
unlockStars: (typeof gm.unlockStars === 'number') ? gm.unlockStars : 0,
|
||||
par_ms: (typeof gm.par_ms === 'number') ? gm.par_ms : undefined,
|
||||
subject: row.subject || (spec.goal && spec.goal.subject) || undefined,
|
||||
hint: (spec.goal && spec.goal.hint) || '',
|
||||
spec: spec,
|
||||
_custom: true
|
||||
};
|
||||
}
|
||||
|
||||
/* Загрузить опубликованные + свои игровые custom_sims и смёржить.
|
||||
Возвращает Promise (кэшируется). Тихо игнорирует ошибки/отсутствие LS. */
|
||||
function ensureCustom() {
|
||||
if (_customPromise) return _customPromise;
|
||||
var LS = global.LS;
|
||||
if (!LS || !LS.customSimsList || !LS.customSimGet) {
|
||||
_customPromise = Promise.resolve([]);
|
||||
return _customPromise;
|
||||
}
|
||||
_customPromise = LS.customSimsList().then(function (r) {
|
||||
var sims = (r && r.sims) || [];
|
||||
// только игровая категория (список не содержит spec — его берём отдельно)
|
||||
var games = sims.filter(function (s) { return s && s.cat === 'game'; });
|
||||
return Promise.all(games.map(function (s) {
|
||||
return LS.customSimGet(s.id).then(function (g) {
|
||||
return customToLevel(g && g.sim);
|
||||
}).catch(function () { return null; });
|
||||
}));
|
||||
}).then(function (records) {
|
||||
CUSTOM = records.filter(Boolean);
|
||||
return CUSTOM.slice();
|
||||
}).catch(function () { CUSTOM = []; return []; });
|
||||
return _customPromise;
|
||||
}
|
||||
|
||||
function list() { return LEVELS.concat(CUSTOM); }
|
||||
function get(id) {
|
||||
for (var i = 0; i < LEVELS.length; i++) if (LEVELS[i].id === id) return LEVELS[i];
|
||||
var all = list();
|
||||
for (var i = 0; i < all.length; i++) if (all[i].id === id) return all[i];
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Достать уровень по id асинхронно — для deep-link `custom:<dbid>`, когда он
|
||||
может ещё не быть в смёрженном списке (напр. свой draft). Резолвит через
|
||||
LS.customSimGet с проверкой доступа на сервере (own|published|admin). */
|
||||
function getAsync(id) {
|
||||
var found = get(id);
|
||||
if (found) return Promise.resolve(found);
|
||||
var m = /^custom:(\d+)$/.exec(String(id || ''));
|
||||
var LS = global.LS;
|
||||
if (!m || !LS || !LS.customSimGet) return Promise.resolve(null);
|
||||
return LS.customSimGet(m[1]).then(function (g) {
|
||||
var lvl = customToLevel(g && g.sim);
|
||||
if (lvl) {
|
||||
// подмешать в кэш, чтобы повторное открытие/«Дальше» нашло его синхронно
|
||||
if (!CUSTOM.some(function (c) { return c.id === lvl.id; })) CUSTOM.push(lvl);
|
||||
}
|
||||
return lvl;
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
function chapter(key) { return CHAPTERS[key] || { key: key, title: key, subtitle: '', accent: '#22D3EE' }; }
|
||||
|
||||
global.QuantikLevels = {
|
||||
list: list, get: get, chapter: chapter,
|
||||
LEVELS: LEVELS, CHAPTERS: CHAPTERS
|
||||
list: list, get: get, getAsync: getAsync, ensureCustom: ensureCustom, chapter: chapter,
|
||||
LEVELS: LEVELS, CHAPTERS: CHAPTERS,
|
||||
customToLevel: customToLevel
|
||||
};
|
||||
|
||||
})(typeof window !== 'undefined' ? window : this);
|
||||
|
||||
Reference in New Issue
Block a user