Files
OpenRA/OpenRA.Mods.Common/Traits/Player/MissionObjectives.cs
2024-10-04 15:11:27 +03:00

290 lines
8.6 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.Graphics;
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;
}
}
[TraitLocation(SystemActors.Player)]
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;
[FluentReference(optional: true)]
public readonly string WinTextNotification = null;
[NotificationReference("Speech")]
public readonly string LoseNotification = null;
[FluentReference(optional: true)]
public readonly string LoseTextNotification = null;
[NotificationReference("Speech")]
public readonly string LeaveNotification = null;
[FluentReference(optional: true)]
public readonly string LeaveTextNotification = null;
public override object Create(ActorInitializer init) { return new MissionObjectives(init.Self.Owner, this); }
}
public class MissionObjectives : INotifyWinStateChanged, ISync, IResolveOrder, IWorldLoaded
{
public readonly MissionObjectivesInfo Info;
readonly List<MissionObjective> objectives = new();
readonly Player player;
public IReadOnlyList<MissionObjective> Objectives => objectives;
Player[] enemies;
Player[] allies;
[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(Player player, MissionObjectivesInfo info)
{
Info = info;
this.player = player;
}
void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr)
{
// Players and NonCombatants are fixed once the game starts, but the result of IsAlliedWith
// may change once players are marked as spectators, so cache these
allies = player.World.Players.Where(p => !p.NonCombatant && player.IsAlliedWith(p)).ToArray();
enemies = player.World.Players.Where(p => !p.NonCombatant && player.RelationshipWith(p) == PlayerRelationship.Enemy).ToArray();
}
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<INotifyObjectivesUpdated>())
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<INotifyObjectivesUpdated>())
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<INotifyWinStateChanged>())
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<INotifyObjectivesUpdated>())
inou.OnObjectiveFailed(player, objectiveID);
if (objectives[objectiveID].Required)
{
foreach (var inwc in player.PlayerActor.TraitsImplementing<INotifyWinStateChanged>())
inwc.OnPlayerLost(player);
CheckIfGameIsOver(player);
}
}
void CheckIfGameIsOver(Player player)
{
var gameOver = player.World.Players.All(p => p.NonCombatant || p.WinState != WinState.Undefined || !p.HasObjectives);
if (gameOver)
{
Game.RunAfterDelay(Info.GameOverDelay, () =>
{
if (Game.IsCurrentWorld(player.World))
player.World.EndGame();
});
}
}
void INotifyWinStateChanged.OnPlayerWon(Player player)
{
if (Info.Cooperative)
{
WinStateCooperative = WinState.Won;
if (allies.All(p => p.PlayerActor.Trait<MissionObjectives>().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<MissionObjectives>().ForceDefeat(p);
}
}
else
{
player.WinState = WinState.Won;
player.World.OnPlayerWinStateChanged(player);
if (Info.EarlyGameOver)
foreach (var p in enemies)
p.PlayerActor.Trait<MissionObjectives>().ForceDefeat(p);
}
CheckIfGameIsOver(player);
}
void INotifyWinStateChanged.OnPlayerLost(Player player)
{
if (Info.Cooperative)
{
WinStateCooperative = WinState.Lost;
if (allies.Any(p => p.PlayerActor.Trait<MissionObjectives>().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<Player, bool> ObjectiveAdded = (player, inhibitAnnouncement) => player.HasObjectives = true;
public void ResolveOrder(Actor self, Order order)
{
if (order.OrderString == "Surrender")
ForceDefeat(self.Owner);
}
}
[TraitLocation(SystemActors.World)]
[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 readonly string PanelName = null;
[Desc("in ms")]
public readonly int ExitDelay = 1400;
public override object Create(ActorInitializer init) { return new ObjectivesPanel(this); }
}
[TraitLocation(SystemActors.World)]
public class ObjectivesPanel : IObjectivesPanel
{
readonly ObjectivesPanelInfo info;
public ObjectivesPanel(ObjectivesPanelInfo info) { this.info = info; }
public string PanelName => info.PanelName;
public int ExitDelay => info.ExitDelay;
}
}