|
local FlightTile = const.FlightTile |
|
if not FlightTile then |
|
return |
|
end |
|
|
|
FlightDbgResults = empty_func |
|
FlightDbgMark = empty_func |
|
FlightDbgBreak = empty_func |
|
|
|
local efResting = const.efResting |
|
|
|
local pfFinished = const.pfFinished |
|
local pfFailed = const.pfFailed |
|
local pfTunnel = const.pfTunnel |
|
local pfDestLocked = const.pfDestLocked |
|
local pfSmartDestlockDist = const.pfSmartDestlockDist |
|
|
|
local tfrPassClass = const.tfrPassClass |
|
local tfrLimitDist = const.tfrLimitDist |
|
local tfrCanDestlock = const.tfrCanDestlock |
|
local tfrLuaFilter = const.tfrLuaFilter |
|
|
|
local Min, Max, Clamp, AngleDiff = Min, Max, Clamp, AngleDiff |
|
local IsValid, IsValidPos = IsValid, IsValidPos |
|
local ResolveZ = ResolveZ |
|
|
|
local InvalidZ = const.InvalidZ |
|
local anim_min_time = 100 |
|
local time_ahead = 10 |
|
local tplCheck = const.tplCheck |
|
local step_search_dist = 2*FlightTile |
|
local dest_search_dist = 4*FlightTile |
|
local max_search_dist = 10*FlightTile |
|
local max_takeoff_dist = 64*guim |
|
|
|
local flight_default_flags = const.ffpSplines | const.ffpPhysics | const.ffpSmooth |
|
local ffpAdjustTarget = const.ffpAdjustTarget |
|
|
|
local flight_flags_values = { |
|
Splines = const.ffpSplines, |
|
Physics = const.ffpPhysics, |
|
Smooth = const.ffpSmooth, |
|
AdjustTarget = const.ffpAdjustTarget, |
|
Debug = const.ffpDebug, |
|
} |
|
local flight_flags_names = table.keys(flight_flags_values, true) |
|
local function FlightFlagsToSet(flags) |
|
local fset = {} |
|
for name, flag in pairs(flight_flags_values) do |
|
if (flags & flag) ~= 0 then |
|
fset[name] = true |
|
end |
|
end |
|
return fset |
|
end |
|
local function FlightSetToFlags(fset) |
|
local flags = 0 |
|
for name in pairs(fset) do |
|
flags = flags | flight_flags_values[name] |
|
end |
|
return flags |
|
end |
|
local path_errors = { |
|
invalid = const.fpsInvalid, |
|
max_iters = const.fpsMaxIters, |
|
max_steps = const.fpsMaxSteps, |
|
max_loops = const.fpsMaxLoops, |
|
max_stops = const.fpsMaxStops, |
|
} |
|
function FlightGetErrors(status) |
|
status = status or 0 |
|
local errors |
|
for name, value in pairs(path_errors) do |
|
if status & value ~= 0 then |
|
errors = table.create_add(errors, name) |
|
end |
|
end |
|
if errors then |
|
table.sort(errors) |
|
return errors |
|
end |
|
end |
|
|
|
function FlightInitVars() |
|
FlightMap = false |
|
FlightEnergy = false |
|
FlightFrom = false |
|
FlightTo = false |
|
FlightFlags = 0 |
|
FlightDestRange = 0 |
|
FlightMarkFrom = false |
|
FlightMarkTo = false |
|
FlightMarkBorder = 0 |
|
FlightMarkMinHeight = 0 |
|
FlightMarkObjRadius = 0 |
|
FlightMarkIdx = 0 |
|
FlightArea = false |
|
FlightEnergyMin = false |
|
FlightSlopePenalty = 0 |
|
FlightSmoothDist = 0 |
|
FlightGrowObstacles = false |
|
FlightTimestamp = 0 |
|
FlightPassVersion = false |
|
end |
|
|
|
if FirstLoad then |
|
FlightInitVars() |
|
end |
|
|
|
function OnMsg.DoneMap() |
|
if FlightMap then |
|
FlightMap:free() |
|
end |
|
if FlightEnergy then |
|
FlightEnergy:free() |
|
end |
|
FlightInitVars() |
|
end |
|
|
|
local StayAboveMapItems = { |
|
{ value = const.FlightRestrictNone, text = "None", help = "The object is allowed to fall under the flight map" }, |
|
{ value = const.FlightRestrictAboveTerrain, text = "Above Terrain", help = "The object is allowed to fall under the flight map, but not under the terrain" }, |
|
{ value = const.FlightRestrictAboveWalkable, text = "Above Walkable", help = "The object is allowed to fall under the flight map, but not under a walkable surface (inlcuding the terrain)" }, |
|
{ value = const.FlightRestrictAboveMap, text = "Above Flight Map", help = "The object is not allowed to fall under the flight map" }, |
|
} |
|
|
|
|
|
|
|
MapVar("FlyingObjs", function() return sync_set() end) |
|
|
|
DefineClass.FlyingObj = { |
|
__parents = { "Object" }, |
|
flags = { cofComponentInterpolation = true, cofComponentCurvature = true }, |
|
properties = { |
|
{ category = "Flight", id = "FlightMinPitch", name = "Pitch Min", editor = "number", default = -2700, scale = "deg", template = true }, |
|
{ category = "Flight", id = "FlightMaxPitch", name = "Pitch Max", editor = "number", default = 2700, scale = "deg", template = true }, |
|
{ category = "Flight", id = "FlightPitchSmooth", name = "Pitch Smooth", editor = "number", default = 100, min = 0, max = 500, scale = 100, slider = true, template = true, help = "Smooth the pitch angular speed changes" }, |
|
{ category = "Flight", id = "FlightMaxPitchSpeed", name = "Pitch Speed Limit (deg/s)",editor = "number", default = 90*60, scale = 60, template = true, help = "Smooth the pitch angular speed changes" }, |
|
{ category = "Flight", id = "FlightSpeedToPitch", name = "Speed to Pitch", editor = "number", default = 100, min = 0, max = 100, scale = "%", slider = true, template = true, help = "How much the flight speed affects the pitch angle" }, |
|
{ category = "Flight", id = "FlightMaxRoll", name = "Roll Max", editor = "number", default = 2700, min = 0, max = 180*60, scale = "deg", slider = true, template = true }, |
|
{ category = "Flight", id = "FlightMaxRollSpeed", name = "Roll Speed Limit (deg/s)", editor = "number", default = 90*60, scale = 60, template = true, help = "Smooth the row angular speed changes" }, |
|
{ category = "Flight", id = "FlightRollSmooth", name = "Roll Smooth", editor = "number", default = 100, min = 0, max = 500, scale = 100, slider = true, template = true, help = "Smooth the row angular speed changes" }, |
|
{ category = "Flight", id = "FlightSpeedToRoll", name = "Speed to Roll", editor = "number", default = 0, min = 0, max = 100, scale = "%", slider = true, template = true, help = "How much the flight speed affects the roll angle" }, |
|
{ category = "Flight", id = "FlightYawSmooth", name = "Yaw Smooth", editor = "number", default = 100, min = 0, max = 500, scale = 100, slider = true, template = true, help = "Smooth the yaw angular speed changes" }, |
|
{ category = "Flight", id = "FlightMaxYawSpeed", name = "Yaw Speed Limit (deg/s)", editor = "number", default = 360*60, scale = 60, template = true, help = "Smooth the yaw angular speed changes" }, |
|
{ category = "Flight", id = "FlightYawRotToRoll", name = "Yaw Rot to Roll", editor = "number", default = 100, min = 0, max = 300, scale = "%", slider = true, template = true, help = "Links the row angle to the yaw rotation speed" }, |
|
{ category = "Flight", id = "FlightYawRotFriction", name = "Yaw Rot Friction", editor = "number", default = 100, min = 0, max = 1000, scale = "%", slider = true, template = true, help = "Friction caused by 90 deg/s yaw rotation speed" }, |
|
{ category = "Flight", id = "FlightSpeedStop", name = "Speed Stop (m/s)", editor = "number", default = false, scale = guim, template = true, help = "Will use the min speed if not specified. Stopping is possible only if the deceleration distance is not zero" }, |
|
{ category = "Flight", id = "FlightSpeedMin", name = "Speed Min (m/s)", editor = "number", default = 6 * guim, scale = guim, template = true }, |
|
{ category = "Flight", id = "FlightSpeedMax", name = "Speed Max (m/s)", editor = "number", default = 15 * guim, scale = guim, template = true }, |
|
{ category = "Flight", id = "FlightFriction", name = "Friction", editor = "number", default = 30, min = 0, max = 300, slider = true, scale = "%", template = true, help = "Friction coefitient, affects the max achievable speed. Should be adjusted so that both the max speed and the achievable one are matching." }, |
|
{ category = "Flight", id = "FlightAccelMax", name = "Accel Max (m/s^2)", editor = "number", default = 10*guim, scale = guim, template = true }, |
|
{ category = "Flight", id = "FlightDecelMax", name = "Decel Max (m/s^2)", editor = "number", default = 20*guim, scale = guim, template = true }, |
|
{ category = "Flight", id = "FlightAccelDist", name = "Accel Dist", editor = "number", default = 20*guim, scale = "m", template = true }, |
|
{ category = "Flight", id = "FlightDecelDist", name = "Decel Dist", editor = "number", default = 20*guim, scale = "m", template = true }, |
|
{ category = "Flight", id = "FlightStopDist", name = "Force Stop Dist", editor = "number", default = 1*guim, scale = "m", template = true, help = "Critical distance where to dorce a stop animation even if the conditions for such are not met" }, |
|
{ category = "Flight", id = "FlightStopMinTime", name = "Min Stop Time", editor = "number", default = 50, min = 0, template = true, help = "Try to play stop anim only if enough time is available" }, |
|
{ category = "Flight", id = "FlightPathStepMax", name = "Path Step Max", editor = "number", default = 2*guim, scale = "m", template = true, help = "Step dist at max speed" }, |
|
{ category = "Flight", id = "FlightPathStepMin", name = "Path Step Min", editor = "number", default = guim, scale = "m", template = true, help = "Step dist at min speed" }, |
|
{ category = "Flight", id = "FlightAnimStart", name = "Anim Fly Start", editor = "text", default = false, template = true }, |
|
{ category = "Flight", id = "FlightAnim", name = "Anim Fly", editor = "text", default = false, template = true }, |
|
{ category = "Flight", id = "FlightAnimDecel", name = "Anim Fly Decel", editor = "text", default = false, template = true }, |
|
{ category = "Flight", id = "FlightAnimStop", name = "Anim Fly Stop", editor = "text", default = false, template = true }, |
|
|
|
{ category = "Flight", id = "FlightAnimIdle", name = "Anim Fly Idle", editor = "text", default = false, template = true }, |
|
{ category = "Flight", id = "FlightAnimSpeedMin", name = "Anim Speed Min", editor = "number", default = 1000, min = 0, max = 1000, scale = 1000, slider = true, template = true }, |
|
{ category = "Flight", id = "FlightAnimSpeedMax", name = "Anim Speed Max", editor = "number", default = 1000, min = 1000, max = 3000, scale = 1000, slider = true, template = true }, |
|
{ category = "Flight", id = "FlightAnimStopFOV", name = "Anim Fly Stop FoV", editor = "number", default = 90*60, min = 0, max = 360*60, scale = "deg", slider = true, template = true, help = "Required FoV towards the target in order to switch to anim_stop/landing anim" }, |
|
|
|
{ category = "Flight Path", id = "FlightSimHeightMin", name = "Min Height", editor = "number", default = 3*guim, min = guim, max = 50*guim, slider = true, scale = "m", template = true, sim = true, help = "Min flight height. If below, the flying obj will try to go up (lift)." }, |
|
{ category = "Flight Path", id = "FlightSimHeightMax", name = "Max Height", editor = "number", default = 5*guim, min = guim, max = 50*guim, slider = true, scale = "m", template = true, sim = true, help = "Max flight height. If above, the flying obj will try to go down (weight)." }, |
|
{ category = "Flight Path", id = "FlightSimHeightRestrict", name = "Height Restriction", editor = "choice", default = const.FlightRestrictNone, template = true, sim = true, items = StayAboveMapItems, help = "Avoid entering the height map. As the height map is not precise, this could lead to strange visual behavior." }, |
|
{ category = "Flight Path", id = "FlightSimSpeedLimit", name = "Speed Limit (m/s)", editor = "number", default = 10*guim, min = 1, max = 50*guim, slider = true, scale = guim, template = true, sim = true, help = "Max speed during simulation. Should be limited to ensure precision." }, |
|
{ category = "Flight Path", id = "FlightSimInertia", name = "Inertia", editor = "number", default = 100, min = 10, max = 1000, slider = true, exponent = 2, scale = 100, template = true, sim = true, help = "How inert is the object." }, |
|
{ category = "Flight Path", id = "FlightSimFrictionXY", name = "Friction XY", editor = "number", default = 20, min = 1, max = 300, slider = true, scale = "%", template = true, sim = true, help = "Horizontal friction min coefitient." }, |
|
{ category = "Flight Path", id = "FlightSimFrictionZ", name = "Friction Z", editor = "number", default = 50, min = 1, max = 300, slider = true, scale = "%", template = true, sim = true, help = "Vertical friction coefitient." }, |
|
{ category = "Flight Path", id = "FlightSimFrictionStop", name = "Friction Stop", editor = "number", default = 80, min = 1, max = 300, slider = true, scale = "%", template = true, sim = true, help = "Horizontal friction max coefitient." }, |
|
{ category = "Flight Path", id = "FlightSimAttract", name = "Attract", editor = "number", default = guim, min = 0, max = 30*guim, slider = true, scale = 1000, template = true, sim = true, help = "Attraction force per energy unit difference. The force pushing the unit towards its final destination." }, |
|
{ category = "Flight Path", id = "FlightSimLift", name = "Lift", editor = "number", default = guim/3, min = 0, max = 30*guim, slider = true, scale = 1000, template = true, sim = true, help = "Lift force per meter. The force trying to bring back UP the unit at its best height level." }, |
|
{ category = "Flight Path", id = "FlightSimMaxLift", name = "Max Lift", editor = "number", default = 10*guim, min = 0, max = 30*guim, slider = true, scale = 1000, template = true, sim = true, help = "Max lift force." }, |
|
{ category = "Flight Path", id = "FlightSimWeight", name = "Weight", editor = "number", default = guim/3, min = 0, max = 20*guim, slider = true, scale = 1000, template = true, sim = true, help = "Weight force per meter. The force trying to bring back DOWN the unit at its best height level." }, |
|
{ category = "Flight Path", id = "FlightSimMaxWeight", name = "Max Weight", editor = "number", default = 3*guim, min = 0, max = 20*guim, slider = true, scale = 1000, template = true, sim = true, help = "Max weight force." }, |
|
{ category = "Flight Path", id = "FlightSimMaxThrust", name = "Max Thrust", editor = "number", default = 10*guim, min = 0, max = 50*guim, slider = true, scale = 1000, template = true, sim = true, help = "Max cummulative thrust." }, |
|
{ category = "Flight Path", id = "FlightSimInterval", name = "Update Interval (ms)", editor = "number", default = 50, min = 1, max = 1000, slider = true, template = true, sim = true, help = "Simulation update interval. Lower values ensure better precision, but makes the sim more expensive" }, |
|
{ category = "Flight Path", id = "FlightSimMinStep", name = "Min Path Step", editor = "number", default = FlightTile, min = 0, max = 100*guim, scale = "m", slider = true, template = true, sim = true, help = "Min path step (approx)." }, |
|
{ category = "Flight Path", id = "FlightSimMaxStep", name = "Max Path Step", editor = "number", default = 8*FlightTile, min = 0, max = 100*guim, scale = "m", slider = true, template = true, sim = true, help = "Max path step (approx)." }, |
|
{ category = "Flight Path", id = "FlightSimDecelDist", name = "Decel Dist", editor = "number", default = 10*guim, min = 1, max = 300*guim, slider = true, scale = "m", template = true, sim = true, help = "At that distance to the target, the movement will try to go towards the target ignoring most considerations." }, |
|
{ category = "Flight Path", id = "FlightSimLookAhead", name = "Look Ahead", editor = "number", default = 4000, min = 0, max = 10000, scale = "sec", slider = true, template = true, sim = true, help = "Give some time to adjust the flight height before reaching a too high obstacle." }, |
|
{ category = "Flight Path", id = "FlightSimSplineAlpha", name = "Spline Alpha", editor = "number", default = 1365, min = 0, max = 4096, scale = 4096, slider = true, template = true, sim = true, help = "Defines the spline smoothness." }, |
|
{ category = "Flight Path", id = "FlightSimSplineErr", name = "Spline Tolerance", editor = "number", default = FlightTile/4, min = 0, max = FlightTile, scale = "m", slider = true, template = true, sim = true, help = "Max spline deviation form the precise trajectory. Lower values imply more path steps as the longer splines deviate stronger." }, |
|
{ category = "Flight Path", id = "FlightSimMaxIters", name = "Max Compute Iters", editor = "number", default = 16 * 1024, template = true, sim = true, help = "Max number of compute iterations. Used for a sanity check against infinite loops." }, |
|
|
|
{ category = "Flight Path", id = "FlightSlopePenalty", name = "Slope Penalty", editor = "number", default = 300, scale = "%", template = true, sim = true, min = 10, max = 1000, slider = true, exponent = 2, help = "How difficult it is to flight over against going around obstacles." }, |
|
{ category = "Flight Path", id = "FlightSmoothDist", name = "Smooth Obstacles Dist",editor = "number", default = 0, template = true, sim = true, help = "Better obstacle avoidance withing that distance at the expense of more processing." }, |
|
{ category = "Flight Path", id = "FlightMinObstacleHeight", name = "Min Obstacle Height", editor = "number", default = 0, scale = "m", template = true, sim = true, step = const.FlightScale, help = "Ignored obstacle height." }, |
|
{ category = "Flight Path", id = "FlightObjRadius", name = "Object Radius", editor = "number", default = 0, scale = "m", template = true, sim = true, help = "To consider when avoiding obstacles." }, |
|
|
|
{ category = "Flight Path", id = "FlightFlags", name = "Flight Flags", editor = "set", default = function(self) return FlightFlagsToSet(flight_default_flags) end, items = flight_flags_names }, |
|
{ category = "Flight Path", id = "FlightPathErrors", name = "Path Errors", editor = "set", default = set(), items = table.keys(path_errors, true), read_only = true, dont_save = true }, |
|
{ category = "Flight Path", id = "FlightPathSplines", name = "Path Splines", editor = "number", default = 0, read_only = true, dont_save = true }, |
|
{ category = "Flight Path", id = "flight_path_iters", name = "Path Iters", editor = "number", default = 0, read_only = true, dont_save = true }, |
|
|
|
}, |
|
flight_target = false, |
|
flight_target_range = 0, |
|
flight_path = false, |
|
flight_path_status = 0, |
|
flight_path_flags = false, |
|
flight_path_collision = false, |
|
flight_spline_idx = 0, |
|
flight_spline_dist = 0, |
|
flight_spline_len = 0, |
|
flight_spline_time = 0, |
|
flight_stop_on_passable = false, |
|
flight_flags = flight_default_flags, |
|
|
|
ResolveFlightTarget = pf.ResolveGotoTargetXYZ, |
|
CanFlyTo = return_true, |
|
} |
|
|
|
function FlyingObj:Init() |
|
FlyingObjs:insert(self) |
|
end |
|
|
|
function FlyingObj:Done() |
|
FlyingObjs:remove(self) |
|
self:UnlockFlightDest() |
|
end |
|
|
|
function FlyingObj:GetFlightPathErrors() |
|
return table.invert(FlightGetErrors(self.flight_path_status)) |
|
end |
|
|
|
function FlyingObj:GetFlightPathSplines() |
|
return #(self.flight_path or "") |
|
end |
|
|
|
function FlyingObj:SetFlightFlag(flag, enable) |
|
enable = enable or false |
|
local flight_flags = self.flight_flags |
|
local enabled = (flight_flags & flag) ~= 0 |
|
if enable == enabled then |
|
return |
|
end |
|
if enable then |
|
self.flight_flags = flight_flags | flag |
|
else |
|
self.flight_flags = flight_flags & ~flag |
|
end |
|
return true |
|
end |
|
|
|
function FlyingObj:GetFlightFlag(flag) |
|
return (self.flight_flags & flag) ~= 0 |
|
end |
|
|
|
function FlyingObj:SetFlightFlags(fset) |
|
self.flight_flags = FlightSetToFlags(fset) |
|
end |
|
|
|
function FlyingObj:GetFlightFlags() |
|
return FlightFlagsToSet(self.flight_flags) |
|
end |
|
|
|
function FlyingObj:SetAdjustFlightTarget(enable) |
|
return self:SetFlightFlag(ffpAdjustTarget, enable) |
|
end |
|
|
|
function FlyingObj:GetAdjustFlightTarget() |
|
return self:GetFlightFlag(ffpAdjustTarget) |
|
end |
|
|
|
function FlyingObj:FlightStop() |
|
if self:TimeToPosInterpolationEnd() == 0 then |
|
return |
|
end |
|
local a = -self.FlightDecelMax |
|
local x, y, z, dt0 = self:GetFinalPosAndTime(0, a) |
|
if not x then |
|
return |
|
end |
|
self:SetPos(x, y, z, dt0) |
|
self:SetAcceleration(a) |
|
return dt0 |
|
end |
|
|
|
function FlyingObj:FindFlightPath(target, range, flight_flags, debug_iter) |
|
if not IsValidPos(target) then |
|
return |
|
end |
|
flight_flags = flight_flags or self.flight_flags |
|
local path, error_status, collision_pos, iters = FlightCalcPathBetween( |
|
self, target, flight_flags, |
|
self.FlightMinObstacleHeight, self.FlightObjRadius, self.FlightSlopePenalty, self.FlightSmoothDist, |
|
range, debug_iter) |
|
self.flight_path = path |
|
self.flight_path_status = error_status |
|
self.flight_path_iters = iters |
|
self.flight_path_flags = flight_flags |
|
self.flight_path_collision = collision_pos |
|
self.flight_target = target |
|
self.flight_target_range = range or nil |
|
self.flight_spline_idx = nil |
|
self.flight_spline_dist = nil |
|
self.flight_spline_len = nil |
|
self.flight_spline_time = nil |
|
dbg(FlightDbgResults(self)) |
|
return path, error_status, collision_pos |
|
end |
|
|
|
function FlyingObj:RecalcFlightPath() |
|
return self:FindFlightPath(self.flight_target, self.flight_target_range, self.flight_path_flags) |
|
end |
|
|
|
function FlyingObj:MarkFlightArea(target) |
|
return FlightMarkBetween(self, target or self, self.FlightMinObstacleHeight, self.FlightObjRadius) |
|
end |
|
|
|
function FlyingObj:MarkFlightAround(target, border) |
|
target = target or self |
|
return FlightMarkBetween(target, target, self.FlightMinObstacleHeight, self.FlightObjRadius, border) |
|
end |
|
|
|
function FlyingObj:LockFlightDest(x, y, z) |
|
return x, y, z |
|
end |
|
FlyingObj.UnlockFlightDest = empty_func |
|
|
|
function FlyingObj:GetPathHash(seed) |
|
local flight_path = self.flight_path |
|
if not flight_path or #flight_path == 0 then return end |
|
local start_idx = self.flight_spline_idx |
|
local spline = flight_path[start_idx] |
|
local hash = xxhash(seed, spline[1], spline[2], spline[3], spline[4]) |
|
for i=start_idx + 1,#flight_path do |
|
spline = flight_path[i] |
|
hash = xxhash(hash, spline[2], spline[3], spline[4]) |
|
end |
|
return hash |
|
end |
|
|
|
function FlyingObj:Step(pt, ...) |
|
|
|
local fx, fy, fz, range = self:ResolveFlightTarget(pt, ...) |
|
local tx, ty, tz = self:LockFlightDest(fx, fy, fz) |
|
if not tx then |
|
return pfFailed |
|
end |
|
local visual_z = ResolveZ(tx, ty, tz) |
|
if self:IsCloser(tx, ty, visual_z, range + 1) then |
|
if range == 0 then |
|
self:SetPos(tx, ty, tz) |
|
self:SetAcceleration(0) |
|
end |
|
fz = fz or InvalidZ |
|
tz = tz or InvalidZ |
|
if fx ~= tx or fy ~= ty or fz ~= tz then |
|
return pfDestLocked |
|
end |
|
return pfFinished |
|
end |
|
local v0 = self:GetVelocity() |
|
local path = self.flight_path |
|
local flight_target = self.flight_target |
|
local prev_range = self.flight_target_range |
|
local prev_flags = self.flight_path_flags |
|
local find_path = not path or not flight_target or prev_flags ~= self.flight_flags |
|
local time_now = GameTime() |
|
local spline_idx, spline_dist, spline_len |
|
local same_target = prev_range == range and flight_target and flight_target:Equal(tx, ty, tz) |
|
if not find_path and not same_target then |
|
|
|
local error_dist = flight_target:Dist(tx, ty, tz) |
|
local retarget_offset_pct = 30 |
|
local threshold_dist = error_dist * 100 / retarget_offset_pct |
|
if v0 > 0 then |
|
local min_retarget_time = 3000 |
|
threshold_dist = Min(threshold_dist, v0 * min_retarget_time / 1000) |
|
end |
|
local x, y, z = ResolveVisualPosXYZ(flight_target) |
|
find_path = self:IsCloser(x, y, z, 1 + threshold_dist) |
|
end |
|
local step_finished |
|
if find_path then |
|
flight_target = point(tx, ty, tz) |
|
path = self:FindFlightPath(flight_target, range) |
|
if not path or #path == 0 then |
|
return pfFailed |
|
end |
|
assert(flight_target == self.flight_target) |
|
spline_idx = 0 |
|
spline_dist = 0 |
|
spline_len = 0 |
|
step_finished = true |
|
same_target = true |
|
else |
|
spline_idx = self.flight_spline_idx |
|
spline_dist = self.flight_spline_dist |
|
spline_len = self.flight_spline_len |
|
step_finished = time_now - self.flight_spline_time >= 0 |
|
end |
|
local spline |
|
local last_step |
|
local BS3_GetSplineLength3D = BS3_GetSplineLength3D |
|
if spline_dist < spline_len or not step_finished then |
|
spline = path[spline_idx] |
|
else |
|
while spline_dist >= spline_len do |
|
spline_idx = spline_idx + 1 |
|
spline = path[spline_idx] |
|
if not spline then |
|
return pfFailed |
|
end |
|
spline_dist = 0 |
|
spline_len = BS3_GetSplineLength3D(spline) |
|
end |
|
self.flight_spline_idx = spline_idx |
|
self.flight_spline_len = spline_len |
|
end |
|
assert(spline) |
|
if not spline then |
|
return pfFailed |
|
end |
|
local last_spline = path[#path] |
|
local flight_dest = last_spline[4] |
|
tx, ty, tz = flight_dest:xyz() |
|
local speed_min, speed_max, speed_stop = self.FlightSpeedMin, self.FlightSpeedMax, self.FlightSpeedStop |
|
if step_finished then |
|
local min_step, max_step = self.FlightPathStepMin, self.FlightPathStepMax |
|
assert(speed_min == speed_max and min_step == max_step or speed_min < speed_max and min_step < max_step) |
|
local spline_step |
|
if v0 <= speed_min then |
|
spline_step = min_step |
|
elseif v0 >= speed_max then |
|
spline_step = max_step |
|
else |
|
spline_step = min_step + (max_step - min_step) * (v0 - speed_min) / (speed_max - speed_min) |
|
end |
|
spline_step = Min(spline_step, spline_len) |
|
spline_dist = spline_dist + spline_step |
|
if spline_dist + spline_step / 2 > spline_len then |
|
spline_dist = spline_len |
|
last_step = spline_idx == #path |
|
end |
|
self.flight_spline_dist = spline_dist |
|
end |
|
|
|
speed_stop = speed_stop or speed_min |
|
local max_roll, roll_max_speed = self.FlightMaxRoll, self.FlightMaxRollSpeed |
|
local pitch_min, pitch_max = self.FlightMinPitch, self.FlightMaxPitch |
|
local yaw_max_speed, pitch_max_speed = self.FlightMaxYawSpeed, self.FlightMaxPitchSpeed |
|
local decel_dist = self.FlightDecelDist |
|
local remaining_len = spline_len - spline_dist |
|
local anim_stop |
|
local fly_anim = self.FlightAnim |
|
local x0, y0, z0 = self:GetVisualPosXYZ() |
|
local speed_lim = speed_max |
|
local x, y, z, dirx, diry, dirz, curvex, curvey, curvez |
|
local roll, pitch, yaw, accel, v, dt |
|
local max_dt = max_int |
|
if decel_dist > 0 and self:IsCloser(flight_dest, decel_dist) and (not self.flight_stop_on_passable or terrain.FindPassableZ(flight_dest, self, 0, 0)) then |
|
local total_remaining_len = remaining_len |
|
local deceleration = true |
|
for i = spline_idx + 1, #path do |
|
if total_remaining_len >= decel_dist then |
|
deceleration = false |
|
break |
|
end |
|
total_remaining_len = total_remaining_len + BS3_GetSplineLength3D(path[i]) |
|
end |
|
if deceleration then |
|
speed_lim = speed_stop + (speed_max - speed_stop) * total_remaining_len / decel_dist |
|
end |
|
fly_anim = self.FlightAnimDecel or fly_anim |
|
|
|
local use_velocity_fov = true |
|
local tz1 = tz + 50 |
|
local critical_stop = deceleration and total_remaining_len < self.FlightStopDist |
|
local fly_anim_stop = self.FlightAnimStop |
|
if fly_anim and fly_anim_stop and deceleration |
|
and (critical_stop or self:HasFov(tx, ty, tz1, self.FlightAnimStopFOV, 0, use_velocity_fov) and TestPointsLOS(tx, ty, tz1, self, tplCheck)) then |
|
dt = GetAnimDuration(self:GetEntity(), fly_anim_stop) |
|
dbg(ReportZeroAnimDuration(self, fly_anim_stop, dt)) |
|
if dt == 0 then |
|
dt = 1000 |
|
end |
|
x, y, z, dirx, diry, dirz = BS3_GetSplinePosDir(last_spline, 4096) |
|
accel, v = self:GetAccelerationAndFinalSpeed(x, y, z, dt) |
|
local speed_stop = Max(v0, speed_min) |
|
if v <= speed_stop then |
|
anim_stop = true |
|
local anim_speed = 1000 |
|
if v < 0 then |
|
local stop_time |
|
accel, stop_time = self:GetAccelerationAndTime(x, y, z, speed_stop) |
|
if stop_time > self.FlightStopMinTime then |
|
anim_speed = 1000 * dt / stop_time |
|
else |
|
anim_stop = false |
|
end |
|
end |
|
if anim_stop then |
|
if dirx == 0 and diry == 0 then |
|
dirx, diry = x - x0, y - y0 |
|
end |
|
yaw = atan(diry, dirx) |
|
roll, pitch = 0, 0 |
|
self:SetState(fly_anim_stop) |
|
self:SetAnimSpeed(1, anim_speed) |
|
self.flight_spline_dist = spline_len |
|
last_step = true |
|
end |
|
end |
|
end |
|
end |
|
if not anim_stop then |
|
local roll0, pitch0, yaw0 = self:GetRollPitchYaw() |
|
x, y, z, dirx, diry, dirz, curvex, curvey, curvez = BS3_GetSplinePosDirCurve(spline, spline_dist, spline_len) |
|
if dirx == 0 and diry == 0 and dirz == 0 then |
|
dirx, diry, dirz = x - x0, y - y0, z - z0 |
|
end |
|
|
|
pitch, yaw = GetPitchYaw(dirx, diry, dirz) |
|
pitch, yaw = pitch or pitch0, yaw or yaw0 |
|
|
|
local step_len = self:GetVisualDist(x, y, z) |
|
local friction = self.FlightFriction |
|
local dyaw = AngleDiff(yaw, yaw0) * 100 / (100 + self.FlightYawSmooth) |
|
dt = v0 > 0 and MulDivRound(1000, step_len, v0) or 0 |
|
local yaw_rot_est = dt == 0 and 0 or Clamp(1000 * dyaw / dt, -yaw_max_speed, yaw_max_speed) |
|
if yaw_rot_est ~= 0 then |
|
friction = friction + MulDivRound(self.FlightYawRotFriction, abs(yaw_rot_est), 90 * 60) |
|
end |
|
local speed_to_roll, speed_to_pitch = self.FlightSpeedToRoll, self.FlightSpeedToPitch |
|
local accel_max = self.FlightAccelMax |
|
local accel0 = accel_max - v0 * friction / 100 |
|
v, dt = self:GetFinalSpeedAndTime(x, y, z, accel0, v0) |
|
v = v or speed_min |
|
v = Min(v, speed_lim) |
|
v = Max(v, Min(speed_min, v0)) |
|
local at_max_speed = v == speed_max |
|
accel, dt = self:GetAccelerationAndTime(x, y, z, v) |
|
if not at_max_speed and speed_to_pitch > 0 then |
|
local mod_pitch = pitch * v / speed_max |
|
if speed_to_pitch == 100 then |
|
pitch = mod_pitch |
|
else |
|
pitch = pitch + (mod_pitch - pitch) * speed_to_pitch / 100 |
|
end |
|
end |
|
pitch = Clamp(pitch, pitch_min, pitch_max) |
|
local dpitch = AngleDiff(pitch, pitch0) * 100 / (100 + self.FlightPitchSmooth) |
|
local pitch_rot = dt > 0 and Clamp(1000 * dpitch / dt, -pitch_max_speed, pitch_max_speed) or 0 |
|
local yaw_rot = dt > 0 and Clamp(1000 * dyaw / dt, -yaw_max_speed, yaw_max_speed) or 0 |
|
roll = -yaw_rot * self.FlightYawRotToRoll / 100 |
|
if not at_max_speed and speed_to_roll > 0 then |
|
local mod_roll = roll * v / speed_max |
|
if speed_to_roll == 100 then |
|
roll = mod_roll |
|
else |
|
roll = roll + (mod_roll - roll) * speed_to_roll / 100 |
|
end |
|
end |
|
roll = Clamp(roll, -max_roll, max_roll) |
|
local droll = AngleDiff(roll, roll0) * 100 / (100 + self.FlightRollSmooth) |
|
local roll_rot = dt > 0 and Clamp(1000 * droll / dt, -roll_max_speed, roll_max_speed) or 0 |
|
if dt > 0 then |
|
|
|
droll = roll_rot * dt / 1000 |
|
dyaw = yaw_rot * dt / 1000 |
|
dpitch = pitch_rot * dt / 1000 |
|
end |
|
roll = roll0 + droll |
|
yaw = yaw0 + dyaw |
|
pitch = pitch0 + dpitch |
|
if fly_anim then |
|
local anim = GetStateName(self) |
|
if anim ~= fly_anim then |
|
local fly_anim_start = self.FlightAnimStart |
|
if anim ~= fly_anim_start then |
|
self:SetState(fly_anim_start) |
|
else |
|
local remaining_time = self:TimeToAnimEnd() |
|
if remaining_time > anim_min_time then |
|
max_dt = remaining_time |
|
else |
|
self:SetState(fly_anim) |
|
end |
|
end |
|
else |
|
local min_anim_speed, max_anim_speed = self.FlightAnimSpeedMin, self.FlightAnimSpeedMax |
|
if dt > 0 and min_anim_speed < max_anim_speed then |
|
local curve = Max(GetLen(curvex, curvey, curvez), 1) |
|
local coef = 1024 + 1024 * curvez / curve + 1024 * abs(accel0) / accel_max |
|
local anim_speed = min_anim_speed + (max_anim_speed - min_anim_speed) * Clamp(coef, 0, 2048) / 2048 |
|
self:SetAnimSpeed(1, anim_speed) |
|
end |
|
end |
|
end |
|
end |
|
|
|
self:SetRollPitchYaw(roll, pitch, yaw, dt) |
|
self:SetPos(x, y, z, dt) |
|
self:SetAcceleration(accel) |
|
|
|
|
|
if not last_step and not anim_stop and dt > time_ahead then |
|
dt = dt - time_ahead |
|
end |
|
self.flight_spline_time = time_now + dt |
|
local sleep = Min(dt, max_dt) |
|
return sleep |
|
end |
|
|
|
function FlyingObj:ClearFlightPath() |
|
self.flight_path = nil |
|
self.flight_path_status = nil |
|
self.flight_path_iters = nil |
|
self.flight_path_flags = nil |
|
self.flight_path_collision = nil |
|
self.flight_target = nil |
|
self.flight_spline_idx = nil |
|
self.flight_flags = nil |
|
self.flight_stop_on_passable = nil |
|
self:UnlockFlightDest() |
|
end |
|
|
|
FlyingObj.ClearPath = FlyingObj.ClearFlightPath |
|
|
|
function FlyingObj:ResetOrientation(time) |
|
local _, _, yaw = self:GetRollPitchYaw() |
|
self:SetRollPitchYaw(0, 0, yaw, time) |
|
end |
|
|
|
function FlyingObj:Face(target, time) |
|
local pitch, yaw = GetPitchYaw(self, target) |
|
self:SetRollPitchYaw(0, pitch, yaw, time) |
|
end |
|
|
|
function FlyingObj:GetFlightDest() |
|
local path = self.flight_path |
|
local last_spline = path and path[#path] |
|
return last_spline and last_spline[4] |
|
end |
|
|
|
function FlyingObj:GetFinalFlightDirXYZ() |
|
local path = self.flight_path |
|
local last_spline = path and path[#path] |
|
if not last_spline then |
|
return self:GetVelocityVectorXYZ() |
|
end |
|
return BS3_GetSplineDir(last_spline, 4096, 4096) |
|
end |
|
|
|
function FlyingObj:IsFlightAreaMarked(flight_target, mark_border) |
|
flight_target = flight_target or self.flight_target |
|
if not flight_target |
|
or GameTime() ~= FlightTimestamp |
|
or not FlightArea or not FlightMap |
|
or FlightPassVersion ~= PassVersion |
|
or FlightMarkMinHeight ~= self.FlightMinObstacleHeight |
|
or FlightMarkObjRadius ~= self.FlightObjRadius then |
|
return |
|
end |
|
return FlightIsMarked(FlightArea, FlightMarkFrom, FlightMarkTo, FlightMarkBorder, self, flight_target, mark_border) |
|
end |
|
|
|
function FlightGetHeightAt(...) |
|
return FlightGetHeight(FlightMap, FlightArea, ...) |
|
end |
|
|
|
|
|
|
|
DefineClass("FlyingMovableAutoResolve") |
|
|
|
DefineClass.FlyingMovable = { |
|
__parents = { "FlyingObj", "Movable", "FlyingMovableAutoResolve" }, |
|
properties = { |
|
{ category = "Flight", id = "FlightPlanning", name = "Flight Planning", editor = "bool", default = false, template = true, help = "Complex flight planning" }, |
|
{ category = "Flight", id = "FlightMaxFailures", name = "Flight Plan Max Failures", editor = "number", default = 5, template = true, help = "How many times the flight plan can fail before giving up", no_edit = PropChecker("FlightPlanning", false) }, |
|
{ category = "Flight", id = "FlightFailureCooldown", name = "Flight Failure Cooldown", editor = "number", default = 333, template = true, scale = "sec", help = "How often the flight plan can fail before giving up", no_edit = PropChecker("FlightPlanning", false) }, |
|
{ category = "Flight", id = "FlightMaxWalkDist", name = "Max Walk Dist", editor = "number", default = 32 * guim, scale = "m", template = true, help = "Defines the max area where to use walking"}, |
|
{ category = "Flight", id = "FlightMinDist", name = "Min Flight Dist", editor = "number", default = 16 * guim, scale = "m", template = true, help = "Defines the min distance to use flying"}, |
|
{ category = "Flight", id = "FlightWalkExcess", name = "Walk To Fly Excess", editor = "number", default = 30, scale = "%", min = 0, template = true, help = "How much longer should be the walk path to prefer flying", }, |
|
{ category = "Flight", id = "FlightIsHovering", name = "Is Hovering", editor = "bool", default = false, template = true, help = "Is the walking above the ground" }, |
|
}, |
|
flying = false, |
|
flight_stop_on_passable = true, |
|
|
|
flight_pf_ready = false, |
|
flight_landed = false, |
|
flight_land_pos = false, |
|
flight_land_retry = -1, |
|
flight_land_target_pos = false, |
|
flight_takeoff_pos = false, |
|
flight_takeoff_retry = -1, |
|
flight_start_velocity = false, |
|
|
|
flight_plan_failed = 0, |
|
flight_plan_failures = 0, |
|
flight_plan_force_land = true, |
|
|
|
FlightSimHeightRestrict = const.FlightRestrictAboveWalkable, |
|
|
|
OnFlyingChanged = empty_func, |
|
CanTakeOff = return_true, |
|
} |
|
|
|
function FlyingMovable:IsOnPassable() |
|
return terrain.FindPassableZ(self, 0, 0) |
|
end |
|
|
|
function FlyingMovable:OnMoved() |
|
if self.flying and terrain.FindPassableZ(self, 0, 0) then |
|
self:SetFlying(false) |
|
end |
|
end |
|
|
|
function FlyingMovable:SetFlying(flying) |
|
flying = flying or false |
|
if self.flying == flying then |
|
return |
|
end |
|
self:SetAnimSpeed(1, 1000) |
|
if not flying then |
|
self:ClearFlightPath() |
|
self:SetAcceleration(0) |
|
self:ResetOrientation(0) |
|
self:UnlockFlightDest() |
|
self:SetEnumFlags(efResting) |
|
else |
|
pf.ClearPath(self) |
|
assert(self:GetPathPointCount() == 0) |
|
self:SetGravity(0) |
|
self:SetCurvature(false) |
|
self:ClearEnumFlags(efResting) |
|
local start_velocity = self.flight_start_velocity |
|
if start_velocity then |
|
if start_velocity == point30 then |
|
self:StopInterpolation() |
|
else |
|
self:SetPos(self:GetVisualPos() + start_velocity, 1000) |
|
end |
|
self.flight_start_velocity = nil |
|
end |
|
end |
|
self.flying = flying |
|
self:OnFlyingChanged(flying) |
|
end |
|
|
|
FlyingMovable.OnFlyingChanged = empty_func |
|
|
|
function FlyingMovableAutoResolve:OnStopMoving(pf_status) |
|
if self.flying then |
|
if pf_status and IsExactlyOnPassableLevel(self) then |
|
|
|
self:SetFlying(false) |
|
else |
|
self:ClearFlightPath() |
|
end |
|
end |
|
self.flight_pf_ready = nil |
|
self.flight_landed = nil |
|
self.flight_land_pos = nil |
|
self.flight_land_target_pos = nil |
|
self.flight_takeoff_pos = nil |
|
self.flight_start_velocity = nil |
|
self.flight_takeoff_retry = nil |
|
self.flight_land_retry = nil |
|
self.FlightPlanning = nil |
|
end |
|
|
|
local function CanFlyToFilter(x, y, z, self) |
|
return self:CanFlyTo(x, y, z) |
|
end |
|
|
|
function FlyingMovable:FindLandingPos(flight_dests) |
|
if not next(flight_dests) then |
|
return |
|
end |
|
self:MarkFlightArea(flight_dests[#flight_dests]) |
|
local count = Min(4, #flight_dests) |
|
for i=1,count do |
|
local land_pos = FlightFindLandingAround(flight_dests[i], self, dest_search_dist) |
|
if land_pos then |
|
assert(IsPosOutside(land_pos)) |
|
return land_pos |
|
end |
|
end |
|
local land_pos = FlightFindReachableLanding(flight_dests, self) |
|
if land_pos then |
|
return land_pos |
|
end |
|
local has_passable |
|
for _, pt in ipairs(flight_dests) do |
|
if self:CheckPassable(pt) then |
|
if self:CanFlyTo(pt) then |
|
return pt |
|
end |
|
has_passable = true |
|
end |
|
end |
|
if not has_passable then |
|
return |
|
end |
|
for _, pt in ipairs(flight_dests) do |
|
local land_pos = terrain.FindReachable(pt, |
|
tfrPassClass, self, |
|
tfrCanDestlock, self, |
|
tfrLimitDist, max_search_dist, 0, |
|
tfrLuaFilter, CanFlyToFilter, self) |
|
if land_pos then |
|
return land_pos |
|
end |
|
end |
|
end |
|
|
|
function FlyingMovable:FindTakeoffPos() |
|
self:MarkFlightAround(self, max_takeoff_dist) |
|
|
|
|
|
local takeoff_pos, takeoff_reached = FlightFindLandingAround(self, self, max_search_dist) |
|
if not takeoff_pos then |
|
takeoff_pos, takeoff_reached = FlightFindReachableLanding(self, self, "takeoff", max_takeoff_dist) |
|
if not takeoff_pos and self:CanTakeOff() then |
|
takeoff_pos, takeoff_reached = self, true |
|
end |
|
end |
|
assert(IsPosOutside(takeoff_pos)) |
|
return takeoff_pos, takeoff_reached |
|
end |
|
|
|
function FlyingMovable:IsShortPath(walk_excess, max_walk_dist, min_flight_dist) |
|
if self:IsPathPartial() then |
|
return |
|
end |
|
local last = self:GetPathPointCount() > 0 and self:GetPathPoint(1) |
|
if not last then |
|
return true |
|
end |
|
local dist = pf.GetLinearDist(self, last) |
|
if max_walk_dist and dist > max_walk_dist then |
|
return |
|
end |
|
local short_path_len = Max(min_flight_dist or 0, Min(max_walk_dist or max_int, dist * (100 + (walk_excess or 0)) / 100)) |
|
local ignore_tunnels = true |
|
local path_len = self:GetPathLen(1, short_path_len, ignore_tunnels) |
|
return path_len <= short_path_len |
|
end |
|
|
|
function FlyingMovable:Step(dest, ...) |
|
local flight_planning = self.FlightPlanning |
|
if self.flying then |
|
if not flight_planning or self.flight_land_retry > GameTime() then |
|
return FlyingObj.Step(self, dest, ...) |
|
end |
|
local moving_target = IsValid(dest) and dest:TimeToPosInterpolationEnd() > 0 |
|
if moving_target and self:CanFlyTo(dest) then |
|
self.flight_land_pos = nil |
|
return FlyingObj.Step(self, dest, ...) |
|
end |
|
local land_pos = self.flight_land_pos |
|
if land_pos and moving_target then |
|
local prev_target_pos = self.flight_land_target_pos |
|
if not prev_target_pos or not dest:IsCloser(prev_target_pos, self.FlightMaxWalkDist / 2) then |
|
land_pos = false |
|
end |
|
end |
|
if not land_pos then |
|
local dests = pf.ResolveGotoDests(self, dest, ...) |
|
if not dests then |
|
return pfFailed |
|
end |
|
land_pos = self:FindLandingPos(dests) |
|
if not land_pos then |
|
if self.flight_plan_force_land then |
|
return pfFailed |
|
end |
|
self:SetAdjustFlightTarget(true) |
|
self.flight_land_retry = GameTime() + 10000 |
|
return FlyingObj.Step(self, dest, ...) |
|
end |
|
self.flight_land_pos = land_pos |
|
self.flight_land_retry = nil |
|
self.flight_land_target_pos = moving_target and dest:GetVisualPos() |
|
|
|
end |
|
local status = FlyingObj.Step(self, land_pos) |
|
if status == pfFinished then |
|
self.flight_land_pos = nil |
|
self.flight_landed = true |
|
self:SetFlying(false) |
|
return self:Step(dest, ...) |
|
end |
|
return status |
|
end |
|
local walk_excess = self.FlightWalkExcess |
|
if not walk_excess then |
|
return Movable.Step(self, dest, ...) |
|
end |
|
local tx, ty, tz, max_range, min_range, dist, sl = self:ResolveFlightTarget(dest, ...) |
|
if sl then |
|
return Movable.Step(self, dest, ...) |
|
end |
|
if not tx then |
|
return pfFailed |
|
end |
|
local max_walk_dist, min_flight_dist = self.FlightMaxWalkDist, self.FlightMinDist |
|
if not self.FlightPlanning then |
|
local flight_pf_ready = self.flight_pf_ready |
|
local can_fly_to = self:CanFlyTo(tx, ty, tz) |
|
if not flight_pf_ready and max_walk_dist and can_fly_to then |
|
|
|
if dist > max_walk_dist then |
|
self:SetFlying(true) |
|
return self:Step(dest, ...) |
|
end |
|
self:RestrictArea(max_walk_dist) |
|
end |
|
self.flight_pf_ready = true |
|
local status, new_path = Movable.Step(self, dest, ...) |
|
if status == pfFinished or not can_fly_to or (status >= 0 or status == pfTunnel) and self:IsShortPath(walk_excess, max_walk_dist, min_flight_dist) then |
|
return status |
|
end |
|
self:SetFlying(true) |
|
return self:Step(dest, ...) |
|
end |
|
if self.flight_landed or self.flight_takeoff_retry > GameTime() then |
|
return Movable.Step(self, dest, ...) |
|
end |
|
self.flight_start_velocity = self:GetVelocityVector(-1) |
|
local takeoff_pos = self.flight_takeoff_pos |
|
local takeoff_reached |
|
if not takeoff_pos then |
|
local pf_step = true |
|
local flight_pf_ready = self.flight_pf_ready |
|
if self:CheckPassable() then |
|
if not flight_pf_ready then |
|
pf_step = max_range == 0 and ConnectivityCheck(self, dest, ...) |
|
else |
|
pf_step = self:IsShortPath(walk_excess, max_walk_dist, min_flight_dist) |
|
end |
|
end |
|
if pf_step then |
|
self.flight_pf_ready = true |
|
return Movable.Step(self, dest, ...) |
|
end |
|
takeoff_pos, takeoff_reached = self:FindTakeoffPos() |
|
if not takeoff_pos then |
|
self.flight_takeoff_retry = GameTime() + 10000 |
|
|
|
return self:Step(dest, ...) |
|
elseif not takeoff_reached then |
|
|
|
self.flight_pf_ready = nil |
|
self.flight_takeoff_pos = takeoff_pos |
|
|
|
end |
|
end |
|
if not takeoff_reached then |
|
local status = Movable.Step(self, takeoff_pos) |
|
if status ~= pfFinished then |
|
return status |
|
end |
|
end |
|
self.flight_takeoff_pos = nil |
|
if not terrain.IsPassable(tx, ty, tz, 0) then |
|
|
|
if not self:CanFlyTo(tx, ty, tz) then |
|
return pfFailed |
|
end |
|
self:SetFlying(true) |
|
else |
|
local dests = pf.ResolveGotoDests(self, dest, ...) |
|
local land_pos = self:FindLandingPos(dests) |
|
if not land_pos or self:IsCloserWalkDist(land_pos, min_flight_dist) then |
|
self.flight_takeoff_retry = GameTime() + 10000 |
|
else |
|
self.flight_land_pos = land_pos |
|
self:SetFlying(true) |
|
end |
|
end |
|
return self:Step(dest, ...) |
|
end |
|
|
|
function FlyingMovable:TryContinueMove(status, ...) |
|
if status == pfFinished then |
|
return |
|
end |
|
if not self.FlightPlanning then |
|
return Movable.TryContinueMove(self, status, ...) |
|
end |
|
local success = Movable.TryContinueMove(self, status, ...) |
|
if success then |
|
return true |
|
end |
|
local take_off |
|
if self.flying then |
|
if not self.flight_land_pos then |
|
return |
|
end |
|
self.flight_land_pos = nil |
|
elseif self.flight_landed then |
|
self.flight_landed = nil |
|
elseif self.flight_takeoff_pos then |
|
self.flight_takeoff_pos = nil |
|
elseif self:CanTakeOff() and (status ~= pfDestLocked or pf.GetLinearDist(self, ...) >= FlightTile) then |
|
take_off = true |
|
else |
|
return |
|
end |
|
local time = GameTime() |
|
if time - self.flight_plan_failed > self.FlightFailureCooldown then |
|
self.flight_plan_failures = nil |
|
elseif self.flight_plan_failures < self.FlightMaxFailures then |
|
self.flight_plan_failures = self.flight_plan_failures + 1 |
|
else |
|
return |
|
end |
|
self.flight_plan_failed = time |
|
if take_off then |
|
self:TakeOff() |
|
end |
|
return true |
|
end |
|
|
|
function FlyingMovable:ClearPath() |
|
if self.flying then |
|
return self:ClearFlightPath() |
|
end |
|
return Movable.ClearPath(self) |
|
end |
|
|
|
function FlyingMovable:GetPathHash(seed) |
|
if self.flying then |
|
return FlyingObj.GetPathHash(self, seed) |
|
end |
|
return Movable.GetPathHash(self, seed) |
|
end |
|
|
|
function FlyingMovable:LockFlightDest(x, y, z) |
|
local visual_z = ResolveZ(x, y, z) |
|
if not visual_z then |
|
return |
|
end |
|
|
|
if self.outside_pathfinder |
|
or not self:IsCloser(x, y, visual_z, pfSmartDestlockDist) |
|
or not self:CheckPassable(x, y, z) |
|
or PlaceDestlock(self, x, y, z) then |
|
return x, y, z |
|
end |
|
local flight_target = self.flight_target |
|
if not flight_target or flight_target:Equal(x, y, z) or not PlaceDestlock(self, flight_target) then |
|
|
|
flight_target = terrain.FindReachable(x, y, z, |
|
tfrPassClass, self, |
|
tfrCanDestlock, self) |
|
if not flight_target then |
|
return |
|
end |
|
local destlocked = PlaceDestlock(self, flight_target) |
|
assert(destlocked) |
|
end |
|
return flight_target:xyz() |
|
end |
|
|
|
function FlyingMovable:UnlockFlightDest() |
|
if IsValid(self) then |
|
return self:RemoveDestlock() |
|
end |
|
end |
|
|
|
function FlyingMovable:TryLand() |
|
if not self.flying then |
|
return |
|
end |
|
local z = terrain.FindPassableZ(self, 32*guim) |
|
if not z then |
|
return |
|
end |
|
self:ClearPath() |
|
local visual_z = z == InvalidZ and terrain.GetHeight(self) or z |
|
local x, y, z0 = self:GetVisualPosXYZ() |
|
local anim = self.FlightAnimStop |
|
local dt = anim and self:GetAnimDuration(anim) or 0 |
|
if dt > 0 then |
|
self:SetState(anim) |
|
else |
|
dt = 1000 |
|
end |
|
self:SetPos(x, y, visual_z, dt) |
|
self:SetAcceleration(0) |
|
self:ResetOrientation(dt) |
|
self:SetAnimSpeed(1, 1000) |
|
self:SetFlying(false) |
|
end |
|
|
|
function FlyingMovable:TryTakeOff() |
|
self:TakeOff() |
|
return true |
|
end |
|
|
|
function FlyingMovable:TakeOff() |
|
if self.flying then |
|
return |
|
end |
|
self:ClearPath() |
|
local x, y, z0 = self:GetVisualPosXYZ() |
|
local z = z0 + self.FlightSimHeightMin |
|
local anim = self.FlightAnimStart |
|
local dt = anim and self:GetAnimDuration(anim) or 0 |
|
if dt > 0 then |
|
self:SetState(anim) |
|
else |
|
dt = 1000 |
|
end |
|
self:SetPos(x, y, z, dt) |
|
self:SetAcceleration(0) |
|
self:SetFlying(true) |
|
return dt |
|
end |
|
|
|
function FlyingMovable:Face(target, time) |
|
if self.flying then |
|
return FlyingObj.Face(self, target, time) |
|
end |
|
return Movable.Face(self, target, time) |
|
end |
|
|
|
|
|
|
|
local efFlightObstacle = const.efFlightObstacle |
|
|
|
DefineClass.FlightObstacle = { |
|
__parents = { "CObject" }, |
|
flags = { cofComponentFlightObstacle = true, efFlightObstacle = true }, |
|
FlightInitObstacle = FlightInitBox, |
|
} |
|
|
|
function FlightObstacle:InitElementConstruction() |
|
self:ClearEnumFlags(efFlightObstacle) |
|
end |
|
|
|
function FlightObstacle:CompleteElementConstruction() |
|
if self:GetComponentFlags(const.cofComponentFlightObstacle) == 0 then |
|
return |
|
end |
|
self:SetEnumFlags(efFlightObstacle) |
|
self:FlightInitObstacle() |
|
end |
|
|
|
function FlightObstacle:OnMoved() |
|
self:FlightInitObstacle() |
|
end |
|
|
|
|
|
|
|
function FlightInitGrids() |
|
local flight_map, energy_map = FlightMap, FlightEnergy |
|
if not flight_map then |
|
flight_map, energy_map = FlightCreateGrids(mapdata.PassBorder) |
|
FlightMap, FlightEnergy = flight_map, energy_map |
|
end |
|
return flight_map, energy_map |
|
end |
|
|
|
local test_box = box() |
|
|
|
function FlightMarkBetween(ptFrom, ptTo, min_height, obj_radius, mark_border) |
|
min_height = min_height or 0 |
|
obj_radius = obj_radius or 0 |
|
local marked |
|
local flight_area = FlightArea |
|
local now = GameTime() |
|
|
|
if now ~= FlightTimestamp |
|
or not flight_area |
|
or FlightPassVersion ~= PassVersion |
|
or FlightMarkMinHeight ~= min_height |
|
or FlightMarkObjRadius ~= obj_radius |
|
or not FlightIsMarked(flight_area, FlightMarkFrom, FlightMarkTo, FlightMarkBorder, ptFrom, ptTo, mark_border) then |
|
local flight_border |
|
local flight_map = FlightInitGrids() |
|
|
|
flight_area, flight_border = FlightMarkObstacles(flight_map, ptFrom, ptTo, min_height, obj_radius, mark_border) |
|
if not flight_area then |
|
return |
|
end |
|
|
|
FlightEnergyMin = false |
|
FlightMarkMinHeight, FlightMarkObjRadius = min_height, obj_radius |
|
FlightMarkFrom, FlightMarkTo = ResolveVisualPos(ptFrom), ResolveVisualPos(ptTo) |
|
FlightArea = flight_area or false |
|
FlightTimestamp = now |
|
FlightPassVersion = PassVersion |
|
FlightMarkBorder = flight_border |
|
marked = true |
|
end |
|
|
|
return flight_area, marked |
|
end |
|
|
|
function FlightCalcEnergyTo(ptTo, flight_area, slope_penalty, grow_obstacles) |
|
flight_area = flight_area or FlightArea |
|
slope_penalty = slope_penalty or 0 |
|
grow_obstacles = grow_obstacles or false |
|
if not FlightEnergyMin |
|
or FlightArea ~= flight_area |
|
or FlightSlopePenalty ~= slope_penalty |
|
or FlightGrowObstacles ~= grow_obstacles |
|
or not FlightEnergyMin:Equal2D(GameToFlight(ptTo)) then |
|
|
|
FlightEnergyMin = FlightCalcEnergy(FlightMap, FlightEnergy, ptTo, flight_area, slope_penalty, grow_obstacles) or false |
|
FlightSlopePenalty = slope_penalty |
|
FlightGrowObstacles = grow_obstacles |
|
|
|
if not FlightEnergyMin then |
|
return |
|
end |
|
end |
|
return FlightEnergyMin |
|
end |
|
|
|
function FlightCalcPathBetween(ptFrom, ptTo, flags, min_height, obj_radius, slope_penalty, smooth_dist, range, debug_iter) |
|
assert(ptTo and terrain.IsPointInBounds(ptTo, mapdata.PassBorder)) |
|
|
|
local flight_area, marked = FlightMarkBetween(ptFrom, ptTo, min_height, obj_radius) |
|
if not flight_area then |
|
return |
|
end |
|
local grow_obstacles = smooth_dist and IsCloser2D(ptFrom, ptTo, smooth_dist) |
|
if not FlightCalcEnergyTo(ptTo, flight_area, slope_penalty, grow_obstacles) then |
|
return |
|
end |
|
flags = flags or flight_default_flags |
|
range = range or 0 |
|
assert(flags ~= 0) |
|
FlightFrom, FlightTo, FlightFlags, FlightSmoothDist, FlightDestRange = ptFrom, ptTo, flags, smooth_dist, range |
|
return FlightFindPath(ptFrom, ptTo, FlightMap, FlightEnergy, flight_area, flags, range, debug_iter) |
|
end |
|
|
|
|
|
|
|
function FlightInitObstacles() |
|
local _, max_surf_radius = GetMapMaxObjRadius() |
|
local ebox = GetPlayBox():grow(max_surf_radius) |
|
MapForEach(ebox, efFlightObstacle, function(obj) |
|
return obj:FlightInitObstacle() |
|
end) |
|
end |
|
|
|
function FlightInitObstaclesList(objs) |
|
local GetEnumFlags = CObject.GetEnumFlags |
|
for _, obj in ipairs(objs) do |
|
if GetEnumFlags(obj, efFlightObstacle) ~= 0 then |
|
obj:FlightInitObstacle(obj) |
|
end |
|
end |
|
end |
|
|
|
function OnMsg.NewMap() |
|
SuspendProcessing("FlightInitObstacle", "MapLoading", true) |
|
end |
|
|
|
function OnMsg.PostNewMapLoaded() |
|
ResumeProcessing("FlightInitObstacle", "MapLoading", true) |
|
if not mapdata.GameLogic then |
|
return |
|
end |
|
FlightInitObstacles() |
|
end |
|
|
|
function OnMsg.PrefabPlaced(name, objs) |
|
if not mapdata.GameLogic or IsProcessingSuspended("FlightInitObstacle") then |
|
return |
|
end |
|
FlightInitObstaclesList(objs) |
|
end |
|
|
|
function FlightInvalidatePaths(box) |
|
local CheckPassable = pf.CheckPassable |
|
local IsPosOutside = IsPosOutside or return_true |
|
local Point2DInside = box and box.Point2DInside or return_true |
|
local FlightPathIntersectEst = FlightPathIntersectEst |
|
for _, obj in ipairs(FlyingObjs) do |
|
local flight_path = obj.flight_path |
|
if flight_path and #flight_path > 0 and (not box or FlightPathIntersectEst(flight_path, box, obj.flight_spline_idx)) then |
|
obj.flight_path = nil |
|
end |
|
local flight_land_pos = obj.flight_land_pos |
|
if flight_land_pos and Point2DInside(box, flight_land_pos) then |
|
if not CheckPassable(obj, flight_land_pos) or not IsPosOutside(flight_land_pos) then |
|
obj.flight_land_pos = nil |
|
end |
|
end |
|
local flight_takeoff_pos = obj.flight_takeoff_pos |
|
if flight_takeoff_pos and Point2DInside(box, flight_takeoff_pos) then |
|
if not CheckPassable(obj, flight_takeoff_pos) or not IsPosOutside(flight_takeoff_pos) then |
|
obj.flight_takeoff_pos = nil |
|
end |
|
end |
|
end |
|
end |
|
|
|
OnMsg.OnPassabilityChanged = FlightInvalidatePaths |
|
|
|
|
|
|
|
function GetSplineParams(start_pos, start_speed, end_pos, end_speed) |
|
local v0 = start_speed:Len() |
|
local v1 = end_speed:Len() |
|
local dist = start_pos:Dist(end_pos) |
|
assert((v0 > 0 or v1 > 0) and (v0 >= 0 and v1 >= 0)) |
|
assert(dist >= 3) |
|
local pa = (dist >= 3 and v0 > 0) and (start_pos + SetLen(start_speed, dist / 3)) or start_pos |
|
local pb = (dist >= 3 and v1 > 0) and (end_pos - SetLen(end_speed, dist / 3)) or end_pos |
|
local spline = { start_pos, pa, pb, end_pos } |
|
local len = Max(BS3_GetSplineLength3D(spline), 1) |
|
local time_est = MulDivRound(1000, 2 * len, v1 + v0) |
|
return spline, len, v0, v1, time_est |
|
end |
|
|
|
function WaitFollowSpline(obj, spline, len, v0, v1, step_time, min_step, max_step, orient, yaw_to_roll_pct) |
|
if not IsValid(obj) then |
|
return |
|
end |
|
len = len or S3_GetSplineLength3D(spline) |
|
v0 = v0 or obj:GetVelocityVector() |
|
v1 = v1 or v0 |
|
step_time = step_time or 50 |
|
min_step = min_step or Max(1, len/100) |
|
max_step = max_step or Max(min_step, len/10) |
|
local roll, pitch, yaw, yaw0 = 0 |
|
if orient and (yaw_to_roll_pct or 0) ~= 0 then |
|
roll, pitch, yaw0 = obj:GetRollPitchYaw() |
|
end |
|
local v = v0 |
|
local dist = 0 |
|
while true do |
|
local step = Clamp(step_time * v / 1000, min_step, max_step) |
|
dist = dist + step |
|
if dist > len - step / 2 then |
|
dist = len |
|
end |
|
local x, y, z, dirx, diry, dirz = BS3_GetSplinePosDir(spline, dist, len) |
|
v = v0 + (v1 - v0) * dist / len |
|
local accel, dt = obj:GetAccelerationAndTime(x, y, z, v) |
|
if orient then |
|
pitch, yaw = GetPitchYaw(dirx, diry, dirz) |
|
if yaw0 then |
|
roll = 10 * AngleDiff(yaw, yaw0) * yaw_to_roll_pct / dt |
|
yaw0 = yaw |
|
end |
|
obj:SetRollPitchYaw(roll, pitch, yaw, dt) |
|
end |
|
obj:SetPos(x, y, z, dt) |
|
obj:SetAcceleration(accel) |
|
if dist == len then |
|
Sleep(dt) |
|
break |
|
end |
|
Sleep(dt - dt/10) |
|
end |
|
if IsValid(obj) then |
|
obj:SetAcceleration(0) |
|
end |
|
end |
|
|
|
local tfpLanding = const.tfpPassClass | const.tfpCanDestlock | const.tfpLimitDist | const.tfpLuaFilter |
|
|
|
function FlightFindLandingAround(pos, unit, max_radius, min_radius) |
|
local flight_map, flight_area = FlightMap, FlightArea |
|
local landing, valid = FlightIsLandingPos(pos, flight_map, flight_area) |
|
if not valid then |
|
return |
|
end |
|
max_radius = max_radius or max_search_dist |
|
min_radius = min_radius or 0 |
|
if not unit:CheckPassable(pos) then |
|
return terrain.FindPassableTile(pos, tfpLanding, max_radius, min_radius, unit, unit, FlightIsLandingPos, flight_map, flight_area) |
|
end |
|
if min_radius <= 0 and landing then |
|
if not unit or unit:CheckPassable(pos, true) then |
|
return pos, true |
|
end |
|
end |
|
|
|
return terrain.FindReachable(pos, |
|
tfrPassClass, unit, |
|
tfrCanDestlock, unit, |
|
tfrLimitDist, max_radius, min_radius, |
|
tfrLuaFilter, FlightIsLandingPos, flight_map, flight_area) |
|
end |
|
|
|
function FlightFindReachableLanding(target, unit, takeoff, radius) |
|
local flight_map = FlightMap |
|
if not flight_map then |
|
return |
|
end |
|
local pfclass = unit and unit:GetPfClass() or 0 |
|
local max_dist, min_dist = radius or max_int, 0 |
|
local x, y, z, reached = FlightFindLanding(flight_map, target, max_dist, min_dist, unit, ConnectivityCheck, target, pfclass, 0, takeoff) |
|
if not x then |
|
return |
|
end |
|
assert(IsPosOutside(x, y, z)) |
|
local landing = point(x, y, z) |
|
if reached then |
|
return landing, true |
|
end |
|
local src, dst |
|
if takeoff then |
|
src, dst = target, landing |
|
else |
|
src, dst = landing, target |
|
end |
|
local path, has_path = pf.GetPosPath(src, dst, pfclass) |
|
if not path or not has_path then |
|
return |
|
end |
|
local i1, i2, di |
|
if takeoff then |
|
i1, i2, di = #path - 1, 2, -1 |
|
else |
|
i1, i2, di = 2, #path - 1, 1 |
|
end |
|
local last_pt |
|
for i=i1,i2,di do |
|
local pt = path[i] |
|
if not pt then break end |
|
if IsValidPos(pt) then |
|
local found = FlightFindLandingAround(pt, unit, step_search_dist) |
|
|
|
if found then |
|
assert(IsPosOutside(found)) |
|
landing = found |
|
break |
|
end |
|
last_pt = pt |
|
end |
|
end |
|
return landing |
|
end |
|
|
|
|
|
|
|
function FlyingObj:CheatRecalcPath() |
|
self:RecalcFlightPath() |
|
end |