#!/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 .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')