Mission 03: THE DROP — campaign finale

Full base-building map against a live Stream bot holding The Algorithm behind a
defended base. Building the Mastering Studio triggers a scripted all-in
counterattack; silencing The Algorithm wins. Tick scan throttled to 1Hz.

SIDE A is now three missions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-25 23:19:43 +10:00
parent c09da56dbe
commit ef432e4182
18 changed files with 346 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
assets_src/map_m03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -9,3 +9,8 @@ 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
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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,59 @@
-- 03: THE DROP — finale.
CounterattackDone = false
Ticks = 0
WorldLoaded = function()
Mrp = Player.GetPlayer("Player")
Stream = Player.GetPlayer("Enemy")
InitObjectives(Mrp)
StudioObjective = Mrp.AddPrimaryObjective("build-the-studio")
AlgorithmObjective = Mrp.AddPrimaryObjective("silence-the-algorithm")
ShopObjective = Mrp.AddSecondaryObjective("keep-the-shop-standing")
Trigger.OnKilled(TheAlgorithm, function()
Mrp.MarkCompletedObjective(AlgorithmObjective)
end)
-- losing the HQ costs the secondary, not the mission
Trigger.OnKilled(TheVan, function()
Trigger.AfterDelay(DateTime.Seconds(2), function()
local hq = Utils.Where(Map.ActorsInWorld, function(a)
return a.Owner == Mrp and a.Type == "popupshop"
end)
if #hq == 0 then
Mrp.MarkFailedObjective(ShopObjective)
end
end)
end)
end
Tick = function()
Ticks = Ticks + 1
-- once a second is plenty; scanning every actor every frame is wasteful
if Ticks % 25 ~= 0 then
return
end
if StudioObjective == nil or Mrp.IsObjectiveCompleted(StudioObjective) then
return
end
local studios = Utils.Where(Map.ActorsInWorld, function(a)
return a.Owner == Mrp and a.Type == "mastering"
end)
if #studios > 0 then
Mrp.MarkCompletedObjective(StudioObjective)
if not CounterattackDone then
CounterattackDone = true
Media.DisplayMessage(UserInterface.GetFluentMessage("it-heard-you"))
Media.PlaySpeechNotification(Mrp, "BaseAttack")
Utils.Do(Map.ActorsInWorld, function(a)
if a.Owner == Stream and a.HasProperty("Hunt") then
a.Hunt()
end
end)
end
end
end

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,152 @@
MapFormat: 12
RequiresMod: vinylgod
Title: 03 - The Drop
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: 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
Bot: wax
Enemies: Player
Actors:
spawn0: mpspawn
Owner: Neutral
Location: 10,54
TheVan: popupvan
Owner: Player
Location: 10,54
dig1: digger
Owner: Player
Location: 13,54
dig2: digger
Owner: Player
Location: 12,56
guard1: stacktank
Owner: Player
Location: 15,52
guard2: spinner
Owner: Player
Location: 16,55
guard3: djgrunt
Owner: Player
Location: 14,57
guard4: djgrunt
Owner: Player
Location: 15,57
TheAlgorithm: algorithm
Owner: Enemy
Location: 52,8
ehub: streamhub
Owner: Enemy
Location: 48,10
eproc1: transcoder
Owner: Enemy
Location: 44,8
eproc2: transcoder
Owner: Enemy
Location: 55,14
enode1: datacentre
Owner: Enemy
Location: 50,13
enode2: datacentre
Owner: Enemy
Location: 54,11
epods: podfarm
Owner: Enemy
Location: 45,13
eprint: printfarm
Owner: Enemy
Location: 49,16
eport: droneport
Owner: Enemy
Location: 56,18
etur1: adblocker
Owner: Enemy
Location: 46,17
etur2: adblocker
Owner: Enemy
Location: 52,18
etur3: adblocker
Owner: Enemy
Location: 43,11
escrap1: scraper
Owner: Enemy
Location: 47,20
escrap2: scraper
Owner: Enemy
Location: 50,20
emob1: drmwalker
Owner: Enemy
Location: 44,20
emob2: drmwalker
Owner: Enemy
Location: 53,22
emob3: bufferbot
Owner: Enemy
Location: 46,22
emob4: bufferbot
Owner: Enemy
Location: 47,22
emob5: bufferbot
Owner: Enemy
Location: 48,22
mid1: adblocker
Owner: Enemy
Location: 34,28
mid2: bufferbot
Owner: Enemy
Location: 33,30
mid3: bufferbot
Owner: Enemy
Location: 35,30
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

View File

@ -0,0 +1,5 @@
world:
LuaScript:
Scripts: campaign.lua, m03.lua
MissionData:
Briefing: The Algorithm has surfaced. It is sitting in the north-east behind everything it owns, scoring us.\n\nBuild a base. Build a Mastering Studio. Charge the orbital tonearm.\n\nThen put the needle through it.\n\nIt will hear the studio go up, and it will come for you. Let it.

Binary file not shown.

Binary file not shown.

View File

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

124
tools/gen_mission3.py Normal file
View File

@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Generate campaign mission 03 — THE DROP (finale: full base vs a live Stream base)."""
import os, struct
from PIL import Image
SIZE = 64
W = H = SIZE + 2
MAP = os.path.join(os.path.dirname(__file__), '..', 'mods', 'vinylgod', 'maps', 'm03')
os.makedirs(MAP, exist_ok=True)
WAX = [(14, 50, 6), (24, 40, 5), (32, 32, 6), (40, 24, 5), (50, 14, 6), (12, 30, 4), (52, 34, 4)]
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)
ACTORS = [
('spawn0', 'mpspawn', 'Neutral', 10, 54),
# the Party
('TheVan', 'popupvan', 'Player', 10, 54),
('dig1', 'digger', 'Player', 13, 54),
('dig2', 'digger', 'Player', 12, 56),
('guard1', 'stacktank', 'Player', 15, 52),
('guard2', 'spinner', 'Player', 16, 55),
('guard3', 'djgrunt', 'Player', 14, 57),
('guard4', 'djgrunt', 'Player', 15, 57),
# the Algorithm's seat
('TheAlgorithm', 'algorithm', 'Enemy', 52, 8),
('ehub', 'streamhub', 'Enemy', 48, 10),
('eproc1', 'transcoder', 'Enemy', 44, 8),
('eproc2', 'transcoder', 'Enemy', 55, 14),
('enode1', 'datacentre', 'Enemy', 50, 13),
('enode2', 'datacentre', 'Enemy', 54, 11),
('epods', 'podfarm', 'Enemy', 45, 13),
('eprint', 'printfarm', 'Enemy', 49, 16),
('eport', 'droneport', 'Enemy', 56, 18),
('etur1', 'adblocker', 'Enemy', 46, 17),
('etur2', 'adblocker', 'Enemy', 52, 18),
('etur3', 'adblocker', 'Enemy', 43, 11),
('escrap1', 'scraper', 'Enemy', 47, 20),
('escrap2', 'scraper', 'Enemy', 50, 20),
('emob1', 'drmwalker', 'Enemy', 44, 20),
('emob2', 'drmwalker', 'Enemy', 53, 22),
('emob3', 'bufferbot', 'Enemy', 46, 22),
('emob4', 'bufferbot', 'Enemy', 47, 22),
('emob5', 'bufferbot', 'Enemy', 48, 22),
# forward picket in the middle
('mid1', 'adblocker', 'Enemy', 34, 28),
('mid2', 'bufferbot', 'Enemy', 33, 30),
('mid3', 'bufferbot', 'Enemy', 35, 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: 03 - The Drop
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
Bot: wax
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('m03 written')