From a3bf3e7403297951313b4ec32ff946223b0c8fc8 Mon Sep 17 00:00:00 2001 From: Oliver Brakmann Date: Sun, 27 Jul 2014 11:00:55 +0200 Subject: [PATCH] Add support for mission objectives Objectives can be either primary or secondary objectives. Primary ones influence the outcome of the game. If all primary objectives are completed the game is won, and lost when any of them fails. Objectives can be added at any stage during the game, allowing to react dynamically to game events. The objectives backend only contains the information about the objectives themselves. It does not check if objectives are completed or failed. Instead, the state of objectives must be manually marked. The backend, however, does check whether the game is won or lost. --- OpenRA.Game/Traits/TraitsInterfaces.cs | 9 + OpenRA.Game/World.cs | 13 +- OpenRA.Mods.RA/OpenRA.Mods.RA.csproj | 1 + OpenRA.Mods.RA/Player/MissionObjectives.cs | 202 +++++++++++++++++++++ mods/cnc/rules/player.yaml | 1 + mods/d2k/rules/player.yaml | 1 + mods/ra/rules/player.yaml | 1 + mods/ts/rules/player.yaml | 1 + 8 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 OpenRA.Mods.RA/Player/MissionObjectives.cs 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: