Files
mrleo1nidandClaude Opus 5.5 e1d7b29f77 Localization stage 1: stable IDs, import of tl/ translations, language picker
- every script node gets a permanent id, say nodes a Ren'Py-style tid
- all languages from the game's tl/ are imported at conversion: dialogue blocks
  matched by their source-location comment or by label + original text,
  `translate strings` become a per-language strings table
- source language from config.language or detected from the dialogue
- manifest stores locales/<lang>/<module>.json and strings.json; game.json
  lists sourceLang and available locales with progress
- player: language picker in main menu and settings, fallback to source,
  __(), [var!t] and _preferences.language
- parser: `default` with init priority, and one bad statement no longer drops
  the whole file
- docs/LOCALIZATION.md: design for translations, publishing and mods

Line endings of several converter/player files go back to LF: an earlier
commit accidentally stored them as CRLF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 03:39:40 +03:00

75 lines
2.6 KiB
Python

"""Build a small Ren'Py-like game archive for tests and manual checks.
python -m tests.make_sample out.zip
"""
import io
import math
import struct
import sys
import wave
import zipfile
from pathlib import Path
from PIL import Image, ImageDraw
from app.converter.rpa import write_rpa
FIXTURES = Path(__file__).parent / "fixtures" / "sample"
def _png(size, color, text="", alpha=False) -> bytes:
img = Image.new("RGBA" if alpha else "RGB", size, (0, 0, 0, 0) if alpha else color)
d = ImageDraw.Draw(img)
if alpha:
w, h = size
d.ellipse((w * 0.2, h * 0.05, w * 0.8, h * 0.45), fill=color)
d.rectangle((w * 0.25, h * 0.45, w * 0.75, h), fill=color)
else:
for y in range(0, size[1], 40):
d.line((0, y, size[0], y), fill=tuple(min(255, c + 25) for c in color), width=2)
if text:
d.text((20, 20), text, fill=(255, 255, 255, 255))
out = io.BytesIO()
img.save(out, "PNG")
return out.getvalue()
def _wav(seconds=2.0, freq=440) -> bytes:
out = io.BytesIO()
with wave.open(out, "wb") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(22050)
frames = b"".join(struct.pack("<h", int(8000 * math.sin(2 * math.pi * freq * i / 22050)))
for i in range(int(22050 * seconds)))
w.writeframes(frames)
return out.getvalue()
def build(path: Path) -> Path:
script = (FIXTURES / "script.rpy").read_bytes()
options = 'define config.name = "Тестовая новелла"\ninit python:\n gui.init(1280, 720)\n'
rpa_path = path.with_suffix(".rpa.tmp")
write_rpa(rpa_path, {
"script.rpy": script,
"images/bg room.png": _png((1280, 720), (60, 70, 110), "bg room"),
"images/eileen happy.png": _png((400, 650), (220, 160, 170), alpha=True),
})
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
zf.writestr("SampleGame-1.0-pc/game/archive.rpa", rpa_path.read_bytes())
zf.writestr("SampleGame-1.0-pc/game/options.rpy", options)
zf.writestr("SampleGame-1.0-pc/game/tl/english/script.rpy",
(FIXTURES / "tl" / "english" / "script.rpy").read_bytes())
zf.writestr("SampleGame-1.0-pc/game/audio/theme.wav", _wav())
zf.writestr("SampleGame-1.0-pc/game/gui/main_menu.png",
_png((1280, 720), (30, 30, 50), "Main menu"))
zf.writestr("SampleGame-1.0-pc/renpy/common/00start.rpy", "label _start:\n return\n")
zf.writestr("SampleGame-1.0-pc/SampleGame.exe", b"MZ")
rpa_path.unlink()
return path
if __name__ == "__main__":
print(build(Path(sys.argv[1] if len(sys.argv) > 1 else "sample.zip")))