Instantly share code, notes, and snippets.
-
Save ZerothAngel/dfad34c579aa6c25786cc399ccf06ec6 to your computer and use it in GitHub Desktop.
ZerothAngel's From the Depths Naval AI with formation evasion + dodging. Untested.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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