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>
BIN
assets_src/living_buildings.gif
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
assets_src/static_frames/adblocker.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets_src/static_frames/algorithm.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets_src/static_frames/ampstack.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
assets_src/static_frames/datacentre.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets_src/static_frames/droneport.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
assets_src/static_frames/garage.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
assets_src/static_frames/hornpit.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
assets_src/static_frames/lacquer.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
assets_src/static_frames/launchramp.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
assets_src/static_frames/mastering.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
assets_src/static_frames/merchtable.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
assets_src/static_frames/podfarm.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
assets_src/static_frames/popupshop.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
assets_src/static_frames/pressplant.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
assets_src/static_frames/printfarm.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
assets_src/static_frames/streamhub.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
assets_src/static_frames/transcoder.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
assets_src/static_frames/vault.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
assets_src/static_frames/waxseed.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
@ -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
|
||||
|
||||
3
mods/vinylgod/maps/shellmap/rules.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
world:
|
||||
LuaScript:
|
||||
Scripts: shellmap.lua
|
||||
43
mods/vinylgod/maps/shellmap/shellmap.lua
Normal 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
|
||||
@ -106,6 +106,9 @@ djgrunt:
|
||||
|
||||
^EconBuilding:
|
||||
Inherits: ^EconSprite
|
||||
ShakeOnDeath:
|
||||
Duration: 12
|
||||
Intensity: 2
|
||||
-HiddenUnderFog:
|
||||
FrozenUnderFog:
|
||||
WithDamageOverlay:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -73,6 +73,10 @@ world:
|
||||
Sequences: wax01
|
||||
Name: resource-wax
|
||||
ResourceClaimLayer:
|
||||
ScreenShaker:
|
||||
SmudgeLayer@SCORCH:
|
||||
Type: Scorch
|
||||
Sequence: scorches
|
||||
|
||||
editorworld:
|
||||
Inherits: ^baseworld
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 16 KiB |
BIN
mods/vinylgod/sequences/assets/scorch1.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
mods/vinylgod/sequences/assets/scorch2.png
Normal file
|
After Width: | Height: | Size: 1016 B |
BIN
mods/vinylgod/sequences/assets/scorch3.png
Normal file
|
After Width: | Height: | Size: 967 B |
BIN
mods/vinylgod/sequences/assets/scorch4.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.6 KiB |
@ -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
|
||||
|
||||
@ -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
@ -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')
|
||||