IMX6U-Game/tools/find_room_spawns.py

108 lines
3.7 KiB
Python

"""Validate default spawn candidates per 3x3 room of LevelTotal.
Reads tiles[] from src/Apps/LightGame/src/levels/LevelTotalData.h, then for each
candidate (col,row,tile_x,tile_y) checks:
- tile at (tx,ty) and (tx,ty+1)/(tx,ty-1) per collider (8,0)->(24,32) is non-solid
- tile at (tx, ty+1) (the tile directly under feet at world_y=tile_y*32+32) is solid
Player position used by spawn check (LightGameApp.cpp:181):
pos = (tile_x*32, tile_y*32) -- collider min=(8,0), max=(24,32)
world_box = pos + collider = (tx*32+8, ty*32) -> (tx*32+24, ty*32+32)
is_grounded checks tiles below feet; collider rows tile_top..tile_bottom-1 must be non-solid.
Solid ids: 5,6,7,8,9,10,11.
Deadly: 12, 22.
"""
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src/Apps/LightGame/src/levels/LevelTotalData.h"
text = SRC.read_text(encoding="utf-8")
# Parse tiles array
m = re.search(r"static const uint16_t tiles\[\]\s*=\s*\{([^}]*)\};", text, re.DOTALL)
nums = [int(x) for x in re.findall(r"-?\d+", m.group(1))]
W, H = 96, 54
assert len(nums) == W * H, len(nums)
tiles = [nums[y * W:(y + 1) * W] for y in range(H)]
SOLID = {5, 6, 7, 8, 9, 10, 11}
DEADLY = {12, 22}
def at(tx, ty):
if tx < 0 or tx >= W or ty < 0 or ty >= H:
return -1
return tiles[ty][tx]
def collider_overlaps_solid(tx, ty):
# world_box = (tx*32+8, ty*32) -> (tx*32+24, ty*32+32)
# tile rows: ty .. (ty*32+32-1)//32 = ty (since 32+32-1=63, //32=1, so rows ty..ty+1-1 = ty)
# Actually max.y - 1 = ty*32 + 31, //32 = ty. So only row ty.
# tile cols: (tx*32+8)//32 = tx, (tx*32+24-1)//32 = tx. Only col tx.
return at(tx, ty) in SOLID or at(tx, ty) in DEADLY
def has_solid_below(tx, ty):
# Player feet at world_y = ty*32 + 32. is_grounded probes 1px below = ty*32+33.
# Tile row = (ty*32+32)//32 = ty+1.
return at(tx, ty + 1) in SOLID
def safe_above_head(tx, ty):
# don't have ceiling spike just touching
return at(tx, ty - 1) not in DEADLY
def candidate_score(tx, ty):
if collider_overlaps_solid(tx, ty):
return None, "overlap solid/deadly at body row"
if not has_solid_below(tx, ty):
return None, "not grounded"
if not safe_above_head(tx, ty):
return None, "deadly above head"
return True, "ok"
# Room grid: cols 0/32/64, rows 0/18/36; size 32x18
def find_safe_in_room(rcol, rrow, prefer=None):
tx0, ty0 = rcol * 32, rrow * 18
tx1, ty1 = tx0 + 32, ty0 + 18
candidates = []
if prefer:
ptx, pty = prefer
ok, why = candidate_score(ptx, pty)
if ok:
return (ptx, pty, "preferred")
else:
print(f" preferred {(ptx,pty)} rejected: {why}")
# Scan center outward
cx, cy = (tx0 + tx1) // 2, (ty0 + ty1) // 2
for ty in range(ty0, ty1):
for tx in range(tx0, tx1):
ok, why = candidate_score(tx, ty)
if ok:
d = (tx - cx) ** 2 + (ty - cy) ** 2
candidates.append((d, tx, ty))
if not candidates:
return None
candidates.sort()
_, tx, ty = candidates[0]
return (tx, ty, "auto")
# Center room must reuse current spawn (1184, 928) = tile (37, 29)
preferences = {
(1, 1): (37, 29),
}
print(f"{'Room':<8} {'tile':<10} {'pixel':<14} note")
print("-" * 50)
for rrow in range(3):
for rcol in range(3):
prefer = preferences.get((rcol, rrow))
result = find_safe_in_room(rcol, rrow, prefer=prefer)
if result is None:
print(f"({rcol},{rrow}) NO SAFE SPAWN FOUND")
continue
tx, ty, kind = result
# Spawn pos in pixels = (tx*32, ty*32). Verify under feet:
below = at(tx, ty + 1)
print(f"({rcol},{rrow}) ({tx:3d},{ty:3d}) ({tx*32:5d},{ty*32:4d}) below={below} ({kind})")