1385 lines
45 KiB
C#
1385 lines
45 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 System.Linq;
|
|
using OpenRA.Activities;
|
|
using OpenRA.Mods.Common.Activities;
|
|
using OpenRA.Mods.Common.Orders;
|
|
using OpenRA.Primitives;
|
|
using OpenRA.Support;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
public enum IdleBehaviorType
|
|
{
|
|
None,
|
|
Land,
|
|
ReturnToBase,
|
|
LeaveMap,
|
|
LeaveMapAtClosestEdge
|
|
}
|
|
|
|
public class AircraftInfo : PausableConditionalTraitInfo, IPositionableInfo, IFacingInfo, IMoveInfo, ICruiseAltitudeInfo,
|
|
IActorPreviewInitInfo, IEditorActorOptions
|
|
{
|
|
[Desc("Behavior when aircraft becomes idle. Options are Land, ReturnToBase, LeaveMap, and None.",
|
|
"'Land' will behave like 'None' (hover or circle) if a suitable landing site is not available.")]
|
|
public readonly IdleBehaviorType IdleBehavior = IdleBehaviorType.None;
|
|
|
|
public readonly WDist CruiseAltitude = new(1280);
|
|
|
|
[Desc("Whether the aircraft can be repulsed.")]
|
|
public readonly bool Repulsable = true;
|
|
|
|
[Desc("The distance it tries to maintain from other aircraft if repulsable.")]
|
|
public readonly WDist IdealSeparation = new(1706);
|
|
|
|
[Desc("The speed at which the aircraft is repulsed from other aircraft. Specify -1 for normal movement speed.")]
|
|
public readonly int RepulsionSpeed = -1;
|
|
|
|
public readonly WAngle InitialFacing = WAngle.Zero;
|
|
|
|
[Desc("Speed at which the actor turns.")]
|
|
public readonly WAngle TurnSpeed = new(512);
|
|
|
|
[Desc("Turn speed to apply when aircraft flies in circles while idle. Defaults to TurnSpeed if undefined.")]
|
|
public readonly WAngle? IdleTurnSpeed = null;
|
|
|
|
[Desc("When flying if the difference between current facing and desired facing is less than this value, don't turn. This prevents visual jitter.")]
|
|
public readonly WAngle TurnDeadzone = new(2);
|
|
|
|
[Desc("Maximum flight speed when cruising.")]
|
|
public readonly int Speed = 1;
|
|
|
|
[Desc("If non-negative, force the aircraft to move in circles at this speed when idle (a speed of 0 means don't move), ignoring CanHover.")]
|
|
public readonly int IdleSpeed = -1;
|
|
|
|
[Desc("Body pitch when flying forwards. Only relevant for voxel aircraft.")]
|
|
public readonly WAngle Pitch = WAngle.Zero;
|
|
|
|
[Desc("Pitch steps to apply each tick when starting/stopping.")]
|
|
public readonly WAngle PitchSpeed = WAngle.Zero;
|
|
|
|
[Desc("Body roll when turning. Only relevant for voxel aircraft.")]
|
|
public readonly WAngle Roll = WAngle.Zero;
|
|
|
|
[Desc("Body roll to apply when aircraft flies in circles while idle. Defaults to Roll if undefined. Only relevant for voxel aircraft.")]
|
|
public readonly WAngle? IdleRoll = null;
|
|
|
|
[Desc("Roll steps to apply each tick when turning.")]
|
|
public readonly WAngle RollSpeed = WAngle.Zero;
|
|
|
|
[Desc("Minimum altitude where this aircraft is considered airborne.")]
|
|
public readonly int MinAirborneAltitude = 1;
|
|
|
|
public readonly HashSet<string> LandableTerrainTypes = new();
|
|
|
|
[Desc("Can the actor be ordered to move in to shroud?")]
|
|
public readonly bool MoveIntoShroud = true;
|
|
|
|
[Desc("e.g. crate, wall, infantry")]
|
|
public readonly BitSet<CrushClass> Crushes = default;
|
|
|
|
[Desc("Types of damage that are caused while crushing. Leave empty for no damage types.")]
|
|
public readonly BitSet<DamageType> CrushDamageTypes = default;
|
|
|
|
[VoiceReference]
|
|
public readonly string Voice = "Action";
|
|
|
|
[Desc("Color to use for the target line for regular move orders.")]
|
|
public readonly Color TargetLineColor = Color.Green;
|
|
|
|
[GrantedConditionReference]
|
|
[Desc("The condition to grant to self while airborne.")]
|
|
public readonly string AirborneCondition = null;
|
|
|
|
[GrantedConditionReference]
|
|
[Desc("The condition to grant to self while at cruise altitude.")]
|
|
public readonly string CruisingCondition = null;
|
|
|
|
[Desc("Can the actor hover in place mid-air? If not, then the actor will have to remain in motion (circle around).")]
|
|
public readonly bool CanHover = false;
|
|
|
|
[Desc("Can the actor immediately change direction without turning first (doesn't need to fly in a curve)?")]
|
|
public readonly bool CanSlide = false;
|
|
|
|
[Desc("Does the actor land and take off vertically?")]
|
|
public readonly bool VTOL = false;
|
|
|
|
[Desc("Does this VTOL actor need to turn before landing (on terrain)?")]
|
|
public readonly bool TurnToLand = false;
|
|
|
|
[Desc("Does this actor automatically take off after resupplying?")]
|
|
public readonly bool TakeOffOnResupply = false;
|
|
|
|
[Desc("Does this actor automatically take off after creation?")]
|
|
public readonly bool TakeOffOnCreation = true;
|
|
|
|
[Desc("Can this actor be given an explicit land order using the force-move modifier?")]
|
|
public readonly bool CanForceLand = true;
|
|
|
|
[Desc("Altitude at which the aircraft considers itself landed.")]
|
|
public readonly WDist LandAltitude = WDist.Zero;
|
|
|
|
[Desc("Range to search for an alternative landing location if the ordered cell is blocked.")]
|
|
public readonly WDist LandRange = WDist.FromCells(5);
|
|
|
|
[Desc("How fast this actor ascends or descends during horizontal movement.")]
|
|
public readonly WAngle MaximumPitch = WAngle.FromDegrees(10);
|
|
|
|
[Desc("How fast this actor ascends or descends when moving vertically only (vertical take off/landing or hovering towards CruiseAltitude).")]
|
|
public readonly WDist AltitudeVelocity = new(43);
|
|
|
|
[Desc("Sounds to play when the actor is taking off.")]
|
|
public readonly string[] TakeoffSounds = Array.Empty<string>();
|
|
|
|
[Desc("Sounds to play when the actor is landing.")]
|
|
public readonly string[] LandingSounds = Array.Empty<string>();
|
|
|
|
[Desc("The distance of the resupply base that the aircraft will wait for its turn.")]
|
|
public readonly WDist WaitDistanceFromResupplyBase = new(3072);
|
|
|
|
[Desc("The number of ticks that a airplane will wait to make a new search for an available airport.")]
|
|
public readonly int NumberOfTicksToVerifyAvailableAirport = 150;
|
|
|
|
[Desc("Facing to use for actor previews (map editor, color picker, etc)")]
|
|
public readonly WAngle PreviewFacing = new(384);
|
|
|
|
[Desc("Display order for the facing slider in the map editor")]
|
|
public readonly int EditorFacingDisplayOrder = 3;
|
|
|
|
[ConsumedConditionReference]
|
|
[Desc("Boolean expression defining the condition under which the regular (non-force) move cursor is disabled.")]
|
|
public readonly BooleanExpression RequireForceMoveCondition = null;
|
|
|
|
[CursorReference]
|
|
[Desc("Cursor to display when a move order can be issued at target location.")]
|
|
public readonly string Cursor = "move";
|
|
|
|
[CursorReference]
|
|
[Desc("Cursor to display when a move order cannot be issued at target location.")]
|
|
public readonly string BlockedCursor = "move-blocked";
|
|
|
|
[CursorReference]
|
|
[Desc("Cursor to display when able to land at target building.")]
|
|
public readonly string EnterCursor = "enter";
|
|
|
|
[CursorReference]
|
|
[Desc("Cursor to display when unable to land at target building.")]
|
|
public readonly string EnterBlockedCursor = "enter-blocked";
|
|
|
|
public WAngle GetInitialFacing() { return InitialFacing; }
|
|
public WDist GetCruiseAltitude() { return CruiseAltitude; }
|
|
public Color GetTargetLineColor() { return TargetLineColor; }
|
|
|
|
public override object Create(ActorInitializer init) { return new Aircraft(init, this); }
|
|
|
|
IEnumerable<ActorInit> IActorPreviewInitInfo.ActorPreviewInits(ActorInfo ai, ActorPreviewType type)
|
|
{
|
|
yield return new FacingInit(PreviewFacing);
|
|
}
|
|
|
|
public IReadOnlyDictionary<CPos, SubCell> OccupiedCells(ActorInfo info, CPos location, SubCell subCell = SubCell.Any) { return new Dictionary<CPos, SubCell>(); }
|
|
|
|
bool IOccupySpaceInfo.SharesCell => false;
|
|
|
|
// Used to determine if an aircraft can spawn landed
|
|
public bool CanEnterCell(World world, Actor self, CPos cell, SubCell subCell = SubCell.FullCell, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)
|
|
{
|
|
if (!world.Map.Contains(cell))
|
|
return false;
|
|
|
|
var type = world.Map.GetTerrainInfo(cell).Type;
|
|
if (!LandableTerrainTypes.Contains(type))
|
|
return false;
|
|
|
|
if (check == BlockedByActor.None)
|
|
return true;
|
|
|
|
// Since aircraft don't share cells, we don't pass the subCell parameter
|
|
return !world.ActorMap.GetActorsAt(cell).Any(x => x != ignoreActor);
|
|
}
|
|
|
|
IEnumerable<EditorActorOption> IEditorActorOptions.ActorOptions(ActorInfo ai, World world)
|
|
{
|
|
yield return new EditorActorSlider("Facing", EditorFacingDisplayOrder, 0, 1023, 8,
|
|
actor =>
|
|
{
|
|
var init = actor.GetInitOrDefault<FacingInit>(this);
|
|
return (init != null ? init.Value : InitialFacing).Angle;
|
|
},
|
|
(actor, value) => actor.ReplaceInit(new FacingInit(new WAngle((int)value))));
|
|
}
|
|
}
|
|
|
|
public class Aircraft : PausableConditionalTrait<AircraftInfo>, ITick, ISync, IFacing, IPositionable, IMove,
|
|
INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyActorDisposing, INotifyBecomingIdle, ICreationActivity,
|
|
IActorPreviewInitModifier, IDeathActorInitModifier, IIssueDeployOrder, IIssueOrder, IResolveOrder, IOrderVoice
|
|
{
|
|
readonly Actor self;
|
|
|
|
Repairable repairable;
|
|
Rearmable rearmable;
|
|
IAircraftCenterPositionOffset[] positionOffsets;
|
|
IDisposable reservation;
|
|
IEnumerable<int> speedModifiers;
|
|
INotifyMoving[] notifyMoving;
|
|
INotifyCenterPositionChanged[] notifyCenterPositionChanged;
|
|
IOverrideAircraftLanding overrideAircraftLanding;
|
|
|
|
[Sync]
|
|
public WAngle Facing
|
|
{
|
|
get => Orientation.Yaw;
|
|
set => Orientation = Orientation.WithYaw(value);
|
|
}
|
|
|
|
public WAngle Pitch
|
|
{
|
|
get => Orientation.Pitch;
|
|
set => Orientation = Orientation.WithPitch(value);
|
|
}
|
|
|
|
public WAngle Roll
|
|
{
|
|
get => Orientation.Roll;
|
|
set => Orientation = Orientation.WithRoll(value);
|
|
}
|
|
|
|
public WRot Orientation { get; private set; }
|
|
|
|
[Sync]
|
|
public WPos CenterPosition { get; private set; }
|
|
|
|
public CPos TopLeft => self.World.Map.CellContaining(CenterPosition);
|
|
public WAngle TurnSpeed => IsTraitDisabled || IsTraitPaused ? WAngle.Zero : Info.TurnSpeed;
|
|
public WAngle? IdleTurnSpeed => IsTraitDisabled || IsTraitPaused ? null : Info.IdleTurnSpeed;
|
|
|
|
public WAngle GetTurnSpeed(bool isIdleTurn)
|
|
{
|
|
// A MovementSpeed of zero indicates either a speed modifier of zero percent or that the trait is paused or disabled.
|
|
// Bail early in that case.
|
|
if ((isIdleTurn && IdleMovementSpeed == 0) || MovementSpeed == 0)
|
|
return WAngle.Zero;
|
|
|
|
var turnSpeed = isIdleTurn ? IdleTurnSpeed ?? TurnSpeed : TurnSpeed;
|
|
|
|
return new WAngle(Util.ApplyPercentageModifiers(turnSpeed.Angle, speedModifiers).Clamp(1, 1024));
|
|
}
|
|
|
|
public Actor ReservedActor { get; private set; }
|
|
public bool MayYieldReservation { get; private set; }
|
|
public bool ForceLanding { get; private set; }
|
|
|
|
(CPos, SubCell)[] landingCells = Array.Empty<(CPos, SubCell)>();
|
|
public bool RequireForceMove;
|
|
|
|
readonly int creationActivityDelay;
|
|
readonly bool creationByMap;
|
|
readonly CPos[] creationRallyPoint;
|
|
|
|
bool notify = true;
|
|
|
|
public static WPos GroundPosition(Actor self)
|
|
{
|
|
return self.CenterPosition - new WVec(WDist.Zero, WDist.Zero, self.World.Map.DistanceAboveTerrain(self.CenterPosition));
|
|
}
|
|
|
|
public bool AtLandAltitude => self.World.Map.DistanceAboveTerrain(GetPosition()) == LandAltitude;
|
|
|
|
bool airborne;
|
|
bool cruising;
|
|
int airborneToken = Actor.InvalidConditionToken;
|
|
int cruisingToken = Actor.InvalidConditionToken;
|
|
|
|
MovementType movementTypes;
|
|
WPos cachedPosition;
|
|
WAngle cachedFacing;
|
|
|
|
public Aircraft(ActorInitializer init, AircraftInfo info)
|
|
: base(info)
|
|
{
|
|
self = init.Self;
|
|
|
|
var locationInit = init.GetOrDefault<LocationInit>();
|
|
var centerPositionInit = init.GetOrDefault<CenterPositionInit>();
|
|
if (locationInit != null || centerPositionInit != null)
|
|
{
|
|
var pos = centerPositionInit?.Value ?? self.World.Map.CenterOfCell(locationInit.Value);
|
|
creationByMap = init.Contains<SpawnedByMapInit>();
|
|
SetPosition(self, pos);
|
|
}
|
|
|
|
Facing = init.GetValue<FacingInit, WAngle>(Info.InitialFacing);
|
|
creationActivityDelay = init.GetValue<CreationActivityDelayInit, int>(0);
|
|
creationRallyPoint = init.GetOrDefault<RallyPointInit>()?.Value;
|
|
}
|
|
|
|
public WDist LandAltitude
|
|
{
|
|
get
|
|
{
|
|
var alt = Info.LandAltitude;
|
|
foreach (var offset in positionOffsets)
|
|
alt -= new WDist(offset.PositionOffset.Z);
|
|
|
|
return alt;
|
|
}
|
|
}
|
|
|
|
public WPos GetPosition()
|
|
{
|
|
var pos = self.CenterPosition;
|
|
foreach (var offset in positionOffsets)
|
|
pos += offset.PositionOffset;
|
|
|
|
return pos;
|
|
}
|
|
|
|
public override IEnumerable<VariableObserver> GetVariableObservers()
|
|
{
|
|
foreach (var observer in base.GetVariableObservers())
|
|
yield return observer;
|
|
|
|
if (Info.RequireForceMoveCondition != null)
|
|
yield return new VariableObserver(RequireForceMoveConditionChanged, Info.RequireForceMoveCondition.Variables);
|
|
}
|
|
|
|
void RequireForceMoveConditionChanged(Actor self, IReadOnlyDictionary<string, int> conditions)
|
|
{
|
|
RequireForceMove = Info.RequireForceMoveCondition.Evaluate(conditions);
|
|
}
|
|
|
|
protected override void Created(Actor self)
|
|
{
|
|
repairable = self.TraitOrDefault<Repairable>();
|
|
rearmable = self.TraitOrDefault<Rearmable>();
|
|
speedModifiers = self.TraitsImplementing<ISpeedModifier>().ToArray().Select(sm => sm.GetSpeedModifier());
|
|
cachedPosition = self.CenterPosition;
|
|
notifyMoving = self.TraitsImplementing<INotifyMoving>().ToArray();
|
|
positionOffsets = self.TraitsImplementing<IAircraftCenterPositionOffset>().ToArray();
|
|
overrideAircraftLanding = self.TraitOrDefault<IOverrideAircraftLanding>();
|
|
notifyCenterPositionChanged = self.TraitsImplementing<INotifyCenterPositionChanged>().ToArray();
|
|
base.Created(self);
|
|
}
|
|
|
|
void INotifyAddedToWorld.AddedToWorld(Actor self)
|
|
{
|
|
AddedToWorld(self);
|
|
}
|
|
|
|
protected virtual void AddedToWorld(Actor self)
|
|
{
|
|
self.World.AddToMaps(self, this);
|
|
|
|
var altitude = self.World.Map.DistanceAboveTerrain(CenterPosition);
|
|
if (altitude.Length >= Info.MinAirborneAltitude)
|
|
OnAirborneAltitudeReached();
|
|
if (altitude == Info.CruiseAltitude)
|
|
OnCruisingAltitudeReached();
|
|
}
|
|
|
|
void INotifyRemovedFromWorld.RemovedFromWorld(Actor self)
|
|
{
|
|
RemovedFromWorld(self);
|
|
}
|
|
|
|
protected virtual void RemovedFromWorld(Actor self)
|
|
{
|
|
UnReserve();
|
|
self.World.RemoveFromMaps(self, this);
|
|
|
|
OnCruisingAltitudeLeft();
|
|
OnAirborneAltitudeLeft();
|
|
}
|
|
|
|
void ITick.Tick(Actor self)
|
|
{
|
|
Tick(self);
|
|
}
|
|
|
|
protected virtual void Tick(Actor self)
|
|
{
|
|
// Add land activity if Aircraft trait is paused and the actor can land at the current location.
|
|
if (!ForceLanding && IsTraitPaused && airborne && CanLand(self.Location)
|
|
&& !((self.CurrentActivity is Land) || self.CurrentActivity is Turn))
|
|
{
|
|
self.QueueActivity(false, new Land(self));
|
|
ForceLanding = true;
|
|
}
|
|
|
|
// Add takeoff activity if Aircraft trait is not paused and the actor should not land when idle.
|
|
if (ForceLanding && !IsTraitPaused && !cruising && self.CurrentActivity is not TakeOff)
|
|
{
|
|
ForceLanding = false;
|
|
|
|
if (Info.IdleBehavior != IdleBehaviorType.Land)
|
|
self.QueueActivity(false, new TakeOff(self));
|
|
}
|
|
|
|
var oldCachedFacing = cachedFacing;
|
|
cachedFacing = Facing;
|
|
|
|
var oldCachedPosition = cachedPosition;
|
|
cachedPosition = self.CenterPosition;
|
|
|
|
var newMovementTypes = MovementType.None;
|
|
if (oldCachedFacing != Facing)
|
|
newMovementTypes |= MovementType.Turn;
|
|
|
|
if ((oldCachedPosition - cachedPosition).HorizontalLengthSquared != 0)
|
|
newMovementTypes |= MovementType.Horizontal;
|
|
|
|
if ((oldCachedPosition - cachedPosition).VerticalLengthSquared != 0)
|
|
newMovementTypes |= MovementType.Vertical;
|
|
|
|
CurrentMovementTypes = newMovementTypes;
|
|
|
|
if (!CurrentMovementTypes.HasMovementType(MovementType.Horizontal))
|
|
{
|
|
if (Info.Roll != WAngle.Zero && Roll != WAngle.Zero)
|
|
Roll = Util.TickFacing(Roll, WAngle.Zero, Info.RollSpeed);
|
|
|
|
if (Info.Pitch != WAngle.Zero && Pitch != WAngle.Zero)
|
|
Pitch = Util.TickFacing(Pitch, WAngle.Zero, Info.PitchSpeed);
|
|
}
|
|
|
|
Repulse();
|
|
}
|
|
|
|
public void Repulse()
|
|
{
|
|
var repulsionForce = GetRepulsionForce();
|
|
if (repulsionForce == WVec.Zero)
|
|
return;
|
|
|
|
var speed = Info.RepulsionSpeed != -1 ? Info.RepulsionSpeed : MovementSpeed;
|
|
|
|
// HACK: Prevent updating visibility twice per tick. We really shouldn't be
|
|
// moving twice in a tick in the first place.
|
|
notify = false;
|
|
SetPosition(self, CenterPosition + FlyStep(speed, repulsionForce.Yaw));
|
|
notify = true;
|
|
}
|
|
|
|
public virtual WVec GetRepulsionForce()
|
|
{
|
|
if (!Info.Repulsable)
|
|
return WVec.Zero;
|
|
|
|
if (reservation != null)
|
|
{
|
|
var distanceFromReservationActor = (ReservedActor.CenterPosition - self.CenterPosition).HorizontalLength;
|
|
if (distanceFromReservationActor < Info.WaitDistanceFromResupplyBase.Length)
|
|
return WVec.Zero;
|
|
}
|
|
|
|
// Repulsion only applies when we're flying at CruiseAltitude!
|
|
if (!cruising)
|
|
return WVec.Zero;
|
|
|
|
// PERF: Avoid LINQ.
|
|
var repulsionForce = WVec.Zero;
|
|
foreach (var actor in self.World.FindActorsInCircle(self.CenterPosition, Info.IdealSeparation))
|
|
{
|
|
if (actor.IsDead)
|
|
continue;
|
|
|
|
var ai = actor.Info.TraitInfoOrDefault<AircraftInfo>();
|
|
if (ai == null || !ai.Repulsable || ai.CruiseAltitude != Info.CruiseAltitude)
|
|
continue;
|
|
|
|
repulsionForce += GetRepulsionForce(actor);
|
|
}
|
|
|
|
// Actors outside the map bounds receive an extra nudge towards the center of the map
|
|
if (!self.World.Map.Contains(self.Location))
|
|
{
|
|
// The map bounds are in projected coordinates, which is technically wrong for this,
|
|
// but we avoid the issues in practice by guessing the middle of the map instead of the edge
|
|
var center = WPos.Lerp(self.World.Map.ProjectedTopLeft, self.World.Map.ProjectedBottomRight, 1, 2);
|
|
repulsionForce += new WVec(0, 1024, 0).Rotate(WRot.FromYaw((self.CenterPosition - center).Yaw));
|
|
}
|
|
|
|
if (Info.CanSlide)
|
|
return repulsionForce;
|
|
|
|
// Non-hovering actors mush always keep moving forward, so they need extra calculations.
|
|
var currentDir = FlyStep(Facing);
|
|
var length = currentDir.HorizontalLength * repulsionForce.HorizontalLength;
|
|
if (length == 0)
|
|
return WVec.Zero;
|
|
|
|
var dot = WVec.Dot(currentDir, repulsionForce) / length;
|
|
|
|
// avoid stalling the plane
|
|
return dot >= 0 ? repulsionForce : WVec.Zero;
|
|
}
|
|
|
|
public WVec GetRepulsionForce(Actor other)
|
|
{
|
|
if (self == other || other.CenterPosition.Z < self.CenterPosition.Z)
|
|
return WVec.Zero;
|
|
|
|
var d = self.CenterPosition - other.CenterPosition;
|
|
var distSq = d.HorizontalLengthSquared;
|
|
if (distSq > Info.IdealSeparation.LengthSquared)
|
|
return WVec.Zero;
|
|
|
|
if (distSq < 1)
|
|
{
|
|
var yaw = self.World.SharedRandom.Next(0, 1023);
|
|
var rot = new WRot(WAngle.Zero, WAngle.Zero, new WAngle(yaw));
|
|
return new WVec(1024, 0, 0).Rotate(rot);
|
|
}
|
|
|
|
return d * 1024 * 8 / (int)distSq;
|
|
}
|
|
|
|
public Actor GetActorBelow()
|
|
{
|
|
// Map.DistanceAboveTerrain(WPos pos) is called directly because Aircraft is an IPositionable trait
|
|
// and all calls occur in Tick methods.
|
|
if (self.World.Map.DistanceAboveTerrain(CenterPosition) != LandAltitude)
|
|
return null; // Not on the resupplier.
|
|
|
|
return self.World.ActorMap.GetActorsAt(self.Location)
|
|
.FirstOrDefault(a => a.Info.HasTraitInfo<ReservableInfo>());
|
|
}
|
|
|
|
public void MakeReservation(Actor target)
|
|
{
|
|
UnReserve();
|
|
var reservable = target.TraitOrDefault<Reservable>();
|
|
if (reservable != null)
|
|
{
|
|
reservation = reservable.Reserve(target, self, this);
|
|
ReservedActor = target;
|
|
}
|
|
}
|
|
|
|
public void AllowYieldingReservation()
|
|
{
|
|
if (reservation == null)
|
|
return;
|
|
|
|
MayYieldReservation = true;
|
|
}
|
|
|
|
public void UnReserve()
|
|
{
|
|
if (reservation == null)
|
|
return;
|
|
|
|
reservation.Dispose();
|
|
reservation = null;
|
|
ReservedActor = null;
|
|
MayYieldReservation = false;
|
|
}
|
|
|
|
bool AircraftCanEnter(Actor a, TargetModifiers modifiers)
|
|
{
|
|
if (RequireForceMove && !modifiers.HasModifier(TargetModifiers.ForceMove))
|
|
return false;
|
|
|
|
return AircraftCanEnter(a);
|
|
}
|
|
|
|
bool AircraftCanEnter(Actor a)
|
|
{
|
|
if (self.AppearsHostileTo(a))
|
|
return false;
|
|
|
|
var canRearmAtActor = rearmable != null && rearmable.Info.RearmActors.Contains(a.Info.Name);
|
|
var canRepairAtActor = repairable != null && repairable.Info.RepairActors.Contains(a.Info.Name);
|
|
|
|
return canRearmAtActor || canRepairAtActor;
|
|
}
|
|
|
|
bool AircraftCanResupplyAt(Actor a, bool allowedToForceEnter = false)
|
|
{
|
|
if (self.AppearsHostileTo(a))
|
|
return false;
|
|
|
|
var canRearmAtActor = rearmable != null && rearmable.Info.RearmActors.Contains(a.Info.Name);
|
|
var canRepairAtActor = repairable != null && repairable.Info.RepairActors.Contains(a.Info.Name);
|
|
|
|
var allowedToEnterRearmer = canRearmAtActor && (allowedToForceEnter || rearmable.RearmableAmmoPools.Any(p => !p.HasFullAmmo));
|
|
var allowedToEnterRepairer = canRepairAtActor && (allowedToForceEnter || self.GetDamageState() != DamageState.Undamaged);
|
|
|
|
return allowedToEnterRearmer || allowedToEnterRepairer;
|
|
}
|
|
|
|
public int MovementSpeed => !IsTraitDisabled && !IsTraitPaused ? Util.ApplyPercentageModifiers(Info.Speed, speedModifiers) : 0;
|
|
public int IdleMovementSpeed => Info.IdleSpeed < 0 ? MovementSpeed :
|
|
!IsTraitDisabled && !IsTraitPaused ? Util.ApplyPercentageModifiers(Info.IdleSpeed, speedModifiers) : 0;
|
|
|
|
public (CPos Cell, SubCell SubCell)[] OccupiedCells()
|
|
{
|
|
return landingCells;
|
|
}
|
|
|
|
public WVec FlyStep(WAngle facing)
|
|
{
|
|
return FlyStep(MovementSpeed, facing);
|
|
}
|
|
|
|
public WVec FlyStep(int speed, WAngle facing)
|
|
{
|
|
var dir = new WVec(0, -1024, 0).Rotate(WRot.FromYaw(facing));
|
|
return speed * dir / 1024;
|
|
}
|
|
|
|
public CPos? FindLandingLocation(CPos targetCell, WDist maxSearchDistance)
|
|
{
|
|
// The easy case
|
|
if (CanLand(targetCell, blockedByMobile: false))
|
|
return targetCell;
|
|
|
|
var cellRange = (maxSearchDistance.Length + 1023) / 1024;
|
|
var centerPosition = self.World.Map.CenterOfCell(targetCell);
|
|
foreach (var c in self.World.Map.FindTilesInCircle(targetCell, cellRange))
|
|
{
|
|
if (!CanLand(c, blockedByMobile: false))
|
|
continue;
|
|
|
|
var delta = self.World.Map.CenterOfCell(c) - centerPosition;
|
|
if (delta.LengthSquared < maxSearchDistance.LengthSquared)
|
|
return c;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public bool CanLand(IEnumerable<CPos> cells, Actor dockingActor = null, bool blockedByMobile = true)
|
|
{
|
|
foreach (var c in cells)
|
|
if (!CanLand(c, dockingActor, blockedByMobile))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool CanLand(CPos cell, Actor dockingActor = null, bool blockedByMobile = true)
|
|
{
|
|
if (!self.World.Map.Contains(cell))
|
|
return false;
|
|
|
|
foreach (var otherActor in self.World.ActorMap.GetActorsAt(cell))
|
|
if (IsBlockedBy(self, otherActor, dockingActor, blockedByMobile))
|
|
return false;
|
|
|
|
// Terrain type is ignored when docking with an actor
|
|
if (dockingActor != null)
|
|
return true;
|
|
|
|
var landableTerrain = overrideAircraftLanding != null ? overrideAircraftLanding.LandableTerrainTypes : Info.LandableTerrainTypes;
|
|
return landableTerrain.Contains(self.World.Map.GetTerrainInfo(cell).Type);
|
|
}
|
|
|
|
bool IsBlockedBy(Actor self, Actor otherActor, Actor ignoreActor, bool blockedByMobile = true)
|
|
{
|
|
// We are not blocked by the actor we are ignoring.
|
|
if (otherActor == self || otherActor == ignoreActor)
|
|
return false;
|
|
|
|
// We are not blocked by actors we can nudge out of the way
|
|
// TODO: Generalize blocker checks and handling here and in Locomotor
|
|
if (!blockedByMobile && self.Owner.RelationshipWith(otherActor.Owner) == PlayerRelationship.Ally &&
|
|
otherActor.TraitOrDefault<Mobile>() != null && otherActor.CurrentActivity == null)
|
|
return false;
|
|
|
|
// PERF: Only perform ITemporaryBlocker trait look-up if mod/map rules contain any actors that are temporary blockers
|
|
if (self.World.RulesContainTemporaryBlocker)
|
|
{
|
|
// If there is a temporary blocker in our path, but we can remove it, we are not blocked.
|
|
var temporaryBlocker = otherActor.TraitOrDefault<ITemporaryBlocker>();
|
|
if (temporaryBlocker != null && temporaryBlocker.CanRemoveBlockage(otherActor, self))
|
|
return false;
|
|
}
|
|
|
|
// If we cannot crush the other actor in our way, we are blocked.
|
|
if (Info.Crushes.IsEmpty)
|
|
return true;
|
|
|
|
// If the other actor in our way cannot be crushed, we are blocked.
|
|
// PERF: Avoid LINQ.
|
|
foreach (var crushable in otherActor.Crushables)
|
|
if (crushable.CrushableBy(otherActor, self, Info.Crushes))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool CanRearmAt(Actor host)
|
|
{
|
|
return rearmable != null && rearmable.Info.RearmActors.Contains(host.Info.Name) && rearmable.RearmableAmmoPools.Any(p => !p.HasFullAmmo);
|
|
}
|
|
|
|
public bool CanRepairAt(Actor host)
|
|
{
|
|
return repairable != null && repairable.Info.RepairActors.Contains(host.Info.Name) && self.GetDamageState() != DamageState.Undamaged;
|
|
}
|
|
|
|
public void ModifyDeathActorInit(Actor self, TypeDictionary init)
|
|
{
|
|
init.Add(new FacingInit(Facing));
|
|
}
|
|
|
|
void INotifyBecomingIdle.OnBecomingIdle(Actor self)
|
|
{
|
|
OnBecomingIdle(self);
|
|
}
|
|
|
|
protected virtual void OnBecomingIdle(Actor self)
|
|
{
|
|
if (Info.IdleBehavior == IdleBehaviorType.LeaveMap)
|
|
{
|
|
self.QueueActivity(new FlyOffMap(self));
|
|
self.QueueActivity(new RemoveSelf());
|
|
}
|
|
else if (Info.IdleBehavior == IdleBehaviorType.LeaveMapAtClosestEdge)
|
|
{
|
|
var edgeCell = self.World.Map.ChooseClosestEdgeCell(self.Location);
|
|
self.QueueActivity(new FlyOffMap(self, Target.FromCell(self.World, edgeCell)));
|
|
self.QueueActivity(new RemoveSelf());
|
|
}
|
|
else if (Info.IdleBehavior == IdleBehaviorType.ReturnToBase && GetActorBelow() == null)
|
|
self.QueueActivity(new ReturnToBase(self, null, !Info.TakeOffOnResupply));
|
|
else
|
|
{
|
|
var dat = self.World.Map.DistanceAboveTerrain(CenterPosition);
|
|
if (dat == LandAltitude)
|
|
{
|
|
if (!CanLand(self.Location) && ReservedActor == null)
|
|
self.QueueActivity(new TakeOff(self));
|
|
|
|
// All remaining idle behaviors rely on not being at LandAltitude, so unconditionally return
|
|
return;
|
|
}
|
|
|
|
if (Info.IdleBehavior == IdleBehaviorType.Land && Info.LandableTerrainTypes.Count > 0)
|
|
self.QueueActivity(new Land(self));
|
|
else
|
|
self.QueueActivity(new FlyIdle(self));
|
|
}
|
|
}
|
|
|
|
#region Implement IPositionable
|
|
|
|
public bool CanExistInCell(CPos cell) { return true; }
|
|
public bool IsLeavingCell(CPos location, SubCell subCell = SubCell.Any) { return false; } // TODO: Handle landing
|
|
public bool CanEnterCell(CPos cell, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All) { return true; }
|
|
public SubCell GetValidSubCell(SubCell preferred) { return SubCell.Invalid; }
|
|
public SubCell GetAvailableSubCell(CPos a, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)
|
|
{
|
|
// Does not use any subcell
|
|
return SubCell.Invalid;
|
|
}
|
|
|
|
public void SetCenterPosition(Actor self, WPos pos) { SetPosition(self, pos); }
|
|
|
|
// Changes position, but not altitude
|
|
public void SetPosition(Actor self, CPos cell, SubCell subCell = SubCell.Any)
|
|
{
|
|
SetPosition(self, self.World.Map.CenterOfCell(cell) + new WVec(0, 0, CenterPosition.Z));
|
|
}
|
|
|
|
public void SetPosition(Actor self, WPos pos)
|
|
{
|
|
CenterPosition = pos;
|
|
|
|
if (!self.IsInWorld)
|
|
return;
|
|
|
|
var altitude = self.World.Map.DistanceAboveTerrain(CenterPosition);
|
|
|
|
// LandingCells define OccupiedCells, so we need to keep current position with LandindCells in sync.
|
|
// Though we don't want to update LandingCells when the unit is airborne, as when non-VTOL units reserve
|
|
// their landing position it is expected for their landing cell to not match their current position.
|
|
if (HasInfluence() && altitude.Length <= Info.MinAirborneAltitude)
|
|
{
|
|
var currentPos = new[] { (TopLeft, SubCell.FullCell) };
|
|
if (landingCells.SequenceEqual(currentPos))
|
|
{
|
|
self.World.ActorMap.RemoveInfluence(self, this);
|
|
landingCells = currentPos;
|
|
self.World.ActorMap.AddInfluence(self, this);
|
|
}
|
|
}
|
|
|
|
self.World.UpdateMaps(self, this);
|
|
|
|
var isAirborne = altitude.Length >= Info.MinAirborneAltitude;
|
|
if (isAirborne && !airborne)
|
|
OnAirborneAltitudeReached();
|
|
else if (!isAirborne && airborne)
|
|
OnAirborneAltitudeLeft();
|
|
|
|
var isCruising = altitude == Info.CruiseAltitude;
|
|
if (isCruising && !cruising)
|
|
OnCruisingAltitudeReached();
|
|
else if (!isCruising && cruising)
|
|
OnCruisingAltitudeLeft();
|
|
|
|
// NB: This can be called from the constructor before notifyCenterPositionChanged is assigned.
|
|
if (notify && notifyCenterPositionChanged != null)
|
|
foreach (var n in notifyCenterPositionChanged)
|
|
n.CenterPositionChanged(self, 0, 0);
|
|
|
|
FinishedMoving(self);
|
|
}
|
|
|
|
public void FinishedMoving(Actor self)
|
|
{
|
|
// Only crush actors on having landed
|
|
if (!self.IsAtGroundLevel())
|
|
return;
|
|
|
|
CrushAction(self, (notifyCrushed) => notifyCrushed.OnCrush);
|
|
}
|
|
|
|
public void EnteringCell(Actor self)
|
|
{
|
|
CrushAction(self, (notifyCrushed) => notifyCrushed.WarnCrush);
|
|
}
|
|
|
|
void CrushAction(Actor self, Func<INotifyCrushed, Action<Actor, Actor, BitSet<CrushClass>>> action)
|
|
{
|
|
var crushables = self.World.ActorMap.GetActorsAt(TopLeft).Where(a => a != self)
|
|
.SelectMany(a => a.Crushables.Select(t => new TraitPair<ICrushable>(a, t)));
|
|
|
|
// Only crush actors that are on the ground level
|
|
foreach (var crushable in crushables)
|
|
if (crushable.Trait.CrushableBy(crushable.Actor, self, Info.Crushes) && crushable.Actor.IsAtGroundLevel())
|
|
foreach (var notifyCrushed in crushable.Actor.TraitsImplementing<INotifyCrushed>())
|
|
action(notifyCrushed)(crushable.Actor, self, Info.Crushes);
|
|
}
|
|
|
|
public void AddInfluence((CPos, SubCell)[] landingCells)
|
|
{
|
|
if (HasInfluence())
|
|
throw new InvalidOperationException(
|
|
$"Cannot {nameof(AddInfluence)} until previous influence is removed with {nameof(RemoveInfluence)}");
|
|
|
|
this.landingCells = landingCells;
|
|
if (self.IsInWorld)
|
|
self.World.ActorMap.AddInfluence(self, this);
|
|
}
|
|
|
|
public void AddInfluence(CPos landingCell)
|
|
{
|
|
AddInfluence(new[] { (landingCell, SubCell.FullCell) });
|
|
}
|
|
|
|
public void RemoveInfluence()
|
|
{
|
|
if (self.IsInWorld)
|
|
self.World.ActorMap.RemoveInfluence(self, this);
|
|
|
|
landingCells = Array.Empty<(CPos, SubCell)>();
|
|
}
|
|
|
|
public bool HasInfluence()
|
|
{
|
|
return landingCells.Length > 0;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Implement IMove
|
|
|
|
public Activity MoveTo(CPos cell, int nearEnough = 0, Actor ignoreActor = null,
|
|
bool evaluateNearestMovableCell = false, Color? targetLineColor = null)
|
|
{
|
|
return new Fly(self, Target.FromCell(self.World, cell), WDist.FromCells(nearEnough), targetLineColor: targetLineColor);
|
|
}
|
|
|
|
public Activity MoveWithinRange(in Target target, WDist range,
|
|
WPos? initialTargetPosition = null, Color? targetLineColor = null)
|
|
{
|
|
return new Fly(self, target, WDist.Zero, range, initialTargetPosition, targetLineColor);
|
|
}
|
|
|
|
public Activity MoveWithinRange(in Target target, WDist minRange, WDist maxRange,
|
|
WPos? initialTargetPosition = null, Color? targetLineColor = null)
|
|
{
|
|
return new Fly(self, target, minRange, maxRange,
|
|
initialTargetPosition, targetLineColor);
|
|
}
|
|
|
|
public Activity MoveFollow(Actor self, in Target target, WDist minRange, WDist maxRange,
|
|
WPos? initialTargetPosition = null, Color? targetLineColor = null)
|
|
{
|
|
return new FlyFollow(self, target, minRange, maxRange,
|
|
initialTargetPosition, targetLineColor);
|
|
}
|
|
|
|
public Activity ReturnToCell(Actor self) { return null; }
|
|
|
|
public Activity MoveToTarget(Actor self, in Target target,
|
|
WPos? initialTargetPosition = null, Color? targetLineColor = null)
|
|
{
|
|
return new Fly(self, target, initialTargetPosition, targetLineColor);
|
|
}
|
|
|
|
public Activity MoveIntoTarget(Actor self, in Target target)
|
|
{
|
|
return new Land(self, target);
|
|
}
|
|
|
|
public Activity MoveOntoTarget(Actor self, in Target target, in WVec offset, WAngle? facing, Color? targetLineColor = null)
|
|
{
|
|
return new Land(self, target, offset, facing, targetLineColor);
|
|
}
|
|
|
|
public Activity LocalMove(Actor self, WPos fromPos, WPos toPos)
|
|
{
|
|
// TODO: Ignore repulsion when moving
|
|
var activities = new CallFunc(() => SetCenterPosition(self, fromPos));
|
|
activities.Queue(new Fly(self, Target.FromPos(toPos)));
|
|
return activities;
|
|
}
|
|
|
|
public int EstimatedMoveDuration(Actor self, WPos fromPos, WPos toPos)
|
|
{
|
|
var speed = MovementSpeed;
|
|
return speed > 0 ? (toPos - fromPos).Length / speed : 0;
|
|
}
|
|
|
|
public CPos NearestMoveableCell(CPos cell) { return cell; }
|
|
|
|
public MovementType CurrentMovementTypes
|
|
{
|
|
get => movementTypes;
|
|
|
|
set
|
|
{
|
|
var oldValue = movementTypes;
|
|
movementTypes = value;
|
|
if (value != oldValue)
|
|
foreach (var n in notifyMoving)
|
|
n.MovementTypeChanged(self, value);
|
|
}
|
|
}
|
|
|
|
public bool CanEnterTargetNow(Actor self, in Target target)
|
|
{
|
|
// Lambdas can't use 'in' variables, so capture a copy for later
|
|
var targetActor = target;
|
|
if (target.Positions.Any(p => self.World.ActorMap.GetActorsAt(self.World.Map.CellContaining(p)).Any(a => a != self && a != targetActor.Actor)))
|
|
return false;
|
|
|
|
MakeReservation(target.Actor);
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Implement order interfaces
|
|
|
|
public IEnumerable<IOrderTargeter> Orders
|
|
{
|
|
get
|
|
{
|
|
yield return new EnterAlliedActorTargeter<BuildingInfo>(
|
|
"ForceEnter",
|
|
6,
|
|
Info.EnterCursor,
|
|
Info.EnterBlockedCursor,
|
|
(target, modifiers) => Info.CanForceLand && modifiers.HasModifier(TargetModifiers.ForceMove) && AircraftCanEnter(target),
|
|
target => Reservable.IsAvailableFor(target, self) && AircraftCanResupplyAt(target, true));
|
|
|
|
yield return new EnterAlliedActorTargeter<BuildingInfo>(
|
|
"Enter",
|
|
5,
|
|
Info.EnterCursor,
|
|
Info.EnterBlockedCursor,
|
|
AircraftCanEnter,
|
|
target => Reservable.IsAvailableFor(target, self) && AircraftCanResupplyAt(target, !Info.TakeOffOnResupply));
|
|
|
|
yield return new AircraftMoveOrderTargeter(this);
|
|
}
|
|
}
|
|
|
|
public Order IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued)
|
|
{
|
|
if (!IsTraitDisabled &&
|
|
(order.OrderID == "Enter" || order.OrderID == "Move" || order.OrderID == "Land" || order.OrderID == "ForceEnter"))
|
|
return new Order(order.OrderID, self, target, queued);
|
|
|
|
return null;
|
|
}
|
|
|
|
Order IIssueDeployOrder.IssueDeployOrder(Actor self, bool queued)
|
|
{
|
|
if (IsTraitDisabled || rearmable == null || rearmable.Info.RearmActors.Count == 0)
|
|
return null;
|
|
|
|
return new Order("ReturnToBase", self, queued);
|
|
}
|
|
|
|
bool IIssueDeployOrder.CanIssueDeployOrder(Actor self, bool queued) { return rearmable != null && rearmable.Info.RearmActors.Count > 0; }
|
|
|
|
public string VoicePhraseForOrder(Actor self, Order order)
|
|
{
|
|
if (IsTraitDisabled)
|
|
return null;
|
|
|
|
switch (order.OrderString)
|
|
{
|
|
case "Land":
|
|
case "Move":
|
|
if (!Info.MoveIntoShroud && order.Target.Type != TargetType.Invalid)
|
|
{
|
|
var cell = self.World.Map.CellContaining(order.Target.CenterPosition);
|
|
if (!self.Owner.Shroud.IsExplored(cell))
|
|
return null;
|
|
}
|
|
|
|
return Info.Voice;
|
|
case "Enter":
|
|
case "ForceEnter":
|
|
case "Stop":
|
|
case "Scatter":
|
|
return Info.Voice;
|
|
case "ReturnToBase":
|
|
return rearmable != null && rearmable.Info.RearmActors.Count > 0 ? Info.Voice : null;
|
|
default: return null;
|
|
}
|
|
}
|
|
|
|
public void ResolveOrder(Actor self, Order order)
|
|
{
|
|
if (IsTraitDisabled)
|
|
return;
|
|
|
|
var orderString = order.OrderString;
|
|
if (orderString == "Move")
|
|
{
|
|
if (!order.Target.IsValidFor(self))
|
|
return;
|
|
|
|
var cell = self.World.Map.Clamp(self.World.Map.CellContaining(order.Target.CenterPosition));
|
|
if (!Info.MoveIntoShroud && !self.Owner.Shroud.IsExplored(cell))
|
|
return;
|
|
|
|
if (!order.Queued)
|
|
UnReserve();
|
|
|
|
var target = Target.FromCell(self.World, cell);
|
|
|
|
// TODO: this should scale with unit selection group size.
|
|
self.QueueActivity(order.Queued, new Fly(self, target, WDist.FromCells(8), targetLineColor: Info.TargetLineColor));
|
|
self.ShowTargetLines();
|
|
}
|
|
else if (orderString == "Land")
|
|
{
|
|
if (!order.Target.IsValidFor(self))
|
|
return;
|
|
|
|
var cell = self.World.Map.Clamp(self.World.Map.CellContaining(order.Target.CenterPosition));
|
|
if (!Info.MoveIntoShroud && !self.Owner.Shroud.IsExplored(cell))
|
|
return;
|
|
|
|
if (!order.Queued)
|
|
UnReserve();
|
|
|
|
var target = Target.FromCell(self.World, cell);
|
|
|
|
self.QueueActivity(order.Queued, new Land(self, target, targetLineColor: Info.TargetLineColor));
|
|
self.ShowTargetLines();
|
|
}
|
|
else if (orderString == "Enter" || orderString == "ForceEnter" || orderString == "Repair")
|
|
{
|
|
// Enter, ForceEnter and Repair orders are only valid for own/allied actors,
|
|
// which are guaranteed to never be frozen.
|
|
if (order.Target.Type != TargetType.Actor)
|
|
return;
|
|
|
|
var targetActor = order.Target.Actor;
|
|
var isForceEnter = orderString == "ForceEnter";
|
|
var canResupplyAt = AircraftCanResupplyAt(targetActor, isForceEnter || !Info.TakeOffOnResupply);
|
|
|
|
// This is what the order targeter checks to display the correct cursor, so we need to make sure
|
|
// the behavior matches the cursor if the player clicks despite a "blocked" cursor.
|
|
if (!canResupplyAt || !Reservable.IsAvailableFor(targetActor, self))
|
|
return;
|
|
|
|
if (!order.Queued)
|
|
UnReserve();
|
|
|
|
// Aircraft with TakeOffOnResupply would immediately take off again, so there's no point in automatically forcing
|
|
// them to land on a resupplier. For aircraft without it, it makes more sense to land than to idle above a
|
|
// free resupplier.
|
|
var forceLand = isForceEnter || !Info.TakeOffOnResupply;
|
|
self.QueueActivity(order.Queued, new ReturnToBase(self, targetActor, forceLand));
|
|
self.ShowTargetLines();
|
|
}
|
|
else if (orderString == "Stop")
|
|
{
|
|
// We don't want the Stop order to cancel a running Resupply activity.
|
|
// Resupply is always either the main activity or a child of ReturnToBase.
|
|
if (self.CurrentActivity is Resupply ||
|
|
(self.CurrentActivity is ReturnToBase && GetActorBelow() != null))
|
|
return;
|
|
|
|
self.CancelActivity();
|
|
UnReserve();
|
|
}
|
|
else if (orderString == "ReturnToBase")
|
|
{
|
|
// Do nothing if not rearmable and don't restart activity every time deploy hotkey is triggered
|
|
if (rearmable == null || rearmable.Info.RearmActors.Count == 0 || self.CurrentActivity is ReturnToBase || GetActorBelow() != null)
|
|
return;
|
|
|
|
if (!order.Queued)
|
|
UnReserve();
|
|
|
|
// Aircraft with TakeOffOnResupply would immediately take off again, so there's no point in forcing them to land
|
|
// on a resupplier. For aircraft without it, it makes more sense to land than to idle above a free resupplier.
|
|
self.QueueActivity(order.Queued, new ReturnToBase(self, null, !Info.TakeOffOnResupply));
|
|
self.ShowTargetLines();
|
|
}
|
|
else if (orderString == "Scatter")
|
|
{
|
|
self.QueueActivity(order.Queued, new Nudge(self));
|
|
self.ShowTargetLines();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Airborne conditions
|
|
|
|
void OnAirborneAltitudeReached()
|
|
{
|
|
if (airborne)
|
|
return;
|
|
|
|
airborne = true;
|
|
if (airborneToken == Actor.InvalidConditionToken)
|
|
airborneToken = self.GrantCondition(Info.AirborneCondition);
|
|
}
|
|
|
|
void OnAirborneAltitudeLeft()
|
|
{
|
|
if (!airborne)
|
|
return;
|
|
|
|
airborne = false;
|
|
if (airborneToken != Actor.InvalidConditionToken)
|
|
airborneToken = self.RevokeCondition(airborneToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cruising conditions
|
|
|
|
void OnCruisingAltitudeReached()
|
|
{
|
|
if (cruising)
|
|
return;
|
|
|
|
cruising = true;
|
|
if (cruisingToken == Actor.InvalidConditionToken)
|
|
cruisingToken = self.GrantCondition(Info.CruisingCondition);
|
|
}
|
|
|
|
void OnCruisingAltitudeLeft()
|
|
{
|
|
if (!cruising)
|
|
return;
|
|
|
|
cruising = false;
|
|
if (cruisingToken != Actor.InvalidConditionToken)
|
|
cruisingToken = self.RevokeCondition(cruisingToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
void INotifyActorDisposing.Disposing(Actor self)
|
|
{
|
|
UnReserve();
|
|
}
|
|
|
|
void IActorPreviewInitModifier.ModifyActorPreviewInit(Actor self, TypeDictionary inits)
|
|
{
|
|
if (!inits.Contains<DynamicFacingInit>() && !inits.Contains<FacingInit>())
|
|
inits.Add(new DynamicFacingInit(() => Facing));
|
|
}
|
|
|
|
Activity ICreationActivity.GetCreationActivity()
|
|
{
|
|
if (creationRallyPoint != null || creationActivityDelay > 0 || creationByMap)
|
|
return new AssociateWithAirfieldActivity(this, creationActivityDelay, creationRallyPoint, creationByMap);
|
|
|
|
return null;
|
|
}
|
|
|
|
sealed class AssociateWithAirfieldActivity : Activity
|
|
{
|
|
readonly Aircraft aircraft;
|
|
readonly int delay;
|
|
readonly CPos[] rallyPoint;
|
|
readonly bool creationByMap;
|
|
|
|
public AssociateWithAirfieldActivity(Aircraft self, int delay, CPos[] rallyPoint, bool creationByMap)
|
|
{
|
|
aircraft = self;
|
|
this.delay = delay;
|
|
this.rallyPoint = rallyPoint;
|
|
this.creationByMap = creationByMap;
|
|
}
|
|
|
|
protected override void OnFirstRun(Actor self)
|
|
{
|
|
var cpos = self.Location;
|
|
var pos = self.CenterPosition;
|
|
bool TryDock()
|
|
{
|
|
var host = aircraft.GetActorBelow();
|
|
if (host != null)
|
|
{
|
|
// Center the actor on the resupplier.
|
|
var exit = host.NearestExitOrDefault(pos);
|
|
pos = host.CenterPosition;
|
|
pos = new WPos(pos.X, pos.Y, pos.Z - self.World.Map.DistanceAboveTerrain(pos).Length);
|
|
if (exit != null)
|
|
{
|
|
pos += exit.Info.SpawnOffset;
|
|
if (exit.Info.Facing != null)
|
|
aircraft.Facing = exit.Info.Facing.Value;
|
|
}
|
|
|
|
aircraft.AddInfluence(cpos);
|
|
aircraft.SetPosition(self, pos);
|
|
aircraft.MakeReservation(host);
|
|
|
|
// Freshly created aircraft shouldn't block the exit, so we allow them to yield their reservation.
|
|
aircraft.AllowYieldingReservation();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (creationByMap)
|
|
{
|
|
if (TryDock())
|
|
return;
|
|
|
|
pos = new WPos(pos.X, pos.Y, pos.Z - self.World.Map.DistanceAboveTerrain(pos).Length);
|
|
|
|
if (!aircraft.Info.TakeOffOnCreation && aircraft.CanLand(cpos))
|
|
{
|
|
aircraft.AddInfluence(cpos);
|
|
aircraft.SetPosition(self, pos);
|
|
}
|
|
else
|
|
aircraft.SetPosition(self, new WPos(pos.X, pos.Y, pos.Z + aircraft.Info.CruiseAltitude.Length));
|
|
}
|
|
else
|
|
{
|
|
TryDock();
|
|
if (delay > 0)
|
|
QueueChild(new Wait(delay));
|
|
}
|
|
}
|
|
|
|
public override bool Tick(Actor self)
|
|
{
|
|
if (!aircraft.Info.TakeOffOnCreation || IsCanceling)
|
|
return true;
|
|
|
|
if (rallyPoint != null && rallyPoint.Length > 0)
|
|
foreach (var cell in rallyPoint)
|
|
QueueChild(new AttackMoveActivity(self, () => aircraft.MoveTo(cell, 1, evaluateNearestMovableCell: true, targetLineColor: Color.OrangeRed)));
|
|
|
|
if (!creationByMap)
|
|
aircraft.UnReserve();
|
|
|
|
return true;
|
|
}
|
|
|
|
public override IEnumerable<Target> GetTargets(Actor self)
|
|
{
|
|
if (ChildActivity != null)
|
|
return ChildActivity.GetTargets(self);
|
|
|
|
return Target.None;
|
|
}
|
|
|
|
public override IEnumerable<TargetLineNode> TargetLineNodes(Actor self)
|
|
{
|
|
if (ChildActivity != null)
|
|
foreach (var n in ChildActivity.TargetLineNodes(self))
|
|
yield return n;
|
|
}
|
|
}
|
|
|
|
public class AircraftMoveOrderTargeter : IOrderTargeter
|
|
{
|
|
readonly Aircraft aircraft;
|
|
|
|
public string OrderID { get; protected set; }
|
|
public int OrderPriority => 4;
|
|
public bool IsQueued { get; protected set; }
|
|
|
|
public AircraftMoveOrderTargeter(Aircraft aircraft)
|
|
{
|
|
this.aircraft = aircraft;
|
|
OrderID = "Move";
|
|
}
|
|
|
|
public bool TargetOverridesSelection(Actor self, in Target target, List<Actor> actorsAt, CPos xy, TargetModifiers modifiers)
|
|
{
|
|
// Always prioritise orders over selecting other peoples actors or own actors that are already selected
|
|
if (target.Type == TargetType.Actor && (target.Actor.Owner != self.Owner || self.World.Selection.Contains(target.Actor)))
|
|
return true;
|
|
|
|
return modifiers.HasModifier(TargetModifiers.ForceMove);
|
|
}
|
|
|
|
public virtual bool CanTarget(Actor self, in Target target, ref TargetModifiers modifiers, ref string cursor)
|
|
{
|
|
if (target.Type != TargetType.Terrain || (aircraft.RequireForceMove && !modifiers.HasModifier(TargetModifiers.ForceMove)))
|
|
return false;
|
|
|
|
var location = self.World.Map.CellContaining(target.CenterPosition);
|
|
|
|
// Aircraft can be force-landed by issuing a force-move order on a clear terrain cell
|
|
// Cells that contain a blocking building are treated as regular force move orders, overriding
|
|
// selection for left-mouse orders
|
|
if (modifiers.HasModifier(TargetModifiers.ForceMove) && aircraft.Info.CanForceLand)
|
|
{
|
|
var buildingAtLocation = self.World.ActorMap.GetActorsAt(location)
|
|
.Any(a => a.TraitOrDefault<Building>() != null && a.TraitOrDefault<Selectable>() != null);
|
|
|
|
if (!buildingAtLocation || aircraft.CanLand(location, blockedByMobile: false))
|
|
OrderID = "Land";
|
|
}
|
|
|
|
IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue);
|
|
|
|
var explored = self.Owner.Shroud.IsExplored(location);
|
|
cursor = !aircraft.IsTraitPaused && (explored || aircraft.Info.MoveIntoShroud) && self.World.Map.Contains(location) ?
|
|
aircraft.Info.Cursor : aircraft.Info.BlockedCursor;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|