Compare commits

...

118 Commits

Author SHA1 Message Date
Paul Chote
77b91420a9 Work around harvester unloading activity queue issues. 2019-03-02 18:25:47 +01:00
Paul Chote
cdf74cf6d6 Fix maximum range estimation for stationary turrets. 2019-03-02 18:19:56 +01:00
Orb
d5b0a989d4 MGG Speed Reduction 2019-03-02 17:41:14 +01:00
Orb
04c51fce07 Balance Changes for Next Release by Orb 2019-03-02 17:40:55 +01:00
abcdefg30
c41d2aee55 Fix a crash in soviet05 2019-03-01 22:27:25 +00:00
reaperrr
4944f0524f Increase dog attack damage
To make sure it kills veteran Tanya as well as Drop Zone shok troopers.
2019-03-01 15:27:34 +01:00
reaperrr
33006e6ba5 Fix Drop Zone maps
Fixes Lint errors, Bomb truck gfx & lobby options.
2019-03-01 15:27:12 +01:00
Paul Chote
ceff7ae207 Remove ChildActivity from LeapAttack.
Temporary workaround for cancellation issues when
child activities use the Move activity.
2019-02-24 13:23:40 +01:00
netnazgul
f0156083a3 Fix Pitfight: split stacked ore mine actors to different cells 2019-02-24 13:12:37 +01:00
Paul Chote
3a94c74c44 Remove flawed RateLimit implementation. 2019-02-23 18:06:17 +00:00
Punsho
e6a799bdb0 Update fakes.yaml 2019-02-22 21:32:14 +00:00
SoScared
e3b49bc02c Alter default bot and faction dropdownhights for the RA mod. 2019-02-22 21:27:02 +00:00
Paul Chote
29ce5f183c Avoid resetting FrozenActor.Hidden when refreshing GPS. 2019-02-22 20:56:38 +01:00
Paul Chote
091a38937a Reuse GPSDotEffect for the lifetime of the actor. 2019-02-22 20:43:49 +01:00
Paul Chote
bfea2b3b87 Fix editor crash when modifying newly placed actor ID. 2019-02-21 16:27:33 +01:00
Oliver Brakmann
8ca8d32232 Fix pre-placed frozen actors not being targetable 2019-02-20 16:31:29 +00:00
Paul Chote
d3c5aa53be Cache passenger bounty traits.
This avoids querying from potentially dead actors.
2019-02-09 19:47:14 +01:00
Paul Chote
adfb905f97 Abort activities when we don't know how close to move to a target. 2019-02-09 19:20:38 +01:00
portablestew
c2ead21bf0 Fix for #7083: Fly stops turning when target is inside the turn radius 2019-02-08 19:37:44 +01:00
Paul Chote
aaaa5c78de Defer UpdateFrozenActor until the end of the tick.
Updating the frozen actor calls Actor.GetTargetablePositions,
and so we must guarantee that Created has been called for
the ITargetablePositions traits first.
2019-02-07 19:51:11 +00:00
Oliver Brakmann
b5105e5e50 Fix unresponsive aircraft when executing orders queued during resupply 2019-02-06 18:02:20 +00:00
reaperrr
b18110012b Normalize RA tracks 2019-02-03 20:47:47 +01:00
reaperrr
38bf3eb30d Normalize TD track volumes and fix order
Some tracks from the original were falsely listed under CovOps.
2019-02-03 20:47:45 +01:00
abcdefg30
7ad4ba7adb Let the extraction helicopter of Monster Tank Madness leave 2019-02-03 19:33:11 +00:00
Paul Chote
9d624579fc Fix inconsistent FrozenActor state on capture/destruction. 2019-02-03 20:28:49 +01:00
Paul Chote
46f0978789 Remove obsolete LegacyEnter and ResolveFrozenActorOrder. 2019-02-03 20:28:25 +01:00
Paul Chote
880f635e83 Remove ResolveFrozenActorOrder from MadTank. 2019-02-03 20:23:59 +01:00
Paul Chote
3bfb43a305 Remove ResolveFrozenActorOrder from EntersTunnels. 2019-02-03 20:23:59 +01:00
Paul Chote
4ca4aebf50 Remove ResolveFrozenActorOrder from Carryall. 2019-02-03 20:23:59 +01:00
Paul Chote
8df18da419 Port EnterTransport to the new Enter activity.
This dramatically simplifies the reservation logic,
which seemed to be needlessly complicated. This may
regress unexpected edge-cases.
2019-02-03 20:23:59 +01:00
Paul Chote
98b4d6e828 Port DonateExperience to the new Enter activity. 2019-02-03 20:23:59 +01:00
Paul Chote
5f1d4a4ff1 Port DonateCash to the new Enter activity. 2019-02-03 20:23:59 +01:00
Paul Chote
dc722b0a0c Port RepairBridge to the new Enter activity. 2019-02-03 20:23:59 +01:00
Paul Chote
142e6776af Port RepairBuilding to the new Enter activity. 2019-02-03 20:23:59 +01:00
Paul Chote
baa786a4fd Port Demolish to the new Enter activity. 2019-02-03 20:23:59 +01:00
Paul Chote
75ccdfce7a Port Infiltrate to the new Enter activity. 2019-02-03 20:23:59 +01:00
Paul Chote
1672041dd4 Port CaptureActor to the new Enter activity. 2019-02-03 20:23:59 +01:00
Paul Chote
f63bd278e1 Rewrite Enter activity, accounting for frozen and hidden actors. 2019-02-03 20:23:59 +01:00
Paul Chote
0a2731fc72 Rewrite Mobile.MoveIntoTarget to support moving targets. 2019-02-03 20:23:59 +01:00
Paul Chote
c304a0682e Rename Enter to LegacyEnter. 2019-02-03 20:23:59 +01:00
Paul Chote
1372b5dd85 Pause actor movement between cells when Mobile is disabled. 2019-02-03 20:23:59 +01:00
Paul Chote
1f861ce35c Fix turn disabling mobile.IsMoving when setIsMoving is false. 2019-02-03 20:23:59 +01:00
Paul Chote
fce4afd448 Fix Positions returned by FrozenActor targets.
Also removes redundant Targetables check from actors.
2019-02-03 20:23:59 +01:00
reaperrr
210de9440c Fix D2k bots wasting cash on building repairs
D2k bots not repairing buildings when damaged due to placement
without concrete was intentional, and this was bleed's default behavior
before BuildingRepairBotModule got introduced, too.
2019-02-03 18:25:03 +01:00
Paul Chote
d3f1f39635 Disable OpportunityFire on Disruptors.
The original game defined NoMovingFire=true.
2019-02-03 18:12:04 +01:00
Paul Chote
789676919c Allow turreted actors to acquire targets while doing other activities. 2019-02-03 18:11:54 +01:00
Paul Chote
9cafc1d7b4 Implement a secondary target-of-opportunity for AttackFollow. 2019-02-03 18:11:47 +01:00
Smittytron
76497eaea6 Add e6 and thf to Monster Tank Madness 2019-02-03 17:10:17 +01:00
Paul Chote
93cef0241d Fix double-revoke crash in Demolishable. 2019-02-03 16:48:26 +01:00
Paul Chote
a641ac08f8 Prefer target to lastVisible target if it is visible. 2019-02-03 16:34:37 +01:00
matjaeck
2d0ee684b8 Reset unit stance on owner change. 2019-02-03 16:26:25 +01:00
reaperrr
f83d680197 Fix that bots don't re-use capturers
They were never removed from activeCapturers when their target becomes invalid,
preventing the bot from reusing them.
2019-02-03 12:13:09 +00:00
reaperrr
b54e45f4a4 Fix CaptureManagerBotModule crashing on multiple Capturable
By removing the now-redundant CaptureTarget class.
2019-02-03 12:13:04 +00:00
Oliver Brakmann
55b8f42ac5 Consider dead aircraft to no longer be in range 2019-02-03 11:31:55 +00:00
tovl
8f16a792cc add check for NextActivity to FlyCircle
prevent infinite loop

fix for ReturnToBase

cleanup
2019-02-03 11:24:42 +00:00
Paul Chote
66282cbd87 Add VolumeModifier support to music. 2019-02-02 22:29:15 +01:00
Paul Chote
96ce888d97 Remove AttackSuicides trait. 2019-02-01 23:19:49 +00:00
Paul Chote
0c7a1ec3bf Implement Hunter-Seeker kill behaviour. 2019-02-01 23:19:20 +00:00
Paul Chote
5c8aa7f0f7 Replace AttackSuicides with AttackFrontal and conditions in RA. 2019-02-01 23:19:17 +00:00
Paul Chote
45fa0dc41e Remove unused negativeDamage variable from AttackBase. 2019-02-01 23:19:13 +00:00
Smittytron
3cb807329a Reduce thief speed and engineer CaptureDelay 2019-02-01 22:43:39 +01:00
Paul Chote
6215e4358a Enable "campaign" bot for all scripted players in D2k missions. 2019-02-01 21:01:00 +01:00
Paul Chote
caddb39e79 Enable "campaign" bot for all scripted players in TD missions. 2019-02-01 21:00:45 +01:00
Paul Chote
fff1d0bfa9 Enable "campaign" bot for all scripted players in RA missions. 2019-02-01 21:00:28 +01:00
Paul Chote
f26a7d7a3f Amend Force Attack command bar tooltip. 2019-01-28 00:22:24 +01:00
Paul Chote
b9d1a83158 Enable ForceFireIgnoresActors on artillery-style units. 2019-01-28 00:22:11 +01:00
Paul Chote
b568023489 Add ForceFireIgnoresActors to AttackBase. 2019-01-28 00:21:59 +01:00
Paul Chote
f6bc07894b Fix gate animations. 2019-01-27 21:38:42 +01:00
Paul Chote
da451896e8 Fix FrozenUnderFog / FrozenActor visibility consistency.
This fixes cases where both objects return visible / not
when queried at the wrong time during a tick.
2019-01-27 15:27:12 +01:00
Paul Chote
7d26e78cb3 Allow attack orders to preempt move completion for turreted units. 2019-01-27 15:26:48 +01:00
Paul Chote
4c28776a93 Reduce cash tick volume in the default mods. 2019-01-27 15:09:56 +01:00
Paul Chote
6a8f4301fb Add VolumeModifier support to sound definitions. 2019-01-27 15:09:40 +01:00
Paul Chote
6ddbab929c Add AttackMove workaround for Aircraft. 2019-01-27 00:20:06 +00:00
Paul Chote
42779c72cf Fix target invalidation and reacquisition in fly activities. 2019-01-26 22:56:22 +00:00
Paul Chote
35acae244c Fix target invalidation and reacquisition in (Fly|Heli)Attack. 2019-01-26 22:56:22 +00:00
Paul Chote
965de05b0a Fix target invalidation and reacquisition in AttackFollow. 2019-01-26 22:56:22 +00:00
Paul Chote
75f5076ac3 Fix target invalidation and reacquisition in LeapAttack. 2019-01-26 22:56:22 +00:00
Paul Chote
70cddb72f2 Fix target invalidation and reacquisition in Attack. 2019-01-26 22:56:22 +00:00
Paul Chote
19da3bc4c2 Fix target invalidation and reacquisition in Follow. 2019-01-26 22:56:22 +00:00
Paul Chote
f950a61886 Fix target invalidation and reacquisition in MoveAdjacentTo. 2019-01-26 22:56:22 +00:00
Paul Chote
d3fbb83877 Split Target.Recalculate into methods with and without invalidation.
TargetExtensions is moved into its own file.
2019-01-26 22:56:22 +00:00
Paul Chote
8e85431266 Define plumbing to pass initial target positions to inner move activities. 2019-01-26 22:56:22 +00:00
Paul Chote
ecd4d996c7 Pass target line color to inner move activities. 2019-01-26 22:56:22 +00:00
Paul Chote
378fecd2db Add support for Terrain targets with multiple positions. 2019-01-26 22:56:22 +00:00
Paul Chote
67d3010d78 Make Target fields readonly. 2019-01-26 22:56:22 +00:00
Paul Chote
eee71368a8 Fix self parameter name in DrawLineToTarget. 2019-01-26 22:56:21 +00:00
Paul Chote
527b4da3f2 Remove FrameEndTask from DrawLineToTarget.
This is no longer needed and causes ordering
issues when the unit becomes idle in the same
tick that SetTargetLine is called.
2019-01-26 22:56:21 +00:00
Paul Chote
7ae2822b6e Fix source package creation. 2019-01-26 23:08:25 +01:00
Paul Chote
8fdddd238b Map Ctrl to Cmd for editor copy hotkey. 2019-01-26 21:48:53 +00:00
rob-v
8dfcc716c8 Add shortcut for Copy in Map editor 2019-01-26 21:48:47 +00:00
Paul Chote
892b60d0eb Deselect actors when their owner changes. 2019-01-26 21:40:48 +00:00
Paul Chote
987f2a26fa Cache world INotifySelection traits in Selection. 2019-01-26 21:40:43 +00:00
Paul Chote
cc2912b993 Only play queued notification if queue is empty.
This matches the behaviour of the RA2 sidebar.
2019-01-26 21:37:03 +00:00
Paul Chote
76b15d7e37 Replace broken (Disabled)TabClick and with (Disabled)ClickSound. 2019-01-26 21:36:58 +00:00
Paul Chote
1a0b975d31 Disable bot logic during replays. 2019-01-26 21:34:56 +00:00
abcdefg30
b55a1b86db Allies03b: Fix the insertion helicopter revealing shroud 2019-01-26 21:30:12 +00:00
abcdefg30
7840f92f39 Allies03: Remove hacke6 2019-01-26 21:30:08 +00:00
abcdefg30
c13a0e2c9e Allies03b: Fix heavy tank reinforcements triggering twice 2019-01-26 21:30:04 +00:00
abcdefg30
edd92c7d3b Allies01: Remove redundance (caused by inheritance) 2019-01-26 21:30:00 +00:00
abcdefg30
beb9cae9ca Allies01: Add a new line at the end of the briefing 2019-01-26 21:29:56 +00:00
abcdefg30
1ecb921364 Allies01: Fix civilian infantry being visible below fog 2019-01-26 21:29:52 +00:00
abcdefg30
ffc7314a7f Allies01: Fix the extraction helicopter revealing shroud 2019-01-26 21:29:48 +00:00
abcdefg30
a4420bad23 Allies01: Fix the extraction helicopter landing before being removed 2019-01-26 21:29:44 +00:00
abcdefg30
b8c90ef506 Fix OnAllRemovedFromWorld only triggering once 2019-01-22 23:09:36 +00:00
Paul Chote
f30b659b3f Remove Game.Debug messages from ValidateOrder. 2019-01-22 22:59:51 +00:00
rob-v
35ee5b807a No player name in replay for chat commands 2019-01-22 22:57:21 +00:00
abcdefg30
7f21a868bd Fix LeapAttack setting attack.IsAiming too early 2019-01-22 22:54:05 +00:00
abcdefg30
1eb8c395e7 Reset the client state when being moved to spectator 2019-01-22 22:48:05 +00:00
abcdefg30
d2a8a8f19a Only consider system maps in the mission browser 2019-01-19 18:48:04 +00:00
abcdefg30
b826c5e22a Use map folder names instead of paths in the mission browser 2019-01-19 18:47:58 +00:00
rob-v
65f5b763e9 Fix Warhead.IsValidAgainst (FrozenActor.Owner null) 2019-01-19 12:03:25 +00:00
abcdefg30
b167424654 Move Actor103 in Allies02 one cell to the right
He was standing on impassable terrain and therefore couldn't move.
2019-01-19 11:56:41 +00:00
abcdefg30
0777fe534d Let the remaing enemy troops in Allies02 attack the player
once the base is destroyed.
Does not include unit guarding the convoy path.
2019-01-19 11:56:36 +00:00
Smittytron
c3a8065b99 Add IsDead check to fix crash in Sarin Gas 1 2019-01-19 11:45:46 +00:00
abcdefg30
59dcab4db0 Fix a crash in Infiltration 2019-01-19 11:27:28 +00:00
Smittytron
4631cbfc2a Change tree husks from FrozenUnderFog to HiddenUnderShroud 2019-01-19 11:17:05 +00:00
Smittytron
315f1188ed Change LST turn speed to default max 2019-01-07 12:49:16 +01:00
Mustafa Alperen Seki
eae3f297f3 Hide husks under fog regardless of their owner. 2019-01-06 22:11:56 +01:00
248 changed files with 2555 additions and 1429 deletions

View File

@@ -336,6 +336,8 @@ namespace OpenRA
foreach (var t in TraitsImplementing<INotifyOwnerChanged>())
t.OnOwnerChanged(this, oldOwner, newOwner);
World.Selection.OnOwnerChanged(this, oldOwner, newOwner);
if (wasInWorld)
World.Add(this);
}

View File

@@ -19,6 +19,7 @@ namespace OpenRA.GameRules
public readonly string Filename;
public readonly string Title;
public readonly bool Hidden;
public readonly float VolumeModifier = 1f;
public int Length { get; private set; } // seconds
public bool Exists { get; private set; }
@@ -31,6 +32,9 @@ namespace OpenRA.GameRules
if (nd.ContainsKey("Hidden"))
bool.TryParse(nd["Hidden"].Value, out Hidden);
if (nd.ContainsKey("VolumeModifier"))
VolumeModifier = FieldLoader.GetValue<float>("VolumeModifier", nd["VolumeModifier"].Value);
var ext = nd.ContainsKey("Extension") ? nd["Extension"].Value : "aud";
Filename = (nd.ContainsKey("Filename") ? nd["Filename"].Value : key) + "." + ext;
}

View File

@@ -33,7 +33,7 @@ namespace OpenRA.GameRules
{
FieldLoader.Load(this, y);
VoicePools = Exts.Lazy(() => Voices.ToDictionary(a => a.Key, a => new SoundPool(0, a.Value)));
VoicePools = Exts.Lazy(() => Voices.ToDictionary(a => a.Key, a => new SoundPool(1f, a.Value)));
NotificationsPools = Exts.Lazy(() => ParseSoundPool(y, "Notifications"));
}
@@ -43,13 +43,13 @@ namespace OpenRA.GameRules
var classifiction = y.Nodes.First(x => x.Key == key);
foreach (var t in classifiction.Value.Nodes)
{
var rateLimit = 0;
var rateLimitNode = t.Value.Nodes.FirstOrDefault(x => x.Key == "RateLimit");
if (rateLimitNode != null)
rateLimit = FieldLoader.GetValue<int>(rateLimitNode.Key, rateLimitNode.Value.Value);
var volumeModifier = 1f;
var volumeModifierNode = t.Value.Nodes.FirstOrDefault(x => x.Key == "VolumeModifier");
if (volumeModifierNode != null)
volumeModifier = FieldLoader.GetValue<float>(volumeModifierNode.Key, volumeModifierNode.Value.Value);
var names = FieldLoader.GetValue<string[]>(t.Key, t.Value.Value);
var sp = new SoundPool(rateLimit, names);
var sp = new SoundPool(volumeModifier, names);
ret.Add(t.Key, sp);
}
@@ -59,15 +59,14 @@ namespace OpenRA.GameRules
public class SoundPool
{
public readonly float VolumeModifier;
readonly string[] clips;
readonly int rateLimit;
readonly List<string> liveclips = new List<string>();
long lastPlayed = 0;
public SoundPool(int rateLimit, params string[] clips)
public SoundPool(float volumeModifier, params string[] clips)
{
VolumeModifier = volumeModifier;
this.clips = clips;
this.rateLimit = rateLimit;
}
public string GetNext()
@@ -79,16 +78,6 @@ namespace OpenRA.GameRules
if (liveclips.Count == 0)
return null;
// Perform rate limiting if necessary
if (rateLimit != 0)
{
var now = Game.RunTime;
if (lastPlayed != 0 && now < lastPlayed + rateLimit)
return null;
lastPlayed = now;
}
var i = Game.CosmeticRandom.Next(liveclips.Count);
var s = liveclips[i];
liveclips.RemoveAt(i);

View File

@@ -21,7 +21,14 @@ namespace OpenRA
{
public int Hash { get; private set; }
public IEnumerable<Actor> Actors { get { return actors; } }
readonly HashSet<Actor> actors = new HashSet<Actor>();
readonly INotifySelection[] worldNotifySelection;
internal Selection(IEnumerable<INotifySelection> worldNotifySelection)
{
this.worldNotifySelection = worldNotifySelection.ToArray();
}
void UpdateHash()
{
@@ -31,17 +38,41 @@ namespace OpenRA
Hash += 1;
}
public void Add(World w, Actor a)
public void Add(Actor a)
{
actors.Add(a);
UpdateHash();
foreach (var sel in a.TraitsImplementing<INotifySelected>())
sel.Selected(a);
foreach (var ns in w.WorldActor.TraitsImplementing<INotifySelection>())
foreach (var ns in worldNotifySelection)
ns.SelectionChanged();
}
public void Remove(Actor a)
{
if (actors.Remove(a))
{
UpdateHash();
foreach (var ns in worldNotifySelection)
ns.SelectionChanged();
}
}
internal void OnOwnerChanged(Actor a, Player oldOwner, Player newOwner)
{
if (!actors.Contains(a))
return;
// Remove the actor from the original owners selection
// Call UpdateHash directly for everyone else so watchers can account for the owner change if needed
if (oldOwner == a.World.LocalPlayer)
Remove(a);
else
UpdateHash();
}
public bool Contains(Actor a)
{
return actors.Contains(a);
@@ -77,7 +108,7 @@ namespace OpenRA
foreach (var sel in a.TraitsImplementing<INotifySelected>())
sel.Selected(a);
foreach (var ns in world.WorldActor.TraitsImplementing<INotifySelection>())
foreach (var ns in worldNotifySelection)
ns.SelectionChanged();
if (world.IsGameOver)

View File

@@ -214,7 +214,7 @@ namespace OpenRA
Func<ISoundFormat, ISound> stream = soundFormat => soundEngine.Play2DStream(
soundFormat.GetPCMInputStream(), soundFormat.Channels, soundFormat.SampleBits, soundFormat.SampleRate,
false, true, WPos.Zero, MusicVolume);
false, true, WPos.Zero, MusicVolume * m.VolumeModifier);
music = LoadSound(m.Filename, stream);
if (music == null)
@@ -352,7 +352,7 @@ namespace OpenRA
var id = voicedActor != null ? voicedActor.ActorID : 0;
string clip;
SoundPool pool;
var suffix = rules.DefaultVariant;
var prefix = rules.DefaultPrefix;
@@ -361,16 +361,17 @@ namespace OpenRA
if (!rules.VoicePools.Value.ContainsKey(definition))
throw new InvalidOperationException("Can't find {0} in voice pool.".F(definition));
clip = rules.VoicePools.Value[definition].GetNext();
pool = rules.VoicePools.Value[definition];
}
else
{
if (!rules.NotificationsPools.Value.ContainsKey(definition))
throw new InvalidOperationException("Can't find {0} in notification pool.".F(definition));
clip = rules.NotificationsPools.Value[definition].GetNext();
pool = rules.NotificationsPools.Value[definition];
}
var clip = pool.GetNext();
if (string.IsNullOrEmpty(clip))
return false;
@@ -388,7 +389,7 @@ namespace OpenRA
{
var sound = soundEngine.Play2D(sounds[name],
false, relative, pos,
InternalSoundVolume * volumeModifier, attenuateVolume);
InternalSoundVolume * volumeModifier * pool.VolumeModifier, attenuateVolume);
if (id != 0)
{
if (currentSounds.ContainsKey(id))

View File

@@ -18,6 +18,11 @@ using OpenRA.Primitives;
namespace OpenRA.Traits
{
public interface ICreatesFrozenActors
{
void OnVisibilityChanged(FrozenActor frozen);
}
[Desc("Required for FrozenUnderFog to work. Attach this to the player actor.")]
public class FrozenActorLayerInfo : Requires<ShroudInfo>, ITraitInfo
{
@@ -32,11 +37,13 @@ namespace OpenRA.Traits
public readonly PPos[] Footprint;
public readonly WPos CenterPosition;
readonly Actor actor;
readonly ICreatesFrozenActors frozenTrait;
readonly Player viewer;
readonly Shroud shroud;
public Player Owner { get; private set; }
public BitSet<TargetableType> TargetTypes { get; private set; }
public WPos[] TargetablePositions { get; private set; }
public ITooltipInfo TooltipInfo { get; private set; }
public Player TooltipOwner { get; private set; }
@@ -70,9 +77,10 @@ namespace OpenRA.Traits
int flashTicks;
public FrozenActor(Actor actor, PPos[] footprint, Player viewer, bool startsRevealed)
public FrozenActor(Actor actor, ICreatesFrozenActors frozenTrait, PPos[] footprint, Player viewer, bool startsRevealed)
{
this.actor = actor;
this.frozenTrait = frozenTrait;
this.viewer = viewer;
shroud = viewer.Shroud;
NeedRenderables = startsRevealed;
@@ -111,6 +119,7 @@ namespace OpenRA.Traits
{
Owner = actor.Owner;
TargetTypes = actor.GetEnabledTargetTypes();
TargetablePositions = actor.GetTargetablePositions().ToArray();
Hidden = !actor.CanBeViewedByPlayer(viewer);
if (health != null)
@@ -153,6 +162,11 @@ namespace OpenRA.Traits
Shrouded = false;
}
// Force the backing trait to update so other actors can't
// query inconsistent state (both hidden or both visible)
if (Visible != wasVisible)
frozenTrait.OnVisibilityChanged(this);
NeedRenderables |= Visible && !wasVisible;
}

View File

@@ -19,42 +19,74 @@ namespace OpenRA.Traits
public struct Target
{
public static readonly Target[] None = { };
public static readonly Target Invalid = new Target { type = TargetType.Invalid };
public static readonly Target Invalid = new Target();
TargetType type;
Actor actor;
FrozenActor frozen;
WPos pos;
CPos? cell;
SubCell? subCell;
int generation;
readonly TargetType type;
readonly Actor actor;
readonly FrozenActor frozen;
readonly WPos terrainCenterPosition;
readonly WPos[] terrainPositions;
readonly CPos? cell;
readonly SubCell? subCell;
readonly int generation;
public static Target FromPos(WPos p) { return new Target { pos = p, type = TargetType.Terrain }; }
public static Target FromCell(World w, CPos c, SubCell subCell = SubCell.FullCell)
Target(WPos terrainCenterPosition, WPos[] terrainPositions = null)
{
return new Target
{
pos = w.Map.CenterOfSubCell(c, subCell),
cell = c,
subCell = subCell,
type = TargetType.Terrain
};
type = TargetType.Terrain;
this.terrainCenterPosition = terrainCenterPosition;
this.terrainPositions = terrainPositions ?? new[] { terrainCenterPosition };
actor = null;
frozen = null;
cell = null;
subCell = null;
generation = 0;
}
public static Target FromActor(Actor a)
Target(World w, CPos c, SubCell subCell)
{
if (a == null)
return Invalid;
type = TargetType.Terrain;
terrainCenterPosition = w.Map.CenterOfSubCell(c, subCell);
terrainPositions = new[] { terrainCenterPosition };
cell = c;
this.subCell = subCell;
return new Target
{
actor = a,
type = TargetType.Actor,
generation = a.Generation,
};
actor = null;
frozen = null;
generation = 0;
}
public static Target FromFrozenActor(FrozenActor a) { return new Target { frozen = a, type = TargetType.FrozenActor }; }
Target(Actor a)
{
type = TargetType.Actor;
actor = a;
generation = a.Generation;
terrainCenterPosition = WPos.Zero;
terrainPositions = null;
frozen = null;
cell = null;
subCell = null;
}
Target(FrozenActor fa)
{
type = TargetType.FrozenActor;
frozen = fa;
terrainCenterPosition = WPos.Zero;
terrainPositions = null;
actor = null;
cell = null;
subCell = null;
generation = 0;
}
public static Target FromPos(WPos p) { return new Target(p); }
public static Target FromTargetPositions(Target t) { return new Target(t.CenterPosition, t.Positions.ToArray()); }
public static Target FromCell(World w, CPos c, SubCell subCell = SubCell.FullCell) { return new Target(w, c, subCell); }
public static Target FromActor(Actor a) { return a != null ? new Target(a) : Invalid; }
public static Target FromFrozenActor(FrozenActor fa) { return new Target(fa); }
public Actor Actor { get { return actor; } }
public FrozenActor FrozenActor { get { return frozen; } }
@@ -134,7 +166,7 @@ namespace OpenRA.Traits
case TargetType.FrozenActor:
return frozen.CenterPosition;
case TargetType.Terrain:
return pos;
return terrainCenterPosition;
default:
case TargetType.Invalid:
throw new InvalidOperationException("Attempting to query the position of an invalid Target");
@@ -151,14 +183,11 @@ namespace OpenRA.Traits
switch (Type)
{
case TargetType.Actor:
if (!actor.Targetables.Any(Exts.IsTraitEnabled))
return new[] { actor.CenterPosition };
return actor.GetTargetablePositions();
case TargetType.FrozenActor:
return new[] { frozen.CenterPosition };
return frozen.TargetablePositions;
case TargetType.Terrain:
return new[] { pos };
return terrainPositions;
default:
case TargetType.Invalid:
return NoPositions;
@@ -186,7 +215,7 @@ namespace OpenRA.Traits
return frozen.ToString();
case TargetType.Terrain:
return pos.ToString();
return terrainCenterPosition.ToString();
default:
case TargetType.Invalid:
@@ -199,6 +228,6 @@ namespace OpenRA.Traits
internal Actor SerializableActor { get { return actor; } }
internal CPos? SerializableCell { get { return cell; } }
internal SubCell? SerializableSubCell { get { return subCell; } }
internal WPos SerializablePos { get { return pos; } }
internal WPos SerializablePos { get { return terrainCenterPosition; } }
}
}

View File

@@ -142,7 +142,7 @@ namespace OpenRA
}
}
public readonly Selection Selection = new Selection();
public readonly Selection Selection;
public void CancelInputMode() { OrderGenerator = new UnitOrderGenerator(); }
@@ -179,6 +179,8 @@ namespace OpenRA
ActorMap = WorldActor.Trait<IActorMap>();
ScreenMap = WorldActor.Trait<ScreenMap>();
Selection = new Selection(WorldActor.TraitsImplementing<INotifySelection>());
// Add players
foreach (var cmp in WorldActor.TraitsImplementing<ICreatePlayers>())
cmp.CreatePlayers(this);

View File

@@ -9,8 +9,9 @@
*/
#endregion
using System;
using System.Drawing;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Cnc.Traits;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Traits;
@@ -20,32 +21,49 @@ namespace OpenRA.Mods.Cnc.Activities
{
class Infiltrate : Enter
{
readonly Actor target;
readonly Infiltrates infiltrates;
readonly INotifyInfiltration[] notifiers;
Actor enterActor;
public Infiltrate(Actor self, Actor target, Infiltrates infiltrate)
: base(self, target, infiltrate.Info.EnterBehaviour)
public Infiltrate(Actor self, Target target, Infiltrates infiltrates)
: base(self, target, Color.Red)
{
this.target = target;
infiltrates = infiltrate;
this.infiltrates = infiltrates;
notifiers = self.TraitsImplementing<INotifyInfiltration>().ToArray();
}
protected override void OnInside(Actor self)
protected override void TickInner(Actor self, Target target, bool targetIsDeadOrHiddenActor)
{
if (target.IsDead)
return;
if (infiltrates.IsTraitDisabled)
Cancel(self, true);
}
var stance = self.Owner.Stances[target.Owner];
if (!infiltrates.Info.ValidStances.HasStance(stance))
protected override bool TryStartEnter(Actor self, Actor targetActor)
{
// Make sure we can still demolish the target before entering
// (but not before, because this may stop the actor in the middle of nowhere)
if (!infiltrates.CanInfiltrateTarget(self, Target.FromActor(targetActor)))
{
Cancel(self, true);
return false;
}
enterActor = targetActor;
return true;
}
protected override void OnEnterComplete(Actor self, Actor targetActor)
{
// Make sure the target hasn't changed while entering
// OnEnterComplete is only called if targetActor is alive
if (targetActor != enterActor || !infiltrates.CanInfiltrateTarget(self, Target.FromActor(targetActor)))
return;
foreach (var ini in notifiers)
ini.Infiltrating(self);
foreach (var t in target.TraitsImplementing<INotifyInfiltrated>())
t.Infiltrated(target, self, infiltrates.Info.Types);
foreach (var t in targetActor.TraitsImplementing<INotifyInfiltrated>())
t.Infiltrated(targetActor, self, infiltrates.Info.Types);
var exp = self.Owner.PlayerActor.TraitOrDefault<PlayerExperience>();
if (exp != null)
@@ -54,14 +72,11 @@ namespace OpenRA.Mods.Cnc.Activities
if (!string.IsNullOrEmpty(infiltrates.Info.Notification))
Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech",
infiltrates.Info.Notification, self.Owner.Faction.InternalName);
}
public override Activity Tick(Actor self)
{
if (infiltrates.IsTraitDisabled)
Cancel(self);
return base.Tick(self);
if (infiltrates.Info.EnterBehaviour == EnterBehaviour.Dispose)
self.Dispose();
else if (infiltrates.Info.EnterBehaviour == EnterBehaviour.Suicide)
self.Kill(self);
}
}
}

View File

@@ -10,9 +10,11 @@
#endregion
using System;
using System.Drawing;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Cnc.Traits;
using OpenRA.Mods.Common;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
@@ -21,13 +23,17 @@ namespace OpenRA.Mods.Cnc.Activities
{
public class LeapAttack : Activity
{
readonly Target target;
readonly AttackLeapInfo info;
readonly AttackLeap attack;
readonly Mobile mobile, targetMobile;
readonly EdibleByLeap edible;
readonly Mobile mobile;
readonly bool allowMovement;
readonly IFacing facing;
Target target;
Target lastVisibleTarget;
bool useLastVisibleTarget;
WDist lastVisibleMinRange;
WDist lastVisibleMaxRange;
Activity inner;
public LeapAttack(Actor self, Target target, bool allowMovement, AttackLeap attack, AttackLeapInfo info)
{
@@ -35,61 +41,99 @@ namespace OpenRA.Mods.Cnc.Activities
this.info = info;
this.attack = attack;
this.allowMovement = allowMovement;
mobile = self.Trait<Mobile>();
facing = self.TraitOrDefault<IFacing>();
if (target.Type == TargetType.Actor)
// 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)
{
targetMobile = target.Actor.TraitOrDefault<Mobile>();
edible = target.Actor.TraitOrDefault<EdibleByLeap>();
lastVisibleTarget = Target.FromPos(target.CenterPosition);
lastVisibleMinRange = attack.GetMinimumRangeVersusTarget(target);
lastVisibleMaxRange = attack.GetMaximumRangeVersusTarget(target);
}
}
protected override void OnFirstRun(Actor self)
{
attack.IsAiming = true;
}
public override Activity Tick(Actor self)
{
if (IsCanceled || edible == null)
return NextActivity;
// Run this even if the target became invalid to avoid visual glitches
if (ChildActivity != null)
if (inner != null)
{
ChildActivity = ActivityUtils.RunActivity(self, ChildActivity);
inner = ActivityUtils.RunActivity(self, inner);
return this;
}
if (target.Type != TargetType.Actor || !edible.CanLeap(self) || !target.IsValidFor(self) || !attack.HasAnyValidWeapons(target))
if (IsCanceled)
return NextActivity;
var minRange = attack.GetMinimumRangeVersusTarget(target);
var maxRange = attack.GetMaximumRangeVersusTarget(target);
if (!target.IsInRange(self.CenterPosition, maxRange) || target.IsInRange(self.CenterPosition, minRange))
bool targetIsHiddenActor;
target = target.Recalculate(self.Owner, out targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
{
if (!allowMovement)
lastVisibleTarget = Target.FromTargetPositions(target);
lastVisibleMinRange = attack.GetMinimumRangeVersusTarget(target);
lastVisibleMaxRange = attack.GetMaximumRangeVersusTarget(target);
}
var oldUseLastVisibleTarget = useLastVisibleTarget;
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
// 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))
return NextActivity;
var pos = self.CenterPosition;
var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target;
if (!checkTarget.IsInRange(pos, lastVisibleMaxRange) || checkTarget.IsInRange(pos, lastVisibleMinRange))
{
if (!allowMovement || lastVisibleMaxRange == WDist.Zero || lastVisibleMaxRange < lastVisibleMinRange)
return NextActivity;
QueueChild(new MoveWithinRange(self, target, minRange, maxRange));
inner = mobile.MoveWithinRange(target, lastVisibleMinRange, lastVisibleMaxRange, checkTarget.CenterPosition, Color.Red);
return this;
}
// Ready to leap, but target isn't visible
if (targetIsHiddenActor || target.Type != TargetType.Actor)
return NextActivity;
// Target is not valid
if (!target.IsValidFor(self) || !attack.HasAnyValidWeapons(target))
return NextActivity;
var edible = target.Actor.TraitOrDefault<EdibleByLeap>();
if (edible == null || !edible.CanLeap(self))
return NextActivity;
// Can't leap yet
if (attack.Armaments.All(a => a.IsReloading))
return this;
// Use CenterOfSubCell with ToSubCell instead of target.Centerposition
// to avoid continuous facing adjustments as the target moves
var targetMobile = target.Actor.TraitOrDefault<Mobile>();
var targetSubcell = targetMobile != null ? targetMobile.ToSubCell : SubCell.Any;
var destination = self.World.Map.CenterOfSubCell(target.Actor.Location, targetSubcell);
var origin = self.World.Map.CenterOfSubCell(self.Location, mobile.FromSubCell);
var desiredFacing = (destination - origin).Yaw.Facing;
if (facing != null && facing.Facing != desiredFacing)
if (mobile.Facing != desiredFacing)
{
QueueChild(new Turn(self, desiredFacing));
inner = new Turn(self, desiredFacing);
return this;
}
QueueChild(new Leap(self, target, mobile, targetMobile, info.Speed.Length, attack, edible));
inner = new Leap(self, target, mobile, targetMobile, info.Speed.Length, attack, edible);
// Re-queue the child activities to kill the target if it didn't die in one go
return this;
@@ -98,7 +142,14 @@ namespace OpenRA.Mods.Cnc.Activities
protected override void OnLastRun(Actor self)
{
attack.IsAiming = false;
base.OnLastRun(self);
}
public override bool Cancel(Actor self, bool keepQueue = false)
{
if (!IsCanceled && inner != null && !inner.Cancel(self))
return false;
return base.Cancel(self, keepQueue);
}
}
}

View File

@@ -37,7 +37,7 @@ namespace OpenRA.Mods.Cnc.Scripting
if (infiltrates == null)
throw new LuaException("{0} tried to infiltrate invalid target {1}!".F(Self, target));
Self.QueueActivity(new Infiltrate(Self, target, infiltrates));
Self.QueueActivity(new Infiltrate(Self, Target.FromActor(target), infiltrates));
}
}
}

View File

@@ -59,10 +59,10 @@ namespace OpenRA.Mods.Cnc.Traits
{
// Check that AttackTDGunboatTurreted hasn't cancelled the target by modifying attack.Target
// Having both this and AttackTDGunboatTurreted modify that field is a horrible hack.
if (hasTicked && attack.Target.Type == TargetType.Invalid)
if (hasTicked && attack.requestedTarget.Type == TargetType.Invalid)
return NextActivity;
attack.Target = target;
attack.requestedTarget = target;
hasTicked = true;
}

View File

@@ -193,7 +193,7 @@ namespace OpenRA.Mods.Cnc.Traits
nt.AfterTransform(a);
if (selected)
self.World.Selection.Add(self.World, a);
self.World.Selection.Add(a);
if (controlgroup.HasValue)
self.World.Selection.AddToControlGroup(a, controlgroup.Value);

View File

@@ -32,7 +32,11 @@ namespace OpenRA.Mods.Cnc.Traits
// This only makes sense if the frozen actor has already been revealed (i.e. has renderables)
if (fa.HasRenderables)
{
// HACK: RefreshState updated *all* actor state, not just the owner
// This is generally bogus, and specifically breaks cursors and tooltips by setting Hidden to false
var hidden = fa.Hidden;
fa.RefreshState();
fa.Hidden = hidden;
fa.NeedRenderables = true;
}
};

View File

@@ -28,7 +28,7 @@ namespace OpenRA.Mods.Cnc.Traits
public object Create(ActorInitializer init) { return new GpsDot(this); }
}
class GpsDot : INotifyAddedToWorld, INotifyRemovedFromWorld
class GpsDot : INotifyCreated, INotifyAddedToWorld, INotifyRemovedFromWorld
{
readonly GpsDotInfo info;
GpsDotEffect effect;
@@ -38,9 +38,13 @@ namespace OpenRA.Mods.Cnc.Traits
this.info = info;
}
void INotifyAddedToWorld.AddedToWorld(Actor self)
void INotifyCreated.Created(Actor self)
{
effect = new GpsDotEffect(self, info);
}
void INotifyAddedToWorld.AddedToWorld(Actor self)
{
self.World.AddFrameEndTask(w => w.Add(effect));
}

View File

@@ -92,21 +92,34 @@ namespace OpenRA.Mods.Cnc.Traits
? Info.Voice : null;
}
public bool CanInfiltrateTarget(Actor self, Target target)
{
switch (target.Type)
{
case TargetType.Actor:
return Info.Types.Overlaps(target.Actor.GetEnabledTargetTypes()) &&
Info.ValidStances.HasStance(self.Owner.Stances[target.Actor.Owner]);
case TargetType.FrozenActor:
return target.FrozenActor.IsValid && Info.Types.Overlaps(target.FrozenActor.TargetTypes) &&
Info.ValidStances.HasStance(self.Owner.Stances[target.FrozenActor.Owner]);
default:
return false;
}
}
public void ResolveOrder(Actor self, Order order)
{
if (order.OrderString != "Infiltrate" || !IsValidOrder(self, order) || IsTraitDisabled)
return;
var target = self.ResolveFrozenActorOrder(order, Color.Red);
if (target.Type != TargetType.Actor
|| !Info.Types.Overlaps(target.Actor.GetAllTargetTypes()))
if (!CanInfiltrateTarget(self, order.Target))
return;
if (!order.Queued)
self.CancelActivity();
self.SetTargetLine(target, Color.Red);
self.QueueActivity(new Infiltrate(self, target.Actor, this));
self.SetTargetLine(order.Target, Color.Red);
self.QueueActivity(new Infiltrate(self, order.Target, this));
}
}

View File

@@ -197,21 +197,18 @@ namespace OpenRA.Mods.Cnc.Traits
{
if (order.OrderString == "DetonateAttack")
{
var target = self.ResolveFrozenActorOrder(order, Color.Red);
if (target.Type != TargetType.Actor)
return;
if (!order.Queued)
self.CancelActivity();
self.SetTargetLine(target, Color.Red);
self.QueueActivity(new MoveAdjacentTo(self, target));
self.SetTargetLine(order.Target, Color.Red);
self.QueueActivity(new MoveAdjacentTo(self, order.Target, targetLineColor: Color.Red));
self.QueueActivity(new CallFunc(StartDetonationSequence));
}
else if (order.OrderString == "Detonate")
{
if (!order.Queued)
self.CancelActivity();
self.QueueActivity(new CallFunc(StartDetonationSequence));
}
}

View File

@@ -10,6 +10,7 @@
#endregion
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common;
@@ -181,11 +182,15 @@ namespace OpenRA.Mods.Cnc.Traits
public Activity MoveTo(CPos cell, int nearEnough) { return null; }
public Activity MoveTo(CPos cell, Actor ignoreActor) { return null; }
public Activity MoveWithinRange(Target target, WDist range) { return null; }
public Activity MoveWithinRange(Target target, WDist minRange, WDist maxRange) { return null; }
public Activity MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange) { return null; }
public Activity MoveWithinRange(Target target, WDist range,
WPos? initialTargetPosition = null, Color? targetLineColor = null) { return null; }
public Activity MoveWithinRange(Target target, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null) { return null; }
public Activity MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null) { return null; }
public Activity MoveIntoWorld(Actor self, CPos cell, SubCell subCell = SubCell.Any) { return null; }
public Activity MoveToTarget(Actor self, Target target) { return null; }
public Activity MoveToTarget(Actor self, Target target,
WPos? initialTargetPosition = null, Color? targetLineColor = null) { return null; }
public Activity MoveIntoTarget(Actor self, Target target) { return null; }
public Activity VisualMove(Actor self, WPos fromPos, WPos toPos) { return null; }

View File

@@ -20,23 +20,6 @@ namespace OpenRA.Mods.Common
public enum WaterCheck { NotChecked, EnoughWater, NotEnoughWater }
public class CaptureTarget<TInfoType> where TInfoType : class, ITraitInfoInterface
{
internal readonly Actor Actor;
internal readonly TInfoType Info;
/// <summary>The order string given to the capturer so they can capture this actor.</summary>
/// <example>ExternalCaptureActor</example>
internal readonly string OrderString;
internal CaptureTarget(Actor actor, string orderString)
{
Actor = actor;
Info = actor.Info.TraitInfoOrDefault<TInfoType>();
OrderString = orderString;
}
}
public static class AIUtils
{
public static bool IsAreaAvailable<T>(World world, Player player, Map map, int radius, HashSet<string> terrainTypes)

View File

@@ -10,6 +10,7 @@
#endregion
using System.Collections.Generic;
using System.Drawing;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
@@ -19,19 +20,32 @@ namespace OpenRA.Mods.Common.Activities
public class Fly : Activity
{
readonly Aircraft aircraft;
readonly Target target;
readonly WDist maxRange;
readonly WDist minRange;
readonly Color? targetLineColor;
Target target;
Target lastVisibleTarget;
bool useLastVisibleTarget;
bool soundPlayed;
public Fly(Actor self, Target t)
public Fly(Actor self, Target t, WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
aircraft = self.Trait<Aircraft>();
target = t;
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);
else if (initialTargetPosition.HasValue)
lastVisibleTarget = Target.FromPos(initialTargetPosition.Value);
}
public Fly(Actor self, Target t, WDist minRange, WDist maxRange)
: this(self, t)
public Fly(Actor self, Target t, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
: this(self, t, initialTargetPosition, targetLineColor)
{
this.maxRange = maxRange;
this.minRange = minRange;
@@ -61,14 +75,30 @@ namespace OpenRA.Mods.Common.Activities
{
// Refuse to take off if it would land immediately again.
if (aircraft.ForceLanding)
{
Cancel(self);
return NextActivity;
}
if (IsCanceled || !target.IsValidFor(self))
if (IsCanceled)
return NextActivity;
bool targetIsHiddenActor;
target = target.Recalculate(self.Owner, out targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
lastVisibleTarget = Target.FromTargetPositions(target);
var oldUseLastVisibleTarget = useLastVisibleTarget;
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
// Update target lines if required
if (useLastVisibleTarget != oldUseLastVisibleTarget && targetLineColor.HasValue)
self.SetTargetLine(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value, false);
// Target is hidden or dead, and we don't have a fallback position to move towards
if (useLastVisibleTarget && !lastVisibleTarget.IsValidFor(self))
return NextActivity;
var pos = self.CenterPosition;
var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target;
if (!soundPlayed && aircraft.Info.TakeoffSounds.Length > 0 && self.IsAtGroundLevel())
{
Game.Sound.Play(SoundType.World, aircraft.Info.TakeoffSounds.Random(self.World.SharedRandom), aircraft.CenterPosition);
@@ -76,23 +106,43 @@ namespace OpenRA.Mods.Common.Activities
}
// Inside the target annulus, so we're done
var insideMaxRange = maxRange.Length > 0 && target.IsInRange(aircraft.CenterPosition, maxRange);
var insideMinRange = minRange.Length > 0 && target.IsInRange(aircraft.CenterPosition, minRange);
var insideMaxRange = maxRange.Length > 0 && checkTarget.IsInRange(aircraft.CenterPosition, maxRange);
var insideMinRange = minRange.Length > 0 && checkTarget.IsInRange(aircraft.CenterPosition, minRange);
if (insideMaxRange && !insideMinRange)
return NextActivity;
var d = target.CenterPosition - self.CenterPosition;
var delta = checkTarget.CenterPosition - self.CenterPosition;
// The next move would overshoot, so consider it close enough
var move = aircraft.FlyStep(aircraft.Facing);
if (d.HorizontalLengthSquared < move.HorizontalLengthSquared)
if (delta.HorizontalLengthSquared < move.HorizontalLengthSquared)
return NextActivity;
// Don't turn until we've reached the cruise altitude
var desiredFacing = d.Yaw.Facing;
var desiredFacing = delta.Yaw.Facing;
var targetAltitude = aircraft.CenterPosition.Z + aircraft.Info.CruiseAltitude.Length - self.World.Map.DistanceAboveTerrain(aircraft.CenterPosition).Length;
if (aircraft.CenterPosition.Z < targetAltitude)
desiredFacing = aircraft.Facing;
else
{
// Using the turn rate, compute a hypothetical circle traced by a continuous turn.
// If it contains the destination point, it's unreachable without more complex manuvering.
var turnRadius = CalculateTurnRadius(aircraft.MovementSpeed, aircraft.TurnSpeed);
// The current facing is a tangent of the minimal turn circle.
// Make a perpendicular vector, and use it to locate the turn's center.
var turnCenterFacing = aircraft.Facing;
turnCenterFacing += Util.GetNearestFacing(aircraft.Facing, desiredFacing) > 0 ? 64 : -64;
var turnCenterDir = new WVec(0, -1024, 0).Rotate(WRot.FromFacing(turnCenterFacing));
turnCenterDir *= turnRadius;
turnCenterDir /= 1024;
// Compare with the target point, and keep flying away if it's inside the circle.
var turnCenter = aircraft.CenterPosition + turnCenterDir;
if ((checkTarget.CenterPosition - turnCenter).HorizontalLengthSquared < turnRadius * turnRadius)
desiredFacing = aircraft.Facing;
}
FlyToward(self, aircraft, desiredFacing, aircraft.Info.CruiseAltitude);
@@ -103,5 +153,13 @@ namespace OpenRA.Mods.Common.Activities
{
yield return target;
}
public static int CalculateTurnRadius(int speed, int turnSpeed)
{
// turnSpeed -> divide into 256 to get the number of ticks per complete rotation
// speed -> multiply to get distance travelled per rotation (circumference)
// 45 -> divide by 2*pi to get the turn radius: 45==256/(2*pi), with some extra leeway
return 45 * speed / turnSpeed;
}
}
}

View File

@@ -9,6 +9,7 @@
*/
#endregion
using System.Drawing;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
@@ -22,6 +23,9 @@ namespace OpenRA.Mods.Common.Activities
readonly AttackAircraft attackAircraft;
readonly Rearmable rearmable;
Target target;
Target lastVisibleTarget;
WDist lastVisibleMaximumRange;
bool useLastVisibleTarget;
int ticksUntilTurn;
@@ -32,38 +36,81 @@ namespace OpenRA.Mods.Common.Activities
attackAircraft = self.Trait<AttackAircraft>();
rearmable = self.TraitOrDefault<Rearmable>();
ticksUntilTurn = attackAircraft.AttackAircraftInfo.AttackTurnDelay;
// 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 = attackAircraft.GetMaximumRangeVersusTarget(target);
}
}
public override Activity Tick(Actor self)
{
// Refuse to take off if it would land immediately again.
if (aircraft.ForceLanding)
{
Cancel(self);
if (IsCanceled)
return NextActivity;
bool targetIsHiddenActor;
target = target.Recalculate(self.Owner, out targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
{
lastVisibleTarget = Target.FromTargetPositions(target);
lastVisibleMaximumRange = attackAircraft.GetMaximumRangeVersusTarget(target);
}
target = target.Recalculate(self.Owner);
var oldUseLastVisibleTarget = useLastVisibleTarget;
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
if (!target.IsValidFor(self))
// 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))
return NextActivity;
// If all valid weapons have depleted their ammo and Rearmable trait exists, return to RearmActor to reload and then resume the activity
if (rearmable != null && attackAircraft.Armaments.All(x => x.IsTraitPaused || !x.Weapon.IsValidAgainst(target, self.World, self)))
if (rearmable != null && !useLastVisibleTarget && attackAircraft.Armaments.All(x => x.IsTraitPaused || !x.Weapon.IsValidAgainst(target, self.World, self)))
return ActivityUtils.SequenceActivities(new ReturnToBase(self, aircraft.Info.AbortOnResupply), this);
var pos = self.CenterPosition;
var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target;
// We don't know where the target actually is, so move to where we last saw it
if (useLastVisibleTarget)
{
// We've reached the assumed position but it is not there - give up
if (checkTarget.IsInRange(pos, lastVisibleMaximumRange))
return NextActivity;
// Fly towards the last known position
return ActivityUtils.SequenceActivities(
new Fly(self, target, WDist.Zero, lastVisibleMaximumRange, checkTarget.CenterPosition, Color.Red),
this);
}
// If we reach here the target is guaranteed to be both visible and alive, so use target instead of checkTarget.
// The target may not be in range, but try attacking anyway...
attackAircraft.DoAttack(self, target);
if (ChildActivity == null)
{
if (IsCanceled)
return NextActivity;
// TODO: This should fire each weapon at its maximum range
if (attackAircraft != null && target.IsInRange(self.CenterPosition, attackAircraft.Armaments.Where(Exts.IsTraitEnabled).Select(a => a.Weapon.MinRange).Min()))
ChildActivity = ActivityUtils.SequenceActivities(new FlyTimed(ticksUntilTurn, self), new Fly(self, target), new FlyTimed(ticksUntilTurn, self));
if (attackAircraft != null && target.IsInRange(self.CenterPosition, attackAircraft.GetMinimumRange()))
ChildActivity = ActivityUtils.SequenceActivities(
new FlyTimed(ticksUntilTurn, self),
new Fly(self, target, checkTarget.CenterPosition, Color.Red),
new FlyTimed(ticksUntilTurn, self));
else
ChildActivity = ActivityUtils.SequenceActivities(new Fly(self, target), new FlyTimed(ticksUntilTurn, self));
ChildActivity = ActivityUtils.SequenceActivities(
new Fly(self, target, checkTarget.CenterPosition, Color.Red),
new FlyTimed(ticksUntilTurn, self));
// HACK: This needs to be done in this round-about way because TakeOff doesn't behave as expected when it doesn't have a NextActivity.
if (self.World.Map.DistanceAboveTerrain(self.CenterPosition).Length < aircraft.Info.MinAirborneAltitude)

View File

@@ -29,6 +29,9 @@ namespace OpenRA.Mods.Common.Activities
public override Activity Tick(Actor self)
{
if (NextActivity != null && remainingTicks <= 0)
return NextActivity;
// Refuse to take off if it would land immediately again.
if (aircraft.ForceLanding)
{
@@ -41,8 +44,6 @@ namespace OpenRA.Mods.Common.Activities
if (remainingTicks > 0)
remainingTicks--;
else if (remainingTicks == 0)
return NextActivity;
// We can't possibly turn this fast
var desiredFacing = aircraft.Facing + 64;

View File

@@ -9,6 +9,7 @@
*/
#endregion
using System.Drawing;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
@@ -17,38 +18,80 @@ namespace OpenRA.Mods.Common.Activities
{
public class FlyFollow : Activity
{
readonly Aircraft aircraft;
readonly WDist minRange;
readonly WDist maxRange;
readonly Color? targetLineColor;
Target target;
Aircraft aircraft;
WDist minRange;
WDist maxRange;
Target lastVisibleTarget;
bool useLastVisibleTarget;
bool wasMovingWithinRange;
public FlyFollow(Actor self, Target target, WDist minRange, WDist maxRange)
public FlyFollow(Actor self, Target target, WDist minRange, WDist maxRange,
WPos? initialTargetPosition, Color? targetLineColor = null)
{
this.target = target;
aircraft = self.Trait<Aircraft>();
this.minRange = minRange;
this.maxRange = maxRange;
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);
else if (initialTargetPosition.HasValue)
lastVisibleTarget = Target.FromPos(initialTargetPosition.Value);
}
public override Activity Tick(Actor self)
{
// Refuse to take off if it would land immediately again.
if (aircraft.ForceLanding)
{
Cancel(self);
return NextActivity;
}
if (IsCanceled || !target.IsValidFor(self))
if (IsCanceled)
return NextActivity;
if (target.IsInRange(self.CenterPosition, maxRange) && !target.IsInRange(self.CenterPosition, minRange))
bool targetIsHiddenActor;
target = target.Recalculate(self.Owner, out targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
lastVisibleTarget = Target.FromTargetPositions(target);
var oldUseLastVisibleTarget = useLastVisibleTarget;
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
// 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 NextActivity;
wasMovingWithinRange = false;
// Update target lines if required
if (useLastVisibleTarget != oldUseLastVisibleTarget && targetLineColor.HasValue)
self.SetTargetLine(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value, false);
// Target is hidden or dead, and we don't have a fallback position to move towards
if (useLastVisibleTarget && !lastVisibleTarget.IsValidFor(self))
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))
{
Fly.FlyToward(self, aircraft, aircraft.Facing, aircraft.Info.CruiseAltitude);
return this;
return useLastVisibleTarget ? NextActivity : this;
}
return ActivityUtils.SequenceActivities(new Fly(self, target, minRange, maxRange), this);
wasMovingWithinRange = true;
return ActivityUtils.SequenceActivities(
aircraft.MoveWithinRange(target, minRange, maxRange, checkTarget.CenterPosition, targetLineColor),
this);
}
}
}

View File

@@ -21,72 +21,84 @@ namespace OpenRA.Mods.Common.Activities
{
readonly Aircraft aircraft;
readonly AttackAircraft attackAircraft;
readonly bool attackOnlyVisibleTargets;
readonly Rearmable rearmable;
Target target;
bool canHideUnderFog;
protected Target Target
{
get
{
return target;
}
Target lastVisibleTarget;
WDist lastVisibleMaximumRange;
bool useLastVisibleTarget;
private set
{
target = value;
if (target.Type == TargetType.Actor)
canHideUnderFog = target.Actor.Info.HasTraitInfo<HiddenUnderFogInfo>();
}
}
public HeliAttack(Actor self, Target target, bool attackOnlyVisibleTargets = true)
public HeliAttack(Actor self, Target target)
{
Target = target;
this.target = target;
aircraft = self.Trait<Aircraft>();
attackAircraft = self.Trait<AttackAircraft>();
this.attackOnlyVisibleTargets = attackOnlyVisibleTargets;
rearmable = self.TraitOrDefault<Rearmable>();
// 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 = attackAircraft.GetMaximumRangeVersusTarget(target);
}
}
public override Activity Tick(Actor self)
{
// Refuse to take off if it would land immediately again.
if (aircraft.ForceLanding)
{
Cancel(self);
return NextActivity;
}
if (IsCanceled || !target.IsValidFor(self))
if (IsCanceled)
return NextActivity;
var pos = self.CenterPosition;
var targetPos = attackAircraft.GetTargetPosition(pos, target);
if (attackOnlyVisibleTargets && target.Type == TargetType.Actor && canHideUnderFog
&& !target.Actor.CanBeViewedByPlayer(self.Owner))
bool targetIsHiddenActor;
target = target.Recalculate(self.Owner, out targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
{
var newTarget = Target.FromCell(self.World, self.World.Map.CellContaining(targetPos));
Cancel(self);
self.SetTargetLine(newTarget, Color.Green);
return new HeliFly(self, newTarget);
lastVisibleTarget = Target.FromTargetPositions(target);
lastVisibleMaximumRange = attackAircraft.GetMaximumRangeVersusTarget(target);
}
var oldUseLastVisibleTarget = useLastVisibleTarget;
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
// 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))
return NextActivity;
// If all valid weapons have depleted their ammo and Rearmable trait exists, return to RearmActor to reload and then resume the activity
if (rearmable != null && attackAircraft.Armaments.All(x => x.IsTraitPaused || !x.Weapon.IsValidAgainst(target, self.World, self)))
if (rearmable != null && !useLastVisibleTarget && attackAircraft.Armaments.All(x => x.IsTraitPaused || !x.Weapon.IsValidAgainst(target, self.World, self)))
return ActivityUtils.SequenceActivities(new HeliReturnToBase(self, aircraft.Info.AbortOnResupply), this);
var dist = targetPos - pos;
var pos = self.CenterPosition;
var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target;
// Can rotate facing while ascending
var desiredFacing = dist.HorizontalLengthSquared != 0 ? dist.Yaw.Facing : aircraft.Facing;
// Update facing
var delta = attackAircraft.GetTargetPosition(pos, checkTarget) - pos;
var desiredFacing = delta.HorizontalLengthSquared != 0 ? delta.Yaw.Facing : aircraft.Facing;
aircraft.Facing = Util.TickFacing(aircraft.Facing, desiredFacing, aircraft.TurnSpeed);
if (HeliFly.AdjustAltitude(self, aircraft, aircraft.Info.CruiseAltitude))
return this;
// We don't know where the target actually is, so move to where we last saw it
if (useLastVisibleTarget)
{
// We've reached the assumed position but it is not there - give up
if (checkTarget.IsInRange(pos, lastVisibleMaximumRange))
return NextActivity;
// Fly towards the last known position
aircraft.SetPosition(self, aircraft.CenterPosition + aircraft.FlyStep(desiredFacing));
return this;
}
// Fly towards the target
if (!target.IsInRange(pos, attackAircraft.GetMaximumRangeVersusTarget(target)))
aircraft.SetPosition(self, aircraft.CenterPosition + aircraft.FlyStep(desiredFacing));

View File

@@ -10,6 +10,7 @@
#endregion
using System.Collections.Generic;
using System.Drawing;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
@@ -19,19 +20,33 @@ namespace OpenRA.Mods.Common.Activities
public class HeliFly : Activity
{
readonly Aircraft aircraft;
readonly Target target;
readonly WDist maxRange;
readonly WDist minRange;
readonly Color? targetLineColor;
bool soundPlayed;
public HeliFly(Actor self, Target t)
Target target;
Target lastVisibleTarget;
bool useLastVisibleTarget;
public HeliFly(Actor self, Target t, WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
aircraft = self.Trait<Aircraft>();
target = t;
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);
else if (initialTargetPosition.HasValue)
lastVisibleTarget = Target.FromPos(initialTargetPosition.Value);
}
public HeliFly(Actor self, Target t, WDist minRange, WDist maxRange)
: this(self, t)
public HeliFly(Actor self, Target t, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
: this(self, t, initialTargetPosition, targetLineColor)
{
this.maxRange = maxRange;
this.minRange = minRange;
@@ -56,12 +71,25 @@ namespace OpenRA.Mods.Common.Activities
{
// Refuse to take off if it would land immediately again.
if (aircraft.ForceLanding)
{
Cancel(self);
return NextActivity;
}
if (IsCanceled || !target.IsValidFor(self))
if (IsCanceled)
return NextActivity;
bool targetIsHiddenActor;
target = target.Recalculate(self.Owner, out targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
lastVisibleTarget = Target.FromTargetPositions(target);
var oldUseLastVisibleTarget = useLastVisibleTarget;
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
// Update target lines if required
if (useLastVisibleTarget != oldUseLastVisibleTarget && targetLineColor.HasValue)
self.SetTargetLine(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value, false);
// Target is hidden or dead, and we don't have a fallback position to move towards
if (useLastVisibleTarget && !lastVisibleTarget.IsValidFor(self))
return NextActivity;
if (!soundPlayed && aircraft.Info.TakeoffSounds.Length > 0 && self.IsAtGroundLevel())
@@ -73,30 +101,33 @@ namespace OpenRA.Mods.Common.Activities
if (AdjustAltitude(self, aircraft, aircraft.Info.CruiseAltitude))
return this;
var pos = target.CenterPosition;
var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target;
// Rotate towards the target
var dist = pos - self.CenterPosition;
var desiredFacing = dist.HorizontalLengthSquared != 0 ? dist.Yaw.Facing : aircraft.Facing;
// Update facing
var delta = checkTarget.CenterPosition - aircraft.CenterPosition;
var desiredFacing = delta.HorizontalLengthSquared != 0 ? delta.Yaw.Facing : aircraft.Facing;
aircraft.Facing = Util.TickFacing(aircraft.Facing, desiredFacing, aircraft.TurnSpeed);
if (AdjustAltitude(self, aircraft, aircraft.Info.CruiseAltitude))
return this;
var move = aircraft.FlyStep(desiredFacing);
// Inside the minimum range, so reverse
if (minRange.Length > 0 && target.IsInRange(aircraft.CenterPosition, minRange))
if (minRange.Length > 0 && checkTarget.IsInRange(aircraft.CenterPosition, minRange))
{
aircraft.SetPosition(self, aircraft.CenterPosition - move);
return this;
}
// Inside the maximum range, so we're done
if (maxRange.Length > 0 && target.IsInRange(aircraft.CenterPosition, maxRange))
if (maxRange.Length > 0 && checkTarget.IsInRange(aircraft.CenterPosition, maxRange))
return NextActivity;
// The next move would overshoot, so just set the final position
if (dist.HorizontalLengthSquared < move.HorizontalLengthSquared)
if (delta.HorizontalLengthSquared < move.HorizontalLengthSquared)
{
var targetAltitude = aircraft.CenterPosition.Z + aircraft.Info.CruiseAltitude.Length - self.World.Map.DistanceAboveTerrain(aircraft.CenterPosition).Length;
aircraft.SetPosition(self, pos + new WVec(0, 0, targetAltitude - pos.Z));
aircraft.SetPosition(self, checkTarget.CenterPosition + new WVec(0, 0, targetAltitude - checkTarget.CenterPosition.Z));
return NextActivity;
}

View File

@@ -10,6 +10,7 @@
#endregion
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
@@ -89,7 +90,9 @@ namespace OpenRA.Mods.Common.Activities
var target = Target.FromPos(nearestResupplier.CenterPosition + randomPosition);
return ActivityUtils.SequenceActivities(new HeliFly(self, target, WDist.Zero, aircraft.Info.WaitDistanceFromResupplyBase), this);
return ActivityUtils.SequenceActivities(
new HeliFly(self, target, WDist.Zero, aircraft.Info.WaitDistanceFromResupplyBase, targetLineColor: Color.Green),
this);
}
return this;

View File

@@ -38,12 +38,10 @@ namespace OpenRA.Mods.Common.Activities
}
else
{
// HACK: Append NextInQueue to TakeOff to avoid moving to the Rallypoint (if NextInQueue is non-null).
ChildActivity = ActivityUtils.SequenceActivities(
aircraft.GetResupplyActivities(host)
.Append(new AllowYieldingReservation(self))
.Append(new TakeOff(self))
.Append(NextInQueue)
.Append(new TakeOff(self, (a, b, c) => NextInQueue == null && b.NextInQueue == null))
.ToArray());
}
}

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
@@ -52,11 +53,6 @@ namespace OpenRA.Mods.Common.Activities
.ClosestTo(self);
}
int CalculateTurnRadius(int speed)
{
return 45 * speed / aircraft.Info.TurnSpeed;
}
void Calculate(Actor self)
{
if (dest == null || dest.IsDead || Reservable.IsReserved(dest))
@@ -76,7 +72,7 @@ namespace OpenRA.Mods.Common.Activities
// Add 10% to the turning radius to ensure we have enough room
var speed = aircraft.MovementSpeed * 32 / 35;
var turnRadius = CalculateTurnRadius(speed);
var turnRadius = Fly.CalculateTurnRadius(speed, aircraft.Info.TurnSpeed);
// Find the center of the turning circles for clockwise and counterclockwise turns
var angle = WAngle.FromFacing(aircraft.Facing);
@@ -140,7 +136,7 @@ namespace OpenRA.Mods.Common.Activities
if (nearestResupplier != null)
return ActivityUtils.SequenceActivities(
new Fly(self, Target.FromActor(nearestResupplier), WDist.Zero, aircraft.Info.WaitDistanceFromResupplyBase),
new Fly(self, Target.FromActor(nearestResupplier), WDist.Zero, aircraft.Info.WaitDistanceFromResupplyBase, targetLineColor: Color.Green),
new FlyCircle(self, aircraft.Info.NumberOfTicksToVerifyAvailableAirport),
this);
else
@@ -153,7 +149,7 @@ namespace OpenRA.Mods.Common.Activities
List<Activity> landingProcedures = new List<Activity>();
var turnRadius = CalculateTurnRadius(aircraft.Info.Speed);
var turnRadius = Fly.CalculateTurnRadius(aircraft.Info.Speed, aircraft.Info.TurnSpeed);
landingProcedures.Add(new Fly(self, Target.FromPos(w1), WDist.Zero, new WDist(turnRadius * 3)));
landingProcedures.Add(new Fly(self, Target.FromPos(w2)));

View File

@@ -9,6 +9,7 @@
*/
#endregion
using System;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
@@ -18,11 +19,19 @@ namespace OpenRA.Mods.Common.Activities
{
readonly Aircraft aircraft;
readonly IMove move;
Func<Actor, Activity, CPos, bool> moveToRallyPoint;
public TakeOff(Actor self)
{
aircraft = self.Trait<Aircraft>();
move = self.Trait<IMove>();
moveToRallyPoint = (actor, activity, pos) => NextActivity == null;
}
public TakeOff(Actor self, Func<Actor, Activity, CPos, bool> moveToRallyPoint)
: this(self)
{
this.moveToRallyPoint = moveToRallyPoint;
}
public override Activity Tick(Actor self)
@@ -43,10 +52,10 @@ namespace OpenRA.Mods.Common.Activities
var destination = rp != null ? rp.Location :
(hasHost ? self.World.Map.CellContaining(host.CenterPosition) : self.Location);
if (NextInQueue == null)
if (moveToRallyPoint(self, this, destination))
return new AttackMoveActivity(self, move.MoveTo(destination, 1));
else
return NextInQueue;
return NextActivity;
}
}
}

View File

@@ -10,6 +10,7 @@
#endregion
using System;
using System.Drawing;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
@@ -29,7 +30,12 @@ namespace OpenRA.Mods.Common.Activities
readonly IFacing facing;
readonly IPositionable positionable;
readonly bool forceAttack;
protected Target target;
Target lastVisibleTarget;
WDist lastVisibleMaximumRange;
bool useLastVisibleTarget;
bool wasMovingWithinRange;
WDist minRange;
WDist maxRange;
@@ -48,16 +54,71 @@ namespace OpenRA.Mods.Common.Activities
positionable = self.Trait<IPositionable>();
move = allowMovement ? self.TraitOrDefault<IMove>() : null;
// 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 = attackTraits.Where(x => !x.IsTraitDisabled)
.Min(x => x.GetMaximumRangeVersusTarget(target));
}
}
protected virtual Target RecalculateTarget(Actor self)
protected virtual Target RecalculateTarget(Actor self, out bool targetIsHiddenActor)
{
return target.Recalculate(self.Owner);
return target.Recalculate(self.Owner, out targetIsHiddenActor);
}
public override Activity Tick(Actor self)
{
target = RecalculateTarget(self);
if (IsCanceled)
return NextActivity;
bool targetIsHiddenActor;
target = RecalculateTarget(self, out targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
{
lastVisibleTarget = Target.FromTargetPositions(target);
lastVisibleMaximumRange = attackTraits.Where(x => !x.IsTraitDisabled)
.Min(x => x.GetMaximumRangeVersusTarget(target));
}
var oldUseLastVisibleTarget = useLastVisibleTarget;
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
// 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 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))
return NextActivity;
wasMovingWithinRange = false;
var pos = self.CenterPosition;
var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target;
// We don't know where the target actually is, so move to where we last saw it
if (useLastVisibleTarget)
{
// We've reached the assumed position but it is not there or we can't move any further - give up
if (checkTarget.IsInRange(pos, lastVisibleMaximumRange) || move == null || lastVisibleMaximumRange == WDist.Zero)
return NextActivity;
// Move towards the last known position
wasMovingWithinRange = true;
return ActivityUtils.SequenceActivities(
move.MoveWithinRange(target, WDist.Zero, lastVisibleMaximumRange, checkTarget.CenterPosition, Color.Red),
this);
}
turnActivity = moveActivity = null;
attackStatus = AttackStatus.UnableToAttack;
@@ -74,7 +135,10 @@ namespace OpenRA.Mods.Common.Activities
return turnActivity;
if (attackStatus.HasFlag(AttackStatus.NeedsToMove))
{
wasMovingWithinRange = true;
return moveActivity;
}
return NextActivity;
}
@@ -104,7 +168,10 @@ namespace OpenRA.Mods.Common.Activities
var sightRange = rs != null ? rs.Range : WDist.FromCells(2);
attackStatus |= AttackStatus.NeedsToMove;
moveActivity = ActivityUtils.SequenceActivities(move.MoveWithinRange(target, sightRange), this);
moveActivity = ActivityUtils.SequenceActivities(
move.MoveWithinRange(target, sightRange, target.CenterPosition, Color.Red),
this);
return AttackStatus.NeedsToMove;
}
@@ -128,7 +195,12 @@ namespace OpenRA.Mods.Common.Activities
return AttackStatus.UnableToAttack;
attackStatus |= AttackStatus.NeedsToMove;
moveActivity = ActivityUtils.SequenceActivities(move.MoveWithinRange(target, minRange, maxRange), this);
var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target;
moveActivity = ActivityUtils.SequenceActivities(
move.MoveWithinRange(target, minRange, maxRange, checkTarget.CenterPosition, Color.Red),
this);
return AttackStatus.NeedsToMove;
}
@@ -147,47 +219,4 @@ namespace OpenRA.Mods.Common.Activities
return AttackStatus.Attacking;
}
}
public static class TargetExts
{
/// <summary>Update (Frozen)Actor targets to account for visibility changes or actor replacement</summary>
public static Target Recalculate(this Target t, Player viewer)
{
// Check whether the target has transformed into something else
// HACK: This relies on knowing the internal implementation details of Target
if (t.Type == TargetType.Invalid && t.Actor != null && t.Actor.ReplacedByActor != null)
t = Target.FromActor(t.Actor.ReplacedByActor);
// Bot-controlled units aren't yet capable of understanding visibility changes
if (viewer.IsBot)
return t;
if (t.Type == TargetType.Actor)
{
// Actor has been hidden under the fog
if (!t.Actor.CanBeViewedByPlayer(viewer))
{
// Replace with FrozenActor if applicable, otherwise drop the target
var frozen = viewer.FrozenActorLayer.FromID(t.Actor.ActorID);
return frozen != null ? Target.FromFrozenActor(frozen) : Target.Invalid;
}
}
else if (t.Type == TargetType.FrozenActor)
{
// Frozen actor has been revealed
if (!t.FrozenActor.Visible || !t.FrozenActor.IsValid)
{
// Original actor is still alive
if (t.FrozenActor.Actor != null && t.FrozenActor.Actor.CanBeViewedByPlayer(viewer))
return Target.FromActor(t.FrozenActor.Actor);
// Original actor was killed while hidden
if (t.Actor == null)
return Target.Invalid;
}
}
return t;
}
}
}

View File

@@ -9,6 +9,7 @@
*/
#endregion
using System.Drawing;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
@@ -17,49 +18,62 @@ namespace OpenRA.Mods.Common.Activities
{
public class CaptureActor : Enter
{
readonly Actor actor;
readonly CaptureManager targetManager;
readonly CaptureManager manager;
public CaptureActor(Actor self, Actor target)
: base(self, target, EnterBehaviour.Exit)
Actor enterActor;
CaptureManager enterCaptureManager;
public CaptureActor(Actor self, Target target)
: base(self, target, Color.Red)
{
actor = target;
manager = self.Trait<CaptureManager>();
targetManager = target.Trait<CaptureManager>();
}
protected override bool CanReserve(Actor self)
protected override bool TryStartEnter(Actor self, Actor targetActor)
{
return !actor.IsDead && !targetManager.BeingCaptured && targetManager.CanBeTargetedBy(actor, self, manager);
}
if (enterActor != targetActor)
{
enterActor = targetActor;
enterCaptureManager = targetActor.TraitOrDefault<CaptureManager>();
}
// Make sure we can still capture the target before entering
// (but not before, because this may stop the actor in the middle of nowhere)
if (enterCaptureManager == null || !enterCaptureManager.CanBeTargetedBy(enterActor, self, manager))
{
Cancel(self, true);
return false;
}
protected override bool TryStartEnter(Actor self)
{
// StartCapture returns false when a capture delay is enabled
// We wait until it returns true before allowing entering the target
Captures captures;
if (!manager.StartCapture(self, actor, targetManager, out captures))
if (!manager.StartCapture(self, enterActor, enterCaptureManager, out captures))
return false;
if (!captures.Info.ConsumedByCapture)
{
// Immediately capture without entering or disposing the actor
DoCapture(self, captures);
AbortOrExit(self);
Cancel(self, true);
return false;
}
return true;
}
protected override void OnInside(Actor self)
protected override void OnEnterComplete(Actor self, Actor targetActor)
{
if (!CanReserve(self))
// Make sure the target hasn't changed while entering
// OnEnterComplete is only called if targetActor is alive
if (enterActor != targetActor)
return;
if (enterCaptureManager.BeingCaptured || !enterCaptureManager.CanBeTargetedBy(enterActor, self, manager))
return;
// Prioritize capturing over sabotaging
var captures = manager.ValidCapturesWithLowestSabotageThreshold(self, actor, targetManager);
var captures = manager.ValidCapturesWithLowestSabotageThreshold(self, enterActor, enterCaptureManager);
if (captures == null)
return;
@@ -68,23 +82,23 @@ namespace OpenRA.Mods.Common.Activities
void DoCapture(Actor self, Captures captures)
{
var oldOwner = actor.Owner;
var oldOwner = enterActor.Owner;
self.World.AddFrameEndTask(w =>
{
// The target died or was already captured during this tick
if (actor.IsDead || oldOwner != actor.Owner)
if (enterActor.IsDead || oldOwner != enterActor.Owner)
return;
// Sabotage instead of capture
if (captures.Info.SabotageThreshold > 0 && !actor.Owner.NonCombatant)
if (captures.Info.SabotageThreshold > 0 && !enterActor.Owner.NonCombatant)
{
var health = actor.Trait<IHealth>();
var health = enterActor.Trait<IHealth>();
// Cast to long to avoid overflow when multiplying by the health
if (100 * (long)health.HP > captures.Info.SabotageThreshold * (long)health.MaxHP)
{
var damage = (int)((long)health.MaxHP * captures.Info.SabotageHPRemoval / 100);
actor.InflictDamage(self, new Damage(damage, captures.Info.SabotageDamageTypes));
enterActor.InflictDamage(self, new Damage(damage, captures.Info.SabotageDamageTypes));
if (captures.Info.ConsumedByCapture)
self.Dispose();
@@ -94,10 +108,10 @@ namespace OpenRA.Mods.Common.Activities
}
// Do the capture
actor.ChangeOwnerSync(self.Owner);
enterActor.ChangeOwnerSync(self.Owner);
foreach (var t in actor.TraitsImplementing<INotifyCapture>())
t.OnCapture(actor, self, oldOwner, self.Owner, captures.Info.CaptureTypes);
foreach (var t in enterActor.TraitsImplementing<INotifyCapture>())
t.OnCapture(enterActor, self, oldOwner, self.Owner, captures.Info.CaptureTypes);
if (self.Owner.Stances[oldOwner].HasStance(captures.Info.PlayerExperienceStances))
{
@@ -123,17 +137,14 @@ namespace OpenRA.Mods.Common.Activities
base.OnActorDispose(self);
}
void CancelCapture(Actor self)
protected override void OnCancel(Actor self)
{
manager.CancelCapture(self, actor, targetManager);
CancelCapture(self);
}
public override Activity Tick(Actor self)
void CancelCapture(Actor self)
{
if (!targetManager.CanBeTargetedBy(actor, self, manager))
Cancel(self);
return base.Tick(self);
manager.CancelCapture(self, enterActor, enterCaptureManager);
}
}
}

View File

@@ -9,54 +9,79 @@
*/
#endregion
using System;
using System.Drawing;
using System.Linq;
using OpenRA.Mods.Common.Effects;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Activities
{
class Demolish : Enter
{
readonly Actor target;
readonly IDemolishable[] demolishables;
readonly int delay;
readonly int flashes;
readonly int flashesDelay;
readonly int flashInterval;
readonly INotifyDemolition[] notifiers;
readonly EnterBehaviour enterBehaviour;
public Demolish(Actor self, Actor target, EnterBehaviour enterBehaviour, int delay,
Actor enterActor;
IDemolishable[] enterDemolishables;
public Demolish(Actor self, Target target, EnterBehaviour enterBehaviour, int delay,
int flashes, int flashesDelay, int flashInterval)
: base(self, target, enterBehaviour)
: base(self, target, Color.Red)
{
this.target = target;
demolishables = target.TraitsImplementing<IDemolishable>().ToArray();
notifiers = self.TraitsImplementing<INotifyDemolition>().ToArray();
this.delay = delay;
this.flashes = flashes;
this.flashesDelay = flashesDelay;
this.flashInterval = flashInterval;
this.enterBehaviour = enterBehaviour;
}
protected override bool CanReserve(Actor self)
protected override bool TryStartEnter(Actor self, Actor targetActor)
{
return demolishables.Any(i => i.IsValidTarget(target, self));
enterActor = targetActor;
enterDemolishables = targetActor.TraitsImplementing<IDemolishable>().ToArray();
// Make sure we can still demolish the target before entering
// (but not before, because this may stop the actor in the middle of nowhere)
if (!enterDemolishables.Any(i => i.IsValidTarget(enterActor, self)))
{
Cancel(self, true);
return false;
}
return true;
}
protected override void OnInside(Actor self)
protected override void OnEnterComplete(Actor self, Actor targetActor)
{
self.World.AddFrameEndTask(w =>
{
if (target.IsDead)
// Make sure the target hasn't changed while entering
// OnEnterComplete is only called if targetActor is alive
if (targetActor != enterActor)
return;
w.Add(new FlashTarget(target, count: flashes, delay: flashesDelay, interval: flashInterval));
if (!enterDemolishables.Any(i => i.IsValidTarget(enterActor, self)))
return;
w.Add(new FlashTarget(enterActor, count: flashes, delay: flashesDelay, interval: flashInterval));
foreach (var ind in notifiers)
ind.Demolishing(self);
foreach (var d in demolishables)
d.Demolish(target, self, delay);
foreach (var d in enterDemolishables)
d.Demolish(enterActor, self, delay);
if (enterBehaviour == EnterBehaviour.Dispose)
self.Dispose();
else if (enterBehaviour == EnterBehaviour.Suicide)
self.Kill(self);
});
}
}

View File

@@ -9,44 +9,44 @@
*/
#endregion
using System.Drawing;
using OpenRA.Mods.Common.Effects;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Activities
{
class DonateCash : Enter
{
readonly Actor target;
readonly int payload;
readonly int experience;
readonly int playerExperience;
public DonateCash(Actor self, Actor target, int payload, int playerExperience)
: base(self, target, EnterBehaviour.Dispose)
public DonateCash(Actor self, Target target, int payload, int playerExperience)
: base(self, target, Color.Yellow)
{
this.target = target;
this.payload = payload;
this.experience = playerExperience;
this.playerExperience = playerExperience;
}
protected override void OnInside(Actor self)
protected override void OnEnterComplete(Actor self, Actor targetActor)
{
if (target.IsDead)
return;
var donated = target.Owner.PlayerActor.Trait<PlayerResources>().ChangeCash(payload);
var targetOwner = targetActor.Owner;
var donated = targetOwner.PlayerActor.Trait<PlayerResources>().ChangeCash(payload);
var exp = self.Owner.PlayerActor.TraitOrDefault<PlayerExperience>();
if (exp != null && target.Owner != self.Owner)
exp.GiveExperience(experience);
if (exp != null && targetOwner != self.Owner)
exp.GiveExperience(playerExperience);
if (self.Owner.IsAlliedWith(self.World.RenderPlayer))
self.World.AddFrameEndTask(w => w.Add(new FloatingText(target.CenterPosition, target.Owner.Color.RGB, FloatingText.FormatCashTick(donated), 30)));
self.World.AddFrameEndTask(w => w.Add(new FloatingText(targetActor.CenterPosition, targetOwner.Color.RGB, FloatingText.FormatCashTick(donated), 30)));
foreach (var nct in target.TraitsImplementing<INotifyCashTransfer>())
nct.OnAcceptingCash(target, self);
foreach (var nct in targetActor.TraitsImplementing<INotifyCashTransfer>())
nct.OnAcceptingCash(targetActor, self);
foreach (var nct in self.TraitsImplementing<INotifyCashTransfer>())
nct.OnDeliveringCash(self, target);
nct.OnDeliveringCash(self, targetActor);
self.Dispose();
}
}
}

View File

@@ -9,45 +9,60 @@
*/
#endregion
using System.Drawing;
using OpenRA.Activities;
using OpenRA.Mods.Common.Effects;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Activities
{
class DonateExperience : Enter
{
readonly Actor target;
readonly GainsExperience targetGainsExperience;
readonly int level;
readonly int playerExperience;
public DonateExperience(Actor self, Actor target, int level, int playerExperience, GainsExperience targetGainsExperience)
: base(self, target, EnterBehaviour.Dispose)
Actor enterActor;
GainsExperience enterGainsExperience;
public DonateExperience(Actor self, Target target, int level, int playerExperience)
: base(self, target, Color.Yellow)
{
this.target = target;
this.level = level;
this.playerExperience = playerExperience;
this.targetGainsExperience = targetGainsExperience;
}
protected override void OnInside(Actor self)
protected override bool TryStartEnter(Actor self, Actor targetActor)
{
if (target.IsDead)
enterActor = targetActor;
enterGainsExperience = targetActor.TraitOrDefault<GainsExperience>();
if (enterGainsExperience == null || enterGainsExperience.Level == enterGainsExperience.MaxLevel)
{
Cancel(self, true);
return false;
}
return true;
}
protected override void OnEnterComplete(Actor self, Actor targetActor)
{
// Make sure the target hasn't changed while entering
// OnEnterComplete is only called if targetActor is alive
if (targetActor != enterActor)
return;
targetGainsExperience.GiveLevels(level);
if (enterGainsExperience.Level == enterGainsExperience.MaxLevel)
return;
enterGainsExperience.GiveLevels(level);
var exp = self.Owner.PlayerActor.TraitOrDefault<PlayerExperience>();
if (exp != null && target.Owner != self.Owner)
if (exp != null && enterActor.Owner != self.Owner)
exp.GiveExperience(playerExperience);
}
public override Activity Tick(Actor self)
{
if (target.IsDead || targetGainsExperience.Level == targetGainsExperience.MaxLevel)
Cancel(self);
return base.Tick(self);
self.Dispose();
}
}
}

View File

@@ -1,6 +1,6 @@
#region Copyright & License Information
/*
* Copyright 2007-2018 The OpenRA Developers (see AUTHORS)
* Copyright 2007-2019 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, either version 3 of
@@ -10,6 +10,7 @@
#endregion
using System;
using System.Drawing;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
@@ -21,287 +22,154 @@ namespace OpenRA.Mods.Common.Activities
public abstract class Enter : Activity
{
public enum ReserveStatus { None, TooFar, Pending, Ready }
enum EnterState { ApproachingOrEntering, WaitingToEnter, Inside, Exiting, Done }
enum EnterState { Approaching, Waiting, Entering, Exiting }
readonly IMove move;
readonly int maxTries = 0;
readonly EnterBehaviour enterBehaviour;
readonly bool repathWhileMoving;
readonly Color? targetLineColor;
public Target Target { get { return target; } }
Target target;
EnterState nextState = EnterState.ApproachingOrEntering; // Hint/starting point for next state
bool isEnteringOrInside = false; // Used to know if exiting should be used
WPos savedPos; // Position just before entering
Activity inner;
bool firstApproach = true;
Target lastVisibleTarget;
bool useLastVisibleTarget;
EnterState lastState = EnterState.Approaching;
protected Enter(Actor self, Actor target, EnterBehaviour enterBehaviour, int maxTries = 1, bool repathWhileMoving = true)
protected Enter(Actor self, Target target, Color? targetLineColor = null)
{
move = self.Trait<IMove>();
this.target = Target.FromActor(target);
this.maxTries = maxTries;
this.enterBehaviour = enterBehaviour;
this.repathWhileMoving = repathWhileMoving;
this.target = target;
this.targetLineColor = targetLineColor;
}
// CanEnter(target) should to be true; otherwise, Enter may abort.
// Tries counter starts at 1 (reset every tick)
protected virtual bool TryGetAlternateTarget(Actor self, int tries, ref Target target) { return false; }
protected virtual bool CanReserve(Actor self) { return true; }
protected virtual ReserveStatus Reserve(Actor self)
{
return !CanReserve(self) ? ReserveStatus.None : move.CanEnterTargetNow(self, target) ? ReserveStatus.Ready : ReserveStatus.TooFar;
}
protected virtual void Unreserve(Actor self, bool abort) { }
protected virtual void OnInside(Actor self) { }
/// <summary>
/// Called when the actor is ready to transition from approaching to entering the target.
/// Return true to start entering, or false to wait in the WaitingToEnter state.
/// Called early in the activity tick to allow subclasses to update state.
/// Call Cancel(self, true) if it is no longer valid to enter
/// </summary>
protected virtual bool TryStartEnter(Actor self) { return true; }
protected virtual void TickInner(Actor self, Target target, bool targetIsDeadOrHiddenActor) { }
protected bool TryGetAlternateTargetInCircle(
Actor self, WDist radius, Action<Target> update, Func<Actor, bool> primaryFilter, Func<Actor, bool>[] preferenceFilters = null)
/// <summary>
/// Called when the actor is ready to transition from approaching to entering the target actor.
/// Return true to start entering, or false to wait in the WaitingToEnter state.
/// Call Cancel(self, true) before returning false if it is no longer valid to enter
/// </summary>
protected virtual bool TryStartEnter(Actor self, Actor targetActor) { return true; }
/// <summary>
/// Called when the actor has entered the target actor
/// Return true if the action succeeded and the actor should be Killed/Disposed
/// (assuming the relevant EnterBehaviour), or false if the actor should exit unharmed
/// </summary>
protected virtual void OnEnterComplete(Actor self, Actor targetActor) { }
/// <summary>
/// Called when the activity is cancelled to allow subclasses to clean up their own state.
/// </summary>
protected virtual void OnCancel(Actor self) { }
Activity moveActivity;
public override Activity Tick(Actor self)
{
var diff = new WVec(radius, radius, WDist.Zero);
var candidates = self.World.ActorMap.ActorsInBox(self.CenterPosition - diff, self.CenterPosition + diff)
.Where(primaryFilter).Select(a => new { Actor = a, Ls = (self.CenterPosition - a.CenterPosition).HorizontalLengthSquared })
.Where(p => p.Ls <= radius.LengthSquared).OrderBy(p => p.Ls).Select(p => p.Actor);
if (preferenceFilters != null)
foreach (var filter in preferenceFilters)
// Update our view of the target
bool targetIsHiddenActor;
target = target.Recalculate(self.Owner, out targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
lastVisibleTarget = Target.FromTargetPositions(target);
var oldUseLastVisibleTarget = useLastVisibleTarget;
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
// Cancel immediately if the target died while we were entering it
if (!IsCanceled && useLastVisibleTarget && lastState == EnterState.Entering)
Cancel(self, true);
TickInner(self, target, useLastVisibleTarget);
// Update target lines if required
if (useLastVisibleTarget != oldUseLastVisibleTarget && targetLineColor.HasValue)
self.SetTargetLine(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value, false);
// We need to wait for movement to finish before transitioning to
// the next state or next activity
if (moveActivity != null)
{
moveActivity = ActivityUtils.RunActivity(self, moveActivity);
if (moveActivity != null)
return this;
}
// Note that lastState refers to what we have just *finished* doing
switch (lastState)
{
case EnterState.Approaching:
case EnterState.Waiting:
{
var preferredCandidate = candidates.FirstOrDefault(filter);
if (preferredCandidate == null)
continue;
target = Target.FromActor(preferredCandidate);
update(target);
return true;
// NOTE: We can safely cancel in this case because we know the
// actor has finished any in-progress move activities
if (IsCanceled)
return NextActivity;
// Lost track of the target
if (useLastVisibleTarget && lastVisibleTarget.Type == TargetType.Invalid)
return NextActivity;
// We are not next to the target - lets fix that
if (target.Type != TargetType.Invalid && !move.CanEnterTargetNow(self, target))
{
lastState = EnterState.Approaching;
// Target lines are managed by this trait, so we do not pass targetLineColor
var initialTargetPosition = (useLastVisibleTarget ? lastVisibleTarget : target).CenterPosition;
moveActivity = ActivityUtils.RunActivity(self, move.MoveToTarget(self, target, initialTargetPosition));
break;
}
// We are next to where we thought the target should be, but it isn't here
// There's not much more we can do here
if (useLastVisibleTarget || target.Type != TargetType.Actor)
return NextActivity;
// Are we ready to move into the target?
if (TryStartEnter(self, target.Actor))
{
lastState = EnterState.Entering;
moveActivity = ActivityUtils.RunActivity(self, move.MoveIntoTarget(self, target));
return this;
}
// Subclasses can cancel the activity during TryStartEnter
// Return immediately to avoid an extra tick's delay
if (IsCanceled)
return NextActivity;
lastState = EnterState.Waiting;
break;
}
var candidate = candidates.FirstOrDefault();
if (candidate == null)
return false;
target = Target.FromActor(candidate);
update(target);
return true;
}
case EnterState.Entering:
{
// Check that we reached the requested position
var targetPos = target.Positions.PositionClosestTo(self.CenterPosition);
if (!IsCanceled && self.CenterPosition == targetPos && target.Type == TargetType.Actor)
OnEnterComplete(self, target.Actor);
// Called when inner activity is this and returns inner activity for next tick.
protected virtual Activity InsideTick(Actor self) { return null; }
lastState = EnterState.Exiting;
moveActivity = ActivityUtils.RunActivity(self, move.MoveIntoWorld(self, self.Location));
break;
}
// Abort entering and/or leave if necessary
protected virtual void AbortOrExit(Actor self)
{
if (nextState == EnterState.Done)
return;
nextState = isEnteringOrInside ? EnterState.Exiting : EnterState.Done;
if (inner == this)
inner = null;
else if (inner != null)
inner.Cancel(self);
if (isEnteringOrInside)
Unreserve(self, true);
}
case EnterState.Exiting:
return NextActivity;
}
// Cancel inner activity and mark as done unless already leaving or done
protected void Done(Actor self)
{
if (nextState == EnterState.Done)
return;
nextState = EnterState.Done;
if (inner == this)
inner = null;
else if (inner != null)
inner.Cancel(self);
return this;
}
public override bool Cancel(Actor self, bool keepQueue = false)
{
AbortOrExit(self);
if (nextState < EnterState.Exiting)
return base.Cancel(self);
else
NextActivity = null;
OnCancel(self);
return true;
}
if (!IsCanceled && moveActivity != null && !moveActivity.Cancel(self))
return false;
ReserveStatus TryReserveElseTryAlternateReserve(Actor self)
{
for (var tries = 0;;)
{
switch (Reserve(self))
{
case ReserveStatus.None:
if (++tries > maxTries || !TryGetAlternateTarget(self, tries, ref target))
return ReserveStatus.None;
continue;
case ReserveStatus.TooFar:
// Always goto to transport on first approach
if (firstApproach)
{
firstApproach = false;
return ReserveStatus.TooFar;
}
if (++tries > maxTries)
return ReserveStatus.TooFar;
Target t = target;
if (!TryGetAlternateTarget(self, tries, ref t))
return ReserveStatus.TooFar;
var targetPosition = target.Positions.PositionClosestTo(self.CenterPosition);
var alternatePosition = t.Positions.PositionClosestTo(self.CenterPosition);
if ((targetPosition - self.CenterPosition).HorizontalLengthSquared <= (alternatePosition - self.CenterPosition).HorizontalLengthSquared)
return ReserveStatus.TooFar;
target = t;
continue;
case ReserveStatus.Pending:
return ReserveStatus.Pending;
case ReserveStatus.Ready:
return ReserveStatus.Ready;
}
}
}
EnterState FindAndTransitionToNextState(Actor self)
{
switch (nextState)
{
case EnterState.ApproachingOrEntering:
// Reserve to enter or approach
isEnteringOrInside = false;
switch (TryReserveElseTryAlternateReserve(self))
{
case ReserveStatus.None:
return EnterState.Done; // No available target -> abort to next activity
case ReserveStatus.TooFar:
{
var moveTarget = repathWhileMoving ? target : Target.FromPos(target.Positions.PositionClosestTo(self.CenterPosition));
inner = move.MoveToTarget(self, moveTarget); // Approach
return EnterState.ApproachingOrEntering;
}
case ReserveStatus.Pending:
return EnterState.ApproachingOrEntering; // Retry next tick
case ReserveStatus.Ready:
break; // Reserved target -> start entering target
}
// Can we enter yet?
if (!TryStartEnter(self))
return EnterState.WaitingToEnter;
// Entering
isEnteringOrInside = true;
savedPos = self.CenterPosition; // Save position of self, before entering, for returning on exit
inner = move.MoveIntoTarget(self, target); // Enter
if (inner != null)
{
nextState = EnterState.Inside; // Should be inside once inner activity is null
return EnterState.ApproachingOrEntering;
}
// Can enter but there is no activity for it, so go inside without one
goto case EnterState.Inside;
case EnterState.Inside:
// Might as well teleport into target if there is no MoveIntoTarget activity
if (nextState == EnterState.ApproachingOrEntering)
nextState = EnterState.Inside;
// Otherwise, try to recover from moving target
else if (target.Positions.PositionClosestTo(self.CenterPosition) != self.CenterPosition)
{
nextState = EnterState.ApproachingOrEntering;
Unreserve(self, false);
if (Reserve(self) == ReserveStatus.Ready)
{
inner = move.MoveIntoTarget(self, target); // Enter
if (inner != null)
return EnterState.ApproachingOrEntering;
nextState = EnterState.ApproachingOrEntering;
goto case EnterState.ApproachingOrEntering;
}
nextState = EnterState.ApproachingOrEntering;
isEnteringOrInside = false;
inner = move.MoveIntoWorld(self, self.World.Map.CellContaining(savedPos));
return EnterState.ApproachingOrEntering;
}
OnInside(self);
if (enterBehaviour == EnterBehaviour.Suicide)
self.Kill(self);
else if (enterBehaviour == EnterBehaviour.Dispose)
self.Dispose();
// Return if Abort(Actor) or Done(self) was called from OnInside.
if (nextState >= EnterState.Exiting)
return EnterState.Inside;
inner = this; // Start inside activity
nextState = EnterState.Exiting; // Exit once inner activity is null (unless Done(self) is called)
return EnterState.Inside;
// TODO: Handle target moved while inside or always call done for movable targets and use a separate exit activity
case EnterState.Exiting:
inner = move.MoveIntoWorld(self, self.World.Map.CellContaining(savedPos));
// If not successfully exiting, retry on next tick
if (inner == null)
return EnterState.Exiting;
isEnteringOrInside = false;
nextState = EnterState.Done;
return EnterState.Exiting;
case EnterState.Done:
return EnterState.Done;
}
return EnterState.Done; // dummy to quiet dumb compiler
}
Activity CanceledTick(Actor self)
{
if (inner == null)
return ActivityUtils.RunActivity(self, NextActivity);
inner.Cancel(self);
inner.Queue(NextActivity);
return ActivityUtils.RunActivity(self, inner);
}
public override Activity Tick(Actor self)
{
if (IsCanceled)
return CanceledTick(self);
// Check target validity if not exiting or done
if (nextState != EnterState.Done && (target.Type != TargetType.Actor || !target.IsValidFor(self)))
AbortOrExit(self);
// If no current activity, tick next activity
if (inner == null && FindAndTransitionToNextState(self) == EnterState.Done)
return CanceledTick(self);
// Run inner activity/InsideTick
inner = inner == this ? InsideTick(self) : ActivityUtils.RunActivity(self, inner);
// If we are finished, move on to next activity
if (inner == null && nextState == EnterState.Done)
return NextActivity;
return this;
return base.Cancel(self, keepQueue);
}
}
}

View File

@@ -10,6 +10,9 @@
#endregion
using System;
using System.Drawing;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
@@ -18,59 +21,124 @@ namespace OpenRA.Mods.Common.Activities
class EnterTransport : Enter
{
readonly Passenger passenger;
readonly int maxTries;
Actor transport;
Cargo cargo;
public EnterTransport(Actor self, Actor transport, int maxTries = 0, bool repathWhileMoving = true)
: base(self, transport, EnterBehaviour.Exit, maxTries, repathWhileMoving)
Actor enterActor;
Cargo enterCargo;
public EnterTransport(Actor self, Target target)
: base(self, target, Color.Green)
{
this.transport = transport;
this.maxTries = maxTries;
cargo = transport.Trait<Cargo>();
passenger = self.Trait<Passenger>();
}
protected override void Unreserve(Actor self, bool abort) { passenger.Unreserve(self); }
protected override bool CanReserve(Actor self) { return cargo.Unloading || cargo.CanLoad(transport, self); }
protected override ReserveStatus Reserve(Actor self)
protected override bool TryStartEnter(Actor self, Actor targetActor)
{
var status = base.Reserve(self);
if (status != ReserveStatus.Ready)
return status;
if (passenger.Reserve(self, cargo))
return ReserveStatus.Ready;
return ReserveStatus.Pending;
enterActor = targetActor;
enterCargo = targetActor.TraitOrDefault<Cargo>();
// Make sure we can still enter the transport
// (but not before, because this may stop the actor in the middle of nowhere)
if (enterCargo == null || !passenger.Reserve(self, enterCargo))
{
Cancel(self, true);
return false;
}
return true;
}
protected override void OnInside(Actor self)
protected override void OnEnterComplete(Actor self, Actor targetActor)
{
self.World.AddFrameEndTask(w =>
{
if (self.IsDead || transport.IsDead || !cargo.CanLoad(transport, self))
// Make sure the target hasn't changed while entering
// OnEnterComplete is only called if targetActor is alive
if (targetActor != enterActor)
return;
cargo.Load(transport, self);
if (!enterCargo.CanLoad(enterActor, self))
return;
enterCargo.Load(enterActor, self);
w.Remove(self);
// Preemptively cancel any activities to avoid an edge-case where successively queued
// EnterTransports corrupt the actor state. Activities are cancelled again on unload
self.CancelActivity();
});
Done(self);
// Preemptively cancel any activities to avoid an edge-case where successively queued
// EnterTransports corrupt the actor state. Activities are cancelled again on unload
self.CancelActivity();
}
protected override bool TryGetAlternateTarget(Actor self, int tries, ref Target target)
protected override void OnCancel(Actor self)
{
if (tries > maxTries)
passenger.Unreserve(self);
}
protected override void OnLastRun(Actor self)
{
passenger.Unreserve(self);
}
}
class EnterTransports : Activity
{
readonly string type;
readonly Passenger passenger;
Activity enterTransport;
public EnterTransports(Actor self, Target primaryTarget)
{
passenger = self.Trait<Passenger>();
if (primaryTarget.Type == TargetType.Actor)
type = primaryTarget.Actor.Info.Name;
enterTransport = new EnterTransport(self, primaryTarget);
}
public override Activity Tick(Actor self)
{
if (enterTransport != null)
{
enterTransport = ActivityUtils.RunActivity(self, enterTransport);
if (enterTransport != null)
return this;
}
// Try and find a new transport nearby
if (IsCanceled || string.IsNullOrEmpty(type))
return NextActivity;
Func<Actor, bool> isValidTransport = a =>
{
var c = a.TraitOrDefault<Cargo>();
return c != null && c.Info.Types.Contains(passenger.Info.CargoType) &&
(c.Unloading || c.CanLoad(a, self));
};
var candidates = self.World.FindActorsInCircle(self.CenterPosition, passenger.Info.AlternateTransportScanRange)
.Where(isValidTransport)
.ToList();
// Prefer transports of the same type as the primary
var transport = candidates.Where(a => a.Info.Name == type).ClosestTo(self);
if (transport == null)
transport = candidates.ClosestTo(self);
if (transport != null)
{
enterTransport = ActivityUtils.RunActivity(self, new EnterTransport(self, Target.FromActor(transport)));
return this;
}
return NextActivity;
}
public override bool Cancel(Actor self, bool keepQueue = false)
{
if (!IsCanceled && enterTransport != null && !enterTransport.Cancel(self))
return false;
var type = target.Actor.Info.Name;
return TryGetAlternateTargetInCircle(
self, passenger.Info.AlternateTransportScanRange,
t => { transport = t.Actor; cargo = t.Actor.Trait<Cargo>(); }, // update transport and cargo
a => { var c = a.TraitOrDefault<Cargo>(); return c != null && c.Info.Types.Contains(passenger.Info.CargoType) && (c.Unloading || c.CanLoad(a, self)); },
new Func<Actor, bool>[] { a => a.Info.Name == type }); // Prefer transports of the same type
return base.Cancel(self, keepQueue);
}
}
}

View File

@@ -53,7 +53,10 @@ namespace OpenRA.Mods.Common.Activities
return NextActivity;
if (harv.IsFull)
return ActivityUtils.SequenceActivities(new DeliverResources(self), NextActivity);
{
// HACK: DeliverResources is ignored if there are queued activities, so discard NextActivity
return ActivityUtils.SequenceActivities(new DeliverResources(self));
}
var closestHarvestablePosition = ClosestHarvestablePos(self);

View File

@@ -9,6 +9,7 @@
*/
#endregion
using System.Drawing;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
@@ -17,36 +18,74 @@ namespace OpenRA.Mods.Common.Activities
{
public class Follow : Activity
{
readonly Target target;
readonly WDist minRange;
readonly WDist maxRange;
readonly IMove move;
readonly Color? targetLineColor;
Target target;
Target lastVisibleTarget;
bool useLastVisibleTarget;
bool wasMovingWithinRange;
public Follow(Actor self, Target target, WDist minRange, WDist maxRange)
public Follow(Actor self, Target target, WDist minRange, WDist maxRange,
WPos? initialTargetPosition, Color? targetLineColor = null)
{
this.target = target;
this.minRange = minRange;
this.maxRange = maxRange;
this.targetLineColor = targetLineColor;
move = self.Trait<IMove>();
// 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);
else if (initialTargetPosition.HasValue)
lastVisibleTarget = Target.FromPos(initialTargetPosition.Value);
}
public override Activity Tick(Actor self)
{
if (IsCanceled || !target.IsValidFor(self))
if (IsCanceled)
return NextActivity;
var cachedPosition = target.CenterPosition;
var path = move.MoveWithinRange(target, minRange, maxRange);
bool targetIsHiddenActor;
target = target.Recalculate(self.Owner, out targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
lastVisibleTarget = Target.FromTargetPositions(target);
// We are already in range, so wait until the target moves before doing anything
if (target.IsInRange(self.CenterPosition, maxRange) && !target.IsInRange(self.CenterPosition, minRange))
{
var wait = new WaitFor(() => !target.IsValidFor(self) || target.CenterPosition != cachedPosition);
return ActivityUtils.SequenceActivities(wait, path, this);
}
var oldUseLastVisibleTarget = useLastVisibleTarget;
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
return ActivityUtils.SequenceActivities(path, this);
// 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 NextActivity;
wasMovingWithinRange = false;
// Update target lines if required
if (useLastVisibleTarget != oldUseLastVisibleTarget && targetLineColor.HasValue)
self.SetTargetLine(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value, false);
// Target is hidden or dead, and we don't have a fallback position to move towards
if (useLastVisibleTarget && !lastVisibleTarget.IsValidFor(self))
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))
return useLastVisibleTarget ? NextActivity : this;
// Move into range
wasMovingWithinRange = true;
return ActivityUtils.SequenceActivities(
move.MoveWithinRange(target, minRange, maxRange, checkTarget.CenterPosition, targetLineColor),
this);
}
}
}

View File

@@ -386,6 +386,9 @@ namespace OpenRA.Mods.Common.Activities
public override Activity Tick(Actor self)
{
if (Move.mobile.IsTraitDisabled)
return this;
var ret = InnerTick(self, Move.mobile);
Move.mobile.IsMoving = ret is MovePart;

View File

@@ -26,38 +26,45 @@ namespace OpenRA.Mods.Common.Activities
protected readonly Mobile Mobile;
readonly IPathFinder pathFinder;
readonly DomainIndex domainIndex;
readonly Color? targetLineColor;
Target target;
bool canHideUnderFog;
protected Target Target
{
get
{
return target;
}
private set
{
target = value;
if (target.Type == TargetType.Actor)
canHideUnderFog = target.Actor.Info.HasTraitInfo<HiddenUnderFogInfo>();
return useLastVisibleTarget ? lastVisibleTarget : target;
}
}
protected CPos targetPosition;
Target target;
Target lastVisibleTarget;
protected CPos lastVisibleTargetLocation;
bool useLastVisibleTarget;
Activity inner;
bool repath;
public MoveAdjacentTo(Actor self, Target target)
public MoveAdjacentTo(Actor self, Target target, WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
Target = target;
this.target = target;
this.targetLineColor = targetLineColor;
Mobile = self.Trait<Mobile>();
pathFinder = self.World.WorldActor.Trait<IPathFinder>();
domainIndex = self.World.WorldActor.Trait<DomainIndex>();
if (target.IsValidFor(self))
targetPosition = self.World.Map.CellContaining(target.CenterPosition);
// 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);
lastVisibleTargetLocation = self.World.Map.CellContaining(target.CenterPosition);
}
else if (initialTargetPosition.HasValue)
{
lastVisibleTarget = Target.FromPos(initialTargetPosition.Value);
lastVisibleTargetLocation = self.World.Map.CellContaining(initialTargetPosition.Value);
}
repath = true;
}
@@ -67,9 +74,9 @@ namespace OpenRA.Mods.Common.Activities
return false;
}
protected virtual bool ShouldRepath(Actor self, CPos oldTargetPosition)
protected virtual bool ShouldRepath(Actor self, CPos targetLocation)
{
return targetPosition != oldTargetPosition;
return lastVisibleTargetLocation != targetLocation;
}
protected virtual IEnumerable<CPos> CandidateMovementCells(Actor self)
@@ -79,19 +86,30 @@ namespace OpenRA.Mods.Common.Activities
public override Activity Tick(Actor self)
{
var targetIsValid = Target.IsValidFor(self);
// Target moved under the fog. Move to its last known position.
if (Target.Type == TargetType.Actor && canHideUnderFog
&& !Target.Actor.CanBeViewedByPlayer(self.Owner))
bool targetIsHiddenActor;
var oldTargetLocation = lastVisibleTargetLocation;
target = target.Recalculate(self.Owner, out targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
{
if (inner != null)
inner.Cancel(self);
self.SetTargetLine(Target.FromCell(self.World, targetPosition), Color.Green);
return ActivityUtils.RunActivity(self, new AttackMoveActivity(self, Mobile.MoveTo(targetPosition, 0)));
lastVisibleTarget = Target.FromTargetPositions(target);
lastVisibleTargetLocation = self.World.Map.CellContaining(target.CenterPosition);
}
var oldUseLastVisibleTarget = useLastVisibleTarget;
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
// Update target lines if required
if (useLastVisibleTarget != oldUseLastVisibleTarget && targetLineColor.HasValue)
self.SetTargetLine(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value, false);
// Target is hidden or dead, and we don't have a fallback position to move towards
if (useLastVisibleTarget && !lastVisibleTarget.IsValidFor(self))
return NextActivity;
// Target is equivalent to checkTarget variable in other activities
// value is either lastVisibleTarget or target based on visibility and validity
var targetIsValid = Target.IsValidFor(self);
// Inner move order has completed.
if (inner == null)
{
@@ -105,26 +123,16 @@ namespace OpenRA.Mods.Common.Activities
repath = false;
}
if (targetIsValid)
// Cancel the current path if the activity asks to stop, or asks to repath
// The repath happens once the move activity stops in the next cell
var shouldStop = ShouldStop(self, oldTargetLocation);
var shouldRepath = targetIsValid && !repath && ShouldRepath(self, oldTargetLocation);
if (shouldStop || shouldRepath)
{
// Check if the target has moved
var oldTargetPosition = targetPosition;
targetPosition = self.World.Map.CellContaining(Target.CenterPosition);
if (inner != null)
inner.Cancel(self);
var shouldStop = ShouldStop(self, oldTargetPosition);
if (shouldStop || (!repath && ShouldRepath(self, oldTargetPosition)))
{
// Finish moving into the next cell and then repath.
if (inner != null)
inner.Cancel(self);
repath = !shouldStop;
}
}
else
{
// Target became invalid. Move to its last known position.
Target = Target.FromCell(self.World, targetPosition);
repath = shouldRepath;
}
// Ticks the inner move activity to actually move the actor.
@@ -147,7 +155,7 @@ namespace OpenRA.Mods.Common.Activities
return NoPath;
using (var fromSrc = PathSearch.FromPoints(self.World, Mobile.Info.LocomotorInfo, self, searchCells, loc, true))
using (var fromDest = PathSearch.FromPoint(self.World, Mobile.Info.LocomotorInfo, self, loc, targetPosition, true).Reverse())
using (var fromDest = PathSearch.FromPoint(self.World, Mobile.Info.LocomotorInfo, self, loc, lastVisibleTargetLocation, true).Reverse())
return pathFinder.FindBidiPath(fromSrc, fromDest);
}

View File

@@ -10,6 +10,7 @@
#endregion
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using OpenRA.Traits;
@@ -20,8 +21,9 @@ namespace OpenRA.Mods.Common.Activities
readonly WDist maxRange;
readonly WDist minRange;
public MoveWithinRange(Actor self, Target target, WDist minRange, WDist maxRange)
: base(self, target)
public MoveWithinRange(Actor self, Target target, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
: base(self, target, initialTargetPosition, targetLineColor)
{
this.minRange = minRange;
this.maxRange = maxRange;
@@ -36,7 +38,7 @@ namespace OpenRA.Mods.Common.Activities
protected override bool ShouldRepath(Actor self, CPos oldTargetPosition)
{
return targetPosition != oldTargetPosition && (!AtCorrectRange(self.CenterPosition)
return lastVisibleTargetLocation != oldTargetPosition && (!AtCorrectRange(self.CenterPosition)
|| !Mobile.CanInteractWithGroundLayer(self));
}
@@ -46,7 +48,7 @@ namespace OpenRA.Mods.Common.Activities
var maxCells = (maxRange.Length + 1023) / 1024;
var minCells = minRange.Length / 1024;
return map.FindTilesInAnnulus(targetPosition, minCells, maxCells)
return map.FindTilesInAnnulus(lastVisibleTargetLocation, minCells, maxCells)
.Where(c => AtCorrectRange(map.CenterOfSubCell(c, Mobile.FromSubCell)));
}

View File

@@ -0,0 +1,97 @@
#region Copyright & License Information
/*
* Copyright 2007-2019 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, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Activities
{
public class VisualMoveIntoTarget : Activity
{
readonly Mobile mobile;
readonly Target target;
readonly WDist targetMovementThreshold;
WPos targetStartPos;
public VisualMoveIntoTarget(Actor self, Target target, WDist targetMovementThreshold)
{
mobile = self.Trait<Mobile>();
this.target = target;
this.targetMovementThreshold = targetMovementThreshold;
}
protected override void OnFirstRun(Actor self)
{
targetStartPos = target.Positions.PositionClosestTo(self.CenterPosition);
mobile.IsMoving = true;
}
public override Activity Tick(Actor self)
{
if (ChildActivity != null)
{
ChildActivity = ActivityUtils.RunActivity(self, ChildActivity);
if (ChildActivity != null)
return this;
}
if (IsCanceled || target.Type == TargetType.Invalid)
return NextActivity;
if (mobile.IsTraitDisabled)
return this;
var currentPos = self.CenterPosition;
var targetPos = target.Positions.PositionClosestTo(currentPos);
// Give up if the target has moved too far
if (targetMovementThreshold > WDist.Zero && (targetPos - targetStartPos).LengthSquared > targetMovementThreshold.LengthSquared)
return NextActivity;
// Turn if required
var delta = targetPos - currentPos;
var facing = delta.HorizontalLengthSquared != 0 ? delta.Yaw.Facing : mobile.Facing;
if (facing != mobile.Facing)
{
var turn = ActivityUtils.RunActivity(self, new Turn(self, facing));
if (turn != null)
QueueChild(turn);
return this;
}
// Can complete the move in this step
var speed = mobile.MovementSpeedForCell(self, self.Location);
if (delta.LengthSquared <= speed * speed)
{
mobile.SetVisualPosition(self, targetPos);
return NextActivity;
}
// Move towards the target
mobile.SetVisualPosition(self, currentPos + delta * speed / delta.Length);
return this;
}
protected override void OnLastRun(Actor self)
{
mobile.IsMoving = false;
}
public override IEnumerable<Target> GetTargets(Actor self)
{
yield return target;
}
}
}

View File

@@ -9,6 +9,7 @@
*/
#endregion
using System.Drawing;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
@@ -72,7 +73,8 @@ namespace OpenRA.Mods.Common.Activities
switch (state)
{
case PickupState.Intercept:
innerActivity = movement.MoveWithinRange(Target.FromActor(cargo), WDist.FromCells(4));
innerActivity = movement.MoveWithinRange(Target.FromActor(cargo), WDist.FromCells(4),
targetLineColor: Color.Yellow);
state = PickupState.LockCarryable;
return this;

View File

@@ -9,6 +9,7 @@
*/
#endregion
using System.Drawing;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
@@ -16,49 +17,70 @@ namespace OpenRA.Mods.Common.Activities
{
class RepairBridge : Enter
{
readonly Actor target;
readonly LegacyBridgeHut legacyHut;
readonly BridgeHut hut;
readonly EnterBehaviour enterBehaviour;
readonly string notification;
public RepairBridge(Actor self, Actor target, EnterBehaviour enterBehaviour, string notification)
: base(self, target, enterBehaviour)
Actor enterActor;
BridgeHut enterHut;
LegacyBridgeHut enterLegacyHut;
public RepairBridge(Actor self, Target target, EnterBehaviour enterBehaviour, string notification)
: base(self, target, Color.Yellow)
{
this.target = target;
legacyHut = target.TraitOrDefault<LegacyBridgeHut>();
hut = target.TraitOrDefault<BridgeHut>();
this.enterBehaviour = enterBehaviour;
this.notification = notification;
}
protected override bool CanReserve(Actor self)
bool CanEnterHut()
{
if (legacyHut != null)
return legacyHut.BridgeDamageState != DamageState.Undamaged && !legacyHut.Repairing && legacyHut.Bridge.GetHut(0) != null && legacyHut.Bridge.GetHut(1) != null;
if (enterLegacyHut != null)
return enterLegacyHut.BridgeDamageState != DamageState.Undamaged && !enterLegacyHut.Repairing &&
enterLegacyHut.Bridge.GetHut(0) != null && enterLegacyHut.Bridge.GetHut(1) != null;
if (hut != null)
return hut.BridgeDamageState != DamageState.Undamaged && !hut.Repairing;
if (enterHut != null)
return enterHut.BridgeDamageState != DamageState.Undamaged && !enterHut.Repairing;
return false;
}
protected override void OnInside(Actor self)
protected override bool TryStartEnter(Actor self, Actor targetActor)
{
if (legacyHut != null)
{
if (legacyHut.BridgeDamageState == DamageState.Undamaged || legacyHut.Repairing || legacyHut.Bridge.GetHut(0) == null || legacyHut.Bridge.GetHut(1) == null)
return;
enterActor = targetActor;
enterLegacyHut = enterActor.TraitOrDefault<LegacyBridgeHut>();
enterHut = enterActor.TraitOrDefault<BridgeHut>();
legacyHut.Repair(self);
}
else if (hut != null)
// Make sure we can still repair the target before entering
// (but not before, because this may stop the actor in the middle of nowhere)
if (!CanEnterHut())
{
if (hut.BridgeDamageState == DamageState.Undamaged || hut.Repairing)
return;
hut.Repair(target, self);
Cancel(self, true);
return false;
}
return true;
}
protected override void OnEnterComplete(Actor self, Actor targetActor)
{
// Make sure the target hasn't changed while entering
// OnEnterComplete is only called if targetActor is alive
if (targetActor != enterActor)
return;
if (!CanEnterHut())
return;
if (enterLegacyHut != null)
enterLegacyHut.Repair(self);
else if (enterHut != null)
enterHut.Repair(enterActor, self);
Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", notification, self.Owner.Faction.InternalName);
if (enterBehaviour == EnterBehaviour.Dispose)
self.Dispose();
else if (enterBehaviour == EnterBehaviour.Suicide)
self.Kill(self);
}
}
}

View File

@@ -9,39 +9,66 @@
*/
#endregion
using System.Drawing;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Activities
{
class RepairBuilding : Enter
{
readonly Actor target;
readonly IHealth health;
readonly EnterBehaviour enterBehaviour;
readonly Stance validStances;
public RepairBuilding(Actor self, Actor target, EnterBehaviour enterBehaviour, Stance validStances)
: base(self, target, enterBehaviour)
Actor enterActor;
IHealth enterHealth;
public RepairBuilding(Actor self, Target target, EnterBehaviour enterBehaviour, Stance validStances)
: base(self, target, Color.Yellow)
{
this.target = target;
this.enterBehaviour = enterBehaviour;
this.validStances = validStances;
health = target.Trait<IHealth>();
}
protected override bool CanReserve(Actor self)
protected override bool TryStartEnter(Actor self, Actor targetActor)
{
return health.DamageState != DamageState.Undamaged;
enterActor = targetActor;
enterHealth = targetActor.TraitOrDefault<IHealth>();
// Make sure we can still repair the target before entering
// (but not before, because this may stop the actor in the middle of nowhere)
var stance = self.Owner.Stances[enterActor.Owner];
if (enterHealth == null || enterHealth.DamageState == DamageState.Undamaged || !stance.HasStance(validStances))
{
Cancel(self, true);
return false;
}
return true;
}
protected override void OnInside(Actor self)
protected override void OnEnterComplete(Actor self, Actor targetActor)
{
var stance = self.Owner.Stances[target.Owner];
// Make sure the target hasn't changed while entering
// OnEnterComplete is only called if targetActor is alive
if (targetActor != enterActor)
return;
if (enterHealth.DamageState == DamageState.Undamaged)
return;
var stance = self.Owner.Stances[enterActor.Owner];
if (!stance.HasStance(validStances))
return;
if (health.DamageState == DamageState.Undamaged)
if (enterHealth.DamageState == DamageState.Undamaged)
return;
target.InflictDamage(self, new Damage(-health.MaxHP));
enterActor.InflictDamage(self, new Damage(-enterHealth.MaxHP));
if (enterBehaviour == EnterBehaviour.Dispose)
self.Dispose();
else if (enterBehaviour == EnterBehaviour.Suicide)
self.Kill(self);
}
}
}

View File

@@ -135,7 +135,7 @@ namespace OpenRA.Mods.Common.Activities
self.ReplacedByActor = a;
if (selected)
w.Selection.Add(w, a);
w.Selection.Add(a);
if (controlgroup.HasValue)
w.Selection.AddToControlGroup(a, controlgroup.Value);

View File

@@ -61,7 +61,7 @@ namespace OpenRA.Mods.Common.Activities
protected override void OnLastRun(Actor self)
{
// If Mobile.IsMoving was set to 'true' earlier, we want to reset it to 'false' before the next tick.
if (mobile != null && mobile.IsMoving)
if (setIsMoving && mobile != null && mobile.IsMoving)
mobile.IsMoving = false;
}
}

View File

@@ -59,42 +59,6 @@ namespace OpenRA.Mods.Common
return stance == Stance.Enemy;
}
/// <summary>
/// DEPRECATED: Write code that can handle FrozenActors correctly instead.
/// </summary>
public static Target ResolveFrozenActorOrder(this Actor self, Order order, Color targetLine)
{
// Not targeting a frozen actor
if (order.Target.Type != TargetType.FrozenActor)
return order.Target;
var frozen = order.Target.FrozenActor;
self.SetTargetLine(order.Target, targetLine, true);
// Target is still alive - resolve the real order
if (frozen.Actor != null && frozen.Actor.IsInWorld)
return Target.FromActor(frozen.Actor);
if (!order.Queued)
self.CancelActivity();
var move = self.TraitOrDefault<IMove>();
if (move != null)
{
// Move within sight range of the frozen actor
var range = self.TraitsImplementing<RevealsShroud>()
.Where(s => !s.IsTraitDisabled)
.Select(s => s.Range)
.Append(WDist.FromCells(2))
.Max();
self.QueueActivity(move.MoveWithinRange(Target.FromPos(frozen.CenterPosition), range));
}
return Target.Invalid;
}
public static void NotifyBlocker(this Actor self, IEnumerable<Actor> blockers)
{
foreach (var blocker in blockers)

View File

@@ -103,6 +103,7 @@
<Compile Include="Activities\Move\Move.cs" />
<Compile Include="Activities\Move\MoveAdjacentTo.cs" />
<Compile Include="Activities\Move\MoveWithinRange.cs" />
<Compile Include="Activities\Move\VisualMoveIntoTarget.cs" />
<Compile Include="Activities\Parachute.cs" />
<Compile Include="Activities\Rearm.cs" />
<Compile Include="Activities\RemoveSelf.cs" />
@@ -120,6 +121,7 @@
<Compile Include="Activities\WaitForTransport.cs" />
<Compile Include="ActorExts.cs" />
<Compile Include="AIUtils.cs" />
<Compile Include="TargetExtensions.cs" />
<Compile Include="Traits\BotModules\Squads\AttackOrFleeFuzzy.cs" />
<Compile Include="Traits\BotModules\BotModuleLogic\BaseBuilderQueueManager.cs" />
<Compile Include="Traits\Player\ModularBot.cs" />
@@ -288,7 +290,6 @@
<Compile Include="Traits\Attack\AttackFrontal.cs" />
<Compile Include="Traits\Attack\AttackGarrisoned.cs" />
<Compile Include="Traits\Attack\AttackOmni.cs" />
<Compile Include="Traits\Attack\AttackSuicides.cs" />
<Compile Include="Traits\Attack\AttackTurreted.cs" />
<Compile Include="Traits\AttackWander.cs" />
<Compile Include="Traits\AutoTarget.cs" />
@@ -599,6 +600,7 @@
<Compile Include="Traits\World\ActorSpawnManager.cs" />
<Compile Include="Traits\ActorSpawner.cs" />
<Compile Include="UpdateRules\Rules\20181215\RemoveAttackIgnoresVisibility.cs" />
<Compile Include="UpdateRules\Rules\20181215\RemoveAttackSuicides.cs" />
<Compile Include="UpdateRules\Rules\20181215\RemovedDemolishLocking.cs" />
<Compile Include="UpdateRules\Rules\20181215\RemoveNegativeDamageFullHealthCheck.cs" />
<Compile Include="UpdateRules\Rules\20181215\RemoveResourceExplodeModifier.cs" />

View File

@@ -211,8 +211,27 @@ namespace OpenRA.Mods.Common.Scripting
return;
if (!group.Any())
using (f)
f.Call().Dispose();
{
// Functions can only be .Call()ed once, so operate on a copy so we can reuse it later
var temp = (LuaFunction)f.CopyReference();
using (temp)
temp.Call().Dispose();
}
}
catch (Exception e)
{
Context.FatalError(e.Message);
}
};
Action<Actor> onMemberAdded = m =>
{
try
{
if (!actors.Contains(m) || group.Contains(m))
return;
group.Add(m);
}
catch (Exception e)
{
@@ -221,7 +240,10 @@ namespace OpenRA.Mods.Common.Scripting
};
foreach (var a in group)
{
GetScriptTriggers(a).OnRemovedInternal += onMemberRemoved;
GetScriptTriggers(a).OnAddedInternal += onMemberAdded;
}
}
[Desc("Call a function when this actor is captured. The callback function " +

View File

@@ -35,7 +35,7 @@ namespace OpenRA.Mods.Common.Scripting
if (targetManager == null || !targetManager.CanBeTargetedBy(target, Self, captureManager))
throw new LuaException("Actor '{0}' cannot capture actor '{1}'!".F(Self, target));
Self.QueueActivity(new CaptureActor(Self, target));
Self.QueueActivity(new CaptureActor(Self, Target.FromActor(target)));
}
}
}

View File

@@ -33,8 +33,9 @@ namespace OpenRA.Mods.Common.Scripting
[Desc("Deliver cash to the target actor.")]
public void DeliverCash(Actor target)
{
Self.SetTargetLine(Target.FromActor(target), Color.Yellow);
Self.QueueActivity(new DonateCash(Self, target, info.Payload, info.PlayerExperience));
var t = Target.FromActor(target);
Self.SetTargetLine(t, Color.Yellow);
Self.QueueActivity(new DonateCash(Self, t, info.Payload, info.PlayerExperience));
}
}
@@ -64,8 +65,9 @@ namespace OpenRA.Mods.Common.Scripting
var level = gainsExperience.Level;
Self.SetTargetLine(Target.FromActor(target), Color.Yellow);
Self.QueueActivity(new DonateExperience(Self, target, level, deliversExperience.PlayerExperience, targetGainsExperience));
var t = Target.FromActor(target);
Self.SetTargetLine(t, Color.Yellow);
Self.QueueActivity(new DonateExperience(Self, t, level, deliversExperience.PlayerExperience));
}
}
}

View File

@@ -31,7 +31,7 @@ namespace OpenRA.Mods.Common.Scripting
[Desc("Demolish the target actor.")]
public void Demolish(Actor target)
{
Self.QueueActivity(new Demolish(Self, target, info.EnterBehaviour, info.DetonationDelay,
Self.QueueActivity(new Demolish(Self, Target.FromActor(target), info.EnterBehaviour, info.DetonationDelay,
info.Flashes, info.FlashesDelay, info.FlashInterval));
}
}

View File

@@ -60,7 +60,7 @@ namespace OpenRA.Mods.Common.Scripting
[Desc("Move to and enter the transport.")]
public void EnterTransport(Actor transport)
{
Self.QueueActivity(new EnterTransport(Self, transport, 1, false));
Self.QueueActivity(new EnterTransport(Self, Target.FromActor(transport)));
}
[Desc("Whether the actor can move (false if immobilized).")]

View File

@@ -43,6 +43,7 @@ namespace OpenRA.Mods.Common.Scripting
public event Action<Actor> OnKilledInternal = _ => { };
public event Action<Actor> OnCapturedInternal = _ => { };
public event Action<Actor> OnRemovedInternal = _ => { };
public event Action<Actor> OnAddedInternal = _ => { };
public event Action<Actor, Actor> OnProducedInternal = (a, b) => { };
public event Action<Actor, Actor> OnOtherProducedInternal = (a, b) => { };
@@ -340,6 +341,9 @@ namespace OpenRA.Mods.Common.Scripting
return;
}
}
// Run any internally bound callbacks
OnAddedInternal(self);
}
void INotifyRemovedFromWorld.RemovedFromWorld(Actor self)

View File

@@ -654,6 +654,7 @@ namespace OpenRA.Mods.Common.Server
targetClient.SpawnPoint = 0;
targetClient.Team = 0;
targetClient.Color = HSLColor.FromRGB(255, 255, 255);
targetClient.State = Session.ClientState.NotReady;
server.SendMessage("{0} moved {1} to spectators.".F(client.Name, targetClient.Name));
Log.Write("server", "{0} moved {1} to spectators.".F(client.Name, targetClient.Name));
server.SyncLobbyClients();

View File

@@ -0,0 +1,79 @@
#region Copyright & License Information
/*
* Copyright 2007-2019 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, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using OpenRA.Traits;
namespace OpenRA.Mods.Common
{
public static class TargetExtensions
{
/// <summary>
/// Update (Frozen)Actor targets to account for visibility changes or actor replacement.
/// If the target actor becomes hidden without a FrozenActor, the target is invalidated.
/// /// </summary>
public static Target RecalculateInvalidatingHiddenTargets(this Target t, Player viewer)
{
bool targetIsHiddenActor;
var updated = t.Recalculate(viewer, out targetIsHiddenActor);
return targetIsHiddenActor ? Target.Invalid : updated;
}
/// <summary>
/// Update (Frozen)Actor targets to account for visibility changes or actor replacement.
/// If the target actor becomes hidden without a FrozenActor, the target actor is kept
/// and the actorHidden flag is set to true.
/// </summary>
public static Target Recalculate(this Target t, Player viewer, out bool targetIsHiddenActor)
{
targetIsHiddenActor = false;
// Check whether the target has transformed into something else
// HACK: This relies on knowing the internal implementation details of Target
if (t.Type == TargetType.Invalid && t.Actor != null && t.Actor.ReplacedByActor != null)
t = Target.FromActor(t.Actor.ReplacedByActor);
// Bot-controlled units aren't yet capable of understanding visibility changes
if (viewer.IsBot)
return t;
if (t.Type == TargetType.Actor)
{
// Actor has been hidden under the fog
if (!t.Actor.CanBeViewedByPlayer(viewer))
{
// Replace with FrozenActor if applicable, otherwise return target unmodified
var frozen = viewer.FrozenActorLayer.FromID(t.Actor.ActorID);
if (frozen != null)
return Target.FromFrozenActor(frozen);
targetIsHiddenActor = true;
return t;
}
}
else if (t.Type == TargetType.FrozenActor)
{
// Frozen actor has been revealed
if (!t.FrozenActor.Visible || !t.FrozenActor.IsValid)
{
// Original actor is still alive
if (t.FrozenActor.Actor != null && t.FrozenActor.Actor.CanBeViewedByPlayer(viewer))
return Target.FromActor(t.FrozenActor.Actor);
// Original actor was killed while hidden
if (t.Actor == null)
return Target.Invalid;
}
}
return t;
}
}
}

View File

@@ -174,6 +174,7 @@ namespace OpenRA.Mods.Common.Traits
RepairableInfo repairableInfo;
RearmableInfo rearmableInfo;
AttackMove attackMove;
ConditionManager conditionManager;
IDisposable reservation;
IEnumerable<int> speedModifiers;
@@ -231,6 +232,7 @@ namespace OpenRA.Mods.Common.Traits
{
repairableInfo = self.Info.TraitInfoOrDefault<RepairableInfo>();
rearmableInfo = self.Info.TraitInfoOrDefault<RearmableInfo>();
attackMove = self.TraitOrDefault<AttackMove>();
conditionManager = self.TraitOrDefault<ConditionManager>();
speedModifiers = self.TraitsImplementing<ISpeedModifier>().ToArray().Select(sm => sm.GetSpeedModifier());
cachedPosition = self.CenterPosition;
@@ -522,7 +524,12 @@ namespace OpenRA.Mods.Common.Traits
void INotifyBecomingIdle.OnBecomingIdle(Actor self)
{
OnBecomingIdle(self);
// HACK: Work around AttackMove relying on INotifyIdle.TickIdle to continue its path
// AttackMoveActivity needs to be rewritten to use child activities instead!
if (attackMove != null && attackMove.TargetLocation.HasValue)
((INotifyIdle)attackMove).TickIdle(self);
else
OnBecomingIdle(self);
}
protected virtual void OnBecomingIdle(Actor self)
@@ -605,28 +612,35 @@ namespace OpenRA.Mods.Common.Traits
return new HeliFly(self, Target.FromCell(self.World, cell));
}
public Activity MoveWithinRange(Target target, WDist range)
public Activity MoveWithinRange(Target target, WDist range,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
if (!Info.CanHover)
return new Fly(self, target, WDist.Zero, range);
return new Fly(self, target, WDist.Zero, range, initialTargetPosition, targetLineColor);
return new HeliFly(self, target, WDist.Zero, range);
return new HeliFly(self, target, WDist.Zero, range, initialTargetPosition, targetLineColor);
}
public Activity MoveWithinRange(Target target, WDist minRange, WDist maxRange)
public Activity MoveWithinRange(Target target, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
if (!Info.CanHover)
return new Fly(self, target, minRange, maxRange);
return new Fly(self, target, minRange, maxRange,
initialTargetPosition, targetLineColor);
return new HeliFly(self, target, minRange, maxRange);
return new HeliFly(self, target, minRange, maxRange,
initialTargetPosition, targetLineColor);
}
public Activity MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange)
public Activity MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
if (!Info.CanHover)
return new FlyFollow(self, target, minRange, maxRange);
return new FlyFollow(self, target, minRange, maxRange,
initialTargetPosition, targetLineColor);
return new Follow(self, target, minRange, maxRange);
return new Follow(self, target, minRange, maxRange,
initialTargetPosition, targetLineColor);
}
public Activity MoveIntoWorld(Actor self, CPos cell, SubCell subCell = SubCell.Any)
@@ -637,12 +651,16 @@ namespace OpenRA.Mods.Common.Traits
return new HeliFly(self, Target.FromCell(self.World, cell, subCell));
}
public Activity MoveToTarget(Actor self, Target target)
public Activity MoveToTarget(Actor self, Target target,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
if (!Info.CanHover)
return new Fly(self, target, WDist.FromCells(3), WDist.FromCells(5));
return new Fly(self, target, WDist.FromCells(3), WDist.FromCells(5),
initialTargetPosition, targetLineColor);
return ActivityUtils.SequenceActivities(new HeliFly(self, target), new Turn(self, Info.InitialFacing));
return ActivityUtils.SequenceActivities(
new HeliFly(self, target, initialTargetPosition, targetLineColor),
new Turn(self, Info.InitialFacing));
}
public Activity MoveIntoTarget(Actor self, Target target)

View File

@@ -34,6 +34,9 @@ namespace OpenRA.Mods.Common.Traits
[Desc("Allow firing into the fog to target frozen actors without requiring force-fire.")]
public readonly bool TargetFrozenActors = false;
[Desc("Force-fire mode ignores actors and targets the ground instead.")]
public readonly bool ForceFireIgnoresActors = false;
[VoiceReference] public readonly string Voice = "Action";
public override abstract object Create(ActorInitializer init);
@@ -139,8 +142,7 @@ namespace OpenRA.Mods.Common.Traits
if (armament == null)
yield break;
var negativeDamage = (armament.Weapon.Warheads.FirstOrDefault(w => (w is DamageWarhead)) as DamageWarhead).Damage < 0;
yield return new AttackOrderTargeter(this, 6, negativeDamage);
yield return new AttackOrderTargeter(this, 6);
}
}
@@ -358,8 +360,11 @@ namespace OpenRA.Mods.Common.Traits
self.CancelActivity();
self.QueueActivity(GetAttackActivity(self, target, allowMove, forceAttack));
OnQueueAttackActivity(self, target, queued, allowMove, forceAttack);
}
public virtual void OnQueueAttackActivity(Actor self, Target target, bool queued, bool allowMove, bool forceAttack) { }
public bool IsReachableTarget(Target target, bool allowMove)
{
return HasAnyValidWeapons(target)
@@ -381,7 +386,7 @@ namespace OpenRA.Mods.Common.Traits
{
readonly AttackBase ab;
public AttackOrderTargeter(AttackBase ab, int priority, bool negativeDamage)
public AttackOrderTargeter(AttackBase ab, int priority)
{
this.ab = ab;
OrderID = ab.attackOrderName;
@@ -399,6 +404,9 @@ namespace OpenRA.Mods.Common.Traits
if (modifiers.HasModifier(TargetModifiers.ForceMove))
return false;
if (ab.Info.ForceFireIgnoresActors && modifiers.HasModifier(TargetModifiers.ForceAttack))
return false;
// Disguised actors are revealed by the attack cursor
// HACK: works around limitations in the targeting code that force the
// targeting and attacking logic (which should be logically separate)

View File

@@ -10,9 +10,9 @@
#endregion
using System;
using System.Drawing;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Activities;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
@@ -20,27 +20,92 @@ namespace OpenRA.Mods.Common.Traits
[Desc("Actor will follow units until in range to attack them.")]
public class AttackFollowInfo : AttackBaseInfo
{
[Desc("Automatically acquire and fire on targets of opportunity when not actively attacking.")]
public readonly bool OpportunityFire = true;
public override object Create(ActorInitializer init) { return new AttackFollow(init.Self, this); }
}
public class AttackFollow : AttackBase, INotifyOwnerChanged
{
public Target Target { get; protected set; }
protected Target requestedTarget;
protected bool requestedForceAttack;
protected int requestedTargetLastTick;
protected Target opportunityTarget;
protected bool opportunityForceAttack;
Mobile mobile;
AutoTarget autoTarget;
public AttackFollow(Actor self, AttackFollowInfo info)
: base(self, info) { }
protected override void Created(Actor self)
{
mobile = self.TraitOrDefault<Mobile>();
autoTarget = self.TraitOrDefault<AutoTarget>();
base.Created(self);
}
protected bool CanAimAtTarget(Actor self, Target target, bool forceAttack)
{
if (target.Type == TargetType.Actor && !target.Actor.CanBeViewedByPlayer(self.Owner))
return false;
if (target.Type == TargetType.FrozenActor && !target.FrozenActor.IsValid)
return false;
var pos = self.CenterPosition;
var armaments = ChooseArmamentsForTarget(target, forceAttack);
foreach (var a in armaments)
if (target.IsInRange(pos, a.MaxRange()) && (a.Weapon.MinRange == WDist.Zero || !target.IsInRange(pos, a.Weapon.MinRange)))
return true;
return false;
}
protected override void Tick(Actor self)
{
Target = Target.Recalculate(self.Owner);
if (IsTraitDisabled)
requestedTarget = opportunityTarget = Target.Invalid;
if (requestedTargetLastTick != self.World.WorldTick)
{
Target = Target.Invalid;
return;
// Activities tick before traits, so if we are here it means the activity didn't run
// (either queued next or already cancelled) and we need to recalculate the target ourself
bool targetIsHiddenActor;
requestedTarget = requestedTarget.Recalculate(self.Owner, out targetIsHiddenActor);
}
DoAttack(self, Target);
IsAiming = Target.IsValidFor(self);
// Can't fire on anything
if (mobile != null && !mobile.CanInteractWithGroundLayer(self))
return;
if (requestedTarget.Type != TargetType.Invalid)
{
IsAiming = CanAimAtTarget(self, requestedTarget, requestedForceAttack);
if (IsAiming)
DoAttack(self, requestedTarget);
}
else
{
IsAiming = false;
if (opportunityTarget.Type != TargetType.Invalid)
IsAiming = CanAimAtTarget(self, opportunityTarget, opportunityForceAttack);
if (!IsAiming && ((AttackFollowInfo)Info).OpportunityFire && autoTarget != null &&
!autoTarget.IsTraitDisabled && autoTarget.Stance >= UnitStance.Defend)
{
opportunityTarget = autoTarget.ScanForTarget(self, false);
opportunityForceAttack = false;
if (opportunityTarget.Type != TargetType.Invalid)
IsAiming = CanAimAtTarget(self, opportunityTarget, opportunityForceAttack);
}
if (IsAiming)
DoAttack(self, opportunityTarget);
}
base.Tick(self);
}
@@ -50,15 +115,28 @@ namespace OpenRA.Mods.Common.Traits
return new AttackActivity(self, newTarget, allowMove, forceAttack);
}
public override void OnQueueAttackActivity(Actor self, Target target, bool queued, bool allowMove, bool forceAttack)
{
// If not queued we know that the attack activity will run next
// We can improve responsiveness for turreted actors by preempting
// the last order (usually a move) and set the target immediately
if (!queued)
{
requestedTarget = target;
requestedForceAttack = forceAttack;
requestedTargetLastTick = self.World.WorldTick;
}
}
public override void OnStopOrder(Actor self)
{
Target = Target.Invalid;
requestedTarget = opportunityTarget = Target.Invalid;
base.OnStopOrder(self);
}
public void OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
{
Target = Target.Invalid;
requestedTarget = opportunityTarget = Target.Invalid;
}
class AttackActivity : Activity
@@ -67,7 +145,13 @@ namespace OpenRA.Mods.Common.Traits
readonly RevealsShroud[] revealsShroud;
readonly IMove move;
readonly bool forceAttack;
Target target;
Target lastVisibleTarget;
bool useLastVisibleTarget;
WDist lastVisibleMaximumRange;
WDist lastVisibleMinimumRange;
bool wasMovingWithinRange;
bool hasTicked;
public AttackActivity(Actor self, Target target, bool allowMove, bool forceAttack)
@@ -78,59 +162,122 @@ namespace OpenRA.Mods.Common.Traits
this.target = target;
this.forceAttack = forceAttack;
// 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);
}
}
public override Activity Tick(Actor self)
{
target = target.Recalculate(self.Owner);
if (IsCanceled || !target.IsValidFor(self))
if (IsCanceled)
{
// Cancel the requested target, but keep firing on it while in range
attack.opportunityTarget = attack.requestedTarget;
attack.opportunityForceAttack = attack.requestedForceAttack;
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;
var weapon = attack.ChooseArmamentsForTarget(target, forceAttack).FirstOrDefault();
if (weapon != null)
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)
{
// 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.Target.Type == TargetType.Invalid)
return NextActivity;
lastVisibleTarget = Target.FromTargetPositions(target);
lastVisibleMaximumRange = attack.GetMaximumRangeVersusTarget(target);
lastVisibleMinimumRange = attack.GetMinimumRange();
var targetIsMobile = (target.Type == TargetType.Actor && target.Actor.Info.HasTraitInfo<IMoveInfo>())
|| (target.Type == TargetType.FrozenActor && target.FrozenActor.Info.HasTraitInfo<IMoveInfo>());
// Try and sit at least one cell closer than the max range to give some leeway if the target starts moving.
var modifiedRange = weapon.MaxRange();
var maxRange = targetIsMobile ? new WDist(Math.Max(weapon.Weapon.MinRange.Length, modifiedRange.Length - 1024))
: modifiedRange;
// Most actors want to be able to see their target before shooting
if (!attack.Info.TargetFrozenActors && !forceAttack && target.Type == TargetType.FrozenActor)
// 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 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;
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));
}
attack.Target = target;
hasTicked = true;
if (move != null)
return ActivityUtils.SequenceActivities(move.MoveFollow(self, target, weapon.Weapon.MinRange, maxRange), this);
if (target.IsInRange(self.CenterPosition, weapon.MaxRange()) &&
!target.IsInRange(self.CenterPosition, weapon.Weapon.MinRange))
return this;
}
attack.Target = Target.Invalid;
var oldUseLastVisibleTarget = useLastVisibleTarget;
var maxRange = lastVisibleMaximumRange;
var minRange = lastVisibleMinimumRange;
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
return NextActivity;
// 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;
return ActivityUtils.SequenceActivities(
move.MoveWithinRange(target, minRange, maxRange, checkTarget.CenterPosition, Color.Red),
this);
}
}
}

View File

@@ -46,7 +46,9 @@ namespace OpenRA.Mods.Common.Traits
public override Activity Tick(Actor self)
{
target = target.Recalculate(self.Owner);
// This activity can't move to reacquire hidden targets, so use the
// Recalculate overload that invalidates hidden targets.
target = target.RecalculateInvalidatingHiddenTargets(self.Owner);
if (IsCanceled || !target.IsValidFor(self) || !attack.IsReachableTarget(target, allowMove))
return NextActivity;

View File

@@ -1,98 +0,0 @@
#region Copyright & License Information
/*
* Copyright 2007-2018 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, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using System.Drawing;
using OpenRA.Activities;
using OpenRA.Mods.Common.Orders;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[Desc("Does a suicide attack where it moves next to the target when used in combination with `Explodes`.")]
class AttackSuicidesInfo : ConditionalTraitInfo, Requires<IMoveInfo>
{
[Desc("Types defined by `Targetable:` trait that this actor can target.")]
public readonly BitSet<TargetableType> TargetTypes = new BitSet<TargetableType>("DetonateAttack");
[Desc("Types of damage that this trait causes to self while suiciding. Leave empty for no damage types.")]
public readonly BitSet<DamageType> DamageTypes = default(BitSet<DamageType>);
[VoiceReference] public readonly string Voice = "Action";
public override object Create(ActorInitializer init) { return new AttackSuicides(init.Self, this); }
}
class AttackSuicides : ConditionalTrait<AttackSuicidesInfo>, IIssueOrder, IResolveOrder, IOrderVoice, IIssueDeployOrder
{
readonly IMove move;
public AttackSuicides(Actor self, AttackSuicidesInfo info)
: base(info)
{
move = self.Trait<IMove>();
}
public IEnumerable<IOrderTargeter> Orders
{
get
{
if (IsTraitDisabled)
yield break;
yield return new TargetTypeOrderTargeter(Info.TargetTypes, "DetonateAttack", 5, "attack", true, false) { ForceAttack = false };
yield return new DeployOrderTargeter("Detonate", 5);
}
}
public Order IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued)
{
if (order.OrderID != "DetonateAttack" && order.OrderID != "Detonate")
return null;
return new Order(order.OrderID, self, target, queued);
}
Order IIssueDeployOrder.IssueDeployOrder(Actor self, bool queued)
{
return new Order("Detonate", self, queued);
}
bool IIssueDeployOrder.CanIssueDeployOrder(Actor self) { return true; }
public string VoicePhraseForOrder(Actor self, Order order)
{
return Info.Voice;
}
public void ResolveOrder(Actor self, Order order)
{
if (order.OrderString == "DetonateAttack")
{
var target = self.ResolveFrozenActorOrder(order, Color.Red);
if (target.Type != TargetType.Actor)
return;
if (!order.Queued)
self.CancelActivity();
self.SetTargetLine(target, Color.Red);
self.QueueActivity(move.MoveToTarget(self, target));
self.QueueActivity(new CallFunc(() => self.Kill(self, Info.DamageTypes)));
}
else if (order.OrderString == "Detonate")
self.Kill(self, Info.DamageTypes);
}
}
}

View File

@@ -109,10 +109,9 @@ namespace OpenRA.Mods.Common.Traits
}
}
public class AutoTarget : ConditionalTrait<AutoTargetInfo>, INotifyIdle, INotifyDamage, ITick, IResolveOrder, ISync, INotifyCreated
public class AutoTarget : ConditionalTrait<AutoTargetInfo>, INotifyIdle, INotifyDamage, ITick, IResolveOrder, ISync, INotifyCreated, INotifyOwnerChanged
{
readonly IEnumerable<AttackBase> activeAttackBases;
readonly AttackFollow[] attackFollows;
[Sync] int nextScanTime = 0;
public UnitStance Stance { get { return stance; } }
@@ -161,7 +160,6 @@ namespace OpenRA.Mods.Common.Traits
stance = self.Owner.IsBot || !self.Owner.Playable ? info.InitialStanceAI : info.InitialStance;
PredictedStance = stance;
attackFollows = self.TraitsImplementing<AttackFollow>().ToArray();
}
void INotifyCreated.Created(Actor self)
@@ -177,6 +175,12 @@ namespace OpenRA.Mods.Common.Traits
ApplyStanceCondition(self);
}
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
{
PredictedStance = self.Owner.IsBot || !self.Owner.Playable ? Info.InitialStanceAI : Info.InitialStance;
SetStance(self, PredictedStance);
}
void IResolveOrder.ResolveOrder(Actor self, Order order)
{
if (order.OrderString == "SetUnitStance" && Info.EnableStances)
@@ -215,9 +219,8 @@ namespace OpenRA.Mods.Common.Traits
Aggressor = attacker;
bool allowMove;
if (ShouldAttack(out allowMove))
Attack(self, Target.FromActor(Aggressor), allowMove);
var allowMove = Info.AllowMovement && Stance > UnitStance.Defend;
Attack(self, Target.FromActor(Aggressor), allowMove);
}
void INotifyIdle.TickIdle(Actor self)
@@ -225,21 +228,8 @@ namespace OpenRA.Mods.Common.Traits
if (IsTraitDisabled || Stance < UnitStance.Defend)
return;
bool allowMove;
if (ShouldAttack(out allowMove))
ScanAndAttack(self, allowMove);
}
bool ShouldAttack(out bool allowMove)
{
allowMove = Info.AllowMovement && Stance > UnitStance.Defend;
// PERF: Avoid LINQ.
foreach (var attackFollow in attackFollows)
if (!attackFollow.IsTraitDisabled && attackFollow.IsReachableTarget(attackFollow.Target, allowMove))
return false;
return true;
var allowMove = Info.AllowMovement && Stance > UnitStance.Defend;
ScanAndAttack(self, allowMove);
}
void ITick.Tick(Actor self)

View File

@@ -26,6 +26,12 @@ namespace OpenRA.Mods.Common.Traits
void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e)
{
// HACK: We don't want D2k bots to repair all their buildings on placement
// where half their HP is removed via neutral terrain damage.
// TODO: Implement concrete placement for D2k bots and remove this hack.
if (e.Attacker.Owner.Stances[self.Owner] == Stance.Neutral)
return;
var rb = self.TraitOrDefault<RepairableBuilding>();
if (rb != null)
{

View File

@@ -48,7 +48,7 @@ namespace OpenRA.Mods.Common.Traits
readonly World world;
readonly Player player;
readonly Func<Actor, bool> isEnemyUnit;
readonly Predicate<Actor> unitCannotBeOrdered;
readonly Predicate<Actor> unitCannotBeOrderedOrIsIdle;
readonly int maximumCaptureTargetOptions;
int minCaptureDelayTicks;
@@ -69,7 +69,7 @@ namespace OpenRA.Mods.Common.Traits
&& !unit.Info.HasTraitInfo<HuskInfo>()
&& unit.Info.HasTraitInfo<ITargetableInfo>();
unitCannotBeOrdered = a => a.Owner != player || a.IsDead || !a.IsInWorld;
unitCannotBeOrderedOrIsIdle = a => a.Owner != player || a.IsDead || !a.IsInWorld || a.IsIdle;
maximumCaptureTargetOptions = Math.Max(1, Info.MaximumCaptureTargetOptions);
}
@@ -118,13 +118,13 @@ namespace OpenRA.Mods.Common.Traits
if (!Info.CapturingActorTypes.Any() || player.WinState != WinState.Undefined)
return;
activeCapturers.RemoveAll(unitCannotBeOrdered);
activeCapturers.RemoveAll(unitCannotBeOrderedOrIsIdle);
var newUnits = world.ActorsHavingTrait<IPositionable>()
.Where(a => a.Owner == player && !activeCapturers.Contains(a));
var capturers = newUnits
.Where(a => a.IsIdle && Info.CapturingActorTypes.Contains(a.Info.Name))
.Where(a => a.IsIdle && Info.CapturingActorTypes.Contains(a.Info.Name) && a.Info.HasTraitInfo<CapturesInfo>())
.Select(a => new TraitPair<CaptureManager>(a, a.TraitOrDefault<CaptureManager>()))
.Where(tp => tp.Trait != null)
.ToArray();
@@ -140,52 +140,33 @@ namespace OpenRA.Mods.Common.Traits
: GetActorsThatCanBeOrderedByPlayer(randPlayer);
var capturableTargetOptions = targetOptions
.Select(a => new CaptureTarget<CapturableInfo>(a, "CaptureActor"))
.Where(target =>
{
if (target.Info == null)
return false;
var captureManager = target.Actor.TraitOrDefault<CaptureManager>();
var captureManager = target.TraitOrDefault<CaptureManager>();
if (captureManager == null)
return false;
return capturers.Any(tp => captureManager.CanBeTargetedBy(target.Actor, tp.Actor, tp.Trait));
return capturers.Any(tp => captureManager.CanBeTargetedBy(target, tp.Actor, tp.Trait));
})
.OrderByDescending(target => target.Actor.GetSellValue())
.OrderByDescending(target => target.GetSellValue())
.Take(maximumCaptureTargetOptions);
if (Info.CapturableActorTypes.Any())
capturableTargetOptions = capturableTargetOptions.Where(target => Info.CapturableActorTypes.Contains(target.Actor.Info.Name.ToLowerInvariant()));
capturableTargetOptions = capturableTargetOptions.Where(target => Info.CapturableActorTypes.Contains(target.Info.Name.ToLowerInvariant()));
if (!capturableTargetOptions.Any())
return;
var capturesCapturers = capturers.Where(tp => tp.Actor.Info.HasTraitInfo<CapturesInfo>());
foreach (var tp in capturesCapturers)
QueueCaptureOrderFor(bot, tp.Actor, GetCapturerTargetClosestToOrDefault(tp.Actor, capturableTargetOptions));
}
foreach (var capturer in capturers)
{
var targetActor = capturableTargetOptions.MinByOrDefault(target => (target.CenterPosition - capturer.Actor.CenterPosition).LengthSquared);
if (targetActor == null)
continue;
void QueueCaptureOrderFor<TTargetType>(IBot bot, Actor capturer, CaptureTarget<TTargetType> target) where TTargetType : class, ITraitInfoInterface
{
if (capturer == null)
return;
if (target == null)
return;
if (target.Actor == null)
return;
bot.QueueOrder(new Order(target.OrderString, capturer, Target.FromActor(target.Actor), true));
AIUtils.BotDebug("AI ({0}): Ordered {1} to capture {2}", player.ClientIndex, capturer, target.Actor);
activeCapturers.Add(capturer);
}
CaptureTarget<TTargetType> GetCapturerTargetClosestToOrDefault<TTargetType>(Actor capturer, IEnumerable<CaptureTarget<TTargetType>> targets)
where TTargetType : class, ITraitInfoInterface
{
return targets.MinByOrDefault(target => (target.Actor.CenterPosition - capturer.CenterPosition).LengthSquared);
bot.QueueOrder(new Order("CaptureActor", capturer.Actor, Target.FromActor(targetActor), true));
AIUtils.BotDebug("AI ({0}): Ordered {1} to capture {2}", player.ClientIndex, capturer.Actor, targetActor);
activeCapturers.Add(capturer.Actor);
}
}
}
}

View File

@@ -50,7 +50,7 @@ namespace OpenRA.Mods.Common.Traits
: base(info)
{
self = init.Self;
OpenPosition = Info.TransitionDelay;
Position = OpenPosition = Info.TransitionDelay;
building = self.Trait<Building>();
blockedPositions = building.Info.Tiles(self.Location);
Footprint = blockedPositions;

View File

@@ -95,15 +95,11 @@ namespace OpenRA.Mods.Common.Traits
if (order.OrderString != "CaptureActor" || IsTraitDisabled)
return;
var target = self.ResolveFrozenActorOrder(order, Color.Red);
if (target.Type != TargetType.Actor)
return;
if (!order.Queued)
self.CancelActivity();
self.SetTargetLine(target, Color.Red);
self.QueueActivity(new CaptureActor(self, target.Actor));
self.SetTargetLine(order.Target, Color.Red);
self.QueueActivity(new CaptureActor(self, order.Target));
}
protected override void TraitEnabled(Actor self) { captureManager.RefreshCaptures(self); }

View File

@@ -265,18 +265,17 @@ namespace OpenRA.Mods.Common.Traits
{
if (order.OrderString == "PickupUnit")
{
var target = self.ResolveFrozenActorOrder(order, Color.Yellow);
if (target.Type != TargetType.Actor)
if (order.Target.Type != TargetType.Actor)
return;
if (!ReserveCarryable(self, target.Actor))
if (!ReserveCarryable(self, order.Target.Actor))
return;
if (!order.Queued)
self.CancelActivity();
self.SetTargetLine(target, Color.Yellow);
self.QueueActivity(order.Queued, new PickupUnit(self, target.Actor, Info.LoadingDelay));
self.SetTargetLine(order.Target, Color.Yellow);
self.QueueActivity(order.Queued, new PickupUnit(self, order.Target.Actor, Info.LoadingDelay));
}
}
}

View File

@@ -69,15 +69,11 @@ namespace OpenRA.Mods.Common.Traits
if (order.OrderString != "DeliverCash")
return;
var target = self.ResolveFrozenActorOrder(order, Color.Yellow);
if (target.Type != TargetType.Actor)
return;
if (!order.Queued)
self.CancelActivity();
self.SetTargetLine(target, Color.Yellow);
self.QueueActivity(new DonateCash(self, target.Actor, info.Payload, info.PlayerExperience));
self.SetTargetLine(order.Target, Color.Yellow);
self.QueueActivity(new DonateCash(self, order.Target, info.Payload, info.PlayerExperience));
}
void INotifyCashTransfer.OnAcceptingCash(Actor self, Actor donor) { }

View File

@@ -71,21 +71,20 @@ namespace OpenRA.Mods.Common.Traits
if (order.OrderString != "DeliverExperience")
return;
var target = self.ResolveFrozenActorOrder(order, Color.Yellow);
if (target.Type != TargetType.Actor)
return;
var targetGainsExperience = target.Actor.Trait<GainsExperience>();
if (targetGainsExperience.Level == targetGainsExperience.MaxLevel)
if (order.Target.Type == TargetType.Actor)
{
var targetGainsExperience = order.Target.Actor.Trait<GainsExperience>();
if (targetGainsExperience.Level == targetGainsExperience.MaxLevel)
return;
}
else if (order.Target.Type != TargetType.FrozenActor)
return;
if (!order.Queued)
self.CancelActivity();
var level = gainsExperience.Level;
self.SetTargetLine(target, Color.Yellow);
self.QueueActivity(new DonateExperience(self, target.Actor, level, info.PlayerExperience, targetGainsExperience));
self.SetTargetLine(order.Target, Color.Yellow);
self.QueueActivity(new DonateExperience(self, order.Target, gainsExperience.Level, info.PlayerExperience));
}
public class DeliversExperienceOrderTargeter : UnitOrderTargeter

View File

@@ -45,6 +45,7 @@ namespace OpenRA.Mods.Common.Traits
ConditionManager conditionManager;
List<DemolishAction> actions = new List<DemolishAction>();
List<DemolishAction> removeActions = new List<DemolishAction>();
public Demolishable(DemolishableInfo info)
: base(info) { }
@@ -88,9 +89,18 @@ namespace OpenRA.Mods.Common.Traits
if (Util.ApplyPercentageModifiers(100, modifiers) > 0)
self.Kill(a.Saboteur);
else if (a.Token != ConditionManager.InvalidConditionToken)
{
conditionManager.RevokeCondition(self, a.Token);
removeActions.Add(a);
}
}
}
// Remove expired actions to avoid double-revoking
foreach (var a in removeActions)
actions.Remove(a);
removeActions.Clear();
}
}
}

View File

@@ -75,19 +75,18 @@ namespace OpenRA.Mods.Common.Traits
if (order.OrderString != "C4")
return;
var target = self.ResolveFrozenActorOrder(order, Color.Red);
if (target.Type != TargetType.Actor)
return;
var demolishable = target.Actor.TraitOrDefault<IDemolishable>();
if (demolishable == null || !demolishable.IsValidTarget(target.Actor, self))
return;
if (order.Target.Type == TargetType.Actor)
{
var demolishables = order.Target.Actor.TraitsImplementing<IDemolishable>();
if (!demolishables.Any(i => i.IsValidTarget(order.Target.Actor, self)))
return;
}
if (!order.Queued)
self.CancelActivity();
self.SetTargetLine(target, Color.Red);
self.QueueActivity(new Demolish(self, target.Actor, info.EnterBehaviour, info.DetonationDelay,
self.SetTargetLine(order.Target, Color.Red);
self.QueueActivity(new Demolish(self, order.Target, info.EnterBehaviour, info.DetonationDelay,
info.Flashes, info.FlashesDelay, info.FlashInterval));
}

View File

@@ -76,15 +76,11 @@ namespace OpenRA.Mods.Common.Traits
if (order.OrderString != "EngineerRepair" || !IsValidOrder(self, order))
return;
var target = self.ResolveFrozenActorOrder(order, Color.Yellow);
if (target.Type != TargetType.Actor)
return;
if (!order.Queued)
self.CancelActivity();
self.SetTargetLine(target, Color.Yellow);
self.QueueActivity(new RepairBuilding(self, target.Actor, info.EnterBehaviour, info.ValidStances));
self.SetTargetLine(order.Target, Color.Yellow);
self.QueueActivity(new RepairBuilding(self, order.Target, info.EnterBehaviour, info.ValidStances));
}
class EngineerRepairOrderTargeter : UnitOrderTargeter

View File

@@ -62,15 +62,11 @@ namespace OpenRA.Mods.Common.Traits
public void ResolveOrder(Actor self, Order order)
{
if (order.OrderString != "EnterTunnel")
if (order.OrderString != "EnterTunnel" || order.Target.Type != TargetType.Actor)
return;
var target = self.ResolveFrozenActorOrder(order, Color.Red);
if (target.Type != TargetType.Actor)
return;
var tunnel = target.Actor.TraitOrDefault<TunnelEntrance>();
if (!tunnel.Exit.HasValue)
var tunnel = order.Target.Actor.TraitOrDefault<TunnelEntrance>();
if (tunnel == null || !tunnel.Exit.HasValue)
return;
if (!order.Queued)

View File

@@ -9,6 +9,7 @@
*/
#endregion
using System.Collections.Generic;
using System.Linq;
using OpenRA.Mods.Common.Effects;
using OpenRA.Primitives;
@@ -35,20 +36,13 @@ namespace OpenRA.Mods.Common.Traits
public override object Create(ActorInitializer init) { return new GivesBounty(this); }
}
class GivesBounty : ConditionalTrait<GivesBountyInfo>, INotifyKilled
class GivesBounty : ConditionalTrait<GivesBountyInfo>, INotifyKilled, INotifyPassengerEntered, INotifyPassengerExited
{
Cargo cargo;
Dictionary<Actor, GivesBounty[]> passengerBounties = new Dictionary<Actor, GivesBounty[]>();
public GivesBounty(GivesBountyInfo info)
: base(info) { }
protected override void Created(Actor self)
{
base.Created(self);
cargo = self.TraitOrDefault<Cargo>();
}
int GetBountyValue(Actor self)
{
return self.GetSellValue() * Info.Percentage / 100;
@@ -57,15 +51,10 @@ namespace OpenRA.Mods.Common.Traits
int GetDisplayedBountyValue(Actor self)
{
var bounty = GetBountyValue(self);
if (cargo == null)
return bounty;
foreach (var a in cargo.Passengers)
{
var givesBounties = a.TraitsImplementing<GivesBounty>().Where(gb => !gb.IsTraitDisabled);
foreach (var givesBounty in givesBounties)
bounty += givesBounty.GetDisplayedBountyValue(a);
}
foreach (var pb in passengerBounties)
foreach (var b in pb.Value)
if (!b.IsTraitDisabled)
bounty += b.GetDisplayedBountyValue(pb.Key);
return bounty;
}
@@ -87,5 +76,15 @@ namespace OpenRA.Mods.Common.Traits
e.Attacker.Owner.PlayerActor.Trait<PlayerResources>().ChangeCash(GetBountyValue(self));
}
void INotifyPassengerEntered.OnPassengerEntered(Actor self, Actor passenger)
{
passengerBounties.Add(passenger, passenger.TraitsImplementing<GivesBounty>().ToArray());
}
void INotifyPassengerExited.OnPassengerExited(Actor self, Actor passenger)
{
passengerBounties.Remove(passenger);
}
}
}

View File

@@ -55,7 +55,7 @@ namespace OpenRA.Mods.Common.Traits
self.SetTargetLine(target, Color.Yellow);
var range = target.Actor.Info.TraitInfo<GuardableInfo>().Range;
self.QueueActivity(new AttackMoveActivity(self, move.MoveFollow(self, target, WDist.Zero, range)));
self.QueueActivity(new AttackMoveActivity(self, move.MoveFollow(self, target, WDist.Zero, range, targetLineColor: Color.Yellow)));
}
public string VoicePhraseForOrder(Actor self, Order order)

View File

@@ -457,11 +457,33 @@ namespace OpenRA.Mods.Common.Traits
#region IMove
public Activity MoveTo(CPos cell, int nearEnough) { return new Move(self, cell, WDist.FromCells(nearEnough)); }
public Activity MoveTo(CPos cell, Actor ignoreActor) { return new Move(self, cell, WDist.Zero, ignoreActor); }
public Activity MoveWithinRange(Target target, WDist range) { return new MoveWithinRange(self, target, WDist.Zero, range); }
public Activity MoveWithinRange(Target target, WDist minRange, WDist maxRange) { return new MoveWithinRange(self, target, minRange, maxRange); }
public Activity MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange) { return new Follow(self, target, minRange, maxRange); }
public Activity MoveTo(CPos cell, int nearEnough)
{
return new Move(self, cell, WDist.FromCells(nearEnough));
}
public Activity MoveTo(CPos cell, Actor ignoreActor)
{
return new Move(self, cell, WDist.Zero, ignoreActor);
}
public Activity MoveWithinRange(Target target, WDist range,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
return new MoveWithinRange(self, target, WDist.Zero, range, initialTargetPosition, targetLineColor);
}
public Activity MoveWithinRange(Target target, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
return new MoveWithinRange(self, target, minRange, maxRange, initialTargetPosition, targetLineColor);
}
public Activity MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
return new Follow(self, target, minRange, maxRange, initialTargetPosition, targetLineColor);
}
public Activity MoveIntoWorld(Actor self, CPos cell, SubCell subCell = SubCell.Any)
{
@@ -481,12 +503,13 @@ namespace OpenRA.Mods.Common.Traits
return VisualMove(self, pos, self.World.Map.CenterOfSubCell(cell, subCell), cell);
}
public Activity MoveToTarget(Actor self, Target target)
public Activity MoveToTarget(Actor self, Target target,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
if (target.Type == TargetType.Invalid)
return null;
return new MoveAdjacentTo(self, target);
return new MoveAdjacentTo(self, target, initialTargetPosition, targetLineColor);
}
public Activity MoveIntoTarget(Actor self, Target target)
@@ -494,7 +517,9 @@ namespace OpenRA.Mods.Common.Traits
if (target.Type == TargetType.Invalid)
return null;
return VisualMove(self, self.CenterPosition, target.Positions.PositionClosestTo(self.CenterPosition));
// Activity cancels if the target moves by more than half a cell
// to avoid problems with the cell grid
return new VisualMoveIntoTarget(self, target, new WDist(512));
}
public Activity VisualMove(Actor self, WPos fromPos, WPos toPos)

View File

@@ -9,6 +9,7 @@
*/
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
@@ -27,7 +28,7 @@ namespace OpenRA.Mods.Common.Traits
public object Create(ActorInitializer init) { return new FrozenUnderFog(init, this); }
}
public class FrozenUnderFog : IRenderModifier, IDefaultVisibility, ITick, ITickRender, ISync, INotifyCreated
public class FrozenUnderFog : ICreatesFrozenActors, IRenderModifier, IDefaultVisibility, ITick, ITickRender, ISync, INotifyCreated, INotifyOwnerChanged, INotifyActorDisposing
{
[Sync] public int VisibilityHash;
@@ -37,6 +38,7 @@ namespace OpenRA.Mods.Common.Traits
PlayerDictionary<FrozenState> frozenStates;
bool isRendering;
bool created;
class FrozenState
{
@@ -67,14 +69,10 @@ namespace OpenRA.Mods.Common.Traits
{
frozenStates = new PlayerDictionary<FrozenState>(self.World, (player, playerIndex) =>
{
var frozenActor = new FrozenActor(self, footprint, player, startsRevealed);
var frozenActor = new FrozenActor(self, this, footprint, player, startsRevealed);
player.PlayerActor.Trait<FrozenActorLayer>().Add(frozenActor);
return new FrozenState(frozenActor) { IsVisible = startsRevealed };
});
if (startsRevealed)
for (var playerIndex = 0; playerIndex < frozenStates.Count; playerIndex++)
UpdateFrozenActor(self, frozenStates[playerIndex].FrozenActor, playerIndex);
}
void UpdateFrozenActor(Actor self, FrozenActor frozenActor, int playerIndex)
@@ -83,6 +81,17 @@ namespace OpenRA.Mods.Common.Traits
frozenActor.RefreshState();
}
void ICreatesFrozenActors.OnVisibilityChanged(FrozenActor frozen)
{
// Ignore callbacks during initial setup
if (!created)
return;
// Update state visibility to match the frozen actor to ensure consistency within the tick
// The rest of the state will be updated by ITick.Tick below
frozenStates[frozen.Viewer].IsVisible = !frozen.Visible;
}
bool IsVisibleInner(Actor self, Player byPlayer)
{
// If fog is disabled visibility is determined by shroud
@@ -106,6 +115,18 @@ namespace OpenRA.Mods.Common.Traits
if (self.Disposed)
return;
// Set the initial visibility state
// This relies on actor.GetTargetablePositions(), which is not safe to use from Created
// so we defer until the first real tick.
if (!created && startsRevealed)
{
for (var playerIndex = 0; playerIndex < frozenStates.Count; playerIndex++)
UpdateFrozenActor(self, frozenStates[playerIndex].FrozenActor, playerIndex);
created = true;
return;
}
VisibilityHash = 0;
for (var playerIndex = 0; playerIndex < frozenStates.Count; playerIndex++)
@@ -158,6 +179,20 @@ namespace OpenRA.Mods.Common.Traits
{
return bounds;
}
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
{
// Force a state update for the old owner so the tooltip etc doesn't show them as the owner
var oldOwnerIndex = self.World.Players.IndexOf(oldOwner);
UpdateFrozenActor(self, frozenStates[oldOwnerIndex].FrozenActor, oldOwnerIndex);
}
void INotifyActorDisposing.Disposing(Actor self)
{
// Invalidate the frozen actor (which exists if this actor was captured from an enemy)
// for the current owner
frozenStates[self.Owner].FrozenActor.Invalidate();
}
}
public class HiddenUnderFogInit : IActorInit { }

View File

@@ -32,9 +32,6 @@ namespace OpenRA.Mods.Common.Traits
"Default - use force move modifier (Alt) to disable.")]
public readonly AlternateTransportsMode AlternateTransportsMode = AlternateTransportsMode.Force;
[Desc("Number of retries using alternate transports.")]
public readonly int MaxAlternateTransportAttempts = 1;
[Desc("Range from self for looking for an alternate transport (default: 5.5 cells).")]
public readonly WDist AlternateTransportScanRange = WDist.FromCells(11) / 2;
@@ -161,9 +158,11 @@ namespace OpenRA.Mods.Common.Traits
if (!order.Queued)
self.CancelActivity();
var transports = order.OrderString == "EnterTransports";
self.SetTargetLine(order.Target, Color.Green);
self.QueueActivity(new EnterTransport(self, targetActor, transports ? Info.MaxAlternateTransportAttempts : 0, !transports));
if (order.OrderString == "EnterTransports")
self.QueueActivity(new EnterTransports(self, order.Target));
else
self.QueueActivity(new EnterTransport(self, order.Target));
}
public bool Reserve(Actor self, Cargo cargo)

View File

@@ -63,6 +63,11 @@ namespace OpenRA.Mods.Common.Traits
// Called by the host's player creation code
public void Activate(Player p)
{
// Bot logic is not allowed to affect world state, and can only act by issuing orders
// These orders are recorded in the replay, so bots shouldn't be enabled during replays
if (p.World.IsReplay)
return;
IsEnabled = true;
player = p;
tickModules = p.PlayerActor.TraitsImplementing<IBotTick>().ToArray();

View File

@@ -85,7 +85,7 @@ namespace OpenRA.Mods.Common.Traits
bool IRenderAboveShroudWhenSelected.SpatiallyPartitionable { get { return false; } }
void INotifyBecomingIdle.OnBecomingIdle(Actor a)
void INotifyBecomingIdle.OnBecomingIdle(Actor self)
{
targets = null;
}
@@ -115,15 +115,12 @@ namespace OpenRA.Mods.Common.Traits
if (!self.Owner.IsAlliedWith(self.World.LocalPlayer))
return;
self.World.AddFrameEndTask(w =>
{
if (self.Disposed)
return;
if (self.Disposed)
return;
var line = self.TraitOrDefault<DrawLineToTarget>();
if (line != null)
line.SetTarget(self, target, color, display);
});
var line = self.TraitOrDefault<DrawLineToTarget>();
if (line != null)
line.SetTarget(self, target, color, display);
}
}
}

View File

@@ -112,8 +112,12 @@ namespace OpenRA.Mods.Common.Traits
self.CancelActivity();
self.SetTargetLine(order.Target, Color.Green);
self.QueueActivity(new WaitForTransport(self, ActivityUtils.SequenceActivities(movement.MoveToTarget(self, order.Target),
new CallFunc(() => AfterReachActivities(self, order, movement)))));
var activities = ActivityUtils.SequenceActivities(
movement.MoveToTarget(self, order.Target, targetLineColor: Color.Green),
new CallFunc(() => AfterReachActivities(self, order, movement)));
self.QueueActivity(new WaitForTransport(self, activities));
TryCallTransport(self, order.Target, new CallFunc(() => AfterReachActivities(self, order, movement)));
}

View File

@@ -85,7 +85,7 @@ namespace OpenRA.Mods.Common.Traits
if (!order.Queued)
self.CancelActivity();
self.QueueActivity(movement.MoveWithinRange(order.Target, info.CloseEnough));
self.QueueActivity(movement.MoveWithinRange(order.Target, info.CloseEnough, targetLineColor: Color.Green));
self.QueueActivity(new Repair(self, order.Target.Actor, info.CloseEnough));
self.SetTargetLine(order.Target, Color.Green, false);

View File

@@ -82,6 +82,7 @@ namespace OpenRA.Mods.Common.Traits
public void ResolveOrder(Actor self, Order order)
{
// TODO: Add support for FrozenActors
// The activity supports it, but still missing way to freeze bridge state on the hut
if (order.OrderString == "RepairBridge" && order.Target.Type == TargetType.Actor)
{
var targetActor = order.Target.Actor;
@@ -104,7 +105,7 @@ namespace OpenRA.Mods.Common.Traits
self.CancelActivity();
self.SetTargetLine(order.Target, Color.Yellow);
self.QueueActivity(new RepairBridge(self, targetActor, info.EnterBehaviour, info.RepairNotification));
self.QueueActivity(new RepairBridge(self, order.Target, info.EnterBehaviour, info.RepairNotification));
}
}

View File

@@ -106,6 +106,8 @@ namespace OpenRA.Mods.Common.Traits
Action<Actor> onRemovedFromWorld = a =>
{
aircraftInRange[a] = false;
// Checking for attack range is not relevant here because
// aircraft may be shot down before entering. Thus we remove
// the camera and beacon only if the whole squad is dead.

View File

@@ -133,6 +133,8 @@ namespace OpenRA.Mods.Common.Traits
Action<Actor> onRemovedFromWorld = a =>
{
aircraftInRange[a] = false;
// Checking for attack range is not relevant here because
// aircraft may be shot down before entering. Thus we remove
// the camera and beacon only if the whole squad is dead.

View File

@@ -255,12 +255,6 @@ namespace OpenRA.Mods.Common.Traits
if (attack != null && attack.IsAiming)
attack.OnStopOrder(self);
}
protected override void TraitResumed(Actor self)
{
if (attack != null)
FaceTarget(self, attack.Target);
}
}
public class TurretFacingInit : IActorInit<int>

View File

@@ -283,5 +283,10 @@ namespace OpenRA.Mods.Common.Traits
foreach (var preview in previewsForCell.Value)
destinationBuffer.Add(Pair.New(previewsForCell.Key, preview.Owner.Color.RGB));
}
public EditorActorPreview this[string id]
{
get { return previews.FirstOrDefault(p => p.ID.ToLowerInvariant() == id); }
}
}
}

View File

@@ -29,18 +29,17 @@ namespace OpenRA.Mods.Common.Traits
if (subjectClient == null)
{
Game.Debug("Order sent to {0}: resolved ClientIndex `{1}` doesn't exist", order.Subject.Owner.PlayerName, subjectClientId);
Log.Write("debug", "Order sent to {0}: resolved ClientIndex `{1}` doesn't exist", order.Subject.Owner.PlayerName, subjectClientId);
return false;
}
var isBotOrder = subjectClient.Bot != null && clientId == subjectClient.BotControllerClientIndex;
// Drop exploiting orders
// Drop orders from players who shouldn't be able to control this actor
// This may be because the owner changed within the last net tick,
// or, less likely, the client may be trying to do something malicious.
if (subjectClientId != clientId && !isBotOrder)
{
Game.Debug("Detected exploit order from client {0}: {1}", clientId, order.OrderString);
return false;
}
return order.Subject.AcceptsOrder(order.OrderString);
}

View File

@@ -397,11 +397,15 @@ namespace OpenRA.Mods.Common.Traits
{
Activity MoveTo(CPos cell, int nearEnough);
Activity MoveTo(CPos cell, Actor ignoreActor);
Activity MoveWithinRange(Target target, WDist range);
Activity MoveWithinRange(Target target, WDist minRange, WDist maxRange);
Activity MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange);
Activity MoveWithinRange(Target target, WDist range,
WPos? initialTargetPosition = null, Color? targetLineColor = null);
Activity MoveWithinRange(Target target, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null);
Activity MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null);
Activity MoveToTarget(Actor self, Target target,
WPos? initialTargetPosition = null, Color? targetLineColor = null);
Activity MoveIntoWorld(Actor self, CPos cell, SubCell subCell = SubCell.Any);
Activity MoveToTarget(Actor self, Target target);
Activity MoveIntoTarget(Actor self, Target target);
Activity VisualMove(Actor self, WPos fromPos, WPos toPos);
int EstimatedMoveDuration(Actor self, WPos fromPos, WPos toPos);

View File

@@ -0,0 +1,51 @@
#region Copyright & License Information
/*
* Copyright 2007-2018 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, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using System.Linq;
namespace OpenRA.Mods.Common.UpdateRules.Rules
{
public class RemoveAttackSuicides : UpdateRule
{
public override string Name { get { return "AttackSuicides trait has been removed."; } }
public override string Description
{
get
{
return "The AttackSuicides trait has been removed, and should be replaced by\n" +
"AttackFrontal + GrantConditionOnAttack + GrantConditionOnDeploy + a dummy\n" +
"weapon for targeting. Affected actors are listed so that these traits can be defined.";
}
}
readonly List<string> locations = new List<string>();
public override IEnumerable<string> AfterUpdate(ModData modData)
{
if (locations.Any())
yield return "The AttackSuicides trait has been removed from the following actors.\n" +
"You must manually define AttackFrontal, GrantConditionOnAttack, GrantConditionOnDeploy\n" +
"traits and create a dummy weapon to use for targeting:\n" +
UpdateUtils.FormatMessageList(locations);
locations.Clear();
}
public override IEnumerable<string> UpdateActorNode(ModData modData, MiniYamlNode actorNode)
{
if (actorNode.RemoveNodes("AttackSuicides") > 0)
locations.Add("{0} ({1})".F(actorNode.Key, actorNode.Location.Filename));
yield break;
}
}
}

View File

@@ -86,7 +86,7 @@ namespace OpenRA.Mods.Common.UpdateRules
new UpdatePath("release-20180923", "release-20181215", new UpdateRule[0]),
new UpdatePath("release-20181215", new UpdateRule[]
new UpdatePath("release-20181215", "playtest-20190106", new UpdateRule[]
{
// Bleed only changes here
new AddCarryableHarvester(),
@@ -113,6 +113,11 @@ namespace OpenRA.Mods.Common.UpdateRules
new RemoveAttackIgnoresVisibility(),
new ReplacedWithChargeAnimation(),
new RefactorResourceLevelAnimating(),
}),
new UpdatePath("playtest-20190106", new UpdateRule[]
{
new RemoveAttackSuicides(),
})
};

View File

@@ -82,6 +82,9 @@ namespace OpenRA.Mods.Common.Warheads
/// <summary>Checks if the warhead is valid against (can do something to) the frozen actor.</summary>
public bool IsValidAgainst(FrozenActor victim, Actor firedBy)
{
if (!victim.IsValid)
return false;
// AffectsParent checks do not make sense for FrozenActors, so skip to stance checks
var stance = firedBy.Owner.Stances[victim.Owner];
if (!ValidStances.HasStance(stance))

View File

@@ -122,8 +122,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var actorId = actorIDField.Text.ToLowerInvariant();
if (CurrentActor.ID.ToLowerInvariant() != actorId)
{
var found = world.Map.ActorDefinitions.Any(x => x.Key.ToLowerInvariant() == actorId);
if (found)
if (editorActorLayer[actorId] != null)
{
nextActorIDStatus = ActorIDStatus.Duplicate;
return;
@@ -143,8 +142,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic
void SetActorID(World world, string actorId)
{
var actorDef = world.Map.ActorDefinitions.First(x => x.Key == CurrentActor.ID);
actorDef.Key = actorId;
CurrentActor.ID = actorId;
nextActorIDStatus = ActorIDStatus.Normal;
}

View File

@@ -88,6 +88,16 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var copypasteButton = widget.GetOrNull<ButtonWidget>("COPYPASTE_BUTTON");
if (copypasteButton != null)
{
// HACK: Replace Ctrl with Cmd on macOS
// TODO: Add platform-specific override support to HotkeyManager
// and then port the editor hotkeys to this system.
var copyPasteKey = copypasteButton.Key.GetValue();
if (Platform.CurrentPlatform == PlatformType.OSX && copyPasteKey.Modifiers.HasModifier(Modifiers.Ctrl))
{
var modified = new Hotkey(copyPasteKey.Key, copyPasteKey.Modifiers & ~Modifiers.Ctrl | Modifiers.Meta);
copypasteButton.Key = FieldLoader.GetValue<HotkeyReference>("Key", modified.ToString());
}
copypasteButton.OnClick = () => editorViewport.SetBrush(new EditorCopyPasteBrush(editorViewport, worldRenderer, () => copyFilters));
copypasteButton.IsHighlighted = () => editorViewport.CurrentBrush is EditorCopyPasteBrush;
}

View File

@@ -85,8 +85,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
else if (chatTraits != null)
{
var text = chatText.Text.Trim();
var from = world.IsReplay ? null : orderManager.LocalClient.Name;
foreach (var trait in chatTraits)
trait.OnChat(orderManager.LocalClient.Name, text);
trait.OnChat(from, text);
}
}

View File

@@ -70,11 +70,11 @@ namespace OpenRA.Mods.Common.Widgets.Logic
return item;
};
dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 167, options, setupItem);
dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 180, options, setupItem);
}
public static void ShowPlayerActionDropDown(DropDownButtonWidget dropdown, Session.Slot slot,
Session.Client c, OrderManager orderManager, Widget lobby, Action before, Action after)
Session.Client c, OrderManager orderManager, Widget lobby, Action before, Action after)
{
Action<bool> okPressed = tempBan => { orderManager.IssueOrder(Order.Command("kick {0} {1}".F(c.Index, tempBan))); after(); };
var onClick = new Action(() =>
@@ -194,7 +194,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var options = factions.Where(f => f.Value.Selectable).GroupBy(f => f.Value.Side)
.ToDictionary(g => g.Key ?? "", g => g.Select(f => f.Key));
dropdown.ShowDropDown("FACTION_DROPDOWN_TEMPLATE", 150, options, setupItem);
dropdown.ShowDropDown("FACTION_DROPDOWN_TEMPLATE", 154, options, setupItem);
}
public static void ShowColorDropDown(DropDownButtonWidget color, Session.Client client,
@@ -419,7 +419,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
}
public static void SetupPlayerActionWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager,
WorldRenderer worldRenderer, Widget lobby, Action before, Action after)
WorldRenderer worldRenderer, Widget lobby, Action before, Action after)
{
var slot = parent.Get<DropDownButtonWidget>("PLAYER_ACTION");
slot.IsVisible = () => Game.IsHost && c.Index != orderManager.LocalClient.Index;

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using OpenRA.Graphics;
@@ -106,11 +107,11 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var missionMapPaths = kv.Value.Nodes.Select(n => n.Key).ToList();
var previews = modData.MapCache
.Where(p => p.Status == MapStatus.Available)
.Where(p => p.Class == MapClassification.System && p.Status == MapStatus.Available)
.Select(p => new
{
Preview = p,
Index = missionMapPaths.IndexOf(Platform.UnresolvePath(p.Package.Name))
Index = missionMapPaths.IndexOf(Path.GetFileName(p.Package.Name))
})
.Where(x => x.Index != -1)
.OrderBy(x => x.Index)

View File

@@ -47,8 +47,8 @@ namespace OpenRA.Mods.Common.Widgets
public readonly int2 IconMargin = int2.Zero;
public readonly int2 IconSpriteOffset = int2.Zero;
public readonly string TabClick = null;
public readonly string DisabledTabClick = null;
public readonly string ClickSound = ChromeMetrics.Get<string>("ClickSound");
public readonly string ClickDisabledSound = ChromeMetrics.Get<string>("ClickDisabledSound");
public readonly string TooltipContainer;
public readonly string TooltipTemplate = "PRODUCTION_TOOLTIP";
@@ -267,14 +267,15 @@ namespace OpenRA.Mods.Common.Widgets
{
if (PickUpCompletedBuildingIcon(icon, item))
{
Game.Sound.Play(SoundType.UI, TabClick);
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null);
return true;
}
if (item != null && item.Paused)
{
// Resume a paused item
Game.Sound.Play(SoundType.UI, TabClick);
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null);
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", CurrentQueue.Info.QueuedAudio, World.LocalPlayer.Faction.InternalName);
World.IssueOrder(Order.PauseProduction(CurrentQueue.Actor, icon.Name, false));
return true;
}
@@ -284,11 +285,11 @@ namespace OpenRA.Mods.Common.Widgets
if (buildable != null)
{
// Queue a new item
Game.Sound.Play(SoundType.UI, TabClick);
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null);
string notification;
var canQueue = CurrentQueue.CanQueue(buildable, out notification);
if (!CurrentQueue.AllQueued().Any(qi => qi.Item == icon.Name && !qi.Paused && qi.Infinite))
if (!CurrentQueue.AllQueued().Any())
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", notification, World.LocalPlayer.Faction.InternalName);
if (canQueue)
@@ -307,7 +308,7 @@ namespace OpenRA.Mods.Common.Widgets
if (item == null)
return false;
Game.Sound.Play(SoundType.UI, TabClick);
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null);
if (CurrentQueue.Info.DisallowPaused || item.Paused || item.Done || item.TotalCost == item.RemainingCost)
{
@@ -331,7 +332,7 @@ namespace OpenRA.Mods.Common.Widgets
return false;
// Directly cancel, skipping "on-hold"
Game.Sound.Play(SoundType.UI, TabClick);
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null);
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", CurrentQueue.Info.CancelledAudio, World.LocalPlayer.Faction.InternalName);
World.IssueOrder(Order.CancelProduction(CurrentQueue.Actor, icon.Name, handleCount));
@@ -351,7 +352,7 @@ namespace OpenRA.Mods.Common.Widgets
: false;
if (!handled)
Game.Sound.Play(SoundType.UI, DisabledTabClick);
Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickDisabledSound, null);
return true;
}

View File

@@ -80,9 +80,10 @@ namespace OpenRA.Mods.D2k.Traits
public SwallowTarget(Actor self, Target target, bool allowMovement, bool forceAttack)
: base(self, target, allowMovement, forceAttack) { }
protected override Target RecalculateTarget(Actor self)
protected override Target RecalculateTarget(Actor self, out bool targetIsHiddenActor)
{
// Worms ignore visibility, so don't need to recalculate targets
targetIsHiddenActor = false;
return target;
}
}

Some files were not shown because too many files have changed in this diff Show More