Mission 02: CRATE DIGGERS — timed raid with no base building

Strike force + The Selector against a Stream transcoder outpost; 6-minute
countdown shown via SetMissionText, master lacquer is cut (killed) if the timer
beats you. Adds the lacquer prop (deliberately untargetable) and ScriptTriggers
on all actor templates, without which Trigger.OnKilled throws at map load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-25 23:11:20 +10:00
parent e11ec402ff
commit c09da56dbe
24 changed files with 343 additions and 0 deletions

BIN
assets_src/m02_outpost.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

BIN
assets_src/map_m02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 KiB

View File

@ -5,3 +5,7 @@ objective-failed = Objective failed
press-first-records = Build a Pressing Plant and bank 2000 records
survive-the-probe = Survive the Stream's survey probe
keep-the-van-alive = Do not lose the Pop-Up Shop
destroy-the-transcoder = Destroy the Transcoder before the master is cut
keep-the-selector-alive = The Selector must survive
wipe-the-outpost = Level the whole outpost

View File

@ -107,3 +107,5 @@ actor-droneport =
.name = Drone Port
actor-waxseed =
.name = Wax Seep
actor-lacquer =
.name = Master Lacquer

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,57 @@
-- 02: CRATE DIGGERS — timed raid, no base building.
TIME_LIMIT = DateTime.Minutes(6)
Deadline = nil
WorldLoaded = function()
Mrp = Player.GetPlayer("Player")
Stream = Player.GetPlayer("Enemy")
InitObjectives(Mrp)
KillTranscoder = Mrp.AddPrimaryObjective("destroy-the-transcoder")
KeepSelector = Mrp.AddPrimaryObjective("keep-the-selector-alive")
WipeOutpost = Mrp.AddSecondaryObjective("wipe-the-outpost")
Deadline = DateTime.GameTime + TIME_LIMIT
Trigger.OnKilled(Transcoder, function()
if not Mrp.IsObjectiveCompleted(KillTranscoder) then
Mrp.MarkCompletedObjective(KillTranscoder)
Mrp.MarkCompletedObjective(KeepSelector)
UserInterface.SetMissionText("")
end
end)
Trigger.OnKilled(TheSelector, function()
Mrp.MarkFailedObjective(KeepSelector)
end)
local outpost = { Transcoder, outpost1, outpost2, turret1, turret2 }
Trigger.OnAllKilled(outpost, function()
Mrp.MarkCompletedObjective(WipeOutpost)
end)
-- the deadline: the master gets cut and the raid was for nothing
Trigger.AfterDelay(TIME_LIMIT, function()
if not Mrp.IsObjectiveCompleted(KillTranscoder) then
if not MasterLacquer.IsDead then
MasterLacquer.Kill()
end
Media.PlaySpeechNotification(Mrp, "Lose")
Mrp.MarkFailedObjective(KillTranscoder)
end
end)
end
Tick = function()
if Deadline ~= nil and not Mrp.IsObjectiveCompleted(KillTranscoder)
and not Mrp.IsObjectiveFailed(KillTranscoder) then
local left = Deadline - DateTime.GameTime
if left < 0 then
left = 0
end
if left % 25 == 0 then
UserInterface.SetMissionText("Master cut in " .. Utils.FormatTime(left))
end
end
end

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

View File

@ -0,0 +1,127 @@
MapFormat: 12
RequiresMod: vinylgod
Title: 02 - Crate Diggers
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: vinylgod
PlayerReference@Player:
Name: Player
Playable: True
Required: True
AllowBots: False
LockFaction: True
Faction: vinylgod
LockSpawn: True
LockTeam: True
Enemies: Enemy
PlayerReference@Enemy:
Name: Enemy
Faction: stream
Enemies: Player
Actors:
spawn0: mpspawn
Owner: Neutral
Location: 8,40
TheSelector: selector
Owner: Player
Location: 8,40
strike1: stacktank
Owner: Player
Location: 10,41
strike2: spinner
Owner: Player
Location: 7,42
strike3: spinner
Owner: Player
Location: 11,39
strike4: djgrunt
Owner: Player
Location: 9,43
strike5: djgrunt
Owner: Player
Location: 10,43
strike6: djgrunt
Owner: Player
Location: 8,44
strike7: roadie
Owner: Player
Location: 6,41
strike8: boombike
Owner: Player
Location: 12,42
Transcoder: transcoder
Owner: Enemy
Location: 38,9
MasterLacquer: lacquer
Owner: Neutral
Location: 41,10
outpost1: datacentre
Owner: Enemy
Location: 35,12
outpost2: podfarm
Owner: Enemy
Location: 41,13
turret1: adblocker
Owner: Enemy
Location: 34,9
turret2: adblocker
Owner: Enemy
Location: 38,14
guard1: bufferbot
Owner: Enemy
Location: 36,15
guard2: bufferbot
Owner: Enemy
Location: 37,15
guard3: bufferbot
Owner: Enemy
Location: 39,16
guard4: drmwalker
Owner: Enemy
Location: 34,16
guard5: crawler
Owner: Enemy
Location: 30,20
picket1: bufferbot
Owner: Enemy
Location: 24,28
picket2: bufferbot
Owner: Enemy
Location: 25,29
picket3: adblocker
Owner: Enemy
Location: 22,30
waxseed0: waxseed
Owner: Neutral
Location: 12,40
waxseed1: waxseed
Owner: Neutral
Location: 26,26
waxseed2: waxseed
Owner: Neutral
Location: 40,20
Rules: vinylgod|rules/campaign.yaml, rules.yaml
FluentMessages: vinylgod|fluent/lua.ftl

View File

@ -0,0 +1,5 @@
world:
LuaScript:
Scripts: campaign.lua, m02.lua
MissionData:
Briefing: A Stream transcoder outpost has found something under the hill: a master lacquer. The original of an original. The only one.\n\nIn six minutes it will be a receipt.\n\nTake the outpost apart. No van, no plant, no reinforcements - what you brought is what you have.\n\nThe Selector does not get to die out here.

Binary file not shown.

Binary file not shown.

View File

@ -1,2 +1,3 @@
SIDE A - Monster Robot Party:
m01
m02

View File

@ -275,6 +275,7 @@ boombike:
# hit them. Upgrade path: dedicated AA armaments + AirborneActor-only targeting.
^AirUnit:
ScriptTriggers:
BodyOrientation:
QuantizeFacingsFromSequence:
RenderSprites:

View File

@ -1,4 +1,5 @@
^EconSprite:
ScriptTriggers:
BodyOrientation:
QuantizeFacingsFromSequence:
RenderSprites:
@ -288,3 +289,26 @@ waxseed:
MaxRange: 60
MapEditorData:
Categories: Resource spawn
# The master lacquer — mission prop. Deliberately not Targetable: the player
# must not be able to shoot the thing they came to rescue.
lacquer:
ScriptTriggers:
BodyOrientation:
QuantizeFacingsFromSequence:
RenderSprites:
WithSpriteBody:
Interactable:
HiddenUnderShroud:
Building:
Footprint: x
Dimensions: 1,1
TerrainTypes: Clear, Wax
Tooltip:
Name: actor-lacquer.name
AppearsOnRadar:
HitShape:
Health:
HP: 1000
MapEditorData:
Categories: Props

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -269,3 +269,7 @@ compressor:
waxseed:
idle:
Filename: sequences/assets/waxseed.png
lacquer:
idle:
Filename: sequences/assets/lacquer.png

118
tools/gen_mission2.py Normal file
View File

@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""Generate campaign mission 02 — CRATE DIGGERS (timed raid, no base building)."""
import os, struct
from PIL import Image
SIZE = 48
W = H = SIZE + 2
MAP = os.path.join(os.path.dirname(__file__), '..', 'mods', 'vinylgod', 'maps', 'm02')
os.makedirs(MAP, exist_ok=True)
WAX = [(12, 40, 4), (26, 26, 5), (40, 20, 3)]
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):
d2 = (x - cx) ** 2 + (y - cy) ** 2
if d2 <= 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 - (d2 ** 0.5) / (r + 1))))
data += res
open(os.path.join(MAP, 'map.bin'), 'wb').write(data)
# names used as lua globals must not collide with engine globals
ACTORS = [
('spawn0', 'mpspawn', 'Neutral', 8, 40),
# strike force
('TheSelector', 'selector', 'Player', 8, 40),
('strike1', 'stacktank', 'Player', 10, 41),
('strike2', 'spinner', 'Player', 7, 42),
('strike3', 'spinner', 'Player', 11, 39),
('strike4', 'djgrunt', 'Player', 9, 43),
('strike5', 'djgrunt', 'Player', 10, 43),
('strike6', 'djgrunt', 'Player', 8, 44),
('strike7', 'roadie', 'Player', 6, 41),
('strike8', 'boombike', 'Player', 12, 42),
# the outpost
('Transcoder', 'transcoder', 'Enemy', 38, 9),
('MasterLacquer', 'lacquer', 'Neutral', 41, 10),
('outpost1', 'datacentre', 'Enemy', 35, 12),
('outpost2', 'podfarm', 'Enemy', 41, 13),
('turret1', 'adblocker', 'Enemy', 34, 9),
('turret2', 'adblocker', 'Enemy', 38, 14),
('guard1', 'bufferbot', 'Enemy', 36, 15),
('guard2', 'bufferbot', 'Enemy', 37, 15),
('guard3', 'bufferbot', 'Enemy', 39, 16),
('guard4', 'drmwalker', 'Enemy', 34, 16),
('guard5', 'crawler', 'Enemy', 30, 20),
# a picket on the approach
('picket1', 'bufferbot', 'Enemy', 24, 28),
('picket2', 'bufferbot', 'Enemy', 25, 29),
('picket3', 'adblocker', 'Enemy', 22, 30),
]
actors = '\n'.join(f'\t{n}: {t}\n\t\tOwner: {o}\n\t\tLocation: {x},{y}' for n, t, o, x, y in ACTORS)
open(os.path.join(MAP, 'map.yaml'), 'w').write(f"""MapFormat: 12
RequiresMod: vinylgod
Title: 02 - Crate Diggers
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: vinylgod
PlayerReference@Player:
Name: Player
Playable: True
Required: True
AllowBots: False
LockFaction: True
Faction: vinylgod
LockSpawn: True
LockTeam: True
Enemies: Enemy
PlayerReference@Enemy:
Name: Enemy
Faction: stream
Enemies: Player
Actors:
{actors}
Rules: vinylgod|rules/campaign.yaml, rules.yaml
FluentMessages: vinylgod|fluent/lua.ftl
""")
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), (255, 255, 255) if o == 'Player' else (80, 150, 255) if o == 'Enemy' else (240, 210, 90))
im.resize((W * 3, H * 3), Image.NEAREST).save(os.path.join(MAP, 'map.png'))
print('m02 written')