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>
This commit is contained in:
type-two 2026-07-26 18:45:51 +10:00
parent 39ca9c78a5
commit 9bca0f99c2
52 changed files with 230 additions and 15 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -22,35 +22,39 @@ Players:
OwnsWorld: True
NonCombatant: True
Faction: Random
PlayerReference@Creeps:
Name: Creeps
NonCombatant: True
Faction: Random
PlayerReference@ShowA:
Name: ShowA
Faction: vinylgod
Enemies: ShowB
PlayerReference@ShowB:
Name: ShowB
Faction: stream
Enemies: ShowA
Actors:
Actor1: pressplant
Owner: Creeps
Owner: ShowA
Location: 12,12
Actor2: digger
Owner: Creeps
Owner: ShowA
Location: 15,14
Actor3: ampstack
Owner: Creeps
Owner: ShowA
Location: 10,14
Actor4: popupshop
Owner: Creeps
Owner: ShowA
Location: 18,12
Actor5: hornpit
Owner: Creeps
Owner: ShowA
Location: 16,16
Actor6: streamhub
Owner: Creeps
Owner: ShowB
Location: 24,24
Actor7: transcoder
Owner: Creeps
Owner: ShowB
Location: 21,25
Actor8: adblocker
Owner: Creeps
Owner: ShowB
Location: 25,22
waxseed0: waxseed
Owner: Neutral
@ -58,3 +62,5 @@ Actors:
waxseed1: waxseed
Owner: Neutral
Location: 18,10
Rules: rules.yaml

View File

@ -0,0 +1,3 @@
world:
LuaScript:
Scripts: shellmap.lua

View File

@ -0,0 +1,43 @@
-- Menu diorama battle: two lua-driven sides skirmish forever behind the UI.
-- No Bot: players (bot clients stall shellmap order managers — see MEGAPLAN).
Ticks = 0
WaveA = { "djgrunt", "djgrunt", "spinner" }
WaveB = { "bufferbot", "bufferbot", "bufferbot", "drmwalker" }
CountUnits = function(p)
return #Utils.Where(Map.ActorsInWorld, function(a)
return a.Owner == p and a.HasProperty("Move")
end)
end
Spawn = function(owner, types, cx, cy)
Utils.Do(types, function(t)
local a = Actor.Create(t, true, {
Owner = owner,
Location = CPos.New(cx + Utils.RandomInteger(-1, 2), cy + Utils.RandomInteger(-1, 2)),
})
if a.HasProperty("Hunt") then
a.Hunt()
end
end)
end
WorldLoaded = function()
print("SHELLMAP live")
A = Player.GetPlayer("ShowA")
B = Player.GetPlayer("ShowB")
end
Tick = function()
Ticks = Ticks + 1
-- a fresh clash roughly every 30 seconds, kept small
if Ticks % 750 == 250 and A ~= nil then
if CountUnits(A) < 8 then
Spawn(A, WaveA, 14, 15)
end
if CountUnits(B) < 8 then
Spawn(B, WaveB, 23, 23)
end
end
end

View File

@ -106,6 +106,9 @@ djgrunt:
^EconBuilding:
Inherits: ^EconSprite
ShakeOnDeath:
Duration: 12
Intensity: 2
-HiddenUnderFog:
FrozenUnderFog:
WithDamageOverlay:

View File

@ -153,7 +153,7 @@ scraper:
SearchFromProcRadius: 15
SearchFromHarvesterRadius: 8
StoresResources:
Capacity: 14
Capacity: 18
Resources: Wax
DockClientManager:
@ -170,7 +170,7 @@ bufferbot:
Tooltip:
Name: actor-bufferbot.name
Health:
HP: 3500
HP: 5000
Armor:
Type: None
Mobile:
@ -190,7 +190,7 @@ drmwalker:
BuildPaletteOrder: 51
Prerequisites: printfarm
Valued:
Cost: 900
Cost: 750
Tooltip:
Name: actor-drmwalker.name
Health:

View File

@ -73,6 +73,10 @@ world:
Sequences: wax01
Name: resource-wax
ResourceClaimLayer:
ScreenShaker:
SmudgeLayer@SCORCH:
Type: Scorch
Sequence: scorches
editorworld:
Inherits: ^baseworld

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -29,36 +29,48 @@ djgrunt:
popupshop:
idle:
Filename: sequences/assets/popupshop.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_popupshop.png
pressplant:
idle:
Filename: sequences/assets/pressplant.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_pressplant.png
ampstack:
idle:
Filename: sequences/assets/ampstack.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_ampstack.png
vault:
idle:
Filename: sequences/assets/vault.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_vault.png
merchtable:
idle:
Filename: sequences/assets/merchtable.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_merchtable.png
garage:
idle:
Filename: sequences/assets/garage.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_garage.png
@ -92,6 +104,8 @@ roadie:
mastering:
idle:
Filename: sequences/assets/mastering.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_mastering.png
@ -105,30 +119,40 @@ streamvan:
streamhub:
idle:
Filename: sequences/assets/streamhub.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_streamhub.png
datacentre:
idle:
Filename: sequences/assets/datacentre.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_datacentre.png
transcoder:
idle:
Filename: sequences/assets/transcoder.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_transcoder.png
podfarm:
idle:
Filename: sequences/assets/podfarm.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_podfarm.png
printfarm:
idle:
Filename: sequences/assets/printfarm.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_printfarm.png
@ -168,12 +192,16 @@ explosion:
hornpit:
idle:
Filename: sequences/assets/hornpit.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_hornpit.png
adblocker:
idle:
Filename: sequences/assets/adblocker.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_adblocker.png
@ -198,6 +226,8 @@ icon:
algorithm:
idle:
Filename: sequences/assets/algorithm.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_algorithm.png
@ -229,12 +259,16 @@ curator:
launchramp:
idle:
Filename: sequences/assets/launchramp.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_launchramp.png
droneport:
idle:
Filename: sequences/assets/droneport.png
Length: 6
Tick: 130
icon:
Filename: sequences/assets/icon_droneport.png
@ -269,7 +303,21 @@ compressor:
waxseed:
idle:
Filename: sequences/assets/waxseed.png
Length: 6
Tick: 130
lacquer:
idle:
Filename: sequences/assets/lacquer.png
Length: 6
Tick: 130
scorches:
sc1:
Filename: sequences/assets/scorch1.png
sc2:
Filename: sequences/assets/scorch2.png
sc3:
Filename: sequences/assets/scorch3.png
sc4:
Filename: sequences/assets/scorch4.png

View File

@ -90,6 +90,9 @@ bass_ring:
Wood: 100
Light: 75
Heavy: 40
Warhead@3Smu: LeaveSmudge
SmudgeType: Scorch
ValidTargets: Ground
ping:
Inherits: ^GroundWeapon
@ -121,6 +124,9 @@ BuildingExplode:
Explosions: bigboom
ImpactSounds: audio/sfx_boom.wav
ValidTargets: Ground, GroundActor
Warhead@3Smu: LeaveSmudge
SmudgeType: Scorch
ValidTargets: Ground
thedrop_impact:
ValidTargets: Ground, GroundActor, Structure
@ -137,6 +143,9 @@ thedrop_impact:
Explosions: bigboom
ImpactSounds: audio/sfx_boom.wav
ValidTargets: Ground, GroundActor
Warhead@3Smu: LeaveSmudge
SmudgeType: Scorch
ValidTargets: Ground
theshuffle_impact:
ValidTargets: Ground, GroundActor, Structure
@ -153,6 +162,9 @@ theshuffle_impact:
Explosions: bigboom
ImpactSounds: audio/sfx_boom.wav
ValidTargets: Ground, GroundActor
Warhead@3Smu: LeaveSmudge
SmudgeType: Scorch
ValidTargets: Ground
# --- round 5: heroes, scouts, air ---

96
tools/animate_sprites.py Normal file
View File

@ -0,0 +1,96 @@
#!/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')