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