#region Copyright & License Information /* * Copyright 2007-2020 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; using System.Collections.Generic; using System.Linq; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { public enum ObjectiveState { Incomplete, Completed, Failed } public class MissionObjective { public readonly bool Required; public readonly string Description; public readonly string Type; public ObjectiveState State; public MissionObjective(string description, string type, bool required = true) { Description = description; Type = type; Required = required; State = ObjectiveState.Incomplete; } } public class MissionObjectivesInfo : TraitInfo { [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; [NotificationReference("Speech")] public readonly string WinNotification = null; [NotificationReference("Speech")] public readonly string LoseNotification = null; [NotificationReference("Speech")] public readonly string LeaveNotification = null; public override object Create(ActorInitializer init) { return new MissionObjectives(init.World, this); } } public class MissionObjectives : INotifyWinStateChanged, ISync, IResolveOrder { public readonly MissionObjectivesInfo Info; readonly List objectives = new List(); public ReadOnlyList Objectives; [Sync] public int ObjectivesHash { get { var hash = 0; foreach (var objective in objectives) hash ^= Sync.HashUsingHashCode(objective.State); return hash; } } // 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 info) { Info = info; Objectives = new ReadOnlyList(objectives); } public int Add(Player player, string description, string type, bool required = true, bool inhibitAnnouncement = false) { var newID = objectives.Count; objectives.Insert(newID, new MissionObjective(description, type, required)); ObjectiveAdded(player, inhibitAnnouncement); foreach (var inou in player.PlayerActor.TraitsImplementing()) inou.OnObjectiveAdded(player, newID); return newID; } public void MarkCompleted(Player player, int objectiveID) { if (objectiveID >= objectives.Count || objectives[objectiveID].State != ObjectiveState.Incomplete) return; objectives[objectiveID].State = ObjectiveState.Completed; foreach (var inou in player.PlayerActor.TraitsImplementing()) inou.OnObjectiveCompleted(player, objectiveID); if (objectives[objectiveID].Required && objectives.Where(o => o.Required).All(o => o.State == ObjectiveState.Completed)) { foreach (var inwc in player.PlayerActor.TraitsImplementing()) inwc.OnPlayerWon(player); CheckIfGameIsOver(player); } } public void MarkFailed(Player player, int objectiveID) { if (objectiveID >= objectives.Count || objectives[objectiveID].State == ObjectiveState.Failed) return; objectives[objectiveID].State = ObjectiveState.Failed; foreach (var inou in player.PlayerActor.TraitsImplementing()) inou.OnObjectiveFailed(player, objectiveID); if (objectives[objectiveID].Required) { foreach (var inwc in player.PlayerActor.TraitsImplementing()) inwc.OnPlayerLost(player); CheckIfGameIsOver(player); } } void CheckIfGameIsOver(Player player) { var players = player.World.Players.Where(p => !p.NonCombatant); var gameOver = players.All(p => p.WinState != WinState.Undefined || !p.HasObjectives); if (gameOver) Game.RunAfterDelay(Info.GameOverDelay, () => { if (!Game.IsCurrentWorld(player.World)) return; player.World.EndGame(); player.World.SetPauseState(true); player.World.PauseStateLocked = true; }); } void INotifyWinStateChanged.OnPlayerWon(Player player) { var players = player.World.Players.Where(p => !p.NonCombatant); var enemies = players.Where(p => !p.IsAlliedWith(player)); if (Info.Cooperative) { WinStateCooperative = WinState.Won; 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); } if (Info.EarlyGameOver) foreach (var p in enemies) p.PlayerActor.Trait().ForceDefeat(p); } } else { player.WinState = WinState.Won; player.World.OnPlayerWinStateChanged(player); if (Info.EarlyGameOver) foreach (var p in enemies) p.PlayerActor.Trait().ForceDefeat(p); } CheckIfGameIsOver(player); } void INotifyWinStateChanged.OnPlayerLost(Player player) { var players = player.World.Players.Where(p => !p.NonCombatant); var enemies = players.Where(p => !p.IsAlliedWith(player)); if (Info.Cooperative) { WinStateCooperative = WinState.Lost; 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); } if (Info.EarlyGameOver) { foreach (var p in enemies) { p.WinState = WinState.Won; p.World.OnPlayerWinStateChanged(p); } } } } else { player.WinState = WinState.Lost; player.World.OnPlayerWinStateChanged(player); if (Info.EarlyGameOver) { foreach (var p in enemies) { p.WinState = WinState.Won; p.World.OnPlayerWinStateChanged(p); } } } CheckIfGameIsOver(player); } public void ForceDefeat(Player player) { for (var id = 0; id < Objectives.Count; id++) if (Objectives[id].State == ObjectiveState.Incomplete) MarkFailed(player, id); } public event Action ObjectiveAdded = (player, inhibitAnnouncement) => { player.HasObjectives = true; }; public void ResolveOrder(Actor self, Order order) { if (order.OrderString == "Surrender") { ForceDefeat(self.Owner); self.Owner.Spectating = true; } } } [Desc("Provides game mode progress information for players.", "Goes on WorldActor - observers don't have a player it can live on.", "Current options for PanelName are 'SKIRMISH_STATS' and 'MISSION_OBJECTIVES'.")] public class ObjectivesPanelInfo : TraitInfo { public string PanelName = null; [Desc("in ms")] public int ExitDelay = 1400; public override object Create(ActorInitializer init) { return new ObjectivesPanel(this); } } public class ObjectivesPanel : IObjectivesPanel { readonly ObjectivesPanelInfo info; public ObjectivesPanel(ObjectivesPanelInfo info) { this.info = info; } public string PanelName { get { return info.PanelName; } } public int ExitDelay { get { return info.ExitDelay; } } } }