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

+1
View File
@@ -0,0 +1 @@
[{"op":"class","name":"AppError","bases":[{"k":"name","id":"Exception"}],"body":[{"op":"def","name":"__init__","params":[{"n":"self","d":null,"kind":"p"},{"n":"msg","d":null,"kind":"p"},{"n":"code","d":{"k":"const","v":0},"kind":"p"}],"body":[{"op":"eval","e":{"k":"call","f":{"k":"attr","o":{"k":"call","f":{"k":"name","id":"super"},"a":[],"kw":{}},"a":"__init__"},"a":[{"k":"name","id":"msg"}],"kw":{}}},{"op":"set","t":{"k":"attr","o":{"k":"name","id":"self"},"a":"code"},"v":{"k":"name","id":"code"}}],"decos":[]}]},{"op":"def","name":"risky","params":[{"n":"n","d":null,"kind":"p"}],"body":[{"op":"if","c":{"k":"cmp","l":{"k":"name","id":"n"},"ops":["=="],"r":[{"k":"const","v":0}]},"then":[{"op":"raise","v":{"k":"call","f":{"k":"name","id":"AppError"},"a":[{"k":"const","v":"zero"}],"kw":{"code":{"k":"const","v":7}}}}],"else":[]},{"op":"return","v":{"k":"bin","op":"//","l":{"k":"const","v":10},"r":{"k":"name","id":"n"}}}],"decos":[]},{"op":"set","t":{"k":"name","id":"log"},"v":{"k":"list","v":[]}},{"op":"try","body":[{"op":"eval","e":{"k":"call","f":{"k":"name","id":"risky"},"a":[{"k":"const","v":0}],"kw":{}}}],"handlers":[{"types":{"k":"name","id":"AppError"},"name":"e","body":[{"op":"eval","e":{"k":"call","f":{"k":"attr","o":{"k":"name","id":"log"},"a":"append"},"a":[{"k":"call","f":{"k":"name","id":"str"},"a":[{"k":"name","id":"e"}],"kw":{}}],"kw":{}}},{"op":"set","t":{"k":"name","id":"result_code"},"v":{"k":"attr","o":{"k":"name","id":"e"},"a":"code"}}]}],"else":[{"op":"eval","e":{"k":"call","f":{"k":"attr","o":{"k":"name","id":"log"},"a":"append"},"a":[{"k":"const","v":"no"}],"kw":{}}}],"final":[{"op":"eval","e":{"k":"call","f":{"k":"attr","o":{"k":"name","id":"log"},"a":"append"},"a":[{"k":"const","v":"fin"}],"kw":{}}}]},{"op":"try","body":[{"op":"set","t":{"k":"name","id":"d"},"v":{"k":"dict","v":[[{"k":"const","v":"a"},{"k":"const","v":1}]]}},{"op":"set","t":{"k":"name","id":"v"},"v":{"k":"sub","o":{"k":"name","id":"d"},"i":{"k":"const","v":"b"}}}],"handlers":[{"types":{"k":"name","id":"KeyError"},"name":null,"body":[{"op":"set","t":{"k":"name","id":"result_key"},"v":{"k":"const","v":"keyerror"}}]}],"else":[],"final":[]},{"op":"try","body":[{"op":"set","t":{"k":"name","id":"xs"},"v":{"k":"list","v":[{"k":"const","v":1}]}},{"op":"eval","e":{"k":"sub","o":{"k":"name","id":"xs"},"i":{"k":"const","v":5}}}],"handlers":[{"types":{"k":"tuple","v":[{"k":"name","id":"IndexError"},{"k":"name","id":"ValueError"}]},"name":null,"body":[{"op":"set","t":{"k":"name","id":"result_index"},"v":{"k":"const","v":"indexerror"}}]}],"else":[],"final":[]},{"op":"try","body":[{"op":"eval","e":{"k":"call","f":{"k":"name","id":"int"},"a":[{"k":"const","v":"abc"}],"kw":{}}}],"handlers":[{"types":{"k":"name","id":"ValueError"},"name":null,"body":[{"op":"set","t":{"k":"name","id":"result_value"},"v":{"k":"const","v":"valueerror"}}]}],"else":[],"final":[]},{"op":"try","body":[{"op":"eval","e":{"k":"call","f":{"k":"name","id":"risky"},"a":[{"k":"const","v":0}],"kw":{}}}],"handlers":[{"types":{"k":"name","id":"Exception"},"name":null,"body":[{"op":"set","t":{"k":"name","id":"result_generic"},"v":{"k":"const","v":true}}]}],"else":[],"final":[]},{"op":"try","body":[{"op":"set","t":{"k":"name","id":"result_zero"},"v":{"k":"bin","op":"/","l":{"k":"const","v":1},"r":{"k":"const","v":0}}}],"handlers":[{"types":{"k":"name","id":"ZeroDivisionError"},"name":null,"body":[{"op":"set","t":{"k":"name","id":"result_zero"},"v":{"k":"const","v":"zerodiv"}}]}],"else":[],"final":[]},{"op":"def","name":"retry","params":[],"body":[{"op":"for","t":{"k":"name","id":"i"},"it":{"k":"call","f":{"k":"name","id":"range"},"a":[{"k":"const","v":3}],"kw":{}},"body":[{"op":"try","body":[{"op":"if","c":{"k":"cmp","l":{"k":"name","id":"i"},"ops":["<"],"r":[{"k":"const","v":2}]},"then":[{"op":"raise","v":{"k":"call","f":{"k":"name","id":"ValueError"},"a":[{"k":"const","v":"again"}],"kw":{}}}],"else":[]},{"op":"return","v":{"k":"name","id":"i"}}],"handlers":[{"types":{"k":"name","id":"ValueError"},"name":null,"body":[{"op":"continue"}]}],"else":[],"final":[]}],"else":[]},{"op":"return","v":{"k":"unary","op":"-","v":{"k":"const","v":1}}}],"decos":[]},{"op":"set","t":{"k":"name","id":"result_retry"},"v":{"k":"call","f":{"k":"name","id":"retry"},"a":[],"kw":{}}},{"op":"set","t":{"k":"name","id":"found"},"v":{"k":"const","v":null}},{"op":"for","t":{"k":"name","id":"n"},"it":{"k":"list","v":[{"k":"const","v":1},{"k":"const","v":3},{"k":"const","v":5}]},"body":[{"op":"if","c":{"k":"cmp","l":{"k":"bin","op":"%","l":{"k":"name","id":"n"},"r":{"k":"const","v":2}},"ops":["=="],"r":[{"k":"const","v":0}]},"then":[{"op":"set","t":{"k":"name","id":"found"},"v":{"k":"name","id":"n"}},{"op":"break"}],"else":[]}],"else":[{"op":"set","t":{"k":"name","id":"found"},"v":{"k":"const","v":"none"}}]},{"op":"set","t":{"k":"name","id":"result_forelse"},"v":{"k":"name","id":"found"}},{"op":"set","t":{"k":"name","id":"k"},"v":{"k":"const","v":0}},{"op":"while","c":{"k":"cmp","l":{"k":"name","id":"Line truncated
+99
View File
@@ -0,0 +1,99 @@
# Source of exceptions.ir.json. Regenerate from backend/ with:
# python -m tests.make_ir ../frontend/tests/fixtures/exceptions.py ../frontend/tests/fixtures/exceptions.ir.json
class AppError(Exception):
def __init__(self, msg, code=0):
super().__init__(msg)
self.code = code
def risky(n):
if n == 0:
raise AppError("zero", code=7)
return 10 // n
log = []
try:
risky(0)
except AppError as e:
log.append(str(e))
result_code = e.code
else:
log.append("no")
finally:
log.append("fin")
try:
d = {"a": 1}
v = d["b"]
except KeyError:
result_key = "keyerror"
try:
xs = [1]
xs[5]
except (IndexError, ValueError):
result_index = "indexerror"
try:
int("abc")
except ValueError:
result_value = "valueerror"
try:
risky(0)
except Exception:
result_generic = True
try:
result_zero = 1 / 0
except ZeroDivisionError:
result_zero = "zerodiv"
def retry():
for i in range(3):
try:
if i < 2:
raise ValueError("again")
return i
except ValueError:
continue
return -1
result_retry = retry()
found = None
for n in [1, 3, 5]:
if n % 2 == 0:
found = n
break
else:
found = "none"
result_forelse = found
k = 0
while k < 3:
k += 1
else:
k = k * 10
result_whileelse = k
try:
try:
raise RuntimeError("inner")
except RuntimeError:
raise
except RuntimeError as e3:
result_reraise = str(e3)
result_pairs = [(x, y) for x in [1, 2] for y in [3, 4]]
result_dictcomp = {a: b for a in [1, 2] for b in [a * 10] if b > 10}
try:
assert 1 == 2, "boom"
except AssertionError as e4:
result_assert = str(e4)