| Scripts/police_call_AI.js |
"use strict";
this.name = "Police SDD Call AI";
this.descripton = "This is the amended default Police js script, with the new highest priority of rendezvous with player inserted at the top (when script is explicity changed).";
this.aiStarted = function() {
var ai = new worldScripts["oolite-libPriorityAI"].PriorityAIController(this.ship);
ai.setParameter("oolite_flag_listenForDistressCall",true);
ai.setParameter("oolite_flag_markOffenders",true);
ai.setParameter("oolite_flag_fightsNearHostileStations",true);
ai.setParameter("oolite_flag_selfDestructAbandonedShip",true);
if (this.ship.primaryRole == "police-station-patrol")
{
ai.setParameter("oolite_leaderRole","police-station-patrol");
ai.setWaypointGenerator(ai.waypointsStationPatrol);
ai.setParameter("oolite_flag_patrolStation",true);
}
else if (this.ship.primaryRole == "police-witchpoint-patrol")
{
ai.setParameter("oolite_leaderRole","police-witchpoint-patrol");
ai.setWaypointGenerator(ai.waypointsWitchpointPatrol);
}
else
{
// chasing a bandit well off the spacelane is almost as good
// as destroying them
ai.setParameter("oolite_leaderRole","police");
ai.setWaypointGenerator(ai.waypointsSpacelanePatrol);
}
ai.setParameter("oolite_escortRole","wingman");
ai.setParameter("oolite_friendlyRoles",["oolite-trader","oolite-bounty-hunter","oolite-scavenger","oolite-shuttle"]);
ai.setParameter("oolite_personalityMatchesLeader",0.5);
ai.setCommunicationsRole("police");
// Define the priority list
var priorities = [
// NEW HIGHEST PRIORITY: Rendezvous with player
{
condition: function() {
// Return true if NOT close to player, triggering the rendezvous
return !ai.isInRangeOfPlayer(5000); // Adjust range as needed
},
configuration: ai.configurationSetDestinationToPlayer,
behaviour: ai.behaviourApproachDestination,
reconsider: 10
},
// ... (rest of the original police priorities follow here)
/* Fight */
{
preconfiguration: ai.configurationLightsOn,
condition: ai.conditionLosingCombat,
behaviour: ai.behaviourFleeCombat,
reconsider: 5
},
{
condition: ai.conditionInCombat,
configuration: ai.configurationAcquireCombatTarget,
behaviour: ai.behaviourDestroyCurrentTarget,
reconsider: 5
},
/* Check for distress calls */
{
condition: ai.conditionHasReceivedDistressCall,
behaviour: ai.behaviourRespondToDistressCall,
reconsider: 20
},
/* Check for offenders */
{
preconfiguration: ai.configurationCheckScanner,
condition: ai.conditionScannerContainsFugitive,
configuration: ai.configurationAcquireScannedTarget,
behaviour: ai.behaviourCommenceAttackOnCurrentTarget,
reconsider: 1
},
{
condition: ai.conditionScannerContainsSeriousOffender,
configuration: ai.configurationAcquireScannedTarget,
behaviour: ai.behaviourCommenceAttackOnCurrentTarget,
reconsider: 1
},
{
preconfiguration: ai.configurationLightsOff,
condition: ai.conditionScannerContainsFineableOffender,
configuration: ai.configurationAcquireScannedTarget,
behaviour: ai.behaviourFineCurrentTarget,
reconsider: 10
},
/* What about escape pods? */
{
condition: ai.conditionScannerContainsEscapePods,
configuration: ai.configurationAcquireScannedTarget,
behaviour: ai.behaviourCollectSalvage,
reconsider: 20
},
/* Regroup if necessary */
{
preconfiguration: ai.configurationAppointGroupLeader,
condition: ai.conditionGroupIsSeparated,
configuration: ai.configurationSetDestinationToGroupLeader,
behaviour: ai.behaviourApproachDestination,
reconsider: 15
},
{
condition: ai.conditionGroupLeaderIsStation,
/* Group leader is the station: a short-range patrol or
* defense ship */
truebranch: [
{
condition: ai.conditionHasWaypoint,
configuration: ai.configurationSetDestinationToWaypoint,
behaviour: ai.behaviourApproachDestination,
reconsider: 30
},
{
condition: ai.conditionPatrolIsOver,
truebranch: ai.templateReturnToBase()
},
/* No patrol route set up. Make one */
{
configuration: ai.configurationSetWaypoint,
behaviour: ai.behaviourApproachDestination,
reconsider: 30
}
],
/* Group leader is not station: i.e. this is a long-range
* patrol unit */
falsebranch: [
{
/* The group leader leads the patrol */
condition: ai.conditionIsGroupLeader,
truebranch: [
{
/* Sometimes follow, sometimes not */
label: "Consider following suspicious?",
condition: ai.conditionCoinFlip,
truebranch: [
/* Suspicious characters */
{
condition: ai.conditionScannerContainsSuspiciousShip,
configuration: ai.configurationSetDestinationToScannedTarget,
behaviour: ai.behaviourApproachDestination,
reconsider: 20
}
]
},
/* Nothing interesting here. Patrol for a bit */
{
condition: ai.conditionHasWaypoint,
configuration: ai.configurationSetDestinationToWaypoint,
behaviour: ai.behaviourApproachDestination,
reconsider: 30
},
{
condition: ai.conditionPatrolIsOver,
truebranch: [
{
condition: ai.conditionMainPlanetNearby,
truebranch: ai.templateReturnToBase()
}
]
},
/* No patrol route set up. Make one */
{
configuration: ai.configurationSetWaypoint,
behaviour: ai.behaviourApproachDestination,
reconsider: 30
}
],
/* Other ships in the group will set themselves up
* as escorts if possible, or looser followers if
* not */
falsebranch: [
{
preconfiguration: ai.configurationEscortGroupLeader,
condition: ai.conditionIsEscorting,
behaviour: ai.behaviourEscortMothership,
reconsider: 30
},
/* if we can't set up as an escort */
{
behaviour: ai.behaviourFollowGroupLeader,
reconsider: 15
}
]
}
]
}
]);
// apply the amended priority array
ai.setPriorities(priorities);
}
|
| Scripts/sdd_script.js |
"use strict";
this.name = "Defence Rider Drones";
this.author = "Reval";
this.license = "CC-BY-NC-SA 4.0";
this.version = "1.9";
this.description = "FE Shipyards, under Galcop sanction, undertakes to furnish you with Mambita Ship Defence Rider Drones, known as SDDs, to be mounted at dedicated hardpoints on your ship's hull. An SDD's single purpose is to eliminate hostiles, either automatically or on your orders. Charges and fees apply.";
/*
Version 1.9
Suppressed broadcast of spawned High Offenders (use F5->[f] to list, [t] to target).
Low offender scanner colour switched to 'peach' (improved contrast w/ other types).
Code cleanup.
Version 1.8
High Offenders (often 'Fugitives') coloured magenta on scan.
Low Offenders coloured orange on scan (v.1.9 peach).
Added shipDied w.s. event handler for final cleanup.
Nothing displayed on F5F5 if no data, ie. no kills by Riders or Police.
Sundry additional checks implemented.
Tightened some existing checks and conditions.
Version 1.7
F5->[t] Cycle H.O. targets: selection will be tracked in realtime on GETter HUD.
Auto-targeting automatically cancelled when pilot cycles using [t].
Prefix listed High Offenders with their index number a la PT-BVRM.
'Licensee:' line now heads the F5F5 SDD-Net display. Shows commander and ship name.
Version 1.6
OPTION: SDD Rider made CLASS_POLICE if masslock is desired. It will assume game-coded Police - coupled with scripted - behaviour. Careful not to hit one!
High Offenders automatically re-sorted by 'nearest first' in all conceivable cases.
Covered case of target being a victim of the NPB Neutralizer or in a disabled state.
Invalid or disabled victims automatically removed from H.O. list.
Adding a new High Offender to the list (F5F5->[f]) automatically re-sorts it.
Docking with Station clears the current Top Target from GETter HUD display.
Version 1.5
Top High Offender automatically re-tracked in GETter HUD v1.6, on acquisition.
High Offenders instantly updated on elimination of a Top target (by SDD or Mother).
Version 1.4
+10 cr for missile elimination (by Police SDD or SDD Rider)
Incremental 'drop tax' for SDD deployments introduced.
F5F5 Mission screen 'SDD-Network' shows cumulative SDD Rider & Police SDD statistics.
SDD Rider & Police SDD gains/losses tracked in-game and via missionVariables.
Various optimizations and refactorings in all three scripts.
Version 1.3
SDD Mambita's default forward weapon changed to Military Laser: SDD kills are now quicker and from greater distances.
Fully implemented Galcop License-granting conditions.
Version 1.2
SDD Licensees get random percentage of the bounty on Police SDD kills.
Police Mambitas report kills on system-wide SDD Net.
New Police SDDs establish link with player ship's SDD network.
Idle drone(s) vectored to attack when Mother targets an Offender.
SDD Mambita drones now know who spawned them (player or system).
Fix: tightened condition inside the frame callback for close attacks.
Code re-arrangement for shipSpawned() events between world- and ship-scripts.
Tidied and deleted sundry debug log entries.
Version 1.1
Subspace (console message) comms from SDDs out of scan-range.
Top High Offender made SDD target on close range approach by Mother.
Highest offender realtime viewscreen tracking integrated with GETter HUD v.1.6
Highest bounty Offenders in system updated and listed on keypress [f] from F5 screen.
Advisory broadcast to SDDs when Mother targets an Offender.
Galcop prohibition on drone deployment from an offender ship.
SDD Mambita entry and description added to the Ship Library.
Equipment text changed.
*/
/* To Do
Drone deployment given a very small chance of FAILURE, resulting in WARNING and downgrading. (see if (!drone) in deployDrone).
[n] keypress nulls (removes) current HUD-tracked target (makes it "").
shipDied and shipRemoved for Police SDD script (analogous to that in SDD Rider script)
[?] Keypress on F5 screen provides information on Police SDDs in system
Mother can call any unengaged in-system Police SDDs to aid her (via a replacement policeAI.js script, with rendezvous parameters appended as high priority).
Functions for selecting and calling Police SDDs (nearest first etc)
*/
// OPTION: are we logging ALL SDD and Police activities?
this.$log = true;
// OPTION: Do deployed Riders masslock other vessels? ('true' is 'yes')
this.$sddMassLock = false; // default 'no' (they are MINES)
this.shipSpawned = function(ship) {
// check whose drone by scanner colour
if ((ship.shipClassName === "SDD Mambita") && (
(ship.scannerDisplayColor1[0] === 0.5) &&
(ship.scannerDisplayColor1[1] === 0.0) &&
(ship.scannerDisplayColor1[2] === 0.5))) {
// This is a populator-spawned NPC Police Mambita
// give the populator-spawned drone Police AI
ship.setAI("oolite-policeAI.js");
ship.setScript("sdd-police-fallback.js");
} else {
// This is a system-spawned NPC ship,
// processed here only if an offender.
if (this._sddIsOffender(ship)) {
var b = ship.bounty;
var d = this._sddDistanceKm(ship);
var c = this._sddCompassDirection(ship);
var n = ship.shipClassName;
if (this.$log) this._log("Spawned: "+n+" ("+b+") "+d+" km ");
// colour low offenders 'soft coral peach' on scanner
if (b<40) {
ship.scannerDisplayColor1 = [ 1.0, 0.65, 0.55 ];
return;
}
// add high offenders to recallable list
if (b>=40) {
// colour High Offenders magenta on scanner
ship.scannerDisplayColor1 = "magentaColor";
// only add NEW potential targets
var oTarget = this.$sddTargetShip;
var newT = (oTarget !== ship);
// add the new high offender...
this._sddAddHighOffender(ship);
// clean up H.O. list
this.$sddHighOffenders = this.$sddHighOffenders.filter(function(ship) {
return ship && ship.isValid;
});
var ps = player.ship;
// Re-sort by distance to player (nearest first)
this.$sddHighOffenders.sort(function(a, b) {
return a.position.distanceTo(ps) - b.position.distanceTo(ps);
});
// player.commsMessage("High Offender "+n+" at "+d+ " Lk "+c,9);
// Assign new top offender as target
if (this.$sddHighOffenders.length > 0) {
var hf = this.$sddHighOffenders[0];
// change primary target if new
if (hf.isValid && newT) {
this.$sddTargetShip = hf;
// update HUD
this._updateTargetInfo();
}
}
}
}
}};
this.shipWillLaunchFromStation = function() {
var ps = player.ship;
this.$sddHasEQ = (ps.equipmentStatus ("EQ_SDD_MAMBITA") === "EQUIPMENT_OK");
this._sddReset();
// create callback frame for detecting Torus
this.$fcb = addFrameCallback(function(delta) {
// Check Torus status every frame
if (ps.torusEngaged && !this.$torusActive) {
// Torus drive engaged
this._sddReset();
}
this.$torusActive = ps.torusEngaged;
// idle SDDs target highest offender if in range
if ((this.$sddHens>0) && (this.$sddHighOffenders.length>0)) {
var hf = this.$sddHighOffenders[0];
if ((hf.isValid) && (this._sddDistanceKm(hf)<=10.0)) {
this._sddIdleAttack(hf);
}
}
}.bind(this));
}
// establish drone complement by ship-type
this.shipLaunchedFromStation = function(station) {
var pc = player.consoleMessage;
if (this.$sddHasEQ) {
pc("Initializing comm-link to Riders...", 9);
var d = this._sddMaxDrones();
this.$sddMaxHens = (d > 0) ? d : 7;
pc(this.$sddMaxHens+" SDD Rider drones listening.", 9);
pc("Re-toggle WEAPONS to launch a rider.",9);
} else
pc("Mount SDD Rider drones in shipyard.", 9);
// start realtime Target info update
// Use a Timer to update offender ship's distance and direction
// ONLY IF GETter HUD is loaded.
if (worldScripts["GETter HUD"]) {
this._updateTimer = new Timer(this, this._updateTargetInfo.bind(this), 0.5, 0.5);
this._updateTargetInfo();
}
};
this.shipWillDockWithStation = function(station) {
// Remove callback for Torus detection
if (this.$fcb) {
removeFrameCallback(this.$fcb);
this.$fcb = null;
}
// Stop the HUD target info timer
if (this._updateTimer) {
player.ship.setCustomHUDDial("sddTargetInfo", "");
this._updateTimer.stop();
this._updateTimer = null;
}
// clear and reset H.O. list
this.$sddHighOffenders = [];
this.$sddTargetShip = null;
// update F5F5 Mission data
this._updateSDDNetworkDisplay();
}
this.playerStartedJumpCountdown = function(type, seconds) {
// clean slate on H-jump
this._sddReset();
// clear and reset H.O. list
this.$sddHighOffenders = [];
this.$sddTargetShip = null;
}
this.shipEnteredStationAegis = function(station) {
this._sddAccounting();
}
// retrieve drones on receipt of docking clearance
this.playerRequestedDockingClearance = function() {
// unset tracker target on docking
this.$sddTargetShip = null;
this._sddReset();
}
this.playerBoughtEquipment = function(equipment, paid) {
var pc = player.consoleMessage;
if (equipment=="EQ_SDD_MAMBITA") {
if (player.legalStatus=="Clean")
pc("Licence issued: SDD Riders mounted.",9);
else pc("Licence denied. No SDD Riders mounted.",9);
} else
// Licence termination, removal and refund
if (equipment == "EQ_SDD_MAMBITA_REM") {
player.ship.removeEquipment("EQ_SDD_MAMBITA");
player.ship.removeEquipment("EQ_SDD_MAMBITA_REM");
pc("SDD hardpoints have been removed. Half refund.",9);
player.credits += 500;
}
}
this.shipTargetAcquired = function(target) {
if (target.bounty > 10) {
var pc = player.commsMessage;
// The targeted vessel is an offender
pc("Alert SDD: Mother's target is "+target.name+"(+"+target.bounty+")", 9);
this._sddIdleAttack(target);
}
};
// 'put affairs in order'
this.shipDied = function(whom, why) {
// Remove callback for Torus detection
if (this.$fcb) {
removeFrameCallback(this.$fcb);
this.$fcb = null;
}
// Stop the HUD target info timer
if (this._updateTimer) {
player.ship.setCustomHUDDial("sddTargetInfo", "");
this._updateTimer.stop();
this._updateTimer = null;
}
// clear and reset H.O. list
this.$sddHighOffenders = [];
this.$sddTargetShip = null;
}
// mother is under attack: assign nearest drone to defend
this.shipBeingAttacked = function(whom) {
if ((this.$sddHasEQ) && (this.$sddShips.length > 0)) {
this.$sddAttacker = whom;
// Clean up invalid ships
this.$sddShips = this.$sddShips.filter(function(ship) { return ship && ship.isValid; });
// Now assign to nearest valid ship in the same array
let nearestIndex = this._getNearestShipIndex();
if (nearestIndex !== -1) {
let assignedShip = this.$sddShips[nearestIndex];
assignedShip.target = whom;
assignedShip.performAttack();
}
}
}
this.shipAttackedWithMissile = function(missile, whom) {
if ((this.$sddHasEQ) && (this.$sddShips.length>1)) {
this.$sddAttacker = whom;
// assign idle drone to attack
this._sddIdleAttack(whom);
}
}
// re-toggling WEAPONS ON deploys a drone
this.weaponsSystemsToggled = function(state) {
if (player.legalStatus=="Clean") {
if (this.$sddHasEQ)
if (state) this._sddDeployDrone(1);
} else if (state)
player.consoleMessage("GALCOP WARNING: SDD inactive due to ship's legal status.",9);
}
this.alertConditionChanged = function(newCondition, oldCondition) {
if (this.$sddHasEQ) {
var psa = newCondition;
// condition Red-alert: advise to deploy drone...
if (psa==3) {
this._sddDistress(this.$sddAttackMsg);
// assign unengaged SDD to target
// (detecting which attacker within a frame callback)
if (!this.$afcb) {
this.$afcb = addFrameCallback(function(delta) {
var attackers = system.filteredEntities(this, function(e) {
return e.isShip && e.target === player.ship;
}, player.ship);
if (attackers.length > 0) {
for (var i = 0; i < attackers.length; i++) {
if (attackers[i].bounty > 0) {
this._sddIdleAttack(attackers[i]);
break;
}
}
if (this.$afcb) {
removeFrameCallback(this.$afcb);
delete this.$afcb;
}
}
}.bind(this));
}
}
}
}
// instruct all drones to attack <target>
this._sddGroupAttack = function(target) {
// Check if the target is a potential hostile
if (target && (target.scanClass!="CLASS_POLICE") && (target.scanClass!="CLASS_PLAYER") && (target.scanClass!="CLASS_MINE")) {
for (var i = 0; i < this.$sddShips.length; i++) {
var drone = this.$sddShips[i];
drone.target = target;
drone.performAttack();
}
}
}
// instruct an 'idle' drone to attack <target>
this._sddIdleAttack = function(target) {
// Check if the target is a potential hostile
if (target && (target.scanClass!="CLASS_POLICE") && (target.scanClass!="CLASS_PLAYER") && (target.scanClass!="CLASS_MINE")) {
for (var i = 0; i < this.$sddShips.length; i++) {
var drone = this.$sddShips[i];
// if drone is idle, ie. following Mother
if (drone && drone.target === player.ship) {
drone.target = target;
drone.performAttack();
break; // Assign only one drone
}
}
}
}
// deploy a drone or drones
this._sddDeployDrone = function(num) {
var pc = player.consoleMessage;
var ps = player.ship;
if ((this.$sddHasEQ) && (this.$sddHens<this.$sddMaxHens)) {
// spawn a new drone or drones
var drone = system.addShips("[sdd-mambita]", num, ps.position);
// Belt'n'braces check for drone presence and validity
if (!drone) {
var psh = player.ship, pcm = ps.consoleMessage;
var ocolor = psh.messageGuiTextColor;
psh.messageGuiTextColor = "redColor";
pcm("SDD DEPLOYMENT FAILURE:",9);
pcm("Report to CSDDA soonest.",9);
pcm("Until such time, your status: Downgraded.",9);
psh.messageGuiTextColor = ocolor;
// make the possibly defaulting Licensee a small offender
psh.bounty = 5;
if (this.$log) this._log("SDD DEPLOYMENT FAILURE.");
return;
}
// put masslock/no masslock option into effect
for (var d = 0; d < drone.length; d++)
if (this.$sddMassLock) drone[d].scanClass = "CLASS_POLICE";
// set custom property on player-spawned drone(s)
for (var d = 0; d < drone.length; d++)
drone[d].$spawnedByMother = true;
// set scanner lollipop colour for player-spawned drones
for (var d = 0; d < drone.length; d++) {
drone[d].scannerDisplayColor1 = "blueColor";
drone[d].scannerDisplayColor2 = "redColor";
}
// prevent collisions between new drone, player, and other drones
for (var d = 0; d < drone.length; d++)
this._collisionAvoidance(drone[d]);
// Create a group with the first drone as leader
if (!this.$sddGroup) {
this.$sddGroup = new ShipGroup();
this.$sddGroup.leader = drone[0];
}
// Add new drone(s) to the group
for (var d = 0; d < drone.length; d++) {
this.$sddGroup.addShip(drone[d]);
}
// concatenate new drone(s) to existing drone-array
this.$sddShips = this.$sddShips.concat(drone);
// final leader validity check
// using the group's ships array
if (!this.$sddGroup.leader || !this.$sddGroup.leader.isValid) {
// Find a new valid leader from the group
for (var s = 0; s < this.$sddGroup.ships.length; s++) {
if (this.$sddGroup.ships[s].isValid) {
this.$sddGroup.leader = this.$sddGroup.ships[s];
break;
}
}
}
// update drone count
this.$sddHens += num;
// apply per-deployment maintenance tax w/ level-increment
this.$sddDeployTax += this.$sddTaxLevel; // +0.01 cr nominal
// keep a record (total taxes)
this.$sddRTax += this.$sddDeployTax;
// refresh the F5F5 mission screen as the debit happens
this._updateSDDNetworkDisplay();
// debit account for the tax
player.credits -= this.$sddDeployTax;
pc("Deployed SDD rider #"+this.$sddHens+".", 9);
// no more drones to deploy
} else {
if (this.$sddHasEQ) pc("All "+this.$sddHens+" SDD Riders are deployed.",9);
}
}
this.shipAttackedOther = function(other) {
// Mother fires on any ship and expects all drones to engage it
if (other && other.isValid)
this._sddGroupAttack(other);
// only process this victim once
if (other.$sddAttacked) return;
else other.$sddAttacked = true;
// if firing on a neutralized or disabled ship
if (((other.$neutralized) || (other.isDisabled))
&& (other == this.$sddTargetShip)) {
this.$sddTargetShip = null; // reset
// clean up H.O. list
this.$sddHighOffenders = this.$sddHighOffenders.filter(function(ship) {
return ship && ship.isValid;
});
var ps = player.ship;
// Re-sort by distance to player (nearest first)
this.$sddHighOffenders.sort(function(a, b) {
return a.position.distanceTo(ps) - b.position.distanceTo(ps);
});
// Assign new top offender as target
if ((this.$sddHighOffenders.length > 0) && (this.$sddAutoT)) {
var hf = this.$sddHighOffenders[0];
if (hf.isValid) {
this.$sddTargetShip = hf;
// update HUD
this._updateTargetInfo();
if (this.$log) this._log("H.O. Target Ship changed to "+hf+".");
}
}
}
}
this.shipKilledOther = function(whom, damageType) {
// triggered whenever this ship destroys another
if (this.$sddTargetShip === whom) {
this.$sddTargetShip = null; // reset
// clean up H.O. list
this.$sddHighOffenders = this.$sddHighOffenders.filter(function(ship) {
return ship && ship.isValid;
});
var ps = player.ship;
// Re-sort by distance to player (nearest first)
this.$sddHighOffenders.sort(function(a, b) {
return a.position.distanceTo(ps) - b.position.distanceTo(ps);
});
// Assign new top offender as target
if ((this.$sddHighOffenders.length > 0) && (this.$sddAutoT)) {
var hf = this.$sddHighOffenders[0];
if (hf.isValid) {
this.$sddTargetShip = hf;
// update HUD
this._updateTargetInfo();
if (this.$log) this._log("H.O. Target Ship changed to "+hf+".");
}
}
}
};
// prevent potential collisions within group
this._collisionAvoidance = function(ship) {
if (!ship || !ship.isValid) return;
// Prevent collision with player
ship.addCollisionException(player.ship);
// Prevent collision with other drones
for (let i = 0; i < this.$sddShips.length; i++) {
let other = this.$sddShips[i];
if (other && other.isValid && other !== ship) {
ship.addCollisionException(other);
}
}
}
// report condition status
this._sddDoCond = function(cond) {
var ps = player.ship;
var pc = player.consoleMessage;
var c = ps.messageGuiTextColor;
if (cond==0) ps.messageGuiTextColor = "blueColor"; else
if (cond==1) ps.messageGuiTextColor = "greenColor"; else
if (cond==2) ps.messageGuiTextColor = "yellowColor"; else
if (cond==3) ps.messageGuiTextColor = "redColor";
pc("Condition "+this.$sddConds[cond],3);
ps.messageGuiTextColor = c;
}
// put out the distress call
this._sddDistress = function(msg) {
if (this.$sddHasEQ) {
var ps = player.ship;
var pc = player.consoleMessage;
var c = ps.messageGuiTextColor;
ps.messageGuiTextColor = "redColor";
pc(msg,9);
ps.broadcastDistressMessage();
if (this.$sddHens <= this.$sddMaxHens)
pc("Toggle WEAPONS to launch Riders!",9);
ps.messageGuiTextColor = c;
}
}
// determine if a given drone is already engaged
this._isShipAttacking = function(ship) {
if (!ship.isValid) return false;
// Check if in an attack-related AI state
const attackingStates = ["ATTACK_SHIP", "ATTACK_PIRATE"];
return attackingStates.includes(ship.AIState) &&
ship.target &&
ship.target.isValid;
}
// index of nearest drone to player
this._getNearestShipIndex = function() {
let nearestIndex = -1;
let shortestDistance = Infinity;
for (let i = 0; i < this.$sddShips.length; i++) {
const ship = this.$sddShips[i];
if (ship && ship.isValid) {
const dist = player.ship.position.distanceTo(ship.position);
if (dist < shortestDistance) {
shortestDistance = dist;
nearestIndex = i;
}
}
}
return nearestIndex;
};
this._sddCountShips = function() {
var count = 0;
for (var i = 0; i < this.$sddShips.length; i++)
if (this.$sddShips[i].isValid) count++;
return count;
}
// return an array with only the single highest-bounty ship within scanner range (25.6 km). To get the top N, use .slice(0, N).
this._highestBountyShip = function() {
var ships = system.filteredEntities(
this,
function(e) {
return e.isShip && e.bounty > 0;
},
player.ship, // relative to player
25600 // Scanner range in meters
);
ships.sort(function(a, b) {
return b.bounty - a.bounty;
});
return ships.length ? [ships[0]] : [];
};
// recall and re-attach riders (remove from system space)
this._sddReset = function() {
for (var i = 0; i < this.$sddShips.length; i++)
this.$sddShips[i].remove(true);
// Perform other housekeeping (clear the array, etc)
this.$sddShips.length = 0;
this.$sddHens = 0;
this.$sddCharges = 0;
}
// accounting info: all real debits and credits
// are made realtime in the ship-scripts
this._sddAccounting = function() {
var pc = player.commsMessage;
if ((this.$sddHasEQ) && (this.$sddHens>0)){
// count # of surviving drones
var surv = this._sddCountShips();
pc(surv+" SDD drone riders retrieved.",9);
var dead = this.$sddHens-surv;
pc(dead+" SDD drone riders lost.",9);
// nominal charge for deployment (survivors rejoin)
var depcr = (this.$sddHens * 10);
pc("Charges for drones deployed ("+this.$sddHens+"): "+depcr+" cr",9);
var losscr = (dead * 100);
pc("Charges for drones lost ("+dead+"): "+losscr+" cr",9);
this.$sddCharges = (depcr+losscr);
}
}
// detect an offending ship by its bounty
this._sddIsOffender = function(ship) {
if ((ship.isShip) && (ship.shipClassName!=='Asteroid'))
return (ship.bounty > 0);
else return false;
}
this._sddAddHighOffender = function(ship) {
// clear the array of invalid ships
this.$sddHighOffenders = this.$sddHighOffenders.filter(function(s) {
return s && s.isValid;
});
// Add the new ship if valid and not already in the list
if (ship && ship.isValid && this.$sddHighOffenders.indexOf(ship) === -1) {
this.$sddHighOffenders.push(ship);
}
// Re-sort by distance to player (nearest first)
var ps = player.ship;
this.$sddHighOffenders.sort(function(a, b) {
return a.position.distanceTo(ps) - b.position.distanceTo(ps);
});
// Explicitly set the Top target to the first element after sorting
if (this.$sddHighOffenders.length > 0)
this.$sddTargetShip = this.$sddHighOffenders[0];
};
// list the system's high offenders by distance via comm
this.$sddListHighOffenders = function() {
var hof = this.$sddHighOffenders;
// 1. clean existing offender list
hof = hof.filter(function(s) { return s && s.isValid; });
// offenders list is empty
if (hof.length === 0) {
player.commsMessage("No high offenders detected in system.", 5);
return;
}
// 2. SORT the cleaned list by distance from the player's ship (closest first)
hof.sort(function(a, b) {
return player.ship.position.distanceTo(a.position) - player.ship.position.distanceTo(b.position);
});
// 3. send comms messages to report high offenders in system
// (recallable via log & updatable by pressing [f] on F5 screen)
var pc = player.consoleMessage;
pc("High Offenders:", 5);
for (var i = 0; i < hof.length; i++) {
var ship = hof[i];
var b = ship.bounty;
var d = this._sddDistanceKm(ship);
var c = this._sddCompassDirection(ship);
var n = ship.displayName;
pc("["+i+"] "+n+" ("+b+") at "+d+ " Lk "+c,9);
}
};
// Compass direction of the given ship, 'N' = top of scanner, 'S' = bottom.
this._sddCompassDirection = function(ship) {
var ps = player.ship;
// Get the vector from the player to the target ship
var vectorToTarget = ship.position.subtract(ps.position);
// Get the player's forward and right vectors (defining the horizontal plane)
var playerForward = ps.vectorForward;
var playerRight = ps.vectorRight;
// Project the target vector onto the player's horizontal plane (XZ plane)
var dotForward = vectorToTarget.dot(playerForward);
var dotRight = vectorToTarget.dot(playerRight);
// Calculate the angle in radians from the player's forward direction
var angle = Math.atan2(dotRight, dotForward);
// Convert angle from radians to degrees and normalize to 0-360
var degrees = (angle * 180 / Math.PI + 360) % 360;
// Determine the compass direction based on the angle
if (degrees >= 337.5 || degrees < 22.5) return 'N';
else if (degrees < 67.5) return 'NE';
else if (degrees < 112.5) return 'E';
else if (degrees < 157.5) return 'SE';
else if (degrees < 202.5) return 'S';
else if (degrees < 247.5) return 'SW';
else if (degrees < 292.5) return 'W';
else return 'NW';
};
// distance to any ship in km
this._sddDistanceKm = function(ship) {
var distanceInMeters = ship.position.distanceTo(player.ship.position);
return (distanceInMeters / 1000).toFixed(2);
};
this._sddGetRandomInt = function(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// echo to Oolite log for this script only
this._log = function(msg) {
log(this.name+".debug", msg);
}
this.playerWillSaveGame = function(message) {
var mv = missionVariables;
mv.sddRKills = this.$sddRKills; // SDD Rider kills
mv.sddRBounty = this.$sddRBounty; // SDD Earnings (incl. 100 cr 'incentives')
mv.sddRLosses = this.$sddRLosses; // count: SDDs killed or collided
mv.sddRTax = this.$sddRTax; // debit: SDD deployment taxes
mv.sddPKills = this.$sddPKills; // Police 'terminations'
mv.sddPBoons = this.$sddPBoons; // Police 'boons'
// SDD Rider deployment taxation
// current tax level, incremented per 'drop' (nominally +0.01 cr)
mv.sddDeployTax= this.$sddDeployTax;
}
this.startUp = function() {
this.$sddConds = ["DOCKED","GREEN","YELLOW","RED"];
this.$sddTesting = false;
this.$sddHasEQ = false;
this.$sddMaxHens = 9; // arbitrary initialization
this.$sddHens = 0;
this.$sddAttackMsg = "SDD set condition RED.";
this.$sddAlertMsg = "RED alert. RED alert.";
this.$sddCharges = 0;
this.$sddTargetShip = null; // Top target on High Offenders list
this.$sddDeployTax = 0.0; // increases by <tax-level> with each 'drop'
this.$sddTaxLevel = 0.01; // per 'drop' increment for deployment tax
// tallies to be passed from ship-scripts
// and saved/loaded via missionVariables.
this.$sddRKills = 0; // SDD Rider kills
this.$sddPKills = 0; // Police 'terminations'
this.$sddPBoons = 0; // Police 'boons'
this.$sddRBounty= 0; // SDD Earnings (incl. 100 cr 'incentives')
this.$sddRTax = 0; // total SDD deployment taxes
this.$sddRLosses= 0; // count of SDDs killed or collided
this.$sddMKills = 0; // Mother's kills
this.$sddMBounty= 0; // Mother's own bounties
this.$sddIndex = 0; // index into H.O. cycles target
this.$sddAutoT = true; // auto-selecting targets?
// Register the 'x' key for the F5 screen
// for manually recalling drones (v.1.92 +)
// and the 'f' key for listing offenders...
if (oolite.compareVersion("1.92") <= 0) {
setExtraGuiScreenKeys(this.name, {
guiScreen: "GUI_SCREEN_STATUS",
registerKeys: {
"recall-drones": [{key: "x"}],
"list-offenders": [{key: "f"}],
"cycle-hof": [{key: "t"}]
},
callback: this._sddKeyHandler.bind(this)
});
}
// create and declare rider array once here
this.$sddShips = [];
// create and declare high-offender shiplist
this.$sddHighOffenders = [];
// Load persistent incremented variables
// -------------------------------------
var mv = missionVariables;
if (mv.sddRKills !== undefined) this.$sddRKills = mv.sddRKills; // SDD Rider kills
if (mv.sddRBounty!== undefined) this.$sddRBounty= mv.sddRBounty; // SDD Rider Earnings
if (mv.sddRLosses!== undefined) this.$sddRLosses= mv.sddRLosses; // SDD destroyed debit
if (mv.sddRTax !== undefined) this.$sddRTax = mv.sddRTax; // SDD deployment taxes
if (mv.sddPKills !== undefined) this.$sddPKills = mv.sddPKills; // Police SDD kills
if (mv.sddPBoons !== undefined) this.$sddPBoons = mv.sddPBoons; // Police SDD 'boons'
// SDD Rider deployment taxation (floats)
// if undefined, defaults to 0.0 when loading
this.$sddDeployTax = (mv.sddDeployTax !== undefined) ? parseFloat(mv.sddDeployTax) : 0.0;
}
// 'recall-drones' with [x] key - ONLY from F5 status screen,
// 'list-offenders' with [f] key - ditto
// 'cycle-hof' with [t] key - ditto
this._sddKeyHandler = function(keyId) {
if (keyId === "recall-drones") {
// Remove all drones from the system
this._sddReset();
return true; // Consume the keypress
}
if (keyId === "list-offenders") {
// list system's high-offenders
var hof = this.$sddHighOffenders;
this.$sddListHighOffenders();
if ((this.$sddAutoT) && (hof.length>0) && (hof[0].isValid))
this.$sddTargetShip = hof[0];
return true; // Consume the keypress
}
// Cycle primary Target
var hof = this.$sddHighOffenders;
if ((hof) && (hof.length>0)) {
if (keyId === "cycle-hof") {
// Increment index and wrap around if it reaches the end
this.$sddIndex = (this.$sddIndex + 1) % hof.length;
var idx = this.$sddIndex;
// assign this target
this.$sddTargetShip = hof[idx];
this._updateTargetInfo();
// cancel auto-targetting
this.$sddAutoT = false;
let ps = player.ship, pc = player.consoleMessage;
let oc = ps.messageGuiTextColor; // old colour
ps.messageGuiTextColor = "greenColor";
pc("["+idx+"] Target: " + hof[idx].displayName, 7);
ps.messageGuiTextColor = oc; // old colour
return true;
}
} else {
player.consoleMessage("No high-value targets in system.",7);
return true;
}
return false;
}
// Helper method to update the F5F5 SDD-Net Mission Screen
this._updateSDDNetworkDisplay = function() {
if (this.$sddHasEQ) {
var ps = player.ship, dname = ps.displayName, pn = player.name;
// Check if data exists
var hasData = (this.$sddRKills > 0) || (this.$sddPKills > 0);
if (hasData) {
// Show stats
mission.setInstructions([
"SDD-Network",
"Licensee: " + pn + ", " + dname,
"SDD Rider Kills: " + (this.$sddRKills || 0),
"SDD Rider Bounty: " + (this.$sddRBounty || 0) + " cr",
"SDD Rider Taxes: " + (Number(this.$sddRTax) || 0.0).toFixed(2) + " cr",
"SDD 'Drop Tax': " + (Number(this.$sddDeployTax) || 0.0).toFixed(2) + " cr",
"Police SDD Kills: " + (this.$sddPKills || 0),
"Police SDD Boons: " + (this.$sddPBoons || 0) + " cr"
], this.name);
} else {
// Essential: explicitly clear the section to hide it completely
mission.setInstructions(null, this.name);
}
}
};
// Update the F5F5 SDD-Net Mission Screen on displaying it
this.guiScreenChanged = function(from, to) {
if (this.$sddHasEQ) {
if (guiScreen === "GUI_SCREEN_MANIFEST") {
this._updateSDDNetworkDisplay();
}
}
};
// Drone capacities by ship class
this._sddMaxDrones = function() {
var name = player.ship.shipClassName.toLowerCase();
var map = {
"adder": 2,
"anaconda": 20,
"asp mk ii": 6,
"asp mark ii": 6,
"asp explorer": 6,
"boa": 15,
"boa class cruiser": 15,
"cobra mk i": 5,
"cobra mark i": 5,
"cobra mk ii": 6,
"cobra mk iii": 8,
"cobra mk 3": 8,
"cobra mark iii": 8,
"cobra mk iv": 10,
"constrictor": 10,
"fer-de-lance": 6,
"gecko": 2,
"krait": 5,
"mamba": 3,
"moray medical boat": 4,
"moray star boat": 4,
"python": 12,
"sidewinder": 2,
"sidewinder scout ship": 2,
"training fighter": 4,
"transporter": 2,
"viper": 2,
"galcop viper": 2,
"galcop viper interceptor": 3,
"worm": 2
};
return map[name] || 7;
};
// nearest High Offender realtime tracking onscreen (via GETter HUD's hud.plist)
this._updateTargetInfo = function() {
if (!player.ship.isValid) return;
if (this.$sddTargetShip && this.$sddTargetShip.isValid) {
var ps = player.ship, ts = this.$sddTargetShip;
var distance = (ts.position.distanceTo(ps.position) / 1000).toFixed(2);
var direction = this._sddCompassDirection(ts);
// Get the vector from player to target
var relPos = ts.position.subtract(ps.position);
// Project the relative position onto the player's up and forward vectors
var upComponent = relPos.dot(ps.orientation.vectorUp());
var forwardComponent = relPos.dot(ps.heading); // heading is the forward vector
// Determine elevation based on the ratio, avoiding division by zero
var elevation = "";
if (Math.abs(forwardComponent) > 1) { // Use a small threshold instead of zero
var verticalAngle = Math.abs(upComponent / forwardComponent);
elevation = (upComponent > 0) ? (verticalAngle > 0.1 ? "hi" : "") : (verticalAngle > 0.1 ? "lo" : "");
}
ps.setCustomHUDDial("sddTargetInfo", ts.name + " > " + direction + " " + elevation + " < " + distance + "km");
} else {
player.ship.setCustomHUDDial("sddTargetInfo", "");
}
};
|