801f328093
ci / verify (push) Successful in 46s
The ship looked like it played both animations at once because it effectively did. is_moving() read the tail of `pending`, which _reconcile drains on every snapshot -- so it returned false about 20 times a second and the sprite alternated between the run and idle strips. Now reads the last sampled input, which survives the queue being emptied. Bullets were still wrong for a reason my earlier ASCII check could not see: I had dumped alpha only, and this pack animates as a colour shimmer over a fixed silhouette, so identical-looking frames told me nothing. The actual defect was that several sprites overflow their 16px cell and bleed into the neighbour below -- the first "heavy" I picked dragged in a solid slice of the sprite above it. Cells are re-picked to ones with empty borders in every frame, and build_local_assets.py now asserts that rather than trusting the choice, so a future pick that bleeds fails at build time instead of looking like a rendering bug. Added a hitbox overlay on F1 (src/view/debug_draw.gd). It draws what the simulation actually collides against over what is drawn: player hitbox against sprite radius, the muzzle point, enemy and boss radii, aggro rings, every live bullet's radius, and terrain outlined by which of the three flags each tile sets -- so a pit that stops feet but not bullets looks different from a wall. It deliberately ignores fog, since hiding half the evidence would defeat the point, and it reads every number from SimConfig/MapGrid/Content rather than keeping its own copies, or it would just confirm its own mistakes. Nearly every "that looked wrong" report in this project has been art and simulation disagreeing, and each took a round trip to diagnose. This makes that class of bug visible directly. 163 tests. check.sh, test.sh and smoke.sh pass.
79 lines
3.3 KiB
Python
Executable File
79 lines
3.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Compose the local-only sprite atlases from the raw asset packs.
|
|
|
|
Both inputs and outputs are licence-restricted and therefore untracked (see
|
|
docs/ASSETS.md), which is why this is a build step rather than a committed file:
|
|
anyone with the packs can regenerate, and nobody without them is blocked.
|
|
|
|
pip install pillow && python3 tools/build_local_assets.py
|
|
|
|
The bullet pack's layout is not obvious and cost a wrong guess once:
|
|
* Each of the 8 PNGs is one ANIMATION FRAME, not one sprite sheet.
|
|
* Within a file, a row is a bullet shape and a column is a COLOUR variant.
|
|
So animating means cycling files, not walking columns -- walking columns just
|
|
cycles through palettes while the shape sits still.
|
|
|
|
Output is one tidy atlas: 8 frames across, one row per SimConfig.KIND_*.
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
try:
|
|
from PIL import Image
|
|
except ImportError:
|
|
sys.exit("needs pillow: pip install pillow")
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
SRC = os.path.join(ROOT, "assetpacks", "New_All_Fire_Bullet_Pixel_16x16",
|
|
"All_Fire_Bullet_Pixel_16x16_%02d.png")
|
|
OUT = os.path.join(ROOT, "assets", "local", "bullets.png")
|
|
CELL = 16
|
|
FRAMES = 8
|
|
|
|
# (source row, source column) per bullet kind, in SimConfig.KIND_* order.
|
|
# Column selects the colour: 0 red, 1 orange, 2 amber, 3 gold, 4 bright yellow.
|
|
#
|
|
# Cells must have EMPTY BORDERS in every frame. Several sprites in this pack
|
|
# overflow their 16px cell and bleed into the neighbour below, so a cell picked
|
|
# purely by eye can drag in a slice of the sprite above it -- which is exactly
|
|
# what put a solid bar across the top of the first "heavy" I chose.
|
|
KINDS = [
|
|
("player_shot", 7, 4), # small bright-yellow dot
|
|
("orb", 3, 1), # round orange orb
|
|
("needle", 6, 6), # compact dart, drawn rotated to velocity
|
|
("heavy", 5, 0), # larger red ball
|
|
]
|
|
|
|
|
|
def main() -> None:
|
|
for f in range(FRAMES):
|
|
if not os.path.exists(SRC % f):
|
|
sys.exit("missing source frame: %s\n"
|
|
"The raw pack is untracked; see assets/local/README.md."
|
|
% (SRC % f))
|
|
out = Image.new("RGBA", (CELL * FRAMES, CELL * len(KINDS)), (0, 0, 0, 0))
|
|
for f in range(FRAMES):
|
|
src = Image.open(SRC % f).convert("RGBA")
|
|
for k, (_name, row, col) in enumerate(KINDS):
|
|
box = (col * CELL, row * CELL, (col + 1) * CELL, (row + 1) * CELL)
|
|
out.paste(src.crop(box), (f * CELL, k * CELL))
|
|
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
|
# Guard the rule the KINDS comment describes, so a future pick that bleeds
|
|
# fails loudly here instead of looking like a rendering bug.
|
|
for k in range(len(KINDS)):
|
|
for f in range(FRAMES):
|
|
for x in range(CELL):
|
|
for y in (0, CELL - 1):
|
|
if out.getpixel((f * CELL + x, k * CELL + y))[3] > 40:
|
|
sys.exit("cell for %s bleeds at its border; pick another"
|
|
% KINDS[k][0])
|
|
out.save(OUT)
|
|
print("wrote %s (%dx%d): %d kinds x %d frames"
|
|
% (OUT, out.width, out.height, len(KINDS), FRAMES))
|
|
for k, (name, row, col) in enumerate(KINDS):
|
|
print(" row %d = %-12s (source row %2d, colour col %d)" % (k, name, row, col))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|