public AttackActivity(Actor self, Target target, bool allowMove, bool forceAttack, Color?targetLineColor = null) { attack = self.Trait <AttackFollow>(); move = allowMove ? self.TraitOrDefault <IMove>() : null; revealsShroud = self.TraitsImplementing <RevealsShroud>().ToArray(); this.target = target; this.forceAttack = forceAttack; this.targetLineColor = targetLineColor; // The target may become hidden between the initial order request and the first tick (e.g. if queued) // Moving to any position (even if quite stale) is still better than immediately giving up if ((target.Type == TargetType.Actor && target.Actor.CanBeViewedByPlayer(self.Owner)) || target.Type == TargetType.FrozenActor || target.Type == TargetType.Terrain) { lastVisibleTarget = Target.FromPos(target.CenterPosition); lastVisibleMaximumRange = attack.GetMaximumRangeVersusTarget(target); lastVisibleMinimumRange = attack.GetMinimumRangeVersusTarget(target); if (target.Type == TargetType.Actor) { lastVisibleOwner = target.Actor.Owner; lastVisibleTargetTypes = target.Actor.GetEnabledTargetTypes(); } else if (target.Type == TargetType.FrozenActor) { lastVisibleOwner = target.FrozenActor.Owner; lastVisibleTargetTypes = target.FrozenActor.TargetTypes; } } }
public override bool Tick(Actor self) { if (IsCanceling) { return(true); } // Check that AttackFollow hasn't cancelled the target by modifying attack.Target // Having both this and AttackFollow modify that field is a horrible hack. if (hasTicked && attack.RequestedTarget.Type == TargetType.Invalid) { return(true); } if (attack.IsTraitPaused) { return(false); } target = target.Recalculate(self.Owner, out var targetIsHiddenActor); attack.SetRequestedTarget(self, target, forceAttack); hasTicked = true; if (!targetIsHiddenActor && target.Type == TargetType.Actor) { lastVisibleTarget = Target.FromTargetPositions(target); lastVisibleMaximumRange = attack.GetMaximumRangeVersusTarget(target); lastVisibleMinimumRange = attack.GetMinimumRange(); lastVisibleOwner = target.Actor.Owner; lastVisibleTargetTypes = target.Actor.GetEnabledTargetTypes(); // Try and sit at least one cell away from the min or max ranges to give some leeway if the target starts moving. if (move != null && target.Actor.Info.HasTraitInfo <IMoveInfo>()) { var preferMinRange = Math.Min(lastVisibleMinimumRange.Length + 1024, lastVisibleMaximumRange.Length); var preferMaxRange = Math.Max(lastVisibleMaximumRange.Length - 1024, lastVisibleMinimumRange.Length); lastVisibleMaximumRange = new WDist((lastVisibleMaximumRange.Length - 1024).Clamp(preferMinRange, preferMaxRange)); } } // The target may become hidden in the same tick the AttackActivity constructor is called, // causing lastVisible* to remain uninitialized. // Fix the fallback values based on the frozen actor properties else if (target.Type == TargetType.FrozenActor && !lastVisibleTarget.IsValidFor(self)) { lastVisibleTarget = Target.FromTargetPositions(target); lastVisibleMaximumRange = attack.GetMaximumRangeVersusTarget(target); lastVisibleOwner = target.FrozenActor.Owner; lastVisibleTargetTypes = target.FrozenActor.TargetTypes; } var maxRange = lastVisibleMaximumRange; var minRange = lastVisibleMinimumRange; useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self); // Most actors want to be able to see their target before shooting if (target.Type == TargetType.FrozenActor && !attack.Info.TargetFrozenActors && !forceAttack) { var rs = revealsShroud .Where(Exts.IsTraitEnabled) .MaxByOrDefault(s => s.Range); // Default to 2 cells if there are no active traits var sightRange = rs != null ? rs.Range : WDist.FromCells(2); if (sightRange < maxRange) { maxRange = sightRange; } } // If we are ticking again after previously sequencing a MoveWithRange then that move must have completed // Either we are in range and can see the target, or we've lost track of it and should give up if (wasMovingWithinRange && targetIsHiddenActor) { return(true); } // Target is hidden or dead, and we don't have a fallback position to move towards if (useLastVisibleTarget && !lastVisibleTarget.IsValidFor(self)) { return(true); } var pos = self.CenterPosition; var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target; // We've reached the required range - if the target is visible and valid then we wait // otherwise if it is hidden or dead we give up if (checkTarget.IsInRange(pos, maxRange) && !checkTarget.IsInRange(pos, minRange)) { if (useLastVisibleTarget) { return(true); } return(false); } // We can't move into range, so give up if (move == null || maxRange == WDist.Zero || maxRange < minRange) { return(true); } wasMovingWithinRange = true; QueueChild(move.MoveWithinRange(target, minRange, maxRange, checkTarget.CenterPosition)); return(false); }
public override Activity Tick(Actor self) { if (ChildActivity != null) { ChildActivity = ActivityUtils.RunActivity(self, ChildActivity); if (ChildActivity != null) { return(this); } } if (IsCanceling) { // Cancel the requested target, but keep firing on it while in range if (attack.Info.PersistentTargeting) { attack.OpportunityTarget = attack.RequestedTarget; attack.OpportunityForceAttack = attack.RequestedForceAttack; attack.OpportunityTargetIsPersistentTarget = true; } attack.RequestedTarget = Target.Invalid; return(NextActivity); } // Check that AttackFollow hasn't cancelled the target by modifying attack.Target // Having both this and AttackFollow modify that field is a horrible hack. if (hasTicked && attack.RequestedTarget.Type == TargetType.Invalid) { return(NextActivity); } if (attack.IsTraitPaused) { return(this); } bool targetIsHiddenActor; attack.RequestedForceAttack = forceAttack; attack.RequestedTarget = target = target.Recalculate(self.Owner, out targetIsHiddenActor); attack.RequestedTargetLastTick = self.World.WorldTick; hasTicked = true; if (!targetIsHiddenActor && target.Type == TargetType.Actor) { lastVisibleTarget = Target.FromTargetPositions(target); lastVisibleMaximumRange = attack.GetMaximumRangeVersusTarget(target); lastVisibleMinimumRange = attack.GetMinimumRange(); lastVisibleOwner = target.Actor.Owner; lastVisibleTargetTypes = target.Actor.GetEnabledTargetTypes(); // Try and sit at least one cell away from the min or max ranges to give some leeway if the target starts moving. if (move != null && target.Actor.Info.HasTraitInfo <IMoveInfo>()) { var preferMinRange = Math.Min(lastVisibleMinimumRange.Length + 1024, lastVisibleMaximumRange.Length); var preferMaxRange = Math.Max(lastVisibleMaximumRange.Length - 1024, lastVisibleMinimumRange.Length); lastVisibleMaximumRange = new WDist((lastVisibleMaximumRange.Length - 1024).Clamp(preferMinRange, preferMaxRange)); } } var oldUseLastVisibleTarget = useLastVisibleTarget; var maxRange = lastVisibleMaximumRange; var minRange = lastVisibleMinimumRange; useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self); // Most actors want to be able to see their target before shooting if (target.Type == TargetType.FrozenActor && !attack.Info.TargetFrozenActors && !forceAttack) { var rs = revealsShroud .Where(Exts.IsTraitEnabled) .MaxByOrDefault(s => s.Range); // Default to 2 cells if there are no active traits var sightRange = rs != null ? rs.Range : WDist.FromCells(2); if (sightRange < maxRange) { maxRange = sightRange; } } // If we are ticking again after previously sequencing a MoveWithRange then that move must have completed // Either we are in range and can see the target, or we've lost track of it and should give up if (wasMovingWithinRange && targetIsHiddenActor) { attack.RequestedTarget = Target.Invalid; return(NextActivity); } // Update target lines if required if (useLastVisibleTarget != oldUseLastVisibleTarget) { self.SetTargetLine(useLastVisibleTarget ? lastVisibleTarget : target, Color.Red, false); } // Target is hidden or dead, and we don't have a fallback position to move towards if (useLastVisibleTarget && !lastVisibleTarget.IsValidFor(self)) { attack.RequestedTarget = Target.Invalid; return(NextActivity); } var pos = self.CenterPosition; var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target; // We've reached the required range - if the target is visible and valid then we wait // otherwise if it is hidden or dead we give up if (checkTarget.IsInRange(pos, maxRange) && !checkTarget.IsInRange(pos, minRange)) { if (useLastVisibleTarget) { attack.RequestedTarget = Target.Invalid; return(NextActivity); } return(this); } // We can't move into range, so give up if (move == null || maxRange == WDist.Zero || maxRange < minRange) { attack.RequestedTarget = Target.Invalid; return(NextActivity); } wasMovingWithinRange = true; QueueChild(self, move.MoveWithinRange(target, minRange, maxRange, checkTarget.CenterPosition, Color.Red), true); return(this); }