108 lines
3.7 KiB
Python
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})")
|