1
/*jslint indent: 4, maxlen: 120, maxerr: 50, white: true, es5: true, undef: true, regexp: true, newcap: true */
2
/*jshint es5: true, undef: true, eqnull: true, noempty: true, eqeqeq: true, boss: true, loopfunc: true, laxbreak: true,
3
strict: true, curly: true */
4
/*global system, log, worldScripts, Timer */
6
/* Jaguar Company Patrol
8
* Copyright © 2012 Richard Thomas Harrison (Tricky)
10
* This work is licensed under the Creative Commons
11
* Attribution-Noncommercial-Share Alike 3.0 Unported License.
13
* To view a copy of this license, visit
14
* http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
15
* to Creative Commons, 171 Second Street, Suite 300, San Francisco,
16
* California, 94105, USA.
18
* Ship related functions for the patrol and intercept AIs.
19
* Missile subentity code based on tgGeneric_externalMissiles.js by Thargoid (modified)
25
/* Standard public variables for OXP scripts. */
26
this.name = "jaguar_company_patrol.js";
27
this.author = "Tricky";
28
this.copyright = "© 2012 Richard Thomas Harrison (Tricky)";
29
this.license = "CC BY-NC-SA 3.0";
30
this.description = "Ship script for the Jaguar Company Patrol ships.";
33
/* Private variable. */
36
/* Ship event callbacks. */
38
/* Initialise various variables on ship birth. */
39
this.shipSpawned = function () {
42
/* No longer needed after setting up. */
43
delete this.shipSpawned;
45
/* Initialise the p_patrol variable object.
46
* Encapsulates all private global data.
49
/* Cache the world scripts. */
50
mainScript : worldScripts["Jaguar Company"],
51
attackersScript : worldScripts["Jaguar Company Attackers"],
52
/* Local copies of the logging variables. */
53
logging : worldScripts["Jaguar Company"].$logging,
54
logExtra : worldScripts["Jaguar Company"].$logExtra,
55
/* Local copy of the friendRoles array. */
56
friendRoles : worldScripts["Jaguar Company Attackers"].$friendRoles,
57
/* Standard distances. */
63
/* Default missile. */
64
missileRole : "EQ_HARDENED_MISSILE",
65
/* Default number of missiles. */
66
initialMissiles : this.ship.missileCapacity,
67
/* Starting amount of fuel. */
71
/* Register this ship as a friendly. */
72
p_patrol.attackersScript.$addFriendly(this.ship);
73
/* Get a unique name for the patrol ship. */
74
this.ship.displayName = p_patrol.mainScript.$uniqueShipName(this.ship.name);
75
/* Increase the number of patrol ships in the system. */
76
p_patrol.mainScript.$numPatrolShips += 1;
77
/* Timer reference. */
78
this.$addFuelTimerReference = new Timer(this, this.$addFuelTimer, 1, 1);
80
/* Thargoid's missile code. */
81
/* Just to ensure ship is fully loaded with selected missile type and nothing else. */
82
if (this.ship.scriptInfo.missileRole) {
83
/* missileRole should be defined in shipdata.plist */
84
p_patrol.missileRole = this.ship.scriptInfo.missileRole;
87
if (this.ship.scriptInfo.initialMissiles) {
88
p_patrol.initialMissiles = parseInt(this.ship.scriptInfo.initialMissiles, 10);
91
if (this.ship.missiles.length > 0) {
92
/* Remove all spawning missiles. */
93
this.ship.awardEquipment("EQ_MISSILE_REMOVAL");
96
/* Restock with selected ones. */
97
for (counter = 0; counter < p_patrol.initialMissiles; counter += 1) {
98
this.ship.awardEquipment(p_patrol.missileRole);
102
/* Thargoid's missile code. (Simplified - taken out the local function.)
105
* missile - missile entity.
107
this.shipFiredMissile = function (missile) {
112
subEntities = this.ship.subEntities;
114
if (!subEntities || !subEntities.length) {
115
/* If we've run out of sub-ents before we run out of missiles. */
119
/* Set counter to number of sub-ents minus 1 (as entity array goes up from zero). */
120
for (counter = subEntities.length - 1; counter >= 0; counter -= 1) {
121
subEntity = subEntities[counter];
123
if (subEntity.hasRole(missile.primaryRole)) {
124
/* If the sub-ent is the same as the missile being fired. */
125
/* Move the fired missile to the sub-ent position and convert to real-world co-ordinates. */
126
missile.position = this.ship.position.add(subEntity.position.rotateBy(this.ship.orientation));
127
/* Point the missile in the right direction. */
128
missile.orientation = subEntity.orientation.multiply(this.ship.orientation);
129
/* Desired speed of missile is it's maximum speed. */
130
missile.desiredSpeed = missile.maxSpeed;
131
/* Remove the sub-ent version of the missile. */
134
/* Come out of the loop, as we've done our swap. */
140
/* Patrol ship was removed by script. */
141
this.shipRemoved = function (suppressDeathEvent) {
142
if (suppressDeathEvent) {
146
/* Decrease the number of patrol ships in the system. */
147
worldScripts["Jaguar Company"].$numPatrolShips -= 1;
150
/* The patrol ship has just become invalid. */
151
this.entityDestroyed = function () {
152
/* Decrease the number of patrol ships in the system. */
153
worldScripts["Jaguar Company"].$numPatrolShips -= 1;
154
/* Stop and remove the timer. */
155
this.$removeAddFuelTimer();
158
/* Taking damage. Check attacker and what type.
161
* amount - amount of damage.
162
* attacker - entity that caused the damage.
163
* type - type of damage as a string.
165
this.shipTakingDamage = function (amount, attacker, type) {
166
if (!attacker || !attacker.isValid || !attacker.isShip) {
167
/* If it isn't a ship dealing damage then carry on with the damage. */
171
if (p_patrol.friendRoles.indexOf(attacker.entityPersonality) > -1 && type === "scrape damage") {
172
/* Cancel damage from collision with Jaguar Company ships. */
173
this.ship.energy += amount;
175
/* Target the ship we are colliding with. */
176
this.ship.target = attacker;
178
if (this.ship.AI === "jaguar_company_interceptAI.plist") {
179
/* Force an exit of the intercept AI. */
180
this.ship.lightsActive = false;
184
/* Move away from the ship we are colliding with. */
185
this.ship.reactToAIMessage("JAGUAR_COMPANY_TOO_CLOSE");
189
/* Stop and remove the timer. */
190
this.$removeAddFuelTimer = function () {
191
if (this.$addFuelTimerReference) {
192
if (this.$addFuelTimerReference.isRunning) {
193
this.$addFuelTimerReference.stop();
196
delete this.$addFuelTimerReference;
200
/* addFuel in the AI doesn't allow small increases.
202
* This function allow us to increment the amount of fuel in tinier amounts.
203
* Called every second.
205
this.$addFuelTimer = function () {
209
if (this.ship.speed > this.ship.maxSpeed) {
210
/* No fuel collection during Injection or Torus drive. */
214
/* Round off the actual fuel amount to the nearest lowest tenth.
215
* The actual fuel amount can be something like 6.6000000000000005 even though
216
* the system just uses the 1st decimal place.
218
actualFuel = Math.floor(this.ship.fuel * 10) / 10;
219
/* The internal fuel amount is also rounded off in a similar manner for the following adjustment. */
220
internalFuel = Math.floor(p_patrol.fuel * 10) / 10;
222
/* Adjust the internal fuel amount if the actual fuel amount has changed.
223
* Needed if the actual fuel amount has been reduced by Injection or Torus drive.
225
if (actualFuel !== internalFuel) {
226
p_patrol.fuel = actualFuel;
229
if (p_patrol.fuel < 7) {
230
/* 0.1 LY of fuel every 100 seconds. */
231
p_patrol.fuel += 0.001;
233
/* Cap the fuel level. */
234
if (p_patrol.fuel > 7) {
238
/* Set the actual fuel amount to the new fuel amount.
239
* Adjust to the nearest lowest tenth.
241
this.ship.fuel = Math.floor(p_patrol.fuel * 10) / 10;
243
/* Make sure that the fuel tanks aren't over filled. */
249
/* Find the average distance to all the other ships.
252
* otherShips - array of ships.
255
* result - average distance to all the other ships.
257
this.$queryAverageDistance = function (otherShips) {
258
var averageDistance = 0,
264
if (!otherShips.length) {
268
/* Cache the length. */
269
otherShipsLength = otherShips.length;
271
for (otherShipsCounter = 0; otherShipsCounter < otherShipsLength; otherShipsCounter += 1) {
272
otherShip = otherShips[otherShipsCounter];
273
/* Centre to centre distance. */
274
distance = this.ship.position.distanceTo(otherShip.position);
275
/* Take off the collision radius of this ship. */
276
distance -= this.ship.collisionRadius;
277
/* Take off the collision radius of the other ship. */
278
distance -= otherShip.collisionRadius;
279
/* Add this distance to all the other distances. */
280
averageDistance += distance;
283
/* Average all the distances and return it. */
284
return (averageDistance /= otherShipsLength);
287
/* Set the co-ordinates to the surface of the entity.
288
* This borrows some code from 'src/Core/Entities/ShipEntityAI.m - setCourseToPlanet'
291
* entity - entity to set co-ordinates to.
293
this.$setCoordsToEntity = function (entity) {
294
var position = entity.position,
299
/* Calculate a vector position between the entity's surface and the ship. */
300
distance = this.ship.position.distanceTo(position);
301
ratio = (entity.collisionRadius + this.ship.collisionRadius + 100) / distance;
302
position = Vector3D.interpolate(position, this.ship.position, ratio);
304
/* Higher variation if further away. */
305
variation = (distance > 51200 ? 0.5 : 0.2);
307
/* Move the vector a random amount. */
308
position.x += variation * (Math.random() - variation);
309
position.y += variation * (Math.random() - variation);
310
position.z += variation * (Math.random() - variation);
312
/* Save this position for 'setDestinationFromCoordinates' in the AI. */
313
this.ship.savedCoordinates = position;
318
/* Save the current AI state. */
319
this.$saveAIState = function () {
320
p_patrol.saveAIState = this.ship.AIState;
323
/* Recall the saved AI state. */
324
this.$recallAIState = function () {
325
this.ship.AIState = p_patrol.saveAIState;
328
/* Set the co-ordinates to the surface of the main planet. */
329
this.$setCoordsToMainPlanet = function () {
330
this.$setCoordsToEntity(system.mainPlanet);
333
/* Set the co-ordinates to the fake interstellar witchpoint buoy. */
334
this.$setCoordsToInterstellarWitchpoint = function () {
335
this.$setCoordsToEntity(p_patrol.mainScript.$witchpointBuoy);
338
/* Set the co-ordinates to the surface of the witchpoint buoy. */
339
this.$setCoordsToWitchpoint = function () {
340
var witchpointBuoy = p_patrol.mainScript.$witchpointBuoy;
342
if (!witchpointBuoy || !witchpointBuoy.isValid) {
343
/* No witchpoint buoy. Patrol the base to the planet lane. */
344
p_patrol.mainScript.$initRoute("BP");
345
this.ship.reactToAIMessage("JAGUAR_COMPANY_WITCHPOINT_NOT_FOUND");
347
this.$setCoordsToEntity(p_patrol.mainScript.$witchpointBuoy);
348
this.ship.reactToAIMessage("JAGUAR_COMPANY_WITCHPOINT_FOUND");
352
/* Set the co-ordinates to the surface of the base. */
353
this.$setCoordsToJaguarCompanyBase = function () {
354
var base = p_patrol.mainScript.$jaguarCompanyBase;
356
if (!base || !base.isValid) {
357
if (system.isInterstellarSpace) {
359
this.ship.reactToAIMessage("JAGUAR_COMPANY_EXIT_INTERSTELLAR");
361
/* If it has gone, just patrol the witchpoint to the planet lane. */
362
p_patrol.mainScript.$initRoute("WP");
363
this.ship.reactToAIMessage("JAGUAR_COMPANY_BASE_NOT_FOUND");
366
if (p_patrol.mainScript.$buoy && p_patrol.mainScript.$buoy.isValid) {
367
/* Set the coords to the buoy. */
368
this.$setCoordsToEntity(p_patrol.mainScript.$buoy);
370
/* Set the coords to the base. */
371
this.$setCoordsToEntity(base);
374
this.ship.reactToAIMessage("JAGUAR_COMPANY_BASE_FOUND");
378
/* Jaguar Company were forced out of interstellar space as there was no base.
379
* Create a new base in the new system.
381
this.$addPatrolToNewSystem = function () {
382
if (system.shipsWithPrimaryRole("jaguar_company_patrol").length === p_patrol.mainScript.$numPatrolShips) {
383
/* Last ship out will create the base. */
384
if (!p_patrol.mainScript.$setUpCompany()) {
385
/* Base would not be created, just patrol the witchpoint to the planet lane. */
386
p_patrol.mainScript.$initRoute("WP");
388
/* Base was created. Set the route. Should be a route to the base. */
389
p_patrol.mainScript.$changeRoute(-1);
394
/* Check to see if the patrol ship is the initiator of the wormhole or is following. */
395
this.$checkHyperspaceFollow = function () {
396
if (p_patrol.mainScript.$hyperspaceFollow) {
397
/* This ship is following. */
398
this.ship.savedCoordinates = p_patrol.mainScript.$hyperspaceFollow;
399
this.ship.reactToAIMessage("JAGUAR_COMPANY_HYPERSPACE_FOLLOW");
404
/* This ship is opening the initial wormhole. */
405
p_patrol.mainScript.$hyperspaceFollow = this.ship.position;
406
this.ship.reactToAIMessage("JAGUAR_COMPANY_HYPERSPACE");
409
/* Check current patrol route. */
410
this.$checkRoute = function () {
411
/* Call common code used by all of Jaguar Company. */
412
p_patrol.mainScript.$checkRoute(this.ship);
415
/* Finished the current patrol route, change to the next one. */
416
this.$finishedRoute = function () {
417
/* Call common code used by all of Jaguar Company. */
418
p_patrol.mainScript.$finishedRoute(this.ship, "jaguar_company_patrol", "JAGUAR_COMPANY_REGROUP");
421
/* Scan for the other ships to see if the full group is present. */
422
this.$scanForAllJaguarCompany = function () {
430
if (p_patrol.mainScript.$maxPatrolShips === 1) {
431
/* We are on our own. */
432
this.ship.reactToAIMessage("JAGUAR_COMPANY_NOT_FOUND");
437
patrolShips = system.shipsWithPrimaryRole("jaguar_company_patrol");
439
if (patrolShips.length === p_patrol.mainScript.$numPatrolShips ||
440
p_patrol.mainScript.$patrolShipsFullyLaunched) {
441
/* Announce that we have found all of Jaguar Company to the AI. */
442
this.ship.reactToAIMessage("JAGUAR_COMPANY_ALL_PRESENT");
444
base = system.shipsWithRole("jaguar_company_base");
446
if (base.length > 0) {
447
/* There can only really be 1 base. */
448
lurkPosition = base[0].position;
450
/* START OF CODE THAT SHOULD NEVER BE REACHED.
451
* This is here purely for error checking sake.
452
* If the base is destroyed it will set the patrol ships fully launched variable,
453
* therefore this code block shouldn't be reached.
455
if (patrolShips.length === 1) {
456
/* We are on our own. */
457
lurkPosition = patrolShips[0].position;
459
/* Cache the length. */
460
length = patrolShips.length;
462
/* Work out the midpoint position of all ships. */
463
lurkPosition = new Vector3D(0, 0, 0);
465
for (counter = 0; counter < length; counter += 1) {
466
lurkPosition = lurkPosition.add(patrolShips[counter].position);
469
lurkPosition.x /= length;
470
lurkPosition.y /= length;
471
lurkPosition.z /= length;
473
/* Higher variation if further away. */
474
variation = (this.ship.position.distanceTo(lurkPosition) > 51200 ? 0.5 : 0.2);
476
/* Move the vector a random amount. */
477
lurkPosition.x += variation * (Math.random() - variation);
478
lurkPosition.y += variation * (Math.random() - variation);
479
lurkPosition.z += variation * (Math.random() - variation);
481
/* END OF CODE THAT SHOULD NEVER BE REACHED. */
484
/* Move the lurk position 20km out in a random direction and save the co-ordinates for the AI. */
485
this.ship.savedCoordinates = lurkPosition.add(Vector3D.randomDirection(20000));
486
this.ship.reactToAIMessage("JAGUAR_COMPANY_NOT_PRESENT");
490
/* Scan for the other ships and find the midpoint position of the group. */
491
this.$scanForJaguarCompany = function () {
498
otherShips = system.shipsWithPrimaryRole("jaguar_company_patrol", this.ship);
500
if (!otherShips.length) {
501
/* We are on our own. */
502
this.ship.reactToAIMessage("JAGUAR_COMPANY_NOT_FOUND");
507
/* Cache the length. */
508
otherShipsLength = otherShips.length;
510
/* Work out the midpoint position of all ships. */
511
midpointPosition = this.ship.position;
513
for (otherShipsCounter = 0; otherShipsCounter < otherShipsLength; otherShipsCounter += 1) {
514
midpointPosition = midpointPosition.add(otherShips[otherShipsCounter].position);
517
midpointPosition.x /= (otherShipsLength + 1);
518
midpointPosition.y /= (otherShipsLength + 1);
519
midpointPosition.z /= (otherShipsLength + 1);
521
/* Higher variation if further away. */
522
variation = (this.ship.position.distanceTo(midpointPosition) > 51200 ? 0.5 : 0.2);
524
/* Move the vector a random amount. */
525
midpointPosition.x += variation * (Math.random() - variation);
526
midpointPosition.y += variation * (Math.random() - variation);
527
midpointPosition.z += variation * (Math.random() - variation);
529
/* Save the co-ordinates for the AI. */
530
this.ship.savedCoordinates = midpointPosition;
531
/* Announce that we have found Jaguar Company to the AI. */
532
this.ship.reactToAIMessage("JAGUAR_COMPANY_FOUND");
535
/* Check how close we are to other ships.
538
* minimumDistance - closest distance allowed.
540
this.$checkJaguarCompanyClosestDistance = function (minimumDistance) {
544
/* More ships will increase the minimum distance. */
545
actualDistance = minimumDistance * Math.ceil(p_patrol.mainScript.$numPatrolShips / 8);
546
/* Modify for surface to surface. */
547
actualDistance += this.ship.collisionRadius;
548
/* Check for any patrol ships within the calculated sphere. */
549
otherShips = system.shipsWithPrimaryRole("jaguar_company_patrol", this.ship, actualDistance);
551
if (!otherShips.length) {
552
this.ship.reactToAIMessage("JAGUAR_COMPANY_DISTANCE_OK");
554
/* If we are less than the minimum distance from the closest ship then we need to move away. */
555
this.ship.target = otherShips[0];
556
this.ship.reactToAIMessage("JAGUAR_COMPANY_TOO_CLOSE");
562
/* Check our average distance to all other ships. */
563
this.$checkJaguarCompanyAverageDistance = function () {
570
otherShips = system.shipsWithPrimaryRole("jaguar_company_patrol", this.ship);
572
if (!otherShips.length) {
573
/* Return immediately if we are on our own. */
577
/* Find the average distance to all the other ships. */
578
averageDistance = this.$queryAverageDistance(otherShips);
580
close = (p_patrol.distance.close) + ((Math.random() * 2000.0) - 1000.0);
581
nearby = (p_patrol.distance.nearby) + ((Math.random() * 2000.0) - 1000.0);
582
farAway = (p_patrol.distance.farAway) + ((Math.random() * 2000.0) - 1000.0);
584
/* I would love to create a fuzzy logic controller for this. */
585
if (averageDistance < close) {
586
/* We have regrouped. */
587
this.ship.sendAIMessage("JAGUAR_COMPANY_REGROUPED");
588
} else if (averageDistance >= close && averageDistance < nearby) {
590
this.ship.reactToAIMessage("JAGUAR_COMPANY_CLOSE");
591
} else if (averageDistance >= nearby && averageDistance < farAway) {
593
this.ship.reactToAIMessage("JAGUAR_COMPANY_NEARBY");
595
/* We are far away. */
596
this.ship.reactToAIMessage("JAGUAR_COMPANY_FAR_AWAY");
600
/* Tell everyone to regroup if the average distance to all the other ships is too great.
603
* maxDistance - furthest distance allowed before a regroup message is sent out.
605
this.$checkJaguarCompanyRegroup = function (maxDistance) {
610
otherShips = system.shipsWithPrimaryRole("jaguar_company_patrol", this.ship);
612
if (!otherShips.length) {
613
/* Return immediately if we are on our own. */
617
/* Find the average distance to all the other ships. */
618
if (this.$queryAverageDistance(otherShips) >= maxDistance + ((Math.random() * 2000.0) - 1000.0)) {
619
/* Tell all ships, including ourself, to regroup. */
620
this.ship.reactToAIMessage("JAGUAR_COMPANY_REGROUP");
622
/* Cache the length. */
623
otherShipsLength = otherShips.length;
625
for (otherShipsCounter = 0; otherShipsCounter < otherShipsLength; otherShipsCounter += 1) {
626
otherShips[otherShipsCounter].reactToAIMessage("JAGUAR_COMPANY_REGROUP");