#region Copyright & License Information /* * Copyright 2007-2011 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. For more information, * see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using OpenRA.Mods.RA.Activities; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.RA.Move { class Move : Activity { static readonly List NoPath = new List(); CPos? destination; WRange nearEnough; List path; Func> getPath; Actor ignoreBuilding; // For dealing with blockers bool hasWaited; bool hasNotifiedBlocker; int waitTicksRemaining; // Scriptable move order // Ignores lane bias and nearby units public Move(CPos destination) { this.getPath = (self, mobile) => self.World.WorldActor.Trait().FindPath( PathSearch.FromPoint(self.World, mobile.Info, self, mobile.toCell, destination, false) .WithoutLaneBias()); this.destination = destination; this.nearEnough = WRange.Zero; } // HACK: for legacy code public Move(CPos destination, int nearEnough) : this(destination, WRange.FromCells(nearEnough)) { } public Move(CPos destination, WRange nearEnough) { this.getPath = (self, mobile) => self.World.WorldActor.Trait() .FindUnitPath(mobile.toCell, destination, self); this.destination = destination; this.nearEnough = nearEnough; } public Move(CPos destination, Actor ignoreBuilding) { this.getPath = (self, mobile) => self.World.WorldActor.Trait().FindPath( PathSearch.FromPoint(self.World, mobile.Info, self, mobile.toCell, destination, false) .WithIgnoredBuilding(ignoreBuilding)); this.destination = destination; this.nearEnough = WRange.Zero; this.ignoreBuilding = ignoreBuilding; } public Move(Target target, WRange range) { this.getPath = (self, mobile) => { if (!target.IsValidFor(self)) return NoPath; return self.World.WorldActor.Trait().FindUnitPathToRange( mobile.toCell, mobile.toSubCell, target.CenterPosition, range, self); }; this.destination = null; this.nearEnough = range; } public Move(Func> getPath) { this.getPath = (_1, _2) => getPath(); this.destination = null; this.nearEnough = WRange.Zero; } static int HashList(List xs) { int hash = 0; int n = 0; foreach (var x in xs) hash += n++ * x.GetHashCode(); return hash; } List EvalPath(Actor self, Mobile mobile) { var path = getPath(self, mobile).TakeWhile(a => a != mobile.toCell).ToList(); mobile.PathHash = HashList(path); return path; } public override Activity Tick(Actor self) { var mobile = self.Trait(); if (destination == mobile.toCell) return NextActivity; if (path == null) { if (mobile.ticksBeforePathing > 0) { --mobile.ticksBeforePathing; return this; } path = EvalPath(self, mobile); SanityCheckPath(mobile); } if (path.Count == 0) { destination = mobile.toCell; return this; } destination = path[0]; var nextCell = PopPath(self, mobile); if (nextCell == null) return this; var dir = nextCell.Value.First - mobile.fromCell; var firstFacing = Util.GetFacing(dir, mobile.Facing); if (firstFacing != mobile.Facing) { path.Add(nextCell.Value.First); return Util.SequenceActivities(new Turn(firstFacing), this); } else { mobile.SetLocation(mobile.fromCell, mobile.fromSubCell, nextCell.Value.First, nextCell.Value.Second); var move = new MoveFirstHalf( this, mobile.fromCell.CenterPosition + MobileInfo.SubCellOffsets[mobile.fromSubCell], Util.BetweenCells(mobile.fromCell, mobile.toCell) + (MobileInfo.SubCellOffsets[mobile.fromSubCell] + MobileInfo.SubCellOffsets[mobile.toSubCell]) / 2, mobile.Facing, mobile.Facing, 0); return move; } } [Conditional("SANITY_CHECKS")] void SanityCheckPath(Mobile mobile) { if (path.Count == 0) return; var d = path[path.Count - 1] - mobile.toCell; if (d.LengthSquared > 2) throw new InvalidOperationException("(Move) Sanity check failed"); } static void NotifyBlocker(Actor self, CPos nextCell) { foreach (var blocker in self.World.ActorMap.GetUnitsAt(nextCell)) { // Notify the blocker that he's blocking our move: foreach (var moveBlocked in blocker.TraitsImplementing()) moveBlocked.OnNotifyBlockingMove(blocker, self); } } Pair? PopPath(Actor self, Mobile mobile) { if (path.Count == 0) return null; var nextCell = path[path.Count - 1]; // Next cell in the move is blocked by another actor if (!mobile.CanEnterCell(nextCell, ignoreBuilding, true)) { // Are we close enough? var cellRange = nearEnough.Range / 1024; if ((mobile.toCell - destination.Value).LengthSquared <= cellRange * cellRange) { path.Clear(); return null; } // See if they will move if (!hasNotifiedBlocker) { NotifyBlocker(self, nextCell); hasNotifiedBlocker = true; } // Wait a bit to see if they leave if (!hasWaited) { var info = self.Info.Traits.Get(); waitTicksRemaining = info.WaitAverage + self.World.SharedRandom.Next(-info.WaitSpread, info.WaitSpread); hasWaited = true; } if (--waitTicksRemaining >= 0) return null; if (mobile.ticksBeforePathing > 0) { --mobile.ticksBeforePathing; return null; } // Calculate a new path mobile.RemoveInfluence(); var newPath = EvalPath(self, mobile); mobile.AddInfluence(); if (newPath.Count != 0) path = newPath; return null; } hasNotifiedBlocker = false; hasWaited = false; path.RemoveAt(path.Count - 1); var subCell = mobile.GetDesiredSubcell(nextCell, ignoreBuilding); return Pair.New(nextCell, subCell); } public override void Cancel(Actor self) { path = NoPath; base.Cancel(self); } public override IEnumerable GetTargets(Actor self) { if (path != null) return Enumerable.Reverse(path).Select(c => Target.FromCell(c)); if (destination != null) return new Target[] { Target.FromCell(destination.Value) }; return Target.None; } abstract class MovePart : Activity { protected readonly Move move; protected readonly WPos from, to; protected readonly int fromFacing, toFacing; protected readonly int moveFractionTotal; protected int moveFraction; public MovePart(Move move, WPos from, WPos to, int fromFacing, int toFacing, int startingFraction) { this.move = move; this.from = from; this.to = to; this.fromFacing = fromFacing; this.toFacing = toFacing; this.moveFraction = startingFraction; this.moveFractionTotal = (to - from).Length; } public override void Cancel(Actor self) { move.Cancel(self); base.Cancel(self); } public override void Queue(Activity activity) { move.Queue(activity); } public override Activity Tick(Actor self) { var mobile = self.Trait(); var ret = InnerTick(self, mobile); mobile.IsMoving = ret is MovePart; if (moveFraction > moveFractionTotal) moveFraction = moveFractionTotal; UpdateCenterLocation(self, mobile); return ret; } Activity InnerTick(Actor self, Mobile mobile) { moveFraction += mobile.MovementSpeedForCell(self, mobile.toCell); if (moveFraction <= moveFractionTotal) return this; var next = OnComplete(self, mobile, move); if (next != null) return next; return move; } void UpdateCenterLocation(Actor self, Mobile mobile) { // avoid division through zero if (moveFractionTotal != 0) mobile.SetVisualPosition(self, WPos.Lerp(from, to, moveFraction, moveFractionTotal)); else mobile.SetVisualPosition(self, to); if (moveFraction >= moveFractionTotal) mobile.Facing = toFacing & 0xFF; else mobile.Facing = int2.Lerp(fromFacing, toFacing, moveFraction, moveFractionTotal) & 0xFF; } protected abstract MovePart OnComplete(Actor self, Mobile mobile, Move parent); public override IEnumerable GetTargets(Actor self) { return move.GetTargets(self); } } class MoveFirstHalf : MovePart { public MoveFirstHalf(Move move, WPos from, WPos to, int fromFacing, int toFacing, int startingFraction) : base(move, from, to, fromFacing, toFacing, startingFraction) { } static bool IsTurn(Mobile mobile, CPos nextCell) { return nextCell - mobile.toCell != mobile.toCell - mobile.fromCell; } protected override MovePart OnComplete(Actor self, Mobile mobile, Move parent) { var fromSubcellOffset = MobileInfo.SubCellOffsets[mobile.fromSubCell]; var toSubcellOffset = MobileInfo.SubCellOffsets[mobile.toSubCell]; var nextCell = parent.PopPath(self, mobile); if (nextCell != null) { if (IsTurn(mobile, nextCell.Value.First)) { var nextSubcellOffset = MobileInfo.SubCellOffsets[nextCell.Value.Second]; var ret = new MoveFirstHalf( move, Util.BetweenCells(mobile.fromCell, mobile.toCell) + (fromSubcellOffset + toSubcellOffset) / 2, Util.BetweenCells(mobile.toCell, nextCell.Value.First) + (toSubcellOffset + nextSubcellOffset) / 2, mobile.Facing, Util.GetNearestFacing(mobile.Facing, Util.GetFacing(nextCell.Value.First - mobile.toCell, mobile.Facing)), moveFraction - moveFractionTotal); mobile.SetLocation(mobile.toCell, mobile.toSubCell, nextCell.Value.First, nextCell.Value.Second); return ret; } parent.path.Add(nextCell.Value.First); } var ret2 = new MoveSecondHalf( move, Util.BetweenCells(mobile.fromCell, mobile.toCell) + (fromSubcellOffset + toSubcellOffset) / 2, mobile.toCell.CenterPosition + toSubcellOffset, mobile.Facing, mobile.Facing, moveFraction - moveFractionTotal); mobile.EnteringCell(self); mobile.SetLocation(mobile.toCell, mobile.toSubCell, mobile.toCell, mobile.toSubCell); return ret2; } } class MoveSecondHalf : MovePart { public MoveSecondHalf(Move move, WPos from, WPos to, int fromFacing, int toFacing, int startingFraction) : base(move, from, to, fromFacing, toFacing, startingFraction) { } protected override MovePart OnComplete(Actor self, Mobile mobile, Move parent) { mobile.SetPosition(self, mobile.toCell); return null; } } } public static class ActorExtensionsForMove { public static bool IsMoving(this Actor self) { var a = self.GetCurrentActivity(); if (a == null) return false; // HACK: Dirty, but it suffices until we do something better: if (a.GetType() == typeof(Move)) return true; if (a.GetType() == typeof(MoveAdjacentTo)) return true; if (a.GetType() == typeof(AttackMove)) return true; // Not a move: return false; } } }