MAST Tutorial
A tutorial that builds a simple mission from nothing, using most of what MAST has to offer along the way.
Tutorial Goals
General Goals
Upon completion of this tutorial, you will have learned to:
- Create a new mission from a template
- Update the name and description of your mission
- Understand what a 'label' is in MAST
- Spawn player ships
- Mission settings
- Spawn terrain
- Spawn enemies
- Utilize Roles
- Add science scans
- Detect damage and destruction
- Testing the mission
- Recurring Tasks
- Build comms buttons
- Handle game end (coming soon)
Specific Goals
For this tutorial, we are writing a mission about finding and recovering a lost treasure.
- The players will need to fight off scavengers while they search.
- They will need to scan asteroids to find the treasure.
- They will need to shoot at the asteroid to remove debris from the treasure.
- They will need to collect the treasure by giving instructions to a specialist team to retrieve it.
What you will NOT learn
MAST is built on Python: your mission is compiled by it, and Python is what talks to the Cosmos engine underneath. That is what makes mission scripting as flexible as it is. This tutorial is not a python tutorial, and we will touch on some aspects of the language, but if you want a more thorough understanding of python there are loads of resources online.
1. Setting up your first mission
Create the mission
From your missions/ folder, run:
sbs create TreasureHunt --title "Treasure Hunt"
Pick minimal when it asks — this tutorial builds the mission up from nothing, so
starting from an empty one is the point. It creates the folder and downloads the
libraries the mission needs.
Just want a mission that already works?
sbs create MyMission -t sandbox gives you a playable one — two sides, a station
in an asteroid field, player ships and enemy waves — that you can read and pull
apart. sbs templates lists everything on offer.
Everything comes from the mast_starter repository, which you can also browse on GitHub or use with its green Use this template button.
For a full mission with a story already in it, look at Secret Meeting. It ships with Cosmos, but the GitHub copy may be newer, and the newest is always the one to read.
2. What's in a name?
The name of your mission is, of course, a pretty important part of the mission.
Fortunately, it's easy to set the name and description of the mission.
The --title above already set it, so open description.yaml in your mission folder
and you should see:
format version: 1
Category: Standard
Visible Mission Name: Treasure Hunt
Description: Treasure Hunt
Change them whenever you like — this file is read before the mission ever runs.
No dashes, and keep it short
A - anywhere in these values crashes the engine, mid-word included
(Auto-ramping → Auto ramping). Keep Description to one terse phrase; long
ones read badly in the mission browser.
- Category is used to group missions in the browser.
- Visible Mission Name is the mission name shown on the selection button.
- Description is the mission description shown on the selection button.
Note: Older missions used
description.txt(still supported). New missions should usedescription.yaml.
Once you've changed these to fit what you want, close description.yaml and open story.mast:
# Use for startup logic
@map/first_map "Hello Cosmos"
" This is my first map.
story.mast is where the bulk of your mission will eventually go, but right now, we're only concerned with the line that starts with @map.A line starting with
@map defines a label, which we will get into more later. It tells the game that this is a mission, and what its name is.
In our case we want to change the name of the map from "Hello Cosmos" to our chosen mission name - "Treasure Hunt". Note that this is displayed separately from the information in description.yaml. The fields in description.yaml are displayed on the mission selection button. The name and description shown in the UI after selecting the mission are defined here in story.mast.The
/first_map part of the label is used internally by the MAST interpreter, but isn't relevant to us at the moment. You may choose to change this to whatever you want, but it cannot have spaces or special characters in it.The line following the label definition is the label description:
" This is my first map.
" Find the treasure, if you can! Many have sought this long-lost piece of history.
" None have succeeded. Some say it is only a myth. But recent archaeological finds have
" given clues regarding the location of this mysterious artifact.
3. A brief overview of Labels
A label, at its most basic, is a reusable and schedulable section of code. A label is used by MAST to create a task, and that task can be scheduled to run at a particular time, under specific conditions, and more than once, if desired. This gives mission writers a great deal of flexibility.
Label Types
There are multiple kinds of labels:
Media Labels
Map Labels
Route Labels
Main Labels
* Inline Labels (also known as sublabels)
We've already encountered a map label - @map/first_map "Treasure Hunt". In practice, most of the startup code in your mission script will be part of this map label. Media labels are very similar in syntax and meaning. They start with @media, followed by the type of media, followed by the desired filename or directory. Note that the name at the end, unlike that of a map label, currently has no bearing on the mission.
@media/music/default "Cosmos Default Music"
@media/skybox/sky1-bored-alice "borealis"
A skybox label sets the sky and nothing else. Music is chosen independently by the MUSIC_SELECT setting (a bank name, or "random"), which a map's Defaults: and the server console's Music dropdown can override — so declaring a music label makes that bank available, and MUSIC_SELECT decides which one plays. See the media API.
We will cover route labels in later sections, so we will focus now on the last two types of labels.
Main labels (which will hereafter be referred to as just labels) are the mainstay of a mission. Let's dissect the following example:
== Hello_Cosmos
" This is an example label
metadata: ```
some_data: 10
```
== Hello_Cosmos
======Hello_Cosmos==
=== Hello_Cosmos ==========
Sublabels have the same rules, but use dashes instead of equals signs:
--- some_sublabel
The third line and onwards defines the metadata associated with this label. Within the set of triple backticks, you can define keys, and set the default value for that key. This is very useful, because when you schedule a task, you can include different values for each key. This allows a label to not only run multiple times, but use different information while doing so. For both main and sublabels, the description and metadata lines are optional.
You might be able to guess what is special about sublabels. They are defined inside of a main label, and are only applicable to the their parent label. Sublabels with the same parent may not share a name, but if their parent labels are different, sublabel names can be reused.
More details on tasks and labels.
4. Spawning Player Ships
Moving on from all that technical stuff, let's get into actual mission writing! … Which requires some technical stuff.
You've learned that labels are used by MAST to create a task. Well, it turns out that it's time to do so.
await task_schedule(spawn_players)
task_schedule(spawn_players). task_schedule() is a python function (if you're not sure what a function is, do a google search). It takes a label as an argument, and builds a task from that label. "But, wait!" I hear you interrupt. "Where is spawn_players defined? And how do I learn all of these functions and what they do?"
That's a couple of great questions. First, let's take a brief detour and open story.json in your mission folder. You'll see something like this:
{
"sbslib": [
"artemis-sbs.sbs_utils.v1.3.0.sbslib"
],
"mastlib": [
"artemis-sbs.LegendaryMissions.autoplay.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.ai.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.commerce.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.comms.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.consoles.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.damage.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.docking.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.fleets.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.grid_comms.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.hangar.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.internal_comms.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.operator.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.science_scans.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.side_missions.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.upgrades.v1.3.0.mastlib",
"artemis-sbs.LegendaryMissions.zadmiral.v1.3.0.mastlib"
]
}
fleets, docking, prefabs, comms, consoles, and damage. Others like commerce, autoplay, operator, gamemaster, and gamemaster_comms can be removed if your mission doesn't use them. These are made up of MAST files whose labels are directly available to your mission script.If you open the
/Cosmos_Install/data/missions/__lib__/ folder, you'll see these .sbslib and .mastlib files. You can copy and rename them to .zip and unzip them to view their contents, or you can go to the github page.
The spawn_players label is defined in LegendaryMissions/fleets/map_common.mast, if you want to take a look at it.
There's another way to find all these functions, though: if you use VSCode (which I recommend), there's an extension called MAST that is built for us mission writers. It has loads of features that you'll find useful.
But let's get back to our own mission. We've looked at task_schedule(spawn_players), but there's another part of this line that's important. The line begins with await. await tells MAST to pause the current task (our mission) until the completion of the spawn_players task. Without using await, the new task would run concurrently with our mission's current task. We want to make sure that the player ships have spawned before we continue with our task, and await does this quite handily.
5. Mission Settings
While we're on the topic of player ships, you may have noticed that Legendary Missions have various settings, including a setting for the number of player ships. You have probably wondered how you can add these settings to your mission. Let's dive in!
Let's look at the beginning of the Double Front mission. In the metadata for the map label, we have the following:
Properties:
Main:
Player Ships: 'gui_int_slider("$text:int;low: 1.0;high:8.0;", var= "PLAYER_COUNT")'
Difficulty: 'gui_int_slider("$text:int;low: 1.0;high:11.0;", var= "DIFFICULTY")'
Map:
Terrain: 'gui_drop_down("$text: {TERRAIN_SELECT};list: none, few, some, lots, many",var="TERRAIN_SELECT")'
Lethal Terrain: 'gui_drop_down("$text: {LETHAL_SELECT};list: none, few, some, lots, many", var="LETHAL_SELECT")'
Friendly Ships: 'gui_drop_down("$text: {FRIENDLY_SELECT};list: none, few, some, lots, many", var="FRIENDLY_SELECT")'
Monsters: 'gui_drop_down("$text: {MONSTER_SELECT};list: none, few, some, lots, many", var="MONSTER_SELECT")'
Upgrades: 'gui_drop_down("$text: {UPGRADE_SELECT};list: none, few, some, lots, many", var= "UPGRADE_SELECT")'
Time Limit: 'gui_input("desc: Minutes;", var="GAME_TIME_LIMIT")'
There are a bunch of
gui_whatever() functions that can be used, and they all work very similarly. They need a $text attribute and a var attribute. $text is the text displayed on the gui element. The var attribute's value is the name of a variable that can be used by your mission script.You'll notice that within
Properties, there's two sub keys: Main and Map. You can add more of these if you want, they are collapsible groupings. Within each group, you can add however many settings you desire by adding additional gui_whatever() functions, which will most likely be dropdowns and sliders as shown above.
Now how do we use these? Well, as mentioned previously, the var attribute in the gui functions tell MAST that the selected value of the gui element is assigned to the specified variable. If we wanted to, we could use the settings like so:
if LETHAL_SELECT == "none":
mine_count = 0
if LETHAL_SELECT == "few":
mine_count = 20
if LETHAL_SELECT == "some":
mine_count = 40
if LETHAL_SELECT == "lots":
mine_count = 60
if LETHAL_SELECT == "many":
mine_count = 80
terrain_to_value(LETHAL_SELECT) function to convert "none", "few", etc to numeric values, which is usually a much more useful form of information for code.
This is probably a good time to note that if a variable is in all caps like LETHAL_SELECT, it should only be changed if it really needs to be changed. You can define your own variables in all caps as well to identify variables that should not be changed.
We'll come back and use settings more in later sections.
6. Spawning Terrain
So we've got player ships and nothing else on the map right now. Let's add some terrain. For our mission, we want to have an asteroid field. One of these asteroids will contain the treasure. There's a whole slew of ways we could spawn a bunch of randomly placed asteroids. This isn't the place to go through those, so I'll instead refer you to the tutorial on terrain.
There's a very simple way to spawn generic terrain, but without any customization:
# Convert 'some' or whatever to a numeric value
terrain_value = terrain_to_value(TERRAIN_SELECT)
# A function from Legendary Missions that spawns a bunch of asteroids based on the terrain value
terrain_asteroid_clusters(terrain_value)
7. Spawning Enemies
It wouldn't be an Artemis mission without its core gameplay: combat! We need to add some enemies to our map. Similar to terrain, there's a host of ways to spawn enemies. We'll look at two options for now, and other tutorials will go into more detail on the rest.
First, we'll look at the npc_spawn() function. This can be used to spawn any npc, be it an enemy, a friendly, or a neutral.
npc_spawn takes these arguments:
npc_spawn(x, y, z, name, side_csv, art_id, behave_id)
x, y, and z are the spawn coordinates.name is the display name shown in the UI, e.g. "Artemis" or "D51".side_csv is a comma-separated list of roles — the first role is treated as the side (e.g. "tsn", "torgoth"). Additional roles can be stacked here too, e.g. "tsn, station".art_id determines the 3D model and texture. Valid values are the artfileroot fields in shipData.json.behave_id controls default NPC behavior. Use "behav_npcship" for ships or "behav_station" for stations.As an example:
npc_spawn(300, 800, 0, "Ravager of Worlds", "torgoth", "torgoth_leviathan", "behav_npcship")
npc_spawn() is great for spawning a single ship, especially if it has a particular name and art, or a particular purpose in your story. But if you want to spawn, say, a generic Kralien fleet, it would be a hassle to spawn each ship using npc_spawn().
Instead, MAST has a 'prefab' system. A prefab is just a label with metadata describing its configuration. To use a prefab, we call the prefab_spawn() function. LegendaryMissions includes several prefabs, including one specifically for enemy fleets called prefab_fleet_raider. Using it is straightforward:
prefab_spawn(prefab_fleet_raider)
prefab_spawn() has a second argument where you can put that data.
Let's look at the metadata for spawn_fleet_raider:
type: prefab/fleet/raider
race: kralien
ship_roles: raider
fleet_roles: raider_fleet
fleet_difficulty: 5
brain:
The metadata keys type, race, ship_roles, fleet_roles, fleet_difficulty, and bran can all be customized. I've left out the contents of brain for brevity, and because AI brains are beyond the scope of this tutorial. What isn't obvious here is that the variables START_X, START_Y, and START_Z can be set for any label, even if it's not relevant. So how do we use this. Let's say we don't need to change any of the values in the metadata except for fleet_difficulty and the position coordinates.
prefab_spawn(prefab_fleet_raider, {"fleet_difficulty": DIFFICULTY, "START_X": 1000, "START_Y": 20000, "START_Z": -100})
DIFFICULTY setting? This can be used for other labels as well, and makes labels very flexible.
Note that the data argument is effectively a Python Dictionary, if you want to dig into those more. Fleets, their AI, and further customization are topics beyond the scope of this tutorial.
8. Utilizing Roles
In the metadata section above, there were two keys ship_roles and fleet_roles. The Role system in Cosmos is a powerful and integral part of MAST, and its definitely worth discussing roles in some detail here.
Every object in the game has roles associated with it, and they can have any number of roles. Some of the default roles are listed in shipData.json. Remember how I said we'd discuss the side argument in npc_spawn()? Well, it turns out that the side of an object is also a role, but an object can only have one side, as opposed to unlimited roles, and sides determine what shows up in the game as friendly vs enemy vs neutral.
Now, how do we use roles? There are a few functions that use them. The three you will probably use most often are role() add_role(), and has_role().
In my opinion, role() is the really fun one. If we call role("my_role"), it returns a Set containing the IDs of all objects that have the role "my_role". We can use set operations to find specific subgroups in really elegant way. Let's say we want to find all TSN ships that are not carriers:
not_carriers = role("tsn")-role("carrier")
has_role() is simple; it returns True if the specified object has the role, otherwise it returns False:
if has_role(ship, "my_role"):
# Do something
add_role() seems similar in use. It adds the role to the object, but does not return anything.
add_role(ship, "new_role")
For our tutorial mission, we want a particular asteroid to contain treasure. We've spawned the asteroids already, but how can we make one of them special? We can start by using role()!
asteroids = role("asteroid")
random_asteroid = random_id(asteroids)
add_role(random_asteroid, "treasure")
Now let's use our newfound knowledge of roles to do....
9. Science!
In our tutorial mission, we want our players to be able to find the right asteroid. To do that, the asteroid will have special scan text, so the science officer will need to scan all asteroids until the treasure is found. First, we need to tell MAST that science should be enabled for objects that meet certain criteria. In our case, the criteria is an object with the "treasure" role:
//enable/science if has_role(SCIENCE_SELECTED_ID, "treasure")
SCIENCE_SELECTED_ID. All routes have similar variables, and they can be found here. The big difference between route labels and other types of labels is how and when they run. While main and sublabels are scheduled by the mission writer, routes run whenever the specified conditions are met. In this case, the object represented by SCIENCE_SELECTED_ID must have the "treasure" role.We have another important step before we begin adding science scans:
//science if has_role(SCIENCE_SELECTED_ID, "treasure")
//enable/science decides whether an object can be scanned at all; //science decides what the scan says. Without the enable route the science console never offers the scan, and the text you write below is never reached.
Now, on the scan text area of the science console, there are some buttons: "scan", "status", or "info", for example. Buttons are easily added to scan info using the plus sign, followed by the name in quotes:
+ "scan"
+ "status"
+ "bio"
+ "scan"
<scan>
% This asteroid has some odd radiation readings...
+ "scan"
<scan>
% This asteroid has some odd radiation readings...
% Scans of the asteroid show that part of the asteroid is inconsistent with the rest.
% WHOA! These readings are off the charts!
+ "status":
<scan> # Give a hint as to how to access the treasure
% The asteroid has a dense core, surrounded by large quantities of normal asteroid material. Maybe we can blast some of it away...
10. Damage and Destruction
In the last section, we learned about route labels. Remember how I said that routes run whenever the conditions are met? Well, there are routes for damage - several, in fact. Here's the list:
# Something took weapons damage - and see the warning below, because a script
# DELETING an object looks exactly like this too
//damage/object
# An NPC has been killed and is about to be removed from the engine
//damage/killed
# ANYTHING is being removed - NPC, player ship, station, terrain - for any reason
//damage/destroy
# A player ship is taking damage from overheating
//damage/heat
# An engineering node on a player ship is damaged by something other than heat
//damage/internal
Everything below was measured against the engine rather than reasoned about, by two
probes you can re-run: LM_TestRange/maps/test_damage_routes.mast for what a MISSION sees,
and event_probe - a mission that loads no
libraries at all, so what it records is the engine with nothing of ours in between.
//damage/killed is the kill. It fires for NPCs only - the engine's event is literally
called npc_killed, and it names the VICTIM, so it fires whether an NPC or the CREW made
the kill. A player ship's own death never fires it; that arrives as the
player_ship_destroyed signal instead.
//damage/destroy is the removal, and it is not fussy: an NPC, a player ship, a
station, a rock - anything leaving the game fires it, including an object your own script
deletes.
A script delete looks like weapons damage
sbs.delete_object() fires both //damage/destroy and //damage/object. So a
mission that tidies up - unused player slots at startup, a spent pickup, last
chapter's scenery - trips every damage route it has written. This is not theoretical:
a mission counted losses on //damage/destroy and reported casualties before anyone
had been shot at.
Worth knowing why, because it explains the shape of all of this: the ENGINE sends one
event for a removal (damage with sub_tag destroyed, carrying the victim's side
and no attacker). MAST fans that single event out to both routes. So this is true for you
as an author, and it is not the engine sending two things.
Crediting a kill: read parent first, then source
One rule covers every weapon: DAMAGE_SOURCE_ID is whatever dealt the damage, and
DAMAGE_PARENT_ID is the ship behind it. A beam has no projectile, so the shooter
IS the source and parent is 0. A torpedo is its own object, so source is the
torpedo - already gone by the time you look - and parent is the ship that fired it.
So read parent, then fall back to source. Read source alone and every torpedo kill is credited to a torpedo; read parent alone and every beam kill is credited to nobody.
We want the players for our mission to shoot at the asteroid, and to detect when that happens, we'll use the //damage/object route. Once it's been hit, we're going to get rid of the asteroid and replace it with the treasure.
//damage/object if has_role(DAMAGE_TARGET_ID, "treasure")
sbs.delete_object(DAMAGE_TARGET_ID)
sbs module is a set of functions that are directly mapped to C++ functions in the Cosmos game engine. There are a bunch of modules that are built into python and are available for us to use, such as math and random. There's also a few MAST-specific ones, such as faces and scatter.We've now gotten rid of our asteroid. How do we replace it with treasure? Let's say we want the treasure to be an upgrade. We can use the
pickup_spawn() function:
pickup_spawn(x, y, z, "tauron_focuser")
//damage/object if has_role(DAMAGE_TARGET_ID, "treasure")
# Take the role off FIRST - this is the warning above, biting this very example.
# Deleting the asteroid fires //damage/object again for it, and a deleted object
# KEEPS its roles, so the guard still matches and the route runs a second time:
# two treasures, the second one wherever a deleted object reports its position.
remove_role(DAMAGE_TARGET_ID, "treasure")
pos = get_pos(DAMAGE_TARGET_ID) # This line gets the position of an object.
sbs.delete_object(DAMAGE_TARGET_ID)
pickup_spawn(pos.x, pos.y, pos.z, "tauron_focuser")
11. Testing our mission
Great, let's test what we've got so far! To simplify our testing, let's only spawn one asteroid by changing one line:
# Let's just comment this line out while we test
# terrain_asteroid_clusters(terrain_value)
# And we'll add this line instead
treasure = terrain_spawn(200,0,150,None,"#,asteroid","plain_asteroid_9","behav_asteroid")
Fire up Cosmos, check the science scans. All good?
Now flip over to weapons and target the asteroid... and we get an error like this, with a good deal more besides.

Errors are an unavoidable part of mission writing for Cosmos. Fortunately, crashes are very rare, and we can easily restart the mission after we fix the issue. This particular error is a bit of an edge case. It's caused by
resource_gather.py, which is part of the commerce addon in LegendaryMissions. There are a couple of ways around this. The simplest for now is to open story.json and remove the commerce module - nothing in the mission uses it yet.Note that if we use
terrain_asteroid_clusters(), this is not an issue and we can use the commerce addon as normal.
Debug logging
It is often useful to log the value of variable or other bits of information to verify that your code works properly, or to help isolate the cause of an issue. MAST has a built-in logger system, but I prefer to get a message in-game. I usually use the comms_broadcast() function. This function can send a message to the server (and all clients), or it can send the message to only a specific client if necessary.
test_var = 10
comms_broadcast(0, "My test var: {test_var}")
# All clients and the server will show: "My test var: 10"
If you're not feeling adventurous about bugfixes yet, you can skip this next section for now.
For the adventurous
If you open resource_gather.py, and compare with the error message, you'll see that line 31 is where the issue lies. The value of x is None. A few lines earlier: x = blob.get("local_scale_x_coeff",0). The commerce module expects the data_set to contain local_scale_x_coeff, but since it's returning None, the value hasn't been set. terrain_asteroid_clusters() sets this automatically, but terrain_spawn() does not. We just need to set the local scale coefficients ourselves:
treasure.data_set.set("local_scale_x_coeff", 1)
treasure.data_set.set("local_scale_y_coeff", 1)
treasure.data_set.set("local_scale_z_coeff", 1)
Back to mission writing!
Okay, now it's working great. But of our several objectives for this mission, the fourth is this: "They will need to collect the treasure by giving instructions to a specialist team to retrieve it." Upgrades can be collected just by flying the ship near them, so that won't do. Let's spawn a container object at the treasure location instead, using terrain_spawn().
# Instead of
pickup_spawn(pos.x, pos.y, pos.z, "tauron_focuser")
# Let's use
treasure = terrain_spawn(pos.x, pos.y, pos.z, None, None, "container_2a", "behav_pickup")
Challenge Time!
Before we move on, use what you know about roles and route labels to give the treasure some scan information.
12. Recurring Tasks
Let's recap what we've done so far in our mission.
* Spawned player ships and terrain
* Determined the asteroid that has the treasure
* Added scan text to the asteroid
* Detected if the asteroid has been damaged
* Replaced the asteroid with the treasure
Looking back at our list of objectives for our mission, we've completed #2 and #3. But #1 is only partially completed. We only spawn a single fleet of enemies. We want to keep the player on their toes while they search. To do that we're going to implement a recurring event where a fleet of enemies will spawn at a random location roughly 15,000 units from the players every 5 minutes or so.
Labels to the rescue
Remember that one of the key aspects of a label is that it is reusable. We can create a label, and run that label whenever we need to spawn more ships.
First, we need to identify what should happen within the label. The way I see it we have three things we need to do:
* Determine where the players are
* Determine where the enemies should spawn
* Spawn the enemies
To determine the location of the players, you'll need to know that the role for player ships is "__player__". We'll assume that there is only one player ship. Go ahead and see if you can get the id of the player ship on your own!
Now that you've given that a shot, let's see if you did what I did:
Get the player ship
players = role("__player__") # a set of all the players
ship = random_id(players) # Since there's only one (we assume), this returns the id of that ship.
Now let's figure out where enemies should spawn. We want them to spawn about 15k away from the ship, and we haven't specified any particular direction. You could find a random distance in one axis, then use some trig to figure out the distance in the other axis, but we've got other options. I'm looking at the scatter_arc() function:
pos = get_pos(ship)
points = scatter_arc(1, pos.x, pos.y, pos.z, 15000, 280, 320)
We can now spawn our enemies. We've learned a couple different ways to do this, but for now we'll stick with using the existing
prefab_fleet_raider prefab. This is only slightly more complicated because we used a scatter function:
for point in points:
prefab_spawn(prefab_fleet_raider,{"START_X":point.x, "START_Y": point.y, "START_Z": point.z, "fleet_difficulty": DIFFICULTY})
What it should look like
== spawn_area_defenders
" A label to help us spawn new enemies throughout the mission
players = role("__player__") # a set of all the players
ship = random_id(players) # Since there's only one (we assume), this returns the id of that ship.
pos = get_pos(ship)
points = scatter_arc(1, pos.x, pos.y, pos.z, 15000, 280, 320)
for point in points:
prefab_spawn(prefab_fleet_raider,{"START_X":point.x, "START_Y": point.y, "START_Z": point.z, "fleet_difficulty": DIFFICULTY})
Scheduling the label
Now we need to schedule the task. We know how to use the task_schedule() function. But we need to have the enemies spawn periodically. You can get pretty creative with how to do this - you could trigger the label when the last enemy is destroyed. You could set timers for every 5 minutes until the mission ends. Probably the simplest is to use the delay_sim() function at the end of the label and then jump back to the beginning of the label:
await delay_sim(minutes=5)
jump spawn_area_defenders
await again here. This pauses task execution until the delay is complete.I'm sure you've also seen that we're using the
jump keyword instead of task_schedule(). This is a MAST-specific keyword, and it works very similarly to the GOTO keyword in BASIC. There is a difference between using task_schedule() and jump - task_schedule() creates a new, separate task, but continues the execution of the original task, while the jump keyword keeps the execution flow in the same task. This can be useful in a situation where you want to use the same metadata. An example of this can be found in station_prefabs.mast in Legendary Missions.jump could also be replaced with ->. Its meaning and use are identical:
-> spawn_area_defenders
13. Using comms
We now need to learn how to interact with our specialists, who will collect and analyze the treasure. To do this, we need to learn about comms buttons and lifeforms.
Lifeforms
Lifeforms are a flexible way to handle communication with an NPC character. They are tied to a host space object — a ship, station, or other entity — and the comms officer selects that object to open a comms channel with the lifeform.
Coming soon: This section is still being written. For now, see the Simple Comms tutorial for a working example of comms routes and dialogue buttons.
14. Handle game end
(coming soon)