Files
OpenRA/OpenRA.Mods.Common/Activities/Air/Fly.cs
2024-07-29 21:56:36 +02:00

284 lines
11 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Activities
{
public class Fly : Activity
{
readonly Aircraft aircraft;
readonly WDist maxRange;
readonly WDist minRange;
readonly Color? targetLineColor;
readonly WDist nearEnough;
Target target;
Target lastVisibleTarget;
bool useLastVisibleTarget;
readonly RingBuffer<WPos> previousPositions = new(5);
public Fly(Actor self, in Target t, WDist nearEnough, WPos? initialTargetPosition = null, Color? targetLineColor = null)
: this(self, t, initialTargetPosition, targetLineColor)
{
this.nearEnough = nearEnough;
}
public Fly(Actor self, in Target t, WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
aircraft = self.Trait<Aircraft>();
target = t;
this.targetLineColor = targetLineColor;
// The target may become hidden between the initial order request and the first tick (e.g. if queued)
// Moving to any position (even if quite stale) is still better than immediately giving up
if ((target.Type == TargetType.Actor && target.Actor.CanBeViewedByPlayer(self.Owner))
|| target.Type == TargetType.FrozenActor || target.Type == TargetType.Terrain)
lastVisibleTarget = Target.FromPos(target.CenterPosition);
else if (initialTargetPosition.HasValue)
lastVisibleTarget = Target.FromPos(initialTargetPosition.Value);
}
public Fly(Actor self, in Target t, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
: this(self, t, initialTargetPosition, targetLineColor)
{
this.maxRange = maxRange;
this.minRange = minRange;
}
public static void FlyTick(Actor self, Aircraft aircraft, WAngle desiredFacing, WDist desiredAltitude, in WVec moveOverride, bool idleTurn = false)
{
var dat = self.World.Map.DistanceAboveTerrain(aircraft.CenterPosition);
var move = moveOverride != WVec.Zero ? moveOverride : (aircraft.Info.CanSlide ? aircraft.FlyStep(desiredFacing) : aircraft.FlyStep(aircraft.Facing));
var oldFacing = aircraft.Facing;
aircraft.Facing = Util.TickFacing(aircraft.Facing, desiredFacing, aircraft.GetTurnSpeed(idleTurn));
var roll = idleTurn ? aircraft.Info.IdleRoll ?? aircraft.Info.Roll : aircraft.Info.Roll;
if (roll != WAngle.Zero)
{
var desiredRoll = aircraft.Facing == desiredFacing ? WAngle.Zero :
new WAngle(roll.Angle * Util.GetTurnDirection(aircraft.Facing, oldFacing));
aircraft.Roll = Util.TickFacing(aircraft.Roll, desiredRoll, aircraft.Info.RollSpeed);
}
if (aircraft.Info.Pitch != WAngle.Zero)
aircraft.Pitch = Util.TickFacing(aircraft.Pitch, aircraft.Info.Pitch, aircraft.Info.PitchSpeed);
// Note: we assume that if move.Z is not zero, it's intentional and we want to move in that vertical direction instead of towards desiredAltitude.
// If that is not desired, the place that calls this should make sure moveOverride.Z is zero.
if (dat != desiredAltitude || move.Z != 0)
{
var maxDelta = move.HorizontalLength * aircraft.Info.MaximumPitch.Tan() / 1024;
var moveZ = move.Z != 0 ? move.Z : (desiredAltitude.Length - dat.Length);
var deltaZ = moveZ.Clamp(-maxDelta, maxDelta);
move = new WVec(move.X, move.Y, deltaZ);
}
aircraft.SetPosition(self, aircraft.CenterPosition + move);
}
public static void FlyTick(Actor self, Aircraft aircraft, WAngle desiredFacing, WDist desiredAltitude, bool idleTurn = false)
{
FlyTick(self, aircraft, desiredFacing, desiredAltitude, WVec.Zero, idleTurn);
}
// Should only be used for vertical-only movement, usually VTOL take-off or land. Terrain-induced altitude changes should always be handled by FlyTick.
public static bool VerticalTakeOffOrLandTick(Actor self, Aircraft aircraft, WAngle desiredFacing, WDist desiredAltitude, bool idleTurn = false)
{
var turnSpeed = idleTurn ? aircraft.IdleTurnSpeed ?? aircraft.TurnSpeed : aircraft.TurnSpeed;
aircraft.Facing = Util.TickFacing(aircraft.Facing, desiredFacing, turnSpeed);
var dat = self.World.Map.DistanceAboveTerrain(aircraft.CenterPosition);
if (dat == desiredAltitude)
return false;
var maxDelta = aircraft.Info.AltitudeVelocity.Length;
var deltaZ = (desiredAltitude.Length - dat.Length).Clamp(-maxDelta, maxDelta);
aircraft.SetPosition(self, aircraft.CenterPosition + new WVec(0, 0, deltaZ));
return true;
}
public override bool Tick(Actor self)
{
// Refuse to take off if it would land immediately again.
if (aircraft.ForceLanding)
Cancel(self);
var dat = self.World.Map.DistanceAboveTerrain(aircraft.CenterPosition);
var isLanded = dat <= aircraft.LandAltitude;
// HACK: Prevent paused (for example, EMP'd) aircraft from taking off.
// This is necessary until the TODOs in the IsCanceling block below are addressed.
if (isLanded && aircraft.IsTraitPaused)
return false;
if (IsCanceling)
{
// We must return the actor to a sensible height before continuing.
// If the aircraft is on the ground we queue TakeOff to manage the influence reservation and takeoff sounds etc.
// TODO: It would be better to not take off at all, but we lack the plumbing to detect current airborne/landed state.
// If the aircraft lands when idle and is idle, we let the default idle handler manage this.
// TODO: Remove this after fixing all activities to work properly with arbitrary starting altitudes.
var landWhenIdle = aircraft.Info.IdleBehavior == IdleBehaviorType.Land;
var skipHeightAdjustment = landWhenIdle && self.CurrentActivity.IsCanceling && self.CurrentActivity.NextActivity == null;
if (aircraft.Info.CanHover && !skipHeightAdjustment && dat != aircraft.Info.CruiseAltitude)
{
if (isLanded)
QueueChild(new TakeOff(self));
else
VerticalTakeOffOrLandTick(self, aircraft, aircraft.Facing, aircraft.Info.CruiseAltitude);
return false;
}
return true;
}
else if (isLanded)
{
QueueChild(new TakeOff(self));
return false;
}
target = target.Recalculate(self.Owner, out var targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
lastVisibleTarget = Target.FromTargetPositions(target);
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
// Target is hidden or dead, and we don't have a fallback position to move towards
if (useLastVisibleTarget && !lastVisibleTarget.IsValidFor(self))
return true;
var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target;
var pos = aircraft.GetPosition();
var delta = checkTarget.CenterPosition - pos;
// Inside the target annulus, so we're done
var insideMaxRange = maxRange.Length > 0 && checkTarget.IsInRange(pos, maxRange);
var insideMinRange = minRange.Length > 0 && checkTarget.IsInRange(pos, minRange);
if (insideMaxRange && !insideMinRange)
return true;
var isSlider = aircraft.Info.CanSlide;
var desiredFacing = aircraft.Facing;
if (delta.HorizontalLengthSquared != 0)
{
var facing = delta.Yaw;
// Prevent jittering.
var diff = Math.Abs(facing.Angle - desiredFacing.Angle);
var deadzone = aircraft.Info.TurnDeadzone.Angle;
if (diff > deadzone && diff < 1024 - deadzone)
desiredFacing = facing;
}
var move = isSlider ? aircraft.FlyStep(desiredFacing) : aircraft.FlyStep(aircraft.Facing);
// Inside the minimum range, so reverse if we CanSlide, otherwise face away from the target.
if (insideMinRange)
{
if (isSlider)
FlyTick(self, aircraft, desiredFacing, aircraft.Info.CruiseAltitude, -move);
else
FlyTick(self, aircraft, desiredFacing + new WAngle(512), aircraft.Info.CruiseAltitude, move);
return false;
}
// HACK: Consider ourselves blocked if we have moved by less than 64 WDist in the last five ticks
// Stop if we are blocked and close enough
if (previousPositions.Count == previousPositions.Capacity &&
(previousPositions.First() - previousPositions.Last()).LengthSquared < 4096 &&
delta.HorizontalLengthSquared <= nearEnough.LengthSquared)
return true;
// The next move would overshoot, so consider it close enough or set final position if we CanSlide
if (delta.HorizontalLengthSquared < move.HorizontalLengthSquared)
{
// For VTOL landing to succeed, it must reach the exact target position,
// so for the final move it needs to behave as if it had CanSlide.
if (isSlider || aircraft.Info.VTOL)
{
// Set final (horizontal) position
if (delta.HorizontalLengthSquared != 0)
{
// Ensure we don't include a non-zero vertical component here that would move us away from CruiseAltitude
var deltaMove = new WVec(delta.X, delta.Y, 0);
FlyTick(self, aircraft, desiredFacing, dat, deltaMove);
}
// Move to CruiseAltitude, if not already there
if (dat != aircraft.Info.CruiseAltitude)
{
VerticalTakeOffOrLandTick(self, aircraft, aircraft.Facing, aircraft.Info.CruiseAltitude);
return false;
}
}
return true;
}
if (!isSlider)
{
// Using the turn rate, compute a hypothetical circle traced by a continuous turn.
// If it contains the destination point, it's unreachable without more complex maneuvering.
var turnRadius = CalculateTurnRadius(aircraft.MovementSpeed, aircraft.TurnSpeed);
// The current facing is a tangent of the minimal turn circle.
// Make a perpendicular vector, and use it to locate the turn's center.
var turnCenterFacing = aircraft.Facing + new WAngle(Util.GetTurnDirection(aircraft.Facing, desiredFacing) * 256);
var turnCenterDir = new WVec(0, -1024, 0).Rotate(WRot.FromYaw(turnCenterFacing));
turnCenterDir *= turnRadius;
turnCenterDir /= 1024;
// Compare with the target point, and keep flying away if it's inside the circle.
var turnCenter = aircraft.CenterPosition + turnCenterDir;
if ((checkTarget.CenterPosition - turnCenter).HorizontalLengthSquared < turnRadius * turnRadius)
desiredFacing = aircraft.Facing;
}
previousPositions.Add(self.CenterPosition);
FlyTick(self, aircraft, desiredFacing, aircraft.Info.CruiseAltitude, WVec.Zero);
return false;
}
public override IEnumerable<Target> GetTargets(Actor self)
{
yield return target;
}
public override IEnumerable<TargetLineNode> TargetLineNodes(Actor self)
{
if (targetLineColor.HasValue)
yield return new TargetLineNode(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value);
}
public static int CalculateTurnRadius(int speed, WAngle turnSpeed)
{
// turnSpeed -> divide into 256 to get the number of ticks per complete rotation
// speed -> multiply to get distance travelled per rotation (circumference)
// 180 -> divide by 2*pi to get the turn radius: 180==1024/(2*pi), with some extra leeway
return turnSpeed.Angle > 0 ? 180 * speed / turnSpeed.Angle : 0;
}
}
}