Well...
Cholmondely wrote: ↑Sun Jul 11, 2021 12:48 pm
I don't want a step-by-step instruction as to what to do
Now that's a challenge! Haven't written a book before. Just really large OXP's...
Maybe we should start by breaking down what's currently in the Long Way Round mission, then we can nail down what changes we want to make, and then look at ways to implement said changes. This is going to be a long one, folks.
So, what's in the original mission pack?
The heart of the mission is in two files: missiontext.plist, and script.js. missiontext.plist doesn't have anything to do with the flow of the mission, merely holding mission text and descriptions. So, the majority of the functionality of the mission comes from the script file.
Before we look at the content of the script file, it's worth noting that the name of this file is significant. Oolite will automatically include in the world scripts pool any "script.js" file that is placed in the "Config" folder of an OXP. That means we don't need to do anything additional to have the script file included with all the other script files. If we wanted to organise out scripts and break them out in some way, we would need to have a "world-script.plist" file that specifically names all the JS files we're adding, and those JS files would need to be in the "Scripts" folder. But for now, given the simplicity of this mission, we can stick with the single "script.js" file.
The content of this file can be broken down into four main sections:
1. Header and preamble
2. Script-wide variation declarations
3. World event definitions
4. Custom JS routines.
We'll look at each of these in turn
1. Header and preamble
Code: Select all
"use strict";
this.name = "LongWayRound";
this.author = "aegidian, phkb";
this.copyright = "2018 phkb";
this.description = "Script for Long Way Round mission - original script by aegidian, JS script by phkb.";
this.licence = "CC BY-NC-SA 3.0";
These lines are generally at the top of every JS script file. From a functionality point of view, only two items are considered necessary
(a)
"use strict";
tells the Javascript engine to enforce variable declarations and catches a few other common errors. Basically it's a way of making sure the code that follows passes some basic tests. (Edit to add: while it is not strictly necessary to include this like, it is highly recommended).
(b)
this.name = "LongWayRound";
this line defines the name this script will have in the
Javascript object model world script pool. It must be unique.
The other elements are useful, but not necessary for the functionality of the script.
2. Script-wide variable declarations.
Code: Select all
this._counter = 0;
this._timer = null;
This script defines two variables that will be used at various points around the script. The first is
"this._counter = 0;"
. Of note here is the prefix of "this." - any time you see this prefix on something, it is referring to something inside the script file. If that prefix is missing, then the JS engine will assume the reference is to something inside the current function it is executing. (It can get a lot more complicated that this, but we'll leave the discussion about context until later).
"this._counter" is used later in the script to count time before spawning some enemies (which we'll get to later). It is being predefined with a value of 0 (zero), which causes the JS engine to recognise it as an integer number.
You can define variables with different types, based on the content of the variable. Javascript is, shall we say, "flexible" when it comes to variable types. It doesn't enforce strict type definitions. So, you can define a variable as an integer variable (like this._counter), but then later put a string value inside it (ie some value inside quotes), or even an complex object (we'll get to these later). Javascript is fine with that. Your program might not work, but JS will plough on regardless (if the code didn't just implode into a million random bits). JS is not a "type-safe" language. Which means you need to be careful about how your variables are defined and (more importantly) used.
Anyway, because we've defined this._counter as an integer, we can do arithmetic operations to it.
"this._timer" is going to be a reference or pointer to a timer object, but for now is just given a default value of "null", which in essence means it's pointing at nothing. (For non-programmers, there is a distinction between simple objects, like integers, and complex objects, like a timer. With a simple object, the variable is the value. In our example, "this._counter" equals zero. For a complex object, the variable is a pointer to the object, a way of accessing it, like a phone number is a way of accessing a person. Having a reference to an object is important, because otherwise there is no way to access it).
These variables are set up when the script is loaded, and before any of the scripts functions are executed.
3. World event definitions.
The rest of the script file contains functions, the first type being world event function definitions. World events are game-specific events that occur at pre-defined times, usually in response to some sort of game action or activity. To hook into one of these events, the first job is to decide which event you need to use. You can see a full list of all the different events and their function definitions on the wiki.
Once you have chosen the appropriate event, you can copy the definition from the wiki and paste it into your script file. Then, when the event takes place, Oolite will go through all the script files that have been loaded and run each of the event scripts that it finds.
For "Long way round", there are three world events being hooked into: "startUp", "missionScreenOpportunity", and "shipExitedWitchspace".
The first one, "startUp", is called after all OXP scripts have been loaded, either after a new game has been started, or a game file has been loaded. It's normally used to do any initialisation steps that should only be done once (for example, loading game file saved values). In our case, it's doing some cleanup:
Code: Select all
if (missionVariables.longwayround === "MISSION_COMPLETE") {
delete this.missionScreenOpportunity;
delete this.shipExitedWitchspace;
delete this.startUp;
}
What this snippet is doing is checking a variable "longwayround" stored in the "missionVariables" object (I'll get to that shortly). The "longwayround" variable is how this mission keeps track of where the player is up to. When they complete the mission, the "longwayround" variable is set to "MISSION_COMPLETE", and if the game loads in with this stored value, it then deletes all the world event script items so that those pieces of code don't get run unnecessarily.
The "missionVariables" object is a special object used by Oolite to save any information between sessions. Scripts can add anything they like to this object, but to ensure everything works smoothly, the types of things that should be saved are strings (ie any characters between quotation marks) and numbers. If you have a complex object which needs to be stored, like a dictionary or array (we'll get to those things later), OXP's usually convert them into a string. We can cover that later as well when we start needing to store more detailed info.
The next function is "missionScreenOpportunity". This function is called if there are no mission screens active and the player is docked at a station. It can fire multiple times, so any code inside here will need to make sure it can only run once.
In our example, the routine begins with this check:
Code: Select all
if (player.ship.dockedStation.isMainStation && galaxyNumber === 0) {
This is checking to make sure the player is docked at a main station, somewhere in Galaxy 1 (the "galaxyNumber" is zero-based, so a value of 0 means Galaxy 1). If both those things are true, we move on to the next checks:
Code: Select all
if (!missionVariables.longwayround && system.ID === 3) {
This is checking to see if there is no "longwayround" variable inside the missionVariables object (the "!" mark prefixing the statement makes the check into a "not" - the long-winded version of this would be
if (missionVariables.longwayround == undefined...
). We're also checking to see if we're in system ID 3 (Biarge). If those two things are true, we then bring up the first mission page.
Code: Select all
mission.runScreen(
{
screenID: "longwayround",
title: "Incoming Message",
exitScreen: "GUI_SCREEN_STATUS",
messageKey: "long_way_round_Biarge_briefing"
}
);
missionVariables.longwayround = "STAGE1";
mission.markSystem({system:248, name:this.name});
mission.setInstructionsKey("em1_short_desc1", this.name);
return;
There are 4 things happening here. First, we're showing the mission screen. Second, we're changing the value of missionVariables.longwayround to be "STAGE1". Next, we're marking system ID 248 (Soladies) so that it shows on the galaxy map. And finally, were putting some instructions on the F5F5 manifest screen, to remind the player what they should be doing to further this mission.
The mission screen definition is perhaps the most basic example of a mission screen, in that there are no options or anything complicated. We define a screen ID (which isn't used here, but it can be helpful for other OXP's to know), we put a title on it, we tell it where to exit to when the mission screen closes, and we tell it what key to use out of missiontext.plist to get the text to display.
After all these steps are done, we get to the "return;" statement. That will exit the function at that point, without executing any more code.
If, however, we've docked at a main station in a system other that Biarge, the code would fall through to the next IF statement:
Code: Select all
if (missionVariables.longwayround === "STAGE1" && system.ID === 248) {
This is checking to see if our mission tracking variable has been set to "STAGE1" (ie we've started the mission), and we've made it to Soladies. If so, we then display the next mission screen:
Code: Select all
mission.runScreen(
{
screenID: "longwayround",
title: "Incoming Message",
exitScreen: "GUI_SCREEN_STATUS",
messageKey: "long_way_round_Soladies_briefing"
}
);
missionVariables.longwayround = "STAGE2";
player.credits += 500;
player.consoleMessage("You have been awarded 500cr.");
mission.unmarkSystem({system:248, name:this.name});
mission.markSystem({system:233, name:this.name});
mission.setInstructionsKey("em1_short_desc2", this.name);
return;
Again, a very simple mission screen, with no options or complexities. Just a different key to use to find the text to display. We also change the mission tracking variable to "STAGE2", we award the player some credits, then we tell them about their windfall via a consoleMessage. Next, we unmark Soladies on the galaxy map, and mark a new system, ID 233 (Qubeen), and update the instructions on the F5F5 manifest page. Once all these things are done, we exit the function.
We'll assume the player has moved on to Qubeen and docked. That will trigger the third IF statement in this function:
Code: Select all
if (missionVariables.longwayround === "STAGE2" && system.ID === 233) {
If our tracking variable is at "STAGE2" (ie. we've stopped in Soladies and picked up the passenger), and we're now docked in Qubeen, we display the final mission screen.
Code: Select all
mission.runScreen(
{
screenID: "longwayround",
title: "Incoming Message",
overlay: "loyalistflag.png",
exitScreen: "GUI_SCREEN_STATUS",
messageKey: "long_way_round_Qubeen_briefing"
}
);
missionVariables.longwayround = "MISSION_COMPLETE";
player.credits += 2000;
player.consoleMessage("You have been awarded 2000cr.");
mission.unmarkSystem({system:233, name:this.name});
mission.setInstructionsKey(null, this.name);
delete this.shipExitedWitchspace;
delete this.missionScreenOppportunity;
return;
Another simple mission screen, just with an overlay image that will be displayed behind the text and a new key to use to look up the text to display. We set our tracking variable to "MISSION_COMPLETE", we award to player 2K in credits, tell them about it with a console message, then unmark the system on the galaxy map, remove the instructions from the F5F5 manifest screen, and remove the two functions that control the operation of the mission (shipExitedWitchspace and missionScreenOpportunity).
And that's the end of the missionScreenOpportunity function call.
We move on to the final world event in this mission, shipExitedWitchspace. This world event is called after the player arrives in a system and the tunnel effect has been shown. The system should be full populated by this time. In our function we're checking 3 things:
Code: Select all
if (galaxyNumber === 0 && system.ID === 233 && missionVariables.longwayround === "STAGE2") {
Are we in galaxy 1? Are we in system 233 (Qubeen)? And is our tracking variable set to "STAGE2" (ie we have a passenger on board)? If all those things are true, we can start the challenge of the mission, which we initiate by starting a timer.
Code: Select all
this._counter = 0;
this._timer = new Timer(this, this.$addExtraShips, 1, 1);
system.addShips("longWay_rebel", 2);
Here is where we start accessing those variables we defined at the top of our script. At this point though, all we're going to do is just make sure this._counter is set to zero. On your first play-though, it should already be zero, but if you have arrived here previously and jumped out before docking at the main station, it could be at a value greater than zero, so we'll set it to zero for safety.
Next, we start a timer. We keep a reference to the timer in
this._timer
, and anytime you create a timer, you should keep a reference to it in a similar fashion.
You'll notice I said a "reference". A timer is a complex object. An integer or a string is a simple object - they have generally 1 value, and there is a limited number of things that can be done with them. Complex objects, like a timer, can have multiple values, all independent. Each value of an object is called a property. For a timer, there are 3 properties and 2 methods available. The properties are "interval" (the rate at which the timer repeats, in seconds), "isRunning" (a true/false value indicating whether the timer is running or not), and "nextTime" (the next time this timer will fire). And a timer has two methods: "start" and "stop". We'll get to these in a minute.
A timer is created by using the code "new Timer". Into this definition we can pass a number of properties: the first is always "this", which basically indicates where the timer will call home. It sets the context* of what "this" will refer to in the next property, being the function to call when the event fires. In our case, the function we're calling is "this.$addExtraShips". The next two properties are the seconds until the first firing, and the next is how often afterwards the event will fire. If we left off the last number, the event would only fire once.
(* A bit of an aside about context: Context in code is very important. As the Javascript executes the various functions, the execution happens inside a particular context. If something is referenced in that context that hasn't been defined, errors will occur. To use an example: in a group of friends, where "Fred" is present, someone might say, "Fred's shirt is so bright it's hurting my eyes." Now, if there was another group of people, but only one of them knows of Fred and his predilection for loud shirts, and this person says the same thing, no one in the group will understand. They don't have the right context to know who Fred is. So, the context in scope at the time a piece of code is run is very important. Normally, the context of our JS script files is the script file itself - which is why we can refer to "this" throughout the script. And if our code runs in isolation from other scripts, in that we don't need to reference or use anything from somewhere else, we can usually be confident the context will be our own script file. But, timers can throw this out of alignment if we don't set the context correctly when creating them).
Immediately after starting the timer, we add 2 ships with the role "longWay_rebel" to the system at the witchpoint. The witchpoint is the default position when no other parameters are given.
So, we've arrived in the system, and there are now two ships at the witchpoint who are probably trying to poke us with a sharply pointed laser at this point. Then, after 1 second passes, the timer fires, and we execute the "$addExtraShips" function. We now move onto the final section of code.
4. Custom JS routines.
Custom routines are created by the programmer to perform whatever functions are required. You can fill a script file with these functions, but none of them will be called without some trigger. In our case, the trigger is a timer firing and causing the function to execute. But they can also be called from world events, or ship scripts, or any number of other possibilities. The point is, while world event functions have a predefined structure, our custom functions can have whatever properties and parameters we want.
In our example, the structure is quite simple:
Code: Select all
this.$addExtraShips = function $addExtraShips() {
This line of code defines our function. The function is called "$addExtraShips", and it exists in the context of the "LongWayRound" script file (ie, it's what "this" is referring to). The repeated function name is not required, but is quite useful for debugging, particularly with timers. We'll ignore it for now. Finally, the "()" are where we could potentially pass some parameters into our function. In this case, there are no parameters required, so the brackets are empty.
The first section of code in the function is doing a check to see if the player has died:
Code: Select all
if (player.ship.isValid === false || player.ship.isInSpace === false) {
this._timer.stop();
return;
}
If the player.ship object is invalid, or it's not in space, then something has happened to them. There's no point in going on at this point, so the timer is stopped by calling the "stop" method of the object. And we then exit this function.
But, assuming the player has managed to survive, we move onto the next piece of code:
Code: Select all
this._counter += 1;
if (this._counter < 60) {
if (system.countShipsWithPrimaryRole("longWay_rebel") < 5) {
if (Math.random() < 0.5) {
system.addShips("longWay_rebel", 1);
}
}
} else {
this._timer.stop();
}
Here, we're adding 1 to our counter variable. We're then checking to see if this value is less than 60. What this means in practical terms, given that our timer is running every second, is that we will only be doing this for 1 minute after the player arrives. So, if it's less than 60, we're then asking the system to tell us how many ships with the role of "longWay_rebel" are currently present. If that number is less than 5, there is then a 50% chance that another ship will be spawned at the witchpoint.
If it's been more than 60 seconds, though, we tell the timer to stop. That's the bit after the "else" statement.
So, functionally, that's how the script currently works. The OXP also includes a ship definition for the longWay_rebel, with a texture to go with it.
There's more to follow, and I'll probably have some grammar/spelling issues to correct, but that's a start.
(Edited to add some clarity to some of the discussion points)