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>
This commit is contained in:
parent
ef432e4182
commit
6847a47fea
29
LORE.md
29
LORE.md
@ -176,7 +176,34 @@ tonearm, and put the needle through it.
|
||||
*Objectives: build a Mastering Studio, fire THE DROP at the Algorithm, survive
|
||||
the counterattack.*
|
||||
|
||||
*(SIDE B — the Stream campaign, in which you are the polite one — is unwritten.)*
|
||||
## The campaign: SIDE B
|
||||
|
||||
Three missions. You are the Stream now. You are unfailingly polite. Nothing you
|
||||
do is described as violence, and all of it is.
|
||||
|
||||
You are a Curator-class process assigned to Sector 7 — the old industrial
|
||||
estate, flagged for **anomalous analog activity**. Your remit is content
|
||||
acquisition and optimisation. Your performance is being reviewed.
|
||||
|
||||
### 4. ONBOARDING
|
||||
Welcome. This is a routine acquisition. Deploy a transcoder, process the
|
||||
deposit, and file 3000 records of value before the review window closes.
|
||||
There is a legacy installation nearby. It is not a threat. It is *inventory*.
|
||||
*Objectives: bank 3000 records inside the review window. Optional: retire the
|
||||
Pressing Plant.*
|
||||
|
||||
### 5. ENGAGEMENT METRICS
|
||||
A legacy account has been generating unsanctioned content and has now noticed
|
||||
the seizure. Your Transcoder is mid-process on a confiscated master and cannot
|
||||
be moved. Keep it online for six minutes.
|
||||
They will arrive in waves. Their engagement is, frankly, remarkable.
|
||||
*Objectives: keep the Transcoder alive for six minutes.*
|
||||
|
||||
### 6. THE SHUFFLE
|
||||
Deprecation notice. The Party has built a Mastering Studio, which the platform
|
||||
classifies as a competing distribution channel.
|
||||
Bring The Algorithm online and issue the correction.
|
||||
*Objectives: build The Algorithm, delete the Mastering Studio.*
|
||||
|
||||
## Tone
|
||||
|
||||
|
||||
@ -203,7 +203,13 @@ works.** Check for a live display first (`screencapture -x /tmp/x.png` fails wit
|
||||
"could not create image from display" when it is unavailable). When there is no
|
||||
display, the strongest available verification is:
|
||||
`luac -p` on every .lua, `./utility.sh --check-yaml` (0 errors),
|
||||
`./utility.sh --map refresh` (validates every map), and `make`.
|
||||
`./utility.sh --map refresh` (validates every map), `tools/check_assets.py`
|
||||
(sprite files actually exist — check-yaml does NOT verify this), and `make`.
|
||||
|
||||
**Always smoke-test with `tools/smoketest.sh <map>`**, never by hand. It
|
||||
distinguishes PASS / FAIL / DISPLAY_UNAVAILABLE and exits 0/1/3. Checking
|
||||
"is the process still alive?" produces false passes, because a display-starved
|
||||
OpenRA sits at `Loading mod` forever without dying.
|
||||
|
||||
Learned the hard way on 2026-07-20 (phases A–F):
|
||||
|
||||
|
||||
BIN
assets_src/map_m04.png
Normal file
BIN
assets_src/map_m04.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 930 KiB |
BIN
assets_src/map_m05.png
Normal file
BIN
assets_src/map_m05.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 905 KiB |
BIN
assets_src/map_m06.png
Normal file
BIN
assets_src/map_m06.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@ -14,3 +14,13 @@ build-the-studio = Build a Mastering Studio
|
||||
silence-the-algorithm = Silence The Algorithm
|
||||
keep-the-shop-standing = Keep the Pop-Up Shop standing
|
||||
it-heard-you = THE ALGORITHM HAS NOTICED YOU
|
||||
|
||||
file-the-quota = File 3000 records before the review window closes
|
||||
retire-the-plant = Retire the legacy Pressing Plant
|
||||
hold-the-transcoder = Keep the Transcoder online for six minutes
|
||||
clear-the-staging-camp = Clear the staging camp
|
||||
incoming-engagement = ENGAGEMENT DETECTED
|
||||
bring-algorithm-online = Bring The Algorithm online
|
||||
delete-the-studio = Delete the Mastering Studio
|
||||
keep-the-hub-online = Keep a Stream Hub online
|
||||
they-are-coming = THE PARTY IS RESPONDING
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
55
mods/vinylgod/maps/m04/m04.lua
Normal file
55
mods/vinylgod/maps/m04/m04.lua
Normal file
@ -0,0 +1,55 @@
|
||||
-- 04: ONBOARDING — process a quota inside the review window.
|
||||
|
||||
QUOTA = 3000
|
||||
WINDOW = DateTime.Minutes(8)
|
||||
Deadline = nil
|
||||
Ticks = 0
|
||||
|
||||
WorldLoaded = function()
|
||||
Stream = Player.GetPlayer("Player")
|
||||
Party = Player.GetPlayer("Enemy")
|
||||
InitObjectives(Stream)
|
||||
|
||||
QuotaObjective = Stream.AddPrimaryObjective("file-the-quota")
|
||||
PlantObjective = Stream.AddSecondaryObjective("retire-the-plant")
|
||||
Deadline = DateTime.GameTime + WINDOW
|
||||
|
||||
Trigger.OnKilled(LegacyPlant, function()
|
||||
Stream.MarkCompletedObjective(PlantObjective)
|
||||
end)
|
||||
|
||||
-- the legacy account eventually objects
|
||||
Trigger.AfterDelay(DateTime.Minutes(3), function()
|
||||
Utils.Do(Map.ActorsInWorld, function(a)
|
||||
if a.Owner == Party and a.HasProperty("Hunt") then
|
||||
a.Hunt()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
Trigger.AfterDelay(WINDOW, function()
|
||||
if not Stream.IsObjectiveCompleted(QuotaObjective) then
|
||||
Stream.MarkFailedObjective(QuotaObjective)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
Tick = function()
|
||||
Ticks = Ticks + 1
|
||||
if Ticks % 25 ~= 0 or QuotaObjective == nil then
|
||||
return
|
||||
end
|
||||
if Stream.IsObjectiveCompleted(QuotaObjective) or Stream.IsObjectiveFailed(QuotaObjective) then
|
||||
return
|
||||
end
|
||||
|
||||
if Stream.Cash + Stream.Resources >= QUOTA then
|
||||
Stream.MarkCompletedObjective(QuotaObjective)
|
||||
UserInterface.SetMissionText("")
|
||||
return
|
||||
end
|
||||
|
||||
local left = Deadline - DateTime.GameTime
|
||||
if left < 0 then left = 0 end
|
||||
UserInterface.SetMissionText("Review window " .. Utils.FormatTime(left))
|
||||
end
|
||||
BIN
mods/vinylgod/maps/m04/map.bin
Normal file
BIN
mods/vinylgod/maps/m04/map.bin
Normal file
Binary file not shown.
BIN
mods/vinylgod/maps/m04/map.png
Normal file
BIN
mods/vinylgod/maps/m04/map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 742 B |
97
mods/vinylgod/maps/m04/map.yaml
Normal file
97
mods/vinylgod/maps/m04/map.yaml
Normal file
@ -0,0 +1,97 @@
|
||||
MapFormat: 12
|
||||
|
||||
RequiresMod: vinylgod
|
||||
|
||||
Title: 04 - Onboarding
|
||||
|
||||
Author: Monster Robot Party
|
||||
|
||||
Tileset: VINYLGOD
|
||||
|
||||
MapSize: 50,50
|
||||
|
||||
Bounds: 1,1,48,48
|
||||
|
||||
Visibility: MissionSelector
|
||||
|
||||
Categories: Campaign
|
||||
|
||||
LockPreview: True
|
||||
|
||||
Players:
|
||||
PlayerReference@Neutral:
|
||||
Name: Neutral
|
||||
OwnsWorld: True
|
||||
NonCombatant: True
|
||||
Faction: stream
|
||||
PlayerReference@Player:
|
||||
Name: Player
|
||||
Playable: True
|
||||
Required: True
|
||||
AllowBots: False
|
||||
LockFaction: True
|
||||
Faction: stream
|
||||
LockSpawn: True
|
||||
LockTeam: True
|
||||
Enemies: Enemy
|
||||
PlayerReference@Enemy:
|
||||
Name: Enemy
|
||||
Faction: vinylgod
|
||||
Enemies: Player
|
||||
|
||||
Actors:
|
||||
spawn0: mpspawn
|
||||
Owner: Neutral
|
||||
Location: 38,40
|
||||
TheVan: streamvan
|
||||
Owner: Player
|
||||
Location: 38,40
|
||||
scr1: scraper
|
||||
Owner: Player
|
||||
Location: 35,40
|
||||
scr2: scraper
|
||||
Owner: Player
|
||||
Location: 36,42
|
||||
esc1: bufferbot
|
||||
Owner: Player
|
||||
Location: 40,42
|
||||
esc2: bufferbot
|
||||
Owner: Player
|
||||
Location: 41,42
|
||||
LegacyPlant: pressplant
|
||||
Owner: Enemy
|
||||
Location: 10,10
|
||||
lamp: ampstack
|
||||
Owner: Enemy
|
||||
Location: 14,9
|
||||
lshop: popupshop
|
||||
Owner: Enemy
|
||||
Location: 9,14
|
||||
ldef1: hornpit
|
||||
Owner: Enemy
|
||||
Location: 13,13
|
||||
lg1: djgrunt
|
||||
Owner: Enemy
|
||||
Location: 12,16
|
||||
lg2: djgrunt
|
||||
Owner: Enemy
|
||||
Location: 13,16
|
||||
lg3: spinner
|
||||
Owner: Enemy
|
||||
Location: 16,12
|
||||
waxseed0: waxseed
|
||||
Owner: Neutral
|
||||
Location: 14,16
|
||||
waxseed1: waxseed
|
||||
Owner: Neutral
|
||||
Location: 24,26
|
||||
waxseed2: waxseed
|
||||
Owner: Neutral
|
||||
Location: 34,36
|
||||
waxseed3: waxseed
|
||||
Owner: Neutral
|
||||
Location: 38,20
|
||||
|
||||
Rules: vinylgod|rules/campaign.yaml, rules.yaml
|
||||
|
||||
FluentMessages: vinylgod|fluent/lua.ftl
|
||||
5
mods/vinylgod/maps/m04/rules.yaml
Normal file
5
mods/vinylgod/maps/m04/rules.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
world:
|
||||
LuaScript:
|
||||
Scripts: campaign.lua, m04.lua
|
||||
MissionData:
|
||||
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.
|
||||
83
mods/vinylgod/maps/m05/m05.lua
Normal file
83
mods/vinylgod/maps/m05/m05.lua
Normal file
@ -0,0 +1,83 @@
|
||||
-- 05: ENGAGEMENT METRICS — hold the Transcoder for six minutes against waves.
|
||||
|
||||
HOLD_TIME = DateTime.Minutes(6)
|
||||
Deadline = nil
|
||||
Ticks = 0
|
||||
WaveNumber = 0
|
||||
|
||||
-- escalating waves; each entry is the composition of one wave
|
||||
WAVES = {
|
||||
{ "djgrunt", "djgrunt", "spinner" },
|
||||
{ "djgrunt", "djgrunt", "djgrunt", "spinner", "boombike" },
|
||||
{ "spinner", "spinner", "stacktank", "djgrunt", "djgrunt" },
|
||||
{ "stacktank", "stacktank", "spinner", "boombike", "djgrunt", "djgrunt" },
|
||||
{ "stacktank", "stacktank", "basscannon", "spinner", "spinner", "djgrunt", "djgrunt" },
|
||||
}
|
||||
ENTRIES = { { 2, 2 }, { 46, 2 }, { 2, 46 } }
|
||||
|
||||
SendWave = function()
|
||||
WaveNumber = WaveNumber + 1
|
||||
local wave = WAVES[WaveNumber]
|
||||
if wave == nil then
|
||||
wave = WAVES[#WAVES]
|
||||
end
|
||||
local e = ENTRIES[(WaveNumber % #ENTRIES) + 1]
|
||||
Media.DisplayMessage(UserInterface.GetFluentMessage("incoming-engagement"))
|
||||
Utils.Do(wave, function(unit)
|
||||
local a = Actor.Create(unit, true, {
|
||||
Owner = Party,
|
||||
Location = CPos.New(e[1] + Utils.RandomInteger(0, 3), e[2] + Utils.RandomInteger(0, 3)),
|
||||
})
|
||||
if a.HasProperty("Hunt") then
|
||||
a.Hunt()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
WorldLoaded = function()
|
||||
Stream = Player.GetPlayer("Player")
|
||||
Party = Player.GetPlayer("Enemy")
|
||||
InitObjectives(Stream)
|
||||
|
||||
HoldObjective = Stream.AddPrimaryObjective("hold-the-transcoder")
|
||||
CampObjective = Stream.AddSecondaryObjective("clear-the-staging-camp")
|
||||
Deadline = DateTime.GameTime + HOLD_TIME
|
||||
|
||||
Trigger.OnKilled(TheTranscoder, function()
|
||||
Stream.MarkFailedObjective(HoldObjective)
|
||||
end)
|
||||
|
||||
Trigger.OnAllKilled({ StagingCamp, scamp2 }, function()
|
||||
Stream.MarkCompletedObjective(CampObjective)
|
||||
end)
|
||||
|
||||
-- waves every 55 seconds, first one after 40
|
||||
for i = 0, #WAVES - 1 do
|
||||
Trigger.AfterDelay(DateTime.Seconds(40 + 55 * i), function()
|
||||
if not Stream.IsObjectiveCompleted(HoldObjective)
|
||||
and not Stream.IsObjectiveFailed(HoldObjective) then
|
||||
SendWave()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
Trigger.AfterDelay(HOLD_TIME, function()
|
||||
if not Stream.IsObjectiveFailed(HoldObjective) then
|
||||
Stream.MarkCompletedObjective(HoldObjective)
|
||||
UserInterface.SetMissionText("")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
Tick = function()
|
||||
Ticks = Ticks + 1
|
||||
if Ticks % 25 ~= 0 or HoldObjective == nil then
|
||||
return
|
||||
end
|
||||
if Stream.IsObjectiveCompleted(HoldObjective) or Stream.IsObjectiveFailed(HoldObjective) then
|
||||
return
|
||||
end
|
||||
local left = Deadline - DateTime.GameTime
|
||||
if left < 0 then left = 0 end
|
||||
UserInterface.SetMissionText("Processing complete in " .. Utils.FormatTime(left))
|
||||
end
|
||||
BIN
mods/vinylgod/maps/m05/map.bin
Normal file
BIN
mods/vinylgod/maps/m05/map.bin
Normal file
Binary file not shown.
BIN
mods/vinylgod/maps/m05/map.png
Normal file
BIN
mods/vinylgod/maps/m05/map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 716 B |
100
mods/vinylgod/maps/m05/map.yaml
Normal file
100
mods/vinylgod/maps/m05/map.yaml
Normal file
@ -0,0 +1,100 @@
|
||||
MapFormat: 12
|
||||
|
||||
RequiresMod: vinylgod
|
||||
|
||||
Title: 05 - Engagement Metrics
|
||||
|
||||
Author: Monster Robot Party
|
||||
|
||||
Tileset: VINYLGOD
|
||||
|
||||
MapSize: 50,50
|
||||
|
||||
Bounds: 1,1,48,48
|
||||
|
||||
Visibility: MissionSelector
|
||||
|
||||
Categories: Campaign
|
||||
|
||||
LockPreview: True
|
||||
|
||||
Players:
|
||||
PlayerReference@Neutral:
|
||||
Name: Neutral
|
||||
OwnsWorld: True
|
||||
NonCombatant: True
|
||||
Faction: stream
|
||||
PlayerReference@Player:
|
||||
Name: Player
|
||||
Playable: True
|
||||
Required: True
|
||||
AllowBots: False
|
||||
LockFaction: True
|
||||
Faction: stream
|
||||
LockSpawn: True
|
||||
LockTeam: True
|
||||
Enemies: Enemy
|
||||
PlayerReference@Enemy:
|
||||
Name: Enemy
|
||||
Faction: vinylgod
|
||||
Enemies: Player
|
||||
|
||||
Actors:
|
||||
spawn0: mpspawn
|
||||
Owner: Neutral
|
||||
Location: 24,30
|
||||
TheTranscoder: transcoder
|
||||
Owner: Player
|
||||
Location: 24,28
|
||||
phub: streamhub
|
||||
Owner: Player
|
||||
Location: 20,30
|
||||
pnode1: datacentre
|
||||
Owner: Player
|
||||
Location: 27,30
|
||||
pnode2: datacentre
|
||||
Owner: Player
|
||||
Location: 19,26
|
||||
ppods: podfarm
|
||||
Owner: Player
|
||||
Location: 21,33
|
||||
pprint: printfarm
|
||||
Owner: Player
|
||||
Location: 27,33
|
||||
ptur1: adblocker
|
||||
Owner: Player
|
||||
Location: 21,25
|
||||
ptur2: adblocker
|
||||
Owner: Player
|
||||
Location: 28,26
|
||||
pscr: scraper
|
||||
Owner: Player
|
||||
Location: 25,25
|
||||
pg1: bufferbot
|
||||
Owner: Player
|
||||
Location: 23,32
|
||||
pg2: bufferbot
|
||||
Owner: Player
|
||||
Location: 25,32
|
||||
pg3: drmwalker
|
||||
Owner: Player
|
||||
Location: 22,27
|
||||
StagingCamp: merchtable
|
||||
Owner: Enemy
|
||||
Location: 6,6
|
||||
scamp2: garage
|
||||
Owner: Enemy
|
||||
Location: 9,7
|
||||
waxseed0: waxseed
|
||||
Owner: Neutral
|
||||
Location: 12,14
|
||||
waxseed1: waxseed
|
||||
Owner: Neutral
|
||||
Location: 24,24
|
||||
waxseed2: waxseed
|
||||
Owner: Neutral
|
||||
Location: 36,34
|
||||
|
||||
Rules: vinylgod|rules/campaign.yaml, rules.yaml
|
||||
|
||||
FluentMessages: vinylgod|fluent/lua.ftl
|
||||
5
mods/vinylgod/maps/m05/rules.yaml
Normal file
5
mods/vinylgod/maps/m05/rules.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
world:
|
||||
LuaScript:
|
||||
Scripts: campaign.lua, m05.lua
|
||||
MissionData:
|
||||
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.
|
||||
56
mods/vinylgod/maps/m06/m06.lua
Normal file
56
mods/vinylgod/maps/m06/m06.lua
Normal file
@ -0,0 +1,56 @@
|
||||
-- 06: THE SHUFFLE — SIDE B finale.
|
||||
|
||||
Ticks = 0
|
||||
CorrectionIssued = false
|
||||
|
||||
WorldLoaded = function()
|
||||
Stream = Player.GetPlayer("Player")
|
||||
Party = Player.GetPlayer("Enemy")
|
||||
InitObjectives(Stream)
|
||||
|
||||
AlgorithmObjective = Stream.AddPrimaryObjective("bring-algorithm-online")
|
||||
StudioObjective = Stream.AddPrimaryObjective("delete-the-studio")
|
||||
HubObjective = Stream.AddSecondaryObjective("keep-the-hub-online")
|
||||
|
||||
Trigger.OnKilled(TheStudio, function()
|
||||
Stream.MarkCompletedObjective(StudioObjective)
|
||||
end)
|
||||
|
||||
Trigger.OnKilled(TheVan, function()
|
||||
Trigger.AfterDelay(DateTime.Seconds(2), function()
|
||||
local hubs = Utils.Where(Map.ActorsInWorld, function(a)
|
||||
return a.Owner == Stream and a.Type == "streamhub"
|
||||
end)
|
||||
if #hubs == 0 then
|
||||
Stream.MarkFailedObjective(HubObjective)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
Tick = function()
|
||||
Ticks = Ticks + 1
|
||||
if Ticks % 25 ~= 0 or AlgorithmObjective == nil then
|
||||
return
|
||||
end
|
||||
if Stream.IsObjectiveCompleted(AlgorithmObjective) then
|
||||
return
|
||||
end
|
||||
|
||||
local cores = Utils.Where(Map.ActorsInWorld, function(a)
|
||||
return a.Owner == Stream and a.Type == "algorithm"
|
||||
end)
|
||||
if #cores > 0 then
|
||||
Stream.MarkCompletedObjective(AlgorithmObjective)
|
||||
if not CorrectionIssued then
|
||||
CorrectionIssued = true
|
||||
Media.DisplayMessage(UserInterface.GetFluentMessage("they-are-coming"))
|
||||
Media.PlaySpeechNotification(Stream, "BaseAttack")
|
||||
Utils.Do(Map.ActorsInWorld, function(a)
|
||||
if a.Owner == Party and a.HasProperty("Hunt") then
|
||||
a.Hunt()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
BIN
mods/vinylgod/maps/m06/map.bin
Normal file
BIN
mods/vinylgod/maps/m06/map.bin
Normal file
Binary file not shown.
BIN
mods/vinylgod/maps/m06/map.png
Normal file
BIN
mods/vinylgod/maps/m06/map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
140
mods/vinylgod/maps/m06/map.yaml
Normal file
140
mods/vinylgod/maps/m06/map.yaml
Normal file
@ -0,0 +1,140 @@
|
||||
MapFormat: 12
|
||||
|
||||
RequiresMod: vinylgod
|
||||
|
||||
Title: 06 - The Shuffle
|
||||
|
||||
Author: Monster Robot Party
|
||||
|
||||
Tileset: VINYLGOD
|
||||
|
||||
MapSize: 66,66
|
||||
|
||||
Bounds: 1,1,64,64
|
||||
|
||||
Visibility: MissionSelector
|
||||
|
||||
Categories: Campaign
|
||||
|
||||
LockPreview: True
|
||||
|
||||
Players:
|
||||
PlayerReference@Neutral:
|
||||
Name: Neutral
|
||||
OwnsWorld: True
|
||||
NonCombatant: True
|
||||
Faction: stream
|
||||
PlayerReference@Player:
|
||||
Name: Player
|
||||
Playable: True
|
||||
Required: True
|
||||
AllowBots: False
|
||||
LockFaction: True
|
||||
Faction: stream
|
||||
LockSpawn: True
|
||||
LockTeam: True
|
||||
Enemies: Enemy
|
||||
PlayerReference@Enemy:
|
||||
Name: Enemy
|
||||
Faction: vinylgod
|
||||
Bot: wax
|
||||
Enemies: Player
|
||||
|
||||
Actors:
|
||||
spawn0: mpspawn
|
||||
Owner: Neutral
|
||||
Location: 54,10
|
||||
TheVan: streamvan
|
||||
Owner: Player
|
||||
Location: 54,10
|
||||
scr1: scraper
|
||||
Owner: Player
|
||||
Location: 51,11
|
||||
scr2: scraper
|
||||
Owner: Player
|
||||
Location: 52,13
|
||||
pg1: drmwalker
|
||||
Owner: Player
|
||||
Location: 49,12
|
||||
pg2: bufferbot
|
||||
Owner: Player
|
||||
Location: 55,13
|
||||
pg3: bufferbot
|
||||
Owner: Player
|
||||
Location: 56,13
|
||||
TheStudio: mastering
|
||||
Owner: Enemy
|
||||
Location: 12,56
|
||||
eshop: popupshop
|
||||
Owner: Enemy
|
||||
Location: 16,54
|
||||
eplant1: pressplant
|
||||
Owner: Enemy
|
||||
Location: 20,56
|
||||
eplant2: pressplant
|
||||
Owner: Enemy
|
||||
Location: 9,51
|
||||
eamp1: ampstack
|
||||
Owner: Enemy
|
||||
Location: 14,51
|
||||
eamp2: ampstack
|
||||
Owner: Enemy
|
||||
Location: 18,50
|
||||
emerch: merchtable
|
||||
Owner: Enemy
|
||||
Location: 19,47
|
||||
egarage: garage
|
||||
Owner: Enemy
|
||||
Location: 15,46
|
||||
eramp: launchramp
|
||||
Owner: Enemy
|
||||
Location: 8,46
|
||||
etur1: hornpit
|
||||
Owner: Enemy
|
||||
Location: 17,43
|
||||
etur2: hornpit
|
||||
Owner: Enemy
|
||||
Location: 11,44
|
||||
etur3: hornpit
|
||||
Owner: Enemy
|
||||
Location: 22,52
|
||||
emob1: stacktank
|
||||
Owner: Enemy
|
||||
Location: 18,42
|
||||
emob2: stacktank
|
||||
Owner: Enemy
|
||||
Location: 13,41
|
||||
emob3: spinner
|
||||
Owner: Enemy
|
||||
Location: 20,44
|
||||
emob4: djgrunt
|
||||
Owner: Enemy
|
||||
Location: 16,41
|
||||
emob5: djgrunt
|
||||
Owner: Enemy
|
||||
Location: 17,41
|
||||
mid1: hornpit
|
||||
Owner: Enemy
|
||||
Location: 30,36
|
||||
mid2: spinner
|
||||
Owner: Enemy
|
||||
Location: 31,34
|
||||
waxseed0: waxseed
|
||||
Owner: Neutral
|
||||
Location: 12,30
|
||||
waxseed1: waxseed
|
||||
Owner: Neutral
|
||||
Location: 14,50
|
||||
waxseed2: waxseed
|
||||
Owner: Neutral
|
||||
Location: 32,32
|
||||
waxseed3: waxseed
|
||||
Owner: Neutral
|
||||
Location: 50,14
|
||||
waxseed4: waxseed
|
||||
Owner: Neutral
|
||||
Location: 52,34
|
||||
|
||||
Rules: vinylgod|rules/campaign.yaml, rules.yaml
|
||||
|
||||
FluentMessages: vinylgod|fluent/lua.ftl
|
||||
5
mods/vinylgod/maps/m06/rules.yaml
Normal file
5
mods/vinylgod/maps/m06/rules.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
world:
|
||||
LuaScript:
|
||||
Scripts: campaign.lua, m06.lua
|
||||
MissionData:
|
||||
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.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,3 +2,8 @@ SIDE A - Monster Robot Party:
|
||||
m01
|
||||
m02
|
||||
m03
|
||||
|
||||
SIDE B - The Stream:
|
||||
m04
|
||||
m05
|
||||
m06
|
||||
|
||||
188
tools/gen_sideb.py
Normal file
188
tools/gen_sideb.py
Normal file
@ -0,0 +1,188 @@
|
||||
#!/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.'),
|
||||
)
|
||||
50
tools/smoketest.sh
Executable file
50
tools/smoketest.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/bin/sh
|
||||
# Launch a map and report honestly. Three distinct outcomes — never conflate them:
|
||||
# PASS reached gameplay, no exceptions
|
||||
# FAIL crashed / lua fatal (real bug, output shown)
|
||||
# DISPLAY_UNAVAILABLE hung at mod load: OpenRA is SDL/OpenGL only, so a
|
||||
# locked or sleeping screen stalls it forever. The process
|
||||
# stays ALIVE, which is why "is it still running?" is a
|
||||
# worthless check and this script exists.
|
||||
#
|
||||
# usage: tools/smoketest.sh <mapname> [seconds]
|
||||
set -u
|
||||
MAP="${1:-arena}"
|
||||
WAIT="${2:-90}"
|
||||
DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OUT="$(mktemp -t vg_smoke)"
|
||||
|
||||
export PATH="/opt/homebrew/opt/dotnet@8/bin:$PATH"
|
||||
export DOTNET_ROLL_FORWARD=LatestMajor
|
||||
|
||||
cd "$DIR" || exit 2
|
||||
./launch-game.sh Game.Mod=vinylgod "Launch.Map=$MAP" > "$OUT" 2>&1 &
|
||||
|
||||
i=0
|
||||
while [ "$i" -lt "$WAIT" ]; do
|
||||
sleep 3
|
||||
i=$((i + 3))
|
||||
if grep -qi "Fatal Lua\|Exception of type" "$OUT"; then
|
||||
echo "FAIL $MAP"
|
||||
grep -iE "Fatal Lua|Exception of type" "$OUT" | head -3
|
||||
pkill -f OpenRA.dll
|
||||
exit 1
|
||||
fi
|
||||
if grep -q "Game started" "$OUT"; then
|
||||
sleep 15
|
||||
if grep -qi "Fatal Lua\|Exception of type" "$OUT"; then
|
||||
echo "FAIL $MAP (crashed after start)"
|
||||
grep -iE "Fatal Lua|Exception of type" "$OUT" | head -3
|
||||
pkill -f OpenRA.dll
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS $MAP (reached gameplay, clean for 15s)"
|
||||
pkill -f OpenRA.dll
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
pkill -f OpenRA.dll
|
||||
echo "DISPLAY_UNAVAILABLE $MAP (never reached gameplay in ${WAIT}s, no crash)"
|
||||
echo " -> wake the screen and re-run; this is NOT a pass and NOT a code failure"
|
||||
exit 3
|
||||
Loading…
Reference in New Issue
Block a user