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
@@ -6,6 +6,7 @@ from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from app.converter import pyexpr
|
||||
from app.converter.atl import AtlParser, initial_props, static_props
|
||||
from app.converter.pyexpr import Expr, Stmt, Unsupported, const
|
||||
from app.converter.rpy_helpers import (
|
||||
_CALL_LINE,
|
||||
@@ -67,6 +68,7 @@ class ScriptData:
|
||||
characters: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
images: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
transforms: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
atl: dict[str, dict[str, Any]] = field(default_factory=dict) # animated transforms by name
|
||||
transitions: dict[str, dict[str, Any] | None] = field(default_factory=dict)
|
||||
audio: dict[str, str] = field(default_factory=dict)
|
||||
defaults: list[list[Any]] = field(default_factory=list)
|
||||
@@ -776,13 +778,24 @@ class RpyParser:
|
||||
c = self.cursor(line)
|
||||
c.keyword("transform")
|
||||
name = c.word()
|
||||
props: dict[str, Any] = {}
|
||||
for child in line.block:
|
||||
props.update(self.static_atl(child))
|
||||
if name:
|
||||
props, animation = self.atl_spec(line.block)
|
||||
self.d.transforms[name] = props
|
||||
if animation:
|
||||
self.d.atl[name] = animation
|
||||
return []
|
||||
|
||||
def atl_spec(self, lines: list[Line]) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
||||
"""Parse an ATL block: (properties to start with, timeline or None when it is static)."""
|
||||
parser = AtlParser(lambda message, line: self.d.warn(message, self.where(line)),
|
||||
lambda name: self.d.transforms.get(name) if name not in self.d.atl
|
||||
and name in self.d.transforms else None)
|
||||
block = parser.block(lines)
|
||||
static = static_props(block)
|
||||
if static is not None:
|
||||
return static, None
|
||||
return initial_props(block), block
|
||||
|
||||
def static_atl(self, line: Line) -> dict[str, Any]:
|
||||
props = _atl_props(line.text)
|
||||
first = Cursor(line.text).peek_word()
|
||||
@@ -828,10 +841,10 @@ class RpyParser:
|
||||
elif c.keyword("with"):
|
||||
node["with"] = self.transition(c.until(set(), ":"), line)
|
||||
elif c.char(":"):
|
||||
props: dict[str, Any] = {}
|
||||
for child in line.block:
|
||||
props.update(self.static_atl(child))
|
||||
if props:
|
||||
props, animation = self.atl_spec(line.block)
|
||||
if animation:
|
||||
node["at"].append({"props": props, "atl": animation})
|
||||
elif props:
|
||||
node["at"].append({"props": props})
|
||||
else:
|
||||
self.d.warn("Не разобран фрагмент show/scene", f"{self.where(line)}: {c.rest()}")
|
||||
@@ -842,24 +855,50 @@ class RpyParser:
|
||||
if source in POSITIONS:
|
||||
return {"pos": source}
|
||||
if source in self.d.transforms:
|
||||
return {"props": self.d.transforms[source], "name": source}
|
||||
spec = {"props": self.d.transforms[source], "name": source}
|
||||
if source in self.d.atl:
|
||||
spec["atl"] = source
|
||||
return spec
|
||||
e = self.expr(source, line)
|
||||
if e and e["k"] == "call":
|
||||
fname = _dotted_name(e["f"])
|
||||
if fname in ("Position", "Transform"):
|
||||
return {"props": _static_props(e["kw"])}
|
||||
if fname == "Move":
|
||||
moved = self.move_transform(e)
|
||||
if moved:
|
||||
return moved
|
||||
if fname in ("Shake", "SnoopyShake"):
|
||||
args = [_const(a) for a in e["a"]]
|
||||
return {"effect": "shake", "d": args[1] if len(args) > 1 else 0.5,
|
||||
"dist": _const(e["kw"].get("dist")) or 20}
|
||||
if fname in self.d.transforms:
|
||||
return {"props": self.d.transforms[fname], "name": fname}
|
||||
spec = {"props": self.d.transforms[fname], "name": fname}
|
||||
if fname in self.d.atl:
|
||||
spec["atl"] = fname
|
||||
return spec
|
||||
if e and e["k"] == "name" and e["id"] not in self.d.transforms:
|
||||
# transform defined later in another file; the player resolves it by name
|
||||
return {"name": e["id"]}
|
||||
self.d.warn("Неизвестная трансформация", f"{self.where(line)}: {source}")
|
||||
return {"name": source}
|
||||
|
||||
def move_transform(self, call: Expr) -> dict[str, Any] | None:
|
||||
"""``Move((x0, y0), (x1, y1), seconds)``: slide the picture between two positions."""
|
||||
args = [_const(a) for a in call["a"]]
|
||||
kw = {k: _const(v) for k, v in call["kw"].items()}
|
||||
start, end, seconds = (args + [None] * 3)[:3]
|
||||
start, end = start or kw.get("pos1"), end or kw.get("pos2")
|
||||
seconds = seconds if seconds is not None else kw.get("delay")
|
||||
if not (isinstance(start, (list, tuple)) and isinstance(end, (list, tuple))
|
||||
and len(start) == len(end) == 2 and isinstance(seconds, (int, float))):
|
||||
return None
|
||||
block = {"steps": [
|
||||
{"op": "set", "props": {"xpos": start[0], "ypos": start[1]}},
|
||||
{"op": "move", "warp": "linear", "t": float(seconds), "props": {"xpos": end[0], "ypos": end[1]}},
|
||||
], "repeat": None}
|
||||
return {"props": {"xpos": start[0], "ypos": start[1]}, "atl": block}
|
||||
|
||||
def st_scene(self, line: Line) -> list[Node]:
|
||||
c = self.cursor(line)
|
||||
c.keyword("scene")
|
||||
|
||||
Reference in new issue
Block a user