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

+29 -12
View File
@@ -120,14 +120,11 @@ def expr(node: ast.AST) -> Expr:
elif isinstance(v, ast.FormattedValue):
parts.append(expr(v.value))
return {"k": "fmt", "v": parts}
case (ast.ListComp(elt=elt, generators=[ast.comprehension(target=t, iter=it, ifs=ifs,
is_async=0)])
| ast.GeneratorExp(elt=elt, generators=[ast.comprehension(target=t, iter=it,
ifs=ifs, is_async=0)])
| ast.SetComp(elt=elt, generators=[ast.comprehension(target=t, iter=it, ifs=ifs,
is_async=0)])):
return {"k": "comp", "e": expr(elt), "t": target(t), "it": expr(it),
"if": [expr(i) for i in ifs]}
case ast.ListComp(elt=elt, generators=gens) | ast.GeneratorExp(elt=elt, generators=gens) \
| ast.SetComp(elt=elt, generators=gens):
return {"k": "comp", "e": expr(elt), "gens": generators(gens)}
case ast.DictComp(key=key, value=value, generators=gens):
return {"k": "comp", "e": expr(key), "v": expr(value), "gens": generators(gens)}
case ast.Lambda(args=args, body=body):
return {"k": "lambda", "params": params(args), "body": expr(body)}
raise Unsupported(f"выражение {type(node).__name__}")
@@ -170,10 +167,18 @@ def stmt(node: ast.stmt) -> list[Stmt]:
return [{"op": "eval", "e": expr(value)}]
case ast.If(test=test, body=body, orelse=orelse):
return [{"op": "if", "c": expr(test), "then": stmts(body), "else": stmts(orelse)}]
case ast.While(test=test, body=body, orelse=[]):
return [{"op": "while", "c": expr(test), "body": stmts(body)}]
case ast.For(target=t, iter=it, body=body, orelse=[]):
return [{"op": "for", "t": target(t), "it": expr(it), "body": stmts(body)}]
case ast.While(test=test, body=body, orelse=orelse):
return [{"op": "while", "c": expr(test), "body": stmts(body), "else": stmts(orelse)}]
case ast.For(target=t, iter=it, body=body, orelse=orelse):
return [{"op": "for", "t": target(t), "it": expr(it), "body": stmts(body),
"else": stmts(orelse)}]
case ast.Try(body=body, handlers=handlers, orelse=orelse, finalbody=final):
return [{"op": "try", "body": stmts(body), "handlers": [handler(h) for h in handlers],
"else": stmts(orelse), "final": stmts(final)}]
case ast.Raise(exc=exc):
return [{"op": "raise", "v": None if exc is None else expr(exc)}] # `from cause` is dropped
case ast.Assert(test=test, msg=msg):
return [{"op": "assert", "c": expr(test), "m": None if msg is None else expr(msg)}]
case ast.Delete(targets=targets):
return [{"op": "del", "t": target(t)} for t in targets]
case ast.Pass():
@@ -248,3 +253,15 @@ def class_body(name: str, body: list[ast.stmt]) -> list[Stmt]:
and n.name not in SUPPORTED_DUNDERS):
raise Unsupported(f"специальный метод {name}.{n.name}()")
return stmts(body)
def generators(gens: list[ast.comprehension]) -> list[dict[str, Any]]:
if any(g.is_async for g in gens):
raise Unsupported("async comprehension")
return [{"t": target(g.target), "it": expr(g.iter), "if": [expr(i) for i in g.ifs]} for g in gens]
def handler(node: ast.ExceptHandler) -> dict[str, Any]:
"""``types`` is None for a bare ``except:``; ``name`` binds the exception."""
return {"types": None if node.type is None else expr(node.type), "name": node.name,
"body": stmts(node.body)}