diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
index f823a8bd3a..e8f263effe 100644
--- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
+++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
@@ -307,6 +307,7 @@
+
@@ -379,6 +380,7 @@
+
diff --git a/OpenRA.Mods.Common/Traits/Player/EnemyWatcher.cs b/OpenRA.Mods.Common/Traits/Player/EnemyWatcher.cs
new file mode 100644
index 0000000000..e703837a9e
--- /dev/null
+++ b/OpenRA.Mods.Common/Traits/Player/EnemyWatcher.cs
@@ -0,0 +1,115 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using OpenRA.Traits;
+
+namespace OpenRA.Mods.Common.Traits
+{
+ [Desc("Tracks neutral and enemy actors' visibility and notifies the player.",
+ "Attach this to the player actor.")]
+ class EnemyWatcherInfo : ITraitInfo
+ {
+ [Desc("Interval in ticks between scanning for enemies.")]
+ public readonly int ScanInterval = 25;
+
+ [Desc("Minimal interval in ticks between notifications.")]
+ public readonly int NotificationInterval = 200;
+
+ public object Create(ActorInitializer init) { return new EnemyWatcher(init.Self, this); }
+ }
+
+ class EnemyWatcher : ITick
+ {
+ readonly EnemyWatcherInfo info;
+ readonly Lazy radarPings;
+
+ bool announcedAny;
+ int rescanInterval;
+ int ticksBeforeNextNotification;
+ HashSet lastKnownActorIds;
+ HashSet visibleActorIds;
+ HashSet playedNotifications;
+
+ public EnemyWatcher(Actor self, EnemyWatcherInfo info)
+ {
+ lastKnownActorIds = new HashSet();
+ this.info = info;
+ rescanInterval = info.ScanInterval;
+ ticksBeforeNextNotification = info.NotificationInterval;
+ radarPings = Exts.Lazy(() => self.World.WorldActor.Trait());
+ }
+
+ public void Tick(Actor self)
+ {
+ // TODO: Make the AI handle such notifications and remove Owner.IsBot from this check
+ // Disable notifications for AI and neutral players (creeps) and for spectators
+ if (self.Owner.Shroud.Disabled || self.Owner.IsBot || !self.Owner.Playable || self.Owner.PlayerReference.Spectating)
+ return;
+
+ rescanInterval--;
+ ticksBeforeNextNotification--;
+
+ if (rescanInterval > 0 || ticksBeforeNextNotification > 0)
+ return;
+
+ rescanInterval = info.ScanInterval;
+
+ announcedAny = false;
+ visibleActorIds = new HashSet();
+ playedNotifications = new HashSet();
+
+ foreach (var actor in self.World.ActorsWithTrait())
+ {
+ // We only care about enemy actors (creeps should be enemies)
+ if ((actor.Actor.EffectiveOwner != null && self.Owner.Stances[actor.Actor.EffectiveOwner.Owner] != Stance.Enemy)
+ || self.Owner.Stances[actor.Actor.Owner] != Stance.Enemy)
+ continue;
+
+ // The actor is not currently visible
+ if (!self.Owner.Shroud.IsVisible(actor.Actor))
+ continue;
+
+ visibleActorIds.Add(actor.Actor.ActorID);
+
+ // We already know about this actor
+ if (lastKnownActorIds.Contains(actor.Actor.ActorID))
+ continue;
+
+ // We have already played this type of notification
+ if (playedNotifications.Contains(actor.Trait.Info.Notification))
+ continue;
+
+ Announce(self, actor);
+ }
+
+ if (announcedAny)
+ ticksBeforeNextNotification = info.NotificationInterval;
+
+ lastKnownActorIds = visibleActorIds;
+ }
+
+ void Announce(Actor self, TraitPair announce)
+ {
+ // Audio notification
+ if (self.World.LocalPlayer != null)
+ Sound.PlayNotification(self.World.Map.Rules, self.World.LocalPlayer, "Speech", announce.Trait.Info.Notification, self.Owner.Country.Race);
+
+ // Radar notificaion
+ if (announce.Trait.Info.PingRadar && radarPings.Value != null)
+ radarPings.Value.Add(() => true, announce.Actor.CenterPosition, Color.Red, 50);
+
+ playedNotifications.Add(announce.Trait.Info.Notification);
+ announcedAny = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenRA.Mods.Common/Traits/Sound/AnnounceOnSeen.cs b/OpenRA.Mods.Common/Traits/Sound/AnnounceOnSeen.cs
new file mode 100644
index 0000000000..f7277d645a
--- /dev/null
+++ b/OpenRA.Mods.Common/Traits/Sound/AnnounceOnSeen.cs
@@ -0,0 +1,35 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * see COPYING.
+ */
+#endregion
+
+using OpenRA.Traits;
+
+namespace OpenRA.Mods.Common.Traits
+{
+ [Desc("Players will be notified when this actor becomes visible to them.")]
+ public class AnnounceOnSeenInfo : ITraitInfo
+ {
+ [Desc("Should there be a radar ping on enemies' radar at the actor's location when they see him")]
+ public readonly bool PingRadar = false;
+
+ public readonly string Notification = "EnemyUnitSighted";
+
+ public object Create(ActorInitializer init) { return new AnnounceOnSeen(this); }
+ }
+
+ public class AnnounceOnSeen
+ {
+ public readonly AnnounceOnSeenInfo Info;
+
+ public AnnounceOnSeen(AnnounceOnSeenInfo info)
+ {
+ Info = info;
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenRA.Mods.D2k/Traits/World/WormManager.cs b/OpenRA.Mods.D2k/Traits/World/WormManager.cs
index f3a5175656..7f67227259 100644
--- a/OpenRA.Mods.D2k/Traits/World/WormManager.cs
+++ b/OpenRA.Mods.D2k/Traits/World/WormManager.cs
@@ -43,7 +43,6 @@ namespace OpenRA.Mods.D2k.Traits
{
readonly WormManagerInfo info;
readonly Lazy spawnPointActors;
- readonly Lazy radarPings;
int spawnCountdown;
int wormsPresent;
@@ -51,7 +50,6 @@ namespace OpenRA.Mods.D2k.Traits
public WormManager(Actor self, WormManagerInfo info)
{
this.info = info;
- radarPings = Exts.Lazy(() => self.World.WorldActor.Trait());
spawnPointActors = Exts.Lazy(() => self.World.ActorsWithTrait().Select(x => x.Actor).ToArray());
}
@@ -80,8 +78,6 @@ namespace OpenRA.Mods.D2k.Traits
// more we need to reach the defined minimum count.
wormLocations.Add(SpawnWorm(self));
} while (wormsPresent < info.Minimum);
-
- AnnounceWormSign(self, wormLocations);
}
WPos SpawnWorm(Actor self)
@@ -107,17 +103,5 @@ namespace OpenRA.Mods.D2k.Traits
{
wormsPresent--;
}
-
- void AnnounceWormSign(Actor self, IEnumerable wormLocations)
- {
- if (self.World.LocalPlayer != null)
- Sound.PlayNotification(self.World.Map.Rules, self.World.LocalPlayer, "Speech", info.WormSignNotification, self.World.LocalPlayer.Country.Race);
-
- if (radarPings.Value == null)
- return;
-
- foreach (var wormLocation in wormLocations)
- radarPings.Value.Add(() => true, wormLocation, Color.Red, 50);
- }
}
}
diff --git a/mods/d2k/rules/arrakis.yaml b/mods/d2k/rules/arrakis.yaml
index cc93955b22..ea6968d4cb 100644
--- a/mods/d2k/rules/arrakis.yaml
+++ b/mods/d2k/rules/arrakis.yaml
@@ -50,4 +50,7 @@ SANDWORM:
Weapon: WormJaw
Sandworm:
WanderMoveRadius: 5
- IgnoresCloak:
\ No newline at end of file
+ IgnoresCloak:
+ AnnounceOnSeen:
+ Notification: WormSign
+ PingRadar: True
\ No newline at end of file
diff --git a/mods/d2k/rules/defaults.yaml b/mods/d2k/rules/defaults.yaml
index 5facb580e7..f17699fa80 100644
--- a/mods/d2k/rules/defaults.yaml
+++ b/mods/d2k/rules/defaults.yaml
@@ -50,6 +50,8 @@
UpgradeManager:
TemporaryOwnerManager:
MustBeDestroyed:
+ AnnounceOnSeen:
+ Notification: EnemyUnitsApproaching
^Tank:
AppearsOnRadar:
@@ -103,6 +105,8 @@
UpgradeManager:
TemporaryOwnerManager:
MustBeDestroyed:
+ AnnounceOnSeen:
+ Notification: EnemyUnitsApproaching
^Husk:
Health:
@@ -227,6 +231,8 @@
UpgradeMinEnabledLevel: 1
UpgradeManager:
MustBeDestroyed:
+ AnnounceOnSeen:
+ Notification: EnemyUnitsApproaching
^Plane:
AppearsOnRadar:
@@ -260,6 +266,8 @@
UpgradeTypes: selfheal
UpgradeMinEnabledLevel: 1
UpgradeManager:
+ AnnounceOnSeen:
+ Notification: EnemyUnitsApproaching
^Helicopter:
Inherits: ^Plane
diff --git a/mods/d2k/rules/player.yaml b/mods/d2k/rules/player.yaml
index aa08ed5a58..68585dcb96 100644
--- a/mods/d2k/rules/player.yaml
+++ b/mods/d2k/rules/player.yaml
@@ -72,4 +72,4 @@ Player:
ProvidesTechPrerequisite@all:
Name: Unrestricted
Prerequisites: techlevel.low, techlevel.medium, techlevel.high, techlevel.superweapons
-
+ EnemyWatcher: