370 lines
10 KiB
C#
370 lines
10 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright 2007-2019 The OpenRA Developers (see AUTHORS)
|
|
* 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.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using OpenRA.Graphics;
|
|
using OpenRA.Mods.Common.Activities;
|
|
using OpenRA.Mods.Common.Graphics;
|
|
using OpenRA.Mods.Common.Orders;
|
|
using OpenRA.Primitives;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
[Desc("Transports actors with the `Carryable` trait.")]
|
|
public class CarryallInfo : ITraitInfo, Requires<BodyOrientationInfo>, Requires<AircraftInfo>
|
|
{
|
|
[Desc("Delay on the ground while attaching an actor to the carryall.")]
|
|
public readonly int LoadingDelay = 0;
|
|
|
|
[Desc("Delay on the ground while detacting an actor to the carryall.")]
|
|
public readonly int UnloadingDelay = 0;
|
|
|
|
[Desc("Carryable attachment point relative to body.")]
|
|
public readonly WVec LocalOffset = WVec.Zero;
|
|
|
|
[Desc("Radius around the target drop location that are considered if the target tile is blocked.")]
|
|
public readonly WDist DropRange = WDist.FromCells(5);
|
|
|
|
[VoiceReference]
|
|
public readonly string Voice = "Action";
|
|
|
|
public virtual object Create(ActorInitializer init) { return new Carryall(init.Self, this); }
|
|
}
|
|
|
|
public class Carryall : INotifyKilled, ISync, ITick, IRender, INotifyActorDisposing, IIssueOrder, IResolveOrder, IOrderVoice
|
|
{
|
|
public enum CarryallState
|
|
{
|
|
Idle,
|
|
Reserved,
|
|
Carrying
|
|
}
|
|
|
|
public readonly CarryallInfo Info;
|
|
readonly AircraftInfo aircraftInfo;
|
|
readonly BodyOrientation body;
|
|
readonly IMove move;
|
|
readonly IFacing facing;
|
|
|
|
// The actor we are currently carrying.
|
|
[Sync] public Actor Carryable { get; private set; }
|
|
public CarryallState State { get; private set; }
|
|
|
|
int cachedFacing;
|
|
IActorPreview[] carryablePreview = null;
|
|
|
|
/// <summary>Offset between the carryall's and the carried actor's CenterPositions</summary>
|
|
public WVec CarryableOffset { get; private set; }
|
|
|
|
public Carryall(Actor self, CarryallInfo info)
|
|
{
|
|
Info = info;
|
|
|
|
Carryable = null;
|
|
State = CarryallState.Idle;
|
|
|
|
aircraftInfo = self.Info.TraitInfoOrDefault<AircraftInfo>();
|
|
body = self.Trait<BodyOrientation>();
|
|
move = self.Trait<IMove>();
|
|
facing = self.Trait<IFacing>();
|
|
}
|
|
|
|
void ITick.Tick(Actor self)
|
|
{
|
|
// Cargo may be killed in the same tick as, but after they are attached
|
|
if (Carryable != null && Carryable.IsDead)
|
|
DetachCarryable(self);
|
|
|
|
// HACK: We don't have an efficient way to know when the preview
|
|
// bounds change, so assume that we need to update the screen map
|
|
// (only) when the facing changes
|
|
if (facing.Facing != cachedFacing && carryablePreview != null)
|
|
{
|
|
self.World.ScreenMap.AddOrUpdate(self);
|
|
cachedFacing = facing.Facing;
|
|
}
|
|
}
|
|
|
|
void INotifyActorDisposing.Disposing(Actor self)
|
|
{
|
|
if (State == CarryallState.Carrying)
|
|
{
|
|
Carryable.Dispose();
|
|
Carryable = null;
|
|
}
|
|
|
|
UnreserveCarryable(self);
|
|
}
|
|
|
|
void INotifyKilled.Killed(Actor self, AttackInfo e)
|
|
{
|
|
if (State == CarryallState.Carrying)
|
|
{
|
|
if (!Carryable.IsDead)
|
|
{
|
|
var positionable = Carryable.Trait<IPositionable>();
|
|
positionable.SetPosition(Carryable, self.Location);
|
|
Carryable.Kill(e.Attacker);
|
|
}
|
|
|
|
Carryable = null;
|
|
}
|
|
|
|
UnreserveCarryable(self);
|
|
}
|
|
|
|
public virtual bool RequestTransportNotify(Actor self, Actor carryable, CPos destination)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public virtual WVec OffsetForCarryable(Actor self, Actor carryable)
|
|
{
|
|
return Info.LocalOffset - carryable.Info.TraitInfo<CarryableInfo>().LocalOffset;
|
|
}
|
|
|
|
public virtual bool AttachCarryable(Actor self, Actor carryable)
|
|
{
|
|
if (State == CarryallState.Carrying)
|
|
return false;
|
|
|
|
Carryable = carryable;
|
|
State = CarryallState.Carrying;
|
|
self.World.ScreenMap.AddOrUpdate(self);
|
|
|
|
CarryableOffset = OffsetForCarryable(self, carryable);
|
|
return true;
|
|
}
|
|
|
|
public virtual void DetachCarryable(Actor self)
|
|
{
|
|
UnreserveCarryable(self);
|
|
self.World.ScreenMap.AddOrUpdate(self);
|
|
|
|
carryablePreview = null;
|
|
CarryableOffset = WVec.Zero;
|
|
}
|
|
|
|
public virtual bool ReserveCarryable(Actor self, Actor carryable)
|
|
{
|
|
if (State == CarryallState.Reserved)
|
|
UnreserveCarryable(self);
|
|
|
|
if (State != CarryallState.Idle || !carryable.Trait<Carryable>().Reserve(carryable, self))
|
|
return false;
|
|
|
|
Carryable = carryable;
|
|
State = CarryallState.Reserved;
|
|
return true;
|
|
}
|
|
|
|
public virtual void UnreserveCarryable(Actor self)
|
|
{
|
|
if (Carryable != null && Carryable.IsInWorld && !Carryable.IsDead)
|
|
Carryable.Trait<Carryable>().UnReserve(Carryable);
|
|
|
|
Carryable = null;
|
|
State = CarryallState.Idle;
|
|
}
|
|
|
|
IEnumerable<IRenderable> IRender.Render(Actor self, WorldRenderer wr)
|
|
{
|
|
if (State == CarryallState.Carrying && !Carryable.IsDead)
|
|
{
|
|
if (carryablePreview == null)
|
|
{
|
|
var carryableInits = new TypeDictionary()
|
|
{
|
|
new OwnerInit(Carryable.Owner),
|
|
new DynamicFacingInit(() => facing.Facing),
|
|
};
|
|
|
|
foreach (var api in Carryable.TraitsImplementing<IActorPreviewInitModifier>())
|
|
api.ModifyActorPreviewInit(Carryable, carryableInits);
|
|
|
|
var init = new ActorPreviewInitializer(Carryable.Info, wr, carryableInits);
|
|
carryablePreview = Carryable.Info.TraitInfos<IRenderActorPreviewInfo>()
|
|
.SelectMany(rpi => rpi.RenderPreview(init))
|
|
.ToArray();
|
|
}
|
|
|
|
var offset = body.LocalToWorld(CarryableOffset.Rotate(body.QuantizeOrientation(self, self.Orientation)));
|
|
var previewRenderables = carryablePreview
|
|
.SelectMany(p => p.Render(wr, self.CenterPosition + offset))
|
|
.OrderBy(WorldRenderer.RenderableScreenZPositionComparisonKey);
|
|
|
|
foreach (var r in previewRenderables)
|
|
yield return r;
|
|
}
|
|
}
|
|
|
|
IEnumerable<Rectangle> IRender.ScreenBounds(Actor self, WorldRenderer wr)
|
|
{
|
|
if (carryablePreview == null)
|
|
yield break;
|
|
|
|
var pos = self.CenterPosition;
|
|
foreach (var p in carryablePreview)
|
|
foreach (var b in p.ScreenBounds(wr, pos))
|
|
yield return b;
|
|
}
|
|
|
|
IEnumerable<IOrderTargeter> IIssueOrder.Orders
|
|
{
|
|
get
|
|
{
|
|
if (State != CarryallState.Carrying)
|
|
yield return new CarryallPickupOrderTargeter();
|
|
else
|
|
yield return new CarryallDeliverUnitTargeter(aircraftInfo, CarryableOffset);
|
|
}
|
|
}
|
|
|
|
Order IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued)
|
|
{
|
|
if (order.OrderID == "PickupUnit" || order.OrderID == "DeliverUnit" || order.OrderID == "Unload")
|
|
return new Order(order.OrderID, self, target, queued);
|
|
|
|
return null;
|
|
}
|
|
|
|
void IResolveOrder.ResolveOrder(Actor self, Order order)
|
|
{
|
|
if (State == CarryallState.Carrying)
|
|
{
|
|
if (order.OrderString == "DeliverUnit")
|
|
{
|
|
var cell = self.World.Map.Clamp(self.World.Map.CellContaining(order.Target.CenterPosition));
|
|
if (!aircraftInfo.MoveIntoShroud && !self.Owner.Shroud.IsExplored(cell))
|
|
return;
|
|
|
|
var targetLocation = move.NearestMoveableCell(cell);
|
|
self.SetTargetLine(Target.FromCell(self.World, targetLocation), Color.Yellow);
|
|
self.QueueActivity(order.Queued, new DeliverUnit(self, targetLocation));
|
|
}
|
|
else if (order.OrderString == "Unload")
|
|
{
|
|
var targetLocation = move.NearestMoveableCell(self.Location);
|
|
self.SetTargetLine(Target.FromCell(self.World, targetLocation), Color.Yellow);
|
|
self.QueueActivity(order.Queued, new DeliverUnit(self, targetLocation));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (order.OrderString == "PickupUnit")
|
|
{
|
|
if (order.Target.Type != TargetType.Actor)
|
|
return;
|
|
|
|
if (!ReserveCarryable(self, order.Target.Actor))
|
|
return;
|
|
|
|
if (!order.Queued)
|
|
self.CancelActivity();
|
|
|
|
self.SetTargetLine(order.Target, Color.Yellow);
|
|
self.QueueActivity(order.Queued, new PickupUnit(self, order.Target.Actor, Info.LoadingDelay));
|
|
}
|
|
}
|
|
}
|
|
|
|
string IOrderVoice.VoicePhraseForOrder(Actor self, Order order)
|
|
{
|
|
switch (order.OrderString)
|
|
{
|
|
case "DeliverUnit":
|
|
case "Unload":
|
|
case "PickupUnit":
|
|
return Info.Voice;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class CarryallPickupOrderTargeter : UnitOrderTargeter
|
|
{
|
|
public CarryallPickupOrderTargeter()
|
|
: base("PickupUnit", 5, "ability", false, true)
|
|
{
|
|
}
|
|
|
|
static bool CanTarget(Actor self, Actor target)
|
|
{
|
|
if (target == null || !target.AppearsFriendlyTo(self))
|
|
return false;
|
|
|
|
var carryable = target.TraitOrDefault<Carryable>();
|
|
if (carryable == null || carryable.IsTraitDisabled)
|
|
return false;
|
|
|
|
if (carryable.Reserved && carryable.Carrier != self)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public override bool CanTargetActor(Actor self, Actor target, TargetModifiers modifiers, ref string cursor)
|
|
{
|
|
return CanTarget(self, target);
|
|
}
|
|
|
|
public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor)
|
|
{
|
|
return CanTarget(self, target.Actor);
|
|
}
|
|
}
|
|
|
|
class CarryallDeliverUnitTargeter : AircraftMoveOrderTargeter
|
|
{
|
|
readonly AircraftInfo aircraftInfo;
|
|
readonly WVec carryableOffset;
|
|
|
|
public CarryallDeliverUnitTargeter(AircraftInfo aircraftInfo, WVec carryableOffset)
|
|
: base(aircraftInfo)
|
|
{
|
|
OrderID = "DeliverUnit";
|
|
OrderPriority = 6;
|
|
this.carryableOffset = carryableOffset;
|
|
this.aircraftInfo = aircraftInfo;
|
|
}
|
|
|
|
public override bool CanTarget(Actor self, Target target, List<Actor> othersAtTarget, ref TargetModifiers modifiers, ref string cursor)
|
|
{
|
|
if (modifiers.HasModifier(TargetModifiers.ForceMove))
|
|
return false;
|
|
|
|
var type = target.Type;
|
|
if (type == TargetType.Actor && self == target.Actor)
|
|
{
|
|
var altitude = self.World.Map.DistanceAboveTerrain(self.CenterPosition);
|
|
if (altitude.Length - carryableOffset.Z < aircraftInfo.MinAirborneAltitude)
|
|
{
|
|
cursor = "deploy";
|
|
OrderID = "Unload";
|
|
return true;
|
|
}
|
|
}
|
|
else if ((type == TargetType.Actor && target.Actor.Info.HasTraitInfo<BuildingInfo>())
|
|
|| (target.Type == TargetType.FrozenActor && target.FrozenActor.Info.HasTraitInfo<BuildingInfo>()))
|
|
{
|
|
cursor = "move-blocked";
|
|
return true;
|
|
}
|
|
|
|
return base.CanTarget(self, target, othersAtTarget, ref modifiers, ref cursor);
|
|
}
|
|
}
|
|
}
|
|
}
|