RECORDANDRONCO/tools/gen_sideb.py
type-two 6847a47fea SIDE B campaign: three Stream missions (m04 Onboarding quota race, m05 Engagement Metrics wave defence, m06 The Shuffle finale)
Adds tools/gen_sideb.py — one data-driven builder replacing the copy-pasted
gen_missionN.py pattern — and tools/smoketest.sh, which reports
PASS/FAIL/DISPLAY_UNAVAILABLE instead of the process-is-alive check that has
produced false passes twice now.

NOT verified in-game: the display went to sleep mid-session. Static checks all
pass (luac, check-yaml 0 errors, map refresh, check_assets, make) and the
already-verified m03 hangs identically, confirming it is environmental.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:35:15 +10:00

189 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""Generate the SIDE B campaign maps (m04-m06) from one shared builder.
Supersedes the copy-pasted gen_missionN.py approach — mission geometry is data,
not three near-identical scripts.
"""
import os, struct
from PIL import Image
ROOT = os.path.join(os.path.dirname(__file__), '..')
MAPS = os.path.join(ROOT, 'mods', 'vinylgod', 'maps')
def build(name, title, size, wax, actors, player_faction, enemy_faction,
enemy_bot=False, scripts='', briefing=''):
W = H = size + 2
d = os.path.join(MAPS, name)
os.makedirs(d, exist_ok=True)
data = bytearray()
data += struct.pack('<BHH', 2, W, H)
data += struct.pack('<III', 17, 0, 3 * W * H + 17)
data += b'\xff\x00\x00' * (W * H)
res = bytearray(2 * W * H)
for cx, cy, r in wax:
for x in range(cx - r, cx + r + 1):
for y in range(cy - r, cy + r + 1):
q = (x - cx) ** 2 + (y - cy) ** 2
if q <= r * r and 0 < x < W - 1 and 0 < y < H - 1:
o = 2 * (x * H + y)
res[o], res[o + 1] = 1, max(2, round(12 * (1 - (q ** 0.5) / (r + 1))))
data += res
open(os.path.join(d, 'map.bin'), 'wb').write(data)
body = '\n'.join(f'\t{n}: {t}\n\t\tOwner: {o}\n\t\tLocation: {x},{y}' for n, t, o, x, y in actors)
bot = '\n\t\tBot: wax' if enemy_bot else ''
open(os.path.join(d, 'map.yaml'), 'w').write(f"""MapFormat: 12
RequiresMod: vinylgod
Title: {title}
Author: Monster Robot Party
Tileset: VINYLGOD
MapSize: {W},{H}
Bounds: 1,1,{size},{size}
Visibility: MissionSelector
Categories: Campaign
LockPreview: True
Players:
PlayerReference@Neutral:
Name: Neutral
OwnsWorld: True
NonCombatant: True
Faction: {player_faction}
PlayerReference@Player:
Name: Player
Playable: True
Required: True
AllowBots: False
LockFaction: True
Faction: {player_faction}
LockSpawn: True
LockTeam: True
Enemies: Enemy
PlayerReference@Enemy:
Name: Enemy
Faction: {enemy_faction}{bot}
Enemies: Player
Actors:
{body}
Rules: vinylgod|rules/campaign.yaml, rules.yaml
FluentMessages: vinylgod|fluent/lua.ftl
""")
open(os.path.join(d, 'rules.yaml'), 'w').write(
f"world:\n\tLuaScript:\n\t\tScripts: campaign.lua, {scripts}\n\tMissionData:\n\t\tBriefing: {briefing}\n")
im = Image.new('RGB', (W, H), (55, 50, 52))
for cx, cy, r in wax:
for x in range(cx - r, cx + r + 1):
for y in range(cy - r, cy + r + 1):
if (x - cx) ** 2 + (y - cy) ** 2 <= r * r and 0 <= x < W and 0 <= y < H:
im.putpixel((x, y), (255, 45, 150))
for _, _, o, x, y in actors:
im.putpixel((x, y), (120, 190, 255) if o == 'Player' else (255, 120, 90) if o == 'Enemy' else (240, 210, 90))
im.resize((W * 3, H * 3), Image.NEAREST).save(os.path.join(d, 'map.png'))
print(f'{name} written ({W}x{H}, {len(actors)} actors)')
# ---------------------------------------------------------------- m04
build(
'm04', '04 - Onboarding', 48,
wax=[(34, 36, 6), (24, 26, 5), (14, 16, 5), (38, 20, 4)],
actors=[
('spawn0', 'mpspawn', 'Neutral', 38, 40),
('TheVan', 'streamvan', 'Player', 38, 40),
('scr1', 'scraper', 'Player', 35, 40),
('scr2', 'scraper', 'Player', 36, 42),
('esc1', 'bufferbot', 'Player', 40, 42),
('esc2', 'bufferbot', 'Player', 41, 42),
('LegacyPlant', 'pressplant', 'Enemy', 10, 10),
('lamp', 'ampstack', 'Enemy', 14, 9),
('lshop', 'popupshop', 'Enemy', 9, 14),
('ldef1', 'hornpit', 'Enemy', 13, 13),
('lg1', 'djgrunt', 'Enemy', 12, 16),
('lg2', 'djgrunt', 'Enemy', 13, 16),
('lg3', 'spinner', 'Enemy', 16, 12),
],
player_faction='stream', enemy_faction='vinylgod', scripts='m04.lua',
briefing=('Welcome to Sector 7. This is a routine acquisition.\\n\\nDeploy, process the deposit, and file 3000 '
'records of value before the review window closes.\\n\\nThere is a legacy installation in the north-west. '
'It is not a threat. It is inventory.'),
)
# ---------------------------------------------------------------- m05
build(
'm05', '05 - Engagement Metrics', 48,
wax=[(24, 24, 6), (36, 34, 4), (12, 14, 4)],
actors=[
('spawn0', 'mpspawn', 'Neutral', 24, 30),
('TheTranscoder', 'transcoder', 'Player', 24, 28),
('phub', 'streamhub', 'Player', 20, 30),
('pnode1', 'datacentre', 'Player', 27, 30),
('pnode2', 'datacentre', 'Player', 19, 26),
('ppods', 'podfarm', 'Player', 21, 33),
('pprint', 'printfarm', 'Player', 27, 33),
('ptur1', 'adblocker', 'Player', 21, 25),
('ptur2', 'adblocker', 'Player', 28, 26),
('pscr', 'scraper', 'Player', 25, 25),
('pg1', 'bufferbot', 'Player', 23, 32),
('pg2', 'bufferbot', 'Player', 25, 32),
('pg3', 'drmwalker', 'Player', 22, 27),
('StagingCamp', 'merchtable', 'Enemy', 6, 6),
('scamp2', 'garage', 'Enemy', 9, 7),
],
player_faction='stream', enemy_faction='vinylgod', scripts='m05.lua',
briefing=('A legacy account has noticed the seizure.\\n\\nYour Transcoder is mid-process on a confiscated master '
'and cannot be moved. Keep it online for six minutes.\\n\\nThey will arrive in waves. Their engagement '
'is, frankly, remarkable.'),
)
# ---------------------------------------------------------------- m06
build(
'm06', '06 - The Shuffle', 64,
wax=[(50, 14, 6), (40, 24, 5), (32, 32, 6), (24, 40, 5), (14, 50, 6), (52, 34, 4), (12, 30, 4)],
actors=[
('spawn0', 'mpspawn', 'Neutral', 54, 10),
('TheVan', 'streamvan', 'Player', 54, 10),
('scr1', 'scraper', 'Player', 51, 11),
('scr2', 'scraper', 'Player', 52, 13),
('pg1', 'drmwalker', 'Player', 49, 12),
('pg2', 'bufferbot', 'Player', 55, 13),
('pg3', 'bufferbot', 'Player', 56, 13),
('TheStudio', 'mastering', 'Enemy', 12, 56),
('eshop', 'popupshop', 'Enemy', 16, 54),
('eplant1', 'pressplant', 'Enemy', 20, 56),
('eplant2', 'pressplant', 'Enemy', 9, 51),
('eamp1', 'ampstack', 'Enemy', 14, 51),
('eamp2', 'ampstack', 'Enemy', 18, 50),
('emerch', 'merchtable', 'Enemy', 19, 47),
('egarage', 'garage', 'Enemy', 15, 46),
('eramp', 'launchramp', 'Enemy', 8, 46),
('etur1', 'hornpit', 'Enemy', 17, 43),
('etur2', 'hornpit', 'Enemy', 11, 44),
('etur3', 'hornpit', 'Enemy', 22, 52),
('emob1', 'stacktank', 'Enemy', 18, 42),
('emob2', 'stacktank', 'Enemy', 13, 41),
('emob3', 'spinner', 'Enemy', 20, 44),
('emob4', 'djgrunt', 'Enemy', 16, 41),
('emob5', 'djgrunt', 'Enemy', 17, 41),
('mid1', 'hornpit', 'Enemy', 30, 36),
('mid2', 'spinner', 'Enemy', 31, 34),
],
player_faction='stream', enemy_faction='vinylgod', enemy_bot=True, scripts='m06.lua',
briefing=('Deprecation notice.\\n\\nThe Party has built a Mastering Studio. The platform classifies this as a '
'competing distribution channel.\\n\\nBring The Algorithm online and issue the correction.'),
)