1 #region Copyright & License Information
2 /*
3  * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
4  * This file is part of OpenRA, which is free software. It is made
5  * available to you under the terms of the GNU General Public License
6  * as published by the Free Software Foundation, either version 3 of
7  * the License, or (at your option) any later version. For more
8  * information, see COPYING.
9  */
10 #endregion
11 
12 using System;
13 using System.Collections.Generic;
14 using System.Linq;
15 using OpenRA.Activities;
16 using OpenRA.Mods.Common.Activities;
17 using OpenRA.Mods.Common.Pathfinder;
18 using OpenRA.Primitives;
19 using OpenRA.Support;
20 using OpenRA.Traits;
21 
22 namespace OpenRA.Mods.Common.Traits
23 {
24 	[Desc("Unit is able to move.")]
25 	public class MobileInfo : PausableConditionalTraitInfo, IMoveInfo, IPositionableInfo, IFacingInfo, IActorPreviewInitInfo,
26 		IEditorActorOptions
27 	{
28 		[LocomotorReference]
29 		[FieldLoader.Require]
30 		[Desc("Which Locomotor does this trait use. Must be defined on the World actor.")]
31 		public readonly string Locomotor = null;
32 
33 		public readonly int InitialFacing = 0;
34 
35 		[Desc("Speed at which the actor turns.")]
36 		public readonly int TurnSpeed = 255;
37 
38 		public readonly int Speed = 1;
39 
40 		public readonly string Cursor = "move";
41 		public readonly string BlockedCursor = "move-blocked";
42 
43 		[VoiceReference]
44 		public readonly string Voice = "Action";
45 
46 		[Desc("Facing to use for actor previews (map editor, color picker, etc)")]
47 		public readonly int PreviewFacing = 96;
48 
49 		[Desc("Display order for the facing slider in the map editor")]
50 		public readonly int EditorFacingDisplayOrder = 3;
51 
52 		[ConsumedConditionReference]
53 		[Desc("Boolean expression defining the condition under which the regular (non-force) move cursor is disabled.")]
54 		public readonly BooleanExpression RequireForceMoveCondition = null;
55 
56 		[ConsumedConditionReference]
57 		[Desc("Boolean expression defining the condition under which this actor cannot be nudged by other actors.")]
58 		public readonly BooleanExpression ImmovableCondition = null;
59 
IActorPreviewInitInfo.ActorPreviewInits(ActorInfo ai, ActorPreviewType type)60 		IEnumerable<object> IActorPreviewInitInfo.ActorPreviewInits(ActorInfo ai, ActorPreviewType type)
61 		{
62 			yield return new FacingInit(PreviewFacing);
63 		}
64 
Create(ActorInitializer init)65 		public override object Create(ActorInitializer init) { return new Mobile(init, this); }
66 
67 		public LocomotorInfo LocomotorInfo { get; private set; }
68 
RulesetLoaded(Ruleset rules, ActorInfo ai)69 		public override void RulesetLoaded(Ruleset rules, ActorInfo ai)
70 		{
71 			var locomotorInfos = rules.Actors["world"].TraitInfos<LocomotorInfo>();
72 			LocomotorInfo = locomotorInfos.FirstOrDefault(li => li.Name == Locomotor);
73 			if (LocomotorInfo == null)
74 				throw new YamlException("A locomotor named '{0}' doesn't exist.".F(Locomotor));
75 			else if (locomotorInfos.Count(li => li.Name == Locomotor) > 1)
76 				throw new YamlException("There is more than one locomotor named '{0}'.".F(Locomotor));
77 
78 			// We need to reset the reference to the locomotor between each worlds, otherwise we are reference the previous state.
79 			locomotor = null;
80 
81 			base.RulesetLoaded(rules, ai);
82 		}
83 
GetInitialFacing()84 		public int GetInitialFacing() { return InitialFacing; }
85 
86 		// initialized and used by CanEnterCell
87 		Locomotor locomotor;
88 
89 		/// <summary>
90 		/// Note: If the target <paramref name="cell"/> has any free subcell, the value of <paramref name="subCell"/> is ignored.
91 		/// </summary>
CanEnterCell(World world, Actor self, CPos cell, SubCell subCell = SubCell.FullCell, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)92 		public bool CanEnterCell(World world, Actor self, CPos cell, SubCell subCell = SubCell.FullCell, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)
93 		{
94 			// PERF: Avoid repeated trait queries on the hot path
95 			if (locomotor == null)
96 				locomotor = world.WorldActor.TraitsImplementing<Locomotor>()
97 				   .SingleOrDefault(l => l.Info.Name == Locomotor);
98 
99 			if (locomotor.MovementCostForCell(cell) == short.MaxValue)
100 				return false;
101 
102 			return locomotor.CanMoveFreelyInto(self, cell, subCell, check, ignoreActor);
103 		}
104 
CanStayInCell(World world, CPos cell)105 		public bool CanStayInCell(World world, CPos cell)
106 		{
107 			// PERF: Avoid repeated trait queries on the hot path
108 			if (locomotor == null)
109 				locomotor = world.WorldActor.TraitsImplementing<Locomotor>()
110 				   .SingleOrDefault(l => l.Info.Name == Locomotor);
111 
112 			if (cell.Layer == CustomMovementLayerType.Tunnel)
113 				return false;
114 
115 			return locomotor.CanStayInCell(cell);
116 		}
117 
OccupiedCells(ActorInfo info, CPos location, SubCell subCell = SubCell.Any)118 		public IReadOnlyDictionary<CPos, SubCell> OccupiedCells(ActorInfo info, CPos location, SubCell subCell = SubCell.Any)
119 		{
120 			return new ReadOnlyDictionary<CPos, SubCell>(new Dictionary<CPos, SubCell>() { { location, subCell } });
121 		}
122 
123 		bool IOccupySpaceInfo.SharesCell { get { return LocomotorInfo.SharesCell; } }
124 
IEditorActorOptions.ActorOptions(ActorInfo ai, World world)125 		IEnumerable<EditorActorOption> IEditorActorOptions.ActorOptions(ActorInfo ai, World world)
126 		{
127 			yield return new EditorActorSlider("Facing", EditorFacingDisplayOrder, 0, 255, 8,
128 				actor =>
129 				{
130 					var init = actor.Init<FacingInit>();
131 					return init != null ? init.Value(world) : InitialFacing;
132 				},
133 				(actor, value) =>
134 				{
135 					// TODO: This can all go away once turrets are properly defined as a relative facing
136 					var turretInit = actor.Init<TurretFacingInit>();
137 					var turretsInit = actor.Init<TurretFacingsInit>();
138 					var facingInit = actor.Init<FacingInit>();
139 
140 					var oldFacing = facingInit != null ? facingInit.Value(world) : InitialFacing;
141 					var newFacing = (int)value;
142 
143 					if (turretInit != null)
144 					{
145 						var newTurretFacing = (turretInit.Value(world) + newFacing - oldFacing + 255) % 255;
146 						actor.ReplaceInit(new TurretFacingInit(newTurretFacing));
147 					}
148 
149 					if (turretsInit != null)
150 					{
151 						var newTurretFacings = turretsInit.Value(world)
152 							.ToDictionary(kv => kv.Key, kv => (kv.Value + newFacing - oldFacing + 255) % 255);
153 						actor.ReplaceInit(new TurretFacingsInit(newTurretFacings));
154 					}
155 
156 					actor.ReplaceInit(new FacingInit(newFacing));
157 				});
158 		}
159 	}
160 
161 	public class Mobile : PausableConditionalTrait<MobileInfo>, IIssueOrder, IResolveOrder, IOrderVoice, IPositionable, IMove, ITick, ICreationActivity,
162 		IFacing, IDeathActorInitModifier, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyBlockingMove, IActorPreviewInitModifier, INotifyBecomingIdle
163 	{
164 		readonly Actor self;
165 		readonly Lazy<IEnumerable<int>> speedModifiers;
166 
167 		readonly bool returnToCellOnCreation;
168 		readonly bool returnToCellOnCreationRecalculateSubCell = true;
169 		readonly int creationActivityDelay;
170 
171 		#region IMove CurrentMovementTypes
172 		MovementType movementTypes;
173 		public MovementType CurrentMovementTypes
174 		{
175 			get
176 			{
177 				return movementTypes;
178 			}
179 
180 			set
181 			{
182 				var oldValue = movementTypes;
183 				movementTypes = value;
184 				if (value != oldValue)
185 				{
186 					self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
187 					foreach (var n in notifyMoving)
188 						n.MovementTypeChanged(self, value);
189 				}
190 			}
191 		}
192 		#endregion
193 
194 		int oldFacing, facing;
195 		WPos oldPos;
196 		CPos fromCell, toCell;
197 		public SubCell FromSubCell, ToSubCell;
198 		INotifyCustomLayerChanged[] notifyCustomLayerChanged;
199 		INotifyVisualPositionChanged[] notifyVisualPositionChanged;
200 		INotifyMoving[] notifyMoving;
201 		INotifyFinishedMoving[] notifyFinishedMoving;
202 		IWrapMove[] moveWrappers;
203 		bool requireForceMove;
204 
205 		public bool IsImmovable { get; private set; }
206 		public bool TurnToMove;
207 		public bool IsBlocking { get; private set; }
208 
209 		public bool IsMovingBetweenCells
210 		{
211 			get { return FromCell != ToCell; }
212 		}
213 
214 		#region IFacing
215 		[Sync]
216 		public int Facing
217 		{
218 			get { return facing; }
219 			set { facing = value; }
220 		}
221 
222 		public int TurnSpeed { get { return Info.TurnSpeed; } }
223 		#endregion
224 
225 		[Sync]
226 		public CPos FromCell { get { return fromCell; } }
227 
228 		[Sync]
229 		public CPos ToCell { get { return toCell; } }
230 
231 		[Sync]
232 		public int PathHash;	// written by Move.EvalPath, to temporarily debug this crap.
233 
234 		public Locomotor Locomotor { get; private set; }
235 
236 		public IPathFinder Pathfinder { get; private set; }
237 
238 		#region IOccupySpace
239 
240 		[Sync]
241 		public WPos CenterPosition { get; private set; }
242 
243 		public CPos TopLeft { get { return ToCell; } }
244 
OccupiedCells()245 		public Pair<CPos, SubCell>[] OccupiedCells()
246 		{
247 			if (FromCell == ToCell)
248 				return new[] { Pair.New(FromCell, FromSubCell) };
249 
250 			// HACK: Should be fixed properly, see https://github.com/OpenRA/OpenRA/pull/17292 for an explanation
251 			if (Info.LocomotorInfo.SharesCell)
252 				return new[] { Pair.New(ToCell, ToSubCell) };
253 
254 			return new[] { Pair.New(FromCell, FromSubCell), Pair.New(ToCell, ToSubCell) };
255 		}
256 		#endregion
257 
Mobile(ActorInitializer init, MobileInfo info)258 		public Mobile(ActorInitializer init, MobileInfo info)
259 			: base(info)
260 		{
261 			self = init.Self;
262 
263 			speedModifiers = Exts.Lazy(() => self.TraitsImplementing<ISpeedModifier>().ToArray().Select(x => x.GetSpeedModifier()));
264 
265 			ToSubCell = FromSubCell = info.LocomotorInfo.SharesCell ? init.World.Map.Grid.DefaultSubCell : SubCell.FullCell;
266 			if (init.Contains<SubCellInit>())
267 			{
268 				FromSubCell = ToSubCell = init.Get<SubCellInit, SubCell>();
269 				returnToCellOnCreationRecalculateSubCell = false;
270 			}
271 
272 			if (init.Contains<LocationInit>())
273 			{
274 				fromCell = toCell = init.Get<LocationInit, CPos>();
275 				SetVisualPosition(self, init.World.Map.CenterOfSubCell(FromCell, FromSubCell));
276 			}
277 
278 			Facing = oldFacing = init.Contains<FacingInit>() ? init.Get<FacingInit, int>() : info.InitialFacing;
279 
280 			// Sets the initial visual position
281 			// Unit will move into the cell grid (defined by LocationInit) as its initial activity
282 			if (init.Contains<CenterPositionInit>())
283 			{
284 				oldPos = init.Get<CenterPositionInit, WPos>();
285 				SetVisualPosition(self, oldPos);
286 				returnToCellOnCreation = true;
287 			}
288 
289 			if (init.Contains<CreationActivityDelayInit>())
290 				creationActivityDelay = init.Get<CreationActivityDelayInit, int>();
291 		}
292 
Created(Actor self)293 		protected override void Created(Actor self)
294 		{
295 			notifyCustomLayerChanged = self.TraitsImplementing<INotifyCustomLayerChanged>().ToArray();
296 			notifyVisualPositionChanged = self.TraitsImplementing<INotifyVisualPositionChanged>().ToArray();
297 			notifyMoving = self.TraitsImplementing<INotifyMoving>().ToArray();
298 			notifyFinishedMoving = self.TraitsImplementing<INotifyFinishedMoving>().ToArray();
299 			moveWrappers = self.TraitsImplementing<IWrapMove>().ToArray();
300 			Pathfinder = self.World.WorldActor.Trait<IPathFinder>();
301 			Locomotor = self.World.WorldActor.TraitsImplementing<Locomotor>()
302 				.Single(l => l.Info.Name == Info.Locomotor);
303 
304 			base.Created(self);
305 		}
306 
ITick.Tick(Actor self)307 		void ITick.Tick(Actor self)
308 		{
309 			UpdateMovement(self);
310 		}
311 
UpdateMovement(Actor self)312 		public void UpdateMovement(Actor self)
313 		{
314 			var newMovementTypes = MovementType.None;
315 			if ((oldPos - CenterPosition).HorizontalLengthSquared != 0)
316 				newMovementTypes |= MovementType.Horizontal;
317 
318 			if (oldPos.Z != CenterPosition.Z)
319 				newMovementTypes |= MovementType.Vertical;
320 
321 			if (oldFacing != Facing)
322 				newMovementTypes |= MovementType.Turn;
323 
324 			CurrentMovementTypes = newMovementTypes;
325 
326 			oldPos = CenterPosition;
327 			oldFacing = Facing;
328 		}
329 
INotifyAddedToWorld.AddedToWorld(Actor self)330 		void INotifyAddedToWorld.AddedToWorld(Actor self)
331 		{
332 			self.World.AddToMaps(self, this);
333 		}
334 
INotifyRemovedFromWorld.RemovedFromWorld(Actor self)335 		void INotifyRemovedFromWorld.RemovedFromWorld(Actor self)
336 		{
337 			self.World.RemoveFromMaps(self, this);
338 		}
339 
TraitEnabled(Actor self)340 		protected override void TraitEnabled(Actor self)
341 		{
342 			self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
343 		}
344 
TraitDisabled(Actor self)345 		protected override void TraitDisabled(Actor self)
346 		{
347 			self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
348 		}
349 
TraitResumed(Actor self)350 		protected override void TraitResumed(Actor self)
351 		{
352 			self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
353 		}
354 
TraitPaused(Actor self)355 		protected override void TraitPaused(Actor self)
356 		{
357 			self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
358 		}
359 
360 		#region Local misc stuff
361 
Nudge(Actor nudger)362 		public void Nudge(Actor nudger)
363 		{
364 			if (IsTraitDisabled || IsTraitPaused || IsImmovable)
365 				return;
366 
367 			var cell = GetAdjacentCell(nudger.Location);
368 			if (cell != null)
369 				self.QueueActivity(false, MoveTo(cell.Value, 0));
370 		}
371 
GetAdjacentCell(CPos nextCell)372 		public CPos? GetAdjacentCell(CPos nextCell)
373 		{
374 			var availCells = new List<CPos>();
375 			var notStupidCells = new List<CPos>();
376 			foreach (CVec direction in CVec.Directions)
377 			{
378 				var p = ToCell + direction;
379 				if (CanEnterCell(p) && CanStayInCell(p))
380 					availCells.Add(p);
381 				else if (p != nextCell && p != ToCell)
382 					notStupidCells.Add(p);
383 			}
384 
385 			CPos? newCell = null;
386 			if (availCells.Count > 0)
387 				newCell = availCells.Random(self.World.SharedRandom);
388 			else
389 			{
390 				var cellInfo = notStupidCells
391 					.SelectMany(c => self.World.ActorMap.GetActorsAt(c).Where(IsMovable),
392 						(c, a) => new { Cell = c, Actor = a })
393 					.RandomOrDefault(self.World.SharedRandom);
394 				if (cellInfo != null)
395 					newCell = cellInfo.Cell;
396 			}
397 
398 			return newCell;
399 		}
400 
IsMovable(Actor otherActor)401 		static bool IsMovable(Actor otherActor)
402 		{
403 			if (!otherActor.IsIdle)
404 				return false;
405 
406 			var mobile = otherActor.TraitOrDefault<Mobile>();
407 			if (mobile == null || mobile.IsTraitDisabled || mobile.IsTraitPaused || mobile.IsImmovable)
408 				return false;
409 
410 			return true;
411 		}
412 
IsLeaving()413 		public bool IsLeaving()
414 		{
415 			if (CurrentMovementTypes.HasFlag(MovementType.Horizontal))
416 				return true;
417 
418 			if (CurrentMovementTypes.HasFlag(MovementType.Turn))
419 				return TurnToMove;
420 
421 			return false;
422 		}
423 
CanInteractWithGroundLayer(Actor self)424 		public bool CanInteractWithGroundLayer(Actor self)
425 		{
426 			// TODO: Think about extending this to support arbitrary layer-layer checks
427 			// in a way that is compatible with the other IMove types.
428 			// This would then allow us to e.g. have units attack other units inside tunnels.
429 			if (ToCell.Layer == 0)
430 				return true;
431 
432 			ICustomMovementLayer layer;
433 			if (self.World.GetCustomMovementLayers().TryGetValue(ToCell.Layer, out layer))
434 				return layer.InteractsWithDefaultLayer;
435 
436 			return true;
437 		}
438 
439 		#endregion
440 
441 		#region IPositionable
442 
443 		// Returns a valid sub-cell
GetValidSubCell(SubCell preferred = SubCell.Any)444 		public SubCell GetValidSubCell(SubCell preferred = SubCell.Any)
445 		{
446 			// Try same sub-cell
447 			if (preferred == SubCell.Any)
448 				preferred = FromSubCell;
449 
450 			// Fix sub-cell assignment
451 			if (Info.LocomotorInfo.SharesCell)
452 			{
453 				if (preferred <= SubCell.FullCell)
454 					return self.World.Map.Grid.DefaultSubCell;
455 			}
456 			else
457 			{
458 				if (preferred != SubCell.FullCell)
459 					return SubCell.FullCell;
460 			}
461 
462 			return preferred;
463 		}
464 
465 		// Sets the location (fromCell, toCell, FromSubCell, ToSubCell) and visual position (CenterPosition)
SetPosition(Actor self, CPos cell, SubCell subCell = SubCell.Any)466 		public void SetPosition(Actor self, CPos cell, SubCell subCell = SubCell.Any)
467 		{
468 			subCell = GetValidSubCell(subCell);
469 			SetLocation(cell, subCell, cell, subCell);
470 
471 			var position = cell.Layer == 0 ? self.World.Map.CenterOfCell(cell) :
472 				self.World.GetCustomMovementLayers()[cell.Layer].CenterOfCell(cell);
473 
474 			var subcellOffset = self.World.Map.Grid.OffsetOfSubCell(subCell);
475 			SetVisualPosition(self, position + subcellOffset);
476 			FinishedMoving(self);
477 		}
478 
479 		// Sets the location (fromCell, toCell, FromSubCell, ToSubCell) and visual position (CenterPosition)
SetPosition(Actor self, WPos pos)480 		public void SetPosition(Actor self, WPos pos)
481 		{
482 			var cell = self.World.Map.CellContaining(pos);
483 			SetLocation(cell, FromSubCell, cell, FromSubCell);
484 			SetVisualPosition(self, self.World.Map.CenterOfSubCell(cell, FromSubCell) + new WVec(0, 0, self.World.Map.DistanceAboveTerrain(pos).Length));
485 			FinishedMoving(self);
486 		}
487 
488 		// Sets only the visual position (CenterPosition)
SetVisualPosition(Actor self, WPos pos)489 		public void SetVisualPosition(Actor self, WPos pos)
490 		{
491 			CenterPosition = pos;
492 			self.World.UpdateMaps(self, this);
493 
494 			// The first time SetVisualPosition is called is in the constructor before creation, so we need a null check here as well
495 			if (notifyVisualPositionChanged == null)
496 				return;
497 
498 			foreach (var n in notifyVisualPositionChanged)
499 				n.VisualPositionChanged(self, fromCell.Layer, toCell.Layer);
500 		}
501 
IsLeavingCell(CPos location, SubCell subCell = SubCell.Any)502 		public bool IsLeavingCell(CPos location, SubCell subCell = SubCell.Any)
503 		{
504 			return ToCell != location && fromCell == location
505 				&& (subCell == SubCell.Any || FromSubCell == subCell || subCell == SubCell.FullCell || FromSubCell == SubCell.FullCell);
506 		}
507 
GetAvailableSubCell(CPos a, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)508 		public SubCell GetAvailableSubCell(CPos a, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)
509 		{
510 			return Locomotor.GetAvailableSubCell(self, a, check, preferredSubCell, ignoreActor);
511 		}
512 
CanExistInCell(CPos cell)513 		public bool CanExistInCell(CPos cell)
514 		{
515 			return Locomotor.MovementCostForCell(cell) != short.MaxValue;
516 		}
517 
CanEnterCell(CPos cell, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)518 		public bool CanEnterCell(CPos cell, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All)
519 		{
520 			return Info.CanEnterCell(self.World, self, cell, ToSubCell, ignoreActor, check);
521 		}
522 
CanStayInCell(CPos cell)523 		public bool CanStayInCell(CPos cell)
524 		{
525 			return Info.CanStayInCell(self.World, cell);
526 		}
527 
528 		#endregion
529 
530 		#region Local IPositionable-related
531 
532 		// Sets only the location (fromCell, toCell, FromSubCell, ToSubCell)
SetLocation(CPos from, SubCell fromSub, CPos to, SubCell toSub)533 		public void SetLocation(CPos from, SubCell fromSub, CPos to, SubCell toSub)
534 		{
535 			if (FromCell == from && ToCell == to && FromSubCell == fromSub && ToSubCell == toSub)
536 				return;
537 
538 			RemoveInfluence();
539 			fromCell = from;
540 			toCell = to;
541 			FromSubCell = fromSub;
542 			ToSubCell = toSub;
543 			AddInfluence();
544 			IsBlocking = false;
545 
546 			// Most custom layer conditions are added/removed when starting the transition between layers.
547 			if (toCell.Layer != fromCell.Layer)
548 				foreach (var n in notifyCustomLayerChanged)
549 					n.CustomLayerChanged(self, fromCell.Layer, toCell.Layer);
550 		}
551 
FinishedMoving(Actor self)552 		public void FinishedMoving(Actor self)
553 		{
554 			// Need to check both fromCell and toCell because FinishedMoving is called multiple times during the move
555 			if (fromCell.Layer == toCell.Layer)
556 				foreach (var n in notifyFinishedMoving)
557 					n.FinishedMoving(self, fromCell.Layer, toCell.Layer);
558 
559 			// Only make actor crush if it is on the ground
560 			if (!self.IsAtGroundLevel())
561 				return;
562 
563 			var actors = self.World.ActorMap.GetActorsAt(ToCell, ToSubCell).Where(a => a != self).ToList();
564 			if (!AnyCrushables(actors))
565 				return;
566 
567 			var notifiers = actors.SelectMany(a => a.TraitsImplementing<INotifyCrushed>().Select(t => new TraitPair<INotifyCrushed>(a, t)));
568 			foreach (var notifyCrushed in notifiers)
569 				notifyCrushed.Trait.OnCrush(notifyCrushed.Actor, self, Info.LocomotorInfo.Crushes);
570 		}
571 
AnyCrushables(List<Actor> actors)572 		bool AnyCrushables(List<Actor> actors)
573 		{
574 			var crushables = actors.SelectMany(a => a.TraitsImplementing<ICrushable>().Select(t => new TraitPair<ICrushable>(a, t))).ToList();
575 			if (crushables.Count == 0)
576 				return false;
577 
578 			foreach (var crushes in crushables)
579 				if (crushes.Trait.CrushableBy(crushes.Actor, self, Info.LocomotorInfo.Crushes))
580 					return true;
581 
582 			return false;
583 		}
584 
AddInfluence()585 		public void AddInfluence()
586 		{
587 			if (self.IsInWorld)
588 				self.World.ActorMap.AddInfluence(self, this);
589 		}
590 
RemoveInfluence()591 		public void RemoveInfluence()
592 		{
593 			if (self.IsInWorld)
594 				self.World.ActorMap.RemoveInfluence(self, this);
595 		}
596 
597 		#endregion
598 
599 		#region IMove
600 
WrapMove(Activity inner)601 		Activity WrapMove(Activity inner)
602 		{
603 			var moveWrapper = moveWrappers.FirstOrDefault(Exts.IsTraitEnabled);
604 			if (moveWrapper != null)
605 				return moveWrapper.WrapMove(inner);
606 
607 			return inner;
608 		}
609 
MoveTo(CPos cell, int nearEnough = 0, Actor ignoreActor = null, bool evaluateNearestMovableCell = false, Color? targetLineColor = null)610 		public Activity MoveTo(CPos cell, int nearEnough = 0, Actor ignoreActor = null,
611 			bool evaluateNearestMovableCell = false, Color? targetLineColor = null)
612 		{
613 			return WrapMove(new Move(self, cell, WDist.FromCells(nearEnough), ignoreActor, evaluateNearestMovableCell, targetLineColor));
614 		}
615 
MoveWithinRange(Target target, WDist range, WPos? initialTargetPosition = null, Color? targetLineColor = null)616 		public Activity MoveWithinRange(Target target, WDist range,
617 			WPos? initialTargetPosition = null, Color? targetLineColor = null)
618 		{
619 			return WrapMove(new MoveWithinRange(self, target, WDist.Zero, range, initialTargetPosition, targetLineColor));
620 		}
621 
MoveWithinRange(Target target, WDist minRange, WDist maxRange, WPos? initialTargetPosition = null, Color? targetLineColor = null)622 		public Activity MoveWithinRange(Target target, WDist minRange, WDist maxRange,
623 			WPos? initialTargetPosition = null, Color? targetLineColor = null)
624 		{
625 			return WrapMove(new MoveWithinRange(self, target, minRange, maxRange, initialTargetPosition, targetLineColor));
626 		}
627 
MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange, WPos? initialTargetPosition = null, Color? targetLineColor = null)628 		public Activity MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange,
629 			WPos? initialTargetPosition = null, Color? targetLineColor = null)
630 		{
631 			return WrapMove(new Follow(self, target, minRange, maxRange, initialTargetPosition, targetLineColor));
632 		}
633 
ReturnToCell(Actor self)634 		public Activity ReturnToCell(Actor self)
635 		{
636 			return new ReturnToCellActivity(self);
637 		}
638 
639 		public class ReturnToCellActivity : Activity
640 		{
641 			readonly Mobile mobile;
642 			readonly bool recalculateSubCell;
643 
644 			CPos cell;
645 			SubCell subCell;
646 			WPos pos;
647 			int delay;
648 
ReturnToCellActivity(Actor self, int delay = 0, bool recalculateSubCell = false)649 			public ReturnToCellActivity(Actor self, int delay = 0, bool recalculateSubCell = false)
650 			{
651 				mobile = self.Trait<Mobile>();
652 				IsInterruptible = false;
653 				this.delay = delay;
654 				this.recalculateSubCell = recalculateSubCell;
655 			}
656 
OnFirstRun(Actor self)657 			protected override void OnFirstRun(Actor self)
658 			{
659 				pos = self.CenterPosition;
660 				if (self.World.Map.DistanceAboveTerrain(pos) > WDist.Zero && self.TraitOrDefault<Parachutable>() != null)
661 					QueueChild(new Parachute(self));
662 			}
663 
Tick(Actor self)664 			public override bool Tick(Actor self)
665 			{
666 				pos = self.CenterPosition;
667 				cell = mobile.ToCell;
668 				subCell = mobile.ToSubCell;
669 
670 				if (recalculateSubCell)
671 					subCell = mobile.Info.LocomotorInfo.SharesCell ? self.World.ActorMap.FreeSubCell(cell, subCell) : SubCell.FullCell;
672 
673 				// TODO: solve/reduce cell is full problem
674 				if (subCell == SubCell.Invalid)
675 					subCell = self.World.Map.Grid.DefaultSubCell;
676 
677 				// Reserve the exit cell
678 				mobile.SetPosition(self, cell, subCell);
679 				mobile.SetVisualPosition(self, pos);
680 
681 				if (delay > 0)
682 					QueueChild(new Wait(delay));
683 
684 				QueueChild(mobile.VisualMove(self, pos, self.World.Map.CenterOfSubCell(cell, subCell)));
685 				return true;
686 			}
687 
Cancel(Actor self, bool keepQueue = false)688 			public override void Cancel(Actor self, bool keepQueue = false)
689 			{
690 				// If we are forbidden from stopping in this cell, use evaluateNearestMovableCell
691 				// to nudge us to the nearest cell that we can stop in.
692 				if (!mobile.CanStayInCell(cell))
693 					QueueChild(new Move(self, cell, WDist.Zero, null, true));
694 
695 				base.Cancel(self, keepQueue);
696 			}
697 		}
698 
MoveToTarget(Actor self, Target target, WPos? initialTargetPosition = null, Color? targetLineColor = null)699 		public Activity MoveToTarget(Actor self, Target target,
700 			WPos? initialTargetPosition = null, Color? targetLineColor = null)
701 		{
702 			if (target.Type == TargetType.Invalid)
703 				return null;
704 
705 			return WrapMove(new MoveAdjacentTo(self, target, initialTargetPosition, targetLineColor));
706 		}
707 
MoveIntoTarget(Actor self, Target target)708 		public Activity MoveIntoTarget(Actor self, Target target)
709 		{
710 			if (target.Type == TargetType.Invalid)
711 				return null;
712 
713 			// Activity cancels if the target moves by more than half a cell
714 			// to avoid problems with the cell grid
715 			return WrapMove(new VisualMoveIntoTarget(self, target, new WDist(512)));
716 		}
717 
VisualMove(Actor self, WPos fromPos, WPos toPos)718 		public Activity VisualMove(Actor self, WPos fromPos, WPos toPos)
719 		{
720 			return WrapMove(VisualMove(self, fromPos, toPos, self.Location));
721 		}
722 
EstimatedMoveDuration(Actor self, WPos fromPos, WPos toPos)723 		public int EstimatedMoveDuration(Actor self, WPos fromPos, WPos toPos)
724 		{
725 			var speed = MovementSpeedForCell(self, self.Location);
726 			return speed > 0 ? (toPos - fromPos).Length / speed : 0;
727 		}
728 
NearestMoveableCell(CPos target)729 		public CPos NearestMoveableCell(CPos target)
730 		{
731 			// Limit search to a radius of 10 tiles
732 			return NearestMoveableCell(target, 1, 10);
733 		}
734 
CanEnterTargetNow(Actor self, Target target)735 		public bool CanEnterTargetNow(Actor self, Target target)
736 		{
737 			if (target.Type == TargetType.FrozenActor && !target.FrozenActor.IsValid)
738 				return false;
739 
740 			return self.Location == self.World.Map.CellContaining(target.CenterPosition) || Util.AdjacentCells(self.World, target).Any(c => c == self.Location);
741 		}
742 
743 		#endregion
744 
745 		#region Local IMove-related
746 
MovementSpeedForCell(Actor self, CPos cell)747 		public int MovementSpeedForCell(Actor self, CPos cell)
748 		{
749 			var index = cell.Layer == 0 ? self.World.Map.GetTerrainIndex(cell) :
750 				self.World.GetCustomMovementLayers()[cell.Layer].GetTerrainIndex(cell);
751 
752 			if (index == byte.MaxValue)
753 				return 0;
754 
755 			var terrainSpeed = Info.LocomotorInfo.TilesetTerrainInfo[self.World.Map.Rules.TileSet][index].Speed;
756 			if (terrainSpeed == 0)
757 				return 0;
758 
759 			var modifiers = speedModifiers.Value.Append(terrainSpeed);
760 
761 			return Util.ApplyPercentageModifiers(Info.Speed, modifiers);
762 		}
763 
NearestMoveableCell(CPos target, int minRange, int maxRange)764 		public CPos NearestMoveableCell(CPos target, int minRange, int maxRange)
765 		{
766 			// HACK: This entire method is a hack, and needs to be replaced with
767 			// a proper path search that can account for movement layer transitions.
768 			// HACK: Work around code that blindly tries to move to cells in invalid movement layers.
769 			// This will need to change (by removing this method completely as above) before we can
770 			// properly support user-issued orders on to elevated bridges or other interactable custom layers
771 			if (target.Layer != 0)
772 				target = new CPos(target.X, target.Y);
773 
774 			if (target == self.Location && CanStayInCell(target))
775 				return target;
776 
777 			if (CanEnterCell(target, check: BlockedByActor.Immovable) && CanStayInCell(target))
778 				return target;
779 
780 			foreach (var tile in self.World.Map.FindTilesInAnnulus(target, minRange, maxRange))
781 				if (CanEnterCell(tile, check: BlockedByActor.Immovable) && CanStayInCell(tile))
782 					return tile;
783 
784 			// Couldn't find a cell
785 			return target;
786 		}
787 
NearestCell(CPos target, Func<CPos, bool> check, int minRange, int maxRange)788 		public CPos NearestCell(CPos target, Func<CPos, bool> check, int minRange, int maxRange)
789 		{
790 			if (check(target))
791 				return target;
792 
793 			foreach (var tile in self.World.Map.FindTilesInAnnulus(target, minRange, maxRange))
794 				if (check(tile))
795 					return tile;
796 
797 			// Couldn't find a cell
798 			return target;
799 		}
800 
EnteringCell(Actor self)801 		public void EnteringCell(Actor self)
802 		{
803 			// Only make actor crush if it is on the ground
804 			if (!self.IsAtGroundLevel())
805 				return;
806 
807 			var actors = self.World.ActorMap.GetActorsAt(ToCell).Where(a => a != self).ToList();
808 			if (!AnyCrushables(actors))
809 				return;
810 
811 			var notifiers = actors.SelectMany(a => a.TraitsImplementing<INotifyCrushed>().Select(t => new TraitPair<INotifyCrushed>(a, t)));
812 			foreach (var notifyCrushed in notifiers)
813 				notifyCrushed.Trait.WarnCrush(notifyCrushed.Actor, self, Info.LocomotorInfo.Crushes);
814 		}
815 
ScriptedMove(CPos cell)816 		public Activity ScriptedMove(CPos cell) { return new Move(self, cell); }
MoveTo(Func<BlockedByActor, List<CPos>> pathFunc)817 		public Activity MoveTo(Func<BlockedByActor, List<CPos>> pathFunc) { return new Move(self, pathFunc); }
818 
VisualMove(Actor self, WPos fromPos, WPos toPos, CPos cell)819 		Activity VisualMove(Actor self, WPos fromPos, WPos toPos, CPos cell)
820 		{
821 			var speed = MovementSpeedForCell(self, cell);
822 			var length = speed > 0 ? (toPos - fromPos).Length / speed : 0;
823 
824 			var delta = toPos - fromPos;
825 			var facing = delta.HorizontalLengthSquared != 0 ? delta.Yaw.Facing : Facing;
826 
827 			return new Drag(self, fromPos, toPos, length, facing);
828 		}
829 
ClosestGroundCell()830 		CPos? ClosestGroundCell()
831 		{
832 			var above = new CPos(TopLeft.X, TopLeft.Y);
833 			if (CanEnterCell(above))
834 				return above;
835 
836 			var pathFinder = self.World.WorldActor.Trait<IPathFinder>();
837 			List<CPos> path;
838 			using (var search = PathSearch.Search(self.World, Locomotor, self, BlockedByActor.All,
839 					loc => loc.Layer == 0 && CanEnterCell(loc))
840 				.FromPoint(self.Location))
841 				path = pathFinder.FindPath(search);
842 
843 			if (path.Count > 0)
844 				return path[0];
845 
846 			return null;
847 		}
848 
849 		#endregion
850 
IActorPreviewInitModifier.ModifyActorPreviewInit(Actor self, TypeDictionary inits)851 		void IActorPreviewInitModifier.ModifyActorPreviewInit(Actor self, TypeDictionary inits)
852 		{
853 			if (!inits.Contains<DynamicFacingInit>() && !inits.Contains<FacingInit>())
854 				inits.Add(new DynamicFacingInit(() => facing));
855 		}
856 
IDeathActorInitModifier.ModifyDeathActorInit(Actor self, TypeDictionary init)857 		void IDeathActorInitModifier.ModifyDeathActorInit(Actor self, TypeDictionary init)
858 		{
859 			init.Add(new FacingInit(facing));
860 
861 			// Allows the husk to drag to its final position
862 			if (CanEnterCell(self.Location, self, BlockedByActor.Stationary))
863 				init.Add(new HuskSpeedInit(MovementSpeedForCell(self, self.Location)));
864 		}
865 
INotifyBecomingIdle.OnBecomingIdle(Actor self)866 		void INotifyBecomingIdle.OnBecomingIdle(Actor self)
867 		{
868 			if (self.Location.Layer == 0)
869 			{
870 				// Make sure that units aren't left idling in a transit-only cell
871 				// HACK: activities should be making sure that this can't happen in the first place!
872 				if (!Locomotor.CanStayInCell(self.Location))
873 					self.QueueActivity(MoveTo(self.Location, evaluateNearestMovableCell: true));
874 				return;
875 			}
876 
877 			var cml = self.World.WorldActor.TraitsImplementing<ICustomMovementLayer>()
878 				.First(l => l.Index == self.Location.Layer);
879 
880 			if (!cml.ReturnToGroundLayerOnIdle)
881 				return;
882 
883 			var moveTo = ClosestGroundCell();
884 			if (moveTo != null)
885 				self.QueueActivity(MoveTo(moveTo.Value, 0));
886 		}
887 
INotifyBlockingMove.OnNotifyBlockingMove(Actor self, Actor blocking)888 		void INotifyBlockingMove.OnNotifyBlockingMove(Actor self, Actor blocking)
889 		{
890 			if (!self.AppearsFriendlyTo(blocking))
891 				return;
892 
893 			if (self.IsIdle)
894 			{
895 				Nudge(blocking);
896 				return;
897 			}
898 
899 			IsBlocking = true;
900 		}
901 
GetVariableObservers()902 		public override IEnumerable<VariableObserver> GetVariableObservers()
903 		{
904 			foreach (var observer in base.GetVariableObservers())
905 				yield return observer;
906 
907 			if (Info.RequireForceMoveCondition != null)
908 				yield return new VariableObserver(RequireForceMoveConditionChanged, Info.RequireForceMoveCondition.Variables);
909 
910 			if (Info.ImmovableCondition != null)
911 				yield return new VariableObserver(ImmovableConditionChanged, Info.ImmovableCondition.Variables);
912 		}
913 
RequireForceMoveConditionChanged(Actor self, IReadOnlyDictionary<string, int> conditions)914 		void RequireForceMoveConditionChanged(Actor self, IReadOnlyDictionary<string, int> conditions)
915 		{
916 			requireForceMove = Info.RequireForceMoveCondition.Evaluate(conditions);
917 		}
918 
ImmovableConditionChanged(Actor self, IReadOnlyDictionary<string, int> conditions)919 		void ImmovableConditionChanged(Actor self, IReadOnlyDictionary<string, int> conditions)
920 		{
921 			var wasImmovable = IsImmovable;
922 			IsImmovable = Info.ImmovableCondition.Evaluate(conditions);
923 			if (wasImmovable != IsImmovable)
924 				self.World.ActorMap.UpdateOccupiedCells(self.OccupiesSpace);
925 		}
926 
927 		IEnumerable<IOrderTargeter> IIssueOrder.Orders
928 		{
929 			get
930 			{
931 				if (!IsTraitDisabled)
932 					yield return new MoveOrderTargeter(self, this);
933 			}
934 		}
935 
936 		// Note: Returns a valid order even if the unit can't move to the target
IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued)937 		Order IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued)
938 		{
939 			if (order is MoveOrderTargeter)
940 				return new Order("Move", self, target, queued);
941 
942 			return null;
943 		}
944 
IResolveOrder.ResolveOrder(Actor self, Order order)945 		void IResolveOrder.ResolveOrder(Actor self, Order order)
946 		{
947 			if (IsTraitDisabled)
948 				return;
949 
950 			if (order.OrderString == "Move")
951 			{
952 				var cell = self.World.Map.Clamp(this.self.World.Map.CellContaining(order.Target.CenterPosition));
953 				if (!Info.LocomotorInfo.MoveIntoShroud && !self.Owner.Shroud.IsExplored(cell))
954 					return;
955 
956 				self.QueueActivity(order.Queued, WrapMove(new Move(self, cell, WDist.FromCells(8), null, true, Color.Green)));
957 				self.ShowTargetLines();
958 			}
959 
960 			// TODO: This should only cancel activities queued by this trait
961 			else if (order.OrderString == "Stop")
962 				self.CancelActivity();
963 			else if (order.OrderString == "Scatter")
964 				Nudge(self);
965 		}
966 
IOrderVoice.VoicePhraseForOrder(Actor self, Order order)967 		string IOrderVoice.VoicePhraseForOrder(Actor self, Order order)
968 		{
969 			if (IsTraitDisabled)
970 				return null;
971 
972 			switch (order.OrderString)
973 			{
974 				case "Move":
975 					if (!Info.LocomotorInfo.MoveIntoShroud && order.Target.Type != TargetType.Invalid)
976 					{
977 						var cell = self.World.Map.CellContaining(order.Target.CenterPosition);
978 						if (!self.Owner.Shroud.IsExplored(cell))
979 							return null;
980 					}
981 
982 					return Info.Voice;
983 				case "Scatter":
984 				case "Stop":
985 					return Info.Voice;
986 				default:
987 					return null;
988 			}
989 		}
990 
ICreationActivity.GetCreationActivity()991 		Activity ICreationActivity.GetCreationActivity()
992 		{
993 			return returnToCellOnCreation ? new ReturnToCellActivity(self, creationActivityDelay, returnToCellOnCreationRecalculateSubCell) : null;
994 		}
995 
996 		class MoveOrderTargeter : IOrderTargeter
997 		{
998 			readonly Mobile mobile;
999 			readonly LocomotorInfo locomotorInfo;
1000 			readonly bool rejectMove;
TargetOverridesSelection(Actor self, Target target, List<Actor> actorsAt, CPos xy, TargetModifiers modifiers)1001 			public bool TargetOverridesSelection(Actor self, Target target, List<Actor> actorsAt, CPos xy, TargetModifiers modifiers)
1002 			{
1003 				// Always prioritise orders over selecting other peoples actors or own actors that are already selected
1004 				if (target.Type == TargetType.Actor && (target.Actor.Owner != self.Owner || self.World.Selection.Contains(target.Actor)))
1005 					return true;
1006 
1007 				return modifiers.HasModifier(TargetModifiers.ForceMove);
1008 			}
1009 
MoveOrderTargeter(Actor self, Mobile unit)1010 			public MoveOrderTargeter(Actor self, Mobile unit)
1011 			{
1012 				mobile = unit;
1013 				locomotorInfo = mobile.Info.LocomotorInfo;
1014 				rejectMove = !self.AcceptsOrder("Move");
1015 			}
1016 
1017 			public string OrderID { get { return "Move"; } }
1018 			public int OrderPriority { get { return 4; } }
1019 			public bool IsQueued { get; protected set; }
1020 
CanTarget(Actor self, Target target, List<Actor> othersAtTarget, ref TargetModifiers modifiers, ref string cursor)1021 			public bool CanTarget(Actor self, Target target, List<Actor> othersAtTarget, ref TargetModifiers modifiers, ref string cursor)
1022 			{
1023 				if (rejectMove || target.Type != TargetType.Terrain || (mobile.requireForceMove && !modifiers.HasModifier(TargetModifiers.ForceMove)))
1024 					return false;
1025 
1026 				var location = self.World.Map.CellContaining(target.CenterPosition);
1027 				IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue);
1028 
1029 				var explored = self.Owner.Shroud.IsExplored(location);
1030 				cursor = self.World.Map.Contains(location) ?
1031 					(self.World.Map.GetTerrainInfo(location).CustomCursor ?? mobile.Info.Cursor) : mobile.Info.BlockedCursor;
1032 
1033 				if (mobile.IsTraitPaused
1034 					|| (!explored && !locomotorInfo.MoveIntoShroud)
1035 					|| (explored && mobile.Locomotor.MovementCostForCell(location) == short.MaxValue))
1036 					cursor = mobile.Info.BlockedCursor;
1037 
1038 				return true;
1039 			}
1040 		}
1041 	}
1042 }
1043