summaryrefslogtreecommitdiff
path: root/scripts/make-animation-evidence.py
blob: 09c35f0e356badba784539632ffb9983b24db537 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python3
"""Deterministic animation evidence sheets for the P13 art pass.

This script does not touch the game: it crops the *real generated atlases*
using the same source-rectangle math the renderer uses in
`src/LivingVillage.Desktop/VillageArt.fs`, then lays the frames out as
labelled contact sheets. Because the inputs are the shipped PNGs and the
layout is a pure function, running it twice yields byte-identical output.

Mirrored math (keep in sync with VillageArt.fs):
  characterSourceRectangle:
    cellIndex = (variantIndex * 4 + directionIndex) * 6 + frameIndex
    frameIndex: WalkFrame 0..3 -> 0..3, IdleFrame IdleOne/IdleTwo -> 4/5
    rect = (cellIndex * 32, 0, 32, 48)
  tileSourceRectangle:
    Grass=0 Water=1 StonePath=2 ... Lantern=20 WaterSpriteC=21
    SmokeSpriteA/B/C = 22/23/24
    rect = (index * 32, 0, 32, 32)
  waterFrameTick  = (tick / 32) % 3  -> WaterSprite / WaterSpriteB / WaterSpriteC
  smokeFrameTick  = (tick / 40) % 3  -> SmokeSpriteA / SmokeSpriteB / SmokeSpriteC
  idleFrame       = (tick / 24) % 2  -> IdleOne / IdleTwo
  animationFrame  = (tick / 4)  % 4  -> FrameOne..FrameFour
"""

import hashlib
import os
from PIL import Image, ImageDraw

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WORLD = os.path.join(ROOT, "src/LivingVillage.Desktop/Assets/jiangnan-world.png")
CHARS = os.path.join(ROOT, "src/LivingVillage.Desktop/Assets/jiangnan-characters.png")
OUT = os.path.join(ROOT, "evidence")

TILE = 32
CHAR_W = 32
CHAR_H = 48

DIRECTIONS = [("North", 0), ("South", 1), ("West", 2), ("East", 3)]
VARIANT = ("StrawHat", 4)

TILE_NAMES = {1: "Water(0)", 13: "Water(1)", 21: "Water(2)",
              22: "Smoke(0)", 23: "Smoke(1)", 24: "Smoke(2)"}


def sha256(path):
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()


def char_cell(sheet, variant, direction, frame):
    cell = (variant * 4 + direction) * 6 + frame
    return sheet.crop((cell * CHAR_W, 0, cell * CHAR_W + CHAR_W, CHAR_H))


def tile_cell(sheet, index):
    return sheet.crop((index * TILE, 0, index * TILE + TILE, TILE))


def zoom(img, factor):
    return img.resize((img.width * factor, img.height * factor), Image.NEAREST)


def label(draw, xy, text):
    draw.text(xy, text, fill=(20, 20, 20))


def make_walk_sheet(sheet, path):
    scale = 4
    cw, ch = CHAR_W * scale, CHAR_H * scale
    cols = 6
    rows = len(DIRECTIONS)
    pad = 8
    header = 26
    width = pad + cols * (cw + pad)
    height = header + rows * (ch + pad + 16)
    canvas = Image.new("RGB", (width, height), (245, 243, 236))
    draw = ImageDraw.Draw(canvas)
    label(draw, (pad, 6), "walk frames 0..3 + idle 0..1  (variant StrawHat)")
    for r, (name, di) in enumerate(DIRECTIONS):
        y = header + r * (ch + pad + 16)
        label(draw, (pad, y), name)
        for f in range(6):
            x = pad + f * (cw + pad)
            img = zoom(char_cell(sheet, VARIANT[1], di, f), scale)
            canvas.paste(img, (x, y + 16))
            tag = f"walk{f}" if f < 4 else f"idle{f - 4}"
            label(draw, (x, y + 16 + ch + 2), tag)
    canvas.save(path)


def make_phase_sheet(world_sheet, indices, title, path):
    scale = 6
    size = TILE * scale
    pad = 12
    header = 26
    n = len(indices)
    width = pad + n * (size + pad)
    height = header + size + pad + 18
    canvas = Image.new("RGB", (width, height), (245, 243, 236))
    draw = ImageDraw.Draw(canvas)
    label(draw, (pad, 6), title)
    for i, idx in enumerate(indices):
        x = pad + i * (size + pad)
        img = zoom(tile_cell(world_sheet, idx), scale)
        canvas.paste(img, (x, header))
        label(draw, (x, header + size + 2), TILE_NAMES[idx])
    canvas.save(path)


def main():
    os.makedirs(OUT, exist_ok=True)
    world_sheet = Image.open(WORLD).convert("RGBA")
    char_sheet = Image.open(CHARS).convert("RGBA")

    assert char_sheet.size == (3840, 48), char_sheet.size
    assert world_sheet.size == (800, 32), world_sheet.size

    make_walk_sheet(char_sheet, os.path.join(OUT, "walk-idle-frames.png"))
    make_phase_sheet(world_sheet, [1, 13, 21], "water ripple: 3 phases",
                     os.path.join(OUT, "water-3phase.png"))
    make_phase_sheet(world_sheet, [22, 23, 24], "chimney smoke: 3 phases",
                     os.path.join(OUT, "smoke-3phase.png"))
    # Two-frame comparisons the brief asks for.
    make_phase_sheet(world_sheet, [1, 13], "water: frame 0 vs frame 1",
                     os.path.join(OUT, "water-2frame.png"))
    make_phase_sheet(world_sheet, [22, 23], "smoke: frame 0 vs frame 1",
                     os.path.join(OUT, "smoke-2frame.png"))

    # Deterministic tick -> frame table for the report.
    def walk(t):
        return (t // 4) % 4

    def idle(t):
        return (t // 24) % 2

    def water(t):
        return (t // 32) % 3

    def smoke(t):
        return (t // 40) % 3

    report = []
    report.append("P13 animation evidence")
    report.append(f"jiangnan-world.png  sha256={sha256(WORLD)} size={world_sheet.size}")
    report.append(f"jiangnan-characters.png sha256={sha256(CHARS)} size={char_sheet.size}")
    report.append("")
    report.append("tick -> walkFrame / idleFrame / waterFrame / smokeFrame")
    for t in [0, 4, 8, 12, 16, 24, 32, 40, 48, 64, 80, 96, 128, 160]:
        report.append(f"  tick={t:>4}  walk={walk(t)}  idle={idle(t)}  water={water(t)}  smoke={smoke(t)}")
    text = "\n".join(report) + "\n"
    with open(os.path.join(OUT, "animation-evidence.txt"), "w") as f:
        f.write(text)
    print(text, end="")


if __name__ == "__main__":
    main()