Files
transcience/tools/build_local_assets.py
T
claude afe76c22ce
ci / verify (push) Successful in 46s
Fix bullet atlas, animation speed, sprite flip, fire rate, silent shots
Bullet sprites were wrong because I read the pack's layout backwards. Each of
its 8 PNGs is one ANIMATION FRAME, and a column within a file is a COLOUR
variant -- so walking columns cycled the palette while the shape sat still, and
the rows I picked were in a region of larger multi-cell sprites, which is the
scattered debris that showed on screen. tools/build_local_assets.py now composes
a proper atlas: 8 frames across, one row per SimConfig.KIND_*.

Animations ran at whatever the machine's framerate was, because they counted
_process calls. At 240fps that is four times too fast on its own, before the
requested halving. Both actor and bullet animation are now driven by elapsed
seconds, so they look the same on any machine.

The knight shifted sideways instead of mirroring: a Rect2 with negative width
does not flip, the draw call normalises it, so the sprite kept its orientation
and jumped right by its own width. Mirrored through the canvas transform about
the sprite's centre instead.

Shots were silent when a bullet spawned inside an enemy. The sound rode on
BULLET_SPAWN, which is deliberately suppressed for a bullet resolved and removed
within the same tick -- so no event, no sound, and this got more likely the
closer you stood. Firing is now its own event (PLAYER_FIRED): the shot happened
whether or not a bullet survived to be replicated. Protocol 3 -> 4, because
inserting mid-enum shifts the wire value of every event after it.

Fire cooldown 7 -> 14 ticks (4.3 shots/sec).

Licence question 2, confirmed verbatim: the paid tier DOES grant commercial use
("You may use these assets in personal, commercial or non-commercial
projects"), but its next sentence still forbids redistribution "no matter how
much you modify it". Paying moves these from local-only-and-non-commercial to
local-only-and-commercial; it never makes them committable. Only replacing them
with permissively licensed art does that.

Question 3: CREDITS.md now credits every pack, including the two whose licences
do not require it and the unused ones in considering_dont_use_yet/.

158 tests. check.sh, test.sh and smoke.sh pass.
2026-09-04 00:03:04 +02:00

65 lines
2.5 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.
KINDS = [
("player_shot", 11, 4), # small bright dot
("orb", 2, 1), # round orange orb
("needle", 11, 6), # horizontal dart, drawn rotated to velocity
("heavy", 1, 0), # big 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)
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()