About this series This series documents the development of the EmptyEpsilon scenario Beacon of Light, which is included with the game. EmptyEpsilon uses scenarios scripted in the Lua scripting language, which allows for many types of scenarios: basic sandboxes, story-driven campaigns, randomized enemies, and even dynamic branching dialogue. These scenarios are located in the "scripts" folder where EmptyEpsilon is installed. The Beacon of Light scenario begins with instructions for the players to follow, then a series of challenges for them to overcome. The scenario scripting system can be used to create even more complex scenarios, but this guide won't cover such features. All of the scenarios are viewable and editable, however, and you can look at the "Waves" scenario in the scripts folder to see an example of complex randomized events. If you downloaded the official EmptyEpsilon build or compiled it yourself, it also comes with a scripting guide that lists the available functions. You can follow the first parts of this guide even if you have little or no programming knowledge. Later sections cover more advanced topics. Tools For scenario development, you need a basic text editor. While Windows Notepad can work, I highly recommend something more advanced. There are many options, including these free editors: Programmer's Notepad (Windows)

Sublime Text (Windows, OS X, Linux)

Atom (Windows, OS X, Linux)

Notepad++ (Windows) I use Programmer's Notepad, but any of these editors will do. 1. Setting up the scenario file We'll start with an empty scenario as a template. Open your text editor and enter the following code: -- Name: Beacon of light series -- Description: The beacon of light scenario, build from the series at EmptyEpsilon.org. --- Near the far outpost of Orion-5, Exuari attacks are increasing. A diplomat went missing, and your mission will start with recovering him. -- Type: Mission -- Init is run when the scenario is started. Create your initial world function init() -- Create the main ship for the players. player = PlayerSpaceship():setFaction("Human Navy"):setTemplate("Atlantis") end function update(delta) end Save this file as scenario_99_tutorial.lua in the folder EmptyEpsilon/scripts. Open EmptyEpsilon and start a new server. Your scenario should be at the bottom of the list. Select the scenario and start it, and notice that there a ship for the players is already present. Tips & Tricks The file must start with scenario_ and must be located in the scripts folder for EmptyEpsilon to recognize it as a scenario.

The number in the scenario filename sets the order in which EmptyEpsilon shows the scenarios. The final part of the filename is for reference only.

The scenario name and description displayed in the game are set by the -- Name: and -- Description: lines at the beginning of the scenario file. For multi-line descriptions, each line should immediately follow the previous line and start with three dashes ---.

Other lines starting with a double dash -- are comment lines in the scenario script that you can use for your own reference. The game ignores them.

To quickly examine the scenario, you can use the Game Master screen listed with the Alternative Options on the ship selection menu. This lets you view and modify the entire game world with ease.

Building a universe Now that you have setup a basic script and know how to save and run it, let's populate the universe a bit more. First, let's fill it with some stations, and then add some nebulae and asteroids. Adding stations The only thing in our universe is the player ship. This doesn't make for a very interesting universe, so let's add some space stations that they can dock and interact with. When looking at your scenario from the Game Master screen, the starting point is located at the top left corner of sector F5. These coordinates are "0, 0", with the X coordinate first and the Y coordinate second. If you click on the player ship on the Game Master screen, you can see its coordinates in the top left corner. Note as you move up/north/on heading 0, your Y coordinate decreases; locations north of the starting point have a negative Y coordinate. Moving right/east/on heading 90 increases your X coordinate. Also note that in-game distances are measured in units (U), while scripts measure distances in milliunits -- 1U in the game is equal to 1000 in a script. See this image for an example. Add the following code after the creation of the player ship: SpaceStation():setTemplate("Small Station"):setFaction("Human Navy"):setPosition(23500, 16100) This line places a small Human Navy station 23.5U (23500) to the right of the starting point at sector F5, and 16.1U (16100) down from the starting point. Since each sector is a 20U square, this places the station in sector F6. The player ship also belongs to the Human Navy faction, so the station will be friendly to the players. Next, add these lines to add three more stations: SpaceStation():setTemplate("Medium Station"):setFaction("Human Navy"):setPosition(-25200, 32200) SpaceStation():setTemplate("Large Station"):setFaction("Exuari"):setPosition(-45600, -15800) SpaceStation():setTemplate("Small Station"):setFaction("Independent"):setPosition(9100,-35400) This adds another friendly station in sector G3 (25.2U left and 32.2U down from the starting point), a large enemy Exuari station in sector E2 (45.6U left and 15.8U up from the starting point), and a small neutral station in sector D5 (9.1U right and 35.4U up from the starting point). Now, we want the player to start closer to the small friendly station. So we set the player position near the small station. PlayerSpaceship():setFaction("Human Navy"):setShipTemplate("Atlantis"):setPosition(22400, 16200) Creating nebulae Nebulae are interesting additions to a universe. They block a 5U circle on long-range radar as well as the space behind it, which gives players and enemies room to hide and provide interesting tactical options to the game. Let's surround the enemy station with some nebulae so that it's shielded from sensors. --Nebula that hide the enemy station. Nebula():setPosition(-43300, 2200) Nebula():setPosition(-34000, -700) Nebula():setPosition(-32000,-10000) Nebula():setPosition(-24000,-14300) Nebula():setPosition(-28600,-21900) Such a cloud of nebulae clearly hides something, so let's also add a few random nebulae. --Random nebulae in the system Nebula():setPosition( -8000,-38300) Nebula():setPosition( 24000,-30700) Nebula():setPosition( 42300, 3100) Nebula():setPosition( 49200, 10700) Nebula():setPosition( 3750, 31250) Nebula():setPosition(-39500, 18700) Creating asteroids You can add an asteroid in a similar manner as nebulae and stations: Asteroid():setPosition(-1000, -1000) However, because you usually want belts of 50, or even 100, asteroids, placing them all by hand would take quite a bit of time! Fortunately, we can use Lua functions to help add many asteroids quickly. First, let's use the random() function to place an asteroid in a random location of sector E5. Because sector E5 is one sector to the left of sector F5, the range is from 0 to -20000 on the X axis and 0 to 2000 on the Y axis. Asteroid():setPosition(random(0, 20000), random(-20000, 0)) Now let's use a loop to repeat this 100 times.. --Create 100 asteroids for asteroid_counter=1,100 do Asteroid():setPosition(random(0, 20000), random(-20000, 0)) end This is a for loop. It counts from 1 to 100 and performs the action inside once for each count. In this case, it adds an asteroid to a random position in sector E5. Which... results in a pretty square asteroid field. Not that pretty. So right now, let's tweak the parameters to create a field from sector E4 to G4 in a straight line. --Create 50 asteroids for asteroid_counter=1,50 do Asteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) end That's already much better. But from a 3D view, this asteroid belt looks very flat. To fix that, add some VisualAsteroids as well. These asteroids are above or below the ship's plane and can't be hit. They're only there for visuals. --Create 100 asteroids for asteroid_counter=1,50 do Asteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) VisualAsteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) end Spice it up a bit The stations have automatically assigned callsigns, such as DS-7 and other DS-? numbers. We can change these through the scenario scripts. By calling the setCallSign() function on the stations, we can set a different name for each of them. Keep the callsign short. SpaceStation():setTemplate("Small Station"):setFaction("Human Navy"):setPosition(23500, 16100):setCallSign("Research-1") SpaceStation():setTemplate("Medium Station"):setFaction("Human Navy"):setPosition(-25200, 32200):setCallSign("Orion-5") SpaceStation():setTemplate("Large Station"):setFaction("Exuari"):setPosition(-45600, -15800):setCallSign("Omega") SpaceStation():setTemplate("Small Station"):setFaction("Independent"):setPosition(9100,-35400):setCallSign("Refugee-X") We'll call the small station a research station, which gives it a bit more character. The enemy station is called Omega, as it's big. And the neutral station is called Refugee-X, which makes it sound like it could house refugees. These few simple names give the whole universe more character. Finally, let's christen the player ship the Epsilon. PlayerSpaceship():setFaction("Human Navy"):setShipTemplate("Atlantis"):setPosition(22400, 18200):setCallSign("Epsilon") Tips & Tricks In addition to asteroids, stations, and nebulae, you also have other options to make your universe more exciting: BlackHole() -- Creates a 5U-radius black hole that drags anything that comes toward its center and destroys it. WormHole():setTargetPosition(20000, -20000) -- Creates a 5U-radius wormhole that leads to another point in space. Mine() -- A simple mine causes a 1U-radius explosion when a ship comes too close. WarpJammer():setFaction("Exuari") -- A warp jammer, which jams warp and jump drives of other factions in a 5U radius. Can be destroyed. SupplyDrop():setFaction("Human Navy") -- A supply drop, which a friendly player ship can pick up by flying over it. -- Supply drops are empty by default but can be filled with energy and missile weapons through additional script parameters. Compiled example This is the script so far: -- Name: Beacon of light - Day 1 -- Description: The beacon of light scenario, build from the series at EmptyEpsilon.org --- Near the far outpost of Orion-5, Exuari attacks are increasing. A diplomat went missing, and your mission will start with recovering him. -- Type: Mission -- Init is run when the scenario is started. Create your initial world function init() -- Create the main ship for the players. PlayerSpaceship():setFaction("Human Navy"):setShipTemplate("Atlantis"):setPosition(22400, 18200):setCallSign("Epsilon") SpaceStation():setTemplate("Small Station"):setFaction("Human Navy"):setPosition(23500, 16100):setCallSign("Research-1") SpaceStation():setTemplate("Medium Station"):setFaction("Human Navy"):setPosition(-25200, 32200):setCallSign("Orion-5") SpaceStation():setTemplate("Large Station"):setFaction("Exuari"):setPosition(-45600, -15800):setCallSign("Omega") SpaceStation():setTemplate("Small Station"):setFaction("Independent"):setPosition(9100,-35400):setCallSign("Refugee-X") --Nebulae that hide the enemy station. Nebula():setPosition(-43300, 2200) Nebula():setPosition(-34000, -700) Nebula():setPosition(-32000,-10000) Nebula():setPosition(-24000,-14300) Nebula():setPosition(-28600,-21900) --Random nebulae in the system Nebula():setPosition( -8000,-38300) Nebula():setPosition( 24000,-30700) Nebula():setPosition( 42300, 3100) Nebula():setPosition( 49200, 10700) Nebula():setPosition( 3750, 31250) Nebula():setPosition(-39500, 18700) --Create 100 asteroids for asteroid_counter=1,50 do Asteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) VisualAsteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) end end function update(delta) end

Making friends and enemies Now we'll add some things that really spice things up: other ships. About other ships When working with other ships, remember a few basic things: Factions: A ship's faction defines its relationship with other ships. For example, ships of the Exuari faction attack ships and stations of the Human Navy faction, and vice versa.

A ship's faction defines its relationship with other ships. For example, ships of the Exuari faction attack ships and stations of the Human Navy faction, and vice versa. Orders: Every computer-controlled ship has orders that define how the ship behaves. A ship can be ordered to stay still, attack a target, defend a location, or seek out targets on its own.

Every computer-controlled ship has orders that define how the ship behaves. A ship can be ordered to stay still, attack a target, defend a location, or seek out targets on its own. Templates: Every ship's appearance, equipment, and base statistics are drawn from a template. A script can override nearly any trait in a template, except for the ship's model. Your first enemy Let's create an enemy. CpuShip():setShipTemplate("Adder MK5"):setFaction("Exuari"):setPosition(1000, 1000) Fight it! I'm sure you'll win -- notice that it doesn't do anything, not even respond to your attacks. This is because the default orders for a CpuShip are to stay idle and do nothing. Let's give it one of the most basic orders: to seek new targets and attack them. CpuShip():setShipTemplate("Adder MK5"):setFaction("Exuari"):setPosition(1000, 1000):orderRoaming() Now you have an enemy to fight! Let's start setting up the scenario's opposition with 2 cruisers at the lower nebula. They will fly out and seek the player quite well on their own. --Small Exuari strike team, starting in the nebula at G5. CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3550, 31250):orderRoaming() CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3950, 31250):orderRoaming() Defending the station The Exuari have a large station in this area, so let's set up some defence. We'll create 3 ships to defend Omega station. This means we need a way to refer to the station in our script. Edit the lines for Omega station to assign it to a variable. -- Grab a reference to Omega station. enemy_station = SpaceStation():setTemplate("Large Station"):setFaction("Exuari") enemy_station:setPosition(-45600, -15800):setCallSign("Omega") Note how with a variable as a reference, we can move the setPosition() and setCallSign() functions to a new line. Let's also use this reference to assign new ships to its defense. -- Create the defense for the station CpuShip():setTemplate("Starhammer II"):setFaction("Exuari"):setPosition(-44000, -14000):orderDefendTarget(enemy_station) CpuShip():setTemplate("Phobos T3"):setFaction("Exuari"):setPosition(-47000, -14000):orderDefendTarget(enemy_station) CpuShip():setTemplate("Atlantis X23"):setFaction("Exuari"):setPosition(-46000, -18000):orderDefendTarget(enemy_station) With the DefendTarget orders, the ships will circle around the target and attack any enemies in range. You can set ships to defend any object, including another ship. For example, we can setup two extra fighters which follow and defend the Atlantis X23. enemy_dreadnought = CpuShip():setShipTemplate("Atlantis X23"):setFaction("Exuari") enemy_dreadnought:setPosition(-46000, -18000):orderDefendTarget(enemy_station) CpuShip():setShipTemplate("MT52 Hornet"):setFaction("Exuari"):setPosition(-46000, -18100):orderDefendTarget(enemy_dreadnought) CpuShip():setShipTemplate("MT52 Hornet"):setFaction("Exuari"):setPosition(-46000, -18200):orderDefendTarget(enemy_dreadnought) You lose! (Or, you should be able to) When the player ship is destroyed, or when all enemies are destroyed, nothing happens. Let's fix this. First, we want to end the game with defeat if we lose the player ship. We need to edit the lines that add the player ship to create a reference to it. player = PlayerSpaceship():setFaction("Human Navy"):setShipTemplate("Atlantis") player:setPosition(22400, 18200):setCallSign("Epsilon") Next, we'll finally use the update() function that has been empty so far. This function is special: the game runs any code in the update function every game tick, or about 60 times per second. Let's use that to constantly check whether the player ship is destroyed, and if it is, then end the game. The isValid() function checks an object and returns a value of either true or false: true if the object exists, false if it doesn't. With a conditional statement (also called an if statement), we can ask, "Is the player ship not a valid object?", and if the response is true (the player doesn't exist), we can tell the game to declare victory for the Exuari. Doing so in the update function asks that question repeatedly. function update(delta) --When the player ship is destroyed, call it a victory for the Exuari. if not player:isValid() then victory("Exuari") end end Pretty simple, right? And naturally, you can setup victory for the player the same way by checking Omega station. --When Omega station is destroyed, call it a victory for the Human Navy. if not enemy_station:isValid() then victory("Human Navy") end Tips & Tricks - More about ships For all possible ship templates, check the files starting with shipTemplates_ in the scripts folder. These files contain all of the standard ship definitions.

in the folder. These files contain all of the standard ship definitions. In addition to orderRoaming and orderDefendTarget, CpuShips can also have these orders: orderIdle(): Default state. Do not move or attack. orderStandGround(): Hold this position, but attack enemies when they are in range. The ship will fire missiles as well as beam weapons. orderDefendLocation(x, y): Fly toward the given position and defend it from attacks. orderFlyFormation(target, offset_x, offset_y): Fly in formation with the target by keeping a certain offset. When enemies are near the target, engage them. Gives fleet behaviour. orderFlyTowards(x, y): Fly toward a target position and attack enemies when they come too close. orderFlyTowardsBlind(x, y): Fly toward a target position and ignore everything else. orderAttack(target): Attack a specific target. orderDock(target): Dock with a target. Can be used to make computer-controlled ships dock with stations.

Compiled example This is the script so far: -- Name: Beacon of light series - Day 2 -- Description: The beacon of light scenario, build from the series at EmptyEpsilon.org. --- Near the far outpost of Orion-5, Exuari attacks are increasing. A diplomat went missing, and your mission will start with recovering him. -- Type: Mission --Create the main ship for the players. player = PlayerSpaceship():setFaction("Human Navy"):setTemplate("Atlantis") player:setPosition(22400, 18200):setCallSign("Epsilon") SpaceStation():setTemplate("Small Station"):setFaction("Human Navy"):setPosition(23500, 16100):setCallSign("Research-1") SpaceStation():setTemplate("Medium Station"):setFaction("Human Navy"):setPosition(-25200, 32200):setCallSign("Orion-5") --Grab a reference to Omega station. enemy_station = SpaceStation():setTemplate("Large Station"):setFaction("Exuari") enemy_station:setPosition(-45600, -15800):setCallSign("Omega") SpaceStation():setTemplate("Small Station"):setFaction("Independent"):setPosition(9100,-35400):setCallSign("Refugee-X") --Nebula that hide the enemy station. Nebula():setPosition(-43300, 2200) Nebula():setPosition(-34000, -700) Nebula():setPosition(-32000,-10000) Nebula():setPosition(-24000,-14300) Nebula():setPosition(-28600,-21900) --Random nebulae in the system. Nebula():setPosition( -8000,-38300) Nebula():setPosition( 24000,-30700) Nebula():setPosition( 42300, 3100) Nebula():setPosition( 49200, 10700) Nebula():setPosition( 3750, 31250) Nebula():setPosition(-39500, 18700) --Create 100 asteroids. for asteroid_counter=1,50 do Asteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) VisualAsteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) end --Create the defense for the station. CpuShip():setTemplate("Starhammer II"):setFaction("Exuari"):setPosition(-44000, -14000):orderDefendTarget(enemy_station) CpuShip():setTemplate("Phobos T3"):setFaction("Exuari"):setPosition(-47000, -14000):orderDefendTarget(enemy_station) enemy_dreadnought = CpuShip():setTemplate("Atlantis X23"):setFaction("Exuari") enemy_dreadnought:setPosition(-46000, -18000):orderDefendTarget(enemy_station) CpuShip():setTemplate("MT52 Hornet"):setFaction("Exuari"):setPosition(-46000, -18100):orderDefendTarget(enemy_dreadnought) CpuShip():setTemplate("MT52 Hornet"):setFaction("Exuari"):setPosition(-46000, -18200):orderDefendTarget(enemy_dreadnought) --Small Exuari strike team in the nebula at G5. CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3550, 31250) CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3950, 31250) end function update(delta) --When the player ship is destroyed, call it a victory for the Exuari. if not player:isValid() then victory("Exuari") end --When Omega station is destroyed, call it a victory for the Human Navy. if not enemy_station:isValid() then victory("Human Navy") end end