feat: ATL durations in brackets, offset and GPU hints; screen for-index, glued strings and onlayer

ATL: `ease(1.0) zoom 1.3`, `ease (0.1)xpos 0.5`, `ease(0.1)rotate 5` and `pause(1.5)` take the duration
in brackets; `offset (x, y)` becomes xoffset/yoffset; lines starting with shader, mesh, u_*, blend or
transform_anchor are skipped as GPU hints. thelewdesthouse drops from about 1400 to 111 rejections.
Screens: `for x index k in seq`, `add"x"` without a space, and `onlayer name` after show/call screen
(the layer is ignored, every screen lives in one layer). Transforms named with a capital letter and
declared in another file are resolved by the prescan, and `__("Name") "text"` is a translatable speaker.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-26 04:02:22 +03:00
1 parent 25bd39dd43
commit 5c1e1aee01
4 files changed
+84 -5

No files matched your search

+9 -1
View File
@@ -152,6 +152,9 @@ class ScriptData:
self.warning_examples[kind] = where
TRANSLATED_SPEAKER = re.compile(r"""^_{1,2}\(\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\)(?=\s+["'])""")
class RpyParser:
def __init__(self, data: ScriptData, filename: str):
self.d = data
@@ -1274,7 +1277,7 @@ class RpyParser:
spec["atl"] = fname
spec["args"], spec["kw"] = e["a"], e["kw"]
return spec
if "." not in fname and fname[:1].islower() and fname not in KNOWN_FACTORIES:
if "." not in fname and (fname[:1].islower() or fname in self.d.transform_names) and fname not in KNOWN_FACTORIES:
# parameterised transform defined later (or in another file); the player resolves it by name
self.use_transform(fname)
return {"name": fname, "atl": fname, "args": e["a"], "kw": e["kw"]}
@@ -1343,6 +1346,8 @@ 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)
call = self.expr(source, line) if source else None
if call is None:
self.d.warn("Не разобран вызов screen", self.where(line))
@@ -1623,6 +1628,9 @@ class RpyParser:
# say ------------------------------------------------------------------------------
def say(self, line: Line) -> list[Node] | None:
# `__("Girl") "Hello"`: a translatable speaker name; the name is looked up by its text anyway
if wrapped := TRANSLATED_SPEAKER.match(line.text):
line = replace(line, text=wrapped[1] + line.text[wrapped.end():])
c = self.cursor(line)
who: str | None = None
who_str: str | None = None