diff --git a/OpenRA.Mods.Common/Traits/Player/ConquestVictoryConditions.cs b/OpenRA.Mods.Common/Traits/Player/ConquestVictoryConditions.cs index 84dc4bc665..b178070c49 100644 --- a/OpenRA.Mods.Common/Traits/Player/ConquestVictoryConditions.cs +++ b/OpenRA.Mods.Common/Traits/Player/ConquestVictoryConditions.cs @@ -10,6 +10,7 @@ #endregion using System.Linq; +using OpenRA.Network; using OpenRA.Primitives; using OpenRA.Traits; @@ -29,7 +30,7 @@ namespace OpenRA.Mods.Common.Traits public object Create(ActorInitializer init) { return new ConquestVictoryConditions(init.Self, this); } } - public class ConquestVictoryConditions : ITick, INotifyWinStateChanged + public class ConquestVictoryConditions : ITick, INotifyWinStateChanged, INotifyTimeLimit { readonly ConquestVictoryConditionsInfo info; readonly MissionObjectives mo; @@ -69,6 +70,27 @@ namespace OpenRA.Mods.Common.Traits mo.MarkCompleted(self.Owner, objectiveID); } + void INotifyTimeLimit.NotifyTimerExpired(Actor self) + { + if (objectiveID < 0) + return; + + var myTeam = self.World.LobbyInfo.ClientWithIndex(self.Owner.ClientIndex).Team; + var teams = self.World.Players.Where(p => !p.NonCombatant && p.Playable) + .Select(p => new Pair(p, p.PlayerActor.TraitOrDefault())) + .OrderByDescending(p => p.Second != null ? p.Second.Experience : 0) + .GroupBy(p => (self.World.LobbyInfo.ClientWithIndex(p.First.ClientIndex) ?? new Session.Client()).Team) + .OrderByDescending(g => g.Sum(gg => gg.Second != null ? gg.Second.Experience : 0)); + + if (teams.First().Key == myTeam && (myTeam != 0 || teams.First().First().First == self.Owner)) + { + mo.MarkCompleted(self.Owner, objectiveID); + return; + } + + mo.MarkFailed(self.Owner, objectiveID); + } + void INotifyWinStateChanged.OnPlayerLost(Player player) { foreach (var a in player.World.ActorsWithTrait().Where(a => a.Actor.Owner == player)) diff --git a/OpenRA.Mods.Common/Traits/Player/StrategicVictoryConditions.cs b/OpenRA.Mods.Common/Traits/Player/StrategicVictoryConditions.cs index 043fb76e55..9bda9f3605 100644 --- a/OpenRA.Mods.Common/Traits/Player/StrategicVictoryConditions.cs +++ b/OpenRA.Mods.Common/Traits/Player/StrategicVictoryConditions.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Linq; +using OpenRA.Network; using OpenRA.Primitives; using OpenRA.Traits; @@ -44,7 +45,7 @@ namespace OpenRA.Mods.Common.Traits public object Create(ActorInitializer init) { return new StrategicVictoryConditions(init.Self, this); } } - public class StrategicVictoryConditions : ITick, ISync, INotifyWinStateChanged + public class StrategicVictoryConditions : ITick, ISync, INotifyWinStateChanged, INotifyTimeLimit { readonly StrategicVictoryConditionsInfo info; @@ -108,6 +109,27 @@ namespace OpenRA.Mods.Common.Traits } } + void INotifyTimeLimit.NotifyTimerExpired(Actor self) + { + if (objectiveID < 0) + return; + + var myTeam = self.World.LobbyInfo.ClientWithIndex(self.Owner.ClientIndex).Team; + var teams = self.World.Players.Where(p => !p.NonCombatant && p.Playable) + .Select(p => new Pair(p, p.PlayerActor.TraitOrDefault())) + .OrderByDescending(p => p.Second != null ? p.Second.Experience : 0) + .GroupBy(p => (self.World.LobbyInfo.ClientWithIndex(p.First.ClientIndex) ?? new Session.Client()).Team) + .OrderByDescending(g => g.Sum(gg => gg.Second != null ? gg.Second.Experience : 0)); + + if (teams.First().Key == myTeam && (myTeam != 0 || teams.First().First().First == self.Owner)) + { + mo.MarkCompleted(self.Owner, objectiveID); + return; + } + + mo.MarkFailed(self.Owner, objectiveID); + } + void INotifyWinStateChanged.OnPlayerLost(Player player) { foreach (var a in player.World.ActorsWithTrait().Where(a => a.Actor.Owner == player)) diff --git a/OpenRA.Mods.Common/Traits/Player/TimeLimitManager.cs b/OpenRA.Mods.Common/Traits/Player/TimeLimitManager.cs new file mode 100644 index 0000000000..00cb64c186 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Player/TimeLimitManager.cs @@ -0,0 +1,139 @@ +#region Copyright & License Information +/* + * Copyright 2007-2019 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.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("This trait allows setting a time limit on matches. Attach this to the World actor.")] + public class TimeLimitManagerInfo : ITraitInfo, ILobbyOptions + { + [Desc("Label that will be shown for the time limit option in the lobby.")] + public readonly string TimeLimitLabel = "Time Limit"; + + [Desc("Tooltip description that will be shown for the time limit option in the lobby.")] + public readonly string TimeLimitDescription = "Player or team with the highest score after this time wins"; + + [Desc("Time Limit options that will be shown in the lobby dropdown. Values are in minutes.")] + public readonly int[] TimeLimitOptions = { 0, 10, 20, 30, 40, 60, 90 }; + + [Desc("List of remaining minutes of game time when a text and optional speech notification should be made to players.")] + public readonly Dictionary TimeLimitWarnings = new Dictionary + { + { 1, null }, + { 2, null }, + { 3, null }, + { 4, null }, + { 5, null }, + { 10, null }, + }; + + [Desc("Default selection for the time limit option in the lobby. Should use one of the TimeLimitOptions.")] + public readonly int TimeLimitDefault = 0; + + [Desc("Prevent the time limit option from being changed in the lobby.")] + public readonly bool TimeLimitLocked = false; + + [Desc("Display order for the time limit dropdown in the lobby.")] + public readonly int TimeLimitDisplayOrder = 0; + + [Desc("Notification text for time limit warnings. The string '{0}' will be replaced by the remaining time in minutes, '{1}' is used for the plural form.")] + public readonly string Notification = "{0} minute{1} remaining."; + + IEnumerable ILobbyOptions.LobbyOptions(Ruleset rules) + { + var timelimits = TimeLimitOptions.ToDictionary(c => c.ToString(), c => + { + if (c == 0) + return "No limit"; + else + return c.ToString() + " minute{0}".F(c > 1 ? "s" : null); + }); + + yield return new LobbyOption("timelimit", TimeLimitLabel, TimeLimitDescription, true, TimeLimitDisplayOrder, + new ReadOnlyDictionary(timelimits), TimeLimitDefault.ToString(), TimeLimitLocked); + } + + public object Create(ActorInitializer init) { return new TimeLimitManager(init.Self, this); } + } + + public class TimeLimitManager : INotifyTimeLimit, ITick, IWorldLoaded + { + readonly TimeLimitManagerInfo info; + MapOptions mapOptions; + int ticksRemaining; + + public int TimeLimit; + public string Notification; + + public TimeLimitManager(Actor self, TimeLimitManagerInfo info) + { + this.info = info; + Notification = info.Notification; + + var tl = self.World.LobbyInfo.GlobalSettings.OptionOrDefault("timelimit", info.TimeLimitDefault.ToString()); + if (!int.TryParse(tl, out TimeLimit)) + TimeLimit = info.TimeLimitDefault; + + // Convert from minutes to ticks + TimeLimit *= 60 * (1000 / self.World.Timestep); + } + + void IWorldLoaded.WorldLoaded(World w, OpenRA.Graphics.WorldRenderer wr) + { + mapOptions = w.WorldActor.Trait(); + } + + void ITick.Tick(Actor self) + { + if (TimeLimit <= 0) + return; + + var ticksPerSecond = 1000 / (self.World.IsReplay ? mapOptions.GameSpeed.Timestep : self.World.Timestep); + ticksRemaining = TimeLimit - self.World.WorldTick; + + if (ticksRemaining == 0) + { + foreach (var ntl in self.TraitsImplementing()) + ntl.NotifyTimerExpired(self); + + foreach (var p in self.World.Players) + foreach (var ntl in p.PlayerActor.TraitsImplementing()) + ntl.NotifyTimerExpired(p.PlayerActor); + + return; + } + + if (ticksRemaining < 0) + return; + + foreach (var m in info.TimeLimitWarnings.Keys) + { + if (ticksRemaining == m * 60 * ticksPerSecond) + { + Game.AddSystemLine("Battlefield Control", Notification.F(m, m > 1 ? "s" : null)); + + var faction = self.World.LocalPlayer == null ? null : self.World.LocalPlayer.Faction.InternalName; + Game.Sound.PlayNotification(self.World.Map.Rules, self.World.LocalPlayer, "Speech", info.TimeLimitWarnings[m], faction); + } + } + } + + void INotifyTimeLimit.NotifyTimerExpired(Actor self) + { + Game.AddSystemLine("Battlefield Control", "Time limit has expired."); + } + } +} diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index 409f166e37..4d6b62ed7e 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -583,4 +583,10 @@ namespace OpenRA.Mods.Common.Traits { void MovementTypeChanged(Actor self, MovementType type); } + + [RequireExplicitImplementation] + public interface INotifyTimeLimit + { + void NotifyTimerExpired(Actor self); + } } diff --git a/mods/cnc/rules/world.yaml b/mods/cnc/rules/world.yaml index e52810995b..cdf42bb53c 100644 --- a/mods/cnc/rules/world.yaml +++ b/mods/cnc/rules/world.yaml @@ -259,6 +259,7 @@ World: LoadWidgetAtGameStart: ShellmapRoot: MENU_BACKGROUND ScriptTriggers: + TimeLimitManager: EditorWorld: Inherits: ^BaseWorld diff --git a/mods/d2k/rules/world.yaml b/mods/d2k/rules/world.yaml index c30b552ff6..ed23d06693 100644 --- a/mods/d2k/rules/world.yaml +++ b/mods/d2k/rules/world.yaml @@ -234,6 +234,7 @@ World: LoadWidgetAtGameStart: ScriptTriggers: StartGameNotification: + TimeLimitManager: EditorWorld: Inherits: ^BaseWorld diff --git a/mods/ra/rules/world.yaml b/mods/ra/rules/world.yaml index f704f28ea6..320f7b94bb 100644 --- a/mods/ra/rules/world.yaml +++ b/mods/ra/rules/world.yaml @@ -265,6 +265,17 @@ World: PanelName: SKIRMISH_STATS LoadWidgetAtGameStart: ScriptTriggers: + TimeLimitManager: + TimeLimitWarnings: + 40: FourtyMinutesRemaining + 30: ThirtyMinutesRemaining + 20: TwentyMinutesRemaining + 10: TenMinutesRemaining + 5: WarningFiveMinutesRemaining + 4: WarningFourMinutesRemaining + 3: WarningThreeMinutesRemaining + 2: WarningTwoMinutesRemaining + 1: WarningOneMinuteRemaining EditorWorld: Inherits: ^BaseWorld diff --git a/mods/ts/rules/world.yaml b/mods/ts/rules/world.yaml index 47c01602dd..f6fc19d4d1 100644 --- a/mods/ts/rules/world.yaml +++ b/mods/ts/rules/world.yaml @@ -369,6 +369,7 @@ World: LoadWidgetAtGameStart: ShellmapRoot: MAINMENU_PRERELEASE_NOTIFICATION ScriptTriggers: + TimeLimitManager: EditorWorld: Inherits: ^BaseWorld