Refactor platform UI and data flow across core pages
All checks were successful
Build Org Website / build (push) Successful in 50s
All checks were successful
Build Org Website / build (push) Successful in 50s
This commit is contained in:
141
tools/generate-princess-lima-assets.py
Normal file
141
tools/generate-princess-lima-assets.py
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate deterministic Princess Lima tiles and local narration assets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import math
|
||||
import random
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
IMAGE_DIR = ROOT / "assets/images/play/princess-lima"
|
||||
AUDIO_DIR = ROOT / "assets/audio/princess-lima"
|
||||
TILE = 32
|
||||
|
||||
THEMES = [
|
||||
("village", "#51713f", "#41556b", "#b79861", "#6d4333"),
|
||||
("forest", "#173f31", "#2f6845", "#789b52", "#234f62"),
|
||||
("ruins", "#263f47", "#486468", "#8a8872", "#1b7280"),
|
||||
("mountain", "#6c8094", "#b9c9d6", "#e8f0f2", "#516476"),
|
||||
("camp", "#53442e", "#76603a", "#aa8051", "#683f2b"),
|
||||
("fortressExterior", "#221c2c", "#40344b", "#64566f", "#2b182e"),
|
||||
("fortressInterior", "#201d28", "#393342", "#74636b", "#7e312a"),
|
||||
("bossArena", "#17121f", "#2d2138", "#674371", "#8e365f"),
|
||||
("chamber", "#73869a", "#c9b985", "#e9dfc2", "#31558b"),
|
||||
]
|
||||
|
||||
NARRATION = [
|
||||
"Before shadow crossed the northern road, Princess Lima walked among her people, listening before she ruled and helping before she asked.",
|
||||
"Then Lord Malrec descended from the Fortress of Shadows. His riders carried fear through the valleys, searching for the royal oath.",
|
||||
"Lima stood between the riders and the village. Malrec could not bend her will, so he bound her in shadow and carried her beyond the mountains.",
|
||||
"At dawn, a lone traveller reached the broken village. The road was dangerous, but every rescued life would become another light leading to Lima.",
|
||||
]
|
||||
|
||||
|
||||
def shade(hex_color: str, factor: float) -> tuple[int, int, int, int]:
|
||||
value = hex_color.lstrip("#")
|
||||
channels = [int(value[index:index + 2], 16) for index in (0, 2, 4)]
|
||||
return tuple(max(0, min(255, round(channel * factor))) for channel in channels) + (255,)
|
||||
|
||||
|
||||
def draw_tile(draw: ImageDraw.ImageDraw, x: int, y: int, colors: tuple[str, str, str, str], kind: int, seed: int) -> None:
|
||||
ground, detail, path, hazard = colors
|
||||
randomizer = random.Random(seed)
|
||||
base = [ground, detail, path, hazard][kind]
|
||||
draw.rectangle((x, y, x + TILE - 1, y + TILE - 1), fill=base)
|
||||
dark = shade(base, 0.72)
|
||||
light = shade(base, 1.22)
|
||||
if kind == 0:
|
||||
for _ in range(13):
|
||||
px = x + randomizer.randrange(2, TILE - 2)
|
||||
py = y + randomizer.randrange(2, TILE - 2)
|
||||
draw.point((px, py), fill=light if randomizer.random() > 0.45 else dark)
|
||||
draw.line((x, y + TILE - 2, x + TILE, y + TILE - 2), fill=dark, width=2)
|
||||
elif kind == 1:
|
||||
for row in range(0, TILE, 8):
|
||||
offset = 4 if row % 16 else 0
|
||||
draw.line((x, y + row, x + TILE, y + row), fill=dark)
|
||||
for col in range(-offset, TILE, 12):
|
||||
draw.line((x + col, y + row, x + col + 5, y + row + 4), fill=light, width=2)
|
||||
elif kind == 2:
|
||||
draw.line((x + 3, y + 2, x + 3, y + TILE - 3), fill=dark, width=2)
|
||||
draw.line((x + TILE - 4, y + 2, x + TILE - 4, y + TILE - 3), fill=light, width=2)
|
||||
for row in range(5, TILE, 8):
|
||||
draw.line((x + 6, y + row, x + TILE - 7, y + row + randomizer.choice((-1, 0, 1))), fill=dark)
|
||||
else:
|
||||
for row in range(4, TILE, 7):
|
||||
draw.arc((x - 5, y + row - 4, x + 16, y + row + 4), 195, 345, fill=light, width=2)
|
||||
draw.arc((x + 13, y + row - 4, x + 34, y + row + 4), 195, 345, fill=dark, width=2)
|
||||
|
||||
|
||||
def make_tiles() -> None:
|
||||
image = Image.new("RGBA", (TILE * 4, TILE * len(THEMES)), "#000000")
|
||||
draw = ImageDraw.Draw(image)
|
||||
for row, (_, *colors) in enumerate(THEMES):
|
||||
for kind in range(4):
|
||||
draw_tile(draw, kind * TILE, row * TILE, tuple(colors), kind, row * 101 + kind)
|
||||
image.save(IMAGE_DIR / "world-tiles.png", optimize=True)
|
||||
|
||||
|
||||
def synthesize(text: str, destination: Path) -> bool:
|
||||
library = ctypes.util.find_library("espeak-ng")
|
||||
if not library:
|
||||
return False
|
||||
espeak = ctypes.CDLL(library)
|
||||
samples: list[int] = []
|
||||
callback_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.POINTER(ctypes.c_short), ctypes.c_int, ctypes.c_void_p)
|
||||
|
||||
@callback_type
|
||||
def callback(wav, count, _events):
|
||||
if wav and count > 0:
|
||||
samples.extend(wav[index] for index in range(count))
|
||||
return 0
|
||||
|
||||
espeak.espeak_Initialize.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_int]
|
||||
espeak.espeak_Initialize.restype = ctypes.c_int
|
||||
espeak.espeak_SetSynthCallback.argtypes = [callback_type]
|
||||
espeak.espeak_SetVoiceByName.argtypes = [ctypes.c_char_p]
|
||||
espeak.espeak_Synth.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint, ctypes.c_int,
|
||||
ctypes.c_uint, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.c_void_p,
|
||||
]
|
||||
rate = espeak.espeak_Initialize(1, 0, None, 0)
|
||||
if rate <= 0:
|
||||
return False
|
||||
espeak.espeak_SetSynthCallback(callback)
|
||||
espeak.espeak_SetVoiceByName(b"en-gb")
|
||||
encoded = text.encode("utf-8") + b"\0"
|
||||
identifier = ctypes.c_uint(0)
|
||||
espeak.espeak_Synth(encoded, len(encoded), 0, 0, 0, 1, ctypes.byref(identifier), None)
|
||||
espeak.espeak_Synchronize()
|
||||
if not samples:
|
||||
return False
|
||||
peak = max(1, max(abs(sample) for sample in samples))
|
||||
scaled = [round(sample * min(1.0, 24000 / peak)) for sample in samples]
|
||||
with wave.open(str(destination), "wb") as output:
|
||||
output.setnchannels(1)
|
||||
output.setsampwidth(2)
|
||||
output.setframerate(rate)
|
||||
buffer = (ctypes.c_short * len(scaled))(*scaled)
|
||||
output.writeframes(bytes(buffer))
|
||||
espeak.espeak_Terminate()
|
||||
return True
|
||||
|
||||
|
||||
def make_narration() -> None:
|
||||
for index, line in enumerate(NARRATION, 1):
|
||||
if not synthesize(line, AUDIO_DIR / f"intro-narration-{index}.wav"):
|
||||
raise RuntimeError("Local espeak-ng narration synthesis was unavailable")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
IMAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
AUDIO_DIR.mkdir(parents=True, exist_ok=True)
|
||||
make_tiles()
|
||||
make_narration()
|
||||
Reference in New Issue
Block a user