diff --git a/OpenRA.Mods.CA/Traits/CloneProducer.cs b/OpenRA.Mods.CA/Traits/CloneProducer.cs index 83d0eb144b..3f48fa88a3 100644 --- a/OpenRA.Mods.CA/Traits/CloneProducer.cs +++ b/OpenRA.Mods.CA/Traits/CloneProducer.cs @@ -87,7 +87,7 @@ void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) public void UnitProduced(Actor unit) { - if (Info.InvalidActors.Contains(unit.Info.Name)) + if (self.IsDead || Info.InvalidActors.Contains(unit.Info.Name)) return; var sp = self.TraitsImplementing() diff --git a/OpenRA.Mods.CA/Traits/MindController.cs b/OpenRA.Mods.CA/Traits/MindController.cs index 11f936099b..33bbdcada6 100644 --- a/OpenRA.Mods.CA/Traits/MindController.cs +++ b/OpenRA.Mods.CA/Traits/MindController.cs @@ -308,7 +308,7 @@ public void ResolveOrder(Actor self, Order order) } // For all other orders, if target has changed, reset progress - if (order.Target.Actor != currentTarget.Actor) + if (order.Target.Actor != currentTarget.Actor && !order.Queued) { if (Info.AutoUndeploy && deployTrait != null && deployTrait.DeployState == DeployState.Deployed && currentTarget.Actor != null && order.OrderString != "GrantConditionOnDeploy") deployTrait.Undeploy(); diff --git a/OpenRA.Mods.CA/Traits/Player/BuildOrderTracker.cs b/OpenRA.Mods.CA/Traits/Player/BuildOrderTracker.cs new file mode 100644 index 0000000000..ffee9fa419 --- /dev/null +++ b/OpenRA.Mods.CA/Traits/Player/BuildOrderTracker.cs @@ -0,0 +1,48 @@ +#region Copyright & License Information +/** + * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). + * This file is part of OpenRA Combined Arms, 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.Collections.Generic; +using OpenRA.Traits; + +namespace OpenRA.Mods.CA.Traits +{ + [Desc("Keeps track of player's initial build order for observer stats.")] + public class BuildOrderTrackerInfo : TraitInfo + { + [Desc("Maximum number of items to track.")] + public readonly int MaxItems = 12; + + public override object Create(ActorInitializer init) { return new BuildOrderTracker(init.Self, this); } + } + + public class BuildOrderTracker + { + readonly BuildOrderTrackerInfo info; + List buildOrder; + public int Count { get; private set; } + public List BuildOrder => buildOrder; + + public BuildOrderTracker(Actor self, BuildOrderTrackerInfo info) + { + this.info = info; + buildOrder = new List(); + Count = 0; + } + + public void BuildingCreated(string type) + { + if (Count >= info.MaxItems) + return; + + Count++; + buildOrder.Add(type); + } + } +} diff --git a/OpenRA.Mods.CA/Traits/Player/UpgradesManager.cs b/OpenRA.Mods.CA/Traits/Player/UpgradesManager.cs index deae09e89b..9ed0218502 100644 --- a/OpenRA.Mods.CA/Traits/Player/UpgradesManager.cs +++ b/OpenRA.Mods.CA/Traits/Player/UpgradesManager.cs @@ -31,6 +31,8 @@ public class UpgradesManager public int Hash { get; private set; } + public HashSet UnlockedUpgradeTypes => unlockedUpgradeTypes; + public UpgradesManager(Actor self, UpgradesManagerInfo info) { this.self = self; diff --git a/OpenRA.Mods.CA/Traits/Render/WithEnabledAnimation.cs b/OpenRA.Mods.CA/Traits/Render/WithEnabledAnimation.cs index 16f775a720..91ce76a007 100644 --- a/OpenRA.Mods.CA/Traits/Render/WithEnabledAnimation.cs +++ b/OpenRA.Mods.CA/Traits/Render/WithEnabledAnimation.cs @@ -15,7 +15,7 @@ namespace OpenRA.Mods.CA.Traits.Render { - [Desc("Plays an animation when trait is enabled or re-enabled, replacing the default body animation (plays once, unlike WithEnabledAnimation).")] + [Desc("Plays an animation when trait is enabled or re-enabled, replacing the default body animation (plays once, unlike WithIdleAnimation).")] public class WithEnabledAnimationInfo : ConditionalTraitInfo, Requires { [SequenceReference] diff --git a/OpenRA.Mods.CA/Traits/Sound/AmbientSoundCA.cs b/OpenRA.Mods.CA/Traits/Sound/AmbientSoundCA.cs index 664c48d59e..2e18972593 100644 --- a/OpenRA.Mods.CA/Traits/Sound/AmbientSoundCA.cs +++ b/OpenRA.Mods.CA/Traits/Sound/AmbientSoundCA.cs @@ -69,7 +69,7 @@ void ITick.Tick(Actor self) if (IsTraitDisabled) return; - if (self.World.IsGameOver) + if (self.World.IsGameOver || self.World.Paused) StopSounds(self, false); if (Info.InitialSound != null && !initialSoundComplete) diff --git a/OpenRA.Mods.CA/Traits/UnitConverter.cs b/OpenRA.Mods.CA/Traits/UnitConverter.cs index f006f6b711..1981d1b58a 100644 --- a/OpenRA.Mods.CA/Traits/UnitConverter.cs +++ b/OpenRA.Mods.CA/Traits/UnitConverter.cs @@ -80,6 +80,7 @@ public class UnitConverter : ConditionalTrait, ITick, INotify protected PlayerResources playerResources; int conditionToken = Actor.InvalidConditionToken; bool eject = false; + bool blocked = false; public UnitConverter(ActorInitializer init, UnitConverterInfo info) : base(info) @@ -147,26 +148,31 @@ void ITick.Tick(Actor self) var outputActor = eject ? nextItem.InputActor : nextItem.OutputActor; var exitSound = info.ReadyAudio; - if (!eject) + if (!blocked) { - var expectedRemainingCost = nextItem.BuildDurationRemaining == 1 ? 0 : nextItem.ConversionCost * nextItem.BuildDurationRemaining / Math.Max(1, nextItem.BuildDuration); - var costThisFrame = nextItem.ConversionCostRemaining - expectedRemainingCost; + if (!eject) + { + var expectedRemainingCost = nextItem.BuildDurationRemaining == 1 ? 0 : nextItem.ConversionCost * nextItem.BuildDurationRemaining / Math.Max(1, nextItem.BuildDuration); + var costThisFrame = nextItem.ConversionCostRemaining - expectedRemainingCost; - if (costThisFrame != 0 && !playerResources.TakeCash(costThisFrame, true)) - return; + if (costThisFrame != 0 && !playerResources.TakeCash(costThisFrame, true)) + return; - nextItem.ConversionCostRemaining -= costThisFrame; - nextItem.BuildDurationRemaining -= 1; - if (nextItem.BuildDurationRemaining > 0) - return; - } - else - { - playerResources.GiveCash(nextItem.ConversionCost - nextItem.ConversionCostRemaining); + nextItem.ConversionCostRemaining -= costThisFrame; + nextItem.BuildDurationRemaining -= 1; + if (nextItem.BuildDurationRemaining > 0) + return; + } + else + { + playerResources.GiveCash(nextItem.ConversionCost - nextItem.ConversionCostRemaining); + } } if (nextItem.Producer.Produce(nextItem.Actor, outputActor, nextItem.ProductionType, nextItem.Inits, 0)) { + blocked = false; + if (!eject) Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", exitSound, self.Owner.Faction.InternalName); @@ -178,9 +184,10 @@ void ITick.Tick(Actor self) RevokeCondition(self); } } - else if (!eject) + else if (!eject && !blocked) { Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", Info.BlockedAudio, self.Owner.Faction.InternalName); + blocked = true; } } diff --git a/OpenRA.Mods.CA/Traits/UpdatesBuildOrder.cs b/OpenRA.Mods.CA/Traits/UpdatesBuildOrder.cs new file mode 100644 index 0000000000..4c8e44d019 --- /dev/null +++ b/OpenRA.Mods.CA/Traits/UpdatesBuildOrder.cs @@ -0,0 +1,37 @@ +#region Copyright & License Information +/** + * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). + * This file is part of OpenRA Combined Arms, 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 OpenRA.Traits; + +namespace OpenRA.Mods.CA.Traits +{ + [Desc("Added to build order when the actor is created.")] + public class UpdatesBuildOrderInfo : TraitInfo + { + public override object Create(ActorInitializer init) { return new UpdatesBuildOrder(init, this); } + } + + public class UpdatesBuildOrder : INotifyCreated + { + public readonly UpdatesBuildOrderInfo Info; + readonly BuildOrderTracker buildOrderTracker; + + public UpdatesBuildOrder(ActorInitializer init, UpdatesBuildOrderInfo info) + { + Info = info; + buildOrderTracker = init.Self.Owner.PlayerActor.Trait(); + } + + void INotifyCreated.Created(Actor self) + { + buildOrderTracker.BuildingCreated(self.Info.Name); + } + } +} diff --git a/OpenRA.Mods.CA/Widgets/Logic/Ingame/ObserverStatusLogicCA.cs b/OpenRA.Mods.CA/Widgets/Logic/Ingame/ObserverStatusLogicCA.cs new file mode 100644 index 0000000000..a3e0848668 --- /dev/null +++ b/OpenRA.Mods.CA/Widgets/Logic/Ingame/ObserverStatusLogicCA.cs @@ -0,0 +1,713 @@ +#region Copyright & License Information +/** + * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). + * This file is part of OpenRA Combined Arms, 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.Mods.Common.Lint; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Widgets; +using OpenRA.Network; +using OpenRA.Primitives; +using OpenRA.Widgets; + +namespace OpenRA.Mods.CA.Widgets.Logic +{ + public enum ObserverStatsPanel { None, Basic, Economy, Production, SupportPowers, Combat, Army, Upgrades, BuildOrder, Graph, ArmyGraph, TeamArmyGraph } + + [ChromeLogicArgsHotkeys("StatisticsBasicKey", "StatisticsEconomyKey", "StatisticsProductionKey", "StatisticsSupportPowersKey", "StatisticsCombatKey", "StatisticsArmyKey", "StatisticsGraphKey", + "StatisticsArmyGraphKey")] + public class ObserverStatsLogicCA : ChromeLogic + { + [TranslationReference] + const string InformationNone = "options-observer-stats.none"; + + [TranslationReference] + const string Basic = "options-observer-stats.basic"; + + [TranslationReference] + const string Economy = "options-observer-stats.economy"; + + [TranslationReference] + const string Production = "options-observer-stats.production"; + + [TranslationReference] + const string SupportPowers = "options-observer-stats.support-powers"; + + [TranslationReference] + const string Combat = "options-observer-stats.combat"; + + [TranslationReference] + const string Army = "options-observer-stats.army"; + + [TranslationReference] + const string Upgrades = "options-observer-stats.upgrades"; + + [TranslationReference] + const string BuildOrder = "options-observer-stats.build-order"; + + [TranslationReference] + const string EarningsGraph = "options-observer-stats.earnings-graph"; + + [TranslationReference] + const string ArmyGraph = "options-observer-stats.army-graph"; + + [TranslationReference] + const string TeamArmyGraph = "options-observer-stats.team-army-graph"; + + [TranslationReference("team")] + const string TeamNumber = "label-team-name"; + + [TranslationReference] + const string NoTeam = "label-no-team"; + + readonly ContainerWidget basicStatsHeaders; + readonly ContainerWidget economyStatsHeaders; + readonly ContainerWidget productionStatsHeaders; + readonly ContainerWidget supportPowerStatsHeaders; + readonly ContainerWidget combatStatsHeaders; + readonly ContainerWidget armyHeaders; + readonly ContainerWidget upgradesHeaders; + readonly ContainerWidget buildOrderHeaders; + readonly ScrollPanelWidget playerStatsPanel; + readonly ScrollItemWidget basicPlayerTemplate; + readonly ScrollItemWidget economyPlayerTemplate; + readonly ScrollItemWidget productionPlayerTemplate; + readonly ScrollItemWidget supportPowersPlayerTemplate; + readonly ScrollItemWidget armyPlayerTemplate; + readonly ScrollItemWidget upgradesPlayerTemplate; + readonly ScrollItemWidget buildOrderPlayerTemplate; + readonly ScrollItemWidget combatPlayerTemplate; + readonly ContainerWidget incomeGraphContainer; + readonly ContainerWidget armyValueGraphContainer; + readonly ContainerWidget teamArmyValueGraphContainer; + readonly LineGraphWidget incomeGraph; + readonly LineGraphWidget armyValueGraph; + readonly LineGraphWidget teamArmyValueGraph; + readonly ScrollItemWidget teamTemplate; + readonly IEnumerable players; + readonly IOrderedEnumerable> teams; + readonly bool hasTeams; + readonly World world; + readonly WorldRenderer worldRenderer; + + readonly string clickSound = ChromeMetrics.Get("ClickSound"); + ObserverStatsPanel activePanel; + + readonly Dictionary teamColors; + + [ObjectCreator.UseCtor] + public ObserverStatsLogicCA(World world, ModData modData, WorldRenderer worldRenderer, Widget widget, Dictionary logicArgs) + { + this.world = world; + this.worldRenderer = worldRenderer; + + MiniYaml yaml; + var keyNames = Enum.GetNames(typeof(ObserverStatsPanel)); + var statsHotkeys = new HotkeyReference[keyNames.Length]; + for (var i = 0; i < keyNames.Length; i++) + statsHotkeys[i] = logicArgs.TryGetValue("Statistics" + keyNames[i] + "Key", out yaml) ? modData.Hotkeys[yaml.Value] : new HotkeyReference(); + + players = world.Players.Where(p => !p.NonCombatant && p.Playable); + teams = players.GroupBy(p => (world.LobbyInfo.ClientWithIndex(p.ClientIndex) ?? new Session.Client()).Team).OrderBy(g => g.Key); + hasTeams = !(teams.Count() == 1 && teams.First().Key == 0); + + basicStatsHeaders = widget.Get("BASIC_STATS_HEADERS"); + economyStatsHeaders = widget.Get("ECONOMY_STATS_HEADERS"); + productionStatsHeaders = widget.Get("PRODUCTION_STATS_HEADERS"); + supportPowerStatsHeaders = widget.Get("SUPPORT_POWERS_HEADERS"); + armyHeaders = widget.Get("ARMY_HEADERS"); + upgradesHeaders = widget.Get("UPGRADES_HEADERS"); + buildOrderHeaders = widget.Get("BUILD_ORDER_HEADERS"); + combatStatsHeaders = widget.Get("COMBAT_STATS_HEADERS"); + + playerStatsPanel = widget.Get("PLAYER_STATS_PANEL"); + playerStatsPanel.Layout = new GridLayout(playerStatsPanel); + playerStatsPanel.IgnoreMouseOver = true; + + if (ShowScrollBar) + { + playerStatsPanel.ScrollBar = ScrollBar.Left; + + AdjustHeader(basicStatsHeaders); + AdjustHeader(economyStatsHeaders); + AdjustHeader(productionStatsHeaders); + AdjustHeader(supportPowerStatsHeaders); + AdjustHeader(combatStatsHeaders); + AdjustHeader(armyHeaders); + AdjustHeader(upgradesHeaders); + AdjustHeader(buildOrderHeaders); + } + + basicPlayerTemplate = playerStatsPanel.Get("BASIC_PLAYER_TEMPLATE"); + economyPlayerTemplate = playerStatsPanel.Get("ECONOMY_PLAYER_TEMPLATE"); + productionPlayerTemplate = playerStatsPanel.Get("PRODUCTION_PLAYER_TEMPLATE"); + supportPowersPlayerTemplate = playerStatsPanel.Get("SUPPORT_POWERS_PLAYER_TEMPLATE"); + armyPlayerTemplate = playerStatsPanel.Get("ARMY_PLAYER_TEMPLATE"); + upgradesPlayerTemplate = playerStatsPanel.Get("UPGRADES_PLAYER_TEMPLATE"); + buildOrderPlayerTemplate = playerStatsPanel.Get("BUILD_ORDER_PLAYER_TEMPLATE"); + + combatPlayerTemplate = playerStatsPanel.Get("COMBAT_PLAYER_TEMPLATE"); + + incomeGraphContainer = widget.Get("INCOME_GRAPH_CONTAINER"); + incomeGraph = incomeGraphContainer.Get("INCOME_GRAPH"); + + armyValueGraphContainer = widget.Get("ARMY_VALUE_GRAPH_CONTAINER"); + armyValueGraph = armyValueGraphContainer.Get("ARMY_VALUE_GRAPH"); + + teamArmyValueGraphContainer = widget.Get("TEAM_ARMY_VALUE_GRAPH_CONTAINER"); + teamArmyValueGraph = teamArmyValueGraphContainer.Get("TEAM_ARMY_VALUE_GRAPH"); + + teamTemplate = playerStatsPanel.Get("TEAM_TEMPLATE"); + + var statsDropDown = widget.Get("STATS_DROPDOWN"); + StatsDropDownOption CreateStatsOption(string title, ObserverStatsPanel panel, ScrollItemWidget template, Action a) + { + title = TranslationProvider.GetString(title); + return new StatsDropDownOption + { + Title = TranslationProvider.GetString(title), + IsSelected = () => activePanel == panel, + OnClick = () => + { + ClearStats(); + playerStatsPanel.Visible = true; + statsDropDown.GetText = () => title; + activePanel = panel; + if (template != null) + AdjustStatisticsPanel(template); + + a(); + Ui.ResetTooltips(); + } + }; + } + + var statsDropDownOptions = new StatsDropDownOption[] + { + new StatsDropDownOption + { + Title = TranslationProvider.GetString(InformationNone), + IsSelected = () => activePanel == ObserverStatsPanel.None, + OnClick = () => + { + var informationNone = TranslationProvider.GetString(InformationNone); + statsDropDown.GetText = () => informationNone; + playerStatsPanel.Visible = false; + ClearStats(); + activePanel = ObserverStatsPanel.None; + } + }, + CreateStatsOption(Basic, ObserverStatsPanel.Basic, basicPlayerTemplate, () => DisplayStats(BasicStats)), + CreateStatsOption(Economy, ObserverStatsPanel.Economy, economyPlayerTemplate, () => DisplayStats(EconomyStats)), + CreateStatsOption(Production, ObserverStatsPanel.Production, productionPlayerTemplate, () => DisplayStats(ProductionStats)), + CreateStatsOption(SupportPowers, ObserverStatsPanel.SupportPowers, supportPowersPlayerTemplate, () => DisplayStats(SupportPowerStats)), + CreateStatsOption(Combat, ObserverStatsPanel.Combat, combatPlayerTemplate, () => DisplayStats(CombatStats)), + CreateStatsOption(Army, ObserverStatsPanel.Army, armyPlayerTemplate, () => DisplayStats(ArmyStats)), + CreateStatsOption(Upgrades, ObserverStatsPanel.Upgrades, upgradesPlayerTemplate, () => DisplayStats(UpgradeStats)), + CreateStatsOption(BuildOrder, ObserverStatsPanel.BuildOrder, buildOrderPlayerTemplate, () => DisplayStats(BuildOrderStats)), + CreateStatsOption(EarningsGraph, ObserverStatsPanel.Graph, null, () => IncomeGraph()), + CreateStatsOption(ArmyGraph, ObserverStatsPanel.ArmyGraph, null, () => ArmyValueGraph()), + CreateStatsOption(TeamArmyGraph, ObserverStatsPanel.TeamArmyGraph, null, () => TeamArmyValueGraph()) + }; + + ScrollItemWidget SetupItem(StatsDropDownOption option, ScrollItemWidget template) + { + var item = ScrollItemWidget.Setup(template, option.IsSelected, option.OnClick); + item.Get("LABEL").GetText = () => option.Title; + return item; + } + + var statsDropDownPanelTemplate = logicArgs.TryGetValue("StatsDropDownPanelTemplate", out yaml) ? yaml.Value : "LABEL_DROPDOWN_TEMPLATE"; + + statsDropDown.OnMouseDown = _ => statsDropDown.ShowDropDown(statsDropDownPanelTemplate, 230, statsDropDownOptions, SetupItem); + statsDropDownOptions[0].OnClick(); + + var keyListener = statsDropDown.Get("STATS_DROPDOWN_KEYHANDLER"); + keyListener.AddHandler(e => + { + if (e.Event == KeyInputEvent.Down && !e.IsRepeat) + { + for (var i = 0; i < statsHotkeys.Length; i++) + { + if (statsHotkeys[i].IsActivatedBy(e)) + { + Game.Sound.PlayNotification(modData.DefaultRules, null, "Sounds", clickSound, null); + statsDropDownOptions[i].OnClick(); + return true; + } + } + } + + return false; + }); + + if (logicArgs.TryGetValue("ClickSound", out yaml)) + clickSound = yaml.Value; + + teamColors = new Dictionary + { + { 0, Color.White }, + { 1, Color.DodgerBlue }, + { 2, Color.Red }, + { 3, Color.Green }, + { 4, Color.Yellow }, + { 5, Color.Cyan }, + { 6, Color.Magenta }, + { 7, Color.Orange }, + { 8, Color.Purple }, + { 9, Color.Lime }, + { 10, Color.Teal }, + { 11, Color.Pink }, + { 12, Color.Olive }, + { 13, Color.Maroon }, + { 14, Color.Navy }, + { 15, Color.Gray } + }; + } + + void ClearStats() + { + playerStatsPanel.Children.Clear(); + basicStatsHeaders.Visible = false; + economyStatsHeaders.Visible = false; + productionStatsHeaders.Visible = false; + supportPowerStatsHeaders.Visible = false; + armyHeaders.Visible = false; + upgradesHeaders.Visible = false; + buildOrderHeaders.Visible = false; + combatStatsHeaders.Visible = false; + + incomeGraphContainer.Visible = false; + armyValueGraphContainer.Visible = false; + teamArmyValueGraphContainer.Visible = false; + + incomeGraph.GetSeries = null; + armyValueGraph.GetSeries = null; + teamArmyValueGraph.GetSeries = null; + } + + void IncomeGraph() + { + playerStatsPanel.Visible = false; + incomeGraphContainer.Visible = true; + + incomeGraph.GetSeries = () => + players.Select(p => new LineGraphSeries( + p.PlayerName, + p.Color, + (p.PlayerActor.TraitOrDefault() ?? new PlayerStatistics(p.PlayerActor)).IncomeSamples.Select(s => (float)s))); + } + + void ArmyValueGraph() + { + playerStatsPanel.Visible = false; + armyValueGraphContainer.Visible = true; + + armyValueGraph.GetSeries = () => + players.Select(p => new LineGraphSeries( + p.PlayerName, + p.Color, + (p.PlayerActor.TraitOrDefault() ?? new PlayerStatistics(p.PlayerActor)).ArmySamples.Select(s => (float)s))); + } + + void TeamArmyValueGraph() + { + playerStatsPanel.Visible = false; + teamArmyValueGraphContainer.Visible = true; + + teamArmyValueGraph.GetSeries = () => + teams.Select(t => new LineGraphSeries( + t.Key > 0 ? TranslationProvider.GetString(TeamNumber, Translation.Arguments("team", $"{t.Key} ({t.First().PlayerName})")) : TranslationProvider.GetString(NoTeam), + teamColors[t.Key], + t.Select(p => (p.PlayerActor.TraitOrDefault() ?? new PlayerStatistics(p.PlayerActor)).ArmySamples.Select(s => (float)s)).Aggregate((a, b) => a.Zip(b, (x, y) => x + y)))); + } + + void DisplayStats(Func createItem) + { + foreach (var team in teams) + { + if (hasTeams) + { + var tt = ScrollItemWidget.Setup(teamTemplate, () => false, () => { }); + tt.IgnoreMouseOver = true; + + var teamLabel = tt.Get("TEAM"); + var teamText = team.Key > 0 ? TranslationProvider.GetString(TeamNumber, Translation.Arguments("team", team.Key)) + : TranslationProvider.GetString(NoTeam); + teamLabel.GetText = () => teamText; + tt.Bounds.Width = teamLabel.Bounds.Width = Game.Renderer.Fonts[tt.Font].Measure(teamText).X; + + var colorBlockWidget = tt.Get("TEAM_COLOR"); + var scrollBarOffset = playerStatsPanel.ScrollBar != ScrollBar.Hidden + ? playerStatsPanel.ScrollbarWidth + : 0; + var boundsWidth = tt.Parent.Bounds.Width - scrollBarOffset; + colorBlockWidget.Bounds.Width = boundsWidth - 200; + + var gradient = tt.Get("TEAM_GRADIENT"); + gradient.Bounds.X = boundsWidth - 200; + + playerStatsPanel.AddChild(tt); + } + + foreach (var p in team) + { + var player = p; + playerStatsPanel.AddChild(createItem(player)); + } + } + } + + ScrollItemWidget CombatStats(Player player) + { + combatStatsHeaders.Visible = true; + var template = SetupPlayerScrollItemWidget(combatPlayerTemplate, player); + + AddPlayerFlagAndName(template, player); + + var playerName = template.Get("PLAYER"); + playerName.GetColor = () => Color.White; + + var playerColor = template.Get("PLAYER_COLOR"); + var playerGradient = template.Get("PLAYER_GRADIENT"); + + SetupPlayerColor(player, template, playerColor, playerGradient); + + var stats = player.PlayerActor.TraitOrDefault(); + if (stats == null) + return template; + + var destroyedText = new CachedTransform(i => "$" + i); + template.Get("ASSETS_DESTROYED").GetText = () => destroyedText.Update(stats.KillsCost); + + var lostText = new CachedTransform(i => "$" + i); + template.Get("ASSETS_LOST").GetText = () => lostText.Update(stats.DeathsCost); + + var unitsKilledText = new CachedTransform(i => i.ToString()); + template.Get("UNITS_KILLED").GetText = () => unitsKilledText.Update(stats.UnitsKilled); + + var unitsDeadText = new CachedTransform(i => i.ToString()); + template.Get("UNITS_DEAD").GetText = () => unitsDeadText.Update(stats.UnitsDead); + + var buildingsKilledText = new CachedTransform(i => i.ToString()); + template.Get("BUILDINGS_KILLED").GetText = () => buildingsKilledText.Update(stats.BuildingsKilled); + + var buildingsDeadText = new CachedTransform(i => i.ToString()); + template.Get("BUILDINGS_DEAD").GetText = () => buildingsDeadText.Update(stats.BuildingsDead); + + var armyText = new CachedTransform(i => "$" + i); + template.Get("ARMY_VALUE").GetText = () => armyText.Update(stats.ArmyValue); + + var visionText = new CachedTransform(i => Vision(i)); + template.Get("VISION").GetText = () => player.Shroud.Disabled ? "100%" : visionText.Update(player.Shroud.RevealedCells); + + return template; + } + + ScrollItemWidget ProductionStats(Player player) + { + productionStatsHeaders.Visible = true; + var template = SetupPlayerScrollItemWidget(productionPlayerTemplate, player); + + AddPlayerFlagAndName(template, player); + + var playerName = template.Get("PLAYER"); + playerName.GetColor = () => Color.White; + + var playerColor = template.Get("PLAYER_COLOR"); + var playerGradient = template.Get("PLAYER_GRADIENT"); + + SetupPlayerColor(player, template, playerColor, playerGradient); + + template.Get("PRODUCTION_ICONS").GetPlayer = () => player; + template.IgnoreChildMouseOver = false; + + return template; + } + + ScrollItemWidget SupportPowerStats(Player player) + { + supportPowerStatsHeaders.Visible = true; + var template = SetupPlayerScrollItemWidget(supportPowersPlayerTemplate, player); + + AddPlayerFlagAndName(template, player); + + var playerName = template.Get("PLAYER"); + playerName.GetColor = () => Color.White; + + var playerColor = template.Get("PLAYER_COLOR"); + var playerGradient = template.Get("PLAYER_GRADIENT"); + + SetupPlayerColor(player, template, playerColor, playerGradient); + + template.Get("SUPPORT_POWER_ICONS").GetPlayer = () => player; + template.IgnoreChildMouseOver = false; + + return template; + } + + ScrollItemWidget ArmyStats(Player player) + { + armyHeaders.Visible = true; + var template = SetupPlayerScrollItemWidget(armyPlayerTemplate, player); + + AddPlayerFlagAndName(template, player); + + var playerName = template.Get("PLAYER"); + playerName.GetColor = () => Color.White; + + var playerColor = template.Get("PLAYER_COLOR"); + var playerGradient = template.Get("PLAYER_GRADIENT"); + + SetupPlayerColor(player, template, playerColor, playerGradient); + + template.Get("ARMY_ICONS").GetPlayer = () => player; + template.IgnoreChildMouseOver = false; + + return template; + } + + ScrollItemWidget UpgradeStats(Player player) + { + upgradesHeaders.Visible = true; + var template = SetupPlayerScrollItemWidget(upgradesPlayerTemplate, player); + + AddPlayerFlagAndName(template, player); + + var playerName = template.Get("PLAYER"); + playerName.GetColor = () => Color.White; + + var playerColor = template.Get("PLAYER_COLOR"); + var playerGradient = template.Get("PLAYER_GRADIENT"); + + SetupPlayerColor(player, template, playerColor, playerGradient); + + template.Get("UPGRADES_ICONS").GetPlayer = () => player; + template.IgnoreChildMouseOver = false; + + return template; + } + + ScrollItemWidget BuildOrderStats(Player player) + { + buildOrderHeaders.Visible = true; + var template = SetupPlayerScrollItemWidget(buildOrderPlayerTemplate, player); + + AddPlayerFlagAndName(template, player); + + var playerName = template.Get("PLAYER"); + playerName.GetColor = () => Color.White; + + var playerColor = template.Get("PLAYER_COLOR"); + var playerGradient = template.Get("PLAYER_GRADIENT"); + + SetupPlayerColor(player, template, playerColor, playerGradient); + + template.Get("BUILD_ORDER_ICONS").GetPlayer = () => player; + template.IgnoreChildMouseOver = false; + + return template; + } + + ScrollItemWidget EconomyStats(Player player) + { + economyStatsHeaders.Visible = true; + var template = SetupPlayerScrollItemWidget(economyPlayerTemplate, player); + + AddPlayerFlagAndName(template, player); + + var playerName = template.Get("PLAYER"); + playerName.GetColor = () => Color.White; + + var playerColor = template.Get("PLAYER_COLOR"); + var playerGradient = template.Get("PLAYER_GRADIENT"); + + SetupPlayerColor(player, template, playerColor, playerGradient); + + var stats = player.PlayerActor.TraitOrDefault(); + if (stats == null) + return template; + + var res = player.PlayerActor.Trait(); + var cashText = new CachedTransform(i => "$" + i); + template.Get("CASH").GetText = () => cashText.Update(res.Cash + res.Resources); + + var incomeText = new CachedTransform(i => "$" + i); + template.Get("INCOME").GetText = () => incomeText.Update(stats.DisplayIncome); + + var earnedText = new CachedTransform(i => "$" + i); + template.Get("EARNED").GetText = () => earnedText.Update(res.Earned); + + var spentText = new CachedTransform(i => "$" + i); + template.Get("SPENT").GetText = () => spentText.Update(res.Spent); + + var assetsText = new CachedTransform(i => "$" + i); + template.Get("ASSETS").GetText = () => assetsText.Update(stats.AssetsValue); + + var harvesters = template.Get("HARVESTERS"); + harvesters.GetText = () => world.ActorsWithTrait().Count(a => a.Actor.Owner == player && !a.Actor.IsDead && !a.Trait.IsTraitDisabled).ToString(); + + var carryalls = template.GetOrNull("CARRYALLS"); + if (carryalls != null) + carryalls.GetText = () => world.ActorsWithTrait().Count(a => a.Actor.Owner == player && !a.Actor.IsDead).ToString(); + + var derricks = template.GetOrNull("DERRICKS"); + if (derricks != null) + derricks.GetText = () => world.ActorsHavingTrait().Count(a => a.Owner == player && !a.IsDead).ToString(); + + return template; + } + + ScrollItemWidget BasicStats(Player player) + { + basicStatsHeaders.Visible = true; + var template = SetupPlayerScrollItemWidget(basicPlayerTemplate, player); + + AddPlayerFlagAndName(template, player); + + var playerName = template.Get("PLAYER"); + playerName.GetColor = () => Color.White; + + var playerColor = template.Get("PLAYER_COLOR"); + var playerGradient = template.Get("PLAYER_GRADIENT"); + + SetupPlayerColor(player, template, playerColor, playerGradient); + + var res = player.PlayerActor.Trait(); + var cashText = new CachedTransform(i => "$" + i); + template.Get("CASH").GetText = () => cashText.Update(res.Cash + res.Resources); + + var powerRes = player.PlayerActor.TraitOrDefault(); + if (powerRes != null) + { + var power = template.Get("POWER"); + var powerText = new CachedTransform<(int PowerDrained, int PowerProvided), string>(p => p.PowerDrained + "/" + p.PowerProvided); + power.GetText = () => powerText.Update((powerRes.PowerDrained, powerRes.PowerProvided)); + power.GetColor = () => GetPowerColor(powerRes.PowerState); + } + + var stats = player.PlayerActor.TraitOrDefault(); + if (stats == null) + return template; + + var killsText = new CachedTransform(i => i.ToString()); + template.Get("KILLS").GetText = () => killsText.Update(stats.UnitsKilled + stats.BuildingsKilled); + + var deathsText = new CachedTransform(i => i.ToString()); + template.Get("DEATHS").GetText = () => deathsText.Update(stats.UnitsDead + stats.BuildingsDead); + + var destroyedText = new CachedTransform(i => "$" + i); + template.Get("ASSETS_DESTROYED").GetText = () => destroyedText.Update(stats.KillsCost); + + var lostText = new CachedTransform(i => "$" + i); + template.Get("ASSETS_LOST").GetText = () => lostText.Update(stats.DeathsCost); + + var experienceText = new CachedTransform(i => i.ToString()); + template.Get("EXPERIENCE").GetText = () => experienceText.Update(stats.Experience); + + var actionsText = new CachedTransform(d => AverageOrdersPerMinute(d)); + template.Get("ACTIONS_MIN").GetText = () => actionsText.Update(stats.OrderCount); + + return template; + } + + static void SetupPlayerColor(Player player, ScrollItemWidget template, ColorBlockWidget colorBlockWidget, GradientColorBlockWidget gradientColorBlockWidget) + { + var color = Color.FromArgb(128, player.Color.R, player.Color.G, player.Color.B); + var hoverColor = Color.FromArgb(192, player.Color.R, player.Color.G, player.Color.B); + + var isMouseOver = new CachedTransform(w => w == template || template.Children.Contains(w)); + + colorBlockWidget.GetColor = () => isMouseOver.Update(Ui.MouseOverWidget) ? hoverColor : color; + + gradientColorBlockWidget.GetTopLeftColor = () => isMouseOver.Update(Ui.MouseOverWidget) ? hoverColor : color; + gradientColorBlockWidget.GetBottomLeftColor = () => isMouseOver.Update(Ui.MouseOverWidget) ? hoverColor : color; + gradientColorBlockWidget.GetTopRightColor = () => isMouseOver.Update(Ui.MouseOverWidget) ? hoverColor : Color.Transparent; + gradientColorBlockWidget.GetBottomRightColor = () => isMouseOver.Update(Ui.MouseOverWidget) ? hoverColor : Color.Transparent; + } + + ScrollItemWidget SetupPlayerScrollItemWidget(ScrollItemWidget template, Player player) + { + return ScrollItemWidget.Setup(template, () => false, () => + { + var playerBase = world.ActorsHavingTrait().FirstOrDefault(a => !a.IsDead && a.Owner == player); + if (playerBase != null) + worldRenderer.Viewport.Center(playerBase.CenterPosition); + }); + } + + void AdjustStatisticsPanel(Widget itemTemplate) + { + var height = playerStatsPanel.Bounds.Height; + + var scrollbarWidth = playerStatsPanel.ScrollBar != ScrollBar.Hidden ? playerStatsPanel.ScrollbarWidth : 0; + playerStatsPanel.Bounds.Width = itemTemplate.Bounds.Width + scrollbarWidth; + + if (playerStatsPanel.Bounds.Height < height) + playerStatsPanel.ScrollToTop(); + } + + void AdjustHeader(ContainerWidget headerTemplate) + { + var offset = playerStatsPanel.ScrollbarWidth; + + headerTemplate.Get("HEADER_COLOR").Bounds.Width += offset; + headerTemplate.Get("HEADER_GRADIENT").Bounds.X += offset; + + foreach (var headerLabel in headerTemplate.Children.OfType()) + headerLabel.Bounds.X += offset; + } + + static void AddPlayerFlagAndName(ScrollItemWidget template, Player player) + { + var flag = template.Get("FLAG"); + flag.GetImageCollection = () => "flags"; + flag.GetImageName = () => player.Faction.InternalName; + + var playerName = template.Get("PLAYER"); + WidgetUtils.BindPlayerNameAndStatus(playerName, player); + + playerName.GetColor = () => player.Color; + } + + string AverageOrdersPerMinute(double orders) + { + return (world.WorldTick == 0 ? 0 : orders / (world.WorldTick / 1500.0)).ToString("F1"); + } + + string Vision(int revealedCells) + { + return Math.Ceiling(revealedCells / (float)world.Map.ProjectedCells.Length * 100).ToString("F0") + "%"; + } + + static Color GetPowerColor(PowerState state) + { + if (state == PowerState.Critical) + return Color.Red; + + if (state == PowerState.Low) + return Color.Orange; + + return Color.LimeGreen; + } + + // HACK The height of the templates and the scrollpanel needs to be kept in synch + bool ShowScrollBar => players.Count() + (hasTeams ? teams.Count() : 0) > 10; + + sealed class StatsDropDownOption + { + public string Title; + public Func IsSelected; + public Action OnClick; + } + } +} diff --git a/OpenRA.Mods.CA/Widgets/ObserverBuildOrderIconsWidget.cs b/OpenRA.Mods.CA/Widgets/ObserverBuildOrderIconsWidget.cs new file mode 100644 index 0000000000..ab048d4bb4 --- /dev/null +++ b/OpenRA.Mods.CA/Widgets/ObserverBuildOrderIconsWidget.cs @@ -0,0 +1,215 @@ +#region Copyright & License Information +/** + * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). + * This file is part of OpenRA Combined Arms, 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.Mods.CA.Traits; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets +{ + public class ObserverBuildOrderIconsWidget : Widget + { + public Func GetPlayer; + readonly World world; + readonly WorldRenderer worldRenderer; + + public int IconWidth = 32; + public int IconHeight = 24; + public int IconSpacing = 1; + + readonly float2 iconSize; + public int MinWidth = 240; + + public ArmyUnit TooltipUnit { get; private set; } + public Func GetTooltipUnit; + + public readonly string TooltipTemplate = "ARMY_TOOLTIP"; + public readonly string TooltipContainer; + + readonly Lazy tooltipContainer; + readonly List armyIcons = new(); + + readonly CachedTransform tracker = new(player => player.PlayerActor.TraitOrDefault()); + + IEnumerable buildOrder; + int lastCount; + + int lastIconIdx; + int currentTooltipToken; + + [ObjectCreator.UseCtor] + public ObserverBuildOrderIconsWidget(World world, WorldRenderer worldRenderer) + { + this.world = world; + this.worldRenderer = worldRenderer; + + GetTooltipUnit = () => TooltipUnit; + tooltipContainer = Exts.Lazy(() => + Ui.Root.Get(TooltipContainer)); + } + + protected ObserverBuildOrderIconsWidget(ObserverBuildOrderIconsWidget other) + : base(other) + { + GetPlayer = other.GetPlayer; + world = other.world; + worldRenderer = other.worldRenderer; + + IconWidth = other.IconWidth; + IconHeight = other.IconHeight; + IconSpacing = other.IconSpacing; + iconSize = new float2(IconWidth, IconHeight); + + MinWidth = other.MinWidth; + + TooltipUnit = other.TooltipUnit; + GetTooltipUnit = () => TooltipUnit; + + TooltipTemplate = other.TooltipTemplate; + TooltipContainer = other.TooltipContainer; + + tooltipContainer = Exts.Lazy(() => + Ui.Root.Get(TooltipContainer)); + } + + public override void Draw() + { + armyIcons.Clear(); + + var player = GetPlayer(); + if (player == null) + return; + + var buildOrderTracker = tracker.Update(player); + + if (lastCount != buildOrderTracker.Count) + { + buildOrder = UpdateBuildOrder(buildOrderTracker, player); + } + + Game.Renderer.EnableAntialiasingFilter(); + + var queueCol = 0; + + if (buildOrder != null) + { + foreach (var unit in buildOrder) + { + var icon = unit.Icon; + var topLeftOffset = new int2(queueCol * (IconWidth + IconSpacing), 0); + + var iconTopLeft = RenderOrigin + topLeftOffset; + var centerPosition = iconTopLeft; + + var palette = unit.IconPaletteIsPlayerPalette ? unit.IconPalette + player.InternalName : unit.IconPalette; + WidgetUtils.DrawSpriteCentered(icon.Image, worldRenderer.Palette(palette), centerPosition + 0.5f * iconSize, 0.5f); + + armyIcons.Add(new ArmyIcon + { + Bounds = new Rectangle(iconTopLeft.X, iconTopLeft.Y, (int)iconSize.X, (int)iconSize.Y), + Unit = unit + }); + + queueCol++; + } + } + + var newWidth = Math.Max(queueCol * (IconWidth + IconSpacing), MinWidth); + if (newWidth != Bounds.Width) + { + var wasInBounds = EventBounds.Contains(Viewport.LastMousePos); + Bounds.Width = newWidth; + var isInBounds = EventBounds.Contains(Viewport.LastMousePos); + + // HACK: Ui.MouseOverWidget is normally only updated when the mouse moves + // Call ResetTooltips to force a fake mouse movement so the checks in Tick will work properly + if (wasInBounds != isInBounds) + Game.RunAfterTick(Ui.ResetTooltips); + } + + Game.Renderer.DisableAntialiasingFilter(); + + var parentWidth = Bounds.X + Bounds.Width; + Parent.Bounds.Width = parentWidth; + + var gradient = Parent.Get("PLAYER_GRADIENT"); + + var offset = gradient.Bounds.X - Bounds.X; + var gradientWidth = Math.Max(MinWidth - offset, queueCol * (IconWidth + IconSpacing)); + + gradient.Bounds.Width = gradientWidth; + var widestChildWidth = Parent.Parent.Children.Max(x => x.Bounds.Width); + + Parent.Parent.Bounds.Width = Math.Max(25 + widestChildWidth, Bounds.Left + MinWidth); + } + + IEnumerable UpdateBuildOrder(BuildOrderTracker buildOrderTracker, Player player) + { + lastCount = buildOrderTracker.Count; + return buildOrderTracker.BuildOrder.Select(u => new ArmyUnit(world.Map.Rules.Actors[u], player)); + } + + public override Widget Clone() + { + return new ObserverBuildOrderIconsWidget(this); + } + + public override void Tick() + { + if (TooltipContainer == null) + return; + + if (Ui.MouseOverWidget != this) + { + if (TooltipUnit != null) + { + tooltipContainer.Value.RemoveTooltip(currentTooltipToken); + lastIconIdx = 0; + TooltipUnit = null; + } + + return; + } + + if (TooltipUnit != null && lastIconIdx < armyIcons.Count) + { + var armyIcon = armyIcons[lastIconIdx]; + if (armyIcon.Unit.ActorInfo == TooltipUnit.ActorInfo && armyIcon.Bounds.Contains(Viewport.LastMousePos)) + return; + } + + for (var i = 0; i < armyIcons.Count; i++) + { + var armyIcon = armyIcons[i]; + if (!armyIcon.Bounds.Contains(Viewport.LastMousePos)) + continue; + + lastIconIdx = i; + TooltipUnit = armyIcon.Unit; + currentTooltipToken = tooltipContainer.Value.SetTooltip(TooltipTemplate, new WidgetArgs { { "getTooltipUnit", GetTooltipUnit } }); + + return; + } + + TooltipUnit = null; + } + + sealed class ArmyIcon + { + public Rectangle Bounds { get; set; } + public ArmyUnit Unit { get; set; } + } + } +} diff --git a/OpenRA.Mods.CA/Widgets/ObserverUpgradeIconsWidget.cs b/OpenRA.Mods.CA/Widgets/ObserverUpgradeIconsWidget.cs new file mode 100644 index 0000000000..61f26cb2be --- /dev/null +++ b/OpenRA.Mods.CA/Widgets/ObserverUpgradeIconsWidget.cs @@ -0,0 +1,215 @@ +#region Copyright & License Information +/** + * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). + * This file is part of OpenRA Combined Arms, 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.Mods.CA.Traits; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets +{ + public class ObserverUpgradeIconsWidget : Widget + { + public Func GetPlayer; + readonly World world; + readonly WorldRenderer worldRenderer; + + public int IconWidth = 32; + public int IconHeight = 24; + public int IconSpacing = 1; + + readonly float2 iconSize; + public int MinWidth = 240; + + public ArmyUnit TooltipUnit { get; private set; } + public Func GetTooltipUnit; + + public readonly string TooltipTemplate = "ARMY_TOOLTIP"; + public readonly string TooltipContainer; + + readonly Lazy tooltipContainer; + readonly List armyIcons = new(); + + readonly CachedTransform stats = new(player => player.PlayerActor.TraitOrDefault()); + + IOrderedEnumerable upgrades; + int lastHash; + + int lastIconIdx; + int currentTooltipToken; + + [ObjectCreator.UseCtor] + public ObserverUpgradeIconsWidget(World world, WorldRenderer worldRenderer) + { + this.world = world; + this.worldRenderer = worldRenderer; + + GetTooltipUnit = () => TooltipUnit; + tooltipContainer = Exts.Lazy(() => + Ui.Root.Get(TooltipContainer)); + } + + protected ObserverUpgradeIconsWidget(ObserverUpgradeIconsWidget other) + : base(other) + { + GetPlayer = other.GetPlayer; + world = other.world; + worldRenderer = other.worldRenderer; + + IconWidth = other.IconWidth; + IconHeight = other.IconHeight; + IconSpacing = other.IconSpacing; + iconSize = new float2(IconWidth, IconHeight); + + MinWidth = other.MinWidth; + + TooltipUnit = other.TooltipUnit; + GetTooltipUnit = () => TooltipUnit; + + TooltipTemplate = other.TooltipTemplate; + TooltipContainer = other.TooltipContainer; + + tooltipContainer = Exts.Lazy(() => + Ui.Root.Get(TooltipContainer)); + } + + public override void Draw() + { + armyIcons.Clear(); + + var player = GetPlayer(); + if (player == null) + return; + + var upgradesManager = stats.Update(player); + + if (lastHash != upgradesManager.Hash) + { + upgrades = UpdateUpgrades(upgradesManager, player); + } + + Game.Renderer.EnableAntialiasingFilter(); + + var queueCol = 0; + + if (upgrades != null) + { + foreach (var unit in upgrades) + { + var icon = unit.Icon; + var topLeftOffset = new int2(queueCol * (IconWidth + IconSpacing), 0); + + var iconTopLeft = RenderOrigin + topLeftOffset; + var centerPosition = iconTopLeft; + + var palette = unit.IconPaletteIsPlayerPalette ? unit.IconPalette + player.InternalName : unit.IconPalette; + WidgetUtils.DrawSpriteCentered(icon.Image, worldRenderer.Palette(palette), centerPosition + 0.5f * iconSize, 0.5f); + + armyIcons.Add(new ArmyIcon + { + Bounds = new Rectangle(iconTopLeft.X, iconTopLeft.Y, (int)iconSize.X, (int)iconSize.Y), + Unit = unit + }); + + queueCol++; + } + } + + var newWidth = Math.Max(queueCol * (IconWidth + IconSpacing), MinWidth); + if (newWidth != Bounds.Width) + { + var wasInBounds = EventBounds.Contains(Viewport.LastMousePos); + Bounds.Width = newWidth; + var isInBounds = EventBounds.Contains(Viewport.LastMousePos); + + // HACK: Ui.MouseOverWidget is normally only updated when the mouse moves + // Call ResetTooltips to force a fake mouse movement so the checks in Tick will work properly + if (wasInBounds != isInBounds) + Game.RunAfterTick(Ui.ResetTooltips); + } + + Game.Renderer.DisableAntialiasingFilter(); + + var parentWidth = Bounds.X + Bounds.Width; + Parent.Bounds.Width = parentWidth; + + var gradient = Parent.Get("PLAYER_GRADIENT"); + + var offset = gradient.Bounds.X - Bounds.X; + var gradientWidth = Math.Max(MinWidth - offset, queueCol * (IconWidth + IconSpacing)); + + gradient.Bounds.Width = gradientWidth; + var widestChildWidth = Parent.Parent.Children.Max(x => x.Bounds.Width); + + Parent.Parent.Bounds.Width = Math.Max(25 + widestChildWidth, Bounds.Left + MinWidth); + } + + IOrderedEnumerable UpdateUpgrades(UpgradesManager upgradesManager, Player player) + { + lastHash = upgradesManager.Hash; + return upgradesManager.UnlockedUpgradeTypes.Select(u => new ArmyUnit(player.World.Map.Rules.Actors[u], player)).OrderBy(u => u.BuildPaletteOrder); + } + + public override Widget Clone() + { + return new ObserverUpgradeIconsWidget(this); + } + + public override void Tick() + { + if (TooltipContainer == null) + return; + + if (Ui.MouseOverWidget != this) + { + if (TooltipUnit != null) + { + tooltipContainer.Value.RemoveTooltip(currentTooltipToken); + lastIconIdx = 0; + TooltipUnit = null; + } + + return; + } + + if (TooltipUnit != null && lastIconIdx < armyIcons.Count) + { + var armyIcon = armyIcons[lastIconIdx]; + if (armyIcon.Unit.ActorInfo == TooltipUnit.ActorInfo && armyIcon.Bounds.Contains(Viewport.LastMousePos)) + return; + } + + for (var i = 0; i < armyIcons.Count; i++) + { + var armyIcon = armyIcons[i]; + if (!armyIcon.Bounds.Contains(Viewport.LastMousePos)) + continue; + + lastIconIdx = i; + TooltipUnit = armyIcon.Unit; + currentTooltipToken = tooltipContainer.Value.SetTooltip(TooltipTemplate, new WidgetArgs { { "getTooltipUnit", GetTooltipUnit } }); + + return; + } + + TooltipUnit = null; + } + + sealed class ArmyIcon + { + public Rectangle Bounds { get; set; } + public ArmyUnit Unit { get; set; } + } + } +} diff --git a/OpenRA.Mods.CA/Widgets/SupportPowersScrollableWidget.cs b/OpenRA.Mods.CA/Widgets/SupportPowersScrollableWidget.cs index 020effc108..38a94d4df6 100644 --- a/OpenRA.Mods.CA/Widgets/SupportPowersScrollableWidget.cs +++ b/OpenRA.Mods.CA/Widgets/SupportPowersScrollableWidget.cs @@ -133,6 +133,9 @@ public void RefreshIcons() var powers = spm.Powers.Values.Where(p => !p.Disabled) .OrderBy(p => p.Info.SupportPowerPaletteOrder); + if (CurrentStartIndex > powers.Count() - 1) + CurrentStartIndex = 0; + var oldVisibleIconCount = VisibleIconCount; VisibleIconCount = 0; IconCount = 0; diff --git a/mods/ca/chrome/ingame-observer.yaml b/mods/ca/chrome/ingame-observer.yaml index ee99cf2229..e742bcbe1d 100644 --- a/mods/ca/chrome/ingame-observer.yaml +++ b/mods/ca/chrome/ingame-observer.yaml @@ -227,7 +227,7 @@ Container@OBSERVER_WIDGETS: Width: 245 ReplayYPosModifier: 35 Container@INGAME_OBSERVERSTATS_BG: - Logic: ObserverStatsLogic + Logic: ObserverStatsLogicCA StatisticsNoneKey: StatisticsNone StatisticsBasicKey: StatisticsBasic StatisticsEconomyKey: StatisticsEconomy @@ -538,6 +538,78 @@ Container@OBSERVER_WIDGETS: Font: Bold Text: Army Shadow: True + Container@UPGRADES_HEADERS: + X: 0 + Y: 0 + Width: 400 + Height: PARENT_BOTTOM + Children: + ColorBlock@HEADER_COLOR: + X: 0 + Y: 0 + Color: 00000090 + Width: PARENT_RIGHT - 200 + Height: PARENT_BOTTOM + GradientColorBlock@HEADER_GRADIENT: + X: PARENT_RIGHT - 200 + Y: 0 + TopLeftColor: 00000090 + BottomLeftColor: 00000090 + Width: 200 + Height: PARENT_BOTTOM + Label@PLAYER_HEADER: + X: 40 + Y: 0 + Width: 120 + Height: PARENT_BOTTOM + Font: Bold + Text: Player + Align: Left + Shadow: True + Label@UPGRADES_HEADER: + X: 160 + Y: 0 + Width: 100 + Height: PARENT_BOTTOM + Font: Bold + Text: Upgrades + Shadow: True + Container@BUILD_ORDER_HEADERS: + X: 0 + Y: 0 + Width: 400 + Height: PARENT_BOTTOM + Children: + ColorBlock@HEADER_COLOR: + X: 0 + Y: 0 + Color: 00000090 + Width: PARENT_RIGHT - 200 + Height: PARENT_BOTTOM + GradientColorBlock@HEADER_GRADIENT: + X: PARENT_RIGHT - 200 + Y: 0 + TopLeftColor: 00000090 + BottomLeftColor: 00000090 + Width: 200 + Height: PARENT_BOTTOM + Label@PLAYER_HEADER: + X: 40 + Y: 0 + Width: 120 + Height: PARENT_BOTTOM + Font: Bold + Text: Player + Align: Left + Shadow: True + Label@BUILD_ORDER_HEADER: + X: 160 + Y: 0 + Width: 100 + Height: PARENT_BOTTOM + Font: Bold + Text: Initial Build Order + Shadow: True Container@COMBAT_STATS_HEADERS: X: 0 Y: 0 @@ -938,6 +1010,80 @@ Container@OBSERVER_WIDGETS: Width: 0 Height: PARENT_BOTTOM TooltipContainer: TOOLTIP_CONTAINER + ScrollItem@UPGRADES_PLAYER_TEMPLATE: + X: 0 + Y: 0 + Width: 400 + Height: 25 + Background: scrollitem-nohover + Children: + ColorBlock@PLAYER_COLOR: + X: 0 + Y: 0 + Width: PARENT_RIGHT - 200 + Height: PARENT_BOTTOM + GradientColorBlock@PLAYER_GRADIENT: + X: PARENT_RIGHT - 200 + Y: 0 + Width: 200 + Height: PARENT_BOTTOM + Image@FLAG: + X: 5 + Y: 4 + Width: 35 + Height: PARENT_BOTTOM - 4 + ImageName: random + ImageCollection: flags + Label@PLAYER: + X: 35 + Y: 0 + Width: 120 + Height: PARENT_BOTTOM + Font: Bold + Shadow: True + ObserverUpgradeIcons@UPGRADES_ICONS: + X: 155 + Y: 0 + Width: 0 + Height: PARENT_BOTTOM + TooltipContainer: TOOLTIP_CONTAINER + ScrollItem@BUILD_ORDER_PLAYER_TEMPLATE: + X: 0 + Y: 0 + Width: 400 + Height: 25 + Background: scrollitem-nohover + Children: + ColorBlock@PLAYER_COLOR: + X: 0 + Y: 0 + Width: PARENT_RIGHT - 200 + Height: PARENT_BOTTOM + GradientColorBlock@PLAYER_GRADIENT: + X: PARENT_RIGHT - 200 + Y: 0 + Width: 200 + Height: PARENT_BOTTOM + Image@FLAG: + X: 5 + Y: 4 + Width: 35 + Height: PARENT_BOTTOM - 4 + ImageName: random + ImageCollection: flags + Label@PLAYER: + X: 35 + Y: 0 + Width: 120 + Height: PARENT_BOTTOM + Font: Bold + Shadow: True + ObserverBuildOrderIcons@BUILD_ORDER_ICONS: + X: 155 + Y: 0 + Width: 0 + Height: PARENT_BOTTOM + TooltipContainer: TOOLTIP_CONTAINER ScrollItem@COMBAT_PLAYER_TEMPLATE: X: 0 Y: 0 @@ -1075,3 +1221,29 @@ Container@OBSERVER_WIDGETS: YAxisLabel: Army Value LabelFont: TinyBold AxisFont: TinyBold + Container@TEAM_ARMY_VALUE_GRAPH_CONTAINER: + X: 0 + Y: 30 + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Visible: False + Children: + ColorBlock@GRAPH_BACKGROUND: + X: 0 + Y: 0 + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Color: 00000090 + LineGraph@TEAM_ARMY_VALUE_GRAPH: + X: 0 + Y: 0 + Width: PARENT_RIGHT - 5 + Height: PARENT_BOTTOM + ValueFormat: ${0} + YAxisValueFormat: ${0:F0} + XAxisSize: 40 + XAxisTicksPerLabel: 2 + XAxisLabel: Game Minute + YAxisLabel: Army Value + LabelFont: TinyBold + AxisFont: TinyBold diff --git a/mods/ca/languages/rules/en.ftl b/mods/ca/languages/rules/en.ftl index 14c252c507..cff7a93883 100644 --- a/mods/ca/languages/rules/en.ftl +++ b/mods/ca/languages/rules/en.ftl @@ -61,3 +61,18 @@ options-difficulty = label-player-level = Current Rank: { $level } label-player-level-current-xp = Current XP: { $currentXp } label-player-level-required-xp = Next Rank XP: { $nextLevelXp } + +## ObserverStatsLogic +options-observer-stats = + .none = Information: None + .basic = Basic + .economy = Economy + .production = Production + .support-powers = Support Powers + .combat = Combat + .army = Army + .upgrades = Upgrades + .build-order = Build Order + .earnings-graph = Earnings (graph) + .army-graph = Army Value (graph) + .team-army-graph = Team Value (graph) diff --git a/mods/ca/maps/ca-composition-tester/map.yaml b/mods/ca/maps/ca-composition-tester/map.yaml index 91c9d677d0..f23c6795df 100644 --- a/mods/ca/maps/ca-composition-tester/map.yaml +++ b/mods/ca/maps/ca-composition-tester/map.yaml @@ -785,4 +785,4 @@ Actors: Owner: Neutral Location: 103,166 -Rules: ca|rules/custom/composition-tester.yaml, rules.yaml +Rules: ca|rules/custom/composition-tester.yaml, ca|rules/custom/composition-tester-powers.yaml, rules.yaml diff --git a/mods/ca/maps/ca-composition-tester/rules.yaml b/mods/ca/maps/ca-composition-tester/rules.yaml index bdabde555f..e70047b0b5 100644 --- a/mods/ca/maps/ca-composition-tester/rules.yaml +++ b/mods/ca/maps/ca-composition-tester/rules.yaml @@ -6,95 +6,6 @@ World: Briefing: Map for testing fixed value army compositions against each other. Requires 2+ players. Move the trucks into wormholes to reset/save/restore. Player: - Inherits@VEILOFWARPOWER: ^VeilOfWarPower - Inherits@CLUSTERMINESPOWER: ^ClusterMinesPower - Inherits@STRAFINGRUNPOWER: ^StrafingRunPower - Inherits@IRONCURTAINPOWER: ^IronCurtainPower - Inherits@PARABOMBSPOWER: ^ParabombsPower - Inherits@CARPETBOMBPOWER: ^CarpetBombPower - Inherits@ATOMBOMBPOWER: ^AtomBombPower - Inherits@MUTABOMBPOWER: ^MutaBombPower - Inherits@CHAOSBOMBSPOWER: ^ChaosBombsPower - Inherits@NANITEREPAIRPOWER: ^NaniteRepairPower - Inherits@NANITESHIELDPOWER: ^NaniteShieldPower - Inherits@SHADOWTEAMPOWER: ^ShadowTeamPower - Inherits@@INFERNOBOMBPOWER: ^InfernoBombPower - Inherits@FRENZYPOWER: ^FrenzyPower - Inherits@TIBSTEALTHPOWER: ^TibStealthPower - Inherits@STORMSPIKEPOWER: ^StormSpikePower - Inherits@BUZZERSWARMPOWER: ^BuzzerSwarmPower - Inherits@IONSURGEPOWER: ^IonSurgePower - Inherits@GREATERCOALESCENCEPOWER: ^GreaterCoalescencePower - Inherits@SUPPRESSIONPOWER: ^SuppressionPower - SpawnActorPowerCA@VeilOfWar: - -Prerequisites: - ChargeInterval: 1 - AirstrikePowerCA@clustermines: - -Prerequisites: - ChargeInterval: 1 - ClassicAirstrikePower@Strafe: - -Prerequisites: - ChargeInterval: 1 - AirstrikePowerCA@Russianparabombs: - -Prerequisites: - ChargeInterval: 1 - AirstrikePowerCA@CarpetBomb: - -Prerequisites: - ChargeInterval: 1 - AirstrikePowerCA@Iraqiparabombs: - -Prerequisites: - ChargeInterval: 1 - AirstrikePowerCA@ChaosBombs: - -Prerequisites: - ChargeInterval: 1 - AirstrikePowerCA@MutaBomb: - -Prerequisites: - ChargeInterval: 1 - GrantExternalConditionPowerCA@IRONCURTAIN: - ChargeInterval: 1 - GrantExternalConditionPowerCA@NREPAIR: - -Prerequisites: - ChargeInterval: 1 - SpawnActorPowerCA@NSHIELD: - -Prerequisites: - ChargeInterval: 1 - AirReinforcementsPower@ShadowTeam: - -Prerequisites: - ChargeInterval: 1 - AirstrikePowerCA@BlackhandFirebomb: - -Prerequisites: - ChargeInterval: 1 - GrantExternalConditionPowerCA@Frenzy: - -Prerequisites: - ChargeInterval: 1 - GrantExternalConditionPowerCA@SGEN: - -ActiveCondition: - ChargeInterval: 1 - DetonateWeaponPower@STORMSPIKE: - -Prerequisites: - ChargeInterval: 1 - DetonateWeaponPower@BUZZERSWARM: - -Prerequisites: - ChargeInterval: 1 - DetonateWeaponPower@IONSURGE: - -Prerequisites: - ChargeInterval: 1 - DetonateWeaponPower@GREATERCOALESCENCE: - -Prerequisites: - ChargeInterval: 1 - GrantExternalConditionPowerCA@SUPPRESSION: - -Prerequisites: - ChargeInterval: 1 - ExternalCondition@1: - Condition: disabled - ExternalCondition@2: - Condition: empdisable - ExternalCondition@3: - Condition: being-warped - ExternalCondition@4: - Condition: build-incomplete - ExternalCondition@5: - Condition: tower.shield -ModularBot@BrutalAI: -ModularBot@VeryHardAI: -ModularBot@HardAI: diff --git a/mods/ca/maps/ca-testing-grounds/map.yaml b/mods/ca/maps/ca-testing-grounds/map.yaml index 5dca9292c5..d22f9cc84b 100644 --- a/mods/ca/maps/ca-testing-grounds/map.yaml +++ b/mods/ca/maps/ca-testing-grounds/map.yaml @@ -363,18 +363,6 @@ Actors: Actor1957: nuk2 Owner: HostileDummy Location: 166,126 - Actor1457: stek - Owner: HostileDummy - Location: 176,131 - Actor1458: stek - Owner: HostileDummy - Location: 180,131 - Actor1459: stek - Owner: HostileDummy - Location: 176,135 - Actor1460: stek - Owner: HostileDummy - Location: 180,135 Actor2047: agun Owner: HostileDummy Location: 174,181 @@ -435,12 +423,6 @@ Actors: Actor2073: syrd.gdi Owner: Multi0 Location: 168,155 - Actor2074: syrd.gdi - Owner: Multi0 - Location: 163,159 - Actor2075: syrd.gdi - Owner: Multi0 - Location: 168,159 Actor1960: reck Owner: Multi0 Facing: 384 @@ -2004,8 +1986,8 @@ Actors: Owner: Dummy Location: 150,91 Actor552: proc - Owner: HostileDummy Location: 178,91 + Owner: Dummy Actor553: seek Owner: Dummy Facing: 384 @@ -3641,8 +3623,8 @@ Actors: Owner: Dummy Location: 150,105 Actor919: proc - Owner: HostileDummy Location: 178,105 + Owner: Dummy Actor920: seek Owner: Dummy Facing: 384 @@ -5391,23 +5373,40 @@ Actors: Actor1370: fact Owner: Multi0 Location: 32,140 - Actor1371: gtwr - Owner: Dummy - Location: 154,118 - Actor1372: gtwr - Owner: Dummy - Location: 154,121 - Actor1373: rmbc - Owner: Dummy - SubCell: 3 - Facing: 384 - Location: 154,115 - Actor1374: rmbc - Owner: Dummy - Facing: 384 - SubCell: 3 - Location: 154,125 + Actor1372: tmpl + Owner: Multi0 + Location: 28,141 + Actor1373: eye + Owner: Multi0 + Location: 25,141 + Actor1371: mslo + Owner: Multi0 + Location: 22,143 + Actor1375: weat + Owner: Multi0 + Location: 19,142 + Actor1376: mslo.nod + Owner: Multi0 + Location: 22,141 + Actor1377: rfgn + Owner: Multi0 + Location: 16,142 + Actor1378: alhq + Owner: Multi0 + Location: 39,141 + Actor1374: patr + Owner: Multi0 + Location: 42,142 + Actor1409: sign + Owner: Multi0 + Location: 12,141 + Actor1410: spen + Owner: Multi0 + Location: 163,160 + Actor1436: spen + Owner: Multi0 + Location: 168,160 -Rules: ca|rules/custom/composition-tester.yaml, rules.yaml +Rules: ca|rules/custom/composition-tester.yaml, ca|rules/custom/composition-tester-powers.yaml, rules.yaml Weapons: weapons.yaml diff --git a/mods/ca/maps/ca-testing-grounds/rules.yaml b/mods/ca/maps/ca-testing-grounds/rules.yaml index 04468c1a68..5307f593ad 100644 --- a/mods/ca/maps/ca-testing-grounds/rules.yaml +++ b/mods/ca/maps/ca-testing-grounds/rules.yaml @@ -69,14 +69,35 @@ SYRD: TSLA: AttackTesla: - PauseOnCondition: dormant + PauseOnCondition: build-incomplete || disabled || empdisable || being-warped || dormant + GrantConditionOnBotOwner@dormant: + Condition: dormant + Bots: dormant + +ATWR: + AttackTurreted: + PauseOnCondition: build-incomplete || disabled || empdisable || being-warped || dormant + GrantConditionOnBotOwner@dormant: + Condition: dormant + Bots: dormant + +OBLI: + AttackCharges: + PauseOnCondition: build-incomplete || disabled || empdisable || being-warped || dormant + GrantConditionOnBotOwner@dormant: + Condition: dormant + Bots: dormant + +GTWR: + AttackTurreted: + PauseOnCondition: build-incomplete || empdisable || being-warped || dormant GrantConditionOnBotOwner@dormant: Condition: dormant Bots: dormant FTUR: AttackTurreted: - PauseOnCondition: dormant + PauseOnCondition: build-incomplete || empdisable || being-warped || dormant GrantConditionOnBotOwner@dormant: Condition: dormant Bots: dormant diff --git a/mods/ca/maps/ca-testing-grounds/weapons.yaml b/mods/ca/maps/ca-testing-grounds/weapons.yaml index 37235c1f3b..2a894141de 100644 --- a/mods/ca/maps/ca-testing-grounds/weapons.yaml +++ b/mods/ca/maps/ca-testing-grounds/weapons.yaml @@ -98,6 +98,8 @@ CryoMissile: ValidRelationships: Enemy, Neutral, Ally Warhead@chill3: GrantExternalConditionCA ValidRelationships: Enemy, Neutral, Ally + Warhead@chillally: GrantExternalConditionCA + ValidRelationships: None AtomizerBolts: Warhead@atomize: GrantExternalConditionCA diff --git a/mods/ca/maps/ca07-subversion/rules.yaml b/mods/ca/maps/ca07-subversion/rules.yaml index f3e2514944..01cad8b275 100644 --- a/mods/ca/maps/ca07-subversion/rules.yaml +++ b/mods/ca/maps/ca07-subversion/rules.yaml @@ -221,8 +221,10 @@ EYE: IonCannonPower@surgicals: ChargeInterval: 125 -PauseOnCondition: + -Prerequisites: Power: Amount: 0 + -Buildable: # Hunt() requires only 1 AttackBase BATF.AI: diff --git a/mods/ca/maps/ca12-treachery/rules.yaml b/mods/ca/maps/ca12-treachery/rules.yaml index 3be5604ca0..66e25437e6 100644 --- a/mods/ca/maps/ca12-treachery/rules.yaml +++ b/mods/ca/maps/ca12-treachery/rules.yaml @@ -76,6 +76,7 @@ BORI: AutoTarget: InitialStance: HoldFire -Buildable: + -ChangesHealth@CommandoRegen: GNRL: Tooltip: diff --git a/mods/ca/maps/ca15-domination/rules.yaml b/mods/ca/maps/ca15-domination/rules.yaml index bd7e794803..c7d6685cac 100644 --- a/mods/ca/maps/ca15-domination/rules.yaml +++ b/mods/ca/maps/ca15-domination/rules.yaml @@ -34,6 +34,8 @@ YURI: Range: 10c0 AutoTarget: InitialStance: HoldFire + Buildable: + Description: Elite specialist infantry able to mind-control\n enemy units (up to 3). -Captures@DRIVER_KILL: -GainsExperience: -ProducibleWithLevel: @@ -44,6 +46,7 @@ YURI: DamageMultiplier@YAMLFIX: Modifier: 100 RequiresCondition: maxcontrolled + -ChangesHealth@CommandoRegen: -RangedGpsRadarProvider: -WithRangeCircle: diff --git a/mods/ca/maps/ca20-subjugation/rules.yaml b/mods/ca/maps/ca20-subjugation/rules.yaml index f23c4edee2..338cf2c421 100644 --- a/mods/ca/maps/ca20-subjugation/rules.yaml +++ b/mods/ca/maps/ca20-subjugation/rules.yaml @@ -107,6 +107,7 @@ MAST: Range: 7c0 AutoTarget: InitialStance: HoldFire + -ChangesHealth@CommandoRegen: -RangeMultiplier@RANK-ELITE: Targetable@MASTERMIND: TargetTypes: Mastermind diff --git a/mods/ca/maps/ca22-capitulation/ca22.lua b/mods/ca/maps/ca22-capitulation/ca22.lua index 9606e7dcb8..0c0eb09e11 100644 --- a/mods/ca/maps/ca22-capitulation/ca22.lua +++ b/mods/ca/maps/ca22-capitulation/ca22.lua @@ -75,7 +75,7 @@ Squads = { AttackValuePerSecond = { easy = { { MinTime = 0, Value = 5 }, { MinTime = DateTime.Minutes(14), Value = 10 } }, normal = { { MinTime = 0, Value = 9 }, { MinTime = DateTime.Minutes(12), Value = 12 }, { MinTime = DateTime.Minutes(16), Value = 15 } }, - hard = { { MinTime = 0, Value = 13 }, { MinTime = DateTime.Minutes(10), Value = 20 }, { MinTime = DateTime.Minutes(14), Value = 25 } }, + hard = { { MinTime = 0, Value = 13 }, { MinTime = DateTime.Minutes(10), Value = 20 }, { MinTime = DateTime.Minutes(14), Value = 30 } }, }, QueueProductionStatuses = { Aircraft = false @@ -105,7 +105,7 @@ Squads = { AttackValuePerSecond = { easy = { { MinTime = 0, Value = 5 }, { MinTime = DateTime.Minutes(14), Value = 10 } }, normal = { { MinTime = 0, Value = 9 }, { MinTime = DateTime.Minutes(12), Value = 12 }, { MinTime = DateTime.Minutes(16), Value = 15 } }, - hard = { { MinTime = 0, Value = 13 }, { MinTime = DateTime.Minutes(10), Value = 20 }, { MinTime = DateTime.Minutes(14), Value = 25 } }, + hard = { { MinTime = 0, Value = 13 }, { MinTime = DateTime.Minutes(10), Value = 20 }, { MinTime = DateTime.Minutes(14), Value = 30 } }, }, QueueProductionStatuses = { Aircraft = false @@ -180,9 +180,16 @@ Squads = { { Aircraft = { "kiro" } }, }, hard = { - { Aircraft = { "kiro", "kiro" } }, + { Aircraft = { "kiro", "kiro" }, MaxTime = DateTime.Minutes(15) }, + { Aircraft = { "kiro", "kiro", "kiro" }, MinTime = DateTime.Minutes(15), MaxTime = DateTime.Minutes(20) }, + { Aircraft = { "kiro", "kiro", "kiro", "kiro" }, MinTime = DateTime.Minutes(20), }, } }, + AttackPaths = { + { KirovPath1_1.Location, KirovPath1_2.Location, KirovPath1_3.Location, KirovPath1_4.Location }, + { KirovPath2_1.Location, KirovPath2_2.Location, KirovPath2_3.Location }, + { AttackRally2.Location }, + }, }, } @@ -317,17 +324,17 @@ InitUSSR = function() end) Trigger.AfterDelay(Squads.AirAntiLight.Delay[Difficulty], function() - InitAirAttackSquad(Squads.AirAntiLight, USSR, GDI, { "msam", "xo", "rmbo", "nuk2", "hq", "gtek", "n3" }) + InitAirAttackSquad(Squads.AirAntiLight, USSR, GDI, { "msam", "xo", "rmbo", "nuk2", "hq", "gtek", "n3", "hsam" }) end) Trigger.AfterDelay(Squads.AirAntiHeavy.Delay[Difficulty], function() - InitAirAttackSquad(Squads.AirAntiHeavy, USSR, GDI, { "atwr", "htnk", "htnk.ion", "mtnk", "disr" }) + InitAirAttackSquad(Squads.AirAntiHeavy, USSR, GDI, { "atwr", "htnk", "htnk.ion", "mtnk", "disr", "thwk" }) end) InitAirAttackSquad(Squads.AirAntiAir, USSR, GDI, { "orca", "a10", "a10.upg", "auro" }) Trigger.AfterDelay(Squads.Kirovs.Delay[Difficulty], function() - InitAirAttackSquad(Squads.Kirovs, USSR, GDI, { "proc.td", "nuk2", "hq", "gtek", "atwr", "nuke", "weap.td", "eye", "patr", "pyle" }) + InitAttackSquad(Squads.Kirovs, USSR, GDI) end) Trigger.AfterDelay(ParabombsEnabledDelay[Difficulty], function() diff --git a/mods/ca/maps/ca22-capitulation/map.yaml b/mods/ca/maps/ca22-capitulation/map.yaml index ea777f987f..cc7b172163 100644 --- a/mods/ca/maps/ca22-capitulation/map.yaml +++ b/mods/ca/maps/ca22-capitulation/map.yaml @@ -3824,6 +3824,39 @@ Actors: McvSpawn: waypoint Owner: Neutral Location: 13,96 + Actor1209: camera + Owner: USSR + Location: 46,49 + Actor1229: camera + Owner: USSR + Location: 77,58 + Actor1230: camera + Owner: USSR + Location: 88,46 + Actor1232: camera + Owner: USSR + Location: 34,46 + KirovPath1_1: waypoint + Owner: Neutral + Location: 82,50 + KirovPath1_2: waypoint + Owner: Neutral + Location: 88,63 + KirovPath1_3: waypoint + Owner: Neutral + Location: 61,87 + KirovPath1_4: waypoint + Owner: Neutral + Location: 44,80 + KirovPath2_1: waypoint + Owner: Neutral + Location: 33,21 + KirovPath2_2: waypoint + Owner: Neutral + Location: 10,44 + KirovPath2_3: waypoint + Owner: Neutral + Location: 17,68 Rules: ca|rules/custom/campaign-rules.yaml, ca|rules/custom/campaign-tooltips.yaml, ca|rules/custom/two-tone-nod.yaml, rules.yaml diff --git a/mods/ca/maps/ca22-capitulation/rules.yaml b/mods/ca/maps/ca22-capitulation/rules.yaml index b2b7081f51..803a78606d 100644 --- a/mods/ca/maps/ca22-capitulation/rules.yaml +++ b/mods/ca/maps/ca22-capitulation/rules.yaml @@ -89,7 +89,7 @@ TSLA: RequiresCondition: !buff-removed DamageMultiplier@BUFFED: Modifier: 50 - RequiresCondition: !buff-removed + RequiresCondition: !buff-removed RangeMultiplier@PERMABUFF: Modifier: 110 ExternalCondition@ICOFFLINE: diff --git a/mods/ca/maps/ca22-capitulation/weapons.yaml b/mods/ca/maps/ca22-capitulation/weapons.yaml index 6a2f9c187a..653762b9f6 100644 --- a/mods/ca/maps/ca22-capitulation/weapons.yaml +++ b/mods/ca/maps/ca22-capitulation/weapons.yaml @@ -1,2 +1,10 @@ TeslaZapBoost3: Range: 9c512 + Warhead@1Dam: SpreadDamage + Damage: 20000 + +Nike: + Inherits: ^AntiAirMissile + Range: 8c0 + Projectile: MissileCA + Speed: 448 diff --git a/mods/ca/maps/ca23-emancipation/ca23.lua b/mods/ca/maps/ca23-emancipation/ca23.lua index e5126f27a8..d8fc54866b 100644 --- a/mods/ca/maps/ca23-emancipation/ca23.lua +++ b/mods/ca/maps/ca23-emancipation/ca23.lua @@ -15,6 +15,8 @@ ScrinGroundAttackPaths = { Masterminds = { Mastermind1, Mastermind2, Mastermind3, Mastermind4, Mastermind5 } +MastermindsLocated = {} + MaxEnslavedUnitsKilled = { normal = 20, hard = 10 @@ -283,7 +285,8 @@ WorldLoaded = function() end) Trigger.OnEnteredProximityTrigger(m.CenterPosition, WDist.New(11 * 1024), function(a, id) - if a.Owner == GDI and a.Type ~= "smallcamera" and not m.IsDead then + if a.Owner == GDI and a.Type ~= "smallcamera" and not m.IsDead and not MastermindsLocated[tostring(m)] then + MastermindsLocated[tostring(m)] = true Trigger.RemoveProximityTrigger(id) local camera = Actor.Create("smallcamera", true, { Owner = GDI, Location = m.Location }) Notification("A Mastermind has been located.") diff --git a/mods/ca/maps/ca23-emancipation/map.yaml b/mods/ca/maps/ca23-emancipation/map.yaml index 4eb9cd7f32..13321521a6 100644 --- a/mods/ca/maps/ca23-emancipation/map.yaml +++ b/mods/ca/maps/ca23-emancipation/map.yaml @@ -51,7 +51,7 @@ Players: PlayerReference@GDISlaves: Name: GDISlaves Bot: campaign - Faction: gdi + Faction: arc Color: D500E8 Allies: Scrin Enemies: GDI, Creeps diff --git a/mods/ca/maps/ca23-emancipation/rules.yaml b/mods/ca/maps/ca23-emancipation/rules.yaml index 4b74555899..f7094efb12 100644 --- a/mods/ca/maps/ca23-emancipation/rules.yaml +++ b/mods/ca/maps/ca23-emancipation/rules.yaml @@ -67,6 +67,16 @@ PATR: GDRN: RevealsShroud: Range: 8c0 + Buildable: + Prerequisites: ~vehicles.gdi, ~!tow.upgrade + +GDRN.TOW: + Buildable: + Prerequisites: ~vehicles.gdi, ~tow.upgrade + +MDRN: + Buildable: + Prerequisites: anyradar, ~vehicles.gdi UAV: -Targetable@AIRBORNE: diff --git a/mods/ca/maps/ca24-duality/rules.yaml b/mods/ca/maps/ca24-duality/rules.yaml index 9147557a1e..b3aa201739 100644 --- a/mods/ca/maps/ca24-duality/rules.yaml +++ b/mods/ca/maps/ca24-duality/rules.yaml @@ -5,7 +5,7 @@ World: LuaScript: Scripts: campaign.lua, ca24.lua MissionData: - Briefing: There is no time to lose! The Allied mercenary Tanya was sent to infiltrate a facility that is being used by the Scrin as a Tiberium storage facility.\n\nHer mission was to use explosives to destroy the Scrin stockpiles, however the facility was more heavily guarded than expected. She was injured and is now hiding from the Scrin, unable to reach an exit.\n\nUse one of our own commandos to find Tanya. Working together they should have no problems completing the original mission.\n\nPrior to radio silence, her position was known to be somewhere on the east side of the facility. + Briefing: There is no time to lose! The Allied mercenary Tanya was sent to infiltrate a facility that is being used by the Scrin for stockpiling Tiberium.\n\nHer mission was to use explosives to destroy the Scrin stockpiles, however the facility was more heavily guarded than expected. She was injured and is now hiding from the Scrin, unable to reach an exit.\n\nUse one of our own commandos to find Tanya. Working together they should have no problems completing the original mission.\n\nPrior to radio silence, her position was known to be somewhere on the east side of the facility. MapOptions: ShortGameCheckboxEnabled: False ScriptLobbyDropdown@DIFFICULTY: diff --git a/mods/ca/maps/ca25-singularity/ca25.lua b/mods/ca/maps/ca25-singularity/ca25.lua index a9839205df..d02d7d359d 100644 --- a/mods/ca/maps/ca25-singularity/ca25.lua +++ b/mods/ca/maps/ca25-singularity/ca25.lua @@ -391,8 +391,9 @@ WorldLoaded = function() CyborgsProvoked = true Utils.Do(cyborgs, function(c) if not c.IsDead then - TargetSwapChance(c, 10) + c.Stop() c.GrantCondition("provoked") + ClearCyborgTarget(c) end end) end @@ -419,6 +420,17 @@ Tick = function() PanToFinale() end +ClearCyborgTarget = function(cyborg) + if not IsFinaleStarted then + Trigger.AfterDelay(DateTime.Seconds(5), function() + if not cyborg.IsDead then + cyborg.Stop() + ClearCyborgTarget(cyborg) + end + end) + end +end + OncePerSecondChecks = function() if DateTime.GameTime > 1 and DateTime.GameTime % 25 == 0 then Scrin.Resources = Scrin.ResourceCapacity - 500 @@ -896,6 +908,10 @@ FlipSlaveFaction = function(player) end DoFinale = function() + if IsFinaleStarted then + return + end + IsFinaleStarted = true local pacs = Scrin.GetActorsByTypes({ "pac", "deva", "stmr" }) Utils.Do(pacs, function(a) if not a.IsDead then @@ -923,7 +939,8 @@ DoFinale = function() Actor.Create("wormhole", true, { Owner = Kane, Location = KaneSpawn.Location }) local kane = Actor.Create("kane", true, { Owner = Kane, Location = KaneSpawn.Location, Facing = Angle.South }) - Trigger.AfterDelay(DateTime.Seconds(3), function() + Trigger.AfterDelay(DateTime.Seconds(5), function() + kane.Stop() kane.Move(KaneSpawn.Location + CVec.New(0, 3)) end) @@ -965,6 +982,7 @@ DoFinale = function() Trigger.AfterDelay(AdjustTimeForGameSpeed(DateTime.Seconds(27)), function() if not kane.IsDead then + kane.Stop() kane.Move(WormholeWP.Location) end UserInterface.SetMissionText("To be continued...", HSLColor.Red) diff --git a/mods/ca/maps/ca25-singularity/rules.yaml b/mods/ca/maps/ca25-singularity/rules.yaml index 72af8948b8..a2788125e3 100644 --- a/mods/ca/maps/ca25-singularity/rules.yaml +++ b/mods/ca/maps/ca25-singularity/rules.yaml @@ -280,6 +280,11 @@ mothership.shields: Condition: provoked AttackFrontal: PauseOnCondition: !kane-revealed && bluebuff && !provoked + RangeMultiplier@PROVOKED: + Modifier: 125 + RequiresCondition: provoked + AttackFrontal: + FacingTolerance: 128 RMBC: Inherits@BLUEBUFF: ^BlueBuff @@ -310,6 +315,7 @@ SIGN: RepairPercent: 0 Power: Amount: 0 + -RecallPower@Recall: YF23.Interceptor: Inherits: MIG @@ -386,10 +392,6 @@ STWR: Buildable: Prerequisites: vehicles.any, ~structures.gdi -DISR: - Buildable: - Prerequisites: gtek - TITN: Buildable: Prerequisites: gtek, ~!railgun.upgrade @@ -418,6 +420,18 @@ GDRN.TOW: Buildable: Prerequisites: ~vehicles.gdi, ~tow.upgrade +WOLV: + Buildable: + Prerequisites: gtek, ~vehicles.gdi + +XO: + Buildable: + Prerequisites: gtek, ~vehicles.gdi + +PBUL: + Buildable: + Prerequisites: gtek, ~vehicles.gdi + AURO: Buildable: Prerequisites: afld.gdi, gtek diff --git a/mods/ca/maps/ca25-singularity/weapons.yaml b/mods/ca/maps/ca25-singularity/weapons.yaml index 9754e182b4..2f022d5cce 100644 --- a/mods/ca/maps/ca25-singularity/weapons.yaml +++ b/mods/ca/maps/ca25-singularity/weapons.yaml @@ -110,7 +110,7 @@ MothershipExplosion: InvalidTargets: Cyborg DamageTypes: Prone50Percent, TriggerProne, FireDeath, Incendiary Warhead@wallKiller: SpreadDamage - Spread: 8c0 + Spread: 12c0 Damage: 80000 ValidTargets: Wall Warhead@emp: GrantExternalConditionCA diff --git a/mods/ca/maps/ca26-foothold/ca26.lua b/mods/ca/maps/ca26-foothold/ca26.lua index 309660ef71..5af1d23a93 100644 --- a/mods/ca/maps/ca26-foothold/ca26.lua +++ b/mods/ca/maps/ca26-foothold/ca26.lua @@ -376,7 +376,7 @@ InitTibLifeforms = function() local blobs = TibLifeforms.GetActorsByType("tbcl") local patrolPath2 = { BlobPatrol4.Location, BlobPatrol5.Location, BlobPatrol6.Location, BlobPatrol7.Location, BlobPatrol8.Location, BlobPatrol9.Location, BlobPatrol10.Location, BlobPatrol11.Location, BlobPatrol1.Location, BlobPatrol2.Location, BlobPatrol3.Location } - local patrolPath3 = { BlobPatrolB1.Location, BlobPatrolB2.Location, BlobPatrolB3.Location, BlobPatrolB4.Location, BlobPatrolB5.Location } + local patrolPath3 = { BlobPatrolB1.Location, BlobPatrolB2.Location, BlobPatrolB3.Location, BlobPatrolB4.Location, BlobPatrolB5.Location, BlobPatrolB6.Location } Blob2.Patrol(patrolPath2, true) Blob3.Patrol(patrolPath3, true) diff --git a/mods/ca/maps/ca26-foothold/rules.yaml b/mods/ca/maps/ca26-foothold/rules.yaml index 7c4daff41b..d1b5b9f20b 100644 --- a/mods/ca/maps/ca26-foothold/rules.yaml +++ b/mods/ca/maps/ca26-foothold/rules.yaml @@ -14,7 +14,7 @@ World: LuaScript: Scripts: campaign.lua, ca26.lua MissionData: - Briefing: ———[ Chapter VI Introduction ]———\n\nAs the Scrin mothership fell from the sky in flames, the cyborgs surrounding it revealed their true allegiance. The Scrin, stranded in both space and time, were manipulated into opening a gateway to their homeworld with Kane's army at the threshold. \n\nWhile his ultimate objective is unclear, GDI are unwilling to let him pursue it uncontested. The gateway has remained open, but is rapidly destabilizing and it appears likely that it will collapse soon. Time is running out for GDI to gave chase.\n\n———[ Mission Briefing (GDI) ]———\n\nOur forces are almost ready and our recon drones have confirmed that the atmosphere and climatic conditions on the other side of the gateway will support human life.\n\nClearly, this is uncharted territory. We must establish a foothold from which we can base our operations. We have detected minimal Scrin presence in the immediate vicinity of our destination, and no sign of Kane or his army.\n\nShortly after Kane departed we detected an energy surge emanating from the Scrin signal transmitter. We believe this was caused by the gateway's exit point being shifted. Kane clearly didn't want us following closely behind, but it does beg the question; why did he leave the door open at all?\n\nThe gateway has become too unstable for heavier equipment to make it through, but research into Scrin wormhole technology gathered since their arrival has provided us with some understanding of how the portals work.\n\nA gateway requires bi-directional flow of energy to be sustained. Our side is taken care of, but without a corresponding transmission from the other side the gateway will eventually lose cohesion.\n\nScrin Nerve Centers are capable of providing such an energy flow, so if we can locate and capture one close enough to the exit point, the gateway should be stabilized.\n\nYour first objective is to deploy sensor arrays in the area around the exit. We should then be able to detect a suitable Nerve Center to capture, allowing us to bring heavier forces and establish a base. Then the area must be cleared of any remaining hostile forces. + Briefing: ———[ Chapter VI Introduction ]———\n\nAs the Scrin mothership fell from the sky in flames, the cyborgs surrounding it revealed their true allegiance. The Scrin, stranded in both space and time, were manipulated into opening a gateway to their homeworld with Kane's army at the threshold. \n\nWhile his ultimate objective is unclear, GDI are unwilling to let him pursue it uncontested. The gateway has remained open, but is rapidly destabilizing and it appears likely that it will collapse soon. Time is running out for GDI to give chase.\n\n———[ Mission Briefing (GDI) ]———\n\nOur forces are almost ready and our recon drones have confirmed that the atmosphere and climatic conditions on the other side of the gateway will support human life.\n\nClearly, this is uncharted territory. We must establish a foothold from which we can base our operations. We have detected minimal Scrin presence in the immediate vicinity of our destination, and no sign of Kane or his army.\n\nShortly after Kane departed we detected an energy surge emanating from the Scrin signal transmitter. We believe this was caused by the gateway's exit point being shifted. Kane clearly didn't want us following closely behind, but it does beg the question; why did he leave the door open at all?\n\nThe gateway has become too unstable for heavier equipment to make it through, but research into Scrin wormhole technology gathered since their arrival has provided us with some understanding of how the portals work.\n\nA gateway requires bi-directional flow of energy to be sustained. Our side is taken care of, but without a corresponding transmission from the other side the gateway will eventually lose cohesion.\n\nScrin Nerve Centers are capable of providing such an energy flow, so if we can locate and capture one close enough to the exit point, the gateway should be stabilized.\n\nYour first objective is to deploy sensor arrays in the area around the exit. We should then be able to detect a suitable Nerve Center to capture, allowing us to bring heavier forces and establish a base. Then the area must be cleared of any remaining hostile forces. MapOptions: ShortGameCheckboxEnabled: True ScriptLobbyDropdown@DIFFICULTY: @@ -75,6 +75,10 @@ SILO.SCRIN: Power: Amount: 0 +XO: + Buildable: + Prerequisites: gtek, ~vehicles.gdi + WORMHOLE: -PopControlled: -Targetable: diff --git a/mods/ca/maps/ca27-convergence/rules.yaml b/mods/ca/maps/ca27-convergence/rules.yaml index 1ff5f8e4c0..8f9e6ac62a 100644 --- a/mods/ca/maps/ca27-convergence/rules.yaml +++ b/mods/ca/maps/ca27-convergence/rules.yaml @@ -52,6 +52,10 @@ ATWR: Buildable: Prerequisites: vehicles.any, ~structures.gdi +XO: + Buildable: + Prerequisites: gtek, ~vehicles.gdi + EYE: Inherits@CAMPAIGNDISABLED: ^Disabled diff --git a/mods/ca/maps/ca28-illumination/rules.yaml b/mods/ca/maps/ca28-illumination/rules.yaml index c684aefd6e..8bc03cc823 100644 --- a/mods/ca/maps/ca28-illumination/rules.yaml +++ b/mods/ca/maps/ca28-illumination/rules.yaml @@ -20,7 +20,7 @@ World: LuaScript: Scripts: campaign.lua, ca28.lua MissionData: - Briefing: ———[ Nod Introduction ]———\n\nKane's cyborgs poured from the gateway on the Scrin homeworld. Kane's manipulation of the timeline meant that the Scrin were completely unprepared, however it would not take long for them to mobilize.\n\nThe gateway's exit was shifted to present the Scrin with additional front to contend with, then using cloaked transports Kane accompanied a contingent of his most elite cyborgs, leaving the bulk of his army to prepare for the inevitable arrival of Scrin forces.\n\nHis destination had been revealed to him by the Tacitus—an artifact containing ancient knowledge—which after painstaking efforts had finally revealed its true purpose.\n\n———[ Mission Briefing (Nod) ]———\n\nWe have arrived. Soon our great purpose here will become clear, but we must proceed quickly.\n\nThe Scrin will no doubt have been alerted to our presence, and simply by being here we threaten them in a way they have not been for millennia.\n\nSomewhere within the caves before us are remnants of a great project that was violently ended and buried long ago by those who still rule the Scrin to this day.\n\nA great conflict ended here, and its embers lie beneath our feet. I intend to reignite those embers.\n\nIf we are successful, it will herald the dawn of a new age for both the Scrin and mankind.\n\nTake command. Lead us through the caves. I have brought with me a means of navigating them, and a means of recovering the knowledge that the Scrin rulers thought they had wiped from existence. + Briefing: ———[ Nod Introduction ]———\n\nKane's cyborgs poured from the gateway on the Scrin homeworld. Kane's manipulation of the timeline meant that the Scrin were completely unprepared, however it would not take long for them to mobilize.\n\nThe gateway's exit was shifted to present the Scrin with an additional front to contend with, then using cloaked transports Kane accompanied a contingent of his most elite cyborgs, leaving the bulk of his army to prepare for the inevitable arrival of Scrin forces.\n\nHis destination had been revealed to him by the Tacitus—an artifact containing ancient knowledge—which after painstaking efforts had finally revealed its true purpose.\n\n———[ Mission Briefing (Nod) ]———\n\nWe have arrived. Soon our great purpose here will become clear, but we must proceed quickly.\n\nThe Scrin will no doubt have been alerted to our presence, and simply by being here we threaten them in a way they have not been for millennia.\n\nSomewhere within the caves before us are remnants of a great project that was violently ended and buried long ago by those who still rule the Scrin to this day.\n\nA great conflict ended here, and its embers lie beneath our feet. I intend to reignite those embers.\n\nIf we are successful, it will herald the dawn of a new age for both the Scrin and mankind.\n\nTake command. Lead us through the caves. I have brought with me a means of navigating them, and a means of recovering the knowledge that the Scrin rulers thought they had wiped from existence. MapOptions: ShortGameCheckboxEnabled: True ScriptLobbyDropdown@DIFFICULTY: diff --git a/mods/ca/maps/shellmap/shellmap.lua b/mods/ca/maps/shellmap/shellmap.lua index 8afe2cb6f1..6b33bf7c8f 100644 --- a/mods/ca/maps/shellmap/shellmap.lua +++ b/mods/ca/maps/shellmap/shellmap.lua @@ -21,7 +21,7 @@ UnitCompositionsShellmap = { { Infantry = { "e3", "e1", "e1", "e3", "e1" }, Vehicles = { "apc.ai" } }, { Infantry = { "e3", "e1", "e1", "cryt", "cryt", "e1", "cryt", "e3", "snip" }, Vehicles = { "arty", "apc.ai", "pcan", "ptnk" } }, { Infantry = { "e3", "e1", "e1", "cryt", "cryt", "e1", "e1", "e3", "snip" }, Vehicles = { "2tnk", "apc.ai", "batf.ai", "ptnk" } }, - { Infantry = { "e3", "e1", "e1", "cryt", "enfo", "e1", "e1", "e3", "snip" }, Vehicles = { "gtnk", "apc.ai", "chpr", "ptnk" } }, + { Infantry = { "e3", "e1", "e1", "cryt", "enfo", "e1", "e1", "e3", "snip" }, Vehicles = { "2tnk", "apc.ai", "chpr", "ptnk" } }, { Infantry = { "e3", "e1", "e1", "cryt", "cryt", "e1", "e1", "e3", "snip" }, Vehicles = { "tnkd", "rapc.ai", "batf.ai", "ptnk" } }, { Infantry = { "e3", "e1", "e1", "cryt", "cryt", "e1", "e1", "e3", "snip" }, Vehicles = { "2tnk", "rapc.ai", "cryo", "ptnk" } } } diff --git a/mods/ca/rules/custom/campaign-rules.yaml b/mods/ca/rules/custom/campaign-rules.yaml index 63d0eded57..bab49a9566 100644 --- a/mods/ca/rules/custom/campaign-rules.yaml +++ b/mods/ca/rules/custom/campaign-rules.yaml @@ -340,21 +340,21 @@ PROC.SCRIN: InfiltrateForCash: Maximum: 600 -WOLV: - Buildable: - Prerequisites: gtek, ~vehicles.gdi - -XO: - Buildable: - Prerequisites: gtek, ~vehicles.gdi - -PBUL: +DISR: Buildable: Prerequisites: gtek, ~vehicles.gdi -DISR: +sonic.upgrade: Buildable: - Prerequisites: gtek, ~vehicles.gdi + Prerequisites: gtek + +^GpsPower: + ProduceActorPowerCA@InitialSatelliteScan: + Prerequisites: anyradar, ~!gps.satellite.firstscan, ~fogenabled, ~!botplayer + ProduceActorPowerCA@SatelliteScan: + Prerequisites: anyradar, ~gps.satellite, ~gps.satellite.firstscan, ~fogenabled, ~!botplayer + ProduceActorPowerCA@SatelliteScanNoFog: + Prerequisites: anyradar, ~!fogenabled, ~!botplayer # For attack interval calculations, factoring in extra value from cargo diff --git a/mods/ca/rules/custom/composition-tester-powers.yaml b/mods/ca/rules/custom/composition-tester-powers.yaml new file mode 100644 index 0000000000..7b46dd6e41 --- /dev/null +++ b/mods/ca/rules/custom/composition-tester-powers.yaml @@ -0,0 +1,239 @@ +Player: + Inherits@FORCESHIELDPOWER: ^ForceShieldPower + Inherits@VEILOFWARPOWER: ^VeilOfWarPower + Inherits@CLUSTERMINESPOWER: ^ClusterMinesPower + Inherits@STRAFINGRUNPOWER: ^StrafingRunPower + Inherits@TIMEWARPPOWER: ^TimeWarpPower + Inherits@CRYOSTORMPOWER: ^CryostormPower + Inherits@HELIOSBOMBPOWER: ^HeliosBombPower + Inherits@CHRONOSHIFTPOWER: ^ChronoshiftPower + Inherits@PARABOMBSPOWER: ^ParabombsPower + Inherits@CARPETBOMBPOWER: ^CarpetBombPower + Inherits@ATOMBOMBPOWER: ^AtomBombPower + Inherits@MUTABOMBPOWER: ^MutaBombPower + Inherits@CHAOSBOMBSPOWER: ^ChaosBombsPower + Inherits@ATOMICAMMOPOWER: ^AtomicAmmoPower + Inherits@IRONCURTAINPOWER: ^IronCurtainPower + Inherits@INTERCEPTORSPOWER: ^InterceptorsPower + Inherits@NANITEREPAIRPOWER: ^NaniteRepairPower + Inherits@SURGICALSTRIKEPOWER: ^SurgicalStrikePower + Inherits@NANITESHIELDPOWER: ^NaniteShieldPower + Inherits@HACKERCELLPOWER: ^HackerCellPower + Inherits@SHADOWTEAMPOWER: ^ShadowTeamPower + Inherits@@INFERNOBOMBPOWER: ^InfernoBombPower + Inherits@FRENZYPOWER: ^FrenzyPower + Inherits@TIBSTEALTHPOWER: ^TibStealthPower + Inherits@STORMSPIKEPOWER: ^StormSpikePower + Inherits@BUZZERSWARMPOWER: ^BuzzerSwarmPower + Inherits@IONSURGEPOWER: ^IonSurgePower + Inherits@GREATERCOALESCENCEPOWER: ^GreaterCoalescencePower + Inherits@OVERLORDSWRATHPOWER: ^OverlordsWrathPower + Inherits@SUPPRESSIONPOWER: ^SuppressionPower + GrantExternalConditionPowerCA@FSHIELD: + -Prerequisites: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + SpawnActorPowerCA@VeilOfWar: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + AirstrikePowerCA@clustermines: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + ClassicAirstrikePower@Strafe: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + GrantExternalConditionPowerCA@TimeWarp: + -Prerequisites: + -BeginChargeSpeechNotification: + -EndChargeSpeechNotification: + ChargeInterval: 1 + SpawnActorPowerCA@Cryostorm: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + AirstrikePowerCA@HeliosBomb: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + ChronoshiftPowerCA@chronoshift: + -Prerequisites: + -BeginChargeSpeechNotification: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + AirstrikePowerCA@Russianparabombs: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + AirstrikePowerCA@CarpetBomb: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + AirstrikePowerCA@Iraqiparabombs: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + AirstrikePowerCA@ChaosBombs: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + AirstrikePowerCA@MutaBomb: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + GrantExternalConditionPowerCA@ATOMICAMMO: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + GrantExternalConditionPowerCA@HEROESOFUNION: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + AirstrikePowerCA@KillZone: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + GrantExternalConditionPowerCA@IRONCURTAIN: + -BeginChargeSpeechNotification: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + GrantExternalConditionPowerCA@NREPAIR: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + SpawnActorPowerCA@NSHIELD: + -Prerequisites: + -BeginChargeSpeechNotification: + -EndChargeSpeechNotification: + ChargeInterval: 1 + InterceptorPower@AirDef: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + ProduceActorPowerCA@hackercell: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + AirReinforcementsPower@ShadowTeam: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + AirstrikePowerCA@BlackhandFirebomb: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + GrantExternalConditionPowerCA@Frenzy: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + GrantExternalConditionPowerCA@SGEN: + -ActiveCondition: + -BeginChargeSpeechNotification: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + DetonateWeaponPower@STORMSPIKE: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + DetonateWeaponPower@BUZZERSWARM: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + DetonateWeaponPower@IONSURGE: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + DetonateWeaponPower@GREATERCOALESCENCE: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + MeteorPower@OverlordsWrath: + -Prerequisites: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + GrantExternalConditionPowerCA@SUPPRESSION: + -Prerequisites: + -BeginChargeSpeechNotification: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + ExternalCondition@1: + Condition: disabled + ExternalCondition@2: + Condition: empdisable + ExternalCondition@3: + Condition: being-warped + ExternalCondition@4: + Condition: build-incomplete + ExternalCondition@5: + Condition: tower.shield + +ALHQ: + MissileStrikePower@PatriotStrike: + -Prerequisites: + -EndChargeSpeechNotification: + ChargeInterval: 1 + +PATR: + AttackOrderPowerCA@EMPMISSILE: + -BeginChargeSpeechNotification: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + +TMPL: + NukePower@Cluster: + -Prerequisites: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + +SIGN: + RecallPower@Recall: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + +WEAT: + DetonateWeaponPower@LightningStorm: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + +MSLO: + NukePower@ABomb: + -BeginChargeSpeechNotification: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + +EYE: + IonCannonPower@surgicals: + -Prerequisites: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + DetonateWeaponPower@IonStorm: + -BeginChargeSpeechNotification: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + +MSLO.Nod: + NukePower@Chemmiss: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 + +RFGN: + DetonateWeaponPower@RiftGenerator: + -EndChargeSpeechNotification: + DisplayTimerRelationships: None + ChargeInterval: 1 diff --git a/mods/ca/rules/custom/two-tone-nod.yaml b/mods/ca/rules/custom/two-tone-nod.yaml index 867c4dceae..45ca0f3252 100644 --- a/mods/ca/rules/custom/two-tone-nod.yaml +++ b/mods/ca/rules/custom/two-tone-nod.yaml @@ -78,10 +78,18 @@ HARV.TD: RenderSprites: PlayerPalette: playertd +HARV.TD.Husk: + RenderSprites: + PlayerPalette: playertd + AMCV: RenderSprites: PlayerPalette: playertd +AMCV.Husk: + RenderSprites: + PlayerPalette: playertd + LST: RenderSprites: PlayerPalette: player-twotonenod @@ -106,6 +114,10 @@ SCRN: RenderSprites: PlayerPalette: playertd +SCRN.Husk: + RenderSprites: + PlayerPalette: playertd + BEAG: RenderSprites: PlayerPalette: player-twotonenod diff --git a/mods/ca/rules/defaults.yaml b/mods/ca/rules/defaults.yaml index 893538e0c2..3406925e35 100644 --- a/mods/ca/rules/defaults.yaml +++ b/mods/ca/rules/defaults.yaml @@ -115,9 +115,6 @@ ProvidesPrerequisite@NavalAI: Prerequisite: is-naval-ai RequiresCondition: owned-by-naval-ai - ProvidesPrerequisite@AI: - Prerequisite: botplayer - RequiresCondition: owned-by-brutal-ai || owned-by-vhard-ai || owned-by-hard-ai || owned-by-normal-ai || owned-by-easy-ai || owned-by-naval-ai ProductionCostMultiplier@brutalbonus: Multiplier: 90 Prerequisites: is-brutal-ai @@ -3165,6 +3162,7 @@ Cloak@SGENCLOAK: CloakSound: cloak5.aud UncloakSound: appear1.aud + UpdatesBuildOrder: ^BuildingTD: Inherits@1: ^Building @@ -3260,6 +3258,7 @@ AreaTypes: building, defense GivesExperienceCA: ActorExperienceModifier: 10000 + -UpdatesBuildOrder: ^DefenseTD: Inherits@1: ^Defense diff --git a/mods/ca/rules/infantry.yaml b/mods/ca/rules/infantry.yaml index 8a6524e0cf..5db76b7831 100644 --- a/mods/ca/rules/infantry.yaml +++ b/mods/ca/rules/infantry.yaml @@ -111,6 +111,8 @@ TDOG: EmptyWeapon: DogExplode AttackLeap: TargetFrozenActors: False + Targetable@TerrorDog: + TargetTypes: TerrorDog E1: Inherits: ^Soldier @@ -225,6 +227,9 @@ E2: WithProductionIconOverlay: Types: Veterancy Prerequisites: barracks.upgraded + DamageMultiplier@HeroOfTheUnion: + Modifier: 30 + RequiresCondition: herooftheunion E3: Inherits: ^Soldier @@ -628,6 +633,9 @@ E4: Prerequisites: barracks.upgraded Convertible: SpawnActors: N5 + DamageMultiplier@HeroOfTheUnion: + Modifier: 30 + RequiresCondition: herooftheunion E6: Inherits: ^Soldier diff --git a/mods/ca/rules/player.yaml b/mods/ca/rules/player.yaml index f2dbf50b05..859bae5edb 100644 --- a/mods/ca/rules/player.yaml +++ b/mods/ca/rules/player.yaml @@ -329,6 +329,12 @@ Player: ProvidesPrerequisite@collector: Prerequisite: player.collector Factions: collector + ProvidesPrerequisite@BotPlayer: + Prerequisite: botplayer + RequiresCondition: botplayer + GrantConditionOnBotOwner@BotPlayer: + Condition: botplayer + Bots: brutal, vhard, hard, normal, easy, naval, campaign GrantConditionOnPrerequisiteManager: GrantConditionOnPrerequisiteManagerCA: EnemyWatcher: @@ -357,6 +363,7 @@ Player: AutoDeployManager: CapturedFactionsManager: UpgradesManager: + BuildOrderTracker: ProvidesPrerequisiteOnCounts@ScrinAllegiances: Factions: scrin, reaper, traveler, harbinger, collector Prerequisite: scrin.allegiances.available diff --git a/mods/ca/rules/powers.yaml b/mods/ca/rules/powers.yaml index af7edee7aa..e7a18351dd 100644 --- a/mods/ca/rules/powers.yaml +++ b/mods/ca/rules/powers.yaml @@ -310,6 +310,8 @@ UseDirectionalTarget: True DirectionArrowAnimation: paradirection SupportPowerPaletteOrder: 25 + TargetCircleRange: 8c0 + TargetCircleColor: ffff00AA ^PatriotStrikePower: MissileStrikePower@PatriotStrike: @@ -771,7 +773,7 @@ GrantExternalConditionPowerCA@HEROESOFUNION: OrderName: heroesofunion Icon: heroes - ChargeInterval: 5250 + ChargeInterval: 6000 Name: Heroes of the Union Condition: herooftheunion Range: 3c512 @@ -1640,6 +1642,7 @@ SupportPowerPaletteOrder: 40 ShowTargetCircle: true TargetCircleColor: ff7700 + TargetTintColor: ff770033 ^TibStealthPower: GrantExternalConditionPowerCA@SGEN: @@ -1670,6 +1673,7 @@ SupportPowerPaletteOrder: 20 ShowTargetCircle: true TargetCircleColor: 00ff11 + TargetTintColor: 00ff1133 ^ChemicalMissilePower: NukePower@Chemmiss: diff --git a/mods/ca/rules/scrin.yaml b/mods/ca/rules/scrin.yaml index 36d4f839bd..37120d1de7 100644 --- a/mods/ca/rules/scrin.yaml +++ b/mods/ca/rules/scrin.yaml @@ -1641,24 +1641,24 @@ LCHR: Explodes: Weapon: LeecherExplode EmptyWeapon: LeecherExplode - RequiresCondition: !coalescence-upgrade + RequiresCondition: !coalescence-upgrade && !being-warped Explodes@COALESCENCE: Weapon: LeecherCoalesce EmptyWeapon: LeecherCoalesce - RequiresCondition: coalescence-upgrade + RequiresCondition: coalescence-upgrade && !being-warped GrantConditionOnPrerequisite@COALESCENCE: Condition: coalescence-upgrade Prerequisites: coalescence.upgrade SpawnActorOnDeath: Actor: lchr.orb - RequiresCondition: coalescence-upgrade + RequiresCondition: coalescence-upgrade && !being-warped KillsSelf: RequiresCondition: triggered ActorLostNotification: RequiresCondition: !coalescence-upgrade GrantConditionOnDeploy: DeployedCondition: triggered - RequiresCondition: coalescence-upgrade + RequiresCondition: coalescence-upgrade && !being-warped GivesBounty: RequiresCondition: global-bounty && !coalescence-upgrade TransfersStanceToDeathActor: @@ -1667,6 +1667,7 @@ LCHR.Orb: Inherits@1: ^1x1Shape Inherits@2: ^SelectableSupportUnit Inherits@bounty: ^GlobalBounty + Inherits@Warpable: ^Warpable Selectable: DecorationBounds: 1536, 1536, 0, -128 Tooltip: @@ -1677,6 +1678,11 @@ LCHR.Orb: Image: lchr.orb Palette: caneon WithSpriteBody: + RequiresCondition: being-warped + WithSpriteBody@Warped: + Name: warped + Sequence: warped + RequiresCondition: being-warped ClassicFacingBodyOrientation: QuantizedFacings: 1 Targetable: @@ -1720,6 +1726,7 @@ LCHR.Orb: Count: 1 ShowSelectionBar: true SelectionBarColor: ff0000 + PauseOnCondition: being-warped SpawnActorOnDeath: Actor: lchr RequiresCondition: resurrect @@ -3874,6 +3881,7 @@ SFAC: SpawnActorsOnSell: ActorTypes: s1,s1,s1,s1,s6 GuaranteedActorTypes: s1,s1 + -UpdatesBuildOrder: REAC: Inherits: ^ScrinBuilding diff --git a/mods/ca/rules/structures.yaml b/mods/ca/rules/structures.yaml index 765e7b348b..9dab14d2d8 100644 --- a/mods/ca/rules/structures.yaml +++ b/mods/ca/rules/structures.yaml @@ -1392,6 +1392,7 @@ FACT: Condition: invisibility SpawnActorsOnSell: ActorTypes: e1,e1,e1,e1,e6 + -UpdatesBuildOrder: PROC: Inherits: ^Building @@ -3294,6 +3295,7 @@ AFAC: Condition: invisibility SpawnActorsOnSell: ActorTypes: n1,n1,n1,n1,n6 + -UpdatesBuildOrder: NSAM: Inherits: ^DefenseTD diff --git a/mods/ca/rules/upgrades.yaml b/mods/ca/rules/upgrades.yaml index da1ce2961f..e1f388e669 100644 --- a/mods/ca/rules/upgrades.yaml +++ b/mods/ca/rules/upgrades.yaml @@ -101,7 +101,7 @@ seek.strat: IconPalette: chrometd Description: Speed and weapon range focused strategy, required for:\n• TOW Missile Upgrade\n• Zone Raider\n• Hypersonic Missiles Upgrade\n\n TooltipExtras: - Strengths: + Increases speed and weapon range of vehicles and aircraft by 5%\n• Allows training of Zone Raiders + Strengths: + Increases speed and weapon range of vehicles and aircraft by 5% Attributes: \n(!) Only ONE Strategy may be chosen. Valued: Cost: 1000 @@ -121,7 +121,7 @@ seek2.strat: Prerequisites: ~player.gdi, gtek, ~seek.strat, ~techlevel.high Description: Increase Seek & Destroy strategy bonuses. TooltipExtras: - Strengths: + Increases speed and weapon range of vehicles and aircraft by a further 5%\n• Allows training of Zone Defenders + Strengths: + Increases speed and weapon range of vehicles and aircraft by a further 5%\n• Allows training of Zone Raiders -Attributes: RenderSprites: Image: seek2.strat @@ -156,7 +156,7 @@ hold2.strat: Prerequisites: ~player.gdi, gtek, ~hold.strat, ~techlevel.high Description: Increase Hold the Line strategy bonuses. TooltipExtras: - Strengths: + Increases armor of vehicles and aircraft by a further 5% + Strengths: + Increases armor of vehicles and aircraft by a further 5%\n• Allows training of Zone Defenders -Attributes: RenderSprites: Image: hold2.strat diff --git a/mods/ca/rules/vehicles.yaml b/mods/ca/rules/vehicles.yaml index 0fcbd59de7..fa200610ac 100644 --- a/mods/ca/rules/vehicles.yaml +++ b/mods/ca/rules/vehicles.yaml @@ -214,6 +214,7 @@ NUKC: Voice: Move ImmovableCondition: deployed RequireForceMoveCondition: !undeployed + PauseOnCondition: being-captured || empdisable || being-warped || driver-dead || notmobile || undeploying RevealsShroud: MinRange: 4c0 Range: 5c0 @@ -259,21 +260,32 @@ NUKC: PauseOnCondition: being-warped SmartDeploy: True ValidFacings: 0, 256, 512, 768 + GrantTimedCondition@Deploying: + Condition: reloading + Duration: 25 + RequiresCondition: deployed + GrantTimedCondition@Undeploying: + Condition: undeploying + Duration: 25 + RequiresCondition: undeployed + WithEnabledAnimation@Deploying: + RequiresCondition: deployed + Sequence: make + Body: deployed + WithEnabledAnimation@Undeploying: + RequiresCondition: undeployed + Sequence: undeploy + Body: body WithFacingSpriteBody: - RequiresCondition: !deployed && undeployed - WithFacingSpriteBody@DeployingChassis: - Name: deploying - RequiresCondition: !deployed && !undeployed + RequiresCondition: undeployed WithFacingSpriteBody@DeployedChassis: Name: deployed Sequence: deployed - RequiresCondition: deployed + RequiresCondition: !undeployed WithFacingSpriteBody@Overlay: Name: overlay Sequence: overlay WaitsForTurretAlignmentOnUndeploy: - WithMakeAnimation: - BodyNames: deploying WithSpriteTurret: Sequence: turret Turreted: @@ -4361,7 +4373,7 @@ HTNK: Inherits@UpgradeOverlay: ^UpgradeOverlay Buildable: Queue: VehicleSQ, VehicleMQ - BuildPaletteOrder: 252 + BuildPaletteOrder: 253 IconPalette: chrometd Prerequisites: ~!mdrone.upgrade, gtek, ~vehicles.htnk, ~!ionmam.upgrade, ~!hovermam.upgrade, ~techlevel.high Description: Big and slow tank with anti-air capability. @@ -4463,7 +4475,7 @@ HTNK: HTNK.Ion: Inherits: HTNK Buildable: - BuildPaletteOrder: 253 + BuildPaletteOrder: 254 Prerequisites: gtek, ~vehicles.htnk, ~ionmam.upgrade, ~techlevel.high Tooltip: Name: Ion Mammoth Tank @@ -4486,7 +4498,7 @@ HTNK.Hover: Name: Hover Mammoth Tank GenericName: Tank Buildable: - BuildPaletteOrder: 254 + BuildPaletteOrder: 255 Prerequisites: gtek, ~vehicles.htnk, ~hovermam.upgrade, ~techlevel.high Description: Big and fairly mobile tank with anti-air capability. TooltipExtras: @@ -4526,7 +4538,7 @@ HTNK.Drone: Inherits@HACKABLE: ^Hackable Buildable: Queue: VehicleSQ, VehicleMQ - BuildPaletteOrder: 255 + BuildPaletteOrder: 256 IconPalette: chrometd Prerequisites: ~mdrone.upgrade, gtek, ~vehicles.htnk, ~techlevel.high Description: Remotely piloted heavy tank with anti-air capability. @@ -5507,11 +5519,12 @@ TRPC: Weaknesses: • Unarmed Attributes: • Empowers nearby basic infantry\n• Heals all nearby infantry\n• Detects cloaked units and mines Valued: - Cost: 1500 + Cost: 1600 Tooltip: Name: Troop Crawler UpdatesPlayerStatistics: - AddToArmyValue: true + AddToArmyValue: false + AddToAssetsValue: false Health: HP: 36000 Armor: @@ -5617,6 +5630,8 @@ TRPC: RequiresCondition: cmsr-inspiration Targetable@TroopCrawler: TargetTypes: TroopCrawler + Sellable: + RefundPercent: 10 IFV: Inherits: ^Tank @@ -5835,12 +5850,12 @@ IFV: Armament@E3: Weapon: IFVRocketsE LocalOffset: 192,100,176, 192,-100,176 - RequiresCondition: (samturr || ggiturr) && !cryw-upgrade + RequiresCondition: (samturr || ggiturr) && !cryw-upgrade && !tibcore-upgrade Armament@E3AA: Name: secondary Weapon: IFVRocketsAAE LocalOffset: 192,100,176, 192,-100,176 - RequiresCondition: (samturr || ggiturr) && !cryw-upgrade + RequiresCondition: (samturr || ggiturr) && !cryw-upgrade && !tibcore-upgrade Armament@E3CRYO: Weapon: IFVRocketsE.CRYO LocalOffset: 192,100,176, 192,-100,176 @@ -5850,6 +5865,18 @@ IFV: Weapon: IFVRocketsAAE.CRYO LocalOffset: 192,100,176, 192,-100,176 RequiresCondition: (samturr || ggiturr) && cryw-upgrade + + Armament@E3TIB: + Weapon: IFVRocketsE.TibCore + LocalOffset: 192,100,176, 192,-100,176 + RequiresCondition: (samturr || ggiturr) && tibcore-upgrade && !cryw-upgrade + Armament@E3TIBAA: + Name: secondary + Weapon: IFVRocketsAAE.TibCore + LocalOffset: 192,100,176, 192,-100,176 + RequiresCondition: (samturr || ggiturr) && tibcore-upgrade && !cryw-upgrade + + Armament@E4: Weapon: FireballLauncher Recoil: 85 @@ -6240,6 +6267,9 @@ IFV: GrantConditionOnPrerequisite@CRYO: Condition: cryw-upgrade Prerequisites: cryw.upgrade + GrantConditionOnPrerequisite@TibCore: + Condition: tibcore-upgrade + Prerequisites: tibcore.upgrade GuardsSelection@REPAIR: ValidTargets: Vehicle RequiresCondition: engturr && !stance-attackanything @@ -6523,7 +6553,7 @@ TITN: Inherits@UpgradeOverlay: ^UpgradeOverlay Buildable: Queue: VehicleSQ, VehicleMQ - BuildPaletteOrder: 256 + BuildPaletteOrder: 257 IconPalette: chrometd Prerequisites: gtek, ~vehicles.talon, ~!railgun.upgrade, ~techlevel.high Description: Tough, slow combat battle-mech. @@ -6631,7 +6661,7 @@ TITN.RAIL: Tooltip: Name: Railgun Titan Buildable: - BuildPaletteOrder: 257 + BuildPaletteOrder: 258 Prerequisites: gtek, ~vehicles.talon, ~railgun.upgrade, ~techlevel.high Armament@PRIMARY: Weapon: TitanRailgun @@ -6658,7 +6688,7 @@ JUGG: Tooltip: Name: Juggernaut Buildable: - BuildPaletteOrder: 258 + BuildPaletteOrder: 259 Queue: VehicleSQ, VehicleMQ IconPalette: chrometd Description: Tough artillery battle-mech. @@ -6803,7 +6833,7 @@ DISR: AddToArmyValue: true Buildable: Queue: VehicleSQ, VehicleMQ - BuildPaletteOrder: 259 + BuildPaletteOrder: 260 IconPalette: chrometd Prerequisites: gtek, ~vehicles.zocom, ~techlevel.high Description: Armored high-tech vehicle with medium-range sonic armament. @@ -9196,7 +9226,7 @@ THWK: UpdatesPlayerStatistics: AddToArmyValue: true Buildable: - BuildPaletteOrder: 260 + BuildPaletteOrder: 261 Prerequisites: gtek, ~thwk.upgrade, ~techlevel.high Queue: VehicleSQ, VehicleMQ IconPalette: chrometd diff --git a/mods/ca/sequences/scrin.yaml b/mods/ca/sequences/scrin.yaml index 47d06bafb0..10d82cbb13 100644 --- a/mods/ca/sequences/scrin.yaml +++ b/mods/ca/sequences/scrin.yaml @@ -1435,11 +1435,14 @@ lchr.destroyed: ZOffset: -512 lchr.orb: - idle: + Inherits: ^VehicleOverlays + Defaults: Filename: lchrorb.shp Alpha: 0.75 - Length: * ZOffset: 512 + idle: + Length: * + warped: stcr: Inherits: ^VehicleOverlays diff --git a/mods/ca/sequences/vehicles.yaml b/mods/ca/sequences/vehicles.yaml index 7becd7104d..0b277ef54e 100644 --- a/mods/ca/sequences/vehicles.yaml +++ b/mods/ca/sequences/vehicles.yaml @@ -2418,7 +2418,13 @@ nukc: Filename: nukcd.shp Facings: 4 Length: 4 - Tick: 160 + Tick: 300 + undeploy: + Filename: nukcd.shp + Frames: 3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12 + Facings: 4 + Length: 4 + Tick: 300 deployed: Filename: nukcd.shp Facings: 4 diff --git a/mods/ca/weapons/ballistics.yaml b/mods/ca/weapons/ballistics.yaml index 5a3d39a5eb..72fead4e3f 100644 --- a/mods/ca/weapons/ballistics.yaml +++ b/mods/ca/weapons/ballistics.yaml @@ -566,7 +566,7 @@ TitanGun: LaunchAngle: 92 Inaccuracy: 768 Warhead@1Dam: SpreadDamage - Spread: 488 + Spread: 512 Damage: 6000 Falloff: 1000, 448, 192, 50, 18, 7, 0 Versus: @@ -574,7 +574,7 @@ TitanGun: Wood: 100 Concrete: 100 Light: 70 - Heavy: 55 + Heavy: 70 Warhead@2Dam: SpreadDamage Damage: 70000 Spread: 341 diff --git a/mods/ca/weapons/missiles.yaml b/mods/ca/weapons/missiles.yaml index 12fe3474b9..e54e36b765 100644 --- a/mods/ca/weapons/missiles.yaml +++ b/mods/ca/weapons/missiles.yaml @@ -1447,6 +1447,12 @@ IFVRocketsAAE.CRYO: ValidTargets: Air, AirSmall ValidRelationships: Enemy, Neutral +IFVRocketsE.TibCore: + Inherits: StnkMissile.TibCore + +IFVRocketsAAE.TibCore: + Inherits: StnkMissileAA.TibCore + #Artillery Missiles, lock-on but act dumb 227mm: Inherits: ^AntiGroundMissile diff --git a/mods/ca/weapons/other.yaml b/mods/ca/weapons/other.yaml index 55d4f902eb..3b367d79e5 100644 --- a/mods/ca/weapons/other.yaml +++ b/mods/ca/weapons/other.yaml @@ -612,7 +612,12 @@ DogExplode: Concrete: 75 Heavy: 70 DamageTypes: Prone50Percent, TriggerProne, ExplosionDeath, TankBuster - AffectsParent: true + InvalidTargets: TerrorDog + Warhead@FF: HealthPercentageDamage + Spread: 2c0 + Damage: 25 + DamageTypes: Prone50Percent, TriggerProne, ExplosionDeath + ValidTargets: TerrorDog Warhead@3Eff: CreateEffect Explosions: artillery_explosion ImpactSounds: xplosml2.aud @@ -1228,13 +1233,14 @@ PortaLaser.UPG: IFVLaser: Inherits: PortaLaser + Range: 7c0 -Burst: -StartBurstReport: Report: venmfireupg1.aud, venmfireupg1.aud Projectile: LaserZapCA SecondaryBeamWidth: 75 Warhead@1Dam: SpreadDamage - Damage: 3750 + Damage: 4250 EnlightenedBeam: ReloadDelay: 65 @@ -2691,7 +2697,7 @@ DisruptorPulse: Wood: 60 Light: 28 Heavy: 12 - Concrete: 35 + Concrete: 45 ValidRelationships: Ally DamageTypes: Prone50Percent, TriggerProne, ExplosionDeath InvalidTargets: Disruptor @@ -3147,6 +3153,8 @@ ViperLaser: Report: viper-fire1.aud Projectile: PlasmaBeam Colors: ff0000E6, cc0000E6 + Warhead@1Dam: SpreadDamage + Damage: 1550 ShadeEmp: Inherits: EnlightenedEmp @@ -3564,6 +3572,7 @@ BasiliskPulse: CyclopsZap: Inherits: TTrackZap + Range: 9c0 JackknifeTargeter: Range: 8c0 diff --git a/mods/ca/weapons/scrin.yaml b/mods/ca/weapons/scrin.yaml index d645e68194..cce1004c4c 100644 --- a/mods/ca/weapons/scrin.yaml +++ b/mods/ca/weapons/scrin.yaml @@ -1914,7 +1914,8 @@ ObliteratorBolt: Palette: scrin ImpactInterval: 3 Warhead@1Dam: SpreadDamage - Spread: 256 + Spread: 390 + Falloff: 100, 50, 0 Damage: 7500 Versus: Wood: 40 @@ -1924,7 +1925,8 @@ ObliteratorBolt: DamageTypes: Prone50Percent, TriggerProne, FireDeath ValidRelationships: Enemy, Neutral Warhead@friendlyFire: SpreadDamage - Spread: 256 + Spread: 390 + Falloff: 100, 50, 0 Damage: 1000 Versus: Wood: 40 diff --git a/mods/ca/weapons/superweapons.yaml b/mods/ca/weapons/superweapons.yaml index 709875f170..e6e48aa0e2 100644 --- a/mods/ca/weapons/superweapons.yaml +++ b/mods/ca/weapons/superweapons.yaml @@ -574,7 +574,7 @@ FirestormBarrage: None: 70 Wood: 40 Light: 100 - Heavy: 80 + Heavy: 90 Concrete: 80 Brick: 5 DamageTypes: Prone50Percent, TriggerProne, FireDeath, Incendiary