@
feat(quantik-game): фаза 0 — слой целей в движке (goal/HUD/result) Декларативный блок goal в спеке SimForge (булево SimExpr-условие победы), вычисляемый каждый кадр: фиксация результата (победа/время/попытки/звёзды), callback onGoal, HUD-оверлей (цель/звёзды/подсказка/баннер, inline SVG). API инстанса: onGoal/getResult/resetResult. Серверный validateSpec пропускает goal/game (длина выражений + escape текста, без исполнения). Аддитивно: спека без goal ведёт себя как раньше. Смоук 40/40; npm test 238 pass/8 baseline; lint:routes 0. План фичи (7 фаз) + CONTEXT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
@@ -235,6 +235,43 @@ function validateSpec(spec) {
|
||||
clean.physics = cph;
|
||||
}
|
||||
|
||||
// goal{} — слой цели/победы (Квантик, Фаза 0). Выражения НЕ исполняем (длина),
|
||||
// текст — sanitizeText (escape + обрезка), не более 3 звёзд, hold — число.
|
||||
if (spec.goal && typeof spec.goal === 'object' && !Array.isArray(spec.goal)) {
|
||||
const g = spec.goal;
|
||||
const cg = {};
|
||||
if (g.when !== undefined) { checkExpr(g.when, 'goal.when', errs); cg.when = g.when; }
|
||||
if (g.fail !== undefined) { checkExpr(g.fail, 'goal.fail', errs); cg.fail = g.fail; }
|
||||
if (g.title !== undefined) cg.title = sanitizeText(g.title, 120);
|
||||
if (g.hint !== undefined) cg.hint = sanitizeText(g.hint, 300);
|
||||
if (g.hold !== undefined) {
|
||||
if (typeof g.hold !== 'number') errs.push('goal.hold должно быть числом');
|
||||
else cg.hold = g.hold;
|
||||
}
|
||||
if (g.stars !== undefined) {
|
||||
if (!Array.isArray(g.stars)) {
|
||||
errs.push('goal.stars должно быть массивом');
|
||||
} else if (g.stars.length > 3) {
|
||||
return { ok: false, error: 'goal.stars > 3' };
|
||||
} else {
|
||||
cg.stars = g.stars.map((s, i) => {
|
||||
if (!s || typeof s !== 'object') { errs.push(`goal.stars[${i}]: не объект`); return {}; }
|
||||
const os = {};
|
||||
if (s.when !== undefined) { checkExpr(s.when, `goal.stars[${i}].when`, errs); os.when = s.when; }
|
||||
if (s.label !== undefined) os.label = sanitizeText(s.label, 120);
|
||||
return os;
|
||||
});
|
||||
}
|
||||
}
|
||||
clean.goal = cg;
|
||||
}
|
||||
|
||||
// game{} — зарезервированный блок мета-слоя (Фаза 1/5). Пропускаем как есть
|
||||
// (проверен общими лимитами: размер/глубина). Не исполняем.
|
||||
if (spec.game && typeof spec.game === 'object' && !Array.isArray(spec.game)) {
|
||||
clean.game = spec.game;
|
||||
}
|
||||
|
||||
if (errs.length) return { ok: false, error: errs.slice(0, 8).join('; ') };
|
||||
return { ok: true, clean };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user