From 5da7ab1a90e4118a8b25cc38768ad7f34256d704 Mon Sep 17 00:00:00 2001 From: mrleo1nid <43583385+mrleo1nid@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:37:26 +0300 Subject: [PATCH] feat: converter keeps layers of screens and camera, perspective, matrix calls; That New Teacher runs its own text-tag libraries onlayer on show/hide/call screen and `camera ` reach the player, `perspective` is an ATL property now, RotateMatrix/OffsetMatrix/ScaleMatrix, Text.set_text and renpy.fetch are accepted. The text-effect modules of That New Teacher are no longer skipped (only the shader-based wave tag is). Co-Authored-By: Claude Sonnet 5.5 --- backend/app/converter/atl.py | 2 +- backend/app/converter/compatibility.py | 6 ++++-- backend/app/converter/rpy_helpers.py | 2 +- backend/app/converter/rpy_parser.py | 20 +++++++++++++++----- backend/app/games/renpy_builds.py | 9 ++++----- backend/tests/test_screens.py | 11 +++++++++++ 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/backend/app/converter/atl.py b/backend/app/converter/atl.py index d2b7774..a6fe2e3 100644 --- a/backend/app/converter/atl.py +++ b/backend/app/converter/atl.py @@ -43,7 +43,7 @@ LEADING_ZERO = re.compile(r"(? list[Node]: c = self.cursor(line) c.keyword("camera") - c.word() if not c.peek("at") and not c.peek(":") else None + layer = c.word() if not c.peek("at") and not c.peek(":") else None node: Node = {"op": "camera", "at": []} + if layer: + node["layer"] = layer if c.keyword("at"): for item in _split_top(c.until(set(), ":")): node["at"].append(self.transform_ref(item, line)) @@ -1466,8 +1468,10 @@ class RpyParser: def screen_call(self, c: Cursor, line: Line) -> Node | None: """``name(args)`` after `call/show screen`; trailing `nopredict` and `with` are ignored.""" source = re.split(r"\s+(?:nopredict|with)\b", c.rest(), maxsplit=1)[0].strip() - # `onlayer name` only picks the stacking layer; the player keeps every screen in one layer - source = re.sub(r"\s+onlayer\s+\w+\s*$", "", source) + layer_match = re.search(r"\s+onlayer\s+(\w+)\s*$", source) + layer = layer_match.group(1) if layer_match else None + if layer_match: + source = source[:layer_match.start()] call = self.expr(source, line) if source else None if call is None: self.d.warn("Не разобран вызов screen", self.where(line)) @@ -1480,7 +1484,10 @@ class RpyParser: self.d.warn("Не разобран вызов screen", self.where(line)) return None self.use_screen(name) - return {"name": name, "args": args, "kw": kw} + found: Node = {"name": name, "args": args, "kw": kw} + if layer: + found["layer"] = layer + return found def st_scene(self, line: Line) -> list[Node]: c = self.cursor(line) @@ -1521,7 +1528,10 @@ class RpyParser: name = c.word() if not name: return [] - return [{"op": "hide_screen", "name": name}] # hiding a screen that does not exist is harmless + hidden: Node = {"op": "hide_screen", "name": name} # hiding a screen that does not exist is harmless + if c.keyword("onlayer"): + hidden["layer"] = c.word() + return [hidden] spec = self.image_spec(c, line) node: Node = {"op": "hide", "tag": spec.get("tag", spec.get("name", [""])[0])} self.show_clauses(c, line, node) diff --git a/backend/app/games/renpy_builds.py b/backend/app/games/renpy_builds.py index 80bd046..8a7c6c0 100644 --- a/backend/app/games/renpy_builds.py +++ b/backend/app/games/renpy_builds.py @@ -94,11 +94,10 @@ BUILDS: dict[str, dict] = { "id": "renpy/that-new-teacher-0.9.0", # The Phone library's own statement parsers (ll.require, ll.keyword ...) run while Ren'Py reads scripts; the # converter reads `init phone register` itself (converter/phone_register.py), so the file is not needed - # the old-save fixer reads Ren'Py's pickled saves (the browser keeps its own), and the text-effect modules draw - # letters through the render API: the player has its own {bt} {sc} {fi} {rotat} {glitch} {wave} (text.ts), - # and explode/gradient/ruby text is shown plain - "skip": ["01statements.rpy", "versioning/save_fixer.rpy", "explosion_text.rpy", "glitch_tag.rpy", - "gradient_tags.rpy", "kinetic_text_tags.rpy", "ruby.rpy", "wavey_tag.rpy", + # the old-save fixer reads Ren'Py's pickled saves (the browser keeps its own) + "skip": ["01statements.rpy", "versioning/save_fixer.rpy", + # the wave tag is a shader on every letter (the player has no shaders yet) + "wavey_tag.rpy", # a shader blur patched into Ren's Transform; the player's own blur property needs no patch "blur.rpy"], # group_chats.rpy calls `charcter(char)`, a misspelling of its own `character`, in one branch diff --git a/backend/tests/test_screens.py b/backend/tests/test_screens.py index 38e029a..3a00e07 100644 --- a/backend/tests/test_screens.py +++ b/backend/tests/test_screens.py @@ -259,3 +259,14 @@ def test_input_widget_values_and_focus_actions_are_accepted(): body = data.screens["s"]["body"] assert [n.get("w") for n in body[:3]] == ["input", "input", "textbutton"] assert "default" in body[1]["props"] and "value" in body[0]["props"] + + +def test_layers_of_screens_and_camera_are_kept(): + data = parse('screen s:\n text "x"\n' + 'label start:\n show screen s onlayer hud\n call screen s onlayer overlay\n' + ' hide screen s onlayer hud\n camera hud:\n perspective True\n zpos 5\n' + ' camera:\n zoom 2\n') + body = data.modules["script.rpy"][0]["body"] + assert [(n["op"], n.get("layer")) for n in body] == [ + ("show_screen", "hud"), ("call_screen", "overlay"), ("hide_screen", "hud"), ("camera", "hud"), ("camera", None)] + assert body[3]["at"][-1]["props"] == {"perspective": True, "zpos": 5}