* 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>
97 lines
3.5 KiB
Python
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')
|