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:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-25 12:11:07 +03:00
1 parent 962595c925
commit ff82d882c0
18 files changed
+1050 -53

No files matched your search

+38 -5
View File
@@ -85,10 +85,24 @@ def expression(value, depth=0):
for item in value["kw"].values():
expression(item, depth + 1)
elif kind == "comp":
for key in ("e", "t", "it"):
ex(key)
for item in seq("if"):
expression(item, depth + 1)
ex("e")
ex("v", True)
if "gens" in value:
gens = seq("gens")
if not gens or len(gens) > 8:
raise ValueError("Некорректный генератор")
for gen in gens:
if not isinstance(gen, dict) or not isinstance(gen.get("if"), list):
raise ValueError("Некорректный генератор")
expression(gen.get("t"), depth + 1)
expression(gen.get("it"), depth + 1)
for item in gen["if"]:
expression(item, depth + 1)
else: # saved by an older converter
for key in ("t", "it"):
ex(key)
for item in seq("if"):
expression(item, depth + 1)
else:
raise ValueError(f"Неизвестное выражение: {kind}")
@@ -113,7 +127,8 @@ def statements(code, depth=0):
keys = {"set": ("t", "v"), "aug": ("t", "v"), "eval": ("e",),
"if": ("c",), "while": ("c",), "for": ("t", "it"),
"del": ("t",), "break": (), "continue": (), "def": (), "class": (),
"return": (), "global": (), "nonlocal": ()}
"return": (), "global": (), "nonlocal": (), "try": (), "raise": (),
"assert": ("c",)}
if op not in keys:
raise ValueError(f"Неизвестный оператор AST: {op}")
for key in keys[op]:
@@ -123,6 +138,24 @@ def statements(code, depth=0):
statements(s.get("else"), depth + 1)
if op in ("while", "for"):
statements(s.get("body"), depth + 1)
statements(s.get("else", []), depth + 1)
if op == "try":
statements(s.get("body"), depth + 1)
statements(s.get("else", []), depth + 1)
statements(s.get("final", []), depth + 1)
handlers = s.get("handlers")
if not isinstance(handlers, list) or len(handlers) > 32:
raise ValueError("Некорректные обработчики исключений")
for h in handlers:
if not isinstance(h, dict) or not (h.get("name") is None or isinstance(h.get("name"), str)):
raise ValueError("Некорректный обработчик исключения")
if h.get("types") is not None:
expression(h["types"], depth + 1)
statements(h.get("body"), depth + 1)
if op == "raise" and s.get("v") is not None:
expression(s["v"], depth + 1)
if op == "assert" and s.get("m") is not None:
expression(s["m"], depth + 1)
if op == "def":
if not isinstance(s.get("name"), str):
raise ValueError("У функции нет имени")