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>
189 lines
7.0 KiB
Python
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.'),
|
|
)
|