Skip to content

Instantly share code, notes, and snippets.

@ZerothAngel
Created February 5, 2018 20:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ZerothAngel/dfad34c579aa6c25786cc399ccf06ec6 to your computer and use it in GitHub Desktop.
Save ZerothAngel/dfad34c579aa6c25786cc399ccf06ec6 to your computer and use it in GitHub Desktop.
ZerothAngel's From the Depths Naval AI with formation evasion + dodging. Untested.
-- Generated from ZerothAngel's FtDScripts version b61ea82122c5
-- Modules: naval-aimain, naval-ai, waypointmove, quadraticintercept, avoidance, avoidancevectors, commonsfriends, dodgeyaw, dodgecommon, raysphereintersect, quadraticsolver, commonswarnings, evasion, getbearingtopoint, planarvector, commonstargets, ytdefaults, sixdof, pid, requestcontrol, clamp, propulsionapi, componenttypes, rollturn, sign, getvectorangle, normalizebearing, periodic, firstrun, control, commons, shallowcopy
-- MIT licensed. See https://github.com/ZerothAngel/FtDScripts for raw code.
-- !!! Leading whitespace stripped to save bytes !!!
-- Meant for any vehicle with yaw + propulsion control.
-- If it is a submarine, depth must be controlled some other means (or
-- see my submarine.lua script for hydrofoil-based vehicles).
-- If it is an airship, see my airship.lua script instead.
-- CONFIGURATION
-- Activate on these AI modes. Valid keys are "off", "on", "combat",
-- "patrol", and "fleetmove".
ActivateWhen = {
-- on = true,
combat = true,
fleetmove = true,
}
-- How often to run. At 1, it will run every update. At 10,
-- it will run every 10th update. The lower it is, the more
-- responsive it will be, but it will also take more processing time.
UpdateRate = 4
-- NAVAL AI
-- Target ranges that determine behavior
MinDistance = 300
MaxDistance = 500
-- Attack behavior (MinDistance < target range < MaxDistance)
-- All angles are relative to the target's bearing. So 0 heads
-- straight toward the target and 180 is straight away from it.
AttackAngle = 80
-- Drive fraction from -1 to 1.
-- If you have an urge to set this to 0, look instead at the
-- AttackDistance + AttackReverse options.
AttackDrive = 1
-- Evasion settings have two values:
-- The magnitude of evasive maneuvers, in degrees
-- And a time scale. Generally <1.0 works best,
-- smaller means slower.
-- Set to nil to disable, e.g. AttackEvasion = nil
AttackEvasion = { 10, .125 }
-- If set, it should be between MinDistance and MaxDistance.
-- Only applies when MinDistance < target range < MaxDistance.
-- To have any effect, AttackAngle should NOT be 90 degrees.
AttackDistance = nil
-- PID for adjusting AttackAngle when near AttackDistance.
-- Effective AttackAngle will be scaled between AttackAngle and
-- 180 - AttackAngle depending on output of PID.
AttackPIDConfig = {
Kp = .5,
Ti = 5,
Td = .1,
}
-- If true, then instead of flipping AttackAngle, AttackDrive will
-- be negated (so presumably your vehicle will go in reverse). The PID
-- above will be applied to the throttle instead.
AttackReverse = false
-- Closing behavior (target range > MaxDistance)
-- ClosingAngle should be <90 to actually close with the target.
ClosingAngle = 40
ClosingDrive = 1
ClosingEvasion = { 30, .25 }
-- Escape behavior (target range < MinDistance)
-- EscapeAngle should be >90 to actually head away from the target.
EscapeAngle = 120
EscapeDrive = 1
EscapeEvasion = { 20, .25 }
-- Air raid evasion
-- Overrides current evasion settings when target is
-- above a certain elevation (i.e. altitude relative to ground)
AirRaidAboveElevation = 50
AirRaidEvasion = { 40, .25 }
-- Preferred side to face toward enemy.
-- 1 = Starboard (right)
-- -1 = Port (left)
-- nil = No preference (will pick closest side)
PreferredBroadside = nil
-- If true, the ship will perform attack runs and bounce between
-- MinDistance and MaxDistance.
-- "Closing" settings are used if target range > MaxDistance
-- "Attack" settings are used until MinDistance is reached
-- "Escape" settings are then used until MaxDistance is reached,
-- then the next attack run is started.
AttackRuns = false
-- Forces an attack run after this many seconds. For times when
-- the target is faster than your ship, so your ship won't be
-- stuck constantly trying to escape.
ForceAttackTime = 30
-- Normally the attack run is ended once MinDistance is reached.
-- However this setting ensures that at least this many seconds were spent
-- attacking (either normally or forced). Useful against faster targets...
-- I guess. For best results, should take into account the time it takes to
-- rotate to the proper angle.
MinAttackTime = 10
-- Evasion values to use when in formation mode.
-- Can either explicitly set values or just copy one of the above.
FleetMoveEvasion = AttackEvasion
-- Return-to-origin settings
ReturnToOrigin = true
-- WAYPOINT MOVE SETTINGS
WaypointMoveConfig = {
-- Maximum distance from stationary waypoints.
-- If further than this, set throttle to ClosingDrive (below).
-- Should be comparable to ship's turning radius.
MaxDistance = 250,
-- If farther away than this from a moving waypoint, set throttle
-- to ClosingDrive (below).
ApproachDistance = 100,
-- Throttle when distance from waypoint is >ApproachDistance.
ClosingDrive = 1,
-- Speed relative to waypoint in meters per second when within
-- ApproachDistance. Our speed will be the waypoint speed +/- this.
RelativeApproachSpeed = 1,
-- Minimum speed in meters per second when within ApproachDistance.
-- Probably not a good idea for hydrofoil-based subs to stop.
-- If nil, it will use the script's default, which is 0 in most cases.
MinimumSpeed = nil,
-- Whether or not to stop at stationary waypoints (i.e. the 'M' map
-- waypoint when this vehicle is the flagship). If false, then the
-- MinimumSpeed will be used instead.
-- If nil, it will use the script's default, which is usually "true".
StopOnStationaryWaypoint = nil,
-- Constants for throttle PID, used when within ApproachDistance.
ThrottlePIDConfig = {
Kp = .01,
Ti = 0,
Td = .125,
},
}
-- AVOIDANCE CONFIGURATION
-- How much clearance to require above and below when avoiding
-- terrain and friendlies.
-- Ship's height is multiplied by this and then centered around
-- the ship's physical midpoint (not necessarily the center of mass).
-- ClearanceFactor of 1 means to check exactly the ship's lower
-- and upper bounds.
-- Rotations (pitch, roll) are not accounted for, so it's best to
-- have some padding.
ClearanceFactor = 1.1
-- FRIENDLY AVOIDANCE
-- Distance to check for friendlies. Friendlies outside these distances
-- are ignored. The minimum distance is useful for ignoring docked ships.
FriendlyCheckMinDistance = 25
FriendlyCheckMaxDistance = 500
-- Each of the following has two numbers:
-- The first is a duration of time in seconds. If a collision
-- looks imminent within this time, it will turn away.
-- The second is the absolute minimum distance to be from friendlies.
FriendlyAvoidanceCombat = { 20, 100 }
FriendlyAvoidanceIdle = { 10, 100 }
-- Friendly avoidance weight. Generally should be >1.
-- Set to 0 to disable friendly avoidance.
-- Greater number means it will begin to turn away sooner.
FriendlyAvoidanceWeight = 10
-- TERRAIN AVOIDANCE
-- How many seconds ahead (at current speed) to sample
-- the terrain.
-- At minimum, it should be the time it takes for your ship to
-- complete a 90-degree turn.
LookAheadTime = 20
-- Look-ahead resolution, in meters. The smaller it is, the more
-- samples of the terrain will be taken.
-- Probably shouldn't be larger than the length of your ship.
-- Set to nil to automatically use a quarter of your ship's length.
LookAheadResolution = nil
-- When there's an obstacle in front, this is how far (in degrees)
-- left and right to check for an opening. Should probably be <45
LookAheadAngle = 30
-- By default, the horizontal midpoint and both sides of the ship
-- (after accounting for ClearanceFactor) are extended forward
-- and used to check the terrain. If you have a particularly
-- wide ship, you may want to check more points. Use this to
-- increase the number of subdivisions between the midpoint and
-- the sides. Set to 0 for no extra points, 1 for 1 extra
-- between midpoint and side (so 2 additional total), etc.
-- Don't go too crazy, because it will increase the number of
-- terrain checks dramatically.
TerrainAvoidanceSubdivisions = 0
-- Terrain avoidance weight. Should be >1, set to 0 to disable.
-- Greater number means it will begin to turn away sooner.
TerrainAvoidanceWeight = 100
-- DODGE SETTINGS (yaw-only)
-- If true, the vehicle will direct the impacts to its front rather than
-- steering into (or away).
DodgePreferFront = false
-- If supported by the AI, the drive will be set to this value when
-- dodging. Otherwise if nil, it will continue along the direction it
-- was already going.
DodgeDrive = nil
-- DODGE SETTINGS
-- Vehicle radius for determining the size of the defensive sphere.
-- Any missiles predicted to impact the sphere will cause an evasive
-- maneuver to dodge.
-- Set to nil to use the vehicle's longest (half) dimension multiplied
-- by the padding factor below.
VehicleRadius = nil
-- Only used when VehicleRadius is nil. Multiplied against half of the
-- longest dimension of the vehicle to set VehicleRadius.
VehicleRadiusPadding = 2
-- Whether or not dodging is enabled.
DodgingEnabled = true
-- Only consider missiles that are predicted to impact within this
-- number of seconds.
DodgeTimeHorizon = 10
-- COMMONS MISSILE WARNINGS SETTING
CommonsWarningConfig = {
-- Mainframe to use for missile warnings.
MissileWarningMainframe = 0,
}
-- COMMONS TARGETING SETTINGS
CommonsTargetConfig = {
-- Preferred mainframe indexes for target priority.
-- Once a mainframe is successfully queried, the rest are ignored.
-- First mainframe (in "C" screen) = 0, 2nd = 1, etc.
-- The last one in the list should always be 0.
-- For example, to query the 2nd, 3rd, and 1st (in that order), set
-- to { 1, 2, 0 }
PreferredTargetMainframes = { 0 },
-- Maximum range for an enemy to still be considered a threat.
MaxEnemyRange = 10000,
}
-- CONTROL CONFIGURATION
-- This section enables/disables the control of each axis
-- using a certain means: jets, dediblade spinners, or vehicle controls.
-- The defaults here are suitable for standard vehicle yaw/throttle
-- controls and can be augmented for dediblade spinners (for both yaw
-- and/or propulsion).
-- Generally, if a script includes these defaults, then altitude/
-- pitch/roll are controlled by some other way (hydrofoils, pumps, etc.)
-- and those fractions here should always be 0.
-- Control fractions dedicated to jets & spinners for each axis
JetFractions = {
Altitude = 0,
Yaw = 0,
Pitch = 0,
Roll = 0,
Forward = 0,
Right = 0,
}
SpinnerFractions = {
Altitude = 0,
Yaw = 0, -- Set to positive number to enable dediblade yaw
Pitch = 0,
Roll = 0,
Forward = 0, -- Set to positive number to enable dediblade propulsion
Right = 0,
}
-- Control fractions dedicated to vehicle controls for each axis
-- Note that this clashes with JetFractions. Where a clash is
-- indicated, the corresponding axes in JetFractions should be zeroed.
ControlFractions = {
-- Clashes with JetFractions.Yaw, Forward, AND Right
Yaw = 1,
-- Clashes with JetFractions.Altitude, Pitch, AND Roll
Pitch = 0,
-- Clashes with JetFractions.Altitude, Pitch, AND Roll
Roll = 0,
-- Clashes with JetFractions.Yaw, Forward, AND Right
Forward = 1,
}
-- 6DOF CONFIGURATION
-- PID values
SixDoFPIDConfig = {
Altitude = {
Kp = 5,
Ti = 5,
Td = .3,
},
Yaw = {
Kp = .3,
Ti = 5,
Td = .4,
},
Pitch = {
Kp = .5,
Ti = 5,
Td = .1,
},
Roll = {
Kp = .5,
Ti = 5,
Td = .1,
},
Forward = {
Kp = .5,
Ti = 5,
Td = .1,
},
Right = {
Kp = .5,
Ti = 5,
Td = .1,
},
}
-- Spinner configuration
-- The dediblade "always up" feature requires special handling.
-- If you set the following to true, then ALL upward- and downward-
-- facing dediblades should have "always up" set to 1.
-- Use of "always up" is generally not recommended.
DediBladesAlwaysUp = false
-- THRUST HACK CONFIGURATION
-- Use thrust hack instead of standard Lua control of thrusters.
-- Select a complex controller key for each related axes.
-- All related jets should be bound to that key as a green input.
-- See Lua box help > Propulsion > RequestComplexControllerStimulus
-- for key mapping.
-- Complex controller key for altitude/pitch/roll
-- (upward- and downward-facing thrusters)
APRThrustHackKey = nil
-- Complex controller key for yaw/longitudinal/lateral
-- (forward-, reverse-, left-, and right-facing thrusters)
YLLThrustHackKey = nil
-- ROLL TURN CONFIGURATION
RollTurn = {
-- Set to a number to enable banked turns.
-- If the difference between desired heading and current heading
-- exceeds this number, the vehicle will attempt to roll.
AngleBeforeRoll = nil,
-- Maximum roll angle to perform during banked turn.
MaxRollAngle = 30,
-- To have the roll angle scale based on the magnitude of the relative
-- bearing, set the scaling factor here.
-- To always use MaxRollAngle, set RollAngleGain to nil.
RollAngleGain = nil,
}
-- COMPONENT TYPES (don't change)
-- luacheck: push ignore 131
BALLOON = 0 -- boolean
DRIVEMAINTAINER = 1 -- float
AIRPUMP = 2 -- float/bool
RESOURCEGATHERER = 3 -- boolean
OILDRILL = 4 -- boolean
AMMOPROCESSOR = 5 -- boolean
OILPROCESSOR = 6 -- boolean
TRACTORBEAM = 7 -- boolean
HYDROFOIL = 8 -- float
PROPULSION = 9 -- float
SHIELDPROJECTOR = 10 -- float/int
HELIUMPUMP = 11 -- float/bool
-- luacheck: pop
-- PROPULSION API CONSTANTS (don't change)
-- luacheck: push ignore 131
WATER = 0
LAND = 1
AIR = 2
YAWLEFT = 0
YAWRIGHT = 1
ROLLLEFT = 2
ROLLRIGHT = 3
NOSEUP = 4
NOSEDOWN = 5
INCREASE = 6
DECREASE = 7
MAINPROPULSION = 8
-- luacheck: pop
-- Control mode. Set to WATER, LAND, or AIR.
Mode = WATER
-- !!! CODE BEGINS HERE, EDIT AT YOUR OWN RISK !!!
function shallowcopy(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in pairs(orig) do
copy[orig_key] = orig_value
end
else -- number, string, boolean, etc
copy = orig
end
return copy
end
C = nil
Commons = {}
function Commons.new(I, AttackSalvage)
local self = shallowcopy(Commons)
self.I = I
self.AttackSalvage = AttackSalvage
self._FriendliesById = {}
return self
end
function Commons:Now()
if not self._Now then
self._Now = self.I:GetTimeSinceSpawn()
end
return self._Now
end
function Commons:IsDocked()
if not self._IsDocked then
self._IsDocked = self.I:IsDocked()
end
return self._IsDocked
end
function Commons:Position()
if not self._Position then
self._Position = self.I:GetConstructPosition()
end
return self._Position
end
function Commons:CoM()
if not self._CoM then
self._CoM = self.I:GetConstructCenterOfMass()
end
return self._CoM
end
function Commons:Altitude()
return self:CoM().y
end
function Commons:Yaw()
if not self._Yaw then
self._Yaw = self.I:GetConstructYaw()
end
return self._Yaw
end
function Commons:Pitch()
if not self._Pitch then
self._Pitch = -self.I:GetConstructPitch()
end
return self._Pitch
end
function Commons:Roll()
if not self._Roll then
local Roll = self.I:GetConstructRoll()
if Roll > 180 then
Roll = Roll - 360
end
self._Roll = Roll
end
return self._Roll
end
function Commons:Velocity()
if not self._Velocity then
self._Velocity = self.I:GetVelocityVector()
end
return self._Velocity
end
function Commons:ForwardSpeed()
if not self._ForwardSpeed then
self._ForwardSpeed = self.I:GetForwardsVelocityMagnitude()
end
return self._ForwardSpeed
end
function Commons:ForwardVector()
if not self._ForwardVector then
self._ForwardVector = self.I:GetConstructForwardVector()
end
return self._ForwardVector
end
function Commons:UpVector()
if not self._UpVector then
self._UpVector = self.I:GetConstructUpVector()
end
return self._UpVector
end
function Commons:RightVector()
if not self._RightVector then
self._RightVector = self.I:GetConstructRightVector()
end
return self._RightVector
end
function Commons:ToWorld()
if not self._ToWorld then
self._ToWorld = Quaternion.LookRotation(self:ForwardVector(), self:UpVector())
end
return self._ToWorld
end
function Commons:ToLocal()
if not self._ToLocal then
self._ToLocal = Quaternion.Inverse(self:ToWorld())
end
return self._ToLocal
end
function Commons:Ground()
if not self._Ground then
self._Ground = math.max(0, self.I:GetTerrainAltitudeForPosition(self:CoM()))
end
return self._Ground
end
V = {}
function Vehicle_MakeReset(Table)
local ResetHeading,ResetThrottle,ResetPosition = Table.ResetHeading,Table.ResetThrottle,Table.ResetPosition
function Table.Reset()
if ResetHeading then ResetHeading() end
if ResetThrottle then ResetThrottle() end
if ResetPosition then ResetPosition() end
end
end
function SelectAltitudeImpl(Source, Table) -- luacheck: ignore 131
Table = Table or V
Table.SetAltitude = Source.SetAltitude
function Table.AdjustAltitude(Delta)
Table.SetAltitude(C:Altitude() + Delta)
end
end
function SelectHeadingImpl(Source, Table) -- luacheck: ignore 131
Table = Table or V
Table.SetHeading = Source.SetHeading
Table.ResetHeading = Source.ResetHeading
function Table.AdjustHeading(Bearing)
Table.SetHeading(C:Yaw() + Bearing)
end
Vehicle_MakeReset(Table)
end
function SelectThrottleImpl(Source, Table) -- luacheck: ignore 131
Table = Table or V
Table.SetThrottle = Source.SetThrottle
Table.GetThrottle = Source.GetThrottle
Table.ResetThrottle = Source.ResetThrottle
function Table.AdjustThrottle(Delta)
Table.SetThrottle(Table.GetThrottle() + Delta)
end
Vehicle_MakeReset(Table)
end
function SelectPositionImpl(Source, Table) -- luacheck: ignore 131
Table = Table or V
Table.SetPosition = Source.SetPosition
Table.ResetPosition = Source.ResetPosition
function Table.AdjustPosition(Offset)
Table.SetPosition(C:CoM() + Offset)
end
Vehicle_MakeReset(Table)
end
function SelectPitchImpl(Source, Table) -- luacheck: ignore 131
Table = Table or V
Table.SetPitch = Source.SetPitch
end
function SelectRollImpl(Source, Table) -- luacheck: ignore 131
Table = Table or V
Table.SetRoll = Source.SetRoll
end
FirstRunList = {}
function FirstRun(I)
FirstRun = function (_) end
for _,Function in pairs(FirstRunList) do
Function(I)
end
end
function AddFirstRun(Function)
table.insert(FirstRunList, Function)
end
Periodic = {}
function Periodic.new(Period, Function, Start)
local self = {}
self.Ticks = Start and Start or Period
self.Period = Period
self.Function = Function
if Period then
self.Tick = Periodic.Tick
else
self.Tick = function (_, _) end
end
return self
end
function Periodic:Tick(I)
local Ticks = self.Ticks
Ticks = Ticks + 1
if Ticks >= self.Period then
self.Ticks = 0
self.Function(I)
else
self.Ticks = Ticks
end
end
function NormalizeBearing(Bearing)
Bearing = Bearing % 360
if Bearing > 180 then Bearing = Bearing - 360 end
return Bearing
end
function GetVectorAngle(v)
return math.deg(math.atan2(v.x, v.z))
end
function Sign(n, ZeroSign, Eps)
Eps = Eps or 0
if n > Eps then
return 1
elseif n < -Eps then
return -1
else
return ZeroSign or 0
end
end
RollTurnControl = {}
function RollTurn.SetHeading(Heading)
local AngleBeforeRoll = RollTurn.AngleBeforeRoll
if AngleBeforeRoll then
Heading = Heading % 360
local Bearing = NormalizeBearing(Heading - GetVectorAngle(C:ForwardVector()))
local AbsBearing = math.abs(Bearing)
local MaxRollAngle,RollAngleGain = RollTurn.MaxRollAngle,RollTurn.RollAngleGain
if AbsBearing > AngleBeforeRoll then
local RollAngle = RollAngleGain and math.min(MaxRollAngle, (AbsBearing - AngleBeforeRoll) * RollAngleGain) or MaxRollAngle
RollTurnControl.SetRoll(-Sign(Bearing) * RollAngle)
else
RollTurnControl.SetRoll(0)
end
end
RollTurnControl.SetHeading(Heading)
end
function RollTurn.ResetHeading()
RollTurnControl.ResetHeading()
RollTurnControl.SetRoll(0)
end
function RollTurn.SetRoll(Angle)
if not RollTurn.AngleBeforeRoll then
RollTurnControl.SetRoll(Angle)
end
end
function Clamp(n, Min, Max)
if n < Min then return Min elseif n > Max then return Max else return n end
end
function MakeRequestControl(Scale)
Scale = Scale or 1
return function (I, Fraction, PosControl, NegControl, CV)
if Fraction > 0 then
CV = Clamp(Fraction * CV * Scale, -1, 1)
if PosControl ~= NegControl then
if CV > 0 then
I:RequestControl(Mode, PosControl, CV)
elseif CV < 0 then
I:RequestControl(Mode, NegControl, -CV)
end
else
I:RequestControl(Mode, PosControl, CV)
end
end
end
end
PID = {}
function PID.new(Config, Min, Max)
local self = {}
self.Kp = Config.Kp
if Config.Ti ~= 0 then
self.Ki = Config.Kp / Config.Ti
else
self.Ki = 0
end
self.Kd = Config.Kp * Config.Td
self.Integral = 0.0
self.LastError = 0.0
self.Min = Min
self.Max = Max
self.Control = PID.FirstControl
return self
end
function PID:Control(Error)
local Now = C:Now()
local dt = Now - self.LastTime
local Integral = self.Integral + Error * dt
local Derivative = (Error - self.LastError) / dt
self.LastError = Error
self.LastTime = Now
local CV = (self.Kp * Error) +
(self.Ki * Integral) +
(self.Kd * Derivative)
if CV > self.Max then
if Integral <= self.Integral then self.Integral = Integral end
return self.Max
elseif CV < self.Min then
if Integral >= self.Integral then self.Integral = Integral end
return self.Min
end
self.Integral = Integral
return CV
end
function PID:FirstControl(_)
self.Control = PID.Control
self.LastTime = C:Now()
return 0
end
SixDoF_AltitudePID = PID.new(SixDoFPIDConfig.Altitude, -30, 30)
SixDoF_YawPID = PID.new(SixDoFPIDConfig.Yaw, -30, 30)
SixDoF_PitchPID = PID.new(SixDoFPIDConfig.Pitch, -30, 30)
SixDoF_RollPID = PID.new(SixDoFPIDConfig.Roll, -30, 30)
SixDoF_ForwardPID = PID.new(SixDoFPIDConfig.Forward, -30, 30)
SixDoF_RightPID = PID.new(SixDoFPIDConfig.Right, -30, 30)
SixDoF_DesiredAltitude = 0
SixDoF_DesiredHeading = nil
SixDoF_DesiredPosition = nil
SixDoF_DesiredThrottle = nil
SixDoF_CurrentThrottle = 0
SixDoF_DesiredPitch = 0
SixDoF_DesiredRoll = 0
SixDoF_LastPropulsionCount = 0
SixDoF_PropulsionInfos = {}
SixDoF_LastSpinnerCount = 0
SixDoF_SpinnerInfos = {}
SixDoF_UsesHorizontalJets = (JetFractions.Yaw > 0 or JetFractions.Forward > 0 or JetFractions.Right > 0)
SixDoF_UsesVerticalJets = (JetFractions.Altitude > 0 or JetFractions.Pitch > 0 or JetFractions.Roll > 0)
SixDoF_UsesJets = SixDoF_UsesHorizontalJets or SixDoF_UsesVerticalJets
SixDoF_UsesSpinners = (SpinnerFractions.Altitude > 0 or SpinnerFractions.Yaw > 0 or SpinnerFractions.Pitch > 0 or SpinnerFractions.Roll > 0 or SpinnerFractions.Forward > 0 or SpinnerFractions.Right > 0)
SixDoF_UsesControls = (ControlFractions.Yaw > 0 or ControlFractions.Pitch > 0 or ControlFractions.Roll > 0 or ControlFractions.Forward > 0)
SixDoF_ControlAltitude = (JetFractions.Altitude > 0 or SpinnerFractions.Altitude > 0)
SixDoF_ControlPitch = (JetFractions.Pitch > 0 or SpinnerFractions.Pitch > 0 or ControlFractions.Pitch > 0)
SixDoF_ControlRoll = (JetFractions.Roll > 0 or SpinnerFractions.Roll > 0 or ControlFractions.Roll > 0)
SixDoF_NeedsRelease = false
SixDoF = {}
function SixDoF.SetAltitude(Alt)
SixDoF_DesiredAltitude = Alt
end
function SixDoF.SetHeading(Heading)
SixDoF_DesiredHeading = Heading % 360
end
function SixDoF.ResetHeading()
SixDoF_DesiredHeading = nil
end
function SixDoF.SetPosition(Pos)
SixDoF_DesiredPosition = Vector3(Pos.x, Pos.y, Pos.z)
end
function SixDoF.ResetPosition()
SixDoF_DesiredPosition = nil
end
function SixDoF.SetThrottle(Throttle)
SixDoF_DesiredThrottle = Clamp(Throttle, -1, 1)
end
function SixDoF.GetThrottle()
return SixDoF_CurrentThrottle
end
function SixDoF.ResetThrottle()
SixDoF_DesiredThrottle = nil
end
function SixDoF.SetPitch(Angle)
SixDoF_DesiredPitch = Angle
end
function SixDoF.SetRoll(Angle)
SixDoF_DesiredRoll = Angle
end
SixDoF_Eps = .001
function SixDoF_Classify(Index, BlockInfo, IsSpinner, Fractions, Infos)
local CoMOffset = BlockInfo.LocalPositionRelativeToCom
local LocalForwards = IsSpinner and (BlockInfo.LocalRotation * Vector3.up) or BlockInfo.LocalForwards
local Info = {
Index = Index,
UpSign = 0,
YawSign = 0,
PitchSign = 0,
RollSign = 0,
ForwardSign = 0,
RightSign = 0,
IsVertical = false,
}
local UpSign = Sign(LocalForwards.y, 0, SixDoF_Eps)
if UpSign ~= 0 then
Info.UpSign = UpSign * Fractions.Altitude
Info.PitchSign = Sign(CoMOffset.z) * UpSign * Fractions.Pitch
Info.RollSign = Sign(CoMOffset.x) * UpSign * Fractions.Roll
Info.IsVertical = true
else
local ForwardSign = Sign(LocalForwards.z, 0, SixDoF_Eps)
local RightSign = Sign(LocalForwards.x, 0, SixDoF_Eps)
Info.YawSign = RightSign * Sign(CoMOffset.z) * Fractions.Yaw
Info.ForwardSign = ForwardSign * Fractions.Forward
Info.RightSign = RightSign * Fractions.Right
end
if Info.UpSign ~= 0 or Info.PitchSign ~= 0 or Info.RollSign ~= 0 or Info.YawSign ~= 0 or Info.ForwardSign ~= 0 or Info.RightSign ~= 0 then
table.insert(Infos, Info)
end
end
function SixDoF_ClassifyJets(I)
local PropulsionCount = I:Component_GetCount(PROPULSION)
if PropulsionCount ~= SixDoF_LastPropulsionCount then
SixDoF_LastPropulsionCount = PropulsionCount
SixDoF_PropulsionInfos = {}
for i = 0,PropulsionCount-1 do
local BlockInfo = I:Component_GetBlockInfo(PROPULSION, i)
SixDoF_Classify(i, BlockInfo, false, JetFractions, SixDoF_PropulsionInfos)
end
end
end
function SixDoF_ClassifySpinners(I)
local SpinnerCount = I:GetDedibladeCount()
if SpinnerCount ~= SixDoF_LastSpinnerCount then
SixDoF_LastSpinnerCount = SpinnerCount
SixDoF_SpinnerInfos = {}
for i = 0,SpinnerCount-1 do
local BlockInfo = I:GetDedibladeInfo(i)
SixDoF_Classify(i, BlockInfo, true, SpinnerFractions, SixDoF_SpinnerInfos)
end
if DediBladesAlwaysUp then
for _,Info in pairs(SixDoF_SpinnerInfos) do
local UpSign = Info.UpSign
if UpSign < 0 then
Info.UpSign = -UpSign
Info.PitchSign = -Info.PitchSign
Info.RollSign = -Info.RollSign
end
end
end
end
end
SixDoF_RequestControl = MakeRequestControl(1/30)
function SixDoF.Update(I)
local AltitudeCV = 0
if SixDoF_ControlAltitude then
local AltitudeDelta = SixDoF_DesiredAltitude - C:Altitude()
if not DediBladesAlwaysUp then
AltitudeDelta = AltitudeDelta * C:UpVector().y
end
AltitudeCV = SixDoF_AltitudePID:Control(AltitudeDelta)
end
local YawCV = SixDoF_DesiredHeading and SixDoF_YawPID:Control(NormalizeBearing(SixDoF_DesiredHeading - C:Yaw())) or 0
local PitchCV = SixDoF_ControlPitch and SixDoF_PitchPID:Control(SixDoF_DesiredPitch - C:Pitch()) or 0
local RollCV = SixDoF_ControlRoll and SixDoF_RollPID:Control(SixDoF_DesiredRoll - C:Roll()) or 0
local ForwardCV,RightCV = 0,0
if SixDoF_DesiredPosition then
local Offset = SixDoF_DesiredPosition - C:CoM()
local ZProj = Vector3.Dot(Offset, C:ForwardVector())
local XProj = Vector3.Dot(Offset, C:RightVector())
ForwardCV = SixDoF_ForwardPID:Control(ZProj)
RightCV = SixDoF_RightPID:Control(XProj)
elseif SixDoF_DesiredThrottle then
ForwardCV = 30 * SixDoF_DesiredThrottle -- PID is scaled, so scale up
SixDoF_CurrentThrottle = SixDoF_DesiredThrottle
end
local PlanarMovement = SixDoF_DesiredHeading or SixDoF_DesiredPosition or SixDoF_DesiredThrottle
if SixDoF_UsesJets then
SixDoF_ClassifyJets(I)
if SixDoF_UsesHorizontalJets and PlanarMovement then
if not YLLThrustHackKey then
for i = 0,3 do
I:RequestThrustControl(i)
end
else
I:RequestComplexControllerStimulus(YLLThrustHackKey)
end
end
if SixDoF_UsesVerticalJets then
if not APRThrustHackKey then
I:RequestThrustControl(4)
I:RequestThrustControl(5)
else
I:RequestComplexControllerStimulus(APRThrustHackKey)
end
end
for _,Info in pairs(SixDoF_PropulsionInfos) do
if Info.IsVertical or PlanarMovement then
local Output = Clamp(AltitudeCV * Info.UpSign + YawCV * Info.YawSign + PitchCV * Info.PitchSign + RollCV * Info.RollSign + ForwardCV * Info.ForwardSign + RightCV * Info.RightSign, 0, 30)
I:Component_SetFloatLogic(PROPULSION, Info.Index, Output / 30)
else
I:Component_SetFloatLogic(PROPULSION, Info.Index, 1)
end
end
end
if SixDoF_UsesSpinners then
SixDoF_ClassifySpinners(I)
for _,Info in pairs(SixDoF_SpinnerInfos) do
if Info.IsVertical or PlanarMovement then
local Output = Clamp(AltitudeCV * Info.UpSign + YawCV * Info.YawSign + PitchCV * Info.PitchSign + RollCV * Info.RollSign + ForwardCV * Info.ForwardSign + RightCV * Info.RightSign, -30, 30)
I:SetDedibladeContinuousSpeed(Info.Index, Output)
end
end
end
if SixDoF_UsesControls then
SixDoF_RequestControl(I, ControlFractions.Pitch, NOSEUP, NOSEDOWN, PitchCV)
SixDoF_RequestControl(I, ControlFractions.Roll, ROLLLEFT, ROLLRIGHT, RollCV)
if PlanarMovement then
SixDoF_RequestControl(I, ControlFractions.Yaw, YAWRIGHT, YAWLEFT, YawCV * Sign(C:ForwardSpeed(), 1))
SixDoF_RequestControl(I, ControlFractions.Forward, MAINPROPULSION, MAINPROPULSION, ForwardCV)
end
end
if PlanarMovement then
SixDoF_NeedsRelease = true
end
end
function SixDoF.Disable(I)
SixDoF_CurrentThrottle = 0
if SixDoF_UsesSpinners then
SixDoF_ClassifySpinners(I)
for _,Info in pairs(SixDoF_SpinnerInfos) do
I:SetDedibladeContinuousSpeed(Info.Index, 0)
end
end
if SixDoF_UsesControls then
SixDoF_RequestControl(I, ControlFractions.Forward, MAINPROPULSION, MAINPROPULSION, 0)
end
end
function SixDoF.Release(I)
if SixDoF_NeedsRelease then
if SixDoF_UsesSpinners then
SixDoF_ClassifySpinners(I)
for _,Info in pairs(SixDoF_SpinnerInfos) do
if not Info.IsVertical then
I:SetDedibladeContinuousSpeed(Info.Index, 0)
end
end
end
SixDoF_NeedsRelease = false
end
end
function CommonsTarget_Ground(self, I)
if not self._Ground then
self._Ground = math.max(0, I:GetTerrainAltitudeForPosition(self.AimPoint))
end
return self._Ground
end
function CommonsTarget_Elevation(self, I)
if not self._Elevation then
self._Elevation = self.AimPoint.y - self:Ground(I)
end
return self._Elevation
end
function Commons.ConvertTarget(Index, TargetInfo, Offset, Range)
local Target = {
Id = TargetInfo.Id,
Index = Index,
Position = TargetInfo.Position,
Offset = Offset,
Range = Range,
SqrRange = Range * Range,
AimPoint = TargetInfo.AimPointPosition,
Velocity = TargetInfo.Velocity,
Ground = CommonsTarget_Ground,
Elevation = CommonsTarget_Elevation,
}
return Target
end
function Commons:GatherTargets(Targets, StartIndex, MaxTargets)
local CoM = self:CoM()
local AttackSalvage = self.AttackSalvage
for _,mindex in ipairs(CommonsTargetConfig.PreferredTargetMainframes) do
local TargetCount = self.I:GetNumberOfTargets(mindex)
if TargetCount > 0 then
if not StartIndex then StartIndex = 0 end
if not MaxTargets then MaxTargets = math.huge end
for tindex = StartIndex,TargetCount-1 do
if #Targets >= MaxTargets then break end
local TargetInfo = self.I:GetTargetInfo(mindex, tindex)
if TargetInfo.Valid and (TargetInfo.Protected or AttackSalvage) then
local Offset = TargetInfo.Position - CoM
local Range = Offset.magnitude
if Range <= CommonsTargetConfig.MaxEnemyRange then
table.insert(Targets, Commons.ConvertTarget(tindex, TargetInfo, Offset, Range))
end
end
end
break
end
end
end
function Commons:FirstTarget()
if not self._FirstTarget then
if self._Targets then
local Target = self._Targets[1]
self._FirstTarget = Target and { Target } or {}
else
local Targets = {}
self:GatherTargets(Targets, 0, 1)
self._FirstTarget = Targets
end
end
return self._FirstTarget[1]
end
function Commons:Targets()
if not self._Targets then
local Targets = {}
if self._FirstTarget then
local Target = self._FirstTarget[1]
if not Target then
self._Targets = {}
return self._Targets
end
table.insert(Targets, Target)
self:GatherTargets(Targets, Target.Index+1)
else
self:GatherTargets(Targets, 0)
end
self._Targets = Targets
end
return self._Targets
end
function PlanarVector(Origin, Target)
local NewTarget = Vector3(Target.x, Origin.y, Target.z)
return NewTarget - Origin, NewTarget
end
function GetBearingToPoint(Point)
local Offset = Point - C:CoM()
return Mathf.DeltaAngle(C:Yaw(), GetVectorAngle(Offset))
end
PerlinOffset = 0
function Evasion_FirstRun(_)
PerlinOffset = 1000.0 * math.random()
end
AddFirstRun(Evasion_FirstRun)
function CalculateEvasion(Evasion)
if Evasion then
return Evasion[1] * (2.0 * Mathf.PerlinNoise(Evasion[2] * C:Now(), PerlinOffset) - 1.0)
else
return 0
end
end
function Commons:MissileWarnings()
if not self._MissileWarnings then
local Mainframe = CommonsWarningConfig.MissileWarningMainframe
local Warnings = {}
for i = 0,self.I:GetNumberOfWarnings(Mainframe)-1 do
local Warning = self.I:GetMissileWarning(Mainframe, i)
if Warning.Valid then
local Info = {
Id = Warning.Id,
MainframeIndex = Mainframe, -- Only 1 mainframe for now...
Index = i,
Position = Warning.Position,
Velocity = Warning.Velocity,
Range = Warning.Range,
}
table.insert(Warnings, Info)
end
end
self._MissileWarnings = Warnings
end
return self._MissileWarnings
end
function QuadraticSolver(a, b, c)
if a == 0 then
if b ~= 0 then
return { -c / b }
else
return {} -- Division by zero...
end
else
local disc = b * b - 4 * a * c
if disc < 0 then
return {} -- Imaginary
elseif disc == 0 then
return { -.5 * b / a }
else
local q = (b > 0) and (-.5 * (b + math.sqrt(disc))) or (-.5 * (b - math.sqrt(disc)))
local t1 = q / a
local t2 = c / q
if t1 > t2 then
return { t2, t1 }
else
return { t1, t2 }
end
end
end
end
function RaySphereIntersect(Position, Velocity, RadiusSquared)
local a = Vector3.Dot(Velocity, Velocity)
local b = 2 * Vector3.Dot(Velocity, Position)
local c = Vector3.Dot(Position, Position) - RadiusSquared
local Solutions = QuadraticSolver(a, b, c)
local ImpactTime = nil
if #Solutions == 1 then
local t = Solutions[1]
if t > 0 then ImpactTime = t end
elseif #Solutions == 2 then
local t1 = Solutions[1]
local t2 = Solutions[2]
if t1 > 0 then
ImpactTime = t1
elseif t2 > 0 then
ImpactTime = t2
end
end
if ImpactTime then
local ImpactPoint = Position + Velocity * ImpactTime
return ImpactPoint, ImpactTime
else
return nil
end
end
LastDodgeDirection = nil
LastDodgeProjectile = nil
SqrVehicleRadius = nil
function Dodge_FirstRun(I)
if not VehicleRadius then
local MaxDim = I:GetConstructMaxDimensions()
local MinDim = I:GetConstructMinDimensions()
local Dimensions = MaxDim - MinDim
local HalfDimensions = Dimensions / 2
VehicleRadius = math.max(HalfDimensions.x, math.max(HalfDimensions.y, HalfDimensions.z))
VehicleRadius = VehicleRadius * VehicleRadiusPadding
end
SqrVehicleRadius = VehicleRadius * VehicleRadius
end
AddFirstRun(Dodge_FirstRun)
function CalculateDodge(Projectile)
local RelativePosition = Projectile.Position - C:CoM()
local RelativeVelocity = Projectile.Velocity - C:Velocity()
if Vector3.Dot(RelativePosition, RelativeVelocity) >= 0 then return nil end
local Range,Speed = RelativePosition.magnitude,RelativeVelocity.magnitude
local ImpactPoint,ImpactTime
if Range > VehicleRadius then
ImpactPoint,ImpactTime = RaySphereIntersect(RelativePosition, RelativeVelocity, SqrVehicleRadius)
if not ImpactPoint then return nil end
ImpactTime = ImpactTime + VehicleRadius / Speed
else
ImpactPoint = RelativePosition
ImpactTime = Range / Speed
end
ImpactPoint = C:ToLocal() * ImpactPoint
return { Sign(ImpactPoint.x, 1), Sign(ImpactPoint.y, 1), Sign(ImpactPoint.z, 1) },ImpactTime
end
function Dodge()
if DodgingEnabled then
local DodgeDirection,Soonest,ProjectileId = nil,math.huge,nil
for _,Projectile in pairs(C:MissileWarnings()) do
local Direction,ImpactTime = CalculateDodge(Projectile)
if Direction and ImpactTime <= DodgeTimeHorizon and ImpactTime < Soonest then
DodgeDirection = Direction
Soonest = ImpactTime
ProjectileId = Projectile.Id
end
end
if DodgeDirection then
if (not LastDodgeDirection or LastDodgeProjectile ~= ProjectileId or
DodgeDirection[1] ~= LastDodgeDirection[1] or
DodgeDirection[2] ~= LastDodgeDirection[2] or
DodgeDirection[3] ~= LastDodgeDirection[3]) then
LastDodgeDirection = DodgeDirection
LastDodgeProjectile = ProjectileId
end
return Dodge_LastDodge()
else
LastDodgeDirection = nil
LastDodgeProjectile = nil
end
end
return Dodge_NoDodge()
end
if not DodgePreferFront then
function Dodge_LastDodge()
return -45*LastDodgeDirection[1] * LastDodgeDirection[3],-LastDodgeDirection[2],true
end
else
function Dodge_LastDodge()
return 45*LastDodgeDirection[1],-LastDodgeDirection[2],true
end
end
function Dodge_NoDodge()
return 0,0,false
end
function Commons:Friendlies()
if not self._Friendlies then
local Friendlies = {}
for findex = 0,self.I:GetFriendlyCount()-1 do
local FriendlyInfo = self.I:GetFriendlyInfo(findex)
if FriendlyInfo.Valid then -- Pointless check?
table.insert(Friendlies, FriendlyInfo)
self._FriendliesById[FriendlyInfo.Id] = FriendlyInfo
end
end
self._Friendlies = Friendlies
end
return self._Friendlies
end
function Commons:FriendlyById(Id)
local FriendlyInfo = self._FriendliesById[Id]
if not FriendlyInfo then
FriendlyInfo = self.I:GetFriendlyInfoById(Id)
self._FriendliesById[Id] = FriendlyInfo
end
return FriendlyInfo
end
Avoidance_MidPoint = nil
Avoidance_VerticalClearance = 0
Avoidance_CheckPoints = {}
Avoidance_PreviousTAvoid = Vector3.right
function Avoidance_FirstRun(I)
local MaxDim = I:GetConstructMaxDimensions()
local MinDim = I:GetConstructMinDimensions()
local Dimensions = MaxDim - MinDim
local HalfDimensions = Dimensions / 2
Avoidance_MidPoint = MinDim + HalfDimensions -- Relative to Position and no rotation (local)
Avoidance_VerticalClearance = HalfDimensions.y * ClearanceFactor
local SideClearance = HalfDimensions.x * ClearanceFactor
table.insert(Avoidance_CheckPoints, 0)
table.insert(Avoidance_CheckPoints, -SideClearance)
table.insert(Avoidance_CheckPoints, SideClearance)
if TerrainAvoidanceSubdivisions > 0 then
local Delta = SideClearance / (TerrainAvoidanceSubdivisions+1)
for i=1,TerrainAvoidanceSubdivisions do
local x = i * Delta
table.insert(Avoidance_CheckPoints, -x)
table.insert(Avoidance_CheckPoints, x)
end
end
if not LookAheadResolution then
LookAheadResolution = HalfDimensions.z / 2
end
end
AddFirstRun(Avoidance_FirstRun)
function GetTerrainHits(I, Angle, LowerEdge, Speed)
local Hits = 0
local Rotation = Quaternion.Euler(0, Angle, 0) -- NB Angle is world
local MaxDistance = Speed * LookAheadTime
local Distances = {}
for d = 0,MaxDistance-1,LookAheadResolution do
table.insert(Distances, d)
end
table.insert(Distances, MaxDistance)
for _,Offset in pairs(Avoidance_CheckPoints) do
local Blocked = false
for _,Distance in ipairs(Distances) do
if Blocked then
Hits = Hits + 1
else
local TestPoint = C:CoM() + Rotation * Vector3(Offset, 0, Distance)
if I:GetTerrainAltitudeForPosition(TestPoint) >= LowerEdge then
Hits = Hits + 1
Blocked = true
end
end
end
end
return Hits
end
function FriendlyAvoidanceVector(UpperEdge, LowerEdge, Velocity)
local FCount,FAvoid = 0,Vector3.zero
if FriendlyAvoidanceWeight > 0 then
local AvoidanceTime,MinDistance
if C:FirstTarget() then
AvoidanceTime,MinDistance = unpack(FriendlyAvoidanceCombat)
else
AvoidanceTime,MinDistance = unpack(FriendlyAvoidanceIdle)
end
for _,Friend in pairs(C:Friendlies()) do
if (Friend.AxisAlignedBoundingBoxMinimum.y <= UpperEdge and
Friend.AxisAlignedBoundingBoxMaximum.y >= LowerEdge) then
local Offset,_ = PlanarVector(C:CoM(), Friend.CenterOfMass)
local Distance = Offset.magnitude
if Distance < FriendlyCheckMaxDistance and Distance > FriendlyCheckMinDistance then
local Direction = Offset / Distance -- aka Offset.normalized
local Collision = false
if Distance < MinDistance then
Collision = true
else
local RelativeVelocity = Velocity - Friend.Velocity
local RelativeSpeed = Vector3.Dot(RelativeVelocity, Direction)
if RelativeSpeed > 0.0 and Distance / RelativeSpeed < AvoidanceTime then
Collision = true
end
end
if Collision then
FCount = FCount + 1
FAvoid = FAvoid - Direction
end
end
end
end
if FCount > 0 then
local VNorm = Velocity.normalized
local Rej = FAvoid - VNorm * Vector3.Dot(FAvoid, VNorm)
if Rej.sqrMagnitude == 0 then
FAvoid = Vector3.Cross(Vector3.up, VNorm) * FriendlyAvoidanceWeight
else
FAvoid = Rej.normalized * FriendlyAvoidanceWeight
end
end
end
return FCount, FAvoid
end
function TerrainAvoidanceVector(I, LowerEdge, Velocity, Speed)
local TCount,TAvoid = 0,Vector3.zero
if TerrainAvoidanceWeight > 0 then
local VelocityAngle = GetVectorAngle(Velocity)
local ForwardHits = GetTerrainHits(I, VelocityAngle, LowerEdge, Speed)
if ForwardHits > 0 then
local LeftHits = GetTerrainHits(I, VelocityAngle-LookAheadAngle, LowerEdge, Speed)
local RightHits = GetTerrainHits(I, VelocityAngle+LookAheadAngle, LowerEdge, Speed)
if LeftHits < RightHits then
TAvoid = Vector3.left
elseif RightHits < LeftHits then
TAvoid = Vector3.right
else
TAvoid = Avoidance_PreviousTAvoid
end
Avoidance_PreviousTAvoid = TAvoid
TCount = ForwardHits + LeftHits + RightHits
TAvoid = Quaternion.Euler(0, VelocityAngle, 0) * TAvoid * TerrainAvoidanceWeight
else
Avoidance_PreviousTAvoid = Vector3.right
end
end
return TCount, TAvoid
end
function AvoidanceVectors(I)
local PositionY = C:Position().y + Avoidance_MidPoint.y -- Not necessarily Altitude
local UpperEdge = PositionY + Avoidance_VerticalClearance
local LowerEdge = PositionY - Avoidance_VerticalClearance
local Velocity = C:Velocity()
Velocity = Vector3(Velocity.x, 0, Velocity.z)
local Speed = Velocity.magnitude
local FCount, FAvoid = FriendlyAvoidanceVector(UpperEdge, LowerEdge, Velocity)
local TCount, TAvoid = TerrainAvoidanceVector(I, LowerEdge, Velocity, Speed)
if (FCount + TCount) == 0 then
return nil
else
return FAvoid + TAvoid
end
end
function Avoidance(I, Bearing)
local Avoid = AvoidanceVectors(I)
if not Avoid then
return Bearing
else
local NewTarget = Quaternion.Euler(0, C:Yaw()+Bearing, 0) * Vector3.forward
NewTarget = C:CoM() + NewTarget + Avoid
return GetBearingToPoint(NewTarget)
end
end
function QuadraticIntercept(Position, SpeedSquared, Target, TargetVelocity, DefaultInterceptTime)
if not DefaultInterceptTime then DefaultInterceptTime = 1 end
local Offset = Target - Position
local a = Vector3.Dot(TargetVelocity, TargetVelocity) - SpeedSquared -- aka difference of squares of speeds
local b = 2 * Vector3.Dot(Offset, TargetVelocity)
local c = Vector3.Dot(Offset, Offset) -- Offset.magnitude squared
local Solutions = QuadraticSolver(a, b, c)
local InterceptTime = DefaultInterceptTime
if #Solutions == 1 then
local t = Solutions[1]
if t > 0 then InterceptTime = t end
elseif #Solutions == 2 then
local t1 = Solutions[1]
local t2 = Solutions[2]
if t1 > 0 then
InterceptTime = t1
elseif t2 > 0 then
InterceptTime = t2
end
end
return Target + TargetVelocity * InterceptTime,InterceptTime
end
MTW_ThrottlePID = PID.new(WaypointMoveConfig.ThrottlePIDConfig, -1, 1)
if not WaypointMoveConfig.MinimumSpeed then WaypointMoveConfig.MinimumSpeed = 0 end
if WaypointMoveConfig.StopOnStationaryWaypoint == nil then WaypointMoveConfig.StopOnStationaryWaypoint = true end
function MTW_MatchSpeed(Velocity, TargetVelocity, Faster)
local Speed = Velocity.magnitude
local TargetSpeed = TargetVelocity.magnitude
local VelocityDirection = Velocity / Speed
local TargetVelocityDirection = TargetVelocity / TargetSpeed
local CosAngle = Vector3.Dot(TargetVelocityDirection, VelocityDirection)
local MinimumSpeed = WaypointMoveConfig.MinimumSpeed
if CosAngle > 0 then
local DesiredSpeed = TargetSpeed
DesiredSpeed = DesiredSpeed + Sign(Faster) * WaypointMoveConfig.RelativeApproachSpeed
return math.max(MinimumSpeed, DesiredSpeed),Speed
else
return MinimumSpeed,Speed
end
end
function MoveToWaypoint(Waypoint, AdjustHeading, WaypointVelocity)
local Offset,TargetPosition = PlanarVector(C:CoM(), Waypoint)
local Distance = Offset.magnitude
if not WaypointVelocity then
if Distance >= WaypointMoveConfig.MaxDistance then
local Bearing = GetBearingToPoint(Waypoint)
AdjustHeading(Bearing)
V.SetThrottle(WaypointMoveConfig.ClosingDrive)
elseif WaypointMoveConfig.StopOnStationaryWaypoint then
if V.GetThrottle() > 0 then V.SetThrottle(0) end
else
local Bearing = GetBearingToPoint(Waypoint)
AdjustHeading(Bearing)
local CV = MTW_ThrottlePID:Control(WaypointMoveConfig.MinimumSpeed - C:ForwardSpeed())
local Drive = Clamp(V.GetThrottle() + CV, 0, 1)
V.SetThrottle(Drive)
end
else
local Direction = Offset / Distance
local Velocity = C:Velocity()
Velocity = Vector3(Velocity.x, 0, Velocity.z)
local TargetVelocity = Vector3(WaypointVelocity.x, 0, WaypointVelocity.z)
local TargetPoint = QuadraticIntercept(C:CoM(), Vector3.Dot(Velocity, Velocity), TargetPosition, TargetVelocity)
local Bearing = GetBearingToPoint(TargetPoint)
AdjustHeading(Bearing)
if Distance >= WaypointMoveConfig.ApproachDistance then
V.SetThrottle(WaypointMoveConfig.ClosingDrive)
else
local Faster = Vector3.Dot(C:ForwardVector(), Direction)
local DesiredSpeed,Speed = MTW_MatchSpeed(Velocity, TargetVelocity, Faster)
local Error = DesiredSpeed - Speed
local CV = MTW_ThrottlePID:Control(Error)
local Drive = Clamp(V.GetThrottle() + CV, 0, 1)
V.SetThrottle(Drive)
end
end
end
Attacking = false
LastAttackTime = nil
DodgeAltitudeOffset = nil -- luacheck: ignore 131
AttackPID = PID.new(AttackPIDConfig, -1, 1)
function Evade(Evasion)
if AirRaidEvasion and C:FirstTarget():Elevation(C.I) >= AirRaidAboveElevation then
Evasion = AirRaidEvasion
end
return CalculateEvasion(Evasion)
end
function AdjustHeadingToTarget(I)
local TargetPosition = C:FirstTarget().Position
local Distance = PlanarVector(C:CoM(), TargetPosition).magnitude
local TargetAngle,Drive,Evasion = EscapeAngle,EscapeDrive,EscapeEvasion
local ScaledDrive = false
if AttackRuns then
if not LastAttackTime then
LastAttackTime = C:Now()
end
if Distance > MaxDistance then
TargetAngle = ClosingAngle
Drive = ClosingDrive
Evasion = ClosingEvasion
Attacking = true
LastAttackTime = C:Now()
else
if Attacking then
TargetAngle = AttackAngle
Drive = AttackDrive
Evasion = AttackEvasion
end
end
if Attacking and Distance <= MinDistance and (LastAttackTime + MinAttackTime) < C:Now() then
Attacking = false
elseif not Attacking and (LastAttackTime + ForceAttackTime) < C:Now() then
Attacking = true
LastAttackTime = C:Now()
end
else
if Distance > MaxDistance then
TargetAngle = ClosingAngle
Drive = ClosingDrive
Evasion = ClosingEvasion
elseif Distance > MinDistance then
TargetAngle = AttackAngle
Drive = AttackDrive
Evasion = AttackEvasion
if AttackDistance then
local Delta = AttackDistance - Distance
if AttackReverse then
Drive = AttackPID:Control(Delta) * -AttackDrive
ScaledDrive = true
else
local BroadsideAngle = 90 - AttackAngle
TargetAngle = 90 + AttackPID:Control(Delta) * BroadsideAngle
end
end
end
end
local Bearing
local DodgeAngle,DodgeY,Dodging = Dodge()
if Dodging then
Bearing = DodgeAngle
DodgeAltitudeOffset = DodgeY * VehicleRadius
if DodgeDrive then
Drive = DodgeDrive
elseif ScaledDrive then
Drive = Sign(C:ForwardSpeed(), 1)
end
else
Bearing = GetBearingToPoint(TargetPosition)
Bearing = Bearing - (PreferredBroadside or Sign(Bearing, 1)) * TargetAngle
Bearing = Bearing + Evade(Evasion)
Bearing = NormalizeBearing(Bearing)
DodgeAltitudeOffset = nil
end
V.AdjustHeading(Avoidance(I, Bearing))
V.SetThrottle(Drive)
end
function NavalAI_Reset()
Attacking = false
LastAttackTime = nil
DodgeAltitudeOffset = nil
end
function FormationMove_Evade(DesiredBearing)
if not C:FirstTarget() then return DesiredBearing end
local Bearing
local DodgeAngle,DodgeY,Dodging = Dodge()
if Dodging then
Bearing = DodgeAngle
DodgeAltitudeOffset = DodgeY * VehicleRadius
else
Bearing = NormalizeBearing(DesiredBearing + Evade(FleetMoveEvasion))
DodgeAltitudeOffset = nil
end
return Bearing
end
function Control_MoveToWaypoint(I, Waypoint, WaypointVelocity)
MoveToWaypoint(Waypoint, function (Bearing) V.AdjustHeading(Avoidance(I, FormationMove_Evade(Bearing))) end, WaypointVelocity)
end
function FormationMove(I)
local Flagship = I.Fleet.Flagship
if not I.IsFlagship and Flagship.Valid then
Control_MoveToWaypoint(I, Flagship.ReferencePosition + Flagship.Rotation * I.IdealFleetPosition, Flagship.Velocity)
else
Control_MoveToWaypoint(I, I.Waypoint) -- Waypoint assumed to be stationary
end
end
function NavalAI_Update(I)
V.Reset()
local AIMode = I.AIMode
if AIMode ~= "fleetmove" then
if C:FirstTarget() then
AdjustHeadingToTarget(I)
elseif ReturnToOrigin then
NavalAI_Reset()
FormationMove(I)
else
NavalAI_Reset()
V.AdjustHeading(Avoidance(I, 0))
end
else
NavalAI_Reset()
FormationMove(I)
end
end
NavalAI = Periodic.new(UpdateRate, NavalAI_Update)
SelectHeadingImpl(SixDoF, RollTurnControl)
SelectRollImpl(SixDoF, RollTurnControl)
SelectHeadingImpl(RollTurn)
SelectThrottleImpl(SixDoF)
function Update(I) -- luacheck: ignore 131
C = Commons.new(I)
FirstRun(I)
if not C:IsDocked() then
if ActivateWhen[I.AIMode] then
NavalAI:Tick(I)
I:TellAiThatWeAreTakingControl()
else
NavalAI_Reset()
V.Reset()
SixDoF.Release(I)
end
SixDoF.Update(I)
else
NavalAI_Reset()
SixDoF.Disable(I)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment