A drop-in replacement for the stock AI.

Is much more stable, aims hardmounts better, keeps its distance and even evades cannon fire.

Can use all jets/dediblades/spinblock rotors as long as they provide a force pointing from their base to their top on positive input.

If your craft is too jumpy/reacts too slowly take a look at the PID configurations at the start.

Steam workshop: [url]http://steamcommunity.com/sharedfiles/fi...=709627548[/url]

What's new, 2017-09-05:

Important!

AI modes:

Altitude control:

"StabilityTest"/"OrbiterDummy" blueprint manual controls:

- I, K: forward, backward

- J, L: turn left, turn right

- U, O: move left, move right

- uparrow, downarrow: increase/decrease altitude

Is much more stable, aims hardmounts better, keeps its distance and even evades cannon fire.

Can use all jets/dediblades/spinblock rotors as long as they provide a force pointing from their base to their top on positive input.

If your craft is too jumpy/reacts too slowly take a look at the PID configurations at the start.

Steam workshop: [url]http://steamcommunity.com/sharedfiles/fi...=709627548[/url]

What's new, 2017-09-05:

- Differential thrust for yaw and pitch

- Attack run for bombers/fighters

- Can use pitch to control altitude

Important!

- If you want to use dedispinners make sure that they create a force pointing towards their top on positive input.

The simplest way is just making a prefab out of a spinner on the demo crafts and extending that as neccessary.

- Only watches AI mode on the first mainframe listed in the 'C' menu. If you want to use a different one remove every other mainframe until only that one remains then put the removed ones back.

AI modes:

- Off: Parking mode, hovers in place

- On: Manual mode, only controls altitude/pitch/roll, you can drive around like it was a boat just in the air

- Combat:

Falls back to patrolling if there are no enemies around

- maintains a preset distance and always faces the target, performs evasive manouvers if it's configured to do so

- in orbiter mode circles optimalDistance away from the target, can evade by wildly changing pitch

- in attackrun mode does head-on attack passes

- in repairmode helps damaged allies, when everyone is at full health falls back to fleetmove instead of patrol if the craft isn't a flagship

- Patrol: follows waypoints and stabilizes at 0 speed when not fighting, follows waypoints while facing the target otherwise

- Fleetmove: it's glued to its position in the fleet(relative to the flagship). Out of combat matches the direction the flagship is facing, in combat faces the target and uses side/forward thrusters to follow the flagship without turning.

Altitude control:

- If you add a hydrofoil it'll be used to control the base altitude in 'Off'/'On'/'Combat' modes.

-45° is the minimum altitude, 45° the max(I recommend binding the hydrofoil to a complex controller).

"StabilityTest"/"OrbiterDummy" blueprint manual controls:

- I, K: forward, backward

- J, L: turn left, turn right

- U, O: move left, move right

- uparrow, downarrow: increase/decrease altitude

Code:

`-- Possible modes:`

-- "frontal": turns towards the target, keeps the distance and dodges if configured to do so

-- "orbiter": circles around the target

-- "attackrun": flies towards the target, breaks off at a set distance and reengages after it got far enough or spent enough time flying away

-- "left": orbiter that turns the left side towards the target

-- "right": orbiter that turns the right side towards the target

mode = "frontal"

-- Propulsion components used

useJets = true

useDediSpinners = true

useAllSpinners = false

-- General behavior

matchAltitude = false -- In combat will match the target's altitude(evasion will use this altitude as base, minAltitude and maxAltitude still apply).

repairMode = false -- Won't fight but try to repair friendlies

isSatellite = false -- Will aim at the target with the bottom

repairClosestFirst = true -- In repair mode goes to help the most damaged ally first on false

minDamageNoticed = 1 -- 0.95 means the craft won't bother with anything above 95% health

-- Altitude

optimalAltitude = 300 -- Move at this height when in combat/stay this many meters above target in repair mode(can be negative for repair)

optimalDistance = 1500 -- Try to maintain this distance from target in both modes(doesn't take altitude into account). If you want to get close consider reducing maxPitchMagnitude. -1 means staying stationary.

minAltitude = 30 -- Measured at center of mass, leave some clearance

maxAltitude = 400 -- Without space captain above 300 the atmosphere starts to thin out. Use space propulsion if you want to go much higher

altitudeFeedbackMeters = 10 -- Writes the new target altitude to HUD in off/on/combat mode, if it changed by at least this much. 0 turns feedback off.

-- Pitch/roll/yaw

maxPitchMagnitude = 30 -- Pitch with a bigger absolute value will be clamped to match this

maxRoll = 0 -- Roll with a bigger absolute value will be clamped to match this

maxYawDifferential = 1 -- Values over 0 enable differential trust for yawing(an example: 0.5 means that a left-of-the-CoM, backwards facing jet's output will be increased by up to 50% when turning right, decreased by 50% when turning left)

maxPitchDifferential = 0 -- Values over 0 enable differential trust for pitching

pitchMovementAssist = 0 -- Max angle to pitch at to help forward/backwards movement

rollMovementAssist = 0 -- Max angle to roll at to help left/right movement

-- Evasion

evasiveAltitudeChange = 0 -- Vertical evasive manouvers add/subtract this to/from altitude

evasiveLeftPeriod = 4 -- Go left for this amount of time(seconds)

evasiveRightPeriod = 4 -- Go right for this amount of time(seconds)

-- Balloons

useBalloons = true -- Set it to false when not using balloons for water start

ballonDeployAltitude = 0 -- Deploy balloons below this one

ballonReleaseAltitude = 100 -- Cut balloons above this

-- false: uses upwards propulsion to control altitude, hovers in place when idle

-- true: uses pitch to control altitude, flies in circles when idle(only works in orbiter and attackrun modes)

pitchForAltitude = false

-- Attack run configuration

attackRunBreakDistance = 300 -- Will get this close before turning

attackRunPitchDistance = 0 -- Will pitch to match the target at this distance

attackRunReengageDistance = 1000 -- Will reengage after getting this far

attackRunReengageTime = 30 -- Will reengage after running away for this many seconds

attackRunRollToTurn = false -- Will roll into turns when in attackrun mode

-- Used for getting a serial number(that's not used for anything right now)

blueprintName = "HoverAI"

-- Tune PIDs here

-- P, D, I, OutMax, OutMin, IMax, IMin

rollPIDData = { 0.05, 0.01, 0.0, 1, -1, 1, -1}

pitchPIDData = { 0.1, 0.02, 0.0, 1, -1, 1, -1}

altitudePIDData = { 0.2, 0.05, 0.01, 1, -1, 1, -1}

yawPIDData = { 0.1, 0.02, 0.0, 1, -1, 1, -1}

slipPIDData = { 0.05, 0.05, 0.0, 1, -1, 1, -1}

distancePIDData = { 0.1, 0.5, 0.0, 1, -1, 1, -1}

function Init(I)

-- forgive me for I have sinned

API = I

if not VehicleState then

VehicleState = {}

end

VehicleState.centerOfMass = API:GetConstructCenterOfMass()

VehicleState.pitch = -API:GetConstructPitch()

VehicleState.roll = API:GetConstructRoll()

VehicleState.forwardVector = API:GetConstructForwardVector()

VehicleState.upVector = API:GetConstructUpVector()

VehicleState.yaw = API:GetConstructYaw()

VehicleState.speed = API:GetForwardsVelocityMagnitude()

VehicleState.currentTime = API:GetTime()

VehicleState.velocity = API:GetVelocityVector()

VehicleState.AICount = API:GetNumberOfMainframes()

VehicleState.flagship = API.Fleet.Flagship

propulsionAngleTolerance = 0.5

local horizontalProjection = Vector3(VehicleState.forwardVector.x, 0, VehicleState.forwardVector.z)

VehicleState.projectionRotation = Quaternion.Inverse(Quaternion.LookRotation(horizontalProjection, VehicleState.upVector))

VehicleState.normalRotation = Quaternion.Inverse(Quaternion.LookRotation(VehicleState.forwardVector, VehicleState.upVector))

VehicleState.target = GetCombatTarget()

if repairMode then

VehicleState.repairTarget = GetRepairTarget()

end

VehicleState.targetPosition = nil

if API.AIMode == "fleetmove" and API.IsFlagship then

VehicleState.AIMode = "patrol"

else

VehicleState.AIMode = API.AIMode

end

if VehicleState.pitch<-180 then

VehicleState.pitch = VehicleState.pitch+360

end

if VehicleState.roll>180 then

VehicleState.roll = VehicleState.roll-360

end

if not VehicleState.serial then

SetSerial()

InitPIDs()

if API:Component_GetCount(8) == 0 then

VehicleState.hasAltitudeControl = false

VehicleState.adjustedAltitude = optimalAltitude

else

VehicleState.hasAltitudeControl = true

SetAltitudeHydrofoil(optimalAltitude)

end

end

if VehicleState.hasAltitudeControl then

SetupAltitudeControl()

end

end

function SetupAltitudeControl()

local altitudeFlag = API:Component_GetFloatLogic(8, 0) + 45

local newAltitude = minAltitude + ((maxAltitude-minAltitude) * altitudeFlag / 90)

if altitudeFeedbackMeters > 0 then

local promptAltitude = math.floor(newAltitude / altitudeFeedbackMeters) * altitudeFeedbackMeters

if not VehicleState.lastAltitudePrompt or math.abs(promptAltitude - VehicleState.lastAltitudePrompt) >= altitudeFeedbackMeters then

if VehicleState.AIMode == "on" or VehicleState.AIMode == "off" or VehicleState.AIMode == "combat" then

API:LogToHud("Altitude: " .. promptAltitude .. "m")

end

VehicleState.lastAltitudePrompt = promptAltitude

end

end

VehicleState.adjustedAltitude = newAltitude

end

function SetAltitudeHydrofoil(newAltitude)

local relativeAltitude = math.min(math.max(newAltitude, minAltitude), maxAltitude) - minAltitude

local altitudeFraction = relativeAltitude / (maxAltitude-minAltitude)

local normalizedRange = altitudeFraction - 0.5

local fullAngle = normalizedRange * 90

-- initialize altitude flag hydrofoil angle

API:Component_SetFloatLogic(8, 0, math.min(math.max(fullAngle, -45), 45))

end

-- Take over all jets, not just vertical ones

function EnableAllJets()

VehicleState.jetsAtMax = 0

if useJets then

for i=0,11 do

API:RequestThrustControl(i)

end

end

end

function ResetJetStrengths()

if not VehicleState.jetsAtMax or VehicleState.jetsAtMax == 0 then

API:Component_SetFloatLogicAll(9,1)

VehicleState.jetsAtMax = 1

end

end

function SetupPropulsion()

-- Send an 'on' signal to vertical jets

if useJets then

-- up/down

API:RequestThrustControl(4)

API:RequestThrustControl(5)

-- pitch up/down

API:RequestControl(2, 4, 1)

API:RequestControl(2, 5, 1)

end

if VehicleState.AIMode == "on" or VehicleState.AIMode == "off" then

-- Manual mode, lay off side/forward propulsion strengths

ResetJetStrengths()

else

-- Automatic, toggle everything on

EnableAllJets()

end

JetManager = {

propulsionCount = API:Component_GetCount(9),

GetComponent = function (serial) return API:Component_GetBlockInfo(9,serial) end,

SetImpulse = function (serial,impulse) API:Component_SetFloatLogic(9,serial,math.min(math.max(impulse, 0),1)) end,

GetFowardVector = function (propulsion) return propulsion.Forwards end,

type = "jet"

}

RotorManager = {

propulsionCount = API:GetSpinnerCount(),

GetComponent = function (serial) return API:GetSpinnerInfo(serial) end,

SetImpulse = function (serial,impulse) if useAllSpinners or API:IsSpinnerDedicatedHelispinner(serial) then API:SetSpinnerInstaSpin(serial, math.min(math.max(impulse, -1),1)) end end,

GetFowardVector = function (propulsion) return QuaternionUpVector(propulsion.Rotation) end,

type = "spinner"

}

PropulsionManagers = {}

if useAllSpinners or useDediSpinners then

table.insert(PropulsionManagers, RotorManager)

end

if useJets then

table.insert(PropulsionManagers, JetManager)

end

PropulsionComponents = {}

for i=1,table.getn(PropulsionManagers),1 do

for j=0,PropulsionManagers[i].propulsionCount-1,1 do

local component = PropulsionManagers[i].GetComponent(j)

local globalOrientation = PropulsionManagers[i].GetFowardVector(component)

local localOrientation = VehicleState.normalRotation * globalOrientation

local horizontalOrientation = VehicleState.projectionRotation * globalOrientation

local position = VehicleState.normalRotation * (component.Position - VehicleState.centerOfMass)

local currentComponent = {

serial = j,

position = position,

globalOrientation = globalOrientation,

localOrientation = localOrientation,

horizontalOrientation = horizontalOrientation,

sendImpulse = PropulsionManagers[i].SetImpulse,

type = PropulsionManagers[i].type

}

table.insert(PropulsionComponents, currentComponent)

end

end

end

function AddComponentImpulse(component, impulse)

if not component.impulse then

component.impulse = impulse

else

component.impulse = component.impulse + impulse

end

end

function AddPitchOutput(pitchInput)

for i=1,table.getn(PropulsionComponents) do

local component = PropulsionComponents[i]

if math.abs(component.localOrientation.y)>propulsionAngleTolerance then

if component.position.z<-0.05 then

AddComponentImpulse(component, -pitchInput * component.localOrientation.y)

elseif component.position.z>0.05 then

AddComponentImpulse(component, pitchInput * component.localOrientation.y)

end

end

if maxPitchDifferential > 0 and math.abs(component.localOrientation.z)>propulsionAngleTolerance and math.abs(component.position.y) > 0.5 then

if component.position.y<-0.05 then

AddComponentImpulse(component, maxPitchDifferential * pitchInput * component.localOrientation.z)

elseif component.position.y>0.05 then

AddComponentImpulse(component, maxPitchDifferential * -pitchInput * component.localOrientation.z)

end

end

end

end

function AddYawOutput(yawInput)

for i=1,table.getn(PropulsionComponents) do

local component = PropulsionComponents[i]

if math.abs(component.localOrientation.x)>propulsionAngleTolerance then

if component.position.z<-0.05 then

AddComponentImpulse(component, -yawInput * component.localOrientation.x)

elseif component.position.z>0.05 then

AddComponentImpulse(component, yawInput * component.localOrientation.x)

end

end

if maxYawDifferential > 0 and math.abs(component.localOrientation.z)>propulsionAngleTolerance and math.abs(component.position.x) > 0.5 then

if component.position.x<-0.05 then

AddComponentImpulse(component, maxYawDifferential * yawInput * component.localOrientation.z)

elseif component.position.x>0.05 then

AddComponentImpulse(component, maxYawDifferential * -yawInput * component.localOrientation.z)

end

end

end

end

function AddRollOutput(rollInput)

for i=1,table.getn(PropulsionComponents) do

local component = PropulsionComponents[i]

if math.abs(component.localOrientation.y)>propulsionAngleTolerance then

if component.position.x<-0.05 then

AddComponentImpulse(component, -rollInput * component.localOrientation.y)

elseif component.position.x>0.05 then

AddComponentImpulse(component, rollInput * component.localOrientation.y)

end

end

if VehicleState.AIMode ~= "off" and VehicleState.AIMode ~= "on" then

if math.abs(component.localOrientation.x)>propulsionAngleTolerance then

if component.position.y<-0.05 then

AddComponentImpulse(component, rollInput * component.localOrientation.x)

elseif component.position.y>0.05 then

AddComponentImpulse(component, -rollInput * component.localOrientation.x)

end

end

end

end

end

function AddAltitudeOutput(altitudeInput)

if pitchForAltitude then

SetPitch(altitudeInput * maxPitchMagnitude)

return

end

for i=1,table.getn(PropulsionComponents) do

local component = PropulsionComponents[i]

local magnitude = (component.globalOrientation.y > 0 and 1 or -1)

if math.abs(component.globalOrientation.y)>propulsionAngleTolerance then

AddComponentImpulse(component, altitudeInput * magnitude)

end

end

end

function AddSlipOutput(slipInput)

for i=1,table.getn(PropulsionComponents) do

local component = PropulsionComponents[i]

local magnitude = (component.horizontalOrientation.x > 0 and 1 or -1)

if math.abs(component.horizontalOrientation.x)>propulsionAngleTolerance then

AddComponentImpulse(component, slipInput * magnitude)

end

end

end

function AddForwardOutput(forwardInput)

for i=1,table.getn(PropulsionComponents) do

local component = PropulsionComponents[i]

local magnitude = (component.horizontalOrientation.z > 0 and 1 or -1)

if math.abs(component.horizontalOrientation.z)>propulsionAngleTolerance then

AddComponentImpulse(component, forwardInput * magnitude)

end

end

end

function SendPropulsionImpulses()

for i=1,table.getn(PropulsionComponents) do

local component = PropulsionComponents[i]

if component.impulse ~= nil then

component.sendImpulse(component.serial, component.impulse)

else

if component.type == "spinner" then

API:SetSpinnerSpeedFactor(component.serial, 1)

end

end

end

end

function SetPitch(targetPitch)

targetPitch = math.max(math.min(targetPitch, maxPitchMagnitude), -maxPitchMagnitude)

pitchInput,pitchPID = GetPIDOutput(targetPitch, VehicleState.pitch, pitchPID)

AddPitchOutput(pitchInput)

return pitchInput

end

function SetPitchWrapper(targetPitch)

if not pitchForAltitude then

return SetPitch(targetPitch)

end

end

function SetRoll(targetRoll)

targetRoll = math.max(math.min(targetRoll, maxRoll), -maxRoll)

rollInput,rollPID = GetPIDOutput(targetRoll, VehicleState.roll, rollPID)

AddRollOutput(rollInput)

return rollInput

end

function SetAltitude(targetAltitude)

local highestTerrainAhead = HighestTerrainAhead()

targetAltitude = math.min(math.max(targetAltitude, highestTerrainAhead+minAltitude), maxAltitude)

altitudeInput,altitudePID = GetPIDOutput(targetAltitude, VehicleState.centerOfMass.y, altitudePID)

AddAltitudeOutput(altitudeInput)

return altitudeInput

end

function Turn(targetAzi)

yawInput,yawPID = GetPIDOutput(0, targetAzi, yawPID)

AddYawOutput(yawInput)

return yawInput

end

function SetSlipSpeedForDistance(distance)

slipSpeed,slipPID = GetPIDOutput(0, distance, slipPID)

AddSlipOutput(slipSpeed)

SetRoll(-rollMovementAssist * slipSpeed)

end

function SetForwardSpeedForDistance(distance, usePitch)

forwardSpeed,distancePID = GetPIDOutput(0, distance, distancePID)

AddForwardOutput(-forwardSpeed)

if usePitch then

SetPitchWrapper(pitchMovementAssist * forwardSpeed)

end

end

function GetFrontalEvasionData()

evasionData = {

slip = 0,

altitudeChange = evasiveAltitudeChange * math.sin(VehicleState.currentTime)

}

local slipCycleDuration = evasiveRightPeriod + evasiveLeftPeriod

if slipCycleDuration == 0 then

return evasionData

end

local slipCycleTime = VehicleState.currentTime%slipCycleDuration

if slipCycleTime < evasiveRightPeriod then

evasionData.slip = 1

else

evasionData.slip = -1

end

if math.abs(VehicleState.pitch) > 45 then

evasionData.altitudeChange = 0

end

return evasionData

end

function GetOrbitEvasionPitchOutput(altitude)

if not pitchDown then

pitchDown = false

end

if VehicleState.centerOfMass.y > altitude + evasiveAltitudeChange then

pitchDown = true

elseif VehicleState.centerOfMass.y < altitude - evasiveAltitudeChange then

pitchDown = false

end

if VehicleState.pitch < -maxPitchMagnitude then

pitchDown = false

elseif VehicleState.pitch > maxPitchMagnitude then

pitchDown = true

end

if pitchDown then

return -1

else

return 1

end

end

function HighestTerrainAhead()

local maxAltitude = 0

local nextAltitude

local xStep = VehicleState.velocity.x

local zStep = VehicleState.velocity.z

for i=-0.5,5,0.25 do

nextAltitude = API:GetTerrainAltitudeForPosition(VehicleState.centerOfMass.x + xStep*i, VehicleState.centerOfMass.y, VehicleState.centerOfMass.z + zStep*i)

if nextAltitude>maxAltitude then

maxAltitude = nextAltitude

end

end

return maxAltitude

end

function GetCombatTarget()

local target = API:GetTargetPositionInfo(0,0)

if target.Valid then

local azimuth

if target.Azimuth>180 then

azimuth = target.Azimuth-360

else

azimuth = target.Azimuth

end

return {

Elevation = -math.deg(math.atan((VehicleState.centerOfMass.y-target.AltitudeAboveSeaLevel) / target.GroundDistance)),

Azimuth = azimuth,

GroundDistance = target.GroundDistance,

Altitude = target.AltitudeAboveSeaLevel

}

else

return false

end

end

function GetRepairTarget()

-- Lower means more important

local highestPriorityValue = 1000000

local targetData = false

local friendlyCount = API:GetFriendlyCount()

for i=0,friendlyCount-1,1 do

local friendly = API:GetFriendlyInfo(i)

local targetCenterOfMass = friendly.CenterOfMass

local health = friendly.HealthFraction

if health<=minDamageNoticed and health<1 then

if not repairClosestFirst then

if health<highestPriorityValue then

targetData = friendly

highestPriorityValue = health

end

else

local distance = math.sqrt( (targetCenterOfMass.x-VehicleState.centerOfMass.x)*(targetCenterOfMass.x-VehicleState.centerOfMass.x)

+(targetCenterOfMass.z-VehicleState.centerOfMass.z)*(targetCenterOfMass.z-VehicleState.centerOfMass.z) )

if distance<highestPriorityValue then

targetData = friendly

highestPriorityValue = distance

end

end

end

end

if targetData then

return GetDirectionToCoordinates(targetData.CenterOfMass)

else

return false

end

end

function SetSerial()

local friendlyCount = API:GetFriendlyCount()

local cloneCount = 0

for i=0,friendlyCount-1,1 do

local friendly = API:GetFriendlyInfo(i)

if friendly.BlueprintName==blueprintName then

cloneCount = cloneCount+1

end

end

VehicleState.serial = cloneCount

end

function SetTargetPosition(position)

VehicleState.targetPosition = GetDirectionToCoordinates(position)

VehicleState.targetPosition.x = position.x

VehicleState.targetPosition.y = position.y

VehicleState.targetPosition.z = position.z

VehicleState.targetPosition.sideDistance = math.sin(math.rad(VehicleState.targetPosition.Azimuth)) * VehicleState.targetPosition.GroundDistance

VehicleState.targetPosition.frontDistance = math.cos(math.rad(VehicleState.targetPosition.Azimuth)) * VehicleState.targetPosition.GroundDistance

end

function PitchToTarget()

if isSatellite then

SetPitchWrapper(VehicleState.target.Elevation + 90)

else

if math.abs(VehicleState.target.Azimuth) > 10 then

SetPitchWrapper(0)

else

SetPitchWrapper(VehicleState.target.Elevation)

end

end

end

function Guard()

SetTargetPosition(VehicleState.flagship.CenterOfMass + VehicleState.flagship.Rotation * API.IdealFleetPosition)

if pitchForAltitude then

CircleTarget(VehicleState.targetPosition, false)

return

end

SetAltitude(VehicleState.targetPosition.y)

SetAltitudeHydrofoil(VehicleState.targetPosition.y)

if not VehicleState.target then

Turn(GetVectorAngle(VehicleState.flagship.ForwardVector, VehicleState.forwardVector))

SetSlipSpeedForDistance(VehicleState.targetPosition.sideDistance)

SetForwardSpeedForDistance(VehicleState.targetPosition.frontDistance, true)

else

PitchToTarget()

Turn(VehicleState.target.Azimuth)

SetSlipSpeedForDistance(VehicleState.targetPosition.sideDistance)

SetForwardSpeedForDistance(VehicleState.targetPosition.frontDistance, false)

end

end

function Patrol()

SetTargetPosition(API.Waypoint)

if pitchForAltitude then

CircleTarget(VehicleState.targetPosition, false)

return

end

SetAltitude(VehicleState.targetPosition.y)

SetAltitudeHydrofoil(VehicleState.targetPosition.y)

if not VehicleState.target then

if VehicleState.targetPosition.GroundDistance > 200 then

local slipSpeed = GetSlipSpeed() * 5

slipOutput,slipPID = GetPIDOutput(0, slipSpeed, slipPID)

AddSlipOutput(slipOutput)

SetRoll(-rollMovementAssist * slipOutput)

Turn(VehicleState.targetPosition.Azimuth)

SetForwardSpeedForDistance(VehicleState.targetPosition.GroundDistance, true)

else

SetSlipSpeedForDistance(VehicleState.targetPosition.sideDistance)

SetForwardSpeedForDistance(VehicleState.targetPosition.frontDistance, true)

end

else

PitchToTarget()

Turn(VehicleState.target.Azimuth)

SetSlipSpeedForDistance(VehicleState.targetPosition.sideDistance)

SetForwardSpeedForDistance(VehicleState.targetPosition.frontDistance, false)

end

end

function Repair()

if pitchForAltitude then

CircleTarget(VehicleState.repairTarget, false)

return

end

local evasion = GetFrontalEvasionData()

AddSlipOutput(evasion.slip)

SetRoll(-rollMovementAssist * evasion.slip)

Turn(VehicleState.repairTarget.Azimuth)

SetAltitude(VehicleState.repairTarget.Altitude + VehicleState.adjustedAltitude + evasion.altitudeChange)

SetForwardSpeedForDistance(VehicleState.repairTarget.GroundDistance - optimalDistance, true)

end

function FightOrPatrol()

if not VehicleState.target then

Patrol()

return

end

if mode == "orbiter" or mode == "left" or mode == "right" then

CircleTarget(VehicleState.target, true)

elseif mode == "attackrun" then

AttackRun()

else

FrontalFight()

end

end

function FrontalFight()

local evasion = GetFrontalEvasionData()

if math.abs(VehicleState.target.Azimuth) < 10 then

AddSlipOutput(evasion.slip)

SetRoll(-rollMovementAssist * evasion.slip)

else

SetRoll(0)

end

if matchAltitude then

SetAltitude(VehicleState.target.Altitude + evasion.altitudeChange)

else

SetAltitude(VehicleState.adjustedAltitude + evasion.altitudeChange)

end

PitchToTarget()

Turn(VehicleState.target.Azimuth)

SetForwardSpeedForDistance(VehicleState.target.GroundDistance - optimalDistance, false)

end

function CircleTarget(target, combatMode)

local sign = 1

if orbiterMode == "left" then

sign = 1

elseif orbiterMode == "right" then

sign = -1

elseif target.Azimuth < 0 then

sign = -1

end

local optimalAzi = sign * 90 - sign * 45 * math.min(math.max((target.GroundDistance - optimalDistance) / 200, -1), 1)

if combatMode and math.abs(90 - math.abs(target.Azimuth)) < 20 then

SetRoll(-sign * target.Elevation)

else

SetRoll(0)

end

local altitude

if repairMode then

altitude = target.Altitude + VehicleState.adjustedAltitude

elseif not combatMode or matchAltitude then

altitude = target.Altitude

else

altitude = VehicleState.adjustedAltitude

end

if combatMode and maxPitchMagnitude > 0 and evasiveAltitudeChange > 0 then

AddPitchOutput(GetOrbitEvasionPitchOutput(altitude))

else

SetPitchWrapper(-pitchMovementAssist)

SetAltitude(altitude)

end

Turn(target.Azimuth - optimalAzi)

AddForwardOutput(1)

end

function AttackRun()

local yawOutput

if not running then

yawOutput = Turn(VehicleState.target.Azimuth)

if VehicleState.target.GroundDistance < attackRunBreakDistance then

running = true

lastAttack = 0

end

if VehicleState.target.GroundDistance < attackRunPitchDistance and VehicleState.centerOfMass.y > minAltitude then

PitchToTarget()

else

if matchAltitude then

SetAltitude(VehicleState.target.Altitude)

else

SetAltitude(VehicleState.adjustedAltitude)

end

if not pitchForAltitude then

SetPitch(0)

end

end

else

lastAttack = lastAttack + 1

if VehicleState.target.Azimuth > 0 then

yawOutput = Turn(VehicleState.target.Azimuth - 180)

else

yawOutput = Turn(VehicleState.target.Azimuth + 180)

end

if VehicleState.target.GroundDistance > attackRunReengageDistance or lastAttack > attackRunReengageTime * 40 then

running = false

end

if matchAltitude then

SetAltitude(VehicleState.target.Altitude)

else

SetAltitude(VehicleState.adjustedAltitude)

end

if not pitchForAltitude then

SetPitch(0)

end

end

if attackRunRollToTurn and math.abs(yawOutput) > 0.2 then

SetRoll(-yawOutput * maxRoll)

else

SetRoll(0)

end

AddForwardOutput(1)

end

function Stabilize()

if pitchForAltitude then

Patrol()

return

end

SetAltitude(VehicleState.adjustedAltitude)

SetPitchWrapper(0)

SetRoll(0)

end

function Idle()

SetPitchWrapper(0)

SetRoll(0)

SetAltitude(VehicleState.adjustedAltitude)

AddSlipOutput(-GetSlipSpeed() / 10)

AddForwardOutput(-VehicleState.speed / 10)

end

function Update(I)

if I:IsDocked() then

return

end

I:TellAiThatWeAreTakingControl()

Init(I)

SetupPropulsion(I)

if useBalloons then

BalloonCheck()

end

if repairMode then

if VehicleState.AIMode == "combat" then

if VehicleState.repairTarget then

Repair()

else

if API.IsFlagship then

VehicleState.AIMode = "patrol"

else

VehicleState.AIMode = "fleetmove"

end

end

end

end

if not (repairMode and VehicleState.AIMode == "combat") then

if VehicleState.AIMode == "off" then

Idle()

elseif VehicleState.AIMode == "on" then

Stabilize()

elseif VehicleState.AIMode == "combat" then

FightOrPatrol()

elseif VehicleState.AIMode == "patrol" then

Patrol()

elseif VehicleState.AIMode == "fleetmove" then

if VehicleState.IsFlagship then

FightOrPatrol()

else

Guard()

end

end

end

SendPropulsionImpulses()

end

function BalloonCheck()

if VehicleState.centerOfMass.y < ballonDeployAltitude then

API:Component_SetBoolLogicAll(0, true)

elseif VehicleState.centerOfMass.y > ballonReleaseAltitude then

API:Component_SetBoolLogicAll(0, false)

end

end

-- ___________________________________________________________________________________ Generic helper functions ___________________________________________________________________________________

function GetDirectionToCoordinates(position)

local vectorToPosition = Vector3(position.x-VehicleState.centerOfMass.x, position.y-VehicleState.centerOfMass.y, position.z-VehicleState.centerOfMass.z)

local direction = vectorToPosition.normalized

local aziDot = direction.x*VehicleState.forwardVector.x + direction.z*VehicleState.forwardVector.z

local aziCross = direction.z*VehicleState.forwardVector.x - direction.x*VehicleState.forwardVector.z

local groundDistance = math.sqrt(vectorToPosition.x*vectorToPosition.x + vectorToPosition.z*vectorToPosition.z)

return {

Elevation = -math.deg(math.atan((VehicleState.centerOfMass.y-position.y) / groundDistance)),

Azimuth = math.deg(math.atan2(aziCross, aziDot)),

Altitude = position.y,

GroundDistance = groundDistance

}

end

function GetVectorAngle(vector1, vector2)

local aziDot = vector1.x * vector2.x + vector1.z * vector2.z

local aziCross = vector1.z * vector2.x - vector1.x * vector2.z

return math.deg(math.atan2(aziCross, aziDot));

end

function InitPID(pidData)

PID = {}

PID.Kp = pidData[1]

PID.Kd = pidData[2]

PID.Ki = pidData[3]

PID.OutMax = pidData[4]

PID.OutMin = pidData[5]

PID.IMax = pidData[6]

PID.IMin = pidData[7]

PID.integral = 0

PID.previousError = 0

return PID

end

function InitPIDs()

rollPID = InitPID(rollPIDData)

pitchPID = InitPID(pitchPIDData)

altitudePID = InitPID(altitudePIDData)

yawPID = InitPID(yawPIDData)

slipPID = InitPID(slipPIDData)

distancePID = InitPID(distancePIDData)

end

function GetPIDOutput(SetPoint, ProcessVariable, PID)

local error = SetPoint - ProcessVariable

local timeDelta = 0.025

local derivative

local output

PID.integral = PID.integral + (error*timeDelta) * PID.Ki

if (PID.integral > PID.IMax) then PID.integral = PID.IMax end

if (PID.integral < PID.IMin) then PID.integral = PID.IMin end

derivative = (error - PID.previousError)/timeDelta

output = PID.Kp*error + PID.Kd*derivative + PID.integral

if (output > PID.OutMax) then output = PID.OutMax end

if (output < PID.OutMin) then output = PID.OutMin end

PID.previousError = error

return output,PID

end

-- Props to Evil4Zerggin for the function

function QuaternionUpVector(quaternion)

local x = 2 * (quaternion.x * quaternion.y - quaternion.z * quaternion.w)

local y = 1-2 * (quaternion.x * quaternion.x + quaternion.z * quaternion.z)

local z = 2 * (quaternion.y * quaternion.z + quaternion.x * quaternion.w)

return Vector3(x, y, z).normalized

end

function GetSlipSpeed()

local velocityHeading = 180/math.pi*math.atan2(VehicleState.velocity.x,VehicleState.velocity.z)

if velocityHeading<0 then

velocityHeading = velocityHeading+360

end

local slipAngle = velocityHeading - VehicleState.yaw

local groundSpeedx = VehicleState.velocity.x

local groundSpeedz = VehicleState.velocity.z

local groundSpeed = math.sqrt(groundSpeedx^2 + groundSpeedz^2)

local slipSpeed = groundSpeed*math.sin(slipAngle/180*math.pi)

return slipSpeed

end

**Attached Files**

StabilityTest.blueprint (Size: 89.05 KB / Downloads: 522)

Nova PAC satellite by St0rmWyvrn.blueprint (Size: 106.43 KB / Downloads: 274)

OrbiterDummy.blueprint (Size: 241.04 KB / Downloads: 289)