The three foundations first, since everything else leans on them:
- G.modal is the only thing that stops the world. update() early-returns on it
so no zombie steps, no ball flight, no spawn timers run while a popup is on
screen — you can read it without being eaten. Dismissal swallows the gesture;
the pointerdown dismisser is registered only on pointer:fine, because on touch
pointerdown beats touchstart and a shared handler cleared the modal and then
served with the same thumb.
- UI in config.js is the accent palette. The arcade primaries were shouting over
the artwork; the gore reds deliberately stay loud.
- Every canvas font string now goes through F(). Bare `monospace` resolved to
Courier — a slab serif — which was the "generic serif" bug, not taste.
Then: weapons are only ever WON. deliverTin() has exactly one caller now
(resolveInterlude('P')); the old game schedule calls the DUEL instead, fired off
proceedAfterClear() where the court is guaranteed empty. Four popups (splatter
tutorial with a generated illustration, first duel, weapon received, horde),
wCap 9 with the tin amounts tunable, a 5-hp brute that comes apart over four
wound skins, fire that ignites the neighbours and chains outward, a three-stage
death where you become the biggest gore mound on the court, and a 7s changeover
with a ball boy who finally has a mop.
New art: assets/logo.png, x_howto, z_gw2-4, x_boymop1-4, plus assets/_key.py for
standalone art (chroma-key + crop, no frame splitting, no forced sprite height).
Benchmarks re-run and holding: CPU returns 23/24 serves, 30 human-tapped points
= 50 hits / 0 whiffs, 26 simulated games with no exceptions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
27 lines
1000 B
Python
27 lines
1000 B
Python
# chroma-key a generated image on its corner colour, despeckle, crop to the content bbox,
|
|
# and (optionally) downscale to a target width. For standalone art (logo, popup panels) —
|
|
# unlike assets/_slice.py this does NOT split frames or force a sprite height.
|
|
import sys
|
|
from PIL import Image
|
|
|
|
KEY_D2 = 60 * 60
|
|
|
|
def main(src, dst, target_w=None):
|
|
im = Image.open(src).convert('RGBA')
|
|
w, h = im.size
|
|
px = im.load()
|
|
bg = px[0, 0][:3]
|
|
for x in range(w):
|
|
for y in range(h):
|
|
r, g, b, a = px[x, y]
|
|
if (r - bg[0]) ** 2 + (g - bg[1]) ** 2 + (b - bg[2]) ** 2 < KEY_D2 or (min(r, b) > g + 40):
|
|
px[x, y] = (0, 0, 0, 0)
|
|
im = im.crop(im.getbbox())
|
|
if target_w and im.width > target_w:
|
|
im = im.resize((target_w, max(1, round(im.height * target_w / im.width))), Image.LANCZOS)
|
|
im.save(dst)
|
|
print(dst, im.size)
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv[1], sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else None)
|