diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs
index 780673dd7f..f5dfce3daf 100644
--- a/OpenRA.Game/Traits/TraitsInterfaces.cs
+++ b/OpenRA.Game/Traits/TraitsInterfaces.cs
@@ -273,6 +273,15 @@ namespace OpenRA.Traits
public interface IObjectivesPanel { string ObjectivesPanel { get; } }
+ public interface INotifyObjectivesUpdated
+ {
+ void OnPlayerWon(Player winner);
+ void OnPlayerLost(Player loser);
+ void OnObjectiveAdded(Player player, int objectiveID);
+ void OnObjectiveCompleted(Player player, int objectiveID);
+ void OnObjectiveFailed(Player player, int objectiveID);
+ }
+
public static class DisableExts
{
public static bool IsDisabled(this Actor a)
diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs
index d58d68e474..a2ac5c1ca4 100644
--- a/OpenRA.Game/World.cs
+++ b/OpenRA.Game/World.cs
@@ -42,8 +42,19 @@ namespace OpenRA
public void AddPlayer(Player p) { Players.Add(p); }
public Player LocalPlayer { get; private set; }
- Player renderPlayer;
+ public event Action GameOver = () => { };
+ bool gameOver;
+ public void EndGame()
+ {
+ if (!gameOver)
+ {
+ gameOver = true;
+ GameOver();
+ }
+ }
+
public bool ObserveAfterWinOrLose;
+ Player renderPlayer;
public Player RenderPlayer
{
get { return renderPlayer == null || (ObserveAfterWinOrLose && renderPlayer.WinState != WinState.Undefined) ? null : renderPlayer; }
diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj
index 6c4548cde7..bc21912282 100644
--- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj
+++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj
@@ -284,6 +284,7 @@
+
diff --git a/OpenRA.Mods.RA/Player/MissionObjectives.cs b/OpenRA.Mods.RA/Player/MissionObjectives.cs
new file mode 100644
index 0000000000..7fdbcf42c5
--- /dev/null
+++ b/OpenRA.Mods.RA/Player/MissionObjectives.cs
@@ -0,0 +1,202 @@
+#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.Collections.Generic;
+using System.Linq;
+using OpenRA.Primitives;
+using OpenRA.Traits;
+
+namespace OpenRA.Mods.RA
+{
+ public enum ObjectiveType { Primary, Secondary };
+ public enum ObjectiveState { Incomplete, Completed, Failed };
+
+ public class MissionObjective
+ {
+ public readonly ObjectiveType Type;
+ public readonly string Description;
+ public ObjectiveState State;
+
+ public MissionObjective(ObjectiveType type, string description)
+ {
+ Type = type;
+ Description = description;
+ State = ObjectiveState.Incomplete;
+ }
+ }
+
+ public class MissionObjectivesInfo : ITraitInfo
+ {
+ [Desc("Set this to true if multiple cooperative players have a distinct set of " +
+ "objectives that each of them has to complete to win the game. This is mainly " +
+ "useful for multiplayer coop missions. Do not use this for skirmish team games.")]
+ public readonly bool Cooperative = false;
+
+ [Desc("If set to true, this setting causes the game to end immediately once the first " +
+ "player (or team of cooperative players) fails or completes his objectives. If " +
+ "set to false, players that fail their objectives will stick around and become observers.")]
+ public readonly bool EarlyGameOver = false;
+
+ [Desc("Delay between the game over condition being met, and the game actually ending, in milliseconds.")]
+ public readonly int GameOverDelay = 1500;
+
+ public object Create(ActorInitializer init) { return new MissionObjectives(init.world, this); }
+ }
+
+ public class MissionObjectives : INotifyObjectivesUpdated, ISync
+ {
+ readonly MissionObjectivesInfo info;
+ readonly List objectives = new List();
+ public ReadOnlyList Objectives;
+
+ [Sync]
+ public int ObjectivesHash { get { return Objectives.Aggregate(0, (code, objective) => code ^ Sync.hash(objective.State)); } }
+
+ // This property is used as a flag in 'Cooperative' games to mark that the player has completed all his objectives.
+ // The player's WinState is only updated when his allies have all completed their objective as well.
+ public WinState WinStateCooperative { get; private set; }
+
+ public MissionObjectives(World world, MissionObjectivesInfo moInfo)
+ {
+ info = moInfo;
+ Objectives = new ReadOnlyList(objectives);
+
+ world.ObserveAfterWinOrLose = !info.EarlyGameOver;
+ }
+
+ public int Add(Player player, string description, ObjectiveType type = ObjectiveType.Primary)
+ {
+ var newID = objectives.Count;
+
+ objectives.Insert(newID, new MissionObjective(type, description));
+
+ foreach (var imo in player.PlayerActor.TraitsImplementing())
+ imo.OnObjectiveAdded(player, newID);
+
+ return newID;
+ }
+
+ public void MarkCompleted(Player player, int objectiveID)
+ {
+ if (objectiveID >= objectives.Count || objectives[objectiveID].State == ObjectiveState.Completed)
+ return;
+
+ objectives[objectiveID].State = ObjectiveState.Completed;
+
+ if (objectives[objectiveID].Type == ObjectiveType.Primary)
+ {
+ var playerWon = objectives.Where(o => o.Type == ObjectiveType.Primary).All(o => o.State == ObjectiveState.Completed);
+
+ foreach (var imo in player.PlayerActor.TraitsImplementing())
+ {
+ imo.OnObjectiveCompleted(player, objectiveID);
+
+ if (playerWon)
+ imo.OnPlayerWon(player);
+ }
+
+ if (playerWon)
+ CheckIfGameIsOver(player);
+ }
+ }
+
+ public void MarkFailed(Player player, int objectiveID)
+ {
+ if (objectiveID >= objectives.Count || objectives[objectiveID].State == ObjectiveState.Failed)
+ return;
+
+ objectives[objectiveID].State = ObjectiveState.Failed;
+
+ if (objectives[objectiveID].Type == ObjectiveType.Primary)
+ {
+ var playerLost = objectives.Where(o => o.Type == ObjectiveType.Primary).Any(o => o.State == ObjectiveState.Failed);
+
+ foreach (var imo in player.PlayerActor.TraitsImplementing())
+ {
+ imo.OnObjectiveFailed(player, objectiveID);
+
+ if (playerLost)
+ imo.OnPlayerLost(player);
+ }
+
+ if (playerLost)
+ CheckIfGameIsOver(player);
+ }
+ }
+
+ void CheckIfGameIsOver(Player player)
+ {
+ var players = player.World.Players.Where(p => !p.NonCombatant);
+ var allies = players.Where(p => p.IsAlliedWith(player));
+
+ var gameOver = ((info.EarlyGameOver && !info.Cooperative && player.WinState != WinState.Undefined) ||
+ (info.EarlyGameOver && info.Cooperative && allies.All(p => p.WinState != WinState.Undefined)) ||
+ players.All(p => p.WinState != WinState.Undefined));
+
+ if (gameOver)
+ {
+ Game.RunAfterDelay(info.GameOverDelay, () =>
+ {
+ player.World.EndGame();
+ player.World.SetPauseState(true);
+ player.World.PauseStateLocked = true;
+ });
+ }
+ }
+
+ public void OnPlayerWon(Player player)
+ {
+ if (info.Cooperative)
+ {
+ WinStateCooperative = WinState.Won;
+ var players = player.World.Players.Where(p => !p.NonCombatant);
+ var allies = players.Where(p => p.IsAlliedWith(player));
+
+ if (allies.All(p => p.PlayerActor.Trait().WinStateCooperative == WinState.Won))
+ foreach (var p in allies)
+ {
+ p.WinState = WinState.Won;
+ p.World.OnPlayerWinStateChanged(p);
+ }
+ }
+ else
+ {
+ player.WinState = WinState.Won;
+ player.World.OnPlayerWinStateChanged(player);
+ }
+ }
+
+ public void OnPlayerLost(Player player)
+ {
+ if (info.Cooperative)
+ {
+ WinStateCooperative = WinState.Lost;
+ var players = player.World.Players.Where(p => !p.NonCombatant);
+ var allies = players.Where(p => p.IsAlliedWith(player));
+
+ if (allies.Any(p => p.PlayerActor.Trait().WinStateCooperative == WinState.Lost))
+ foreach (var p in allies)
+ {
+ p.WinState = WinState.Lost;
+ p.World.OnPlayerWinStateChanged(p);
+ }
+ }
+ else
+ {
+ player.WinState = WinState.Lost;
+ player.World.OnPlayerWinStateChanged(player);
+ }
+ }
+
+ public void OnObjectiveAdded(Player player, int id) {}
+ public void OnObjectiveCompleted(Player player, int id) {}
+ public void OnObjectiveFailed(Player player, int id) {}
+ }
+}
diff --git a/mods/cnc/rules/player.yaml b/mods/cnc/rules/player.yaml
index d1e0dd8ff8..6fabed5517 100644
--- a/mods/cnc/rules/player.yaml
+++ b/mods/cnc/rules/player.yaml
@@ -2,6 +2,7 @@ Player:
PlaceBuilding:
TechTree:
SupportPowerManager:
+ MissionObjectives:
ConquestVictoryConditions:
PowerManager:
AllyRepair:
diff --git a/mods/d2k/rules/player.yaml b/mods/d2k/rules/player.yaml
index 1112e28208..4cbf92ba82 100644
--- a/mods/d2k/rules/player.yaml
+++ b/mods/d2k/rules/player.yaml
@@ -34,6 +34,7 @@ Player:
BlockedAudio: NoRoom
PlaceBuilding:
SupportPowerManager:
+ MissionObjectives:
ConquestVictoryConditions:
PowerManager:
AdviceInterval: 650
diff --git a/mods/ra/rules/player.yaml b/mods/ra/rules/player.yaml
index 2ed6bd6b24..dfc510caf6 100644
--- a/mods/ra/rules/player.yaml
+++ b/mods/ra/rules/player.yaml
@@ -42,6 +42,7 @@ Player:
RequireOwner: false
PlaceBuilding:
SupportPowerManager:
+ MissionObjectives:
ConquestVictoryConditions:
PowerManager:
AllyRepair:
diff --git a/mods/ts/rules/player.yaml b/mods/ts/rules/player.yaml
index f44970cc41..48e5ad9742 100644
--- a/mods/ts/rules/player.yaml
+++ b/mods/ts/rules/player.yaml
@@ -26,6 +26,7 @@ Player:
LowPowerSlowdown: 3
PlaceBuilding:
SupportPowerManager:
+ MissionObjectives:
ConquestVictoryConditions:
PowerManager:
AllyRepair: