合并外部维护版本并同步自动每日功能

This commit is contained in:
2026-09-28 13:55:07 +08:00
parent 2d81615f78
commit 9040bcb5e5
59 changed files with 3176 additions and 179 deletions
+94
View File
@@ -0,0 +1,94 @@
"""Update the Kirara sprite rectangles in web/index.html from transparent PNGs.
Usage: .venv/Scripts/python.exe scripts/build_kirara_sprite_catalog.py
"""
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / "web" / "uploads"
PAGE = ROOT / "web" / "index.html"
START = " // Source rectangles come from the four generated transparent 2×2 walk sheets."
END = " let kiraraDirection = \"front\";"
def frame_boxes(path: Path) -> list[list[int]]:
image = Image.open(path).convert("RGBA")
width, height = image.size
if width < 100 or height < 100:
raise ValueError(f"Sprite is unexpectedly small: {path}")
boxes = []
for frame in range(4):
start = round(frame * width / 4)
stop = round((frame + 1) * width / 4)
alpha = image.crop((start, 0, stop, height)).getchannel("A")
content = alpha.point(lambda value: 255 if value >= 16 else 0).getbbox()
if content is None:
raise ValueError(f"Empty frame {frame}: {path}")
left = max(0, content[0] - 3)
top = max(0, content[1] - 3)
right = min(stop - start, content[2] + 3)
bottom = min(height, content[3] + 3)
boxes.append([start + left, top, right - left, bottom - top])
return boxes
names = [
*(f"kirara_walk_{direction}" for direction in ("front", "back", "left", "right")),
"kirara_eat_rice", "kirara_eat_dango", "kirara_bath_sponge", "kirara_bath_shower",
*(f"kirara_idle_{name}" for name in (
"delivery", "reading", "phone", "sleep", "stretch", "groom",
"butterfly", "tea", "yarn", "humming")),
"kirara_rest_breathe",
*(f"kirara_emotion_{name}_{variant}" for name in (
"plus3", "plus1", "minus1", "minus3") for variant in ("a", "b")),
]
lines = [
" // Crops follow each generated sprite's transparent silhouette.",
" const kiraraSpriteBoxes = {",
]
for name in names:
boxes = frame_boxes(ASSETS / f"{name}.png")
formatted = ", ".join(str(box).replace(" ", "") for box in boxes)
lines.append(f' "{name}": [{formatted}],')
lines += [
" };",
" const kiraraSpriteAsset = name => ({",
" src: `/uploads/${name}.png`, boxes: kiraraSpriteBoxes[name]",
" });",
" const kiraraWalkAssets = Object.fromEntries(",
" [\"front\", \"back\", \"left\", \"right\"].map(direction =>",
" [direction, kiraraSpriteAsset(`kirara_walk_${direction}`)])",
" );",
" const kiraraActionAssets = {",
" feed: [kiraraSpriteAsset(\"kirara_eat_rice\"), kiraraSpriteAsset(\"kirara_eat_dango\")],",
" bath: [kiraraSpriteAsset(\"kirara_bath_sponge\"), kiraraSpriteAsset(\"kirara_bath_shower\")]",
" };",
" const kiraraIdleAssets = Object.fromEntries(",
" [\"delivery\", \"reading\", \"phone\", \"sleep\", \"stretch\",",
" \"groom\", \"butterfly\", \"tea\", \"yarn\", \"humming\"].map(type =>",
" [type, kiraraSpriteAsset(`kirara_idle_${type}`)])",
" );",
" const kiraraRestAsset = kiraraSpriteAsset(\"kirara_rest_breathe\");",
" const kiraraEmotionAssets = {",
]
for delta, word in ((3, "plus3"), (1, "plus1"), (-1, "minus1"), (-3, "minus3")):
lines.append(
f' "{delta}": [kiraraSpriteAsset("kirara_emotion_{word}_a"), '
f'kiraraSpriteAsset("kirara_emotion_{word}_b")],'
)
lines += [" };", ""]
text = PAGE.read_text(encoding="utf-8")
start = text.find(START)
if start < 0:
start = text.find(" // Crops follow each generated sprite's transparent silhouette.")
end = text.find(END, start)
if start < 0 or end < 0:
raise ValueError("Kirara asset block was not found")
PAGE.write_text(text[:start] + "\n".join(lines) + text[end:], encoding="utf-8")
print(f"Updated {len(names)} Kirara sprite sheets in {PAGE}")