feat: add ATL animation timelines and try/raise to the Ren'Py player
Python sandbox - try/except/else/finally, raise (incl. re-raise), assert, for/while ... else - Multi-generator and dict comprehensions - Builtin exception hierarchy; user exceptions subclass it - Inside try, missing keys/indexes, division by zero, attribute access on None and unknown names raise Python exceptions; lookups stay lenient elsewhere - int()/float() raise ValueError on non-numeric strings ATL - converter/atl.py: set/move (linear, ease, easein, easeout), pause, repeat, parallel, choice, block, on show/hide and named transforms become a timeline shipped as game.json "atl"; Move() becomes a linear slide - player/atl.ts: timeline runner wired into the renderer; `on hide` plays before the picture is removed - function, time, contains, matrixcolor and expressions still block the import Tests: exceptions/ATL fixtures and runtime tests, converter ATL tests; README and ARCHITECTURE updated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
962595c925
commit
ff82d882c0
18 files changed
+1050
-53
No files matched your search
@@ -14,7 +14,7 @@ const ir = (name) => JSON.parse(readFileSync(new URL(`./fixtures/${name}.ir.json
|
||||
function runtime() {
|
||||
const store = Object.create(null);
|
||||
const builtins = {
|
||||
any: (xs) => xs.some(Boolean), max: Math.max, int: Number, str: pyStr, list: (xs = []) => [...xs],
|
||||
any: (xs) => xs.some(Boolean), range: (n) => Array.from({ length: n }, (_, i) => i), max: Math.max, int: Number, str: pyStr, list: (xs = []) => [...xs],
|
||||
};
|
||||
const scope = {
|
||||
get: (n) => (n in store ? store[n] : builtins[n]),
|
||||
@@ -82,3 +82,31 @@ test("unbounded recursion and bad calls fail with explicit errors", () => {
|
||||
assert.throws(() => py.eval({ k: "call", f: { k: "name", id: "one" },
|
||||
a: [{ k: "const", v: 1 }, { k: "const", v: 2 }], kw: {} }), /слишком много/);
|
||||
});
|
||||
|
||||
test("try/except/else/finally, raise, loop else, assert and multi-generator comprehensions", () => {
|
||||
const { py, store } = runtime();
|
||||
py.exec(ir("exceptions"));
|
||||
assert.deepEqual(store.log, ["zero", "fin"]);
|
||||
assert.equal(store.result_code, 7);
|
||||
assert.equal(store.result_key, "keyerror"); // dict[...] is strict inside try
|
||||
assert.equal(store.result_index, "indexerror");
|
||||
assert.equal(store.result_value, "valueerror"); // int("abc")
|
||||
assert.equal(store.result_generic, true);
|
||||
assert.equal(store.result_zero, "zerodiv");
|
||||
assert.equal(store.result_retry, 2); // return/continue pass through try
|
||||
assert.equal(store.result_forelse, "none");
|
||||
assert.equal(store.result_whileelse, 30);
|
||||
assert.equal(store.result_reraise, "inner");
|
||||
assert.deepEqual(store.result_pairs, [[1, 3], [1, 4], [2, 3], [2, 4]]);
|
||||
assert.deepEqual([...store.result_dictcomp.map.entries()], [[2, 20]]);
|
||||
assert.equal(store.result_assert, "boom");
|
||||
});
|
||||
|
||||
test("lookups stay lenient outside try, uncaught exceptions are explicit errors", () => {
|
||||
const { py, store } = runtime();
|
||||
py.exec([{ op: "set", t: { k: "name", id: "d" }, v: { k: "dict", v: [] } }]);
|
||||
assert.equal(py.eval({ k: "sub", o: { k: "name", id: "d" }, i: { k: "const", v: "missing" } }), null);
|
||||
assert.throws(() => py.exec([{ op: "raise", v: { k: "name", id: "ValueError" } }]), /ValueError/);
|
||||
assert.equal(py.strict, 0);
|
||||
void store;
|
||||
});
|
||||
Reference in new issue
Block a user