Add multi-cell box shapes with per-cell collision

Generate 1x2/2x1 dominoes, 2x2 (square + 4 L-trominoes) and all
connected 3x2/2x3 polyominoes as R/G/B channel masks, reusing the
distance-based concentric-ring format that reproduces box_r/g/b
exactly. Outer silhouette keeps a 1px margin; connected cells stay
seamless. Each shape ships a {class="moveable"} .meta so getAllAssets
auto-registers it as a GenericMoveable.

Wire the occupied-cell shape (precomputed once by getAllAssets via
getCellsFromSprite) through to entities: createEntity now passes the
cached cells as cellShape, so an L/U/notched block collides on its real
cells and its gaps read as empty space. Fix the per-colour cache that
was reset every channel so only one colour survived.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-08-11 12:42:33 -04:00
parent 5b7ea9ec20
commit d96f57efb6
154 changed files with 194 additions and 9 deletions

View file

@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Generate multi-cell "box" sprites for CHROMA SOLSTICE.
Each cell is 16x16. A single box has a 1px transparent margin on every edge
(so a 14x14 body inside 16x16). For multi-cell blocks we keep that 1px margin
only on the *outer* silhouette of the block -- edges that could touch other
things in the level -- while cells that are part of the same block stay truly
connected (no seam, no internal margin).
The per-channel fill is the same concentric-ring format as the shipped
box_r / box_g / box_b: a pixel is decided purely by k, its inset depth
(chebyshev distance in from the nearest outside pixel, minus 1; k==0 is the
margin ring and is always transparent). The three channels each leave their
own gaps so the colors underneath stay visible when drawn additively.
red ON: k in {1,2} or k>=5 (2px ring, 2px gap, solid core)
green ON: k odd or k>=5 (alternating 1px rings, solid core)
blue ON: k==1 or k>=4 (1px ring, 2px gap, solid core)
Run from inside art/game/ : python3 gen_composite_boxes.py
"""
from itertools import product
from collections import deque
from PIL import Image
CELL = 16
RULES = {
'r': lambda k: (1 <= k <= 2) or (k >= 5),
'g': lambda k: (k >= 5) or (k >= 1 and k % 2 == 1),
'b': lambda k: (k == 1) or (k >= 4),
}
def inset_grid(cells, W, H):
"""Return k[y][x] for the W*CELL x H*CELL pixel grid (-1 = outside)."""
Wp, Hp = W * CELL, H * CELL
occ = [[False] * Wp for _ in range(Hp)]
for (cx, cy) in cells:
for y in range(cy * CELL, cy * CELL + CELL):
for x in range(cx * CELL, cx * CELL + CELL):
occ[y][x] = True
INF = 10 ** 9
dist = [[INF] * Wp for _ in range(Hp)]
dq = deque()
for y in range(Hp):
for x in range(Wp):
if not occ[y][x]:
dist[y][x] = 0
dq.append((x, y))
while dq: # multi-source BFS with chebyshev (8-neighbour) steps
x, y = dq.popleft()
for dy in (-1, 0, 1):
for dx in (-1, 0, 1):
if dx == 0 and dy == 0:
continue
nx, ny = x + dx, y + dy
if 0 <= nx < Wp and 0 <= ny < Hp and dist[ny][nx] > dist[y][x] + 1:
dist[ny][nx] = dist[y][x] + 1
dq.append((nx, ny))
k = [[-1] * Wp for _ in range(Hp)]
for y in range(Hp):
for x in range(Wp):
if occ[y][x]:
# the region beyond the image edge counts as outside too
edged = min(x + 1, Wp - x, y + 1, Hp - y)
k[y][x] = min(dist[y][x], edged) - 1
return k
def render(cells, W, H, channel):
k = inset_grid(cells, W, H)
rule = RULES[channel]
img = Image.new('RGBA', (W * CELL, H * CELL), (0, 0, 0, 0))
px = img.load()
for y in range(H * CELL):
for x in range(W * CELL):
if k[y][x] >= 0 and rule(k[y][x]):
px[x, y] = (255, 255, 255, 255)
return img
def connected(cells):
cells = set(cells)
start = next(iter(cells))
seen = {start}
stack = [start]
while stack:
x, y = stack.pop()
for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
n = (x + dx, y + dy)
if n in cells and n not in seen:
seen.add(n)
stack.append(n)
return len(seen) == len(cells)
def enum(W, H):
"""All connected polyominoes whose bounding box is exactly W x H."""
coords = [(x, y) for y in range(H) for x in range(W)]
out = []
for bits in range(1, 1 << (W * H)):
cells = [coords[i] for i in range(W * H) if bits >> i & 1]
if len(cells) < 2:
continue
xs = [c[0] for c in cells]
ys = [c[1] for c in cells]
if min(xs) != 0 or max(xs) != W - 1 or min(ys) != 0 or max(ys) != H - 1:
continue
if not connected(cells):
continue
out.append(cells)
return out
def mask_name(cells, W, H):
rows = [['0'] * W for _ in range(H)]
for x, y in cells:
rows[y][x] = '1'
return '-'.join(''.join(r) for r in rows)
BBOXES = [(1, 2), (2, 1), (2, 2), (3, 2), (2, 3)]
if __name__ == '__main__':
total = 0
for (W, H) in BBOXES:
for cells in enum(W, H):
mask = mask_name(cells, W, H)
for ch in 'rgb':
fn = 'box_%dx%d_%s_%s.png' % (W, H, mask, ch)
render(cells, W, H, ch).save(fn)
total += 1
print('wrote %d files' % total)