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
|
||||
foal4 = 31
|
||||
horse4 = 32
|
||||
foal5 = 33
|
||||
horse5 = 34
|
||||
|
||||
baby_bird1 = 33
|
||||
bird1 = 34
|
||||
baby_bird2 = 35
|
||||
bird2 = 36
|
||||
cardinal_chick = 37
|
||||
cardinal = 38
|
||||
baby_eagle = 39
|
||||
eagle = 40
|
||||
baby_crow = 41
|
||||
crow = 42
|
||||
baby_bird1 = 35
|
||||
bird1 = 36
|
||||
baby_bird2 = 37
|
||||
bird2 = 38
|
||||
cardinal_chick = 39
|
||||
cardinal = 40
|
||||
baby_eagle = 41
|
||||
eagle = 42
|
||||
baby_crow = 43
|
||||
crow = 44
|
||||
|
||||
piglet = 43
|
||||
pig = 44
|
||||
calf = 45
|
||||
cattle = 46
|
||||
kid = 47
|
||||
goat = 48
|
||||
lamb = 49
|
||||
sheep = 50
|
||||
duckling = 51
|
||||
duck = 52
|
||||
wolf_pup = 53
|
||||
wolf = 54
|
||||
doe = 55
|
||||
buck = 56
|
||||
boar_piglet = 57
|
||||
boar = 58
|
||||
bear_cub = 59
|
||||
bear = 60
|
||||
squirrel_kit = 61
|
||||
squirrel = 62
|
||||
turkey_chick = 63
|
||||
turkey = 64
|
||||
opossum_joey = 65
|
||||
opossum = 66
|
||||
skunk_kit = 67
|
||||
skunk = 68
|
||||
raccoon_cub = 69
|
||||
raccoon = 70
|
||||
piglet = 45
|
||||
pig = 46
|
||||
calf = 47
|
||||
cattle = 48
|
||||
kid = 49
|
||||
goat = 50
|
||||
lamb = 51
|
||||
sheep = 52
|
||||
duckling = 53
|
||||
duck = 54
|
||||
wolf_pup = 55
|
||||
wolf = 56
|
||||
doe = 57
|
||||
buck = 58
|
||||
boar_piglet = 59
|
||||
boar = 60
|
||||
bear_cub = 61
|
||||
bear = 62
|
||||
squirrel_kit = 63
|
||||
squirrel = 64
|
||||
turkey_chick = 65
|
||||
turkey = 66
|
||||
opossum_joey = 67
|
||||
opossum = 68
|
||||
skunk_kit = 69
|
||||
skunk = 70
|
||||
raccoon_cub = 71
|
||||
raccoon = 72
|
||||
|
||||
fish1 = 71
|
||||
fish2 = 72
|
||||
fish3 = 73
|
||||
fish4 = 74
|
||||
bees = 75
|
||||
bugs1 = 76
|
||||
bugs2 = 77
|
||||
butterfly1 = 78
|
||||
butterfly2 = 79
|
||||
butterfly3 = 80
|
||||
butterfly4 = 81
|
||||
fish1 = 73
|
||||
fish2 = 74
|
||||
fish3 = 75
|
||||
fish4 = 76
|
||||
bees = 77
|
||||
bugs1 = 78
|
||||
bugs2 = 79
|
||||
butterfly1 = 80
|
||||
butterfly2 = 81
|
||||
butterfly3 = 82
|
||||
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]
|
||||
fn manifest_names_resolve_and_bootstrap_sprites_have_idle() {
|
||||
fn frames_are_read_off_the_sheet() {
|
||||
let s = sprites();
|
||||
for name in ["player", "hen", "fox"] {
|
||||
let id = s.id(name);
|
||||
let idle = s.frame(id, State::Idle, 0).unwrap();
|
||||
assert_ne!(idle.mask, 0, "{name}");
|
||||
// Only one idle frame so far: index 1 wraps to it. No walk yet: falls back.
|
||||
assert_eq!(s.frame(id, State::Idle, 1).unwrap().px, idle.px);
|
||||
assert_eq!(s.frame(id, State::Walk, 0).unwrap().px, idle.px, "{name}");
|
||||
}
|
||||
let hen = s.id("hen");
|
||||
let idle0 = s.frame(hen, State::Idle, 0).unwrap();
|
||||
let idle1 = s.frame(hen, State::Idle, 1).unwrap();
|
||||
assert_ne!(idle0.mask, 0);
|
||||
assert_ne!(idle0.px, idle1.px, "hen has two distinct idle frames");
|
||||
assert_eq!(s.frame(hen, State::Idle, 2).unwrap().px, idle0.px, "wraps");
|
||||
// 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]
|
||||
fn empty_rows_have_nothing() {
|
||||
fn missing_state_falls_back_to_idle() {
|
||||
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`
|
||||
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
|
||||
is: edit the manifest, rerun the tool, paste, save as `entities.png`. Game code refers
|
||||
to sprites by name (`sprites.id("hen")`); the id doubles as the entity's `type_id`.
|
||||
is: edit the manifest, rerun the tool, paste, save as `entities.png`.
|
||||
`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
|
||||
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