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: