RECORDANDRONCO/tools/animate_sprites.py
type-two 9bca0f99c2 Juice pass: living buildings, battle scars, screen shake, living menu, Stream buffs
* All 19 structures now breathe: procedural pulse-glow animation generated from
  each sprite's own lit pixels (tools/animate_sprites.py), 6 frames at 130ms.
  WithSpriteBody PlayRepeating()s multi-frame idles, so no trait changes needed.
* Big explosions leave scorch smudges on the ground (SmudgeLayer + LeaveSmudge
  on bass_ring, both superweapon impacts, and building deaths).
* Buildings shake the screen when they die (ScreenShaker + ShakeOnDeath).
* The main menu is alive again: shellmap split into two lua-driven sides that
  skirmish forever. No Bot: players, so no order-manager stall.
* Stream buffs owed by telemetry: bufferbot HP 3500->5000 (survives a second
  seven-inch), drmwalker 750, scraper capacity 18.

Verified: arena smoketest PASS with all changes, shellmap lua confirmed live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 18:45:51 +10:00

97 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Turn static building sprites into looping pulse animations.
For each building sprite: find its glowing pixels (high saturation or very
bright — the pink trims, blue rings, neon), then emit N frames where those
pixels breathe between base and boosted brightness on a sine. WithSpriteBody
PlayRepeating()s the idle sequence, so multi-frame sheets animate for free.
Regenerates <name>.png in sequences/assets as an N-frame sheet and rewrites the
matching `idle:` block in sequences/economy.yaml (Length + Tick).
Safe to re-run: it always starts from assets_src frame-0 backups it maintains.
"""
import math, os, re, shutil
from PIL import Image
from PIL.PngImagePlugin import PngInfo
ROOT = os.path.join(os.path.dirname(__file__), '..')
ASSETS = os.path.join(ROOT, 'mods', 'vinylgod', 'sequences', 'assets')
BACKUP = os.path.join(ROOT, 'assets_src', 'static_frames')
os.makedirs(BACKUP, exist_ok=True)
FRAMES = 6
TICK = 130 # ms per frame -> ~0.8s pulse
BUILDINGS = ['popupshop', 'pressplant', 'ampstack', 'vault', 'merchtable', 'garage',
'mastering', 'hornpit', 'streamhub', 'datacentre', 'transcoder', 'podfarm',
'printfarm', 'adblocker', 'algorithm', 'launchramp', 'droneport', 'waxseed',
'lacquer']
def glow_mask(im):
"""Alpha-scaled mask of 'lit' pixels: saturated or very bright."""
hsv = im.convert('RGB').convert('HSV')
h, s, v = hsv.split()
a = im.getchannel('A')
mask = Image.new('L', im.size, 0)
mp, sp, vp, ap = mask.load(), s.load(), v.load(), a.load()
for y in range(im.height):
for x in range(im.width):
if ap[x, y] > 40 and (sp[x, y] > 130 and vp[x, y] > 110 or vp[x, y] > 225):
mp[x, y] = ap[x, y]
return mask
def brighten(im, factor):
r, g, b, a = im.split()
out = Image.merge('RGBA', tuple(ch.point(lambda p: min(255, int(p * factor))) for ch in (r, g, b)) + (a,))
return out
def animate(name):
src = os.path.join(ASSETS, f'{name}.png')
if not os.path.exists(src):
return None
back = os.path.join(BACKUP, f'{name}.png')
if not os.path.exists(back):
im = Image.open(src).convert('RGBA')
# if this is already a sheet somehow, keep frame 0
if im.width > im.height:
im = im.crop((0, 0, im.height, im.height))
im.save(back)
base = Image.open(back).convert('RGBA')
w, h = base.size
mask = glow_mask(base)
sheet = Image.new('RGBA', (w * FRAMES, h), (0, 0, 0, 0))
for i in range(FRAMES):
t = 0.5 + 0.5 * math.sin(2 * math.pi * i / FRAMES)
lit = brighten(base, 1.0 + 0.55 * t)
frame = Image.composite(lit, base, mask.point(lambda p: int(p * t)))
sheet.paste(frame, (i * w, 0))
meta = PngInfo()
meta.add_text('FrameSize', f'{w},{h}')
sheet.save(src, pnginfo=meta)
return w
def patch_sequences(done):
p = os.path.join(ROOT, 'mods', 'vinylgod', 'sequences', 'economy.yaml')
s = open(p).read()
for name in done:
# idle block: add/replace Length+Tick right after the Filename line
pat = re.compile(
rf'(^{name}:\n\tidle:\n\t\tFilename: sequences/assets/{name}\.png\n)(?:\t\tLength: \d+\n)?(?:\t\tTick: \d+\n)?',
re.M)
s = pat.sub(rf'\g<1>\t\tLength: {FRAMES}\n\t\tTick: {TICK}\n', s)
open(p, 'w').write(s)
done = []
for b in BUILDINGS:
if animate(b):
done.append(b)
print(f'animated {b}')
patch_sequences(done)
print(f'{len(done)} sprites animated, sequences patched')