712086ddca
Показ анимации кубиков игрокам, закрытие оверлеев с окнами, ручной ввод количества, меньший радиус кисти, блок спонсоров в «О программе»; плюс черновик game systems / character sheet. Co-authored-by: Cursor <cursoragent@cursor.com>
172 lines
5.1 KiB
TypeScript
172 lines
5.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
|
|
import { evaluateFormula, buildFormulaContext } from './formula';
|
|
import { computeDerived, sortDerived, CycleError } from './derived';
|
|
import { validatePointBuy, validateArray, rollDice, sum, validateCreationRules } from './creation';
|
|
import { snapshotHash, canonicalJson } from './snapshot';
|
|
import { validateSystemDefinition } from './systemValidate';
|
|
import type { SystemDefinition } from './types';
|
|
import { FORMAT_VERSION, SYSTEM_FORMAT } from './types';
|
|
|
|
test('evaluateFormula: D&D ability modifier', () => {
|
|
const ctx = { 'attr.str': 16 };
|
|
assert.equal(evaluateFormula('floor((attr.str - 10) / 2)', ctx), 3);
|
|
assert.equal(evaluateFormula('(attr.str - 10) / 2', ctx, 'floor'), 3);
|
|
});
|
|
|
|
test('evaluateFormula: clamp min max', () => {
|
|
assert.equal(evaluateFormula('clamp(attr.str, 1, 20)', { 'attr.str': 25 }), 20);
|
|
});
|
|
|
|
test('computeDerived topo order', () => {
|
|
const derived = computeDerived(
|
|
[
|
|
{
|
|
key: 'strMod',
|
|
label: 'STR mod',
|
|
formula: 'floor((attr.str - 10) / 2)',
|
|
dependsOn: ['attr.str'],
|
|
round: 'none',
|
|
},
|
|
{
|
|
key: 'carry',
|
|
label: 'Carry',
|
|
formula: 'attr.str * 15',
|
|
dependsOn: ['attr.str'],
|
|
},
|
|
],
|
|
{ attributes: { str: 12 } },
|
|
);
|
|
assert.equal(derived.strMod, 1);
|
|
assert.equal(derived.carry, 180);
|
|
});
|
|
|
|
test('sortDerived detects cycles', () => {
|
|
assert.throws(
|
|
() =>
|
|
sortDerived([
|
|
{ key: 'a', label: 'a', formula: 'derived.b', dependsOn: ['derived.b'] },
|
|
{ key: 'b', label: 'b', formula: 'derived.a', dependsOn: ['derived.a'] },
|
|
]),
|
|
CycleError,
|
|
);
|
|
});
|
|
|
|
test('point buy budget', () => {
|
|
const rule = {
|
|
id: 'pb',
|
|
type: 'point_buy' as const,
|
|
appliesTo: 'attribute' as const,
|
|
params: {
|
|
budget: 27,
|
|
keys: ['str', 'dex', 'con', 'int', 'wis', 'cha'],
|
|
},
|
|
};
|
|
const ok = validatePointBuy(rule, { str: 15, dex: 14, con: 13, int: 12, wis: 10, cha: 8 });
|
|
assert.equal(ok.length, 0);
|
|
const bad = validatePointBuy(rule, { str: 15, dex: 15, con: 15, int: 15, wis: 15, cha: 15 });
|
|
assert.ok(bad.some((e) => e.code === 'point_buy_budget'));
|
|
});
|
|
|
|
test('array permutation', () => {
|
|
const rule = {
|
|
id: 'arr',
|
|
type: 'array' as const,
|
|
appliesTo: 'attribute' as const,
|
|
params: { array: [15, 14, 13, 12, 10, 8], keys: ['str', 'dex', 'con', 'int', 'wis', 'cha'] },
|
|
};
|
|
assert.equal(
|
|
validateArray(rule, { str: 15, dex: 14, con: 13, int: 12, wis: 10, cha: 8 }).length,
|
|
0,
|
|
);
|
|
assert.ok(validateArray(rule, { str: 15, dex: 15, con: 13, int: 12, wis: 10, cha: 8 }).length > 0);
|
|
});
|
|
|
|
test('rollDice 4d6dl', () => {
|
|
let i = 0;
|
|
const seq = [0.0, 0.9, 0.5, 0.2]; // -> 1,6,4,2 drop 1 => 6+4+2=12
|
|
const rolls = rollDice('4d6dl', () => seq[i++] ?? 0);
|
|
assert.equal(sum(rolls), 12);
|
|
});
|
|
|
|
test('snapshotHash stable', () => {
|
|
const system = minimalSystem();
|
|
const a = snapshotHash(system);
|
|
const b = snapshotHash({ ...system, name: 'Other display name' });
|
|
// name is not part of hash payload — hash equal
|
|
assert.equal(a, b);
|
|
const c = snapshotHash({ ...system, revision: 2 });
|
|
assert.notEqual(a, c);
|
|
});
|
|
|
|
test('canonicalJson sorts keys', () => {
|
|
assert.equal(canonicalJson({ b: 1, a: 2 }), '{"a":2,"b":1}');
|
|
});
|
|
|
|
test('validateSystemDefinition', () => {
|
|
const r = validateSystemDefinition(minimalSystem());
|
|
assert.equal(r.ok, true);
|
|
});
|
|
|
|
test('validateCreationRules integrates point_buy', () => {
|
|
const system = minimalSystem();
|
|
const result = validateCreationRules(
|
|
system,
|
|
{ attributes: { str: 15, dex: 14, con: 13, int: 12, wis: 10, cha: 8 } },
|
|
'point_buy',
|
|
);
|
|
assert.equal(result.ok, true);
|
|
});
|
|
|
|
test('buildFormulaContext', () => {
|
|
const ctx = buildFormulaContext({ attributes: { str: 10 }, resources: { hp: 8 } });
|
|
assert.equal(ctx['attr.str'], 10);
|
|
assert.equal(ctx['resource.hp'], 8);
|
|
});
|
|
|
|
function minimalSystem(): SystemDefinition {
|
|
return {
|
|
format: SYSTEM_FORMAT,
|
|
formatVersion: FORMAT_VERSION,
|
|
id: 'test',
|
|
name: 'Test',
|
|
schemaVersion: 1,
|
|
revision: 1,
|
|
bricks: {
|
|
attributes: [
|
|
{ key: 'str', label: 'STR', scale: 'numeric', min: 8, max: 15, default: 10 },
|
|
{ key: 'dex', label: 'DEX', scale: 'numeric', min: 8, max: 15, default: 10 },
|
|
{ key: 'con', label: 'CON', scale: 'numeric', min: 8, max: 15, default: 10 },
|
|
{ key: 'int', label: 'INT', scale: 'numeric', min: 8, max: 15, default: 10 },
|
|
{ key: 'wis', label: 'WIS', scale: 'numeric', min: 8, max: 15, default: 10 },
|
|
{ key: 'cha', label: 'CHA', scale: 'numeric', min: 8, max: 15, default: 10 },
|
|
],
|
|
derived: [
|
|
{
|
|
key: 'strMod',
|
|
label: 'STR Mod',
|
|
formula: 'floor((attr.str - 10) / 2)',
|
|
dependsOn: ['attr.str'],
|
|
},
|
|
],
|
|
},
|
|
creationRules: [
|
|
{
|
|
id: 'pb',
|
|
type: 'point_buy',
|
|
appliesTo: 'attribute',
|
|
params: {
|
|
budget: 27,
|
|
keys: ['str', 'dex', 'con', 'int', 'wis', 'cha'],
|
|
},
|
|
},
|
|
],
|
|
sheetLayout: {
|
|
pageSize: 'fluid',
|
|
grid: { size: 8 },
|
|
pages: [{ id: 'main', label: 'Main', widgets: [] }],
|
|
},
|
|
};
|
|
}
|