Back to Index Page generated: Dec 20, 2024, 7:22:09 AM

Expansion Lave Academy

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Introduces the Lave Academy Orbital Station to the Lave and optionally other systems. Also features three mini-games that test basic flight skills. Introduces the Lave Academy Orbital Station to the Lave and optionally other systems. Also features three mini-games that test basic flight skills.
Identifier oolite.oxp.Thargoid.LaveAcademy oolite.oxp.Thargoid.LaveAcademy
Title Lave Academy Lave Academy
Category Dockables Dockables
Author Thargoid Thargoid
Version 2.1 2.1
Tags dockables, missions, new commanders, OXPConfig, activities, Library Config dockables, missions, new commanders, OXPConfig, activities, Library Config
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL https://wiki.alioth.net/index.php/Lave_Academy_OXP n/a
Download URL https://wiki.alioth.net/img_auth.php/6/69/LaveAcademy_2.1.oxz n/a
License Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme file Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme file
File Size n/a
Upload date 1709981833

Documentation

Also read http://wiki.alioth.net/index.php/Lave%20Academy

Lave Academy ReadMe & License.txt

Lave Academy OXP by Thargoid
----------------------------

With the rise of interest in becoming a space pilot following the recent galactic financial troubles, Lave Academy has never been busier. To help in both the training of new recruits, plus the skills maintenance and re-certification of more experienced commanders, they have invested in a new orbital station equipped to offer training in three key areas - piloting, gunnery and docking.

It is designed for both new commanders who need a bit of practice before venturing out into the wild black galaxy. The courses are however also open to more seasoned veterans who would like to return to their alma mata and show off their skills to the student populus.

As the popularity has grown further, new stations have been planned for other locations, both in Galaxy 1 and beyond.

This updated version is for Oolite 1.77 and higher. It will not work with older versions of the game.

------------------------------------------------------------------------------------------------------------------------------------------------------------
Brief Instructions
------------------

* Station located at the edge of the security zone of Lave station (G1). A new buoy is also spawned part-way between the main station Nav-buoy and the Academy. Simply fly to the Nav-buoy and face towards the new buoy on your scanner to line up with the Academy (it should be visible anyway). Alternatively you can follow "A" on your ASC where fitted.
* Dock with station by flying into the blue glow in the centre.
* Three training missions available, via special screen accessible by F8 when docked at the station.
* Instructions given in-game, plus detailed instructions below.
* Once test is selected, launch and the fly to the relevant course buoy. It will communicate to your ship to indicate which one to approach.
* For the docking and gunnery tests, keep the main course buoy on-screen at all times during the test.
* All tests are against the clock, with the best time for each recorded.

------------------------------------------------------------------------------------------------------------------------------------------------------------
Known Issues and Incompatibilities
----------------------------------

* Take care with the docking test, as the drones are very solid and can make just as much a mess of your ship as a real station can.

* To prevent distracting information appearing from the course buoys, the OXP disables the Planetary Information System (Welcome Mat) at the beginning of the tests. It is re-enabled once they are completed or aborted.

------------------------------------------------------------------------------------------------------------------------------------------------------------
OXPConfig settings
------------------

This OXP is designed to work with OXPconfig, to allow tweaks to the number of academies that appear:

ExtraA = false, (ExtraB ignored) - only the academy at Lave will appear.
ExtraA = true, ExtraB = false - One academy will appear per galaxy.
ExtraA = true, ExtraB = true - Five academies will appear per galaxy.

The locations are as follows:

Galaxy 1 - Lave, Esteonbi, Enonla, Esanbe, Xequerin.
Galaxy 2 - Maesaron, Erenanri, Reveabe, Legeara, Tigebere.
Galaxy 3 - Radiqu, Edxeri, Ceedleon, Atius,Rerebi.
Galaxy 4 - Mavelege, Bemate, Cebitiza,  Mausra, Ensoor.
Galaxy 5 - Zaaner, Vebi, Inenares, Azaenbi, Dioris.
Galaxy 6 - Teesso, Oresmaa, Celaan, Ariqu, Inesbe.
Galaxy 7 - Isdilaon, Aenbi, Ataer, Orreedon, Qutegequ.
Galaxy 8 - Ceenza, Aarzari, Biatzate, Inbein, Oredrier.

For one academy per galaxy, the first planet in each list will be its host.

------------------------------------------------------------------------------------------------------------------------------------------------------------
Full Details and Instructions
-----------------------------

Lave Academy Orbital Station
----------------------------

This large structure can be found in orbit around the planet Lave in Galaxy 1, just at the edge of the security zone of the main planetary station. It can easily be found by launching from there and locating the way marker buoy on your scanner. This is roughly half way between the station and the Academy, which should be easily visible behind it. Alternatively for ships equipped with such equipment, it can be found by selecting "A" on your advanced space compass. 

As this is an official Galcop location, trading of any commodity is banned, and due to its academic nature the only general equipment available is the witchspace fuel that may be needed for or consumed in undertaking the training tests. Therefore the normal marketplace information screen (F8) is now replaced by an access screen where you can review and sign up for the three offered training courses.

The station itself has been especially designed for its purpose, with thought towards its users and their general lack of experience in maneuvering and docking. At great expense a special system has been employed of tractor-beam assisted docking. To engage this, all the commander needs to do is to fly into the blue glowing area in the stations centre and the system will automatically take over and bring the ship into the station. For commanders whose ships are equipped with docking computers, these may also be used. It should also be noted that after some recent unfortunate incidents with ships equipped with semi-AI defense turrets, such ships are presently banned from docking at the Lave Academy orbital station.

Once docking is completed, one of the three available tests can be selected from the F8 screen. Each of these has built-in instructions, but more details are given below. All three are based around specially equipped course buoys which can be found in the vicinity of the station (all are within standard scanner range at launch). When a course is accepted and the commander launches from the station, the relevant buoy will automatically detect the ship and initiate communications with it to guide it to the correct place. Each course will begin within a few seconds of the ship approaching the buoy.

One final note, for those wishing to cause trouble, the academy is defended by a small squadron of very well equipped Vipers and Viper Interceptors who are experienced at dealing with unruly students.


Piloting Course
---------------

This course is offered to improve and test the commanders skills in astro-navigation and in ship handling. Once the test has begun, the commander needs to fly in sequence to a number of buoys which make up the course. Each buoy will identify itself by communicating with the ship, and must be approached to proceed to the next one. To make things more interesting a number of asteroids also populate the course area, which must be avoided. They can also be destroyed, but this is considered minor cheating and incurs a small time penalty. The course is run against the clock, with the best time being noted.

Also randomly placed within the course are a number of standard-issue ring drones. When flown through, these give a bonus time reduction to the commanders overall score. It is up to their skill and judgement to decide whether the time taken to detour through them is worth the bonus that they give (flying through them is optional, the course can be successfully completed without utilising them).


Gunnery Course
--------------

This is the most popular course in the academy, colloquially known as the "Shooting Gallery". When the buoy is approached and the test begun, it generates in sequence three sets of 5 target drones, each of which must be destroyed. These drones have a maximum lifetime of 30 to 60 seconds (depending on the commanders rank and ships equipment), after which they automatically self-destruct. The drone lifetime is announced at the beginning of the test by the buoy. The drones at level one are static, whilst those of the other two levels move faster and are increasingly more agile and a challenge to hit.

The test ends once all fifteen drones have been destroyed or have self-destructed, and the best time achieved is recorded. One other key consideration is that throughout the test the commander must keep the main buoy within scanner range, or else the test will be aborted. 


Docking Course
--------------

The last of the three tests, and arguably the most challenging, is the docking practice course. This simulates the requirements to dock with a standard station "letterbox" docking bay, one of the most difficult routine tasks a commander faces (and no, in this one you can't use a docking computer!). Once buoy approach is completed and the test begun, three sets of three drones will appear in sequence. The first set is simply for familiarisation, and has a large ring which must be flown through. The second set is similar, but an insert appears within the ring, gradually restricting its size down to that of a standard docking bay. The third set are the same as the second, except this time both the ring and insert begin to rotate, to fully simulate the conditions when docking with a real station for the final buoy.

It should be noted that approaches to the drones should be made slowly, as there have been cases of students who got over-confident with the accelerator smashing their ships against the drones with fatal consequences. To aid in successful traversal a line of glowing mini-buoys (nicknamed the "trail of breadcrumbs" by students) indicate the ideal path to take through the drone. The drones are also programmed to orient themselves perpendicularly to the main course buoy, so this can also be used to orientate the ship in lieu of the normal station nav-buoy.

Finally each drone must be successfully traversed within a three minute window of its appearance, or else it will self-destruct. And at all times the commander must keep the main buoy on his scanner, or else the test will automatically be aborted. 

------------------------------------------------------------------------------------------------------------------------------------------------------------

License:

This OXP is released under the Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with the following clauses:

* Whilst you are free (and encouraged) to re-use any of the scripting, models or texturing in this OXP, the usage must be distinct from that within this OXP. Unique identifiers such as (but not limited to) unique shipdata.plist entity keys, mission variables, script names (this.name), equipment identity strings (EQ_), description list arrays and entity roles must not be re-used without prior agreement. Basically if it's unique or would identify or overwrite anything in the original OXP, then you may not re-use it (for obvious compatibility reasons).
* rebundling of this OXP within another distribution is permitted as long as it is unchanged. The following derivates however are permitted and except from the above:
	* the conversion of files between XML and openStep.
	* the merging of files with other files of the same type from other OXPs.
* The license information (either as this file or merged into a larger one) must be included in the OXP.
* Even though it is not compulsory, if you are re-using any sizable or recognisable piece of this OXP, please let me know :)


------------------------------------------------------------------------------------------------------------------------------------------------------------
Instructions
------------

Unzip the file, and then move the folder "Lave Academy 1.33.oxp" to the AddOns directory of your Oolite installation. Then start the game up and the ships should be added. 

------------------------------------------------------------------------------------------------------------------------------------------------------------
Version History
---------------
23/02/2024 - Version 2.1, added missing ";" to various entries in the shipdata.plist file.
13/02/2024 - Version 2.0, Updated Academy textures, added normal map and specular map, switched to using default Oolite shaders.
		Turned off "smooth" on the Academy definition.
		Made it possible to save your game at the Academy.
		Switched Academy Waymarker Buoy to use default Oolite buoy.
		Tweaked text of F4 interface entry.
		Added interface to Library Config for controlling when to spawn Academies.
		Spawning settings now saved with the save game.
		Changed course progression messages to always come from the associated marker buoy.
		Added a docking texture for BGS.
		Code refactoring.
07/10/2014 - Version 1.34, tweak to shipdata.plist for iMac compatibility.
05/02/2013 - Version 1.33, update to restore compatibility with OXPConfig.
08/01/2013 - Version 1.32, update for v1.77 (now minimum version) with new logs and interface access.
31/12/2011 - Version 1.31, small script update to stop log errors in 1.76.
20/02/2011 - Version 1.30, update to try and make docking rings less deadly.
13/02/2011 - Version 1.21, removal of upper limit, to allow running with 1.75
17/04/2010 - Version 1.20, scripting for v1.74 - now no longer compatible with older versions of the game.
15/09/2009 - Version 1.11, minor change to shipdata.plist to remove a potential issue.
25/08/2009 - Version 1.1, script update for v1.73 compatibility (setPosition and setOrientation depreciation), plus optional addition of extra academies.
10/03/2009 - Version 1.02, scripting adjustment to spawn the Academy and buoys (and the pilot course) relative to the main station, rather than absolute.
09/03/2009 - Version 1.01, modifications based on feedback:
		Added a buoy part-way between the main station Nav-buoy and the academy, to point people in the right direction.
		Modified the target drones lifetime to 30s + 10s each for commanders with <16 kills, without fuel injectors and with only pulse lasers.
		Tweaked the script a bit to prevent incorrect messages for new Jamesons, due to lack of save game & use of mission variables.
		Reworked the pilot buoy/drone scripts to prevent the crash to desktop if the 15 minute timer elapses.
04/03/2009 - Version 1.00, Initial release.

------------------------------------------------------------------------------------------------------------------------------------------------------------
Acknowledgements
----------------

With thanks to:
* Griff for the superb shaders and overall appearance of the station.
* Lestradae and PAGroove for the discussions and beta-testing.
* Eric Walch and Kaks for deep playtesting and bug reporting on the positioning issue (which was a bug in the 1.72.2 trunk).
* _ds_ for the core patch to enable icons in the ASC, and for the fuel pump icon in this OXP.
* Dr Beeb for the idea of multiple galaxies.
* Wildeblood for the heads-up on the 1.76 log errors.
* Captain Beatnik for the iMac heads-up for v1.34.

Equipment

This expansion declares no equipment.

Ships

Name
Academy Orbital Station
Academy Dock Ring (Hidden)
Docking Course Buoy
Docking Drone
Docking Drone
laveAcademy_dockingDroneL2a
laveAcademy_dockingDroneL2b
laveAcademy_dockingDroneL2c
Docking Drone
laveAcademy_dockingDroneL3a
laveAcademy_dockingDroneL3b
laveAcademy_dockingDroneL3c
Docking Drone Insert (Rotating)
Docking Drone Insert (Rotating)
Docking Drone Insert (Rotating)
Docking Drone Insert a
Docking Drone Insert b
Docking Drone Insert c
Docking Drone Ring
Docking Drone Ring (Rotating)
Docking Drone Ring (Rotating)
Docking Drone Ring (Rotating)
Generic Academy Buoy
Academy
Asteroid
Pilot Course Buoy
Pilot Circuit Buoy 1
Pilot Circuit Buoy 2
Pilot Circuit Buoy 3
Pilot Circuit Buoy 4
Pilot Circuit Buoy 5
Pilot Circuit Buoy 6
Pilot Circuit Buoy 7
Time Bonus Ring
Academy Security Patrol
Academy Security Interceptor
Target Practice Course Buoy
Target Drone
Target Drone
Target Drone
Target Drone
Academy Waymarker Buoy

Models

This expansion declares no models.

Scripts

Path
Scripts/laveAcademy_dockingBuoy.js
"use strict";
this.name = "LaveAcademy_dockingBuoy";
this.author = "Thargoid";
this.copyright = "Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme.txt.";
this.description = "Control of academy docking practice buoys in the Lave system";
this.version = "1.3";

this.examStart = function () {
	this.examStartTime = clock.absoluteSeconds;
	this.buoyPosition = this.ship.position;
	missionVariables.laveAcademy_droneCount = 0;
	missionVariables.laveAcademy_droneAbort = null;
	player.consoleMessage("Docking Practice Course Started!", 6);
	if (this.launchCheckTimer) {
		this.launchCheckTimer.start();
	}
	else {
		this.launchCheckTimer = new Timer(this, this.droneCheck, 0, 1);
	}
}

this.droneCheck = function droneCheck () {
	if (player.ship.docked) {// if the player is docked (at the Academy) then stop the test quietly
		this.launchCheckTimer.stop();
		missionVariables.laveAcademy_droneAbort = true;
		this.ship.AIState = "LIGHTS_OFF";
		missionVariables.laveAcademyExam = null;
		return;
	}

	function isPlayer(entity) { return entity.isShip && entity.isPlayer };
	this.playerInRange = system.filteredEntities(this, isPlayer, this.ship, 25600).length;
	if (this.playerInRange == 0) {
		this.launchCheckTimer.stop();
		missionVariables.laveAcademy_droneCount -= 1;
		this.cancelExam();
	}

	if (missionVariables.laveAcademy_droneAbort) {
		this.launchCheckTimer.stop();
		return;
	}

	function isDockingDrone(entity) { return entity.isShip && entity.hasRole("laveAcademy_dockingDrone") };
	if (system.filteredEntities(this, isDockingDrone, this.ship, 25600).length == 0) {
		if (missionVariables.laveAcademy_droneCount == 10) {
			this.endExam();
		}
		else {
			missionVariables.laveAcademy_droneCount += 1;
			switch (true) {
				case (missionVariables.laveAcademy_droneCount == 1):
					{
						this.ship.commsMessage("Level One.", player.ship);
						//system.legacy_addShipsWithinRadius("laveAcademy_dockingDroneL1", 1, "abs", this.buoyPosition, 14000);
						system.addShips("laveAcademy_dockingDroneL1", 1, this.buoyPosition, 14000);
						break;
					}
				case (missionVariables.laveAcademy_droneCount == 2 || missionVariables.laveAcademy_droneCount == 3):
					{
						//system.legacy_addShipsWithinRadius("laveAcademy_dockingDroneL1", 1, "abs", this.buoyPosition, 14000);
						system.addShips("laveAcademy_dockingDroneL1", 1, this.buoyPosition, 14000);
						break;
					}

				case (missionVariables.laveAcademy_droneCount == 4):
					{
						this.ship.commsMessage("Level Two.", player.ship);
						//system.legacy_addShipsWithinRadius("laveAcademy_dockingDroneL2a", 1, "abs", this.buoyPosition, 14000);
						system.addShips("laveAcademy_dockingDroneL2a", 1, this.buoyPosition, 14000);
						break;
					}
				case (missionVariables.laveAcademy_droneCount == 5):
					{
						//system.legacy_addShipsWithinRadius("laveAcademy_dockingDroneL2b", 1, "abs", this.buoyPosition, 14000);
						system.addShips("laveAcademy_dockingDroneL2b", 1, this.buoyPosition, 14000);
						break;
					}
				case (missionVariables.laveAcademy_droneCount == 6):
					{
						//system.legacy_addShipsWithinRadius("laveAcademy_dockingDroneL2c", 1, "abs", this.buoyPosition, 14000);
						system.addShips("laveAcademy_dockingDroneL2c", 1, this.buoyPosition, 14000);
						break;
					}

				case (missionVariables.laveAcademy_droneCount == 7):
					{
						this.ship.commsMessage("Level Three.", player.ship);
						//system.legacy_addShipsWithinRadius("laveAcademy_dockingDroneL3a", 1, "abs", this.buoyPosition, 14000);
						system.addShips("laveAcademy_dockingDroneL3a", 1, this.buoyPosition, 14000);
						break;
					}
				case (missionVariables.laveAcademy_droneCount == 8):
					{
						//system.legacy_addShipsWithinRadius("laveAcademy_dockingDroneL3b", 1, "abs", this.buoyPosition, 14000);
						system.addShips("laveAcademy_dockingDroneL3b", 1, this.buoyPosition, 14000);
						break;
					}
				case (missionVariables.laveAcademy_droneCount == 9):
					{
						//system.legacy_addShipsWithinRadius("laveAcademy_dockingDroneL3c", 1, "abs", this.buoyPosition, 14000);
						system.addShips("laveAcademy_dockingDroneL3c", 1, this.buoyPosition, 14000);
						break;
					}
			}
		}
	}
}

this.endExam = function () {
	this.launchCheckTimer.stop();
	if (worldScripts["Welcome Information Script"]) { // if Welcome Mat has been disabled earlier, restart it
		if (!worldScripts["Welcome Information Script"].welcomeTimer.isRunning) {
			worldScripts["Welcome Information Script"].welcomeTimer.start();
			if (player.ship.equipmentStatus("EQ_WELCOME_MAT") == "EQUIPMENT_OK") {
				player.consoleMessage("Planetary Information System restarted.", 6);
			}
		}
	}
	this.examStopTime = clock.absoluteSeconds;
	this.examElapsedTime = Math.floor(this.examStopTime - this.examStartTime);
	this.examTimeMinutes = Math.floor(this.examElapsedTime / 60);
	this.examTimeSeconds = this.examElapsedTime - (60 * this.examTimeMinutes);
	missionVariables.laveAcademy_droneAbort = true;
	this.ship.commsMessage("You completed the test in " + this.examTimeMinutes + "m " + this.examTimeSeconds + "s.", player.ship);
	this.ship.AIState = "LIGHTS_OFF";
	missionVariables.laveAcademyExam = null;
	this.bestTime = (missionVariables.laveAcademy_bestDockingTimeM * 60) + missionVariables.laveAcademy_bestDockingTimeS;

	if (this.bestTime == 0 && this.examElapsedTime > 0) {
		this.ship.commsMessage("We have registered your new best time.", player.ship);
		missionVariables.laveAcademy_bestDockingTimeM = Math.floor(this.examElapsedTime / 60);
		missionVariables.laveAcademy_bestDockingTimeS = this.examElapsedTime - (60 * (Math.floor(this.examElapsedTime / 60)));
		return;
	}

	if (this.bestTime > this.examElapsedTime) {
		this.ship.commsMessage("Congratulations, you've improved your best time!", player.ship);
		missionVariables.laveAcademy_bestDockingTimeM = Math.floor(this.examElapsedTime / 60);
		missionVariables.laveAcademy_bestDockingTimeS = this.examElapsedTime - (60 * (Math.floor(this.examElapsedTime / 60)));
		return;
	}
}

this.cancelExam = function () {
	this.examStopTime = clock.absoluteSeconds;
	this.examElapsedTime = Math.floor(this.examStopTime - this.examStartTime);
	this.examTimeMinutes = Math.floor(this.examElapsedTime / 60);
	this.examTimeSeconds = this.examElapsedTime - (60 * this.examTimeMinutes);
	missionVariables.laveAcademy_droneAbort = true;
	this.droneArray = system.shipsWithRole("laveAcademy_dockingDrone")
	if (this.droneArray.length > 0) {
		this.droneArray.forEach(
			function (drone) {
				drone.remove()
			}
		)
	}
	player.commsMessage("Docking test aborted after " + this.examTimeMinutes + "m " + this.examTimeSeconds + "s.", 10);
	this.ship.AIState = "LIGHTS_OFF";
	missionVariables.laveAcademyExam = null;
	if (worldScripts["Welcome Information Script"]) { // if Welcome Mat has been disabled earlier, restart it
		if (!worldScripts["Welcome Information Script"].welcomeTimer.isRunning) {
			worldScripts["Welcome Information Script"].welcomeTimer.start();
			if (player.ship.equipmentStatus("EQ_WELCOME_MAT") == "EQUIPMENT_OK") {
				player.consoleMessage("Planetary Information System restarted.", 6);
			}
		}
	}
}

this.playerWillEnterWitchspace = function () {
	if (worldScripts["Welcome Information Script"] && !worldScripts["Welcome Information Script"].welcomeTimer.isRunning) {
		worldScripts["Welcome Information Script"].welcomeTimer.start();
	}

	if (this.launchCheckTimer) {
		this.launchCheckTimer.stop();
	}
}
Scripts/laveAcademy_dockingDrone.js
"use strict";
this.name = "LaveAcademy_dockingDrone";
this.author = "Thargoid";
this.copyright = "Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme.txt.";
this.description = "Script for the docking drones in the docking test of Lave Academy";
this.version = "1.3";

this.shipSpawned = function () {
	this.ship.scannerDisplayColor1 = "yellowColor";
	this.ship.scannerDisplayColor2 = "whiteColor";
	this.rotateQ = function (q, axis, angle) { return q.rotate(axis, angle); }

	var buoyArray = system.shipsWithPrimaryRole("laveAcademy_dockingBuoy"); // find the buoy for the docking course

	if (buoyArray.length == 1) { // set the drone at right angles to the vector away from the buoy
		this.dockingBuoy = buoyArray[0];
		let droneVector = this.ship.position.subtract(this.dockingBuoy.position).direction(); // unit vector pointing away from the buoy
		let dronePosition = this.ship.position.add(droneVector.multiply(1000)); // add an offset 1km away from the planet
		let angle = this.ship.heading.angleTo(droneVector); // angle between current heading and target heading
		let cross = this.ship.heading.cross(droneVector).direction(); // set the plane where we should rotate in
		this.ship.orientation = this.rotateQ(this.ship.orientation, cross, -angle); // re-orient the drone at right angle to the buoy vector
		this.ship.position = dronePosition; // move the drone 1km away from the buoy, to ensure there's room for traverse
	}
	else { // randomise the ring's orientation instead
		// when would we get here?
		// only spawn code is inside laveAcademy_dockingBuoy, which should mean the buoyArray.length = 1
		// maybe if the buoy has been destroyed, in which case the course cannot start
		// because you have to get close to the buoy to trigger it. I think this is redundant code
		this.newW = (Math.random() * 2) - 1; // a value between -1 and 1 for each component
		this.newX = (Math.random() * 2) - 1;
		this.newY = (Math.random() * 2) - 1;
		this.newZ = (Math.random() * 2) - 1;
		this.newOrientation = new Quaternion(this.newW, this.newX, this.newY, this.newZ);
		this.ship.orientation = this.newOrientation;
	}

	this.destructTimer = new Timer(this, this.selfDestruct, 180);  // give the drone a 3 minute lifetime.
}

this.flyThru = function () {
	this.destructTimer.stop();
	this.dockingBuoy.commsMessage("Traverse successful.", player.ship);
	this.ship.remove();
}

this.selfDestruct = function selfDestruct () {
	if (!missionVariables.laveAcademy_droneAbort && !player.ship.docked) { // if the exam is still going but the player didn't "dock" in time
		this.dockingBuoy.commsMessage("Traverse failed.", player.ship);
	}
	this.ship.remove();
}

this.shipTakingDamage = function (amount, fromEntity, damageType) {
	if (fromEntity && fromEntity.isPlayer && amount && amount > 0 && damageType && damageType === "scrape damage") {
		this.dockingBuoy.commsMessage("You scraped the ring - take care!", player.ship);
		player.ship.energy += amount;
		this.ship.energy += amount;
	}
}

this.playerWillEnterWitchspace = function () {
	if (this.destructTimer) {
		this.destructTimer.stop;
	}
}
Scripts/laveAcademy_pilotAsteroid.js
"use strict";
this.name = "LaveAcademy_pilotAsteroid";
this.author = "Thargoid";
this.copyright = "Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme.txt.";
this.description = "Script for the asteroids in pilot course of Lave Academy";
this.version = "1.3";

this.shipDied = function (whom, why) {
	if (whom && whom.isPlayer && missionVariables.laveAcademyExam == "PILOT") { // player kill during pilot course
		missionVariables.laveAcademy_pilotScore -= 2; // player penalised 2 seconds for killing the asteroid.
		player.score -= 1; // as this is an exam, the asteroids shouldn't count as kills for rank etc.
	}
}
Scripts/laveAcademy_pilotBuoy.js
"use strict";
this.name = "LaveAcademy_pilotBuoy";
this.author = "Thargoid";
this.copyright = "Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme.txt.";
this.description = "Control of academy pilot practice course in the Lave system";
this.version = "1.3";

this.examStart = function () { // first set up the asteroids and rings

	this.removeCourse(); // just in case we're repeating the exam and we've still got debris around.

	this.buoy1Position = this.ship.position.add([-20000, 0, 0]);
	this.buoy2Position = this.buoy1Position.add([0, 20000, 0]);
	this.buoy3Position = this.buoy2Position.add([0, 0, -20000]);
	this.buoy4Position = this.buoy3Position.add([-20000, 0, 0]);
	this.buoy5Position = this.buoy4Position.add([0, -20000, 0]);
	this.buoy6Position = this.buoy5Position.add([20000, 0, 0]);
	this.buoy7Position = this.buoy6Position.add([20000, 0, 0]);

	this.ring1Position = this.buoy1Position.add([0, 10000, 0]);
	this.ring2Position = this.buoy3Position.add([-10000, 0, 0]);
	this.ring3Position = this.buoy5Position.add([10000, 0, 0]);

	//system.legacy_addShipsWithinRadius("laveAcademy_pilotAsteroid", 5, "abs", this.buoy2Position, 7000); // 5 rocks around buoy 2.
	system.addShips("laveAcademy_pilotAsteroid", 5, this.buoy2Position, 7000);
	//system.legacy_addShipsWithinRadius("laveAcademy_pilotRing", 1, "abs", this.ring1Position, 7000); // 1 bonus ring between buoys 1 & 2.
	system.addShips("laveAcademy_pilotRing", 1, this.ring1Position, 7000);
	//system.legacy_addShipsWithinRadius("laveAcademy_pilotAsteroid", 7, "abs", this.buoy3Position, 7000); // 7 rocks around buoy 3.
	system.addShips("laveAcademy_pilotAsteroid", 7, this.buoy3Position, 7000);
	//system.legacy_addShipsWithinRadius("laveAcademy_pilotAsteroid", 12, "abs", this.buoy4Position, 7000); // 12 rocks around buoy 4.
	system.addShips("laveAcademy_pilotAsteroid", 12, this.buoy4Position, 7000);
	//system.legacy_addShipsWithinRadius("laveAcademy_pilotRing", 2, "abs", this.ring2Position, 7000); // 2 bonus rings between buoys 3 & 4.
	system.addShips("laveAcademy_pilotRing", 2, this.ring2Position, 7000);
	//system.legacy_addShipsWithinRadius("laveAcademy_pilotAsteroid", 7, "abs", this.buoy5Position, 7000); // 7 rocks around buoy 5.
	system.addShips("laveAcademy_pilotAsteroid", 7, this.buoy5Position, 7000);
	//system.legacy_addShipsWithinRadius("laveAcademy_pilotAsteroid", 5, "abs", this.buoy6Position, 7000); // 5 rocks around buoy 6.
	system.addShips("laveAcademy_pilotAsteroid", 5, this.buoy6Position, 7000);
	//system.legacy_addShipsWithinRadius("laveAcademy_pilotRing", 1, "abs", this.ring3Position, 7000); // 1 bonus rings between buoys 5 & 6.
	system.addShips("laveAcademy_pilotRing", 1, this.ring3Position, 7000);


	// Reset the mission variables, start the clock and tell the player we're underway
	this.examStartTime = clock.absoluteSeconds;
	missionVariables.laveAcademy_pilotScore = 0; // bonus and penalty counter
	missionVariables.laveAcademy_pilotCurrentBuoy = 0; // the number of the buoy we are currently at
	missionVariables.laveAcademy_pilotNextBuoy = 1; //  the number of the next buoy to be reached
	missionVariables.laveAcademy_pilotNextBuoyName = "pilotBuoy1";
	player.consoleMessage("Pilot Course Started!", 6);

	if (this.pilotCourseTimer) {
		this.pilotCourseTimer.start();
	}
	else {
		this.pilotCourseTimer = new Timer(this, this.runCourse, 0, 1);
	}
}

this.runCourse = function runCourse () {
	if (player.ship.docked) { // if the player is docked then stop the test quietly
		this.quietEnd();
		return;
	}

	this.examElapsedTime = Math.floor(clock.absoluteSeconds - this.examStartTime); // absolute time since test began, without penalties or bonuses 
	if (this.examElapsedTime > 900) { // if we've been at the test for over 15 minutes
		this.ship.commsMessage("Pilot test expired, please return to the Academy.", player.ship);
		this.quietEnd();
		return;
	}

	if (missionVariables.laveAcademy_pilotNextBuoy == 8 || missionVariables.laveAcademy_pilotNextBuoyName == "FinishLine") { // if we reached the 7th buoy
		this.pilotCourseTimer.stop();
		this.finishLine();
		return;
	}

	if (missionVariables.laveAcademy_pilotCurrentBuoy < missionVariables.laveAcademy_pilotNextBuoy) { // if we reached a buoy successfully spawn & set next
		missionVariables.laveAcademy_pilotCurrentBuoy += 1;
		switch (missionVariables.laveAcademy_pilotNextBuoyName) {
			case "pilotBuoy1":
				{
					let pilotBuoy1 = this.ship.spawnOne('laveAcademy_pilotCircuitBuoy1');
					pilotBuoy1.position = this.buoy1Position;
					pilotBuoy1.AIState = "LIGHTS_ON";
					pilotBuoy1.scannerDisplayColor1 = "redColor";
					pilotBuoy1.scannerDisplayColor2 = "whiteColor";
					break;
				}

			case "pilotBuoy2":
				{
					let pilotBuoy2 = this.ship.spawnOne('laveAcademy_pilotCircuitBuoy2');
					pilotBuoy2.position = this.buoy2Position;
					pilotBuoy2.AIState = "LIGHTS_ON";
					pilotBuoy2.scannerDisplayColor1 = "redColor";
					pilotBuoy2.scannerDisplayColor2 = "whiteColor";
					break;
				}

			case "pilotBuoy3":
				{
					let pilotBuoy3 = this.ship.spawnOne('laveAcademy_pilotCircuitBuoy3');
					pilotBuoy3.position = this.buoy3Position;
					pilotBuoy3.AIState = "LIGHTS_ON";
					pilotBuoy3.scannerDisplayColor1 = "redColor";
					pilotBuoy3.scannerDisplayColor2 = "whiteColor";
					break;
				}

			case "pilotBuoy4":
				{
					let pilotBuoy4 = this.ship.spawnOne('laveAcademy_pilotCircuitBuoy4');
					pilotBuoy4.position = this.buoy4Position;
					pilotBuoy4.AIState = "LIGHTS_ON";
					pilotBuoy4.scannerDisplayColor1 = "redColor";
					pilotBuoy4.scannerDisplayColor2 = "whiteColor";
					break;

				}
			case "pilotBuoy5":
				{
					let pilotBuoy5 = this.ship.spawnOne('laveAcademy_pilotCircuitBuoy5');
					pilotBuoy5.position = this.buoy5Position;
					pilotBuoy5.AIState = "LIGHTS_ON";
					pilotBuoy5.scannerDisplayColor1 = "redColor";
					pilotBuoy5.scannerDisplayColor2 = "whiteColor";
					break;
				}

			case "pilotBuoy6":
				{
					let pilotBuoy6 = this.ship.spawnOne('laveAcademy_pilotCircuitBuoy6');
					pilotBuoy6.position = this.buoy6Position;
					pilotBuoy6.AIState = "LIGHTS_ON";
					pilotBuoy6.scannerDisplayColor1 = "redColor";
					pilotBuoy6.scannerDisplayColor2 = "whiteColor";
					break;
				}

			case "pilotBuoy7":
				{
					let pilotBuoy7 = this.ship.spawnOne('laveAcademy_pilotCircuitBuoy7');
					pilotBuoy7.position = this.buoy7Position;
					pilotBuoy7.AIState = "LIGHTS_ON";
					pilotBuoy7.scannerDisplayColor1 = "redColor";
					pilotBuoy7.scannerDisplayColor2 = "whiteColor";
					break;
				}
		}
	}
}

this.finishLine = function () {
	this.ship.AIState = "FINISH_LINE"; // comms message the player to return to the main buoy to finish the course
	if (this.pilotFinishTimer) {
		this.pilotFinishTimer.start();
	}
	else {
		this.pilotFinishTimer = new Timer(this, this.scanPlayer, 0, 1);
	}
}

this.scanPlayer = function scanPlayer () {
	if (player.ship.docked) { // if the player is docked then stop the test quietly
		this.quietEnd();
	}

	this.buoyDistance = player.ship.position.distanceTo(this.ship.position); // how far the player ship is from the main buoy
	if (this.buoyDistance < 1000) { // if we're within 1000m, we can end the test
		this.pilotFinishTimer.stop();
		this.ship.AIState = "LIGHTS_OFF";
		this.examEnd();
	}
}

this.examEnd = function () {
	if (worldScripts["Welcome Information Script"]) { // if Welcome Mat has been disabled earlier, restart it
		if (!worldScripts["Welcome Information Script"].welcomeTimer.isRunning) {
			worldScripts["Welcome Information Script"].welcomeTimer.start();
			if (player.ship.equipmentStatus("EQ_WELCOME_MAT") == "EQUIPMENT_OK") {
				player.consoleMessage("Planetary Information System restarted.", 6);
			}
		}
	}
	this.examStopTime = clock.absoluteSeconds;
	this.examElapsedTime = Math.floor(this.examStopTime - this.examStartTime - missionVariables.laveAcademy_pilotScore);
	this.examTimeMinutes = Math.floor(this.examElapsedTime / 60);
	this.examTimeSeconds = this.examElapsedTime - (60 * this.examTimeMinutes);
	this.removeCourse();
	this.ship.AIState = "LIGHTS_OFF";
	missionVariables.laveAcademyExam = null;
	this.ship.commsMessage("Your overall time for the course was " + this.examTimeMinutes + "m " + this.examTimeSeconds + "s.", player.ship);

	this.bestTime = (missionVariables.laveAcademy_bestPilotTimeM * 60) + missionVariables.laveAcademy_bestPilotTimeS;

	if (this.bestTime == 0 && this.examElapsedTime > 0) {
		this.ship.commsMessage("We have registered your new best time.", player.ship);
		missionVariables.laveAcademy_bestPilotTimeM = Math.floor(this.examElapsedTime / 60);
		missionVariables.laveAcademy_bestPilotTimeS = this.examElapsedTime - (60 * (Math.floor(this.examElapsedTime / 60)));
		return;
	}

	if (this.bestTime > this.examElapsedTime) {
		this.ship.commsMessage("Congratulations, you've improved your best time!", player.ship);
		missionVariables.laveAcademy_bestPilotTimeM = Math.floor(this.examElapsedTime / 60);
		missionVariables.laveAcademy_bestPilotTimeS = this.examElapsedTime - (60 * (Math.floor(this.examElapsedTime / 60)));
		return;
	}
}

this.playerWillEnterWitchspace = this.shipDied = this.quietEnd = function () {
	if (this.pilotCourseTimer) {
		this.pilotCourseTimer.stop();
	}
	if (this.pilotFinishTimer) {
		this.pilotFinishTimer.stop();
	}
	this.removeCourse();
	missionVariables.laveAcademyExam = null;
	missionVariables.laveAcademy_pilotScore = 0;
	missionVariables.laveAcademy_pilotCurrentBuoy = 0;
	missionVariables.laveAcademy_pilotNextBuoy = 1;
	missionVariables.laveAcademy_pilotNextBuoyName = "pilotBuoy1";
	if (worldScripts["Welcome Information Script"]) // if Welcome Mat has been disabled earlier, restart it
	{
		if (!worldScripts["Welcome Information Script"].welcomeTimer.isRunning) {
			worldScripts["Welcome Information Script"].welcomeTimer.start();
		}
	}
	return;
}

this.removeCourse = function () {
	this.courseList = system.shipsWithRole("laveAcademy_pilotCourse"); // find all course entities so we can remove them
	if (this.courseList.length > 0) { // if there are parts of the course still present, loop through them and remove
		let loopCounter = 0; // reset the counter
		for (loopCounter = 0; loopCounter < this.courseList.length; loopCounter++) {
			if (this.courseList[loopCounter].scanClass == "CLASS_BUOY") {
				this.courseList[loopCounter].AIState = "REMOVE_BUOY";
			}
			else {
				this.courseList[loopCounter].remove();
			}
		}
	}
}			
Scripts/laveAcademy_pilotCircuitBuoy.js
"use strict";
this.name = "LaveAcademy_pilotCircuitBuoy";
this.author = "Thargoid";
this.copyright = "Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme.txt.";
this.description = "Control of pilot course circuit buoys";
this.version = "1.3";

this.buoyActive = function () {
	if (this.playerScanTimer) {
		this.playerScanTimer.start();
	}
	else {
		this.playerScanTimer = new Timer(this, this.rangeCheck, 0, 1);
	}
}

this.rangeCheck = function rangeCheck () {
	if (!this.ship) {
		this.playerScanTimer.stop();
		return;
	}

	if (player.ship.docked) { // if the player is docked (at the Academy) then stop the test quietly
		this.playerScanTimer.stop();
		this.ship.AIState = "LIGHTS_OFF";
		this.ship.scannerDisplayColor1 = "yellowColor";
		this.ship.scannerDisplayColor2 = "whiteColor";
		missionVariables.laveAcademyExam = null;
		return;
	}

	this.buoyDistance = player.ship.position.distanceTo(this.ship.position); // how far the player ship is from the buoy
	if (this.buoyDistance < 1000) { // if we're within 1000m.
		this.playerScanTimer.stop();
		this.ship.AIState = "LIGHTS_OFF";
		player.consoleMessage("Buoy " + missionVariables.laveAcademy_pilotNextBuoy + " reached.", 6);
		this.ship.scannerDisplayColor1 = "yellowColor";
		this.ship.scannerDisplayColor2 = "whiteColor";
		missionVariables.laveAcademy_pilotNextBuoy += 1;
		missionVariables.laveAcademy_pilotNextBuoyName = this.ship.scriptInfo.nextBuoy;
	}
}

this.removeBuoy = function () {
	if (this.playerScanTimer && this.playerScanTimer.isRunning) {
		this.playerScanTimer.stop();
	}
	this.ship.remove();
}

this.playerWillEnterWitchspace = function () {
	if (this.playerScanTimer) {
		this.playerScanTimer.stop();
	}
}
Scripts/laveAcademy_systemScript.js
"use strict";
this.name = "LaveAcademy";
this.author = "Thargoid";
this.copyright = "Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme.txt.";
this.description = "Control of new additions to the Lave (and other) systems";
this.version = "2.0";

this.extraA = true;   //   Academies in all galaxies? If false only appear at Lave, nowhere else - ignore extraB
this.extraB = false;   //   Single or multiple academies if extraA set. False gives 1 per galaxy, true gives 5

this.oxpcSettings = {
	Info: { Name: this.name, Display: this.name, InfoB: "1 - If false only appear at Lave, nowhere else - ignore extraB.\n2 - Single or multiple academies if extraA set. False gives 1 per galaxy, true gives 5." },
	Bool0: { Name: "extraA", Def: true, Desc: "Academies in all galaxies?" },
	Bool1: { Name: "extraB", Def: false, Desc: "Multiple academies per galaxy." }
};

this._libSettings = {
	Name: this.name,
	Alias: "Lave Academy",
	Display: "Settings",
	Alive: "_libSettings",
	Bool: {
		B0: { Name: "extraA", Def: true, Desc: "All galaxies" },
		B1: { Name: "extraB", Def: false, Desc: "Multiple academies" },
		Info: "0: If false only appear at Lave, nowhere else - ignore 1.\n1: Single or multiple academies if 0 set. False gives 1 per galaxy, true gives 5."
	},
};

this.startUp = function () {
	if (missionVariables.laveAcademy_bestTargetTimeM == null) { missionVariables.laveAcademy_bestTargetTimeM = 0; }
	if (missionVariables.laveAcademy_bestTargetTimeS == null) { missionVariables.laveAcademy_bestTargetTimeS = 0; }
	if (missionVariables.laveAcademy_bestPilotScore == null) { missionVariables.laveAcademy_bestPilotScore = 0; }
	if (missionVariables.laveAcademy_bestPilotTimeM == null) { missionVariables.laveAcademy_bestPilotTimeM = 0; }
	if (missionVariables.laveAcademy_bestPilotTimeS == null) { missionVariables.laveAcademy_bestPilotTimeS = 0; }
	if (missionVariables.laveAcademy_bestDockingScore == null) { missionVariables.laveAcademy_bestDockingScore = 0; }
	if (missionVariables.laveAcademy_bestDockingTimeM == null) { missionVariables.laveAcademy_bestDockingTimeM = 0; }
	if (missionVariables.laveAcademy_bestDockingTimeS == null) { missionVariables.laveAcademy_bestDockingTimeS = 0; }
	this.deactivateWIS = false;

	if (missionVariables.LaveAcademy_extraA) this.extraA = (missionVariables.LaveAcademy_extraA == "1" ? true : false);
	if (missionVariables.LaveAcademy_extraB) this.extraB = (missionVariables.LaveAcademy_extraB == "1" ? true : false);

	this.setUpArrays();
}

this.startUpComplete = function() {
	// register our settings, if Lib_Config is present
	if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._libSettings);
}

this.playerWillSaveGame = function() {
	missionVariables.LaveAcademy_extraA = (this.extraA ? "1" : "0");
	missionVariables.LaveAcademy_extraB = (this.extraB ? "1" : "0");
}

this.setUpArrays = function () {
	this.academyList = [0, 1, 2, 3, 4, 5, 6, 7]; // galaxies 0-7
	this.academyList[0] = [7, 173, 168, 133, 4]; // Lave, Esteonbi, Enonla, Esanbe, Xequerin
	this.academyList[1] = [24, 92, 210, 55, 194]; // Maesaron, Erenanri, Reveabe, Legeara, Tigebere
	this.academyList[2] = [58, 165, 106, 198, 164]; // Radiqu, Edxeri, Ceedleon, Atius,Rerebi
	this.academyList[3] = [13, 47, 130, 18, 122]; // Mavelege, Bemate, Cebitiza,  Mausra, Ensoor
	this.academyList[4] = [16, 193, 231, 92, 9]; // Zaaner, Vebi, Inenares, Azaenbi, Dioris
	this.academyList[5] = [151, 6, 85, 146, 240]; // Teesso, Oresmaa, Celaan, Ariqu, Inesbe
	this.academyList[6] = [189, 146, 130, 118, 37]; // Isdilaon, Aenbi, Ataer, Orreedon, Qutegequ
	this.academyList[7] = [177, 31, 211, 157, 20]; // Ceenza, Aarzari, Biatzate, Inbein, Oredrier
}

this.systemCheck = function (planetNum, galNum) {
	if (!this.extraA) // academy at Lave only?
	{
		if (planetNum == 7 && galNum == 0) { return true; }
		else { return false; }
	}
	else {
		if (!this.extraB)	// 1 academy per galaxy
		{
			if (planetNum == this.academyList[galNum][0]) { return true; }
			else { return false; }
		}
		else	// 5 academies per galaxy
		{
			if (this.academyList[galNum].indexOf(planetNum) != -1) { return true; }
			else { return false; }
		}
	}
}

this.setUpSystem = function () {
	if (this.systemCheck(system.ID, galaxyNumber)) {
		var posLA = system.mainStation.position.add([0, 50000, 0]);
		system.setPopulator("laveAcademy", {
			callback: function(pos) {
				var ws = worldScripts.LaveAcademy;
				// because we're doing the setup during "systemWillPopulate", we don't need a lot of the arrays and checks from v1.33
				// find the academy (if it already exists)
				var academyArray = system.shipsWithPrimaryRole("laveAcademy_academy");
				if (academyArray.length === 0) academyArray = system.addShips("laveAcademy_academy", 1, pos, 0);
				var academy = academyArray[0];
				academy.scannerDisplayColor1 = "greenColor";
				academy.scannerDisplayColor2 = "brownColor";

				// set up the marker buoys
				system.addShips("laveAcademy_wayBuoy", 1, system.mainStation.position.add([0, 20000, 0]));
				ws.targetBuoy = system.addShips("laveAcademy_targetBuoy", 1, academy.position.add([0, 0, 20000]))[0];
				ws.dockingBuoy = system.addShips("laveAcademy_dockingBuoy", 1, academy.position.add([20000, 0, 0]))[0];
				ws.pilotBuoy = system.addShips("laveAcademy_pilotBuoy", 1, academy.position.add([-20000, 0, 0]))[0];

				ws.targetBuoy.AIState = "LIGHTS_OFF";
				ws.dockingBuoy.AIState = "LIGHTS_OFF";
				ws.pilotBuoy.AIState = "LIGHTS_OFF";

				//	Set the academy syllabus on the F4 screen, and remove the trunk ones for this station.
				academy.setInterface("LaveAcademy",
					{
						title: "Open Academy Syllabus",
						category: "Station Interfaces",
						summary: "Display the courses available to undertake at the Academy.",
						callback: ws.showCourses.bind(ws)
					});
				academy.setInterface("oolite-contracts-cargo", null);
				academy.setInterface("oolite-contracts-parcels", null);
				academy.setInterface("oolite-contracts-passengers", null);
			},
			location:"COORDINATES",
			coordinates:posLA,
			deterministic:true
		});
	}
}

this.showCourses = function () {
	mission.runScreen({ 
		title: "Academy Syllabus", 
		messageKey: "laveAcademy_examOffer", 
		choicesKey: "laveAcademy_offerChoice", 
		screenID:"lave_academy"
	}, this.choseExam);
}

this.shipLaunchedFromStation = function () {
	if (this.systemCheck(system.ID, galaxyNumber) && this.deactivateWIS) {
		// if Welcome Mat is loaded & running, disable it to stop data messages with the course buoys
		if (worldScripts["Welcome Information Script"]) {
			if (worldScripts["Welcome Information Script"].welcomeTimer.isRunning) {
				worldScripts["Welcome Information Script"].welcomeTimer.stop();
				if (player.ship.equipmentStatus("EQ_WELCOME_MAT") == "EQUIPMENT_OK") player.consoleMessage("Planetary Information System deactivated for exam.", 6);
			}
		}
		this.deactivateWIS = false;
	}
}

this.systemWillPopulate = function () {
	this.setUpSystem()
}

this.shipWillLaunchFromStation = function (station) {
	if (station.hasRole("laveAcademy_academy")) {
		// reactivate the trumble mission if it was available before docking.
		missionVariables.trumbles = missionVariables.laveAcademy_storeTrumble;

		switch (missionVariables.laveAcademyExam) { // Get the course started by setting the buoys AI states
			case "TARGET":
				{
					this.targetBuoy.AIState = "LIGHTS_ON";
					this.dockingBuoy.AIState = "LIGHTS_OFF";
					this.pilotBuoy.AIState = "LIGHTS_OFF";
					this.buoyID = this.targetBuoy;
					this.deactivateWIS = true;
					if (this.buoyTimer) { this.buoyTimer.start(); }
					else { this.buoyTimer = new Timer(this, this.checkBuoyDistance, 0, 10); }
					break;
				}
			case "DOCKING":
				{
					this.targetBuoy.AIState = "LIGHTS_OFF";
					this.dockingBuoy.AIState = "LIGHTS_ON";
					this.pilotBuoy.AIState = "LIGHTS_OFF";
					this.buoyID = this.dockingBuoy;
					this.deactivateWIS = true;
					if (this.buoyTimer) { this.buoyTimer.start(); }
					else { this.buoyTimer = new Timer(this, this.checkBuoyDistance, 0, 10); }
					break;
				}
			case "PILOT":
				{
					this.targetBuoy.AIState = "LIGHTS_OFF";
					this.dockingBuoy.AIState = "LIGHTS_OFF";
					this.pilotBuoy.AIState = "LIGHTS_ON";
					this.buoyID = this.pilotBuoy;
					this.deactivateWIS = true;
					if (this.buoyTimer) { this.buoyTimer.start(); }
					else { this.buoyTimer = new Timer(this, this.checkBuoyDistance, 0, 2); }
					break;
				}
			default:
				{
					this.targetBuoy.AIState = "LIGHTS_OFF";
					this.dockingBuoy.AIState = "LIGHTS_OFF";
					this.pilotBuoy.AIState = "LIGHTS_OFF";
					this.deactivateWIS = false;
					break;
				}
		}
	}
}

this.stopTimers = function () {
	if (this.buoyTimer) { this.buoyTimer.stop(); }
	if (this.targetTimer) { this.targetTimer.stop(); }
	if (this.pilotTimer) { this.pilotTimer.stop(); }
	if (this.dockingTimer) { this.dockingTimer.stop(); }

	if (this.targetBuoy && this.targetBuoy.script.launchCheckTimer) this.targetBuoy.script.launchCheckTimer.stop();
	if (this.dockingBuoy && this.dockingBuoy.script.launchCheckTimer) this.dockingBuoy.script.launchCheckTimer.stop();
	if (this.pilotBuoy) {
		if (this.pilotBuoy.script.pilotCourseTimer) this.pilotBuoy.script.pilotCourseTimer.stop();
		if (this.pilotBuoy.script.pilotFinishTimer) this.pilotBuoy.script.pilotFinishTimer.stop();
	}
}

this.shipWillEnterWitchspace = this.shipDied = function () {
	if (this.systemCheck(system.ID, galaxyNumber)) {
		this.stopTimers();
		this.deactivateWIS = false;
		this.targetBuoy.explode();
		this.dockingBuoy.explode();
		this.pilotBuoy.explode();
		missionVariables.laveAcademyExam = null;
		if (worldScripts["Welcome Information Script"]) { // if Welcome Mat has been disabled earlier, restart it
			if (!worldScripts["Welcome Information Script"].welcomeTimer.isRunning) {
				worldScripts["Welcome Information Script"].welcomeTimer.start();
				if (player.ship.equipmentStatus("EQ_WELCOME_MAT") == "EQUIPMENT_OK") { player.consoleMessage("Planetary Information System restarted.", 6); }
			}
		}
	}
}

this.shipWillDockWithStation = function (station) {
	if (this.systemCheck(system.ID, galaxyNumber)) {
		this.deactivateWIS = false;
		missionVariables.laveAcademyExam = null;
		this.stopTimers();
	}

	// this seems unnecessary, as the trumbles mission is only offered at main stations
	if (station.hasRole("laveAcademy_academy")) { // stop the trumble mission offering whilst docked at the academy
		missionVariables.laveAcademy_storeTrumble = missionVariables.trumbles;
		missionVariables.trumbles = "";
	}
}

this.checkBuoyDistance = function checkBuoyDistance () {
	var buoyDistance = player.ship.position.distanceTo(this.buoyID.position); // how far the player ship is from the buoy
	if (buoyDistance < 1000) { // if we're within 1000m.
		this.buoyTimer.stop();
		this.buoyID.AIState = "START_EXAM";
	}
}

this.guiScreenChanged = function () {
	// for GUI screen changes whilst in flight, which we can ignore
	if (!player.ship.docked) return; 

	// if we're not at Lave Academy
	if (!player.ship.dockedStation.hasRole("laveAcademy_academy")) return; 

	// replace marketplace screen with exam offering mission screen
	if (guiScreen == "GUI_SCREEN_MARKET") this.showCourses();
}


this.choseExam = function (examChoice) {
	switch (examChoice) {
		case "ACADEMY_1_TARGET":
			{
				mission.runScreen({ title: "Gunnery Exam", messageKey: "laveAcademy_targetInfo", choicesKey: "laveAcademy_targetYesNo", screenID:"lave_academy" }, this.examYesNo);
				break;
			}
		case "ACADEMY_2_PILOT":
			{
				mission.runScreen({ title: "Piloting Exam", messageKey: "laveAcademy_pilotInfo", choicesKey: "laveAcademy_pilotYesNo", screenID:"lave_academy" }, this.examYesNo);
				break;
			}
		case "ACADEMY_3_DOCKING":
			{
				mission.runScreen({ title: "Docking Exam", messageKey: "laveAcademy_dockingInfo", choicesKey: "laveAcademy_dockingYesNo", screenID:"lave_academy" }, this.examYesNo);
				break;
			}
		case "ACADEMY_4_RESET":
			{
				missionVariables.laveAcademyExam = null;
				missionVariables.laveAcademy_bestTargetTimeM = 0;
				missionVariables.laveAcademy_bestTargetTimeS = 0;
				missionVariables.laveAcademy_bestPilotTimeM = 0;
				missionVariables.laveAcademy_bestPilotTimeS = 0;
				missionVariables.laveAcademy_bestDockingScore = 0;
				missionVariables.laveAcademy_bestDockingTimeM = 0;
				missionVariables.laveAcademy_bestDockingTimeS = 0;
				mission.runScreen({ title: "Academy Syllabus", messageKey: "laveAcademy_examOffer", choicesKey: "laveAcademy_offerChoice", screenID:"lave_academy" }, this.choseExam);
				break;
			}
		case "ACADEMY_5_DECLINE":
			{
				missionVariables.laveAcademyExam = null;
				break;
			}
	}
}

this.examYesNo = function (selection) {
	switch (selection) {
		case "ACADEMY_1_TARGETYES":
			{
				missionVariables.laveAcademyExam = "TARGET";
				break;
			}
		case "ACADEMY_2_TARGETNO":
			{
				mission.runScreen({ title: "Academy Syllabus", messageKey: "laveAcademy_examOffer", choicesKey: "laveAcademy_offerChoice", screenID:"lave_academy" }, this.choseExam);
				break;
			}
		case "ACADEMY_1_PILOTYES":
			{
				missionVariables.laveAcademyExam = "PILOT";
				break;
			}
		case "ACADEMY_2_PILOTNO":
			{
				mission.runScreen({ title: "Academy Syllabus", messageKey: "laveAcademy_examOffer", choicesKey: "laveAcademy_offerChoice", screenID:"lave_academy" }, this.choseExam);
				break;
			}
		case "ACADEMY_1_DOCKINGYES":
			{
				missionVariables.laveAcademyExam = "DOCKING";
				break;
			}
		case "ACADEMY_2_DOCKINGNO":
			{
				mission.runScreen({ title: "Academy Syllabus", messageKey: "laveAcademy_examOffer", choicesKey: "laveAcademy_offerChoice", screenID:"lave_academy" }, this.choseExam);
				break;
			}
		default:
			{
				missionVariables.laveAcademyExam = null;
				break;
			}
	}
}
Scripts/laveAcademy_targetBuoy.js
"use strict";
this.name = "LaveAcademy_targetBuoy";
this.author = "Thargoid";
this.copyright = "Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme.txt.";
this.description = "Control of academy target practice buoys in the Lave system";
this.version = "1.3";

this.examStart = function () {
	missionVariables.laveAcademy_droneLifetime = 45;

	if (player.score < 16) { // if player is ranked harmless or mostly harmless, give another 10 seconds to the drone
		missionVariables.laveAcademy_droneLifetime += 10;
	}

	if (player.ship.equipmentStatus("EQ_FUEL_INJECTION") != "EQUIPMENT_OK") { // if player doesn't have fuel injectors, add 15 more seconds
		missionVariables.laveAcademy_droneLifetime += 15;
	}

	if (player.ship.weaponRange == 12500) { // if player has only a short range laser, add 10 more seconds
		missionVariables.laveAcademy_droneLifetime += 10;
	}

	this.examStartTime = clock.absoluteSeconds;
	this.buoyPosition = this.ship.position;
	missionVariables.laveAcademy_droneCount = 0;
	missionVariables.laveAcademy_droneAbort = null;
	player.consoleMessage("Target Practice Course Started!", 6);
	player.consoleMessage("Drone liftime set to " + missionVariables.laveAcademy_droneLifetime + "s.", 6);
	if (this.launchCheckTimer) {
		this.launchCheckTimer.start();
	}
	else {
		this.launchCheckTimer = new Timer(this, this.droneCheck, 0, 1);
	}
}

this.droneCheck = function droneCheck () {
	if (player.ship.docked) { // if the player is docked (at the Academy) then stop the test quietly
		this.launchCheckTimer.stop();
		missionVariables.laveAcademy_droneAbort = true;
		this.ship.AIState = "LIGHTS_OFF";
		missionVariables.laveAcademyExam = null;
		return;
	}

	function isPlayer(entity) { return entity.isShip && entity.isPlayer };
	this.playerInRange = system.filteredEntities(this, isPlayer, this.ship, 25600).length;
	if (this.playerInRange == 0) {
		missionVariables.laveAcademy_droneAbort = true;
		missionVariables.laveAcademy_droneCount -= 1;
		this.cancelExam();
	}

	if (missionVariables.laveAcademy_droneAbort) {
		this.launchCheckTimer.stop();
		return;
	}

	function isTargetDrone(entity) { return entity.isShip && entity.hasRole("laveAcademy_targetDrone") };
	if (system.filteredEntities(this, isTargetDrone, this.ship, 25600).length == 0) {
		if (missionVariables.laveAcademy_droneCount == 15) {
			this.endExam();
		}
		else {
			missionVariables.laveAcademy_droneCount += 1;
			switch (true) {
				case (missionVariables.laveAcademy_droneCount == 1):
					{
						this.ship.commsMessage("Level One.", player.ship);
						break;
					}
				case (missionVariables.laveAcademy_droneCount == 6):
					{
						this.ship.commsMessage("Level Two.", player.ship);
						break;
					}
				case (missionVariables.laveAcademy_droneCount == 11):
					{
						this.ship.commsMessage("Level Three.", player.ship);
						break;
					}
			}
			this.examLevel = (Math.ceil(missionVariables.laveAcademy_droneCount * 0.2));
			this.droneRole = "laveAcademy_targetDroneL" + this.examLevel;
			//system.legacy_addShipsWithinRadius(this.droneRole, 1, "abs", this.buoyPosition, 12000);
			var dr = system.addShips(this.droneRole, 1, this.buoyPosition, 12000)[0];
		}
	}
}

this.endExam = function () {
	this.launchCheckTimer.stop();
	if (worldScripts["Welcome Information Script"]) { // if Welcome Mat has been disabled earlier, restart it
		if (!worldScripts["Welcome Information Script"].welcomeTimer.isRunning) {
			worldScripts["Welcome Information Script"].welcomeTimer.start();
			if (player.ship.equipmentStatus("EQ_WELCOME_MAT") == "EQUIPMENT_OK") {
				player.consoleMessage("Planetary Information System restarted.", 6);
			}
		}
	}
	this.examStopTime = clock.absoluteSeconds;
	this.examElapsedTime = Math.floor(this.examStopTime - this.examStartTime);
	this.examTimeMinutes = Math.floor(this.examElapsedTime / 60);
	this.examTimeSeconds = this.examElapsedTime - (60 * this.examTimeMinutes);
	missionVariables.laveAcademy_droneAbort = true;
	this.ship.commsMessage("You completed the test in " + this.examTimeMinutes + "m " + this.examTimeSeconds + "s.", player.ship);
	this.ship.AIState = "LIGHTS_OFF";
	missionVariables.laveAcademyExam = null;
	this.bestTime = (missionVariables.laveAcademy_bestTargetTimeM * 60) + missionVariables.laveAcademy_bestTargetTimeS;

	if (this.bestTime == 0 && this.examElapsedTime > 0) {
		this.ship.commsMessage("We have registered your new best time.", player.ship);
		missionVariables.laveAcademy_bestTargetTimeM = Math.floor(this.examElapsedTime / 60);
		missionVariables.laveAcademy_bestTargetTimeS = this.examElapsedTime - (60 * (Math.floor(this.examElapsedTime / 60)));
		return;
	}

	if (this.bestTime > this.examElapsedTime) {
		this.ship.commsMessage("Congratulations, you've improved your best time!", player.ship);
		missionVariables.laveAcademy_bestTargetTimeM = Math.floor(this.examElapsedTime / 60);
		missionVariables.laveAcademy_bestTargetTimeS = this.examElapsedTime - (60 * (Math.floor(this.examElapsedTime / 60)));
		return;
	}
}

this.cancelExam = function () {
	this.launchCheckTimer.stop();
	this.examStopTime = clock.absoluteSeconds;
	this.examElapsedTime = Math.floor(this.examStopTime - this.examStartTime);
	this.examTimeMinutes = Math.floor(this.examElapsedTime / 60);
	this.examTimeSeconds = this.examElapsedTime - (60 * this.examTimeMinutes);
	missionVariables.laveAcademy_droneAbort = true;
	this.droneArray = system.shipsWithRole("laveAcademy_targetDrone")
	if (this.droneArray.length > 0) {
		this.droneArray.forEach(
			function (drone) {
				drone.remove()
			}
		)
	}
	this.ship.commsMessage("Gunnery test aborted after " + this.examTimeMinutes + "m " + this.examTimeSeconds + "s.", player.ship);
	this.ship.AIState = "LIGHTS_OFF";
	missionVariables.laveAcademyExam = null;
	if (worldScripts["Welcome Information Script"]) { // if Welcome Mat has been disabled earlier, restart it
		if (!worldScripts["Welcome Information Script"].welcomeTimer.isRunning) {
			worldScripts["Welcome Information Script"].welcomeTimer.start();
			if (player.ship.equipmentStatus("EQ_WELCOME_MAT") == "EQUIPMENT_OK") {
				player.consoleMessage("Planetary Information System restarted.", 6);
			}
		}
	}
}

this.playerWillEnterWitchspace = function () {
	if (worldScripts["Welcome Information Script"] && !worldScripts["Welcome Information Script"].welcomeTimer.isRunning) {
		worldScripts["Welcome Information Script"].welcomeTimer.start();
	}

	if (this.launchCheckTimer) {
		this.launchCheckTimer.stop();
	}
}
Scripts/laveAcademy_targetDrone.js
"use strict";
this.name = "LaveAcademy_targetDrone";
this.author = "Thargoid";
this.copyright = "Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme.txt.";
this.description = "Script for the target drones in the shooting gallery of Lave Academy";
this.version = "1.3";

this.shipSpawned = function () {
	this.ship.scannerDisplayColor1 = "yellowColor";
	this.ship.scannerDisplayColor2 = "whiteColor";
	if (!missionVariables.laveAcademy_droneLifetime) {
		log("Lave Academy", "***** ALERT - Target drone lifetime mission variable not set! *****")
		missionVariables.laveAcademy_droneLifetime = 45;
	}
	this.destructTimer = new Timer(this, this.selfDestruct, missionVariables.laveAcademy_droneLifetime);  // set the drone's lifetime.
	this.targetBuoy = system.shipsWithPrimaryRole("laveAcademy_targetBuoy")[0];
}

this.shipDied = function (whom, why) {
	if (this.destructTimer && this.destructTimer.isRunning && whom && whom.isPlayer) {
		this.destructTimer.stop();
		player.score -= 1; // as this is an exam, the drones shouldn't count as kills for rank etc.
		this.targetBuoy.commsMessage("Drone destroyed.", player.ship);
	}
}

this.selfDestruct = function selfDestruct () {
	if (!this.ship) {
		return;
	}

	if (!missionVariables.laveAcademy_droneAbort && !player.ship.docked) { // if the exam is still going but the player didn't kill the drone
		this.targetBuoy.commsMessage("Drone missed.", player.ship);
	}
	this.ship.explode();
}

this.findBuoy = function () {
	if (!this.ship) {
		return;
	}

	this.buoyDistance = this.ship.position.distanceTo(this.targetBuoy.position); // how far the drone is from the buoy
	if (this.buoyDistance > 24000) { // if the drone is going out of scanner range
		this.ship.AIState = "BACK_TO_BUOY";
	}
}

this.playerWillEnterWitchspace = function () {
	if (this.destructTimer) {
		this.destructTimer.stop();
	}
}