325 lines
12 KiB
C#
325 lines
12 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.Traits;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Activities
|
|
{
|
|
public class Resupply : Activity
|
|
{
|
|
readonly IHealth health;
|
|
readonly RepairsUnits[] allRepairsUnits;
|
|
readonly Target host;
|
|
readonly WDist closeEnough;
|
|
readonly Repairable repairable;
|
|
readonly RepairableNear repairableNear;
|
|
readonly Rearmable rearmable;
|
|
readonly INotifyResupply[] notifyResupplies;
|
|
readonly INotifyDockHost[] notifyDockHosts;
|
|
readonly INotifyDockClient[] notifyDockClients;
|
|
readonly ICallForTransport[] transportCallers;
|
|
readonly IMove move;
|
|
readonly Aircraft aircraft;
|
|
readonly IMoveInfo moveInfo;
|
|
readonly bool stayOnResupplier;
|
|
readonly bool wasRepaired;
|
|
readonly PlayerResources playerResources;
|
|
readonly int unitCost;
|
|
readonly MoveCooldownHelper moveCooldownHelper;
|
|
|
|
int remainingTicks;
|
|
bool played;
|
|
bool actualResupplyStarted;
|
|
ResupplyType activeResupplyTypes = ResupplyType.None;
|
|
|
|
public Resupply(Actor self, Actor host, WDist closeEnough, bool stayOnResupplier = false)
|
|
{
|
|
this.host = Target.FromActor(host);
|
|
this.closeEnough = closeEnough;
|
|
this.stayOnResupplier = stayOnResupplier;
|
|
allRepairsUnits = host.TraitsImplementing<RepairsUnits>().ToArray();
|
|
health = self.TraitOrDefault<IHealth>();
|
|
repairable = self.TraitOrDefault<Repairable>();
|
|
repairableNear = self.TraitOrDefault<RepairableNear>();
|
|
rearmable = self.TraitOrDefault<Rearmable>();
|
|
notifyResupplies = host.TraitsImplementing<INotifyResupply>().ToArray();
|
|
notifyDockHosts = host.TraitsImplementing<INotifyDockHost>().ToArray();
|
|
notifyDockClients = self.TraitsImplementing<INotifyDockClient>().ToArray();
|
|
transportCallers = self.TraitsImplementing<ICallForTransport>().ToArray();
|
|
move = self.Trait<IMove>();
|
|
aircraft = move as Aircraft;
|
|
moveInfo = self.Info.TraitInfo<IMoveInfo>();
|
|
playerResources = self.Owner.PlayerActor.Trait<PlayerResources>();
|
|
moveCooldownHelper = new MoveCooldownHelper(self.World, move as Mobile) { RetryIfDestinationBlocked = true };
|
|
|
|
var valued = self.Info.TraitInfoOrDefault<ValuedInfo>();
|
|
unitCost = valued != null ? valued.Cost : 0;
|
|
|
|
var cannotRepairAtHost = health == null || health.DamageState == DamageState.Undamaged
|
|
|| allRepairsUnits.Length == 0
|
|
|| ((repairable == null || !repairable.Info.RepairActors.Contains(host.Info.Name))
|
|
&& (repairableNear == null || !repairableNear.Info.RepairActors.Contains(host.Info.Name)));
|
|
|
|
if (!cannotRepairAtHost)
|
|
{
|
|
activeResupplyTypes |= ResupplyType.Repair;
|
|
|
|
// HACK: Reservable logic can't handle repairs, so force a take-off if resupply included repairs.
|
|
// TODO: Make reservation logic or future docking logic properly handle this.
|
|
wasRepaired = true;
|
|
}
|
|
|
|
var cannotRearmAtHost = rearmable == null || !rearmable.Info.RearmActors.Contains(host.Info.Name) || rearmable.RearmableAmmoPools.All(p => p.HasFullAmmo);
|
|
if (!cannotRearmAtHost)
|
|
activeResupplyTypes |= ResupplyType.Rearm;
|
|
}
|
|
|
|
public override bool Tick(Actor self)
|
|
{
|
|
// Wait for the cooldown to expire before releasing the unit if this was cancelled
|
|
if (IsCanceling && remainingTicks > 0)
|
|
{
|
|
remainingTicks--;
|
|
return false;
|
|
}
|
|
|
|
var isHostInvalid = host.Type != TargetType.Actor || !host.Actor.IsInWorld;
|
|
var isCloseEnough = false;
|
|
if (!isHostInvalid)
|
|
{
|
|
// Negative means there's no distance limit.
|
|
// If RepairableNear, use TargetablePositions instead of CenterPosition
|
|
// to ensure the actor moves close enough to the host.
|
|
// Otherwise check against host CenterPosition.
|
|
if (closeEnough < WDist.Zero)
|
|
isCloseEnough = true;
|
|
else if (repairableNear != null)
|
|
isCloseEnough = host.IsInRange(self.CenterPosition, closeEnough);
|
|
else
|
|
isCloseEnough = (host.CenterPosition - self.CenterPosition).HorizontalLengthSquared <= closeEnough.LengthSquared;
|
|
}
|
|
|
|
// This ensures transports are also cancelled when the host becomes invalid
|
|
if (!IsCanceling && isHostInvalid)
|
|
Cancel(self, true);
|
|
|
|
if (IsCanceling || isHostInvalid)
|
|
{
|
|
// Only tick host INotifyResupply traits one last time if host is still alive
|
|
if (!isHostInvalid)
|
|
foreach (var notifyResupply in notifyResupplies)
|
|
notifyResupply.ResupplyTick(host.Actor, self, ResupplyType.None);
|
|
|
|
// HACK: If the activity is cancelled while we're on the host resupplying (or about to start resupplying),
|
|
// move actor outside the resupplier footprint to prevent it from blocking other actors.
|
|
// Additionally, if the host is no longer valid, make aircraft take off.
|
|
if (isCloseEnough || isHostInvalid)
|
|
OnResupplyEnding(self, isHostInvalid);
|
|
|
|
return true;
|
|
}
|
|
|
|
var result = moveCooldownHelper.Tick(false);
|
|
if (result != null)
|
|
return result.Value;
|
|
|
|
if (activeResupplyTypes != 0 && aircraft == null && !isCloseEnough)
|
|
{
|
|
var targetCell = self.World.Map.CellContaining(host.Actor.CenterPosition);
|
|
|
|
// HACK: Repairable needs the actor to move to host center.
|
|
// TODO: Get rid of this or at least replace it with something less hacky.
|
|
moveCooldownHelper.NotifyMoveQueued();
|
|
if (repairableNear == null)
|
|
QueueChild(move.MoveOntoTarget(self, host, WVec.Zero, null, moveInfo.GetTargetLineColor()));
|
|
else
|
|
QueueChild(move.MoveWithinRange(host, closeEnough, targetLineColor: moveInfo.GetTargetLineColor()));
|
|
|
|
var delta = (self.CenterPosition - host.CenterPosition).LengthSquared;
|
|
transportCallers.FirstOrDefault(t => t.MinimumDistance.LengthSquared < delta)?.RequestTransport(self, targetCell);
|
|
|
|
return false;
|
|
}
|
|
|
|
// We don't want to trigger this until we've reached the resupplier and can start resupplying
|
|
if (!actualResupplyStarted && activeResupplyTypes > 0)
|
|
{
|
|
actualResupplyStarted = true;
|
|
foreach (var notifyResupply in notifyResupplies)
|
|
notifyResupply.BeforeResupply(host.Actor, self, activeResupplyTypes);
|
|
|
|
foreach (var nd in notifyDockClients)
|
|
nd.Docked(self, host.Actor);
|
|
|
|
foreach (var nd in notifyDockHosts)
|
|
nd.Docked(host.Actor, self);
|
|
}
|
|
|
|
if (activeResupplyTypes.HasFlag(ResupplyType.Repair))
|
|
RepairTick(self);
|
|
|
|
if (activeResupplyTypes.HasFlag(ResupplyType.Rearm) && rearmable.RearmTick(self))
|
|
activeResupplyTypes &= ~ResupplyType.Rearm;
|
|
|
|
foreach (var notifyResupply in notifyResupplies)
|
|
notifyResupply.ResupplyTick(host.Actor, self, activeResupplyTypes);
|
|
|
|
if (activeResupplyTypes == 0)
|
|
{
|
|
OnResupplyEnding(self);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public override void Cancel(Actor self, bool keepQueue = false)
|
|
{
|
|
// HACK: force move activities to ignore the transit-only cells when cancelling
|
|
// The idle handler will take over and move them into a safe cell
|
|
if (ChildActivity != null)
|
|
foreach (var c in ChildActivity.ActivitiesImplementing<Move>())
|
|
c.Cancel(self, false, true);
|
|
|
|
foreach (var t in transportCallers)
|
|
t.MovementCancelled(self);
|
|
|
|
base.Cancel(self, keepQueue);
|
|
}
|
|
|
|
public override IEnumerable<TargetLineNode> TargetLineNodes(Actor self)
|
|
{
|
|
if (ChildActivity == null)
|
|
yield return new TargetLineNode(host, moveInfo.GetTargetLineColor());
|
|
else
|
|
{
|
|
var current = ChildActivity;
|
|
while (current != null)
|
|
{
|
|
foreach (var n in current.TargetLineNodes(self))
|
|
yield return n;
|
|
|
|
current = current.NextActivity;
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnResupplyEnding(Actor self, bool isHostInvalid = false)
|
|
{
|
|
var rp = !isHostInvalid ? host.Actor.TraitOrDefault<RallyPoint>() : null;
|
|
if (aircraft != null)
|
|
{
|
|
if (wasRepaired || isHostInvalid || (!stayOnResupplier && aircraft.Info.TakeOffOnResupply))
|
|
{
|
|
if (self.CurrentActivity.NextActivity == null && rp != null && rp.Path.Count > 0)
|
|
{
|
|
moveCooldownHelper.NotifyMoveQueued();
|
|
foreach (var cell in rp.Path)
|
|
QueueChild(new AttackMoveActivity(self, () => move.MoveTo(
|
|
cell,
|
|
1,
|
|
ignoreActor: repairableNear != null ? null : host.Actor,
|
|
targetLineColor: aircraft.Info.TargetLineColor)));
|
|
}
|
|
else
|
|
QueueChild(new TakeOff(self));
|
|
|
|
aircraft.UnReserve();
|
|
}
|
|
|
|
// Aircraft without TakeOffOnResupply remain on the resupplier until something else needs it
|
|
// The rally point location is queried by the aircraft before it takes off
|
|
else
|
|
aircraft.AllowYieldingReservation();
|
|
}
|
|
else if (!stayOnResupplier && !isHostInvalid)
|
|
{
|
|
// If there's no next activity, move to rallypoint if available, else just leave host if Repairable.
|
|
// Do nothing if RepairableNear (RepairableNear actors don't enter their host and will likely remain within closeEnough).
|
|
// If there's a next activity and we're not RepairableNear, first leave host if the next activity is not a Move.
|
|
moveCooldownHelper.NotifyMoveQueued();
|
|
if (self.CurrentActivity.NextActivity == null)
|
|
{
|
|
if (rp != null && rp.Path.Count > 0)
|
|
foreach (var cell in rp.Path)
|
|
QueueChild(new AttackMoveActivity(self, () => move.MoveTo(cell, 1, repairableNear != null ? null : host.Actor, true, moveInfo.GetTargetLineColor())));
|
|
else if (repairableNear == null)
|
|
QueueChild(move.MoveToTarget(self, host));
|
|
}
|
|
else if (repairableNear == null && self.CurrentActivity.NextActivity is not Move)
|
|
QueueChild(move.MoveToTarget(self, host));
|
|
}
|
|
|
|
foreach (var nd in notifyDockClients)
|
|
nd.Undocked(self, host.Actor);
|
|
|
|
foreach (var nd in notifyDockHosts)
|
|
nd.Undocked(host.Actor, self);
|
|
}
|
|
|
|
void RepairTick(Actor self)
|
|
{
|
|
var repairsUnits = allRepairsUnits.FirstOrDefault(r => !r.IsTraitDisabled && !r.IsTraitPaused);
|
|
if (repairsUnits == null)
|
|
{
|
|
if (!allRepairsUnits.Any(r => r.IsTraitPaused))
|
|
activeResupplyTypes &= ~ResupplyType.Repair;
|
|
|
|
return;
|
|
}
|
|
|
|
if (health.DamageState == DamageState.Undamaged)
|
|
{
|
|
if (host.Actor.Owner != self.Owner)
|
|
host.Actor.Owner.PlayerActor.TraitOrDefault<PlayerExperience>()?.GiveExperience(repairsUnits.Info.PlayerExperience);
|
|
|
|
Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", repairsUnits.Info.FinishRepairingNotification, self.Owner.Faction.InternalName);
|
|
TextNotificationsManager.AddTransientLine(self.Owner, repairsUnits.Info.FinishRepairingTextNotification);
|
|
|
|
activeResupplyTypes &= ~ResupplyType.Repair;
|
|
return;
|
|
}
|
|
|
|
if (remainingTicks == 0)
|
|
{
|
|
var hpToRepair = repairable != null && repairable.Info.HpPerStep > 0 ? repairable.Info.HpPerStep : repairsUnits.Info.HpPerStep;
|
|
|
|
// Cast to long to avoid overflow when multiplying by the health
|
|
var value = (long)unitCost * repairsUnits.Info.ValuePercentage;
|
|
var cost = value == 0 ? 0 : Math.Max(1, (int)(hpToRepair * value / (health.MaxHP * 100L)));
|
|
|
|
if (!played)
|
|
{
|
|
played = true;
|
|
Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", repairsUnits.Info.StartRepairingNotification, self.Owner.Faction.InternalName);
|
|
TextNotificationsManager.AddTransientLine(self.Owner, repairsUnits.Info.StartRepairingTextNotification);
|
|
}
|
|
|
|
if (!playerResources.TakeCash(cost, true))
|
|
{
|
|
remainingTicks = 1;
|
|
return;
|
|
}
|
|
|
|
self.InflictDamage(host.Actor, new Damage(-hpToRepair, repairsUnits.Info.RepairDamageTypes));
|
|
remainingTicks = repairsUnits.Info.Interval;
|
|
}
|
|
else
|
|
--remainingTicks;
|
|
}
|
|
}
|
|
}
|