Skip to content

Scenarios ​

A scenario is authored content the engine plays: where the units start, what the zones are, what happens when the player reaches one, what they are being asked to do, and who says what along the way. Coilbox's campaigns stop at the engine boundary, so a campaign mission on its own is a skirmish setup with a briefing over it. A scenario is the other half, and it runs inside the game.

The Scenario Builder is an advanced-mode tool: turn on Advanced mode in Settings > General and it appears in the sidebar as Campaign Builder > Scenarios. A scenario you can play also shows up under Play > Scenarios.

Two things to know before you spend an evening on this:

  • The game has to have adopted coilbox's mission runtime, or the scenario will be played through a generated test game instead. See Which game can play it and What a player needs to play your scenario.
  • Scenarios are single player. Nothing stops you giving a scenario several human participants, but nothing has been built or tested for it, and two of the actions ignore teams entirely (issue #827).

Scenario, mission, campaign ​

Three words that sound alike and mean different things:

  • A scenario is the in-engine content. It carries its own skirmish setup (game, map, participants, mod options) alongside spawns, zones, triggers, objectives and dialogue. It is a standalone document, playable on its own from the Scenarios page.
  • A mission wraps a scenario and adds presentation: the briefing text, panorama, side graphic, voiceover and cutscene the player sees before the engine starts. Missions live inside campaigns. See Campaigns.
  • A campaign is an ordered list of missions, unchanged by any of this.

A mission does not have to have a scenario. A mission built from a preset alone is the kind campaigns have always had, and it still plays as an ordinary skirmish.

Which game can play it ​

A scenario is played by coilbox's mission runtime, a set of Lua files the game archive bundles.

Playing a scenario is an ordinary skirmish plus two things. The scenario, which is JSON, is compiled to a Lua file at missions/<id>/mission.lua where the game will look for it, and the start script is given coilbox_mission = <scenario id>. That mod option is the only signal the runtime has. Without it the runtime switches itself off before it reads anything, so a game that has adopted it plays every ordinary match exactly as it did before.

Which route your scenario takes is decided per launch, and coilbox tells you which one it picked before you press the button. The two differ only in where that compiled file is written and which game the start script names:

  • The game ships the mission itself. Nothing is written at all. The game already holds both the runtime and the compiled mission, so it is launched as itself and told which of its own missions to play. This is the route a mission that comes with a game takes, and it is open to a packaged .sd7 or .sdz as well as a loose .sdd.
  • The game archive bundles the coilbox mission runtime. The compiled mission is written into the game's own missions/ folder and the game is launched as itself. This is the route a shipped scenario is meant to take.
  • Anything else goes through the test mutator: coilbox writes a small game of its own, coilbox-mission-test.sdd, into your content root's games/ folder. It depends on the game you set the scenario in for units, sides and everything else, and adds coilbox's runtime plus the one scenario under test. Your install is not touched, and deleting that folder undoes everything the flow has ever written.

The question a packaged .sd7 or .sdz is asked is whether the mission is already in it. One it ships plays as itself. One it does not takes the mutator route, because a packaged archive cannot be written into and the mission has to go somewhere. For a game to play scenarios you wrote, it needs a loose .sdd copy with the runtime installed, which is done from Content > Games. See the mission runtime.

Author a scenario ​

Go to Campaign Builder > Scenarios and press New scenario. Give it a name and, if you like, a description.

Set the game and map ​

A new scenario has no game and no map, and the editor cannot draw anything until it does. Press Set up from preset and pick a saved singleplayer preset. That copies the whole setup in: game, map, participants, start position type and mod options.

So the fastest start is to go to Play > Singleplayer first, configure the game, map, teams and AI you want, and save it as a preset. Picking a preset again later, with Change setup, replaces the setup and clears the per-team settings, because those are keyed to the participants the old preset had.

Setting a scenario's game and map without a preset is not possible yet: issue #821.

The map scene ​

The map is drawn as a 3D scene from the same terrain data the content browser previews maps with. Everything you place is drawn with its real game model.

  • Drag or middle-drag to pan, right-drag to turn, scroll to zoom toward the cursor.
  • Frame map puts the camera back where it opened.
  • A unit type the game does not have is drawn as a plain box, and the count of those is written in the corner so you know it is a missing unit rather than a placement you forgot.

The mode rail ​

The column of buttons down the left of the scene decides what a click on the ground does. Hover one for its name, what it makes, and the number key that reaches it. The mode's own controls, a unit picker or a count, appear in a bar along the top while that mode is on.

Undo and redo sit at the top of the same column. Turn and delete appear at the bottom of it whenever something is selected, and act on the whole selection. The bar along the top says what is selected and carries that thing's own controls.

ModeWhat it does
PanWhere the editor opens. Drag to move the view. Nothing is placed, and a click still selects.
SelectDrag a box round things to select them all. Click bare ground to deselect.
ZonesDrag to draw a zone. Pick Box or Circle first.
ActorsPick a unit and a team, then click to place one unit.
GroupsPick a unit, a team and a count, then click to place a block of them.
BasesPick a building and click. Clicks add to the base you have selected, or press New base to start another.
BlueprintsPick a saved layout and a team, then click to place the whole base. Each one is drawn in the list, so you can tell them apart before you place one.

Nothing is placed until you have picked a unit, so a stray click in Actors mode with an empty picker just deselects.

In Select and Zones the left button is taken by the drag that draws, so middle-drag is how you pan while either is on. Pan mode is the other way out of that: it gives the left button back to the camera, which is why it is where the editor opens. Zones also stop being clickable while a placing mode is active, so a zone over a corner of the map does not block placing there.

Move, turn and delete ​

Click a thing to select it, then drag it to move it. Dragging one of a group's units moves the whole group, and dragging a base's building moves it within the base. Dragging a zone moves it, and dragging its handles resizes it.

Rotation is the Turn button at the foot of the mode rail, which turns a quarter turn at a time. A unit faces one of four ways and nothing in between, which is what the engine gives a placed unit. A group's units all face south and cannot be turned individually, so the button says so rather than disappearing. Delete is beside it, and a zone has only that one: a zone is a footprint with no facing.

Both act on everything selected, and the bar along the top says what that is.

There is no Save button and no undo. Every change is written to disk as you make it. Text fields commit when you leave them or press Enter, sliders when you let go, and a drag when you release it.

What you can place ​

Actors ​

An actor is one named unit: the commander to defend, the base the mission is about, the character who has to survive. It has an id, a unit type, a team, a position and a facing, and its Details popover sets:

  • Starting health, as a percentage of the unit's maximum.
  • Invulnerable, for a unit the mission is not asking anyone to kill.
  • Unselectable, for a unit the player should not be ordering about.
  • Display name, drawn over the unit in game. This is the one piece of actor state with no engine call behind it, so the name comes from coilbox's own panel rather than from the unit.

Triggers refer to actors by id, and so do orders. That is what unit_dead, unit_health_below and unit_captured are asking about.

Groups ​

A group is a block of units spawned and ordered as one: a raiding party, a reinforcement wave, a garrison. You say what is in it (counts per unit type), where it lands, what its orders are, and whether it is there from the start.

A group has two states, and both matter:

  • On the map or not. A group is placed at game start unless you tick Waits for a trigger to spawn it, which makes it dormant.
  • Awake or asleep. Asleep is standing on hold position: the units exist and defend themselves and do not wander off. Awake is running the group's orders.

So a dormant garrison is spawn_group when you want it standing there and wake_group when you want it moving. A reinforcement wave is wake_group on its own, which spawns it and sends it off in one action.

Orders are one queue given to every unit in the group. Move along, Patrol and Fight along take a list of waypoints you draw by clicking on the map. Guard and Attack take an actor, a base's building or another group as the target. A patrol closes its loop back to where the group is standing, exactly as it would if a player had shift-clicked the same points.

Bases ​

A base is a group of buildings you drag around as one piece. The buildings are stored as offsets from the base's origin, so moving the origin moves the lot.

The format keeps a base in two halves. The blueprint is the layout: which buildings, where they sit relative to the origin, and which way each faces. The base is the placement of one blueprint: whose it is, where on the map it stands, and the fields only a mission uses. That is why the same layout can be a mission's enemy compound and something you stamp out in a live game, and it is what the base blueprint import and export work builds on.

The layout has a name, which you set in the base's buildings popover and which is what a picker will list it by. Layouts written before names existed are numbered "Layout 1", "Layout 2" and so on when the scenario is opened.

Two bases can be placed from one layout. When they are, the popover says so, and adding, moving, turning or deleting a building gives the base you are working on a copy of its own, leaving the other bases standing where they were. That is what you want almost every time: you dropped your opening base in twice and nudged a solar in one of them. When you do mean to change all of them at once, turn on Edit the layout they all use in the same popover. It applies to the base you are working on, and it is off again as soon as you select another.

Each building carries its own facing. A factory also carries the Queue it starts with and whether it builds that queue over and over. The picker is limited to the game's static units, because a mobile unit inside a base would be placed off the build grid.

The square drawn under each building is the ground it really stands on, taken from the game's own footprintx and footprintz. Placing, dragging and turning one puts it on the build grid, which is where the engine will build it and nowhere else, and moving a whole base goes in whole build squares so the layout survives the move. Turning matters because the sides swap on an east or west facing, so a rectangle takes a different part of the grid once it is on its side. Two buildings wanting the same ground both turn red and the base's buildings popover names them, because the engine will build one of them and refuse the rest.

Every building you place can be named by a trigger, the same way an actor can. It appears in the actor picker as "Base 1's corlab", so "when the keep's factory dies" is a unit dead condition on that building. A scenario that names one needs mission runtime 2, which is what stops a game with an older runtime playing it with that trigger dead.

Zones ​

A zone is a named area: a box with two corners or a circle with a centre and a radius. Both are flat. A scenario carries no heights anywhere, because everything in one sits on terrain, so a zone is a footprint and a unit is in it or is not whatever its altitude.

The name is what triggers refer to, so it is the only thing worth editing after you have drawn one. Membership is the engine's own spatial query, so a zone contains what everything else in the game would say it contains, including units nobody can see.

Per-team settings ​

Each participant can be given a starting bank, free income per second, a list of units to spawn on its start position, and no commander. Resources are set for every team the scenario declares, so a team you say nothing about starts with nothing rather than with the game's usual opening bank. The commander is only suppressed for a team you actually mark no commander, because the format has a flag per team and a flat rule would make it meaningless.

Difficulty ​

One scenario can be three. An actor, a group, a base and a trigger each carry an optional Only from and Only up to, over easy, normal and hard. Leave both on Any and the thing is always there, which is what everything you have already written says.

So the extra turret is a base marked "Only from hard". The mercy waypoint marker is a trigger marked "Only up to easy". The second wave is a group marked "Only from hard", set once on the group rather than on every trigger that sends it: a group outside its range is not placed at the start and is not placed by spawn_group or wake_group either.

A trigger outside its range is not armed and cannot be armed. enable_trigger on it does nothing, so "on hard the reinforcements arrive twice" is a second trigger set to hard rather than a difficulty test written into the trigger you already have.

The player picks the difficulty in the same drawer they press Play in, and you pick it in Test in game. The picker only appears for a scenario that actually varies by it.

In a campaign the level belongs to the whole run rather than to one mission. The picker sits on the briefing of a mission that varies by difficulty, right above Start Mission, and what the player picks there holds for every mission after it until they change it again. So a player who finds mission 4 too hard drops the level on mission 4's briefing and plays on. Missions they already beat stay beaten.

A campaign run nobody has picked a level for plays at normal, which is what every campaign did before this existed.

A range that crosses itself, "only from hard" and "only up to easy" together, is a warning on the launch rather than a refusal. Nothing in the mission can satisfy it, so the thing never appears at any setting.

A scenario that sets any range needs mission runtime 6. One that sets none asks for whatever it asked for before, plays identically at every setting, and compiles to the same file it always did.

Triggers ​

A trigger is "when these conditions hold, run these actions". The list is flat, and triggers that enable and disable other triggers are what turn a flat list into a state machine.

Each trigger has:

  • Armed at the start, off for a trigger something else has to enable.
  • Fires every time, off for a trigger that fires once and disarms itself. Enabling it again re-arms it.
  • Waits N seconds between firings, for a repeating trigger. With no wait, a repeating trigger fires on every pass its conditions hold, which on the polled tick is about twice a second.
  • Fires when, a list of conditions with all of these hold or any of these hold over it. There is no nesting. An empty list holds under all and never holds under any.
  • Then, the actions, run in the order you list them.
  • Only from and Only up to, the difficulties it runs at. See Difficulty.

A trigger's name is a label. Call it what you like, call two triggers the same thing if that reads better, and rename it whenever. Nothing points at the name: enable_trigger and the compiled mission point at an id the editor mints once and never changes. So a rename moves nothing, and the mission a game ships does not need recompiling because you thought of a better name.

A trigger written before names existed shows the id it was called by, which is the name it was displayed under anyway. Nothing to do.

Conditions are checked two ways, and you do not choose which. A condition about a thing that happened, a unit dying or being captured, is checked when it happens. A condition about an aggregate, a count or a zone's contents or a clock, is checked on a slow tick every 15 frames. A trigger with one aggregate condition is checked on the tick.

Conditions ​

The names below are the ones in the compiled mission. The editor shows them with the underscores taken out and the first letter capitalised, so units_in_zone reads as Units in zone.

TypeWhat it asks
units_in_zoneHow many units are in a zone, optionally one team's and optionally only certain unit types. Holds when the count is between min and max. Neither number means at least one. max = 0 is how you ask whether a zone is clear.
unit_countHow many units a team has, optionally only certain unit types, between min and max.
unit_deadAn actor has died.
unit_health_belowAn actor's health is under a fraction of its maximum.
unit_builtA team has finished building this many of a unit type.
unit_capturedAn actor has changed hands, optionally to a named team.
unit_selectedA player has a unit selected. Any unit, or one of a unit type, or one actor. Optionally one team's player.
command_givenA player has given an order since the trigger was armed. Any order, or one command, or an order to build one unit type. Optionally one team's player.
dialogue_dismissedA player has clicked away a dialogue line that was set to wait for them, since the trigger was armed.
time_elapsedThis many seconds since the mission started.
varA variable compared against a number, or against another variable, with eq, ne, lt, lte, gt or gte.
zone_held_forA team has had a unit in a zone continuously for this many seconds. Leaving resets the clock. Uncontested asks for control instead of presence.

zone_held_for is presence by default: a team standing in a zone holds it whether or not anyone else is standing there too, so one scout parked in a keep an enemy army is also sitting in satisfies "hold the keep for 60 seconds". Tick uncontested and anyone the holding team is not allied with breaks the hold for as long as they are in the zone, and the clock starts again from nothing when they leave.

Gaia does not contest. It owns the map's own furniture, critters and the units some maps place, which belongs to no side and fights for none, so a mission that told the player to clear the keep would otherwise be asking them to hunt down a deer. Allies do not contest either. A scenario that ticks the box needs mission runtime 3.

You cannot build this out of units_in_zone with max = 0. That reads the moment the timer runs out rather than the whole minute leading up to it.

unit_selected and command_given are the two conditions that read what a player did, and they are what a first lesson is made of. A scenario that uses either needs mission runtime 8.

unit_selected holds for as long as the unit stays selected. If the player already has their builder selected when "select your builder" is armed, the trigger fires straight away. It counts units the player owns, and it ignores spectators.

command_given counts orders given after the trigger was armed, and after each firing for a trigger that fires every time. So "give a move order" as a lesson's third step is not answered by the move order from its first step. The commands on offer are the engine's own: move, attack, fight, patrol, guard, stop, wait, repair, reclaim, resurrect, capture, load_units and unload_units, plus build for any build order. Pick a unit type under unit to build to ask for one building or one unit out of a factory. Orders the mission gives its own groups do not count, and neither do an AI's.

Both work in a multiplayer mission and in a replay. With no team set, any player's selection or order will do.

dialogue_dismissed is the third condition that reads the player. It names one dialogue line, and it holds once a player has clicked that line away. Only a line said with "wait for the player" switched on can be dismissed, so the condition never holds for an ordinary timed line. Like command_given, it counts a dismissal made after the trigger was armed. A scenario that uses it needs mission runtime 9. See Stop and wait for the player.

Actions ​

TypeWhat it does
spawn_groupPlaces a group, unless it already has units standing. It does not wake one.
wake_groupRuns a group's orders, placing it first if it is not on the map.
give_ordersReplaces a group's orders and wakes it. It does not place one.
gift_unitsHands a group's units to another participant. The group keeps them, so the mission can go on ordering a squad it gave away.
release_groupStops the mission ordering a group. The units stay on the map and stay whoever's they are.
set_var / add_varWrite a variable, or move one by a delta. Either takes a number or another variable.
enable_trigger / disable_triggerArm or disarm another trigger.
complete_objective / fail_objectiveSettle an objective. The first outcome sticks.
dialogueSay one of the scenario's declared lines. Switch on "wait for the player" to keep the line on screen until the player clicks it away.
play_soundPlay a sound by name, either an entry in the game's own sounds.lua or a file in the game.
reveal_areaLift the fog over a zone for a participant, for a number of seconds or the rest of the mission. See the limits.
unlock_unitLift the scenario's build restriction on one unit type for one participant.
camera_panMove the camera to a point over a number of seconds, one second by default. Optionally one participant's camera.
map_markerDrop one of the map's own labelled points, with your label or none. Optionally on one participant's map.
pause_game / unpause_gamePause the game, or start it again. Single player only.
victoryEnd the mission with the named participant's ally team as the winner.
defeatEnd the mission with every other ally team as the winner.

gift_units and release_group are separate on purpose. A mission that hands over a rescued convoy gifts it and releases it in the same trigger. One that lends an escort gifts it now and releases it when the loan ends, which one flag on the gift could not say. Releasing a group the mission still has orders for is the mission's own bug, and it is reported in the infolog rather than refused. A scenario using release_group needs mission runtime 3.

camera_pan and map_marker with no participant named reach everyone, which is what a single player scenario wants. Naming one is for a co-op or head-to-head mission, where yanking both players' cameras to one side's ambush shows the other player what is coming. A scenario that names one needs mission runtime 3.

victory and defeat with no participant named mean the team a human is playing. Name one only when you mean it. The result the campaign records comes out of the replay, and the reader asks whether the player's ally team is among the winners. A victory naming a participant the human is not playing therefore records a defeat for the player. See Win and loss.

What the palette greys out ​

The Add a condition and Add an action dropdowns list every type coilbox knows, and grey out the ones the runtime that will actually play this scenario does not implement, with the reason beside each. A type that is greyed out cannot be added, because a runtime that does not know a trigger type ignores it and plays a quietly broken mission.

Which runtime it measures against depends on the route. A game that has adopted the runtime is measured by the version it bundles. Anything else is measured by the runtime this build of coilbox ships, because that is what the test mutator carries. If the game is not installed, or the scan has not answered yet, nothing is greyed.

A type can also be greyed because there is nothing for it to point at yet. units_in_zone needs a zone to exist first.

Types the game adds ​

The lists are not only coilbox's. A game can declare condition and action types of its own, for systems coilbox has never heard of, and those appear in the same two dropdowns under the names the game gave them, with its own one-line description and a form for its parameters. Splinter Faction's research points are the worked example: a Grant research action and a Research above condition, neither of which coilbox understands a word of.

They are never greyed out on a runtime version, because the game implements them rather than the runtime, so they work on either route.

The game writes them in missions/extensions.lua, and declare your own condition and action types is the format. An extension can only add the game's own concepts: anything engine-level, spawning, orders, zones, sight, restrictions, the ending, the camera, is the runtime's and a game naming one of those is refused.

Objectives ​

An objective is a line of text the player is working towards, with a Primary or Secondary kind and an option to keep it hidden until it is settled. complete_objective and fail_objective settle one, and the first outcome sticks, so an author who wants a second chance at something writes a second objective.

Settling an objective does not end the mission. Three objectives and one ending is the ordinary case, and victory or defeat is what says it is over.

The player sees them in a panel in game: primaries first, then secondaries, in the order you listed them, with a hidden one left out until it settles.

Rewrite the text whenever. complete_objective and fail_objective point at an id the editor mints once and never changes, so a better wording moves nothing. The id is shown beside the objective, and you cannot edit it: it is what mission.lua carries, and a mission problem quotes it after the text so either one finds the row.

Dialogue and sound ​

A dialogue line is a radio message: a speaker, a line of text, and optionally a portrait image and a voice clip. Triggers fire them with the dialogue action.

Change the speaker and the words whenever. The dialogue action points at an id the editor mints once and never changes, so rewriting a line does not unhook the trigger that plays it. The id is shown beside the line and you cannot edit it, the same as an objective.

Import an image and Import a clip copy the file into the scenario's own store, verbatim, with no re-encoding. Portraits take png, jpg, jpeg, dds and bmp, and clips take ogg, wav and mp3. The files travel with an export, and they are copied into the game beside the compiled mission at launch, which is how the engine can load them.

In game, lines queue rather than interrupt, because a trigger with two lines in it is an author writing an exchange. A line holds the panel for as long as its text takes to read, at least three seconds and at most twelve, and the backlog behind it is capped at six.

Dialogue lives on the scenario, not on the campaign mission, because triggers fire it while the game is running. The mission keeps only what the player sees before the engine starts.

Stop and wait for the player ​

A new player reads slower than a mission runs. Three things let a scenario wait for them. All three need mission runtime 9.

  • The dialogue action has a "wait for the player" switch. With it on, the line stays on the panel until the player clicks it, and the panel says "Click to continue" under the text. Lines behind it wait their turn.
  • The dialogue_dismissed condition holds once the player has clicked that line away. Use it to carry on with the lesson.
  • The pause_game and unpause_game actions stop and start the game clock.

The usual pattern is two triggers. The first runs pause_game and a dialogue action that waits for the player. The second waits on dialogue_dismissed for that line and runs unpause_game, then whatever comes next.

A paused game does not run the mission's clock either. While the game is paused, triggers are checked only when the player does something: dismisses a line, selects a unit or gives an order. Dismissing a line checks every armed trigger. Selecting a unit or giving an order checks only the triggers that wait on that selection or order and on nothing else. Nothing that waits on time, such as time_elapsed or a cooldown, moves until the game is running again. So always unpause from a trigger that waits on dialogue_dismissed. The player's own pause key works the whole time, so a mission that forgets to unpause does not trap them.

Pausing is single player only. A pause stops the game for everyone in it, so in a game with more than one player pause_game and unpause_game do nothing, and the infolog says so once. A held line still works in multiplayer. Each player clicks their own copy away, and the first click is the one dialogue_dismissed hears.

A spectator sees a held line as an ordinary timed one, and so does anyone watching a replay. A replay does not pause. The engine does not record the wait, and the mission's request to pause is not sent during playback.

If the game has no LuaUI, or the player has switched the mission widget off, there is no panel to click. The runtime then dismisses a held line for the player straight away, so the lesson carries on without the message instead of waiting for ever.

Restrictions ​

Building is either anything the game allows, only a list of units, or everything but a list. Commands withheld is a list of engine command names the mission takes away, named the way the engine names them, so self destruct is selfd.

A restriction binds every team the scenario declares, not the player. The format names no team, so binding the player alone would be a rule you never wrote, and it would leave you no way to restrict an enemy at all. unlock_unit is the other end of it: to make a rule that applies to the player only, write the restriction and then unlock the unit for everyone else.

Two things the runtime does not restrict: what the mission itself places, because the engine only consults the build rule for builders and factories, and a command the mission's own Lua gave, so a mission that withholds attack can still order its own raiders to attack.

These are not the engine's [RESTRICT] block, which is global and permanent. unlock_unit is why: a restriction lifted halfway through a mission for one participant is exactly what [RESTRICT] cannot express. A campaign mission's own Restricted Units list is still [RESTRICT], and still applies to everyone. See Campaigns.

Variables ​

A variable is a named number belonging to one scenario: a kill counter, a phase number, a flag saying which branch the player took. Numbers and nothing else, so add_var always has something to add to and the var condition is one comparison.

Renaming one carries the triggers that read it over. Undeclaring one leaves them alone, and they then read it as 0 and say so in the infolog.

Anywhere a var condition, set_var or add_var asks for a number you can name a variable instead, with the dropdown beside the box. So "kills reached the quota" is a var condition on kills reading quota, and "add the bonus to the score" is an add_var on score reading bonus. A scenario that does this needs mission runtime 3, which is what stops an older game reading the variable as nothing and comparing against zero.

Test and play ​

Test in game is in the Setup card at the top of the editor. It compiles the scenario, writes it where the game will look for it, reads it back out and checks every reference resolves, and only then starts the engine. A zone name a trigger points at that no longer exists stops there rather than playing as a trigger that never fires.

If anything fails to resolve, nothing launches and every problem is listed at once, each located in editor terms, for example Trigger "The gates open" (trigger-3), action 1, group.

A problem names what you called the thing, then the id in brackets. The name is the row to go and click. The id is what mission.lua carries, and it is the part that tells two zones called "north" apart, because a name is yours to reuse. Something you have not named yet, and anything with no name of its own, is named by its id alone.

The drawer also says which route the launch is taking and why, before you press anything. On the mutator route it says what it is about to write and that deleting that one folder undoes it.

The button is disabled with a reason for the things you have to fix elsewhere: no engine installed, no game and map set, the game not installed, or a game already running.

Both the editor's drawer and the player's offer a difficulty, for a scenario that varies by one. See Difficulty.

Play > Scenarios is the player-facing half. It lists every scenario that names a game and a map, with its contents, and gives each one a Play button that runs the same pipeline. It also has its own Import, so a scenario someone sent you can be brought in and played without Advanced mode or the editor. The sidebar item is hidden until at least one scenario is ready to play.

A player is told the same facts as an author in different words. "Install it from Content" is something anyone can do, so it is said either way, while "set the scenario up on a game you have", the test mutator and mission runtime versions are the author's and are not shown on the Scenarios page at all. A player is told that coilbox sets up what the game needs and leaves their copy of it alone.

Share a scenario ​

Share offers three ways out, the same three a preset, a challenge and a setup pack offer. Copy code puts a cbz1. code on the clipboard to paste into chat. Copy link wraps that code as a coilbox:// link. Export as file writes a single .json. All three carry the same thing: a coilbox container, "kind": "scenario", holding the document plus every portrait and voice clip the dialogue names, inlined as data: URIs beside the document.

A scenario is the one shareable thing with no upper bound on size, because those clips travel inside it. A code is capped at 512 KB of content, which no amount of authoring will reach on its own (the Silence the Jericho mission below is a 3,155 character code, and copying it out to 581 triggers still only makes an 18,000 character one), but which 380 KB of portraits and voice clips passes by itself. When a scenario is past it, Share says so and offers the file alone, rather than handing you a code that would fail on the other end. A link is capped lower still, so a scenario with any real amount of media gets a code and a file but no link.

Import reads any of them back, from either Play > Scenarios or the builder: paste a code into the box, or press Import from file…. It always makes a new copy: importing your own export gives you a second scenario with a new id, never an overwrite. Something that is not a coilbox scenario, is damaged, or was made by a newer coilbox is rejected with a reason rather than half-read. A clip an import cannot write, one over 16 MB for instance, is skipped and the line that named it loses it, rather than the whole scenario being refused.

If the scenario is played on a game or a map you do not have, import offers to download them first, the same way importing a campaign does. Nothing is written until they are both here, so cancelling leaves you with nothing to clean up.

The other way to ship one is to attach it to a campaign mission and bundle the campaign, which carries the whole scenario document. See Campaigns.

Ship a scenario in a distribution ​

A distribution profile can hand out scenarios the same way it hands out campaigns. Share the scenario, choose Export as file, and drop the .json into the portable .coilbox/scenarios/ folder beside the executable, with any name you like. Coilbox lists it alongside the player's own, marked Bundled.

A bundled scenario is read-only:

  • It plays from Play > Scenarios exactly as any other scenario does. Its dialogue portraits and voice clips travel inside the export file and are written out the first time it is played, so its radio messages have their pictures and voices.
  • It cannot be edited or deleted. Opening its editor route says so instead of opening the editor, and the Scenario Builder row has no Edit and no Delete.
  • It can still be Shared. Import the code or the file back and you have a local copy that is yours to change.

Coilbox never writes into .coilbox/, so removing a bundled scenario means removing the file from the package. Its clips are cleaned up on the next start once it is gone.

What a player needs to play your scenario ​

A code, a link, an export file and a bundled scenario all carry the same thing: the scenario document, plus the portraits and clips its dialogue names. None of them carries the game, the map or the mission runtime. Coilbox picks the route again on the player's machine, against their copy of the game, so a scenario you tested on a game that bundles the runtime can still play through the mutator for them.

If they do not have the game or the map, import offers to download both. Past that there is nothing for a player to set up. Coilbox tells them it is setting up what the game needs and leaving their copy alone, and says nothing about runtimes, mutators or archive formats, because none of that is theirs to fix.

What the mutator route costs a player is quality, not effort. A downloaded game arrives as a packaged .sd7 or .sdz, and a scenario you sent them is not one it ships, so it takes that route and can run into the game's own end conditions and opening phases. Fixing that is the game's job, which is the next section.

Getting a game to play your scenario properly ​

Why the mutator is not a way to ship a scenario. It carries the runtime and the compiled mission, and it cannot carry the two guards a game adds to its own gadgets when it adopts the runtime. One stops the game ending while a mission is running. The other stops it starting a team the mission has already started.

Part of that the runtime covers by itself. An anchor unit on each human mission team stops the engine ending the mission early, and a commander the game spawned before the end of game frame 1 is removed again from any team the scenario said starts without one. What it cannot cover is a game that declares a winner by its own end conditions, or one that spawns and asks its opening questions after frame 1. Splinter Faction spawns on frame 1800, so through the mutator its faction picker and start-spot picker run over the top of a mission that has already placed its units, and its end conditions can declare a winner while the objectives are still open.

So a scenario you want other people to play as you wrote it needs their copy of the game to be a loose .sdd, bundling a runtime at least as new as the scenario needs, with both guards in it. None of that is a player's job. The guards are the game maintainer's to add, once, in the copy everybody downloads, and the adoption contract is the page to send them. A player who already has a loose copy can install the runtime from Content > Games themselves, but nobody outside the game can add the guards.

If you ship the game as well as the scenario, the runtime and the guards are simply part of it, taken once into your own source rather than installed copy by copy. Ship the missions in it too and the packaging stops mattering: a game that holds a mission plays it as itself, packaged or loose, because there is nothing left to write. That is shipping missions in your game below.

The packaging only decides what happens to a mission the game does not hold. Coilbox writes one of those into the game's missions/ folder as it launches, which needs a loose .sdd. A packaged .sd7 or .sdz falls to the mutator for those, however complete its runtime is. See Distributing coilbox with your game.

Ship missions in your game ​

A mission inside your game's own archive is your content. It is listed, played and shared like any other scenario, and a player is told it comes with the game.

Each one is a folder under missions/ holding the compiled mission.lua, the scenario.json it was built from, and the dialogue portraits and clips it names:

missions/
  first-contact/
    mission.lua
    scenario.json
    kesh.png
    kesh-briefing.ogg

Write scenarios the ordinary way first. Every scenario starts in coilbox's own store, whoever made it, so nothing you import or write is ever put in somebody's game without you saying so. When one is ready, open it in the Scenario Builder and press Put this mission in the game in the Setup card. That moves it: the document and the compiled mission are written into the game and the local copy goes, so there is one of it. Take it out of the game is the reverse.

Editing follows the archive format. A mission in a loose .sdd is edited in place, and each save rewrites both the document and the compiled mission beside it, so the two never disagree. A mission in a packaged .sd7 or .sdz is read-only, because coilbox cannot write into one. It still plays, and it can still be shared as a code or a link.

If the two ever do disagree, because the archive was built by hand or the document was edited outside coilbox, a loose game is corrected on the next launch and a packaged one plays what it ships and tells the author. A player is not told, since they cannot rebuild somebody else's game.

Play the example mission ​

Silence the Jericho is the first mission authored end to end in the Scenario Builder, and you can download it as an export: silence-the-jericho.json.

Importing it takes nothing but coilbox. Open Play > Scenarios, press Import, then Import from file… and pick it. With Advanced mode on you can do the same from Campaign Builder > Scenarios and get a scenario you can read and edit like any other.

Playing it needs the game it was written for. That machine also needs, in this order:

  1. A loose Splinter Faction, cloned into your Spring data folder as games/SplinterFaction.sdd. A packaged .sd7 or .sdz cannot be written into, so it cannot take the runtime. The mission names its game SplinterFaction $VERSION, which is the archive name a loose checkout reports. A copy reporting a different name is a different game as far as the launch is concerned.
  2. The mission runtime installed into it. Open Content > Games, pick Splinter Faction, and press Install the mission runtime.
  3. The three adoption guards. They are not upstream in Splinter Faction yet, so this repo keeps them as scripts/sf-proof/splinterfaction-guards.patch. Without them the game's own end conditions end the mission early, and its faction picker and start-spot picker run over the top of a mission that is already playing. See running the proof yourself.
  4. The map AcidicQuarry 5.17 in maps/. A scenario's zones are map coordinates, so this mission plays on that map and no other.

With all four the mission appears under Play > Scenarios with a Play button, and Test in game in the editor runs the same pipeline.

The export is built from src/scenario/fixtures/jericho.json by bun scripts/build-jericho-export.mjs, and src/scenario/example.test.ts fails if the two ever drift apart.

Win and loss ​

The runtime ends a mission by calling the engine's own game over with the winning ally teams, so the outcome lands in the replay and coilbox reads it back with the same replay detection campaigns already use. There is no separate channel.

What the reader asks is whether the player's ally team is in the winning list. Three consequences worth designing around:

  • A victory naming a participant the human is not playing records a defeat for the player. A victory naming a different participant on the human's own ally team still reads as a victory, because the test is the ally team rather than the participant.
  • defeat declares every other ally team the winner, Gaia aside, which is what a losing player's replay has to say.
  • A mission the player quits before it ends asks the player for the result. The replay has no game over, so it names no winner, and coilbox shows the "How did it go?" prompt. A mission the runtime ended with nobody winning still records a game over, so it reads as a defeat for the player.

The manual Victory / Defeat prompt appears when the replay cannot answer at all: no new replay was found, the decode failed, the engine's demotool is missing, or the replay has no game over because the player quit. It does not appear when the replay answered wrongly.

What a scenario cannot do yet ​

Honest limits, all of them things you can hit while authoring:

  • reveal_area reveals a circle. No engine call grants sight over a region, so a reveal is implemented as a short-lived invisible unit with sight. A box zone is covered by the circle around its corners, so a reveal spills past them. Under-revealing would leave the thing you drew the box around in the dark.
  • Terrain occludes a reveal. Sight is cast from the spotter, so a ridge inside the zone shadows its far side, exactly as it would for a scout standing there. Air sight is not occluded, so aircraft over the zone are always seen.
  • The player's unit count is one too high. The runtime keeps one invisible anchor unit on each mission team a human plays, so the engine's own "this ally team has no units" rule cannot end the mission early when the player legitimately reaches zero units. The anchor is a real unit and shows in the game's own unit count. The runtime's own counting leaves it out, so unit_count and units_in_zone are unaffected: issue #820.
  • A restricted unit's build icon is greyed out, and a game may un-grey it. The runtime greys the icons your restrictions forbid, on every builder, and ungreys one an unlock_unit frees. A game that paints its own build icons, for a tech tree or a supply limit, can repaint over that, and then a forbidden unit looks available. The build is still refused either way.
  • A mission coilbox wrote into a game stays there. There is no in-app way to remove one: issue #814.
  • A scenario cannot be set up without a preset: issue #821.

One thing nobody has watched happen, so treat it as unproven rather than working:

  • The objectives panel, dialogue panel and debrief have never been drawn in a real engine. That includes the "Click to continue" line under a held dialogue line, and the click that dismisses it. What they decide to draw is tested outside one, but nothing has confirmed they appear, that they do not sit on top of the game's own UI, or that a portrait loads: issue #850.

For what the runtime does and does not do inside the engine, and how a game adopts it, see the mission runtime.

MIT-licensed, and the Lua that runs in the engine is MIT or GPL-2.0-or-later. Bundles pr-downloader (GPL-2.0-or-later, © the Spring/Recoil authors).