Realign pack sheets into the entities layout and fill the sheet
tools/realign_sheet.py strips the blank spacer rows from a documented Animals/Monsters sheet, drops the dark-box preview copies and moves the light frames into the manifest's slot columns, so a sheet pastes into the template as one block. Both sheets are pasted; the manifest gains the fifth foal/horse pair the Animals sheet actually has. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 44 KiB |
@@ -62,55 +62,57 @@ foal3 = 29
|
|||||||
horse3 = 30
|
horse3 = 30
|
||||||
foal4 = 31
|
foal4 = 31
|
||||||
horse4 = 32
|
horse4 = 32
|
||||||
|
foal5 = 33
|
||||||
|
horse5 = 34
|
||||||
|
|
||||||
baby_bird1 = 33
|
baby_bird1 = 35
|
||||||
bird1 = 34
|
bird1 = 36
|
||||||
baby_bird2 = 35
|
baby_bird2 = 37
|
||||||
bird2 = 36
|
bird2 = 38
|
||||||
cardinal_chick = 37
|
cardinal_chick = 39
|
||||||
cardinal = 38
|
cardinal = 40
|
||||||
baby_eagle = 39
|
baby_eagle = 41
|
||||||
eagle = 40
|
eagle = 42
|
||||||
baby_crow = 41
|
baby_crow = 43
|
||||||
crow = 42
|
crow = 44
|
||||||
|
|
||||||
piglet = 43
|
piglet = 45
|
||||||
pig = 44
|
pig = 46
|
||||||
calf = 45
|
calf = 47
|
||||||
cattle = 46
|
cattle = 48
|
||||||
kid = 47
|
kid = 49
|
||||||
goat = 48
|
goat = 50
|
||||||
lamb = 49
|
lamb = 51
|
||||||
sheep = 50
|
sheep = 52
|
||||||
duckling = 51
|
duckling = 53
|
||||||
duck = 52
|
duck = 54
|
||||||
wolf_pup = 53
|
wolf_pup = 55
|
||||||
wolf = 54
|
wolf = 56
|
||||||
doe = 55
|
doe = 57
|
||||||
buck = 56
|
buck = 58
|
||||||
boar_piglet = 57
|
boar_piglet = 59
|
||||||
boar = 58
|
boar = 60
|
||||||
bear_cub = 59
|
bear_cub = 61
|
||||||
bear = 60
|
bear = 62
|
||||||
squirrel_kit = 61
|
squirrel_kit = 63
|
||||||
squirrel = 62
|
squirrel = 64
|
||||||
turkey_chick = 63
|
turkey_chick = 65
|
||||||
turkey = 64
|
turkey = 66
|
||||||
opossum_joey = 65
|
opossum_joey = 67
|
||||||
opossum = 66
|
opossum = 68
|
||||||
skunk_kit = 67
|
skunk_kit = 69
|
||||||
skunk = 68
|
skunk = 70
|
||||||
raccoon_cub = 69
|
raccoon_cub = 71
|
||||||
raccoon = 70
|
raccoon = 72
|
||||||
|
|
||||||
fish1 = 71
|
fish1 = 73
|
||||||
fish2 = 72
|
fish2 = 74
|
||||||
fish3 = 73
|
fish3 = 75
|
||||||
fish4 = 74
|
fish4 = 76
|
||||||
bees = 75
|
bees = 77
|
||||||
bugs1 = 76
|
bugs1 = 78
|
||||||
bugs2 = 77
|
bugs2 = 79
|
||||||
butterfly1 = 78
|
butterfly1 = 80
|
||||||
butterfly2 = 79
|
butterfly2 = 81
|
||||||
butterfly3 = 80
|
butterfly3 = 82
|
||||||
butterfly4 = 81
|
butterfly4 = 83
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
+23
-11
@@ -125,21 +125,33 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn manifest_names_resolve_and_bootstrap_sprites_have_idle() {
|
fn frames_are_read_off_the_sheet() {
|
||||||
let s = sprites();
|
let s = sprites();
|
||||||
for name in ["player", "hen", "fox"] {
|
let hen = s.id("hen");
|
||||||
let id = s.id(name);
|
let idle0 = s.frame(hen, State::Idle, 0).unwrap();
|
||||||
let idle = s.frame(id, State::Idle, 0).unwrap();
|
let idle1 = s.frame(hen, State::Idle, 1).unwrap();
|
||||||
assert_ne!(idle.mask, 0, "{name}");
|
assert_ne!(idle0.mask, 0);
|
||||||
// Only one idle frame so far: index 1 wraps to it. No walk yet: falls back.
|
assert_ne!(idle0.px, idle1.px, "hen has two distinct idle frames");
|
||||||
assert_eq!(s.frame(id, State::Idle, 1).unwrap().px, idle.px);
|
assert_eq!(s.frame(hen, State::Idle, 2).unwrap().px, idle0.px, "wraps");
|
||||||
assert_eq!(s.frame(id, State::Walk, 0).unwrap().px, idle.px, "{name}");
|
// The hen walks with 4 frames; index 4 wraps back to the first.
|
||||||
}
|
let walk0 = s.frame(hen, State::Walk, 0).unwrap();
|
||||||
|
assert_eq!(s.frame(hen, State::Walk, 4).unwrap().px, walk0.px);
|
||||||
|
assert_ne!(s.frame(hen, State::Walk, 3).unwrap().px, walk0.px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_rows_have_nothing() {
|
fn missing_state_falls_back_to_idle() {
|
||||||
let s = sprites();
|
let s = sprites();
|
||||||
assert!(s.frame(s.id("butterfly4"), State::Idle, 0).is_none());
|
let ghost = s.id("ghost1"); // ghosts never sit
|
||||||
|
assert_eq!(s.frame(ghost, State::Sit, 0).unwrap().px,
|
||||||
|
s.frame(ghost, State::Idle, 0).unwrap().px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn player_row_has_only_a_bootstrap_idle() {
|
||||||
|
let s = sprites();
|
||||||
|
let p = s.id("player");
|
||||||
|
assert!(s.frame(p, State::Idle, 0).is_some());
|
||||||
|
assert_eq!(s.frame(p, State::Walk, 0).unwrap().px, s.frame(p, State::Idle, 0).unwrap().px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -23,8 +23,13 @@ many frames a row has, the loader (`game/src/sprites.rs`) reads off the pixels
|
|||||||
(leading non-empty cells; empty slot → falls back to idle). `tools/mktemplate.py`
|
(leading non-empty cells; empty slot → falls back to idle). `tools/mktemplate.py`
|
||||||
renders the paste-in template `entities-template.png` (labels, header, slot guides at
|
renders the paste-in template `entities-template.png` (labels, header, slot guides at
|
||||||
alpha 48 — invisible to the loader) and carries existing sprites over, so adding rows
|
alpha 48 — invisible to the loader) and carries existing sprites over, so adding rows
|
||||||
is: edit the manifest, rerun the tool, paste, save as `entities.png`. Game code refers
|
is: edit the manifest, rerun the tool, paste, save as `entities.png`.
|
||||||
to sprites by name (`sprites.id("hen")`); the id doubles as the entity's `type_id`.
|
`tools/realign_sheet.py <pack sheet>` turns a documented Animals/Monsters sheet into
|
||||||
|
`assets/work/<Name>-aligned.png`: blank spacer rows removed, only the light frames,
|
||||||
|
already in the slot columns — one block paste per sheet (Monsters → rows 1–9, Animals
|
||||||
|
→ rows 10–83; the manifest order follows the sheets, including five foal/horse
|
||||||
|
pairs). Game code refers to sprites by name (`sprites.id("hen")`); the id doubles as
|
||||||
|
the entity's `type_id`.
|
||||||
|
|
||||||
History: this fork started from a client/server multiplayer architecture (authoritative UDP
|
History: this fork started from a client/server multiplayer architecture (authoritative UDP
|
||||||
server, thin client with prediction). The scope was cut to single player in September 2026;
|
server, thin client with prediction). The scope was cut to single player in September 2026;
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rearrange a documented Mini Medieval sheet into the entities.png slot layout.
|
||||||
|
|
||||||
|
The packs' Animals/Monsters sheets share our state order (idle, sit, walk, action 1,
|
||||||
|
action 2, hit, dead) but pad groups with blank rows and show every frame twice (on
|
||||||
|
light and dark preview boxes). This writes assets/work/<Name>-aligned.png: the label
|
||||||
|
column, then per row the light frames of each state placed in the manifest's slot
|
||||||
|
columns, blank rows removed. That block pastes into the template in one go.
|
||||||
|
|
||||||
|
python3 tools/realign_sheet.py assets/packs/Mini-Medieval-Documented-8x8/Animals.png
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tomllib
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
MANIFEST = "assets/tilesets/entities.toml"
|
||||||
|
HEADER = 16 # y of the first sprite row in the source
|
||||||
|
CELL = 8
|
||||||
|
LABEL_W = 91 # source label column (up to the first separator)
|
||||||
|
BLOCK_X = 96 # where the realigned frames start in the output
|
||||||
|
|
||||||
|
|
||||||
|
def is_dark(p):
|
||||||
|
return p[3] > 0 and p[0] + p[1] + p[2] < 120
|
||||||
|
|
||||||
|
|
||||||
|
def separators(px, w, h):
|
||||||
|
"""x of the vertical rule lines (dark for most of the sheet's height)."""
|
||||||
|
xs = [x for x in range(LABEL_W, w) if sum(is_dark(px[x, y]) for y in range(h)) > h * 0.9]
|
||||||
|
# Rules are 2 px wide; keep one x per rule.
|
||||||
|
return [x for i, x in enumerate(xs) if i == 0 or x != xs[i - 1] + 1]
|
||||||
|
|
||||||
|
|
||||||
|
def region_frames(px, x0, x1, h):
|
||||||
|
"""Cell columns of the light frames in a region: from the first content column
|
||||||
|
(past the rule line, snapped to the 8 px grid) up to the first cell column where
|
||||||
|
a preview box (a fully opaque cell in some row) starts."""
|
||||||
|
def is_rule(x):
|
||||||
|
return sum(is_dark(px[x, y]) for y in range(h)) > h * 0.9
|
||||||
|
ys = range(HEADER, h)
|
||||||
|
start = next((x for x in range(x0, x1)
|
||||||
|
if not is_rule(x) and any(px[x, y][3] > 0 for y in ys)), None)
|
||||||
|
if start is None:
|
||||||
|
return None, 0
|
||||||
|
start -= start % CELL
|
||||||
|
n = 0
|
||||||
|
x = start
|
||||||
|
while x + CELL <= x1:
|
||||||
|
boxed = any(all(px[x + dx, y + dy][3] > 0 for dx in range(CELL) for dy in range(CELL))
|
||||||
|
for y in range(HEADER, h - CELL + 1, CELL))
|
||||||
|
if boxed:
|
||||||
|
break
|
||||||
|
n += 1
|
||||||
|
x += CELL
|
||||||
|
return start, n
|
||||||
|
|
||||||
|
|
||||||
|
def main(path):
|
||||||
|
m = tomllib.load(open(MANIFEST, "rb"))
|
||||||
|
slots = list(m["layout"]["states"].values()) # [(col, max)] in state order
|
||||||
|
total_cols = max(c + n for c, n in slots)
|
||||||
|
|
||||||
|
img = Image.open(path).convert("RGBA")
|
||||||
|
px = img.load()
|
||||||
|
w, h = img.size
|
||||||
|
|
||||||
|
# Non-empty source rows (labels or sprites, ignoring rules and boxes).
|
||||||
|
def band_empty(y0):
|
||||||
|
for y in range(y0, y0 + CELL):
|
||||||
|
for x in range(w):
|
||||||
|
p = px[x, y]
|
||||||
|
if p[3] == 0 or (p[:3] == (18, 14, 35)) or (is_dark(p) and x > LABEL_W - 4):
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
src_rows = [y for y in range(HEADER, h - CELL + 1, CELL) if not band_empty(y)]
|
||||||
|
|
||||||
|
seps = separators(px, w, h)
|
||||||
|
regions = list(zip(seps, seps[1:] + [w]))
|
||||||
|
if len(regions) < len(slots):
|
||||||
|
sys.exit(f"{path}: found {len(regions)} state columns, manifest has {len(slots)}")
|
||||||
|
|
||||||
|
out = Image.new("RGBA", (BLOCK_X + total_cols * CELL, HEADER + len(src_rows) * CELL), (0, 0, 0, 0))
|
||||||
|
out.paste(img.crop((0, 0, LABEL_W, HEADER)), (0, 0))
|
||||||
|
for (x0, x1), (slot_col, slot_max) in zip(regions, slots):
|
||||||
|
start, n = region_frames(px, x0, x1, h)
|
||||||
|
n = min(n, slot_max)
|
||||||
|
for i, sy in enumerate(src_rows):
|
||||||
|
dy = HEADER + i * CELL
|
||||||
|
out.paste(img.crop((0, sy, LABEL_W, sy + CELL)), (0, dy))
|
||||||
|
if n:
|
||||||
|
frames = img.crop((start, sy, start + n * CELL, sy + CELL))
|
||||||
|
out.paste(frames, (BLOCK_X + slot_col * CELL, dy), frames)
|
||||||
|
|
||||||
|
name = os.path.splitext(os.path.basename(path))[0]
|
||||||
|
dst = f"assets/work/{name}-aligned.png"
|
||||||
|
out.save(dst)
|
||||||
|
print(f"wrote {dst}: {len(src_rows)} rows; paste the block ({BLOCK_X}, {HEADER})–"
|
||||||
|
f"({BLOCK_X + total_cols * CELL}, {out.size[1]}) at (64, 16 + 8·row)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for p in sys.argv[1:]:
|
||||||
|
main(p)
|
||||||
Reference in New Issue
Block a user