oolite/Resources/Scripts/oolite-ailib.js
cim cda41d73eb Redo JSAI comms methodology
Now much more possible for OXPs to add more comms messages.
Priority system to allow occasional messages on particular topics without overwhelming the comms with constant updates.

Also, fix my text editor using 2 tabs as the indent level for JS...
2013-08-01 22:40:23 +01:00

3897 lines
94 KiB
JavaScript

/*
oolite-ailib.js
Priority-based Javascript AI library
Oolite
Copyright © 2004-2013 Giles C Williams and contributors
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
MA 02110-1301, USA.
*/
"use strict";
/* AI Library */
this.name = "oolite-libPriorityAI";
this.version = "1.79";
this.copyright = "© 2008-2013 the Oolite team.";
this.author = "cim";
/* Constructor */
this.AILib = function(ship)
{
// the ship property must be read-only
Object.defineProperty(this, "ship", {
value: ship,
writable: false,
enumerable: true,
configurable: false
});
this.ship.AIScript.oolite_intership = {};
this.ship.AIScript.oolite_priorityai = this;
var activeHandlers = [];
var priorityList = null;
var parameters = {};
var lastCommSent = 0;
var lastCommHeard = 0;
var commsRole = "generic";
var commsPersonality = "generic";
var waypointgenerator = null;
/* Private utility functions. Cannot be called from external code */
/* Considers a priority list, potentially recursively */
function _reconsiderList(priorities) {
var l = priorities.length;
for (var i = 0; i < l; i++)
{
var priority = priorities[i];
if (this.getParameter("oolite_flag_behaviourLogging"))
{
if (priority.label)
{ log(this.ship.name,"Considering: "+priority.label);
}
else
{
log(this.ship.name,"Considering: entry "+i);
}
}
// always call the preconfiguration function at this point
// to set up condition parameters
if (priority.preconfiguration)
{
priority.preconfiguration.call(this);
}
// allow inverted conditions
var condmet = true;
if (priority.notcondition)
{
condmet = !priority.notcondition.call(this);
}
else if (priority.condition)
{
condmet = priority.condition.call(this);
}
// absent condition is always true
if (condmet)
{
if (this.getParameter("oolite_flag_behaviourLogging"))
{
log(this.ship.name,"Conditions met");
}
// always call the configuration function at this point
if (priority.configuration)
{
priority.configuration.call(this);
}
// this is what we're doing
if (priority.behaviour)
{
if (this.getParameter("oolite_flag_behaviourLogging"))
{
log(this.ship.name,"Executing behaviour");
}
if (priority.reconsider)
{
_resetReconsideration.call(this,priority.reconsider);
}
return priority.behaviour;
}
// otherwise this is what we might be doing
if (priority.truebranch)
{
if (this.getParameter("oolite_flag_behaviourLogging"))
{
log(this.ship.name,"Entering truebranch");
}
var branch = _reconsiderList.call(this,priority.truebranch);
if (branch != null)
{
return branch;
}
// otherwise nothing in the branch was usable, so move on
}
}
else
{
if (priority.falsebranch)
{
if (this.getParameter("oolite_flag_behaviourLogging"))
{
log(this.ship.name,"Entering falsebranch");
}
var branch = _reconsiderList.call(this,priority.falsebranch);
if (branch != null)
{
return branch;
}
// otherwise nothing in the branch was usable, so move on
}
}
}
if (this.getParameter("oolite_flag_behaviourLogging"))
{
log(this.ship.name,"Exiting branch");
}
return null; // nothing in the list is usable, so return
};
/* Only call this from aiAwoken to avoid loops */
function _reconsider() {
if (!this.ship || !this.ship.isValid || !this.ship.isInSpace)
{
return;
}
var newBehaviour = _reconsiderList.call(this,priorityList);
if (newBehaviour == null) {
log(this.name,"AI '"+this.ship.AIScript.name+"' for ship "+this.ship+" had all priorities fail. All priority based AIs should end with an unconditional entry.");
return false;
}
if (this.getParameter("oolite_flag_behaviourLogging"))
{
log(this.ship.name,newBehaviour);
}
newBehaviour.call(this);
return true;
};
/* Resets the reconsideration timer. */
function _resetReconsideration(delay)
{
this.ship.AIScriptWakeTime = clock.adjustedSeconds + delay;
};
/* ****************** General AI functions. ************** */
/* These privileged functions interface with the private functions
* and variables. Do not override them. */
this.applyHandlers = function(handlers)
{
// step 1: go through activeHandlers, and delete those
// functions from this.ship.AIScript
for (var i=0; i < activeHandlers.length ; i++)
{
delete this.ship.AIScript[activeHandlers[i]];
}
/* This handler must always exist for a priority AI, and must
* be set here. */
handlers.aiAwoken = function()
{
_reconsider.call(this);
}
// step 2: go through the keys in handlers and put those handlers
// into this.ship.AIScript and the keys into activeHandlers
activeHandlers = Object.keys(handlers);
for (var i=0; i < activeHandlers.length ; i++)
{
this.ship.AIScript[activeHandlers[i]] = handlers[[activeHandlers[i]]].bind(this);
}
}
this.communicate = function(key,params,priority)
{
if (priority > 1)
{
var send = clock.adjustedSeconds - lastCommSent;
if (priority == 2)
{
if (send < 10)
{
return;
}
}
var recv = clock.adjustedSeconds - lastCommHeard;
if (priority == 3)
{
if (recv < 10 || send < 10)
{
return;
}
}
else
{
if (recv < 60 || send < 60)
{
return;
}
}
}
var template = worldScripts["oolite-libPriorityAI"]._getCommunication(commsRole,commsPersonality,key);
if (template != "")
{
var message = expandDescription(template,params);
if (message != "")
{
this.ship.commsMessage(message);
lastCommSent = clock.adjustedSeconds;
}
}
}
this.getParameter = function(key)
{
if (key in parameters)
{
return parameters[key];
}
return null;
}
this.getWaypointGenerator = function()
{
return waypointgenerator;
}
this.noteCommsHeard = function()
{
lastCommHeard = clock.adjustedSeconds;
}
/* Requests reconsideration of behaviour ahead of schedule. */
this.reconsiderNow = function()
{
_resetReconsideration.call(this,0.05);
}
this.setCommunicationsRole = function(role)
{
commsRole = role;
}
this.setCommunicationsPersonality = function(personality)
{
commsPersonality = personality;
}
// parameters created by Oolite must always be prefixed oolite_
this.setParameter = function(key, value)
{
parameters[key] = value;
}
this.setPriorities = function(priorities)
{
priorityList = priorities;
this.applyHandlers({});
this.reconsiderNow();
}
// set the waypoint generator function
this.setWaypointGenerator = function(value)
{
waypointgenerator = value;
}
}; // end object constructor
/* Object prototype */
AILib.prototype.constructor = AILib;
AILib.prototype.name = this.name;
/* ****************** AI utility functions. ************** */
/* These functions provide standard checks for consistency in
* conditions and other functions. */
AILib.prototype.allied = function(ship1,ship2)
{
// ships in same group
if (ship1.group && ship1.group.containsShip(ship2))
{
return true;
}
if (ship1.group && ship1.group.leader)
{
// ship1 is escort of ship in same group as ship2
if (ship1.group.leader.group && ship1.group.leader.group.containsShip(ship2))
{
return true;
}
}
// or in reverse, ship2 is the escort
if (ship2.group && ship2.group.leader)
{
// ship2 is escort of ship in same group as ship1
if (ship2.group.leader.group && ship2.group.leader.group.containsShip(ship1))
{
return true;
}
}
// ship1 is escort of a ship, ship2 is escort of a ship, both
// those ships are in the same group
if (ship1.group && ship1.group.leader && ship2.group && ship2.group.leader && ship1.group.leader.group && ship1.group.leader.group.containsShip(ship2.group.leader))
{
return true;
}
// Okay, these ships really do have nothing to do with each other...
return false;
}
AILib.prototype.broadcastDistressMessage = function()
{
this.ship.broadcastDistressMessage();
if (this.ship.AIPrimaryAggressor)
{
this.communicate("oolite_makeDistressCall",this.entityCommsParams(this.ship.AIPrimaryAggressor),2);
}
}
AILib.prototype.checkScannerWithPredicate = function(predicate)
{
var scan = this.getParameter("oolite_scanResults");
if (scan == null || predicate == null)
{
return false;
}
for (var i = 0 ; i < scan.length ; i++)
{
if (predicate.call(this,scan[i]))
{
this.setParameter("oolite_scanResultSpecific",scan[i]);
return true;
}
}
return false;
}
AILib.prototype.cruiseSpeed = function()
{
var cruise = this.ship.maxSpeed * 0.8;
if (this.ship.group)
{
for (var i = 0 ; i < this.ship.group.ships.length ; i++)
{
if (this.ship.group.ships[i].maxSpeed >= this.ship.maxSpeed/4)
{
if (cruise > this.ship.group.ships[i].maxSpeed)
{
cruise = this.ship.group.ships[i].maxSpeed;
}
}
}
}
if (this.ship.escortGroup)
{
for (var i = 0 ; i < this.ship.escortGroup.ships.length ; i++)
{
if (this.ship.escortGroup.ships[i].maxSpeed >= this.ship.maxSpeed/4)
{
if (cruise > this.ship.escortGroup.ships[i].maxSpeed)
{
cruise = this.ship.escortGroup.ships[i].maxSpeed;
}
}
}
}
return cruise;
}
// gets a standard comms params object
AILib.prototype.entityCommsParams = function(entity)
{
var params = {};
if (entity.isShip)
{
// TODO: extend the ship object so more precise names can be
// returned?
params["oolite_entityClass"] = entity.name; //ship.shipClassName;
params["oolite_entityName"] = entity.displayName; // ship.shipName;
}
else if (entity.name)
{
params["oolite_entityClass"] = entity.name;
params["oolite_entityName"] = entity.name;
}
return params;
}
AILib.prototype.fineThreshold = function()
{
return 50 - (system.info.government * 7);
}
AILib.prototype.friendlyStation = function(station)
{
if (station.isMainStation && this.ship.bounty > this.fineThreshold())
{
return false;
}
return (station.target != this.ship || !station.hasHostileTarget);
}
AILib.prototype.homeStation = function()
{
// home station might be the owner of the ship, or might just
// be a group member
if (this.ship.owner && this.ship.owner.isStation && this.friendlyStation(this.ship.owner))
{
return this.ship.owner;
}
if (this.ship.group)
{
for (var i = 0 ; i < this.ship.group.ships.length ; i++)
{
if (this.ship.group.ships[i] != this.ship && this.ship.group.ships[i].isStation && this.friendlyStation(this.ship.group.ships[i].isStation))
{
return this.ship.group.ships[i];
}
}
}
return null;
}
AILib.prototype.isAggressive = function(ship)
{
if (ship && ship.isPlayer)
{
return !ship.isFleeing;
}
return ship && ship.hasHostileTarget && !ship.isFleeing && !ship.isDerelict;
}
AILib.prototype.isFighting = function(ship)
{
if (ship.isStation)
{
return ship.alertCondition == 3 && ship.target;
}
return ship && ship.hasHostileTarget;
}
/* ****************** Condition functions ************** */
/* Conditions. Any function which returns true or false can be used as
* a condition. They do not have to be part of the AI library, but
* several common conditions are provided here. */
/*** Combat-related conditions ***/
AILib.prototype.conditionCascadeDetected = function()
{
var cpos = this.getParameter("oolite_cascadeDetected");
if (cpos != null)
{
if (cpos.distanceTo(this.ship) < this.ship.scannerRange)
{
return true;
}
this.setParameter("oolite_cascadeDetected",null);
}
return false;
}
AILib.prototype.conditionCombatOddsGood = function()
{
// TODO: this should consider what the ships are, somehow
var us = 1;
if (this.ship.group)
{
us += this.ship.group.count - 1;
}
if (this.ship.escortGroup)
{
us += this.ship.escortGroup.count - 1;
}
var them = 1;
if (!this.ship.target)
{
return false;
}
else
{
if (this.ship.target.group)
{
them += this.ship.target.group.count - 1;
}
if (this.ship.target.escortGroup)
{
them += this.ship.target.escortGroup.count - 1;
}
}
return us >= them;
}
AILib.prototype.conditionInCombat = function()
{
if (this.isFighting(this.ship))
{
return true;
}
var dts = this.ship.defenseTargets;
for (var i=0; i < dts.length; i++)
{
if (dts[i].position.squaredDistanceTo(this.ship) < this.ship.scannerRange * this.ship.scannerRange)
{
return true;
}
}
if (this.ship.group != null)
{
for (var i = 0 ; i < this.ship.group.count ; i++)
{
if (this.isFighting(this.ship.group.ships[i]))
{
return true;
}
}
}
if (this.ship.escortGroup != null)
{
for (var i = 0 ; i < this.ship.escortGroup.count ; i++)
{
if (this.isFighting(this.ship.escortGroup.ships[i]))
{
return true;
}
}
}
delete this.ship.AIScript.oolite_intership.cargodemandpaid;
return false;
}
/* Ships being attacked are firing back */
AILib.prototype.conditionInCombatWithHostiles = function()
{
if (this.isFighting(this.ship) && this.isAggressive(this.ship.target))
{
return true;
}
var dts = this.ship.defenseTargets;
for (var i=0; i < dts.length; i++)
{
if (this.isAggressive(dts[i]) && dts[i].position.squaredDistanceTo(this.ship) < this.ship.scannerRange * this.ship.scannerRange)
{
return true;
}
else
{
// this is safe to do mid-loop as dts is a copy of the
// actual defense target list
this.ship.removeDefenseTarget(dts[i]);
}
}
if (this.ship.group != null)
{
for (var i = 0 ; i < this.ship.group.count ; i++)
{
if (this.isFighting(this.ship.group.ships[i]) && this.isAggressive(this.ship.group.ships[i].target))
{
return true;
}
}
}
if (this.ship.escortGroup != null)
{
for (var i = 0 ; i < this.ship.escortGroup.count ; i++)
{
if (this.isFighting(this.ship.escortGroup.ships[i]) && this.isAggressive(this.ship.escortGroup.ships[i].target))
{
return true;
}
}
}
delete this.ship.AIScript.oolite_intership.cargodemandpaid;
return false;
}
AILib.prototype.conditionLosingCombat = function()
{
var cascade = this.getParameter("oolite_cascadeDetected");
if (cascade != null)
{
if (cascade.distanceTo(this.ship) < 25600)
{
return true;
}
else
{
this.setParameter("oolite_cascadeDetected",null);
}
}
if (this.ship.energy == this.ship.maxEnergy)
{
// forget previous defeats
this.setParameter("oolite_lastFleeing",null);
}
if (!this.conditionInCombat())
{
return false;
}
var lastThreat = this.getParameter("oolite_lastFleeing");
if (lastThreat != null && this.ship.position.distanceTo(lastThreat) < 25600)
{
// the thing that attacked us is still nearby
return true;
}
if (this.ship.energy * 4 < this.ship.maxEnergy)
{
// TODO: adjust threshold based on group odds
return true; // losing if less than 1/4 energy
}
var dts = this.ship.defenseTargets;
for (var i = 0 ; i < dts.length ; i++)
{
if (dts[i].scanClass == "CLASS_MISSILE" && dts[i].target == this.ship)
{
return true;
}
if (dts[i].scanClass == "CLASS_MINE")
{
return true;
}
}
// if we've dumped cargo or the group leader has, then we're losing
if (this.ship.AIScript.oolite_intership.cargodemandpaid)
{
return true;
}
if (this.ship.group && this.ship.group.leader && this.ship.group.leader.AIScript.oolite_intership && this.ship.group.leader.AIScript.oolite_intership.cargodemandpaid)
{
return true;
}
// TODO: add some reassessment of odds based on group size
return false; // not losing yet
}
AILib.prototype.conditionMothershipInCombat = function()
{
if (this.ship.group && this.ship.group.leader && this.ship.group.leader != this.ship)
{
var leader = this.ship.group.leader;
if (leader.position.distanceTo(this.ship) > this.ship.scannerRange)
{
return false; // can't tell
}
if (this.isFighting(leader))
{
return true;
}
if (leader.target && leader.target.target == leader && leader.target.hasHostileTarget)
{
return true;
}
var dts = leader.defenseTargets;
for (var i = 0 ; i < dts.length ; i++)
{
if (dts[i].target == leader && dts[i].hasHostileTarget)
{
return true;
}
}
return false;
}
else
{
// no mothership
return false;
}
}
AILib.prototype.conditionMothershipIsAttacking = function()
{
if (this.ship.group && this.ship.group.leader != this.ship)
{
var leader = this.ship.group.leader;
if (leader.target && this.isFighting(leader) && leader.target.position.distanceTo(this.ship) < this.ship.scannerRange)
{
return true;
}
}
return false;
}
// as MothershipIsAttacking, but leader.target must be aggressive
AILib.prototype.conditionMothershipIsAttackingHostileTarget = function()
{
if (this.ship.group && this.ship.group.leader != this.ship)
{
var leader = this.ship.group.leader;
if (leader.target && this.isFighting(leader) && this.isAggressive(leader.target) && leader.target.position.distanceTo(this.ship) < this.ship.scannerRange)
{
return true;
}
}
return false;
}
AILib.prototype.conditionMothershipUnderAttack = function()
{
if (this.ship.group && this.ship.group.leader != this.ship)
{
var leader = this.ship.group.leader;
if (leader.target && leader.target.target == leader && leader.target.hasHostileTarget && leader.target.position.distanceTo(this.ship) < this.ship.scannerRange)
{
return true;
}
var dts = leader.defenseTargets;
for (var i = 0 ; i < dts.length ; i++)
{
if (dts[i].target == leader && dts[i].hasHostileTarget && dts[i].position.distanceTo(this.ship) < this.ship.scannerRange)
{
return true;
}
}
return false;
}
else
{
return false;
}
}
/*** Navigation-related conditions ***/
AILib.prototype.conditionCanWitchspaceOut = function()
{
if (!this.ship.hasHyperspaceMotor)
{
return false;
}
return (system.info.systemsInRange(this.ship.fuel).length > 0);
}
AILib.prototype.conditionFriendlyStationExists = function()
{
var stations = system.stations;
for (var i = 0 ; i < stations.length ; i++)
{
var station = stations[i];
if (this.friendlyStation(station))
{
// this is not a very good check for friendliness, but
// it will have to do for now
return true;
}
}
return false;
}
AILib.prototype.conditionFriendlyStationNearby = function()
{
var stations = system.stations;
for (var i = 0 ; i < stations.length ; i++)
{
var station = stations[i];
if (this.friendlyStation(station))
{
// this is not a very good check for friendliness, but
// it will have to do for now
if (station.position.distanceTo(this.ship) < this.ship.scannerRange)
{
return true;
}
}
}
return false;
}
AILib.prototype.conditionGroupIsSeparated = function()
{
if (!this.ship.group || !this.ship.group.leader)
{
return false;
}
if (this.ship.group.leader.isStation)
{
// can get 2x as far from station
return (this.ship.position.distanceTo(this.ship.group.leader) > this.ship.scannerRange * 2);
}
else
{
return (this.ship.position.distanceTo(this.ship.group.leader) > this.ship.scannerRange);
}
}
AILib.prototype.conditionHasSelectedPlanet = function()
{
var planet = this.getParameter("oolite_selectedPlanet");
if (planet && (!planet.isValid || !planet.isPlanet))
{
this.setParameter("oolite_selectedPlanet",null);
return false;
}
return planet != null;
}
AILib.prototype.conditionHasSelectedStation = function()
{
var station = this.getParameter("oolite_selectedStation");
if (station && (!station.isValid || !station.isStation))
{
this.setParameter("oolite_selectedStation",null);
return false;
}
return station != null;
}
AILib.prototype.conditionHomeStationExists = function()
{
return (this.homeStation() != null);
}
AILib.prototype.conditionHomeStationNearby = function()
{
var home = this.homeStation();
if (home == null)
{
return false;
}
return this.ship.position.distanceTo(home) < this.ship.scannerRange;
}
AILib.prototype.conditionInInterstellarSpace = function()
{
return system.isInterstellarSpace;
}
AILib.prototype.conditionMainPlanetNearby = function()
{
if (!system.mainPlanet)
{
return false;
}
if (this.ship.position.distanceTo(system.mainPlanet) < system.mainPlanet.radius * 4)
{
return true;
}
return false;
}
AILib.prototype.conditionNearDestination = function()
{
return (this.ship.destination.squaredDistanceTo(this.ship) < this.ship.desiredRange * this.ship.desiredRange);
}
AILib.prototype.conditionPlayerNearby = function()
{
return this.ship.position.distanceTo(player.ship) < this.ship.scannerRange;
}
AILib.prototype.conditionReadyToSunskim = function()
{
return (system.sun && this.ship.position.distanceTo(system.sun) < system.sun.radius * 1.15);
}
AILib.prototype.conditionSelectedStationNearby = function()
{
var station = this.getParameter("oolite_selectedStation");
if (station && station.position.distanceTo(this.ship) < this.ship.scannerRange)
{
return true;
}
return false;
}
AILib.prototype.conditionSelectedStationNearMainPlanet = function()
{
if (!system.mainPlanet)
{
return false;
}
var station = this.getParameter("oolite_selectedStation");
if (station && station.position.distanceTo(system.mainPlanet) < system.mainPlanet.radius * 4)
{
return true;
}
return false;
}
AILib.prototype.conditionSunskimPossible = function()
{
return (system.sun &&
!system.sun.hasGoneNova &&
!system.sun.isGoingNova &&
this.ship.fuel < 7 &&
this.ship.equipmentStatus("EQ_FUEL_SCOOPS") == "EQUIPMENT_OK" &&
(this.ship.heatInsulation > 1000/this.ship.maxSpeed || this.ship.heatInsulation >= 12));
}
/*** Pirate conditions ***/
AILib.prototype.conditionCargoDemandsMet = function()
{
if (!this.getParameter("oolite_flag_watchForCargo"))
{
log(this.name,"AI '"+this.ship.AIScript.name+"' for ship "+this.ship+" is asking if cargo demands are met but has not set 'oolite_flag_watchForCargo'");
return true;
}
var seen = this.getParameter("oolite_cargoDropped");
if (seen != null)
{
var recorder = null;
var demand = 0;
if (this.ship.group)
{
if (this.ship.group.leader && this.ship.group.leader.AIScript.oolite_intership && this.ship.group.leader.AIScript.oolite_intership.cargodemanded > 0)
{
if (this.ship.group.leader.AIScript.oolite_intership.cargodemandmet)
{
return true;
}
recorder = this.ship.group.leader;
demand = this.ship.group.leader.AIScript.oolite_intership.cargodemanded;
}
else if (this.ship.group.ships[0].AIScript.oolite_intership && this.ship.group.ships[0].AIScript.oolite_intership.cargodemanded > 0)
{
demand = this.ship.group.ships[0].AIScript.oolite_intership.cargodemanded;
if (this.ship.group.ships[0].AIScript.oolite_intership.cargodemandmet)
{
return true;
}
recorder = this.ship.group.ships[0];
}
}
else
{
if (this.ship.AIScript.oolite_intership.cargodemanded > 0)
{
if (this.ship.AIScript.oolite_intership.cargodemandmet)
{
return true;
}
demand = this.ship.AIScript.oolite_intership.cargodemanded;
recorder = this.ship;
}
}
if (demand == 0)
{
return true; // no demand made
}
if (demand <= seen)
{
recorder.AIScript.oolite_intership.cargodemandmet = true;
return true;
}
}
return false;
}
AILib.prototype.conditionGroupHasEnoughLoot = function()
{
var used = 0;
var available = 0;
if (!this.ship.group)
{
used = this.ship.cargoSpaceUsed;
if (this.ship.equipmentStatus("EQ_FUEL_SCOOPS") == "EQUIPMENT_OK")
{
available = this.ship.cargoSpaceAvailable;
}
}
else
{
for (var i = 0; i < this.ship.group.ships.length; i++)
{
used += this.ship.group.ships[i].cargoSpaceUsed;
if (this.ship.equipmentStatus("EQ_FUEL_SCOOPS") == "EQUIPMENT_OK")
{
available += this.ship.group.ships[i].cargoSpaceAvailable;
}
}
}
if (available < used || available == 0)
{
/* Over half-full. This will do for now. TODO: cutting
* losses if group is taking damage, losing ships, running
* low on consumables, etc. */
return true;
}
return false;
}
AILib.prototype.conditionPiratesCanBePaidOff = function()
{
if (this.ship.AIScript.oolite_intership.cargodemandpaid)
{
return false;
}
// TODO: need some way for the player to set this
if (!this.ship.AIScript.oolite_intership.cargodemand)
{
return false;
}
if (this.ship.cargoSpaceUsed < this.ship.AIScript.oolite_intership.cargodemand)
{
return false;
}
return true;
}
/*** Scanner conditions ***/
AILib.prototype.conditionScannerContainsEscapePods = function()
{
return this.checkScannerWithPredicate(function(s) {
return s.primaryRole == "escape-capsule" && s.isInSpace && s.scanClass == "CLASS_CARGO" && s.velocity.magnitude() < this.ship.maxSpeed && this.conditionCanScoopCargo();
});
}
AILib.prototype.conditionScannerContainsFineableOffender = function()
{
return this.checkScannerWithPredicate(function(s) {
var threshold = this.fineThreshold();
return s.isInSpace && s.bounty <= threshold && s.bounty > 0 && !s.markedForFines && (s.scanClass == "CLASS_NEUTRAL" || s.isPlayer) && !s.isDerelict;
});
}
AILib.prototype.conditionScannerContainsFugitive = function()
{
return this.checkScannerWithPredicate(function(s) {
return s.isInSpace && s.bounty > 50 && s.scanClass != "CLASS_CARGO" && s.scanClass != "CLASS_ROCK";
});
}
AILib.prototype.conditionScannerContainsHuntableOffender = function()
{
return this.checkScannerWithPredicate(function(s) {
var threshold = this.fineThreshold();
return s.isInSpace && s.bounty > threshold && s.scanClass != "CLASS_CARGO" && s.scanClass != "CLASS_ROCK";
});
}
AILib.prototype.conditionScannerContainsHunters = function()
{
return this.checkScannerWithPredicate(function(s) {
return s.primaryRole == "hunter" || s.scanClass == "CLASS_POLICE" || (s.isStation && s.isMainStation);
});
}
AILib.prototype.conditionScannerContainsMiningOpportunity = function()
{
// if hold full, no
if (!this.conditionCanScoopCargo())
{
return false;
}
// need a mining laser, and for now a forward one
if (!this.ship.forwardWeapon == "EQ_WEAPON_MINING_LASER")
{
return false;
}
return this.conditionScannerContainsRocks();
}
AILib.prototype.conditionScannerContainsNonThargoid = function()
{
var prioritytargets = this.checkScannerWithPredicate(function(s) {
return s.scanClass != "CLASS_THARGOID" && s.scanClass != "CLASS_ROCK" && s.scanClass != "CLASS_CARGO";
});
if (prioritytargets)
{
return true;
}
return this.checkScannerWithPredicate(function(s) {
return s.scanClass != "CLASS_THARGOID";
});
}
AILib.prototype.conditionScannerContainsPirateVictims = function()
{
return this.checkScannerWithPredicate(function(s) {
// is a pirate victim
// has some cargo on board
// hasn't already paid up
return s.isPirateVictim && s.cargoSpaceUsed > 0 && (!s.AIScript || !s.AIScript.oolite_intership || !s.AIScript.oolite_intership.cargodemandpaid);
});
}
AILib.prototype.conditionScannerContainsReadyThargoidMothership = function()
{
return this.checkScannerWithPredicate(function(s) {
return s.hasRole("thargoid-mothership") && (!s.escortGroup || s.escortGroup.count <= 16);
});
}
AILib.prototype.conditionScannerContainsRocks = function()
{
var scan1 = this.checkScannerWithPredicate(function(s) {
return s.isInSpace && s.isBoulder;
});
if (scan1)
{
return true;
}
// no boulders, what about asteroids?
return this.checkScannerWithPredicate(function(s) {
return s.isInSpace && s.hasRole("asteroid");
});
}
AILib.prototype.conditionScannerContainsSalvage = function()
{
return this.checkScannerWithPredicate(function(s) {
return s.isInSpace && s.scanClass == "CLASS_CARGO";
});
}
AILib.prototype.conditionScannerContainsSalvageForGroup = function()
{
var maxspeed = 0;
if (this.conditionCanScoopCargo())
{
maxspeed = this.ship.maxSpeed;
}
if (this.ship.group)
{
for (var i = 0; i < this.ship.group.ships.length ; i++)
{
var ship = this.ship.group.ships[i];
if (ship.cargoSpaceAvailable > 0 && ship.equipmentStatus("EQ_FUEL_SCOOPS") == "EQUIPMENT_OK" && ship.maxSpeed > maxspeed)
{
maxspeed = ship.maxSpeed;
}
}
}
return this.checkScannerWithPredicate(function(s) {
return s.isInSpace && s.scanClass == "CLASS_CARGO" && s.velocity.magnitude() < maxspeed;
});
}
AILib.prototype.conditionScannerContainsSalvageForMe = function()
{
if (!this.conditionCanScoopCargo())
{
return false;
}
return this.checkScannerWithPredicate(function(s) {
return s.isInSpace && s.scanClass == "CLASS_CARGO" && s.velocity.magnitude() < this.ship.maxSpeed;
});
}
AILib.prototype.conditionScannerContainsShipNeedingEscort = function()
{
if (this.ship.bounty == 0)
{
return this.checkScannerWithPredicate(function(s) {
return s.scanClass == this.ship.scanClass && s.bounty == 0 && (!s.escortGroup || s.escortGroup.count <= s.maxEscorts);
});
}
else
{
return this.checkScannerWithPredicate(function(s) {
return s.scanClass == this.ship.scanClass && s.bounty > 0 && (!s.escortGroup || s.escortGroup.count <= s.maxEscorts);
});
}
}
AILib.prototype.conditionScannerContainsThargoidMothership = function()
{
return this.checkScannerWithPredicate(function(s) {
return s.hasRole("thargoid-mothership");
});
}
/*** State conditions ***/
AILib.prototype.conditionAllEscortsInFlight = function()
{
if (!this.ship.escortGroup)
{
return true; // there are no escorts not in flight
}
for (var i = 0 ; i < this.ship.escortGroup.ships.length ; i++)
{
if (this.ship.escortGroup.ships[i].status != "STATUS_IN_FLIGHT")
{
return false;
}
}
// if just exited witchspace, escorts might not have rejoined escort
// group yet.
if (!this.ship.group)
{
return true;
}
for (var i = 0 ; i < this.ship.group.ships.length ; i++)
{
if (this.ship.group.ships[i].status != "STATUS_IN_FLIGHT")
{
return false;
}
}
return true;
}
AILib.prototype.conditionCanScoopCargo = function()
{
if (this.ship.cargoSpaceAvailable == 0 || this.ship.equipmentStatus("EQ_FUEL_SCOOPS") != "EQUIPMENT_OK")
{
return false;
}
return true;
}
AILib.prototype.conditionCargoIsProfitableHere = function()
{
/* TODO: in the Mainly X systems, it's not impossible for
* PLENTIFUL_GOODS to generate a hold which is profitable in that
* system, and SCARCE_GOODS not to do so. Cargo should never be
* profitable in its origin system. */
if (!system.mainStation)
{
return false;
}
if (this.ship.cargoSpaceUsed == 0)
{
return false;
}
var cargo = this.ship.cargoList;
var profit = 0;
var multiplier = (system.info.economy <= 3)?-1:1;
for (var i = 0 ; i < cargo.length ; i++)
{
var commodity = cargo[i].commodity;
var quantity = cargo[i].quantity;
var adjust = system.mainStation.market[commodity].marketEcoAdjustPrice * multiplier * quantity / system.mainStation.market[commodity].marketMaskPrice;
profit += adjust;
}
return (profit >= 0);
}
AILib.prototype.conditionGroupLeaderIsStation = function()
{
return (this.ship.group && this.ship.group.leader && this.ship.group.leader.isStation);
}
AILib.prototype.conditionHasInterceptCoordinates = function()
{
return (this.getParameter("oolite_interceptCoordinates") != null);
}
AILib.prototype.conditionHasMothership = function()
{
return (this.ship.group && this.ship.group.leader && this.ship.group.leader != this.ship);
}
AILib.prototype.conditionHasNonThargoidTarget = function()
{
return (this.ship.target && this.ship.target.scanClass != "CLASS_THARGOID");
}
AILib.prototype.conditionHasReceivedDistressCall = function()
{
var aggressor = this.getParameter("oolite_distressAggressor");
var sender = this.getParameter("oolite_distressSender");
var ts = this.getParameter("oolite_distressTimestamp");
if (aggressor == null || !aggressor.isInSpace || sender == null || !sender.isInSpace || sender.position.distanceTo(this.ship) > this.ship.scannerRange || ts+30 < clock.adjustedSeconds)
{
// no, or it has expired
this.setParameter("oolite_distressAggressor",null);
this.setParameter("oolite_distressSender",null);
this.setParameter("oolite_distressTimestamp",null);
return false;
}
return true;
}
AILib.prototype.conditionHasTarget = function()
{
return this.ship.target != null;
}
AILib.prototype.conditionHasWaypoint = function()
{
return this.getParameter("oolite_waypoint") != null;
}
AILib.prototype.conditionIsActiveThargon = function()
{
return this.ship.scanClass == "CLASS_THARGOID" && this.ship.hasRole("EQ_THARGON");
}
AILib.prototype.conditionIsEscorting = function()
{
if (!this.ship.group || !this.ship.group.leader || this.ship.group.leader == this.ship)
{
return false;
}
if (this.ship.group.leader.escortGroup && this.ship.group.leader.escortGroup.containsShip(this.ship))
{
if (this.ship.group.leader.status == "STATUS_ENTERING_WITCHSPACE")
{
var hole = this.getParameter("oolite_witchspaceWormhole");
if (hole == null || hole.expiryTime < clock.seconds)
{
// has been left behind
this.configurationLeaveEscortGroup();
this.setParameter("oolite_witchspaceWormhole",false);
return false;
}
}
return true;
}
return false;
}
AILib.prototype.conditionIsGroupLeader = function()
{
if (!this.ship.group)
{
return true;
}
return (this.ship.group.leader == this.ship);
}
AILib.prototype.conditionMissileOutOfFuel = function()
{
var range = 30000; // 30 km default
if (this.ship.scriptInfo.oolite_missile_range)
{
range = this.ship.scriptInfo.oolite_missile_range;
}
return range < this.ship.distanceTravelled;
}
AILib.prototype.conditionWitchspaceEntryRequested = function()
{
return (this.getParameter("oolite_witchspaceWormhole") != null);
}
/* ****************** Behaviour functions ************** */
/* Behaviours. Behaviours are effectively a state definition,
* defining a set of events and responses. They are aided in this
* by the 'responses', which mean that the event handlers for the
* behaviour within the definition can itself be templated. */
AILib.prototype.behaviourApproachDestination = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
handlers.shipAchievedDesiredRange = function()
{
var waypoints = this.getParameter("oolite_waypoints");
if (waypoints != null)
{
if (waypoints.length > 0)
{
waypoints.pop();
if (waypoints.length == 0)
{
waypoints = null;
}
this.setParameter("oolite_waypoints",waypoints);
}
}
else
{
var patrol = this.getParameter("oolite_waypoint");
if (patrol != null && this.ship.destination.distanceTo(patrol) < 1000+this.getParameter("oolite_waypointRange"))
{
// finished patrol to waypoint
// clear route
this.communicate("oolite_waypointReached",{},3);
this.setParameter("oolite_waypoint",null);
this.setParameter("oolite_waypointRange",null);
if (this.getParameter("oolite_flag_patrolStation"))
{
if (this.ship.group)
{
var station = this.ship.group.leader;
if (station != null && station.isStation)
{
this.communicate("oolite_patrolReportIn",this.entityCommsParams(station),4);
this.ship.patrolReportIn(station);
}
}
}
}
}
this.reconsiderNow();
};
var waypoints = this.getParameter("oolite_waypoints");
if (waypoints != null)
{
this.ship.destination = waypoints[waypoints.length-1];
this.ship.desiredRange = 1000;
}
var blocker = this.ship.checkCourseToDestination();
if (blocker)
{
if (blocker.isPlanet || blocker.isSun)
{
// the selected planet can't block
if (blocker.isSun || this.getParameter("oolite_selectedPlanet") != blocker)
{
if (this.ship.position.distanceTo(blocker) < blocker.radius * 3)
{
if (waypoints == null)
{
waypoints = [];
}
waypoints.push(this.ship.getSafeCourseToDestination());
this.ship.destination = waypoints[waypoints.length-1];
this.ship.desiredRange = 1000;
}
}
}
else if (blocker.isShip)
{
if (this.ship.position.distanceTo(blocker) < 25600)
{
if (!blocker.group || !blocker.group.leader == this.ship)
{
// our own escorts are not a blocker!
if (waypoints == null)
{
waypoints = [];
}
waypoints.push(this.ship.getSafeCourseToDestination());
this.ship.destination = waypoints[waypoints.length-1];
this.ship.desiredRange = 1000;
}
}
}
}
this.setParameter("oolite_waypoints",waypoints);
this.applyHandlers(handlers);
this.ship.performFlyToRangeFromDestination();
}
AILib.prototype.behaviourAvoidCascadeExplosion = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
var cascade = this.getParameter("oolite_cascadeDetected");
if (cascade != null)
{
if (cascade.distanceTo(this.ship) < 25600)
{
if (this.ship.defenseTargets.length > 0 && this.ship.defenseTargets[0].scanClass == "CLASS_MINE")
{
// if the mine is still visible, conventional fleeing works
this.ship.target = this.ship.defenseTargets[0];
this.ship.desiredRange = 30000;
this.ship.performFlee();
return;
}
else
{
if (this.ship.destination != cascade)
{
this.communicate("oolite_quiriumCascade",{},3);
}
this.ship.destination = cascade;
this.ship.desiredRange = 30000;
this.ship.desiredSpeed = 10*this.ship.maxSpeed;
this.ship.performFlyToRangeFromDestination();
return;
}
}
else
{
this.setParameter("oolite_cascadeDetected",null);
}
}
}
AILib.prototype.behaviourBecomeInactiveThargon = function()
{
this.applyHandlers({});
this.ship.scanClass = "CLASS_CARGO";
this.ship.target = null;
this.ship.clearDefenseTargets();
if (this.ship.group)
{
this.ship.group.removeShip(this.ship);
this.ship.group = null;
}
if (this.ship.escortGroup)
{
this.ship.escortGroup.removeShip(this.ship);
}
this.ship.desiredSpeed = 0;
this.ship.performStop();
var nearby = this.ship.checkScanner();
for (var i = 0 ; i < nearby.length ; i++)
{
var ship = nearby[i];
if (ship.target == this.ship && !ship.isPlayer && ship.hasHostileTarget)
{
ship.target = null;
}
ship.removeDefenseTarget(this.ship);
}
}
AILib.prototype.behaviourCollectSalvage = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
handlers.shipScoopedOther = function(other)
{
this.setParameter("oolite_cargoDropped",null);
this.reconsiderNow();
}
this.applyHandlers(handlers);
this.ship.performCollect();
}
AILib.prototype.behaviourDestroyCurrentTarget = function()
{
this.setParameter("oolite_witchspaceEntry",null);
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
if (this.ship.target && !this.ship.hasHostileTarget)
{
// entering attack mode
this.communicate("oolite_beginningAttack",this.entityCommsParams(this.ship.target),2);
}
else
{
this.communicate("oolite_continuingAttack",this.entityCommsParams(this.ship.target),4);
}
this.ship.performAttack();
}
AILib.prototype.behaviourDockWithStation = function()
{
// may need to release escorts
if (this.ship.escortGroup && this.ship.escortGroup.count > 1)
{
this.ship.dockEscorts();
}
var station = this.getParameter("oolite_dockingStation");
this.ship.target = station;
var handlers = {};
this.responsesAddStandard(handlers);
this.responsesAddDocking(handlers);
this.ship.requestDockingInstructions();
if (!this.ship.dockingInstructions)
{
this.ship.performIdle();
this.reconsiderNow();
return;
}
switch (this.ship.dockingInstructions.ai_message)
{
case "TOO_BIG_TO_DOCK":
case "DOCKING_REFUSED":
this.ship.setParameter("oolite_dockingStation",null);
this.ship.target = null;
this.reconsiderNow();
break;
case "TRY_AGAIN_LATER":
if (this.ship.target.position.distanceTo(this.ship) < 10000)
{
this.ship.destination = this.ship.target.position;
this.ship.desiredRange = 12500;
this.ship.desiredSpeed = this.cruiseSpeed();
this.ship.performFlyToRangeFromDestination();
break;
}
// else fall through
case "HOLD_POSITION":
this.ship.destination = this.ship.target.position;
this.ship.performFaceDestination();
// and will reconsider in a little bit
break;
case "APPROACH":
case "APPROACH_COORDINATES":
case "BACK_OFF":
this.ship.performFlyToRangeFromDestination();
break;
}
this.applyHandlers(handlers);
}
AILib.prototype.behaviourEnterWitchspace = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
var wormhole = this.getParameter("oolite_witchspaceWormhole");
if (wormhole && wormhole.expiryTime < clock.adjustedSeconds)
{
// the wormhole we were trying for has expired
this.setParameter("oolite_witchspaceWormhole",null);
}
else if (wormhole)
{
handlers.playerWillEnterWitchspace = function()
{
var wormhole = this.getParameter("oolite_witchspaceWormhole");
if (wormhole != null)
{
this.ship.enterWormhole(wormhole);
}
else
{
this.ship.enterWormhole();
}
}
this.ship.destination = wormhole.position;
this.ship.desiredRange = 0;
this.ship.desiredSpeed = this.ship.maxSpeed;
this.ship.performFlyToRangeFromDestination();
this.applyHandlers(handlers);
return;
}
var destID = this.getParameter("oolite_witchspaceDestination");
if (destID == null)
{
// look for wormholes out of here
// no systems in range
handlers.wormholeSuggested = function(hole)
{
this.ship.destination = hole.position;
this.ship.desiredRange = 0;
this.ship.desiredSpeed = this.ship.maxSpeed;
this.ship.performFlyToRangeFromDestination();
this.setParameter("oolite_witchspaceWormhole",hole);
// don't reconsider
}
handlers.playerWillEnterWitchspace = function()
{
var wormhole = this.getParameter("oolite_witchspaceWormhole");
if (wormhole != null)
{
this.ship.enterWormhole(wormhole);
}
else
{
this.ship.enterWormhole();
}
}
this.applyHandlers(handlers);
return;
}
else
{
handlers.shipWitchspaceBlocked = function(blocker)
{
this.ship.setDestination = blocker.position;
this.ship.setDesiredRange = 30000;
this.ship.setDesiredSpeed = this.cruiseSpeed();
this.ship.performFlyToRangeFromDestination();
this.setParameter("oolite_witchspaceEntry",null);
// no reconsidering yet
}
// set up the handlers before trying it
this.applyHandlers(handlers);
var entry = this.getParameter("oolite_witchspaceEntry");
// wait for escorts to launch
if (!this.conditionAllEscortsInFlight())
{
this.ship.destination = this.ship.position;
this.ship.desiredRange = 10000;
this.ship.desiredSpeed = this.cruiseSpeed();
if (this.ship.checkCourseToDestination())
{
this.ship.destination = this.ship.getSafeCourseToDestination();
}
this.ship.performFlyToRangeFromDestination();
}
else if (entry != null && entry < clock.seconds)
{
// this should work
var result = this.ship.exitSystem(destID);
// if it doesn't, we'll get blocked
if (result)
{
this.setParameter("oolite_witchspaceEntry",null);
}
}
else
{
if (entry == null)
{
this.communicate("oolite_engageWitchspaceDrive",{},4);
this.setParameter("oolite_witchspaceEntry",clock.seconds + 15);
}
this.ship.destination = this.ship.position;
this.ship.desiredRange = 10000;
this.ship.desiredSpeed = this.cruiseSpeed();
if (this.ship.checkCourseToDestination())
{
this.ship.destination = this.ship.getSafeCourseToDestination();
}
this.ship.performFlyToRangeFromDestination();
}
}
}
AILib.prototype.behaviourEscortMothership = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
this.responsesAddEscort(handlers);
this.applyHandlers(handlers);
this.ship.desiredRange = 0;
this.ship.performEscort();
}
AILib.prototype.behaviourFineCurrentTarget = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
if (this.ship.scanClass == "CLASS_POLICE" && this.ship.target)
{
this.communicate("oolite_markForFines",this.entityCommsParams(this.ship.target),1);
this.ship.markTargetForFines();
}
this.ship.performIdle();
}
AILib.prototype.behaviourFleeCombat = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
var cascade = this.getParameter("oolite_cascadeDetected");
if (cascade != null)
{
if (cascade.distanceTo(this.ship) < 25600)
{
if (this.ship.defenseTargets.length > 0 && this.ship.defenseTargets[0].scanClass == "CLASS_MINE")
{
// if the mine is still visible, conventional fleeing works
this.ship.target = this.ship.defenseTargets[0];
this.ship.desiredRange = 30000;
this.ship.performFlee();
return;
}
else
{
if (this.ship.destination != cascade)
{
this.communicate("oolite_quiriumCascade",{},4);
}
this.ship.destination = cascade;
this.ship.desiredRange = 30000;
this.ship.desiredSpeed = 10*this.ship.maxSpeed;
this.ship.performFlyToRangeFromDestination();
return;
}
}
else
{
this.setParameter("oolite_cascadeDetected",null);
}
}
this.ship.target = this.ship.AIPrimaryAggressor;
if (!this.ship.target || this.ship.position.distanceTo(this.ship.target) > 25600)
{
var dts = this.ship.defenseTargets;
for (var i = 0 ; i < dts.length ; i++)
{
this.ship.position.distanceTo(dts[i]) < 25600;
this.ship.target = dts[i];
break;
}
}
this.setParameter("oolite_lastFleeing",this.ship.target);
this.ship.desiredRange = this.ship.scannerRange;
this.ship.performFlee();
}
/* Follow the group leader in a less organised way than escorting them */
AILib.prototype.behaviourFollowGroupLeader = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
if (!this.ship.group || !this.ship.group.leader)
{
this.ship.performIdle();
}
else
{
this.ship.destination = this.ship.group.leader.position;
this.ship.desiredRange = 2000+Math.random()*2000;
this.ship.desiredSpeed = this.ship.maxSpeed;
this.ship.performFlyToRangeFromDestination();
}
}
AILib.prototype.behaviourGuardTarget = function()
{
if (!this.ship.target)
{
this.ship.destination = this.ship.position;
}
else
{
this.ship.destination = this.ship.target.position;
}
this.ship.desiredSpeed = this.cruiseSpeed();
this.ship.desiredRange = 2500;
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
this.ship.performFlyToRangeFromDestination();
}
AILib.prototype.behaviourLandOnPlanet = function()
{
this.ship.desiredSpeed = this.ship.maxSpeed / 4;
this.ship.performLandOnPlanet();
this.ship.AIScriptWakeTime = 0; // cancel reconsiderations
this.applyHandlers({}); // cancel interruptions
this.communicate("oolite_landingOnPlanet",{},4);
}
AILib.prototype.behaviourLeaveVicinityOfTarget = function()
{
if (!this.ship.target)
{
this.reconsiderNow();
return;
}
this.ship.destination = this.ship.target.position;
this.ship.desiredRange = 27500;
this.ship.desiredSpeed = this.ship.maxSpeed;
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
this.ship.performFlyToRangeFromDestination();
}
AILib.prototype.behaviourMineTarget = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
this.ship.performMining();
}
AILib.prototype.behaviourOfferToEscort = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
var possible = this.getParameter("oolite_scanResultSpecific");
if (possible == null)
{
this.reconsiderNow();
}
else
{
if (this.ship.offerToEscort(possible))
{
// accepted
this.reconsiderNow();
}
// if rejected, wait for next scheduled reconsideration
}
}
AILib.prototype.behaviourPayOffPirates = function()
{
this.ship.dumpCargo(this.ship.AIScript.oolite_intership.cargodemand);
this.communicate("oolite_agreeingToDumpCargo",{"oolite_demandSize":this.ship.AIScript.oolite_intership.cargodemand},2);
delete this.ship.AIScript.oolite_intership.cargodemand;
this.ship.AIScript.oolite_intership.cargodemandpaid = true;
this.behaviourFleeCombat();
}
AILib.prototype.behaviourReconsider = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
this.reconsiderNow();
}
// Separate behaviour to EscortMothership in case we want to change it later
// This is the one to catch up with a distant mothership
AILib.prototype.behaviourRejoinMothership = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
this.responsesAddEscort(handlers);
this.applyHandlers(handlers);
// to consider: should this behaviour use injectors if
// possible? so few escorts have them that it's probably not
// worth it.
this.ship.desiredRange = 0;
this.ship.performEscort();
}
AILib.prototype.behaviourRepelCurrentTarget = function()
{
this.setParameter("oolite_witchspaceEntry",null);
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
if (!this.ship.target || !this.ship.target.isValid || !this.ship.target.isShip)
{
this.reconsiderNow();
return;
}
if (!this.isAggressive(this.ship.target))
{
var target = this.ship.target;
// repelling succeeded
if (this.ship.escortGroup)
{
// also tell escorts to stop attacking it
for (var i = 0 ; i < this.ship.escortGroup.ships.length ; i++)
{
this.ship.escortGroup.ships[i].removeDefenseTarget(target);
if (this.ship.escortGroup.ships[i].target == target)
{
this.ship.escortGroup.ships[i].target = null;
}
}
}
this.ship.removeDefenseTarget(target);
this.ship.target = null;
}
else
{
if (!this.ship.hasHostileTarget)
{
// entering attack mode
this.communicate("oolite_beginningAttack",this.entityCommsParams(this.ship.target),3);
}
this.ship.performAttack();
}
}
/* Standard "help the innocent" distress call response. Perhaps
* there should be a 'blood in the water' response available
* too... */
AILib.prototype.behaviourRespondToDistressCall = function()
{
var aggressor = this.getParameter("oolite_distressAggressor");
var sender = this.getParameter("oolite_distressSender");
if (aggressor && aggressor.isShip && sender && sender.isShip)
{
if (sender.bounty > aggressor.bounty)
{
var tmp = sender;
sender = aggressor;
aggressor = tmp;
}
if (aggressor.position.distanceTo(this.ship) < this.ship.scannerRange)
{
this.ship.target = aggressor;
this.ship.performAttack();
this.reconsiderNow();
this.communicate("oolite_distressResponseAggressor",this.entityCommsParams(aggressor),2);
}
else
{ // we can't actually see what's attacking the sender yet
this.ship.destination = sender.position;
this.ship.desiredRange = 1000+sender.collisionRadius+this.ship.collisionRadius;
this.ship.desiredSpeed = 7 * this.ship.maxSpeed; // use injectors if possible
this.ship.performFlyToRangeFromDestination();
// and when we next reconsider, hopefully the aggressor will be on the scanner
this.communicate("oolite_distressResponseSender",this.entityCommsParams(sender),2);
}
}
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
}
AILib.prototype.behaviourRobTarget = function()
{
var demand = null;
if (this.ship.group && this.ship.group.leader)
{
if (this.ship.group.leader.AIScript.oolite_intership && this.ship.group.leader.AIScript.oolite_intership.cargodemanded)
{
demand = this.ship.group.leader.AIScript.oolite_intership.cargodemanded;
}
}
else
{
if (this.ship.AIScript.oolite_intership.cargodemanded)
{
demand = this.ship.AIScript.oolite_intership.cargodemanded;
}
}
if (demand == null)
{
var hascargo = this.ship.target.cargoSpaceUsed+this.ship.target.cargoSpaceAvailable;
// blowing them up probably gets ~10%, so how much we feel
// confident in demanding depends on how likely patrols
// are to come along and interfere.
demand = (hascargo/20);
demand = demand * (1+Math.random()+(8-system.info.government)/8);
// between 5% and 15% of cargo
demand = Math.ceil(demand); // round it up so there's always at least 1
var maxdemand = 0;
var gc = 1;
if (!this.ship.group)
{
if (this.ship.equipmentStatus("EQ_FUEL_SCOOPS") == "EQUIPMENT_OK")
{
maxdemand = this.ship.cargoSpaceAvailable;
}
}
else
{
gc = this.ship.group.ships.length;
for (var i = 0; i < gc ; i++)
{
var ship = this.ship.group.ships[i];
if (ship.equipmentStatus("EQ_FUEL_SCOOPS") == "EQUIPMENT_OK")
{
maxdemand += ship.cargoSpaceAvailable;
}
else
{
gc--; // this ship can't help scoop
}
}
}
if (demand > maxdemand)
{
demand = maxdemand; // don't ask for more than we can carry
}
while (demand > gc * 5)
{
// asking for more than 5TC each probably means there
// won't be time to pick it all up anyway
demand = Math.ceil(demand/2);
}
if (demand < 2)
{
demand = 2;
}
/* Record our demand with the group leader */
if (this.ship.group && this.ship.group.leader)
{
this.ship.group.leader.AIScript.oolite_intership.cargodemanded = demand;
}
else
{
this.ship.AIScript.oolite_intership.cargodemanded = demand;
}
/* Inform the victim of the demand, if possible */
if (this.ship.target.AIScript && this.ship.target.AIScript.oolite_intership)
{
this.ship.target.AIScript.oolite_intership.cargodemand = demand;
}
var commsparams = this.entityCommsParams(this.ship.target);
commsparams["oolite_demandSize"] = demand;
this.communicate("oolite_makePirateDemand",commsparams,1);
this.ship.requestHelpFromGroup();
/* }
else
{
log(this.ship.displayName,"Already asked for "+demand); */
}
var handlers = {};
this.responsesAddStandard(handlers);
this.applyHandlers(handlers);
this.ship.performAttack();
this.ship.requestHelpFromGroup();
}
AILib.prototype.behaviourSunskim = function()
{
var handlers = {};
this.responsesAddStandard(handlers);
this.responsesAddScooping(handlers);
this.applyHandlers(handlers);
this.ship.performFlyToRangeFromDestination();
}
AILib.prototype.behaviourTumble = function()
{
this.applyHandlers({});
this.ship.performTumble();
}
/* Missile behaviours: have different standard handler sets */
AILib.prototype.behaviourMissileInterceptTarget = function()
{
var handlers = {};
this.responsesAddMissile(handlers);
this.applyHandlers(handlers);
if (this.ship.scriptInfo.oolite_missile_proximity)
{
this.ship.desiredRange = this.ship.scriptInfo.oolite_missile_proximity;
}
else
{
this.ship.desiredRange = 25;
}
this.ship.performIntercept();
}
AILib.prototype.behaviourMissileInterceptCoordinates = function()
{
var handlers = {};
this.responsesAddMissile(handlers);
this.applyHandlers(handlers);
if (this.ship.scriptInfo.oolite_missile_proximity)
{
this.ship.desiredRange = this.ship.scriptInfo.oolite_missile_proximity;
}
else
{
this.ship.desiredRange = 25;
}
var dest = this.getParameter("oolite_interceptCoordinates");
if (dest == null)
{
return;
}
this.ship.destination = dest
this.ship.desiredSpeed = this.ship.maxSpeed;
this.ship.performFlyToRangeFromDestination();
// if we have an intercept target, try to restore it
var oldtarget = this.getParameter("oolite_interceptTarget");
if (oldtarget && !oldtarget.isCloaked && oldtarget.isInSpace)
{
this.ship.target = oldtarget;
}
}
AILib.prototype.behaviourMissileSelfDestruct = function() {
this.ship.explode();
}
/* Station behaviours: have different standard handler sets */
AILib.prototype.behaviourStationLaunchDefenseShips = function()
{
if (this.ship.target && this.isAggressive(this.ship.target))
{
this.alertCondition = 3;
this.ship.launchDefenseShip();
this.ship.requestHelpFromGroup();
}
var handlers = {};
this.responsesAddStation(handlers);
this.applyHandlers(handlers);
}
AILib.prototype.behaviourStationLaunchMiner = function()
{
if (this.alertCondition > 1)
{
this.alertCondition--;
}
var handlers = {};
this.responsesAddStation(handlers);
this.applyHandlers(handlers);
if (this.ship.group)
{
for (var i = 0 ; i < this.ship.group.ships.length ; i++)
{
if (this.ship.group.ships[i].primaryRole == "miner")
{
// only one in flight at once
return;
}
}
}
this.ship.launchMiner();
}
AILib.prototype.behaviourStationLaunchPatrol = function()
{
if (this.alertCondition > 1)
{
this.alertCondition--;
}
var handlers = {};
this.responsesAddStation(handlers);
this.applyHandlers(handlers);
if (this.ship.group)
{
for (var i = 0 ; i < this.ship.group.ships.length ; i++)
{
if (this.ship.group.ships[i].primaryRole == this.getParameter("oolite_stationPatrolRole"))
{
// only one in flight at once
return;
}
}
}
this.ship.launchPatrol();
}
AILib.prototype.behaviourStationLaunchSalvager = function()
{
if (this.alertCondition > 1)
{
this.alertCondition--;
}
this.ship.launchScavenger();
var handlers = {};
this.responsesAddStation(handlers);
this.applyHandlers(handlers);
}
AILib.prototype.behaviourStationManageTraffic = function()
{
var handlers = {};
this.responsesAddStation(handlers);
this.applyHandlers(handlers);
if (this.ship.hasNPCTraffic)
{
if (Math.random() < 0.3)
{
var trader = this.ship.launchShipWithRole("trader");
trader.setCargoType("PLENTIFUL_GOODS");
}
if (Math.random() < 0.1)
{
this.ship.launchShuttle();
}
// TODO: integrate with system repopulator rather than just
// launching ships at random
}
}
AILib.prototype.behaviourStationRespondToDistressCall = function()
{
var aggressor = this.getParameter("oolite_distressAggressor");
var sender = this.getParameter("oolite_distressSender");
if (sender.bounty > aggressor.bounty)
{
var tmp = sender;
sender = aggressor;
aggressor = tmp;
}
if (aggressor.position.distanceTo(this.ship) < this.ship.scannerRange)
{
this.ship.target = aggressor;
this.ship.alertCondition = 3;
this.ship.launchDefenseShip();
this.communicate("oolite_distressResponseAggressor",this.entityCommsParams(aggressor),2);
this.ship.requestHelpFromGroup();
}
else
{
this.communicate("oolite_distressResponseSender",this.entityCommsParams(sender),3);
}
var handlers = {};
this.responsesAddStation(handlers);
this.applyHandlers(handlers);
}
/* ****************** Configuration functions ************** */
/* Configurations. Configurations are set up actions for a behaviour
* or behaviours. They can also be used on a fall-through conditional
* to set parameters for later tests */
/*** Target acquisition configuration ***/
AILib.prototype.configurationAcquireCombatTarget = function()
{
if (this.ship.target && this.allied(this.ship,this.ship.target))
{
// don't shoot at allies even if they have ended up as a target...
this.ship.removeDefenseTarget(this.ship.target);
this.ship.target = null;
}
if (this.ship.target && this.ship.target.scanClass == "CLASS_CARGO")
{
this.ship.target = null;
}
/* Iff the ship does not currently have a target, select a new one
* from the defense target list. */
if (this.ship.target)
{
if (this.ship.target.isInSpace)
{
return;
}
this.ship.removeDefenseTarget(this.ship.target);
this.ship.target = null;
}
var dts = this.ship.defenseTargets
for (var i = 0; i < dts.length ; i++)
{
if (dts[i].position.distanceTo(this.ship) < this.ship.scannerRange)
{
this.ship.target = dts[0];
return;
}
}
if (this.ship.group != null)
{
for (var i = 0 ; i < this.ship.group.count ; i++)
{
if (this.ship.group.ships[i] != this.ship)
{
if (this.ship.group.ships[i].target && this.isFighting(this.ship.group.ships[i]) && this.ship.group.ships[i].target.position.distanceTo(this.ship) < this.ship.scannerRange)
{
this.ship.target = this.ship.group.ships[i].target;
return;
}
}
}
}
if (this.ship.escortGroup != null)
{
for (var i = 0 ; i < this.ship.escortGroup.count ; i++)
{
if (this.ship.escortGroup.ships[i] != this.ship)
{
if (this.ship.escortGroup.ships[i].target && this.isFighting(this.ship.escortGroup.ships[i]) && this.ship.escortGroup.ships[i].target.position.distanceTo(this.ship) < this.ship.scannerRange)
{
this.ship.target = this.ship.escortGroup.ships[i].target;
return;
}
}
}
}
}
AILib.prototype.configurationAcquireDefensiveEscortTarget = function()
{
if (this.ship.group && this.ship.group.leader)
{
var leader = this.ship.group.leader;
if (leader.target && leader.target.target == leader && this.isFighting(leader) && leader.target.position.distanceTo(this.ship) < this.ship.scannerRange)
{
this.ship.target = leader.target;
}
else
{
var dts = leader.defenseTargets;
for (var i = 0 ; i < dts.length ; i++)
{
if (dts[i].target == leader && this.isAggressive(dts[i]) && dts[i].position.distanceTo(this.ship) < this.ship.scannerRange)
{
this.ship.target = dts[i];
}
}
}
}
}
// TODO: reuse code from AcquireCombatTarget better
AILib.prototype.configurationAcquireHostileCombatTarget = function()
{
if (this.ship.target && this.allied(this.ship,this.ship.target))
{
// don't shoot at allies even if they have ended up as a target...
this.ship.removeDefenseTarget(this.ship.target);
this.ship.target = null;
}
/* Iff the ship does not currently have a target, select a new one
* from the defense target list. */
if (this.ship.target)
{
if (this.ship.target.isInSpace && this.isAggressive(this.ship.target))
{
return;
}
this.ship.removeDefenseTarget(this.ship.target);
this.ship.target = null;
}
var dts = this.ship.defenseTargets
for (var i = 0; i < dts.length ; i++)
{
if (dts[i].position.distanceTo(this.ship) < this.ship.scannerRange && this.isAggressive(dts[i]))
{
this.ship.target = dts[0];
return;
}
}
if (this.ship.group != null)
{
for (var i = 0 ; i < this.ship.group.count ; i++)
{
if (this.ship.group.ships[i] != this.ship)
{
if (this.ship.group.ships[i].target && this.isFighting(this.ship.group.ships[i]) && this.ship.group.ships[i].target.position.distanceTo(this.ship) < this.ship.scannerRange && this.isAggressive(this.ship.group.ships[i].target))
{
this.ship.target = this.ship.group.ships[i].target;
return;
}
}
}
}
if (this.ship.escortGroup != null)
{
for (var i = 0 ; i < this.ship.escortGroup.count ; i++)
{
if (this.ship.escortGroup.ships[i] != this.ship)
{
if (this.ship.escortGroup.ships[i].target && this.isFighting(this.ship.escortGroup.ships[i]) && this.ship.escortGroup.ships[i].target.position.distanceTo(this.ship) < this.ship.scannerRange && this.isAggressive(this.ship.escortGroup.ships[i].target))
{
this.ship.target = this.ship.escortGroup.ships[i].target;
return;
}
}
}
}
}
AILib.prototype.configurationAcquireOffensiveEscortTarget = function()
{
if (this.ship.group && this.ship.group.leader && this.ship.group.leader.target && this.ship.group.leader.hasHostileTarget)
{
if (this.ship.position.distanceTo(this.ship.group.leader.target) < this.ship.scannerRange)
{
this.ship.target = this.ship.group.leader.target;
this.ship.addDefenseTarget(this.ship.target);
}
}
}
AILib.prototype.configurationAcquirePlayerAsTarget = function()
{
this.ship.target = player.ship;
}
AILib.prototype.configurationAcquireScannedTarget = function()
{
this.ship.target = this.getParameter("oolite_scanResultSpecific");
}
AILib.prototype.configurationCheckScanner = function()
{
this.setParameter("oolite_scanResults",this.ship.checkScanner());
this.setParameter("oolite_scanResultSpecific",null);
}
/*** Navigation configuration ***/
AILib.prototype.configurationSelectRandomTradeStation = function()
{
var stations = system.stations;
var threshold = 1E16;
var chosenStation = null;
if (this.ship.bounty == 0)
{
if (Math.random() < 0.9 && this.friendlyStation(system.mainStation))
{
this.setParameter("oolite_selectedStation",system.mainStation);
return;
}
}
else if (this.ship.bounty <= this.fineThreshold())
{
if (Math.random() < 0.5 && this.friendlyStation(system.mainStation))
{
this.setParameter("oolite_selectedStation",system.mainStation);
return;
}
}
var friendlies = 0;
for (var i = 0 ; i < stations.length ; i++)
{
var station = stations[i];
if (this.friendlyStation(station))
{
friendlies++;
// equivalent to filtering the list to only contain
// friendlies, then picking a random element.
if (Math.random() < 1/friendlies)
{
chosenStation = station;
}
}
}
this.setParameter("oolite_selectedStation",chosenStation);
this.communicate("oolite_selectedStation",this.entityCommsParams(chosenStation),4);
}
AILib.prototype.configurationSelectShuttleDestination = function()
{
var possibles = system.planets.concat(system.stations);
var destinations1 = [];
var destinations2 = [];
for (var i = 0; i < possibles.length ; i++)
{
var possible = possibles[i];
// travel at least a little way
var distance = possible.position.distanceTo(this.ship);
if (distance > possible.collisionRadius + 10000)
{
// must be friendly destination and not moving too fast
if (possible.isPlanet || this.friendlyStation(possible) || possible.maxSpeed > this.ship.maxSpeed / 5)
{
if (distance > system.mainPlanet.radius * 5)
{
destinations2.push(possible);
}
else
{
destinations1.push(possible);
}
}
}
}
// no nearby destinations
if (destinations1.length == 0)
{
destinations1 = destinations2;
}
// no destinations
if (destinations1.length == 0)
{
return;
}
var destination = destinations1[Math.floor(Math.random()*destinations1.length)];
if (destination.isPlanet)
{
this.setParameter("oolite_selectedPlanet",destination);
this.setParameter("oolite_selectedStation",null);
}
else
{
this.setParameter("oolite_selectedStation",destination);
this.setParameter("oolite_selectedPlanet",null);
}
}
AILib.prototype.configurationSelectWitchspaceDestination = function()
{
if (!this.ship.hasHyperspaceMotor)
{
this.setParameter("oolite_witchspaceDestination",null);
return;
}
var preselected = this.getParameter("oolite_witchspaceDestination");
if (preselected != system.ID && system.info.distanceToSystem(System.infoForSystem(galaxyNumber,preselected)) <= this.ship.fuel)
{
// we've already got a destination
return;
}
var possible = system.info.systemsInRange(this.ship.fuel);
var selected = possible[Math.floor(Math.random()*possible.length)];
this.setParameter("oolite_witchspaceDestination",selected.systemID);
this.communicate("oolite_selectedWitchspaceDestination",{"oolite_witchspaceDestination":selected.name},4);
}
/*** Destination configuration ***/
AILib.prototype.configurationSetDestinationToNearestFriendlyStation = function()
{
var stations = system.stations;
var threshold = 1E16;
var chosenStation = null;
for (var i = 0 ; i < stations.length ; i++)
{
var station = stations[i];
if (this.friendlyStation(station))
{
var distance = station.position.distanceTo(this.ship);
if (distance < threshold)
{
threshold = distance;
chosenStation = station;
}
}
}
if (chosenStation == null)
{
this.ship.destination = this.ship.position;
this.ship.desiredRange = 0;
}
else
{
this.ship.destination = chosenStation.position;
this.ship.desiredRange = 15000;
this.ship.desiredSpeed = this.cruiseSpeed();
}
}
AILib.prototype.configurationSetDestinationToHomeStation = function()
{
var home = this.homeStation();
if (home != null)
{
this.ship.destination = home.position;
this.ship.desiredRange = 15000;
this.ship.desiredSpeed = this.cruiseSpeed();
}
else
{
this.ship.destination = this.ship.position;
this.ship.desiredRange = 0;
}
}
AILib.prototype.configurationSetDestinationToGroupLeader = function()
{
if (!this.ship.group || !this.ship.group.leader)
{
this.ship.destination = this.ship.position;
}
else
{
this.ship.destination = this.ship.group.leader.position;
}
this.ship.desiredRange = 2000;
this.ship.desiredSpeed = this.ship.maxSpeed;
}
AILib.prototype.configurationSetDestinationToMainPlanet = function()
{
if (system.mainPlanet)
{
this.ship.destination = system.mainPlanet.position;
this.ship.desiredRange = system.mainPlanet.radius * 3;
this.ship.desiredSpeed = this.cruiseSpeed();
}
}
AILib.prototype.configurationSetDestinationToMainStation = function()
{
this.ship.destination = system.mainStation.position;
this.ship.desiredRange = 15000;
this.ship.desiredSpeed = this.cruiseSpeed();
}
AILib.prototype.configurationSetDestinationToPirateLurk = function()
{
var lurk = this.getParameter("oolite_pirateLurk");
if (lurk != null)
{
this.ship.destination = lurk;
}
else
{
var code = "WITCHPOINT";
var p = this.ship.position;
// if already on a lane, stay on it
if (p.z < system.mainPlanet.position.z && ((p.x * p.x) + (p.y * p.y)) < this.scannerRange * this.scannerRange * 4)
{
lurk = p;
}
else if (p.subtract(system.mainPlanet).dot(p.subtract(system.sun)) < -0.9)
{
lurk = p;
}
else if (p.dot(system.sun.position) > 0.9)
{
lurk = p;
}
else // not on a lane, so pick somewhere at random
{
var choice = Math.random();
if (choice < 0.8)
{
code = "LANE_WP";
}
else
{
code = "LANE_PS";
}
// code = "LANE_WS"? "WITCHPOINT"?
// what about other locations in less policed systems?
lurk = system.locationFromCode(code);
}
this.setParameter("oolite_pirateLurk",lurk);
}
this.ship.desiredRange = 1000;
this.ship.desiredSpeed = this.cruiseSpeed();
}
AILib.prototype.configurationSetDestinationToSelectedPlanet = function()
{
var planet = this.getParameter("oolite_selectedPlanet");
if (planet)
{
this.ship.destination = planet.position;
this.ship.desiredRange = planet.radius+100;
this.ship.desiredSpeed = this.cruiseSpeed();
}
}
AILib.prototype.configurationSetDestinationToSelectedStation = function()
{
var station = this.getParameter("oolite_selectedStation");
if (station)
{
this.ship.destination = station.position;
this.ship.desiredRange = 15000;
this.ship.desiredSpeed = this.cruiseSpeed();
}
}
AILib.prototype.configurationSetDestinationToSunskimEnd = function()
{
if (system.sun)
{
var direction = Vector3D.random().cross(this.ship.position.subtract(system.sun.position));
// 2km parallel to local sun surface for every LY of fuel
this.ship.destination = this.ship.position.add(direction.multiply(2000*(7-this.ship.fuel)));
// max sunskim height is sqrt(4/3) radius
this.ship.desiredRange = 0;
this.ship.desiredSpeed = this.ship.maxSpeed;
}
}
AILib.prototype.configurationSetDestinationToSunskimStart = function()
{
if (system.sun)
{
this.ship.destination = system.sun.position;
// max sunskim height is sqrt(4/3) radius
this.ship.desiredRange = system.sun.radius * 1.125;
this.ship.desiredSpeed = this.cruiseSpeed();
}
}
AILib.prototype.configurationSetDestinationToWaypoint = function()
{
if (this.getParameter("oolite_waypoint") != null && this.getParameter("oolite_waypointRange") != null)
{
this.ship.destination = this.getParameter("oolite_waypoint");
this.ship.desiredRange = this.getParameter("oolite_waypointRange");
this.ship.desiredSpeed = this.cruiseSpeed();
}
}
AILib.prototype.configurationSetDestinationToWitchpoint = function()
{
this.ship.destination = new Vector3D(0,0,0);
this.ship.desiredRange = 10000;
this.ship.desiredSpeed = this.cruiseSpeed();
}
AILib.prototype.configurationSetWaypoint = function()
{
var gen = this.getWaypointGenerator();
if(gen != null)
{
gen.call(this);
this.configurationSetDestinationToWaypoint();
}
}
/*** Docking configurations ***/
AILib.prototype.configurationSetNearbyFriendlyStationForDocking = function()
{
var stations = system.stations;
for (var i = 0 ; i < stations.length ; i++)
{
var station = stations[i];
if (this.friendlyStation(station))
{
// this is not a very good check for friendliness, but
// it will have to do for now
if (station.position.distanceTo(this.ship) < this.ship.scannerRange)
{
this.setParameter("oolite_dockingStation",station)
return;
}
}
}
}
AILib.prototype.configurationSetHomeStationForDocking = function()
{
if (this.ship.owner && this.ship.owner.isStation && this.friendlyStation(this.ship.owner))
{
this.setParameter("oolite_dockingStation",this.ship.owner)
return;
}
}
AILib.prototype.configurationSetSelectedStationForDocking = function()
{
this.setParameter("oolite_dockingStation",this.getParameter("oolite_selectedStation"));
}
/*** Miscellaneous configuration ***/
AILib.prototype.configurationAppointGroupLeader = function()
{
if (this.ship.group && !this.ship.group.leader)
{
this.ship.group.leader = this.ship.group.ships[0];
for (var i = 0 ; i < this.ship.group.ships.length ; i++)
{
if (this.ship.group.ships[i].hasHyperspaceMotor)
{
// bias towards jump-capable ships
this.ship.group.leader = this.ship.group.ships[i];
break;
}
}
var leadrole = this.getParameter("oolite_leaderRole")
if (leadrole != null)
{
this.ship.group.leader.primaryRole = leadrole;
}
}
}
AILib.prototype.configurationEscortGroupLeader = function()
{
if (!this.ship.group || !this.ship.group.leader || this.ship.group.leader == this.ship)
{
return;
}
if (this.ship.group.leader.escortGroup && this.ship.group.leader.escortGroup.containsShip(this.ship))
{
return;
}
var escrole = this.getParameter("oolite_escortRole")
if (escrole != null)
{
var oldrole = this.ship.primaryRole;
this.ship.primaryRole = escrole;
var accepted = this.ship.offerToEscort(this.ship.group.leader);
if (!accepted)
{
this.ship.primaryRole = oldrole;
}
}
}
AILib.prototype.configurationForgetCargoDemand = function()
{
/* if (this.ship.group && this.ship.group.leader && this.ship.group.leader.AIScript.oolite_intership.cargodemanded)
{
delete this.ship.group.leader.AIScript.oolite_intership.cargodemanded;
} */ // not sure about this, maybe not needed
if (this.ship.AIScript.oolite_intership.cargodemanded)
{
delete this.ship.AIScript.oolite_intership.cargodemanded;
delete this.ship.AIScript.oolite_intership.cargodemandmet;
// and make the group lose the cargo count from the last demand
if (this.ship.group)
{
for (var i = 0 ; i < this.ship.group.ships.length ; i++)
{
var ship = this.ship.group.ships[i];
if (ship.AIScript && ship.AIScript.oolite_priorityai)
{
ship.AIScript.oolite_priorityai.setParameter("oolite_cargoDropped",0);
}
}
}
}
}
AILib.prototype.configurationLeaveEscortGroup = function()
{
if (this.ship.group && this.ship.group.leader && this.ship.group.leader != this.ship && this.ship.group.leader.escortGroup && this.ship.group.leader.escortGroup.containsShip(this.ship))
{
this.ship.group.leader.escortGroup.removeShip(this.ship);
if (this.ship.group)
{
this.ship.group.removeShip(this.ship);
this.ship.group = null;
}
}
}
/*** Station configuration ***/
AILib.prototype.configurationStationReduceAlertLevel = function()
{
if (this.ship.alertCondition > 1)
{
this.ship.alertCondition--;
}
}
AILib.prototype.configurationStationValidateTarget = function()
{
if (this.ship.target)
{
if(this.ship.position.distanceTo(this.ship.target) > this.ship.scannerRange)
{
// station behaviour does not generally validate target
this.ship.target = null;
}
}
}
/* ****************** Response definition functions ************** */
/* Standard state-machine responses. These set up a set of standard
* state machine responses where incoming events will cause reasonable
* default behaviour and often force a reconsideration of
* priorities. Many behaviours will need to supplement the standard
* responses with additional definitions. */
AILib.prototype.responsesAddStandard = function(handlers)
{
handlers.commsMessageReceived = function(message)
{
this.noteCommsHeard();
}
handlers.cascadeWeaponDetected = function(weapon)
{
this.ship.clearDefenseTargets();
this.ship.addDefenseTarget(weapon);
this.setParameter("oolite_cascadeDetected",weapon.position);
this.ship.target = weapon;
this.ship.performFlee();
this.reconsiderNow();
};
handlers.shipAttackedWithMissile = function(missile,whom)
{
if (!this.ship.hasHostileTarget && this.getParameter("oolite_flag_sendsDistressCalls"))
{
this.broadcastDistressMessage();
}
if (this.ship.equipmentStatus("EQ_ECM") == "EQUIPMENT_OK")
{
this.ship.fireECM();
this.ship.addDefenseTarget(missile);
this.ship.addDefenseTarget(whom);
// but don't reconsider immediately
}
else
{
this.ship.addDefenseTarget(missile);
this.ship.addDefenseTarget(whom);
var tmp = this.ship.target;
this.ship.target = whom;
this.ship.requestHelpFromGroup();
this.ship.target = tmp;
this.reconsiderNow();
}
};
handlers.shipBeingAttacked = function(whom)
{
if (whom.target != this.ship && whom != player.ship)
{
// was accidental
if (this.allied(whom,this.ship))
{
this.communicate("oolite_friendlyFire",this.entityCommsParams(whom),3);
// ignore it
return;
}
if (Math.random() > 0.1)
{
// usually ignore it anyway
return;
}
}
if (!this.ship.hasHostileTarget && this.getParameter("oolite_flag_sendsDistressCalls"))
{
this.broadcastDistressMessage();
}
if (this.ship.defenseTargets.indexOf(whom) < 0)
{
this.ship.addDefenseTarget(whom);
this.reconsiderNow();
}
else
{
// else we know about this attacker already
if (this.ship.energy * 4 < this.ship.maxEnergy)
{
// but at low energy still reconsider
this.reconsiderNow();
this.ship.requestHelpFromGroup();
}
}
if (this.ship.hasHostileTarget)
{
if (!this.isAggressive(this.ship.target))
{
// if our current target is running away, switch targets
this.ship.target = whom;
}
else if (this.ship.target.target != this.ship)
{
// if our current target isn't aiming at us
if (Math.random() < 0.2)
{
// occasionally switch
this.ship.target = whom;
}
}
}
if (this.ship.escortGroup != null)
{
this.ship.requestHelpFromGroup();
}
};
handlers.shipBeingAttackedUnsuccessfully = function(whom)
{
if (!this.ship.hasHostileTarget && this.getParameter("oolite_flag_sendsDistressCalls"))
{
this.broadcastDistressMessage();
}
if (this.ship.defenseTargets.indexOf(whom) < 0)
{
this.ship.addDefenseTarget(whom);
this.reconsiderNow();
}
};
handlers.shipTargetLost = function(target)
{
this.reconsiderNow();
};
// overridden for escorts
handlers.helpRequestReceived = function(ally, enemy)
{
this.ship.addDefenseTarget(enemy);
if (enemy.scanClass == "CLASS_MISSILE" && enemy.position.distanceTo(this.ship) < this.ship.scannerRange && this.ship.equipmentStatus("EQ_ECM") == "EQUIPMENT_OK")
{
this.ship.fireECM();
}
if (!this.ship.hasHostileTarget)
{
this.reconsiderNow();
return; // not in a combat mode
}
if (ally.energy / ally.maxEnergy < this.ship.energy / this.ship.maxEnergy)
{
// not in worse shape than ally
if (this.ship.target.target != ally && this.ship.target != ally.target)
{
// not already helping, go for it...
this.ship.target = enemy;
this.reconsiderNow();
}
}
}
handlers.cargoDumpedNearby = function(cargo,ship)
{
if (this.getParameter("oolite_flag_watchForCargo"))
{
var previously = this.getParameter("oolite_cargoDropped");
if (previously == null)
{
previously = 0;
}
previously++;
this.setParameter("oolite_cargoDropped",previously);
}
}
handlers.approachingPlanetSurface = function()
{
if (this.getParameter("oolite_flag_allowPlanetaryLanding"))
{
this.ship.desiredSpeed = this.ship.maxSpeed / 4;
this.ship.performLandOnPlanet();
this.ship.AIScriptWakeTime = 0; // cancel reconsiderations
this.applyHandlers({}); // cancel interruptions
this.communicate("oolite_landingOnPlanet",{},4);
}
else
{
this.reconsiderNow();
}
}
handlers.shipLaunchedFromStation = function(station)
{
// clear the station
this.ship.destination = station.position;
this.ship.desiredSpeed = this.cruiseSpeed();
this.ship.desiredRange = 15000;
this.ship.performFlyToRangeFromDestination();
}
handlers.shipWillEnterWormhole = function()
{
this.setParameter("oolite_witchspaceWormhole",null);
this.applyHandlers({});
}
handlers.shipExitedWormhole = function()
{
this.ship.AIScript.oolite_intership = {};
// this.reconsiderNow();
}
handlers.distressMessageReceived = function(aggressor, sender)
{
if (this.getParameter("oolite_flag_listenForDistressCall") != true)
{
return;
}
this.setParameter("oolite_distressAggressor",aggressor);
this.setParameter("oolite_distressSender",sender);
this.setParameter("oolite_distressTimestamp",clock.adjustedSeconds);
this.reconsiderNow();
}
handlers.offenceCommittedNearby = function(attacker, victim)
{
if (this.getParameter("oolite_flag_markOffenders"))
{
attacker.setBounty(attacker.bounty | 7,"seen by police");
this.ship.addDefenseTarget(attacker);
this.reconsiderNow();
}
}
handlers.playerWillEnterWitchspace = function()
{
var wormhole = this.getParameter("oolite_witchspaceWormhole");
if (wormhole != null && wormhole.isWormhole)
{
this.ship.enterWormhole(wormhole);
}
}
handlers.wormholeSuggested = function(hole)
{
this.setParameter("oolite_witchspaceWormhole",hole);
this.reconsiderNow();
}
// TODO: more event handlers
}
/* Additional handlers for use while docking */
AILib.prototype.responsesAddDocking = function(handlers)
{
handlers.stationWithdrewDockingClearance = function()
{
this.setParameter("oolite_dockingStation",null);
this.reconsiderNow();
};
handlers.shipAchievedDesiredRange = function()
{
var message = this.ship.dockingInstructions.ai_message;
if (message == "APPROACH" || message == "BACK_OFF" || message == "APPROACH_COORDINATES")
{
this.reconsiderNow();
}
};
}
/* Override of standard handlers for use while escorting */
AILib.prototype.responsesAddEscort = function(handlers)
{
handlers.helpRequestReceived = function(ally, enemy)
{
// always help the leader
if (ally == this.ship.group.leader)
{
if (!this.ship.target || this.ship.target.target != ally)
{
this.ship.target = enemy;
this.reconsiderNow();
return;
}
}
this.ship.addDefenseTarget(enemy);
if (!this.ship.hasHostileTarget)
{
this.reconsiderNow();
return; // not in a combat mode
}
if (ally.energy / ally.maxEnergy < this.ship.energy / this.ship.maxEnergy)
{
// not in worse shape than ally
if (this.ship.target.target != ally && this.ship.target != ally.target)
{
// not already helping, go for it...
this.ship.target = enemy;
this.reconsiderNow();
}
}
}
handlers.escortDock = function()
{
this.reconsiderNow();
}
}
/* Additional handlers for scooping */
AILib.prototype.responsesAddScooping = function(handlers)
{
handlers.shipAchievedDesiredRange = function()
{
this.reconsiderNow();
}
handlers.shipScoopedFuel = function()
{
if (this.ship.fuel == 7)
{
this.reconsiderNow();
}
}
}
// shorter list than before
AILib.prototype.responsesAddStation = function(handlers)
{
handlers.commsMessageReceived = function(message)
{
this.noteCommsHeard();
}
handlers.cascadeWeaponDetected = function(weapon)
{
this.ship.alertCondition = 3;
this.reconsiderNow();
};
handlers.shipAttackedWithMissile = function(missile,whom)
{
this.ship.alertCondition = 3;
if (this.ship.equipmentStatus("EQ_ECM") == "EQUIPMENT_OK")
{
this.ship.fireECM();
this.ship.addDefenseTarget(missile);
this.ship.addDefenseTarget(whom);
// but don't reconsider immediately
}
else
{
this.ship.addDefenseTarget(missile);
this.ship.addDefenseTarget(whom);
var tmp = this.ship.target;
this.ship.target = whom;
this.ship.requestHelpFromGroup();
this.ship.target = tmp;
this.reconsiderNow();
}
};
handlers.shipBeingAttacked = function(whom)
{
if (!whom)
{
this.reconsiderNow();
return;
}
if (whom.target != this.ship && whom != player.ship)
{
// was accidental
if (this.allied(whom,this.ship))
{
this.communicate("oolite_friendlyFire",this.entityCommsParams(whom),4);
// ignore it
return;
}
if (Math.random() > 0.1)
{
// usually ignore it anyway
return;
}
}
this.ship.alertCondition = 3;
if (this.ship.defenseTargets.indexOf(whom) < 0)
{
this.ship.addDefenseTarget(whom);
this.reconsiderNow();
}
else
{
// else we know about this attacker already
if (this.ship.energy * 4 < this.ship.maxEnergy)
{
// but at low energy still reconsider
this.reconsiderNow();
this.ship.requestHelpFromGroup();
}
}
if (this.ship.hasHostileTarget)
{
if (!this.isAggressive(this.ship.target))
{
// if our current target is running away, switch targets
this.ship.target = whom;
}
else if (this.ship.target.target != this.ship)
{
// if our current target isn't aiming at us
if (Math.random() < 0.2)
{
// occasionally switch
this.ship.target = whom;
}
}
}
};
handlers.shipTargetLost = function(target)
{
this.reconsiderNow();
};
handlers.helpRequestReceived = function(ally, enemy)
{
this.ship.addDefenseTarget(enemy);
if (!this.ship.alertCondition == 3)
{
this.ship.target = enemy;
this.reconsiderNow();
return; // not in a combat mode
}
this.ship.target = enemy;
}
handlers.distressMessageReceived = function(aggressor, sender)
{
if (this.getParameter("oolite_flag_listenForDistressCall") != true)
{
return;
}
this.setParameter("oolite_distressAggressor",aggressor);
this.setParameter("oolite_distressSender",sender);
this.setParameter("oolite_distressTimestamp",clock.adjustedSeconds);
this.reconsiderNow();
}
handlers.offenceCommittedNearby = function(attacker, victim)
{
if (this.getParameter("oolite_flag_markOffenders"))
{
attacker.setBounty(attacker.bounty | 7,"seen by police");
this.ship.addDefenseTarget(attacker);
if (this.ship.alertCondition < 3)
{
this.ship.alertCondition = 3;
this.ship.target = attacker;
}
this.reconsiderNow();
}
}
}
AILib.prototype.responsesAddMissile = function(handlers) {
handlers.commsMessageReceived = function(message)
{
this.noteCommsHeard();
}
handlers.shipHitByECM = function()
{
if (this.ship.scriptInfo.oolite_missile_ecmResponse)
{
var fn = this.ship.scriptInfo.oolite_missile_ecmResponse;
if (this.ship.AIScript[fn])
{
this.ship.AIScript[fn]();
this.reconsiderNow();
return;
}
if (this.ship.script[fn])
{
this.ship.script[fn]();
this.reconsiderNow();
return;
}
}
/* This section for the hardheads should be an ECM
* response function, and that is used in the default
* shipdata.plist, but for compatibility with older OXPs
* it's also hardcoded here for now.
*
* OXPs wanting to overrule this for hardheads can set a
* response function to do so.
*/
if (this.ship.primaryRole == "EQ_HARDENED_MISSILE")
{
if (Math.random() < 0.1) //10% chance per pulse
{
if (Math.random() < 0.5)
{
// 50% chance responds by detonation
this.ship.AIScript.shipAchievedDesiredRange();
return;
}
// otherwise explode as normal below
}
else // 90% chance unaffected
{
return;
}
}
this.ship.explode();
}
handlers.shipTargetCloaked = function()
{
this.setParameter("oolite_interceptCoordinates",this.ship.target.position);
this.setParameter("oolite_interceptTarget",this.ship.target);
// stops performIntercept sending AchievedDesiredRange
this.ship.performIdle();
}
handlers.shipTargetLost = function()
{
this.reconsiderNow();
}
handlers.shipAchievedDesiredRange = function()
{
if (this.ship.scriptInfo.oolite_missile_detonation)
{
var fn = this.ship.scriptInfo.oolite_missile_detonation;
if (this.ship.AIScript[fn])
{
this.ship.AIScript[fn]();
this.reconsiderNow();
return;
}
if (this.ship.script[fn])
{
this.ship.script[fn]();
this.reconsiderNow();
return;
}
}
/* Defaults to standard missile settings, in case they're
* not specified in scriptInfo */
var blastpower = 170;
var blastradius = 32.5;
var blastshaping = 0.25;
if (this.ship.scriptInfo.oolite_missile_blastPower)
{
blastpower = this.ship.scriptInfo.oolite_missile_blastPower;
}
if (this.ship.scriptInfo.oolite_missile_blastRadius)
{
blastradius = this.ship.scriptInfo.oolite_missile_blastRadius;
}
if (this.ship.scriptInfo.oolite_missile_blastShaping)
{
blastshaping = this.ship.scriptInfo.oolite_missile_blastShaping;
}
this.ship.dealEnergyDamage(blastpower,blastradius,blastshaping);
this.ship.explode();
}
}
/* ******************* Waypoint generators *********************** */
/* Waypoint generators. When these are called, they should set up
* the next waypoint for the ship. Ideally ships should either
* reach that waypoint or formally give up on it before asking for
* the next one, but the generator shouldn't assume that unless
* it's one written specifically for a particular AI. */
AILib.prototype.waypointsSpacelanePatrol = function()
{
var p = this.ship.position;
var choice = "";
if (p.magnitude() < 10000)
{
// near witchpoint
if (Math.random() < 0.9)
{
// mostly return to planet
choice = "PLANET";
}
else
{
choice = "SUN";
}
}
else if (p.distanceTo(system.mainPlanet) < system.mainPlanet.radius * 2)
{
// near planet
if (Math.random() < 0.75)
{
// mostly go to witchpoint
choice = "WITCHPOINT";
}
else
{
choice = "SUN";
}
}
else if (p.distanceTo(system.sun) < system.sun.radius * 3)
{
// near sun
if (Math.random() < 0.9)
{
// mostly return to planet
choice = "PLANET";
}
else
{
choice = "SUN";
}
}
else if (p.z < system.mainPlanet.position.z && ((p.x * p.x) + (p.y * p.y)) < this.ship.scannerRange * this.ship.scannerRange * 4)
{
// on lane 1
if (Math.random() < 0.5)
{
choice = "PLANET";
}
else
{
choice = "WITCHPOINT";
}
}
else if (p.subtract(system.mainPlanet).dot(p.subtract(system.sun)) < -0.9)
{
// on lane 2
if (Math.random() < 0.5)
{
choice = "PLANET";
}
else
{
choice = "SUN";
}
}
else if (p.dot(system.sun.position) > 0.9)
{
// on lane 3
if (Math.random() < 0.5)
{
choice = "WITCHPOINT";
}
else
{
choice = "SUN";
}
}
else
{
// we're not on any lane. Return to the planet
choice = "PLANET";
}
// having chosen, now set up the next stop on the patrol
switch (choice) {
case "WITCHPOINT":
this.setParameter("oolite_waypoint",new Vector3D(0,0,0));
this.setParameter("oolite_waypointRange",7500);
break;
case "PLANET":
this.setParameter("oolite_waypoint",system.mainPlanet.position);
this.setParameter("oolite_waypointRange",system.mainPlanet.radius*2);
break;
case "SUN":
this.setParameter("oolite_waypoint",system.sun.position);
this.setParameter("oolite_waypointRange",system.sun.radius*2.5);
break;
}
}
AILib.prototype.waypointsStationPatrol = function()
{
var station = null;
if (this.ship.group)
{
station = this.ship.group.leader;
}
if (!station)
{
station = system.mainStation;
if (!station)
{
this.setParameter("oolite_waypoint",new Vector3D(0,0,0));
this.setParameter("oolite_waypointRange",7500);
return;
}
}
var z = station.vectorForward;
var tmp = new Vector3D(0,1,0);
if (system.sun)
{
tmp = z.cross(system.sun.position.direction());
}
var x = z.cross(tmp);
var y = z.cross(x);
// x and y now consistent vectors relative to a rotating station
var waypoints = [
station.position.add(x.multiply(25000)),
station.position.add(y.multiply(25000)),
station.position.add(x.multiply(-25000)),
station.position.add(y.multiply(-25000))
];
var waypoint = waypoints[0];
for (var i=0;i<=3;i++)
{
if (this.ship.position.distanceTo(waypoints[i]) < 500)
{
waypoint = waypoints[(i+1)%4];
break;
}
}
this.setParameter("oolite_waypoint",waypoint);
this.setParameter("oolite_waypointRange",100);
}
/* ********** Communications data ****************/
/* Warning: OXPs should only interact with this through the provided
* API functions. The internals of data storage may be changed at any
* time. This data is global. */
this.startUp = function()
{
// initial definition is just essential communications for now
this.$commsSettings = {
generic: {
generic: {
oolite_makePirateDemand: "[oolite-comms-makePirateDemand]",
oolite_acceptPirateDemand: "[oolite-comms-acceptPirateDemand]",
oolite_makeDistressCall: "[oolite-comms-makeDistressCall]"
}
},
police: {
generic: {
oolite_markForFines: "[oolite-comms-markForFines]",
oolite_distressResponseAggressor: "[oolite-comms-distressResponseAggressor]",
oolite_offenceDetected: "[oolite-comms-offenceDetected]",
}
}
};
/* These are temporary for testing. Remove before release... */
this.$commsSettings.generic.generic.oolite_continuingAttack = "I've got the [oolite_entityClass]";
this.$commsSettings.generic.generic.oolite_beginningAttack = "Die, [oolite_entityClass]!";
}
/* Set a communication for the specified role, personality and comms
* key. "generic" is used as a fallback role and personality. */
this._setCommunication = function(role, personality, key, value)
{
if (!this.$commsSettings[role])
{
this.$commsSettings[role] = {};
}
if (!this.$commsSettings[role][personality])
{
this.$commsSettings[role][personality] = {};
}
this.$commsSettings[role][personality][key] = value;
}
/* Search through communications from most specific to least specific.
* role+personality
* "generic"+personality
* role+"generic"
* "generic"+"generic"
* A return value of "" means no communication is set.
*/
this._getCommunication = function(role, personality, key)
{
if (this.$commsSettings[role] && this.$commsSettings[role][personality] && this.$commsSettings[role][personality][key] && this.$commsSettings[role][personality][key] != "")
{
return this.$commsSettings[role][personality][key];
}
if (this.$commsSettings["generic"] && this.$commsSettings["generic"][personality] && this.$commsSettings["generic"][personality][key] && this.$commsSettings["generic"][personality][key] != "")
{
return this.$commsSettings["generic"][personality][key];
}
if (this.$commsSettings[role] && this.$commsSettings[role]["generic"] && this.$commsSettings[role]["generic"][key] && this.$commsSettings[role]["generic"][key] != "")
{
return this.$commsSettings[role]["generic"][key];
}
if (this.$commsSettings["generic"] && this.$commsSettings["generic"]["generic"] && this.$commsSettings["generic"]["generic"][key] && this.$commsSettings["generic"]["generic"][key] != "")
{
return this.$commsSettings["generic"]["generic"][key];
}
return "";
}