diff --git a/.editorconfig b/.editorconfig index eacc64333d80..f6f3e2f00e53 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,15 +9,11 @@ insert_final_newline = true trim_trailing_whitespace = true ; 4-column tab indentation -[*.yaml] -indent_style = tab -indent_size = 4 - -; 4-column tab indentation and .NET coding conventions -[*.cs] +[*.{cs,csproj,yaml,lua,sh,ps1}] indent_style = tab indent_size = 4 +; .NET coding conventions #### Code Style Rules #### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ @@ -950,37 +946,180 @@ dotnet_diagnostic.CA2251.severity = warning # Ensure ThreadStatic is only used with static fields. dotnet_diagnostic.CA2259.severity = suggestion # TODO: Change to warning once using .NET 7 or later. -### Roslynator -### https://github.com/JosefPihrt/Roslynator/tree/main/docs/analyzers +### Roslynator.Analyzers +### https://josefpihrt.github.io/docs/roslynator/analyzers +# We disable the rule category by setting severity to none. # Below we enable specific rules by setting severity to warning. +dotnet_analyzer_diagnostic.category-roslynator.severity = none + +# Remove redundant 'sealed' modifier. +dotnet_diagnostic.RCS1034.severity = warning + +# Remove argument list from attribute. +dotnet_diagnostic.RCS1039.severity = warning + +# Remove empty initializer. +dotnet_diagnostic.RCS1041.severity = warning + +# Remove enum default underlying type. +dotnet_diagnostic.RCS1042.severity = warning + +# Remove 'partial' modifier from type with a single part. +dotnet_diagnostic.RCS1043.severity = warning + +# Use lambda expression instead of anonymous method. +dotnet_diagnostic.RCS1048.severity = warning + +# Simplify boolean comparison. +dotnet_diagnostic.RCS1049.severity = warning + +# Use compound assignment. +dotnet_diagnostic.RCS1058.severity = warning + +# Avoid locking on publicly accessible instance. +dotnet_diagnostic.RCS1059.severity = warning + +# Remove empty 'finally' clause. +dotnet_diagnostic.RCS1066.severity = warning + +# Simplify logical negation. +dotnet_diagnostic.RCS1068.severity = warning + +# Remove redundant base constructor call. +dotnet_diagnostic.RCS1071.severity = warning + +# Remove empty namespace declaration. +dotnet_diagnostic.RCS1072.severity = warning + +# Remove redundant constructor. +dotnet_diagnostic.RCS1074.severity = warning # Use 'Count' property instead of 'Any' method. dotnet_diagnostic.RCS1080.severity = warning -# Use read-only auto-implemented property. -dotnet_diagnostic.RCS1170.severity = warning +# Use coalesce expression instead of conditional expression. +dotnet_diagnostic.RCS1084.severity = warning -# Unnecessary interpolated string. -dotnet_diagnostic.RCS1214.severity = warning +# Remove empty region. +dotnet_diagnostic.RCS1091.severity = warning + +# Default label should be the last label in a switch section. +dotnet_diagnostic.RCS1099.severity = warning + +# Unnecessary interpolation. +dotnet_diagnostic.RCS1105.severity = warning + +# Remove redundant 'ToCharArray' call. +dotnet_diagnostic.RCS1107.severity = warning + +# Add 'static' modifier to all partial class declarations. +dotnet_diagnostic.RCS1108.severity = warning + +# Combine 'Enumerable.Where' method chain. +dotnet_diagnostic.RCS1112.severity = warning + +# Use 'string.IsNullOrEmpty' method. +dotnet_diagnostic.RCS1113.severity = warning + +# Bitwise operation on enum without Flags attribute. +dotnet_diagnostic.RCS1130.severity = warning + +# Remove redundant overriding member. +dotnet_diagnostic.RCS1132.severity = warning + +# Remove redundant Dispose/Close call. +dotnet_diagnostic.RCS1133.severity = warning + +# Remove redundant statement. +dotnet_diagnostic.RCS1134.severity = warning + +# Merge switch sections with equivalent content. +dotnet_diagnostic.RCS1136.severity = warning + +# Simplify coalesce expression. +dotnet_diagnostic.RCS1143.severity = warning + +# Remove redundant cast. +dotnet_diagnostic.RCS1151.severity = warning + +# Use StringComparison when comparing strings. +dotnet_diagnostic.RCS1155.severity = warning + +# Use EventHandler. +dotnet_diagnostic.RCS1159.severity = warning + +# Unused type parameter. +dotnet_diagnostic.RCS1164.severity = warning + +# Use 'is' operator instead of 'as' operator. +dotnet_diagnostic.RCS1172.severity = warning + +# Unused 'this' parameter. +dotnet_diagnostic.RCS1175.severity = warning + +# Unnecessary assignment. +dotnet_diagnostic.RCS1179.severity = warning + +# Use constant instead of field. +dotnet_diagnostic.RCS1187.severity = warning + +# Join string expressions. +dotnet_diagnostic.RCS1190.severity = warning + +# Declare enum value as combination of names. +dotnet_diagnostic.RCS1191.severity = warning # Unnecessary usage of verbatim string literal. dotnet_diagnostic.RCS1192.severity = warning -# Use pattern matching instead of combination of 'as' operator and null check. -dotnet_diagnostic.RCS1221.severity = warning +# Overriding member should not change 'params' modifier. +dotnet_diagnostic.RCS1193.severity = warning + +# Use ^ operator. +dotnet_diagnostic.RCS1195.severity = warning + +# Unnecessary null check. +dotnet_diagnostic.RCS1199.severity = warning + +# Use EventArgs.Empty. +dotnet_diagnostic.RCS1204.severity = warning + +# Order type parameter constraints. +dotnet_diagnostic.RCS1209.severity = warning + +# Unnecessary interpolated string. +dotnet_diagnostic.RCS1214.severity = warning # Expression is always equal to 'true'. dotnet_diagnostic.RCS1215.severity = warning -# Use StringComparison when comparing strings. -dotnet_diagnostic.RCS1155.severity = warning +# Unnecessary unsafe context. +dotnet_diagnostic.RCS1216.severity = warning + +# Simplify code branching. +dotnet_diagnostic.RCS1218.severity = warning + +# Use pattern matching instead of combination of 'is' operator and cast operator. +dotnet_diagnostic.RCS1220.severity = warning + +# Make class sealed. +dotnet_diagnostic.RCS1225.severity = warning + +# Add paragraph to documentation comment. +dotnet_diagnostic.RCS1226.severity = warning + +# Validate arguments correctly. +dotnet_diagnostic.RCS1227.severity = warning + +# Unnecessary explicit use of enumerator. +dotnet_diagnostic.RCS1230.severity = warning -# Abstract type should not have public constructors. -dotnet_diagnostic.RCS1160.severity = warning +# Use short-circuiting operator. +dotnet_diagnostic.RCS1233.severity = warning -# Optimize 'Dictionary.ContainsKey' call. +# Optimize method call. dotnet_diagnostic.RCS1235.severity = warning -# Call extension method as instance method. -dotnet_diagnostic.RCS1196.severity = warning +# Use 'for' statement instead of 'while' statement. +dotnet_diagnostic.RCS1239.severity = warning diff --git a/.gitattributes b/.gitattributes index 949fc9677150..2980f58dc28f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,7 @@ *.csproj eol=lf *.sln eol=lf * text=lf +*.csproj eol=lf # Custom for Visual Studio *.cs diff=csharp diff --git a/.gitignore b/.gitignore index a84fd6521bff..78002ea0658c 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ update.log # IntelliJ files .idea + +#Attacque Supérior +/mods/as/ diff --git a/AUTHORS b/AUTHORS index 4e478b75829f..6e755fa8fb2f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -150,6 +150,7 @@ Also thanks to: * Teemu Nieminen (Temeez) * Thomas Christlieb (ThomasChr) * Tim Mylemans (gecko) + * Tinix * Tirili * Tomas Einarsson (Mesacer) * Tom van Leth (tovl) diff --git a/AUTHORS.AS b/AUTHORS.AS new file mode 100644 index 000000000000..2c1174a22f6e --- /dev/null +++ b/AUTHORS.AS @@ -0,0 +1,110 @@ +Attacque Supérior is a third-party logic +extension based on OpenRA. + +The plugin is maintained by + * Gyula Zimmermann (Graion Dilach) + + Authors of logics: + * Gyula Zimmermann (Graion Dilach) + > everything unless listed + + * Andre Mohren (IceReaper) + > KKND laser implementation + + * Boolbada/forcecore + > RadBeam implementation + > initial AIDeployHelper + > GrantStackableConditionOnFire aka Gattling + > ArcRenderable + > Nydus tunnel logic + > mind control traits + > Berserkable + > TintedCells (former radioactivity) + > PointDefense logic + > Prism Forwarding logic + > Slave Miner logic + + * CastleJing + > Bugfixes on mind controller logic + + * CombinE88 + > BackFireShrapnelWarhead + + * Darkademic + > Production Tooltip armor type indicator + > Shielded logic + + * darkscrypt (Devon/wolfbyte) + > Major rewrite on garrison code + > Smart deploy logic + + * DnAp + > Initial garrison logic + + * Dnqbob + > All AI squad logic fix + > PowerDownBotModule + > CncEngineerBotModule + > PowerDownBotModule + > LoadCargoBotModule + > LoadGarrisonerBotModule + > SharedCargoBotModule + > SendUnitToAttackBotModule + > WeaponTriggerCells + > TriggerLayerWeaponWarhead + > IgnoreHeightDamageWarhead + > MissileTA fix up + + * DoDoCat + > Slave Miner maintaince + + * EoralMilk + > Initial MissileTA + + * Holloweye + > SmokeParticle(Emitter) basic implementation + + * lucasss + > (Gives)ProximityBounty basic implementation + + * Matthias Mailänder (Mailaender) + > WithExitOverlay + + * Martin Lang (Gerseras/Seraphinexxx) + > DelayedWeapon system + + * Mustafa Alperen Seki (MustaphaTR) + > mindcontrol traits cleanup + > /taunt command + > Intelligence logic + > Infect logic + > ClearsResources + > GrantTimedCondition + > GrantConditionAfterDelay + > Garrison logic maintainance + > OpenToppedDamageWarhead + > Ranged GPS + > CPs and Upgrades Observer Tab + > Selected Actors Widgets + > Contition Prerequisite + > Instant Cash Drain + + * Sean Hunt (coppro) + > Chrono Miner implementation (ChronoResourceDelivery) + + * jrb0001 + > AmmoPool support for ExplodeWeapon + + * Wojciech Walaszek (Voidwalker) + > TeleportNetwork maintaince + > WarheadTrailProjectile + > TintedCells maintaince + + Authors of documents used in this plugin: + * Apollo + > additional blending modes + +The project owes thanks for OpenRA for it's modularity +which allows this project to exist in the first place. + +Also thanks for you, the user for using this project. diff --git a/Directory.Build.props b/Directory.Build.props index b6d5db7d22f1..68744418df76 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,7 +33,7 @@ false true - true @@ -51,8 +51,10 @@ - + + + diff --git a/GeoLite2-Country.mmdb.gz b/GeoLite2-Country.mmdb.gz new file mode 100644 index 000000000000..f76b1cabefea Binary files /dev/null and b/GeoLite2-Country.mmdb.gz differ diff --git a/Makefile b/Makefile index acfa25f4338a..6c9d277a695d 100644 --- a/Makefile +++ b/Makefile @@ -153,6 +153,9 @@ tests: ############# LOCAL INSTALLATION AND DOWNSTREAM PACKAGING ############## # +setupasfolder: + @mkdir -p ./mods/as + version: VERSION mods/ra/mod.yaml mods/cnc/mod.yaml mods/d2k/mod.yaml mods/ts/mod.yaml mods/modcontent/mod.yaml mods/all/mod.yaml ifeq ($(VERSION),) $(error Unable to determine new version (requires git or override of variable VERSION)) @@ -161,7 +164,7 @@ endif @sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/ra/mod.yaml mods/cnc/mod.yaml mods/d2k/mod.yaml mods/ts/mod.yaml mods/modcontent/mod.yaml mods/all/mod.yaml' install: - @sh -c '. ./packaging/functions.sh; install_assemblies $(CWD) $(DESTDIR)$(gameinstalldir) $(TARGETPLATFORM) $(RUNTIME) True True True' + @sh -c '. ./packaging/functions.sh; install_assemblies $(CWD) $(DESTDIR)$(gameinstalldir) $(TARGETPLATFORM) $(RUNTIME) True True True True' @sh -c '. ./packaging/functions.sh; install_data $(CWD) $(DESTDIR)$(gameinstalldir) cnc d2k ra' install-linux-shortcuts: diff --git a/OpenRA.Game/Activities/Activity.cs b/OpenRA.Game/Activities/Activity.cs index b4a31d33b42f..47eccd8e27aa 100644 --- a/OpenRA.Game/Activities/Activity.cs +++ b/OpenRA.Game/Activities/Activity.cs @@ -19,6 +19,7 @@ namespace OpenRA.Activities { public enum ActivityState { Queued, Active, Canceling, Done } + public enum ActivityType { Undefined, Move, Attack, Ability } // Used for AI module micro-manage and judge squad combat condition public class TargetLineNode { @@ -48,10 +49,11 @@ public TargetLineNode(in Target target, Color color, Sprite tile = null) */ public abstract class Activity : IActivityInterface { + public ActivityType ActivityType { get; protected set; } public ActivityState State { get; private set; } Activity childActivity; - protected Activity ChildActivity + public Activity ChildActivity { get => SkipDoneActivities(childActivity); private set => childActivity = value; @@ -88,6 +90,7 @@ internal static Activity SkipDoneActivities(Activity first) protected Activity() { + ActivityType = ActivityType.Undefined; IsInterruptible = true; ChildHasPriority = true; } @@ -146,18 +149,22 @@ protected bool TickChild(Actor self) } /// + /// /// Called every tick to run activity logic. Returns false if the activity should /// remain active, or true if it is complete. Cancelled activities must ensure they /// return the actor to a consistent state before returning true. - /// + /// + /// /// Child activities can be queued using QueueChild, and these will be ticked /// instead of the parent while they are active. Activities that need to run logic /// in parallel with child activities should set ChildHasPriority to false and /// manually call TickChildren. - /// + /// + /// /// Queuing one or more child activities and returning true is valid, and causes /// the activity to be completed immediately (without ticking again) once the /// children have completed. + /// /// public virtual bool Tick(Actor self) { @@ -222,10 +229,11 @@ public void QueueChild(Activity activity) } /// - /// Prints the activity tree, starting from the top or optionally from a given origin. - /// + /// Prints the activity tree, starting from the top or optionally from a given origin. + /// /// Call this method from any place that's called during a tick, such as the Tick() method itself or /// the Before(First|Last)Run() methods. The origin activity will be marked in the output. + /// /// /// The actor performing this activity. /// Activity from which to start traversing, and which to mark. If null, mark the calling activity, and start traversal from the top. diff --git a/OpenRA.Game/Activities/CallFunc.cs b/OpenRA.Game/Activities/CallFunc.cs index a30cefa2e71a..922e5be8b8a4 100644 --- a/OpenRA.Game/Activities/CallFunc.cs +++ b/OpenRA.Game/Activities/CallFunc.cs @@ -18,6 +18,7 @@ public class CallFunc : Activity public CallFunc(Action a) { this.a = a; } public CallFunc(Action a, bool interruptible) { + ActivityType = ActivityType.Undefined; this.a = a; IsInterruptible = interruptible; } diff --git a/OpenRA.Game/FieldLoader.cs b/OpenRA.Game/FieldLoader.cs index 60cf702b186a..c8f860202794 100644 --- a/OpenRA.Game/FieldLoader.cs +++ b/OpenRA.Game/FieldLoader.cs @@ -531,7 +531,7 @@ static object ParseNullable(string fieldName, Type fieldType, string value, Mini if (string.IsNullOrEmpty(value)) return null; - var innerType = fieldType.GetGenericArguments().First(); + var innerType = fieldType.GetGenericArguments()[0]; var innerValue = GetValue("Nullable", innerType, value, field); return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue }); } diff --git a/OpenRA.Game/FieldSaver.cs b/OpenRA.Game/FieldSaver.cs index e6d0e4d39796..b75df1415501 100644 --- a/OpenRA.Game/FieldSaver.cs +++ b/OpenRA.Game/FieldSaver.cs @@ -15,6 +15,7 @@ using System.Globalization; using System.Linq; using System.Reflection; +using System.Text; using OpenRA.Primitives; namespace OpenRA @@ -84,7 +85,7 @@ public static string FormatValue(object v) // This is only for documentation generation if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { - var result = ""; + var result = new StringBuilder(); var dict = (System.Collections.IDictionary)v; foreach (var kvp in dict) { @@ -94,10 +95,10 @@ public static string FormatValue(object v) var formattedKey = FormatValue(key); var formattedValue = FormatValue(value); - result += $"{formattedKey}: {formattedValue}{Environment.NewLine}"; + result.Append($"{formattedKey}: {formattedValue}{Environment.NewLine}"); } - return result; + return result.ToString(); } if (v is DateTime d) diff --git a/OpenRA.Game/GameRules/WeaponInfo.cs b/OpenRA.Game/GameRules/WeaponInfo.cs index bac06ff470dd..6ed012499b1e 100644 --- a/OpenRA.Game/GameRules/WeaponInfo.cs +++ b/OpenRA.Game/GameRules/WeaponInfo.cs @@ -90,6 +90,12 @@ public sealed class WeaponInfo [Desc("The sound played when the weapon is reloaded.")] public readonly string[] AfterFireSound = null; + [Desc("Do the sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the report sounds played at.")] + public readonly float SoundVolume = 1f; + [Desc("Delay in ticks to play reloading sound.")] public readonly int AfterFireSoundDelay = 0; @@ -102,6 +108,9 @@ public sealed class WeaponInfo [Desc("Can this weapon target the attacker itself?")] public readonly bool CanTargetSelf = false; + [Desc("What player relationships are affected.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Ally | PlayerRelationship.Neutral | PlayerRelationship.Enemy; + [Desc("What types of targets are affected.")] public readonly BitSet ValidTargets = new("Ground", "Water"); @@ -213,8 +222,11 @@ public bool IsValidAgainst(Actor victim, Actor firedBy) if (!CanTargetSelf && victim == firedBy) return false; - var targetTypes = victim.GetEnabledTargetTypes(); + var relationship = firedBy.Owner.RelationshipWith(victim.Owner); + if (!ValidRelationships.HasRelationship(relationship)) + return false; + var targetTypes = victim.GetEnabledTargetTypes(); return IsValidTarget(targetTypes); } @@ -227,6 +239,10 @@ public bool IsValidAgainst(FrozenActor victim, Actor firedBy) if (!CanTargetSelf && victim.Actor == firedBy) return false; + var relationship = firedBy.Owner.RelationshipWith(victim.Owner); + if (!ValidRelationships.HasRelationship(relationship)) + return false; + return IsValidTarget(victim.TargetTypes); } diff --git a/OpenRA.Game/Graphics/ModelVertex.cs b/OpenRA.Game/Graphics/ModelVertex.cs index b4f591d18485..42ffbf6e6d80 100644 --- a/OpenRA.Game/Graphics/ModelVertex.cs +++ b/OpenRA.Game/Graphics/ModelVertex.cs @@ -45,9 +45,9 @@ public ModelShaderBindings() public override ShaderVertexAttribute[] Attributes { get; } = new[] { - new ShaderVertexAttribute("aVertexPosition", 3, 0), - new ShaderVertexAttribute("aVertexTexCoord", 4, 12), - new ShaderVertexAttribute("aVertexTexMetadata", 2, 28), + new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0), + new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12), + new ShaderVertexAttribute("aVertexTexMetadata", ShaderVertexAttributeType.Float, 2, 28), }; } } diff --git a/OpenRA.Game/Graphics/PaletteReference.cs b/OpenRA.Game/Graphics/PaletteReference.cs index 00e196e40457..149687d1d713 100644 --- a/OpenRA.Game/Graphics/PaletteReference.cs +++ b/OpenRA.Game/Graphics/PaletteReference.cs @@ -13,19 +13,17 @@ namespace OpenRA.Graphics { public sealed class PaletteReference { - readonly float index; readonly HardwarePalette hardwarePalette; public readonly string Name; public IPalette Palette { get; internal set; } - public float TextureIndex => index / hardwarePalette.Height; - public float TextureMidIndex => (index + 0.5f) / hardwarePalette.Height; + public int TextureIndex { get; } public PaletteReference(string name, int index, IPalette palette, HardwarePalette hardwarePalette) { Name = name; Palette = palette; - this.index = index; + TextureIndex = index; this.hardwarePalette = hardwarePalette; } diff --git a/OpenRA.Game/Graphics/PlatformInterfaces.cs b/OpenRA.Game/Graphics/PlatformInterfaces.cs index 76c900123185..f11fa66fe631 100644 --- a/OpenRA.Game/Graphics/PlatformInterfaces.cs +++ b/OpenRA.Game/Graphics/PlatformInterfaces.cs @@ -20,13 +20,12 @@ public enum GLProfile Automatic, ANGLE, Modern, - Embedded, - Legacy + Embedded } public interface IPlatform { - IPlatformWindow CreateWindow(Size size, WindowMode windowMode, float scaleModifier, int vertexBatchSize, int indexBatchSize, int videoDisplay, GLProfile profile, bool enableLegacyGL); + IPlatformWindow CreateWindow(Size size, WindowMode windowMode, float scaleModifier, int vertexBatchSize, int indexBatchSize, int videoDisplay, GLProfile profile); ISoundEngine CreateSound(string device); IFont CreateFont(byte[] data); } @@ -108,7 +107,7 @@ public interface IRenderer { void BeginFrame(); void EndFrame(); - void SetPalette(ITexture palette); + void SetPalette(HardwarePalette palette); } public interface IVertexBuffer : IDisposable where T : struct @@ -157,6 +156,7 @@ public interface ITexture : IDisposable { void SetData(byte[] colors, int width, int height); void SetFloatData(float[] data, int width, int height); + void SetDataFromReadBuffer(Rectangle rect); byte[] GetData(); Size Size { get; } TextureScaleFilter ScaleFilter { get; set; } diff --git a/OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs b/OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs new file mode 100644 index 000000000000..0bb501e49a8d --- /dev/null +++ b/OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs @@ -0,0 +1,64 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Runtime.InteropServices; + +namespace OpenRA.Graphics +{ + [StructLayout(LayoutKind.Sequential)] + public readonly struct RenderPostProcessPassVertex + { + public readonly float X, Y; + + public RenderPostProcessPassVertex(float x, float y) + { + X = x; Y = y; + } + } + + [StructLayout(LayoutKind.Sequential)] + public readonly struct RenderPostProcessPassTexturedVertex + { + // 3d position + public readonly float X, Y; + public readonly float S, T; + + public RenderPostProcessPassTexturedVertex(float x, float y, float s, float t) + { + X = x; Y = y; + S = s; T = t; + } + } + + public sealed class RenderPostProcessPassShaderBindings : ShaderBindings + { + public RenderPostProcessPassShaderBindings(string name) + : base("postprocess", "postprocess_" + name) { } + + public override ShaderVertexAttribute[] Attributes { get; } = new[] + { + new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0) + }; + } + + public sealed class RenderPostProcessPassTexturedShaderBindings : ShaderBindings + { + public RenderPostProcessPassTexturedShaderBindings(string name) + : base("postprocess_textured", "postprocess_textured_" + name) + { } + + public override ShaderVertexAttribute[] Attributes { get; } = new[] + { + new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0), + new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 2, 8), + }; + } +} diff --git a/OpenRA.Game/Graphics/RgbaColorRenderer.cs b/OpenRA.Game/Graphics/RgbaColorRenderer.cs index 6927b39f0f0f..d9a4436cb6d2 100644 --- a/OpenRA.Game/Graphics/RgbaColorRenderer.cs +++ b/OpenRA.Game/Graphics/RgbaColorRenderer.cs @@ -45,10 +45,10 @@ public void DrawLine(in float3 start, in float3 end, float width, Color startCol var eb = endColor.B / 255.0f; var ea = endColor.A / 255.0f; - vertices[0] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0, 0); - vertices[1] = new Vertex(start + corner + Offset, sr, sg, sb, sa, 0, 0); - vertices[2] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0, 0); - vertices[3] = new Vertex(end - corner + Offset, er, eg, eb, ea, 0, 0); + vertices[0] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0); + vertices[1] = new Vertex(start + corner + Offset, sr, sg, sb, sa, 0); + vertices[2] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0); + vertices[3] = new Vertex(end - corner + Offset, er, eg, eb, ea, 0); parent.DrawRGBAQuad(vertices, blendMode); } @@ -64,10 +64,10 @@ public void DrawLine(in float3 start, in float3 end, float width, Color color, B var b = color.B / 255.0f; var a = color.A / 255.0f; - vertices[0] = new Vertex(start - corner + Offset, r, g, b, a, 0, 0); - vertices[1] = new Vertex(start + corner + Offset, r, g, b, a, 0, 0); - vertices[2] = new Vertex(end + corner + Offset, r, g, b, a, 0, 0); - vertices[3] = new Vertex(end - corner + Offset, r, g, b, a, 0, 0); + vertices[0] = new Vertex(start - corner + Offset, r, g, b, a, 0); + vertices[1] = new Vertex(start + corner + Offset, r, g, b, a, 0); + vertices[2] = new Vertex(end + corner + Offset, r, g, b, a, 0); + vertices[3] = new Vertex(end - corner + Offset, r, g, b, a, 0); parent.DrawRGBAQuad(vertices, blendMode); } @@ -153,10 +153,10 @@ void DrawConnectedLine(float3[] points, float width, Color color, bool closed, B var cd = closed || i < limit - 1 ? IntersectionOf(end - corner, dir, end - nextCorner, nextDir) : end - corner; // Fill segment - vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0, 0); - vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0, 0); - vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0, 0); - vertices[3] = new Vertex(cd + Offset, r, g, b, a, 0, 0); + vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0); + vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0); + vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0); + vertices[3] = new Vertex(cd + Offset, r, g, b, a, 0); parent.DrawRGBAQuad(vertices, blendMode); // Advance line segment @@ -209,10 +209,10 @@ public void FillRect(in float3 a, in float3 b, in float3 c, in float3 d, Color c var cb = color.B / 255.0f; var ca = color.A / 255.0f; - vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0); - vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0, 0); - vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0); - vertices[3] = new Vertex(d + Offset, cr, cg, cb, ca, 0, 0); + vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0); + vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0); + vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0); + vertices[3] = new Vertex(d + Offset, cr, cg, cb, ca, 0); parent.DrawRGBAQuad(vertices, blendMode); } @@ -234,7 +234,7 @@ static Vertex VertexWithColor(in float3 xyz, Color color) var cb = color.B / 255.0f; var ca = color.A / 255.0f; - return new Vertex(xyz, cr, cg, cb, ca, 0, 0); + return new Vertex(xyz, cr, cg, cb, ca, 0); } public void FillEllipse(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha) diff --git a/OpenRA.Game/Graphics/ShaderBindings.cs b/OpenRA.Game/Graphics/ShaderBindings.cs index 71adb20666f1..6e11b3a63d7c 100644 --- a/OpenRA.Game/Graphics/ShaderBindings.cs +++ b/OpenRA.Game/Graphics/ShaderBindings.cs @@ -14,15 +14,26 @@ namespace OpenRA.Graphics { + public enum ShaderVertexAttributeType + { + // Assign the underlying OpenGL type values + // to simplify enum use in the shader + Float = 0x1406, // GL_FLOAT + Int = 0x1404, // GL_INT + UInt = 0x1405 // GL_UNSIGNED_INT + } + public readonly struct ShaderVertexAttribute { public readonly string Name; + public readonly ShaderVertexAttributeType Type; public readonly int Components; public readonly int Offset; - public ShaderVertexAttribute(string name, int components, int offset) + public ShaderVertexAttribute(string name, ShaderVertexAttributeType type, int components, int offset) { Name = name; + Type = type; Components = components; Offset = offset; } @@ -39,11 +50,14 @@ public abstract class ShaderBindings : IShaderBindings public abstract ShaderVertexAttribute[] Attributes { get; } protected ShaderBindings(string name) + : this(name, name) { } + + protected ShaderBindings(string vertexName, string fragmentName) { Stride = Attributes.Sum(a => a.Components * 4); - VertexShaderName = name; + VertexShaderName = vertexName; VertexShaderCode = GetShaderCode(VertexShaderName + ".vert"); - FragmentShaderName = name; + FragmentShaderName = fragmentName; FragmentShaderCode = GetShaderCode(FragmentShaderName + ".frag"); } diff --git a/OpenRA.Game/Graphics/SheetBuilder.cs b/OpenRA.Game/Graphics/SheetBuilder.cs index c96bddaf0f20..f1389aefb8e5 100644 --- a/OpenRA.Game/Graphics/SheetBuilder.cs +++ b/OpenRA.Game/Graphics/SheetBuilder.cs @@ -82,16 +82,16 @@ public SheetBuilder(SheetType t, Func allocateSheet, int margin = 1) this.margin = margin; } - public Sprite Add(ISpriteFrame frame) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset); } - public Sprite Add(byte[] src, SpriteFrameType type, Size size) { return Add(src, type, size, 0, float3.Zero); } - public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset) + public Sprite Add(ISpriteFrame frame, bool premultiplied = false) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset, premultiplied); } + public Sprite Add(byte[] src, SpriteFrameType type, Size size, bool premultiplied = false) { return Add(src, type, size, 0, float3.Zero, premultiplied); } + public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset, bool premultiplied = false) { // Don't bother allocating empty sprites if (size.Width == 0 || size.Height == 0) return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha); var rect = Allocate(size, zRamp, spriteOffset); - Util.FastCopyIntoChannel(rect, src, type); + Util.FastCopyIntoChannel(rect, src, type, premultiplied); Current.CommitBufferedData(); return rect; } diff --git a/OpenRA.Game/Graphics/SpriteCache.cs b/OpenRA.Game/Graphics/SpriteCache.cs index c981b5e342c3..e43d375446de 100644 --- a/OpenRA.Game/Graphics/SpriteCache.cs +++ b/OpenRA.Game/Graphics/SpriteCache.cs @@ -24,7 +24,7 @@ public sealed class SpriteCache : IDisposable readonly ISpriteLoader[] loaders; readonly IReadOnlyFileSystem fileSystem; - readonly Dictionary spriteReservations = new(); + readonly Dictionary spriteReservations = new(); readonly Dictionary frameReservations = new(); readonly Dictionary> reservationsByFilename = new(); @@ -47,10 +47,10 @@ public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int this.loaders = loaders; } - public int ReserveSprites(string filename, IEnumerable frames, MiniYamlNode.SourceLocation location) + public int ReserveSprites(string filename, IEnumerable frames, MiniYamlNode.SourceLocation location, bool premultiplied = false) { var token = nextReservationToken++; - spriteReservations[token] = (frames?.ToArray(), location); + spriteReservations[token] = (frames?.ToArray(), location, premultiplied); reservationsByFilename.GetOrAdd(filename, _ => new List()).Add(token); return token; } @@ -91,14 +91,14 @@ public void LoadReservations(ModData modData) var loadedFrames = GetFrames(fileSystem, filename, loaders, out _); foreach (var token in tokens) { - if (frameReservations.TryGetValue(token, out var r)) + if (frameReservations.TryGetValue(token, out var rf)) { if (loadedFrames != null) { - if (r.Frames != null) + if (rf.Frames != null) { var resolved = new ISpriteFrame[loadedFrames.Length]; - foreach (var i in r.Frames) + foreach (var i in rf.Frames) resolved[i] = loadedFrames[i]; resolvedFrames[token] = resolved; } @@ -108,26 +108,32 @@ public void LoadReservations(ModData modData) else { resolvedFrames[token] = null; - missingFiles[token] = (filename, r.Location); + missingFiles[token] = (filename, rf.Location); } } - if (spriteReservations.TryGetValue(token, out r)) + if (spriteReservations.TryGetValue(token, out var rs)) { if (loadedFrames != null) { var resolved = new Sprite[loadedFrames.Length]; - var frames = r.Frames ?? Enumerable.Range(0, loadedFrames.Length); + var frames = rs.Frames ?? Enumerable.Range(0, loadedFrames.Length); + + // Premultiplied and non-premultiplied sprites must be cached separately + // to cover the case where the same image is requested in both versions. + // The premultiplied sprites are stored with an index offset for efficiency + // rather than allocating a second dictionary. + var di = rs.Premultiplied ? loadedFrames.Length : 0; foreach (var i in frames) - resolved[i] = spriteCache.GetOrAdd(i, - f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f].Type)].Add(loadedFrames[f])); + resolved[i] = spriteCache.GetOrAdd(i + di, + f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f - di].Type)].Add(loadedFrames[f - di], rs.Premultiplied)); resolvedSprites[token] = resolved; } else { resolvedSprites[token] = null; - missingFiles[token] = (filename, r.Location); + missingFiles[token] = (filename, rs.Location); } } } diff --git a/OpenRA.Game/Graphics/SpriteRenderer.cs b/OpenRA.Game/Graphics/SpriteRenderer.cs index 9ac831ba1119..3b07b3563937 100644 --- a/OpenRA.Game/Graphics/SpriteRenderer.cs +++ b/OpenRA.Game/Graphics/SpriteRenderer.cs @@ -115,7 +115,7 @@ int2 SetRenderStateForSprite(Sprite s) return new int2(sheetIndex, secondarySheetIndex); } - static float ResolveTextureIndex(Sprite s, PaletteReference pal) + static int ResolveTextureIndex(Sprite s, PaletteReference pal) { if (pal == null) return 0; @@ -129,7 +129,7 @@ static float ResolveTextureIndex(Sprite s, PaletteReference pal) return pal.TextureIndex; } - internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, in float3 scale, float rotation = 0f) + internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, in float3 scale, float rotation = 0f) { var samplers = SetRenderStateForSprite(s); Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, vertexCount, scale * s.Size, float3.Ones, @@ -137,7 +137,7 @@ internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location vertexCount += 4; } - internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale, float rotation = 0f) + internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, float scale, float rotation = 0f) { var samplers = SetRenderStateForSprite(s); Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, vertexCount, scale * s.Size, float3.Ones, @@ -150,7 +150,7 @@ public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, rotation); } - internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale, in float3 tint, float alpha, + internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, float scale, in float3 tint, float alpha, float rotation = 0f) { var samplers = SetRenderStateForSprite(s); @@ -165,7 +165,7 @@ public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, tint, alpha, rotation); } - internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 a, in float3 b, in float3 c, in float3 d, in float3 tint, float alpha) + internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 a, in float3 b, in float3 c, in float3 d, in float3 tint, float alpha) { var samplers = SetRenderStateForSprite(s); Util.FastCreateQuad(vertices, a, b, c, d, s, samplers, paletteTextureIndex, tint, alpha, vertexCount); @@ -210,10 +210,11 @@ internal void DrawRGBAQuad(Vertex[] v, BlendMode blendMode) vertexCount += 4; } - public void SetPalette(ITexture palette, ITexture colorShifts) + public void SetPalette(HardwarePalette palette) { - shader.SetTexture("Palette", palette); - shader.SetTexture("ColorShifts", colorShifts); + shader.SetTexture("Palette", palette.Texture); + shader.SetTexture("ColorShifts", palette.ColorShifts); + shader.SetVec("PaletteRows", palette.Height); } public void SetViewportParams(Size sheetSize, int downscale, float depthMargin, int2 scroll) diff --git a/OpenRA.Game/Graphics/TerrainSpriteLayer.cs b/OpenRA.Game/Graphics/TerrainSpriteLayer.cs index 28c8e46a7654..58913cd21bcb 100644 --- a/OpenRA.Game/Graphics/TerrainSpriteLayer.cs +++ b/OpenRA.Game/Graphics/TerrainSpriteLayer.cs @@ -76,7 +76,8 @@ void UpdatePaletteIndices() { var v = vertices[i]; var p = palettes[i / 4]?.TextureIndex ?? 0; - vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, p, v.C, v.R, v.G, v.B, v.A); + var c = (uint)((p & 0xFFFF) << 16) | (v.C & 0xFFFF); + vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, c, v.R, v.G, v.B, v.A); } for (var row = 0; row < map.MapSize.Y; row++) @@ -113,7 +114,7 @@ void UpdateTint(MPos uv) for (var i = 0; i < 4; i++) { var v = vertices[offset + i]; - vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.P, v.C, v.A * float3.Ones, v.A); + vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.C, v.A * float3.Ones, v.A); } return; @@ -138,7 +139,7 @@ void UpdateTint(MPos uv) for (var i = 0; i < 4; i++) { var v = vertices[offset + i]; - vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.P, v.C, v.A * weights[i], v.A); + vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.C, v.A * weights[i], v.A); } dirtyRows.Add(uv.V); diff --git a/OpenRA.Game/Graphics/Util.cs b/OpenRA.Game/Graphics/Util.cs index e72d9e9c375b..4df6d26c7c5e 100644 --- a/OpenRA.Game/Graphics/Util.cs +++ b/OpenRA.Game/Graphics/Util.cs @@ -30,7 +30,7 @@ public static uint[] CreateQuadIndices(int quads) return indices; } - public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, float paletteTextureIndex, int nv, + public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, int paletteTextureIndex, int nv, in float3 size, in float3 tint, float alpha, float rotation = 0f) { float3 a, b, c, d; @@ -72,7 +72,7 @@ public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 public static void FastCreateQuad(Vertex[] vertices, in float3 a, in float3 b, in float3 c, in float3 d, - Sprite r, int2 samplers, float paletteTextureIndex, + Sprite r, int2 samplers, int paletteTextureIndex, in float3 tint, float alpha, int nv) { float sl = 0; @@ -94,14 +94,16 @@ public static void FastCreateQuad(Vertex[] vertices, attribC |= samplers.Y << 9; } - var fAttribC = (float)attribC; - vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, paletteTextureIndex, fAttribC, tint, alpha); - vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, paletteTextureIndex, fAttribC, tint, alpha); - vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, paletteTextureIndex, fAttribC, tint, alpha); - vertices[nv + 3] = new Vertex(d, r.Left, r.Bottom, sl, sb, paletteTextureIndex, fAttribC, tint, alpha); + attribC |= (paletteTextureIndex & 0xFFFF) << 16; + + var uAttribC = (uint)attribC; + vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, uAttribC, tint, alpha); + vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, uAttribC, tint, alpha); + vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, uAttribC, tint, alpha); + vertices[nv + 3] = new Vertex(d, r.Left, r.Bottom, sl, sb, uAttribC, tint, alpha); } - public static void FastCopyIntoChannel(Sprite dest, byte[] src, SpriteFrameType srcType) + public static void FastCopyIntoChannel(Sprite dest, byte[] src, SpriteFrameType srcType, bool premultiplied = false) { var destData = dest.Sheet.GetData(); var width = dest.Bounds.Width; @@ -152,7 +154,10 @@ public static void FastCopyIntoChannel(Sprite dest, byte[] src, SpriteFrameType } var cc = Color.FromArgb(a, r, g, b); - data[(y + j) * destStride + x + i] = PremultiplyAlpha(cc).ToArgb(); + if (premultiplied) + data[(y + j) * destStride + x + i] = cc.ToArgb(); + else + data[(y + j) * destStride + x + i] = PremultiplyAlpha(cc).ToArgb(); } } } diff --git a/OpenRA.Game/Graphics/Vertex.cs b/OpenRA.Game/Graphics/Vertex.cs index c84163e34346..c3b90d1cd43a 100644 --- a/OpenRA.Game/Graphics/Vertex.cs +++ b/OpenRA.Game/Graphics/Vertex.cs @@ -23,26 +23,26 @@ public readonly struct Vertex public readonly float S, T, U, V; // Palette and channel flags - public readonly float P, C; + public readonly uint C; // Color tint public readonly float R, G, B, A; - public Vertex(in float3 xyz, float s, float t, float u, float v, float p, float c) - : this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, p, c, float3.Ones, 1f) { } + public Vertex(in float3 xyz, float s, float t, float u, float v, uint c) + : this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, c, float3.Ones, 1f) { } - public Vertex(in float3 xyz, float s, float t, float u, float v, float p, float c, in float3 tint, float a) - : this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, p, c, tint.X, tint.Y, tint.Z, a) { } + public Vertex(in float3 xyz, float s, float t, float u, float v, uint c, in float3 tint, float a) + : this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, c, tint.X, tint.Y, tint.Z, a) { } - public Vertex(float x, float y, float z, float s, float t, float u, float v, float p, float c, in float3 tint, float a) - : this(x, y, z, s, t, u, v, p, c, tint.X, tint.Y, tint.Z, a) { } + public Vertex(float x, float y, float z, float s, float t, float u, float v, uint c, in float3 tint, float a) + : this(x, y, z, s, t, u, v, c, tint.X, tint.Y, tint.Z, a) { } - public Vertex(float x, float y, float z, float s, float t, float u, float v, float p, float c, float r, float g, float b, float a) + public Vertex(float x, float y, float z, float s, float t, float u, float v, uint c, float r, float g, float b, float a) { X = x; Y = y; Z = z; S = s; T = t; U = u; V = v; - P = p; C = c; + C = c; R = r; G = g; B = b; A = a; } } @@ -55,10 +55,10 @@ public CombinedShaderBindings() public override ShaderVertexAttribute[] Attributes { get; } = new[] { - new ShaderVertexAttribute("aVertexPosition", 3, 0), - new ShaderVertexAttribute("aVertexTexCoord", 4, 12), - new ShaderVertexAttribute("aVertexTexMetadata", 2, 28), - new ShaderVertexAttribute("aVertexTint", 4, 36) + new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0), + new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12), + new ShaderVertexAttribute("aVertexAttributes", ShaderVertexAttributeType.UInt, 1, 28), + new ShaderVertexAttribute("aVertexTint", ShaderVertexAttributeType.Float, 4, 32) }; } } diff --git a/OpenRA.Game/Graphics/WorldRenderer.cs b/OpenRA.Game/Graphics/WorldRenderer.cs index d61352b2af6c..85b95ee9d554 100644 --- a/OpenRA.Game/Graphics/WorldRenderer.cs +++ b/OpenRA.Game/Graphics/WorldRenderer.cs @@ -45,6 +45,8 @@ public sealed class WorldRenderer : IDisposable readonly List renderablesBuffer = new(); readonly IRenderer[] renderers; + readonly IRenderPostProcessPass[] postProcessPasses; + readonly ITexture postProcessTexture; internal WorldRenderer(ModData modData, World world) { @@ -71,6 +73,10 @@ internal WorldRenderer(ModData modData, World world) terrainRenderer = world.WorldActor.TraitOrDefault(); debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault()); + + postProcessPasses = world.WorldActor.TraitsImplementing().ToArray(); + if (postProcessPasses.Length > 0) + postProcessTexture = Game.Renderer.Context.CreateTexture(); } public void BeginFrame() @@ -284,6 +290,8 @@ public void Draw() if (enableDepthBuffer) Game.Renderer.ClearDepthBuffer(); + ApplyPostProcessing(PostProcessPassType.AfterActors); + World.ApplyToActorsWithTrait((actor, trait) => { if (actor.IsInWorld && !actor.Disposed) @@ -293,6 +301,8 @@ public void Draw() if (enableDepthBuffer) Game.Renderer.ClearDepthBuffer(); + ApplyPostProcessing(PostProcessPassType.AfterWorld); + World.ApplyToActorsWithTrait((actor, trait) => trait.RenderShroud(this)); if (enableDepthBuffer) @@ -306,9 +316,28 @@ public void Draw() foreach (var r in g) r.Render(this); + ApplyPostProcessing(PostProcessPassType.AfterShroud); + Game.Renderer.Flush(); } + void ApplyPostProcessing(PostProcessPassType type) + { + var size = Game.Renderer.WorldFrameBufferSize; + var rect = new Rectangle(0, 0, size.Width, size.Height); + foreach (var pass in postProcessPasses) + { + if (pass.Type != type || !pass.Enabled) + continue; + + // Make a copy of the world texture to avoid reading and writing on the same buffer + Game.Renderer.Flush(); + postProcessTexture.SetDataFromReadBuffer(rect); + Game.Renderer.Flush(); + pass.Draw(this, postProcessTexture); + } + } + public void DrawAnnotations() { Game.Renderer.EnableAntialiasingFilter(); diff --git a/OpenRA.Game/Map/ActorInitializer.cs b/OpenRA.Game/Map/ActorInitializer.cs index cf6d7dc403f8..774aab40940f 100644 --- a/OpenRA.Game/Map/ActorInitializer.cs +++ b/OpenRA.Game/Map/ActorInitializer.cs @@ -175,8 +175,7 @@ public abstract class CompositeActorInit : ActorInit protected CompositeActorInit(TraitInfo info) : base(info.InstanceName) { } - protected CompositeActorInit() - : base() { } + protected CompositeActorInit() { } public virtual void Initialize(MiniYaml yaml) { diff --git a/OpenRA.Game/Map/Map.cs b/OpenRA.Game/Map/Map.cs index f506b2e73320..f6c0ba46c8bc 100644 --- a/OpenRA.Game/Map/Map.cs +++ b/OpenRA.Game/Map/Map.cs @@ -1353,13 +1353,18 @@ public IEnumerable FindTilesInAnnulus(CPos center, int minRange, int maxRa throw new ArgumentOutOfRangeException(nameof(maxRange), $"The requested range ({maxRange}) cannot exceed the value of MaximumTileSearchRange ({Grid.MaximumTileSearchRange})"); - for (var i = minRange; i <= maxRange; i++) + return FindTilesInAnnulus(); + + IEnumerable FindTilesInAnnulus() { - foreach (var offset in Grid.TilesByDistance[i]) + for (var i = minRange; i <= maxRange; i++) { - var t = offset + center; - if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t)) - yield return t; + foreach (var offset in Grid.TilesByDistance[i]) + { + var t = offset + center; + if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t)) + yield return t; + } } } } diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 40259d5cae9b..cdcb13da584a 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -10,7 +10,7 @@ - + diff --git a/OpenRA.Game/Primitives/BitSet.cs b/OpenRA.Game/Primitives/BitSet.cs index bd7d2ff77ea1..a12611aa6b4d 100644 --- a/OpenRA.Game/Primitives/BitSet.cs +++ b/OpenRA.Game/Primitives/BitSet.cs @@ -86,7 +86,7 @@ public BitSet(params string[] values) public static BitSet FromStringsNoAlloc(string[] values) { - return new BitSet(BitSetAllocator.GetBitsNoAlloc(values)) { }; + return new BitSet(BitSetAllocator.GetBitsNoAlloc(values)); } public override string ToString() diff --git a/OpenRA.Game/Primitives/LongBitSet.cs b/OpenRA.Game/Primitives/LongBitSet.cs index 8395c1bb40d8..2566b577a666 100644 --- a/OpenRA.Game/Primitives/LongBitSet.cs +++ b/OpenRA.Game/Primitives/LongBitSet.cs @@ -99,7 +99,7 @@ public LongBitSet(params string[] values) public static LongBitSet FromStringsNoAlloc(string[] values) { - return new LongBitSet(LongBitSetAllocator.GetBitsNoAlloc(values)) { }; + return new LongBitSet(LongBitSetAllocator.GetBitsNoAlloc(values)); } public static void Reset() diff --git a/OpenRA.Game/Renderer.cs b/OpenRA.Game/Renderer.cs index 877dc7b2057c..313cc21ac2d1 100644 --- a/OpenRA.Game/Renderer.cs +++ b/OpenRA.Game/Renderer.cs @@ -82,7 +82,7 @@ public Renderer(IPlatform platform, GraphicSettings graphicSettings) Window = platform.CreateWindow(new Size(resolution.Width, resolution.Height), graphicSettings.Mode, graphicSettings.UIScale, TempVertexBufferSize, TempIndexBufferSize, - graphicSettings.VideoDisplay, graphicSettings.GLProfile, !graphicSettings.DisableLegacyGL); + graphicSettings.VideoDisplay, graphicSettings.GLProfile); Context = Window.Context; @@ -303,11 +303,11 @@ public void SetPalette(HardwarePalette palette) Flush(); currentPaletteTexture = palette.Texture; - SpriteRenderer.SetPalette(currentPaletteTexture, palette.ColorShifts); - WorldSpriteRenderer.SetPalette(currentPaletteTexture, palette.ColorShifts); + SpriteRenderer.SetPalette(palette); + WorldSpriteRenderer.SetPalette(palette); foreach (var r in WorldRenderers) - r.SetPalette(currentPaletteTexture); + r.SetPalette(palette); } public void EndFrame(IInputHandler inputHandler) diff --git a/OpenRA.Game/Scripting/ScriptContext.cs b/OpenRA.Game/Scripting/ScriptContext.cs index 84c50f87461b..16de82dd5fe6 100644 --- a/OpenRA.Game/Scripting/ScriptContext.cs +++ b/OpenRA.Game/Scripting/ScriptContext.cs @@ -74,14 +74,17 @@ protected ScriptPlayerProperties(ScriptContext context, Player player) /// Provides global bindings in Lua code. /// /// + /// /// Instance methods and properties declared in derived classes will be made available in Lua. Use /// on your derived class to specify the name exposed in Lua. It is recommended /// to apply against each method or property to provide a description of what it does. - /// + /// + /// /// Any parameters to your method that are s will be disposed automatically when your method /// completes. If you need to return any of these values, or need them to live longer than your method, you must /// use to get your own copy of the value. Any copied values you return will /// be disposed automatically, but you assume responsibility for disposing any other copies. + /// /// public abstract class ScriptGlobal : ScriptObjectWrapper { diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index a1ad8092f6c0..5ca4d2ee07be 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -210,9 +210,6 @@ public class GraphicSettings [Desc("Disable operating-system provided cursor rendering.")] public bool DisableHardwareCursors = false; - [Desc("Disable legacy OpenGL 2.1 support.")] - public bool DisableLegacyGL = true; - [Desc("Display index to use in a multi-monitor fullscreen setup.")] public int VideoDisplay = 0; diff --git a/OpenRA.Game/Sound/Sound.cs b/OpenRA.Game/Sound/Sound.cs index f4ad227bffbd..9055b654596c 100644 --- a/OpenRA.Game/Sound/Sound.cs +++ b/OpenRA.Game/Sound/Sound.cs @@ -163,7 +163,9 @@ public void SetMusicLooped(bool loop) public ISound PlayToPlayer(SoundType type, Player player, string name) { return Play(type, player, name, true, WPos.Zero, 1f); } public ISound PlayToPlayer(SoundType type, Player player, string name, WPos pos) { return Play(type, player, name, false, pos, 1f); } public ISound PlayLooped(SoundType type, string name) { return Play(type, null, name, true, WPos.Zero, 1f, true); } + public ISound PlayLooped(SoundType type, string name, float volumeModifier) { return Play(type, null, name, true, WPos.Zero, volumeModifier, true); } public ISound PlayLooped(SoundType type, string name, WPos pos) { return Play(type, null, name, false, pos, 1f, true); } + public ISound PlayLooped(SoundType type, string name, WPos pos, float volumeModifier) { return Play(type, null, name, false, pos, volumeModifier, true); } public ISound Play(SoundType type, string[] names, World world, Player player = null, float volumeModifier = 1f) { diff --git a/OpenRA.Game/Support/LaunchArguments.cs b/OpenRA.Game/Support/LaunchArguments.cs index 19d01935afb9..a529a9818c11 100644 --- a/OpenRA.Game/Support/LaunchArguments.cs +++ b/OpenRA.Game/Support/LaunchArguments.cs @@ -37,8 +37,8 @@ public LaunchArguments(Arguments args) return; foreach (var f in GetType().GetFields()) - if (args.Contains("Launch" + "." + f.Name)) - FieldLoader.LoadField(this, f.Name, args.GetValue("Launch" + "." + f.Name, "")); + if (args.Contains("Launch." + f.Name)) + FieldLoader.LoadField(this, f.Name, args.GetValue("Launch." + f.Name, "")); } public ConnectionTarget GetConnectEndPoint() diff --git a/OpenRA.Game/Support/MersenneTwister.cs b/OpenRA.Game/Support/MersenneTwister.cs index e31bd0ad59f4..9c217a79a5a5 100644 --- a/OpenRA.Game/Support/MersenneTwister.cs +++ b/OpenRA.Game/Support/MersenneTwister.cs @@ -79,7 +79,7 @@ void Generate() var y = (mt[i] & 0x80000000) | (mt[(i + 1) % 624] & 0x7fffffff); mt[i] = mt[(i + 397u) % 624u] ^ (y >> 1); if ((y & 1) == 1) - mt[i] = mt[i] ^ 2567483615; + mt[i] ^= 2567483615; } } } diff --git a/OpenRA.Game/Support/VariableExpression.cs b/OpenRA.Game/Support/VariableExpression.cs index 275f8d720ad2..08fed5dc9890 100644 --- a/OpenRA.Game/Support/VariableExpression.cs +++ b/OpenRA.Game/Support/VariableExpression.cs @@ -676,7 +676,7 @@ static IEnumerable ToPostfix(IEnumerable tokens) else if (t.Closes != Grouping.None) { Token temp; - while (!((temp = s.Pop()).Opens != Grouping.None)) + while ((temp = s.Pop()).Opens == Grouping.None) yield return temp; } else if (t.OperandSides == Sides.None) diff --git a/OpenRA.Game/Sync.cs b/OpenRA.Game/Sync.cs index b93166fa4910..ae8db21c1ef7 100644 --- a/OpenRA.Game/Sync.cs +++ b/OpenRA.Game/Sync.cs @@ -151,8 +151,8 @@ public static int HashTarget(Target t) case TargetType.Terrain: return HashUsingHashCode(t.CenterPosition); - default: case TargetType.Invalid: + default: return 0; } } diff --git a/OpenRA.Game/Traits/Target.cs b/OpenRA.Game/Traits/Target.cs index 74741b0677da..c021bcf1e5d4 100644 --- a/OpenRA.Game/Traits/Target.cs +++ b/OpenRA.Game/Traits/Target.cs @@ -120,8 +120,8 @@ public bool IsValidFor(Actor targeter) return FrozenActor.IsValid && FrozenActor.Visible && !FrozenActor.Hidden; case TargetType.Invalid: return false; - default: case TargetType.Terrain: + default: return true; } } @@ -164,8 +164,8 @@ public WPos CenterPosition return FrozenActor.CenterPosition; case TargetType.Terrain: return terrainCenterPosition; - default: case TargetType.Invalid: + default: throw new InvalidOperationException("Attempting to query the position of an invalid Target"); } } @@ -186,8 +186,8 @@ public IEnumerable Positions return FrozenActor.TargetablePositions ?? NoPositions; case TargetType.Terrain: return terrainPositions; - default: case TargetType.Invalid: + default: return NoPositions; } } @@ -215,8 +215,8 @@ public override string ToString() case TargetType.Terrain: return terrainCenterPosition.ToString(); - default: case TargetType.Invalid: + default: return "Invalid"; } } @@ -239,8 +239,8 @@ public override string ToString() case TargetType.FrozenActor: return me.FrozenActor == other.FrozenActor; - default: case TargetType.Invalid: + default: return false; } } @@ -270,8 +270,8 @@ public override int GetHashCode() case TargetType.FrozenActor: return FrozenActor.GetHashCode(); - default: case TargetType.Invalid: + default: return 0; } } diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index 7bb928dea14d..6ce8d3842a30 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -459,6 +459,16 @@ public interface IRenderAnnotationsWhenSelected bool SpatiallyPartitionable { get; } } + public enum PostProcessPassType { AfterShroud, AfterWorld, AfterActors } + + [RequireExplicitImplementation] + public interface IRenderPostProcessPass + { + PostProcessPassType Type { get; } + bool Enabled { get; } + void Draw(WorldRenderer wr, ITexture worldTexture); + } + [Flags] public enum SelectionPriorityModifiers { @@ -573,7 +583,7 @@ public LobbyOption(MapPreview map, string id, string name, string description, b { Id = id; Name = map.GetLocalisedString(name); - Description = description != null ? map.GetLocalisedString(description) : null; + Description = description != null ? map.GetLocalisedString(description).Replace(@"\n", "\n") : null; IsVisible = visible; DisplayOrder = displayorder; Values = values.ToDictionary(v => v.Key, v => map.GetLocalisedString(v.Value)); diff --git a/OpenRA.Game/TranslationProvider.cs b/OpenRA.Game/TranslationProvider.cs index 3620d8432222..c8b7c387b357 100644 --- a/OpenRA.Game/TranslationProvider.cs +++ b/OpenRA.Game/TranslationProvider.cs @@ -35,7 +35,7 @@ public static void Initialize(ModData modData, IReadOnlyFileSystem fileSystem) public static string GetString(string key, IDictionary args = null) { lock (SyncObject) - { + { // By prioritizing mod-level translations we prevent maps from overwriting translation keys. We do not want to // allow maps to change the UI nor any other strings not exposed to the map. if (modTranslation.TryGetString(key, out var message, args)) @@ -51,7 +51,7 @@ public static string GetString(string key, IDictionary args = nu public static bool TryGetString(string key, out string message, IDictionary args = null) { lock (SyncObject) - { + { // By prioritizing mod-level translations we prevent maps from overwriting translation keys. We do not want to // allow maps to change the UI nor any other strings not exposed to the map. if (modTranslation.TryGetString(key, out message, args)) diff --git a/OpenRA.Game/UtilityCommands/ExtractChromeStrings.cs b/OpenRA.Game/UtilityCommands/ExtractChromeStrings.cs new file mode 100644 index 000000000000..cd51bdaccad6 --- /dev/null +++ b/OpenRA.Game/UtilityCommands/ExtractChromeStrings.cs @@ -0,0 +1,323 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using OpenRA.Widgets; + +namespace OpenRA.UtilityCommands +{ + sealed class ExtractChromeStringsCommand : IUtilityCommand + { + string IUtilityCommand.Name { get { return "--extract-chrome-strings"; } } + + bool IUtilityCommand.ValidateArguments(string[] args) + { + return true; + } + + [Desc("Extract translatable strings that are not yet localized and update chrome layout.")] + void IUtilityCommand.Run(Utility utility, string[] args) + { + // HACK: The engine code assumes that Game.modData is set. + var modData = Game.ModData = utility.ModData; + + var translatableFields = modData.ObjectCreator.GetTypes() + .Where(t => t.Name.EndsWith("Widget", StringComparison.InvariantCulture) && t.IsSubclassOf(typeof(Widget))) + .ToDictionary( + t => t.Name[..^6], + t => t.GetFields().Where(f => f.HasAttribute()).Select(f => f.Name).ToArray()) + .Where(t => t.Value.Length > 0) + .ToDictionary(t => t.Key, t => t.Value); + + var chromeLayouts = modData.Manifest.ChromeLayout.GroupBy(c => c.Split('/')[0].Split('|')[0], c => c); + + foreach (var layout in chromeLayouts) + { + var fluentFolder = layout.Key + "|languages"; + var fluentPackage = modData.ModFiles.OpenPackage(fluentFolder); + var fluentPath = Path.Combine(fluentPackage.Name, "chrome/en.ftl"); + + var unsortedCandidates = new List(); + var groupedCandidates = new Dictionary, List>(); + var chromeFiles = new List<(string Path, List Nodes)>(); + + // Get all translations. + foreach (var chrome in layout) + { + modData.ModFiles.TryGetPackageContaining(chrome, out var chromePackage, out var chromeName); + var chromePath = Path.Combine(chromePackage.Name, chromeName); + + var yaml = MiniYaml.FromFile(chromePath, false).Select(n => new MiniYamlNodeBuilder(n)).ToList(); + chromeFiles.Add((chromePath, yaml)); + + var translationCandidates = new List(); + foreach (var node in yaml) + { + if (node.Key != null) + { + var nodeSplit = node.Key.Split('@'); + var nodeId = nodeSplit.Length > 1 ? ClearContainersAndToLower(nodeSplit[1]) : null; + FromChromeLayout(node, translatableFields, nodeId, ref translationCandidates); + } + } + + if (translationCandidates.Count > 0) + { + var chromeFilename = chrome.Split('/').Last(); + groupedCandidates[new HashSet() { chromeFilename }] = new List(); + for (var i = 0; i < translationCandidates.Count; i++) + { + var candidate = translationCandidates[i]; + candidate.Chrome = chromeFilename; + unsortedCandidates.Add(candidate); + } + } + } + + // Join matching translations. + foreach (var candidate in unsortedCandidates) + { + HashSet foundHash = null; + TranslationCandidate found = default; + foreach (var (hash, translation) in groupedCandidates) + { + foreach (var c in translation) + { + if (c.Key == candidate.Key && c.Type == candidate.Type && c.Translation == candidate.Translation) + { + foundHash = hash; + found = c; + break; + } + } + + if (foundHash != null) + break; + } + + if (foundHash == null) + { + var hash = groupedCandidates.Keys.First(t => t.First() == candidate.Chrome); + groupedCandidates[hash].Add(candidate); + continue; + } + + var newHash = foundHash.Append(candidate.Chrome).ToHashSet(); + candidate.Nodes.AddRange(found.Nodes); + groupedCandidates[foundHash].Remove(found); + + var nHash = groupedCandidates.FirstOrDefault(t => t.Key.SetEquals(newHash)); + if (nHash.Key != null) + groupedCandidates[nHash.Key].Add(candidate); + else + groupedCandidates[newHash] = new List() { candidate }; + } + + // Write to translation and yaml files. + Directory.CreateDirectory(Path.GetDirectoryName(fluentPath)); + using (var fluentWriter = new StreamWriter(fluentPath, append: true)) + { + foreach (var (chromeFilename, candidates) in groupedCandidates.OrderBy(t => string.Join(',', t.Key))) + { + if (candidates.Count == 0) + continue; + + fluentWriter.WriteLine("## " + string.Join(", ", chromeFilename)); + + // Pushing blocks of translations to string first allows for fancier formatting. + var build = ""; + foreach (var grouping in candidates.GroupBy(t => t.Key)) + { + if (grouping.Count() == 1) + { + var candidate = grouping.First(); + var translationKey = candidate.Key; + if (candidate.Type == "text") + translationKey = $"{translationKey}"; + else + translationKey = $"{translationKey}-" + candidate.Type.Replace("text", ""); + + build += $"{translationKey} = {candidate.Translation}\n"; + foreach (var node in candidate.Nodes) + node.Value.Value = translationKey; + } + else + { + if (build.Length > 1 && build.Substring(build.Length - 2, 2) != "\n\n") + build += "\n"; + + var translationKey = grouping.Key; + build += $"{translationKey} =\n"; + foreach (var candidate in grouping) + { + var type = candidate.Type; + if (candidate.Type != "label") + { + if (candidate.Type == "text") + type = "label"; + else + type = type.Replace("text", ""); + } + + build += $" .{type} = {candidate.Translation}\n"; + foreach (var node in candidate.Nodes) + node.Value.Value = $"{translationKey}.{type}"; + } + + build += "\n"; + } + } + + fluentWriter.WriteLine(build.Trim('\n') + '\n'); + } + } + + foreach (var chromeFile in chromeFiles) + { + using (var chromeLayoutWriter = new StreamWriter(chromeFile.Path)) + chromeLayoutWriter.WriteLine(chromeFile.Nodes.WriteToString()); + } + } + } + + static bool IsAlreadyTranslated(string translation) + { + if (translation == translation.ToLowerInvariant() && translation.Any(c => c == '-')) + { + Console.WriteLine("Skipping " + translation + " because it is already translated."); + return true; + } + + return false; + } + + struct TranslationCandidate + { + public string Chrome; + public readonly string Key; + public readonly string Type; + public readonly string Translation; + public readonly List Nodes; + + public TranslationCandidate(string key, string type, string translation, MiniYamlNodeBuilder node) + { + Chrome = null; + Key = key; + Type = type; + Translation = translation; + Nodes = new List() { node }; + } + } + + static string ClearContainersAndToLower(string node) + { + return node + .Replace("Background", "") + .Replace("Container", "") + .Replace("Panel", "") + .ToLowerInvariant() + .Replace("headers", ""); + } + + static string ClearTypesAndToLower(string node) + { + return node + .Replace("LabelForInput", "Label") + .Replace("LabelWithHighlight", "Label") + .Replace("DropdownButton", "Dropdown") + .Replace("CheckboxButton", "Checkbox") + .Replace("MenuButton", "Button") + .Replace("WorldButton", "Button") + .Replace("ProductionTypeButton", "Button") + .ToLowerInvariant(); + } + + static void FromChromeLayout(MiniYamlNodeBuilder node, Dictionary translatables, string container, ref List translations) + { + var nodeSplit = node.Key.Split('@'); + var nodeType = nodeSplit[0]; + var nodeId = nodeSplit.Length > 1 ? ClearContainersAndToLower(nodeSplit[1]) : null; + + if ((nodeType == "Background" || nodeType == "Container") && nodeId != null) + container = nodeId; + + // Get translatable types. + var validChildTypes = new List<(MiniYamlNodeBuilder Node, string Type, string Value)>(); + foreach (var childNode in node.Value.Nodes) + { + if (translatables.ContainsKey(nodeType)) + { + var childType = childNode.Key.Split('@')[0]; + if (translatables[nodeType].Contains(childType) + && !string.IsNullOrEmpty(childNode.Value.Value) + && !IsAlreadyTranslated(childNode.Value.Value) + && childNode.Value.Value.Any(char.IsLetterOrDigit)) + { + var translationValue = childNode.Value.Value + .Replace("\\n", "\n ") + .Replace("{", "<") + .Replace("}", ">") + .Trim().Trim('\n'); + + validChildTypes.Add((childNode, childType.ToLowerInvariant(), translationValue)); + } + } + } + + // Generate translation key. + if (validChildTypes.Count > 0) + { + nodeType = ClearTypesAndToLower(nodeType); + + var translationKey = nodeType; + if (!string.IsNullOrEmpty(container)) + { + var containerType = string.Join('-', container.Split('_').Exclude(nodeType).Where(s => !string.IsNullOrEmpty(s))); + if (!string.IsNullOrEmpty(containerType)) + translationKey = $"{translationKey}-{containerType}"; + } + + if (!string.IsNullOrEmpty(nodeId)) + { + nodeId = string.Join('-', nodeId.Split('_') + .Except(string.IsNullOrEmpty(container) ? new string[] { nodeType } : container.Split('_').Append(nodeType)) + .Where(s => !string.IsNullOrEmpty(s))); + + if (!string.IsNullOrEmpty(nodeId)) + translationKey = $"{translationKey}-{nodeId}"; + } + + foreach (var (childNode, childType, translationValue) in validChildTypes) + translations.Add(new TranslationCandidate(translationKey, childType, translationValue.Trim().Trim('\n'), childNode)); + } + + // Recurse. + foreach (var childNode in node.Value.Nodes) + if (childNode.Key == "Children") + foreach (var n in childNode.Value.Nodes) + FromChromeLayout(n, translatables, container, ref translations); + } + + /// This is a helper method to find untranslated strings in chrome layouts. + public static void FindUntranslatedStringFields(ModData modData) + { + var types = modData.ObjectCreator.GetTypes(); + foreach (var (type, fields) in types.Where(t => t.Name.EndsWith("Widget", StringComparison.InvariantCulture) && t.IsSubclassOf(typeof(Widget))).ToDictionary(t => t.Name[..^6], + t => t.GetFields().Where(f => f.Name != "Id" && f.IsPublic && f.FieldType == typeof(string) && !f.HasAttribute()).Distinct().Select(f => f.Name).ToList())) + if (fields.Count > 0) + Console.WriteLine($"{type}Widget:\n {string.Join("\n ", fields)}"); + } + } +} diff --git a/OpenRA.Game/WVec.cs b/OpenRA.Game/WVec.cs index ab9de7152db2..87c655c7b97a 100644 --- a/OpenRA.Game/WVec.cs +++ b/OpenRA.Game/WVec.cs @@ -73,6 +73,18 @@ public WAngle Yaw } } + public WAngle Pitch + { + get + { + if (LengthSquared == 0) + return WAngle.Zero; + + // OpenRA defines north as -y + return WAngle.ArcTan(Z, HorizontalLength); + } + } + public static WVec Lerp(in WVec a, in WVec b, int mul, int div) { return a + (b - a) * mul / div; } public static WVec LerpQuadratic(in WVec a, in WVec b, WAngle pitch, int mul, int div) diff --git a/OpenRA.Mods.AS/Activities/AttackFrontalFollowActivity.cs b/OpenRA.Mods.AS/Activities/AttackFrontalFollowActivity.cs new file mode 100644 index 000000000000..74137ee37b92 --- /dev/null +++ b/OpenRA.Mods.AS/Activities/AttackFrontalFollowActivity.cs @@ -0,0 +1,200 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + public class AttackFrontalFollowActivity : Activity, IActivityNotifyStanceChanged + { + readonly AttackFollowFrontal attack; + readonly RevealsShroud[] revealsShroud; + readonly IMove move; + readonly bool forceAttack; + readonly Color? targetLineColor; + readonly IFacing facing; + + Target target; + Target lastVisibleTarget; + bool useLastVisibleTarget; + WDist lastVisibleMaximumRange; + WDist lastVisibleMinimumRange; + BitSet lastVisibleTargetTypes; + Player lastVisibleOwner; + bool wasMovingWithinRange; + bool hasTicked; + + public AttackFrontalFollowActivity(Actor self, in Target target, bool allowMove, bool forceAttack, Color? targetLineColor = null) + { + ActivityType = ActivityType.Attack; + attack = self.Trait(); + move = allowMove ? self.TraitOrDefault() : null; + revealsShroud = self.TraitsImplementing().ToArray(); + facing = self.Trait(); + + this.target = target; + this.forceAttack = forceAttack; + this.targetLineColor = targetLineColor; + + // The target may become hidden between the initial order request and the first tick (e.g. if queued) + // Moving to any position (even if quite stale) is still better than immediately giving up + if ((target.Type == TargetType.Actor && target.Actor.CanBeViewedByPlayer(self.Owner)) + || target.Type == TargetType.FrozenActor || target.Type == TargetType.Terrain) + { + lastVisibleTarget = Target.FromPos(target.CenterPosition); + lastVisibleMaximumRange = attack.GetMaximumRangeVersusTarget(target); + lastVisibleMinimumRange = attack.GetMinimumRangeVersusTarget(target); + + if (target.Type == TargetType.Actor) + { + lastVisibleOwner = target.Actor.Owner; + lastVisibleTargetTypes = target.Actor.GetEnabledTargetTypes(); + } + else if (target.Type == TargetType.FrozenActor) + { + lastVisibleOwner = target.FrozenActor.Owner; + lastVisibleTargetTypes = target.FrozenActor.TargetTypes; + } + } + } + + public override bool Tick(Actor self) + { + if (IsCanceling) + return true; + + // Check that AttackFollow hasn't cancelled the target by modifying attack.Target + // Having both this and AttackFollow modify that field is a horrible hack. + if (hasTicked && attack.RequestedTarget.Type == TargetType.Invalid) + return true; + + if (attack.IsTraitPaused) + return false; + + target = target.Recalculate(self.Owner, out var targetIsHiddenActor); + attack.SetRequestedTarget(self, target, forceAttack); + hasTicked = true; + + if (!targetIsHiddenActor && target.Type == TargetType.Actor) + { + lastVisibleTarget = Target.FromTargetPositions(target); + lastVisibleMaximumRange = attack.GetMaximumRangeVersusTarget(target); + lastVisibleMinimumRange = attack.GetMinimumRange(); + lastVisibleOwner = target.Actor.Owner; + lastVisibleTargetTypes = target.Actor.GetEnabledTargetTypes(); + + var leeway = attack.Info.RangeMargin.Length; + if (leeway != 0 && move != null && target.Actor.Info.HasTraitInfo()) + { + var preferMinRange = Math.Min(lastVisibleMinimumRange.Length + leeway, lastVisibleMaximumRange.Length); + var preferMaxRange = Math.Max(lastVisibleMaximumRange.Length - leeway, lastVisibleMinimumRange.Length); + lastVisibleMaximumRange = new WDist((lastVisibleMaximumRange.Length - leeway).Clamp(preferMinRange, preferMaxRange)); + } + } + + // The target may become hidden in the same tick the AttackActivity constructor is called, + // causing lastVisible* to remain uninitialized. + // Fix the fallback values based on the frozen actor properties + else if (target.Type == TargetType.FrozenActor && !lastVisibleTarget.IsValidFor(self)) + { + lastVisibleTarget = Target.FromTargetPositions(target); + lastVisibleMaximumRange = attack.GetMaximumRangeVersusTarget(target); + lastVisibleOwner = target.FrozenActor.Owner; + lastVisibleTargetTypes = target.FrozenActor.TargetTypes; + } + + var maxRange = lastVisibleMaximumRange; + var minRange = lastVisibleMinimumRange; + useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self); + + // Most actors want to be able to see their target before shooting + if (target.Type == TargetType.FrozenActor && !attack.Info.TargetFrozenActors && !forceAttack) + { + var rs = revealsShroud + .Where(Exts.IsTraitEnabled) + .MaxByOrDefault(s => s.Range); + + // Default to 2 cells if there are no active traits + var sightRange = rs != null ? rs.Range : WDist.FromCells(2); + if (sightRange < maxRange) + maxRange = sightRange; + } + + // If we are ticking again after previously sequencing a MoveWithRange then that move must have completed + // Either we are in range and can see the target, or we've lost track of it and should give up + if (wasMovingWithinRange && targetIsHiddenActor) + return true; + + // Target is hidden or dead, and we don't have a fallback position to move towards + if (useLastVisibleTarget && !lastVisibleTarget.IsValidFor(self)) + return true; + + var pos = self.CenterPosition; + var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target; + + // We've reached the required range - if the target is visible and valid then we wait + // otherwise if it is hidden or dead we give up + if (checkTarget.IsInRange(pos, maxRange) && !checkTarget.IsInRange(pos, minRange)) + { + var extraTurning = attack.Info.MustFaceTarget ? WAngle.Zero : attack.Info.FacingTolerance; + if (!attack.TargetInFiringArc(self, checkTarget, extraTurning)) + { + var desiredFacing = (attack.GetTargetPosition(pos, checkTarget) - pos).Yaw; + facing.Facing = Util.TickFacing(facing.Facing, desiredFacing, facing.TurnSpeed); + } + + if (useLastVisibleTarget) + return true; + + return false; + } + + // We can't move into range, so give up + if (move == null || maxRange == WDist.Zero || maxRange < minRange) + return true; + + wasMovingWithinRange = true; + QueueChild(move.MoveWithinRange(target, minRange, maxRange, checkTarget.CenterPosition)); + return false; + } + + protected override void OnLastRun(Actor self) + { + // Cancel the requested target, but keep firing on it while in range + attack.ClearRequestedTarget(); + } + + void IActivityNotifyStanceChanged.StanceChanged(Actor self, AutoTarget autoTarget, UnitStance oldStance, UnitStance newStance) + { + // Cancel non-forced targets when switching to a more restrictive stance if they are no longer valid for auto-targeting + if (newStance > oldStance || forceAttack) + return; + + // If lastVisibleTarget is invalid we could never view the target in the first place, so we just drop it here too + if (!lastVisibleTarget.IsValidFor(self) || !autoTarget.HasValidTargetPriority(self, lastVisibleOwner, lastVisibleTargetTypes)) + attack.ClearRequestedTarget(); + } + + public override IEnumerable TargetLineNodes(Actor self) + { + if (targetLineColor != null) + yield return new TargetLineNode(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value); + } + } +} diff --git a/OpenRA.Mods.AS/Activities/BallisticMissileFly.cs b/OpenRA.Mods.AS/Activities/BallisticMissileFly.cs new file mode 100644 index 000000000000..6e3b6e4fc49d --- /dev/null +++ b/OpenRA.Mods.AS/Activities/BallisticMissileFly.cs @@ -0,0 +1,277 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + public class BallisticMissileFly : Activity + { + enum BMFlyStatus { Prepare, Launch, NoCruiseLaunch, LazyCurve, Cruise, Hit, Unknown } + + readonly BallisticMissile bm; + readonly BallisticMissileInfo bmInfo; + readonly WPos initPos; + readonly WPos targetPos; + int ticks = 0; + BMFlyStatus status = BMFlyStatus.Prepare; + + int speed = 0; + readonly int dSpeed = 0; + + readonly int horizontalLength; + readonly WAngle preparePitchIncrement; + + readonly int lazyCurveLength = 0; + int lazyCurveTick = 0; + + public BallisticMissileFly(Actor self, Target t, BallisticMissile bm) + { + this.bm = bm; + bmInfo = bm.Info; + initPos = self.CenterPosition; + targetPos = t.CenterPosition; + + horizontalLength = (initPos - targetPos).HorizontalLength; + + if (bmInfo.LaunchAcceleration == WDist.Zero) + { + speed = bmInfo.Speed.Length; + dSpeed = 0; + } + else + { + speed = 0; + dSpeed = bmInfo.LaunchAcceleration.Length; + } + + if (bmInfo.LazyCurve) + { + lazyCurveLength = Math.Max((targetPos - initPos).Length / this.bm.Info.Speed.Length, 1); + } + + preparePitchIncrement = new WAngle((bmInfo.LaunchAngle - bmInfo.CreateAngle).Angle / bmInfo.PrepareTick); + + if (bmInfo.WithoutCruise) + { + preparePitchIncrement = new WAngle((new WAngle(256) - bmInfo.CreateAngle).Angle / bmInfo.PrepareTick); + } + } + + protected override void OnFirstRun(Actor self) + { + bm.Pitch = bmInfo.CreateAngle; + } + + void MoveForward(Actor self) + { + var move = new WVec(0, -speed, 0).Rotate(new WRot(bm.Pitch, WAngle.Zero, bm.Facing)); + bm.SetPosition(self, bm.CenterPosition + move); + if (!self.IsInWorld) + status = BMFlyStatus.Unknown; + } + + void PrepareStatusHandle(Actor self) + { + if (ticks < bmInfo.PrepareTick) + bm.Pitch += preparePitchIncrement; + else + { + if (bm.Info.AudibleThroughFog || (!self.World.ShroudObscures(bm.CenterPosition) && !self.World.FogObscures(bm.CenterPosition))) + Game.Sound.Play(SoundType.World, bm.Info.LaunchSounds, self.World, bm.CenterPosition, null, bm.Info.SoundVolume); + if (bmInfo.WithoutCruise) + { + status = BMFlyStatus.NoCruiseLaunch; + return; + } + + if (bmInfo.LazyCurve) + { + status = BMFlyStatus.LazyCurve; + return; + } + + status = BMFlyStatus.Launch; + } + } + + void LaunchStatusHandle(Actor self) + { + MoveForward(self); + speed = speed + dSpeed > bmInfo.Speed.Length ? bmInfo.Speed.Length : speed + dSpeed; + if (bm.CenterPosition.Z - initPos.Z > bmInfo.BeginCruiseAltitude.Length) + { + status = BMFlyStatus.Cruise; + } + } + + void CruiseStatusHandle(Actor self) + { + MoveForward(self); + if (bm.Pitch != WAngle.Zero) + { + if ((bm.Pitch.Angle < bm.TurnSpeed.Angle) || (1024 - bm.Pitch.Angle < bm.TurnSpeed.Angle)) + { + bm.Pitch = WAngle.Zero; + } + else + { + bm.Pitch -= bm.TurnSpeed; + } + } + + var targetYaw = (targetPos - bm.CenterPosition).Yaw; + var yawDiff = targetYaw - bm.Facing; + if (yawDiff != WAngle.Zero) + { + if ((yawDiff.Angle < bm.TurnSpeed.Angle) || (1024 - yawDiff.Angle < bm.TurnSpeed.Angle)) + { + bm.Facing = targetYaw; + } + else + { + if (yawDiff.Angle < 512) + bm.Facing += bm.TurnSpeed; + else + bm.Facing -= bm.TurnSpeed; + } + } + + if ((targetPos - bm.CenterPosition).HorizontalLength < bmInfo.BeginHitRange.Length) + { + status = BMFlyStatus.Hit; + } + } + + void HitStatusHandle(Actor self) + { + MoveForward(self); + speed += bmInfo.HitAcceleration.Length; + var targetPitch = (targetPos - bm.CenterPosition).Pitch; + var pitchDiff = targetPitch - bm.Pitch; + if (pitchDiff != WAngle.Zero) + { + if ((pitchDiff.Angle < bm.TurnSpeed.Angle) || (1024 - pitchDiff.Angle < bm.TurnSpeed.Angle)) + { + bm.Pitch = targetPitch; + } + else + { + if (pitchDiff.Angle < 512) + bm.Pitch += bm.TurnSpeed; + else + bm.Pitch -= bm.TurnSpeed; + } + } + + var targetYaw = (targetPos - bm.CenterPosition).Yaw; + var yawDiff = targetYaw - bm.Facing; + if (yawDiff != WAngle.Zero) + { + if ((yawDiff.Angle < bm.TurnSpeed.Angle) || (1024 - yawDiff.Angle < bm.TurnSpeed.Angle)) + { + bm.Facing = targetYaw; + } + else + { + if (yawDiff.Angle < 512) + bm.Facing += bm.TurnSpeed; + else + bm.Facing -= bm.TurnSpeed; + } + } + + if ((targetPos - bm.CenterPosition).Length < bmInfo.ExplosionRange.Length) + { + status = BMFlyStatus.Unknown; + } + } + + void NoCruiseLaunchStatusHandle(Actor self) + { + MoveForward(self); + speed = speed + dSpeed > bmInfo.Speed.Length ? bmInfo.Speed.Length : speed + dSpeed; + if (bm.CenterPosition.Z - initPos.Z < horizontalLength && (bm.CenterPosition - targetPos).HorizontalLength > horizontalLength * 3 / 5) + { + return; + } + + if (bm.Pitch != WAngle.Zero) + { + var newTurnSpeed = new WAngle(8192 * bm.TurnSpeed.Angle / horizontalLength); + if ((bm.Pitch.Angle < newTurnSpeed.Angle) || (1024 - bm.Pitch.Angle < newTurnSpeed.Angle)) + { + bm.Pitch = WAngle.Zero; + } + else + { + bm.Pitch -= newTurnSpeed; + } + + return; + } + + status = BMFlyStatus.Hit; + } + + void LazyCurveHandle(Actor self) + { + var pos = WPos.LerpQuadratic(initPos, targetPos, bm.Info.LaunchAngle, lazyCurveTick, lazyCurveLength); + bm.Pitch = (pos - bm.CenterPosition).Pitch; + bm.SetPosition(self, pos); + lazyCurveTick++; + if ((targetPos - bm.CenterPosition).Length < bmInfo.ExplosionRange.Length) + { + status = BMFlyStatus.Unknown; + } + } + + public override bool Tick(Actor self) + { + switch (status) + { + case BMFlyStatus.Prepare: + PrepareStatusHandle(self); + break; + case BMFlyStatus.Launch: + LaunchStatusHandle(self); + break; + case BMFlyStatus.NoCruiseLaunch: + NoCruiseLaunchStatusHandle(self); + break; + case BMFlyStatus.Cruise: + CruiseStatusHandle(self); + break; + case BMFlyStatus.Hit: + HitStatusHandle(self); + break; + case BMFlyStatus.LazyCurve: + LazyCurveHandle(self); + break; + default: + bm.SetPosition(self, targetPos); + Queue(new CallFunc(() => self.Kill(self, bm.Info.DamageTypes))); + return true; + } + + ticks++; + return false; + } + + public override IEnumerable GetTargets(Actor self) + { + yield return Target.FromPos(targetPos); + } + } +} diff --git a/OpenRA.Mods.AS/Activities/ChronoResourceTeleport.cs b/OpenRA.Mods.AS/Activities/ChronoResourceTeleport.cs new file mode 100644 index 000000000000..a38d377e438b --- /dev/null +++ b/OpenRA.Mods.AS/Activities/ChronoResourceTeleport.cs @@ -0,0 +1,69 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + public class ChronoResourceTeleport : Activity + { + readonly CPos destination; + readonly ChronoResourceDeliveryInfo info; + readonly CPos harvestedField; + readonly Actor refinery; + + public ChronoResourceTeleport(CPos destination, ChronoResourceDeliveryInfo info, CPos harvestedField, Actor refinery) + { + this.destination = destination; + this.info = info; + this.harvestedField = harvestedField; + this.refinery = refinery; + } + + public override bool Tick(Actor self) + { + var image = info.Image ?? self.Info.Name; + + var sourcepos = self.CenterPosition; + + if (info.WarpInSequence != null) + self.World.AddFrameEndTask(w => w.Add(new SpriteEffect(sourcepos, w, image, info.WarpInSequence, info.Palette))); + + if (info.WarpInSound != null && (info.AudibleThroughFog || !self.World.FogObscures(sourcepos))) + Game.Sound.Play(SoundType.World, info.WarpInSound, self.CenterPosition, info.SoundVolume); + + if (info.ExposeInfectors) + foreach (var i in self.TraitsImplementing()) + i.RemoveInfector(self, false); + + self.Trait().SetPosition(self, destination); + self.Generation++; + + var destinationpos = self.CenterPosition; + + if (info.WarpOutSequence != null) + self.World.AddFrameEndTask(w => w.Add(new SpriteEffect(destinationpos, w, image, info.WarpOutSequence, info.Palette))); + + if (info.WarpOutSound != null && (info.AudibleThroughFog || !self.World.FogObscures(sourcepos))) + Game.Sound.Play(SoundType.World, info.WarpOutSound, self.CenterPosition, info.SoundVolume); + + if (refinery == null) + self.QueueActivity(new FindAndDeliverResources(self, harvestedField)); + else + self.QueueActivity(new FindAndDeliverResources(self, refinery.Location)); + + return true; + } + } +} diff --git a/OpenRA.Mods.AS/Activities/EnterAirstrikeMaster.cs b/OpenRA.Mods.AS/Activities/EnterAirstrikeMaster.cs new file mode 100644 index 000000000000..520a5afe702c --- /dev/null +++ b/OpenRA.Mods.AS/Activities/EnterAirstrikeMaster.cs @@ -0,0 +1,88 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + class EnterAirstrikeMaster : Activity + { + readonly Actor master; + readonly AirstrikeMaster spawnerMaster; + + public EnterAirstrikeMaster(Actor master, AirstrikeMaster spawnerMaster) + { + this.master = master; + this.spawnerMaster = spawnerMaster; + } + + public override bool Tick(Actor self) + { + if (master.IsDead) + return true; + + self.World.AddFrameEndTask(w => + { + if (self.IsDead || master.IsDead) + return; + + spawnerMaster.PickupSlave(master, self); + w.Remove(self); + + if (spawnerMaster.AirstrikeMasterInfo.InstantRepair) + { + var health = self.Trait(); + self.InflictDamage(self, new Damage(-health.MaxHP)); + } + + // Delayed launching is handled at spawner. + var ammoPools = self.TraitsImplementing().ToArray(); + if (ammoPools != null) + foreach (var pool in ammoPools) + while (pool.GiveAmmo(self, 1)) + { } + }); + + return true; + } + } + + class ReturnAirstrikeMaster : Activity + { + readonly Actor master; + readonly AirstrikeMaster spawnerMaster; + readonly WPos edgePos; + + public ReturnAirstrikeMaster(Actor master, AirstrikeMaster spawnerMaster, WPos edgePos) + { + this.master = master; + this.spawnerMaster = spawnerMaster; + this.edgePos = edgePos; + } + + protected override void OnFirstRun(Actor self) + { + if (spawnerMaster.AirstrikeMasterInfo.SendAndForget) + { + QueueChild(new FlyOffMap(self)); + } + else + { + QueueChild(new Fly(self, Target.FromPos(edgePos))); + QueueChild(new EnterAirstrikeMaster(master, spawnerMaster)); + } + } + } +} diff --git a/OpenRA.Mods.AS/Activities/EnterCarrierMaster.cs b/OpenRA.Mods.AS/Activities/EnterCarrierMaster.cs new file mode 100644 index 000000000000..075461bf7b4b --- /dev/null +++ b/OpenRA.Mods.AS/Activities/EnterCarrierMaster.cs @@ -0,0 +1,59 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + class EnterCarrierMaster : Enter + { + readonly Actor master; + readonly CarrierMaster spawnerMaster; + + public EnterCarrierMaster(Actor self, Actor master, CarrierMaster spawnerMaster) + : base(self, Target.FromActor(master)) + { + this.master = master; + this.spawnerMaster = spawnerMaster; + } + + protected override void OnEnterComplete(Actor self, Actor targetActor) + { + if (master.IsDead) + return; + + self.World.AddFrameEndTask(w => + { + if (self.IsDead || master.IsDead) + return; + + spawnerMaster.PickupSlave(master, self); + w.Remove(self); + + if (spawnerMaster.CarrierMasterInfo.InstantRepair) + { + var health = self.Trait(); + self.InflictDamage(self, new Damage(-health.MaxHP)); + } + + // Delayed launching is handled at spawner. + var ammoPools = self.TraitsImplementing().ToArray(); + if (ammoPools != null) + foreach (var pool in ammoPools) + while (pool.GiveAmmo(self, 1)) + { } + }); + } + } +} diff --git a/OpenRA.Mods.AS/Activities/EnterGarrison.cs b/OpenRA.Mods.AS/Activities/EnterGarrison.cs new file mode 100644 index 000000000000..ccd1dae2028c --- /dev/null +++ b/OpenRA.Mods.AS/Activities/EnterGarrison.cs @@ -0,0 +1,202 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + class EnterGarrison : Activity + { + enum EnterState { Approaching, Entering, Exiting, Finished } + + readonly IMove move; + readonly Color? targetLineColor; + readonly Garrisoner garrisoner; + Target target; + Target lastVisibleTarget; + bool useLastVisibleTarget; + EnterState lastState = EnterState.Approaching; + + // EnterGarrison Properties + Actor enterActor; + Garrisonable enterGarrison; + Aircraft enterAircraft; + + public EnterGarrison(Actor self, Target target, Color? targetLineColor = null) + { + // Base - Enter Properties + move = self.Trait(); + this.target = target; + this.targetLineColor = targetLineColor; + + // EnterGarrison Properties + garrisoner = self.Trait(); + } + + protected bool TryStartEnter(Actor self, Actor targetActor) + { + enterActor = targetActor; + enterGarrison = targetActor.TraitOrDefault(); + enterAircraft = targetActor.TraitOrDefault(); + + // Make sure we can still enter the transport + // (but not before, because this may stop the actor in the middle of nowhere) + if (enterGarrison == null || enterGarrison.IsTraitDisabled || enterGarrison.IsTraitPaused || !garrisoner.Reserve(self, enterGarrison)) + { + Cancel(self, true); + return false; + } + + if (enterAircraft != null && !enterAircraft.AtLandAltitude) + return false; + + return true; + } + + protected void OnEnterComplete(Actor self, Actor targetActor) + { + self.World.AddFrameEndTask(w => + { + if (self.IsDead) + return; + + // Make sure the target hasn't changed while entering + // OnEnterComplete is only called if targetActor is alive + if (targetActor != enterActor) + return; + + if (enterActor.AppearsHostileTo(self)) + return; + + if (!enterGarrison.CanLoad(self)) + return; + + foreach (var inl in targetActor.TraitsImplementing()) + inl.Loading(self); + + enterGarrison.Load(enterActor, self); + w.Remove(self); + }); + } + + // Base Enter Methods Below + public override bool Tick(Actor self) + { + // Update our view of the target + target = target.Recalculate(self.Owner, out var targetIsHiddenActor); + + // Re-acquire the target after change owner has happened. + if (target.Type == TargetType.Invalid) + target = Target.FromActor(target.Actor); + + if (!targetIsHiddenActor && target.Type == TargetType.Actor) + lastVisibleTarget = Target.FromTargetPositions(target); + + useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self); + + // Cancel immediately if the target died while we were entering it + if (!IsCanceling && useLastVisibleTarget && lastState == EnterState.Entering) + Cancel(self, true); + + // We need to wait for movement to finish before transitioning to + // the next state or next activity + if (!TickChild(self)) + return false; + + // Note that lastState refers to what we have just *finished* doing + switch (lastState) + { + case EnterState.Approaching: + { + // NOTE: We can safely cancel in this case because we know the + // actor has finished any in-progress move activities + if (IsCanceling) + return true; + + // Lost track of the target + if (useLastVisibleTarget && lastVisibleTarget.Type == TargetType.Invalid) + return true; + + // We are not next to the target - lets fix that + if (target.Type != TargetType.Invalid && !move.CanEnterTargetNow(self, target)) + { + // Target lines are managed by this trait, so we do not pass targetLineColor + var initialTargetPosition = (useLastVisibleTarget ? lastVisibleTarget : target).CenterPosition; + QueueChild(move.MoveToTarget(self, target, initialTargetPosition)); + return false; + } + + // We are next to where we thought the target should be, but it isn't here + // There's not much more we can do here + if (useLastVisibleTarget || target.Type != TargetType.Actor) + return true; + + // Are we ready to move into the target? + if (TryStartEnter(self, target.Actor)) + { + lastState = EnterState.Entering; + QueueChild(move.MoveIntoTarget(self, target)); + return false; + } + + // Subclasses can cancel the activity during TryStartEnter + // Return immediately to avoid an extra tick's delay + if (IsCanceling) + return true; + + return false; + } + + case EnterState.Entering: + { + // Re-acquire the target after change owner has happened. + if (target.Type == TargetType.Invalid) + target = Target.FromActor(target.Actor); + + // Check that we reached the requested position + var targetPos = target.Positions.ClosestToWithPathFrom(self); + if (!IsCanceling && self.CenterPosition == targetPos && target.Type == TargetType.Actor) + OnEnterComplete(self, target.Actor); + else + { + // Need to move again as we have re-aquired a target + QueueChild(move.MoveToTarget(self, target, targetPos)); + lastState = EnterState.Approaching; + return false; + } + + lastState = EnterState.Exiting; + return false; + } + + case EnterState.Exiting: + { + QueueChild(move.ReturnToCell(self)); + lastState = EnterState.Finished; + return false; + } + } + + return true; + } + + public override IEnumerable TargetLineNodes(Actor self) + { + if (targetLineColor != null) + yield return new TargetLineNode(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value); + } + } +} diff --git a/OpenRA.Mods.AS/Activities/EnterTeleportNetwork.cs b/OpenRA.Mods.AS/Activities/EnterTeleportNetwork.cs new file mode 100644 index 000000000000..cf22ef5119a7 --- /dev/null +++ b/OpenRA.Mods.AS/Activities/EnterTeleportNetwork.cs @@ -0,0 +1,101 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + class EnterTeleportNetwork : Enter + { + readonly string type; + + public EnterTeleportNetwork(Actor self, Target target, string type) + : base(self, target, Color.Yellow) + { + this.type = type; + } + + protected override bool TryStartEnter(Actor self, Actor targetActor) + { + return targetActor.IsValidTeleportNetworkUser(self); + } + + protected override void OnEnterComplete(Actor self, Actor targetActor) + { + // entered the teleport network canal but the entrance is dead immediately. + if (targetActor.IsDead || self.IsDead) + return; + + // Find the primary teleport network exit. + var pri = targetActor.Owner.PlayerActor.TraitsImplementing().First(x => x.Type == type).PrimaryActor; + + var exitinfo = pri.Info.TraitInfo(); + var rp = pri.TraitOrDefault(); + + var exit = CPos.Zero; // spawn point + var exitLocations = new List(); // dest to move (cell pos) + var dest = Target.Invalid; // destination to move (in Target) + + if (pri.OccupiesSpace != null) + { + exit = pri.Location + exitinfo.ExitCell; + var spawn = pri.CenterPosition + exitinfo.SpawnOffset; + var to = self.World.Map.CenterOfCell(exit); + + WAngle initialFacing; + if (!exitinfo.Facing.HasValue) + { + var delta = to - spawn; + if (delta.HorizontalLengthSquared == 0) + initialFacing = WAngle.Zero; + else + initialFacing = delta.Yaw; + + var fi = self.TraitOrDefault(); + if (fi != null) + fi.Facing = initialFacing; + } + + exitLocations = rp != null ? rp.Path : new List { exit }; + dest = Target.FromCell(self.World, exitLocations.Last()); + } + + // Teleport myself to primary actor. + self.Trait().SetPosition(self, exit); + + // Cancel all activities (like PortableChrono does) + self.CancelActivity(); + + // Issue attack move to the rally point. + self.World.AddFrameEndTask(w => + { + var move = self.TraitOrDefault(); + if (move != null) + { + // Exit delay is ignored. + if (rp != null) + foreach (var cell in rp.Path) + self.QueueActivity(new AttackMoveActivity( + self, () => move.MoveTo(cell, 1, targetLineColor: Color.OrangeRed))); + else + foreach (var cell in exitLocations) + self.QueueActivity(new AttackMoveActivity( + self, () => move.MoveTo(cell, 1, targetLineColor: Color.OrangeRed))); + } + }); + } + } +} diff --git a/OpenRA.Mods.AS/Activities/FallDown.cs b/OpenRA.Mods.AS/Activities/FallDown.cs new file mode 100644 index 000000000000..477a4a32278b --- /dev/null +++ b/OpenRA.Mods.AS/Activities/FallDown.cs @@ -0,0 +1,69 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Activities; +using OpenRA.Mods.Common.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + public class FallDown : Activity + { + readonly IPositionable pos; + readonly WVec fallVector; + + readonly WPos dropPosition; + WPos currentPosition; + bool triggered = false; + + public FallDown(Actor self, WPos dropPosition, int fallRate) + { + pos = self.TraitOrDefault(); + IsInterruptible = false; + fallVector = new WVec(0, 0, fallRate); + this.dropPosition = dropPosition; + } + + bool FirstTick(Actor self) + { + triggered = true; + + // Place the actor and retrieve its visual position (CenterPosition) + pos.SetPosition(self, dropPosition); + currentPosition = self.CenterPosition; + + return false; + } + + bool LastTick(Actor self) + { + var dat = self.World.Map.DistanceAboveTerrain(currentPosition); + pos.SetPosition(self, currentPosition - new WVec(WDist.Zero, WDist.Zero, dat)); + + return true; + } + + public override bool Tick(Actor self) + { + // If this is the first tick + if (!triggered) + return FirstTick(self); + + currentPosition -= fallVector; + + // If the unit has landed, this will be the last tick + if (self.World.Map.DistanceAboveTerrain(currentPosition).Length <= 0) + return LastTick(self); + + pos.SetCenterPosition(self, currentPosition); + + return false; + } + } +} diff --git a/OpenRA.Mods.AS/Activities/Infect.cs b/OpenRA.Mods.AS/Activities/Infect.cs new file mode 100644 index 000000000000..f152a4688a02 --- /dev/null +++ b/OpenRA.Mods.AS/Activities/Infect.cs @@ -0,0 +1,159 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + class Infect : Enter + { + readonly AttackInfect infector; + readonly AttackInfectInfo info; + readonly Target target; + + bool jousting; + + public Infect(Actor self, Target target, AttackInfect infector, AttackInfectInfo info, Color? targetLineColor) + : base(self, target, targetLineColor) + { + this.target = target; + this.infector = infector; + this.info = info; + } + + protected override void OnFirstRun(Actor self) + { + infector.IsAiming = true; + } + + protected override void OnLastRun(Actor self) + { + infector.IsAiming = false; + } + + protected override void OnEnterComplete(Actor self, Actor targetActor) + { + self.World.AddFrameEndTask(w => + { + if (self.IsDead || infector.IsTraitDisabled) + return; + + if (jousting) + { + infector.RevokeJoustCondition(self); + jousting = false; + } + + infector.DoAttack(self, target); + + var infectable = targetActor.TraitOrDefault(); + if (infectable == null || infectable.IsTraitDisabled || infectable.Infector != null) + return; + + w.Remove(self); + + infectable.Infector = Tuple.Create(self, infector, info); + infectable.FirepowerMultipliers = self.TraitsImplementing() + .Select(a => a.GetFirepowerModifier(infector.InfectInfo.Name)).ToArray(); + infectable.Ticks = info.DamageInterval; + infectable.GrantCondition(targetActor); + infectable.RevokeCondition(targetActor, self); + }); + } + + void CancelInfection(Actor self) + { + if (jousting) + { + infector.RevokeJoustCondition(self); + jousting = false; + } + + if (target.Type != TargetType.Actor) + return; + + if (target.Actor.IsDead) + return; + + var infectable = target.Actor.TraitOrDefault(); + if (infectable == null || infectable.IsTraitDisabled || infectable.Infector != null) + return; + + infectable.RevokeCondition(target.Actor, self); + } + + bool IsValidInfection(Actor self, Actor targetActor) + { + if (infector.IsTraitDisabled) + return false; + + if (targetActor.IsDead) + return false; + + if (!target.IsValidFor(self) || !infector.HasAnyValidWeapons(target)) + return false; + + var infectable = targetActor.TraitOrDefault(); + if (infectable == null || infectable.IsTraitDisabled || infectable.Infector != null) + return false; + + return true; + } + + bool CanStartInfect(Actor self, Actor targetActor) + { + if (!IsValidInfection(self, targetActor)) + return false; + + // IsValidInfection validated the lookup, no need to check here. + var infectable = targetActor.Trait(); + return infectable.TryStartInfecting(targetActor, self); + } + + protected override bool TryStartEnter(Actor self, Actor targetActor) + { + var canStartInfect = CanStartInfect(self, targetActor); + if (canStartInfect == false) + { + CancelInfection(self); + Cancel(self, true); + } + + // Can't leap yet + if (infector.Armaments.All(a => a.IsReloading)) + return false; + + return true; + } + + protected override void TickInner(Actor self, in Target target, bool targetIsDeadOrHiddenActor) + { + if (target.Type != TargetType.Actor || !IsValidInfection(self, target.Actor)) + { + CancelInfection(self); + Cancel(self, true); + return; + } + + if (!jousting && !IsCanceling && (self.CenterPosition - target.CenterPosition).Length < info.JoustRange.Length) + { + jousting = true; + infector.GrantJoustCondition(self); + IsInterruptible = false; + } + } + } +} diff --git a/OpenRA.Mods.AS/Activities/LeapAS.cs b/OpenRA.Mods.AS/Activities/LeapAS.cs new file mode 100644 index 000000000000..380567bb090c --- /dev/null +++ b/OpenRA.Mods.AS/Activities/LeapAS.cs @@ -0,0 +1,84 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + class LeapAS : Activity + { + readonly Mobile mobile; + readonly Armament armament; + readonly int length; + readonly AttackLeapAS trait; + readonly WAngle angle; + readonly Target target; + + readonly WPos from; + readonly WPos to; + int ticks; + + public LeapAS(Actor self, Actor target, Armament a, AttackLeapAS trait) + { + var targetMobile = target.TraitOrDefault(); + if (targetMobile == null) + throw new InvalidOperationException("Leap requires a target actor with the Mobile trait"); + + armament = a; + angle = trait.LeapInfo.Angle; + this.trait = trait; + this.target = Target.FromActor(target); + mobile = self.Trait(); + mobile.SetLocation(mobile.FromCell, mobile.FromSubCell, targetMobile.FromCell, targetMobile.FromSubCell); + + from = self.CenterPosition; + to = self.World.Map.CenterOfSubCell(targetMobile.FromCell, targetMobile.FromSubCell); + length = Math.Max((to - from).Length / trait.LeapInfo.Speed.Length, 1); + + if (armament.Weapon.Report != null && armament.Weapon.Report.Any()) + { + var pos = self.CenterPosition; + if (armament.Weapon.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, armament.Weapon.Report.Random(self.World.SharedRandom), pos, armament.Weapon.SoundVolume); + } + } + + public override bool Tick(Actor self) + { + if (ticks == 0 && IsCanceling) + return true; + + mobile.SetCenterPosition(self, WPos.LerpQuadratic(from, to, angle, ++ticks, length)); + if (ticks >= length) + { + mobile.SetLocation(mobile.ToCell, mobile.ToSubCell, mobile.ToCell, mobile.ToSubCell); + mobile.FinishedMoving(self); + + trait.NotifyAttacking(self, target, armament); + + var actors = self.World.ActorMap.GetActorsAt(mobile.ToCell, mobile.ToSubCell) + .Except(new[] { self }).Where(t => armament.Weapon.IsValidAgainst(t, self)); + foreach (var a in actors) + a.Kill(self, trait.LeapInfo.DamageTypes); + + trait.FinishAttacking(self); + + return true; + } + + return false; + } + } +} diff --git a/OpenRA.Mods.AS/Activities/RideSharedTransport.cs b/OpenRA.Mods.AS/Activities/RideSharedTransport.cs new file mode 100644 index 000000000000..ece2879401b8 --- /dev/null +++ b/OpenRA.Mods.AS/Activities/RideSharedTransport.cs @@ -0,0 +1,93 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + class RideSharedTransport : Enter + { + readonly SharedPassenger passenger; + + Actor enterActor; + SharedCargo enterCargo; + Aircraft enterAircraft; + + public RideSharedTransport(Actor self, in Target target, Color? targetLineColor) + : base(self, target, targetLineColor) + { + passenger = self.Trait(); + } + + protected override bool TryStartEnter(Actor self, Actor targetActor) + { + enterActor = targetActor; + enterCargo = targetActor.TraitOrDefault(); + enterAircraft = targetActor.TraitOrDefault(); + + // Make sure we can still enter the transport + // (but not before, because this may stop the actor in the middle of nowhere) + if (enterCargo == null || !passenger.Reserve(self, enterCargo)) + { + Cancel(self, true); + return false; + } + + if (enterAircraft != null && !enterAircraft.AtLandAltitude) + return false; + + return true; + } + + protected override void OnEnterComplete(Actor self, Actor targetActor) + { + self.World.AddFrameEndTask(w => + { + if (self.IsDead) + return; + + // Make sure the target hasn't changed while entering + // OnEnterComplete is only called if targetActor is alive + if (targetActor != enterActor) + return; + + if (!enterCargo.CanLoad(enterActor, self)) + return; + + foreach (var inl in targetActor.TraitsImplementing()) + inl.Loading(self); + + enterCargo.Load(enterActor, self); + w.Remove(self); + + // Preemptively cancel any activities to avoid an edge-case where successively queued + // EnterTransports corrupt the actor state. Activities are cancelled again on unload + self.CancelActivity(); + }); + } + + protected override void OnLastRun(Actor self) + { + passenger.Unreserve(self); + } + + public override void Cancel(Actor self, bool keepQueue = false) + { + passenger.Unreserve(self); + + base.Cancel(self, keepQueue); + } + } +} diff --git a/OpenRA.Mods.AS/Activities/SlaveMinerHarvesterHarvest.cs b/OpenRA.Mods.AS/Activities/SlaveMinerHarvesterHarvest.cs new file mode 100644 index 000000000000..ea704995c461 --- /dev/null +++ b/OpenRA.Mods.AS/Activities/SlaveMinerHarvesterHarvest.cs @@ -0,0 +1,260 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + public class SlaveMinerHarvesterHarvest : Activity + { + readonly SlaveMinerHarvester harv; + readonly SlaveMinerHarvesterInfo harvInfo; + readonly Mobile mobile; + readonly ResourceClaimLayer claimLayer; + readonly Transforms transforms; + CPos deployDestPosition; + readonly CPos? avoidCell; + int cellRange; + + public SlaveMinerHarvesterHarvest(Actor self) + { + harv = self.Trait(); + harvInfo = self.Info.TraitInfo(); + mobile = self.Trait(); + claimLayer = self.World.WorldActor.TraitOrDefault(); + transforms = self.Trait(); + ChildHasPriority = false; + } + + public SlaveMinerHarvesterHarvest(Actor self, CPos avoidCell) + : this(self) + { + this.avoidCell = avoidCell; + } + + void ScanAndMove(Actor self, out MiningState state) + { + var closestHarvestablePosition = ClosestHarvestablePos(self, harvInfo.LongScanRadius); + + // No suitable resource field found. + // We only have to wait for resource to regen. + if (!closestHarvestablePosition.HasValue) + { + var randFrames = self.World.SharedRandom.Next(100, 175); + + // Avoid creating an activity cycle + QueueChild(new Wait(randFrames)); + state = MiningState.Scan; + } + + // ... Don't claim resource layer here. Slaves will claim by themselves. + + // If not given a direct order, assume ordered to the first resource location we find: + if (!harv.LastOrderLocation.HasValue) + harv.LastOrderLocation = closestHarvestablePosition; + + // Calculate best depoly position. + var deployPosition = CalcTransformPosition(self, closestHarvestablePosition.Value); + + // Just sit there until we can. Won't happen unless the map is filled with units. + if (deployPosition == null) + { + QueueChild(new Wait(harvInfo.KickDelay)); + state = MiningState.Scan; + } + + // TODO: The harvest-deliver-return sequence is a horrible mess of duplicated code and edge-cases + var notify = self.TraitsImplementing(); + foreach (var n in notify) + n.MovingToResources(self, deployPosition.Value); + + state = MiningState.Moving; + + // When it reached the best position, we will let it do this activity again + deployDestPosition = deployPosition.Value; + cellRange = 2; + var moveActivity = mobile.MoveTo(deployPosition.Value, cellRange); + moveActivity.Queue(this); + QueueChild(moveActivity); + } + + void CheckIfReachedBestLocation(Actor self, out MiningState state) + { + if ((self.Location - deployDestPosition).LengthSquared <= cellRange * cellRange) + { + ChildActivity.Cancel(self); + state = MiningState.TryDeploy; + } + else + { + state = MiningState.Moving; + } + } + + void TryDeploy(out MiningState state) + { + if (!transforms.CanDeploy()) + { + // If we can't deploy, go back to scan state so that we scan try deploy again. + state = MiningState.Scan; + } + else + { + IsInterruptible = false; + + var transformsActivity = transforms.GetTransformActivity(); + QueueChild(transformsActivity); + + state = MiningState.Deploying; + } + } + + void Deploying(out MiningState state) + { + // deploy failure. + if (!transforms.CanDeploy()) + { + QueueChild(new Wait(15)); + state = MiningState.Scan; + } + else + { + state = MiningState.Mining; + } + } + + Activity Mining(out MiningState state) + { + // Let the harvester become idle so it can shoot enemies. + // Tick in SpawnerHarvester trait will kick activity back to KickTick. + state = MiningState.Packaging; + return ChildActivity; + } + + void UndeployingCheck(Actor self, out MiningState state) + { + var closestHarvestablePosition = ClosestHarvestablePos(self, harvInfo.KickScanRadius); + if (closestHarvestablePosition.HasValue) + { + // I may stay mining. + state = MiningState.Mining; + } + else + { + // get going + harv.LastOrderLocation = null; + CheckWheteherNeedUndeployAndGo(out state); + } + } + + Activity CheckWheteherNeedUndeployAndGo(out MiningState state) + { + // QueueChild(new DeployForGrantedCondition(self, deploy)); + state = MiningState.Scan; + return this; + } + + public override bool Tick(Actor self) + { + if (IsCanceling) + return true; + + switch (harv.MiningState) + { + case MiningState.Scan: + ScanAndMove(self, out harv.MiningState); + break; + case MiningState.Moving: + CheckIfReachedBestLocation(self, out harv.MiningState); + break; + case MiningState.TryDeploy: + TryDeploy(out harv.MiningState); + break; + case MiningState.Deploying: + Deploying(out harv.MiningState); + break; + case MiningState.Mining: + Mining(out harv.MiningState); + break; + case MiningState.Packaging: + UndeployingCheck(self, out harv.MiningState); + break; + } + + return TickChild(self); + } + + // Find a nearest Transformable position from harvestablePos + CPos? CalcTransformPosition(Actor self, CPos harvestablePos) + { + var transformActorInfo = self.World.Map.Rules.Actors[transforms.Info.IntoActor]; + var transformBuildingInfo = transformActorInfo.TraitInfoOrDefault(); + + // FindTilesInAnnulus gives sorted cells by distance :) Nice. + foreach (var tile in self.World.Map.FindTilesInAnnulus(harvestablePos, 0, harvInfo.DeployScanRadius)) + if (mobile.CanEnterCell(tile) && self.World.CanPlaceBuilding(tile + transforms.Info.Offset, transformActorInfo, transformBuildingInfo, self)) + return tile; + + // Try broader search if unable to find deploy location + foreach (var tile in self.World.Map.FindTilesInAnnulus(harvestablePos, harvInfo.DeployScanRadius, harvInfo.LongScanRadius)) + if (mobile.CanEnterCell(tile) && self.World.CanPlaceBuilding(tile + transforms.Info.Offset, transformActorInfo, transformBuildingInfo, self)) + return tile; + + return null; + } + + /// + /// Using LastOrderLocation and self.Location as starting points, + /// perform A* search to find the nearest accessible and harvestable cell. + /// + CPos? ClosestHarvestablePos(Actor self, int searchRadius) + { + if (harv.CanHarvestCell(self.Location) && claimLayer.CanClaimCell(self, self.Location)) + return self.Location; + + // Determine where to search from and how far to search: + var searchFromLoc = harv.LastOrderLocation ?? self.Location; + var searchRadiusSquared = searchRadius * searchRadius; + + // Find any harvestable resources: + // var passable = (uint)mobileInfo.GetMovementClass(self.World.Map.Rules.TileSet); + var path = mobile.PathFinder.FindPathToTargetCellByPredicate( + self, + new[] { searchFromLoc, self.Location }, + loc => + harv.CanHarvestCell(loc) && + claimLayer.CanClaimCell(self, loc), + BlockedByActor.All, + loc => + { + if ((avoidCell.HasValue && loc == avoidCell.Value) || + (loc - self.Location).LengthSquared > searchRadiusSquared) + return int.MaxValue; + + return 0; + }); + + if (path.Count > 0) + return path[0]; + + return null; + } + + public override IEnumerable GetTargets(Actor self) + { + yield return Target.FromCell(self.World, self.Location); + } + } +} diff --git a/OpenRA.Mods.AS/Activities/SlaveMinerMasterHarvest.cs b/OpenRA.Mods.AS/Activities/SlaveMinerMasterHarvest.cs new file mode 100644 index 000000000000..d06f791d3a82 --- /dev/null +++ b/OpenRA.Mods.AS/Activities/SlaveMinerMasterHarvest.cs @@ -0,0 +1,158 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + public class SlaveMinerMasterHarvest : Activity + { + readonly SlaveMinerMaster harv; + readonly SlaveMinerMasterInfo harvInfo; + readonly ResourceClaimLayer claimLayer; + int lastScanRange = 1; + + readonly CPos? avoidCell; + + public SlaveMinerMasterHarvest(Actor self) + { + harv = self.Trait(); + harvInfo = self.Info.TraitInfo(); + claimLayer = self.World.WorldActor.TraitOrDefault(); + lastScanRange = harvInfo.LongScanRadius; + ChildHasPriority = false; + } + + public SlaveMinerMasterHarvest(Actor self, CPos avoidCell) + : this(self) + { + this.avoidCell = avoidCell; + } + + Activity Mining(out MiningState state) + { + // Let the harvester become idle so it can shoot enemies. + // Tick in SpawnerHarvester trait will kick activity back to KickTick. + state = MiningState.Mining; + return ChildActivity; + } + + Activity Kick(Actor self, out MiningState state) + { + var closestHarvestablePosition = ClosestHarvestablePos(self, harvInfo.KickScanRadius); + if (closestHarvestablePosition.HasValue) + { + // I may stay mining. + state = MiningState.Mining; + return ChildActivity; + } + + // get going + harv.LastOrderLocation = null; + closestHarvestablePosition = ClosestHarvestablePos(self, lastScanRange); + if (closestHarvestablePosition != null) + { + state = MiningState.Undeploy; + harv.ForceMove(closestHarvestablePosition.Value); + } + else + { + state = MiningState.Packaging; + lastScanRange *= 2; // larger search range + } + + return this; + } + + public override bool Tick(Actor self) + { + /* + We just need to confirm one thing: when the nearest resource is finished, + just find the next resource point and transform and move to that location + */ + + if (IsCanceling) + return false; + + // Erm... looking at this, I could split these into separte activites... + // I prefer finite state machine style though... + // I can see what is going on at high level in this single place -_- + // I think this is less horrible than OpenRA FindResources... stuff. + // We are losing one tick, but so what? + // If this loss isn't acceptable, call ATick() from BTick() or something. + switch (harv.MiningState) + { + case MiningState.Mining: + QueueChild(Mining(out harv.MiningState)); + return false; + case MiningState.Packaging: + QueueChild(Kick(self, out harv.MiningState)); + return false; + } + + return true; + } + + /// + /// Using LastOrderLocation and self.Location as starting points, + /// perform A* search to find the nearest accessible and harvestable cell. + /// + CPos? ClosestHarvestablePos(Actor self, int searchRadius) + { + if (harv.CanHarvestCell(self.Location) && claimLayer.CanClaimCell(self, self.Location)) + return self.Location; + + // Determine where to search from and how far to search: + var searchFromLoc = harv.LastOrderLocation ?? self.Location; + var searchRadiusSquared = searchRadius * searchRadius; + + BaseSpawnerSlaveEntry choosenSlave = null; + var slaves = harv.GetSlaves(); + if (slaves.Length > 0) + { + choosenSlave = slaves[0]; + + var mobile = choosenSlave.Actor.Trait(); + + // Find any harvestable resources: + // var passable = (uint)mobileInfo.GetMovementClass(self.World.Map.Rules.TileSet); + var path = mobile.PathFinder.FindPathToTargetCellByPredicate( + self, + new[] { searchFromLoc, self.Location }, + loc => + harv.CanHarvestCell(loc) && + claimLayer.CanClaimCell(self, loc), + BlockedByActor.All, + loc => + { + if ((avoidCell.HasValue && loc == avoidCell.Value) || + (loc - self.Location).LengthSquared > searchRadiusSquared) + return int.MaxValue; + + return 0; + }); + + if (path.Count > 0) + return path[0]; + } + + return null; + } + + public override IEnumerable GetTargets(Actor self) + { + yield return Target.FromCell(self.World, self.Location); + } + } +} diff --git a/OpenRA.Mods.AS/Activities/UnloadGarrison.cs b/OpenRA.Mods.AS/Activities/UnloadGarrison.cs new file mode 100644 index 000000000000..1b8393881043 --- /dev/null +++ b/OpenRA.Mods.AS/Activities/UnloadGarrison.cs @@ -0,0 +1,148 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + public class UnloadGarrison : Activity + { + readonly Actor self; + readonly Garrisonable garrison; + readonly INotifyUnloadCargo[] notifiers; + readonly bool unloadAll; + readonly Aircraft aircraft; + readonly Mobile mobile; + readonly bool assignTargetOnFirstRun; + readonly WDist unloadRange; + + Target destination; + bool takeOffAfterUnload; + + public UnloadGarrison(Actor self, WDist unloadRange, bool unloadAll = true) + : this(self, Target.Invalid, unloadRange, unloadAll) + { + assignTargetOnFirstRun = true; + } + + public UnloadGarrison(Actor self, Target destination, WDist unloadRange, bool unloadAll = true) + { + this.self = self; + garrison = self.Trait(); + notifiers = self.TraitsImplementing().ToArray(); + this.unloadAll = unloadAll; + aircraft = self.TraitOrDefault(); + mobile = self.TraitOrDefault(); + this.destination = destination; + this.unloadRange = unloadRange; + } + + public (CPos Cell, SubCell SubCell)? ChooseExitSubCell(Actor passenger) + { + var pos = passenger.Trait(); + + return garrison.CurrentAdjacentCells + .Shuffle(self.World.SharedRandom) + .Select(c => (c, pos.GetAvailableSubCell(c))) + .Cast<(CPos, SubCell SubCell)?>() + .FirstOrDefault(s => s.Value.SubCell != SubCell.Invalid); + } + + IEnumerable BlockedExitCells(Actor passenger) + { + var pos = passenger.Trait(); + + // Find the cells that are blocked by transient actors + return garrison.CurrentAdjacentCells + .Where(c => pos.CanEnterCell(c, null, BlockedByActor.All) != pos.CanEnterCell(c, null, BlockedByActor.None)); + } + + protected override void OnFirstRun(Actor self) + { + if (assignTargetOnFirstRun) + destination = Target.FromCell(self.World, self.Location); + + // Move to the target destination + if (aircraft != null) + { + // Queue the activity even if already landed in case self.Location != destination + QueueChild(new Land(self, destination, unloadRange)); + takeOffAfterUnload = !aircraft.AtLandAltitude; + } + else if (mobile != null) + { + var cell = self.World.Map.Clamp(this.self.World.Map.CellContaining(destination.CenterPosition)); + QueueChild(new Move(self, cell, unloadRange)); + } + + QueueChild(new Wait(garrison.Info.BeforeUnloadDelay)); + } + + public override bool Tick(Actor self) + { + if (IsCanceling || garrison.IsEmpty()) + return true; + + if (garrison.CanUnload()) + { + foreach (var inu in notifiers) + inu.Unloading(self); + + var actor = garrison.Peek(); + var spawn = self.CenterPosition; + + var exitSubCell = ChooseExitSubCell(actor); + if (exitSubCell == null) + { + self.NotifyBlocker(BlockedExitCells(actor)); + + Queue(new Wait(10)); + return false; + } + + garrison.Unload(self); + self.World.AddFrameEndTask(w => + { + if (actor.Disposed) + return; + + var move = actor.Trait(); + var pos = actor.Trait(); + + pos.SetPosition(actor, exitSubCell.Value.Cell, exitSubCell.Value.SubCell); + pos.SetCenterPosition(actor, spawn); + + actor.CancelActivity(); + w.Add(actor); + }); + } + + if (!unloadAll || !garrison.CanUnload()) + { + if (garrison.Info.AfterUnloadDelay > 0) + QueueChild(new Wait(garrison.Info.AfterUnloadDelay, false)); + + if (takeOffAfterUnload) + QueueChild(new TakeOff(self)); + + return true; + } + + return false; + } + } +} diff --git a/OpenRA.Mods.AS/Activities/UnloadSharedCargo.cs b/OpenRA.Mods.AS/Activities/UnloadSharedCargo.cs new file mode 100644 index 000000000000..5b18f28a80dd --- /dev/null +++ b/OpenRA.Mods.AS/Activities/UnloadSharedCargo.cs @@ -0,0 +1,159 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Activities +{ + public class UnloadSharedCargo : Activity + { + readonly Actor self; + readonly SharedCargo cargo; + readonly INotifyUnloadCargo[] notifiers; + readonly bool unloadAll; + readonly Aircraft aircraft; + readonly Mobile mobile; + readonly bool assignTargetOnFirstRun; + readonly WDist unloadRange; + + Target destination; + bool takeOffAfterUnload; + + public UnloadSharedCargo(Actor self, WDist unloadRange, bool unloadAll = true) + : this(self, Target.Invalid, unloadRange, unloadAll) + { + ActivityType = ActivityType.Move; + assignTargetOnFirstRun = true; + } + + public UnloadSharedCargo(Actor self, in Target destination, WDist unloadRange, bool unloadAll = true) + { + ActivityType = ActivityType.Move; + this.self = self; + cargo = self.Trait(); + notifiers = self.TraitsImplementing().ToArray(); + this.unloadAll = unloadAll; + aircraft = self.TraitOrDefault(); + mobile = self.TraitOrDefault(); + this.destination = destination; + this.unloadRange = unloadRange; + } + + public (CPos Cell, SubCell SubCell)? ChooseExitSubCell(Actor passenger) + { + var pos = passenger.Trait(); + + return cargo.CurrentAdjacentCells + .Shuffle(self.World.SharedRandom) + .Select(c => (c, pos.GetAvailableSubCell(c))) + .Cast<(CPos, SubCell SubCell)?>() + .FirstOrDefault(s => s.Value.SubCell != SubCell.Invalid); + } + + IEnumerable BlockedExitCells(Actor passenger) + { + var pos = passenger.Trait(); + + // Find the cells that are blocked by transient actors + return cargo.CurrentAdjacentCells + .Where(c => pos.CanEnterCell(c, null, BlockedByActor.All) != pos.CanEnterCell(c, null, BlockedByActor.None)); + } + + protected override void OnFirstRun(Actor self) + { + if (assignTargetOnFirstRun) + destination = Target.FromCell(self.World, self.Location); + + // Move to the target destination + if (aircraft != null) + { + // Queue the activity even if already landed in case self.Location != destination + QueueChild(new Land(self, destination, unloadRange)); + takeOffAfterUnload = !aircraft.AtLandAltitude; + } + else if (mobile != null) + { + var cell = self.World.Map.Clamp(this.self.World.Map.CellContaining(destination.CenterPosition)); + QueueChild(new Move(self, cell, unloadRange)); + } + + QueueChild(new Wait(cargo.Info.BeforeUnloadDelay)); + } + + public override bool Tick(Actor self) + { + if (IsCanceling || cargo.Manager.IsEmpty()) + return true; + + if (cargo.CanUnload()) + { + foreach (var inu in notifiers) + inu.Unloading(self); + + var actor = cargo.Peek(); + var spawn = self.CenterPosition; + + var exitSubCell = ChooseExitSubCell(actor); + if (exitSubCell == null) + { + self.NotifyBlocker(BlockedExitCells(actor)); + QueueChild(new Wait(10)); + return false; + } + + cargo.Unload(self); + self.World.AddFrameEndTask(w => + { + if (actor.Disposed) + return; + + var move = actor.Trait(); + var pos = actor.Trait(); + + pos.SetPosition(actor, exitSubCell.Value.Cell, exitSubCell.Value.SubCell); + pos.SetCenterPosition(actor, spawn); + + actor.CancelActivity(); + w.Add(actor); + + if (!self.IsDead) + { + var rp = self.TraitOrDefault(); + var exitLocations = rp != null && rp.Path.Count > 0 ? rp.Path : new List(); + foreach (var cell in exitLocations) + actor.QueueActivity(new AttackMoveActivity(actor, () => move.MoveTo(cell, 1, evaluateNearestMovableCell: true, targetLineColor: Color.OrangeRed))); + } + }); + } + + if (!unloadAll || !cargo.CanUnload()) + { + if (cargo.Info.AfterUnloadDelay > 0) + QueueChild(new Wait(cargo.Info.AfterUnloadDelay, false)); + + if (takeOffAfterUnload) + QueueChild(new TakeOff(self)); + + return true; + } + + return false; + } + } +} diff --git a/OpenRA.Mods.AS/Commands/TauntCommands.cs b/OpenRA.Mods.AS/Commands/TauntCommands.cs new file mode 100644 index 000000000000..e86c4435c11b --- /dev/null +++ b/OpenRA.Mods.AS/Commands/TauntCommands.cs @@ -0,0 +1,64 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Graphics; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Commands; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Commands +{ + [Desc("Allows the player to play taunts via the chatbox. Attach this to the world actor.")] + public class TauntCommandsInfo : TraitInfo { } + + public class TauntCommands : IChatCommand, IWorldLoaded + { + World world; + Taunts taunts; + + public void WorldLoaded(World w, WorldRenderer wr) + { + world = w; + var console = world.WorldActor.Trait(); + var help = world.WorldActor.Trait(); + + void Register(string name, string helpText) + { + console.RegisterCommand(name, this); + help.RegisterHelp(name, helpText); + } + + if (world.LocalPlayer != null) + { + taunts = world.LocalPlayer.PlayerActor.TraitOrDefault(); + if (taunts != null) + Register("taunt", "plays a taunt"); + } + } + + public void InvokeCommand(string name, string arg) + { + switch (name) + { + case "taunt": + if (!taunts.Enabled) + { + TextNotificationsManager.Debug("Taunts are disabled."); + return; + } + + if (world.LocalPlayer != null) + world.IssueOrder(new Order("Taunt", world.LocalPlayer.PlayerActor, false) { TargetString = arg }); + + break; + } + } + } +} diff --git a/OpenRA.Mods.AS/Duplicates/Projectiles/BulletAS.cs b/OpenRA.Mods.AS/Duplicates/Projectiles/BulletAS.cs new file mode 100644 index 000000000000..c82cfd1f6e59 --- /dev/null +++ b/OpenRA.Mods.AS/Duplicates/Projectiles/BulletAS.cs @@ -0,0 +1,395 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; +using Util = OpenRA.Mods.Common.Util; + +namespace OpenRA.Mods.AS.Projectiles +{ + public class BulletASInfo : IProjectileInfo + { + [Desc("Projectile speed in WDist / tick, two values indicate variable velocity.")] + public readonly WDist[] Speed = { new(17) }; + + [Desc("The maximum/constant/incremental inaccuracy used in conjunction with the InaccuracyType property.")] + public readonly WDist Inaccuracy = WDist.Zero; + + [Desc("Controls the way inaccuracy is calculated. Possible values are 'Maximum' - scale from 0 to max with range, 'PerCellIncrement' - scale from 0 with range and 'Absolute' - use set value regardless of range.")] + public readonly InaccuracyType InaccuracyType = InaccuracyType.Maximum; + + [Desc("Image to display.")] + public readonly string Image = null; + + [SequenceReference(nameof(Image), allowNullImage: true)] + [Desc("Loop a randomly chosen sequence of Image from this list while this projectile is moving.")] + public readonly string[] Sequences = { "idle" }; + + [Desc("The palette used to draw this projectile.")] + [PaletteReference(nameof(IsPlayerPalette))] + public readonly string Palette = "effect"; + + public readonly bool IsPlayerPalette = false; + + [Desc("Does this projectile have a shadow?")] + public readonly bool Shadow = false; + + [Desc("Color to draw shadow if Shadow is true.")] + public readonly Color ShadowColor = Color.FromArgb(140, 0, 0, 0); + + [Desc("Palette to use for this projectile's shadow if Shadow is true.")] + [PaletteReference] + public readonly string ShadowPalette = "shadow"; + + [Desc("Trail animation.")] + public readonly string TrailImage = null; + + [Desc("Loop a randomly chosen sequence of TrailImage from this list while this projectile is moving.")] + [SequenceReference(nameof(TrailImage), allowNullImage: true)] + public readonly string[] TrailSequences = { "idle" }; + + [Desc("Is this blocked by actors with BlocksProjectiles trait.")] + public readonly bool Blockable = true; + + [Desc("Width of projectile (used for finding blocking actors).")] + public readonly WDist Width = new(1); + + [Desc("Arc in WAngles, two values indicate variable arc.")] + public readonly WAngle[] LaunchAngle = { WAngle.Zero }; + + [Desc("Up to how many times does this bullet bounce when touching ground without hitting a target.", + "0 implies exploding on contact with the originally targeted position.")] + public readonly int BounceCount = 0; + + [Desc("Sound to play when the projectile hits the ground, but not the target.")] + public readonly string BounceSound = null; + + [Desc("Terrain where the projectile explodes instead of bouncing.")] + public readonly HashSet InvalidBounceTerrain = new(); + + [Desc("Modify distance of each bounce by this percentage of previous distance.")] + public readonly int BounceRangeModifier = 60; + + [Desc("If projectile touches an actor with one of these stances during or after the first bounce, trigger explosion.")] + public readonly PlayerRelationship ValidBounceBlockerStances = PlayerRelationship.Enemy | PlayerRelationship.Neutral; + + [Desc("Altitude above terrain below which to explode. Zero effectively deactivates airburst.")] + public readonly WDist AirburstAltitude = WDist.Zero; + + [Desc("Altitude where this bullet should explode when reached.", + "Negative values allow this bullet to pass cliffs and terrain bumps.")] + public readonly WDist ExplodeUnderThisAltitude = new(-1536); + + [Desc("Interval in ticks between each spawned Trail animation.")] + public readonly int TrailInterval = 2; + + [Desc("Delay in ticks until trail animation is spawned.")] + public readonly int TrailDelay = 1; + + [Desc("Palette used to render the trail sequence.")] + [PaletteReference(nameof(TrailUsePlayerPalette))] + public readonly string TrailPalette = "effect"; + + [Desc("Use the Player Palette to render the trail sequence.")] + public readonly bool TrailUsePlayerPalette = false; + + [Desc("Types of point defense weapons that can target this projectile.")] + public readonly BitSet PointDefenseTypes = default; + + [Desc("When set, display a line behind the actor. Length is measured in ticks after appearing.")] + public readonly int ContrailLength = 0; + + [Desc("Time (in ticks) after which the line should appear. Controls the distance to the actor.")] + public readonly int ContrailDelay = 1; + + [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")] + public readonly int ContrailZOffset = 2047; + + [Desc("Thickness of the emitted line at the start of the contrail.")] + public readonly WDist ContrailStartWidth = new(64); + + [Desc("Thickness of the emitted line at the end of the contrail. Will default to " + nameof(ContrailStartWidth) + " if left undefined")] + public readonly WDist? ContrailEndWidth = null; + + [Desc("RGB color at the contrail start.")] + public readonly Color ContrailStartColor = Color.White; + + [Desc("Use player remap color instead of a custom color at the contrail the start.")] + public readonly bool ContrailStartColorUsePlayerColor = false; + + [Desc("The alpha value [from 0 to 255] of color at the contrail the start.")] + public readonly int ContrailStartColorAlpha = 255; + + [Desc("RGB color at the contrail end. Will default to " + nameof(ContrailStartColor) + " if left undefined")] + public readonly Color? ContrailEndColor; + + [Desc("Use player remap color instead of a custom color at the contrail end.")] + public readonly bool ContrailEndColorUsePlayerColor = false; + + [Desc("The alpha value [from 0 to 255] of color at the contrail end.")] + public readonly int ContrailEndColorAlpha = 0; + + public IProjectile Create(ProjectileArgs args) { return new BulletAS(this, args); } + } + + public class BulletAS : IProjectile, ISync + { + readonly BulletASInfo info; + readonly ProjectileArgs args; + readonly Animation anim; + + readonly float3 shadowColor; + readonly float shadowAlpha; + + [Sync] + readonly WAngle angle; + [Sync] + readonly WDist speed; + [Sync] + readonly WAngle facing; + + readonly string trailPalette; + readonly string paletteName; + + readonly ContrailRenderable contrail; + + [Sync] + WPos pos, lastPos, target, source; + int length; + int ticks, smokeTicks; + int remainingBounces; + + public Actor SourceActor { get { return args.SourceActor; } } + + public BulletAS(BulletASInfo info, ProjectileArgs args) + { + this.info = info; + this.args = args; + pos = args.Source; + source = args.Source; + + var world = args.SourceActor.World; + + paletteName = info.Palette; + if (info.IsPlayerPalette) + paletteName += args.SourceActor.Owner.InternalName; + + if (info.LaunchAngle.Length > 1) + angle = new WAngle(world.SharedRandom.Next(info.LaunchAngle[0].Angle, info.LaunchAngle[1].Angle)); + else + angle = info.LaunchAngle[0]; + + if (info.Speed.Length > 1) + speed = new WDist(world.SharedRandom.Next(info.Speed[0].Length, info.Speed[1].Length)); + else + speed = info.Speed[0]; + + target = args.PassiveTarget; + if (info.Inaccuracy.Length > 0) + { + var maxInaccuracyOffset = Util.GetProjectileInaccuracy(info.Inaccuracy.Length, info.InaccuracyType, args); + target += WVec.FromPDF(world.SharedRandom, 2) * maxInaccuracyOffset / 1024; + } + + if (info.AirburstAltitude > WDist.Zero) + { + target += new WVec(WDist.Zero, WDist.Zero, info.AirburstAltitude); + } + + facing = (target - pos).Yaw; + length = Math.Max((target - pos).Length / speed.Length, 1); + + if (!string.IsNullOrEmpty(info.Image)) + { + anim = new Animation(world, info.Image, new Func(GetEffectiveFacing)); + anim.PlayRepeating(info.Sequences.Random(world.SharedRandom)); + } + + if (info.ContrailLength > 0) + { + var startcolor = info.ContrailStartColorUsePlayerColor ? Color.FromArgb(info.ContrailStartColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailStartColorAlpha, info.ContrailStartColor); + var endcolor = info.ContrailEndColorUsePlayerColor ? Color.FromArgb(info.ContrailEndColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailEndColorAlpha, info.ContrailEndColor ?? startcolor); + contrail = new ContrailRenderable(world, startcolor, endcolor, info.ContrailStartWidth, info.ContrailEndWidth ?? info.ContrailStartWidth, info.ContrailLength, info.ContrailDelay, info.ContrailZOffset); + } + + trailPalette = info.TrailPalette; + if (info.TrailUsePlayerPalette) + trailPalette += args.SourceActor.Owner.InternalName; + + smokeTicks = info.TrailDelay; + remainingBounces = info.BounceCount; + + shadowColor = new float3(info.ShadowColor.R, info.ShadowColor.G, info.ShadowColor.B) / 255f; + shadowAlpha = info.ShadowColor.A / 255f; + } + + WAngle GetEffectiveFacing() + { + var at = (float)ticks / (length - 1); + var attitude = angle.Tan() * (1 - 2 * at) / (4 * 1024); + + var u = facing.Angle % 512 / 512f; + var scale = 2048 * u * (1 - u); + + var effective = (int)(facing.Angle < 512 + ? facing.Angle - scale * attitude + : facing.Angle + scale * attitude); + + return new WAngle(effective); + } + + public void Tick(World world) + { + anim?.Tick(); + + lastPos = pos; + pos = WPos.LerpQuadratic(source, target, angle, ticks, length); + + if (ShouldExplode(world)) + Explode(world); + } + + bool ShouldExplode(World world) + { + if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, args.SourceActor.Owner, lastPos, pos, info.Width, out var blockedPos)) + { + pos = blockedPos; + return true; + } + + if (!string.IsNullOrEmpty(info.TrailImage) && --smokeTicks < 0) + { + var delayedPos = WPos.LerpQuadratic(source, target, angle, ticks - info.TrailDelay, length); + world.AddFrameEndTask(w => w.Add(new SpriteEffect(delayedPos, GetEffectiveFacing(), w, + info.TrailImage, info.TrailSequences.Random(world.SharedRandom), trailPalette))); + + smokeTicks = info.TrailInterval; + } + + if (info.ContrailLength > 0) + contrail.Update(pos); + + var flightLengthReached = ticks++ >= length; + var shouldBounce = remainingBounces > 0; + + if (flightLengthReached && shouldBounce) + { + var cell = world.Map.CellContaining(pos); + if (!world.Map.Contains(cell)) + return true; + + if (info.InvalidBounceTerrain.Contains(world.Map.GetTerrainInfo(cell).Type)) + return true; + + if (AnyValidTargetsInRadius(world, pos, info.Width, args.SourceActor, true)) + return true; + + target += (pos - source) * info.BounceRangeModifier / 100; + var dat = world.Map.DistanceAboveTerrain(target); + target += new WVec(0, 0, -dat.Length); + length = Math.Max((target - pos).Length / speed.Length, 1); + + ticks = 0; + source = pos; + Game.Sound.Play(SoundType.World, info.BounceSound, source); + remainingBounces--; + } + + // Flight length reached / exceeded + if (flightLengthReached && !shouldBounce) + return true; + + // Driving into cell with different height level + if (world.Map.DistanceAboveTerrain(pos) < info.ExplodeUnderThisAltitude) + return true; + + // After first bounce, check for targets each tick + if (remainingBounces < info.BounceCount && AnyValidTargetsInRadius(world, pos, info.Width, args.SourceActor, true)) + return true; + + if (!info.PointDefenseTypes.IsEmpty && world.ActorsWithTrait().Any(x => x.Trait.Destroy(pos, args.SourceActor.Owner, info.PointDefenseTypes))) + return true; + + return false; + } + + public IEnumerable Render(WorldRenderer wr) + { + if (info.ContrailLength > 0) + yield return contrail; + + if (anim == null || ticks >= length) + yield break; + + var world = args.SourceActor.World; + if (!world.FogObscures(pos)) + { + var palette = wr.Palette(paletteName); + + if (info.Shadow) + { + var dat = world.Map.DistanceAboveTerrain(pos); + var shadowPos = pos - new WVec(0, 0, dat.Length); + foreach (var r in anim.Render(shadowPos, palette)) + yield return ((IModifyableRenderable)r) + .WithTint(shadowColor, ((IModifyableRenderable)r).TintModifiers | TintModifiers.ReplaceColor) + .WithAlpha(shadowAlpha); + } + + foreach (var r in anim.Render(pos, palette)) + yield return r; + } + } + + void Explode(World world) + { + if (info.ContrailLength > 0) + world.AddFrameEndTask(w => w.Add(new ContrailFader(pos, contrail))); + + world.AddFrameEndTask(w => w.Remove(this)); + + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(lastPos, pos), args.Facing), + ImpactPosition = pos, + }; + + args.Weapon.Impact(Target.FromPos(pos), warheadArgs); + } + + bool AnyValidTargetsInRadius(World world, WPos pos, WDist radius, Actor firedBy, bool checkTargetType) + { + foreach (var victim in world.FindActorsInCircle(pos, radius)) + { + if (checkTargetType && !Target.FromActor(victim).IsValidFor(firedBy)) + continue; + + if (!info.ValidBounceBlockerStances.HasRelationship(firedBy.Owner.RelationshipWith(victim.Owner))) + continue; + + // If the impact position is within any actor's HitShape, we have a direct hit + var activeShapes = victim.TraitsImplementing().Where(Exts.IsTraitEnabled); + if (activeShapes.Any(i => i.DistanceFromEdge(victim, pos).Length <= 0)) + return true; + } + + return false; + } + } +} diff --git a/OpenRA.Mods.AS/Duplicates/Projectiles/MissileTA.cs b/OpenRA.Mods.AS/Duplicates/Projectiles/MissileTA.cs new file mode 100644 index 000000000000..084e9b5ddb62 --- /dev/null +++ b/OpenRA.Mods.AS/Duplicates/Projectiles/MissileTA.cs @@ -0,0 +1,1152 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; +using Util = OpenRA.Mods.Common.Util; + +namespace OpenRA.Mods.TA.Projectiles +{ + [Desc("Missile used by TA")] + public class MissileTAInfo : IProjectileInfo + { + [Desc("Name of the image containing the projectile sequence.")] + public readonly string Image = null; + + [SequenceReference(nameof(Image), allowNullImage: true)] + [Desc("Loop a randomly chosen sequence of Image from this list while this projectile is moving.")] + public readonly string[] Sequences = { "idle" }; + + [PaletteReference(nameof(IsPlayerPalette))] + [Desc("Palette used to render the projectile sequence.")] + public readonly string Palette = "effect"; + + [Desc("Palette is a player palette BaseName")] + public readonly bool IsPlayerPalette = false; + + [Desc("Does this projectile have a shadow?")] + public readonly bool Shadow = false; + + [Desc("Color to draw shadow if Shadow is true.")] + public readonly Color ShadowColor = Color.FromArgb(140, 0, 0, 0); + + [Desc("Minimum vertical launch angle (pitch).")] + public readonly WAngle MinimumLaunchAngle = new(-64); + + [Desc("Maximum vertical launch angle (pitch).")] + public readonly WAngle MaximumLaunchAngle = new(128); + + [Desc("Minimum launch speed in WDist / tick. Defaults to Speed if -1.")] + public readonly WDist MinimumLaunchSpeed = new(-1); + + [Desc("Maximum launch speed in WDist / tick. Defaults to Speed if -1.")] + public readonly WDist MaximumLaunchSpeed = new(-1); + + [Desc("Maximum projectile speed in WDist / tick")] + public readonly WDist Speed = new(384); + + [Desc("Projectile acceleration when propulsion activated.")] + public readonly WDist Acceleration = new(5); + + public readonly bool CanSlowDown = false; + + [Desc("Make missile become normal on slope.")] + public readonly int LookaheadDistanceRate = 4; + + [Desc("Make missile become normal on slope.")] + public readonly int LookaheadStepSize = 32; + + [Desc("Make missile give up locking after some ticks.")] + public readonly int LockOnLoopCount = 3; + + [Desc("What types of targets are locked by this missile. Leaves empty to lock on all target.")] + public readonly BitSet LockOnTargets = default; + + [Desc("Is the missile blocked by actors with BlocksProjectiles: trait.")] + public readonly bool Blockable = true; + + [Desc("Is this blocked by actors with BlocksEnemyProjectiles trait.")] + public readonly bool EnemyBlockable = true; + + [Desc("Is the missile aware of terrain height levels. Only needed for mods with real, non-visual height levels.")] + public readonly bool TerrainHeightAware = true; + + [Desc("Width of projectile (used for finding blocking actors).")] + public readonly WDist Width = new(1024); + + [Desc("The maximum/constant/incremental inaccuracy used in conjunction with the InaccuracyType property.")] + public readonly WDist Inaccuracy = WDist.Zero; + + [Desc("Controls the way inaccuracy is calculated. Possible values are 'Maximum' - scale from 0 to max with range, 'PerCellIncrement' - scale from 0 with range and 'Absolute' - use set value regardless of range.")] + public readonly InaccuracyType InaccuracyType = InaccuracyType.Absolute; + + [Desc("Inaccuracy override when sucessfully locked onto target. Defaults to Inaccuracy if negative.")] + public readonly WDist LockOnInaccuracy = new(-1); + + [Desc("Probability of locking onto and following target.")] + public readonly int LockOnProbability = 100; + + [Desc("Horizontal rate of turn.")] + public readonly WAngle HorizontalRateOfTurn = new(20); + + [Desc("Reach horizontal rate of turn as speed reach the maxSpeed.")] + public readonly WAngle HorizontalRateOfTurnAcceleration = new(4); + + [Desc("Reach horizontal rate of turn as speed reach the maxSpeed.")] + public readonly WAngle HorizontalRateOfTurnStart = new(8); + + [Desc("Vertical rate of turn.")] + public readonly WAngle VerticalRateOfTurn = new(24); + + [Desc("Gravity applied while in free fall.")] + public readonly int Gravity = 10; + + [Desc("Run out of fuel after covering this distance. Zero for defaulting to weapon range. Negative for unlimited fuel.")] + public readonly WDist RangeLimit = WDist.Zero; + + [Desc("Explode when running out of fuel.")] + public readonly bool ExplodeWhenEmpty = false; + + [Desc("Altitude above terrain below which to explode. Zero effectively deactivates airburst.")] + public readonly WDist AirburstAltitude = WDist.Zero; + + [Desc("Cruise altitude. Zero means no cruise altitude used.")] + public readonly WDist CruiseAltitude = WDist.Zero; + + [Desc("Activate homing mechanism after this many ticks.")] + public readonly int HomingActivationDelay = 0; + + [Desc("Ignition after this many ticks.")] + public readonly int ActivationDelay = 0; + + [Desc("Image that contains the jet animation")] + public readonly string JetImage = null; + + [SequenceReference(nameof(JetImage), allowNullImage: true)] + [Desc("Loop a randomly chosen sequence of JetImage from this list while this projectile is moving.")] + public readonly string[] JetSequences = { "idle" }; + + [PaletteReference(nameof(JetUsePlayerPalette))] + [Desc("Palette used to render the jet sequence. ")] + public readonly string JetPalette = "effect"; + + [Desc("Use the Player Palette to render the jet sequence.")] + public readonly bool JetUsePlayerPalette = false; + + [Desc("Image that contains the trail animation.")] + public readonly string TrailImage = null; + + [SequenceReference(nameof(TrailImage), allowNullImage: true)] + [Desc("Loop a randomly chosen sequence of TrailImage from this list while this projectile is moving.")] + public readonly string[] TrailSequences = { "idle" }; + + [PaletteReference(nameof(TrailUsePlayerPalette))] + [Desc("Palette used to render the trail sequence.")] + public readonly string TrailPalette = "effect"; + + [Desc("Use the Player Palette to render the trail sequence.")] + public readonly bool TrailUsePlayerPalette = false; + + [Desc("Interval in ticks between spawning trail animation.")] + public readonly int TrailInterval = 2; + + [Desc("Should trail animation be spawned when the propulsion is not activated.")] + public readonly bool TrailWhenDeactivated = false; + + [Desc("Should missile targeting be thrown off by nearby actors with JamsMissiles.")] + public readonly bool Jammable = false; + + [Desc("Range of facings by which jammed missiles can stray from current path.")] + public readonly int JammedDiversionRange = 256; + + [Desc("When jammed, turn VFacing to this value when VFacing is bigger than this value. Value from -1023(downward) to 1023(upward).")] + public readonly int JammedVFacing = 0; + + [Desc("Image that contains the jet animation")] + public readonly string JammedEffectImage = null; + + [SequenceReference(nameof(JammedEffectImage), allowNullImage: true)] + [Desc("Loop a randomly chosen sequence of JetImage from this list while this projectile is moving.")] + public readonly string JammedEffectSequence = "idle"; + + [Desc("Palette used to render the jet sequence. ")] + public readonly string JammedEffectPalette = "effect"; + + [Desc("Types of point defense weapons that can target this projectile.")] + public readonly BitSet PointDefenseTypes = default; + + [Desc("Explodes when leaving the following terrain type, e.g., Water for torpedoes.")] + public readonly string BoundToTerrainType = ""; + + [Desc("Allow the missile to snap to the target, meaning jumping to the target immediately when", + "the missile enters the radius of the current speed around the target.")] + public readonly bool AllowSnapping = false; + + [Desc("Explodes when inside this proximity radius to target.")] + public readonly WDist CloseEnough = new(298); + + [Desc("Altitude where this bullet should explode when reached.", + "Negative values allow this bullet to pass cliffs and terrain bumps.")] + public readonly WDist ExplodeUnderThisAltitude = new(-1536); + + [Desc("Allow when missile is jammed or shut down, set ExplodeUnderThisAltitude to zero")] + public readonly bool ResetExplodeAltitudeWhenJammedOrShutDown = true; + + [Desc("When set, display a line behind the actor. Length is measured in ticks after appearing.")] + public readonly int ContrailLength = 0; + + [Desc("Time (in ticks) after which the line should appear. Controls the distance to the actor.")] + public readonly int ContrailDelay = 1; + + [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")] + public readonly int ContrailZOffset = 2047; + + [Desc("Thickness of the emitted line at the start of the contrail.")] + public readonly WDist ContrailStartWidth = new(64); + + [Desc("Thickness of the emitted line at the end of the contrail. Will default to " + nameof(ContrailStartWidth) + " if left undefined")] + public readonly WDist? ContrailEndWidth = null; + + [Desc("RGB color at the contrail start.")] + public readonly Color ContrailStartColor = Color.White; + + [Desc("Use player remap color instead of a custom color at the contrail the start.")] + public readonly bool ContrailStartColorUsePlayerColor = false; + + [Desc("The alpha value [from 0 to 255] of color at the contrail the start.")] + public readonly int ContrailStartColorAlpha = 255; + + [Desc("RGB color at the contrail end. Will default to " + nameof(ContrailStartColor) + " if left undefined")] + public readonly Color? ContrailEndColor; + + [Desc("Use player remap color instead of a custom color at the contrail end.")] + public readonly bool ContrailEndColorUsePlayerColor = false; + + [Desc("The alpha value [from 0 to 255] of color at the contrail end.")] + public readonly int ContrailEndColorAlpha = 0; + + public IProjectile Create(ProjectileArgs args) { return new MissileTA(this, args); } + } + + // TODO: double check square roots!!! + public class MissileTA : IProjectile, ISync + { + enum States + { + Freefall, + Homing, + Hitting, + } + + readonly MissileTAInfo info; + readonly ProjectileArgs args; + readonly Animation anim; + readonly Animation jetanim; + readonly Animation jammedanim; + readonly WVec gravity; + readonly int minLaunchSpeed; + readonly int maxLaunchSpeed; + readonly int maxSpeed; + readonly long closeEnoughLengthSquare; + readonly WAngle minLaunchAngle; + readonly WAngle maxLaunchAngle; + WDist cruiseHt; + int ticks; + + int ticksToNextSmoke; + readonly ContrailRenderable contrail; + readonly string trailPalette; + + States state; + bool slowDown; + bool ignite; + bool shutDown; + bool targetPassedBy; + bool allowPassBy; // TODO: use this also with high minimum launch angle settings + bool jammed; + readonly bool lockOn; + readonly World world; + WPos targetPosition; + readonly WVec offset; + + WVec tarVel; + WVec predVel; + + readonly float3 shadowColor; + readonly float shadowAlpha; + + [Sync] + WPos pos; + + WVec velocity; + int speed; + int loopRadius; + int explodeAltitude; + WDist distanceCovered; + readonly WDist rangeLimit; + WAngle currentHorizontalRateOfTurn; + + WAngle renderFacing; + + [Sync] + int hFacing; + + [Sync] + int vFacing; + + public MissileTA(MissileTAInfo info, ProjectileArgs args) + { + this.info = info; + this.args = args; + pos = args.Source; + hFacing = args.Facing.Facing; + gravity = new WVec(0, 0, -info.Gravity); + targetPosition = args.PassiveTarget; + var limit = info.RangeLimit != WDist.Zero ? info.RangeLimit : args.Weapon.Range; + rangeLimit = new WDist(Util.ApplyPercentageModifiers(limit.Length, args.RangeModifiers)); + minLaunchSpeed = info.MinimumLaunchSpeed.Length > -1 ? info.MinimumLaunchSpeed.Length : info.Speed.Length; + maxLaunchSpeed = info.MaximumLaunchSpeed.Length > -1 ? info.MaximumLaunchSpeed.Length : info.Speed.Length; + maxSpeed = info.Speed.Length; + minLaunchAngle = info.MinimumLaunchAngle; + maxLaunchAngle = info.MaximumLaunchAngle; + closeEnoughLengthSquare = (long)info.CloseEnough.Length * info.CloseEnough.Length; + explodeAltitude = info.ExplodeUnderThisAltitude.Length; + + world = args.SourceActor.World; + + cruiseHt = WDist.Zero; + if (info.CruiseAltitude == WDist.Zero) + cruiseHt = world.Map.DistanceAboveTerrain(pos); + else + cruiseHt = info.CruiseAltitude; + + currentHorizontalRateOfTurn = info.HorizontalRateOfTurnStart; + + // Hack: OpenRA consider "GuidedTarget.Actor == null" is a valid lock on terrain, + // the missile does not lock on will lose air burst ability. + var validlocked = args.GuidedTarget.Actor == null || info.LockOnTargets.IsEmpty || info.LockOnTargets.Overlaps(args.GuidedTarget.Actor.GetEnabledTargetTypes()); + if (validlocked && world.SharedRandom.Next(100) <= info.LockOnProbability) + lockOn = true; + + var inaccuracy = lockOn && info.LockOnInaccuracy.Length > -1 ? info.LockOnInaccuracy.Length : info.Inaccuracy.Length; + if (inaccuracy > 0) + { + var maxInaccuracyOffset = Util.GetProjectileInaccuracy(inaccuracy, info.InaccuracyType, args); + offset = WVec.FromPDF(world.SharedRandom, 2) * maxInaccuracyOffset / 1024; + } + + DetermineLaunchSpeedAndAngle(world, out speed, out vFacing); + + velocity = new WVec(0, -speed, 0) + .Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero)) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing))); + + if (!string.IsNullOrEmpty(info.Image)) + { + anim = new Animation(world, info.Image, () => renderFacing); + anim.PlayRepeating(info.Sequences.Random(world.SharedRandom)); + } + + if (!string.IsNullOrEmpty(info.JetImage)) + { + jetanim = new Animation(world, info.JetImage); + jetanim.PlayRepeating(info.JetSequences.Random(world.SharedRandom)); + } + + if (!string.IsNullOrEmpty(info.JammedEffectImage)) + { + jammedanim = new Animation(world, info.JammedEffectImage); + jammedanim.Play(info.JammedEffectSequence); + } + + trailPalette = info.TrailPalette; + if (info.TrailUsePlayerPalette) + trailPalette += args.SourceActor.Owner.InternalName; + + // ignition + if (info.ContrailLength > 0) + { + var startcolor = info.ContrailStartColorUsePlayerColor ? Color.FromArgb(info.ContrailStartColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailStartColorAlpha, info.ContrailStartColor); + var endcolor = info.ContrailEndColorUsePlayerColor ? Color.FromArgb(info.ContrailEndColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailEndColorAlpha, info.ContrailEndColor ?? startcolor); + contrail = new ContrailRenderable(world, startcolor, endcolor, info.ContrailStartWidth, info.ContrailEndWidth ?? info.ContrailStartWidth, info.ContrailLength, info.ContrailDelay, info.ContrailZOffset); + } + + shadowColor = new float3(info.ShadowColor.R, info.ShadowColor.G, info.ShadowColor.B) / 255f; + shadowAlpha = info.ShadowColor.A / 255f; + } + + static int LoopRadius(int speed, int rot) + { + // loopRadius in w-units = speed in w-units per tick / angular speed in radians per tick + // angular speed in radians per tick = rot in facing units per tick * (pi radians / 128 facing units) + // pi = 314 / 100 + // ==> loopRadius = (speed * 128 * 100) / (314 * rot) + return speed * 6400 / (157 * rot); + } + + void DetermineLaunchSpeedAndAngleForIncline(int predClfDist, int diffClfMslHgt, int relTarHorDist, + out int speed, out int vFacing) + { + speed = maxLaunchSpeed; + + // Find smallest vertical facing, for which the missile will be able to climb terrAltDiff w-units + // within hHeightChange w-units all the while ending the ascent with vertical facing 0 + vFacing = maxLaunchAngle.Angle >> 2; + + // Compute minimum speed necessary to both be able to face directly upwards and have enough space + // to hit the target without passing it by (and thus having to do horizontal loops) + var minSpeed = (Math.Min(predClfDist * 1024 / (1024 - WAngle.FromFacing(vFacing).Sin()), + (relTarHorDist + predClfDist) * 1024 / (2 * (2048 - WAngle.FromFacing(vFacing).Sin()))) + * info.VerticalRateOfTurn.Facing * 157 / 6400).Clamp(minLaunchSpeed, maxLaunchSpeed); + + if ((sbyte)vFacing < 0) + speed = minSpeed; + else if (!WillClimbWithinDistance(vFacing, loopRadius, predClfDist, diffClfMslHgt) + && !WillClimbAroundInclineTop(vFacing, loopRadius, predClfDist, diffClfMslHgt)) + { + // Find highest speed greater than the above minimum that allows the missile + // to surmount the incline + var vFac = vFacing; + speed = BisectionSearch(minSpeed, maxLaunchSpeed, spd => + { + var lpRds = LoopRadius(spd, info.VerticalRateOfTurn.Facing); + return WillClimbWithinDistance(vFac, lpRds, predClfDist, diffClfMslHgt) + || WillClimbAroundInclineTop(vFac, lpRds, predClfDist, diffClfMslHgt); + }); + } + else + { + // Find least vertical facing that will allow the missile to climb + // terrAltDiff w-units within hHeightChange w-units + // all the while ending the ascent with vertical facing 0 + vFacing = BisectionSearch(Math.Max((sbyte)(minLaunchAngle.Angle >> 2), (sbyte)0), + (sbyte)(maxLaunchAngle.Angle >> 2), + vFac => !WillClimbWithinDistance(vFac, loopRadius, predClfDist, diffClfMslHgt)) + 1; + } + } + + // TODO: Double check Launch parameter determination + void DetermineLaunchSpeedAndAngle(World world, out int speed, out int vFacing) + { + speed = maxLaunchSpeed; + loopRadius = LoopRadius(speed, info.VerticalRateOfTurn.Facing); + + // Compute current distance from target position + var tarDistVec = targetPosition + offset - pos; + var relTarHorDist = tarDistVec.HorizontalLength; + + var predClfHgt = 0; + var predClfDist = 0; + var lastHt = 0; + if (info.TerrainHeightAware) + InclineLookahead(world, relTarHorDist, out predClfHgt, out predClfDist, out _, out lastHt); + + // Height difference between the incline height and missile height + var diffClfMslHgt = predClfHgt - pos.Z; + + // Incline coming up + if (info.TerrainHeightAware && diffClfMslHgt >= 0 && predClfDist > 0) + DetermineLaunchSpeedAndAngleForIncline(predClfDist, diffClfMslHgt, relTarHorDist, out speed, out vFacing); + else if (lastHt != 0) + { + vFacing = Math.Max((sbyte)(minLaunchAngle.Angle >> 2), (sbyte)0); + speed = maxLaunchSpeed; + } + else + { + // Set vertical facing so that the missile faces its target + var vDist = new WVec(-tarDistVec.Z, -relTarHorDist, 0); + vFacing = (sbyte)vDist.Yaw.Facing; + + // Do not accept -1 as valid vertical facing since it is usually a numerical error + // and will lead to premature descent and crashing into the ground + if (vFacing == -1) + vFacing = 0; + + // Make sure the chosen vertical facing adheres to prescribed bounds + vFacing = vFacing.Clamp((sbyte)(minLaunchAngle.Angle >> 2), + (sbyte)(maxLaunchAngle.Angle >> 2)); + } + } + + // Will missile be able to climb terrAltDiff w-units within hHeightChange w-units + // all the while ending the ascent with vertical facing 0 + // Calling this function only makes sense when vFacing is nonnegative + static bool WillClimbWithinDistance(int vFacing, int loopRadius, int predClfDist, int diffClfMslHgt) + { + // Missile's horizontal distance from loop's center + var missDist = loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024; + + // Missile's height below loop's top + var missHgt = loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024; + + // Height that would be climbed without changing vertical facing + // for a horizontal distance hHeightChange - missDist + var hgtChg = (predClfDist - missDist) * WAngle.FromFacing(vFacing).Tan() / 1024; + + // Check if total manoeuvre height enough to overcome the incline's height + return hgtChg + missHgt >= diffClfMslHgt; + } + + // This function checks if the missile's vertical facing is + // nonnegative, and the incline top's horizontal distance from the missile is + // less than loopRadius * (1024 - WAngle.FromFacing(vFacing).Sin()) / 1024 + static bool IsNearInclineTop(int vFacing, int loopRadius, int predClfDist) + { + return vFacing >= 0 && predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFacing).Sin()) / 1024; + } + + // Will missile climb around incline top if bringing vertical facing + // down to zero on an arc of radius loopRadius + // Calling this function only makes sense when IsNearInclineTop returns true + static bool WillClimbAroundInclineTop(int vFacing, int loopRadius, int predClfDist, int diffClfMslHgt) + { + // Vector from missile's current position pointing to the loop's center + var radius = new WVec(loopRadius, 0, 0) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(Math.Max(0, 64 - vFacing)))); + + // Vector from loop's center to incline top + 64 hardcoded in height buffer zone + var topVector = new WVec(predClfDist, diffClfMslHgt + 64, 0) - radius; + + // Check if incline top inside of the vertical loop + return topVector.Length <= loopRadius; + } + + static int BisectionSearch(int lowerBound, int upperBound, Func testCriterion) + { + // Assuming that there exists an integer N between lowerBound and upperBound + // for which testCriterion returns true as well as all integers less than N, + // and for which testCriterion returns false for all integers greater than N, + // this function finds N. + while (upperBound - lowerBound > 1) + { + var middle = (upperBound + lowerBound) / 2; + + if (testCriterion(middle)) + lowerBound = middle; + else + upperBound = middle; + } + + return lowerBound; + } + + bool JammedBy(TraitPair tp) + { + if ((tp.Actor.CenterPosition - pos).HorizontalLengthSquared > tp.Trait.Range.LengthSquared) + return false; + + if (!tp.Trait.DeflectionStances.HasRelationship(tp.Actor.Owner.RelationshipWith(args.SourceActor.Owner))) + return false; + + return tp.Actor.World.SharedRandom.Next(100) < tp.Trait.Chance; + } + + void ChangeSpeed(int sign = 1) + { + speed = (speed + sign * info.Acceleration.Length).Clamp(0, maxSpeed); + + // Compute the vertical loop radius + loopRadius = LoopRadius(speed, info.VerticalRateOfTurn.Facing); + } + + WVec FreefallTick() + { + // Compute the projectile's freefall displacement + var move = velocity + gravity / 2; + velocity += gravity; + var velRatio = maxSpeed * 1024 / velocity.Length; + if (velRatio < 1024) + velocity = velocity * velRatio / 1024; + + return move; + } + + // Adjust missile direction as the terrain ahead + void InclineLookahead(World world, int distCheck, out int predClfHgt, out int predClfDist, out int lastHtChg, out int lastHt) + { + predClfHgt = 0; // Highest probed terrain height + predClfDist = 0; // Distance from highest point + lastHtChg = 0; // Distance from last time the height changes + lastHt = 0; // Height just before the last height change + + var step = new WVec(0, -info.LookaheadStepSize, 0) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing))); // Step vector of length 128 + + // Probe terrain ahead of the missile + // NOTE: Might be desired to unhardcode maximum lookahead distance + var maxLookaheadDistance = loopRadius * info.LookaheadDistanceRate; + var posProbe = pos; + var curDist = 0; + var tickLimit = Math.Min(maxLookaheadDistance, distCheck) / info.LookaheadStepSize; + var prevHt = 0; + + // TODO: Make sure cell on map!!! + for (var tick = 0; tick <= tickLimit; tick++) + { + posProbe += step; + if (!world.Map.Contains(world.Map.CellContaining(posProbe))) + break; + + // ÕâÀïÉæ¼°µ½oraµÄ¸ß¶È·½ÏòÒ»²ãµÄÖµÊǶàÉÙ£¬ÓÉÓÚoraµÄÖÇÕÏËã·¨£¬ÕâÀïÖ»ÄÜÔÝʱȡ´íÎóµÄÖµ724 + // There is an expample of what is happenning when OpenRA foolishly use 724 as height for isometric map, + // so we have to hardcode height as the incorrect number of 724 + var ht = world.Map.Height[world.Map.CellContaining(posProbe)] * 724; + + curDist += info.LookaheadStepSize; + if (ht > predClfHgt) + { + predClfHgt = ht; + predClfDist = curDist; + } + + if (prevHt != ht) + { + lastHtChg = curDist; + lastHt = prevHt; + prevHt = ht; + } + } + } + + int IncreaseAltitude(int predClfDist, int diffClfMslHgt, int vFacing) + { + var desiredVFacing = vFacing; + + // If missile is below incline top height and facing downwards, bring back + // its vertical facing above zero as soon as possible + if ((sbyte)vFacing < 0) + desiredVFacing = info.VerticalRateOfTurn.Facing; + + // Missile will climb around incline top if bringing vertical facing + // down to zero on an arc of radius loopRadius + else if (IsNearInclineTop(vFacing, loopRadius, predClfDist) + && WillClimbAroundInclineTop(vFacing, loopRadius, predClfDist, diffClfMslHgt)) + desiredVFacing = 0; + + // Missile will not climb terrAltDiff w-units within hHeightChange w-units + // all the while ending the ascent with vertical facing 0 + else if (!WillClimbWithinDistance(vFacing, loopRadius, predClfDist, diffClfMslHgt)) + + // Find smallest vertical facing, attainable in the next tick, + // for which the missile will be able to climb terrAltDiff w-units + // within hHeightChange w-units all the while ending the ascent + // with vertical facing 0 + for (var vFac = Math.Min(vFacing + info.VerticalRateOfTurn.Facing - 1, 63); vFac >= vFacing; vFac--) + if (!WillClimbWithinDistance(vFac, loopRadius, predClfDist, diffClfMslHgt) + && !(predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFac).Sin()) / 1024 + && WillClimbAroundInclineTop(vFac, loopRadius, predClfDist, diffClfMslHgt))) + { + desiredVFacing = vFac + 1; + break; + } + + // Attained height after ascent as predicted from upper part of incline surmounting manoeuvre + /* + var predAttHght = loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024 - diffClfMslHgt; + */ + + // Should the missile be slowed down in order to make it more manoeuverable + /* + var slowDown = info.CanSlowDown && info.Acceleration.Length != 0 // Possible to decelerate + && ((desiredVFacing != 0 // Lower part of incline surmounting manoeuvre + + // Incline will be hit before vertical facing attains 64 + && (predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFacing).Sin()) / 1024 + + // When evaluating this the incline will be *not* be hit before vertical facing attains 64 + // At current speed target too close to hit without passing it by + || relTarHorDist <= 2 * loopRadius * (2048 - WAngle.FromFacing(vFacing).Sin()) / 1024 - predClfDist)) + + || (desiredVFacing == 0 // Upper part of incline surmounting manoeuvre + && relTarHorDist <= loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024 + + Exts.ISqrt(predAttHght * (2 * loopRadius - predAttHght)))); // Target too close to hit at current speed + + if (slowDown) + slowDown = true; + */ + + return desiredVFacing; + } + + int HomingInnerTick(int predClfDist, int diffClfMslHgt, int relTarHorDist, int lastHtChg, int lastHt, + int relTarHgt, int vFacing, bool targetPassedBy) + { + var slowdown = false; + int desiredVFacing; + + // Incline coming up -> attempt to reach the incline so that after predClfDist + // the height above the terrain is positive but as close to 0 as possible + // Also, never change horizontal facing and never travel backwards + // Possible techniques to avoid close cliffs are deceleration, turning + // as sharply as possible to travel directly upwards and then returning + // to zero vertical facing as low as possible while still not hitting the + // high terrain. A last technique (and the preferred one, normally used when + // the missile hasn't been fired near a cliff) is simply finding the smallest + // vertical facing that allows for a smooth climb to the new terrain's height + // and coming in at predClfDist at exactly zero vertical facing + if (info.TerrainHeightAware && diffClfMslHgt >= 0 && !allowPassBy) + desiredVFacing = IncreaseAltitude(predClfDist, diffClfMslHgt, vFacing); + else if (relTarHorDist <= info.LockOnLoopCount * loopRadius || state == States.Hitting) + { + // No longer travel at cruise altitude + state = States.Hitting; + + if (lastHt >= targetPosition.Z) + allowPassBy = true; + + if (!allowPassBy && (lastHt < targetPosition.Z || targetPassedBy)) + { + // Aim for the target + var vDist = new WVec(-relTarHgt, -relTarHorDist, 0); + desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing; + + // Do not accept -1 as valid vertical facing since it is usually a numerical error + // and will lead to premature descent and crashing into the ground + if (desiredVFacing == -1) + desiredVFacing = 0; + + // If the target has been passed by, limit the absolute value of + // vertical facing by the maximum vertical rate of turn + // Do this because the missile will be looping horizontally + // and thus needs smaller vertical facings so as not + // to hit the ground prematurely + if (targetPassedBy) + desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn.Facing, info.VerticalRateOfTurn.Facing); + else if (lastHt == 0) + { // Before the target is passed by, missile speed should be changed + // Target's height above loop's center + var tarHgt = (loopRadius * WAngle.FromFacing(vFacing).Cos() / 1024 - Math.Abs(relTarHgt)).Clamp(0, loopRadius); + + // Target's horizontal distance from loop's center + var tarDist = Exts.ISqrt(loopRadius * loopRadius - tarHgt * tarHgt); + + // Missile's horizontal distance from loop's center + var missDist = loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024; + + // If the current height does not permit the missile + // to hit the target before passing it by, lower speed + // Otherwise, increase speed + if (info.CanSlowDown && relTarHorDist <= tarDist - Math.Sign(relTarHgt) * missDist) + slowdown = true; + } + } + else if (allowPassBy || (lastHt != 0 && relTarHorDist - lastHtChg < loopRadius)) + { + // Only activate this part if target too close to cliff + allowPassBy = true; + + // Vector from missile's current position pointing to the loop's center + /* + var radius = new WVec(loopRadius, 0, 0) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(64 - vFacing))); + */ + + // Vector from loop's center to incline top hardcoded in height buffer zone + /* + var edgeVector = new WVec(lastHtChg, lastHt - pos.Z, 0) - radius; + */ + + if (!targetPassedBy) + { + // Climb to critical height + if (relTarHorDist > 2 * loopRadius) + { + // Target's distance from cliff + var d1 = relTarHorDist - lastHtChg; + if (d1 < 0) + d1 = 0; + if (d1 > 2 * loopRadius) + return 0; + + // Find critical height at which the missile must be once it is at one loopRadius + // away from the target + var h1 = loopRadius - Exts.ISqrt(d1 * (2 * loopRadius - d1)) - (pos.Z - lastHt); + + if (h1 > loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024) + desiredVFacing = WAngle.ArcTan(Exts.ISqrt(h1 * (2 * loopRadius - h1 > 0 ? 2 * loopRadius - h1 : 0)), loopRadius - h1).Angle >> 2; + else + desiredVFacing = 0; + + // TODO: deceleration checks!!! + } + + /* TODO: I don't konw if we need this "Avoid the cliff edge" here, due to it makes missile behave stange on slope and cliff + * while affecting gameplay severely. + else + { + // Avoid the cliff edge + if (info.TerrainHeightAware && edgeVector.Length > loopRadius && lastHt > targetPosition.Z) + { + int vFac; + for (vFac = vFacing + 1; vFac <= vFacing + info.VerticalRateOfTurn.Facing - 1; vFac++) + { + // Vector from missile's current position pointing to the loop's center + radius = new WVec(loopRadius, 0, 0) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(64 - vFac))); + + // Vector from loop's center to incline top + 64 hardcoded in height buffer zone + edgeVector = new WVec(lastHtChg, lastHt - pos.Z, 0) - radius; + if (edgeVector.Length <= loopRadius) + break; + } + desiredVFacing = vFac; + } + else + { + // Aim for the target + var vDist = new WVec(-relTarHgt, -relTarHorDist, 0); + desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing; + if (desiredVFacing < 0 && info.VerticalRateOfTurn.Facing < (sbyte)vFacing) + desiredVFacing = 0; + } + } + */ + else + { + // Aim for the target + var vDist = new WVec(-relTarHgt, -relTarHorDist, 0); + desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing; + if (desiredVFacing < 0 && info.VerticalRateOfTurn.Facing < (sbyte)vFacing) + desiredVFacing = 0; + } + } + else + { + // Aim for the target + var vDist = new WVec(-relTarHgt, relTarHorDist, 0); + desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing; + if (desiredVFacing < 0 && info.VerticalRateOfTurn.Facing < (sbyte)vFacing) + desiredVFacing = 0; + } + } + else + { + // Aim to attain cruise altitude as soon as possible while having the absolute value + // of vertical facing bound by the maximum vertical rate of turn + if (info.CruiseAltitude == WDist.Zero) + cruiseHt = world.Map.DistanceAboveTerrain(pos); + else + cruiseHt = info.CruiseAltitude; + + var vDist = new WVec(-diffClfMslHgt - cruiseHt.Length, -speed, 0); + desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing; + + // If the missile is launched above CruiseAltitude, it has to descend instead of climbing + if (-diffClfMslHgt > cruiseHt.Length) + desiredVFacing = -desiredVFacing; + + desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn.Facing, info.VerticalRateOfTurn.Facing); + } + } + else + { + if (info.CruiseAltitude == WDist.Zero) + cruiseHt = world.Map.DistanceAboveTerrain(pos); + else + cruiseHt = info.CruiseAltitude; + + // Aim to attain cruise altitude as soon as possible while having the absolute value + // of vertical facing bound by the maximum vertical rate of turn + var vDist = new WVec(-diffClfMslHgt - cruiseHt.Length, -speed, 0); + desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing; + + // If the missile is launched above CruiseAltitude, it has to descend instead of climbing + if (-diffClfMslHgt > cruiseHt.Length) + desiredVFacing = -desiredVFacing; + + desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn.Facing, info.VerticalRateOfTurn.Facing); + } + + if (slowdown) + slowDown = true; + + return desiredVFacing; + } + + int jammedDesiredHFacing; + WVec HomingTick(World world, WVec tarDistVec, int relTarHorDist) + { + // Check whether the homing mechanism is jammed, jammed once is all jammed for PERF. + if (!jammed) + { + if (jammed = jammed || (info.Jammable && world.ActorsWithTrait().Any(JammedBy))) + { + jammedDesiredHFacing = hFacing + world.SharedRandom.Next(-info.JammedDiversionRange, info.JammedDiversionRange + 1); + if (info.ResetExplodeAltitudeWhenJammedOrShutDown) + explodeAltitude = 0; + } + else + { + var predClfHgt = 0; + var predClfDist = 0; + var lastHtChg = 0; + var lastHt = 0; + var desiredHFacing = 0; + var desiredVFacing = 0; + if (info.TerrainHeightAware) + InclineLookahead(world, relTarHorDist, out predClfHgt, out predClfDist, out lastHtChg, out lastHt); + + // Height difference between the incline height and missile height + var diffClfMslHgt = predClfHgt - pos.Z; + + // Target height relative to the missile + var relTarHgt = tarDistVec.Z; + + // Compute which direction the projectile should be facing + var velVec = tarDistVec + predVel; + desiredHFacing = velVec.HorizontalLengthSquared != 0 ? velVec.Yaw.Facing : hFacing; + + var delta = Util.NormalizeFacing(hFacing - desiredHFacing); + if (allowPassBy && delta > 64 && delta < 192) + { + desiredHFacing = (desiredHFacing + 128) & 0xFF; + targetPassedBy = true; + } + else + targetPassedBy = false; + + desiredVFacing = HomingInnerTick(predClfDist, diffClfMslHgt, relTarHorDist, lastHtChg, lastHt, + relTarHgt, vFacing, targetPassedBy); + + // The target has been passed by + if (tarDistVec.HorizontalLength < speed * WAngle.FromFacing(vFacing).Cos() / 1024) + targetPassedBy = true; + + // Compute new direction the projectile will be facing + hFacing = Util.TickFacing(hFacing, desiredHFacing, currentHorizontalRateOfTurn.Facing); + vFacing = Util.TickFacing(vFacing, desiredVFacing, info.VerticalRateOfTurn.Facing); + + currentHorizontalRateOfTurn = (currentHorizontalRateOfTurn + info.HorizontalRateOfTurnAcceleration).Angle > info.HorizontalRateOfTurn.Angle ? info.HorizontalRateOfTurn : currentHorizontalRateOfTurn + info.HorizontalRateOfTurnAcceleration; + } + } + + if (jammed) + { + hFacing = Util.TickFacing(hFacing, jammedDesiredHFacing, currentHorizontalRateOfTurn.Facing); + + // When jammed, vFacing will slowly return to 0, if vFacing is upwards. + if (vFacing > info.JammedVFacing) + vFacing = Util.TickFacing(vFacing, info.JammedVFacing, info.VerticalRateOfTurn.Facing); + } + + // Compute the projectile's guided displacement + return new WVec(0, -1024 * speed, 0) + .Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero)) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing))) + / 1024; + } + + public void Tick(World world) + { + ticks++; + anim?.Tick(); + jetanim?.Tick(); + + if (jammed) + jammedanim?.Tick(); + + // Switch from freefall mode to homing mode + if (ticks >= info.HomingActivationDelay + 1 && !ignite) + { + ignite = true; + state = States.Homing; + speed = velocity.Length; + + // Compute the vertical loop radius + loopRadius = LoopRadius(speed, info.VerticalRateOfTurn.Facing); + } + + // Switch from homing mode to freefall mode + if (rangeLimit >= WDist.Zero && distanceCovered > rangeLimit) + { + state = States.Freefall; + shutDown = true; + if (info.ResetExplodeAltitudeWhenJammedOrShutDown) + explodeAltitude = 0; + + velocity = new WVec(0, -speed, 0) + .Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero)) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing))); + } + + // Check if target position should be updated (actor visible & locked on) + var newTarPos = targetPosition; + if (args.GuidedTarget.IsValidFor(args.SourceActor) && lockOn) + newTarPos = (args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(args.Source)) + + new WVec(WDist.Zero, WDist.Zero, info.AirburstAltitude); + + // Compute target's predicted velocity vector (assuming uniform circular motion) + var yaw1 = tarVel.HorizontalLengthSquared != 0 ? tarVel.Yaw : WAngle.FromFacing(hFacing); + tarVel = newTarPos - targetPosition; + var yaw2 = tarVel.HorizontalLengthSquared != 0 ? tarVel.Yaw : WAngle.FromFacing(hFacing); + predVel = tarVel.Rotate(WRot.FromYaw(yaw2 - yaw1)); + targetPosition = newTarPos; + + // Compute current distance from target position + var tarDistVec = targetPosition + offset - pos; + var relTarDist = tarDistVec.Length; + var relTarHorDist = tarDistVec.HorizontalLength; + + // If missile can reach and hit the target when not moving, just explode at where it are. + var shouldExplode = false; + var reachAirburstRadius = false; + if (state != States.Freefall && relTarDist <= info.CloseEnough.Length) + { + shouldExplode = true; + } + else + { + WVec move; + if (state == States.Freefall) + move = FreefallTick(); + else + { + if (slowDown) + ChangeSpeed(-1); + else + ChangeSpeed(); + move = HomingTick(world, tarDistVec, relTarHorDist); + } + + renderFacing = new WVec(move.X, move.Y - move.Z, 0).Yaw; + + var lastPos = pos; + + // Move the missile and check if it hit the target + // HACK: "WVec.Length" is not cheap, should reuse the result if possible. + if (state != States.Freefall) + { + // If missile can snap and it is in the snap range, jump to target and explode + if (info.AllowSnapping && move.Length > relTarDist) + { + shouldExplode = true; + pos = targetPosition + offset; + } + else + { + pos += move; + if (!(shouldExplode = (pos - targetPosition - offset).LengthSquared <= closeEnoughLengthSquare)) + { + if (info.AirburstAltitude != WDist.Zero && (pos - targetPosition - offset).HorizontalLengthSquared <= closeEnoughLengthSquare) + reachAirburstRadius = true; + } + } + } + + // Only move the missile if Freefalling. + else + pos += move; + + // Check for walls or other blocking obstacles + if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, args.SourceActor.Owner, lastPos, pos, info.Width, out var blockedPos)) + { + pos = blockedPos; + shouldExplode = true; + } + else if (!info.PointDefenseTypes.IsEmpty && world.ActorsWithTrait().Any(a => a.Trait.Destroy(pos, args.SourceActor.Owner, info.PointDefenseTypes))) + shouldExplode = true; + + // Create the sprite trail effect + if (!string.IsNullOrEmpty(info.TrailImage) && --ticksToNextSmoke < 0 && (state != States.Freefall || info.TrailWhenDeactivated)) + { + world.AddFrameEndTask(w => w.Add(new SpriteEffect(pos, renderFacing, w, + info.TrailImage, info.TrailSequences.Random(world.SharedRandom), trailPalette))); + + ticksToNextSmoke = info.TrailInterval; + } + + if (info.ContrailLength > 0 && state != States.Freefall) + contrail.Update(pos); + + distanceCovered += new WDist(speed); + } + + var cell = world.Map.CellContaining(pos); + var height = world.Map.DistanceAboveTerrain(pos); + shouldExplode |= height.Length < explodeAltitude // Hit the ground + || (info.ExplodeWhenEmpty && rangeLimit >= WDist.Zero && distanceCovered > rangeLimit) // Ran out of fuel + || !world.Map.Contains(cell) // This also avoids an IndexOutOfRangeException in GetTerrainInfo below. + || (!string.IsNullOrEmpty(info.BoundToTerrainType) && world.Map.GetTerrainInfo(cell).Type != info.BoundToTerrainType) // Hit incompatible terrain + || (reachAirburstRadius && height.Length < info.AirburstAltitude.Length); // Airburst + + if (shouldExplode) + Explode(world); + } + + void Explode(World world) + { + if (info.ContrailLength > 0 && state != States.Freefall) + world.AddFrameEndTask(w => w.Add(new ContrailFader(pos, contrail))); + + world.AddFrameEndTask(w => w.Remove(this)); + + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, WAngle.FromFacing(vFacing), WAngle.FromFacing(hFacing)), + ImpactPosition = pos, + }; + + args.Weapon.Impact(Target.FromPos(pos), warheadArgs); + } + + public IEnumerable Render(WorldRenderer wr) + { + if (info.ContrailLength > 0) + yield return contrail; + + var world = args.SourceActor.World; + + if (!world.FogObscures(pos)) + { + if (anim != null) + { + var palette = wr.Palette(info.Palette + (info.IsPlayerPalette ? args.SourceActor.Owner.InternalName : "")); + foreach (var r in anim.Render(pos, palette)) + yield return r; + + if (info.Shadow) + { + var dat = world.Map.DistanceAboveTerrain(pos); + var shadowPos = pos - new WVec(0, 0, dat.Length); + foreach (var r in anim.Render(shadowPos, palette)) + yield return ((IModifyableRenderable)r) + .WithTint(shadowColor, ((IModifyableRenderable)r).TintModifiers | TintModifiers.ReplaceColor) + .WithAlpha(shadowAlpha); + } + } + + if (jetanim != null && !shutDown) + { + var palette = wr.Palette(info.JetPalette + (info.JetUsePlayerPalette ? args.SourceActor.Owner.InternalName : "")); + foreach (var r in jetanim.Render(pos, palette)) + yield return r; + } + + if (jammed && jammedanim != null && jammedanim.CurrentFrame < jammedanim.CurrentSequence.Length - 1) + { + var palette = wr.Palette(info.JammedEffectPalette); + foreach (var r in jammedanim.Render(pos, palette)) + yield return r; + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Duplicates/README.md b/OpenRA.Mods.AS/Duplicates/README.md new file mode 100644 index 000000000000..b0c617ce8411 --- /dev/null +++ b/OpenRA.Mods.AS/Duplicates/README.md @@ -0,0 +1,5 @@ +# The traits in these folders are OpenRA trait duplicates. + +This means they are edited traits - might be even outdated variants - of their OpenRA equvivalents, with some minor changes involved. Might not even work with everything their base traits work with properly. + +Avoid using them unless you are explicitly sure the changed traits are exactly what you're looking for. \ No newline at end of file diff --git a/OpenRA.Mods.AS/Duplicates/Traits/EjectOnDeathAS.cs b/OpenRA.Mods.AS/Duplicates/Traits/EjectOnDeathAS.cs new file mode 100644 index 000000000000..a0744fee2d89 --- /dev/null +++ b/OpenRA.Mods.AS/Duplicates/Traits/EjectOnDeathAS.cs @@ -0,0 +1,99 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Eject a ground soldier or a paratrooper while in the air. Carries over the veterancy level.")] + public class EjectOnDeathASInfo : EjectOnDeathInfo + { + [Desc("Only spawn the pilot when there is a veterancy to carry over?")] + public readonly bool SpawnOnlyWhenPromoted = true; + + public new object Create(ActorInitializer init) { return new EjectOnDeathAS(this); } + } + + class EjectOnDeathAS : ConditionalTrait, INotifyKilled + { + readonly EjectOnDeathASInfo info; + + public EjectOnDeathAS(EjectOnDeathASInfo info) + : base(info) + { + this.info = info; + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (self.Owner.WinState == WinState.Lost || !self.World.Map.Contains(self.Location)) + return; + + var r = self.World.SharedRandom.Next(1, 100); + + if (r <= 100 - Info.SuccessRate) + return; + + var cp = self.CenterPosition; + var inAir = !self.IsAtGroundLevel(); + if ((inAir && !Info.EjectInAir) || (!inAir && !Info.EjectOnGround)) + return; + + var ge = self.TraitOrDefault(); + if ((ge == null || ge.Level == 0) && info.SpawnOnlyWhenPromoted) + return; + + var pilot = self.World.CreateActor(false, Info.PilotActor.ToLowerInvariant(), + new TypeDictionary { new OwnerInit(self.Owner), new LocationInit(self.Location) }); + + var pilotPositionable = pilot.TraitOrDefault(); + var pilotCell = self.Location; + var pilotSubCell = pilotPositionable.GetAvailableSubCell(pilotCell); + if (pilotSubCell == SubCell.Invalid) + { + if (!Info.AllowUnsuitableCell) + { + pilot.Dispose(); + return; + } + + pilotSubCell = SubCell.Any; + } + + if (inAir) + { + self.World.AddFrameEndTask(w => + { + pilotPositionable.SetPosition(pilot, pilotCell, pilotSubCell); + w.Add(pilot); + + var dropPosition = pilot.CenterPosition + new WVec(0, 0, self.CenterPosition.Z - pilot.CenterPosition.Z); + pilotPositionable.SetCenterPosition(pilot, dropPosition); + pilot.QueueActivity(new Parachute(pilot)); + }); + + Game.Sound.Play(SoundType.World, Info.ChuteSound, cp); + } + else + { + self.World.AddFrameEndTask(w => + { + w.Add(pilot); + pilotPositionable.SetPosition(pilot, pilotCell, pilotSubCell); + pilot.QueueActivity(false, new Nudge(pilot)); + }); + } + } + } +} diff --git a/OpenRA.Mods.AS/Duplicates/Traits/Sound/AnnounceOnKillAS.cs b/OpenRA.Mods.AS/Duplicates/Traits/Sound/AnnounceOnKillAS.cs new file mode 100644 index 000000000000..3a1539824b1c --- /dev/null +++ b/OpenRA.Mods.AS/Duplicates/Traits/Sound/AnnounceOnKillAS.cs @@ -0,0 +1,59 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Play the Kill voice of this actor when eliminating enemies.")] + public class AnnounceOnKillASInfo : TraitInfo + { + [Desc("Minimum duration (in seconds) between sound events.")] + public readonly int Interval = 5; + + [VoiceReference] + [Desc("Voice to use when killing something.")] + public readonly string Voice = "Kill"; + + [Desc("Should the voice be played for the owner alone?")] + public readonly bool OnlyToOwner = false; + + public override object Create(ActorInitializer init) { return new AnnounceOnKillAS(this); } + } + + public class AnnounceOnKillAS : INotifyAppliedDamage + { + readonly AnnounceOnKillASInfo info; + + int lastAnnounce; + + public AnnounceOnKillAS(AnnounceOnKillASInfo info) + { + this.info = info; + lastAnnounce = -info.Interval * 25; + } + + void INotifyAppliedDamage.AppliedDamage(Actor self, Actor damaged, AttackInfo e) + { + // Don't notify suicides + if (e.DamageState == DamageState.Dead && damaged != e.Attacker) + { + if (info.OnlyToOwner && self.Owner != self.World.RenderPlayer) + return; + + if (self.World.WorldTick - lastAnnounce > info.Interval * 25) + self.PlayVoice(info.Voice); + + lastAnnounce = self.World.WorldTick; + } + } + } +} diff --git a/OpenRA.Mods.AS/Duplicates/Traits/TemporaryOwnerManagerAS.cs b/OpenRA.Mods.AS/Duplicates/Traits/TemporaryOwnerManagerAS.cs new file mode 100644 index 000000000000..c7391c441f36 --- /dev/null +++ b/OpenRA.Mods.AS/Duplicates/Traits/TemporaryOwnerManagerAS.cs @@ -0,0 +1,97 @@ +#region Copyright & License Information +/* + * Copyright 2007-2019 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Interacts with the ChangeOwner warhead.", + "Displays a bar how long this actor is affected and reverts back to the old owner on temporary changes.")] + public class TemporaryOwnerManagerASInfo : TraitInfo + { + public readonly Color BarColor = Color.Orange; + + [GrantedConditionReference] + public readonly string Condition = null; + + public override object Create(ActorInitializer init) { return new TemporaryOwnerManagerAS(init.Self, this); } + } + + public class TemporaryOwnerManagerAS : ISelectionBar, ITick, ISync, INotifyOwnerChanged + { + readonly TemporaryOwnerManagerASInfo info; + + int conditionToken = Actor.InvalidConditionToken; + + Player originalOwner; + Player changingOwner; + + [Sync] + int remaining = -1; + int duration; + + public TemporaryOwnerManagerAS(Actor self, TemporaryOwnerManagerASInfo info) + { + this.info = info; + originalOwner = self.Owner; + } + + public void ChangeOwner(Actor self, Player newOwner, int duration) + { + remaining = this.duration = duration; + changingOwner = newOwner; + self.ChangeOwner(newOwner); + + if (conditionToken == Actor.InvalidConditionToken) + conditionToken = self.GrantCondition(info.Condition); + } + + void ITick.Tick(Actor self) + { + if (!self.IsInWorld) + return; + + if (--remaining == 0) + { + changingOwner = originalOwner; + self.ChangeOwner(originalOwner); + self.CancelActivity(); // Stop shooting, you have got new enemies + + if (conditionToken != Actor.InvalidConditionToken) + conditionToken = self.RevokeCondition(conditionToken); + } + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + if (changingOwner == null || changingOwner != newOwner) + originalOwner = newOwner; // It wasn't a temporary change, so we need to update here + else + changingOwner = null; // It was triggered by this trait: reset + } + + float ISelectionBar.GetValue() + { + if (remaining <= 0) + return 0; + + return (float)remaining / duration; + } + + Color ISelectionBar.GetColor() + { + return info.BarColor; + } + + bool ISelectionBar.DisplayWhenEmpty { get { return false; } } + } +} diff --git a/OpenRA.Mods.AS/Duplicates/Warheads/ChangeOwnerASWarhead.cs b/OpenRA.Mods.AS/Duplicates/Warheads/ChangeOwnerASWarhead.cs new file mode 100644 index 000000000000..558cf44279f7 --- /dev/null +++ b/OpenRA.Mods.AS/Duplicates/Warheads/ChangeOwnerASWarhead.cs @@ -0,0 +1,95 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("Interacts with the TemporaryOwnerManager trait.")] + public class ChangeOwnerASWarhead : WarheadAS + { + [Desc("Duration of the owner change (in ticks). Set to 0 to make it permanent.")] + public readonly int Duration = 0; + + [Desc("The condition to apply. Must be included in the target actor's ExternalConditions list.")] + public readonly string Condition = null; + + public readonly WDist Range = WDist.FromCells(1); + + [Desc("What types of targets are affected.")] + public readonly BitSet ChangeOwnerValidTargets = new("Ground", "Water"); + + [Desc("What types of targets are unaffected.", "Overrules ChangeOwnerValidTargets.")] + public readonly BitSet ChangeOwnerInvalidTargets; + + [Desc("What diplomatic stances are affected.")] + public readonly PlayerRelationship ChangeOwnerValidStances = PlayerRelationship.Ally | PlayerRelationship.Neutral | PlayerRelationship.Enemy; + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var actors = firedBy.World.FindActorsInCircle(target.CenterPosition, Range); + + foreach (var a in actors) + { + if (!IsValidForOwnerChange(a, firedBy)) + continue; + + if (Duration == 0) + a.ChangeOwner(firedBy.Owner); // Permanent + else + { + var tempOwnerManager = a.TraitOrDefault(); + if (tempOwnerManager == null) + continue; + + tempOwnerManager.ChangeOwner(a, firedBy.Owner, Duration); + } + + var external = a.TraitsImplementing() + .FirstOrDefault(t => t.Info.Condition == Condition && t.CanGrantCondition(firedBy)); + + external?.GrantCondition(a, firedBy, Duration); + + // Stop shooting, you have new enemies + a.CancelActivity(); + } + } + + bool IsValidForOwnerChange(Actor victim, Actor firedBy) + { + var relationship = firedBy.Owner.RelationshipWith(victim.Owner); + if (!ChangeOwnerValidStances.HasRelationship(relationship)) + return false; + + // A target type is valid if it is in the valid targets list, and not in the invalid targets list. + if (!IsValidTargetForOwnerChange(victim.GetEnabledTargetTypes())) + return false; + + return true; + } + + bool IsValidTargetForOwnerChange(BitSet targetTypes) + { + return ChangeOwnerValidTargets.Overlaps(targetTypes) && !ChangeOwnerInvalidTargets.Overlaps(targetTypes); + } + } +} diff --git a/OpenRA.Mods.AS/Duplicates/Widgets/Logic/Ingame/ObserverStatsRVLogic.cs b/OpenRA.Mods.AS/Duplicates/Widgets/Logic/Ingame/ObserverStatsRVLogic.cs new file mode 100644 index 000000000000..9fc47b32a700 --- /dev/null +++ b/OpenRA.Mods.AS/Duplicates/Widgets/Logic/Ingame/ObserverStatsRVLogic.cs @@ -0,0 +1,677 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Lint; +using OpenRA.Mods.Common.Traits; +using OpenRA.Network; +using OpenRA.Primitives; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets.Logic +{ + public enum ObserverStatsRVPanel { None, Minimal, Basic, Economy, Production, SupportPowers, Combat, Army, Upgrades, Graph, ArmyGraph } + + [ChromeLogicArgsHotkeys("StatisticsMinimalKey", "StatisticsBasicKey", "StatisticsEconomyKey", "StatisticsProductionKey", "StatisticsSupportPowersKey", "StatisticsCombatKey", "StatisticsArmyKey", "StatisticsUpgradesKey", "StatisticsGraphKey", + "StatisticsArmyGraphKey")] + public class ObserverStatsRVLogic : ChromeLogic + { + [TranslationReference] + const string Minimal = "options-observer-stats.minimal"; + + [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 CPsAndUpgrades = "options-observer-stats.cps-and-upgrades"; + + [TranslationReference] + const string EarningsGraph = "options-observer-stats.earnings-graph"; + + [TranslationReference] + const string ArmyGraph = "options-observer-stats.army-graph"; + + [TranslationReference("team")] + const string TeamNumber = "label-team-name"; + + [TranslationReference] + const string NoTeam = "label-no-team"; + + readonly ContainerWidget minimalStatsHeaders; + readonly ContainerWidget basicStatsHeaders; + readonly ContainerWidget economyStatsHeaders; + readonly ContainerWidget productionStatsHeaders; + readonly ContainerWidget supportPowerStatsHeaders; + readonly ContainerWidget combatStatsHeaders; + readonly ContainerWidget armyHeaders; + readonly ContainerWidget upgradesHeaders; + readonly ScrollPanelWidget playerStatsPanel; + readonly ScrollItemWidget minimalPlayerTemplate; + readonly ScrollItemWidget basicPlayerTemplate; + readonly ScrollItemWidget economyPlayerTemplate; + readonly ScrollItemWidget productionPlayerTemplate; + readonly ScrollItemWidget supportPowersPlayerTemplate; + readonly ScrollItemWidget armyPlayerTemplate; + readonly ScrollItemWidget upgradesPlayerTemplate; + readonly ScrollItemWidget combatPlayerTemplate; + readonly ContainerWidget incomeGraphContainer; + readonly ContainerWidget armyValueGraphContainer; + readonly LineGraphWidget incomeGraph; + readonly LineGraphWidget armyValueGraph; + readonly ScrollItemWidget teamTemplate; + readonly IEnumerable players; + readonly IOrderedEnumerable> teams; + readonly bool hasTeams; + readonly World world; + readonly WorldRenderer worldRenderer; + + readonly string clickSound = ChromeMetrics.Get("ClickSound"); + ObserverStatsRVPanel activePanel; + + [ObjectCreator.UseCtor] + public ObserverStatsRVLogic(World world, ModData modData, WorldRenderer worldRenderer, Widget widget, Dictionary logicArgs) + { + this.world = world; + this.worldRenderer = worldRenderer; + + MiniYaml yaml; + var keyNames = Enum.GetNames(typeof(ObserverStatsRVPanel)); + 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); + + minimalStatsHeaders = widget.Get("MINIMAL_STATS_HEADERS"); + 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"); + 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(minimalStatsHeaders); + AdjustHeader(basicStatsHeaders); + AdjustHeader(economyStatsHeaders); + AdjustHeader(productionStatsHeaders); + AdjustHeader(supportPowerStatsHeaders); + AdjustHeader(combatStatsHeaders); + AdjustHeader(armyHeaders); + AdjustHeader(upgradesHeaders); + } + + minimalPlayerTemplate = playerStatsPanel.Get("MINIMAL_PLAYER_TEMPLATE"); + 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"); + 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"); + + teamTemplate = playerStatsPanel.Get("TEAM_TEMPLATE"); + + var statsDropDown = widget.Get("STATS_DROPDOWN"); + StatsDropDownOption CreateStatsOption(string title, ObserverStatsRVPanel 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 == ObserverStatsRVPanel.None, + OnClick = () => + { + var informationNone = TranslationProvider.GetString(InformationNone); + statsDropDown.GetText = () => informationNone; + playerStatsPanel.Visible = false; + ClearStats(); + activePanel = ObserverStatsRVPanel.None; + } + }, + CreateStatsOption(Minimal, ObserverStatsRVPanel.Minimal, minimalPlayerTemplate, () => DisplayStats(MinimalStats)), + CreateStatsOption(Basic, ObserverStatsRVPanel.Basic, basicPlayerTemplate, () => DisplayStats(BasicStats)), + CreateStatsOption(Economy, ObserverStatsRVPanel.Economy, economyPlayerTemplate, () => DisplayStats(EconomyStats)), + CreateStatsOption(Production, ObserverStatsRVPanel.Production, productionPlayerTemplate, () => DisplayStats(ProductionStats)), + CreateStatsOption(SupportPowers, ObserverStatsRVPanel.SupportPowers, supportPowersPlayerTemplate, () => DisplayStats(SupportPowerStats)), + CreateStatsOption(Combat, ObserverStatsRVPanel.Combat, combatPlayerTemplate, () => DisplayStats(CombatStats)), + CreateStatsOption(Army, ObserverStatsRVPanel.Army, armyPlayerTemplate, () => DisplayStats(ArmyStats)), + CreateStatsOption(CPsAndUpgrades, ObserverStatsRVPanel.Upgrades, upgradesPlayerTemplate, () => DisplayStats(UpgradesStats)), + CreateStatsOption(EarningsGraph, ObserverStatsRVPanel.Graph, null, () => IncomeGraph()), + CreateStatsOption(ArmyGraph, ObserverStatsRVPanel.ArmyGraph, null, () => ArmyValueGraph()), + }; + + 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, 280, statsDropDownOptions, SetupItem); + statsDropDownOptions[1].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; + } + + void ClearStats() + { + playerStatsPanel.Children.Clear(); + minimalStatsHeaders.Visible = false; + basicStatsHeaders.Visible = false; + economyStatsHeaders.Visible = false; + productionStatsHeaders.Visible = false; + supportPowerStatsHeaders.Visible = false; + armyHeaders.Visible = false; + upgradesHeaders.Visible = false; + combatStatsHeaders.Visible = false; + + incomeGraphContainer.Visible = false; + armyValueGraphContainer.Visible = false; + + incomeGraph.GetSeries = null; + armyValueGraph.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 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.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("ASSETS_DESTROYED").GetText = () => destroyedText.Update(stats.KillsCost); + + var lostText = new CachedTransform(i => "$" + i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("ASSETS_LOST").GetText = () => lostText.Update(stats.DeathsCost); + + var unitsKilledText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("UNITS_KILLED").GetText = () => unitsKilledText.Update(stats.UnitsKilled); + + var unitsDeadText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("UNITS_DEAD").GetText = () => unitsDeadText.Update(stats.UnitsDead); + + var buildingsKilledText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("BUILDINGS_KILLED").GetText = () => buildingsKilledText.Update(stats.BuildingsKilled); + + var buildingsDeadText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("BUILDINGS_DEAD").GetText = () => buildingsDeadText.Update(stats.BuildingsDead); + + var armyText = new CachedTransform(i => "$" + i.ToString(NumberFormatInfo.CurrentInfo)); + 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 UpgradesStats(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 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(NumberFormatInfo.CurrentInfo); + + var derricks = template.GetOrNull("DERRICKS"); + if (derricks != null) + derricks.GetText = () => world.ActorsHavingTrait().Count(a => a.Owner == player && !a.IsDead).ToString(NumberFormatInfo.CurrentInfo); + + return template; + } + + ScrollItemWidget MinimalStats(Player player) + { + minimalStatsHeaders.Visible = true; + var template = SetupPlayerScrollItemWidget(minimalPlayerTemplate, 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 harvesters = template.Get("HARVESTERS"); + harvesters.GetText = () => world.ActorsWithTrait().Count(a => a.Actor.Owner == player && !a.Actor.IsDead && !a.Trait.IsTraitDisabled).ToString(NumberFormatInfo.CurrentInfo); + + 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(NumberFormatInfo.CurrentInfo)); + template.Get("KILLS").GetText = () => killsText.Update(stats.UnitsKilled + stats.BuildingsKilled); + + var deathsText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("DEATHS").GetText = () => deathsText.Update(stats.UnitsDead + stats.BuildingsDead); + + var destroyedText = new CachedTransform(i => "$" + i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("ASSETS_DESTROYED").GetText = () => destroyedText.Update(stats.KillsCost); + + var lostText = new CachedTransform(i => "$" + i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("ASSETS_LOST").GetText = () => lostText.Update(stats.DeathsCost); + + var experienceText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + 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", NumberFormatInfo.CurrentInfo); + } + + string Vision(int revealedCells) + { + return (Math.Ceiling(revealedCells * 100d / world.Map.ProjectedCells.Length) / 100).ToString("P0", NumberFormatInfo.CurrentInfo); + } + + 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; + + class StatsDropDownOption + { + public string Title; + public Func IsSelected; + public Action OnClick; + } + } +} diff --git a/OpenRA.Mods.AS/Duplicates/Widgets/Logic/Ingame/ObserverStatsSPLogic.cs b/OpenRA.Mods.AS/Duplicates/Widgets/Logic/Ingame/ObserverStatsSPLogic.cs new file mode 100644 index 000000000000..e10f77bdfc71 --- /dev/null +++ b/OpenRA.Mods.AS/Duplicates/Widgets/Logic/Ingame/ObserverStatsSPLogic.cs @@ -0,0 +1,646 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Lint; +using OpenRA.Mods.Common.Traits; +using OpenRA.Network; +using OpenRA.Primitives; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets.Logic +{ + public enum ObserverStatsSPPanel { None, Minimal, Basic, Economy, Production, SupportPowers, Combat, Army, Graph, ArmyGraph } + + [ChromeLogicArgsHotkeys("StatisticsMinimalKey", "StatisticsBasicKey", "StatisticsEconomyKey", "StatisticsProductionKey", "StatisticsSupportPowersKey", "StatisticsCombatKey", "StatisticsArmyKey", "StatisticsGraphKey", + "StatisticsArmyGraphKey")] + public class ObserverStatsSPLogic : ChromeLogic + { + [TranslationReference] + const string Minimal = "options-observer-stats.minimal"; + + [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 EarningsGraph = "options-observer-stats.earnings-graph"; + + [TranslationReference] + const string ArmyGraph = "options-observer-stats.army-graph"; + + [TranslationReference("team")] + const string TeamNumber = "label-team-name"; + + [TranslationReference] + const string NoTeam = "label-no-team"; + + readonly ContainerWidget minimalStatsHeaders; + readonly ContainerWidget basicStatsHeaders; + readonly ContainerWidget economyStatsHeaders; + readonly ContainerWidget productionStatsHeaders; + readonly ContainerWidget supportPowerStatsHeaders; + readonly ContainerWidget combatStatsHeaders; + readonly ContainerWidget armyHeaders; + readonly ScrollPanelWidget playerStatsPanel; + readonly ScrollItemWidget minimalPlayerTemplate; + readonly ScrollItemWidget basicPlayerTemplate; + readonly ScrollItemWidget economyPlayerTemplate; + readonly ScrollItemWidget productionPlayerTemplate; + readonly ScrollItemWidget supportPowersPlayerTemplate; + readonly ScrollItemWidget armyPlayerTemplate; + readonly ScrollItemWidget combatPlayerTemplate; + readonly ContainerWidget incomeGraphContainer; + readonly ContainerWidget armyValueGraphContainer; + readonly LineGraphWidget incomeGraph; + readonly LineGraphWidget armyValueGraph; + readonly ScrollItemWidget teamTemplate; + readonly IEnumerable players; + readonly IOrderedEnumerable> teams; + readonly bool hasTeams; + readonly World world; + readonly WorldRenderer worldRenderer; + + readonly string clickSound = ChromeMetrics.Get("ClickSound"); + ObserverStatsSPPanel activePanel; + + [ObjectCreator.UseCtor] + public ObserverStatsSPLogic(World world, ModData modData, WorldRenderer worldRenderer, Widget widget, Dictionary logicArgs) + { + this.world = world; + this.worldRenderer = worldRenderer; + + MiniYaml yaml; + var keyNames = Enum.GetNames(typeof(ObserverStatsSPPanel)); + 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); + + minimalStatsHeaders = widget.Get("MINIMAL_STATS_HEADERS"); + 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"); + 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(minimalStatsHeaders); + AdjustHeader(basicStatsHeaders); + AdjustHeader(economyStatsHeaders); + AdjustHeader(productionStatsHeaders); + AdjustHeader(supportPowerStatsHeaders); + AdjustHeader(combatStatsHeaders); + AdjustHeader(armyHeaders); + } + + minimalPlayerTemplate = playerStatsPanel.Get("MINIMAL_PLAYER_TEMPLATE"); + 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"); + 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"); + + teamTemplate = playerStatsPanel.Get("TEAM_TEMPLATE"); + + var statsDropDown = widget.Get("STATS_DROPDOWN"); + StatsDropDownOption CreateStatsOption(string title, ObserverStatsSPPanel 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 == ObserverStatsSPPanel.None, + OnClick = () => + { + var informationNone = TranslationProvider.GetString(InformationNone); + statsDropDown.GetText = () => informationNone; + playerStatsPanel.Visible = false; + ClearStats(); + activePanel = ObserverStatsSPPanel.None; + } + }, + CreateStatsOption(Minimal, ObserverStatsSPPanel.Minimal, minimalPlayerTemplate, () => DisplayStats(MinimalStats)), + CreateStatsOption(Basic, ObserverStatsSPPanel.Basic, basicPlayerTemplate, () => DisplayStats(BasicStats)), + CreateStatsOption(Economy, ObserverStatsSPPanel.Economy, economyPlayerTemplate, () => DisplayStats(EconomyStats)), + CreateStatsOption(Production, ObserverStatsSPPanel.Production, productionPlayerTemplate, () => DisplayStats(ProductionStats)), + CreateStatsOption(SupportPowers, ObserverStatsSPPanel.SupportPowers, supportPowersPlayerTemplate, () => DisplayStats(SupportPowerStats)), + CreateStatsOption(Combat, ObserverStatsSPPanel.Combat, combatPlayerTemplate, () => DisplayStats(CombatStats)), + CreateStatsOption(Army, ObserverStatsSPPanel.Army, armyPlayerTemplate, () => DisplayStats(ArmyStats)), + CreateStatsOption(EarningsGraph, ObserverStatsSPPanel.Graph, null, () => IncomeGraph()), + CreateStatsOption(ArmyGraph, ObserverStatsSPPanel.ArmyGraph, null, () => ArmyValueGraph()), + }; + + 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, 255, statsDropDownOptions, SetupItem); + statsDropDownOptions[1].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; + } + + void ClearStats() + { + playerStatsPanel.Children.Clear(); + minimalStatsHeaders.Visible = false; + basicStatsHeaders.Visible = false; + economyStatsHeaders.Visible = false; + productionStatsHeaders.Visible = false; + supportPowerStatsHeaders.Visible = false; + armyHeaders.Visible = false; + combatStatsHeaders.Visible = false; + + incomeGraphContainer.Visible = false; + armyValueGraphContainer.Visible = false; + + incomeGraph.GetSeries = null; + armyValueGraph.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 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.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("ASSETS_DESTROYED").GetText = () => destroyedText.Update(stats.KillsCost); + + var lostText = new CachedTransform(i => "$" + i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("ASSETS_LOST").GetText = () => lostText.Update(stats.DeathsCost); + + var unitsKilledText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("UNITS_KILLED").GetText = () => unitsKilledText.Update(stats.UnitsKilled); + + var unitsDeadText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("UNITS_DEAD").GetText = () => unitsDeadText.Update(stats.UnitsDead); + + var buildingsKilledText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("BUILDINGS_KILLED").GetText = () => buildingsKilledText.Update(stats.BuildingsKilled); + + var buildingsDeadText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("BUILDINGS_DEAD").GetText = () => buildingsDeadText.Update(stats.BuildingsDead); + + var armyText = new CachedTransform(i => "$" + i.ToString(NumberFormatInfo.CurrentInfo)); + 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 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(NumberFormatInfo.CurrentInfo); + + var derricks = template.GetOrNull("DERRICKS"); + if (derricks != null) + derricks.GetText = () => world.ActorsHavingTrait().Count(a => a.Owner == player && !a.IsDead).ToString(NumberFormatInfo.CurrentInfo); + + return template; + } + + ScrollItemWidget MinimalStats(Player player) + { + minimalStatsHeaders.Visible = true; + var template = SetupPlayerScrollItemWidget(minimalPlayerTemplate, 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 harvesters = template.Get("HARVESTERS"); + harvesters.GetText = () => world.ActorsWithTrait().Count(a => a.Actor.Owner == player && !a.Actor.IsDead && !a.Trait.IsTraitDisabled).ToString(NumberFormatInfo.CurrentInfo); + + 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(NumberFormatInfo.CurrentInfo)); + template.Get("KILLS").GetText = () => killsText.Update(stats.UnitsKilled + stats.BuildingsKilled); + + var deathsText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("DEATHS").GetText = () => deathsText.Update(stats.UnitsDead + stats.BuildingsDead); + + var destroyedText = new CachedTransform(i => "$" + i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("ASSETS_DESTROYED").GetText = () => destroyedText.Update(stats.KillsCost); + + var lostText = new CachedTransform(i => "$" + i.ToString(NumberFormatInfo.CurrentInfo)); + template.Get("ASSETS_LOST").GetText = () => lostText.Update(stats.DeathsCost); + + var experienceText = new CachedTransform(i => i.ToString(NumberFormatInfo.CurrentInfo)); + 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", NumberFormatInfo.CurrentInfo); + } + + string Vision(int revealedCells) + { + return (Math.Ceiling(revealedCells * 100d / world.Map.ProjectedCells.Length) / 100).ToString("P0", NumberFormatInfo.CurrentInfo); + } + + 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; + + class StatsDropDownOption + { + public string Title; + public Func IsSelected; + public Action OnClick; + } + } +} diff --git a/OpenRA.Mods.AS/Duplicates/Widgets/Logic/Ingame/ProductionTooltipLogicCA.cs b/OpenRA.Mods.AS/Duplicates/Widgets/Logic/Ingame/ProductionTooltipLogicCA.cs new file mode 100644 index 000000000000..610340fe95a8 --- /dev/null +++ b/OpenRA.Mods.AS/Duplicates/Widgets/Logic/Ingame/ProductionTooltipLogicCA.cs @@ -0,0 +1,278 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Globalization; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Widgets; +using OpenRA.Primitives; +using OpenRA.Widgets; + +namespace OpenRA.Mods.AS.Widgets.Logic +{ + public class ProductionTooltipLogicCA : ChromeLogic + { + [TranslationReference("prequisites")] + const string Requires = "label-requires"; + + [ObjectCreator.UseCtor] + public ProductionTooltipLogicCA(Widget widget, TooltipContainerWidget tooltipContainer, Player player, Func getTooltipIcon) + { + var world = player.World; + var mapRules = world.Map.Rules; + var pm = player.PlayerActor.TraitOrDefault(); + var pr = player.PlayerActor.Trait(); + + widget.IsVisible = () => getTooltipIcon() != null && getTooltipIcon().Actor != null && BuildableInfo.GetTraitForQueue(getTooltipIcon().Actor, getTooltipIcon().ProductionQueue?.Info.Type).ShowTooltip; + var nameLabel = widget.Get("NAME"); + var hotkeyLabel = widget.Get("HOTKEY"); + var requiresLabel = widget.Get("REQUIRES"); + var powerLabel = widget.Get("POWER"); + var powerIcon = widget.Get("POWER_ICON"); + var armorTypeLabel = widget.Get("ARMORTYPE"); + var armorTypeIcon = widget.Get("ARMORTYPE_ICON"); + var timeLabel = widget.Get("TIME"); + var timeIcon = widget.Get("TIME_ICON"); + var costLabel = widget.Get("COST"); + var costIcon = widget.Get("COST_ICON"); + var descLabel = widget.Get("DESC"); + /* var strengthsLabel = widget.Get("STRENGTHS"); + var weaknessesLabel = widget.Get("WEAKNESSES"); + var attributesLabel = widget.Get("ATTRIBUTES"); */ + + var iconMargin = timeIcon.Bounds.X; + + var font = Game.Renderer.Fonts[nameLabel.Font]; + var descFont = Game.Renderer.Fonts[descLabel.Font]; + var requiresFont = Game.Renderer.Fonts[requiresLabel.Font]; + var formatBuildTime = new CachedTransform(time => WidgetUtils.FormatTime(time, world.Timestep)); + + ActorInfo lastActor = null; + var lastHotkey = Hotkey.Invalid; + var lastPowerState = pm == null ? PowerState.Normal : pm.PowerState; + var descLabelY = descLabel.Bounds.Y; + var descLabelPadding = descLabel.Bounds.Height; + + tooltipContainer.BeforeRender = () => + { + var tooltipIcon = getTooltipIcon(); + if (tooltipIcon == null) + return; + + var actor = tooltipIcon.Actor; + if (actor == null) + return; + + var hotkey = tooltipIcon.Hotkey != null ? tooltipIcon.Hotkey.GetValue() : Hotkey.Invalid; + if (actor == lastActor && hotkey == lastHotkey && (pm == null || pm.PowerState == lastPowerState)) + return; + + var tooltip = actor.TraitInfos().FirstOrDefault(info => info.EnabledByDefault); + var name = tooltip != null ? tooltip.Name : actor.Name; + var buildable = BuildableInfo.GetTraitForQueue(actor, tooltipIcon.ProductionQueue?.Info.Type); + + var cost = 0; + if (tooltipIcon.ProductionQueue != null) + cost = tooltipIcon.ProductionQueue.GetProductionCost(actor); + else + { + var valued = actor.TraitInfoOrDefault(); + if (valued != null) + cost = valued.Cost; + } + + nameLabel.Text = name; + + var nameSize = font.Measure(name); + var hotkeyWidth = 0; + hotkeyLabel.Visible = hotkey.IsValid(); + + armorTypeLabel = GetArmorTypeLabel(armorTypeLabel, actor); + /* var tooltipExtras = actor.TraitInfos().FirstOrDefault(info => info.IsStandard); + + if (tooltipExtras != null) + { + strengthsLabel.Text = tooltipExtras.Strengths.Replace("\\n", "\n"); + weaknessesLabel.Text = tooltipExtras.Weaknesses.Replace("\\n", "\n"); + attributesLabel.Text = tooltipExtras.Attributes.Replace("\\n", "\n"); + } + else + { + strengthsLabel.Text = ""; + weaknessesLabel.Text = ""; + attributesLabel.Text = ""; + } */ + + if (hotkeyLabel.Visible) + { + var hotkeyText = $"({hotkey.DisplayString()})"; + + hotkeyWidth = font.Measure(hotkeyText).X + 2 * nameLabel.Bounds.X; + hotkeyLabel.Text = hotkeyText; + hotkeyLabel.Bounds.X = nameSize.X + 2 * nameLabel.Bounds.X; + } + + var prereqs = buildable.Prerequisites.Select(a => ActorName(mapRules, a)) + .Where(s => !s.StartsWith("~", StringComparison.Ordinal) && !s.StartsWith("!", StringComparison.Ordinal)); + + var requiresSize = int2.Zero; + if (prereqs.Any()) + { + requiresLabel.Text = TranslationProvider.GetString(Requires, Translation.Arguments("prequisites", prereqs.JoinWith(", "))); + requiresSize = requiresFont.Measure(requiresLabel.Text); + requiresLabel.Visible = true; + descLabel.Bounds.Y = descLabelY + requiresLabel.Bounds.Height + descLabel.Bounds.X / 2; + } + else + { + requiresLabel.Visible = false; + descLabel.Bounds.Y = descLabelY; + } + + var buildTime = tooltipIcon.ProductionQueue == null ? 0 : tooltipIcon.ProductionQueue.GetBuildTime(actor, buildable); + var timeModifier = pm != null && pm.PowerState != PowerState.Normal ? tooltipIcon.ProductionQueue.Info.LowPowerModifier : 100; + + timeLabel.Text = formatBuildTime.Update(buildTime * timeModifier / 100); + timeLabel.TextColor = (pm != null && pm.PowerState != PowerState.Normal && tooltipIcon.ProductionQueue.Info.LowPowerModifier > 100) ? Color.Red : Color.White; + var timeSize = font.Measure(timeLabel.Text); + costLabel.IsVisible = () => cost != 0; + costIcon.IsVisible = () => cost != 0; + + costLabel.Text = cost.ToString(NumberFormatInfo.CurrentInfo); + costLabel.GetColor = () => pr.Cash + pr.Resources >= cost ? Color.White : Color.Red; + var costSize = font.Measure(costLabel.Text); + + var powerSize = new int2(0, 0); + var power = 0; + var armorTypeSize = armorTypeLabel.Text != "" ? font.Measure(armorTypeLabel.Text) : new int2(0, 0); + armorTypeIcon.Visible = armorTypeSize.Y > 0; + + if (pm != null) + { + power = actor.TraitInfos().Where(i => i.EnabledByDefault).Sum(i => i.Amount); + powerLabel.Text = power.ToString(NumberFormatInfo.CurrentInfo); + powerLabel.GetColor = () => (pm.PowerProvided - pm.PowerDrained >= -power || power > 0) + ? Color.White : Color.Red; + powerLabel.Visible = power != 0; + powerIcon.Visible = power != 0; + powerSize = font.Measure(powerLabel.Text); + } + + if (armorTypeLabel.Text != "" && power != 0) + armorTypeIcon.Bounds.Y = armorTypeLabel.Bounds.Y = powerLabel.Bounds.Bottom; + else + armorTypeIcon.Bounds.Y = armorTypeLabel.Bounds.Y = timeLabel.Bounds.Bottom; + + var extrasSpacing = descLabel.Bounds.X / 2; + + descLabel.Text = buildable.Description.Replace("\\n", "\n"); + var descSize = descFont.Measure(descLabel.Text); + descLabel.Bounds.Width = descSize.X; + descLabel.Bounds.Height = descSize.Y; + + /* var strengthsSize = strengthsLabel.Text != "" ? descFont.Measure(strengthsLabel.Text) : new int2(0, 0); + var weaknessesSize = weaknessesLabel.Text != "" ? descFont.Measure(weaknessesLabel.Text) : new int2(0, 0); + var attributesSize = attributesLabel.Text != "" ? descFont.Measure(attributesLabel.Text) : new int2(0, 0); + + strengthsLabel.Bounds.Y = descLabel.Bounds.Bottom + extrasSpacing; + weaknessesLabel.Bounds.Y = descLabel.Bounds.Bottom + strengthsSize.Y + extrasSpacing; + attributesLabel.Bounds.Y = descLabel.Bounds.Bottom + strengthsSize.Y + weaknessesSize.Y + extrasSpacing; + + descLabel.Bounds.Height += strengthsSize.Y + weaknessesSize.Y + attributesSize.Y + descLabelPadding + extrasSpacing; + + var leftWidth = new[] { nameSize.X + hotkeyWidth, requiresSize.X, descSize.X, strengthsSize.X, weaknessesSize.X, attributesSize.X }.Aggregate(Math.Max); */ + var leftWidth = new[] { nameSize.X + hotkeyWidth, requiresSize.X, descSize.X }.Aggregate(Math.Max); + var rightWidth = new[] { powerSize.X, timeSize.X, costSize.X, armorTypeSize.X }.Aggregate(Math.Max); + + timeIcon.Bounds.X = powerIcon.Bounds.X = costIcon.Bounds.X = armorTypeIcon.Bounds.X = leftWidth + 2 * nameLabel.Bounds.X; + timeLabel.Bounds.X = powerLabel.Bounds.X = costLabel.Bounds.X = armorTypeLabel.Bounds.X = timeIcon.Bounds.Right + iconMargin; + widget.Bounds.Width = leftWidth + rightWidth + 3 * nameLabel.Bounds.X + timeIcon.Bounds.Width + iconMargin; + + // Set the bottom margin to match the left margin + var leftHeight = descLabel.Bounds.Bottom + descLabel.Bounds.X; + + // Set the bottom margin to match the top margin + var rightHeight = armorTypeIcon.Bounds.Bottom + costIcon.Bounds.Top; + + widget.Bounds.Height = Math.Max(leftHeight, rightHeight); + + lastActor = actor; + lastHotkey = hotkey; + if (pm != null) + lastPowerState = pm.PowerState; + }; + } + + static string ActorName(Ruleset rules, string a) + { + if (rules.Actors.TryGetValue(a.ToLowerInvariant(), out var ai)) + { + var actorTooltip = ai.TraitInfos().FirstOrDefault(info => info.EnabledByDefault); + if (actorTooltip != null) + return actorTooltip.Name; + } + + return a; + } + + static LabelWidget GetArmorTypeLabel(LabelWidget armorTypeLabel, ActorInfo actor) + { + var armor = actor.TraitInfos().FirstOrDefault(); + armorTypeLabel.Text = armor != null ? armor.Type : ""; + + /* if (armorTypeLabel.Text != "" && actor.HasTraitInfo()) + armorTypeLabel.Text = "Aircraft"; + + // hard coded, specific to CA - find a better way to set user-friendly names and colors for armor types + switch (armorTypeLabel.Text) + { + case "None": + armorTypeLabel.Text = "Infantry"; + armorTypeLabel.TextColor = Color.ForestGreen; + break; + + case "Light": + armorTypeLabel.TextColor = Color.MediumPurple; + break; + + case "Heavy": + armorTypeLabel.TextColor = Color.Firebrick; + break; + + case "Concrete": + armorTypeLabel.Text = "Defense"; + armorTypeLabel.TextColor = Color.RoyalBlue; + break; + + case "Wood": + armorTypeLabel.Text = "Building"; + armorTypeLabel.TextColor = Color.Peru; + break; + + case "Brick": + armorTypeLabel.Text = "Wall"; + armorTypeLabel.TextColor = Color.RosyBrown; + break; + + case "Aircraft": + armorTypeLabel.TextColor = Color.SkyBlue; + break; + + default: + armorTypeLabel.Text = ""; + break; + } */ + + return armorTypeLabel; + } + } +} diff --git a/OpenRA.Mods.AS/Duplicates/Widgets/ObserverUpgradesIconsWidget.cs b/OpenRA.Mods.AS/Duplicates/Widgets/ObserverUpgradesIconsWidget.cs new file mode 100644 index 000000000000..0a2ec29feafc --- /dev/null +++ b/OpenRA.Mods.AS/Duplicates/Widgets/ObserverUpgradesIconsWidget.cs @@ -0,0 +1,202 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets +{ + public class ObserverUpgradesIconsWidget : 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()); + + int lastIconIdx; + int currentTooltipToken; + + [ObjectCreator.UseCtor] + public ObserverUpgradesIconsWidget(World world, WorldRenderer worldRenderer) + { + this.world = world; + this.worldRenderer = worldRenderer; + + GetTooltipUnit = () => TooltipUnit; + tooltipContainer = Exts.Lazy(() => + Ui.Root.Get(TooltipContainer)); + } + + protected ObserverUpgradesIconsWidget(ObserverUpgradesIconsWidget 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 playerStatistics = stats.Update(player); + + var items = playerStatistics.Units.Values + .Where(u => u.Count > 0 && u.Icon != null && u.Upgrade) + .OrderBy(u => u.ProductionQueueOrder) + .ThenBy(u => u.BuildPaletteOrder); + + Game.Renderer.EnableAntialiasingFilter(); + + var queueCol = 0; + foreach (var unit in items) + { + 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); + } + + public override Widget Clone() + { + return new ObserverUpgradesIconsWidget(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; + } + + class ArmyIcon + { + public Rectangle Bounds { get; set; } + public ArmyUnit Unit { get; set; } + } + } +} diff --git a/OpenRA.Mods.AS/Effects/AirstrikePowerASEffect.cs b/OpenRA.Mods.AS/Effects/AirstrikePowerASEffect.cs new file mode 100644 index 000000000000..617394bb66f8 --- /dev/null +++ b/OpenRA.Mods.AS/Effects/AirstrikePowerASEffect.cs @@ -0,0 +1,130 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Effects; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Effects; +using OpenRA.Primitives; + +namespace OpenRA.Mods.AS.Effects +{ + public class AirstrikePowerASEffect : IEffect + { + readonly AirstrikePowerASInfo info; + readonly Player owner; + readonly World world; + readonly WPos pos; + + IEnumerable planes; + Actor camera = null; + Beacon beacon = null; + bool enteredRange = false; + + public AirstrikePowerASEffect(World world, Player p, WPos pos, IEnumerable planes, AirstrikePowerAS power, AirstrikePowerASInfo info) + { + this.info = info; + this.world = world; + owner = p; + this.pos = pos; + this.planes = planes; + + if (info.DisplayBeacon) + { + var distance = (planes.First().OccupiesSpace.CenterPosition - pos).HorizontalLength; + + beacon = new Beacon( + owner, + pos - new WVec(WDist.Zero, WDist.Zero, world.Map.DistanceAboveTerrain(pos)), + info.BeaconPaletteIsPlayerPalette, + info.BeaconPalette, + info.BeaconImage, + info.BeaconPosters.First(bp => bp.Key == power.GetLevel()).Value, + info.BeaconPosterPalette, + info.BeaconSequence, + info.ArrowSequence, + info.CircleSequence, + info.ClockSequence, + () => 1 - ((planes.First().OccupiesSpace.CenterPosition - pos).HorizontalLength - info.BeaconDistanceOffset.Length) * 1f / distance, + info.BeaconDelay); + + world.AddFrameEndTask(w => w.Add(beacon)); + } + } + + void IEffect.Tick(World world) + { + planes = planes.Where(p => p.IsInWorld && !p.IsDead); + + if (!enteredRange && planes.Any(p => (p.OccupiesSpace.CenterPosition - pos).Length < info.BeaconDistanceOffset.Length)) + { + OnEnterRange(); + enteredRange = true; + } + + if (!planes.Any() || (enteredRange && planes.All(p => (p.OccupiesSpace.CenterPosition - pos).Length > info.BeaconDistanceOffset.Length))) + { + OnExitRange(); + world.AddFrameEndTask(w => w.Remove(this)); + } + } + + void OnEnterRange() + { + // Spawn a camera and remove the beacon when the first plane enters the target area + if (info.CameraActor != null) + { + world.AddFrameEndTask(w => + { + camera = w.CreateActor(info.CameraActor, new TypeDictionary + { + new LocationInit(world.Map.CellContaining(pos)), + new OwnerInit(owner), + }); + }); + } + + TryRemoveBeacon(); + } + + void OnExitRange() + { + if (camera != null) + { + camera.QueueActivity(new Wait(info.CameraRemoveDelay)); + camera.QueueActivity(new RemoveSelf()); + } + + camera = null; + + TryRemoveBeacon(); + } + + void TryRemoveBeacon() + { + if (beacon != null) + { + world.AddFrameEndTask(w => + { + w.Remove(beacon); + beacon = null; + }); + } + } + + IEnumerable IEffect.Render(WorldRenderer r) + { + return Enumerable.Empty(); + } + } +} diff --git a/OpenRA.Mods.AS/Effects/AirstrikePowerRVEffect.cs b/OpenRA.Mods.AS/Effects/AirstrikePowerRVEffect.cs new file mode 100644 index 000000000000..d4281f875909 --- /dev/null +++ b/OpenRA.Mods.AS/Effects/AirstrikePowerRVEffect.cs @@ -0,0 +1,189 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Effects; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Effects; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Effects +{ + public class AirstrikePowerRVEffect : IEffect + { + readonly AirstrikePowerRVInfo info; + readonly Player owner; + readonly World world; + readonly WPos target; + readonly WPos finishEdge; + readonly WRot attackRotation; + readonly int level; + + int ticks = 0; + + readonly Actor[] aircraft; + Actor camera = null; + Beacon beacon = null; + bool spawned = false; + bool enteredRange = false; + + public AirstrikePowerRVEffect(World world, Player p, WPos target, WPos startEdge, WPos finishEdge, WRot attackRotation, int altitude, int level, Actor[] aircraft, AirstrikePowerRV power, AirstrikePowerRVInfo info) + { + this.info = info; + this.world = world; + owner = p; + this.target = target; + this.finishEdge = finishEdge; + this.attackRotation = attackRotation; + this.level = level; + this.aircraft = aircraft; + ticks = 0; + + if (info.DisplayBeacon) + { + var distance = (target - startEdge).HorizontalLength; + var distanceTestActor = aircraft.Last(); + + beacon = new Beacon( + owner, + target - new WVec(0, 0, altitude), + info.BeaconPaletteIsPlayerPalette, + info.BeaconPalette, + info.BeaconImage, + info.BeaconPosters.First(bp => bp.Key == level).Value, + info.BeaconPosterPalette, + info.BeaconSequence, + info.ArrowSequence, + info.CircleSequence, + info.ClockSequence, + () => FractionComplete(distanceTestActor, target, distance), + info.BeaconDelay); + + world.AddFrameEndTask(w => w.Add(beacon)); + } + } + + void IEffect.Tick(World world) + { + if (ticks < info.ActivationDelay) + { + ticks++; + + return; + } + + if (!spawned) + { + world.AddFrameEndTask(w => + { + var j = 0; + var squadSize = info.SquadSizes.First(ss => ss.Key == level).Value; + for (var i = -squadSize / 2; i <= squadSize / 2; i++) + { + // Even-sized squads skip the lead plane + if (i == 0 && (squadSize & 1) == 0) + continue; + + // Includes the 90 degree rotation between body and world coordinates + var so = info.SquadOffset; + var spawnOffset = new WVec(i * so.Y, -Math.Abs(i) * so.X, 0).Rotate(attackRotation); + + var a = aircraft[j++]; + if (a.IsDead) + continue; + + world.Add(a); + + a.QueueActivity(new Fly(a, Target.FromPos(target + spawnOffset))); + a.QueueActivity(new Fly(a, Target.FromPos(finishEdge + spawnOffset))); + a.QueueActivity(new RemoveSelf()); + } + }); + + spawned = true; + } + + var onMap = aircraft.Where(p => p.IsInWorld && !p.IsDead).ToArray(); + + if (!enteredRange && onMap.Any(p => (p.OccupiesSpace.CenterPosition - target).Length < info.BeaconDistanceOffset.Length)) + { + OnEnterRange(); + enteredRange = true; + } + + if (!onMap.Any() || (enteredRange && onMap.All(p => (p.OccupiesSpace.CenterPosition - target).Length > info.BeaconDistanceOffset.Length))) + { + OnExitRange(); + world.AddFrameEndTask(w => w.Remove(this)); + } + } + + float FractionComplete(Actor distanceTestActor, WPos target, int distance) + { + if (info.ActivationDelay > 0) + return (ticks * 1f / info.ActivationDelay + (1 - ((distanceTestActor.CenterPosition - target).HorizontalLength - info.BeaconDistanceOffset.Length * 1f) / distance)) / 2; + + return 1 - ((distanceTestActor.CenterPosition - target).HorizontalLength - info.BeaconDistanceOffset.Length) * 1f / distance; + } + + void OnEnterRange() + { + // Spawn a camera and remove the beacon when the first plane enters the target area + if (info.CameraActor != null) + { + world.AddFrameEndTask(w => + { + camera = w.CreateActor(info.CameraActor, new TypeDictionary + { + new LocationInit(world.Map.CellContaining(target)), + new OwnerInit(owner), + }); + }); + } + + TryRemoveBeacon(); + } + + void OnExitRange() + { + if (camera != null) + { + camera.QueueActivity(new Wait(info.CameraRemoveDelay)); + camera.QueueActivity(new RemoveSelf()); + } + + camera = null; + + TryRemoveBeacon(); + } + + void TryRemoveBeacon() + { + if (beacon != null) + { + world.AddFrameEndTask(w => + { + w.Remove(beacon); + beacon = null; + }); + } + } + + IEnumerable IEffect.Render(WorldRenderer r) + { + return Enumerable.Empty(); + } + } +} diff --git a/OpenRA.Mods.AS/Effects/GpsDotEffectAS.cs b/OpenRA.Mods.AS/Effects/GpsDotEffectAS.cs new file mode 100644 index 000000000000..98255abb1b6b --- /dev/null +++ b/OpenRA.Mods.AS/Effects/GpsDotEffectAS.cs @@ -0,0 +1,119 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Effects; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Effects +{ + class GpsDotEffectAS : IEffect, IEffectAboveShroud + { + readonly Actor actor; + readonly GpsDotAS trait; + readonly Animation anim; + + readonly PlayerDictionary dotStates; + readonly IDefaultVisibility visibility; + readonly IVisibilityModifier[] visibilityModifiers; + + class DotState + { + public readonly GpsASWatcher Watcher; + public readonly FrozenActor FrozenActor; + public bool Visible; + public DotState(Actor a, GpsASWatcher watcher, FrozenActorLayer frozenLayer) + { + Watcher = watcher; + if (frozenLayer != null) + FrozenActor = frozenLayer.FromID(a.ActorID); + } + } + + public GpsDotEffectAS(Actor actor, GpsDotAS trait) + { + this.actor = actor; + this.trait = trait; + anim = new Animation(actor.World, trait.Info.Image); + anim.PlayRepeating(trait.Info.Sequence); + + visibility = actor.Trait(); + visibilityModifiers = actor.TraitsImplementing().ToArray(); + + dotStates = new PlayerDictionary(actor.World, + p => new DotState(actor, p.PlayerActor.Trait(), p.FrozenActorLayer)); + } + + bool ShouldRender(DotState state, Player toPlayer) + { + // Hide the indicator if the owner trait is disabled + if (trait.IsTraitDisabled) + return false; + + // Hide the indicator if no watchers are available + if (!state.Watcher.Granted && !state.Watcher.GrantedAllies) + return false; + + // Hide the indicator if a frozen actor portrait is visible + if (state.FrozenActor != null && state.FrozenActor.HasRenderables) + return false; + + // Hide the indicator if the unit appears to be owned by an allied player + if (actor.EffectiveOwner != null && actor.EffectiveOwner.Owner != null && + toPlayer.IsAlliedWith(actor.EffectiveOwner.Owner)) + return false; + + // Hide indicator if the actor wouldn't otherwise be visible if there wasn't fog + foreach (var visibilityModifier in visibilityModifiers) + if (!visibilityModifier.IsVisible(actor, toPlayer)) + return false; + + // Hide the indicator behind shroud + if (!trait.Info.VisibleInShroud && !toPlayer.Shroud.IsExplored(actor.CenterPosition)) + return false; + + // Hide the indicator if it is not in range of a provider + if (trait.Info.Range > WDist.Zero && !actor.World.FindActorsInCircle(actor.CenterPosition, trait.Info.Range).Any(a => a.Info.HasTraitInfo() && state.Watcher.Providers.Contains(a.Trait()))) + return false; + + return !visibility.IsVisible(actor, toPlayer); + } + + void IEffect.Tick(World world) + { + for (var playerIndex = 0; playerIndex < dotStates.Count; playerIndex++) + { + var state = dotStates[playerIndex]; + state.Visible = ShouldRender(state, world.Players[playerIndex]); + } + } + + IEnumerable IEffect.Render(WorldRenderer wr) + { + return SpriteRenderable.None; + } + + IEnumerable IEffectAboveShroud.RenderAboveShroud(WorldRenderer wr) + { + if (actor.World.RenderPlayer == null || !dotStates[actor.World.RenderPlayer].Visible) + return SpriteRenderable.None; + + var effectiveOwner = actor.EffectiveOwner != null && actor.EffectiveOwner.Owner != null ? + actor.EffectiveOwner.Owner : actor.Owner; + + var palette = wr.Palette(trait.Info.IndicatorPalettePrefix + effectiveOwner.InternalName); + return anim.Render(actor.CenterPosition, palette); + } + } +} diff --git a/OpenRA.Mods.AS/Effects/RangedGpsDotEffect.cs b/OpenRA.Mods.AS/Effects/RangedGpsDotEffect.cs new file mode 100644 index 000000000000..5460123d3e71 --- /dev/null +++ b/OpenRA.Mods.AS/Effects/RangedGpsDotEffect.cs @@ -0,0 +1,119 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Effects; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Effects +{ + class RangedGpsDotEffect : IEffect, IEffectAboveShroud + { + readonly Actor actor; + readonly RangedGpsDot trait; + readonly Animation anim; + + readonly PlayerDictionary dotStates; + readonly IDefaultVisibility visibility; + readonly IVisibilityModifier[] visibilityModifiers; + + class RangedDotState + { + public readonly RangedGpsWatcher Watcher; + public readonly FrozenActor FrozenActor; + public bool Visible; + public RangedDotState(Actor a, RangedGpsWatcher watcher, FrozenActorLayer frozenLayer) + { + Watcher = watcher; + if (frozenLayer != null) + FrozenActor = frozenLayer.FromID(a.ActorID); + } + } + + public RangedGpsDotEffect(Actor actor, RangedGpsDot trait) + { + this.actor = actor; + this.trait = trait; + anim = new Animation(actor.World, trait.Info.Image); + anim.PlayRepeating(trait.Info.Sequence); + + visibility = actor.Trait(); + visibilityModifiers = actor.TraitsImplementing().ToArray(); + + dotStates = new PlayerDictionary(actor.World, + p => new RangedDotState(actor, p.PlayerActor.Trait(), p.FrozenActorLayer)); + } + + bool ShouldRender(RangedDotState state, Player toPlayer) + { + // Hide the indicator if the owner trait is disabled + if (trait.IsTraitDisabled) + return false; + + // Hide the indicator if no watchers are available + if (!state.Watcher.Granted && !state.Watcher.GrantedAllies) + return false; + + // Hide the indicator if a frozen actor portrait is visible + if (state.FrozenActor != null && state.FrozenActor.HasRenderables) + return false; + + // Hide the indicator if the unit appears to be owned by an allied player + if (actor.EffectiveOwner != null && actor.EffectiveOwner.Owner != null && + toPlayer.IsAlliedWith(actor.EffectiveOwner.Owner)) + return false; + + // Hide indicator if the actor wouldn't otherwise be visible if there wasn't fog + foreach (var visibilityModifier in visibilityModifiers) + if (!visibilityModifier.IsVisible(actor, toPlayer)) + return false; + + // Hide the indicator behind shroud + if (!trait.Info.VisibleInShroud && !toPlayer.Shroud.IsExplored(actor.CenterPosition)) + return false; + + // Hide the indicator if it is not in range of a provider + if (!trait.Providers.Any(p => p.Owner == toPlayer && !p.IsDead)) + return false; + + return !visibility.IsVisible(actor, toPlayer); + } + + void IEffect.Tick(World world) + { + for (var playerIndex = 0; playerIndex < dotStates.Count; playerIndex++) + { + var state = dotStates[playerIndex]; + state.Visible = ShouldRender(state, world.Players[playerIndex]); + } + } + + IEnumerable IEffect.Render(WorldRenderer wr) + { + return SpriteRenderable.None; + } + + IEnumerable IEffectAboveShroud.RenderAboveShroud(WorldRenderer wr) + { + if (actor.World.RenderPlayer == null || !dotStates[actor.World.RenderPlayer].Visible) + return SpriteRenderable.None; + + var effectiveOwner = actor.EffectiveOwner != null && actor.EffectiveOwner.Owner != null ? + actor.EffectiveOwner.Owner : actor.Owner; + + var palette = wr.Palette(trait.Info.IndicatorPalettePrefix + effectiveOwner.InternalName); + return anim.Render(actor.CenterPosition, palette); + } + } +} diff --git a/OpenRA.Mods.AS/Effects/SmokeParticle.cs b/OpenRA.Mods.AS/Effects/SmokeParticle.cs new file mode 100644 index 000000000000..a17bde6f6683 --- /dev/null +++ b/OpenRA.Mods.AS/Effects/SmokeParticle.cs @@ -0,0 +1,154 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Effects; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Effects +{ + class SmokeParticle : IEffect + { + readonly Actor invoker; + readonly World world; + readonly ISmokeParticleInfo smoke; + readonly Animation anim; + readonly WDist[] speed; + readonly WDist[] gravity; + readonly bool visibleThroughFog; + readonly bool canDamage; + readonly int turnRate; + readonly HashSet reloadModifiers = new(); + readonly HashSet damageModifiers = new(); + + WPos pos, lastPos; + int lifetime; + int explosionInterval; + + int facing; + bool ending; + + public SmokeParticle(Actor invoker, ISmokeParticleInfo smoke, WPos pos, int facing = -1, bool visibleThroughFog = false) + { + this.invoker = invoker; + world = invoker.World; + this.pos = pos; + this.smoke = smoke; + speed = smoke.Speed; + gravity = smoke.Gravity; + this.visibleThroughFog = visibleThroughFog; + + if (invoker != null && !invoker.IsDead) + { + reloadModifiers = invoker.TraitsImplementing().ToHashSet(); + damageModifiers = invoker.TraitsImplementing().ToHashSet(); + } + + this.facing = facing > -1 + ? facing + : world.SharedRandom.Next(256); + + turnRate = smoke.TurnRate; + anim = new Animation(world, smoke.Image, () => WAngle.FromFacing(facing)); + if (smoke.StartSequences != null && smoke.StartSequences.Any()) + anim.PlayThen(smoke.StartSequences.Random(world.SharedRandom), + () => anim.PlayRepeating(smoke.Sequences.Random(world.SharedRandom))); + else + anim.PlayRepeating(smoke.Sequences.Random(world.SharedRandom)); + world.ScreenMap.Add(this, pos, anim.Image); + lifetime = smoke.Duration.Length == 2 + ? world.SharedRandom.Next(smoke.Duration[0], smoke.Duration[1]) + : smoke.Duration[0]; + + canDamage = smoke.Weapon != null; + } + + public void Tick(World world) + { + lastPos = pos; + if (--lifetime < 0 && !ending) + { + if (smoke.EndSequences != null && smoke.EndSequences.Length > 0) + { + ending = true; + anim.PlayThen(smoke.EndSequences.Random(world.SharedRandom), () => + { + world.AddFrameEndTask(w => + { + w.Remove(this); + w.ScreenMap.Remove(this); + }); + }); + } + else + { + world.AddFrameEndTask(w => + { + w.Remove(this); + w.ScreenMap.Remove(this); + }); + } + + return; + } + + anim.Tick(); + + var forward = speed.Length == 2 + ? world.SharedRandom.Next(speed[0].Length, speed[1].Length) + : speed[0].Length; + + var height = gravity.Length == 2 + ? world.SharedRandom.Next(gravity[0].Length, gravity[1].Length) + : gravity[0].Length; + + var offset = new WVec(forward, 0, height); + + if (turnRate > 0) + facing = (facing + world.SharedRandom.Next(-turnRate, turnRate)) & 0xFF; + + offset = offset.Rotate(WRot.FromFacing(facing)); + + pos += offset; + + world.ScreenMap.Update(this, pos, anim.Image); + + if (canDamage && --explosionInterval < 0) + { + var args = new WarheadArgs + { + Weapon = smoke.Weapon, + DamageModifiers = damageModifiers.Select(a => a.GetFirepowerModifier(null)).ToArray(), + Source = pos, + SourceActor = invoker, + WeaponTarget = Target.FromPos(pos), + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(lastPos, pos), WAngle.FromFacing(facing)), + ImpactPosition = pos + }; + + smoke.Weapon.Impact(Target.FromPos(pos), args); + explosionInterval = Common.Util.ApplyPercentageModifiers(smoke.Weapon.ReloadDelay, reloadModifiers.Select(m => m.GetReloadModifier(null))); + } + } + + public IEnumerable Render(WorldRenderer wr) + { + if (world.FogObscures(pos) && !visibleThroughFog) + return SpriteRenderable.None; + + return anim.Render(pos, WVec.Zero, 0, wr.Palette(smoke.Palette)); + } + } +} diff --git a/OpenRA.Mods.AS/Effects/WarheadTrailProjectileEffect.cs b/OpenRA.Mods.AS/Effects/WarheadTrailProjectileEffect.cs new file mode 100644 index 000000000000..b36e1a321c85 --- /dev/null +++ b/OpenRA.Mods.AS/Effects/WarheadTrailProjectileEffect.cs @@ -0,0 +1,184 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Effects; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Projectiles; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Effects +{ + public class WarheadTrailProjectileEffect : IEffect, ISync + { + readonly WarheadTrailProjectileInfo info; + readonly ProjectileArgs args; + readonly Animation anim; + readonly string trailPalette; + readonly World world; + + [Sync] + readonly WPos targetpos, source; + [Sync] + readonly WAngle facing; + + readonly int lifespan, estimatedlifespan; + readonly bool forceToGround; + + readonly ContrailRenderable contrail; + + [Sync] + WPos projectilepos, lastPos; + + int ticks, smokeTicks; + public bool DetonateSelf { get; private set; } + public WPos Position { get { return projectilepos; } } + + public WarheadTrailProjectileEffect(WarheadTrailProjectileInfo info, ProjectileArgs args, int lifespan, int estimatedlifespan, bool forceToGround) + { + this.info = info; + this.args = args; + this.lifespan = lifespan; + this.estimatedlifespan = estimatedlifespan; + this.forceToGround = forceToGround; + projectilepos = args.Source; + source = args.Source; + + world = args.SourceActor.World; + targetpos = args.PassiveTarget; + facing = args.Facing; + + if (!string.IsNullOrEmpty(info.Image)) + { + anim = new Animation(world, info.Image, new Func(GetEffectiveFacing)); + anim.PlayRepeating(info.Sequences.Random(world.SharedRandom)); + } + + if (info.ContrailLength > 0) + { + var startcolor = info.ContrailStartColorUsePlayerColor ? Color.FromArgb(info.ContrailStartColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailStartColorAlpha, info.ContrailStartColor); + var endcolor = info.ContrailEndColorUsePlayerColor ? Color.FromArgb(info.ContrailEndColorAlpha, args.SourceActor.Owner.Color) : Color.FromArgb(info.ContrailEndColorAlpha, info.ContrailEndColor ?? startcolor); + contrail = new ContrailRenderable(world, startcolor, endcolor, info.ContrailStartWidth, info.ContrailEndWidth ?? info.ContrailStartWidth, info.ContrailLength, info.ContrailDelay, info.ContrailZOffset); + } + + trailPalette = info.TrailPalette; + if (info.TrailUsePlayerPalette) + trailPalette += args.SourceActor.Owner.InternalName; + + smokeTicks = info.TrailDelay; + } + + WAngle GetEffectiveFacing() + { + var at = (float)ticks / (lifespan - 1); + var attitude = WAngle.Zero.Tan() * (1 - 2 * at) / (4 * 1024); + + var u = facing.Angle % 512 / 512f; + var scale = 2048 * u * (1 - u); + + var effective = (int)(facing.Angle < 512 + ? facing.Angle - scale * attitude + : facing.Angle + scale * attitude); + + return new WAngle(effective); + } + + public IEnumerable Render(WorldRenderer wr) + { + if (info.ContrailLength > 0) + yield return contrail; + + if (anim == null || ticks >= lifespan) + yield break; + + if (!world.FogObscures(projectilepos)) + { + if (info.Shadow) + { + var dat = world.Map.DistanceAboveTerrain(projectilepos); + var shadowPos = projectilepos - new WVec(0, 0, dat.Length); + foreach (var r in anim.Render(shadowPos, wr.Palette(info.ShadowPalette))) + yield return r; + } + + var palette = wr.Palette(info.Palette); + foreach (var r in anim.Render(projectilepos, palette)) + yield return r; + } + } + + public void Tick(World world) + { + ticks++; + anim?.Tick(); + + lastPos = projectilepos; + projectilepos = WPos.Lerp(source, targetpos, ticks, estimatedlifespan); + + // Check for walls or other blocking obstacles. + if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, args.SourceActor.Owner, lastPos, projectilepos, info.Width, out var blockedPos)) + { + projectilepos = blockedPos; + DetonateSelf = true; + } + + if (!string.IsNullOrEmpty(info.TrailImage) && --smokeTicks < 0) + { + var delayedPos = WPos.Lerp(source, targetpos, ticks - info.TrailDelay, estimatedlifespan); + world.AddFrameEndTask(w => w.Add(new SpriteEffect(delayedPos, GetEffectiveFacing(), w, + info.TrailImage, info.TrailSequences.Random(world.SharedRandom), trailPalette))); + + smokeTicks = info.TrailInterval; + } + + if (info.ContrailLength > 0) + contrail.Update(projectilepos); + + var flightLengthReached = ticks >= lifespan; + + if (flightLengthReached) + DetonateSelf = true; + + // Driving into cell with higher height level + DetonateSelf |= world.Map.DistanceAboveTerrain(projectilepos) < info.ExplodeUnderThisAltitude; + + if (DetonateSelf) + Explode(world); + } + + public void Explode(World world) + { + Impact(); + + if (info.ContrailLength > 0) + world.AddFrameEndTask(w => w.Add(new ContrailFader(projectilepos, contrail))); + + world.AddFrameEndTask(w => w.Remove(this)); + } + + public void Impact() + { + var pos = forceToGround ? projectilepos - new WVec(0, 0, world.Map.DistanceAboveTerrain(projectilepos).Length) : projectilepos; + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(lastPos, projectilepos), args.Facing), + ImpactPosition = pos, + }; + + args.Weapon.Impact(Target.FromPos(pos), warheadArgs); + } + } +} diff --git a/OpenRA.Mods.AS/Graphics/ArcRenderable.cs b/OpenRA.Mods.AS/Graphics/ArcRenderable.cs new file mode 100644 index 000000000000..f6cf03d4d814 --- /dev/null +++ b/OpenRA.Mods.AS/Graphics/ArcRenderable.cs @@ -0,0 +1,60 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Graphics; +using OpenRA.Primitives; + +namespace OpenRA.Mods.AS.Graphics +{ + public readonly struct ArcRenderable : IRenderable, IFinalizedRenderable + { + readonly Color color; + readonly WPos b; + readonly WAngle angle; + readonly WDist width; + readonly int segments; + + public ArcRenderable(WPos a, WPos b, int zOffset, WAngle angle, Color color, WDist width, int segments) + { + Pos = a; + this.b = b; + this.angle = angle; + this.color = color; + ZOffset = zOffset; + this.width = width; + this.segments = segments; + } + + public WPos Pos { get; } + public PaletteReference Palette { get { return null; } } + public int ZOffset { get; } + public bool IsDecoration { get { return true; } } + + public IRenderable WithPalette(PaletteReference newPalette) { return new ArcRenderable(Pos, b, ZOffset, angle, color, width, segments); } + public IRenderable WithZOffset(int newOffset) { return new ArcRenderable(Pos, b, ZOffset, angle, color, width, segments); } + public IRenderable OffsetBy(in WVec vec) { return new ArcRenderable(Pos + vec, b + vec, ZOffset, angle, color, width, segments); } + public IRenderable AsDecoration() { return this; } + + public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; } + public void Render(WorldRenderer wr) + { + var screenWidth = wr.ScreenVector(new WVec(width, WDist.Zero, WDist.Zero))[0]; + + var points = new float3[segments + 1]; + for (var i = 0; i <= segments; i++) + points[i] = wr.Screen3DPosition(WPos.LerpQuadratic(Pos, b, angle, i, segments)); + + Game.Renderer.WorldRgbaColorRenderer.DrawLine(points, screenWidth, color, false); + } + + public void RenderDebugGeometry(WorldRenderer wr) { } + public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; } + } +} diff --git a/OpenRA.Mods.AS/Graphics/ElectricBoltRenderable.cs b/OpenRA.Mods.AS/Graphics/ElectricBoltRenderable.cs new file mode 100644 index 000000000000..e941222139f1 --- /dev/null +++ b/OpenRA.Mods.AS/Graphics/ElectricBoltRenderable.cs @@ -0,0 +1,58 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Primitives; + +namespace OpenRA.Mods.AS.Graphics +{ + public readonly struct ElectricBoltRenderable : IRenderable, IFinalizedRenderable + { + readonly WPos[] offsets; + readonly WDist width; + readonly Color color; + + public ElectricBoltRenderable(WPos[] offsets, int zOffset, WDist width, Color color) + { + this.offsets = offsets; + ZOffset = zOffset; + this.width = width; + this.color = color; + } + + public WPos Pos { get { return new WPos(offsets[0].X, offsets[0].Y, 0); } } + public PaletteReference Palette { get { return null; } } + public int ZOffset { get; } + public bool IsDecoration { get { return true; } } + + public IRenderable WithPalette(PaletteReference newPalette) { return this; } + public IRenderable WithZOffset(int newOffset) { return new ElectricBoltRenderable(offsets, newOffset, width, color); } + public IRenderable OffsetBy(in WVec vec) + { + // Lambdas can't use 'in' variables, so capture a copy for later + var offset = vec; + return new ElectricBoltRenderable(offsets.Select(o => o + offset).ToArray(), ZOffset, width, color); + } + + public IRenderable AsDecoration() { return this; } + + public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; } + public void Render(WorldRenderer wr) + { + var screenWidth = wr.ScreenVector(new WVec(width, WDist.Zero, WDist.Zero))[0]; + + Game.Renderer.WorldRgbaColorRenderer.DrawLine(offsets.Select(offset => wr.Screen3DPosition(offset)), screenWidth, color, false); + } + + public void RenderDebugGeometry(WorldRenderer wr) { } + public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; } + } +} diff --git a/OpenRA.Mods.AS/Graphics/KKNDLaserRenderable.cs b/OpenRA.Mods.AS/Graphics/KKNDLaserRenderable.cs new file mode 100644 index 000000000000..ef8fb10952db --- /dev/null +++ b/OpenRA.Mods.AS/Graphics/KKNDLaserRenderable.cs @@ -0,0 +1,58 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Primitives; + +namespace OpenRA.Mods.AS.Graphics +{ + public readonly struct KKNDLaserRenderable : IRenderable, IFinalizedRenderable + { + readonly WPos[] offsets; + readonly WDist width; + readonly Color color; + + public KKNDLaserRenderable(WPos[] offsets, int zOffset, WDist width, Color color) + { + this.offsets = offsets; + ZOffset = zOffset; + this.width = width; + this.color = color; + } + + public WPos Pos { get { return new WPos(offsets[0].X, offsets[0].Y, 0); } } + public PaletteReference Palette { get { return null; } } + public int ZOffset { get; } + public bool IsDecoration { get { return true; } } + + public IRenderable WithPalette(PaletteReference newPalette) { return this; } + public IRenderable WithZOffset(int newOffset) { return new KKNDLaserRenderable(offsets, newOffset, width, color); } + public IRenderable OffsetBy(in WVec vec) + { + // Lambdas can't use 'in' variables, so capture a copy for later + var offset = vec; + return new KKNDLaserRenderable(offsets.Select(o => o + offset).ToArray(), ZOffset, width, color); + } + + public IRenderable AsDecoration() { return this; } + + public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; } + public void Render(WorldRenderer wr) + { + var screenWidth = wr.ScreenVector(new WVec(width, WDist.Zero, WDist.Zero))[0]; + + Game.Renderer.WorldRgbaColorRenderer.DrawLine(offsets.Select(offset => wr.Screen3DPosition(offset)), screenWidth, color, false); + } + + public void RenderDebugGeometry(WorldRenderer wr) { } + public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; } + } +} diff --git a/OpenRA.Mods.AS/Graphics/RadBeamRenderable.cs b/OpenRA.Mods.AS/Graphics/RadBeamRenderable.cs new file mode 100644 index 000000000000..093db81a4a82 --- /dev/null +++ b/OpenRA.Mods.AS/Graphics/RadBeamRenderable.cs @@ -0,0 +1,98 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Graphics; +using OpenRA.Primitives; + +namespace OpenRA.Mods.AS.Graphics +{ + public readonly struct RadBeamRenderable : IRenderable, IFinalizedRenderable + { + readonly WVec sourceToTarget; + readonly WDist width; + readonly Color color; + readonly WDist amplitude; + readonly WDist wavelength; + readonly int quantizationCount; + + public RadBeamRenderable( + WPos pos, + int zOffset, + WVec sourceToTarget, + WDist width, Color color, + WDist amplitude, WDist wavelength, + int quantizationCount) + { + Pos = pos; + ZOffset = zOffset; + this.sourceToTarget = sourceToTarget; + this.width = width; + this.color = color; + this.amplitude = amplitude; + this.wavelength = wavelength; + this.quantizationCount = quantizationCount; + } + + public WPos Pos { get; } + public PaletteReference Palette { get { return null; } } + public int ZOffset { get; } + public bool IsDecoration { get { return true; } } + + public IRenderable WithPalette(PaletteReference newPalette) + { + return new RadBeamRenderable(Pos, ZOffset, sourceToTarget, width, color, amplitude, wavelength, quantizationCount); + } + + public IRenderable WithZOffset(int newOffset) { return new RadBeamRenderable(Pos, ZOffset, sourceToTarget, width, color, amplitude, wavelength, quantizationCount); } + + public IRenderable OffsetBy(in WVec vec) { return new RadBeamRenderable(Pos + vec, ZOffset, sourceToTarget, width, color, amplitude, wavelength, quantizationCount); } + + public IRenderable AsDecoration() { return this; } + + public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; } + public void Render(WorldRenderer wr) + { + if (sourceToTarget == WVec.Zero) + return; + + // WAngle.Sin(x) = 1024 * Math.Sin(2pi/1024 * x) + + // forward step, pointing from src to target. + // QuantizationCont * forwardStep == One cycle of beam in src2target direction. + var forwardStep = wavelength.Length * sourceToTarget / (quantizationCount * sourceToTarget.Length); + + var cycleCnt = sourceToTarget.Length / wavelength.Length; + if (sourceToTarget.Length % wavelength.Length != 0) + cycleCnt += 1; // I'm emulating math.ceil + + var screenWidth = wr.ScreenVector(new WVec(width, WDist.Zero, WDist.Zero))[0]; + + var angle = new WAngle(0); + var angleStep = new WAngle(1024 / quantizationCount); + + // last point the rad beam "reached" + var pos = Pos; // where we are. + var last = wr.Screen3DPosition(pos); // we start from the shooter + for (var i = 0; i < cycleCnt * quantizationCount; i++) + { + var y = new WVec(0, 0, amplitude.Length * angle.Sin() / 1024); + var end = wr.Screen3DPosition(pos + y); + Game.Renderer.WorldRgbaColorRenderer.DrawLine(last, end, screenWidth, color); + + pos += forwardStep; // keep moving along x axis + last = end; + angle += angleStep; + } + } + + public void RenderDebugGeometry(WorldRenderer wr) { } + public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; } + } +} diff --git a/OpenRA.Mods.AS/Graphics/TintedCell.cs b/OpenRA.Mods.AS/Graphics/TintedCell.cs new file mode 100644 index 000000000000..37929f57a15e --- /dev/null +++ b/OpenRA.Mods.AS/Graphics/TintedCell.cs @@ -0,0 +1,107 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Effects; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Traits; +using OpenRA.Primitives; + +namespace OpenRA.Mods.AS.Graphics +{ + public class TintedCell : IRenderable, IFinalizedRenderable, IEffect + { + public int Ticks = 0; + readonly TintedCellsLayer layer; + readonly CPos cpos; + readonly WPos wpos; + + public int Level { get; private set; } + public int ZOffset { get { return layer.Info.ZOffset; } } + + public TintedCell(TintedCellsLayer layer, CPos cpos, WPos wpos) + { + this.layer = layer; + this.cpos = cpos; + this.wpos = wpos; + } + + public TintedCell(TintedCell src) + { + Ticks = src.Ticks; + Level = src.Level; + layer = src.layer; + cpos = src.cpos; + wpos = src.wpos; + } + + public IRenderable WithPalette(PaletteReference newPalette) { return this; } + public IRenderable WithZOffset(int newOffset) { return this; } + public IRenderable OffsetBy(in WVec vec) { return this; } + public IRenderable AsDecoration() { return this; } + + public PaletteReference Palette { get { return null; } } + public bool IsDecoration { get { return false; } } + + WPos IRenderable.Pos { get { return wpos; } } + + IFinalizedRenderable IRenderable.PrepareRender(WorldRenderer wr) { return this; } + + bool firstTime = true; + float3[] screen; + int alpha; + public void Render(WorldRenderer wr) + { + if (firstTime) + { + var map = wr.World.Map; + var terrainInfo = wr.World.Map.Rules.TerrainInfo; + var uv = cpos.ToMPos(map); + + if (!map.Height.Contains(uv)) + return; + + var tile = map.Tiles[uv]; + var ti = terrainInfo.GetTerrainInfo(tile); + var ramp = ti != null ? ti.RampType : 0; + + var corners = map.Grid.Ramps[ramp].Corners; + screen = corners.Select(c => wr.Screen3DPxPosition(wpos + c - new WVec(0, 0, map.Grid.Ramps[ramp].CenterHeightOffset) + new WVec(0, 0, ZOffset))).ToArray(); + SetLevel(Level); + firstTime = false; + } + + if (Level == 0) + return; + + Game.Renderer.WorldRgbaColorRenderer.FillRect(screen[0], screen[1], screen[2], screen[3], Color.FromArgb(alpha, layer.Info.Color)); + } + + public void SetLevel(int value) + { + Level = value; + + if (layer == null) + return; + + // Saturate the visualization to MaxLevel + var level = Level.Clamp(0, layer.Info.MaxLevel); + + // Linear interpolation + alpha = layer.Info.Darkest + layer.TintLevel * level / 255; + } + + public void RenderDebugGeometry(WorldRenderer wr) { } + public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; } + public void Tick(World world) { } + IEnumerable IEffect.Render(WorldRenderer r) { yield return this; } + } +} diff --git a/OpenRA.Mods.AS/Lint/CheckSpawnActorWarheads.cs b/OpenRA.Mods.AS/Lint/CheckSpawnActorWarheads.cs new file mode 100644 index 000000000000..f84b94bb390b --- /dev/null +++ b/OpenRA.Mods.AS/Lint/CheckSpawnActorWarheads.cs @@ -0,0 +1,51 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.Mods.AS.Warheads; +using OpenRA.Mods.Common.Lint; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Lint +{ + class CheckSpawnActorWarheads : ILintRulesPass + { + public void Run(Action emitError, Action emitWarning, ModData modData, Ruleset rules) + { + foreach (var weaponInfo in rules.Weapons) + { + var warheads = weaponInfo.Value.Warheads.OfType().ToList(); + + foreach (var warhead in warheads) + { + foreach (var a in warhead.Actors) + { + if (!rules.Actors.ContainsKey(a.ToLowerInvariant())) + { + emitError($"Warhead type {weaponInfo.Key} tries to spawn invalid actor {a}!"); + break; + } + + if (!rules.Actors[a.ToLowerInvariant()].HasTraitInfo()) + emitError($"Warhead type {weaponInfo.Key} tries to spawn unpositionable actor {a}!"); + + if (rules.Actors[a.ToLowerInvariant()].HasTraitInfo()) + emitError($"Warhead type {weaponInfo.Key} tries to spawn building {a}!"); + + if (!rules.Actors[a.ToLowerInvariant()].HasTraitInfo() && warhead.Paradrop) + emitError($"Warhead type {weaponInfo.Key} tries to paradrop actor {a} which doesn't have the Parachutable trait!"); + } + } + } + } + } +} diff --git a/OpenRA.Mods.AS/OpenRA.Mods.AS.csproj b/OpenRA.Mods.AS/OpenRA.Mods.AS.csproj new file mode 100644 index 000000000000..c1f714f45b4e --- /dev/null +++ b/OpenRA.Mods.AS/OpenRA.Mods.AS.csproj @@ -0,0 +1,16 @@ + + + false + + + + False + + + False + + + False + + + diff --git a/OpenRA.Mods.AS/Orders/EnterGarrisonOrderTargeter.cs b/OpenRA.Mods.AS/Orders/EnterGarrisonOrderTargeter.cs new file mode 100644 index 000000000000..da8b11d59eb9 --- /dev/null +++ b/OpenRA.Mods.AS/Orders/EnterGarrisonOrderTargeter.cs @@ -0,0 +1,53 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Orders; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Orders +{ + public class EnterGarrisonOrderTargeter : UnitOrderTargeter where GarrisonableInfo : TraitInfo + { + readonly Func canTarget; + readonly Func useEnterCursor; + readonly GarrisonerInfo garrisonerInfo; + + public EnterGarrisonOrderTargeter(string order, int priority, + Func canTarget, Func useEnterCursor, GarrisonerInfo garrisonerInfo) + : base(order, priority, "enter", true, true) + { + this.canTarget = canTarget; + this.useEnterCursor = useEnterCursor; + this.garrisonerInfo = garrisonerInfo; + } + + public override bool CanTargetActor(Actor self, Actor target, TargetModifiers modifiers, ref string cursor) + { + if (garrisonerInfo.TargetRelationships.HasRelationship(self.Owner.RelationshipWith(target.Owner)) && target.Info.HasTraitInfo() && canTarget(target, modifiers)) + { + cursor = useEnterCursor(target) ? "enter" : "enter-blocked"; + return true; + } + else + return false; + } + + public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor) + { + /* Frozen Actor garrisoning is broken af currently. Disallow it for now, plus seems like CnC3 didn't allow that either. + if (target.Info.HasTraitInfo()) + return true; */ + + return false; + } + } +} diff --git a/OpenRA.Mods.AS/Projectiles/ArcLaserZap.cs b/OpenRA.Mods.AS/Projectiles/ArcLaserZap.cs new file mode 100644 index 000000000000..5b11017748f3 --- /dev/null +++ b/OpenRA.Mods.AS/Projectiles/ArcLaserZap.cs @@ -0,0 +1,162 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Projectiles +{ + [Desc("Not a sprite, but an engine effect.")] + public class ArcLaserZapInfo : IProjectileInfo + { + [Desc("The width of the zap.")] + public readonly WDist Width = new(86); + + [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")] + public readonly int ZOffset = 0; + + public readonly int Duration = 10; + + [Desc("The angle of the arc of the beam.")] + public readonly WAngle Angle = WAngle.FromDegrees(30); + + [Desc("Controls how fine-grained the resulting arc should be.")] + public readonly int QuantizedSegments = 32; + + public readonly bool UsePlayerColor = false; + + [Desc("Color of the beam.")] + public readonly Color Color = Color.Red; + + [Desc("Beam follows the target.")] + public readonly bool TrackTarget = true; + + [Desc("Beam follows the source.")] + public readonly bool TrackSource = true; + + [Desc("Maximum offset at the maximum range.")] + public readonly WDist Inaccuracy = WDist.Zero; + + [Desc("Beam can be blocked.")] + public readonly bool Blockable = false; + + [Desc("Impact animation.")] + public readonly string HitAnim = null; + + [Desc("Sequence of impact animation to use.")] + [SequenceReference(nameof(HitAnim), allowNullImage: true)] + public readonly string HitAnimSequence = "idle"; + + [PaletteReference] + public readonly string HitAnimPalette = "effect"; + + public IProjectile Create(ProjectileArgs args) + { + var c = UsePlayerColor ? args.SourceActor.Owner.Color : Color; + return new ArcLaserZap(this, args, c); + } + } + + public class ArcLaserZap : IProjectile, ISync + { + readonly ProjectileArgs args; + readonly ArcLaserZapInfo info; + readonly Animation hitanim; + readonly Color color; + + [Sync] + WPos source; + + int ticks = 0; + bool doneDamage; + bool animationComplete; + + [Sync] + WPos target; + + public ArcLaserZap(ArcLaserZapInfo info, ProjectileArgs args, Color color) + { + this.args = args; + this.info = info; + this.color = color; + target = args.PassiveTarget; + source = args.Source; + + if (info.Inaccuracy.Length > 0) + { + var inaccuracy = Common.Util.ApplyPercentageModifiers(info.Inaccuracy.Length, args.InaccuracyModifiers); + var maxOffset = inaccuracy * (target - source).Length / args.Weapon.Range.Length; + target += WVec.FromPDF(args.SourceActor.World.SharedRandom, 2) * maxOffset / 1024; + } + + if (!string.IsNullOrEmpty(info.HitAnim)) + hitanim = new Animation(args.SourceActor.World, info.HitAnim); + } + + public void Tick(World world) + { + if (info.TrackSource) + source = args.CurrentSource(); + + // Beam tracks target + if (info.TrackTarget && args.GuidedTarget.IsValidFor(args.SourceActor)) + target = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(source); + + // Check for blocking actors + if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, args.SourceActor.Owner, source, target, + info.Width, out var blockedPos)) + target = blockedPos; + + if (!doneDamage) + { + if (hitanim != null) + hitanim.PlayThen(info.HitAnimSequence, () => animationComplete = true); + else + animationComplete = true; + + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(source, target), args.CurrentMuzzleFacing()), + ImpactPosition = target, + }; + + args.Weapon.Impact(Target.FromPos(target), warheadArgs); + doneDamage = true; + } + + hitanim?.Tick(); + + if (++ticks >= info.Duration && animationComplete) + world.AddFrameEndTask(w => w.Remove(this)); + } + + public IEnumerable Render(WorldRenderer wr) + { + if (wr.World.FogObscures(target) && + wr.World.FogObscures(source)) + yield break; + + if (ticks < info.Duration) + { + var rc = Color.FromArgb((info.Duration - ticks) * color.A / info.Duration, color); + yield return new ArcRenderable(source, target, info.ZOffset, info.Angle, rc, info.Width, info.QuantizedSegments); + } + + if (hitanim != null) + foreach (var r in hitanim.Render(target, wr.Palette(info.HitAnimPalette))) + yield return r; + } + } +} diff --git a/OpenRA.Mods.AS/Projectiles/AthenaProjectile.cs b/OpenRA.Mods.AS/Projectiles/AthenaProjectile.cs new file mode 100644 index 000000000000..fa757a50edde --- /dev/null +++ b/OpenRA.Mods.AS/Projectiles/AthenaProjectile.cs @@ -0,0 +1,71 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Projectiles +{ + [Desc("Dummy projectile exploding on/above the target actor/position after a specified delay.")] + public class AthenaProjectileInfo : IProjectileInfo + { + [Desc("Explosion altitude added to target actor.")] + public readonly WDist Altitude = WDist.Zero; + + [Desc("Delay between firing and exploding.")] + public readonly int Delay = 0; + + public IProjectile Create(ProjectileArgs args) { return new AthenaProjectile(this, args); } + } + + class AthenaProjectile : IProjectile + { + readonly ProjectileArgs args; + readonly WDist altitude; + + int delay; + + public AthenaProjectile(AthenaProjectileInfo info, ProjectileArgs args) + { + this.args = args; + altitude = info.Altitude; + delay = info.Delay; + } + + public void Tick(World world) + { + if (--delay < 0) + { + WPos target; + if (args.GuidedTarget.IsValidFor(args.SourceActor)) + target = args.GuidedTarget.CenterPosition + new WVec(WDist.Zero, WDist.Zero, altitude); + else + target = args.PassiveTarget + new WVec(WDist.Zero, WDist.Zero, altitude); + + world.AddFrameEndTask(w => w.Remove(this)); + + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(args.Source, target), args.CurrentMuzzleFacing()), + ImpactPosition = target, + }; + + args.Weapon.Impact(Target.FromPos(target), warheadArgs); + } + } + + public IEnumerable Render(WorldRenderer wr) + { + yield break; + } + } +} diff --git a/OpenRA.Mods.AS/Projectiles/ElectricBolt.cs b/OpenRA.Mods.AS/Projectiles/ElectricBolt.cs new file mode 100644 index 000000000000..b2fe45684f4f --- /dev/null +++ b/OpenRA.Mods.AS/Projectiles/ElectricBolt.cs @@ -0,0 +1,188 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Graphics; +using OpenRA.Mods.Common.Effects; +using OpenRA.Primitives; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Projectiles +{ + [Desc("Not a sprite, but an engine effect.")] + public class ElectricBoltInfo : IProjectileInfo + { + [Desc("The width of the zap.")] + public readonly WDist Width = new(12); + + [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")] + public readonly int ZOffset = 0; + + [Desc("The maximum duration (in ticks) of the beam's existence.")] + public readonly int Duration = 5; + + [Desc("Colors of the zaps. The amount of zaps are the amount of colors listed here and PlayerColorZaps.")] + public readonly Color[] Colors = + { + Color.FromArgb(80, 80, 255), + Color.FromArgb(80, 80, 255), + Color.FromArgb(255, 255, 255) + }; + + [Desc("Additional zaps colored with the player's color.")] + public readonly int PlayerColorZaps = 0; + + [Desc("Distortion offset.")] + public readonly int Distortion = 128; + + [Desc("The maximum angle of the arc of the bolt.")] + public readonly WAngle Angle = WAngle.FromDegrees(90); + + [Desc("Maximum length per segment.")] + public readonly WDist SegmentLength = new(320); + + [Desc("Image containing launch effect sequence.")] + public readonly string LaunchEffectImage = null; + + [Desc("Launch effect sequence to play.")] + [SequenceReference(nameof(LaunchEffectImage), allowNullImage: true)] + public readonly string LaunchEffectSequence = null; + + [Desc("Palette to use for launch effect.")] + [PaletteReference] + public readonly string LaunchEffectPalette = "effect"; + + public IProjectile Create(ProjectileArgs args) + { + return new ElectricBolt(this, args); + } + } + + public class ElectricBolt : IProjectile, ISync + { + readonly ProjectileArgs args; + readonly ElectricBoltInfo info; + readonly WVec leftVector; + readonly WVec upVector; + readonly MersenneTwister random; + readonly bool hasLaunchEffect; + readonly HashSet<(Color Color, WPos[] Positions, WPos[] PosCache)> zaps; + + [Sync] + readonly WPos target, source; + + int ticks = 0; + + public ElectricBolt(ElectricBoltInfo info, ProjectileArgs args) + { + this.args = args; + this.info = info; + var playerColors = args.SourceActor.Owner.Color; + var colors = info.Colors; + for (var i = 0; i < info.PlayerColorZaps; i++) + colors.Append(playerColors); + + target = args.PassiveTarget; + source = args.Source; + random = args.SourceActor.World.LocalRandom; + + hasLaunchEffect = !string.IsNullOrEmpty(info.LaunchEffectImage) && !string.IsNullOrEmpty(info.LaunchEffectSequence); + + var direction = args.PassiveTarget - args.Source; + + if (info.Distortion != 0) + { + leftVector = new WVec(direction.Y, -direction.X, 0); + if (leftVector.Length != 0) + leftVector = 1024 * leftVector / leftVector.Length; + + upVector = leftVector.Length != 0 + ? new WVec( + -direction.X * direction.Z, + -direction.Z * direction.Y, + direction.X * direction.X + direction.Y * direction.Y) + : new WVec(direction.Z, direction.Z, 0); + if (upVector.Length != 0) + upVector = 1024 * upVector / upVector.Length; + } + + zaps = new HashSet<(Color Color, WPos[] Positions, WPos[] PosCache)>(); + foreach (var c in colors) + { + var numSegments = (direction.Length - 1) / info.SegmentLength.Length + 1; + var offsets = new WPos[numSegments + 1]; + offsets[0] = args.Source; + offsets[^1] = args.PassiveTarget; + + var angle = new WAngle(-info.Angle.Angle / 2 + random.Next(info.Angle.Angle)); + + for (var i = 1; i < numSegments; i++) + offsets[i] = WPos.LerpQuadratic(source, target, angle, i, numSegments); + + zaps.Add((c, offsets, (WPos[])offsets.Clone())); + } + } + + public void Tick(World world) + { + foreach (var zap in zaps) + { + for (var i = 1; i < zap.Positions.Length - 1; i++) + { + var angle = WAngle.FromDegrees(random.Next(360)); + var distortion = random.Next(info.Distortion); + + var offset = distortion * angle.Cos() * leftVector / (1024 * 1024) + + distortion * angle.Sin() * upVector / (1024 * 1024); + + zap.PosCache[i] = zap.Positions[i] + offset; + } + } + + if (hasLaunchEffect && ticks == 0) + { + world.AddFrameEndTask(w => w.Add(new SpriteEffect(args.CurrentSource, () => args.CurrentMuzzleFacing(), world, + info.LaunchEffectImage, info.LaunchEffectSequence, info.LaunchEffectPalette))); + } + + if (ticks == 0) + { + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(source, target), args.CurrentMuzzleFacing()), + ImpactPosition = target, + }; + + args.Weapon.Impact(Target.FromPos(target), warheadArgs); + } + + if (++ticks >= info.Duration) + world.AddFrameEndTask(w => w.Remove(this)); + } + + public IEnumerable Render(WorldRenderer wr) + { + if (wr.World.FogObscures(target) && + wr.World.FogObscures(source)) + yield break; + + if (ticks < info.Duration) + { + foreach (var zap in zaps) + { + yield return new ElectricBoltRenderable(zap.PosCache, info.ZOffset, info.Width, zap.Color); + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Projectiles/InstantExplode.cs b/OpenRA.Mods.AS/Projectiles/InstantExplode.cs new file mode 100644 index 000000000000..195263ff360e --- /dev/null +++ b/OpenRA.Mods.AS/Projectiles/InstantExplode.cs @@ -0,0 +1,50 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Projectiles +{ + public class InstantExplodeInfo : IProjectileInfo + { + public IProjectile Create(ProjectileArgs args) { return new InstantExplode(args); } + } + + class InstantExplode : IProjectile + { + readonly ProjectileArgs args; + + public InstantExplode(ProjectileArgs args) + { + this.args = args; + } + + public void Tick(World world) + { + world.AddFrameEndTask(w => w.Remove(this)); + + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, WAngle.Zero, args.CurrentMuzzleFacing()), + ImpactPosition = args.Source, + }; + + args.Weapon.Impact(Target.FromPos(args.Source), warheadArgs); + } + + public IEnumerable Render(WorldRenderer wr) + { + yield break; + } + } +} diff --git a/OpenRA.Mods.AS/Projectiles/KKNDLaser.cs b/OpenRA.Mods.AS/Projectiles/KKNDLaser.cs new file mode 100644 index 000000000000..396fbb51db3e --- /dev/null +++ b/OpenRA.Mods.AS/Projectiles/KKNDLaser.cs @@ -0,0 +1,176 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Graphics; +using OpenRA.Primitives; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Projectiles +{ + [Desc("A beautiful generated laser beam.")] + public class KKNDLaserInfo : IProjectileInfo + { + [Desc("The maximum duration (in ticks) of the beam's existence.")] + public readonly int Duration = 10; + + [Desc("Color of the beam. Default falls back to player color.")] + public readonly Color Color = Color.Transparent; + + [Desc("Inner lightness of the beam.")] + public readonly byte InnerLightness = 0xff; + + [Desc("Outer lightness of the beam.")] + public readonly byte OuterLightness = 0x80; + + [Desc("The radius of the beam.")] + public readonly int Radius = 3; + + [Desc("Distortion offset.")] + public readonly int Distortion = 0; + + [Desc("Distortion animation offset.")] + public readonly int DistortionAnimation = 0; + + [Desc("Maximum length per segment.")] + public readonly WDist SegmentLength = WDist.Zero; + + [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")] + public readonly int ZOffset = 0; + + public IProjectile Create(ProjectileArgs args) { return new KKNDLaser(args, this); } + } + + public class KKNDLaser : IProjectile, ISync + { + readonly KKNDLaserInfo info; + readonly Color[] colors; + readonly WPos[] offsets; + + readonly WVec leftVector; + readonly WVec upVector; + readonly MersenneTwister random; + + [Sync] + readonly WPos target; + + [Sync] + readonly WPos source; + + int ticks; + + public KKNDLaser(ProjectileArgs args, KKNDLaserInfo info) + { + this.info = info; + + colors = new Color[info.Radius]; + for (var i = 0; i < info.Radius; i++) + { + var color = info.Color == Color.Transparent ? args.SourceActor.Owner.Color : info.Color; + var bw = (float)((info.InnerLightness - info.OuterLightness) * i / (info.Radius - 1) + info.OuterLightness) / 0xff; + var dstR = bw > .5 ? 1 - (1 - 2 * (bw - .5)) * (1 - (float)color.R / 0xff) : 2 * bw * ((float)color.R / 0xff); + var dstG = bw > .5 ? 1 - (1 - 2 * (bw - .5)) * (1 - (float)color.G / 0xff) : 2 * bw * ((float)color.G / 0xff); + var dstB = bw > .5 ? 1 - (1 - 2 * (bw - .5)) * (1 - (float)color.B / 0xff) : 2 * bw * ((float)color.B / 0xff); + colors[i] = Color.FromArgb((int)(dstR * 0xff), (int)(dstG * 0xff), (int)(dstB * 0xff)); + } + + target = args.PassiveTarget; + source = args.Source; + + var direction = target - source; + + if (info.Distortion != 0 || info.DistortionAnimation != 0) + { + leftVector = new WVec(direction.Y, -direction.X, 0); + if (leftVector.Length != 0) + leftVector = 1024 * leftVector / leftVector.Length; + + upVector = leftVector.Length != 0 + ? new WVec( + -direction.X * direction.Z, + -direction.Z * direction.Y, + direction.X * direction.X + direction.Y * direction.Y) + : new WVec(direction.Z, direction.Z, 0); + if (upVector.Length != 0) + upVector = 1024 * upVector / upVector.Length; + + random = args.SourceActor.World.SharedRandom; + } + + if (this.info.SegmentLength == WDist.Zero) + offsets = new[] { source, target }; + else + { + var numSegments = (direction.Length - 1) / info.SegmentLength.Length + 1; + offsets = new WPos[numSegments + 1]; + offsets[0] = source; + offsets[^1] = target; + + for (var i = 1; i < numSegments; i++) + { + var segmentStart = direction / numSegments * i; + offsets[i] = source + segmentStart; + + if (info.Distortion != 0) + { + var angle = WAngle.FromDegrees(random.Next(360)); + var distortion = random.Next(info.Distortion); + + var offset = distortion * angle.Cos() * leftVector / (1024 * 1024) + + distortion * angle.Sin() * upVector / (1024 * 1024); + + offsets[i] += offset; + } + } + } + + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(source, target), args.CurrentMuzzleFacing()), + ImpactPosition = target, + }; + + args.Weapon.Impact(Target.FromPos(target), warheadArgs); + } + + public void Tick(World world) + { + if (++ticks >= info.Duration) + world.AddFrameEndTask(w => w.Remove(this)); + else if (info.DistortionAnimation != 0) + { + for (var i = 1; i < offsets.Length - 1; i++) + { + var angle = WAngle.FromDegrees(random.Next(360)); + var distortion = random.Next(info.DistortionAnimation); + + var offset = distortion * angle.Cos() * leftVector / (1024 * 1024) + + distortion * angle.Sin() * upVector / (1024 * 1024); + + offsets[i] += offset; + } + } + } + + public IEnumerable Render(WorldRenderer worldRenderer) + { + if (worldRenderer.World.FogObscures(target) && + worldRenderer.World.FogObscures(source)) + yield break; + + for (var i = 0; i < offsets.Length - 1; i++) + for (var j = 0; j < info.Radius; j++) + yield return new KKNDLaserRenderable(offsets, info.ZOffset, new WDist(32 + (info.Radius - j - 1) * 64), colors[j]); + } + } +} diff --git a/OpenRA.Mods.AS/Projectiles/ParaBomb.cs b/OpenRA.Mods.AS/Projectiles/ParaBomb.cs new file mode 100644 index 000000000000..b168cf157697 --- /dev/null +++ b/OpenRA.Mods.AS/Projectiles/ParaBomb.cs @@ -0,0 +1,195 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Projectiles +{ + public class ParaBombInfo : IProjectileInfo + { + [FieldLoader.Require] + public readonly string Image = null; + + [Desc("Loop a randomly chosen sequence of Image from this list while falling.")] + [SequenceReference(nameof(Image))] + public readonly string[] Sequences = { "idle" }; + + [Desc("Sequence to play when launched. Skipped if null or empty.")] + [SequenceReference(nameof(Image))] + public readonly string OpenSequence = null; + + [Desc("The palette used to draw this projectile.")] + [PaletteReference] + public readonly string Palette = "effect"; + + [Desc("Palette is a player palette BaseName")] + public readonly bool IsPlayerPalette = false; + + [Desc("Parachute opening sequence.")] + [SequenceReference(nameof(Image))] + public readonly string ParachuteOpeningSequence = null; + + [Desc("Parachute idle sequence.")] + [SequenceReference(nameof(Image))] + public readonly string ParachuteSequence = null; + + [Desc("Parachute closing sequence. Defaults to opening sequence played backwards.")] + [SequenceReference(nameof(Image))] + public readonly string ParachuteClosingSequence = null; + + [Desc("Palette used to render the parachute.")] + [PaletteReference(nameof(ParachuteIsPlayerPalette))] + public readonly string ParachutePalette = "player"; + public readonly bool ParachuteIsPlayerPalette = true; + + [Desc("Parachute position relative to the paradropped unit.")] + public readonly WVec ParachuteOffset = new(0, 0, 0); + + public readonly bool Shadow = false; + + [PaletteReference] + public readonly string ShadowPalette = "shadow"; + + [Desc("Projectile movement vector per tick (forward, right, up), use negative values for opposite directions.")] + public readonly WVec Velocity = WVec.Zero; + + [Desc("Value added to Velocity every tick.")] + public readonly WVec Acceleration = new(0, 0, -15); + + [Desc("Types of point defense weapons that can target this projectile.")] + public readonly BitSet PointDefenseTypes = default; + + public IProjectile Create(ProjectileArgs args) { return new ParaBomb(this, args); } + } + + public class ParaBomb : IProjectile, ISync + { + readonly ParaBombInfo info; + readonly Animation anim, parachute; + readonly ProjectileArgs args; + readonly WVec acceleration; + + [Sync] + WVec velocity; + [Sync] + WPos pos, lastPos; + + bool exploded; + + public ParaBomb(ParaBombInfo info, ProjectileArgs args) + { + this.info = info; + this.args = args; + pos = args.Source; + var convertedVelocity = new WVec(info.Velocity.Y, -info.Velocity.X, info.Velocity.Z); + velocity = convertedVelocity.Rotate(WRot.FromYaw(args.Facing)); + var convertedAcceleration = new WVec(info.Acceleration.Y, -info.Acceleration.X, info.Acceleration.Z); + acceleration = convertedAcceleration.Rotate(WRot.FromYaw(args.Facing)); + + if (!string.IsNullOrEmpty(info.Image)) + { + anim = new Animation(args.SourceActor.World, info.Image, () => args.Facing); + + if (!string.IsNullOrEmpty(info.OpenSequence)) + anim.PlayThen(info.OpenSequence, () => anim.PlayRepeating(info.Sequences.Random(args.SourceActor.World.SharedRandom))); + else + anim.PlayRepeating(info.Sequences.Random(args.SourceActor.World.SharedRandom)); + + parachute = new Animation(args.SourceActor.World, info.Image, () => args.Facing); + parachute.PlayThen(info.ParachuteOpeningSequence, () => parachute.PlayRepeating(info.ParachuteSequence)); + } + } + + public void Tick(World world) + { + if (!exploded) + { + lastPos = pos; + pos += velocity; + velocity += acceleration; + + if (pos.Z <= args.PassiveTarget.Z) + { + pos += new WVec(0, 0, args.PassiveTarget.Z - pos.Z); + + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(lastPos, pos), args.Facing), + ImpactPosition = pos, + }; + + args.Weapon.Impact(Target.FromPos(pos), warheadArgs); + exploded = true; + + if (!string.IsNullOrEmpty(info.ParachuteClosingSequence)) + parachute.PlayThen(info.ParachuteClosingSequence, () => world.AddFrameEndTask(w => w.Remove(this))); + else + parachute.PlayBackwardsThen(info.ParachuteOpeningSequence, () => world.AddFrameEndTask(w => w.Remove(this))); + } + + if (!exploded && !info.PointDefenseTypes.IsEmpty) + { + var shouldExplode = world.ActorsWithTrait().Any(x => x.Trait.Destroy(pos, args.SourceActor.Owner, info.PointDefenseTypes)); + if (shouldExplode) + { + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(lastPos, pos), args.Facing), + ImpactPosition = pos, + }; + + args.Weapon.Impact(Target.FromPos(pos), warheadArgs); + exploded = true; + world.AddFrameEndTask(w => w.Remove(this)); + } + } + + anim?.Tick(); + } + + parachute?.Tick(); + } + + public IEnumerable Render(WorldRenderer wr) + { + if (anim == null && parachute == null) + yield break; + + var world = args.SourceActor.World; + if (!world.FogObscures(pos)) + { + if (!exploded) + { + if (info.Shadow) + { + var dat = world.Map.DistanceAboveTerrain(pos); + var shadowPos = pos - new WVec(0, 0, dat.Length); + foreach (var r in anim.Render(shadowPos, wr.Palette(info.ShadowPalette))) + yield return r; + } + + var palette = wr.Palette(info.Palette + (info.IsPlayerPalette ? args.SourceActor.Owner.InternalName : "")); + foreach (var r in anim.Render(pos, palette)) + yield return r; + } + + var chutepalette = wr.Palette(info.ParachutePalette + (info.ParachuteIsPlayerPalette ? args.SourceActor.Owner.InternalName : "")); + foreach (var r in parachute.Render(pos + info.ParachuteOffset, chutepalette)) + yield return r; + } + } + } +} diff --git a/OpenRA.Mods.AS/Projectiles/RadBeam.cs b/OpenRA.Mods.AS/Projectiles/RadBeam.cs new file mode 100644 index 000000000000..503d4fbbc168 --- /dev/null +++ b/OpenRA.Mods.AS/Projectiles/RadBeam.cs @@ -0,0 +1,135 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Graphics; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Projectiles +{ + [Desc("Not a sprite, but an engine effect.")] + public class RadBeamInfo : IProjectileInfo + { + [Desc("The thickness of the beam. (in WDist)")] + public readonly WDist Thickness = new(16); + + [Desc("The amplitude of the beam (in WDist).")] + public readonly WDist Amplitude = new(128); + + [Desc("The wavelength of the beam. (in WDist)")] + public readonly WDist WaveLength = new(512); + + [Desc("Draw each cycle with this many quantization steps")] + public readonly int QuantizationCount = 8; + + [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")] + public readonly int ZOffset = 0; + + [Desc("Duration of the beam.")] + public readonly int BeamDuration = 15; + + public readonly bool ScaleAmplitudeWithDuration = true; + + public readonly bool UsePlayerColor = false; + + [Desc("Beam color in (A),R,G,B.")] + public readonly Color Color = Color.FromArgb(128, 0, 255, 0); + + [Desc("Impact animation.")] + public readonly string HitAnim = null; + + [Desc("Sequence of impact animation to use.")] + [SequenceReference(nameof(HitAnim), allowNullImage: true)] + public readonly string HitAnimSequence = "idle"; + + [PaletteReference] + public readonly string HitAnimPalette = "effect"; + + public IProjectile Create(ProjectileArgs args) + { + var c = UsePlayerColor ? args.SourceActor.Owner.Color : Color; + return new RadBeam(args, this, c); + } + } + + public class RadBeam : IProjectile + { + readonly ProjectileArgs args; + readonly RadBeamInfo info; + readonly Animation hitanim; + readonly Color color; + int ticks = 0; + bool doneDamage; + bool animationComplete; + WPos target; + + public RadBeam(ProjectileArgs args, RadBeamInfo info, Color color) + { + this.args = args; + this.info = info; + target = args.PassiveTarget; + this.color = color; + + if (!string.IsNullOrEmpty(info.HitAnim)) + hitanim = new Animation(args.SourceActor.World, info.HitAnim); + } + + public void Tick(World world) + { + // Beam tracks target + if (args.GuidedTarget.IsValidFor(args.SourceActor)) + target = args.GuidedTarget.CenterPosition; + + if (!doneDamage) + { + if (hitanim != null) + hitanim.PlayThen(info.HitAnimSequence, () => animationComplete = true); + else + animationComplete = true; + + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(args.Source, target), args.CurrentMuzzleFacing()), + ImpactPosition = target, + }; + + args.Weapon.Impact(Target.FromPos(target), warheadArgs); + doneDamage = true; + } + + hitanim?.Tick(); + + if (++ticks >= info.BeamDuration && animationComplete) + world.AddFrameEndTask(w => w.Remove(this)); + } + + public IEnumerable Render(WorldRenderer wr) + { + if (wr.World.FogObscures(target) && + wr.World.FogObscures(args.Source)) + yield break; + + if (ticks < info.BeamDuration) + { + var amp = info.ScaleAmplitudeWithDuration + ? info.Amplitude * ticks / info.BeamDuration + : info.Amplitude; + yield return new RadBeamRenderable(args.Source, info.ZOffset, target - args.Source, info.Thickness, color, amp, info.WaveLength, info.QuantizationCount); + } + + if (hitanim != null) + foreach (var r in hitanim.Render(target, wr.Palette(info.HitAnimPalette))) + yield return r; + } + } +} diff --git a/OpenRA.Mods.AS/Projectiles/SmokeParticleRailgun.cs b/OpenRA.Mods.AS/Projectiles/SmokeParticleRailgun.cs new file mode 100644 index 000000000000..69fc6ca302d8 --- /dev/null +++ b/OpenRA.Mods.AS/Projectiles/SmokeParticleRailgun.cs @@ -0,0 +1,350 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Effects; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Projectiles +{ + [Desc("Laser effect with helix coiling made of sprite animations around.")] + public class SmokeParticleRailgunInfo : IProjectileInfo, ISmokeParticleInfo, IRulesetLoaded + { + [Desc("The width of the main trajectory used for damaging.", + "Leave it on 0 to disable line damage and deliver damage only at the target position.")] + public readonly WDist LineWidth = WDist.Zero; + + [Desc("Maximum offset at the maximum range.")] + public readonly WDist Inaccuracy = WDist.Zero; + + [Desc("Can this projectile be blocked when hitting actors with an IBlocksProjectiles trait.")] + public readonly bool Blockable = false; + + [Desc("Duration of the beam.")] + public readonly int Duration = 15; + + [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")] + public readonly int ZOffset = 0; + + [Desc("The width of an optional laser beam.")] + public readonly WDist BeamWidth = new(86); + + [Desc("The shape of the beam. Accepts values Cylindrical or Flat.")] + public readonly BeamRenderableShape BeamShape = BeamRenderableShape.Cylindrical; + + [Desc("Beam color in (A),R,G,B.")] + public readonly Color BeamColor = Color.FromArgb(128, 255, 255, 255); + + [Desc("When true, this will override BeamColor parameter and draw the laser with player color." + + " (Still uses BeamColor's alpha information)")] + public readonly bool BeamPlayerColor = false; + + [Desc("Beam alpha gets + this value per tick during drawing; hence negative value makes it fade over time.")] + public readonly int BeamAlphaDeltaPerTick = -8; + + [Desc("The radius of the spiral effect. (WDist)")] + public readonly WDist HelixRadius = new(64); + + [Desc("Height of one complete helix turn, measured parallel to the axis of the helix (WDist)")] + public readonly WDist HelixPitch = new(512); + + [Desc("Draw each cycle of helix with this many quantization steps")] + public readonly int QuantizationCount = 16; + + [Desc("Helix animation.")] + public readonly string HelixImage = "particles"; + + [Desc("Sequence of helix animation to use at the start.")] + [SequenceReference(nameof(HelixImage))] + public readonly string[] HelixStartSequences; + + [FieldLoader.Require] + [Desc("Sequence of helix animation to use.")] + [SequenceReference(nameof(HelixImage))] + public readonly string[] HelixSequences; + + [Desc("Sequence of helix animation to use at the end.")] + [SequenceReference(nameof(HelixImage))] + public readonly string[] HelixEndSequences; + + [PaletteReference(nameof(IsHelixPlayerPalette))] + public readonly string HelixPalette = "effect"; + + public readonly bool IsHelixPlayerPalette = false; + + [FieldLoader.Require] + [Desc("The duration of an individual particle. Two values mean actual lifetime will vary between them.")] + public readonly int[] HelixDuration; + + [Desc("Randomize particle forward movement.")] + public readonly WDist[] HelixSpeed = { WDist.Zero }; + + [Desc("Randomize particle gravity.")] + public readonly WDist[] HelixGravity = { WDist.Zero }; + + [Desc("Randomize particle turnrate.")] + public readonly int HelixTurnRate = 0; + + [Desc("Randomize particle facing.")] + public readonly bool HelixRandomFacing = true; + + [Desc("Rate to reset particle movement properties.")] + public readonly int HelixRandomRate = 4; + + [WeaponReference] + [Desc("Has to be defined in weapons.yaml, if defined, as well.")] + public readonly string HelixWeapon = null; + + [Desc("Impact animation.")] + public readonly string HitAnim = null; + + [Desc("Sequence of impact animation to use.")] + [SequenceReference(nameof(HitAnim), allowNullImage: true)] + public readonly string HitAnimSequence = "idle"; + + [PaletteReference] + public readonly string HitAnimPalette = "effect"; + + WeaponInfo helixWeapon; + + public IProjectile Create(ProjectileArgs args) + { + var bc = BeamPlayerColor ? Color.FromArgb(BeamColor.A, args.SourceActor.Owner.Color) : BeamColor; + return new SmokeParticleRailgun(args, this, bc); + } + + string ISmokeParticleInfo.Image + { + get { return HelixImage; } + } + + string[] ISmokeParticleInfo.StartSequences + { + get { return HelixStartSequences; } + } + + string[] ISmokeParticleInfo.Sequences + { + get { return HelixSequences; } + } + + string[] ISmokeParticleInfo.EndSequences + { + get { return HelixEndSequences; } + } + + string ISmokeParticleInfo.Palette + { + get { return HelixPalette; } + } + + bool ISmokeParticleInfo.IsPlayerPalette + { + get { return IsHelixPlayerPalette; } + } + + WDist[] ISmokeParticleInfo.Speed + { + get { return HelixSpeed; } + } + + int[] ISmokeParticleInfo.Duration + { + get { return HelixDuration; } + } + + WDist[] ISmokeParticleInfo.Gravity + { + get { return HelixGravity; } + } + + WeaponInfo ISmokeParticleInfo.Weapon + { + get { return helixWeapon; } + } + + int ISmokeParticleInfo.TurnRate + { + get { return HelixTurnRate; } + } + + int ISmokeParticleInfo.RandomRate + { + get { return HelixRandomRate; } + } + + void IRulesetLoaded.RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (string.IsNullOrEmpty(HelixWeapon)) + return; + + if (!rules.Weapons.TryGetValue(HelixWeapon.ToLowerInvariant(), out helixWeapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{HelixWeapon.ToLowerInvariant()}'"); + } + } + + public class SmokeParticleRailgun : IProjectile + { + readonly ProjectileArgs args; + readonly SmokeParticleRailgunInfo info; + readonly Animation hitanim; + public readonly Color BeamColor; + + int ticks = 0; + bool animationComplete; + WPos target; + + int cycleCount; + WVec forwardStep; + WVec leftVector; + WVec upVector; + WAngle angleStep; + + public SmokeParticleRailgun(ProjectileArgs args, SmokeParticleRailgunInfo info, Color beamColor) + { + this.args = args; + this.info = info; + target = args.PassiveTarget; + BeamColor = beamColor; + + if (!string.IsNullOrEmpty(info.HitAnim)) + hitanim = new Animation(args.SourceActor.World, info.HitAnim); + + CalculateVectors(); + + var pos = args.Source; + var angle = WAngle.Zero; + for (var i = cycleCount * info.QuantizationCount - 1; i >= 0; i--) + { + // Make it narrower near the end. + var rad = i < info.QuantizationCount ? info.HelixRadius / 4 : + i < 2 * info.QuantizationCount ? info.HelixRadius / 2 : + info.HelixRadius; + + // Note: WAngle.Sin(x) = 1024 * Math.Sin(2pi/1024 * x) + var offset = rad.Length * angle.Cos() * leftVector / (1024 * 1024) + + rad.Length * angle.Sin() * upVector / (1024 * 1024); + var animpos = pos + offset; + args.SourceActor.World.AddFrameEndTask(w => w.Add(new SmokeParticle(args.SourceActor, info, animpos, info.HelixRandomFacing ? -1 : angle.Facing))); + + pos += forwardStep; + angle += angleStep; + } + } + + void CalculateVectors() + { + // Check for blocking actors + if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(args.SourceActor.World, args.SourceActor.Owner, target, args.Source, + info.LineWidth, out var blockedPos)) + target = blockedPos; + + // Note: WAngle.Sin(x) = 1024 * Math.Sin(2pi/1024 * x) + angleStep = new WAngle(1024 / info.QuantizationCount); + + var sourceToTarget = target - args.Source; + + // Forward step, pointing from src to target. + // QuantizationCont * forwardStep == One cycle of beam in src2target direction. + forwardStep = info.HelixPitch.Length * sourceToTarget / (info.QuantizationCount * sourceToTarget.Length); + + if (forwardStep == WVec.Zero) + return; + + // An easy vector to find which is perpendicular vector to forwardStep, with 0 Z component + leftVector = new WVec(forwardStep.Y, -forwardStep.X, 0); + leftVector = 1024 * leftVector / leftVector.Length; + + // Vector that is pointing upwards from the ground + upVector = leftVector.Length != 0 + ? new WVec( + -forwardStep.X * forwardStep.Z, + -forwardStep.Z * forwardStep.Y, + forwardStep.X * forwardStep.X + forwardStep.Y * forwardStep.Y) + : new WVec(forwardStep.Z, forwardStep.Z, 0); + upVector = 1024 * upVector / upVector.Length; + + //// LeftVector and UpVector are unit vectors of size 1024. + + cycleCount = sourceToTarget.Length / info.HelixPitch.Length; + if (sourceToTarget.Length % info.HelixPitch.Length != 0) + cycleCount += 1; // math.ceil, int version. + } + + public void Tick(World world) + { + if (ticks == 0) + { + if (hitanim != null) + hitanim.PlayThen(info.HitAnimSequence, () => animationComplete = true); + else + animationComplete = true; + + if (info.LineWidth.Length > 0) + { + var actors = world.FindActorsOnLine(args.Source, target, info.LineWidth); + foreach (var a in actors) + { + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(args.Source, target), args.Facing), + + // Calculating an impact position is bogus for line damage. + // FindActorsOnLine guarantees that the beam touches the target's HitShape, + // so we just assume a center hit to avoid bogus warhead recalculations. + ImpactPosition = a.CenterPosition, + }; + + args.Weapon.Impact(Target.FromActor(a), warheadArgs); + } + } + else + { + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(args.Source, target), args.Facing), + ImpactPosition = target, + }; + + args.Weapon.Impact(Target.FromPos(target), warheadArgs); + } + } + + hitanim?.Tick(); + + if (ticks++ > info.Duration && animationComplete) + world.AddFrameEndTask(w => w.Remove(this)); + } + + public IEnumerable Render(WorldRenderer wr) + { + if (wr.World.FogObscures(target) && + wr.World.FogObscures(args.Source)) + yield break; + + if (info.BeamWidth.Length > 0 && ticks < info.Duration) + { + yield return new BeamRenderable(args.Source, info.ZOffset, args.PassiveTarget - args.Source, info.BeamShape, info.BeamWidth, + Color.FromArgb(BeamColor.A + info.BeamAlphaDeltaPerTick * ticks, BeamColor)); + } + + if (hitanim != null) + foreach (var r in hitanim.Render(target, wr.Palette(info.HitAnimPalette))) + yield return r; + } + } +} diff --git a/OpenRA.Mods.AS/Projectiles/SpriteRailgun.cs b/OpenRA.Mods.AS/Projectiles/SpriteRailgun.cs new file mode 100644 index 000000000000..539a96389f83 --- /dev/null +++ b/OpenRA.Mods.AS/Projectiles/SpriteRailgun.cs @@ -0,0 +1,247 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Projectiles +{ + [Desc("Laser effect with helix coiling made of sprite animations around.")] + public class SpriteRailgunInfo : IProjectileInfo + { + [Desc("The width of the main trajectory used for damaging.", + "Leave it on 0 to disable line damage and deliver damage only at the target position.")] + public readonly WDist LineWidth = WDist.Zero; + + [Desc("Maximum offset at the maximum range.")] + public readonly WDist Inaccuracy = WDist.Zero; + + [Desc("Can this projectile be blocked when hitting actors with an IBlocksProjectiles trait.")] + public readonly bool Blockable = false; + + [Desc("Duration of the beam.")] + public readonly int Duration = 15; + + [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")] + public readonly int ZOffset = 0; + + [Desc("The width of an optional laser beam.")] + public readonly WDist BeamWidth = new(86); + + [Desc("The shape of the beam. Accepts values Cylindrical or Flat.")] + public readonly BeamRenderableShape BeamShape = BeamRenderableShape.Cylindrical; + + [Desc("Beam color in (A),R,G,B.")] + public readonly Color BeamColor = Color.FromArgb(128, 255, 255, 255); + + [Desc("When true, this will override BeamColor parameter and draw the laser with player color." + + " (Still uses BeamColor's alpha information)")] + public readonly bool BeamPlayerColor = false; + + [Desc("Beam alpha gets + this value per tick during drawing; hence negative value makes it fade over time.")] + public readonly int BeamAlphaDeltaPerTick = -8; + + [Desc("The radius of the spiral effect. (WDist)")] + public readonly WDist HelixRadius = new(64); + + [Desc("Height of one complete helix turn, measured parallel to the axis of the helix (WDist)")] + public readonly WDist HelixPitch = new(512); + + [Desc("Draw each cycle of helix with this many quantization steps")] + public readonly int QuantizationCount = 16; + + [Desc("Helix animation.")] + public readonly string HelixAnim = null; + + [Desc("Sequence of helix animation to use.")] + [SequenceReference(nameof(HelixAnim), allowNullImage: true)] + public readonly string HelixAnimSequence = "idle"; + + [PaletteReference] + public readonly string HelixAnimPalette = "effect"; + + [Desc("Impact animation.")] + public readonly string HitAnim = null; + + [Desc("Sequence of impact animation to use.")] + [SequenceReference(nameof(HitAnim), allowNullImage: true)] + public readonly string HitAnimSequence = "idle"; + + [PaletteReference] + public readonly string HitAnimPalette = "effect"; + + public IProjectile Create(ProjectileArgs args) + { + var bc = BeamPlayerColor ? Color.FromArgb(BeamColor.A, args.SourceActor.Owner.Color) : BeamColor; + return new SpriteRailgun(args, this, bc); + } + } + + public class SpriteRailgun : IProjectile + { + readonly ProjectileArgs args; + readonly SpriteRailgunInfo info; + readonly Animation hitanim; + public readonly Color BeamColor; + + int ticks = 0; + bool animationComplete; + WPos target; + + int cycleCount; + WVec forwardStep; + WVec leftVector; + WVec upVector; + WAngle angleStep; + + public SpriteRailgun(ProjectileArgs args, SpriteRailgunInfo info, Color beamColor) + { + this.args = args; + this.info = info; + target = args.PassiveTarget; + BeamColor = beamColor; + + if (!string.IsNullOrEmpty(info.HitAnim)) + hitanim = new Animation(args.SourceActor.World, info.HitAnim); + + CalculateVectors(); + + var pos = args.Source; + var angle = WAngle.Zero; + for (var i = cycleCount * info.QuantizationCount - 1; i >= 0; i--) + { + // Make it narrower near the end. + var rad = i < info.QuantizationCount ? info.HelixRadius / 4 : + i < 2 * info.QuantizationCount ? info.HelixRadius / 2 : + info.HelixRadius; + + // Note: WAngle.Sin(x) = 1024 * Math.Sin(2pi/1024 * x) + var offset = rad.Length * angle.Cos() * leftVector / (1024 * 1024) + + rad.Length * angle.Sin() * upVector / (1024 * 1024); + var animpos = pos + offset; + args.SourceActor.World.AddFrameEndTask(w => w.Add(new SpriteEffect(animpos, angle, w, + info.HelixAnim, info.HelixAnimSequence, info.HelixAnimPalette))); + + pos += forwardStep; + angle += angleStep; + } + } + + void CalculateVectors() + { + // Check for blocking actors + if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(args.SourceActor.World, args.SourceActor.Owner, target, args.Source, + info.LineWidth, out var blockedPos)) + target = blockedPos; + + // Note: WAngle.Sin(x) = 1024 * Math.Sin(2pi/1024 * x) + angleStep = new WAngle(1024 / info.QuantizationCount); + + var sourceToTarget = target - args.Source; + + // Forward step, pointing from src to target. + // QuantizationCont * forwardStep == One cycle of beam in src2target direction. + forwardStep = info.HelixPitch.Length * sourceToTarget / (info.QuantizationCount * sourceToTarget.Length); + + if (forwardStep == WVec.Zero) + return; + + // An easy vector to find which is perpendicular vector to forwardStep, with 0 Z component + leftVector = new WVec(forwardStep.Y, -forwardStep.X, 0); + if (leftVector.Length != 0) + leftVector = 1024 * leftVector / leftVector.Length; + + // Vector that is pointing upwards from the ground + upVector = leftVector.Length != 0 + ? new WVec( + -forwardStep.X * forwardStep.Z, + -forwardStep.Z * forwardStep.Y, + forwardStep.X * forwardStep.X + forwardStep.Y * forwardStep.Y) + : new WVec(forwardStep.Z, forwardStep.Z, 0); + if (upVector.Length != 0) + upVector = 1024 * upVector / upVector.Length; + + //// LeftVector and UpVector are unit vectors of size 1024. + + cycleCount = sourceToTarget.Length / info.HelixPitch.Length; + if (sourceToTarget.Length % info.HelixPitch.Length != 0) + cycleCount += 1; // math.ceil, int version. + } + + public void Tick(World world) + { + if (ticks == 0) + { + if (hitanim != null) + hitanim.PlayThen(info.HitAnimSequence, () => animationComplete = true); + else + animationComplete = true; + + if (info.LineWidth.Length > 0) + { + var actors = world.FindActorsOnLine(args.Source, target, info.LineWidth); + foreach (var a in actors) + { + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(args.Source, target), args.Facing), + + // Calculating an impact position is bogus for line damage. + // FindActorsOnLine guarantees that the beam touches the target's HitShape, + // so we just assume a center hit to avoid bogus warhead recalculations. + ImpactPosition = a.CenterPosition, + }; + + args.Weapon.Impact(Target.FromActor(a), warheadArgs); + } + } + else + { + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Common.Util.GetVerticalAngle(args.Source, target), args.Facing), + ImpactPosition = target, + }; + + args.Weapon.Impact(Target.FromPos(target), warheadArgs); + } + } + + hitanim?.Tick(); + + if (ticks++ > info.Duration && animationComplete) + world.AddFrameEndTask(w => w.Remove(this)); + } + + public IEnumerable Render(WorldRenderer wr) + { + if (wr.World.FogObscures(target) && + wr.World.FogObscures(args.Source)) + yield break; + + if (info.BeamWidth.Length > 0 && ticks < info.Duration) + { + yield return new BeamRenderable(args.Source, info.ZOffset, args.PassiveTarget - args.Source, info.BeamShape, info.BeamWidth, + Color.FromArgb(BeamColor.A + info.BeamAlphaDeltaPerTick * ticks, BeamColor)); + } + + if (hitanim != null) + foreach (var r in hitanim.Render(target, wr.Palette(info.HitAnimPalette))) + yield return r; + } + } +} diff --git a/OpenRA.Mods.AS/Projectiles/WarheadTrailProjectile.cs b/OpenRA.Mods.AS/Projectiles/WarheadTrailProjectile.cs new file mode 100644 index 000000000000..4d335a0156f3 --- /dev/null +++ b/OpenRA.Mods.AS/Projectiles/WarheadTrailProjectile.cs @@ -0,0 +1,302 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Effects; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Projectiles +{ + public enum FireMode + { + Spread, + Line, + Focus + } + + [Desc("Detonates all warheads attached to Weapon each ExplosionInterval ticks.")] + public class WarheadTrailProjectileInfo : IProjectileInfo, IRulesetLoaded + { + [Desc("Warhead explosion offsets")] + public readonly WVec[] Offsets = { new WVec(0, 1, 0) }; + + [Desc("Projectile speed in WDist / tick, two values indicate variable velocity.")] + public readonly WDist[] Speed = { new WDist(17) }; + + [Desc("Maximum inaccuracy offset.")] + public readonly WDist Inaccuracy = WDist.Zero; + + [Desc("How many ticks will pass between explosions.")] + public readonly int ExplosionInterval = 8; + + [FieldLoader.Require] + [WeaponReference] + [Desc("Weapon that's detonated every interval.")] + public readonly string Weapon = null; + + public WeaponInfo WeaponInfo { get; private set; } + + [Desc("If it's true then weapon won't continue firing past the target.")] + public readonly bool KillProjectilesWhenReachedTargetLocation = false; + + [Desc("Where shall the bullets fly after instantiating? Possible values are Spread, Line and Focus")] + public readonly FireMode FireMode = FireMode.Spread; + + [Desc("Impact the warheads at ground level, regardless of explosion altitude.")] + public readonly bool ForceAtGroundLevel = false; + + [Desc("Interval in ticks between each spawned Trail animation.")] + public readonly int TrailInterval = 2; + + [Desc("Image to display.")] + public readonly string Image = null; + + [Desc("Loop a randomly chosen sequence of Image from this list while this projectile is moving.")] + [SequenceReference(nameof(Image), allowNullImage: true)] + public readonly string[] Sequences = { "idle" }; + + [Desc("The palette used to draw this projectile.")] + [PaletteReference] + public readonly string Palette = "effect"; + + [Desc("Does this projectile have a shadow?")] + public readonly bool Shadow = false; + + [Desc("Palette to use for this projectile's shadow if Shadow is true.")] + [PaletteReference] + public readonly string ShadowPalette = "shadow"; + + [Desc("Trail animation.")] + public readonly string TrailImage = null; + + [Desc("Loop a randomly chosen sequence of TrailImage from this list while this projectile is moving.")] + [SequenceReference(nameof(TrailImage), allowNullImage: true)] + public readonly string[] TrailSequences = { "idle" }; + + [Desc("Delay in ticks until trail animation is spawned.")] + public readonly int TrailDelay = 1; + + [Desc("Palette used to render the trail sequence.")] + [PaletteReference(nameof(TrailUsePlayerPalette))] + public readonly string TrailPalette = "effect"; + + [Desc("Use the Player Palette to render the trail sequence.")] + public readonly bool TrailUsePlayerPalette = false; + + [Desc("When set, display a line behind the actor. Length is measured in ticks after appearing.")] + public readonly int ContrailLength = 0; + + [Desc("Time (in ticks) after which the line should appear. Controls the distance to the actor.")] + public readonly int ContrailDelay = 1; + + [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")] + public readonly int ContrailZOffset = 2047; + + [Desc("Thickness of the emitted line at the start of the contrail.")] + public readonly WDist ContrailStartWidth = new(64); + + [Desc("Thickness of the emitted line at the end of the contrail. Will default to " + nameof(ContrailStartWidth) + " if left undefined")] + public readonly WDist? ContrailEndWidth = null; + + [Desc("RGB color at the contrail start.")] + public readonly Color ContrailStartColor = Color.White; + + [Desc("Use player remap color instead of a custom color at the contrail the start.")] + public readonly bool ContrailStartColorUsePlayerColor = false; + + [Desc("The alpha value [from 0 to 255] of color at the contrail the start.")] + public readonly int ContrailStartColorAlpha = 255; + + [Desc("RGB color at the contrail end. Will default to " + nameof(ContrailStartColor) + " if left undefined")] + public readonly Color? ContrailEndColor; + + [Desc("Use player remap color instead of a custom color at the contrail end.")] + public readonly bool ContrailEndColorUsePlayerColor = false; + + [Desc("The alpha value [from 0 to 255] of color at the contrail end.")] + public readonly int ContrailEndColorAlpha = 0; + + [Desc("Altitude where this bullet should explode when reached.", + "Negative values allow this bullet to pass cliffs and terrain bumps.")] + public readonly WDist ExplodeUnderThisAltitude = new(-1536); + + [Desc("Is this blocked by actors with BlocksProjectiles trait.")] + public readonly bool Blockable = true; + + [Desc("Width of projectile (used for finding blocking actors).")] + public readonly WDist Width = new(1); + + [Desc("If projectile touches an actor with one of these stances during or after the first bounce, trigger explosion.")] + public readonly PlayerRelationship ValidBounceBlockerPlayerRelationships = PlayerRelationship.Enemy | PlayerRelationship.Neutral | PlayerRelationship.Ally; + + public IProjectile Create(ProjectileArgs args) { return new WarheadTrailProjectile(this, args); } + + void IRulesetLoaded.RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out var weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'"); + WeaponInfo = weapon; + } + } + + public class WarheadTrailProjectile : IProjectile, ISync + { + readonly WarheadTrailProjectileInfo info; + readonly ProjectileArgs args; + + [Sync] + readonly WDist speed; + + readonly WRot offsetRotation; + readonly int lifespan; + readonly int mindelay; + + [Sync] + readonly WPos projectilepos, targetpos, sourcepos, offsetTargetPos = WPos.Zero; + + readonly WarheadTrailProjectileEffect[] projectiles; // offset projectiles + + readonly WPos offsetSourcePos = WPos.Zero; + readonly World world; + + int ticks; + + public Actor SourceActor { get { return args.SourceActor; } } + + public WarheadTrailProjectile(WarheadTrailProjectileInfo info, ProjectileArgs args) + { + this.info = info; + this.args = args; + + projectilepos = args.Source; + sourcepos = args.Source; + + var firedBy = args.SourceActor; + + world = args.SourceActor.World; + + if (info.Speed.Length > 1) + speed = new WDist(world.SharedRandom.Next(info.Speed[0].Length, info.Speed[1].Length)); + else + speed = info.Speed[0]; + + targetpos = GetTargetPos(); + + mindelay = args.Weapon.MinRange.Length / speed.Length; + + projectiles = new WarheadTrailProjectileEffect[info.Offsets.Length]; + var range = Common.Util.ApplyPercentageModifiers(args.Weapon.Range.Length, args.RangeModifiers); + var mainFacing = (targetpos - sourcepos).Yaw.Facing + 64; + + // used for lerping projectiles at the same pace + var estimatedLifespan = Math.Max(args.Weapon.Range.Length / speed.Length, 1); + + // target that will be assigned + Target target; + + for (var i = 0; i < info.Offsets.Length; i++) + { + switch (info.FireMode) + { + case FireMode.Focus: + offsetRotation = WRot.FromFacing(mainFacing); + offsetTargetPos = sourcepos + new WVec(range, 0, 0).Rotate(offsetRotation); + offsetSourcePos = sourcepos + info.Offsets[i].Rotate(offsetRotation); + break; + case FireMode.Line: + offsetRotation = WRot.FromFacing(mainFacing); + offsetTargetPos = sourcepos + new WVec(range + info.Offsets[i].X, info.Offsets[i].Y, info.Offsets[i].Z).Rotate(offsetRotation); + offsetSourcePos = sourcepos + info.Offsets[i].Rotate(offsetRotation); + break; + case FireMode.Spread: + offsetRotation = WRot.FromFacing(info.Offsets[i].Yaw.Facing - 64) + WRot.FromFacing(mainFacing); + offsetSourcePos = sourcepos + info.Offsets[i].Rotate(offsetRotation); + offsetTargetPos = sourcepos + new WVec(range + info.Offsets[i].X, info.Offsets[i].Y, info.Offsets[i].Z).Rotate(offsetRotation); + break; + } + + if (info.Inaccuracy.Length > 0) + { + var inaccuracy = Common.Util.ApplyPercentageModifiers(info.Inaccuracy.Length, args.InaccuracyModifiers); + var maxOffset = inaccuracy * (args.PassiveTarget - projectilepos).Length / range; + var inaccuracyOffset = WVec.FromPDF(world.SharedRandom, 2) * maxOffset / 1024; + offsetTargetPos += inaccuracyOffset; + } + + target = Target.FromPos(offsetTargetPos); + + // If it's true then lifespan is counted from source position to target instead of max range. + lifespan = info.KillProjectilesWhenReachedTargetLocation + ? Math.Max((args.PassiveTarget - args.Source).Length / speed.Length, 1) + : estimatedLifespan; + + var facing = (offsetTargetPos - offsetSourcePos).Yaw; + var projectileArgs = new ProjectileArgs + { + Weapon = info.WeaponInfo, + DamageModifiers = args.DamageModifiers, + Facing = facing, + Source = offsetSourcePos, + CurrentSource = () => offsetSourcePos, + SourceActor = firedBy, + GuidedTarget = args.GuidedTarget, + PassiveTarget = target.CenterPosition + }; + + projectiles[i] = new WarheadTrailProjectileEffect(info, projectileArgs, lifespan, estimatedLifespan, info.ForceAtGroundLevel); + } + + foreach (var p in projectiles) + world.AddFrameEndTask(w => w.Add(p)); + } + + // gets where main projectile should fly to + WPos GetTargetPos() + { + var targetpos = args.PassiveTarget; + var div = (targetpos - sourcepos).Length; + + return WPos.Lerp(sourcepos, targetpos, args.Weapon.Range.Length, div > 0 ? div : 1); + } + + public void Tick(World world) + { + if (ticks % info.ExplosionInterval == 0 && mindelay <= ticks) + DoImpact(); + + if (ticks >= lifespan) + { + foreach (var projectile in projectiles) + projectile.Explode(world); + + world.AddFrameEndTask(w => w.Remove(this)); + } + + ticks++; + } + + void DoImpact() + { + // Trigger all so-far-untriggered explosions. + foreach (var projectile in projectiles) + if (!projectile.DetonateSelf) + projectile.Impact(); + } + + public IEnumerable Render(WorldRenderer wr) + { + yield break; + } + } +} diff --git a/OpenRA.Mods.AS/README.md b/OpenRA.Mods.AS/README.md new file mode 100644 index 000000000000..21ac9ef810cd --- /dev/null +++ b/OpenRA.Mods.AS/README.md @@ -0,0 +1,9 @@ +# This repository is a collection of mod-specific logics for the OpenRA mod Attacque Supérior. + +While the logics might be written with one mod in mind, the repository's aim is to be used by the modding community as an additional plugin for their mods. + +The repository will be aimed against the bleed version of OpenRA. Playtest-compatible versions might be tagged though. Release-specific versions probably not - although probably the last playtest of that release cycle will work with them. + +For specific mod logic authors, refer the AUTHORS file. The repository's code is under the GPLv3 license. All code uploaded into this repository follows the OpenRA code standard and was evaulated by StyleCop. + +Do not ask about the mod here. This is a strict code repository. You can follow Attacque Supérior at http://www.moddb.com/mods/attacque-suprior or https://www.facebook.com/AttacqueSuperior. diff --git a/OpenRA.Mods.AS/Scripting/Global/ActorTagGlobal.cs b/OpenRA.Mods.AS/Scripting/Global/ActorTagGlobal.cs new file mode 100644 index 000000000000..10f942cd00a7 --- /dev/null +++ b/OpenRA.Mods.AS/Scripting/Global/ActorTagGlobal.cs @@ -0,0 +1,30 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.AS.Traits; +using OpenRA.Scripting; + +namespace OpenRA.Mods.AS.Scripting +{ + [ScriptGlobal("ActorTag")] + public class ActorTagGlobal : ScriptGlobal + { + public ActorTagGlobal(ScriptContext context) + : base(context) { } + + [Desc("Returns all actor types which has the specified actorTag string in their actorTag trait.")] + public string[] ReturnActorTypes(string actorTag) + { + return Context.World.Map.Rules.Actors.Values.Where(a => a.HasTraitInfo() + && a.TraitInfos().Any(c => c.Type.Contains(actorTag))).Select(a => a.Name).ToArray(); + } + } +} diff --git a/OpenRA.Mods.AS/Scripting/Global/TauntsGlobal.cs b/OpenRA.Mods.AS/Scripting/Global/TauntsGlobal.cs new file mode 100644 index 000000000000..16e08564f082 --- /dev/null +++ b/OpenRA.Mods.AS/Scripting/Global/TauntsGlobal.cs @@ -0,0 +1,33 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Scripting; + +namespace OpenRA.Mods.AS.Scripting +{ + [ScriptGlobal("Taunts")] + public class TauntsGlobal : ScriptGlobal + { + readonly World world; + + public TauntsGlobal(ScriptContext context) + : base(context) + { + world = context.World; + } + + [Desc("Play a taunt listed in taunts.yaml")] + public void PlayTauntNotification(Player player, string notification) + { + Game.Sound.PlayNotification(world.Map.Rules, world.LocalPlayer, "Taunts", notification, player?.Faction.InternalName); + } + } +} diff --git a/OpenRA.Mods.AS/Scripting/Properties/GrantConditionOnDeployProperties.cs b/OpenRA.Mods.AS/Scripting/Properties/GrantConditionOnDeployProperties.cs new file mode 100644 index 000000000000..d5b430b75458 --- /dev/null +++ b/OpenRA.Mods.AS/Scripting/Properties/GrantConditionOnDeployProperties.cs @@ -0,0 +1,47 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Scripting; + +namespace OpenRA.Mods.AS.Scripting +{ + [ScriptPropertyGroup("General")] + public class GrantConditionOnDeployProperties : ScriptActorProperties + { + readonly GrantConditionOnDeploy[] gcods; + + public GrantConditionOnDeployProperties(ScriptContext context, Actor self) + : base(context, self) + { + gcods = self.TraitsImplementing().ToArray(); + } + + [ScriptActorPropertyActivity] + [Desc("Deploy the actor.")] + public void SwitchToDeploy() + { + foreach (var gcod in gcods) + if (!gcod.IsTraitDisabled && !gcod.IsTraitPaused) + gcod.Deploy(); + } + + [ScriptActorPropertyActivity] + [Desc("Undeploy the actor.")] + public void SwitchToUndeploy() + { + foreach (var gcod in gcods) + if (!gcod.IsTraitDisabled && !gcod.IsTraitPaused) + gcod.Undeploy(); + } + } +} diff --git a/OpenRA.Mods.AS/Scripting/Properties/MobileASProperties.cs b/OpenRA.Mods.AS/Scripting/Properties/MobileASProperties.cs new file mode 100644 index 000000000000..b87286c6a27e --- /dev/null +++ b/OpenRA.Mods.AS/Scripting/Properties/MobileASProperties.cs @@ -0,0 +1,39 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Scripting; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Scripting +{ + [ScriptPropertyGroup("Movement")] + public class MobileASProperties : ScriptActorProperties, Requires + { + public MobileASProperties(ScriptContext context, Actor self) + : base(context, self) { } + + [ScriptActorPropertyActivity] + [Desc("Move to and enter the shared transport.")] + public void EnterSharedTransport(Actor transport) + { + Self.QueueActivity(new RideSharedTransport(Self, Target.FromActor(transport), null)); + } + + [ScriptActorPropertyActivity] + [Desc("Move to and enter the transport.")] + public void EnterGarrisonable(Actor transport) + { + Self.QueueActivity(new EnterGarrison(Self, Target.FromActor(transport), null)); + } + } +} diff --git a/OpenRA.Mods.AS/Scripting/Properties/PositionProperties.cs b/OpenRA.Mods.AS/Scripting/Properties/PositionProperties.cs new file mode 100644 index 000000000000..e68b0ca96301 --- /dev/null +++ b/OpenRA.Mods.AS/Scripting/Properties/PositionProperties.cs @@ -0,0 +1,28 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common; +using OpenRA.Scripting; + +namespace OpenRA.Mods.AS.Scripting +{ + [ScriptPropertyGroup("General")] + public class PositionProperties : ScriptActorProperties + { + public PositionProperties(ScriptContext context, Actor self) + : base(context, self) { } + + [Desc("Is the actor at ground.")] + public bool IsAtGroundLevel() + { + return Self.IsAtGroundLevel(); + } + } +} diff --git a/OpenRA.Mods.AS/Scripting/Properties/TransportASProperties.cs b/OpenRA.Mods.AS/Scripting/Properties/TransportASProperties.cs new file mode 100644 index 000000000000..98a649a275a5 --- /dev/null +++ b/OpenRA.Mods.AS/Scripting/Properties/TransportASProperties.cs @@ -0,0 +1,112 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Linq; +using Eluant; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Scripting; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Scripting +{ + [ScriptPropertyGroup("Transports")] + public class SharedTransportProperties : ScriptActorProperties, Requires + { + readonly SharedCargo sharedCargo; + + public SharedTransportProperties(ScriptContext context, Actor self) + : base(context, self) + { + sharedCargo = self.Trait(); + } + + [Desc("Returns references to passengers inside the shared transport.")] + public Actor[] SharedPassengers => sharedCargo.Manager.Passengers.ToArray(); + + [Desc("Specifies whether shared transport has any passengers.")] + public bool HasSharedPassengers => sharedCargo.Manager.Passengers.Any(); + + [Desc("Specifies the amount of passengers.")] + public int SharedPassengerCount => sharedCargo.Manager.Passengers.Count(); + + [Desc("Teleport an existing actor inside this shared transport.")] + public void LoadSharedPassenger(Actor a) + { + if (!a.IsIdle) + throw new LuaException("LoadSharedPassenger requires the shared passenger to be idle."); + + sharedCargo.Load(Self, a); + } + + [Desc("Remove an existing actor (or first actor if none specified) from the shared transport. This actor is not added to the world.")] + public Actor UnloadSharedPassenger(Actor a = null) { return sharedCargo.Unload(Self, a); } + + [ScriptActorPropertyActivity] + [Desc("Command shared transport to unload passengers.")] + public void UnloadSharedPassengers(CPos? cell = null, int unloadRange = 5) + { + if (cell.HasValue) + { + var destination = Target.FromCell(Self.World, cell.Value); + Self.QueueActivity(new UnloadSharedCargo(Self, destination, WDist.FromCells(unloadRange))); + } + else + Self.QueueActivity(new UnloadSharedCargo(Self, WDist.FromCells(unloadRange))); + } + } + + [ScriptPropertyGroup("Transports")] + public class GarrisonableProperties : ScriptActorProperties, Requires + { + readonly Garrisonable garrisonable; + + public GarrisonableProperties(ScriptContext context, Actor self) + : base(context, self) + { + garrisonable = self.Trait(); + } + + [Desc("Returns references to garrisoners inside the transport.")] + public Actor[] Garrisoners => garrisonable.Garrisoners.ToArray(); + + [Desc("Specifies whether transport has any garrisoners.")] + public bool HasGarrisoners => garrisonable.Garrisoners.Any(); + + [Desc("Specifies the amount of garrisoners.")] + public int GarrisonerCount => garrisonable.Garrisoners.Count(); + + [Desc("Teleport an existing actor inside this transport.")] + public void LoadGarrisoner(Actor a) + { + if (!a.IsIdle) + throw new LuaException("LoadGarrisoner requires the garrisoner to be idle."); + + garrisonable.Load(Self, a); + } + + [Desc("Remove an existing actor (or first actor if none specified) from the transport. This actor is not added to the world.")] + public Actor UnloadGarrisoner(Actor a = null) { return garrisonable.Unload(Self, a); } + + [ScriptActorPropertyActivity] + [Desc("Command transport to unload garrisoners.")] + public void UnloadGarrisoners(CPos? cell = null, int unloadRange = 5) + { + if (cell.HasValue) + { + var destination = Target.FromCell(Self.World, cell.Value); + Self.QueueActivity(new UnloadGarrison(Self, destination, WDist.FromCells(unloadRange))); + } + else + Self.QueueActivity(new UnloadGarrison(Self, WDist.FromCells(unloadRange))); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/ActorStatOverride.cs b/OpenRA.Mods.AS/Traits/ActorStatOverride.cs new file mode 100644 index 000000000000..cd8890ab1e64 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/ActorStatOverride.cs @@ -0,0 +1,87 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class ActorStatOverrideInfo : ConditionalTraitInfo, Requires + { + [Desc("Overrides the icon for the unit for the stats.")] + public readonly string Icon; + + [ActorReference] + [Desc("Actor to use for Tooltip when hovering of the icon.")] + public readonly string TooltipActor; + + [Desc("Types of stats to show.")] + public readonly ActorStatContent[] Stats; + + [Desc("Overrides the health value for the unit for the stats.")] + public readonly int? Health; + + [Desc("Overrides the damage value for the unit for the stats.")] + public readonly int? Damage; + + [ActorReference] + [Desc("Upgrades this actor is affected by.")] + public readonly string[] Upgrades; + + [Desc("Relationships that the override will apply for.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Ally | PlayerRelationship.Neutral | PlayerRelationship.Enemy; + + public override object Create(ActorInitializer init) { return new ActorStatOverride(init, this); } + } + + public class ActorStatOverride : ConditionalTrait, ITick + { + readonly ActorStatValues asv; + Player cachedRenderPlayer; + + public ActorStatOverride(ActorInitializer init, ActorStatOverrideInfo info) + : base(info) + { + asv = init.Self.Trait(); + + cachedRenderPlayer = init.World.RenderPlayer; + } + + void UpdateData() + { + if (Info.Icon != null) + asv.CalculateIcon(); + if (Info.TooltipActor != null) + asv.CalculateTooltipActor(); + if (Info.Stats != null) + asv.CalculateStats(); + if (Info.Health != null) + asv.CalculateHealthStat(); + if (Info.Damage != null) + asv.CalculateDamageStat(); + if (Info.Upgrades != null) + asv.CalculateUpgrades(); + } + + void ITick.Tick(Actor self) + { + if (cachedRenderPlayer != self.World.RenderPlayer) + { + cachedRenderPlayer = self.World.RenderPlayer; + UpdateData(); + } + } + + protected override void TraitEnabled(Actor self) { UpdateData(); } + + protected override void TraitDisabled(Actor self) { UpdateData(); } + } +} diff --git a/OpenRA.Mods.AS/Traits/ActorStatValues.cs b/OpenRA.Mods.AS/Traits/ActorStatValues.cs new file mode 100644 index 000000000000..c478f0cbf614 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/ActorStatValues.cs @@ -0,0 +1,802 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using OpenRA.Mods.Cnc.Traits; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Mods.Common.Widgets; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public enum ActorStatContent { None, Armor, Sight, Speed, Power, Damage, MindControl, Spread, ReloadDelay, MinRange, MaxRange, Harvester, Collector, CashTrickler, PeriodicProducer, Cargo, Carrier, Mob, Drones } + + public class ActorStatValuesInfo : TraitInfo + { + [Desc("Overrides the icon for the unit for the stats.")] + public readonly string Icon; + + // Doesn't work properly with `bool?`. + // [PaletteReference(nameof(IconPaletteIsPlayerPalette))] + [Desc("Overrides the icon palette for the unit for the stats.")] + public readonly string IconPalette; + + [Desc("Overrides if icon palette for the unit for the stats is a player palette.")] + public readonly bool? IconPaletteIsPlayerPalette; + + [Desc("Types of stats to show.")] + public readonly ActorStatContent[] Stats = { ActorStatContent.Armor, ActorStatContent.Sight }; + + [Desc("Armament names to use for weapon stats.")] + public readonly string[] Armaments; + + [Desc("Use this value for base damage of the unit for the stats.")] + public readonly int? Damage; + + [Desc("Use this value for weapon spread of the unit for the stats.")] + public readonly WDist? Spread; + + [Desc("Overrides the reload delay value from the weapons for the stats.")] + public readonly int? ReloadDelay; + + [Desc("Overrides the sight value from RevealsShroud trait for the stats.")] + public readonly WDist? Sight; + + [Desc("Overrides the range value from the weapons for the stats, enter 2 values for short and long range.")] + public readonly WDist[] Range = Array.Empty(); + + [Desc("Overrides the minimum range value from the weapons for the stats.")] + public readonly WDist? MinimumRange; + + [Desc("Overrides the movement speed value from Mobile or Aircraft traits for the stats.")] + public readonly int? Speed; + + [Desc("Don't show these armor classes for the Armor stat.")] + public readonly string[] ArmorsToIgnore = Array.Empty(); + + [Desc("Show shield level in place of Armor when actor has active Shielded trait.")] + public readonly bool ShowShield = true; + + [ActorReference] + [Desc("Actor to use for Tooltip when hovering of the icon.")] + public readonly string TooltipActor; + + [Desc("Prerequisites to enable upgrades, without them upgrades won't be shown." + + "Only checked at the actor creation.")] + public readonly string[] UpgradePrerequisites = Array.Empty(); + + [ActorReference] + [Desc("Upgrades this actor is affected by.")] + public readonly string[] Upgrades = Array.Empty(); + + [ActorReference] + [Desc("Which of the actors defined under Upgrades are produced by the actor itself, and only effects it.")] + public readonly string[] LocalUpgrades = Array.Empty(); + + public override object Create(ActorInitializer init) { return new ActorStatValues(init, this); } + } + + public class ActorStatValues : INotifyCreated, INotifyDisguised, INotifyOwnerChanged, INotifyProduction + { + readonly Actor self; + public ActorStatValuesInfo Info; + + public BuildableInfo BuildableInfo; + public string Icon; + public string IconPalette; + public bool IconPaletteIsPlayerPalette; + public WithStatIconOverlay[] IconOverlays; + + public ActorStatContent[] CurrentStats; + public ActorStatOverride[] TooltipActorOverrides; + public ActorStatOverride[] IconOverrides; + public ActorStatOverride[] StatClassOverrides; + public ActorStatOverride[] HealthStatOverrides; + public ActorStatOverride[] DamageStatOverrides; + public ActorStatOverride[] UpgradeOverrides; + + public int CurrentMaxHealth; + public int? CurrentDamage; + public int Speed; + + public ITooltip[] Tooltips; + public Armor[] Armors; + public RevealsShroud[] RevealsShrouds; + public AttackBase[] AttackBases; + public Armament[] Armaments; + public Power[] Powers; + + public IHealth Health; + public Shielded Shielded; + + public Mobile Mobile; + public Aircraft Aircraft; + + public MindController MindController; + + public IStoresResources ResourceHold; + public ISupplyCollector Collector; + public CashTrickler[] CashTricklers = Array.Empty(); + public PeriodicProducer[] PeriodicProducers = Array.Empty(); + public Cargo Cargo; + public SharedCargo SharedCargo; + public Garrisonable Garrisonable; + public CarrierMaster CarrierMaster; + public MobSpawnerMaster[] MobSpawnerMasters; + public DroneSpawnerMaster[] DroneSpawnerMasters; + + public IRevealsShroudModifier[] SightModifiers; + public IFirepowerModifier[] FirepowerModifiers; + public IReloadModifier[] ReloadModifiers; + public IRangeModifier[] RangeModifiers; + public ISpeedModifier[] SpeedModifiers; + public IPowerModifier[] PowerModifiers; + public IResourceValueModifier[] ResourceValueModifiers; + + public ActorInfo TooltipActor; + + PlayerResources playerResources; + TechTree techTree; + + public bool UpgradesEnabled; + public string[] CurrentUpgrades = Array.Empty(); + public Dictionary Upgrades = new(); + + public bool Disguised; + public Player DisguisePlayer; + public string DisguiseImage; + public int DisguiseMaxHealth = 0; + public string[] DisguiseStatIcons = new string[9]; + public string[] DisguiseStats = new string[9]; + public Dictionary DisguiseUpgrades = new(); + public string[] DisguiseCurrentUpgrades = Array.Empty(); + + public ActorStatValues(ActorInitializer init, ActorStatValuesInfo info) + { + Info = info; + self = init.Self; + + self.World.ActorAdded += ActorAdded; + self.World.ActorRemoved += ActorRemoved; + } + + void INotifyCreated.Created(Actor self) + { + IconOverrides = self.TraitsImplementing().Where(aso => aso.Info.Icon != null).ToArray(); + TooltipActorOverrides = self.TraitsImplementing().Where(aso => aso.Info.TooltipActor != null).ToArray(); + StatClassOverrides = self.TraitsImplementing().Where(aso => aso.Info.Stats != null).ToArray(); + HealthStatOverrides = self.TraitsImplementing().Where(aso => aso.Info.Health != null).ToArray(); + DamageStatOverrides = self.TraitsImplementing().Where(aso => aso.Info.Damage != null).ToArray(); + UpgradeOverrides = self.TraitsImplementing().Where(aso => aso.Info.Upgrades != null).ToArray(); + CalculateStats(); + + BuildableInfo = self.Info.TraitInfos().FirstOrDefault(); + SetupCameos(); + IconOverlays = self.TraitsImplementing().ToArray(); + + Tooltips = self.TraitsImplementing().ToArray(); + Armors = self.TraitsImplementing().Where(a => !Info.ArmorsToIgnore.Contains(a.Info.Type)).ToArray(); + RevealsShrouds = self.TraitsImplementing().ToArray(); + Powers = self.TraitsImplementing().ToArray(); + + AttackBases = self.TraitsImplementing().ToArray(); + Armaments = self.TraitsImplementing().Where(a => IsValidArmament(a.Info.Name)).ToArray(); + + Health = self.TraitOrDefault(); + Shielded = self.TraitOrDefault(); + + Mobile = self.TraitOrDefault(); + Aircraft = self.TraitOrDefault(); + + MindController = self.TraitOrDefault(); + + ResourceHold = self.TraitOrDefault(); + Collector = self.TraitOrDefault(); + CashTricklers = self.TraitsImplementing().ToArray(); + PeriodicProducers = self.TraitsImplementing().ToArray(); + Cargo = self.TraitOrDefault(); + SharedCargo = self.TraitOrDefault(); + Garrisonable = self.TraitOrDefault(); + CarrierMaster = self.TraitOrDefault(); + MobSpawnerMasters = self.TraitsImplementing().ToArray(); + DroneSpawnerMasters = self.TraitsImplementing().ToArray(); + + CalculateHealthStat(); + CalculateDamageStat(); + if (Info.Speed != null) + Speed = Info.Speed.Value; + else if (Aircraft != null) + Speed = Aircraft.Info.Speed; + else if (Mobile != null) + Speed = Mobile.Info.Speed; + + SightModifiers = self.TraitsImplementing().ToArray(); + FirepowerModifiers = self.TraitsImplementing().ToArray(); + ReloadModifiers = self.TraitsImplementing().ToArray(); + RangeModifiers = self.TraitsImplementing().ToArray(); + SpeedModifiers = self.TraitsImplementing().ToArray(); + PowerModifiers = self.TraitsImplementing().ToArray(); + ResourceValueModifiers = self.TraitsImplementing().ToArray(); + + playerResources = self.Owner.PlayerActor.Trait(); + techTree = self.Owner.PlayerActor.Trait(); + + UpgradesEnabled = techTree.HasPrerequisites(Info.UpgradePrerequisites); + CalculateUpgrades(); + } + + void SetupCameos() + { + CalculateIcon(); + + if (Info.IconPalette != null) + IconPalette = Info.IconPalette; + else if (BuildableInfo != null) + IconPalette = BuildableInfo.IconPalette; + + if (Info.IconPaletteIsPlayerPalette != null) + IconPaletteIsPlayerPalette = Info.IconPaletteIsPlayerPalette.Value; + else if (BuildableInfo != null) + IconPaletteIsPlayerPalette = BuildableInfo.IconPaletteIsPlayerPalette; + + CalculateTooltipActor(); + } + + public void CalculateIcon() + { + if (Info.Icon != null) + Icon = Info.Icon; + else if (BuildableInfo != null) + Icon = BuildableInfo.Icon; + + var viewer = self.World.RenderPlayer ?? self.World.LocalPlayer; + var iconOverride = IconOverrides.Where(aso => !aso.IsTraitDisabled && (viewer == null || aso.Info.ValidRelationships.HasRelationship(self.Owner.RelationshipWith(viewer)))).FirstOrDefault(); + if (iconOverride != null) + Icon = iconOverride.Info.Icon; + } + + public void CalculateTooltipActor() + { + if (Info.TooltipActor != null) + TooltipActor = self.World.Map.Rules.Actors[Info.TooltipActor]; + else + TooltipActor = self.Info; + + var viewer = self.World.RenderPlayer ?? self.World.LocalPlayer; + var tooltipActorOverride = TooltipActorOverrides.Where(aso => !aso.IsTraitDisabled && (viewer == null || aso.Info.ValidRelationships.HasRelationship(self.Owner.RelationshipWith(viewer)))).FirstOrDefault(); + if (tooltipActorOverride != null) + TooltipActor = self.World.Map.Rules.Actors[tooltipActorOverride.Info.TooltipActor]; + } + + public void CalculateStats() + { + CurrentStats = Info.Stats; + var viewer = self.World.RenderPlayer ?? self.World.LocalPlayer; + var statOverride = StatClassOverrides.Where(aso => !aso.IsTraitDisabled && (viewer == null || aso.Info.ValidRelationships.HasRelationship(self.Owner.RelationshipWith(viewer)))).FirstOrDefault(); + if (statOverride != null) + CurrentStats = statOverride.Info.Stats; + } + + public void CalculateHealthStat() + { + if (Health != null) + CurrentMaxHealth = Health.MaxHP; + + var viewer = self.World.RenderPlayer ?? self.World.LocalPlayer; + var healthOverride = HealthStatOverrides.Where(aso => !aso.IsTraitDisabled && (viewer == null || aso.Info.ValidRelationships.HasRelationship(self.Owner.RelationshipWith(viewer)))).FirstOrDefault(); + if (healthOverride != null) + CurrentMaxHealth = healthOverride.Info.Health.Value; + } + + public void CalculateDamageStat() + { + CurrentDamage = Info.Damage; + var viewer = self.World.RenderPlayer ?? self.World.LocalPlayer; + var damageOverride = DamageStatOverrides.Where(aso => !aso.IsTraitDisabled && (viewer == null || aso.Info.ValidRelationships.HasRelationship(self.Owner.RelationshipWith(viewer)))).FirstOrDefault(); + if (damageOverride != null) + CurrentDamage = damageOverride.Info.Damage.Value; + } + + public void CalculateUpgrades() + { + if (!UpgradesEnabled) + return; + + CurrentUpgrades = Info.Upgrades; + var viewer = self.World.RenderPlayer ?? self.World.LocalPlayer; + var upgradeOverride = UpgradeOverrides.Where(aso => !aso.IsTraitDisabled && (viewer == null || aso.Info.ValidRelationships.HasRelationship(self.Owner.RelationshipWith(viewer)))).FirstOrDefault(); + if (upgradeOverride != null) + CurrentUpgrades = upgradeOverride.Info.Upgrades; + + Upgrades.Clear(); + foreach (var upgrade in CurrentUpgrades) + Upgrades.Add(upgrade, self.World.Actors.Any(a => a.Owner == self.Owner && a.Info.Name == upgrade)); + } + + void ActorAdded(Actor a) + { + if (!UpgradesEnabled || Info.LocalUpgrades.Contains(a.Info.Name)) + return; + + if (a.Owner == self.Owner && Upgrades.ContainsKey(a.Info.Name)) + Upgrades[a.Info.Name] = true; + + if (a.Owner == DisguisePlayer && DisguiseUpgrades.ContainsKey(a.Info.Name)) + DisguiseUpgrades[a.Info.Name] = true; + } + + void ActorRemoved(Actor a) + { + if (!UpgradesEnabled || Info.LocalUpgrades.Contains(a.Info.Name)) + return; + + // There may be others, just check in general. + if (a.Owner == self.Owner && Upgrades.ContainsKey(a.Info.Name)) + Upgrades[a.Info.Name] = self.World.Actors.Any(other => other.Owner == self.Owner && other.Info.Name == a.Info.Name); + + if (a.Owner == DisguisePlayer && DisguiseUpgrades.ContainsKey(a.Info.Name)) + DisguiseUpgrades[a.Info.Name] = self.World.Actors.Any(other => other.Owner == DisguisePlayer && other.Info.Name == a.Info.Name); + } + + void INotifyProduction.UnitProduced(Actor self, Actor other, CPos exit) + { + if (Info.LocalUpgrades.Length == 0) + return; + + if (Info.LocalUpgrades.Contains(other.Info.Name) && Upgrades.ContainsKey(other.Info.Name)) + Upgrades[other.Info.Name] = true; + } + + public bool IsValidArmament(string armament) + { + if (Info.Armaments != null) + return Info.Armaments.Contains(armament); + else + return AttackBases.Any(ab => ab.Info.Armaments.Contains(armament)); + } + + public string CalculateArmor() + { + if (Info.ShowShield && Shielded != null && !Shielded.IsTraitDisabled && Shielded.Strength > 0) + return (Shielded.Strength / 100).ToString(NumberFormatInfo.CurrentInfo) + " / " + (Shielded.Info.MaxStrength / 100).ToString(NumberFormatInfo.CurrentInfo); + + var activeArmor = Armors.FirstOrDefault(a => !a.IsTraitDisabled); + if (activeArmor == null) + return TranslationProvider.GetString("label-armor-class.no-armor"); + + return TranslationProvider.GetString("label-armor-class." + activeArmor?.Info.Type.Replace('.', '-')); + } + + public string CalculateSight() + { + var revealsShroudValue = WDist.Zero; + if (Info.Sight != null) + revealsShroudValue = Info.Sight.Value; + else + foreach (var rs in RevealsShrouds) + if (!rs.IsTraitDisabled) + revealsShroudValue = revealsShroudValue > rs.Info.Range ? revealsShroudValue : rs.Info.Range; + + foreach (var rsm in SightModifiers.Select(rsm => rsm.GetRevealsShroudModifier())) + revealsShroudValue = revealsShroudValue * rsm / 100; + + return Math.Round((float)revealsShroudValue.Length / 1024, 2).ToString(NumberFormatInfo.CurrentInfo); + } + + public string CalculateSpeed() + { + if (Mobile == null && Aircraft == null) + return "0"; + + var speedValue = Speed; + foreach (var sm in SpeedModifiers.Select(sm => sm.GetSpeedModifier())) + speedValue = speedValue * sm / 100; + + return speedValue.ToString(NumberFormatInfo.CurrentInfo); + } + + public string CalculatePower() + { + var powerValue = 0; + foreach (var p in Powers) + if (!p.IsTraitDisabled) + powerValue += p.Info.Amount; + + foreach (var pm in PowerModifiers.Select(pm => pm.GetPowerModifier())) + powerValue = powerValue * pm / 100; + + return powerValue.ToString(NumberFormatInfo.CurrentInfo); + } + + public string CalculateMindControl() + { + if (MindController == null) + return "0 / 0"; + + return MindController.Slaves.Count().ToString(NumberFormatInfo.CurrentInfo) + " / " + MindController.Info.Capacity.ToString(NumberFormatInfo.CurrentInfo); + } + + public string CalculateDamage() + { + var damageValue = 0; + if (CurrentDamage != null) + damageValue = CurrentDamage.Value; + else + foreach (var ar in Armaments) + if (!ar.IsTraitDisabled) + damageValue += ar.Info.Damage ?? 0; + + foreach (var dm in FirepowerModifiers.Select(fm => fm.GetFirepowerModifier(null))) + damageValue = damageValue * dm / 100; + + return damageValue.ToString(NumberFormatInfo.CurrentInfo); + } + + public string CalculateSpread() + { + var spreadValue = WDist.Zero; + if (Info.Spread != null) + spreadValue = Info.Spread.Value; + else + foreach (var ar in Armaments) + if (!ar.IsTraitDisabled) + { + var sv = ar.Info.Spread ?? WDist.Zero; + spreadValue = spreadValue > sv ? spreadValue : sv; + } + + return Math.Round((float)spreadValue.Length / 1024, 2).ToString(NumberFormatInfo.CurrentInfo); + } + + public string CalculateRoF() + { + var rofValue = int.MaxValue; + if (Info.ReloadDelay != null) + rofValue = Info.ReloadDelay.Value; + else + { + foreach (var ar in Armaments) + if (!ar.IsTraitDisabled) + { + var rof = ar.Info.ReloadDelay ?? ar.Weapon.ReloadDelay; + rofValue = rofValue < rof ? rofValue : rof; + } + } + + if (rofValue != int.MaxValue) + foreach (var rm in ReloadModifiers.Select(sm => sm.GetReloadModifier(null))) + rofValue = rofValue * rm / 100; + else + rofValue = 0; + + return rofValue.ToString(NumberFormatInfo.CurrentInfo); + } + + public string CalculateRange(int slot) + { + var shortRangeValue = WDist.MaxValue; + var longRangeValue = WDist.Zero; + + if (Info.Range.Length >= 1) + { + shortRangeValue = Info.Range.Min(); + longRangeValue = Info.Range.Max(); + } + else + { + foreach (var ar in Armaments) + if (!ar.IsTraitDisabled) + { + var wr = ar.Info.Range ?? ar.Weapon.Range; + longRangeValue = longRangeValue > wr ? longRangeValue : wr; + shortRangeValue = shortRangeValue < wr ? shortRangeValue : wr; + } + } + + if (shortRangeValue == WDist.MaxValue) + shortRangeValue = WDist.Zero; + + foreach (var rm in RangeModifiers.Select(rm => rm.GetRangeModifier())) + { + shortRangeValue = shortRangeValue * rm / 100; + longRangeValue = longRangeValue * rm / 100; + } + + var text = ""; + if (CurrentStats[slot - 1] == ActorStatContent.MaxRange) + text += Math.Round((float)longRangeValue.Length / 1024, 2).ToString(NumberFormatInfo.CurrentInfo); + else if (CurrentStats[slot - 1] == ActorStatContent.MinRange) + text += Math.Round((float)shortRangeValue.Length / 1024, 2).ToString(NumberFormatInfo.CurrentInfo); + + var minimumRangeValue = WDist.MaxValue; + if (Info.MinimumRange != null) + minimumRangeValue = Info.MinimumRange.Value; + else + { + foreach (var ar in Armaments) + if (!ar.IsTraitDisabled) + { + var mr = ar.Info.MinimumRange ?? ar.Weapon.MinRange; + minimumRangeValue = minimumRangeValue < mr ? minimumRangeValue : mr; + } + } + + if (minimumRangeValue.Length > 100 && minimumRangeValue != WDist.MaxValue) + text = Math.Round((float)minimumRangeValue.Length / 1024, 2).ToString(NumberFormatInfo.CurrentInfo) + "-" + text; + + return text; + } + + public string CalculateResourceHold() + { + if (ResourceHold == null) + return "$0"; + + var currentContents = ResourceHold.Contents.Values.Sum().ToString(NumberFormatInfo.CurrentInfo); + var capacity = ResourceHold.Capacity.ToString(NumberFormatInfo.CurrentInfo); + + var value = 0; + foreach (var content in ResourceHold.Contents) + value += playerResources.Info.ResourceValues[content.Key] * content.Value; + + return currentContents + " / " + capacity + " ($" + value.ToString(NumberFormatInfo.CurrentInfo) + ")"; + } + + public string CalculateCollector() + { + if (Collector == null) + return "$0"; + + var value = Collector.Amount(); + foreach (var dm in ResourceValueModifiers.Select(rvm => rvm.GetResourceValueModifier())) + value = value * dm / 100; + + return "$" + value.ToString(NumberFormatInfo.CurrentInfo); + } + + public string CalculateCashTrickler() + { + var minTicks = int.MaxValue; + foreach (var ct in CashTricklers) + if (!ct.IsTraitDisabled) + minTicks = Math.Min(minTicks, ct.Ticks); + + return WidgetUtils.FormatTime(minTicks == int.MaxValue ? 0 : minTicks, self.World.Timestep); + } + + public string CalculatePeriodicProducer() + { + var minTicks = int.MaxValue; + foreach (var pp in PeriodicProducers) + if (!pp.IsTraitDisabled) + minTicks = Math.Min(minTicks, pp.Ticks); + + return WidgetUtils.FormatTime(minTicks == int.MaxValue ? 0 : minTicks, self.World.Timestep); + } + + public string CalculateCargo() + { + if (Cargo != null) + return Cargo.TotalWeight + " / " + Cargo.Info.MaxWeight; + else if (SharedCargo != null) + return SharedCargo.Manager.TotalWeight + " / " + SharedCargo.Manager.Info.MaxWeight; + else if (Garrisonable != null) + return Garrisonable.TotalWeight + " / " + Garrisonable.Info.MaxWeight; + else + return "0 / 0"; + } + + public string CalculateCarrier() + { + if (CarrierMaster == null) + return "0 / 0 / 0"; + + var stored = 0; + var valid = 0; + + foreach (var s in CarrierMaster.SlaveEntries) + if (s.IsValid) + { + valid++; + if (!s.IsLaunched) stored++; + } + + return stored.ToString(NumberFormatInfo.CurrentInfo) + " / " + valid.ToString(NumberFormatInfo.CurrentInfo) + " / " + CarrierMaster.Info.Actors.Length.ToString(NumberFormatInfo.CurrentInfo); + } + + public string CalculateMobSpawner() + { + var total = 0; + var spawned = 0; + foreach (var mobSpawnerMaster in MobSpawnerMasters) + { + if (!mobSpawnerMaster.IsTraitDisabled) + { + total += mobSpawnerMaster.Info.Actors.Length; + spawned += mobSpawnerMaster.SlaveEntries.Where(s => s.IsValid).Count(); + } + } + + return spawned.ToString(NumberFormatInfo.CurrentInfo) + " / " + total.ToString(NumberFormatInfo.CurrentInfo); + } + + public string CalculateDroneSpawner() + { + var total = 0; + var spawned = 0; + foreach (var droneSpawnerMaster in DroneSpawnerMasters) + { + if (!droneSpawnerMaster.IsTraitDisabled) + { + total += droneSpawnerMaster.Info.Actors.Length; + spawned += droneSpawnerMaster.SlaveEntries.Where(s => s.IsValid).Count(); + } + } + + return spawned.ToString(NumberFormatInfo.CurrentInfo) + " / " + total.ToString(NumberFormatInfo.CurrentInfo); + } + + public string GetIconFor(int slot) + { + if (CurrentStats.Length < slot || CurrentStats[slot - 1] == ActorStatContent.None) + return null; + else if (CurrentStats[slot - 1] == ActorStatContent.Armor) + { + if (Info.ShowShield && Shielded != null && !Shielded.IsTraitDisabled && Shielded.Strength > 0) + return "actor-stats-shield"; + + return "actor-stats-armor"; + } + else if (CurrentStats[slot - 1] == ActorStatContent.Sight) + return "actor-stats-sight"; + else if (CurrentStats[slot - 1] == ActorStatContent.Speed) + return "actor-stats-speed"; + else if (CurrentStats[slot - 1] == ActorStatContent.Power) + return "actor-stats-power"; + else if (CurrentStats[slot - 1] == ActorStatContent.Damage) + return "actor-stats-damage"; + else if (CurrentStats[slot - 1] == ActorStatContent.MindControl) + return "actor-stats-mindcontrol"; + else if (CurrentStats[slot - 1] == ActorStatContent.ReloadDelay) + return "actor-stats-rof"; + else if (CurrentStats[slot - 1] == ActorStatContent.Spread) + return "actor-stats-spread"; + else if (CurrentStats[slot - 1] == ActorStatContent.MinRange) + if (CurrentStats.Contains(ActorStatContent.MaxRange)) + return "actor-stats-shortrange"; + else + return "actor-stats-range"; + else if (CurrentStats[slot - 1] == ActorStatContent.MaxRange) + if (CurrentStats.Contains(ActorStatContent.MinRange)) + return "actor-stats-longrange"; + else + return "actor-stats-range"; + else if (CurrentStats[slot - 1] == ActorStatContent.Harvester) + return "actor-stats-resources"; + else if (CurrentStats[slot - 1] == ActorStatContent.Collector) + return "actor-stats-resources"; + else if (CurrentStats[slot - 1] == ActorStatContent.CashTrickler) + return "actor-stats-timer"; + else if (CurrentStats[slot - 1] == ActorStatContent.PeriodicProducer) + return "actor-stats-timer"; + else if (CurrentStats[slot - 1] == ActorStatContent.Cargo) + return "actor-stats-cargo"; + else if (CurrentStats[slot - 1] == ActorStatContent.Carrier) + return "actor-stats-carrier"; + else if (CurrentStats[slot - 1] == ActorStatContent.Mob) + return "actor-stats-mob"; + else if (CurrentStats[slot - 1] == ActorStatContent.Drones) + return "actor-stats-drones"; + else + return null; + } + + public string GetValueFor(int slot) + { + if (CurrentStats.Length < slot || CurrentStats[slot - 1] == ActorStatContent.None) + return null; + else if (CurrentStats[slot - 1] == ActorStatContent.Armor) + return CalculateArmor(); + else if (CurrentStats[slot - 1] == ActorStatContent.Sight) + return CalculateSight(); + else if (CurrentStats[slot - 1] == ActorStatContent.Speed) + return CalculateSpeed(); + else if (CurrentStats[slot - 1] == ActorStatContent.Power) + return CalculatePower(); + else if (CurrentStats[slot - 1] == ActorStatContent.Damage) + return CalculateDamage(); + else if (CurrentStats[slot - 1] == ActorStatContent.MindControl) + return CalculateMindControl(); + else if (CurrentStats[slot - 1] == ActorStatContent.ReloadDelay) + return CalculateRoF(); + else if (CurrentStats[slot - 1] == ActorStatContent.Spread) + return CalculateSpread(); + else if (CurrentStats[slot - 1] == ActorStatContent.MinRange || CurrentStats[slot - 1] == ActorStatContent.MaxRange) + return CalculateRange(slot); + else if (CurrentStats[slot - 1] == ActorStatContent.Harvester) + return CalculateResourceHold(); + else if (CurrentStats[slot - 1] == ActorStatContent.Collector) + return CalculateCollector(); + else if (CurrentStats[slot - 1] == ActorStatContent.CashTrickler) + return CalculateCashTrickler(); + else if (CurrentStats[slot - 1] == ActorStatContent.PeriodicProducer) + return CalculatePeriodicProducer(); + else if (CurrentStats[slot - 1] == ActorStatContent.Cargo) + return CalculateCargo(); + else if (CurrentStats[slot - 1] == ActorStatContent.Carrier) + return CalculateCarrier(); + else if (CurrentStats[slot - 1] == ActorStatContent.Mob) + return CalculateMobSpawner(); + else if (CurrentStats[slot - 1] == ActorStatContent.Drones) + return CalculateDroneSpawner(); + + return ""; + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + foreach (var upgrade in CurrentUpgrades) + { + if (Info.LocalUpgrades.Contains(upgrade)) + continue; + + Upgrades[upgrade] = self.World.Actors.Any(a => a.Owner == newOwner && a.Info.Name == upgrade); + } + } + + void INotifyDisguised.DisguiseChanged(Actor self, Actor target) + { + Disguised = self != target; + + if (Disguised) + { + var targetASV = target.TraitOrDefault(); + if (targetASV != null) + { + Icon = targetASV.Icon; + IconPalette = targetASV.IconPalette; + IconPaletteIsPlayerPalette = targetASV.IconPaletteIsPlayerPalette; + TooltipActor = targetASV.TooltipActor; + + DisguisePlayer = target.Owner; + DisguiseImage = target.TraitOrDefault()?.GetImage(target); + DisguiseMaxHealth = targetASV.CurrentMaxHealth; + + for (var i = 1; i <= 8; i++) + { + DisguiseStatIcons[i] = targetASV.GetIconFor(i); + DisguiseStats[i] = targetASV.GetValueFor(i); + } + + DisguiseUpgrades = targetASV.Upgrades; + DisguiseCurrentUpgrades = targetASV.CurrentUpgrades; + } + else + { + SetupCameos(); + DisguiseImage = null; + DisguiseMaxHealth = 0; + Disguised = false; + } + } + else + { + SetupCameos(); + DisguiseImage = null; + DisguiseMaxHealth = 0; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/ActorTag.cs b/OpenRA.Mods.AS/Traits/ActorTag.cs new file mode 100644 index 000000000000..806220d8aab0 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/ActorTag.cs @@ -0,0 +1,24 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Tag trait for Lua scripts.")] + public class ActorTagInfo : TraitInfo + { + [FieldLoader.Require] + public readonly HashSet Type = new(); + } + + public class ActorTag { } +} diff --git a/OpenRA.Mods.AS/Traits/Air/AutoTakesOff.cs b/OpenRA.Mods.AS/Traits/Air/AutoTakesOff.cs new file mode 100644 index 000000000000..e624487533c0 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Air/AutoTakesOff.cs @@ -0,0 +1,33 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor takes of automatically on creation.")] + public class AutoTakesOffInfo : TraitInfo, Requires + { + public override object Create(ActorInitializer init) { return new AutoTakesOff(this); } + } + + public class AutoTakesOff : INotifyAddedToWorld + { + public AutoTakesOff(AutoTakesOffInfo info) { } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + self.QueueActivity(new TakeOff(self)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/AirstrikeMaster.cs b/OpenRA.Mods.AS/Traits/AirstrikeMaster.cs new file mode 100644 index 000000000000..84a0d622ae50 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/AirstrikeMaster.cs @@ -0,0 +1,297 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor can send in other actors to deliver an airstrike.")] + public class AirstrikeMasterInfo : BaseSpawnerMasterInfo + { + public readonly string Name = "primary"; + + [Desc("Just send the spawnee and forget it.")] + public readonly bool SendAndForget = false; + + [Desc("Spawn rearm delay, in ticks")] + public readonly int RearmTicks = 150; + + [GrantedConditionReference] + [Desc("The condition to grant to self right after launching a spawned unit. (Used by V3 to make immobile.)")] + public readonly string LaunchingCondition = null; + + [Desc("After this many ticks, we remove the condition.")] + public readonly int LaunchingTicks = 15; + + [Desc("Instantly repair spawners when they return?")] + public readonly bool InstantRepair = true; + + [GrantedConditionReference] + [Desc("The condition to grant to self while spawned units are loaded.", + "Condition can stack with multiple spawns.")] + public readonly string LoadedCondition = null; + + [Desc("Conditions to grant when specified actors are contained inside the transport.", + "A dictionary of [actor id]: [condition].")] + public readonly Dictionary SpawnContainConditions = new(); + + [Desc("The sound will be played when mark a target")] + public readonly string MarkSound = ""; + + public readonly int SquadSize = 1; + public readonly WVec SquadOffset = new(-1536, 1536, 0); + + public readonly int QuantizedFacings = 32; + public readonly WDist Cordon = new(5120); + + [GrantedConditionReference] + public IEnumerable LinterSpawnContainConditions { get { return SpawnContainConditions.Values; } } + + public override object Create(ActorInitializer init) { return new AirstrikeMaster(init, this); } + } + + public class AirstrikeMaster : BaseSpawnerMaster, ITick, INotifyAttack + { + class AirstrikeSlaveEntry : BaseSpawnerSlaveEntry + { + public int RearmTicks = 0; + public new AirstrikeSlave SpawnerSlave; + } + + readonly Dictionary> spawnContainTokens = new(); + public readonly AirstrikeMasterInfo AirstrikeMasterInfo; + + readonly Stack loadedTokens = new(); + + WPos finishEdge; + WVec spawnOffset; + WPos targetPos; + + int launchCondition = Actor.InvalidConditionToken; + int launchConditionTicks; + + int respawnTicks = 0; + + public AirstrikeMaster(ActorInitializer init, AirstrikeMasterInfo info) + : base(init, info) + { + AirstrikeMasterInfo = info; + } + + protected override void Created(Actor self) + { + base.Created(self); + + // Spawn initial load. + var burst = Info.InitialActorCount == -1 ? Info.Actors.Length : Info.InitialActorCount; + for (var i = 0; i < burst; i++) + Replenish(self, SlaveEntries); + } + + public override BaseSpawnerSlaveEntry[] CreateSlaveEntries(BaseSpawnerMasterInfo info) + { + var slaveEntries = new AirstrikeSlaveEntry[info.Actors.Length]; // For this class to use + + for (var i = 0; i < slaveEntries.Length; i++) + slaveEntries[i] = new AirstrikeSlaveEntry(); + + return slaveEntries; // For the base class to use + } + + public override void InitializeSlaveEntry(Actor slave, BaseSpawnerSlaveEntry entry) + { + var se = entry as AirstrikeSlaveEntry; + base.InitializeSlaveEntry(slave, se); + + se.RearmTicks = 0; + se.IsLaunched = false; + se.SpawnerSlave = slave.Trait(); + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + + // The rate of fire of the dummy weapon determines the launch cycle as each shot + // invokes Attacking() + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + // HACK: If Armament hits instantly and kills the target, the target will become invalid + if (target.Type == TargetType.Invalid || IsTraitDisabled || IsTraitPaused || (Info.ArmamentNames.Count > 0 && !Info.ArmamentNames.Contains(a.Info.Name))) + return; + + // Issue retarget order for already launched ones + foreach (var slave in SlaveEntries) + if (slave.IsLaunched && slave.IsValid) + slave.SpawnerSlave.Attack(slave.Actor, target); + + var se = GetLaunchable(); + if (se == null) + return; + + se.IsLaunched = true; // mark as launched + + if (AirstrikeMasterInfo.LaunchingCondition != null) + { + if (launchCondition == Actor.InvalidConditionToken) + launchCondition = self.GrantCondition(AirstrikeMasterInfo.LaunchingCondition); + + launchConditionTicks = AirstrikeMasterInfo.LaunchingTicks; + } + + SpawnIntoWorld(self, se.Actor, self.CenterPosition + se.Offset.Rotate(self.Orientation)); + + se.SpawnerSlave.SetSpawnInfo(finishEdge, spawnOffset, targetPos); + + // Lambdas can't use 'in' variables, so capture a copy for later + var delayedTarget = target; + + // Queue attack order, too. + self.World.AddFrameEndTask(w => + { + // The actor might had been trying to do something before entering the carrier. + // Cancel whatever it was trying to do. + se.SpawnerSlave.Stop(se.Actor); + if (!string.IsNullOrEmpty(AirstrikeMasterInfo.MarkSound)) + se.Actor.PlayVoice(AirstrikeMasterInfo.MarkSound); + se.SpawnerSlave.Attack(se.Actor, delayedTarget); + }); + + if (AirstrikeMasterInfo.SendAndForget) + se.Actor = null; + } + + public override void SpawnIntoWorld(Actor self, Actor slave, WPos centerPosition) + { + var w = self.World; + var target = centerPosition; + for (var i = -AirstrikeMasterInfo.SquadSize / 2; i <= AirstrikeMasterInfo.SquadSize / 2; i++) + { + var attackFacing = 256 * self.World.SharedRandom.Next(AirstrikeMasterInfo.QuantizedFacings) / AirstrikeMasterInfo.QuantizedFacings; + + var altitude = self.World.Map.Rules.Actors[slave.Info.Name].TraitInfo().CruiseAltitude.Length; + var attackRotation = WRot.FromFacing(attackFacing); + var delta = new WVec(0, -1024, 0).Rotate(attackRotation); + target += new WVec(0, 0, altitude); + var startEdge = target - (self.World.Map.DistanceToEdge(target, -delta) + AirstrikeMasterInfo.Cordon).Length * delta / 1024; + var finishEdge = target + (self.World.Map.DistanceToEdge(target, delta) + AirstrikeMasterInfo.Cordon).Length * delta / 1024; + + var so = AirstrikeMasterInfo.SquadOffset; + var spawnOffset = new WVec(i * so.Y, -Math.Abs(i) * so.X, 0).Rotate(attackRotation); + var targetOffset = new WVec(i * so.Y, 0, 0).Rotate(attackRotation); + + this.spawnOffset = spawnOffset; + this.finishEdge = finishEdge; + targetPos = target; + + w.AddFrameEndTask(_ => + { + if (!slave.IsInWorld) + w.Add(slave); + + var attack = slave.Trait(); + attack.AttackTarget(Target.FromPos(target + targetOffset), AttackSource.Default, false, true); + }); + } + } + + void Recall() + { + // Tell launched slaves to come back and enter me. + foreach (var se in SlaveEntries) + { + var childSlave = se as AirstrikeSlaveEntry; + if (se.IsLaunched && se.IsValid) + childSlave.SpawnerSlave.LeaveMap(se.Actor); + } + } + + public override void OnSlaveKilled(Actor self, Actor slave) + { + // Set clock so that regen happens. + if (respawnTicks <= 0) // Don't interrupt an already running timer! + respawnTicks = Util.ApplyPercentageModifiers(Info.RespawnTicks, reloadModifiers.Select(rm => rm.GetReloadModifier(AirstrikeMasterInfo.Name))); + } + + AirstrikeSlaveEntry GetLaunchable() + { + foreach (var se in SlaveEntries) + { + var childSlave = se as AirstrikeSlaveEntry; + if (childSlave.RearmTicks <= 0 && !childSlave.IsLaunched && se.IsValid) + return childSlave; + } + + return null; + } + + public void PickupSlave(Actor self, Actor a) + { + AirstrikeSlaveEntry slaveEntry = null; + foreach (var se in SlaveEntries) + if (se.Actor == a) + { + slaveEntry = se as AirstrikeSlaveEntry; + break; + } + + if (slaveEntry == null) + throw new InvalidOperationException("An actor that isn't my slave entered me?"); + + slaveEntry.IsLaunched = false; + + // setup rearm + slaveEntry.RearmTicks = Util.ApplyPercentageModifiers(AirstrikeMasterInfo.RearmTicks, reloadModifiers.Select(rm => rm.GetReloadModifier(AirstrikeMasterInfo.Name))); + + if (AirstrikeMasterInfo.SpawnContainConditions.TryGetValue(a.Info.Name, out var spawnContainCondition)) + spawnContainTokens.GetOrAdd(a.Info.Name).Push(self.GrantCondition(spawnContainCondition)); + + if (!string.IsNullOrEmpty(AirstrikeMasterInfo.LoadedCondition)) + loadedTokens.Push(self.GrantCondition(AirstrikeMasterInfo.LoadedCondition)); + } + + void ITick.Tick(Actor self) + { + if (launchCondition != Actor.InvalidConditionToken && --launchConditionTicks < 0) + launchCondition = self.RevokeCondition(launchCondition); + + if (respawnTicks > 0) + { + respawnTicks--; + + // Time to respawn someting. + if (respawnTicks <= 0) + { + Replenish(self, SlaveEntries); + + // If there's something left to spawn, restart the timer. + if (SelectEntryToSpawn(SlaveEntries) != null) + respawnTicks = Util.ApplyPercentageModifiers(Info.RespawnTicks, reloadModifiers.Select(rm => rm.GetReloadModifier(AirstrikeMasterInfo.Name))); + } + } + + // Rearm + foreach (var se in SlaveEntries) + { + var slaveEntry = se as AirstrikeSlaveEntry; + if (slaveEntry.RearmTicks > 0) + slaveEntry.RearmTicks--; + } + } + + protected override void TraitPaused(Actor self) + { + Recall(); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/AirstrikeSlave.cs b/OpenRA.Mods.AS/Traits/AirstrikeSlave.cs new file mode 100644 index 000000000000..ac7bcde1db41 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/AirstrikeSlave.cs @@ -0,0 +1,88 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.AS.Activities; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Can be slaved to a spawner.")] + public class AirstrikeSlaveInfo : BaseSpawnerSlaveInfo + { + [Desc("Move this close to the spawner, before entering it.")] + public readonly WDist LandingDistance = new(5 * 1024); + + [Desc("We consider this is close enought to the spawner and enter it, instead of trying to reach 0 distance." + + "This allows the spawned unit to enter the spawner while the spawner is moving.")] + public readonly WDist CloseEnoughDistance = new(128); + + public override object Create(ActorInitializer init) { return new AirstrikeSlave(init, this); } + } + + public class AirstrikeSlave : BaseSpawnerSlave, INotifyIdle + { + // readonly AmmoPool[] ammoPools; + public readonly AirstrikeSlaveInfo Info; + + // WPos targetPos; + WPos finishEdge; + WVec spawnOffset; + + AirstrikeMaster spawnerMaster; + + public AirstrikeSlave(ActorInitializer init, AirstrikeSlaveInfo info) + : base(info) + { + Info = info; + /* ammoPools = init.Self.TraitsImplementing().ToArray(); */ + } + + public void SetSpawnInfo(WPos finishEdge, WVec spawnOffset, WPos targetPos) + { + // this.targetPos = targetPos; + this.finishEdge = finishEdge; + this.spawnOffset = spawnOffset; + } + + public void LeaveMap(Actor self) + { + // Hopefully, self will be disposed shortly afterwards by SpawnerSlaveDisposal policy. + if (Master == null || Master.IsDead) + return; + + // Proceed with enter, if already at it. + if (self.CurrentActivity is ReturnAirstrikeMaster) + return; + + // Cancel whatever else self was doing and return. + self.QueueActivity(false, new ReturnAirstrikeMaster(Master, spawnerMaster, finishEdge + spawnOffset)); + } + + public override void LinkMaster(Actor self, Actor master, BaseSpawnerMaster spawnerMaster) + { + base.LinkMaster(self, master, spawnerMaster); + this.spawnerMaster = spawnerMaster as AirstrikeMaster; + } + + /* bool NeedToReload() + { + // The unit may not have ammo but will have unlimited ammunitions. + if (ammoPools.Length == 0) + return false; + + return ammoPools.All(x => !x.HasAmmo); + } */ + + void INotifyIdle.TickIdle(Actor self) + { + LeaveMap(self); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Attack/AttackFollowFrontal.cs b/OpenRA.Mods.AS/Traits/Attack/AttackFollowFrontal.cs new file mode 100644 index 000000000000..c25c7ade0aac --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Attack/AttackFollowFrontal.cs @@ -0,0 +1,226 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Activities; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class AttackFollowFrontalInfo : AttackFollowInfo + { + [Desc("Actor will turn directly to target regardless the FacingTolerance to catch its target in full fire angle.")] + public readonly bool MustFaceTarget = false; + + public override object Create(ActorInitializer init) { return new AttackFollowFrontal(init.Self, this); } + } + + public class AttackFollowFrontal : AttackBase, INotifyOwnerChanged, IOverrideAutoTarget, INotifyStanceChanged + { + public new readonly AttackFollowFrontalInfo Info; + public Target RequestedTarget { get; private set; } + public Target OpportunityTarget { get; private set; } + + Mobile mobile; + AutoTarget autoTarget; + bool requestedForceAttack; + Activity requestedTargetPresetForActivity; + bool opportunityForceAttack; + bool opportunityTargetIsPersistentTarget; + + public void SetRequestedTarget(Actor self, in Target target, bool isForceAttack = false) + { + RequestedTarget = target; + requestedForceAttack = isForceAttack; + requestedTargetPresetForActivity = null; + } + + public void ClearRequestedTarget() + { + if (Info.PersistentTargeting) + { + OpportunityTarget = RequestedTarget; + opportunityForceAttack = requestedForceAttack; + opportunityTargetIsPersistentTarget = true; + } + + RequestedTarget = Target.Invalid; + requestedTargetPresetForActivity = null; + } + + public AttackFollowFrontal(Actor self, AttackFollowFrontalInfo info) + : base(self, info) + { + Info = info; + } + + protected override void Created(Actor self) + { + mobile = self.TraitOrDefault(); + autoTarget = self.TraitOrDefault(); + base.Created(self); + } + + protected bool CanAimAtTarget(Actor self, in Target target, bool forceAttack) + { + if (target.Type == TargetType.Actor && !target.Actor.CanBeViewedByPlayer(self.Owner)) + return false; + + if (target.Type == TargetType.FrozenActor && !target.FrozenActor.IsValid) + return false; + + var pos = self.CenterPosition; + var armaments = ChooseArmamentsForTarget(target, forceAttack); + foreach (var a in armaments) + if (target.IsInRange(pos, a.MaxRange()) && (a.Weapon.MinRange == WDist.Zero || !target.IsInRange(pos, a.Weapon.MinRange))) + if (TargetInFiringArc(self, target, Info.FacingTolerance)) + return true; + + return false; + } + + protected override void Tick(Actor self) + { + if (IsTraitDisabled) + { + RequestedTarget = OpportunityTarget = Target.Invalid; + opportunityTargetIsPersistentTarget = false; + } + + if (requestedTargetPresetForActivity != null) + { + // RequestedTarget was set by OnQueueAttackActivity in preparation for a queued activity + // requestedTargetPresetForActivity will be cleared once the activity starts running and calls UpdateRequestedTarget + if (self.CurrentActivity != null && self.CurrentActivity.NextActivity == requestedTargetPresetForActivity) + { + RequestedTarget = RequestedTarget.Recalculate(self.Owner, out _); + } + + // Requested activity has been canceled + else + ClearRequestedTarget(); + } + + // Can't fire on anything + if (mobile != null && !mobile.CanInteractWithGroundLayer(self)) + return; + + if (RequestedTarget.Type != TargetType.Invalid) + { + IsAiming = CanAimAtTarget(self, RequestedTarget, requestedForceAttack); + if (IsAiming) + DoAttack(self, RequestedTarget); + } + else + { + IsAiming = false; + + if (OpportunityTarget.Type != TargetType.Invalid) + IsAiming = CanAimAtTarget(self, OpportunityTarget, opportunityForceAttack); + + if (!IsAiming && Info.OpportunityFire && autoTarget != null && + !autoTarget.IsTraitDisabled && autoTarget.Stance >= UnitStance.Defend) + { + OpportunityTarget = autoTarget.ScanForTarget(self, false, false); + opportunityForceAttack = false; + opportunityTargetIsPersistentTarget = false; + + if (OpportunityTarget.Type != TargetType.Invalid) + IsAiming = CanAimAtTarget(self, OpportunityTarget, opportunityForceAttack); + } + + if (IsAiming) + DoAttack(self, OpportunityTarget); + } + + base.Tick(self); + } + + public override Activity GetAttackActivity(Actor self, AttackSource source, in Target newTarget, bool allowMove, bool forceAttack, Color? targetLineColor = null) + { + return new AttackFrontalFollowActivity(self, newTarget, allowMove, forceAttack, targetLineColor); + } + + public override void OnResolveAttackOrder(Actor self, Activity activity, in Target target, bool queued, bool forceAttack) + { + // We can improve responsiveness for turreted actors by preempting + // the last order (usually a move) and setting the target immediately + if (!queued) + { + RequestedTarget = target; + requestedForceAttack = forceAttack; + requestedTargetPresetForActivity = activity; + } + } + + public override void OnStopOrder(Actor self) + { + RequestedTarget = OpportunityTarget = Target.Invalid; + opportunityTargetIsPersistentTarget = false; + base.OnStopOrder(self); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + RequestedTarget = OpportunityTarget = Target.Invalid; + opportunityTargetIsPersistentTarget = false; + } + + bool IOverrideAutoTarget.TryGetAutoTargetOverride(Actor self, out Target target) + { + if (RequestedTarget.Type != TargetType.Invalid) + { + target = RequestedTarget; + return true; + } + + if (opportunityTargetIsPersistentTarget && OpportunityTarget.Type != TargetType.Invalid) + { + target = OpportunityTarget; + return true; + } + + target = Target.Invalid; + return false; + } + + void INotifyStanceChanged.StanceChanged(Actor self, AutoTarget autoTarget, UnitStance oldStance, UnitStance newStance) + { + // Cancel opportunity targets when switching to a more restrictive stance if they are no longer valid for auto-targeting + if (newStance > oldStance || opportunityForceAttack) + return; + + if (OpportunityTarget.Type == TargetType.Actor) + { + var a = OpportunityTarget.Actor; + if (!autoTarget.HasValidTargetPriority(self, a.Owner, a.GetEnabledTargetTypes())) + OpportunityTarget = Target.Invalid; + } + else if (OpportunityTarget.Type == TargetType.FrozenActor) + { + var fa = OpportunityTarget.FrozenActor; + if (!autoTarget.HasValidTargetPriority(self, fa.Owner, fa.TargetTypes)) + OpportunityTarget = Target.Invalid; + } + } + + protected override bool CanAttack(Actor self, in Target target) + { + if (!base.CanAttack(self, target)) + return false; + + return TargetInFiringArc(self, target, Info.FacingTolerance); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Attack/AttackInfect.cs b/OpenRA.Mods.AS/Traits/Attack/AttackInfect.cs new file mode 100644 index 000000000000..9bfd84dc070a --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Attack/AttackInfect.cs @@ -0,0 +1,103 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Activities; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Move onto the target then execute the attack.")] + public class AttackInfectInfo : AttackFrontalInfo, Requires + { + public readonly string Name = "primary"; + + [Desc("Range of the final joust of the infector.")] + public readonly WDist JoustRange = WDist.Zero; + + [Desc("Conditions that last from start of the joust until the attack.")] + [GrantedConditionReference] + public readonly string JoustCondition = "jousting"; + + [FieldLoader.Require] + [Desc("How much damage to deal.")] + public readonly int Damage; + + [FieldLoader.Require] + [Desc("How often to deal the damage.")] + public readonly int DamageInterval; + + [Desc("Damage types for the infection damage.")] + public readonly BitSet DamageTypes = default; + + [Desc("If an external actor delivers more damage than this value, the infector is killed immediately.", + "Use -1 to never kill the infector.")] + public readonly int SuppressionDamageThreshold = -1; + + [Desc("If the infected actor receives more damage from external sources than this value, the infector is killed immediately.", + "Use -1 to never kill the infector.")] + public readonly int SuppressionSumThreshold = -1; + + [Desc("If the infected actor receives damage more times from external sources than this value, the infector is killed immediately.", + "Only counted if the value is above 0.")] + public readonly int SuppressionCountThreshold = 0; + + [Desc("Damage type used for the suppression calculations.")] + public readonly BitSet SuppressionDamageType = default; + + [Desc("Damage types which allows the infector survive when it's host dies.")] + public readonly BitSet SurviveHostDamageTypes = default; + + public override object Create(ActorInitializer init) { return new AttackInfect(init.Self, this); } + } + + public class AttackInfect : AttackFrontal + { + public readonly AttackInfectInfo InfectInfo; + + int joustToken = Actor.InvalidConditionToken; + + public AttackInfect(Actor self, AttackInfectInfo info) + : base(self, info) + { + InfectInfo = info; + } + + protected override bool CanAttack(Actor self, in Target target) + { + if (target.Type != TargetType.Actor) + return false; + + if (self.Location == target.Actor.Location && HasAnyValidWeapons(target)) + return true; + + return base.CanAttack(self, target); + } + + public void GrantJoustCondition(Actor self) + { + if (!string.IsNullOrEmpty(InfectInfo.JoustCondition)) + joustToken = self.GrantCondition(InfectInfo.JoustCondition); + } + + public void RevokeJoustCondition(Actor self) + { + if (joustToken != Actor.InvalidConditionToken) + joustToken = self.RevokeCondition(joustToken); + } + + public override Activity GetAttackActivity(Actor self, AttackSource source, in Target newTarget, bool allowMove, bool forceAttack, Color? targetLineColor) + { + return new Infect(self, newTarget, this, InfectInfo, targetLineColor); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Attack/AttackLeapAS.cs b/OpenRA.Mods.AS/Traits/Attack/AttackLeapAS.cs new file mode 100644 index 000000000000..f82f68d14549 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Attack/AttackLeapAS.cs @@ -0,0 +1,105 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Dogs use this attack model.")] + class AttackLeapASInfo : AttackFrontalInfo + { + [Desc("Leap speed (in units/tick).")] + public readonly WDist Speed = new(426); + + public readonly WAngle Angle = WAngle.FromDegrees(20); + + [Desc("Types of damage that this trait causes. Leave empty for no damage types.")] + public readonly BitSet DamageTypes = default; + + [Desc("The condition to apply to the target while leaping. Must be included in the target actor's ExternalConditions list.")] + public readonly string LeapTargetCondition = null; + + public override object Create(ActorInitializer init) { return new AttackLeapAS(init.Self, this); } + } + + class AttackLeapAS : AttackFrontal + { + readonly Barrel barrel; + public readonly AttackLeapASInfo LeapInfo; + + INotifyAttack[] notifyAttacks; + (Actor Actor, int Token) targetCondition; + + public AttackLeapAS(Actor self, AttackLeapASInfo info) + : base(self, info) + { + LeapInfo = info; + barrel = new Barrel { Offset = WVec.Zero, Yaw = WAngle.Zero }; + } + + protected override void Created(Actor self) + { + notifyAttacks = self.TraitsImplementing().ToArray(); + + base.Created(self); + } + + public override void DoAttack(Actor self, in Target target) + { + if (target.Type != TargetType.Actor || !CanAttack(self, target)) + return; + + var a = ChooseArmamentsForTarget(target, true).FirstOrDefault(); + if (a == null) + return; + + if (!target.IsInRange(self.CenterPosition, a.MaxRange())) + return; + + self.CancelActivity(); + + foreach (var na in notifyAttacks) + na.PreparingAttack(self, target, a, barrel); + + if (LeapInfo.LeapTargetCondition != null) + { + // Lambdas can't use 'in' variables, so capture a copy for later + var delayedTarget = target; + var external = target.Actor.TraitsImplementing() + .FirstOrDefault(t => t.Info.Condition == LeapInfo.LeapTargetCondition && t.CanGrantCondition(self)); + + if (external != null) + targetCondition = (target.Actor, external.GrantCondition(target.Actor, self)); + } + + self.QueueActivity(new LeapAS(self, target.Actor, a, this)); + } + + public void NotifyAttacking(Actor self, Target target, Armament a) + { + foreach (var na in notifyAttacks) + na.Attacking(self, target, a, barrel); + } + + public void FinishAttacking(Actor self) + { + if (targetCondition.Actor != null && !targetCondition.Actor.IsDead) + { + foreach (var external in targetCondition.Actor.TraitsImplementing()) + if (external.TryRevokeCondition(targetCondition.Actor, self, targetCondition.Token)) + break; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Attack/AttackOpenTopped.cs b/OpenRA.Mods.AS/Traits/Attack/AttackOpenTopped.cs new file mode 100644 index 000000000000..50b7efa679f9 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Attack/AttackOpenTopped.cs @@ -0,0 +1,194 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Implements the YR OpenTopped logic where transported actors used separate firing offsets, ignoring facing." + + "Compatible with both `Cargo`/`Passengers` or `Garrionable`/`Garrisoners` logic.")] + public class AttackOpenToppedInfo : AttackFollowInfo, IRulesetLoaded + { + [FieldLoader.Require] + [Desc("Fire port offsets in local coordinates.")] + public readonly WVec[] PortOffsets = null; + + public override object Create(ActorInitializer init) { return new AttackOpenTopped(init.Self, this); } + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + if (PortOffsets.Length == 0) + throw new YamlException("PortOffsets must have at least one entry."); + + base.RulesetLoaded(rules, ai); + } + } + + public class AttackOpenTopped : AttackFollow, INotifyGarrisonerEntered, INotifyGarrisonerExited, IRender, INotifyPassengerEntered, INotifyPassengerExited + { + public new readonly AttackOpenToppedInfo Info; + readonly Lazy coords; + readonly List actors; + readonly List armaments; + readonly HashSet<(AnimationWithOffset Animation, string Sequence)> muzzles; + readonly Dictionary paxFacing; + readonly Dictionary paxPos; + readonly Dictionary paxRender; + + public AttackOpenTopped(Actor self, AttackOpenToppedInfo info) + : base(self, info) + { + Info = info; + coords = Exts.Lazy(() => self.Trait()); + actors = new List(); + armaments = new List(); + muzzles = new HashSet<(AnimationWithOffset Animation, string Sequence)>(); + paxFacing = new Dictionary(); + paxPos = new Dictionary(); + paxRender = new Dictionary(); + } + + protected override Func> InitializeGetArmaments(Actor self) + { + return () => armaments; + } + + void OnActorEntered(Actor enterer) + { + actors.Add(enterer); + paxFacing.Add(enterer, enterer.Trait()); + paxPos.Add(enterer, enterer.Trait()); + paxRender.Add(enterer, enterer.Trait()); + armaments.AddRange( + enterer.TraitsImplementing() + .Where(a => Info.Armaments.Contains(a.Info.Name))); + } + + void OnActorExited(Actor exiter) + { + actors.Remove(exiter); + paxFacing.Remove(exiter); + paxPos.Remove(exiter); + paxRender.Remove(exiter); + armaments.RemoveAll(a => a.Actor == exiter); + } + + void INotifyGarrisonerEntered.OnGarrisonerEntered(Actor self, Actor garrisoner) + { + OnActorEntered(garrisoner); + } + + void INotifyGarrisonerExited.OnGarrisonerExited(Actor self, Actor garrisoner) + { + OnActorExited(garrisoner); + } + + void INotifyPassengerEntered.OnPassengerEntered(Actor self, Actor passenger) + { + OnActorEntered(passenger); + } + + void INotifyPassengerExited.OnPassengerExited(Actor self, Actor passenger) + { + OnActorExited(passenger); + } + + WVec SelectFirePort(Actor firer) + { + var passengerIndex = actors.IndexOf(firer); + if (passengerIndex == -1) + return new WVec(0, 0, 0); + + var portIndex = passengerIndex % Info.PortOffsets.Length; + + return Info.PortOffsets[portIndex]; + } + + WVec PortOffset(Actor self, WVec offset) + { + var bodyOrientation = coords.Value.QuantizeOrientation(self.Orientation); + return coords.Value.LocalToWorld(offset.Rotate(bodyOrientation)); + } + + public override void DoAttack(Actor self, in Target target) + { + if (!CanAttack(self, target)) + return; + + var pos = self.CenterPosition; + var targetedPosition = GetTargetPosition(pos, target); + var targetYaw = (targetedPosition - pos).Yaw; + + foreach (var a in Armaments) + { + if (a.IsTraitDisabled) + continue; + + var port = SelectFirePort(a.Actor); + + var muzzleFacing = targetYaw; + paxFacing[a.Actor].Facing = muzzleFacing; + paxPos[a.Actor].SetCenterPosition(a.Actor, pos + PortOffset(self, port)); + + if (!a.CheckFire(a.Actor, facing, target, true)) + continue; + + if (a.Info.MuzzleSequence != null) + { + // Muzzle facing is fixed once the firing starts + var muzzleAnim = new Animation(self.World, paxRender[a.Actor].GetImage(a.Actor), () => targetYaw); + var sequence = a.Info.MuzzleSequence; + var palette = a.Info.MuzzlePalette; + + var muzzleFlash = new AnimationWithOffset(muzzleAnim, + () => PortOffset(self, port), + () => false, + p => RenderUtils.ZOffsetFromCenter(self, p, 1024)); + + var pair = (muzzleFlash, palette); + muzzles.Add(pair); + muzzleAnim.PlayThen(sequence, () => muzzles.Remove(pair)); + } + + foreach (var npa in self.TraitsImplementing()) + npa.Attacking(self, target, a, null); + } + } + + IEnumerable IRender.Render(Actor self, WorldRenderer wr) + { + // Display muzzle flashes + foreach (var m in muzzles) + foreach (var r in m.Animation.Render(self, wr.Palette(m.Sequence))) + yield return r; + } + + IEnumerable IRender.ScreenBounds(Actor self, WorldRenderer wr) + { + // Muzzle flashes don't contribute to actor bounds + yield break; + } + + protected override void Tick(Actor self) + { + base.Tick(self); + + // Take a copy so that Tick() can remove animations + foreach (var m in muzzles.ToArray()) + m.Animation.Animation.Tick(); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Attack/AttackPrism.cs b/OpenRA.Mods.AS/Traits/Attack/AttackPrism.cs new file mode 100644 index 000000000000..7decaeaf0417 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Attack/AttackPrism.cs @@ -0,0 +1,183 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Activities; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Implements the charge-then-burst attack logic specific to the RA tesla coil.")] + public class AttackPrismInfo : AttackBaseInfo + { + [Desc("How many charges this actor has to attack with, once charged.")] + public readonly int MaxCharges = 1; + + [Desc("Reload time for all charges (in ticks).")] + public readonly int ReloadDelay = 120; + + [Desc("Delay for initial charge attack (in ticks).")] + public readonly int InitialChargeDelay = 22; + + [Desc("Delay between charge attacks (in ticks).")] + public readonly int ChargeDelay = 3; + + [Desc("Sound to play when actor charges.")] + public readonly string ChargeAudio = null; + + [Desc("Do the charge audio play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the sounds played at.")] + public readonly float SoundVolume = 1f; + + public override object Create(ActorInitializer init) { return new AttackPrism(init.Self, this); } + } + + public class AttackPrism : AttackBase, ITick, INotifyAttack + { + readonly AttackPrismInfo info; + + [Sync] + protected int charges; + + [Sync] + protected int timeToRecharge; + + public AttackPrism(Actor self, AttackPrismInfo info) + : base(self, info) + { + this.info = info; + charges = info.MaxCharges; + } + + void ITick.Tick(Actor self) + { + if (--timeToRecharge <= 0) + charges = info.MaxCharges; + } + + protected override bool CanAttack(Actor self, in Target target) + { + if (!IsReachableTarget(target, true)) + return false; + + return base.CanAttack(self, target); + } + + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + --charges; + timeToRecharge = info.ReloadDelay; + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + + public override Activity GetAttackActivity(Actor self, AttackSource source, in Target newTarget, bool allowMove, bool forceAttack, Color? targetLineColor = null) + { + return new ChargeAttack(this, newTarget, forceAttack, targetLineColor); + } + + class ChargeAttack : Activity, IActivityNotifyStanceChanged + { + readonly AttackPrism attack; + readonly Target target; + readonly bool forceAttack; + readonly Color? targetLineColor; + + public ChargeAttack(AttackPrism attack, in Target target, bool forceAttack, Color? targetLineColor = null) + { + this.attack = attack; + this.target = target; + this.forceAttack = forceAttack; + this.targetLineColor = targetLineColor; + } + + public override bool Tick(Actor self) + { + if (IsCanceling || !attack.CanAttack(self, target)) + return true; + + if (attack.charges == 0) + return false; + + foreach (var notify in self.TraitsImplementing()) + notify.Charging(self, target); + + if (!string.IsNullOrEmpty(attack.info.ChargeAudio)) + { + var pos = self.CenterPosition; + if (attack.info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, attack.info.ChargeAudio, pos, attack.info.SoundVolume); + } + + QueueChild(new Wait(attack.info.InitialChargeDelay)); + QueueChild(new ChargeFire(attack, target)); + return false; + } + + void IActivityNotifyStanceChanged.StanceChanged(Actor self, AutoTarget autoTarget, UnitStance oldStance, UnitStance newStance) + { + // Cancel non-forced targets when switching to a more restrictive stance if they are no longer valid for auto-targeting + if (newStance > oldStance || forceAttack) + return; + + if (target.Type == TargetType.Actor) + { + var a = target.Actor; + if (!autoTarget.HasValidTargetPriority(self, a.Owner, a.GetEnabledTargetTypes())) + Cancel(self, true); + } + else if (target.Type == TargetType.FrozenActor) + { + var fa = target.FrozenActor; + if (!autoTarget.HasValidTargetPriority(self, fa.Owner, fa.TargetTypes)) + Cancel(self, true); + } + } + + public override IEnumerable TargetLineNodes(Actor self) + { + if (targetLineColor != null) + yield return new TargetLineNode(target, targetLineColor.Value); + } + } + + protected class ChargeFire : Activity + { + readonly AttackPrism attack; + readonly Target target; + + public ChargeFire(AttackPrism attack, in Target target) + { + this.attack = attack; + this.target = target; + } + + public override bool Tick(Actor self) + { + if (IsCanceling || !attack.CanAttack(self, target)) + return true; + + if (attack.charges == 0) + return true; + + attack.DoAttack(self, target); + + QueueChild(new Wait(attack.info.ChargeDelay)); + return false; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Attack/AttackPrismSupported.cs b/OpenRA.Mods.AS/Traits/Attack/AttackPrismSupported.cs new file mode 100644 index 000000000000..519c0632195c --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Attack/AttackPrismSupported.cs @@ -0,0 +1,402 @@ +#region Copyright & License Information +/* + * OpenRA License: + * + * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +/* + * I tried implementing distance vector routing algorithm + * but now I think it is an overkill, + * because they take memory for each actor and they need to eachange information. + * Also, C&C games, people just sell towers when they are done with it so + * that makes these overheads less worthy. + */ + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Implements the charge-then-burst attack logic specific to the RA tesla coil.")] + public class AttackPrismSupportedInfo : AttackPrismInfo + { + [Desc("Max hops the supporters can reach.")] + public readonly int MaxHops = 3; + + [Desc("Ticks each hop will take for each hop of support weapon to jump across support network")] + public readonly int TicksPerHop = 1; + + [Desc("Max support an attacker can get.")] + public readonly int MaxSupportersPerAttacker = 9; + + [Desc("Can support allies too? Instead of just owners?")] + public readonly bool CanSupportAllies = false; + + [Desc("When supporting, what armament will this actor use?")] + public readonly string SupportArmament = "support"; + + [Desc("Only actors with this trait and the matching SupportType can buff each other.")] + public readonly string SupportType = "prism"; + + [Desc("When receiving the support, where in this actor will be the hit location?")] + public readonly WVec ReceiverOffset = WVec.Zero; + + [GrantedConditionReference] + [Desc("Condition stack to grant upon receiving the buffs.")] + public readonly string BuffCondition = "prism-stack"; + + public override object Create(ActorInitializer init) { return new AttackPrismSupported(init.Self, this); } + } + + public class AttackPrismSupported : AttackPrism, ITick, INotifyAttack, INotifyBecomingIdle + { + readonly AttackPrismSupportedInfo info; + readonly Stack buffTokens = new(); + + public AttackPrismSupported(Actor self, AttackPrismSupportedInfo info) + : base(self, info) + { + this.info = info; + } + + bool IsValidStance(Actor self, Actor otherNode) + { + if (self.Owner == otherNode.Owner) + return true; + + // It might be interesting if neutral actors support everybody hmmm... + return info.CanSupportAllies && self.Owner.IsAlliedWith(otherNode.Owner); + } + + // Check if self may support the receiver, + // where the receiver may be the one getting buffed or a relay node. + // We do all but range check here. + public bool MaySupport(Actor self, Actor receiver, bool selfMustBeIdle) + { + if (receiver.IsDead || !receiver.IsInWorld) + return false; + + if (!IsValidStance(self, receiver)) + return false; + + // I'm busy trying to do something else! + if (selfMustBeIdle && self.CurrentActivity != null) + return false; + + // Check traits + return info.SupportType == receiver.Trait().info.SupportType; + } + + void ITick.Tick(Actor self) + { + if (--timeToRecharge <= 0) + charges = info.MaxCharges; + } + + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + --charges; + timeToRecharge = info.ReloadDelay; + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + + public override Activity GetAttackActivity(Actor self, AttackSource source, in Target newTarget, bool allowMove, bool forceAttack, Color? targetLineColor = null) + { + return new ChargeSupportedAttack(this, newTarget, info, targetLineColor); + } + + public virtual void FireSupportArmament(Actor self, in Target target, Actor buffReceiver) + { + if (!CanAttack(self, target)) + return; + + var receiverTrait = buffReceiver.Trait(); + var offsetedTarget = Target.FromPos(target.CenterPosition + receiverTrait.info.ReceiverOffset); + + var supportArmament = self.TraitsImplementing().First(a => a.Info.Name == info.SupportArmament); + supportArmament.CheckFire(self, facing, offsetedTarget, true); + + // Grant the buff condition + receiverTrait.AddBuffStack(buffReceiver); + } + + // Check if self can reach target with the support armament. + public bool CheckSupportRange(Actor self, Actor target) + { + var t = Target.FromActor(target); + var supportArmament = self.TraitsImplementing().First(a => a.Info.Name == info.SupportArmament); + return t.IsInRange(self.CenterPosition, supportArmament.MaxRange()); + } + + void AddBuffStack(Actor self) + { + buffTokens.Push(self.GrantCondition(info.BuffCondition)); + } + + void ClearBuffStack(Actor self) + { + while (buffTokens.Count > 0) + self.RevokeCondition(buffTokens.Pop()); + } + + void INotifyBecomingIdle.OnBecomingIdle(Actor self) + { + ClearBuffStack(self); + } + + class ChargeSupportedAttack : Activity + { + readonly AttackPrismSupported attack; + readonly Target target; + /* readonly bool forceAttack; */ + readonly Color? targetLineColor; + readonly AttackPrismSupportedInfo supportInfo; + + public ChargeSupportedAttack( + AttackPrismSupported attack, + in Target target, + AttackPrismSupportedInfo supportInfo, + Color? targetLineColor = null) + { + this.attack = attack; + this.target = target; + this.supportInfo = supportInfo; + this.targetLineColor = targetLineColor; + } + + // Run BFS rooted at self, to get multi-hop supporters. + // Returns (supporter, relay, hops) triplets + public IEnumerable<(Actor Supporter, Actor Relay, int Hops)> RecruitSupporters(Actor self) + { + var candidates = self.World.ActorsHavingTrait(); + var isVisited = new HashSet() { self }; + var hops = new Dictionary() { { self, 0 } }; + var parent = new Dictionary() { { self, null } }; + var queue = new Queue(); + var maxHops = 0; + + queue.Enqueue(self); + while (queue.Count > 0) + { + var node = queue.Dequeue(); + foreach (var adjacent in GetValidNeighborSupporters(node, candidates)) + { + if (isVisited.Contains(adjacent)) + continue; + + isVisited.Add(adjacent); + hops[adjacent] = hops[node] + 1; + parent[adjacent] = node; + + if (maxHops < hops[adjacent]) + maxHops = hops[adjacent]; + + if (isVisited.Count > supportInfo.MaxSupportersPerAttacker) + { + queue.Clear(); // terminate the search + break; + } + + if (hops[adjacent] < supportInfo.MaxHops) + queue.Enqueue(adjacent); + } + } + + foreach (var node in isVisited) + { + if (node == self) + continue; + + yield return (node, parent[node], hops[node]); + } + } + + public static IEnumerable GetValidNeighborSupporters(Actor self, IEnumerable traitedActors) + { + // My guess is that is is more common to have more actors within our range + // than actors with this trait in the world. + foreach (var cand in traitedActors) + { + if (cand == self) + continue; + + var candAttack = cand.Trait(); + + if (!candAttack.MaySupport(cand, self, true)) + continue; + + // Implemented this way as the support may have different armament than self. + if (candAttack.CheckSupportRange(cand, self)) + yield return cand; + } + } + + public override bool Tick(Actor self) + { + if (IsCanceling || !attack.CanAttack(self, target)) + return true; + + if (attack.charges == 0) + return false; + + foreach (var notify in self.TraitsImplementing()) + notify.Charging(self, target); + + if (!string.IsNullOrEmpty(attack.info.ChargeAudio)) + { + var pos = self.CenterPosition; + if (attack.info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, attack.info.ChargeAudio, pos, attack.info.SoundVolume); + } + + var relays = RecruitSupporters(self); // You need to recruit every time you fire as the battlefield is a very dynamic place. + var maxHops = relays.Any() ? relays.MaxBy(x => x.Hops).Hops : 0; + + if (relays.Any()) + { + foreach ((var supporter, var relay, var hop) in relays) + { + var attack = supporter.Trait(); + if (!attack.MaySupport(supporter, relay, true)) + continue; + + supporter.QueueActivity(new FireSupportingWeapon(attack, Target.FromActor(relay), self, (maxHops - hop) * attack.info.TicksPerHop, Color.OrangeRed)); + } + } + + QueueChild(new Wait(attack.info.InitialChargeDelay + maxHops * attack.info.TicksPerHop)); + QueueChild(new ChargeFireBuffed(attack, target)); + return false; + } + + public override IEnumerable TargetLineNodes(Actor self) + { + if (targetLineColor != null) + yield return new TargetLineNode(target, targetLineColor.Value); + } + } + + class ChargeAndFireSupportWeapon : Activity + { + readonly AttackPrismSupported attack; + readonly Target target; + readonly Actor buffReceiver; + + public ChargeAndFireSupportWeapon(AttackPrismSupported attack, in Target target, Actor buffReceiver) + { + this.attack = attack; + this.target = target; + this.buffReceiver = buffReceiver; + } + + public override bool Tick(Actor self) + { + if (IsCanceling || !attack.CanAttack(self, target)) + return true; + + if (attack.charges == 0) + return true; + + if (!attack.MaySupport(self, buffReceiver, false)) + return true; + + if (buffReceiver.IsIdle) + return true; + + attack.FireSupportArmament(self, target, buffReceiver); + return false; + } + } + + class FireSupportingWeapon : Activity + { + readonly AttackPrismSupported attack; + readonly Target target; + readonly Actor buffReceiver; // buff receiver is NOT target, as the buff may travel multiple hops. + readonly int hopDelay; + readonly Color? targetLineColor; + + public FireSupportingWeapon(AttackPrismSupported attack, Target target, Actor buffReceiver, int hopDelay, Color? targetLineColor = null) + { + this.attack = attack; + this.target = target; + this.buffReceiver = buffReceiver; + this.hopDelay = hopDelay; + this.targetLineColor = targetLineColor; + } + + public override bool Tick(Actor self) + { + if (IsCanceling || !attack.CanAttack(self, target)) + return true; + + if (buffReceiver.IsIdle) + return true; + + if (attack.charges == 0) + return false; + + foreach (var notify in self.TraitsImplementing()) + notify.Charging(self, target); + + if (!string.IsNullOrEmpty(attack.info.ChargeAudio)) + { + var pos = self.CenterPosition; + if (attack.info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, attack.info.ChargeAudio, pos, attack.info.SoundVolume); + } + + QueueChild(new Wait(attack.info.InitialChargeDelay + hopDelay)); + QueueChild(new ChargeAndFireSupportWeapon(attack, target, buffReceiver)); + return true; // Works only once + } + + public override IEnumerable TargetLineNodes(Actor self) + { + if (targetLineColor != null) + yield return new TargetLineNode(target, targetLineColor.Value); + } + } + + protected class ChargeFireBuffed : Activity + { + readonly AttackPrismSupported attack; + readonly Target target; + + public ChargeFireBuffed(AttackPrismSupported attack, in Target target) + { + this.attack = attack; + this.target = target; + } + + public override bool Tick(Actor self) + { + if (IsCanceling || !attack.CanAttack(self, target)) + return true; + + if (attack.charges == 0) + { + return true; + } + + attack.DoAttack(self, target); + attack.ClearBuffStack(self); + QueueChild(new Wait(attack.info.ChargeDelay)); + return false; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BallisticMissile.cs b/OpenRA.Mods.AS/Traits/BallisticMissile.cs new file mode 100644 index 000000000000..e09f1ca29d4b --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BallisticMissile.cs @@ -0,0 +1,324 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This unit, when add to world, will fly in ballistic path then will detonate itself upon reaching target.")] + public class BallisticMissileInfo : TraitInfo, IMoveInfo, IPositionableInfo, IFacingInfo + { + [Desc("Pitch angle at which the actor will be created.")] + public readonly WAngle CreateAngle = WAngle.Zero; + [Desc("The time it takes for the actor to be created to launch.")] + public readonly int PrepareTick = 10; + [Desc("The altitude at which the actor begins to cruise.")] + public readonly WDist BeginCruiseAltitude = WDist.FromCells(7); + [Desc("Turn speed.")] + public readonly WAngle TurnSpeed = new(25); + [Desc("The actor starts hitting the target when the horizontal distance is less than this value.")] + public readonly WDist BeginHitRange = WDist.FromCells(4); + [Desc("If the actor is closer to the target than this value, it will explode.")] + public readonly WDist ExplosionRange = new(1536); + [Desc("The acceleration of the actor during the launch phase, the speed during the launch phase will not be more than the speed value.")] + public readonly WDist LaunchAcceleration = WDist.Zero; + [Desc("Unit acceleration during the strike, no upper limit for speed value.")] + public readonly WDist HitAcceleration = new(20); + [Desc("Simplify the trajectory into a parabola, the following values will have no effect:BeginCruiseAltitude, TurnSpeed, BeginHitRange, ExplosionRange, LaunchAcceleration, HitAcceleration")] + public readonly bool LazyCurve = false; + [Desc("Skip the cruise phase, BeginCruiseAltitude and BeginHitRange will no longer be valid, LaunchAngle is hard-coded to 256.")] + public readonly bool WithoutCruise = false; + + [Desc("Projectile speed in WDist / tick, two values indicate variable velocity.")] + public readonly WDist Speed = new(17); + + [Desc("In angle. Missile is launched at this pitch and the intial tangential line of the ballistic path will be this.")] + public readonly WAngle LaunchAngle = WAngle.Zero; + + [Desc("Minimum altitude where this missile is considered airborne")] + public readonly int MinAirborneAltitude = 5; + + [Desc("Types of damage missile explosion is triggered with. Leave empty for no damage types.")] + public readonly BitSet DamageTypes = default; + + [GrantedConditionReference] + [Desc("The condition to grant to self while airborne.")] + public readonly string AirborneCondition = null; + + [Desc("Sounds to play when the actor is taking off.")] + public readonly string[] LaunchSounds = Array.Empty(); + + [Desc("Do the launching sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the LaunchSounds played at.")] + public readonly float SoundVolume = 1f; + + public override object Create(ActorInitializer init) { return new BallisticMissile(init, this); } + + public IReadOnlyDictionary OccupiedCells(ActorInfo info, CPos location, SubCell subCell = SubCell.Any) { return new Dictionary(); } + bool IOccupySpaceInfo.SharesCell { get { return false; } } + public bool CanEnterCell(World world, Actor self, CPos cell, SubCell subCell = SubCell.FullCell, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All) + { + // SBMs may not land. + return false; + } + + // set by spawned logic, not this. + public WAngle GetInitialFacing() { return WAngle.Zero; } + public Color GetTargetLineColor() { return Color.Green; } + } + + public class BallisticMissile : ISync, IFacing, IMove, IPositionable, + INotifyCreated, INotifyAddedToWorld, INotifyRemovedFromWorld, IOccupySpace + { + static readonly (CPos Cell, SubCell SubCell)[] NoCells = Array.Empty<(CPos Cell, SubCell SubCell)>(); + + public readonly BallisticMissileInfo Info; + readonly Actor self; + public Target Target; + + IEnumerable speedModifiers; + + [Sync] + public WAngle Facing + { + get => Orientation.Yaw; + set => Orientation = Orientation.WithYaw(value); + } + + public WAngle Pitch + { + get => Orientation.Pitch; + set => Orientation = Orientation.WithPitch(value); + } + + public WAngle Roll + { + get => Orientation.Roll; + set => Orientation = Orientation.WithRoll(value); + } + + public WRot Orientation { get; private set; } + + [Sync] + public WPos CenterPosition { get; private set; } + + public CPos TopLeft { get { return self.World.Map.CellContaining(CenterPosition); } } + + bool airborne; + int airborneToken = Actor.InvalidConditionToken; + + public BallisticMissile(ActorInitializer init, BallisticMissileInfo info) + { + Info = info; + self = init.Self; + + var locationInit = init.GetOrDefault(info); + if (locationInit != null) + SetPosition(self, locationInit.Value); + + var centerPositionInit = init.GetOrDefault(info); + if (centerPositionInit != null) + SetPosition(self, centerPositionInit.Value); + + // I need facing but initial facing doesn't matter, they are determined by the spawner's facing. + Facing = init.GetValue(info, WAngle.Zero); + } + + // This kind of missile will not turn anyway. Hard-coding here. + public WAngle TurnSpeed => Info.TurnSpeed; + + void INotifyCreated.Created(Actor self) + { + speedModifiers = self.TraitsImplementing().ToArray().Select(sm => sm.GetSpeedModifier()); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + self.World.AddToMaps(self, this); + Pitch = Info.CreateAngle; + self.QueueActivity(new BallisticMissileFly(self, Target, this)); + + var altitude = self.World.Map.DistanceAboveTerrain(CenterPosition); + if (altitude.Length >= Info.MinAirborneAltitude) + OnAirborneAltitudeReached(); + } + + (CPos Cell, SubCell SubCell)[] IOccupySpace.OccupiedCells() + { + return NoCells; + } + + public int MovementSpeed + { + get { return Util.ApplyPercentageModifiers(Info.Speed.Length, speedModifiers); } + } + + public WVec FlyStep(WAngle facing) + { + return FlyStep(MovementSpeed, facing); + } + + public WVec FlyStep(int speed, WAngle facing) + { + var dir = new WVec(0, -1024, 0).Rotate(WRot.FromFacing(facing.Facing)); + return speed * dir / 1024; + } + + #region Implement IPositionable + + public bool CanExistInCell(CPos cell) { return true; } + public bool IsLeavingCell(CPos location, SubCell subCell = SubCell.Any) { return false; } // TODO: Handle landing + public bool CanEnterCell(CPos location, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All) { return true; } + public SubCell GetValidSubCell(SubCell preferred) { return SubCell.Invalid; } + public SubCell GetAvailableSubCell(CPos location, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, BlockedByActor check = BlockedByActor.All) + { + // Does not use any subcell + return SubCell.Invalid; + } + + public void SetCenterPosition(Actor self, WPos pos) { SetPosition(self, pos); } + + // Changes position, but not altitude + public void SetPosition(Actor self, CPos cell, SubCell subCell = SubCell.Any) + { + SetPosition(self, self.World.Map.CenterOfCell(cell) + new WVec(0, 0, CenterPosition.Z)); + } + + public void SetPosition(Actor self, WPos pos) + { + CenterPosition = pos; + + if (!self.IsInWorld) + return; + + self.World.UpdateMaps(self, this); + + var altitude = self.World.Map.DistanceAboveTerrain(CenterPosition); + var isAirborne = altitude.Length >= Info.MinAirborneAltitude; + if (isAirborne && !airborne) + OnAirborneAltitudeReached(); + else if (!isAirborne && airborne) + OnAirborneAltitudeLeft(); + } + + #endregion + + #region Implement IMove + + public Activity MoveTo(CPos cell, int nearEnough = 0, Actor ignoreActor = null, + bool evaluateNearestMovableCell = false, Color? targetLineColor = null) + { + return null; + } + + public Activity MoveWithinRange(in Target target, WDist range, + WPos? initialTargetPosition = null, Color? targetLineColor = null) + { + return null; + } + + public Activity MoveWithinRange(in Target target, WDist minRange, WDist maxRange, + WPos? initialTargetPosition = null, Color? targetLineColor = null) + { + return null; + } + + public Activity MoveFollow(Actor self, in Target target, WDist minRange, WDist maxRange, + WPos? initialTargetPosition = null, Color? targetLineColor = null) + { + return null; + } + + public Activity ReturnToCell(Actor self) + { + return null; + } + + public Activity MoveToTarget(Actor self, in Target target, + WPos? initialTargetPosition = null, Color? targetLineColor = null) + { + return null; + } + + public Activity MoveIntoTarget(Actor self, in Target target) + { + return null; + } + + public Activity MoveOntoTarget(Actor self, in Target target, in WVec offset, WAngle? facing, Color? targetLineColor = null) + { + return null; + } + + public Activity LocalMove(Actor self, WPos fromPos, WPos toPos) + { + return null; + } + + public int EstimatedMoveDuration(Actor self, WPos fromPos, WPos toPos) + { + var speed = MovementSpeed; + return speed > 0 ? (toPos - fromPos).Length / speed : 0; + } + + public CPos NearestMoveableCell(CPos cell) { return cell; } + + // Actors with BallisticMissile always move + public MovementType CurrentMovementTypes { get => MovementType.Horizontal | MovementType.Vertical; set { } } + + public bool CanEnterTargetNow(Actor self, in Target target) + { + // you can never control ballistic missiles anyway + return false; + } + + #endregion + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + self.World.RemoveFromMaps(self, this); + OnAirborneAltitudeLeft(); + } + + #region Airborne conditions + + void OnAirborneAltitudeReached() + { + if (airborne) + return; + + airborne = true; + if (!string.IsNullOrEmpty(Info.AirborneCondition) && airborneToken == Actor.InvalidConditionToken) + airborneToken = self.GrantCondition(Info.AirborneCondition); + } + + void OnAirborneAltitudeLeft() + { + if (!airborne) + return; + + airborne = false; + if (airborneToken != Actor.InvalidConditionToken) + airborneToken = self.RevokeCondition(airborneToken); + } + + #endregion + } +} diff --git a/OpenRA.Mods.AS/Traits/BaseSpawnerMaster.cs b/OpenRA.Mods.AS/Traits/BaseSpawnerMaster.cs new file mode 100644 index 000000000000..2604f1a7e625 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BaseSpawnerMaster.cs @@ -0,0 +1,289 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + // What to do when master is killed or mind controlled + public enum SpawnerSlaveDisposal + { + DoNothing, + KillSlaves, + GiveSlavesToAttacker + } + + public class BaseSpawnerSlaveEntry + { + public string ActorName = null; + public Actor Actor = null; + public BaseSpawnerSlave SpawnerSlave = null; + public bool IsLaunched; + public WVec Offset; + + public bool IsValid { get { return Actor != null && !Actor.IsDead; } } + } + + [Desc("This actor can spawn actors.")] + public class BaseSpawnerMasterInfo : PausableConditionalTraitInfo + { + [Desc("Spawn these units. Define this like paradrop support power.")] + public readonly string[] Actors = Array.Empty(); + + [Desc("Should this actor link to the actor who create them? This will pass master as the Parent Actor to slaves.")] + public readonly bool LinkToParent = true; + + [Desc("Place slave will be created.")] + public readonly WVec[] SpawnOffset = Array.Empty(); + + [Desc("Slave actors to contain upon creation. Set to -1 to start with full slaves.")] + public readonly int InitialActorCount = -1; + + [Desc("Name of the armaments that control the slaves to attack.")] + public readonly HashSet ArmamentNames = new(); + + [Desc("What happens to the slaves when the master is killed?")] + public readonly SpawnerSlaveDisposal SlaveDisposalOnKill = SpawnerSlaveDisposal.KillSlaves; + + [Desc("What happens to the slaves when the master is mind controlled?")] + public readonly SpawnerSlaveDisposal SlaveDisposalOnOwnerChange = SpawnerSlaveDisposal.GiveSlavesToAttacker; + + [Desc("Only spawn initial load of slaves?")] + public readonly bool NoRegeneration = false; + + [Desc("Spawn all slaves at once when regenerating slaves, instead of one by one?")] + public readonly bool SpawnAllAtOnce = false; + + [Desc("Spawn regen delay, in ticks")] + public readonly int RespawnTicks = 150; + + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + base.RulesetLoaded(rules, ai); + + if (Actors == null || Actors.Length == 0) + throw new YamlException($"Actors is null or empty for a spawner trait in actor type {ai.Name}!"); + + if (SpawnOffset.Length > Actors.Length) + throw new YamlException($"lenght of SpawnOffset can't be larger than the actors defined! (Actor type = {ai.Name})"); + + if (InitialActorCount > Actors.Length) + throw new YamlException($"InitialActorCount can't be larger than the actors defined! (Actor type = {ai.Name})"); + + if (InitialActorCount < -1) + throw new YamlException($"InitialActorCount must be -1 or non-negative. Actor type = {ai.Name}"); + } + + public override object Create(ActorInitializer init) { return new BaseSpawnerMaster(init, this); } + } + + public class BaseSpawnerMaster : PausableConditionalTrait, INotifyKilled, INotifyOwnerChanged, INotifyActorDisposing + { + readonly Actor self; + + IFacing facing; + + protected IReloadModifier[] reloadModifiers; + + public BaseSpawnerSlaveEntry[] SlaveEntries; + + public BaseSpawnerMaster(ActorInitializer init, BaseSpawnerMasterInfo info) + : base(info) + { + self = init.Self; + + // Initialize slave entries (doesn't instantiate the slaves yet) + SlaveEntries = CreateSlaveEntries(info); + + for (var i = 0; i < info.Actors.Length; i++) + { + var entry = SlaveEntries[i]; + entry.ActorName = info.Actors[i].ToLowerInvariant(); + } + + for (var i = 0; i < Info.SpawnOffset.Length; i++) + { + SlaveEntries[i].Offset = Info.SpawnOffset[i]; + } + } + + public virtual BaseSpawnerSlaveEntry[] CreateSlaveEntries(BaseSpawnerMasterInfo info) + { + var slaveEntries = new BaseSpawnerSlaveEntry[info.Actors.Length]; + + for (var i = 0; i < slaveEntries.Length; i++) + slaveEntries[i] = new BaseSpawnerSlaveEntry(); + + return slaveEntries; + } + + protected override void Created(Actor self) + { + base.Created(self); + + facing = self.TraitOrDefault(); + + reloadModifiers = self.TraitsImplementing().ToArray(); + } + + /// + /// Replenish destoyed slaves or create new ones from nothing. + /// Follows policy defined by Info.OneShotSpawn. + /// + public void Replenish(Actor self, BaseSpawnerSlaveEntry[] slaveEntries) + { + if (Info.SpawnAllAtOnce) + { + foreach (var se in slaveEntries) + if (!se.IsValid) + Replenish(self, se); + } + else + { + var entry = SelectEntryToSpawn(slaveEntries); + + // All are alive and well. + if (entry == null) + return; + + Replenish(self, entry); + } + } + + /// + /// Replenish one slave entry. + /// + public virtual void Replenish(Actor self, BaseSpawnerSlaveEntry entry) + { + if (entry.IsValid) + throw new InvalidOperationException("Replenish must not be run on a valid entry!"); + + var td = new TypeDictionary { new OwnerInit(self.Owner) }; + + if (Info.LinkToParent) + td.Add(new ParentActorInit(self)); + + // Some members are missing. Create a new one. + var slave = self.World.CreateActor(false, entry.ActorName, td); + + // Initialize slave entry + InitializeSlaveEntry(slave, entry); + entry.SpawnerSlave.LinkMaster(entry.Actor, self, this); + } + + /// + /// Slave entry initializer function. + /// Override this function from derived classes to initialize their own specific stuff. + /// + public virtual void InitializeSlaveEntry(Actor slave, BaseSpawnerSlaveEntry entry) + { + entry.Actor = slave; + entry.SpawnerSlave = slave.Trait(); + } + + protected BaseSpawnerSlaveEntry SelectEntryToSpawn(BaseSpawnerSlaveEntry[] slaveEntries) + { + // If any thing is marked dead or null, that's a candidate. + var candidates = slaveEntries.Where(m => !m.IsValid); + if (!candidates.Any()) + return null; + + return candidates.Random(self.World.SharedRandom); + } + + public virtual void OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + self.World.AddFrameEndTask(w => + { + foreach (var slaveEntry in SlaveEntries) + if (slaveEntry.IsValid) + slaveEntry.SpawnerSlave.OnMasterOwnerChanged(slaveEntry.Actor, oldOwner, newOwner, Info.SlaveDisposalOnOwnerChange); + }); + } + + void INotifyActorDisposing.Disposing(Actor self) + { + // Just dispose them regardless of slave disposal options. + foreach (var slaveEntry in SlaveEntries) + if (slaveEntry.IsValid) + slaveEntry.Actor.Dispose(); + } + + public virtual void SpawnIntoWorld(Actor self, Actor slave, WPos centerPosition) + { + var exit = self.RandomExitOrDefault(self.World, null); + SetSpawnedFacing(slave, exit); + + self.World.AddFrameEndTask(w => + { + if (self.IsDead) + return; + + var spawnOffset = exit == null ? WVec.Zero : exit.Info.SpawnOffset; + var positionable = slave.Trait(); + positionable.SetPosition(slave, centerPosition + spawnOffset.Rotate(self.Orientation)); + positionable.SetCenterPosition(slave, centerPosition + spawnOffset.Rotate(self.Orientation)); + + var location = self.World.Map.CellContaining(centerPosition + spawnOffset.Rotate(self.Orientation)); + + var mv = slave.Trait(); + slave.QueueActivity(mv.ReturnToCell(slave)); + + slave.QueueActivity(mv.MoveTo(location, 2)); + + w.Add(slave); + }); + } + + protected void SetSpawnedFacing(Actor spawned, Exit exit) + { + var facingOffset = facing == null ? WAngle.Zero : facing.Facing; + + var exitFacing = WAngle.Zero; + if (exit != null && exit.Info.Facing.HasValue) + exitFacing = exit.Info.Facing.Value; + + var spawnFacing = spawned.TraitOrDefault(); + if (spawnFacing != null) + spawnFacing.Facing = facingOffset + exitFacing; + } + + public void StopSlaves() + { + foreach (var slaveEntry in SlaveEntries) + { + if (!slaveEntry.IsValid) + continue; + + slaveEntry.SpawnerSlave.Stop(slaveEntry.Actor); + } + } + + public virtual void OnSlaveKilled(Actor self, Actor slave) { } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + Killed(self, e); + } + + protected virtual void Killed(Actor self, AttackInfo e) + { + // Notify slaves. + foreach (var slaveEntry in SlaveEntries) + if (slaveEntry.IsValid) + slaveEntry.SpawnerSlave.OnMasterKilled(slaveEntry.Actor, e.Attacker, Info.SlaveDisposalOnKill); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BaseSpawnerSlave.cs b/OpenRA.Mods.AS/Traits/BaseSpawnerSlave.cs new file mode 100644 index 000000000000..742d2f73b83f --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BaseSpawnerSlave.cs @@ -0,0 +1,191 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Can be slaved to a SpawnerMaster.")] + public class BaseSpawnerSlaveInfo : TraitInfo + { + [GrantedConditionReference] + [Desc("The condition to grant to slaves when the master actor is killed.")] + public readonly string MasterDeadCondition = null; + + [Desc("Can these actors be mind controlled or captured?")] + public readonly bool AllowOwnerChange = false; + + [Desc("Types of damage this actor explodes with due to an unallowed slave action. Leave empty for no damage types.")] + public readonly BitSet DamageTypes = default; + + public override object Create(ActorInitializer init) { return new BaseSpawnerSlave(this); } + } + + public class BaseSpawnerSlave : INotifyCreated, INotifyKilled, INotifyOwnerChanged + { + protected AttackBase[] attackBases; + + readonly BaseSpawnerSlaveInfo info; + + public bool HasFreeWill = false; + + BaseSpawnerMaster spawnerMaster = null; + + public Actor Master { get; private set; } + + // Make this actor attack a target. + Target lastTarget; + + public BaseSpawnerSlave(BaseSpawnerSlaveInfo info) + { + this.info = info; + } + + void INotifyCreated.Created(Actor self) + { + Created(self); + } + + protected virtual void Created(Actor self) + { + attackBases = self.TraitsImplementing().ToArray(); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (Master == null || Master.IsDead) + return; + + spawnerMaster.OnSlaveKilled(Master, self); + } + + public virtual void LinkMaster(Actor self, Actor master, BaseSpawnerMaster spawnerMaster) + { + Master = master; + this.spawnerMaster = spawnerMaster; + } + + static bool TargetSwitched(Target lastTarget, Target newTarget) + { + if (newTarget.Type != lastTarget.Type) + return true; + + if (newTarget.Type == TargetType.Terrain) + return newTarget.CenterPosition != lastTarget.CenterPosition; + + if (newTarget.Type == TargetType.Actor) + return lastTarget.Actor != newTarget.Actor; + + return false; + } + + // Stop what self was doing. + public virtual void Stop(Actor self) + { + // Drop the target so that Attack() feels the need to assign target for this slave. + lastTarget = Target.Invalid; + + self.CancelActivity(); + } + + public virtual void Attack(Actor self, Target target) + { + // Don't have to change target or alter current activity. + if (!TargetSwitched(lastTarget, target)) + return; + + if (!target.IsValidFor(self)) + { + Stop(self); + return; + } + + lastTarget = target; + + foreach (var ab in attackBases) + { + if (ab.IsTraitDisabled) + continue; + + ab.AttackTarget(target, AttackSource.Default, false, true, true); + } + } + + public virtual void OnMasterKilled(Actor self, Actor attacker, SpawnerSlaveDisposal disposal) + { + // Grant MasterDead condition. + if (!string.IsNullOrEmpty(info.MasterDeadCondition)) + self.GrantCondition(info.MasterDeadCondition); + + switch (disposal) + { + case SpawnerSlaveDisposal.KillSlaves: + self.Kill(attacker, info.DamageTypes); + break; + case SpawnerSlaveDisposal.GiveSlavesToAttacker: + self.CancelActivity(); + self.ChangeOwner(attacker.Owner); + break; + case SpawnerSlaveDisposal.DoNothing: + // fall through + default: + break; + } + } + + // What if the master gets mind controlled? + public virtual void OnMasterOwnerChanged(Actor self, Player oldOwner, Player newOwner, SpawnerSlaveDisposal disposal) + { + switch (disposal) + { + case SpawnerSlaveDisposal.KillSlaves: + self.Kill(self, info.DamageTypes); + break; + case SpawnerSlaveDisposal.GiveSlavesToAttacker: + self.CancelActivity(); + self.ChangeOwner(newOwner); + break; + case SpawnerSlaveDisposal.DoNothing: + // fall through + default: + break; + } + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + OnOwnerChanged(self, oldOwner, newOwner); + } + + // What if the slave gets mind controlled? + // Slaves aren't good without master so, kill it. + public virtual void OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + // In this case, the slave will be disposed, one way or other. + if (Master == null || !Master.IsDead) + return; + + // This function got triggered because the master got mind controlled and + // thus triggered slave.ChangeOwner(). + // In this case, do nothing. + if (Master.Owner == newOwner) + return; + + // These are independent, so why not let it be controlled? + if (info.AllowOwnerChange) + return; + + self.Kill(self, info.DamageTypes); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Berserkable.cs b/OpenRA.Mods.AS/Traits/Berserkable.cs new file mode 100644 index 000000000000..289e3b026e1f --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Berserkable.cs @@ -0,0 +1,131 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("When enabled, the actor will randomly try to attack nearby allied actors.")] + public class BerserkableInfo : ConditionalTraitInfo + { + [Desc("Do not attack this type of actors when berserked.")] + public readonly string[] ActorsToIgnore = Array.Empty(); + + public override object Create(ActorInitializer init) { return new Berserkable(this); } + } + + class Berserkable : ConditionalTrait, INotifyIdle, INotifyCreated + { + AttackBase[] attackBases; + AutoTarget[] autoTargets; + + public Berserkable(BerserkableInfo info) + : base(info) { } + + static void Blink(Actor self) + { + self.World.AddFrameEndTask(w => + { + if (self.IsInWorld) + { + var stop = new Order("Stop", self, false); + foreach (var t in self.TraitsImplementing()) + t.ResolveOrder(self, stop); + + w.Remove(self); + self.Generation++; + w.Add(self); + } + }); + } + + protected override void Created(Actor self) + { + attackBases = self.TraitsImplementing().ToArray(); + autoTargets = self.TraitsImplementing().ToArray(); + base.Created(self); + } + + protected override void TraitEnabled(Actor self) + { + // Getting enraged cancels current activity. + Blink(self); + } + + protected override void TraitDisabled(Actor self) + { + // Getting unraged should drop the target, too. + Blink(self); + } + + WDist GetScanRange(List atbs) + { + var range = WDist.Zero; + + // Get max value of autotarget scan range. + foreach (var at in autoTargets.Where(a => !a.IsTraitDisabled)) + { + var r = at.Info.ScanRadius; + if (r > range.Length) + range = WDist.FromCells(r); + } + + // Get maxrange weapon. + foreach (var atb in atbs) + { + var r = atb.GetMaximumRange(); + if (r.Length > range.Length) + range = r; + } + + return range; + } + + void INotifyIdle.TickIdle(Actor self) + { + if (IsTraitDisabled) + return; + + var atbs = attackBases.Where(a => !a.IsTraitDisabled && !a.IsTraitPaused).ToList(); + if (atbs.Count == 0) + { + self.QueueActivity(new Wait(15)); + return; + } + + var range = GetScanRange(atbs); + + var targets = self.World.FindActorsInCircle(self.CenterPosition, range) + .Where(a => a != self && a.IsTargetableBy(self) && !Info.ActorsToIgnore.Contains(a.Info.Name)).ToArray(); + + var preferredtargets = targets.Where(a => a.Owner.IsAlliedWith(self.Owner)); + + if (!preferredtargets.Any()) + { + preferredtargets = targets.Where(a => !a.Owner.IsAlliedWith(self.Owner)); + + if (!preferredtargets.Any()) + { + self.QueueActivity(new Wait(15)); + return; + } + } + + // Attack a random target. + var target = Target.FromActor(preferredtargets.Random(self.World.SharedRandom)); + self.QueueActivity(atbs.First().GetAttackActivity(self, AttackSource.Default, target, true, true)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/BevManagerBotModule.cs b/OpenRA.Mods.AS/Traits/BotModules/BevManagerBotModule.cs new file mode 100644 index 000000000000..0e3f6b190cc2 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/BevManagerBotModule.cs @@ -0,0 +1,179 @@ +#region Copyright & License Information +/* + * Copyright 2007-2019 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Manages AI Base Expansion Vehicles. Does not handle building.")] + public class BevManagerBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actor types that are considered construction yards (base centers).")] + public readonly HashSet ConstructionYardTypes = new(); + + [Desc("Actor types that are considered BEVs (deploy into base expansions).")] + public readonly HashSet BevTypes = new(); + + [Desc("Delay (in ticks) between looking for and giving out orders to new BEVs.")] + public readonly int ScanForNewBevInterval = 20; + + [Desc("Minimum distance in cells from center of the base when checking for BEV deployment location.")] + public readonly int MinBaseRadius = 2; + + [Desc("Maximum distance in cells from center of the base when checking for BEV deployment location.", + "Only applies if RestrictBevDeploymentFallbackToBase is enabled and there's at least one construction yard.")] + public readonly int MaxBaseRadius = 20; + + [Desc("Should deployment of additional BEVs be restricted to MaxBaseRadius if explicit deploy locations are missing or occupied?")] + public readonly bool RestrictBevDeploymentFallbackToBase = true; + + public override object Create(ActorInitializer init) { return new BevManagerBotModule(init.Self, this); } + } + + public class BevManagerBotModule : ConditionalTrait, IBotTick, IBotPositionsUpdated, IGameSaveTraitData + { + public CPos GetRandomBaseCenter() + { + var randomConstructionYard = world.Actors.Where(a => a.Owner == player && + Info.ConstructionYardTypes.Contains(a.Info.Name)) + .RandomOrDefault(world.LocalRandom); + + return randomConstructionYard != null ? randomConstructionYard.Location : initialBaseCenter; + } + + readonly World world; + readonly Player player; + + CPos initialBaseCenter; + int scanInterval; + bool firstTick = true; + + public BevManagerBotModule(Actor self, BevManagerBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + scanInterval = world.LocalRandom.Next(Info.ScanForNewBevInterval, Info.ScanForNewBevInterval * 2); + } + + void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) + { + initialBaseCenter = newLocation; + } + + void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } + + void IBotTick.BotTick(IBot bot) + { + if (firstTick) + { + DeployMcvs(bot, false); + firstTick = false; + } + + if (--scanInterval <= 0) + { + scanInterval = Info.ScanForNewBevInterval; + DeployMcvs(bot, true); + } + } + + void DeployMcvs(IBot bot, bool chooseLocation) + { + var newBEVs = world.ActorsHavingTrait() + .Where(a => a.Owner == player && a.IsIdle && Info.BevTypes.Contains(a.Info.Name)); + + foreach (var bev in newBEVs) + DeployBev(bot, bev, chooseLocation); + } + + // Find any BEV and deploy them at a sensible location. + void DeployBev(IBot bot, Actor bev, bool move) + { + if (move) + { + // If we lack a base, we need to make sure we don't restrict deployment of the MCV to the base! + var restrictToBase = Info.RestrictBevDeploymentFallbackToBase && AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) > 0; + + var transformsInfo = bev.Info.TraitInfo(); + var desiredLocation = ChooseBevDeployLocation(transformsInfo.IntoActor, transformsInfo.Offset, restrictToBase); + if (desiredLocation == null) + return; + + bot.QueueOrder(new Order("Move", bev, Target.FromCell(world, desiredLocation.Value), true)); + } + + bot.QueueOrder(new Order("DeployTransform", bev, true)); + } + + CPos? ChooseBevDeployLocation(string actorType, CVec offset, bool distanceToBaseIsImportant) + { + var actorInfo = world.Map.Rules.Actors[actorType]; + var bi = actorInfo.TraitInfoOrDefault(); + if (bi == null) + return null; + + // Find the buildable cell that is closest to pos and centered around center + var baseCenter = GetRandomBaseCenter(); + + return ((Func)((center, target, minRange, maxRange) => + { + var cells = world.Map.FindTilesInAnnulus(center, minRange, maxRange); + + // Sort by distance to target if we have one + if (center != target) + cells = cells.OrderBy(c => (c - target).LengthSquared); + else + cells = cells.Shuffle(world.LocalRandom); + + foreach (var cell in cells) + if (world.CanPlaceBuilding(cell + offset, actorInfo, bi, null)) + return cell; + + return null; + }))(baseCenter, baseCenter, Info.MinBaseRadius, + distanceToBaseIsImportant ? Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange); + } + + List IGameSaveTraitData.IssueTraitData(Actor self) + { + if (IsTraitDisabled) + return null; + + return new List() + { + new MiniYamlNode("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)) + }; + } + + void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) + { + if (self.World.IsReplay) + return; + + var nodes = data.ToDictionary(); + + if (nodes.TryGetValue("InitialBaseCenter", out var initialBaseCenterNode)) + initialBaseCenter = FieldLoader.GetValue("InitialBaseCenter", initialBaseCenterNode.Value); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/BotModuleBinding/IssueOrderToBot.cs b/OpenRA.Mods.AS/Traits/BotModules/BotModuleBinding/IssueOrderToBot.cs new file mode 100644 index 000000000000..a1ba8f000264 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/BotModuleBinding/IssueOrderToBot.cs @@ -0,0 +1,143 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Flags] + public enum OrderTriggers + { + None = 0, + Attack = 1, + Damage = 2, + Heal = 4, + Periodically = 8, + BecomingIdle = 16 + } + + [Desc("Allow this actor to automatically issue orders to bot player, and processed by " + nameof(ExternalBotOrdersManager) + ". Only support order without target")] + public class IssueOrderToBotInfo : ConditionalTraitInfo + { + [Desc("Events leading to the actor issue order. Possible values are: None, Attack, Damage, Heal, Periodically, BecomingIdle.")] + public readonly OrderTriggers OrderTrigger = OrderTriggers.Attack | OrderTriggers.Damage; + + [FieldLoader.Require] + [Desc("Order name to issue.")] + public readonly string OrderName = null; + + [Desc("Second order name to issue.")] + public readonly string SecondOrderName = null; + + [Desc("Chance of the order take effect.")] + public readonly int OrderChance = 50; + + [Desc("Chance of the second order take effect.")] + public readonly int SecondOrderChance = 50; + + [Desc("Delay between two successful issued orders.")] + public readonly int OrderInterval = 2500; + + [Desc("Delay to issue second order after a first order.", + "Note: if set > 0, next issued order will be OrderInterval + SecondOrderDelay")] + public readonly int SecondOrderDelay = -1; + + public override object Create(ActorInitializer init) { return new IssueOrderToBot(this); } + } + + public class IssueOrderToBot : ConditionalTrait, INotifyAttack, ITick, INotifyDamage, INotifyCreated, ISync, INotifyOwnerChanged, INotifyBecomingIdle + { + int secondOrderTicks = -1, firstOrderTicks; + ExternalBotOrdersManager orderManager; + + public IssueOrderToBot(IssueOrderToBotInfo info) + : base(info) { } + + protected override void Created(Actor self) + { + orderManager = self.Owner.PlayerActor.Trait(); + } + + void TryIssueFirstOrder(Actor self) + { + if (!orderManager.ManagerRunning || firstOrderTicks > 0 || orderManager.IsTraitDisabled) + return; + + orderManager.AddEntry(self, Info.OrderName, Info.OrderChance); + + firstOrderTicks = Info.OrderInterval; + secondOrderTicks = Info.SecondOrderDelay; + } + + void TryIssueSecondOrder(Actor self) + { + if (!orderManager.ManagerRunning || orderManager.IsTraitDisabled) + return; + + orderManager.AddEntry(self, Info.SecondOrderName, Info.SecondOrderChance); + } + + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + if (!orderManager.ManagerRunning || IsTraitDisabled || orderManager.IsTraitDisabled) + return; + + if (Info.OrderTrigger.HasFlag(OrderTriggers.Attack)) + TryIssueFirstOrder(self); + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + + void ITick.Tick(Actor self) + { + if (!orderManager.ManagerRunning || IsTraitDisabled || orderManager.IsTraitDisabled) + return; + + if (Info.SecondOrderDelay > -1) + { + if (--secondOrderTicks < 0) + TryIssueSecondOrder(self); + + return; + } + + if (--firstOrderTicks < 0 && Info.OrderTrigger.HasFlag(OrderTriggers.Periodically)) + TryIssueFirstOrder(self); + } + + void INotifyDamage.Damaged(Actor self, AttackInfo e) + { + if (!orderManager.ManagerRunning || IsTraitDisabled || orderManager.IsTraitDisabled) + return; + + if (e.Damage.Value > 0 && Info.OrderTrigger.HasFlag(OrderTriggers.Damage)) + TryIssueFirstOrder(self); + + if (e.Damage.Value < 0 && Info.OrderTrigger.HasFlag(OrderTriggers.Heal)) + TryIssueFirstOrder(self); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + orderManager = newOwner.PlayerActor.Trait(); + } + + void INotifyBecomingIdle.OnBecomingIdle(Actor self) + { + if (!orderManager.ManagerRunning || IsTraitDisabled || orderManager.IsTraitDisabled) + return; + + if (Info.OrderTrigger.HasFlag(OrderTriggers.BecomingIdle)) + TryIssueFirstOrder(self); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/BotModuleLogic/SupportPowerDecisionAS.cs b/OpenRA.Mods.AS/Traits/BotModules/BotModuleLogic/SupportPowerDecisionAS.cs new file mode 100644 index 000000000000..0ee525db5eaa --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/BotModuleLogic/SupportPowerDecisionAS.cs @@ -0,0 +1,197 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Adds metadata for the AI bots.")] + public class SupportPowerDecisionAS + { + [Desc("What is the minimum attractiveness we will use this power for?")] + public readonly int MinimumAttractiveness = 1; + + [Desc("What support power does this decision apply to?")] + public readonly string OrderName = "AirstrikePowerInfoOrder"; + + [FieldLoader.LoadUsing(nameof(LoadConsiderations))] + [Desc("The decisions associated with this power")] + public readonly List Considerations = new(); + + [Desc("Against whom should this power be used?", "Allowed keywords: Ally, Neutral, Enemy")] + public readonly PlayerRelationship Against = PlayerRelationship.Enemy; + + [Desc("What types should the desired targets of this power be?")] + public readonly BitSet Types = new("Air", "Ground", "Water"); + + [Desc("Should the AI ignore visibility rules during activation?")] + public readonly bool IgnoreVisibility = false; + + [Desc("Minimum ticks to wait until next Decision scan attempt.")] + public readonly int MinimumScanTimeInterval = 250; + + [Desc("Maximum ticks to wait until next Decision scan attempt.")] + public readonly int MaximumScanTimeInterval = 262; + + public SupportPowerDecisionAS(MiniYaml yaml) + { + FieldLoader.Load(this, yaml); + } + + static object LoadConsiderations(MiniYaml yaml) + { + var ret = new List(); + foreach (var d in yaml.Nodes) + if (d.Key.Split('@')[0] == "Consideration") + ret.Add(new Consideration(d.Value)); + + return ret; + } + + /// Evaluates the attractiveness of a position according to all considerations. + public int GetAttractiveness(WPos pos, Player firedBy) + { + var answer = 0; + var world = firedBy.World; + var targetTile = world.Map.CellContaining(pos); + + if (!world.Map.Contains(targetTile)) + return 0; + + foreach (var consideration in Considerations) + { + var radiusToUse = new WDist(consideration.CheckRadius.Length); + + var checkActors = world.FindActorsInCircle(pos, radiusToUse); + foreach (var scrutinized in checkActors) + answer += consideration.GetAttractiveness(scrutinized, firedBy.RelationshipWith(scrutinized.Owner), firedBy, IgnoreVisibility); + + var delta = new WVec(radiusToUse, radiusToUse, WDist.Zero); + var tl = world.Map.CellContaining(pos - delta); + var br = world.Map.CellContaining(pos + delta); + var checkFrozen = firedBy.FrozenActorLayer.FrozenActorsInRegion(new CellRegion(world.Map.Grid.Type, tl, br)); + + // IsValid check filters out Frozen Actors that have not initizialized their Owner + foreach (var scrutinized in checkFrozen) + answer += consideration.GetAttractiveness(scrutinized, firedBy.RelationshipWith(scrutinized.Owner), firedBy); + } + + return answer; + } + + public int GetAttractiveness(IEnumerable frozenActors, Player firedBy) + { + var answer = 0; + + foreach (var consideration in Considerations) + foreach (var scrutinized in frozenActors) + if (scrutinized.IsValid && scrutinized.Visible) + answer += consideration.GetAttractiveness(scrutinized, firedBy.RelationshipWith(scrutinized.Owner), firedBy); + + return answer; + } + + public int GetNextScanTime(World world) { return world.LocalRandom.Next(MinimumScanTimeInterval, MaximumScanTimeInterval); } + + /// Makes up part of a decision, describing how to evaluate a target. + public class Consideration + { + public enum DecisionMetric { Health, Value, None } + + [Desc("Against whom should this power be used?", "Allowed keywords: Ally, Neutral, Enemy")] + public readonly PlayerRelationship Against = PlayerRelationship.Enemy; + + [Desc("What types should the desired targets of this power be?")] + public readonly BitSet Types = new("Air", "Ground", "Water"); + + [Desc("How attractive are these types of targets?")] + public readonly int Attractiveness = 100; + + [Desc("Weight the target attractiveness by this property", "Allowed keywords: Health, Value, None")] + public readonly DecisionMetric TargetMetric = DecisionMetric.None; + + [Desc("What is the check radius of this decision?")] + public readonly WDist CheckRadius = WDist.FromCells(5); + + public Consideration(MiniYaml yaml) + { + FieldLoader.Load(this, yaml); + } + + /// Evaluates a single actor according to the rules defined in this consideration. + public int GetAttractiveness(Actor a, PlayerRelationship relationship, Player firedBy, bool ignoreVisibility) + { + if (relationship != Against) + return 0; + + if (a == null) + return 0; + + if (!ignoreVisibility && (!a.IsTargetableBy(firedBy.PlayerActor) || !a.CanBeViewedByPlayer(firedBy))) + return 0; + + if (Types.Overlaps(a.GetEnabledTargetTypes())) + { + switch (TargetMetric) + { + case DecisionMetric.Value: + var valueInfo = a.Info.TraitInfoOrDefault(); + return (valueInfo != null) ? valueInfo.Cost * Attractiveness : 0; + + case DecisionMetric.Health: + var health = a.TraitOrDefault(); + + if (health == null) + return 0; + + // Cast to long to avoid overflow when multiplying by the health + return (int)((long)health.HP * Attractiveness / health.MaxHP); + + default: + return Attractiveness; + } + } + + return 0; + } + + public int GetAttractiveness(FrozenActor fa, PlayerRelationship relationship, Player firedBy) + { + if (relationship != Against) + return 0; + + if (fa == null || !fa.IsValid || !fa.Visible) + return 0; + + if (Types.Overlaps(fa.TargetTypes)) + { + switch (TargetMetric) + { + case DecisionMetric.Value: + var valueInfo = fa.Info.TraitInfoOrDefault(); + return (valueInfo != null) ? valueInfo.Cost * Attractiveness : 0; + + case DecisionMetric.Health: + var healthInfo = fa.Info.TraitInfoOrDefault(); + return (healthInfo != null) ? fa.HP * Attractiveness / healthInfo.MaxHP : 0; + + default: + return Attractiveness; + } + } + + return 0; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/BotModuleLogic/UnitAttackOptions.cs b/OpenRA.Mods.AS/Traits/BotModules/BotModuleLogic/UnitAttackOptions.cs new file mode 100644 index 000000000000..fc174931b6b7 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/BotModuleLogic/UnitAttackOptions.cs @@ -0,0 +1,53 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; + +namespace OpenRA.Mods.AS.Traits +{ + [Flags] + public enum AttackRequires + { + None = 0, + CargoLoaded = 1, + Disguised = 2, + } + + [Desc("Adds metadata for the " + nameof(SendUnitToAttackBotModule) + ".")] + public sealed class UnitAttackOptions + { + [Desc("Base desire provided for attack desire for each of this unit.", + "When desire reach 100, AI will send them to attack")] + public readonly int AttackDesireOfEach = 20; + + [Desc("Order used for closing in the target before attack. Left empty for attack directly.")] + public readonly string MoveToOrderName = null; + + [Desc("Attack order name. Used for actor to attack target.")] + public readonly string AttackOrderName = "Attack"; + + [Desc("Order used for moving the unit back to where it was after attack. Left empty for no return.")] + public readonly string MoveBackOrderName = null; + + [Desc("Disguise before attack, if possible.")] + public readonly bool TryDisguise = false; + + [Desc("Repaired before attack, if possible.")] + public readonly bool TryGetHealed = true; + + [Desc("Filters units don't meet the requirements. Possible values are None, CargoLoaded, Disguised.")] + public readonly AttackRequires AttackRequires = AttackRequires.None; + + public UnitAttackOptions(MiniYaml yaml) + { + FieldLoader.Load(this, yaml); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/CaptureManagerBotASModule.cs b/OpenRA.Mods.AS/Traits/BotModules/CaptureManagerBotASModule.cs new file mode 100644 index 000000000000..60ea536f792b --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/CaptureManagerBotASModule.cs @@ -0,0 +1,246 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Manages AI capturing logic.")] + public class CaptureManagerBotASModuleInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [Desc("Actor types that can capture other actors (via `Captures`).")] + public readonly HashSet CapturingActorTypes = new(); + + [Desc("Percentage chance of trying a priority capture.")] + public readonly int PriorityCaptureChance = 75; + + [Desc("Actor types that should be priorizited to be captured.", + "Leave this empty to include all actors.")] + public readonly HashSet PriorityCapturableActorTypes = new(); + + [Desc("Actor types that can be targeted for capturing.", + "Leave this empty to include all actors.")] + public readonly HashSet CapturableActorTypes = new(); + + [Desc("Minimum delay (in ticks) between trying to capture with CapturingActorTypes.")] + public readonly int MinimumCaptureDelay = 375; + + [Desc("Maximum number of options to consider for capturing.", + "If a value less than 1 is given 1 will be used instead.")] + public readonly int MaximumCaptureTargetOptions = 10; + + [Desc("Should visibility (Shroud, Fog, Cloak, etc) be considered when searching for capturable targets?")] + public readonly bool CheckCaptureTargetsForVisibility = true; + + [Desc("Player stances that capturers should attempt to target.")] + public readonly PlayerRelationship CapturableRelationships = PlayerRelationship.Enemy | PlayerRelationship.Neutral; + + public override object Create(ActorInitializer init) { return new CaptureManagerBotASModule(init.Self, this); } + } + + public class CaptureManagerBotASModule : ConditionalTrait, IBotTick, IBotPositionsUpdated, IGameSaveTraitData + { + readonly World world; + readonly Player player; + readonly Func isEnemyUnit; + readonly int maximumCaptureTargetOptions; + + int minCaptureDelayTicks; + CPos initialBaseCenter; + + public CaptureManagerBotASModule(Actor self, CaptureManagerBotASModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + + if (world.Type == WorldType.Editor) + return; + + isEnemyUnit = unit => + player.RelationshipWith(unit.Owner) == PlayerRelationship.Enemy + && !unit.Info.HasTraitInfo() + && unit.Info.HasTraitInfo(); + + maximumCaptureTargetOptions = Math.Max(1, Info.MaximumCaptureTargetOptions); + } + + void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) + { + initialBaseCenter = newLocation; + } + + void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + minCaptureDelayTicks = world.LocalRandom.Next(Info.MinimumCaptureDelay); + } + + void IBotTick.BotTick(IBot bot) + { + if (--minCaptureDelayTicks <= 0) + { + minCaptureDelayTicks = Info.MinimumCaptureDelay; + QueueCaptureOrders(bot); + } + } + + internal Actor FindClosestEnemy(WPos pos) + { + return world.Actors.Where(isEnemyUnit).ClosestToIgnoringPath(pos); + } + + internal Actor FindClosestEnemy(WPos pos, WDist radius) + { + return world.FindActorsInCircle(pos, radius).Where(isEnemyUnit).ClosestToIgnoringPath(pos); + } + + IEnumerable GetVisibleActorsBelongingToPlayer(Player owner) + { + foreach (var actor in GetActorsThatCanBeOrderedByPlayer(owner)) + if (actor.CanBeViewedByPlayer(player)) + yield return actor; + } + + IEnumerable GetActorsThatCanBeOrderedByPlayer(Player owner) + { + foreach (var actor in world.Actors) + if (actor.Owner == owner && !actor.IsDead && actor.IsInWorld) + yield return actor; + } + + void QueueCaptureOrders(IBot bot) + { + if (player.WinState != WinState.Undefined) + return; + + var newUnits = world.ActorsHavingTrait() + .Where(a => a.Owner == player && !a.IsDead && a.IsInWorld); + + if (!newUnits.Any()) + return; + + var capturers = newUnits + .Where(a => a.IsIdle && Info.CapturingActorTypes.Contains(a.Info.Name)) + .Select(a => new TraitPair(a, a.TraitOrDefault())) + .Where(tp => tp.Trait != null); + + if (!capturers.Any()) + return; + + var baseCenter = world.Map.CenterOfCell(initialBaseCenter); + + if (world.LocalRandom.Next(100) < Info.PriorityCaptureChance) + { + var priorityTargets = world.Actors.Where(a => + !a.IsDead && a.IsInWorld && Info.CapturableRelationships.HasRelationship(player.RelationshipWith(a.Owner)) + && Info.PriorityCapturableActorTypes.Contains(a.Info.Name.ToLowerInvariant())); + + if (Info.CheckCaptureTargetsForVisibility) + priorityTargets = priorityTargets.Where(a => a.CanBeViewedByPlayer(player)); + + if (priorityTargets.Any()) + { + priorityTargets = priorityTargets.OrderBy(a => (a.CenterPosition - baseCenter).LengthSquared); + + var priorityCaptures = Math.Min(capturers.Count(), priorityTargets.Count()); + + for (var i = 0; i < priorityCaptures; i++) + { + var capturer = capturers.First(); + var priorityTarget = priorityTargets.First(); + + var captureManager = priorityTarget.TraitOrDefault(); + if (captureManager != null && capturer.Trait.CanTarget(captureManager)) + { + bot.QueueOrder(new Order("CaptureActor", capturer.Actor, Target.FromActor(priorityTarget), true)); + AIUtils.BotDebug("AI ({0}): Ordered {1} {2} to capture {3} {4} in priority mode.", + player.ClientIndex, capturer.Actor, capturer.Actor.ActorID, priorityTarget, priorityTarget.ActorID); + + capturers = capturers.Skip(1); + } + + priorityTargets = priorityTargets.Skip(1); + } + } + + if (!capturers.Any()) + return; + } + + var randPlayer = world.Players.Where(p => !p.Spectating + && Info.CapturableRelationships.HasRelationship(player.RelationshipWith(p))).Random(world.LocalRandom); + + var targetOptions = Info.CheckCaptureTargetsForVisibility + ? GetVisibleActorsBelongingToPlayer(randPlayer) + : GetActorsThatCanBeOrderedByPlayer(randPlayer); + + var capturableTargetOptions = targetOptions + .Where(target => + { + var captureManager = target.TraitOrDefault(); + if (captureManager == null) + return false; + + return capturers.Any(tp => tp.Trait.CanTarget(captureManager)); + }) + .OrderBy(target => (target.CenterPosition - baseCenter).LengthSquared) + .Take(maximumCaptureTargetOptions); + + if (Info.CapturableActorTypes.Any()) + capturableTargetOptions = capturableTargetOptions.Where(target => Info.CapturableActorTypes.Contains(target.Info.Name.ToLowerInvariant())); + + if (!capturableTargetOptions.Any()) + return; + + foreach (var capturer in capturers) + { + var targetActor = capturableTargetOptions.MinByOrDefault(target => (target.CenterPosition - capturer.Actor.CenterPosition).LengthSquared); + if (targetActor == null) + continue; + + bot.QueueOrder(new Order("CaptureActor", capturer.Actor, Target.FromActor(targetActor), true)); + AIUtils.BotDebug("AI ({0}): Ordered {1} {2} to capture {3} {4}.", + player.ClientIndex, capturer.Actor, capturer.Actor.ActorID, targetActor, targetActor.ActorID); + } + } + + List IGameSaveTraitData.IssueTraitData(Actor self) + { + if (IsTraitDisabled) + return null; + + return new List() + { + new MiniYamlNode("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)) + }; + } + + void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) + { + if (self.World.IsReplay) + return; + + var nodes = data.ToDictionary(); + + if (nodes.TryGetValue("InitialBaseCenter", out var initialBaseCenterNode)) + initialBaseCenter = FieldLoader.GetValue("InitialBaseCenter", initialBaseCenterNode.Value); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/CncEngineerManagerBotModule.cs b/OpenRA.Mods.AS/Traits/BotModules/CncEngineerManagerBotModule.cs new file mode 100644 index 000000000000..6a3a73923b18 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/CncEngineerManagerBotModule.cs @@ -0,0 +1,281 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Flags] + enum EngineerAction { CaptureActor = 1, RepairBase = 2, RepairBridge = 3 } + + [TraitLocation(SystemActors.Player)] + [Desc("Manages AI traditional cnc engineer logic. Only consider closest target.", + "Check only one of the enabled logics (capture, repair building and repair bridge) per tick.")] + public sealed class CncEngineerBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actor types that used for engineer.", + "Leave this empty to disable this bot module.")] + public readonly HashSet EngineerActorTypes = new(); + + [Desc("Actor types that can be targeted for capturing (via `Captures`).", + "Leave this empty to disable capture check.")] + public readonly HashSet CapturableActorTypes = new(); + + [Desc("Player relationships that capturers should attempt to target.")] + public readonly PlayerRelationship CapturableRelationships = PlayerRelationship.Enemy | PlayerRelationship.Neutral; + + [Desc("Actor types that can be targeted for engineer repairing (via `InstantlyRepairs`).", + "Leave this empty to disable repair building check.")] + public readonly HashSet RepairableActorTypes = new(); + + [Desc("Engineer repair actor when at this damage state.")] + public readonly DamageState RepairableDamageState = DamageState.Heavy; + + [Desc("Actor types that can be targeted for bridge repairing (via `RepairsBridges`).", + "Leave this empty to disable repair bridge check.")] + public readonly HashSet RepairableHutActorTypes = new(); + + [Desc("Minimum delay (in ticks) between trying to giving out order for engineer.")] + public readonly int AssignRoleDelay = 120; + + public override object Create(ActorInitializer init) { return new CncEngineerManagerBotModule(init.Self, this); } + } + + public class CncEngineerManagerBotModule : ConditionalTrait, IBotTick + { + readonly World world; + readonly Player player; + readonly Predicate unitCannotBeOrderedOrIsIdle; + readonly Predicate unitCannotBeOrderedOrIsBusy; + readonly Predicate unitCannotBeOrdered; + + // Units that the bot already knows about and has given a capture order. Any unit not on this list needs to be given a new order. + readonly List activeEngineers = new(); + readonly List stuckEngineers = new(); + readonly EngineerAction[] enabledEngineerActions = Array.Empty(); + int minAssignRoleDelayTicks; + int currentAction; + + public CncEngineerManagerBotModule(Actor self, CncEngineerBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + + if (world.Type == WorldType.Editor) + return; + + unitCannotBeOrdered = a => a == null || a.Owner != player || a.IsDead || !a.IsInWorld; + unitCannotBeOrderedOrIsIdle = a => unitCannotBeOrdered(a) || a.IsIdle; + unitCannotBeOrderedOrIsBusy = a => unitCannotBeOrdered(a) || !(a.IsIdle || a.CurrentActivity is FlyIdle); + + var engineerActions = new List(); + if (info.CapturableActorTypes.Count > 0) + engineerActions.Add(EngineerAction.CaptureActor); + if (info.RepairableActorTypes.Count > 0) + engineerActions.Add(EngineerAction.RepairBase); + if (info.RepairableHutActorTypes.Count > 0) + engineerActions.Add(EngineerAction.RepairBridge); + + enabledEngineerActions = engineerActions.ToArray(); + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + minAssignRoleDelayTicks = world.LocalRandom.Next(0, Info.AssignRoleDelay); + } + + void IBotTick.BotTick(IBot bot) + { + if (--minAssignRoleDelayTicks <= 0) + { + minAssignRoleDelayTicks = Info.AssignRoleDelay; + + activeEngineers.RemoveAll(u => unitCannotBeOrderedOrIsIdle(u.Actor)); + stuckEngineers.RemoveAll(a => unitCannotBeOrdered(a)); + for (var i = 0; i < activeEngineers.Count; i++) + { + var engineer = activeEngineers[i]; + if (engineer.Actor.CurrentActivity.ChildActivity != null && engineer.Actor.CurrentActivity.ChildActivity.ActivityType == ActivityType.Move && engineer.Actor.CenterPosition == engineer.WPos) + { + stuckEngineers.Add(engineer.Actor); + bot.QueueOrder(new Order("Stop", engineer.Actor, false)); + activeEngineers.RemoveAt(i); + i--; + } + + engineer.WPos = engineer.Actor.CenterPosition; + } + + switch (enabledEngineerActions[currentAction]) + { + case EngineerAction.RepairBase: + QueueRepairBuildingOrders(bot); + break; + case EngineerAction.CaptureActor: + QueueCaptureOrders(bot); + break; + case EngineerAction.RepairBridge: + QueueRepairBridgeOrders(bot); + break; + } + + currentAction = (currentAction + 1) % enabledEngineerActions.Length; + } + } + + void QueueCaptureOrders(IBot bot) + { + if (Info.EngineerActorTypes.Count == 0 || player.WinState != WinState.Undefined) + return; + + var capturers = world.ActorsHavingTrait() + .Where(a => Info.EngineerActorTypes.Contains(a.Info.Name) && a.Owner == player && !unitCannotBeOrderedOrIsBusy(a) && !stuckEngineers.Contains(a)) + .Select(a => new TraitPair(a, a.TraitOrDefault())) + .Where(tp => tp.Trait != null) + .ToArray(); + + if (capturers.Length == 0) + return; + + var targets = world.ActorsHavingTrait().Where(a => Info.CapturableActorTypes.Contains(a.Info.Name)).ToArray(); + if (targets.Length == 0) + return; + + var capturerSent = false; + foreach (var capturer in capturers) + { + foreach (var target in targets.OrderBy(a => (capturer.Actor.Location - a.Location).LengthSquared)) + { + var captureManager = target.TraitOrDefault(); + if (captureManager == null) + continue; + + if (!capturer.Trait.CanTarget(captureManager)) + continue; + + if (!AIUtils.PathExist(capturer.Actor, target.Location, target)) + continue; + + bot.QueueOrder(new Order("CaptureActor", capturer.Actor, Target.FromActor(target), true)); + AIUtils.BotDebug("AI ({0}): Ordered {1} to capture {2}", player.ClientIndex, capturer.Actor, target); + activeEngineers.Add(new UnitWposWrapper(capturer.Actor)); + capturerSent = true; + break; + } + + if (capturerSent) + break; + } + } + + void QueueRepairBuildingOrders(IBot bot) + { + if (Info.EngineerActorTypes.Count == 0 || player.WinState != WinState.Undefined) + return; + + var repairers = world.ActorsHavingTrait() + .Where(a => Info.EngineerActorTypes.Contains(a.Info.Name) && a.Owner == player && !unitCannotBeOrderedOrIsBusy(a) && !stuckEngineers.Contains(a)) + .ToArray(); + + if (repairers.Length == 0) + return; + + var targets = world.ActorsHavingTrait().Where(target => + { + if (!Info.RepairableActorTypes.Contains(target.Info.Name)) + return false; + + if (target.Owner.RelationshipWith(player) != PlayerRelationship.Ally) + return false; + + var health = target.TraitOrDefault(); + + if (health == null || health.DamageState < Info.RepairableDamageState) + return false; + + return true; + }).ToArray(); + + if (targets.Length == 0) + return; + + var repairerSent = false; + foreach (var r in repairers) + { + foreach (var target in targets.OrderBy(a => (r.Location - a.Location).LengthSquared)) + { + if (!AIUtils.PathExist(r, target.Location, target)) + continue; + + bot.QueueOrder(new Order("InstantRepair", r, Target.FromActor(target), true)); + AIUtils.BotDebug("AI ({0}): Ordered {1} to Repair {2}", player.ClientIndex, r, target); + activeEngineers.Add(new UnitWposWrapper(r)); + repairerSent = true; + break; + } + + if (repairerSent) + break; + } + } + + void QueueRepairBridgeOrders(IBot bot) + { + if (Info.EngineerActorTypes.Count == 0 || player.WinState != WinState.Undefined) + return; + + var brigdeRepairers = world.ActorsHavingTrait() + .Where(a => Info.EngineerActorTypes.Contains(a.Info.Name) && a.Owner == player && !unitCannotBeOrderedOrIsBusy(a) && !stuckEngineers.Contains(a)) + .ToArray(); + + if (brigdeRepairers.Length == 0) + return; + + // There is not many bridge actors in the map, we can use List here. + var targets = world.ActorsWithTrait().Where(at => Info.RepairableHutActorTypes.Contains(at.Actor.Info.Name) + && at.Trait.BridgeDamageState >= DamageState.Dead).Select(at => at.Actor).ToList(); + + targets.AddRange(world.ActorsWithTrait().Where(at => Info.RepairableHutActorTypes.Contains(at.Actor.Info.Name) + && at.Trait.BridgeDamageState >= DamageState.Dead).Select(at => at.Actor)); + + if (targets.Count == 0) + return; + + var repairerSent = false; + foreach (var r in brigdeRepairers) + { + foreach (var target in targets.OrderBy(a => (r.Location - a.Location).LengthSquared)) + { + if (!AIUtils.PathExist(r, target.Location, target)) + continue; + + bot.QueueOrder(new Order("RepairBridge", r, Target.FromActor(target), true)); + AIUtils.BotDebug("AI ({0}): Ordered {1} to repair bridge hut {2}", player.ClientIndex, r, target); + activeEngineers.Add(new UnitWposWrapper(r)); + repairerSent = true; + break; + } + + if (repairerSent) + break; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/ExternalBotOrdersManager.cs b/OpenRA.Mods.AS/Traits/BotModules/ExternalBotOrdersManager.cs new file mode 100644 index 000000000000..04f9adbfa367 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/ExternalBotOrdersManager.cs @@ -0,0 +1,64 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Allows the player to issue the orders from actors with " + nameof(IssueOrderToBot) + ".")] + public class ExternalBotOrdersManagerInfo : ConditionalTraitInfo + { + public override object Create(ActorInitializer init) { return new ExternalBotOrdersManager(init.Self, this); } + } + + public class ExternalBotOrdersManager : ConditionalTrait, IBotTick + { + readonly List<(Actor Actor, string Order, int Chance)> entries = new(); + readonly World world; + + public bool ManagerRunning { get; private set; } + + public ExternalBotOrdersManager(Actor self, ExternalBotOrdersManagerInfo info) + : base(info) + { + world = self.World; + ManagerRunning = false; + } + + public void AddEntry(Actor issuer, string order, int chance) + { + entries.Add(new(issuer, order, chance)); + } + + void IBotTick.BotTick(IBot bot) + { + // "ManagerRunning = true" means IBotTick is running, and the game is + // 1. not a replay + // 2. not saved game still loading + // 3. the game running on the host where AI is enabled + ManagerRunning = true; + + foreach (var entry in entries) + { + if (entry.Actor.IsDead || !entry.Actor.IsInWorld || entry.Actor.Owner != bot.Player) + continue; + + if (world.LocalRandom.Next(100) > entry.Chance) + continue; + + bot.QueueOrder(new(entry.Order, entry.Actor, false)); + } + + entries.Clear(); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/LoadCargoBotModule.cs b/OpenRA.Mods.AS/Traits/BotModules/LoadCargoBotModule.cs new file mode 100644 index 000000000000..2b89cc297931 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/LoadCargoBotModule.cs @@ -0,0 +1,178 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Flags] + public enum LoadRequirement + { + All = 0, + IdleUnit = 1, + } + + [Flags] + public enum TransportOwner + { + Self = 0, + AlliedBot = 1, + Allies = 2 + } + + [TraitLocation(SystemActors.Player)] + [Desc("Manages AI load unit related with " + nameof(Cargo) + " and " + nameof(Passenger) + " traits.")] + public class LoadCargoBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actor types that can be targeted for load, must have " + nameof(Cargo) + ".", + "The flag represents if this transport only requires idle unit. Possible values are: All, IdleUnit")] + public readonly Dictionary TransportTypesAndLoadRequirement = default; + + [FieldLoader.Require] + [Desc("Actor types that used for loading, must have " + nameof(Passenger) + ".")] + public readonly HashSet PassengerTypes = default; + + [Desc("The type of relationship that can be targeted for load. Possible values are: Self, AlliedBot and Allies", + "AlliedBot means AI will load transport for another allied bot player.")] + public readonly TransportOwner ValidTransportOwner = TransportOwner.Self; + + [Desc("Scan suitable actors and target in this interval.")] + public readonly int ScanTick = 317; + + [Desc("Don't load passengers to this actor if damage state is worse than this.")] + public readonly DamageState ValidDamageState = DamageState.Heavy; + + [Desc("Don't load passengers that are further than this distance to this actor.")] + public readonly WDist MaxDistance = WDist.FromCells(20); + + public override object Create(ActorInitializer init) { return new LoadCargoBotModule(init.Self, this); } + } + + public class LoadCargoBotModule : ConditionalTrait, IBotTick + { + readonly World world; + readonly Player player; + readonly Predicate unitCannotBeOrdered; + readonly Predicate unitCannotBeOrderedOrIsBusy; + readonly Predicate unitCannotBeOrderedOrIsIdle; + readonly Predicate invalidTransport; + + readonly List activePassengers = new(); + readonly List stuckPassengers = new(); + int minAssignRoleDelayTicks; + + public LoadCargoBotModule(Actor self, LoadCargoBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + switch (info.ValidTransportOwner) + { + case TransportOwner.Self: + invalidTransport = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player; + break; + case TransportOwner.AlliedBot: + invalidTransport = a => a == null || a.IsDead || !a.IsInWorld || !a.Owner.IsBot || a.Owner.RelationshipWith(player) != PlayerRelationship.Ally; + break; + case TransportOwner.Allies: + invalidTransport = a => a == null || a.IsDead || !a.IsInWorld || a.Owner.RelationshipWith(player) != PlayerRelationship.Ally; + break; + } + + unitCannotBeOrdered = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player; + unitCannotBeOrderedOrIsBusy = a => unitCannotBeOrdered(a) || !(a.IsIdle || a.CurrentActivity is FlyIdle); + unitCannotBeOrderedOrIsIdle = a => unitCannotBeOrdered(a) || a.IsIdle || a.CurrentActivity is FlyIdle; + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + minAssignRoleDelayTicks = world.LocalRandom.Next(0, Info.ScanTick); + } + + void IBotTick.BotTick(IBot bot) + { + if (--minAssignRoleDelayTicks <= 0) + { + minAssignRoleDelayTicks = Info.ScanTick; + + activePassengers.RemoveAll(u => unitCannotBeOrderedOrIsIdle(u.Actor)); + stuckPassengers.RemoveAll(a => unitCannotBeOrdered(a)); + for (var i = 0; i < activePassengers.Count; i++) + { + var p = activePassengers[i]; + if (p.Actor.CurrentActivity.ChildActivity != null && p.Actor.CurrentActivity.ChildActivity.ActivityType == ActivityType.Move && p.Actor.CenterPosition == p.WPos) + { + stuckPassengers.Add(p.Actor); + bot.QueueOrder(new Order("Stop", p.Actor, false)); + activePassengers.RemoveAt(i); + i--; + } + + p.WPos = p.Actor.CenterPosition; + } + + var tcs = world.ActorsWithTrait().Where( + at => + { + var health = at.Actor.TraitOrDefault()?.DamageState; + return Info.TransportTypesAndLoadRequirement.ContainsKey(at.Actor.Info.Name) && !invalidTransport(at.Actor) + && !at.Trait.IsTraitDisabled && at.Trait.HasSpace(1) && (health == null || health < Info.ValidDamageState); + }).ToList(); + + if (tcs.Count == 0) + return; + + var tc = tcs.Random(world.LocalRandom); + var cargo = tc.Trait; + var transport = tc.Actor; + var spaceTaken = 0; + + Predicate invalidPassenger; + if (Info.TransportTypesAndLoadRequirement[transport.Info.Name] == LoadRequirement.IdleUnit) + invalidPassenger = unitCannotBeOrderedOrIsBusy; + else + invalidPassenger = unitCannotBeOrdered; + + var passengers = world.ActorsWithTrait().Where(at => Info.PassengerTypes.Contains(at.Actor.Info.Name) && !invalidPassenger(at.Actor) && cargo.HasSpace(at.Trait.Info.Weight) && (at.Actor.CenterPosition - transport.CenterPosition).HorizontalLengthSquared <= Info.MaxDistance.LengthSquared) + .OrderBy(at => (at.Actor.CenterPosition - transport.CenterPosition).HorizontalLengthSquared); + + var orderedActors = new List(); + + foreach (var p in passengers) + { + if (!AIUtils.PathExist(p.Actor, transport.Location, transport)) + continue; + + if (cargo.HasSpace(spaceTaken + p.Trait.Info.Weight)) + { + spaceTaken += p.Trait.Info.Weight; + orderedActors.Add(p.Actor); + activePassengers.Add(new UnitWposWrapper(p.Actor)); + } + + if (!cargo.HasSpace(spaceTaken + 1)) + break; + } + + if (orderedActors.Count > 0) + bot.QueueOrder(new Order("EnterTransport", null, Target.FromActor(transport), false, groupedActors: orderedActors.ToArray())); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/LoadGarrisonerBotModule.cs b/OpenRA.Mods.AS/Traits/BotModules/LoadGarrisonerBotModule.cs new file mode 100644 index 000000000000..c78b02f72bdb --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/LoadGarrisonerBotModule.cs @@ -0,0 +1,144 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA2.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Manages AI load unit related with " + nameof(Garrisonable) + " and " + nameof(Garrisoner) + " traits.")] + public class LoadGarrisonerBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actor types that can be targeted for load, must have " + nameof(Garrisonable) + ".", + "Leave this empty to include all actors.")] + public readonly HashSet GarrisonableUnit = null; + + [Desc("Actor types that used for loading, must have " + nameof(Passenger) + ".", + "Leave this empty to include all actors.")] + public readonly HashSet GarrisonerUnit = null; + + [Desc("Scan suitable actors and target in this interval.")] + public readonly int ScanTick = 457; + + [Desc("Don't load Garrisoner to this actor if damage state is worse than this.")] + public readonly DamageState ValidDamageState = DamageState.Heavy; + + [Desc("Load passengers max to this amount per scan.")] + public readonly int PassengersPerScan = 2; + + public override object Create(ActorInitializer init) { return new LoadGarrisonerBotModule(init.Self, this); } + } + + public class LoadGarrisonerBotModule : ConditionalTrait, IBotTick + { + readonly World world; + readonly Player player; + readonly Predicate unitCannotBeOrdered; + readonly Predicate unitCannotBeOrderedOrIsBusy; + readonly Predicate unitCannotBeOrderedOrIsIdle; + readonly Predicate invalidTransport; + + readonly List activeGarrisoner = new(); + readonly List stuckGarrisoner = new(); + int minAssignRoleDelayTicks; + + public LoadGarrisonerBotModule(Actor self, LoadGarrisonerBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + invalidTransport = a => a == null || a.IsDead || !a.IsInWorld || (a.Owner.RelationshipWith(player) != PlayerRelationship.Neutral && a.Owner != player); + unitCannotBeOrdered = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player; + unitCannotBeOrderedOrIsBusy = a => unitCannotBeOrdered(a) || !(a.IsIdle || a.CurrentActivity is FlyIdle); + unitCannotBeOrderedOrIsIdle = a => unitCannotBeOrdered(a) || a.IsIdle || a.CurrentActivity is FlyIdle; + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + minAssignRoleDelayTicks = world.LocalRandom.Next(0, Info.ScanTick); + } + + void IBotTick.BotTick(IBot bot) + { + if (--minAssignRoleDelayTicks <= 0) + { + minAssignRoleDelayTicks = Info.ScanTick; + + activeGarrisoner.RemoveAll(u => unitCannotBeOrderedOrIsIdle(u.Actor)); + stuckGarrisoner.RemoveAll(a => unitCannotBeOrdered(a)); + for (var i = 0; i < activeGarrisoner.Count; i++) + { + var p = activeGarrisoner[i]; + if (p.Actor.CurrentActivity.ChildActivity != null && p.Actor.CurrentActivity.ChildActivity.ActivityType == ActivityType.Move && p.Actor.CenterPosition == p.WPos) + { + stuckGarrisoner.Add(p.Actor); + bot.QueueOrder(new Order("Stop", p.Actor, false)); + activeGarrisoner.RemoveAt(i); + i--; + } + + p.WPos = p.Actor.CenterPosition; + } + + var tcs = world.ActorsWithTrait().Where( + at => + { + var health = at.Actor.TraitOrDefault()?.DamageState; + return (Info.GarrisonableUnit == null || Info.GarrisonableUnit.Contains(at.Actor.Info.Name)) && !invalidTransport(at.Actor) + && at.Trait.HasSpace(1) && (health == null || health < Info.ValidDamageState); + }).ToArray(); + + if (tcs.Length == 0) + return; + + var tc = tcs.Random(world.LocalRandom); + var garrisonable = tc.Trait; + var transport = tc.Actor; + var spaceTaken = 0; + + var garrisoner = world.ActorsWithTrait().Where(at => !unitCannotBeOrderedOrIsBusy(at.Actor) && (Info.GarrisonerUnit == null || Info.GarrisonerUnit.Contains(at.Actor.Info.Name)) && !stuckGarrisoner.Contains(at.Actor) && garrisonable.HasSpace(at.Trait.Info.Weight)) + .OrderBy(at => (at.Actor.CenterPosition - transport.CenterPosition).HorizontalLengthSquared); + + var orderedActors = new List(); + + var passengerCount = 0; + foreach (var g in garrisoner) + { + if (!AIUtils.PathExist(g.Actor, transport.Location, transport)) + continue; + + if (garrisonable.HasSpace(spaceTaken + g.Trait.Info.Weight)) + { + spaceTaken += g.Trait.Info.Weight; + orderedActors.Add(g.Actor); + activeGarrisoner.Add(new UnitWposWrapper(g.Actor)); + passengerCount++; + } + + if (!garrisonable.HasSpace(spaceTaken + 1) || passengerCount >= Info.PassengersPerScan) + break; + } + + if (orderedActors.Count > 0) + bot.QueueOrder(new Order("EnterGarrison", null, Target.FromActor(transport), false, groupedActors: orderedActors.ToArray())); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/McvManagerASBotModule.cs b/OpenRA.Mods.AS/Traits/BotModules/McvManagerASBotModule.cs new file mode 100644 index 000000000000..4ca50451135b --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/McvManagerASBotModule.cs @@ -0,0 +1,302 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Manages AI MCVs For SP. Focus on aircraft MCV, MCV stuck problem and MCV build control")] + public class McvManagerASBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actor types that are considered MCVs (deploy into base builders).")] + public readonly HashSet McvTypes = new(); + + [Desc("Actor types that are considered construction yards (base builders).")] + public readonly HashSet ConstructionYardTypes = new(); + + [Desc("Actor types that are able to produce MCVs.")] + public readonly HashSet McvFactoryTypes = new(); + + [Desc("Try to maintain at least this many ConstructionYardTypes, build an MCV if number is below this.", + "Increased by AddtionalConstructionYardCount after AddtionalConstructionYardInterval, max to MaxmiumConstructionYardCount")] + public readonly int MinimumConstructionYardCount = 1; + + [Desc("See description of MinimumConstructionYardCount")] + public readonly int AddtionalConstructionYardInterval = 8000; + + [Desc("See description of MinimumConstructionYardCount")] + public readonly int AddtionalConstructionYardCount = 1; + + [Desc("See description of MinimumConstructionYardCount")] + public readonly int MaxmiumConstructionYardCount = 1; + + [Desc("Delay (in ticks) between looking for and giving out orders to new MCVs.")] + public readonly int ScanForNewMcvInterval = 20; + + [Desc("Minimum distance in cells from center of the base when checking for MCV deployment location.")] + public readonly int MinBaseRadius = 2; + + [Desc("Maximum distance in cells from center of the base when checking for MCV deployment location.", + "Only applies if RestrictMCVDeploymentFallbackToBase is enabled and there's at least one construction yard.")] + public readonly int MaxBaseRadius = 20; + + [Desc("Should deployment of additional MCVs be restricted to MaxBaseRadius if explicit deploy locations are missing or occupied?")] + public readonly bool RestrictMCVDeploymentFallbackToBase = true; + + public override object Create(ActorInitializer init) { return new McvManagerASBotModule(init.Self, this); } + } + + public class McvManagerASBotModule : ConditionalTrait, IBotTick, IBotPositionsUpdated, IGameSaveTraitData + { + public CPos GetRandomBaseCenter() + { + var randomConstructionYard = world.Actors.Where(a => a.Owner == player && + Info.ConstructionYardTypes.Contains(a.Info.Name)) + .RandomOrDefault(world.LocalRandom); + + return randomConstructionYard?.Location ?? initialBaseCenter; + } + + readonly World world; + readonly Player player; + + readonly Predicate unitCannotBeOrdered; + + IBotPositionsUpdated[] notifyPositionsUpdated; + IBotRequestUnitProduction[] requestUnitProduction; + readonly List activeMCV = new(); + + CPos initialBaseCenter; + int scanInterval; + bool firstTick = true; + int baseShouldHave; + int countdown; + + public McvManagerASBotModule(Actor self, McvManagerASBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + unitCannotBeOrdered = a => a == null || a.Owner != player || a.IsDead || !a.IsInWorld; + baseShouldHave = info.MinimumConstructionYardCount; + countdown = info.AddtionalConstructionYardInterval; + } + + protected override void Created(Actor self) + { + notifyPositionsUpdated = self.Owner.PlayerActor.TraitsImplementing().ToArray(); + requestUnitProduction = self.Owner.PlayerActor.TraitsImplementing().ToArray(); + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + scanInterval = world.LocalRandom.Next(Info.ScanForNewMcvInterval, Info.ScanForNewMcvInterval * 2); + } + + void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) + { + initialBaseCenter = newLocation; + } + + void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } + + void IBotTick.BotTick(IBot bot) + { + if (--countdown <= 0) + { + countdown = Info.AddtionalConstructionYardInterval; + baseShouldHave += Info.AddtionalConstructionYardCount; + + if (baseShouldHave >= Info.MaxmiumConstructionYardCount) + { + baseShouldHave = Info.MaxmiumConstructionYardCount; + countdown = int.MaxValue; + } + } + + if (firstTick) + { + DeployMcvsFirstTick(bot); + firstTick = false; + } + + if (--scanInterval <= 0) + { + scanInterval = Info.ScanForNewMcvInterval; + DeployMcvs(bot, true); + + // No construction yards - Build a new MCV + if (ShouldBuildMCV()) + { + var unitBuilder = requestUnitProduction.FirstOrDefault(Exts.IsTraitEnabled); + if (unitBuilder != null) + { + var mcvInfo = AIUtils.GetInfoByCommonName(Info.McvTypes, player); + if (unitBuilder.RequestedProductionCount(bot, mcvInfo.Name) == 0) + unitBuilder.RequestUnitProduction(bot, mcvInfo.Name); + } + } + } + } + + bool ShouldBuildMCV() + { + // Only build MCV if we don't already have one in the field. + var allowedToBuildMCV = AIUtils.CountActorByCommonName(Info.McvTypes, player) == 0; + if (!allowedToBuildMCV) + return false; + + // Build MCV if we don't have the desired number of construction yards, unless we have no factory (can't build it). + return AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) < baseShouldHave && + AIUtils.CountBuildingByCommonName(Info.McvFactoryTypes, player) > 0; + } + + void DeployMcvsFirstTick(IBot bot) + { + var newMCVs = world.ActorsHavingTrait() + .Where(a => Info.McvTypes.Contains(a.Info.Name) && !unitCannotBeOrdered(a)); + + foreach (var mcv in newMCVs) + DeployMcv(bot, mcv, false, false); + } + + void DeployMcvs(IBot bot, bool chooseLocation) + { + for (var i = 0; i < activeMCV.Count; i++) + { + var mw = activeMCV[i]; + + if (unitCannotBeOrdered(mw.Actor)) + { + activeMCV.RemoveAt(i); + i--; + continue; + } + + if (mw.WPos == mw.Actor.CenterPosition) + DeployMcv(bot, mw.Actor, chooseLocation, false); + + mw.WPos = mw.Actor.CenterPosition; + } + + var newMCVs = world.ActorsHavingTrait() + .Where(a => Info.McvTypes.Contains(a.Info.Name) && !unitCannotBeOrdered(a)); + + foreach (var mcv in newMCVs) + { + var skip = false; + + foreach (var amcv in activeMCV) + { + if (mcv == amcv.Actor) + { + skip = true; + break; + } + } + + if (!skip) + activeMCV.Add(new UnitWposWrapper(mcv)); + } + } + + // Find any MCV and deploy them at a sensible location. + void DeployMcv(IBot bot, Actor mcv, bool move, bool queueOrder) + { + if (move) + { + // If we lack a base, we need to make sure we don't restrict deployment of the MCV to the base! + var restrictToBase = Info.RestrictMCVDeploymentFallbackToBase && AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) > 0; + + var transformsInfo = mcv.Info.TraitInfo(); + var desiredLocation = ChooseMcvDeployLocation(transformsInfo.IntoActor, transformsInfo.Offset, restrictToBase); + if (desiredLocation == null) + return; + + bot.QueueOrder(new Order("Move", mcv, Target.FromCell(world, desiredLocation.Value), queueOrder)); + } + + // If the MCV has to move first, we can't be sure it reaches the destination alive, so we only + // update base and defense center if the MCV is deployed immediately (i.e. at game start). + // TODO: This could be addressed via INotifyTransform. + foreach (var n in notifyPositionsUpdated) + { + n.UpdatedBaseCenter(mcv.Location); + n.UpdatedDefenseCenter(mcv.Location); + } + + bot.QueueOrder(new Order("DeployTransform", mcv, true)); + } + + CPos? ChooseMcvDeployLocation(string actorType, CVec offset, bool distanceToBaseIsImportant) + { + var actorInfo = world.Map.Rules.Actors[actorType]; + var bi = actorInfo.TraitInfoOrDefault(); + if (bi == null) + return null; + + // Find the buildable cell that is closest to pos and centered around center + var baseCenter = GetRandomBaseCenter(); + return ((Func)((center, target, minRange, maxRange) => + { + var cells = world.Map.FindTilesInAnnulus(center, minRange, maxRange); + + // Sort by distance to target if we have one + if (center != target) + cells = cells.OrderBy(c => (c - target).LengthSquared); + else + cells = cells.Shuffle(world.LocalRandom); + + foreach (var cell in cells) + if (world.CanPlaceBuilding(cell + offset, actorInfo, bi, null)) + return cell; + + return null; + }))(baseCenter, baseCenter, Info.MinBaseRadius, + distanceToBaseIsImportant ? Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange); + } + + List IGameSaveTraitData.IssueTraitData(Actor self) + { + if (IsTraitDisabled) + return null; + + return new List() + { + new MiniYamlNode("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)), + new MiniYamlNode("BaseShouldHave", FieldSaver.FormatValue(baseShouldHave)), + new MiniYamlNode("Countdown", FieldSaver.FormatValue(countdown)), + }; + } + + void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) + { + if (self.World.IsReplay) + return; + + var nodes = data.ToDictionary(); + + if (nodes.TryGetValue("InitialBaseCenter", out var initialBaseCenterNode)) + initialBaseCenter = FieldLoader.GetValue("InitialBaseCenter", initialBaseCenterNode.Value); + + if (nodes.TryGetValue("BaseShouldHave", out var baseShouldHaveNode)) + baseShouldHave = FieldLoader.GetValue("BaseShouldHave", baseShouldHaveNode.Value); + + if (nodes.TryGetValue("Countdown", out var countdownNode)) + countdown = FieldLoader.GetValue("Countdown", countdownNode.Value); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/PlugSpawnerBotModule.cs b/OpenRA.Mods.AS/Traits/BotModules/PlugSpawnerBotModule.cs new file mode 100644 index 000000000000..51e592978917 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/PlugSpawnerBotModule.cs @@ -0,0 +1,142 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Allows the AI to have a single plug type.", "Plugs are all spawned.", "Use multiple variants of this trait to support more kind.")] + public class PlugSpawnerBotModuleInfo : ConditionalTraitInfo + { + [ActorReference(typeof(PlugInfo))] + [FieldLoader.Require] + [Desc("What plug the AI can spawn.")] + public readonly string Plug = null; + + [ActorReference(typeof(PluggableInfo))] + [FieldLoader.Require] + [Desc("What actors the AI can spawn this plug on.")] + public readonly HashSet Pluggables = new() { }; + + [Desc("Plug spawning interval.")] + public readonly int Interval = 50; + + public override object Create(ActorInitializer init) { return new PlugSpawnerBotModule(init.Self, this); } + } + + public class PlugSpawnerBotModule : ConditionalTrait, IBotTick, IResolveOrder, INotifyCreated + { + readonly World world; + + string plugType; + int ticks; + + public PlugSpawnerBotModule(Actor self, PlugSpawnerBotModuleInfo info) + : base(info) + { + world = self.World; + ticks = Info.Interval; + } + + protected override void Created(Actor self) + { + plugType = world.Map.Rules.Actors[Info.Plug].TraitInfo().Type; + + base.Created(self); + } + + void IBotTick.BotTick(IBot bot) + { + if (--ticks > 0) + return; + + var player = bot.Player; + + var targetActors = world.Actors.Where(x => x.IsInWorld && !x.IsDead && x.Owner == player && Info.Pluggables.Contains(x.Info.Name)); + + if (!targetActors.Any()) + return; + + var target = targetActors + .Select(x => (x, x.TraitsImplementing().FirstOrDefault(p => p.AcceptsPlug(plugType)))) + .FirstOrDefault(x => x.Item2 != null); + + if (target.x != null) + { + var order = new Order("PlacePlugAI", player.PlayerActor, Target.FromActor(target.x), false) + { + TargetString = Info.Plug, + ExtraData = player.PlayerActor.ActorID, + SuppressVisualFeedback = true + }; + + world.IssueOrder(order); + } + + ticks = Info.Interval; + } + + void IResolveOrder.ResolveOrder(Actor self, Order order) + { + if (IsTraitDisabled) + return; + + var os = order.OrderString; + if (os != "PlacePlugAI") + return; + + var ts = order.TargetString; + if (ts != Info.Plug) + return; + + self.World.AddFrameEndTask(w => + { + var playerActor = w.GetActorById(order.ExtraData); + var targetActor = order.Target.Actor; + + if (playerActor == null || playerActor.IsDead || targetActor == null || targetActor.IsDead) + return; + + var actorInfo = self.World.Map.Rules.Actors[order.TargetString]; + + var faction = self.Owner.Faction.InternalName; + var buildingInfo = actorInfo.TraitInfo(); + + var buildableInfo = actorInfo.TraitInfos().FirstOrDefault(); + if (buildableInfo != null && buildableInfo.ForceFaction != null) + faction = buildableInfo.ForceFaction; + + var plugInfo = actorInfo.TraitInfoOrDefault(); + if (plugInfo == null) + return; + + var location = targetActor.Location; + var pluggable = targetActor.TraitsImplementing() + .FirstOrDefault(p => p.AcceptsPlug(plugInfo.Type)); + + if (pluggable == null) + return; + + pluggable.EnablePlug(targetActor, plugInfo.Type); + foreach (var s in buildingInfo.BuildSounds) + Game.Sound.PlayToPlayer(SoundType.World, order.Player, s, targetActor.CenterPosition); + }); + } + + protected override void TraitEnabled(Actor self) + { + ticks = Info.Interval; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/PowerDownBotManager.cs b/OpenRA.Mods.AS/Traits/BotModules/PowerDownBotManager.cs new file mode 100644 index 000000000000..423348db3e8d --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/PowerDownBotManager.cs @@ -0,0 +1,195 @@ +#region Copyright & License Information +/* + * Copyright 2007-2023 The RV-Engine Developers, + * This file is part of RV, SP and GenAlpha, 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.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Manages AI powerdown.", + "You need to use PowerMultiplier on toggle control only on related buildings, for calculation of this bot module")] + public class PowerDownBotModuleInfo : ConditionalTraitInfo + { + [Desc("Delay (in ticks) between toggling powerdown.")] + public readonly int Interval = 150; + + public override object Create(ActorInitializer init) { return new PowerDownBotModule(init.Self, this); } + } + + public class PowerDownBotModule : ConditionalTrait, IBotTick, IGameSaveTraitData + { + readonly World world; + readonly Player player; + + PowerManager playerPower; + int toggleTick; + + readonly Func isToggledBuildingsValid; + + // We keep a list to track toggled buildings for performance. + List toggledBuildings = new(); + + sealed class BuildingPowerWrapper + { + public int ExpectedPowerChanging; + public Actor Actor; + + public BuildingPowerWrapper(Actor a, int p) + { + Actor = a; + ExpectedPowerChanging = p; + } + } + + public PowerDownBotModule(Actor self, PowerDownBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + + isToggledBuildingsValid = a => a != null && a.Owner == self.Owner && !a.IsDead && a.IsInWorld; + } + + protected override void Created(Actor self) + { + playerPower = self.Owner.PlayerActor.TraitOrDefault(); + } + + protected override void TraitEnabled(Actor self) + { + toggleTick = world.LocalRandom.Next(Info.Interval); + } + + static int GetTogglePowerChanging(Actor a) + { + var powerChangingIfToggled = 0; + var powerTraits = a.TraitsImplementing().Where(t => !t.IsTraitDisabled).ToArray(); + if (powerTraits.Length > 0) + { + var powerMulTraits = a.TraitsImplementing().ToArray(); + powerChangingIfToggled = powerTraits.Sum(p => p.Info.Amount) * (powerMulTraits.Sum(p => p.Info.Modifier) - 100) / 100; + if (powerMulTraits.Any(t => !t.IsTraitDisabled)) + powerChangingIfToggled = -powerChangingIfToggled; + } + + return powerChangingIfToggled; + } + + IEnumerable GetToggleableBuildings(IBot bot) + { + var toggleable = bot.Player.World.ActorsHavingTrait(t => !t.IsTraitDisabled && !t.IsTraitPaused) + .Where(a => a != null && !a.IsDead && a.Owner == player && a.Info.HasTraitInfo() && a.Info.HasTraitInfo() && a.Info.HasTraitInfo()); + + return toggleable; + } + + IEnumerable GetOnlineBuildings(IBot bot) + { + var toggleableBuildings = new List(); + + foreach (var a in GetToggleableBuildings(bot)) + { + var powerChanging = GetTogglePowerChanging(a); + if (powerChanging > 0) + toggleableBuildings.Add(new BuildingPowerWrapper(a, powerChanging)); + } + + return toggleableBuildings.OrderBy(bpw => bpw.ExpectedPowerChanging); + } + + void IBotTick.BotTick(IBot bot) + { + if (toggleTick > 0 || playerPower == null) + { + toggleTick--; + return; + } + + var power = playerPower.ExcessPower; + var togglingBuildings = new List(); + + // When there is extra power, check if AI can toggle on + if (power > 0) + { + toggledBuildings = toggledBuildings.Where(bpw => isToggledBuildingsValid(bpw.Actor)).OrderByDescending(bpw => bpw.ExpectedPowerChanging).ToList(); + for (var i = 0; i < toggledBuildings.Count; i++) + { + var bpw = toggledBuildings[i]; + if (power + bpw.ExpectedPowerChanging < 0) + continue; + + togglingBuildings.Add(bpw.Actor); + power += bpw.ExpectedPowerChanging; + toggledBuildings.RemoveAt(i); + } + } + + // When there is no power, check if AI can toggle off + // and add those toggled to list for toggling on + else if (power < 0) + { + var buildingsCanBeOff = GetOnlineBuildings(bot); + foreach (var bpw in buildingsCanBeOff) + { + if (power > 0) + break; + + togglingBuildings.Add(bpw.Actor); + toggledBuildings.Add(new BuildingPowerWrapper(bpw.Actor, -bpw.ExpectedPowerChanging)); + power += bpw.ExpectedPowerChanging; + } + } + + if (togglingBuildings.Count > 0) + bot.QueueOrder(new Order("PowerDown", null, false, groupedActors: togglingBuildings.ToArray())); + + toggleTick = Info.Interval; + } + + List IGameSaveTraitData.IssueTraitData(Actor self) + { + if (IsTraitDisabled) + return null; + + var data = new List(); + foreach (var tb in toggledBuildings.Where(td => isToggledBuildingsValid(td.Actor))) + data.Add(new MiniYamlNode(FieldSaver.FormatValue(tb.Actor.ActorID), FieldSaver.FormatValue(tb.ExpectedPowerChanging))); + + return new List() + { + new MiniYamlNode("ToggledBuildings", new MiniYaml("", data)) + }; + } + + void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) + { + if (self.World.IsReplay) + return; + + var nodes = data.ToDictionary(); + + if (nodes.TryGetValue("ToggledBuildings", out var toggledBuildingsNode)) + { + foreach (var n in toggledBuildingsNode.Nodes) + { + var a = self.World.GetActorById(FieldLoader.GetValue(n.Key, n.Key)); + + if (isToggledBuildingsValid(a)) + toggledBuildings.Add(new BuildingPowerWrapper(a, FieldLoader.GetValue(n.Key, n.Value.Value))); + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/SendUnitToAttackBotModule.cs b/OpenRA.Mods.AS/Traits/BotModules/SendUnitToAttackBotModule.cs new file mode 100644 index 000000000000..3b1fb265def5 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/SendUnitToAttackBotModule.cs @@ -0,0 +1,389 @@ +#region Copyright & License Information +/* + * Copyright 2007-2023 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.Cnc.Traits; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Flags] + public enum TargetDistance + { + Closest = 0, + Furthest = 1, + Random = 2 + } + + [TraitLocation(SystemActors.Player)] + [Desc("Bot logic for units that should not be sent with a regular squad, like suicide or subterranean units.")] + public sealed class SendUnitToAttackBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actors used for attack, and their options on attacking.")] + [FieldLoader.LoadUsing(nameof(LoadOptions))] + public readonly Dictionary ActorTypesAndAttackOptions = default; + + [Desc("Target types that can be targeted.")] + public readonly BitSet ValidTargets = new("Structure"); + + [Desc("Target types that can't be targeted.")] + public readonly BitSet InvalidTargets; + + [Desc("Player relationships that will be targeted.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Enemy; + + [Desc("Should attack the furthest or closest target. Possible values are Closest, Furthest, Random", + "Multiple values mean the distance randomizes between them")] + public readonly TargetDistance[] TargetDistances = { TargetDistance.Closest }; + + [Desc("Prepare unit, disguise unit and try attack target in this interval.")] + public readonly int ScanTick = 463; + + [Desc("The total attack desire increases by this amount per scan", + "Note: When there is no attack unit, the total attack desire will return to 0.")] + public readonly int AttackDesireIncreasedPerScan = 10; + + static object LoadOptions(MiniYaml yaml) + { + var ret = new Dictionary(); + var options = yaml.Nodes.FirstOrDefault(n => n.Key == "ActorTypesAndAttackOptions"); + if (options != null) + foreach (var d in options.Value.Nodes) + { + ret.Add(d.Key, new UnitAttackOptions(d.Value)); + } + + return ret; + } + + public override object Create(ActorInitializer init) { return new SendUnitToAttackBotModule(init.Self, this); } + } + + public class SendUnitToAttackBotModule : ConditionalTrait, IBotTick + { + const PlayerRelationship ValidDisguiseRelationship = PlayerRelationship.Ally | PlayerRelationship.Neutral; + + readonly World world; + readonly Player player; + + readonly Predicate unitCannotBeOrdered; + readonly Predicate unitCannotBeOrderedOrIsBusy; + readonly Predicate unitCannotBeOrderedOrIsIdle; + readonly Predicate isInvalidActor; + + readonly List activeActors = new(); + readonly List stuckActors = new(); + + readonly List> disguisePairs = new(); + BitSet disguiseTypes; + List attackActors = new(); + + int prepareAttackTicks; + int disguiseDelayTicks; + int assignAttackTicks; + + Player targetPlayer; + int desireIncreased; + + public SendUnitToAttackBotModule(Actor self, SendUnitToAttackBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + isInvalidActor = a => a == null || a.IsDead || !a.IsInWorld; + unitCannotBeOrdered = a => isInvalidActor(a) || a.Owner != player; + unitCannotBeOrderedOrIsBusy = a => unitCannotBeOrdered(a) || !(a.IsIdle || a.CurrentActivity is FlyIdle); + unitCannotBeOrderedOrIsIdle = a => unitCannotBeOrdered(a) || a.IsIdle || a.CurrentActivity is FlyIdle; + desireIncreased = 0; + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + // and we divide preparing stage, disguising stage and attacking stage for PERF. + prepareAttackTicks = world.LocalRandom.Next(0, Info.ScanTick); + disguiseDelayTicks = prepareAttackTicks + Info.ScanTick / 3; + assignAttackTicks = disguiseDelayTicks + Info.ScanTick / 3; + } + + void IBotTick.BotTick(IBot bot) + { + if (--prepareAttackTicks <= 0) + { + prepareAttackTicks = Info.ScanTick; + KickStuckTick(bot); + PrepareAttackTick(bot); + } + + if (--disguiseDelayTicks < 0) + { + disguiseDelayTicks = Info.ScanTick; + if (targetPlayer.WinState != WinState.Lost) + DisguiseTicks(bot); + } + + if (--assignAttackTicks <= 0) + { + assignAttackTicks = Info.ScanTick; + if (targetPlayer.WinState != WinState.Lost) + AttackTicks(bot); + } + } + + void KickStuckTick(IBot bot) + { + activeActors.RemoveAll(u => unitCannotBeOrderedOrIsIdle(u.Actor)); + stuckActors.RemoveAll(a => unitCannotBeOrdered(a)); + for (var i = 0; i < activeActors.Count; i++) + { + var p = activeActors[i]; + if (p.Actor.CurrentActivity.ChildActivity != null && p.Actor.CurrentActivity.ChildActivity.ActivityType == ActivityType.Move && p.Actor.CenterPosition == p.WPos) + { + stuckActors.Add(p.Actor); + bot.QueueOrder(new Order("Stop", p.Actor, false)); + activeActors.RemoveAt(i); + i--; + } + + p.WPos = p.Actor.CenterPosition; + } + } + + void PrepareAttackTick(IBot bot) + { + // Randomly choose target player to attack + var targetPlayers = world.Players.Where(p => p.WinState != WinState.Lost && Info.ValidRelationships.HasRelationship(p.RelationshipWith(player))).ToList(); + if (targetPlayers.Count == 0) + return; + targetPlayer = targetPlayers.Random(world.LocalRandom); + + attackActors = world.ActorsHavingTrait().Where(a => + { + if (!unitCannotBeOrderedOrIsBusy(a) && !stuckActors.Contains(a) && Info.ActorTypesAndAttackOptions.TryGetValue(a.Info.Name, out var option)) + { + if (option.TryGetHealed && TryGetHeal(bot, a)) + return false; + + if (option.AttackRequires.HasFlag(AttackRequires.CargoLoaded) && a.TraitsImplementing().FirstOrDefault(t => !t.IsTraitDisabled) is Cargo cargo && cargo.IsEmpty()) + return false; + + if (option.TryDisguise && a.TraitOrDefault() is Disguise disguise && !disguise.Disguised) + { + disguisePairs.Add(new TraitPair(a, disguise)); + disguiseTypes = disguiseTypes.Union(disguise.Info.TargetTypes); + if (option.AttackRequires.HasFlag(AttackRequires.Disguised)) + return false; + } + + return true; + } + + return false; + }).ToList(); + } + + void DisguiseTicks(IBot bot) + { + var invalidActors = new HashSet(); + foreach (var p in disguisePairs) + { + if (isInvalidActor(p.Actor)) + invalidActors.Add(p.Actor); + } + + disguisePairs.RemoveAll(p => invalidActors.Contains(p.Actor)); + + if (disguisePairs.Count <= 0) + return; + + var targets = world.Actors.Where(a => + { + if (isInvalidActor(a) || !ValidDisguiseRelationship.HasRelationship(a.Owner.RelationshipWith(targetPlayer)) || a.Info.HasTraitInfo()) + return false; + + var t = a.GetEnabledTargetTypes(); + + if (!disguiseTypes.Overlaps(t)) + return false; + + var hasModifier = false; + var visModifiers = a.TraitsImplementing(); + foreach (var v in visModifiers) + { + if (v.IsVisible(a, player)) + return true; + + hasModifier = true; + } + + return !hasModifier; + }); + + foreach (var t in targets) + { + invalidActors.Clear(); + foreach (var p in disguisePairs) + { + if (!p.Trait.Info.TargetTypes.Overlaps(t.GetEnabledTargetTypes())) + continue; + + bot.QueueOrder(new Order("Disguise", p.Actor, Target.FromActor(t), true)); + invalidActors.Add(p.Actor); + attackActors.Add(p.Actor); + } + + disguisePairs.RemoveAll(p => invalidActors.Contains(p.Actor)); + if (disguisePairs.Count == 0) + break; + } + + disguisePairs.Clear(); + } + + void AttackTicks(IBot bot) + { + var attackdesire = 0; + + var invalidActors = new HashSet(); + foreach (var a in attackActors) + { + if (unitCannotBeOrderedOrIsBusy(a)) + invalidActors.Add(a); + else + attackdesire += Info.ActorTypesAndAttackOptions[a.Info.Name].AttackDesireOfEach; + } + + attackActors.RemoveAll(invalidActors.Contains); + invalidActors.Clear(); + + if (attackActors.Count == 0) + { + desireIncreased = 0; + return; + } + + desireIncreased += Info.AttackDesireIncreasedPerScan; + if (desireIncreased + attackdesire < 100) + return; + + var targets = world.Actors.Where(a => + { + if (isInvalidActor(a) || a.Owner != targetPlayer) + return false; + + var t = a.GetEnabledTargetTypes(); + + if (!Info.ValidTargets.Overlaps(t) || Info.InvalidTargets.Overlaps(t)) + return false; + + var hasModifier = false; + var visModifiers = a.TraitsImplementing(); + foreach (var v in visModifiers) + { + if (v.IsVisible(a, player)) + return true; + + hasModifier = true; + } + + return !hasModifier; + }); + + var targetDistance = Info.TargetDistances.Random(world.LocalRandom); + switch (targetDistance) + { + case TargetDistance.Closest: + targets = targets.OrderBy(a => (a.CenterPosition - attackActors.First().CenterPosition).HorizontalLengthSquared); + break; + case TargetDistance.Furthest: + targets = targets.OrderByDescending(a => (a.CenterPosition - attackActors.First().CenterPosition).HorizontalLengthSquared); + break; + case TargetDistance.Random: + targets = targets.Shuffle(world.LocalRandom); + break; + } + + foreach (var t in targets) + { + foreach (var a in attackActors) + { + if (!AIUtils.PathExist(a, t.Location, t)) + continue; + + AssignAttackOrders(bot, a, t); + invalidActors.Add(a); + activeActors.Add(new UnitWposWrapper(a)); + } + + attackActors.RemoveAll(invalidActors.Contains); + invalidActors.Clear(); + + if (attackActors.Count == 0) + break; + } + + attackActors.Clear(); + } + + void AssignAttackOrders(IBot bot, Actor attacker, Actor victim) + { + var option = Info.ActorTypesAndAttackOptions[attacker.Info.Name]; + if (option.MoveToOrderName != null) + bot.QueueOrder(new Order(option.MoveToOrderName, attacker, Target.FromCell(world, victim.Location), true)); + + bot.QueueOrder(new Order(option.AttackOrderName, attacker, Target.FromActor(victim), true)); + + if (option.MoveBackOrderName != null) + bot.QueueOrder(new Order(option.MoveBackOrderName, attacker, Target.FromCell(world, attacker.Location), true)); + } + + protected static bool TryGetHeal(IBot bot, Actor unit) + { + var health = unit.TraitOrDefault(); + + if (health != null && health.DamageState > DamageState.Undamaged) + { + // Try repair units + Actor repairBuilding = null; + var orderId = "Repair"; + var repairable = unit.TraitOrDefault(); + if (repairable != null) + repairBuilding = repairable.FindRepairBuilding(unit); + else + { + var repairableNear = unit.TraitOrDefault(); + if (repairableNear != null) + { + orderId = "RepairNear"; + repairBuilding = repairableNear.FindRepairBuilding(unit); + } + } + + if (repairBuilding != null) + { + bot.QueueOrder(new Order(orderId, unit, Target.FromActor(repairBuilding), true)); + return true; + } + + return false; + } + else + return false; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/SharedCargoBotModule.cs b/OpenRA.Mods.AS/Traits/BotModules/SharedCargoBotModule.cs new file mode 100644 index 000000000000..de35811dfd41 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/SharedCargoBotModule.cs @@ -0,0 +1,164 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Manages AI load unit related with " + nameof(SharedCargo) + " and " + nameof(SharedPassenger) + " traits.")] + public class SharedCargoBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actor types that can be targeted for load, must have " + nameof(SharedCargo) + ".")] + public readonly HashSet Transports = default; + + [Desc("Actor types that used for loading, must have " + nameof(SharedPassengerInfo) + ".")] + public readonly HashSet Passengers = default; + + [Desc("Actor relationship that can be targeted for load.")] + public readonly bool OnlyEnterOwnerPlayer = true; + + [Desc("Scan suitable actors and target in this interval.")] + public readonly int ScanTick = 443; + + [Desc("Load passengers max to this amount per scan.")] + public readonly int PassengersPerScan = 2; + + [Desc("Load passengers max to this amount.")] + public readonly int MaxPassengers = 6; + + [Desc("Don't load passengers to this actor if damage state is worse than this.")] + public readonly DamageState ValidDamageState = DamageState.Heavy; + + [Desc("Don't load passengers that are further than this distance to this actor.")] + public readonly WDist MaxDistance = WDist.FromCells(40); + + public override object Create(ActorInitializer init) { return new SharedCargoBotModule(init.Self, this); } + } + + public class SharedCargoBotModule : ConditionalTrait, IBotTick + { + readonly World world; + readonly Player player; + readonly Predicate unitCannotBeOrdered; + readonly Predicate unitCannotBeOrderedOrIsBusy; + readonly Predicate unitCannotBeOrderedOrIsIdle; + readonly Predicate invalidTransport; + + readonly List activePassengers = new(); + readonly List stuckPassengers = new(); + int minAssignRoleDelayTicks; + SharedCargoManager sharedCargoManager; + + public SharedCargoBotModule(Actor self, SharedCargoBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + if (info.OnlyEnterOwnerPlayer) + invalidTransport = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player; + else + invalidTransport = a => a == null || a.IsDead || !a.IsInWorld || a.Owner.RelationshipWith(player) != PlayerRelationship.Ally; + unitCannotBeOrdered = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player; + unitCannotBeOrderedOrIsBusy = a => unitCannotBeOrdered(a) || !(a.IsIdle || a.CurrentActivity is FlyIdle); + unitCannotBeOrderedOrIsIdle = a => unitCannotBeOrdered(a) || a.IsIdle || a.CurrentActivity is FlyIdle; + } + + protected override void Created(Actor self) + { + sharedCargoManager = self.TraitsImplementing().FirstOrDefault(); + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + minAssignRoleDelayTicks = world.LocalRandom.Next(0, Info.ScanTick); + } + + void IBotTick.BotTick(IBot bot) + { + if (--minAssignRoleDelayTicks <= 0 && sharedCargoManager != null && Info.MaxPassengers > sharedCargoManager.PassengerCount) + { + minAssignRoleDelayTicks = Info.ScanTick; + + activePassengers.RemoveAll(u => unitCannotBeOrderedOrIsIdle(u.Actor)); + stuckPassengers.RemoveAll(a => unitCannotBeOrdered(a)); + for (var i = 0; i < activePassengers.Count; i++) + { + var p = activePassengers[i]; + if (p.Actor.CurrentActivity.ChildActivity != null && p.Actor.CurrentActivity.ChildActivity.ActivityType == ActivityType.Move && p.Actor.CenterPosition == p.WPos) + { + stuckPassengers.Add(p.Actor); + bot.QueueOrder(new Order("Stop", p.Actor, false)); + activePassengers.RemoveAt(i); + i--; + } + + p.WPos = p.Actor.CenterPosition; + } + + if (!sharedCargoManager.HasSpace(1)) + return; + + var tcs = world.ActorsWithTrait().Where( + at => + { + if (!Info.Transports.Contains(at.Actor.Info.Name) || at.Trait.IsTraitDisabled || invalidTransport(at.Actor)) + return false; + + var health = at.Actor.TraitOrDefault()?.DamageState; + return health == null || health < Info.ValidDamageState; + }).ToArray(); + + if (tcs.Length == 0) + return; + + var tc = tcs.Random(world.LocalRandom); + var cargo = tc.Trait; + var transport = tc.Actor; + var spaceTaken = 0; + + var passengers = world.ActorsWithTrait().Where(at => !unitCannotBeOrderedOrIsBusy(at.Actor) && Info.Passengers.Contains(at.Actor.Info.Name) && !stuckPassengers.Contains(at.Actor) && sharedCargoManager.HasSpace(at.Trait.Info.Weight) && (at.Actor.CenterPosition - transport.CenterPosition).HorizontalLengthSquared <= Info.MaxDistance.LengthSquared) + .OrderBy(at => (at.Actor.CenterPosition - transport.CenterPosition).HorizontalLengthSquared); + + var orderedActors = new List(); + + var passengerCount = 0; + foreach (var p in passengers) + { + if (!AIUtils.PathExist(p.Actor, transport.Location, transport)) + continue; + + if (sharedCargoManager.HasSpace(spaceTaken + p.Trait.Info.Weight)) + { + spaceTaken += p.Trait.Info.Weight; + orderedActors.Add(p.Actor); + passengerCount++; + activePassengers.Add(new UnitWposWrapper(p.Actor)); + } + + if (!sharedCargoManager.HasSpace(spaceTaken + 1) || passengerCount >= Info.PassengersPerScan) + break; + } + + if (orderedActors.Count > 0) + bot.QueueOrder(new Order("EnterSharedTransport", null, Target.FromActor(transport), false, groupedActors: orderedActors.ToArray())); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/BotModules/SupportPowerBotASModule.cs b/OpenRA.Mods.AS/Traits/BotModules/SupportPowerBotASModule.cs new file mode 100644 index 000000000000..10fbc001ba5f --- /dev/null +++ b/OpenRA.Mods.AS/Traits/BotModules/SupportPowerBotASModule.cs @@ -0,0 +1,189 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Manages bot support power handling.")] + public class SupportPowerBotASModuleInfo : ConditionalTraitInfo, Requires + { + [Desc("Tells the AI how to use its support powers.")] + [FieldLoader.LoadUsing(nameof(LoadDecisions))] + public readonly List Decisions = new(); + + static object LoadDecisions(MiniYaml yaml) + { + var ret = new List(); + var decisions = yaml.Nodes.FirstOrDefault(n => n.Key == "Decisions"); + if (decisions != null) + foreach (var d in decisions.Value.Nodes) + ret.Add(new SupportPowerDecisionAS(d.Value)); + + return ret; + } + + public override object Create(ActorInitializer init) { return new SupportPowerBotASModule(init.Self, this); } + } + + public class SupportPowerBotASModule : ConditionalTrait, IBotTick, IGameSaveTraitData + { + readonly World world; + readonly Player player; + readonly Dictionary waitingPowers = new(); + readonly Dictionary powerDecisions = new(); + readonly List stalePowers = new(); + PlayerResources playerResource; + SupportPowerManager supportPowerManager; + + public SupportPowerBotASModule(Actor self, SupportPowerBotASModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + self.World.AddFrameEndTask(w => playerResource = player.PlayerActor.Trait()); + } + + protected override void TraitEnabled(Actor self) + { + supportPowerManager = player.PlayerActor.Trait(); + foreach (var decision in Info.Decisions) + powerDecisions.Add(decision.OrderName, decision); + } + + void IBotTick.BotTick(IBot bot) + { + foreach (var sp in supportPowerManager.Powers.Values) + { + if (sp.Disabled) + continue; + + // Add power to dictionary if not in delay dictionary yet + if (!waitingPowers.ContainsKey(sp)) + waitingPowers.Add(sp, 0); + + if (waitingPowers[sp] > 0) + waitingPowers[sp]--; + + // If we have recently tried and failed to find a use location for a power, then do not try again until later + var isDelayed = waitingPowers[sp] > 0; + if (sp.Ready && !isDelayed && powerDecisions.TryGetValue(sp.Info.OrderName, out var powerDecision)) + { + if (powerDecision == null) + { + AIUtils.BotDebug("Bot Bug: FindAttackLocationToSupportPower, couldn't find powerDecision for {0}", sp.Info.OrderName); + continue; + } + + if (sp.Info.Cost != 0 && playerResource.Cash + playerResource.Resources < sp.Info.Cost) + { + AIUtils.BotDebug("AI: {1} can't afford the activation of support power {0}. Delaying rescan.", sp.Info.OrderName, player.PlayerName); + waitingPowers[sp] += powerDecision.GetNextScanTime(world); + + continue; + } + + var attackLocation = FindAttackLocationToSupportPower(sp); + if (attackLocation == null) + { + AIUtils.BotDebug("AI: {1} can't find suitable attack location for support power {0}. Delaying rescan.", sp.Info.OrderName, player.PlayerName); + waitingPowers[sp] += powerDecision.GetNextScanTime(world); + + continue; + } + + // Valid target found, delay by a few ticks to avoid rescanning before power fires via order + AIUtils.BotDebug("AI: {2} found new target location {0} for support power {1}.", attackLocation, sp.Info.OrderName, player.PlayerName); + waitingPowers[sp] += 10; + + // Note: SelectDirectionalTarget uses uint.MaxValue in ExtraData to indicate that the player did not pick a direction. + bot.QueueOrder(new Order(sp.Key, supportPowerManager.Self, Target.FromCell(world, attackLocation.Value), false) { SuppressVisualFeedback = true, ExtraData = uint.MaxValue }); + } + } + + // Remove stale powers + stalePowers.AddRange(waitingPowers.Keys.Where(wp => !supportPowerManager.Powers.ContainsKey(wp.Key))); + foreach (var p in stalePowers) + waitingPowers.Remove(p); + + stalePowers.Clear(); + } + + /// Detail scans an area, evaluating positions. + CPos? FindAttackLocationToSupportPower(SupportPowerInstance readyPower) + { + CPos? bestLocation = null; + var bestAttractiveness = 0; + var powerDecision = powerDecisions[readyPower.Info.OrderName]; + if (powerDecision == null) + { + AIUtils.BotDebug("Bot Bug: FindAttackLocationToSupportPower, couldn't find powerDecision for {0}", readyPower.Info.OrderName); + return null; + } + + var availableTargets = world.ActorsHavingTrait().Where(x => x.IsInWorld && !x.IsDead && + (powerDecision.IgnoreVisibility || x.CanBeViewedByPlayer(player)) && + powerDecision.Against.HasRelationship(player.RelationshipWith(x.Owner)) && + powerDecision.Types.Overlaps(x.GetEnabledTargetTypes())); + + foreach (var a in availableTargets) + { + var pos = a.CenterPosition; + var consideredAttractiveness = 0; + consideredAttractiveness += powerDecision.GetAttractiveness(pos, player); + + if (consideredAttractiveness <= bestAttractiveness || consideredAttractiveness < powerDecision.MinimumAttractiveness) + continue; + + bestAttractiveness = consideredAttractiveness; + bestLocation = world.Map.CellContaining(pos); + } + + return bestLocation; + } + + List IGameSaveTraitData.IssueTraitData(Actor self) + { + if (IsTraitDisabled) + return null; + + var waitingPowersNodes = waitingPowers + .Select(kv => new MiniYamlNode(kv.Key.Key, FieldSaver.FormatValue(kv.Value))) + .ToList(); + + return new List() + { + new MiniYamlNode("WaitingPowers", "", waitingPowersNodes) + }; + } + + void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) + { + if (self.World.IsReplay) + return; + + var nodes = data.ToDictionary(); + + if (nodes.TryGetValue("WaitingPowers", out var waitingPowersNode)) + { + foreach (var n in waitingPowersNode.Nodes) + { + if (supportPowerManager.Powers.TryGetValue(n.Key, out var instance)) + waitingPowers[instance] = FieldLoader.GetValue("WaitingPowers", n.Value.Value); + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/CarrierMaster.cs b/OpenRA.Mods.AS/Traits/CarrierMaster.cs new file mode 100644 index 000000000000..26222ee426e0 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/CarrierMaster.cs @@ -0,0 +1,266 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor can spawn actors.")] + public class CarrierMasterInfo : BaseSpawnerMasterInfo + { + public readonly string Name = "primary"; + + [Desc("Spawn is a missile that dies and not return.")] + public readonly bool SpawnIsMissile = false; + + [Desc("Spawn rearm delay, in ticks")] + public readonly int RearmTicks = 150; + + [GrantedConditionReference] + [Desc("The condition to grant to self right after launching a spawned unit. (Used by V3 to make immobile.)")] + public readonly string LaunchingCondition = null; + + [Desc("After this many ticks, we remove the condition.")] + public readonly int LaunchingTicks = 15; + + [Desc("Instantly repair spawners when they return?")] + public readonly bool InstantRepair = true; + + [GrantedConditionReference] + [Desc("The condition to grant to self while spawned units are loaded.", + "Condition can stack with multiple spawns.")] + public readonly string LoadedCondition = null; + + [Desc("Conditions to grant when specified actors are contained inside the transport.", + "A dictionary of [actor id]: [condition].")] + public readonly Dictionary SpawnContainConditions = new(); + + [GrantedConditionReference] + public IEnumerable LinterSpawnContainConditions { get { return SpawnContainConditions.Values; } } + + public override object Create(ActorInitializer init) { return new CarrierMaster(init, this); } + } + + public class CarrierMaster : BaseSpawnerMaster, ITick, IResolveOrder, INotifyAttack + { + class CarrierSlaveEntry : BaseSpawnerSlaveEntry + { + public int RearmTicks = 0; + public new CarrierSlave SpawnerSlave; + } + + readonly Dictionary> spawnContainTokens = new(); + public readonly CarrierMasterInfo CarrierMasterInfo; + + readonly Stack loadedTokens = new(); + int respawnTicks = 0; + + int launchCondition = Actor.InvalidConditionToken; + int launchConditionTicks; + + public CarrierMaster(ActorInitializer init, CarrierMasterInfo info) + : base(init, info) + { + CarrierMasterInfo = info; + } + + protected override void Created(Actor self) + { + base.Created(self); + + // Spawn initial load. + var burst = Info.InitialActorCount == -1 ? Info.Actors.Length : Info.InitialActorCount; + for (var i = 0; i < burst; i++) + Replenish(self, SlaveEntries); + } + + public override BaseSpawnerSlaveEntry[] CreateSlaveEntries(BaseSpawnerMasterInfo info) + { + var slaveEntries = new CarrierSlaveEntry[info.Actors.Length]; // For this class to use + + for (var i = 0; i < slaveEntries.Length; i++) + slaveEntries[i] = new CarrierSlaveEntry(); + + return slaveEntries; // For the base class to use + } + + public override void InitializeSlaveEntry(Actor slave, BaseSpawnerSlaveEntry entry) + { + base.InitializeSlaveEntry(slave, entry); + + var carrierSlaveEntry = entry as CarrierSlaveEntry; + carrierSlaveEntry.RearmTicks = 0; + carrierSlaveEntry.IsLaunched = false; + carrierSlaveEntry.SpawnerSlave = slave.Trait(); + } + + public void ResolveOrder(Actor self, Order order) + { + if (order.OrderString == "Stop") + Recall(); + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + + // The rate of fire of the dummy weapon determines the launch cycle as each shot + // invokes Attacking() + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + // HACK: If Armament hits instantly and kills the target, the target will become invalid + if (target.Type == TargetType.Invalid || IsTraitDisabled || IsTraitPaused || (Info.ArmamentNames.Count > 0 && !Info.ArmamentNames.Contains(a.Info.Name))) + return; + + // Issue retarget order for already launched ones + foreach (var slave in SlaveEntries) + if (slave.IsLaunched && slave.IsValid) + slave.SpawnerSlave.Attack(slave.Actor, target); + + var carrierSlaveEntry = GetLaunchable(); + if (carrierSlaveEntry == null) + return; + + carrierSlaveEntry.IsLaunched = true; // mark as launched + + if (CarrierMasterInfo.LaunchingCondition != null) + { + if (launchCondition == Actor.InvalidConditionToken) + launchCondition = self.GrantCondition(CarrierMasterInfo.LaunchingCondition); + + launchConditionTicks = CarrierMasterInfo.LaunchingTicks; + } + + SpawnIntoWorld(self, carrierSlaveEntry.Actor, self.CenterPosition + carrierSlaveEntry.Offset.Rotate(self.Orientation)); + + if (spawnContainTokens.TryGetValue(a.Info.Name, out var spawnContainToken) && spawnContainToken.Count > 0) + self.RevokeCondition(spawnContainToken.Pop()); + + if (loadedTokens.Count > 0) + self.RevokeCondition(loadedTokens.Pop()); + + // Lambdas can't use 'in' variables, so capture a copy for later + var delayedTarget = target; + + // Queue attack order, too. + self.World.AddFrameEndTask(w => + { + // The actor might had been trying to do something before entering the carrier. + // Cancel whatever it was trying to do. + carrierSlaveEntry.SpawnerSlave.Stop(carrierSlaveEntry.Actor); + + carrierSlaveEntry.SpawnerSlave.Attack(carrierSlaveEntry.Actor, delayedTarget); + }); + } + + void Recall() + { + // Tell launched slaves to come back and enter me. + foreach (var slaveEntry in SlaveEntries) + if (slaveEntry.IsLaunched && slaveEntry.IsValid) + { + var carrierSlaveEntry = slaveEntry as CarrierSlaveEntry; + carrierSlaveEntry.SpawnerSlave.EnterSpawner(slaveEntry.Actor); + } + } + + public override void OnSlaveKilled(Actor self, Actor slave) + { + // Set clock so that regen happens. + if (respawnTicks <= 0) // Don't interrupt an already running timer! + respawnTicks = Info.RespawnTicks; + } + + CarrierSlaveEntry GetLaunchable() + { + foreach (var slaveEntry in SlaveEntries) + { + var carrierSlaveEntry = slaveEntry as CarrierSlaveEntry; + if (carrierSlaveEntry.RearmTicks <= 0 && !slaveEntry.IsLaunched && slaveEntry.IsValid) + return carrierSlaveEntry; + } + + return null; + } + + public void PickupSlave(Actor self, Actor a) + { + CarrierSlaveEntry slaveEntry = null; + foreach (var carrierSlaveEntry in SlaveEntries) + if (carrierSlaveEntry.Actor == a) + { + slaveEntry = carrierSlaveEntry as CarrierSlaveEntry; + break; + } + + if (slaveEntry == null) + throw new InvalidOperationException("An actor that isn't my slave entered me?"); + + slaveEntry.IsLaunched = false; + + // setup rearm + slaveEntry.RearmTicks = Util.ApplyPercentageModifiers(CarrierMasterInfo.RearmTicks, reloadModifiers.Select(rm => rm.GetReloadModifier(CarrierMasterInfo.Name))); + + if (CarrierMasterInfo.SpawnContainConditions.TryGetValue(a.Info.Name, out var spawnContainCondition)) + spawnContainTokens.GetOrAdd(a.Info.Name).Push(self.GrantCondition(spawnContainCondition)); + + if (!string.IsNullOrEmpty(CarrierMasterInfo.LoadedCondition)) + loadedTokens.Push(self.GrantCondition(CarrierMasterInfo.LoadedCondition)); + } + + public override void Replenish(Actor self, BaseSpawnerSlaveEntry entry) + { + base.Replenish(self, entry); + + if (CarrierMasterInfo.SpawnContainConditions.TryGetValue(entry.Actor.Info.Name, out var spawnContainCondition)) + spawnContainTokens.GetOrAdd(entry.Actor.Info.Name).Push(self.GrantCondition(spawnContainCondition)); + + if (!string.IsNullOrEmpty(CarrierMasterInfo.LoadedCondition)) + loadedTokens.Push(self.GrantCondition(CarrierMasterInfo.LoadedCondition)); + } + + void ITick.Tick(Actor self) + { + if (launchCondition != Actor.InvalidConditionToken && --launchConditionTicks < 0) + launchCondition = self.RevokeCondition(launchCondition); + + if (respawnTicks > 0) + { + respawnTicks--; + + // Time to respawn someting. + if (respawnTicks <= 0) + { + Replenish(self, SlaveEntries); + + // If there's something left to spawn, restart the timer. + if (SelectEntryToSpawn(SlaveEntries) != null) + respawnTicks = Util.ApplyPercentageModifiers(Info.RespawnTicks, reloadModifiers.Select(rm => rm.GetReloadModifier(CarrierMasterInfo.Name))); + } + } + + // Rearm + foreach (var slaveEntry in SlaveEntries) + { + var carrierSlaveEntry = slaveEntry as CarrierSlaveEntry; + if (carrierSlaveEntry.RearmTicks > 0) + carrierSlaveEntry.RearmTicks--; + } + } + + protected override void TraitPaused(Actor self) + { + Recall(); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/CarrierSlave.cs b/OpenRA.Mods.AS/Traits/CarrierSlave.cs new file mode 100644 index 000000000000..abc871dfa288 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/CarrierSlave.cs @@ -0,0 +1,79 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.AS.Activities; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Can be slaved to a spawner.")] + public class CarrierSlaveInfo : BaseSpawnerSlaveInfo + { + [Desc("Move this close to the spawner, before entering it.")] + public readonly WDist LandingDistance = new(5 * 1024); + + public override object Create(ActorInitializer init) { return new CarrierSlave(init, this); } + } + + public class CarrierSlave : BaseSpawnerSlave, INotifyIdle + { + // readonly AmmoPool[] ammoPools; + public readonly CarrierSlaveInfo Info; + + CarrierMaster spawnerMaster; + + public CarrierSlave(ActorInitializer init, CarrierSlaveInfo info) + : base(info) + { + Info = info; + /* ammoPools = init.Self.TraitsImplementing().ToArray(); */ + } + + public void EnterSpawner(Actor self) + { + // Hopefully, self will be disposed shortly afterwards by SpawnerSlaveDisposal policy. + if (Master == null || Master.IsDead) + return; + + // Proceed with enter, if already at it. + if (self.CurrentActivity is EnterCarrierMaster) + return; + + // Cancel whatever else self was doing and return. + self.QueueActivity(false, new EnterCarrierMaster(self, Master, spawnerMaster)); + } + + public override void LinkMaster(Actor self, Actor master, BaseSpawnerMaster spawnerMaster) + { + base.LinkMaster(self, master, spawnerMaster); + this.spawnerMaster = spawnerMaster as CarrierMaster; + } + + /* bool NeedToReload() + { + // The unit may not have ammo but will have unlimited ammunitions. + if (ammoPools.Length == 0) + return false; + + return ammoPools.All(x => !x.HasAmmo); + } */ + + void INotifyIdle.TickIdle(Actor self) + { + EnterSpawner(self); + } + + public override void Stop(Actor self) + { + base.Stop(self); + EnterSpawner(self); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/CashCollectable.cs b/OpenRA.Mods.AS/Traits/CashCollectable.cs new file mode 100644 index 000000000000..be8349c31638 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/CashCollectable.cs @@ -0,0 +1,35 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; + +namespace OpenRA.Mods.AS.Traits +{ + public class CashCollectableType { } + + [Desc("Tag trait for CashCollector to function.")] + public class CashCollectableInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + public readonly int Value; + + [FieldLoader.Require] + public readonly BitSet Types = default; + + public override object Create(ActorInitializer init) { return new CashCollectable(this); } + } + + public class CashCollectable : ConditionalTrait + { + public CashCollectable(CashCollectableInfo info) + : base(info) { } + } +} diff --git a/OpenRA.Mods.AS/Traits/CashCollector.cs b/OpenRA.Mods.AS/Traits/CashCollector.cs new file mode 100644 index 000000000000..a5c8104bb5ea --- /dev/null +++ b/OpenRA.Mods.AS/Traits/CashCollector.cs @@ -0,0 +1,181 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Periodically collects cash from actors with CashCollectable traits.")] + public class CashCollectorInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [Desc("The range within cash gets collected.")] + public readonly WDist Range; + + [Desc("The maximum vertical range above terrain within cash gets collected.", + "Ignored if 0 (actors are upgraded regardless of vertical distance).")] + public readonly WDist MaximumVerticalOffset = WDist.Zero; + + [Desc("What diplomatic stances cash is collected from.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Ally; + + [FieldLoader.Require] + [Desc("Delay between two collections.")] + public readonly int Delay; + + [FieldLoader.Require] + [Desc("The type which allows the actor to collect nearby cash.")] + public readonly BitSet Type = default; + + [Desc("Whether to show a floating text.")] + public readonly bool ShowTicks = true; + + public readonly string EnableSound = null; + public readonly string DisableSound = null; + + public override object Create(ActorInitializer init) { return new CashCollector(init.Self, this); } + } + + public class CashCollector : ConditionalTrait, ITick, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOtherProduction + { + readonly Actor self; + + int proximityTrigger; + WPos cachedPosition; + WDist cachedRange; + WDist desiredRange; + WDist cachedVRange; + WDist desiredVRange; + + readonly HashSet collectables; + + bool cachedDisabled = true; + int ticks; + + public CashCollector(Actor self, CashCollectorInfo info) + : base(info) + { + this.self = self; + cachedRange = info.Range; + cachedVRange = info.MaximumVerticalOffset; + ticks = Info.Delay; + collectables = new HashSet(); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + cachedPosition = self.CenterPosition; + proximityTrigger = self.World.ActorMap.AddProximityTrigger(cachedPosition, cachedRange, cachedVRange, ActorEntered, ActorExited); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + self.World.ActorMap.RemoveProximityTrigger(proximityTrigger); + CollectCash(); + } + + void ITick.Tick(Actor self) + { + var disabled = IsTraitDisabled; + + if (cachedDisabled != disabled) + { + Game.Sound.Play(SoundType.World, disabled ? Info.DisableSound : Info.EnableSound, self.CenterPosition); + desiredRange = disabled ? WDist.Zero : Info.Range; + desiredVRange = disabled ? WDist.Zero : Info.MaximumVerticalOffset; + cachedDisabled = disabled; + } + + if (self.CenterPosition != cachedPosition || desiredRange != cachedRange || desiredVRange != cachedVRange) + { + cachedPosition = self.CenterPosition; + cachedRange = desiredRange; + cachedVRange = desiredVRange; + self.World.ActorMap.UpdateProximityTrigger(proximityTrigger, cachedPosition, cachedRange, cachedVRange); + } + + if (!IsTraitDisabled && --ticks < 0) + { + CollectCash(); + + ticks = Info.Delay; + } + } + + void CollectCash() + { + var cash = 0; + + foreach (var trait in collectables) + { + if (!trait.IsTraitDisabled) + cash += trait.Info.Value; + } + + if (Info.ShowTicks && self.Owner.IsAlliedWith(self.World.RenderPlayer)) + self.World.AddFrameEndTask(w => w.Add(new FloatingText(self.CenterPosition, self.Owner.Color, FloatingText.FormatCashTick(cash), 30))); + + self.Owner.PlayerActor.Trait().GiveCash(cash); + } + + void ActorEntered(Actor a) + { + if (a == self || a.Disposed || self.Disposed) + return; + + var relationship = self.Owner.RelationshipWith(a.Owner); + if (!Info.ValidRelationships.HasRelationship(relationship)) + return; + + var cc = a.TraitsImplementing().Where(t => t.Info.Types.Overlaps(Info.Type)); + foreach (var trait in cc) + collectables.Add(trait); + } + + void INotifyOtherProduction.UnitProducedByOther(Actor self, Actor producer, Actor produced, string productionType, TypeDictionary init) + { + if (produced.OccupiesSpace == null) + return; + + if (IsTraitDisabled) + return; + + if ((produced.CenterPosition - self.CenterPosition).HorizontalLengthSquared <= Info.Range.LengthSquared) + { + var relationship = self.Owner.RelationshipWith(produced.Owner); + if (!Info.ValidRelationships.HasRelationship(relationship)) + return; + + var cc = produced.TraitsImplementing().Where(t => t.Info.Types.Overlaps(Info.Type)); + foreach (var trait in cc) + collectables.Add(trait); + } + } + + void ActorExited(Actor a) + { + if (a == self || a.Disposed || self.Disposed) + return; + + var relationship = self.Owner.RelationshipWith(a.Owner); + if (!Info.ValidRelationships.HasRelationship(relationship)) + return; + + var cc = a.TraitsImplementing().Where(t => t.Info.Types.Overlaps(Info.Type)); + foreach (var trait in cc) + collectables.Remove(trait); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/ChangeOwnerOnGarrisoner.cs b/OpenRA.Mods.AS/Traits/ChangeOwnerOnGarrisoner.cs new file mode 100644 index 000000000000..a50572ec5f68 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/ChangeOwnerOnGarrisoner.cs @@ -0,0 +1,99 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class ChangeOwnerOnGarrisonerInfo : TraitInfo, Requires + { + [Desc("Speech notification played when the first actor enters this garrison.")] + public readonly string EnterNotification = null; + + [Desc("Speech notification played when the last actor leaves this garrison.")] + public readonly string ExitNotification = null; + + [Desc("List of sounds to be randomly played when the first actor enters this garrison.")] + public readonly string[] EnterSounds = Array.Empty(); + + [Desc("List of sounds to be randomly played when the last actor exits this garrison.")] + public readonly string[] ExitSounds = Array.Empty(); + + [Desc("Does the sound play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the EnterSounds and ExitSounds played at.")] + public readonly float SoundVolume = 1; + + public override object Create(ActorInitializer init) { return new ChangeOwnerOnGarrisoner(init.Self, this); } + } + + public class ChangeOwnerOnGarrisoner : INotifyGarrisonerEntered, INotifyGarrisonerExited, INotifyOwnerChanged + { + readonly ChangeOwnerOnGarrisonerInfo info; + readonly Garrisonable garrison; + + Player originalOwner; + bool garrisoning; + + public ChangeOwnerOnGarrisoner(Actor self, ChangeOwnerOnGarrisonerInfo info) + { + this.info = info; + garrison = self.Trait(); + originalOwner = self.Owner; + } + + void INotifyGarrisonerEntered.OnGarrisonerEntered(Actor self, Actor garrisoner) + { + var newOwner = garrisoner.Owner; + if (self.Owner != originalOwner || self.Owner == newOwner || self.Owner.IsAlliedWith(garrisoner.Owner)) + return; + + garrisoning = true; + self.ChangeOwner(newOwner); + + if (info.EnterSounds.Length > 0) + { + var pos = self.CenterPosition; + if (info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, info.EnterSounds, self.World, pos, null, info.SoundVolume); + } + + Game.Sound.PlayNotification(self.World.Map.Rules, garrisoner.Owner, "Speech", info.EnterNotification, newOwner.Faction.InternalName); + self.World.AddFrameEndTask(_ => garrisoning = false); + } + + void INotifyGarrisonerExited.OnGarrisonerExited(Actor self, Actor garrisoner) + { + if (garrison.GarrisonerCount > 0) + return; + + garrisoning = true; + + if (info.ExitSounds.Length > 0) + { + var pos = self.CenterPosition; + if (info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, info.ExitSounds, self.World, pos, null, info.SoundVolume); + } + + Game.Sound.PlayNotification(self.World.Map.Rules, garrisoner.Owner, "Speech", info.ExitNotification, garrisoner.Owner.Faction.InternalName); + self.ChangeOwner(originalOwner); + self.World.AddFrameEndTask(_ => garrisoning = false); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + if (!garrisoning) + originalOwner = newOwner; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/ChronoResourceDelivery.cs b/OpenRA.Mods.AS/Traits/ChronoResourceDelivery.cs new file mode 100644 index 000000000000..5403b3fded7c --- /dev/null +++ b/OpenRA.Mods.AS/Traits/ChronoResourceDelivery.cs @@ -0,0 +1,125 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("When returning to a refinery to deliver resources, this actor will teleport if possible.")] + public class ChronoResourceDeliveryInfo : ConditionalTraitInfo, Requires + { + [Desc("The number of ticks between each check to see if we can teleport to the refinery.")] + public readonly int CheckTeleportDelay = 10; + + [Desc("Image used for the teleport effects. Defaults to the actor's type.")] + public readonly string Image = null; + + [SequenceReference(nameof(Image), allowNullImage: true)] + [Desc("Sequence used for the effect played where the harvester jumped from.")] + public readonly string WarpInSequence = null; + + [SequenceReference(nameof(Image), allowNullImage: true)] + [Desc("Sequence used for the effect played where the harvester jumped to.")] + public readonly string WarpOutSequence = null; + + [PaletteReference] + [Desc("Palette to render the warp in/out sprites in.")] + public readonly string Palette = "effect"; + + [Desc("Sound played where the harvester jumped from.")] + public readonly string WarpInSound = null; + + [Desc("Sound where the harvester jumped to.")] + public readonly string WarpOutSound = null; + + [Desc("Does the sound play under shroud or fog.")] + public readonly bool AudibleThroughFog = true; + + [Desc("Volume the WarpInSound and WarpOutSound played at.")] + public readonly float SoundVolume = 1; + + [Desc("Should parasites be teleported along?")] + public readonly bool ExposeInfectors = true; + + public override object Create(ActorInitializer init) { return new ChronoResourceDelivery(this); } + } + + public class ChronoResourceDelivery : ConditionalTrait, INotifyHarvestAction, ITick + { + CPos? destination = null; + Actor refinery = null; + CPos harvestedField; + int ticksTillCheck = 0; + + public ChronoResourceDelivery(ChronoResourceDeliveryInfo info) + : base(info) { } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled || destination == null) + return; + + if (ticksTillCheck <= 0) + { + ticksTillCheck = Info.CheckTeleportDelay; + + TeleportIfPossible(self); + } + else + ticksTillCheck--; + } + + public void MovingToResources(Actor self, CPos targetCell) { Reset(); } + + public void MovingToDock(Actor self, Actor hostActor, IDockHost host) + { + var deliverypos = self.World.Map.CellContaining(host.DockPosition); + + if (destination != null && destination.Value != deliverypos) + ticksTillCheck = 0; + + harvestedField = self.World.Map.CellContaining(self.CenterPosition); + + refinery = hostActor; + destination = deliverypos; + } + + public void MovementCancelled(Actor self) { Reset(); } + + public void Harvested(Actor self, string resourceType) { } + public void Docked() { } + public void Undocked() { } + + void TeleportIfPossible(Actor self) + { + // We're already here; no need to interfere. + if (self.Location == destination.Value) + { + Reset(); + return; + } + + var pos = self.Trait(); + if (pos.CanEnterCell(destination.Value)) + { + self.QueueActivity(false, new ChronoResourceTeleport(destination.Value, Info, harvestedField, refinery)); + Reset(); + } + } + + void Reset() + { + ticksTillCheck = 0; + destination = null; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/ClearsResources.cs b/OpenRA.Mods.AS/Traits/ClearsResources.cs new file mode 100644 index 000000000000..f40a81e3282d --- /dev/null +++ b/OpenRA.Mods.AS/Traits/ClearsResources.cs @@ -0,0 +1,58 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Removes all the resources from the map when enabled.")] + public class ClearsResourcesInfo : ConditionalTraitInfo + { + [Desc("Resource types to remove with this trait.", "If empty, all resource types will be removed.")] + public readonly HashSet ResourceTypes = new(); + + public override object Create(ActorInitializer init) { return new ClearsResources(this, init.Self); } + } + + public class ClearsResources : ConditionalTrait + { + readonly IResourceLayer resourceLayer; + readonly ResourceRenderer resourceRenderer; + readonly PPos[] allCells; + + public ClearsResources(ClearsResourcesInfo info, Actor self) + : base(info) + { + resourceLayer = self.World.WorldActor.Trait(); + resourceRenderer = self.World.WorldActor.Trait(); + allCells = self.World.Map.ProjectedCells.ToArray(); + } + + protected override void TraitEnabled(Actor self) + { + var removeAllTypes = Info.ResourceTypes.Count == 0; + + foreach (var cell in allCells) + { + var pos = ((MPos)cell).ToCPos(self.World.Map); + var cellContents = resourceLayer.GetResource(pos); + + if (removeAllTypes || Info.ResourceTypes.Contains(cellContents.Type)) + { + resourceLayer.ClearResources(pos); + resourceRenderer.UpdateRenderedSprite(pos, RendererCellContents.Empty); + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantConditionAboveAltitude.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionAboveAltitude.cs new file mode 100644 index 000000000000..bf8c9b727899 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionAboveAltitude.cs @@ -0,0 +1,68 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants a condition above a certain altitude.")] + public class GrantConditionAboveAltitudeInfo : TraitInfo + { + [GrantedConditionReference] + [FieldLoader.Require] + [Desc("The condition to grant.")] + public readonly string Condition = null; + + public readonly int MinAltitude = 1; + + public override object Create(ActorInitializer init) { return new GrantConditionAboveAltitude(this); } + } + + public class GrantConditionAboveAltitude : ITick, INotifyAddedToWorld, INotifyRemovedFromWorld + { + readonly GrantConditionAboveAltitudeInfo info; + + int token = Actor.InvalidConditionToken; + + public GrantConditionAboveAltitude(GrantConditionAboveAltitudeInfo info) + { + this.info = info; + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + var altitude = self.World.Map.DistanceAboveTerrain(self.CenterPosition); + if (altitude.Length >= info.MinAltitude) + { + token = self.GrantCondition(info.Condition); + } + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + if (token != Actor.InvalidConditionToken) + token = self.RevokeCondition(token); + } + + void ITick.Tick(Actor self) + { + if (self.World.Map.DistanceAboveTerrain(self.CenterPosition).Length >= info.MinAltitude) + { + if (token == Actor.InvalidConditionToken) + token = self.GrantCondition(info.Condition); + } + else + { + if (token != Actor.InvalidConditionToken) + token = self.RevokeCondition(token); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantConditionAfterDelay.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionAfterDelay.cs new file mode 100644 index 000000000000..a5dd46edafc5 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionAfterDelay.cs @@ -0,0 +1,93 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Gives a condition to the actor after a delay.")] + public class GrantConditionAfterDelayInfo : PausableConditionalTraitInfo + { + [GrantedConditionReference] + [Desc("The condition to grant")] + public readonly string Condition = null; + + [Desc("Number of ticks to wait before applying the condition.")] + public readonly int Delay = 50; + + public readonly bool ShowSelectionBar = true; + public readonly bool ShowFullBarAfterGranted = true; + public readonly Color SelectionBarColor = Color.Magenta; + + public override object Create(ActorInitializer init) { return new GrantConditionAfterDelay(this); } + } + + public class GrantConditionAfterDelay : PausableConditionalTrait, ITick, ISync, ISelectionBar + { + readonly GrantConditionAfterDelayInfo info; + int token = Actor.InvalidConditionToken; + + [Sync] + public int Ticks { get; private set; } + + public GrantConditionAfterDelay(GrantConditionAfterDelayInfo info) + : base(info) + { + this.info = info; + Ticks = info.Delay; + } + + void GrantCondition(Actor self, string cond) + { + if (string.IsNullOrEmpty(cond)) + return; + + token = self.GrantCondition(cond); + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled) + Ticks = info.Delay; + + if (IsTraitPaused || IsTraitDisabled) + return; + + if (--Ticks < 0) + if (token == Actor.InvalidConditionToken) + GrantCondition(self, info.Condition); + } + + float ISelectionBar.GetValue() + { + if (IsTraitDisabled || !Info.ShowSelectionBar || (1f - (float)Ticks / Info.Delay > 1f && !info.ShowFullBarAfterGranted)) + return 0f; + + if (1f - (float)Ticks / Info.Delay > 1f && info.ShowFullBarAfterGranted) + return 1f; + + return 1f - (float)Ticks / Info.Delay; + } + + bool ISelectionBar.DisplayWhenEmpty { get { return !IsTraitDisabled && Info.ShowSelectionBar; } } + + Color ISelectionBar.GetColor() { return Info.SelectionBarColor; } + + protected override void TraitDisabled(Actor self) + { + if (token == Actor.InvalidConditionToken) + return; + + token = self.RevokeCondition(token); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnActivity.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnActivity.cs new file mode 100644 index 000000000000..abd2a7da703f --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnActivity.cs @@ -0,0 +1,87 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Activities; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public enum ActivityClass { FlyAttack, Fly, ReturnToBase } + + public class GrantConditionOnActivityInfo : TraitInfo + { + [Desc("Activity to grant condition on", + "Currently valid activities are `Fly`, `FlyAttack` and `ReturnToBase`.")] + public readonly ActivityClass Activity = ActivityClass.FlyAttack; + + [GrantedConditionReference] + [Desc("The condition to grant")] + public readonly string Condition = null; + + public override object Create(ActorInitializer init) { return new GrantConditionOnActivity(this); } + } + + public class GrantConditionOnActivity : ITick + { + readonly GrantConditionOnActivityInfo info; + + int token = Actor.InvalidConditionToken; + + public GrantConditionOnActivity(GrantConditionOnActivityInfo info) + { + this.info = info; + } + + void GrantCondition(Actor self, string cond) + { + if (string.IsNullOrEmpty(cond)) + return; + + token = self.GrantCondition(cond); + } + + void RevokeCondition(Actor self) + { + if (token == Actor.InvalidConditionToken) + return; + + token = self.RevokeCondition(token); + } + + bool IsValidActivity(Actor self) + { + if (self.CurrentActivity is Fly && info.Activity == ActivityClass.Fly) + return true; + + if (self.CurrentActivity is FlyAttack && info.Activity == ActivityClass.FlyAttack) + return true; + + if (self.CurrentActivity is ReturnToBase && info.Activity == ActivityClass.ReturnToBase) + return true; + + return false; + } + + void ITick.Tick(Actor self) + { + if (IsValidActivity(self)) + { + if (token == Actor.InvalidConditionToken) + GrantCondition(self, info.Condition); + } + else + { + if (token != Actor.InvalidConditionToken) + RevokeCondition(self); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnCapture.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnCapture.cs new file mode 100644 index 000000000000..4cbe588c7de5 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnCapture.cs @@ -0,0 +1,57 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants a condition after actor gets captured.")] + public class GrantConditionOnCaptureInfo : TraitInfo + { + [GrantedConditionReference] + [Desc("The condition to grant")] + public readonly string Condition = null; + + [Desc("Grant condition only if the capturer's CaptureTypes overlap with these types. Leave empty to allow all types.")] + public readonly BitSet CaptureTypes = default; + + public override object Create(ActorInitializer init) { return new GrantConditionOnCapture(init.Self, this); } + } + + public class GrantConditionOnCapture : INotifyCapture + { + readonly GrantConditionOnCaptureInfo info; + + int token = Actor.InvalidConditionToken; + + public GrantConditionOnCapture(Actor self, GrantConditionOnCaptureInfo info) + { + this.info = info; + } + + void GrantCondition(Actor self, string cond) + { + if (string.IsNullOrEmpty(cond)) + return; + + if (token == Actor.InvalidConditionToken) + token = self.GrantCondition(cond); + } + + void INotifyCapture.OnCapture(Actor self, Actor captor, Player oldOwner, Player newOwner, BitSet captureTypes) + { + if (info.CaptureTypes.IsEmpty || info.CaptureTypes.Overlaps(captureTypes)) + GrantCondition(self, info.Condition); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnHarvest.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnHarvest.cs new file mode 100644 index 000000000000..96602ff488e8 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnHarvest.cs @@ -0,0 +1,60 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class GrantConditionOnHarvestInfo : TraitInfo, Requires + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant after harvesting.")] + public readonly string Condition = null; + + [Desc("How long the condition lasts for.")] + public readonly int Duration = 25; + + public override object Create(ActorInitializer init) { return new GrantConditionOnHarvest(this); } + } + + public class GrantConditionOnHarvest : INotifyHarvestAction, ITick + { + public readonly GrantConditionOnHarvestInfo Info; + + int token = Actor.InvalidConditionToken; + int timer; + + public GrantConditionOnHarvest(GrantConditionOnHarvestInfo info) + { + Info = info; + } + + void ITick.Tick(Actor self) + { + if (token == Actor.InvalidConditionToken) + return; + + if (--timer <= 0) + token = self.RevokeCondition(token); + } + + void INotifyHarvestAction.Harvested(Actor self, string resourceType) + { + timer = Info.Duration; + if (token == Actor.InvalidConditionToken) + token = self.GrantCondition(Info.Condition); + } + + void INotifyHarvestAction.MovingToResources(Actor self, CPos targetCell) { } + void INotifyHarvestAction.MovementCancelled(Actor self) { } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnInfiltration.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnInfiltration.cs new file mode 100644 index 000000000000..b5230704b320 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnInfiltration.cs @@ -0,0 +1,79 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants a condition when this building is infiltrated.")] + class GrantConditionOnInfiltrationInfo : ConditionalTraitInfo + { + [Desc("The `TargetTypes` from `Targetable` that are allowed to enter.")] + public readonly BitSet Types = default; + + [FieldLoader.Require] + [GrantedConditionReference] + public readonly string Condition = null; + + [Desc("Use `TimedConditionBar` for visualization.")] + public readonly int Duration = 0; + + public override object Create(ActorInitializer init) { return new GrantConditionOnInfiltration(this); } + } + + class GrantConditionOnInfiltration : ConditionalTrait, INotifyInfiltrated, INotifyCreated, ITick + { + int conditionToken = Actor.InvalidConditionToken; + int duration; + IConditionTimerWatcher[] watchers; + + public GrantConditionOnInfiltration(GrantConditionOnInfiltrationInfo info) + : base(info) { } + + void INotifyInfiltrated.Infiltrated(Actor self, Actor infiltrator, BitSet types) + { + if (!Info.Types.Overlaps(types) || IsTraitDisabled) + return; + + duration = Info.Duration; + + if (conditionToken == Actor.InvalidConditionToken) + conditionToken = self.GrantCondition(Info.Condition); + } + + bool Notifies(IConditionTimerWatcher watcher) { return watcher.Condition == Info.Condition; } + + protected override void Created(Actor self) + { + watchers = self.TraitsImplementing().Where(Notifies).ToArray(); + + base.Created(self); + } + + void ITick.Tick(Actor self) + { + if (conditionToken != Actor.InvalidConditionToken && Info.Duration > 0) + { + if (--duration < 0) + { + conditionToken = self.RevokeCondition(conditionToken); + foreach (var w in watchers) + w.Update(0, 0); + } + else + foreach (var w in watchers) + w.Update(Info.Duration, duration); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnInternalOwner.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnInternalOwner.cs new file mode 100644 index 000000000000..699805d5fd61 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnInternalOwner.cs @@ -0,0 +1,72 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants a condition when owner of the actor is a specified player.")] + public class GrantConditionOnInternalOwnerInfo : TraitInfo + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant")] + public readonly string Condition = null; + + [FieldLoader.Require] + [Desc("Map player names to grant the condition to.")] + public readonly HashSet InternalOwners = new(); + + public override object Create(ActorInitializer init) { return new GrantConditionOnInternalOwner(init.Self, this); } + } + + public class GrantConditionOnInternalOwner : INotifyOwnerChanged + { + readonly GrantConditionOnInternalOwnerInfo info; + + int token = Actor.InvalidConditionToken; + + public GrantConditionOnInternalOwner(Actor self, GrantConditionOnInternalOwnerInfo info) + { + this.info = info; + + if (info.InternalOwners.Contains(self.Owner.InternalName)) + GrantCondition(self, info.Condition); + else + RevokeCondition(self); + } + + void GrantCondition(Actor self, string cond) + { + if (token != Actor.InvalidConditionToken) + return; + + token = self.GrantCondition(cond); + } + + void RevokeCondition(Actor self) + { + if (token == Actor.InvalidConditionToken) + return; + + token = self.RevokeCondition(token); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + if (info.InternalOwners.Contains(self.Owner.InternalName)) + GrantCondition(self, info.Condition); + else + RevokeCondition(self); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnResourceDelivery.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnResourceDelivery.cs new file mode 100644 index 000000000000..be132811bb9f --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnResourceDelivery.cs @@ -0,0 +1,64 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants a condition when this refinery receives resources.")] + public class GrantConditionOnResourceDeliveryInfo : PausableConditionalTraitInfo, Requires + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant.")] + public readonly string Condition = null; + + [FieldLoader.Require] + public readonly int Duration; + + public override object Create(ActorInitializer init) { return new GrantConditionOnResourceDelivery(this); } + } + + public class GrantConditionOnResourceDelivery : PausableConditionalTrait, ITick, INotifyCreated, IRefineryResourceDelivered + { + readonly GrantConditionOnResourceDeliveryInfo info; + + int token = Actor.InvalidConditionToken; + + int ticks; + + public GrantConditionOnResourceDelivery(GrantConditionOnResourceDeliveryInfo info) + : base(info) + { + this.info = info; + } + + void IRefineryResourceDelivered.ResourceGiven(Actor self, int amount) + { + if (IsTraitDisabled) + return; + + ticks = info.Duration; + + if (token == Actor.InvalidConditionToken) + token = self.GrantCondition(info.Condition); + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled || IsTraitPaused || --ticks > 0) + return; + + if (token != Actor.InvalidConditionToken) + token = self.RevokeCondition(token); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnResourcePurify.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnResourcePurify.cs new file mode 100644 index 000000000000..529dbb20af74 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantConditionOnResourcePurify.cs @@ -0,0 +1,66 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants a condition when a refinery receives resources.")] + public class GrantConditionOnResourcePurifyInfo : PausableConditionalTraitInfo + { + [GrantedConditionReference] + [FieldLoader.Require] + [Desc("The condition to grant.")] + public readonly string Condition = null; + + [FieldLoader.Require] + public readonly int Duration; + + public override object Create(ActorInitializer init) { return new GrantConditionOnResourcePurify(init.Self, this); } + } + + public class GrantConditionOnResourcePurify : PausableConditionalTrait, ITick, IResourcePurifier + { + readonly Actor self; + readonly GrantConditionOnResourcePurifyInfo info; + + int token = Actor.InvalidConditionToken; + + int ticks; + + public GrantConditionOnResourcePurify(Actor self, GrantConditionOnResourcePurifyInfo info) + : base(info) + { + this.self = self; + this.info = info; + } + + void IResourcePurifier.RefineAmount(int amount) + { + if (IsTraitDisabled) + return; + + ticks = info.Duration; + + if (token == Actor.InvalidConditionToken) + token = self.GrantCondition(info.Condition); + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled || IsTraitPaused || --ticks > 0) + return; + + if (token != Actor.InvalidConditionToken) + token = self.RevokeCondition(token); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantExternalConditionToKiller.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantExternalConditionToKiller.cs new file mode 100644 index 000000000000..4d5a390fef83 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantExternalConditionToKiller.cs @@ -0,0 +1,62 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grant an external condition to the killer.")] + public class GrantExternalConditionToKillerInfo : TraitInfo + { + [Desc("The condition to apply. Must be included among the target actor's ExternalCondition traits.")] + public readonly string Condition = null; + + [Desc("Duration of the condition (in ticks). Set to 0 for a permanent upgrade.")] + public readonly int Duration = 0; + + [Desc("Stance the attacking player needs to receive the condition.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Neutral | PlayerRelationship.Enemy; + + [Desc("DeathType(s) that grant the condition. Leave empty to always grant the condition.")] + public readonly BitSet DeathTypes = default; + + public override object Create(ActorInitializer init) { return new GrantExternalConditionToKiller(this); } + } + + public class GrantExternalConditionToKiller : INotifyKilled + { + public readonly GrantExternalConditionToKillerInfo Info; + + public GrantExternalConditionToKiller(GrantExternalConditionToKillerInfo info) + { + Info = info; + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (e.Attacker == null || e.Attacker.Disposed) + return; + + if (!Info.DeathTypes.IsEmpty && !e.Damage.DamageTypes.Overlaps(Info.DeathTypes)) + return; + + if (!Info.ValidRelationships.HasRelationship(e.Attacker.Owner.RelationshipWith(self.Owner))) + return; + + var external = e.Attacker.TraitsImplementing() + .FirstOrDefault(t => t.Info.Condition == Info.Condition && t.CanGrantCondition(self)); + + external?.GrantCondition(e.Attacker, self, Info.Duration); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantExternalConditionToOwner.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantExternalConditionToOwner.cs new file mode 100644 index 000000000000..888bb2c8be04 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantExternalConditionToOwner.cs @@ -0,0 +1,85 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants an external condition to the owner player's actor.")] + class GrantExternalConditionToOwnerInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + public readonly string Condition = null; + + public override object Create(ActorInitializer init) { return new GrantExternalConditionToOwner(this); } + } + + class GrantExternalConditionToOwner : ConditionalTrait, INotifyRemovedFromWorld, INotifyAddedToWorld, INotifyOwnerChanged, INotifyKilled + { + int conditionToken = Actor.InvalidConditionToken; + ExternalCondition playerConditionTrait; + + public GrantExternalConditionToOwner(GrantExternalConditionToOwnerInfo info) + : base(info) { } + + protected override void Created(Actor self) + { + base.Created(self); + + UpdatePlayerConditionReference(self); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + UpdatePlayerConditionReference(self); + } + + protected override void TraitEnabled(Actor self) + { + if (!self.IsDead && self.IsInWorld && conditionToken == Actor.InvalidConditionToken) + conditionToken = playerConditionTrait.GrantCondition(self.Owner.PlayerActor, self); + } + + protected override void TraitDisabled(Actor self) + { + if (!self.IsDead && self.IsInWorld && conditionToken != Actor.InvalidConditionToken) + if (playerConditionTrait.TryRevokeCondition(self.Owner.PlayerActor, self, conditionToken)) + conditionToken = Actor.InvalidConditionToken; + } + + void UpdatePlayerConditionReference(Actor self) + { + playerConditionTrait = self.Owner.PlayerActor.TraitsImplementing() + .FirstOrDefault(t => t.Info.Condition == Info.Condition); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + if (!self.IsDead && !IsTraitDisabled && conditionToken == Actor.InvalidConditionToken) + conditionToken = playerConditionTrait.GrantCondition(self.Owner.PlayerActor, self); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + if (!self.IsDead && !IsTraitDisabled && conditionToken != Actor.InvalidConditionToken) + if (playerConditionTrait.TryRevokeCondition(self.Owner.PlayerActor, self, conditionToken)) + conditionToken = Actor.InvalidConditionToken; + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (conditionToken != Actor.InvalidConditionToken) + if (playerConditionTrait.TryRevokeCondition(self.Owner.PlayerActor, self, conditionToken)) + conditionToken = Actor.InvalidConditionToken; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantExternalConditionToSpawnedMissile.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantExternalConditionToSpawnedMissile.cs new file mode 100644 index 000000000000..70de52df4b7c --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantExternalConditionToSpawnedMissile.cs @@ -0,0 +1,79 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class GrantExternalConditionToSpawnedMissileInfo : ConditionalTraitInfo, Requires + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant to the missiles.")] + public readonly string Condition = null; + + public override object Create(ActorInitializer init) { return new GrantExternalConditionToSpawnedMissile(init, this); } + } + + public class GrantExternalConditionToSpawnedMissile : ConditionalTrait + { + readonly MissileSpawnerMaster spawner; + readonly Dictionary tokens = new(); + + public GrantExternalConditionToSpawnedMissile(ActorInitializer init, GrantExternalConditionToSpawnedMissileInfo info) + : base(info) + { + spawner = init.Self.Trait(); + } + + public void GrantCondition(Actor self, Actor slave) + { + if (tokens.ContainsKey(slave)) + return; + + var external = slave.TraitsImplementing() + .FirstOrDefault(t => t.Info.Condition == Info.Condition && t.CanGrantCondition(self)); + + if (external != null) + tokens[slave] = external.GrantCondition(slave, self); + } + + protected override void TraitEnabled(Actor self) + { + foreach (var se in spawner.SlaveEntries) + { + if (!se.IsValid) + continue; + + GrantCondition(self, se.Actor); + } + } + + protected override void TraitDisabled(Actor self) + { + foreach (var se in spawner.SlaveEntries) + { + if (!se.IsValid) + continue; + + var a = se.Actor; + if (!tokens.TryGetValue(a, out var token)) + continue; + + foreach (var external in a.TraitsImplementing()) + if (external.TryRevokeCondition(a, self, token)) + break; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantHordeBonus.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantHordeBonus.cs new file mode 100644 index 000000000000..69e2f6b12042 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantHordeBonus.cs @@ -0,0 +1,33 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants horde types for the HordeBonus traits to work.")] + public class GrantHordeBonusInfo : TraitInfo + { + public readonly string HordeType = "horde"; + + public override object Create(ActorInitializer init) { return new GrantHordeBonus(this); } + } + + public class GrantHordeBonus + { + public readonly GrantHordeBonusInfo Info; + + public GrantHordeBonus(GrantHordeBonusInfo info) + { + Info = info; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantPeriodicCondition.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantPeriodicCondition.cs new file mode 100644 index 000000000000..696d8aa141af --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantPeriodicCondition.cs @@ -0,0 +1,171 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants a condition periodically.")] + public class GrantPeriodicConditionInfo : PausableConditionalTraitInfo + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant.")] + public readonly string Condition = null; + + [Desc("The range of time (in ticks) with the condition being disabled.")] + public readonly int[] CooldownDuration = { 1000 }; + + [Desc("The range of time (in ticks) with the condition being enabled.")] + public readonly int[] ActiveDuration = { 100 }; + + public readonly bool StartsGranted = false; + + public readonly bool ShowSelectionBar = false; + public readonly Color CooldownColor = Color.DarkRed; + public readonly Color ActiveColor = Color.DarkMagenta; + + public override object Create(ActorInitializer init) { return new GrantPeriodicCondition(init, this); } + } + + public class GrantPeriodicCondition : PausableConditionalTrait, ISelectionBar, ITick, ISync + { + readonly Actor self; + readonly GrantPeriodicConditionInfo info; + + [Sync] + int ticks; + + int cooldown, active; + bool isSuspended; + int token = Actor.InvalidConditionToken; + + bool IsEnabled { get { return token != Actor.InvalidConditionToken; } } + + public GrantPeriodicCondition(ActorInitializer init, GrantPeriodicConditionInfo info) + : base(info) + { + self = init.Self; + this.info = info; + } + + void SetDefaultState() + { + if (info.StartsGranted) + { + ticks = info.ActiveDuration.Length == 2 + ? self.World.SharedRandom.Next(info.ActiveDuration[0], info.ActiveDuration[1]) + : info.ActiveDuration[0]; + active = ticks; + if (info.StartsGranted != IsEnabled) + EnableCondition(); + } + else + { + ticks = info.CooldownDuration.Length == 2 + ? self.World.SharedRandom.Next(info.CooldownDuration[0], info.CooldownDuration[1]) + : info.CooldownDuration[0]; + cooldown = ticks; + if (info.StartsGranted != IsEnabled) + DisableCondition(); + } + + isSuspended = false; + } + + protected override void Created(Actor self) + { + if (!IsTraitDisabled) + SetDefaultState(); + + base.Created(self); + } + + void ITick.Tick(Actor self) + { + if (!IsTraitDisabled && !IsTraitPaused && --ticks < 0) + { + if (IsEnabled) + { + ticks = info.CooldownDuration.Length == 2 + ? self.World.SharedRandom.Next(info.CooldownDuration[0], info.CooldownDuration[1]) + : info.CooldownDuration[0]; + cooldown = ticks; + DisableCondition(); + } + else + { + ticks = info.ActiveDuration.Length == 2 + ? self.World.SharedRandom.Next(info.ActiveDuration[0], info.ActiveDuration[1]) + : info.ActiveDuration[0]; + active = ticks; + EnableCondition(); + } + } + } + + protected override void TraitEnabled(Actor self) + { + SetDefaultState(); + } + + protected override void TraitDisabled(Actor self) + { + if (IsEnabled) + DisableCondition(); + } + + protected override void TraitPaused(Actor self) + { + if (IsEnabled) + { + DisableCondition(); + isSuspended = true; + } + } + + protected override void TraitResumed(Actor self) + { + if (isSuspended) + { + EnableCondition(); + isSuspended = false; + } + } + + void EnableCondition() + { + if (token == Actor.InvalidConditionToken) + token = self.GrantCondition(info.Condition); + } + + void DisableCondition() + { + if (token != Actor.InvalidConditionToken) + token = self.RevokeCondition(token); + } + + float ISelectionBar.GetValue() + { + if (!info.ShowSelectionBar) + return 0f; + + return IsEnabled + ? (float)(active - ticks) / active + : (float)ticks / cooldown; + } + + bool ISelectionBar.DisplayWhenEmpty { get { return info.ShowSelectionBar; } } + + Color ISelectionBar.GetColor() { return IsEnabled ? info.ActiveColor : info.CooldownColor; } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantPeriodicConditionOnEvent.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantPeriodicConditionOnEvent.cs new file mode 100644 index 000000000000..712a9ffc8cb8 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantPeriodicConditionOnEvent.cs @@ -0,0 +1,217 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Flags] + public enum PeriodicConditionTrigger + { + None = 0, + Attack = 1, + Move = 2, + Damage = 4, + Heal = 8 + } + + [Desc("Grants a condition when a selected event occurs.")] + public class GrantPeriodicConditionOnEventInfo : PausableConditionalTraitInfo + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant.")] + public readonly string Condition = null; + + [Desc("The range of time (in ticks) with the condition being disabled.")] + public readonly int[] CooldownDuration = { 1000 }; + + [Desc("The range of time (in ticks) with the condition being enabled.")] + public readonly int[] ActiveDuration = { 100 }; + + public readonly PeriodicConditionTrigger Triggers = PeriodicConditionTrigger.Damage; + + public readonly bool StartsCharged = false; + + public readonly bool ShowSelectionBar = false; + public readonly Color CooldownColor = Color.DarkRed; + public readonly Color ActiveColor = Color.DarkMagenta; + + public override object Create(ActorInitializer init) { return new GrantPeriodicConditionOnEvent(init, this); } + } + + public enum PeriodicConditionState { Charging, Ready, Active } + + public class GrantPeriodicConditionOnEvent : PausableConditionalTrait, ISelectionBar, + ITick, ISync, INotifyDamage, INotifyAttack + { + readonly Actor self; + readonly GrantPeriodicConditionOnEventInfo info; + + [Sync] + int ticks; + + int cooldown, active; + int token = Actor.InvalidConditionToken; + WPos? lastPos; + + bool IsEnabled { get { return token != Actor.InvalidConditionToken; } } + + PeriodicConditionState state; + + public GrantPeriodicConditionOnEvent(ActorInitializer init, GrantPeriodicConditionOnEventInfo info) + : base(info) + { + self = init.Self; + this.info = info; + } + + void SetDefaultState() + { + if (info.StartsCharged) + { + ticks = info.ActiveDuration.Length == 2 + ? self.World.SharedRandom.Next(info.ActiveDuration[0], info.ActiveDuration[1]) + : info.ActiveDuration[0]; + active = ticks; + state = PeriodicConditionState.Ready; + } + else + { + ticks = info.CooldownDuration.Length == 2 + ? self.World.SharedRandom.Next(info.CooldownDuration[0], info.CooldownDuration[1]) + : info.CooldownDuration[0]; + cooldown = ticks; + state = PeriodicConditionState.Charging; + if (IsEnabled) + DisableCondition(); + } + } + + protected override void Created(Actor self) + { + if (!IsTraitDisabled) + SetDefaultState(); + + base.Created(self); + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled || IsTraitPaused) + return; + + if (state != PeriodicConditionState.Ready && --ticks < 0) + { + if (IsEnabled) + { + ticks = info.CooldownDuration.Length == 2 + ? self.World.SharedRandom.Next(info.CooldownDuration[0], info.CooldownDuration[1]) + : info.CooldownDuration[0]; + cooldown = ticks; + DisableCondition(); + state = PeriodicConditionState.Charging; + } + else + { + ticks = info.ActiveDuration.Length == 2 + ? self.World.SharedRandom.Next(info.ActiveDuration[0], info.ActiveDuration[1]) + : info.ActiveDuration[0]; + active = ticks; + state = PeriodicConditionState.Ready; + } + } + + if (Info.Triggers.HasFlag(PeriodicConditionTrigger.Move)) + { + if (state == PeriodicConditionState.Ready && (lastPos == null || lastPos.Value != self.CenterPosition)) + TryEnableCondition(); + + lastPos = self.CenterPosition; + } + } + + protected override void TraitEnabled(Actor self) + { + SetDefaultState(); + } + + protected override void TraitDisabled(Actor self) + { + if (IsEnabled) + { + DisableCondition(); + state = PeriodicConditionState.Ready; + } + } + + protected override void TraitPaused(Actor self) + { + TraitDisabled(self); + } + + protected override void TraitResumed(Actor self) + { + TryEnableCondition(); + } + + void TryEnableCondition() + { + if (IsTraitDisabled) + return; + + if (state == PeriodicConditionState.Ready && token == Actor.InvalidConditionToken) + { + token = self.GrantCondition(info.Condition); + state = PeriodicConditionState.Active; + } + } + + void DisableCondition() + { + if (token != Actor.InvalidConditionToken) + token = self.RevokeCondition(token); + } + + float ISelectionBar.GetValue() + { + if (!info.ShowSelectionBar || IsTraitDisabled) + return 0f; + + return state != PeriodicConditionState.Charging + ? (float)ticks / active + : (float)(cooldown - ticks) / cooldown; + } + + bool ISelectionBar.DisplayWhenEmpty { get { return info.ShowSelectionBar && !IsTraitDisabled; } } + + Color ISelectionBar.GetColor() { return state == PeriodicConditionState.Charging ? info.CooldownColor : info.ActiveColor; } + + void INotifyDamage.Damaged(Actor self, AttackInfo e) + { + if (Info.Triggers.HasFlag(PeriodicConditionTrigger.Damage) && e.Damage.Value > 0) + TryEnableCondition(); + + if (Info.Triggers.HasFlag(PeriodicConditionTrigger.Heal) && e.Damage.Value < 0) + TryEnableCondition(); + } + + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + if (Info.Triggers.HasFlag(PeriodicConditionTrigger.Attack)) + TryEnableCondition(); + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantRandomConditionOnOwnerChange.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantRandomConditionOnOwnerChange.cs new file mode 100644 index 000000000000..b53a9eb8f0a2 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantRandomConditionOnOwnerChange.cs @@ -0,0 +1,58 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants a random condition from a predefined list to the actor when created." + + "Rerandomized when the actor changes ownership.")] + public class GrantRandomConditionOnOwnerChangeInfo : TraitInfo + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("List of conditions to grant from.")] + public readonly string[] Conditions = null; + + public override object Create(ActorInitializer init) { return new GrantRandomConditionOnOwnerChange(this); } + } + + public class GrantRandomConditionOnOwnerChange : INotifyCreated, INotifyOwnerChanged + { + readonly GrantRandomConditionOnOwnerChangeInfo info; + + int conditionToken = Actor.InvalidConditionToken; + + public GrantRandomConditionOnOwnerChange(GrantRandomConditionOnOwnerChangeInfo info) + { + this.info = info; + } + + void INotifyCreated.Created(Actor self) + { + if (!info.Conditions.Any()) + return; + + var condition = info.Conditions.Random(self.World.SharedRandom); + conditionToken = self.GrantCondition(condition); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + if (conditionToken != Actor.InvalidConditionToken) + { + self.RevokeCondition(conditionToken); + var condition = info.Conditions.Random(self.World.SharedRandom); + conditionToken = self.GrantCondition(condition); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantTimedCondition.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantTimedCondition.cs new file mode 100644 index 000000000000..245296c8e01b --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantTimedCondition.cs @@ -0,0 +1,97 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Gives a condition to the actor for a limited time.")] + public class GrantTimedConditionInfo : PausableConditionalTraitInfo + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant.")] + public readonly string Condition = null; + + [Desc("Number of ticks to wait before revoking the condition.")] + public readonly int Duration = 50; + + public override object Create(ActorInitializer init) { return new GrantTimedCondition(this); } + } + + public class GrantTimedCondition : PausableConditionalTrait, ITick, ISync, INotifyCreated + { + readonly GrantTimedConditionInfo info; + int token = Actor.InvalidConditionToken; + IConditionTimerWatcher[] watchers; + + [Sync] + public int Ticks { get; private set; } + + public GrantTimedCondition(GrantTimedConditionInfo info) + : base(info) + { + this.info = info; + Ticks = info.Duration; + } + + protected override void Created(Actor self) + { + watchers = self.TraitsImplementing().Where(Notifies).ToArray(); + + base.Created(self); + } + + void GrantCondition(Actor self, string condition) + { + if (string.IsNullOrEmpty(condition)) + return; + + if (token == Actor.InvalidConditionToken) + { + Ticks = info.Duration; + token = self.GrantCondition(condition); + } + } + + void RevokeCondition(Actor self) + { + if (token != Actor.InvalidConditionToken) + token = self.RevokeCondition(token); + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled && token != Actor.InvalidConditionToken) + RevokeCondition(self); + + if (IsTraitPaused || IsTraitDisabled) + return; + + foreach (var w in watchers) + w.Update(info.Duration, Ticks); + + if (token == Actor.InvalidConditionToken) + return; + + if (--Ticks < 0) + RevokeCondition(self); + } + + protected override void TraitEnabled(Actor self) + { + GrantCondition(self, info.Condition); + } + + bool Notifies(IConditionTimerWatcher watcher) { return watcher.Condition == Info.Condition; } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/GrantTimedConditionOnDeploy.cs b/OpenRA.Mods.AS/Traits/Conditions/GrantTimedConditionOnDeploy.cs new file mode 100644 index 000000000000..a1187876085f --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/GrantTimedConditionOnDeploy.cs @@ -0,0 +1,276 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Activities; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Orders; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class GrantTimedConditionOnDeployInfo : PausableConditionalTraitInfo + { + [GrantedConditionReference] + [Desc("The condition granted during deploying.")] + public readonly string DeployingCondition = null; + + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition granted after deploying.")] + public readonly string DeployedCondition = null; + + [Desc("Cooldown in ticks until the unit can deploy.")] + public readonly int CooldownTicks; + + [Desc("The deployed state's length in ticks.")] + public readonly int DeployedTicks; + + [Desc("Cursor to display when able to (un)deploy the actor.")] + public readonly string DeployCursor = "deploy"; + + [Desc("Cursor to display when unable to (un)deploy the actor.")] + public readonly string DeployBlockedCursor = "deploy-blocked"; + + [SequenceReference] + [Desc("Animation to play for deploying.")] + public readonly string DeployAnimation = null; + + [SequenceReference] + [Desc("Animation to play for undeploying.")] + public readonly string UndeployAnimation = null; + + [Desc("Apply (un)deploy animations to sprite bodies with these names.")] + public readonly string[] BodyNames = { "body" }; + + [Desc("Facing that the actor must face before deploying. Set to -1 to deploy regardless of facing.")] + public readonly int Facing = -1; + + [Desc("Sound to play when deploying.")] + public readonly string DeploySound = null; + + [Desc("Sound to play when undeploying.")] + public readonly string UndeploySound = null; + + public readonly bool StartsFullyCharged = false; + + [VoiceReference] + public readonly string Voice = "Action"; + + public readonly bool ShowSelectionBar = true; + public readonly Color ChargingColor = Color.DarkRed; + public readonly Color DischargingColor = Color.DarkMagenta; + + public override object Create(ActorInitializer init) { return new GrantTimedConditionOnDeploy(init.Self, this); } + } + + public enum TimedDeployState { Charging, Ready, Active, Deploying, Undeploying } + + public class GrantTimedConditionOnDeploy : PausableConditionalTrait, + IResolveOrder, IIssueOrder, ISelectionBar, IOrderVoice, ISync, ITick, IIssueDeployOrder + { + readonly Actor self; + readonly bool canTurn; + int deployedToken = Actor.InvalidConditionToken; + int deployingToken = Actor.InvalidConditionToken; + + [Sync] + int ticks; + + WithSpriteBody[] wsbs; + TimedDeployState deployState; + + public GrantTimedConditionOnDeploy(Actor self, GrantTimedConditionOnDeployInfo info) + : base(info) + { + this.self = self; + canTurn = self.Info.HasTraitInfo(); + } + + protected override void Created(Actor self) + { + wsbs = self.TraitsImplementing().Where(w => Info.BodyNames.Contains(w.Info.Name)).ToArray(); + + if (Info.StartsFullyCharged) + { + ticks = Info.DeployedTicks; + deployState = TimedDeployState.Ready; + } + else + { + ticks = Info.CooldownTicks; + deployState = TimedDeployState.Charging; + } + + base.Created(self); + } + + Order IIssueDeployOrder.IssueDeployOrder(Actor self, bool queued) + { + return new Order("GrantTimedConditionOnDeploy", self, queued); + } + + bool IIssueDeployOrder.CanIssueDeployOrder(Actor self, bool queued) { return !IsTraitPaused && !IsTraitDisabled; } + + IEnumerable IIssueOrder.Orders + { + get + { + if (!IsTraitDisabled) + yield return new DeployOrderTargeter("GrantTimedConditionOnDeploy", 5, + () => IsCursorBlocked() ? Info.DeployBlockedCursor : Info.DeployCursor); + } + } + + Order IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) + { + if (order.OrderID == "GrantTimedConditionOnDeploy") + return new Order(order.OrderID, self, queued); + + return null; + } + + void IResolveOrder.ResolveOrder(Actor self, Order order) + { + if (IsTraitDisabled || IsTraitPaused) + return; + + if (order.OrderString != "GrantTimedConditionOnDeploy" || deployState != TimedDeployState.Ready) + return; + + if (!order.Queued) + self.CancelActivity(); + + // Turn to the required facing. + if (Info.Facing != -1 && canTurn) + self.QueueActivity(new Turn(self, WAngle.FromFacing(Info.Facing))); + + self.QueueActivity(new CallFunc(Deploy)); + } + + bool IsCursorBlocked() + { + return deployState != TimedDeployState.Ready || IsTraitPaused; + } + + string IOrderVoice.VoicePhraseForOrder(Actor self, Order order) + { + return order.OrderString == "GrantTimedConditionOnDeploy" && deployState == TimedDeployState.Ready ? Info.Voice : null; + } + + void Deploy() + { + // Something went wrong, most likely due to deploy order spam and the fact that this is a delayed action. + if (deployState != TimedDeployState.Ready) + return; + + deployState = TimedDeployState.Deploying; + + if (!string.IsNullOrEmpty(Info.DeploySound)) + Game.Sound.Play(SoundType.World, Info.DeploySound, self.CenterPosition); + + var wsb = wsbs.FirstEnabledTraitOrDefault(); + + // If there is no animation to play just grant the upgrades that are used while deployed. + // Alternatively, play the deploy animation and then grant the upgrades. + if (string.IsNullOrEmpty(Info.DeployAnimation) || wsb == null) + OnDeployCompleted(); + else + { + if (!string.IsNullOrEmpty(Info.DeployingCondition) && deployingToken == Actor.InvalidConditionToken) + deployingToken = self.GrantCondition(Info.DeployingCondition); + wsb.PlayCustomAnimation(self, Info.DeployAnimation, OnDeployCompleted); + } + } + + void OnDeployCompleted() + { + if (!string.IsNullOrEmpty(Info.DeployedCondition) && deployedToken == Actor.InvalidConditionToken) + deployedToken = self.GrantCondition(Info.DeployedCondition); + + if (deployingToken != Actor.InvalidConditionToken) + deployingToken = self.RevokeCondition(deployingToken); + + deployState = TimedDeployState.Active; + } + + void RevokeDeploy() + { + deployState = TimedDeployState.Undeploying; + + if (!string.IsNullOrEmpty(Info.UndeploySound)) + Game.Sound.Play(SoundType.World, Info.UndeploySound, self.CenterPosition); + + var wsb = wsbs.FirstEnabledTraitOrDefault(); + + if (string.IsNullOrEmpty(Info.UndeployAnimation) || wsb == null) + OnUndeployCompleted(); + else + { + if (!string.IsNullOrEmpty(Info.DeployingCondition) && deployingToken == Actor.InvalidConditionToken) + deployingToken = self.GrantCondition(Info.DeployingCondition); + wsb.PlayCustomAnimation(self, Info.UndeployAnimation, OnUndeployCompleted); + } + } + + void OnUndeployCompleted() + { + if (deployedToken != Actor.InvalidConditionToken) + deployedToken = self.RevokeCondition(deployedToken); + + if (deployingToken != Actor.InvalidConditionToken) + deployingToken = self.RevokeCondition(deployingToken); + + deployState = TimedDeployState.Charging; + ticks = Info.CooldownTicks; + } + + void ITick.Tick(Actor self) + { + if (IsTraitPaused || IsTraitDisabled) + return; + + if (deployState == TimedDeployState.Ready || deployState == TimedDeployState.Deploying || deployState == TimedDeployState.Undeploying) + return; + + if (--ticks < 0) + { + if (deployState == TimedDeployState.Charging) + { + ticks = Info.DeployedTicks; + deployState = TimedDeployState.Ready; + } + else + RevokeDeploy(); + } + } + + float ISelectionBar.GetValue() + { + if (IsTraitDisabled || !Info.ShowSelectionBar || deployState == TimedDeployState.Undeploying) + return 0f; + + if (deployState == TimedDeployState.Deploying || deployState == TimedDeployState.Ready) + return 1f; + + return deployState == TimedDeployState.Charging + ? (float)(Info.CooldownTicks - ticks) / Info.CooldownTicks + : (float)ticks / Info.DeployedTicks; + } + + bool ISelectionBar.DisplayWhenEmpty { get { return !IsTraitDisabled && Info.ShowSelectionBar; } } + + Color ISelectionBar.GetColor() { return deployState == TimedDeployState.Charging ? Info.ChargingColor : Info.DischargingColor; } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/HordeBonus.cs b/OpenRA.Mods.AS/Traits/Conditions/HordeBonus.cs new file mode 100644 index 000000000000..20411004414f --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/HordeBonus.cs @@ -0,0 +1,176 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants a condition based on the amount of actors with an eligible GrantHordeBonus trait around this actor.")] + public class HordeBonusInfo : ConditionalTraitInfo + { + [Desc("The range within eligible GrantHordeBonus actors are considered.")] + public readonly WDist Range = WDist.FromCells(2); + + [Desc("The maximum vertical range above terrain within the actors get considered.", + "Ignored if 0 (actors are considered regardless of vertical distance).")] + public readonly WDist MaximumVerticalOffset = WDist.Zero; + + [Desc("What diplomatic stances are considered.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Ally; + + [Desc("Specifies the eligible GrantHordeBonus trait type.")] + public readonly string HordeType = "horde"; + + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant.")] + public readonly string Condition = null; + + public readonly int Minimum = 4; + public readonly int Maximum = int.MaxValue; + + public readonly string EnableSound = null; + public readonly string DisableSound = null; + + public override object Create(ActorInitializer init) { return new HordeBonus(init.Self, this); } + } + + public class HordeBonus : ConditionalTrait, ITick, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOtherProduction + { + readonly Actor self; + + int proximityTrigger; + WPos cachedPosition; + WDist cachedRange; + WDist desiredRange; + WDist cachedVRange; + WDist desiredVRange; + + bool cachedDisabled = true; + + readonly HashSet sources; + + int token = Actor.InvalidConditionToken; + + bool IsEnabled { get { return token != Actor.InvalidConditionToken; } } + + public HordeBonus(Actor self, HordeBonusInfo info) + : base(info) + { + this.self = self; + cachedRange = Info.Range; + cachedVRange = Info.MaximumVerticalOffset; + sources = new HashSet(); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + cachedPosition = self.CenterPosition; + proximityTrigger = self.World.ActorMap.AddProximityTrigger(cachedPosition, cachedRange, cachedVRange, ActorEntered, ActorExited); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + self.World.ActorMap.RemoveProximityTrigger(proximityTrigger); + } + + void ITick.Tick(Actor self) + { + var disabled = IsTraitDisabled; + + if (cachedDisabled != disabled) + { + desiredRange = disabled ? WDist.Zero : Info.Range; + desiredVRange = disabled ? WDist.Zero : Info.MaximumVerticalOffset; + cachedDisabled = disabled; + } + + if (self.CenterPosition != cachedPosition || desiredRange != cachedRange || desiredVRange != cachedVRange) + { + cachedPosition = self.CenterPosition; + cachedRange = desiredRange; + cachedVRange = desiredVRange; + self.World.ActorMap.UpdateProximityTrigger(proximityTrigger, cachedPosition, cachedRange, cachedVRange); + } + } + + void ActorEntered(Actor a) + { + if (a == self || a.Disposed || self.Disposed) + return; + + var relationship = self.Owner.RelationshipWith(a.Owner); + if (!Info.ValidRelationships.HasRelationship(relationship)) + return; + + if (a.TraitsImplementing().All(h => h.Info.HordeType != Info.HordeType)) + return; + + sources.Add(a); + UpdateConditionState(); + } + + void INotifyOtherProduction.UnitProducedByOther(Actor self, Actor producer, Actor produced, string productionType, TypeDictionary init) + { + // If the produced Actor doesn't occupy space, it can't be in range + if (produced.OccupiesSpace == null) + return; + + // We don't grant conditions when disabled + if (IsTraitDisabled) + return; + + // Work around for actors produced within the region not triggering until the second tick + if ((produced.CenterPosition - self.CenterPosition).HorizontalLengthSquared <= Info.Range.LengthSquared) + { + var relationship = self.Owner.RelationshipWith(produced.Owner); + if (!Info.ValidRelationships.HasRelationship(relationship)) + return; + + if (produced.TraitsImplementing().All(h => h.Info.HordeType != Info.HordeType)) + return; + + sources.Add(produced); + UpdateConditionState(); + } + } + + void ActorExited(Actor a) + { + sources.Remove(a); + UpdateConditionState(); + } + + void UpdateConditionState() + { + if (sources.Count > Info.Minimum && sources.Count < Info.Maximum) + { + if (!IsEnabled) + { + token = self.GrantCondition(Info.Condition); + Game.Sound.Play(SoundType.World, Info.EnableSound, self.CenterPosition); + } + } + else + { + if (IsEnabled) + { + token = self.RevokeCondition(token); + Game.Sound.Play(SoundType.World, Info.DisableSound, self.CenterPosition); + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Conditions/TransformOnCondition.cs b/OpenRA.Mods.AS/Traits/Conditions/TransformOnCondition.cs new file mode 100644 index 000000000000..e5d74a2ad082 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Conditions/TransformOnCondition.cs @@ -0,0 +1,50 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class TransformOnConditionInfo : ConditionalTraitInfo + { + [ActorReference] + public readonly string IntoActor = null; + public readonly int ForceHealthPercentage = 0; + public readonly bool SkipMakeAnims = true; + + public override object Create(ActorInitializer init) { return new TransformOnCondition(init, this); } + } + + public class TransformOnCondition : ConditionalTrait + { + readonly TransformOnConditionInfo info; + readonly string faction; + + public TransformOnCondition(ActorInitializer init, TransformOnConditionInfo info) + : base(info) + { + this.info = info; + faction = init.GetValue(info, init.Self.Owner.Faction.InternalName); + } + + protected override void TraitEnabled(Actor self) + { + var facing = self.TraitOrDefault(); + var transform = new Transform(info.IntoActor) { ForceHealthPercentage = info.ForceHealthPercentage, Faction = faction }; + if (facing != null) transform.Facing = facing.Facing; + transform.SkipMakeAnims = info.SkipMakeAnims; + self.CancelActivity(); + self.QueueActivity(transform); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Crate/GiveRandomActorCrateAction.cs b/OpenRA.Mods.AS/Traits/Crate/GiveRandomActorCrateAction.cs new file mode 100644 index 000000000000..ea120f25e41f --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Crate/GiveRandomActorCrateAction.cs @@ -0,0 +1,115 @@ +#region Copyright & License Information +/* + * Copyright 2007-2016 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Spawns a random actor with the `EligibleForRandomActorCrate` trait when collected.")] + class GiveRandomActorCrateActionInfo : CrateActionInfo + { + [Desc("Factions that are allowed to trigger this action.")] + public readonly HashSet ValidFactions = new(); + + [Desc("Override the owner of the newly spawned unit: e.g. Creeps or Neutral")] + public readonly string Owner = null; + + [Desc("Valid `EligibleForRandomActorCrate` types this crate can pick from.")] + public readonly HashSet Type = new() { "crateunit" }; + + public override object Create(ActorInitializer init) { return new GiveRandomActorCrateAction(init.Self, this); } + } + + class GiveRandomActorCrateAction : CrateAction + { + readonly Actor self; + readonly GiveRandomActorCrateActionInfo info; + + readonly IEnumerable eligibleActors; + + IEnumerable validActors; + + public GiveRandomActorCrateAction(Actor self, GiveRandomActorCrateActionInfo info) + : base(self, info) + { + this.self = self; + this.info = info; + + eligibleActors = self.World.Map.Rules.Actors.Values.Where(a => a.HasTraitInfo() + && a.TraitInfos().Any(c => info.Type.Contains(c.Type))); + } + + public bool CanGiveTo(Actor collector) + { + if (collector.Owner.NonCombatant) + return false; + + if (info.ValidFactions.Any() && !info.ValidFactions.Contains(collector.Owner.Faction.InternalName)) + return false; + + var cells = collector.World.Map.FindTilesInCircle(self.Location, 2); + + validActors = eligibleActors.Where(a => ValidActor(a, cells)); + + return validActors.Any(); + } + + bool ValidActor(ActorInfo a, IEnumerable cells) + { + foreach (var c in cells) + { + var mi = a.TraitInfoOrDefault(); + if (mi != null && mi.CanEnterCell(self.World, self, c)) + return true; + } + + return false; + } + + public override int GetSelectionShares(Actor collector) + { + if (!CanGiveTo(collector)) + return 0; + + return base.GetSelectionShares(collector); + } + + public override void Activate(Actor collector) + { + var unit = validActors.Random(self.World.SharedRandom); + + var cells = collector.World.Map.FindTilesInCircle(self.Location, 2); + + foreach (var c in cells) + { + var mi = unit.TraitInfoOrDefault(); + if (mi != null && mi.CanEnterCell(self.World, self, c)) + { + var cell = c; + var td = new TypeDictionary + { + new LocationInit(cell), + new OwnerInit(info.Owner ?? collector.Owner.InternalName) + }; + + collector.World.AddFrameEndTask(w => w.CreateActor(unit.Name, td)); + + base.Activate(collector); + + return; + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/DamagedByTintedCells.cs b/OpenRA.Mods.AS/Traits/DamagedByTintedCells.cs new file mode 100644 index 000000000000..77324dab2dd5 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/DamagedByTintedCells.cs @@ -0,0 +1,91 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor receives damage when in TintedCell area.")] + class DamagedByTintedCellsInfo : ConditionalTraitInfo, Requires, IRulesetLoaded + { + [Desc("Receive damage from the TintedCell layer with this name.")] + public readonly string LayerName = "radioactivity"; + + [Desc("Damage received per level, per DamageInterval. (Damage = CellLevel / DamageLevel * Damage")] + public readonly int Damage = 500; + + [Desc("How much TintedCell.Level it takes for it to inflict damage X times.")] + public readonly int DamageLevel = 100; + + [Desc("Delay (in ticks) between receiving damage.")] + public readonly int DamageInterval = 16; + + [Desc("Apply the damage using these damagetypes.")] + public readonly BitSet DamageTypes = default; + + public override object Create(ActorInitializer init) { return new DamagedByTintedCells(init.Self, this); } + + public override void RulesetLoaded(Ruleset rules, ActorInfo info) + { + base.RulesetLoaded(rules, info); + + if (DamageLevel == 0) + throw new YamlException("DamageLevel of DamagedByTintedCells of actor \"" + info.Name + "\" cannot be 0."); + + var layers = rules.Actors["world"].TraitInfos() + .Where(l => l.Name == LayerName); + + if (!layers.Any()) + throw new InvalidOperationException("There is no TintedCellsLayer named \"" + LayerName + "\" to match DamagedByTintedCells of actor \"" + info.Name + "\""); + + if (layers.Count() > 1) + throw new InvalidOperationException("There are multiple TintedCellsLayers named \"" + + LayerName + "\" to match DamagedByTintedCells of actor \"" + info.Name + "\""); + } + } + + class DamagedByTintedCells : ConditionalTrait, ITick, ISync + { + readonly TintedCellsLayer tcLayer; + + [Sync] + int damageTicks; + + public DamagedByTintedCells(Actor self, DamagedByTintedCellsInfo info) + : base(info) + { + tcLayer = self.World.WorldActor.TraitsImplementing() + .First(l => l.Info.Name == info.LayerName); + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled || --damageTicks > 0) + return; + + // Prevents harming cargo. + if (!self.IsInWorld) + return; + + var level = tcLayer.GetLevel(self.Location); + if (level <= 0) + return; + + var dmg = level / Info.DamageLevel * Info.Damage; + self.InflictDamage(self.World.WorldActor, new Damage(dmg, Info.DamageTypes)); + + damageTicks = Info.DamageInterval; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/DelayedWeaponAttachable.cs b/OpenRA.Mods.AS/Traits/DelayedWeaponAttachable.cs new file mode 100644 index 000000000000..0e1543a78537 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/DelayedWeaponAttachable.cs @@ -0,0 +1,154 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This trait interacts with and provides a container for Attach/DetachDelayedWeaponWarheads.")] + public class DelayedWeaponAttachableInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [Desc("Type of DelayedWeapons that can be attached to this trait.")] + public readonly string Type = "bomb"; + + [Desc("Defines the maximum of DelayedWeapons which can be attached at any given time.")] + public readonly int AttachLimit = 1; + + [Desc("Show a bar indicating the progress until triggering the DelayedWeapon with the smallest remaining time.")] + public readonly bool ShowProgressBar = true; + + [GrantedConditionReference] + [Desc("The condition to grant while any DelayedWeapon is attached.")] + public readonly string Condition = null; + + public readonly Color ProgressBarColor = Color.DarkRed; + + public override object Create(ActorInitializer init) { return new DelayedWeaponAttachable(init.Self, this); } + } + + public class DelayedWeaponAttachable : ConditionalTrait, ITick, INotifyKilled, ISelectionBar, INotifyTransform + { + public HashSet Container { get; private set; } + + readonly Actor self; + readonly HashSet detectors = new(); + readonly bool isValidCondition; + + int token = Actor.InvalidConditionToken; + public bool IsEnabled { get { return token != Actor.InvalidConditionToken; } } + + public DelayedWeaponAttachable(Actor self, DelayedWeaponAttachableInfo info) + : base(info) + { + this.self = self; + Container = new HashSet(); + isValidCondition = !string.IsNullOrEmpty(info.Condition); + } + + void ITick.Tick(Actor self) + { + if (!IsTraitDisabled) + { + foreach (var trigger in Container) + trigger.Tick(self); + + Container.RemoveWhere(p => !p.IsValid); + + if (isValidCondition && token != Actor.InvalidConditionToken && !Container.Any()) + token = self.RevokeCondition(token); + } + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (!IsTraitDisabled) + { + foreach (var trigger in Container) + { + if (!trigger.DeathTypes.IsEmpty && !e.Damage.DamageTypes.Overlaps(trigger.DeathTypes)) + continue; + + trigger.Activate(self); + } + + Container.RemoveWhere(p => !p.IsValid); + } + } + + public bool CanAttach(string type) + { + return !IsTraitDisabled && Info.Type == type && Container.Count < Info.AttachLimit; + } + + public void Attach(DelayedWeaponTrigger trigger) + { + if (isValidCondition && token == Actor.InvalidConditionToken) + token = self.GrantCondition(Info.Condition); + + Container.Add(trigger); + } + + public void AddDetector(Actor detector) + { + detectors.Add(detector); + } + + public void RemoveDetector(Actor detector) + { + if (detectors.Contains(detector)) + detectors.Remove(detector); + } + + bool ISelectionBar.DisplayWhenEmpty { get { return false; } } + + float ISelectionBar.GetValue() + { + var value = 0f; + + if (!Info.ShowProgressBar || Container.Count == 0) + return value; + + var smallestTrigger = Container.Where(b => b.AttachedBy.Owner.IsAlliedWith(self.World.LocalPlayer) || detectors.Any(d => d.Owner.IsAlliedWith(self.World.LocalPlayer))) + .MinByOrDefault(t => t.RemainingTime); + if (smallestTrigger == null) + return value; + + return smallestTrigger.RemainingTime * 1.0f / smallestTrigger.TriggerTime; + } + + Color ISelectionBar.GetColor() + { + return Info.ProgressBarColor; + } + + void INotifyTransform.BeforeTransform(Actor self) + { + if (!IsTraitDisabled) + { + foreach (var trigger in Container) + trigger.Activate(self); + + Container.RemoveWhere(p => !p.IsValid); + + if (isValidCondition && token != Actor.InvalidConditionToken && !Container.Any()) + token = self.RevokeCondition(token); + } + } + + void INotifyTransform.OnTransform(Actor self) { } + + void INotifyTransform.AfterTransform(Actor toActor) { } + } +} diff --git a/OpenRA.Mods.AS/Traits/DelayedWeaponDetector.cs b/OpenRA.Mods.AS/Traits/DelayedWeaponDetector.cs new file mode 100644 index 000000000000..22983e7a98b1 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/DelayedWeaponDetector.cs @@ -0,0 +1,102 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This trait can reveal DelayedWeapon progressbars on DelayedWeaponAttachable traits.")] + public class DelayedWeaponDetectorInfo : ConditionalTraitInfo + { + [Desc("Type of DelayedWeapons that can be detected.")] + public readonly HashSet Types = new() { "bomb" }; + + [Desc("Range of detection.")] + public readonly WDist Range = WDist.FromCells(1); + + public override object Create(ActorInitializer init) { return new DelayedWeaponDetector(init.Self, this); } + } + + public class DelayedWeaponDetector : ConditionalTrait, ITick, INotifyAddedToWorld, INotifyRemovedFromWorld + { + WPos cachedPosition; + WDist cachedRange; + WDist desiredRange; + readonly WDist cachedVRange = new(1536); + + int proximityTrigger; + bool cachedDisabled = true; + readonly Actor self; + + public DelayedWeaponDetector(Actor self, DelayedWeaponDetectorInfo info) + : base(info) + { + this.self = self; + cachedRange = info.Range; + } + + void ITick.Tick(Actor self) + { + var disabled = IsTraitDisabled; + + if (cachedDisabled != disabled) + { + desiredRange = disabled ? WDist.Zero : Info.Range; + cachedDisabled = disabled; + } + + if (self.CenterPosition != cachedPosition || desiredRange != cachedRange) + { + cachedPosition = self.CenterPosition; + cachedRange = desiredRange; + self.World.ActorMap.UpdateProximityTrigger(proximityTrigger, cachedPosition, cachedRange, cachedVRange); + } + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + cachedPosition = self.CenterPosition; + proximityTrigger = self.World.ActorMap.AddProximityTrigger(cachedPosition, cachedRange, cachedVRange, ActorEntered, ActorExited); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + self.World.ActorMap.RemoveProximityTrigger(proximityTrigger); + } + + void ActorEntered(Actor a) + { + if (a == self || a.Disposed || self.Disposed) + return; + var attachables = a.TraitsImplementing().Where(t => Info.Types.Contains(t.Info.Type)); + + foreach (var attachable in attachables) + { + attachable.AddDetector(self); + } + } + + void ActorExited(Actor a) + { + if (a.IsDead) + return; + + var attachables = a.TraitsImplementing().Where(t => Info.Types.Contains(t.Info.Type)); + + foreach (var attachable in attachables) + { + attachable.RemoveDetector(self); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/DelayedWeaponTrigger.cs b/OpenRA.Mods.AS/Traits/DelayedWeaponTrigger.cs new file mode 100644 index 000000000000..b415655d94c9 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/DelayedWeaponTrigger.cs @@ -0,0 +1,66 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.GameRules; +using OpenRA.Mods.AS.Warheads; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class DelayedWeaponTrigger + { + readonly WarheadArgs args; + + public readonly BitSet DeathTypes; + + public readonly int TriggerTime; + + public int RemainingTime { get; private set; } + + public Actor AttachedBy { get; private set; } + + readonly WeaponInfo weaponInfo; + + public bool IsValid { get; private set; } + + public DelayedWeaponTrigger(AttachDelayedWeaponWarhead warhead, WarheadArgs args) + { + this.args = args; + TriggerTime = warhead.TriggerTime; + RemainingTime = TriggerTime; + DeathTypes = warhead.DeathTypes; + weaponInfo = warhead.WeaponInfo; + AttachedBy = args.SourceActor; + IsValid = true; + } + + public void Tick(Actor attachable) + { + if (attachable.IsDead || !attachable.IsInWorld || !IsValid || TriggerTime < 0) + return; + + if (--RemainingTime < 0) + Activate(attachable); + } + + public void Activate(Actor attachable) + { + IsValid = false; + var target = Target.FromPos(attachable.CenterPosition); + attachable.World.AddFrameEndTask(w => weaponInfo.Impact(target, args)); + } + + public void Deactivate() + { + IsValid = false; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/DevOffsetOverlay.cs b/OpenRA.Mods.AS/Traits/DevOffsetOverlay.cs new file mode 100644 index 000000000000..3eaa51708695 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/DevOffsetOverlay.cs @@ -0,0 +1,147 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Displays a developer offset point, controllable via chat commands." + + "Rendering is enabled automatically with the first valid command." + + "Available commands:" + + "`body`: Sets the reference point to the actor's center position." + + "`turret X`: where X is the turret index whose center the reference point should be set. Falls back to actor center position on wrong index." + + "`set X,Y,Z`: Sets the offset. No spaces are supported between the values." + + "`add X,Y,Z`: Adds the value to the current offset. Negative values function to subtract. No spaces are supported between the values." + + "`query`: Returns the current offset value in the chat." + + "`disable`: Disables rendering of the offset.")] + public class DevOffsetOverlayInfo : TraitInfo + { + public override object Create(ActorInitializer init) { return new DevOffsetOverlay(init.Self); } + } + + public class DevOffsetOverlay : Requires, IRenderAnnotations, INotifyCreated + { + static readonly WVec TargetPosHLine = new(0, 128, 0); + static readonly WVec TargetPosVLine = new(128, 0, 0); + + readonly BodyOrientation coords; + + Turreted[] turrets; + WVec devOffset; + int turret = -1; + bool enabled; + + public DevOffsetOverlay(Actor self) + { + coords = self.Trait(); + } + + void INotifyCreated.Created(Actor self) + { + turrets = self.TraitsImplementing().ToArray(); + } + + IEnumerable IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr) + { + if (!enabled || self.World.FogObscures(self)) + yield break; + + var referencePoint = self.CenterPosition; + var bodyOrientation = coords.QuantizeOrientation(self.Orientation); + var devRenderOffset = devOffset; + + if (turret != -1) + { + var turretOrientation = turrets[turret].WorldOrientation - bodyOrientation; + devRenderOffset = devRenderOffset.Rotate(turretOrientation); + referencePoint += coords.LocalToWorld(turrets[turret].Offset.Rotate(bodyOrientation)); + } + + devRenderOffset = coords.LocalToWorld(devRenderOffset.Rotate(bodyOrientation)); + + yield return new LineAnnotationRenderable(referencePoint - TargetPosHLine, referencePoint + TargetPosHLine, 1, Color.Magenta); + yield return new LineAnnotationRenderable(referencePoint - TargetPosVLine, referencePoint + TargetPosVLine, 1, Color.Magenta); + + var devPoint = referencePoint + devRenderOffset; + var devOrientation = turret != -1 ? turrets[turret].WorldOrientation : + coords.QuantizeOrientation(self.Orientation); + var dirOffset = new WVec(0, -224, 0).Rotate(devOrientation); + yield return new LineAnnotationRenderable(devPoint, devPoint + dirOffset, 1, Color.Magenta); + } + + bool IRenderAnnotations.SpatiallyPartitionable { get { return true; } } + + public void ParseCommand(Actor self, string message) + { + var command = message.Split(' ')[0].ToLowerInvariant(); + + switch (command) + { + case "body": + turret = -1; + enabled = true; + break; + + case "turret": + int turretIndex; + var parse = int.TryParse(message.Split(' ')[1], out turretIndex); + if (!parse || turretIndex >= turrets.Length) + turret = -1; + else + turret = turretIndex; + enabled = true; + break; + + case "set": + var setoffsets = message.Split(' ')[1].Split(','); + if (setoffsets.Length != 3) + break; + + var setoffset = new int[3]; + for (var i = 0; i < setoffsets.Length; i++) + int.TryParse(setoffsets[i], out setoffset[i]); + + devOffset = new WVec(setoffset[0], setoffset[1], setoffset[2]); + enabled = true; + break; + + case "add": + var addoffsets = message.Split(' ')[1].Split(','); + if (addoffsets.Length != 3) + break; + + var addoffset = new int[3]; + for (var i = 0; i < addoffsets.Length; i++) + int.TryParse(addoffsets[i], out addoffset[i]); + + devOffset += new WVec(addoffset[0], addoffset[1], addoffset[2]); + enabled = true; + break; + + case "query": + TextNotificationsManager.Debug("The current DevOffset on actor {0} {1} is: {2},{3},{4}", self.Info.Name, self.ActorID, devOffset.X, devOffset.Y, devOffset.Z); + break; + + case "disable": + enabled = false; + break; + + default: + break; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Doctrine.cs b/OpenRA.Mods.AS/Traits/Doctrine.cs new file mode 100644 index 000000000000..0d22f3b9b310 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Doctrine.cs @@ -0,0 +1,47 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("When created, this actor kills all actors with this trait owned by it's owner.")] + public class DoctrineInfo : TraitInfo + { + [Desc("Type of the doctrine. If empty, it falls back to the actor's type.")] + public readonly string Type = null; + + public override object Create(ActorInitializer init) { return new Doctrine(init.Self, this); } + } + + public class Doctrine : INotifyCreated + { + public readonly string Type; + + public Doctrine(Actor self, DoctrineInfo info) + { + Type = string.IsNullOrEmpty(info.Type) ? self.Info.Name : info.Type; + } + + void INotifyCreated.Created(Actor self) + { + var actors = self.World.ActorsWithTrait().Where(x => x.Trait.Type == Type && x.Actor.Owner == self.Owner && x.Actor != self); + + foreach (var a in actors) + { + if (a.Actor.TraitOrDefault() != null) + a.Actor.Kill(a.Actor); + else + a.Actor.Dispose(); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/DroneSpawnerMaster.cs b/OpenRA.Mods.AS/Traits/DroneSpawnerMaster.cs new file mode 100644 index 000000000000..d8396c612df3 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/DroneSpawnerMaster.cs @@ -0,0 +1,327 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor can spawn actors. Disable this trait to disable drone control, Pause this trait to stop drone spawning")] + public class DroneSpawnerMasterInfo : BaseSpawnerMasterInfo + { + [Desc("Can the slaves be controlled independently?")] + public readonly bool SlavesHaveFreeWill = false; + + [Desc("Place slave will gather to. Only recommended to used on building master")] // TODO: Test it on ground unit on map edges + public readonly CVec[] GatherCell = Array.Empty(); + + [Desc("When idle and not moving, master check slaves and gathers them in this many tick. Set it properly can save performance")] + public readonly int IdleCheckTick = 103; + + [Desc("After master attack, slaves will stop and follow master in this many tick. Mainly used for long-range attack also use drone")] + public readonly int FollowAfterAttackDelay = 0; + + [Desc("Spawn initial load all at once?")] + public readonly bool ShouldSpawnInitialLoad = true; + + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + base.RulesetLoaded(rules, ai); + + if (Actors == null || Actors.Length == 0) + throw new YamlException($"Actors is null or empty for DroneSpawner for actor type {ai.Name}!"); + + if (InitialActorCount > Actors.Length || InitialActorCount < -1) + throw new YamlException("DroneSpawner can't have more InitialActorCount than the actors defined!"); + + if (GatherCell.Length > Actors.Length) + throw new YamlException($"Length of GatherOffsetCell can't be larger than the actors defined! (Actor type = {ai.Name})"); + } + + public override object Create(ActorInitializer init) { return new DroneSpawnerMaster(init, this); } + } + + public class DroneSpawnerMaster : BaseSpawnerMaster, INotifyOwnerChanged, ITick, + IResolveOrder, INotifyAttack + { + class DroneSpawnerSlaveEntry : BaseSpawnerSlaveEntry + { + public new DroneSpawnerSlave SpawnerSlave; + public CVec GatherOffsetCell = CVec.Zero; + } + + public new DroneSpawnerMasterInfo Info { get; private set; } + + DroneSpawnerSlaveEntry[] slaveEntries; + int spawnReplaceTicks; + int followTick; + + ActivityType preState; + + WPos preLoc; + + int remainingIdleCheckTick; + bool isAircraft; + bool hasSpawnInitialLoad; + + public DroneSpawnerMaster(ActorInitializer init, DroneSpawnerMasterInfo info) + : base(init, info) + { + Info = info; + preLoc = WPos.Zero; + followTick = 0; + } + + protected override void Created(Actor self) + { + base.Created(self); + + remainingIdleCheckTick = Info.IdleCheckTick; + + for (var i = 0; i < Info.GatherCell.Length; i++) + slaveEntries[i].GatherOffsetCell = Info.GatherCell[i]; + + isAircraft = self.Info.HasTraitInfo(); + + if (Info.ShouldSpawnInitialLoad) + hasSpawnInitialLoad = false; + else + hasSpawnInitialLoad = true; + } + + public override BaseSpawnerSlaveEntry[] CreateSlaveEntries(BaseSpawnerMasterInfo info) + { + slaveEntries = new DroneSpawnerSlaveEntry[info.Actors.Length]; // For this class to use + + for (var i = 0; i < slaveEntries.Length; i++) + slaveEntries[i] = new DroneSpawnerSlaveEntry(); + + return slaveEntries; // For the base class to use + } + + public override void InitializeSlaveEntry(Actor slave, BaseSpawnerSlaveEntry entry) + { + var se = entry as DroneSpawnerSlaveEntry; + base.InitializeSlaveEntry(slave, se); + + se.SpawnerSlave = slave.Trait(); + } + + public void ResolveOrder(Actor self, Order order) + { + if (Info.SlavesHaveFreeWill) + return; + + switch (order.OrderString) + { + case "Stop": + StopSlaves(); + break; + default: + break; + } + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + // Drone Master only pause attack when trait is Disabled + // HACK: If Armament hits instantly and kills the target, the target will become invalid + if (target.Type == TargetType.Invalid || (Info.ArmamentNames.Count > 0 && !Info.ArmamentNames.Contains(a.Info.Name)) || Info.SlavesHaveFreeWill || IsTraitDisabled) + return; + + AssignTargetsToSlaves(self, target); + followTick = Info.FollowAfterAttackDelay; + } + + void ITick.Tick(Actor self) + { + if (!self.IsInWorld) + return; + + if (!hasSpawnInitialLoad) + { + // Spawn initial load. + var burst = Info.InitialActorCount == -1 ? Info.Actors.Length : Info.InitialActorCount; + for (var i = 0; i < burst; i++) + Replenish(self, SlaveEntries); + + // The base class creates the slaves but doesn't move them into world. + // Let's do it here. + SpawnReplenishedSlaves(self); + spawnReplaceTicks = -1; + hasSpawnInitialLoad = true; + } + + // Time to respawn something. + if (!IsTraitPaused) + { + if (spawnReplaceTicks < 0) + { + // If there's something left to spawn, restart the timer. + if (SelectEntryToSpawn(slaveEntries) != null) + spawnReplaceTicks = Info.RespawnTicks; + } + else if (spawnReplaceTicks == 0) + { + Replenish(self, slaveEntries); + SpawnReplenishedSlaves(self); + spawnReplaceTicks--; + } + else + spawnReplaceTicks--; + } + + if (!Info.SlavesHaveFreeWill) + AssignSlaveActivity(self); + + if (followTick > 0) + followTick--; + } + + void SpawnReplenishedSlaves(Actor self) + { + foreach (var se in slaveEntries) + if (se.IsValid && !se.Actor.IsInWorld) + SpawnIntoWorld(self, se.Actor, self.CenterPosition + se.Offset.Rotate(self.Orientation)); + } + + public override void OnSlaveKilled(Actor self, Actor slave) + { + if (spawnReplaceTicks <= 0) + spawnReplaceTicks = Info.RespawnTicks; + } + + void AssignTargetsToSlaves(Actor self, Target target) + { + foreach (var se in slaveEntries) + { + if (!se.IsValid) + continue; + if (se.SpawnerSlave.Info.AttackCallBackDistance.LengthSquared > (self.CenterPosition - target.CenterPosition).HorizontalLengthSquared) + se.SpawnerSlave.Attack(se.Actor, target); + else if (preLoc != self.CenterPosition) + { + MoveSlaves(self); + remainingIdleCheckTick = Info.IdleCheckTick; + } + } + } + + void MoveSlaves(Actor self) + { + foreach (var se in slaveEntries) + { + if (!se.IsValid || !se.Actor.IsInWorld) + continue; + + if (!se.SpawnerSlave.IsMoving(self.Location + se.GatherOffsetCell)) + { + se.SpawnerSlave.Stop(se.Actor); + se.SpawnerSlave.Move(se.Actor, self.Location + se.GatherOffsetCell); + } + } + } + + void AssignSlaveActivity(Actor self) + { + if (followTick > 0) + return; + + var effectiveActivity = self.CurrentActivity; + if (!self.IsIdle) + { + while (effectiveActivity.ChildActivity != null) + effectiveActivity = effectiveActivity.ChildActivity; + } + + // 1. Drone may get away from master due to auto-targeting. + if (effectiveActivity == null || effectiveActivity.ActivityType == ActivityType.Ability || effectiveActivity.ActivityType == ActivityType.Undefined) + { + if (remainingIdleCheckTick < 0) + { + MoveSlaves(self); + remainingIdleCheckTick = Info.IdleCheckTick; + } + + // 1.1 There is situation like teleport will just change actor's position without activity + else if (preLoc != self.CenterPosition) + { + MoveSlaves(self); + remainingIdleCheckTick = Info.IdleCheckTick; + } + else + remainingIdleCheckTick--; + } + + // 2. Stop the drone attacking when move for special case of fire at an ally. + // Only move slaves when position change + // Note: because aircraft always Fly, so drone may get away from master due to auto-targeting + // when actor moves. + else if (effectiveActivity.ActivityType == ActivityType.Move) + { + if (preState == ActivityType.Attack) + { + StopSlaves(); + remainingIdleCheckTick = Info.IdleCheckTick; + } + else if (preLoc != self.CenterPosition) + { + MoveSlaves(self); + remainingIdleCheckTick = Info.IdleCheckTick; + } + else if (remainingIdleCheckTick < 0 && isAircraft) + { + MoveSlaves(self); + remainingIdleCheckTick = Info.IdleCheckTick; + } + else if (isAircraft) + remainingIdleCheckTick--; + } + + // Actually, new code here or old code in MobSpawnerMaster is not working + // The only working code is in INotifyAttack. It is due to Activity of attack + // do not achieve `GetTargets(actor)` + // 3. Stop the slaves move when prepare to attack + else if (effectiveActivity.ActivityType == ActivityType.Attack) + { + if (preState == ActivityType.Move) + { + StopSlaves(); + remainingIdleCheckTick = Info.IdleCheckTick; + } + else if (preState == ActivityType.Undefined || preState == ActivityType.Ability) + { + StopSlaves(); + remainingIdleCheckTick = Info.IdleCheckTick; + } + } + + preState = effectiveActivity == null ? ActivityType.Undefined : effectiveActivity.ActivityType; + preLoc = self.CenterPosition; + } + + /* Debug + , ITickRender + void ITickRender.TickRender(Graphics.WorldRenderer wr, Actor self) + { + var font = Game.Renderer.Fonts["Bold"]; + foreach (var kv in Info.GatherOffsetCell) + { + var i = new FloatingText(self.World.Map.CenterOfCell(kv + self.Location), Color.Gold, "1", 1); + self.World.Add(i); + } + } + */ + } +} diff --git a/OpenRA.Mods.AS/Traits/DroneSpawnerSlave.cs b/OpenRA.Mods.AS/Traits/DroneSpawnerSlave.cs new file mode 100644 index 000000000000..93f229f55f9a --- /dev/null +++ b/OpenRA.Mods.AS/Traits/DroneSpawnerSlave.cs @@ -0,0 +1,105 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.Mods.Common.Traits; + +/* + * Needs base engine modification. (Becaus DroneSpawner.cs mods it) + */ + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Can be slaved to a drone spawner.")] + public class DroneSpawnerSlaveInfo : BaseSpawnerSlaveInfo + { + [Desc("Aircraft slaves outside of this range from master while moving will be call back")] + public readonly int MovingCallBackCellDistance = 2; + + [Desc("Slaves will follow master instead of attack while target outside of this range")] + public readonly WDist AttackCallBackDistance = WDist.FromCells(10); + + public override object Create(ActorInitializer init) { return new DroneSpawnerSlave(this); } + } + + public class DroneSpawnerSlave : BaseSpawnerSlave + { + public IMove[] Moves { get; private set; } + public IPositionable Positionable { get; private set; } + public bool IsAircraft; + public readonly DroneSpawnerSlaveInfo Info; + Actor currentActor; + Actor masterActor; + public readonly Predicate InvalidActor; + + public bool IsMoving(CPos gatherlocation) + { + if (IsAircraft) + { + if (!InvalidActor(currentActor) && !InvalidActor(masterActor) && + (currentActor.Location - gatherlocation).LengthSquared > Info.MovingCallBackCellDistance * Info.MovingCallBackCellDistance) + return false; + + return true; + } + + var groundmove = Moves.Any(m => m.IsTraitEnabled() && (m.CurrentMovementTypes.HasFlag(MovementType.Horizontal) || m.CurrentMovementTypes.HasFlag(MovementType.Vertical))); + return groundmove; + } + + public DroneSpawnerSlave(DroneSpawnerSlaveInfo info) + : base(info) + { + InvalidActor = a => a == null || a.IsDead || !a.IsInWorld; + Info = info; + } + + protected override void Created(Actor self) + { + base.Created(self); + + currentActor = self; + + Moves = self.TraitsImplementing().ToArray(); + + var positionables = self.TraitsImplementing(); + if (positionables.Count() != 1) + throw new InvalidOperationException($"Actor {self} has multiple (or no) traits implementing IPositionable."); + + Positionable = positionables.First(); + + IsAircraft = self.Info.HasTraitInfo(); + } + + public override void LinkMaster(Actor self, Actor master, BaseSpawnerMaster spawnerMaster) + { + base.LinkMaster(self, master, spawnerMaster); + masterActor = master; + } + + public void Move(Actor self, CPos location) + { + // And tell attack bases to stop attacking. + if (Moves.Length == 0) + return; + + foreach (var mv in Moves) + if (mv.IsTraitEnabled()) + { + if (IsAircraft) + self.QueueActivity(mv.MoveTo(location, 0)); + else + self.QueueActivity(mv.MoveTo(location, 2)); + break; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/EligibleForRandomActorCrate.cs b/OpenRA.Mods.AS/Traits/EligibleForRandomActorCrate.cs new file mode 100644 index 000000000000..5e3f3a1c40a7 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/EligibleForRandomActorCrate.cs @@ -0,0 +1,22 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Tag trait for `GiveRandomActor` crate action.")] + public class EligibleForRandomActorCrateInfo : TraitInfo + { + public readonly string Type = "crateunit"; + } + + public class EligibleForRandomActorCrate { } +} diff --git a/OpenRA.Mods.AS/Traits/ExplodesForMaster.cs b/OpenRA.Mods.AS/Traits/ExplodesForMaster.cs new file mode 100644 index 000000000000..0bf207e6bb09 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/ExplodesForMaster.cs @@ -0,0 +1,153 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor explodes when killed and the kill XP goes to the parent actor.", + "Hack: Explodes cannot pass XP because it is KIA and XP cannot pass in INotifyKilled, we will use this instead.")] + public class ExplodesForMasterInfo : ExplodesInfo + { + [Desc("Armament used by parent or master. Share the same modifier.")] + public readonly string MasterArmamentName = null; + + [Desc("Allow share the same modifier from mindcontrol master.")] + public readonly bool AllowShareFromMindControlMaster = false; + + [Desc("Allow share the same modifier from parent actor.")] + public readonly bool AllowShareFromParent = true; + + public override object Create(ActorInitializer init) { return new ExplodesForMaster(this, init.Self); } + } + + public class ExplodesForMaster : ConditionalTrait, INotifyKilled, INotifyDamage + { + readonly Health health; + BuildingInfo buildingInfo; + Armament[] armaments; + + public ExplodesForMaster(ExplodesForMasterInfo info, Actor self) + : base(info) + { + health = self.Trait(); + } + + protected override void Created(Actor self) + { + buildingInfo = self.Info.TraitInfoOrDefault(); + armaments = self.TraitsImplementing().ToArray(); + + base.Created(self); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (IsTraitDisabled || !self.IsInWorld) + return; + + if (self.World.SharedRandom.Next(100) > Info.Chance) + return; + + if (!Info.DeathTypes.IsEmpty && !e.Damage.DamageTypes.Overlaps(Info.DeathTypes)) + return; + + var weapon = ChooseWeaponForExplosion(self); + if (weapon == null) + return; + + if (weapon.Report != null && weapon.Report.Any()) + Game.Sound.Play(SoundType.World, weapon.Report.Random(self.World.SharedRandom), self.CenterPosition, weapon.SoundVolume); + + Actor attacker = null; + var modifierActor = self; + foreach (var mindControllable in self.TraitsImplementing()) + if (mindControllable.MasterWhenDie != null && !mindControllable.MasterWhenDie.IsDead) + { + attacker = mindControllable.MasterWhenDie; + modifierActor = Info.AllowShareFromMindControlMaster ? attacker : self; + break; + } + + if (attacker == null || attacker.IsDead) + { + attacker = self.TraitOrDefault()?.Parent; + if (attacker == null || attacker.IsDead) + attacker = self; + else + modifierActor = Info.AllowShareFromParent ? attacker : self; + } + + var args = new ProjectileArgs + { + Weapon = weapon, + Facing = WAngle.Zero, + CurrentMuzzleFacing = () => WAngle.Zero, + + DamageModifiers = Info.MasterArmamentName != null && !modifierActor.IsDead ? modifierActor.TraitsImplementing() + .Select(a => a.GetFirepowerModifier(Info.MasterArmamentName)).ToArray() : Array.Empty(), + + InaccuracyModifiers = Array.Empty(), + + RangeModifiers = Array.Empty(), + + Source = self.CenterPosition, + CurrentSource = () => self.CenterPosition, + SourceActor = attacker, + PassiveTarget = self.CenterPosition + }; + + if (Info.Type == ExplosionType.Footprint && buildingInfo != null) + { + var cells = buildingInfo.OccupiedTiles(self.Location); + foreach (var c in cells) + weapon.Impact(Target.FromPos(self.World.Map.CenterOfCell(c)), new WarheadArgs(args)); + + return; + } + + // Use .FromPos since this actor is killed. Cannot use Target.FromActor + weapon.Impact(Target.FromPos(self.CenterPosition), new WarheadArgs(args)); + } + + WeaponInfo ChooseWeaponForExplosion(Actor self) + { + if (armaments.Length == 0) + return Info.WeaponInfo; + else if (self.World.SharedRandom.Next(100) > Info.LoadedChance) + return Info.EmptyWeaponInfo; + + // PERF: Avoid LINQ + foreach (var a in armaments) + if (!a.IsReloading) + return Info.WeaponInfo; + + return Info.EmptyWeaponInfo; + } + + void INotifyDamage.Damaged(Actor self, AttackInfo e) + { + if (Info.DamageThreshold == 0 || IsTraitDisabled || !self.IsInWorld) + return; + + if (!Info.DeathTypes.IsEmpty && !e.Damage.DamageTypes.Overlaps(Info.DeathTypes)) + return; + + // Cast to long to avoid overflow when multiplying by the health + var source = Info.DamageSource == DamageSource.Self ? self : e.Attacker; + if (health.HP * 100L < Info.DamageThreshold * (long)health.MaxHP) + self.World.AddFrameEndTask(w => self.Kill(source, e.Damage.DamageTypes)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/FreePassenger.cs b/OpenRA.Mods.AS/Traits/FreePassenger.cs new file mode 100644 index 000000000000..0a242192bd7c --- /dev/null +++ b/OpenRA.Mods.AS/Traits/FreePassenger.cs @@ -0,0 +1,80 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Player receives listed units for free as passenger once the trait is enabled.")] + public class FreePassengerInfo : ConditionalTraitInfo, Requires + { + [ActorReference] + [FieldLoader.Require] + [Desc("Name of the actor.")] + public readonly string[] Actors = Array.Empty(); + + [Desc("Whether another actor should spawn upon re-enabling the trait.")] + public readonly bool AllowRespawn = false; + + public override object Create(ActorInitializer init) { return new FreePassenger(init, this); } + } + + public class FreePassenger : ConditionalTrait + { + protected bool allowSpawn = true; + protected string faction; + readonly Cargo cargo; + + public FreePassenger(ActorInitializer init, FreePassengerInfo info) + : base(info) + { + faction = init.GetValue(init.Self.Owner.Faction.InternalName); + cargo = init.Self.Trait(); + } + + protected override void TraitEnabled(Actor self) + { + if (!allowSpawn) + return; + + allowSpawn = Info.AllowRespawn; + + self.World.AddFrameEndTask(w => + { + if (self.IsDead) + return; + + foreach (var actor in Info.Actors) + { + var passenger = self.World.Map.Rules.Actors[actor].TraitInfoOrDefault(); + + if (passenger == null || !cargo.Info.Types.Contains(passenger.CargoType) || !cargo.HasSpace(passenger.Weight)) + return; + + var a = w.CreateActor(actor, new TypeDictionary + { + new ParentActorInit(self), + new LocationInit(self.Location), + new OwnerInit(self.Owner), + new FactionInit(faction), + }); + + w.Remove(a); + cargo.Load(self, a); + } + }); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/FrozenUnderFogUpdatedByGpsAS.cs b/OpenRA.Mods.AS/Traits/FrozenUnderFogUpdatedByGpsAS.cs new file mode 100644 index 000000000000..0df44482824f --- /dev/null +++ b/OpenRA.Mods.AS/Traits/FrozenUnderFogUpdatedByGpsAS.cs @@ -0,0 +1,109 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + using FrozenActorAction = Action; + + [Desc("Updates frozen actors of actors that change owners, are sold or die whilst having an active GPS power.")] + public class FrozenUnderFogUpdatedByGpsASInfo : TraitInfo, Requires + { + public override object Create(ActorInitializer init) { return new FrozenUnderFogUpdatedByGpsAS(init); } + } + + public class FrozenUnderFogUpdatedByGpsAS : INotifyOwnerChanged, INotifyActorDisposing, IOnGpsASRefreshed + { + static readonly FrozenActorAction Refresh = (fufubg, fal, gps, fa) => + { + // Refreshes the visual state of the frozen actor, so ownership changes can be seen. + // This only makes sense if the frozen actor has already been revealed (i.e. has renderables) + if (fa.HasRenderables) + { + fa.RefreshState(); + fa.NeedRenderables = true; + } + }; + static readonly FrozenActorAction Remove = (fufubg, fal, gps, fa) => + { + // Removes the frozen actor. Once done, we no longer need to track GPS updates. + fa.Invalidate(); + fal.Remove(fa); + gps.UnregisterForOnGpsRefreshed(fufubg.self, fufubg); + }; + + class Traits + { + public readonly FrozenActorLayer FrozenActorLayer; + public readonly GpsASWatcher GpsWatcher; + public Traits(Player player, FrozenUnderFogUpdatedByGpsAS frozenUnderFogUpdatedByGps) + { + FrozenActorLayer = player.FrozenActorLayer; + GpsWatcher = player.PlayerActor.TraitOrDefault(); + GpsWatcher.RegisterForOnGpsRefreshed(frozenUnderFogUpdatedByGps.self, frozenUnderFogUpdatedByGps); + } + } + + readonly PlayerDictionary traits; + readonly Actor self; + + public FrozenUnderFogUpdatedByGpsAS(ActorInitializer init) + { + self = init.Self; + traits = new PlayerDictionary(init.World, player => new Traits(player, this)); + } + + public void OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + ActOnFrozenActorsForAllPlayers(Refresh); + } + + void INotifyActorDisposing.Disposing(Actor self) + { + ActOnFrozenActorsForAllPlayers(Remove); + } + + public void OnGpsASRefresh(Actor self, Player player) + { + if (self.IsDead) + ActOnFrozenActorForPlayer(player, Remove); + else + ActOnFrozenActorForPlayer(player, Refresh); + } + + void ActOnFrozenActorsForAllPlayers(FrozenActorAction action) + { + for (var playerIndex = 0; playerIndex < traits.Count; playerIndex++) + ActOnFrozenActorForTraits(traits[playerIndex], action); + } + + void ActOnFrozenActorForPlayer(Player player, FrozenActorAction action) + { + ActOnFrozenActorForTraits(traits[player], action); + } + + void ActOnFrozenActorForTraits(Traits t, FrozenActorAction action) + { + if (t.FrozenActorLayer == null || t.GpsWatcher == null || + !t.GpsWatcher.Granted || !t.GpsWatcher.GrantedAllies) + return; + + var fa = t.FrozenActorLayer.FromID(self.ActorID); + if (fa == null) + return; + + action(this, t.FrozenActorLayer, t.GpsWatcher, fa); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Garrisonable.cs b/OpenRA.Mods.AS/Traits/Garrisonable.cs new file mode 100644 index 000000000000..b186265196a4 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Garrisonable.cs @@ -0,0 +1,541 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Orders; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor can store Garrisoner actors.")] + public class GarrisonableInfo : PausableConditionalTraitInfo, Requires + { + [Desc("The maximum sum of Garrisoner.Weight that this actor can support.")] + public readonly int MaxWeight = 0; + + [Desc("`Garrisoner.GarrisonType`s that can be loaded into this actor.")] + public readonly HashSet Types = new(); + + [Desc("A list of actor types that are initially spawned into this actor.")] + public readonly string[] InitialUnits = Array.Empty(); + + [Desc("When this actor is sold should all of its garrisoners be unloaded?")] + public readonly bool EjectOnSell = true; + + [Desc("When this actor dies should all of its garrisoners be unloaded?")] + public readonly bool EjectOnDeath = false; + + [Desc("Terrain types that this actor is allowed to eject actors onto. Leave empty for all terrain types.")] + public readonly HashSet UnloadTerrainTypes = new(); + + [VoiceReference] + [Desc("Voice to play when ordered to unload the garrisoners.")] + public readonly string UnloadVoice = "Action"; + + [Desc("Radius to search for a load/unload location if the ordered cell is blocked.")] + public readonly WDist LoadRange = WDist.FromCells(5); + + [Desc("Which direction the garrisoner will face (relative to the transport) when unloading.")] + public readonly int GarrisonerFacing = 128; + + [Desc("Delay (in ticks) before continuing after loading a passenger.")] + public readonly int AfterLoadDelay = 8; + + [Desc("Delay (in ticks) before unloading the first passenger.")] + public readonly int BeforeUnloadDelay = 8; + + [Desc("Delay (in ticks) before continuing after unloading a passenger.")] + public readonly int AfterUnloadDelay = 25; + + [Desc("Cursor to display when able to unload the garrisoners.")] + public readonly string UnloadCursor = "deploy"; + + [Desc("Cursor to display when unable to unload the garrisoners.")] + public readonly string UnloadBlockedCursor = "deploy-blocked"; + + [GrantedConditionReference] + [Desc("The condition to grant to self while waiting for garrisonable to load.")] + public readonly string LoadingCondition = null; + + [GrantedConditionReference] + [Desc("The condition to grant to self while garrisoners are loaded.", + "Condition can stack with multiple garrisoners.")] + public readonly string LoadedCondition = null; + + [Desc("Conditions to grant when specified actors are loaded inside the transport.", + "A dictionary of [actor id]: [condition].")] + public readonly Dictionary GarrisonerConditions = new(); + + [GrantedConditionReference] + public IEnumerable LinterGarrisonerConditions { get { return GarrisonerConditions.Values; } } + + [Desc("Change the passengers owner if transport owner changed")] + public readonly bool OwnerChangedAffectsGarrisoners = true; + + public override object Create(ActorInitializer init) { return new Garrisonable(init, this); } + } + + public class Garrisonable : PausableConditionalTrait, IIssueOrder, IResolveOrder, IOrderVoice, INotifyCreated, INotifyKilled, + INotifyOwnerChanged, INotifyAddedToWorld, ITick, INotifySold, INotifyActorDisposing, IIssueDeployOrder, + ITransformActorInitModifier, INotifyPassengersDamage + { + readonly Actor self; + readonly List garrisonable = new(); + readonly HashSet reserves = new(); + readonly Dictionary> garrisonerTokens = new(); + readonly Lazy facing; + readonly bool checkTerrainType; + readonly Stack loadedTokens = new(); + + public int TotalWeight = 0; + int reservedWeight = 0; + Aircraft aircraft; + int loadingToken = Actor.InvalidConditionToken; + bool takeOffAfterLoad; + bool initialised; + + CPos currentCell; + public IEnumerable CurrentAdjacentCells { get; private set; } + public IEnumerable Garrisoners { get { return garrisonable; } } + public int GarrisonerCount { get { return garrisonable.Count; } } + + enum State { Free, Locked } + State state = State.Free; + + public Garrisonable(ActorInitializer init, GarrisonableInfo info) + : base(info) + { + self = init.Self; + checkTerrainType = info.UnloadTerrainTypes.Count > 0; + + var runtimeGarrisonInit = init.GetOrDefault(info); + var garrisonInit = init.GetOrDefault(info); + if (runtimeGarrisonInit != null) + { + garrisonable = runtimeGarrisonInit.Value.ToList(); + TotalWeight = garrisonable.Sum(c => GetWeight(c)); + } + else if (garrisonInit != null) + { + foreach (var u in garrisonInit.Value) + { + var unit = self.World.CreateActor(false, u.ToLowerInvariant(), + new TypeDictionary { new OwnerInit(self.Owner) }); + + garrisonable.Add(unit); + } + + TotalWeight = garrisonable.Sum(c => GetWeight(c)); + } + else + { + foreach (var u in info.InitialUnits) + { + var unit = self.World.CreateActor(false, u.ToLowerInvariant(), + new TypeDictionary { new OwnerInit(self.Owner) }); + + garrisonable.Add(unit); + } + + TotalWeight = garrisonable.Sum(c => GetWeight(c)); + } + + facing = Exts.Lazy(self.TraitOrDefault); + } + + protected override void Created(Actor self) + { + base.Created(self); + + aircraft = self.TraitOrDefault(); + + if (garrisonable.Any()) + { + foreach (var c in garrisonable) + { + if (Info.GarrisonerConditions.TryGetValue(c.Info.Name, out var garrisonerCondition)) + garrisonerTokens.GetOrAdd(c.Info.Name).Push(self.GrantCondition(garrisonerCondition)); + } + + if (!string.IsNullOrEmpty(Info.LoadedCondition)) + loadedTokens.Push(self.GrantCondition(Info.LoadedCondition)); + } + + // Defer notifications until we are certain all traits on the transport are initialised + self.World.AddFrameEndTask(w => + { + foreach (var c in garrisonable) + { + c.Trait().Transport = self; + + foreach (var nec in c.TraitsImplementing()) + nec.OnEnteredGarrison(c, self); + + foreach (var npe in self.TraitsImplementing()) + npe.OnGarrisonerEntered(self, c); + } + + initialised = true; + }); + } + + static int GetWeight(Actor a) { return a.Info.TraitInfo().Weight; } + + public IEnumerable Orders + { + get + { + yield return new DeployOrderTargeter("Unload", 10, + () => CanUnload() ? Info.UnloadCursor : Info.UnloadBlockedCursor); + } + } + + public Order IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) + { + if (order.OrderID == "Unload") + return new Order(order.OrderID, self, queued); + + return null; + } + + Order IIssueDeployOrder.IssueDeployOrder(Actor self, bool queued) + { + return new Order("Unload", self, queued); + } + + bool IIssueDeployOrder.CanIssueDeployOrder(Actor self, bool queued) { return true; } + + public void ResolveOrder(Actor self, Order order) + { + if (order.OrderString == "Unload") + { + if (!order.Queued && !CanUnload()) + return; + + self.QueueActivity(new UnloadGarrison(self, Info.LoadRange)); + } + } + + IEnumerable GetAdjacentCells() + { + return Util.AdjacentCells(self.World, Target.FromActor(self)).Where(c => self.Location != c); + } + + public bool CanUnload(BlockedByActor check = BlockedByActor.None) + { + if (checkTerrainType) + { + if (!self.World.Map.Contains(self.Location)) + return false; + + if (!Info.UnloadTerrainTypes.Contains(self.World.Map.GetTerrainInfo(self.Location).Type)) + return false; + } + + return !IsEmpty() && (aircraft == null || aircraft.CanLand(self.Location, blockedByMobile: false)) + && CurrentAdjacentCells != null && CurrentAdjacentCells.Any(c => Garrisoners.Any(p => !p.IsDead && p.Trait().CanEnterCell(c, null, check))); + } + + public bool CanLoad(Actor a) + { + return reserves.Contains(a) || HasSpace(GetWeight(a)); + } + + internal bool ReserveSpace(Actor a) + { + if (reserves.Contains(a)) + return true; + + var w = GetWeight(a); + if (!HasSpace(w)) + return false; + + if (loadingToken == Actor.InvalidConditionToken && !string.IsNullOrEmpty(Info.LoadingCondition)) + loadingToken = self.GrantCondition(Info.LoadingCondition); + + reserves.Add(a); + reservedWeight += w; + LockForPickup(self); + + return true; + } + + internal void UnreserveSpace(Actor a) + { + if (!reserves.Contains(a) || self.IsDead) + return; + + reservedWeight -= GetWeight(a); + reserves.Remove(a); + ReleaseLock(self); + + if (loadingToken != Actor.InvalidConditionToken) + loadingToken = self.RevokeCondition(loadingToken); + } + + // Prepare for transport pickup + void LockForPickup(Actor self) + { + if (state == State.Locked) + return; + + state = State.Locked; + + self.CancelActivity(); + + var air = self.TraitOrDefault(); + if (air != null && !air.AtLandAltitude) + { + takeOffAfterLoad = true; + self.QueueActivity(new Land(self)); + } + + self.QueueActivity(new WaitFor(() => state != State.Locked, false)); + } + + void ReleaseLock(Actor self) + { + if (reservedWeight != 0) + return; + + state = State.Free; + + self.QueueActivity(new Wait(Info.AfterLoadDelay, false)); + if (takeOffAfterLoad) + self.QueueActivity(new TakeOff(self)); + + takeOffAfterLoad = false; + } + + public string VoicePhraseForOrder(Actor self, Order order) + { + if (order.OrderString != "Unload" || IsEmpty() || !self.HasVoice(Info.UnloadVoice)) + return null; + + return Info.UnloadVoice; + } + + public bool HasSpace(int weight) { return TotalWeight + reservedWeight + weight <= Info.MaxWeight; } + public bool IsEmpty() { return garrisonable.Count == 0; } + + public Actor Peek() { return garrisonable.Last(); } + + public Actor Unload(Actor self, Actor passenger = null) + { + passenger ??= garrisonable.Last(); + if (!garrisonable.Remove(passenger)) + throw new ArgumentException("Attempted to ungarrison an actor that is not a garrisoner."); + + TotalWeight -= GetWeight(passenger); + + SetGarrisonerFacing(passenger); + + foreach (var npe in self.TraitsImplementing()) + npe.OnGarrisonerExited(self, passenger); + + foreach (var nec in passenger.TraitsImplementing()) + nec.OnExitedGarrison(passenger, self); + + var p = passenger.Trait(); + p.Transport = null; + + if (garrisonerTokens.TryGetValue(passenger.Info.Name, out var garrisonerToken) && garrisonerToken.Any()) + self.RevokeCondition(garrisonerToken.Pop()); + + if (loadedTokens.Any()) + self.RevokeCondition(loadedTokens.Pop()); + + return passenger; + } + + void SetGarrisonerFacing(Actor garrisoner) + { + if (facing.Value == null) + return; + + var garrisonerFacing = garrisoner.TraitOrDefault(); + if (garrisonerFacing != null) + garrisonerFacing.Facing = facing.Value.Facing + WAngle.FromFacing(Info.GarrisonerFacing); + } + + public void Load(Actor self, Actor a) + { + garrisonable.Add(a); + var w = GetWeight(a); + TotalWeight += w; + if (reserves.Contains(a)) + { + reservedWeight -= w; + reserves.Remove(a); + ReleaseLock(self); + + if (loadingToken != Actor.InvalidConditionToken) + loadingToken = self.RevokeCondition(loadingToken); + } + + // Don't initialise (effectively twice) if this runs before the FrameEndTask from Created + if (initialised) + { + a.Trait().Transport = self; + + foreach (var nec in a.TraitsImplementing()) + nec.OnEnteredGarrison(a, self); + + foreach (var npe in self.TraitsImplementing()) + npe.OnGarrisonerEntered(self, a); + } + + if (Info.GarrisonerConditions.TryGetValue(a.Info.Name, out var garrisonerCondition)) + garrisonerTokens.GetOrAdd(a.Info.Name).Push(self.GrantCondition(garrisonerCondition)); + + if (!string.IsNullOrEmpty(Info.LoadedCondition)) + loadedTokens.Push(self.GrantCondition(Info.LoadedCondition)); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (Info.EjectOnDeath) + while (!IsEmpty() && CanUnload(BlockedByActor.All)) + { + var garrisoner = Unload(self); + var cp = self.CenterPosition; + var inAir = self.World.Map.DistanceAboveTerrain(cp).Length != 0; + var positionable = garrisoner.Trait(); + positionable.SetPosition(garrisoner, self.Location); + + if (!inAir && positionable.CanEnterCell(self.Location, self, BlockedByActor.None)) + { + self.World.AddFrameEndTask(w => w.Add(garrisoner)); + var nbms = garrisoner.TraitsImplementing(); + foreach (var nbm in nbms) + nbm.OnNotifyBlockingMove(garrisoner, garrisoner); + } + else + garrisoner.Kill(e.Attacker); + } + + foreach (var c in garrisonable) + c.Kill(e.Attacker); + + garrisonable.Clear(); + } + + void INotifyActorDisposing.Disposing(Actor self) + { + foreach (var c in garrisonable) + c.Dispose(); + + garrisonable.Clear(); + } + + void INotifySold.Selling(Actor self) { } + void INotifySold.Sold(Actor self) + { + if (!Info.EjectOnSell || garrisonable == null) + return; + + while (!IsEmpty()) + SpawnGarrisoner(Unload(self)); + } + + void SpawnGarrisoner(Actor garrisoner) + { + self.World.AddFrameEndTask(w => + { + w.Add(garrisoner); + garrisoner.Trait().SetPosition(garrisoner, self.Location); + + // TODO: this won't work well for >1 actor as they should move towards the next enterable (sub) cell instead + }); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + if (!Info.OwnerChangedAffectsGarrisoners || garrisonable == null) + return; + + foreach (var p in Garrisoners) + p.ChangeOwner(newOwner); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + // Force location update to avoid issues when initial spawn is outside map + currentCell = self.Location; + CurrentAdjacentCells = GetAdjacentCells(); + } + + void ITick.Tick(Actor self) + { + var cell = self.World.Map.CellContaining(self.CenterPosition); + if (currentCell != cell) + { + currentCell = cell; + CurrentAdjacentCells = GetAdjacentCells(); + } + } + + void ITransformActorInitModifier.ModifyTransformActorInit(Actor self, TypeDictionary init) + { + init.Add(new RuntimeGarrisonInit(Info, Garrisoners.ToArray())); + } + + protected override void TraitDisabled(Actor self) + { + if (!CanUnload()) + return; + + self.CancelActivity(); + self.QueueActivity(new UnloadGarrison(self, Info.LoadRange)); + } + + public int DamageVersus(Actor victim, Dictionary versus) + { + // If no Versus values are defined, DamageVersus would return 100 anyway, so we might as well do that early. + if (versus.Count == 0 || victim.IsDead) + return 100; + + var armor = victim.TraitsImplementing() + .Where(a => !a.IsTraitDisabled && a.Info.Type != null && versus.ContainsKey(a.Info.Type)) + .Select(a => versus[a.Info.Type]); + + return Util.ApplyPercentageModifiers(100, armor); + } + + void INotifyPassengersDamage.DamagePassengers(int damage, Actor attacker, int amount, Dictionary versus, BitSet damageTypes, IEnumerable damageModifiers) + { + var passengersToDamage = amount > 0 && amount < garrisonable.Count ? garrisonable.Shuffle(self.World.SharedRandom).Take(amount) : garrisonable; + foreach (var passenger in passengersToDamage) + { + var d = Util.ApplyPercentageModifiers(damage, damageModifiers.Append(DamageVersus(passenger, versus))); + passenger.InflictDamage(attacker, new Damage(d, damageTypes)); + } + } + } + + public class RuntimeGarrisonInit : ValueActorInit, ISuppressInitExport + { + public RuntimeGarrisonInit(TraitInfo info, Actor[] value) + : base(info, value) { } + } + + public class GarrisonInit : ValueActorInit + { + public GarrisonInit(TraitInfo info, string[] value) + : base(info, value) { } + } +} diff --git a/OpenRA.Mods.AS/Traits/Garrisoner.cs b/OpenRA.Mods.AS/Traits/Garrisoner.cs new file mode 100644 index 000000000000..60440bfba3ec --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Garrisoner.cs @@ -0,0 +1,221 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.AS.Orders; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor can enter Garrisonable actors.")] + public class GarrisonerInfo : TraitInfo + { + public readonly string GarrisonType = null; + + [Desc("If defined, use a custom pip type defined on the transport's WithGarrisonPipsDecoration.CustomPipSequences list.")] + public readonly string CustomPipType = null; + + public readonly int Weight = 1; + + [Desc("What diplomatic stances can be Garrisoned by this actor.")] + public readonly PlayerRelationship TargetRelationships = PlayerRelationship.Ally | PlayerRelationship.Neutral; + + [GrantedConditionReference] + [Desc("The condition to grant to when this actor is loaded inside any transport.")] + public readonly string GarrisonCondition = null; + + [Desc("Conditions to grant when this actor is loaded inside specified transport.", + "A dictionary of [actor id]: [condition].")] + public readonly Dictionary GarrisonConditions = new(); + + [GrantedConditionReference] + public IEnumerable LinterGarrisonConditions { get { return GarrisonConditions.Values; } } + + [VoiceReference] + public readonly string Voice = "Action"; + + [ConsumedConditionReference] + [Desc("Boolean expression defining the condition under which the regular (non-force) enter cursor is disabled.")] + public readonly BooleanExpression RequireForceMoveCondition = null; + + public override object Create(ActorInitializer init) { return new Garrisoner(this); } + } + + public class Garrisoner : IIssueOrder, IResolveOrder, IOrderVoice, INotifyRemovedFromWorld, INotifyEnteredGarrison, INotifyExitedGarrison, INotifyKilled, IObservesVariables + { + public readonly GarrisonerInfo Info; + public Actor Transport; + bool requireForceMove; + + int anyGarrisonToken = Actor.InvalidConditionToken; + int specificGarrisonToken = Actor.InvalidConditionToken; + + public Garrisoner(GarrisonerInfo info) + { + Info = info; + } + + public Garrisonable ReservedGarrison { get; private set; } + + IEnumerable IIssueOrder.Orders + { + get + { + yield return new EnterGarrisonOrderTargeter("EnterGarrison", 5, IsCorrectGarrisonType, CanEnter, Info); + } + } + + public Order IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) + { + if (order.OrderID == "EnterGarrison") + return new Order(order.OrderID, self, target, queued); + + return null; + } + + bool IsCorrectGarrisonType(Actor target, TargetModifiers modifiers) + { + if (requireForceMove && !modifiers.HasModifier(TargetModifiers.ForceMove)) + return false; + + return IsCorrectGarrisonType(target); + } + + bool IsCorrectGarrisonType(Actor target) + { + var ci = target.Info.TraitInfo(); + return ci.Types.Contains(Info.GarrisonType); + } + + bool CanEnter(Garrisonable garrison) + { + return garrison != null && garrison.HasSpace(Info.Weight) && !garrison.IsTraitPaused && !garrison.IsTraitDisabled; + } + + bool CanEnter(Actor target) + { + return CanEnter(target.TraitOrDefault()); + } + + public string VoicePhraseForOrder(Actor self, Order order) + { + if (order.OrderString != "EnterGarrison") + return null; + + if (order.Target.Type != TargetType.Actor || !CanEnter(order.Target.Actor)) + return null; + + return Info.Voice; + } + + void INotifyEnteredGarrison.OnEnteredGarrison(Actor self, Actor garrison) + { + if (anyGarrisonToken == Actor.InvalidConditionToken && !string.IsNullOrEmpty(Info.GarrisonCondition)) + anyGarrisonToken = self.GrantCondition(Info.GarrisonCondition); + + if (specificGarrisonToken == Actor.InvalidConditionToken && Info.GarrisonConditions.TryGetValue(garrison.Info.Name, out var specificGarrisonCondition)) + specificGarrisonToken = self.GrantCondition(specificGarrisonCondition); + + // Allow scripted / initial actors to move from the unload point back into the cell grid on unload + // This is handled by the RideTransport activity for player-loaded cargo + if (self.IsIdle) + { + // IMove is not used anywhere else in this trait, there is no benefit to caching it from Created. + var move = self.TraitOrDefault(); + if (move != null) + self.QueueActivity(move.ReturnToCell(self)); + } + } + + void INotifyExitedGarrison.OnExitedGarrison(Actor self, Actor garrison) + { + if (anyGarrisonToken != Actor.InvalidConditionToken) + anyGarrisonToken = self.RevokeCondition(anyGarrisonToken); + + if (specificGarrisonToken != Actor.InvalidConditionToken) + specificGarrisonToken = self.RevokeCondition(specificGarrisonToken); + } + + void IResolveOrder.ResolveOrder(Actor self, Order order) + { + if (order.OrderString != "EnterGarrison") + return; + + if (order.Target.Type == TargetType.Actor) + { + var targetActor = order.Target.Actor; + if (!CanEnter(targetActor)) + return; + + if (!IsCorrectGarrisonType(targetActor)) + return; + } + + /* + else + { + var targetActor = order.Target.FrozenActor; + } + */ + + self.QueueActivity(order.Queued, new EnterGarrison(self, order.Target, Color.Green)); + self.ShowTargetLines(); + } + + public bool Reserve(Actor self, Garrisonable garrison) + { + if (garrison == ReservedGarrison) + return true; + + Unreserve(self); + if (!garrison.ReserveSpace(self)) + return false; + + ReservedGarrison = garrison; + return true; + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) { Unreserve(self); } + + public void Unreserve(Actor self) + { + if (ReservedGarrison == null) + return; + + ReservedGarrison.UnreserveSpace(self); + ReservedGarrison = null; + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (Transport == null) + return; + + // Something killed us, but it wasn't our transport blowing up. Remove us from the cargo. + if (!Transport.IsDead) + self.World.AddFrameEndTask(w => Transport.Trait().Unload(Transport, self)); + } + + IEnumerable IObservesVariables.GetVariableObservers() + { + if (Info.RequireForceMoveCondition != null) + yield return new VariableObserver(RequireForceMoveConditionChanged, Info.RequireForceMoveCondition.Variables); + } + + void RequireForceMoveConditionChanged(Actor self, IReadOnlyDictionary conditions) + { + requireForceMove = Info.RequireForceMoveCondition.Evaluate(conditions); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/GivesExperienceToMasterOrTransport.cs b/OpenRA.Mods.AS/Traits/GivesExperienceToMasterOrTransport.cs new file mode 100644 index 000000000000..27d5c2d73201 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/GivesExperienceToMasterOrTransport.cs @@ -0,0 +1,112 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor gives experience to a GainsExperience actor when they are killed.")] + class GivesExperienceToMasterOrTransportInfo : TraitInfo + { + [Desc("If -1, use the value of the unit cost.")] + public readonly int Experience = -1; + + [Desc("Player relationships the attacking player needs to receive the experience.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Neutral | PlayerRelationship.Enemy; + + [Desc("Percentage of the `Experience` value that is being granted to the killing actor's mindcontrol master.")] + public readonly int ActorExperienceModifierOnMindControlMaster = 5000; + + [Desc("Percentage of the `Experience` value that is being granted to the killing actor's parent.")] + public readonly int ActorExperienceModifierOnSummonMaster = 10000; + + [Desc("Percentage of the `Experience` value that is being granted to the killing actor's transport.")] + public readonly int ActorExperienceModifierOnTransport = 5000; + + public override object Create(ActorInitializer init) { return new GivesExperienceToMasterOrTransport(this); } + } + + class GivesExperienceToMasterOrTransport : INotifyKilled, INotifyCreated + { + readonly GivesExperienceToMasterOrTransportInfo info; + + int exp; + IEnumerable experienceModifiers; + + public GivesExperienceToMasterOrTransport(GivesExperienceToMasterOrTransportInfo info) + { + this.info = info; + } + + void INotifyCreated.Created(Actor self) + { + var valued = self.Info.TraitInfoOrDefault(); + exp = info.Experience >= 0 ? info.Experience + : valued != null ? valued.Cost : 0; + + experienceModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetGivesExperienceModifier()); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (exp == 0 || e.Attacker == null || e.Attacker.Disposed) + return; + + exp = Util.ApplyPercentageModifiers(exp, experienceModifiers); + + var killer = e.Attacker; + if (killer != null) + { + if (info.ActorExperienceModifierOnMindControlMaster > 0) + { + foreach (var mindControllable in killer.TraitsImplementing()) + if (mindControllable.Master != null) + GiveExperience(self, mindControllable.Master, exp, info.ActorExperienceModifierOnMindControlMaster); + } + + if (info.ActorExperienceModifierOnTransport > 0) + { + foreach (var pass in killer.TraitsImplementing()) + if (pass.Transport != null) + GiveExperience(self, pass.Transport, exp, info.ActorExperienceModifierOnTransport); + } + + if (info.ActorExperienceModifierOnSummonMaster > 0) + { + var parent = killer.TraitOrDefault()?.Parent; + if (parent != null) + GiveExperience(self, parent, exp, info.ActorExperienceModifierOnSummonMaster); + } + } + } + + void GiveExperience(Actor self, Actor killer, int exp, int baseModifier) + { + if (killer.IsDead) + return; + + if (!info.ValidRelationships.HasRelationship(killer.Owner.RelationshipWith(self.Owner))) + return; + + var gainsExperience = killer.TraitOrDefault(); + if (gainsExperience == null) + return; + + var experienceModifier = killer.TraitsImplementing() + .Select(x => x.GetGainsExperienceModifier()).Append(baseModifier); + gainsExperience.GiveExperience(Util.ApplyPercentageModifiers(exp, experienceModifier)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/GivesIntelligence.cs b/OpenRA.Mods.AS/Traits/GivesIntelligence.cs new file mode 100644 index 000000000000..280724f446fc --- /dev/null +++ b/OpenRA.Mods.AS/Traits/GivesIntelligence.cs @@ -0,0 +1,79 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor activates other player's actors with `" + nameof(RevealsShroudToIntelligenceOwner) + "` trait to its owner.")] + public class GivesIntelligenceInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [Desc("Types of intelligence this actor gives.")] + public readonly HashSet Types = new(); + + public override object Create(ActorInitializer init) { return new GivesIntelligence(this); } + } + + public class GivesIntelligence : ConditionalTrait, INotifyActorDisposing, INotifyKilled + { + public GivesIntelligence(GivesIntelligenceInfo info) + : base(info) { } + + void RemoveIntelligence(Actor self) + { + foreach (var a in self.World.ActorsWithTrait() + .Where(rs => rs.Trait.RSTIOInfo.Types.Overlaps(Info.Types) && !rs.Actor.Owner.NonCombatant)) + { + if (!self.World.ActorsWithTrait() + .Any(gi => gi.Actor != self && gi.Actor.Owner == self.Owner && gi.Trait.Info.Types.Overlaps(a.Trait.RSTIOInfo.Types))) + { + a.Trait.RemoveCellsFromIntelligenceOwnerShroud(a.Actor, self.Owner); + a.Trait.IntelOwners.Remove(self.Owner); + } + } + } + + protected override void TraitEnabled(Actor self) + { + foreach (var a in self.World.ActorsWithTrait() + .Where(rs => rs.Trait.RSTIOInfo.Types.Overlaps(Info.Types) && !rs.Actor.Owner.NonCombatant)) + { + if (!a.Actor.IsInWorld) + return; + + var cells = a.Trait.GetIntelligenceProjectedCells(a.Actor); + + a.Trait.RemoveCellsFromIntelligenceOwnerShroud(a.Actor, self.Owner); + a.Trait.AddCellsToIntelligenceOwnerShroud(a.Actor, self.Owner, cells); + if (!a.Trait.IntelOwners.Contains(self.Owner)) + a.Trait.IntelOwners.Add(self.Owner); + } + } + + protected override void TraitDisabled(Actor self) + { + RemoveIntelligence(self); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + RemoveIntelligence(self); + } + + void INotifyActorDisposing.Disposing(Actor self) + { + RemoveIntelligence(self); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/GivesProximityBounty.cs b/OpenRA.Mods.AS/Traits/GivesProximityBounty.cs new file mode 100644 index 000000000000..8f282ae87afd --- /dev/null +++ b/OpenRA.Mods.AS/Traits/GivesProximityBounty.cs @@ -0,0 +1,107 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class ProximityBountyType { } + + [Desc("When killed, this actor causes nearby actors with the ProximityBounty trait to receive money.")] + class GivesProximityBountyInfo : ConditionalTraitInfo + { + [Desc("Percentage of the killed actor's Cost or CustomSellValue to be given.")] + public readonly int Percentage = 10; + + [Desc("Stance the attacking player needs to grant bounty to actors.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Neutral | PlayerRelationship.Enemy; + + [Desc("DeathTypes for which a bounty should be granted.", + "Use an empty list (the default) to allow all DeathTypes.")] + public readonly BitSet DeathTypes = default; + + [Desc("Bounty types for the ProximityBounty traits which a bounty should be granted.", + "Use an empty list (the default) to allow all of them.")] + public readonly BitSet BountyTypes = default; + + public override object Create(ActorInitializer init) { return new GivesProximityBounty(this); } + } + + class GivesProximityBounty : ConditionalTrait, INotifyKilled + { + public HashSet Collectors; + Cargo cargo; + + public GivesProximityBounty(GivesProximityBountyInfo info) + : base(info) + { + Collectors = new HashSet(); + } + + protected override void Created(Actor self) + { + cargo = self.TraitOrDefault(); + + base.Created(self); + } + + int GetBountyValue(Actor self) + { + return !IsTraitDisabled ? self.GetSellValue() * Info.Percentage / 100 : 0; + } + + int GetDisplayedBountyValue(Actor self, BitSet deathTypes, BitSet bountyType) + { + var bounty = GetBountyValue(self); + if (cargo == null) + return bounty; + + foreach (var a in cargo.Passengers) + { + var givesProximityBounty = a.TraitsImplementing().Where(gpb => deathTypes.Overlaps(gpb.Info.DeathTypes) + && gpb.Info.BountyTypes.Overlaps(bountyType)); + foreach (var gpb in givesProximityBounty) + bounty += gpb.GetDisplayedBountyValue(a, deathTypes, bountyType); + } + + return bounty; + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (!Collectors.Any()) + return; + + if (e.Attacker == null || e.Attacker.Disposed) + return; + + if (!Info.ValidRelationships.HasRelationship(e.Attacker.Owner.RelationshipWith(self.Owner))) + return; + + if (!Info.DeathTypes.IsEmpty && !e.Damage.DamageTypes.Overlaps(Info.DeathTypes)) + return; + + foreach (var c in Collectors) + { + if (!Info.BountyTypes.Overlaps(c.Info.BountyType)) + return; + + if (!c.Info.ValidRelationships.HasRelationship(e.Attacker.Owner.RelationshipWith(self.Owner))) + return; + + c.AddBounty(GetDisplayedBountyValue(self, e.Damage.DamageTypes, c.Info.BountyType)); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/GpsASProvider.cs b/OpenRA.Mods.AS/Traits/GpsASProvider.cs new file mode 100644 index 000000000000..8f43cdf57d89 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/GpsASProvider.cs @@ -0,0 +1,53 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor provides AS GPS.")] + public class GpsASProviderInfo : ConditionalTraitInfo + { + public override object Create(ActorInitializer init) { return new GpsASProvider(this); } + } + + public class GpsASProvider : ConditionalTrait, INotifyAddedToWorld, INotifyRemovedFromWorld + { + public GpsASProvider(GpsASProviderInfo info) + : base(info) { } + + GpsASWatcher watcher; + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + watcher = self.Owner.PlayerActor.Trait(); + + if (!IsTraitDisabled) + TraitEnabled(self); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + if (!IsTraitDisabled) + TraitDisabled(self); + } + + protected override void TraitEnabled(Actor self) + { + watcher.ActivateGps(this, self.Owner); + } + + protected override void TraitDisabled(Actor self) + { + watcher.DeactivateGps(this, self.Owner); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/GpsDotAS.cs b/OpenRA.Mods.AS/Traits/GpsDotAS.cs new file mode 100644 index 000000000000..309143eee428 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/GpsDotAS.cs @@ -0,0 +1,61 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.AS.Effects; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Show an indicator revealing the actor underneath the fog when a GpsASProvider is activated.")] + class GpsDotASInfo : ConditionalTraitInfo + { + [Desc("Sprite collection for symbols.")] + public readonly string Image = "gpsdot"; + + [SequenceReference(nameof(Image))] + [Desc("Sprite used for this actor.")] + public readonly string Sequence = "idle"; + + [PaletteReference(true)] + public readonly string IndicatorPalettePrefix = "player"; + + public readonly bool VisibleInShroud = true; + + public readonly WDist Range = WDist.Zero; + + public override object Create(ActorInitializer init) { return new GpsDotAS(this); } + } + + class GpsDotAS : ConditionalTrait, INotifyAddedToWorld, INotifyRemovedFromWorld + { + GpsDotEffectAS effect; + + public GpsDotAS(GpsDotASInfo info) + : base(info) { } + + protected override void Created(Actor self) + { + effect = new GpsDotEffectAS(self, this); + + base.Created(self); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + self.World.AddFrameEndTask(w => w.Add(effect)); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + self.World.AddFrameEndTask(w => w.Remove(effect)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/HasParent.cs b/OpenRA.Mods.AS/Traits/HasParent.cs new file mode 100644 index 000000000000..6781fcac659d --- /dev/null +++ b/OpenRA.Mods.AS/Traits/HasParent.cs @@ -0,0 +1,39 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Hack: store the parent actor that spawn this actor.")] + public sealed class HasParentInfo : TraitInfo + { + public override object Create(ActorInitializer init) { return new HasParent(init); } + } + + public sealed class HasParent : ITransformActorInitModifier + { + public Actor Parent { get; private set; } + public HasParent(ActorInitializer init) + { + var pa = init.GetOrDefault()?.Value; + if (pa != null) + init.World.AddFrameEndTask(_ => Parent = pa.Actor(init.World).Value); + } + + void ITransformActorInitModifier.ModifyTransformActorInit(Actor self, TypeDictionary init) + { + init.Add(new ParentActorInit(Parent)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Infectable.cs b/OpenRA.Mods.AS/Traits/Infectable.cs new file mode 100644 index 000000000000..6adcedab57bb --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Infectable.cs @@ -0,0 +1,203 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Handle infection by infector units.")] + public class InfectableInfo : ConditionalTraitInfo, Requires + { + [Desc("Damage types that removes the infector.")] + public readonly BitSet RemoveInfectorDamageTypes = default; + + [Desc("Damage types that kills the infector.")] + public readonly BitSet KillInfectorDamageTypes = default; + + [GrantedConditionReference] + [Desc("The condition to grant to self while infected by any actor.")] + public readonly string InfectedCondition = null; + + [GrantedConditionReference] + [Desc("Condition granted when being infected by another actor.")] + public readonly string BeingInfectedCondition = null; + + [Desc("Conditions to grant when infected by specified actors.", + "A dictionary of [actor id]: [condition].")] + public readonly Dictionary InfectedByConditions = new(); + + [GrantedConditionReference] + public IEnumerable LinterConditions { get { return InfectedByConditions.Values; } } + + public override object Create(ActorInitializer init) { return new Infectable(init.Self, this); } + } + + public class Infectable : ConditionalTrait, ISync, ITick, INotifyCreated, INotifyDamage, INotifyKilled, IRemoveInfector + { + readonly Health health; + + public Tuple Infector; + public int[] FirepowerMultipliers = Array.Empty(); + + [Sync] + public int Ticks; + + int beingInfectedToken = Actor.InvalidConditionToken; + Actor enteringInfector; + int infectedToken = Actor.InvalidConditionToken; + int infectedByToken = Actor.InvalidConditionToken; + + int dealtDamage = 0; + int suppressionCount = 0; + + public Infectable(Actor self, InfectableInfo info) + : base(info) + { + health = self.Trait(); + } + + public bool TryStartInfecting(Actor self, Actor infector) + { + if (infector != null) + { + if (enteringInfector == null) + { + enteringInfector = infector; + + if (beingInfectedToken == Actor.InvalidConditionToken && !string.IsNullOrEmpty(Info.BeingInfectedCondition)) + beingInfectedToken = self.GrantCondition(Info.BeingInfectedCondition); + + return true; + } + } + + return false; + } + + public void GrantCondition(Actor self) + { + if (infectedToken == Actor.InvalidConditionToken && !string.IsNullOrEmpty(Info.InfectedCondition)) + infectedToken = self.GrantCondition(Info.InfectedCondition); + + if (Info.InfectedByConditions.TryGetValue(Infector.Item1.Info.Name, out var infectedByCondition)) + infectedByToken = self.GrantCondition(infectedByCondition); + } + + public void RevokeCondition(Actor self, Actor infector = null) + { + if (infector != null) + { + if (enteringInfector == infector) + { + enteringInfector = null; + + if (beingInfectedToken != Actor.InvalidConditionToken) + beingInfectedToken = self.RevokeCondition(beingInfectedToken); + } + } + else + { + if (infectedToken != Actor.InvalidConditionToken) + infectedToken = self.RevokeCondition(infectedToken); + + if (infectedByToken != Actor.InvalidConditionToken) + infectedByToken = self.RevokeCondition(infectedByToken); + } + } + + void RemoveInfector(Actor self, bool kill, AttackInfo e) + { + if (Infector != null && !Infector.Item1.IsDead) + { + Infector.Item1.TraitOrDefault().SetPosition(Infector.Item1, self.CenterPosition); + self.World.AddFrameEndTask(w => + { + if (Infector == null || Infector.Item1.IsDead) + return; + + w.Add(Infector.Item1); + + if (kill) + { + if (e != null) + Infector.Item1.Kill(e.Attacker, e.Damage.DamageTypes); + else + Infector.Item1.Kill(self); + } + else + Infector.Item1.QueueActivity(false, new Nudge(Infector.Item1)); + + RevokeCondition(self); + Infector = null; + FirepowerMultipliers = Array.Empty(); + dealtDamage = 0; + suppressionCount = 0; + }); + } + } + + void INotifyDamage.Damaged(Actor self, AttackInfo e) + { + if (Infector != null) + { + if (e.Damage.DamageTypes.Overlaps(Info.KillInfectorDamageTypes)) + RemoveInfector(self, true, e); + else if (e.Damage.DamageTypes.Overlaps(Info.RemoveInfectorDamageTypes)) + RemoveInfector(self, false, e); + else if (e.Attacker != Infector.Item1 && e.Damage.DamageTypes.Overlaps(Infector.Item3.SuppressionDamageType)) + { + var kill = Infector.Item3.SuppressionDamageThreshold > 0 && e.Damage.Value > Infector.Item3.SuppressionDamageThreshold; + + dealtDamage += e.Damage.Value; + kill |= Infector.Item3.SuppressionSumThreshold > 0 && dealtDamage > Infector.Item3.SuppressionSumThreshold; + + suppressionCount++; + kill |= Infector.Item3.SuppressionCountThreshold > 0 && suppressionCount > Infector.Item3.SuppressionCountThreshold; + + if (kill) + RemoveInfector(self, true, e); + } + } + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (Infector != null) + { + var kill = !Infector.Item3.SurviveHostDamageTypes.Overlaps(e.Damage.DamageTypes); + RemoveInfector(self, kill, e); + } + } + + void ITick.Tick(Actor self) + { + if (!IsTraitDisabled && Infector != null) + { + if (--Ticks < 0) + { + var damage = Util.ApplyPercentageModifiers(Infector.Item3.Damage, FirepowerMultipliers); + health.InflictDamage(self, Infector.Item1, new Damage(damage, Infector.Item3.DamageTypes), false); + + Ticks = Infector.Item3.DamageInterval; + } + } + } + + void IRemoveInfector.RemoveInfector(Actor self, bool kill, AttackInfo e) + { + RemoveInfector(self, kill, e); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/InfiltrateForProxyActor.cs b/OpenRA.Mods.AS/Traits/InfiltrateForProxyActor.cs new file mode 100644 index 000000000000..e3fba95bc478 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/InfiltrateForProxyActor.cs @@ -0,0 +1,50 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + class InfiltrateForProxyActorInfo : ConditionalTraitInfo + { + [ActorReference] + [FieldLoader.Require] + public readonly string ProxyActor = null; + + public readonly HashSet Types = new(); + + public override object Create(ActorInitializer init) { return new InfiltrateForProxyActor(this); } + } + + class InfiltrateForProxyActor : ConditionalTrait, INotifyInfiltrated + { + readonly InfiltrateForProxyActorInfo info; + + public InfiltrateForProxyActor(InfiltrateForProxyActorInfo info) + : base(info) + { + this.info = info; + } + + void INotifyInfiltrated.Infiltrated(Actor self, Actor infiltrator, BitSet types) + { + if (IsTraitDisabled || !info.Types.Overlaps(types)) + return; + + infiltrator.World.AddFrameEndTask(w => w.CreateActor(info.ProxyActor, new TypeDictionary + { + new OwnerInit(infiltrator.Owner) + })); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/MindControllable.cs b/OpenRA.Mods.AS/Traits/MindControllable.cs new file mode 100644 index 000000000000..bfe82c9186b2 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/MindControllable.cs @@ -0,0 +1,177 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor can be mind controlled by other actors.")] + public class MindControllableInfo : PausableConditionalTraitInfo + { + [Desc("Condition to grant when under mindcontrol.")] + [GrantedConditionReference] + public readonly string Condition = null; + + [Desc("The sound played when the mindcontrol is revoked.")] + public readonly string[] RevokeControlSounds = Array.Empty(); + + [Desc("Do the sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the RevokeControlSounds played at.")] + public readonly float Volume = 1f; + + [Desc("Map player to transfer this actor to if the owner lost the game.")] + public readonly string FallbackOwner = "Creeps"; + + public override object Create(ActorInitializer init) { return new MindControllable(this); } + } + + public class MindControllable : PausableConditionalTrait, INotifyKilled, INotifyActorDisposing, INotifyOwnerChanged, INotifyTransform + { + readonly MindControllableInfo info; + Player creatorOwner; + bool controlChanging; + Actor oldSelf = null; + + int token = Actor.InvalidConditionToken; + + public Actor Master { get; private set; } + + // HACK: used for pass EXP to master or other thing when actor use Explodes attack. + public Actor MasterWhenDie { get; private set; } + + public MindControllable(MindControllableInfo info) + : base(info) + { + this.info = info; + } + + public void LinkMaster(Actor self, Actor master) + { + self.CancelActivity(); + + if (Master == null) + creatorOwner = self.Owner; + + controlChanging = true; + + var oldOwner = self.Owner; + self.ChangeOwner(master.Owner); + + UnlinkMaster(self, Master); + Master = master; + + if (token == Actor.InvalidConditionToken && !string.IsNullOrEmpty(Info.Condition)) + token = self.GrantCondition(Info.Condition); + + if (master.Owner == creatorOwner) + UnlinkMaster(self, master); + + self.World.AddFrameEndTask(_ => controlChanging = false); + } + + public void UnlinkMaster(Actor self, Actor master) + { + if (master == null) + return; + + self.World.AddFrameEndTask(_ => + { + if (master.IsDead || master.Disposed) + return; + + master.Trait().UnlinkSlave(master, self); + }); + + Master = null; + + if (token != Actor.InvalidConditionToken) + token = self.RevokeCondition(token); + } + + public void RevokeMindControl(Actor self) + { + self.CancelActivity(); + + controlChanging = true; + + if (creatorOwner.WinState == WinState.Lost) + self.ChangeOwner(self.World.Players.First(p => p.InternalName == info.FallbackOwner)); + else + self.ChangeOwner(creatorOwner); + + UnlinkMaster(self, Master); + + if (info.RevokeControlSounds.Any()) + { + var pos = self.CenterPosition; + if (info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, info.RevokeControlSounds.Random(self.World.SharedRandom), pos, info.Volume); + } + + self.World.AddFrameEndTask(_ => controlChanging = false); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + MasterWhenDie = Master; + UnlinkMaster(self, Master); + } + + void INotifyActorDisposing.Disposing(Actor self) + { + UnlinkMaster(self, Master); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + if (!controlChanging) + UnlinkMaster(self, Master); + } + + protected override void TraitDisabled(Actor self) + { + if (Master != null) + RevokeMindControl(self); + } + + void TransferMindControl(Actor self, MindControllable mc) + { + Master = mc.Master; + creatorOwner = mc.creatorOwner; + controlChanging = mc.controlChanging; + + if (token == Actor.InvalidConditionToken && !string.IsNullOrEmpty(Info.Condition)) + token = self.GrantCondition(Info.Condition); + } + + void INotifyTransform.BeforeTransform(Actor self) { oldSelf = self; } + void INotifyTransform.OnTransform(Actor self) { } + void INotifyTransform.AfterTransform(Actor self) + { + if (Master != null) + { + var mc = self.TraitOrDefault(); + if (mc != null) + { + mc.TransferMindControl(self, this); + if (oldSelf != null) + Master.Trait().TransformSlave(oldSelf, self); + } + else + self.ChangeOwner(creatorOwner); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/MindController.cs b/OpenRA.Mods.AS/Traits/MindController.cs new file mode 100644 index 000000000000..9d71726c9451 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/MindController.cs @@ -0,0 +1,174 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor can mind control other actors.")] + public class MindControllerInfo : PausableConditionalTraitInfo, Requires, Requires + { + [Desc("Name of the armaments that grant this condition.")] + public readonly HashSet ArmamentNames = new() { "primary" }; + + [Desc("Up to how many units can this unit control?", + "Use 0 or negative numbers for infinite.")] + public readonly int Capacity = 1; + + [Desc("If the capacity is reached, discard the oldest mind controlled unit and control the new one", + "If false, controlling new units is forbidden after capacity is reached.")] + public readonly bool DiscardOldest = true; + + [Desc("Condition to grant to self when controlling actors. Can stack up by the number of enslaved actors. You can use this to forbid firing of the dummy MC weapon.")] + [GrantedConditionReference] + public readonly string ControllingCondition; + + [Desc("The sound played when the unit is mindcontrolled.")] + public readonly string[] Sounds = Array.Empty(); + + [Desc("Do the sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the Sounds played at.")] + public readonly float Volume = 1f; + + public override object Create(ActorInitializer init) { return new MindController(this); } + } + + public class MindController : PausableConditionalTrait, INotifyAttack, INotifyKilled, INotifyActorDisposing, INotifyCreated, INotifyOwnerChanged + { + readonly MindControllerInfo info; + readonly List slaves = new(); + readonly Stack controllingTokens = new(); + + public IEnumerable Slaves { get { return slaves; } } + + public MindController(MindControllerInfo info) + : base(info) + { + this.info = info; + } + + void StackControllingCondition(Actor self, string condition) + { + if (string.IsNullOrEmpty(condition)) + return; + + controllingTokens.Push(self.GrantCondition(condition)); + } + + void UnstackControllingCondition(Actor self, string condition) + { + if (string.IsNullOrEmpty(condition)) + return; + + self.RevokeCondition(controllingTokens.Pop()); + } + + public void UnlinkSlave(Actor self, Actor slave) + { + if (slaves.Contains(slave)) + { + slaves.Remove(slave); + UnstackControllingCondition(self, info.ControllingCondition); + } + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + if (IsTraitDisabled || IsTraitPaused) + return; + + if (!info.ArmamentNames.Contains(a.Info.Name)) + return; + + if (target.Actor == null || !target.IsValidFor(self)) + return; + + if (self.Owner.RelationshipWith(target.Actor.Owner) == PlayerRelationship.Ally) + return; + + var mindControllable = target.Actor.TraitOrDefault(); + + if (mindControllable == null) + { + throw new InvalidOperationException( + $"`{self.Info.Name}` tried to mindcontrol `{target.Actor.Info.Name}`, but the latter does not have the necessary trait!"); + } + + if (mindControllable.IsTraitDisabled || mindControllable.IsTraitPaused) + return; + + if (info.Capacity > 0 && !info.DiscardOldest && slaves.Count >= info.Capacity) + return; + + slaves.Add(target.Actor); + StackControllingCondition(self, info.ControllingCondition); + mindControllable.LinkMaster(target.Actor, self); + + if (info.Sounds.Any()) + { + var pos = self.CenterPosition; + if (info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, info.Sounds.Random(self.World.SharedRandom), pos, info.Volume); + } + + if (info.Capacity > 0 && info.DiscardOldest && slaves.Count > info.Capacity) + slaves[0].Trait().RevokeMindControl(slaves[0]); + } + + void ReleaseSlaves(Actor self) + { + foreach (var s in slaves) + { + if (s.IsDead || s.Disposed) + continue; + + s.Trait().RevokeMindControl(s); + } + + slaves.Clear(); + while (controllingTokens.Any()) + UnstackControllingCondition(self, info.ControllingCondition); + } + + public void TransformSlave(Actor oldSlave, Actor newSlave) + { + if (slaves.Contains(oldSlave)) + slaves[slaves.FindIndex(o => o == oldSlave)] = newSlave; + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + ReleaseSlaves(self); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + ReleaseSlaves(self); + } + + void INotifyActorDisposing.Disposing(Actor self) + { + ReleaseSlaves(self); + } + + protected override void TraitDisabled(Actor self) + { + ReleaseSlaves(self); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/MissileSpawnerMaster.cs b/OpenRA.Mods.AS/Traits/MissileSpawnerMaster.cs new file mode 100644 index 000000000000..2dde8d221a73 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/MissileSpawnerMaster.cs @@ -0,0 +1,175 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor can spawn missile actors.")] + public class MissileSpawnerMasterInfo : BaseSpawnerMasterInfo + { + public readonly string Name = "primary"; + + [GrantedConditionReference] + [Desc("The condition to grant to self right after launching a spawned unit. (Used by V3 to make immobile.)")] + public readonly string LaunchingCondition = null; + + [Desc("After this many ticks, we remove the condition.")] + public readonly int LaunchingTicks = 15; + + [GrantedConditionReference] + [Desc("The condition to grant to self while spawned units are loaded.", + "Condition can stack with multiple spawns.")] + public readonly string LoadedCondition = null; + + [Desc("Conditions to grant when specified actors are contained inside the transport.", + "A dictionary of [actor id]: [condition].")] + public readonly Dictionary SpawnContainConditions = new(); + + [GrantedConditionReference] + public IEnumerable LinterSpawnContainConditions { get { return SpawnContainConditions.Values; } } + + public override object Create(ActorInitializer init) { return new MissileSpawnerMaster(init, this); } + } + + public class MissileSpawnerMaster : BaseSpawnerMaster, ITick, INotifyAttack + { + readonly Dictionary> spawnContainTokens = new(); + public readonly MissileSpawnerMasterInfo MissileSpawnerMasterInfo; + readonly Stack loadedTokens = new(); + + int respawnTicks = 0; + + int launchCondition = Actor.InvalidConditionToken; + int launchConditionTicks; + + GrantExternalConditionToSpawnedMissile[] gectsms; + + public MissileSpawnerMaster(ActorInitializer init, MissileSpawnerMasterInfo info) + : base(init, info) + { + MissileSpawnerMasterInfo = info; + } + + protected override void Created(Actor self) + { + base.Created(self); + + gectsms = self.TraitsImplementing().ToArray(); + + // Spawn initial load. + var burst = Info.InitialActorCount == -1 ? Info.Actors.Length : Info.InitialActorCount; + for (var i = 0; i < burst; i++) + Replenish(self, SlaveEntries); + } + + public override void OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + // Do nothing, because missiles can't be captured or mind controlled. + return; + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + + // The rate of fire of the dummy weapon determines the launch cycle as each shot + // invokes Attacking() + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + // HACK: If Armament hits instantly and kills the target, the target will become invalid + if (target.Type == TargetType.Invalid || IsTraitDisabled || IsTraitPaused || (Info.ArmamentNames.Count > 0 && !Info.ArmamentNames.Contains(a.Info.Name))) + return; + + // Issue retarget order for already launched ones + foreach (var slave in SlaveEntries) + if (slave.IsValid) + slave.SpawnerSlave.Attack(slave.Actor, target); + + var se = GetLaunchable(); + if (se == null) + return; + + if (MissileSpawnerMasterInfo.LaunchingCondition != null) + { + if (launchCondition == Actor.InvalidConditionToken) + launchCondition = self.GrantCondition(MissileSpawnerMasterInfo.LaunchingCondition); + + launchConditionTicks = MissileSpawnerMasterInfo.LaunchingTicks; + } + + // Program the trajectory. + var bm = se.Actor.Trait(); + bm.Target = Target.FromPos(target.CenterPosition); + + SpawnIntoWorld(self, se.Actor, self.CenterPosition + se.Offset.Rotate(self.Orientation)); + + if (spawnContainTokens.TryGetValue(a.Info.Name, out var spawnContainToken) && spawnContainToken.Any()) + self.RevokeCondition(spawnContainToken.Pop()); + + if (loadedTokens.Any()) + self.RevokeCondition(loadedTokens.Pop()); + + // Queue attack order, too. + // invalidate the slave entry so that slave will regen. + self.World.AddFrameEndTask(w => se.Actor = null); + + // Set clock so that regen happens. + if (respawnTicks <= 0) // Don't interrupt an already running timer! + respawnTicks = Info.RespawnTicks; + } + + BaseSpawnerSlaveEntry GetLaunchable() + { + foreach (var se in SlaveEntries) + if (se.IsValid) + return se; + + return null; + } + + public override void Replenish(Actor self, BaseSpawnerSlaveEntry entry) + { + base.Replenish(self, entry); + + foreach (var gectsm in gectsms.Where(t => !t.IsTraitDisabled)) + gectsm.GrantCondition(self, entry.Actor); + + if (MissileSpawnerMasterInfo.SpawnContainConditions.TryGetValue(entry.Actor.Info.Name, out var spawnContainCondition)) + spawnContainTokens.GetOrAdd(entry.Actor.Info.Name).Push(self.GrantCondition(spawnContainCondition)); + + if (!string.IsNullOrEmpty(MissileSpawnerMasterInfo.LoadedCondition)) + loadedTokens.Push(self.GrantCondition(MissileSpawnerMasterInfo.LoadedCondition)); + } + + void ITick.Tick(Actor self) + { + if (launchCondition != Actor.InvalidConditionToken && --launchConditionTicks < 0) + launchCondition = self.RevokeCondition(launchCondition); + + if (respawnTicks > 0) + { + respawnTicks--; + + // Time to respawn someting. + if (respawnTicks <= 0) + { + Replenish(self, SlaveEntries); + + // If there's something left to spawn, restart the timer. + if (SelectEntryToSpawn(SlaveEntries) != null) + respawnTicks = Util.ApplyPercentageModifiers(Info.RespawnTicks, reloadModifiers.Select(rm => rm.GetReloadModifier(MissileSpawnerMasterInfo.Name))); + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/MissileSpawnerSlave.cs b/OpenRA.Mods.AS/Traits/MissileSpawnerSlave.cs new file mode 100644 index 000000000000..c4e741fef174 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/MissileSpawnerSlave.cs @@ -0,0 +1,30 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +/* +Works without base engine modification. +However, Mods.Common\Activities\Air\Land.cs is modified to support the air units to land "mid air!" +See landHeight private variable to track the changes. +*/ + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This unit is \"slaved\" to a missile spawner master.")] + public class MissileSpawnerSlaveInfo : BaseSpawnerSlaveInfo + { + public override object Create(ActorInitializer init) { return new MissileSpawnerSlave(this); } + } + + public class MissileSpawnerSlave : BaseSpawnerSlave + { + public MissileSpawnerSlave(MissileSpawnerSlaveInfo info) + : base(info) { } + } +} diff --git a/OpenRA.Mods.AS/Traits/MobSpawnerMaster.cs b/OpenRA.Mods.AS/Traits/MobSpawnerMaster.cs new file mode 100644 index 000000000000..ea171613b975 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/MobSpawnerMaster.cs @@ -0,0 +1,384 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +/* + * Needs base engine modification. + * In AttackOmni.cs, SetTarget() made public. + * In Mobile.cs, OccupySpace (true by default) added. Aircraft trait for a dummy unit wasn't the greatest idea as they fly over anything. + * Move.cs, uses my PR which isn't in bleed yet. (PR to make Move use parent child activity) + * + * The difference between Spawner (carrier logic) and this is that + * carriers have units going in and out of the master actor for reload activities, + * while MobSpawner doesn't, thus MobSpawner has much simpler code. + */ + +/* + * The code is very similar to Spawner.cs. + * Sometimes it is neater to have a duplicate than to have wrong inheirtances. + */ + +namespace OpenRA.Mods.AS.Traits +{ + // What to do when master is killed or mind controlled + public enum MobMemberDisposal + { + DoNothing, + KillSlaves, + GiveSlavesToAttacker + } + + [Desc("This actor can spawn actors.")] + public class MobSpawnerMasterInfo : BaseSpawnerMasterInfo + { + public readonly string Name = "primary"; + + [Desc("Spawn at a member, not the nexus?")] + public readonly bool ExitByBudding = true; + + [Desc("Can the slaves be controlled independently?")] + public readonly bool SlavesHaveFreeWill = false; + + [Desc("This is a dummy spawner like in C&C Generals and use virtual position and health.")] + public readonly bool AggregateHealth = true; + + public readonly int AggregateHealthUpdateDelay = 17; // Just a visual parameter, Doesn't affect the game. + + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + base.RulesetLoaded(rules, ai); + + if (Actors == null || Actors.Length == 0) + throw new YamlException($"Actors is null or empty for MobSpawner for actor type {ai.Name}!"); + + if (InitialActorCount > Actors.Length || InitialActorCount < -1) + throw new YamlException("MobSpawner can't have more InitialActorCount than the actors defined!"); + + if (InitialActorCount == 0 && AggregateHealth) + throw new YamlException("You can't have InitialActorCount == 0 and AggregateHealth"); + } + + public override object Create(ActorInitializer init) { return new MobSpawnerMaster(init, this); } + } + + public class MobSpawnerMaster : BaseSpawnerMaster, INotifyOwnerChanged, ITick, + IResolveOrder, INotifyAttack + { + class MobSpawnerSlaveEntry : BaseSpawnerSlaveEntry + { + public new MobSpawnerSlave SpawnerSlave; + public Health Health; + } + + public new MobSpawnerMasterInfo Info { get; private set; } + + MobSpawnerSlaveEntry[] slaveEntries; + + bool hasSpawnedInitialLoad = false; + int spawnReplaceTicks = 0; + + IPositionable position; + Aircraft aircraft; + Health health; + + public MobSpawnerMaster(ActorInitializer init, MobSpawnerMasterInfo info) + : base(init, info) + { + Info = info; + } + + protected override void Created(Actor self) + { + position = self.TraitOrDefault(); + health = self.Trait(); + aircraft = self.TraitOrDefault(); + + base.Created(self); + + if (!IsTraitDisabled) + { + // Spawn initial load. + var burst = Info.InitialActorCount == -1 ? Info.Actors.Length : Info.InitialActorCount; + for (var i = 0; i < burst; i++) + Replenish(self, SlaveEntries); + + // The base class creates the slaves but doesn't move them into world. + // Let's do it here. + SpawnReplenishedSlaves(self); + + hasSpawnedInitialLoad = true; + } + } + + public override BaseSpawnerSlaveEntry[] CreateSlaveEntries(BaseSpawnerMasterInfo info) + { + slaveEntries = new MobSpawnerSlaveEntry[info.Actors.Length]; // For this class to use + + for (var i = 0; i < slaveEntries.Length; i++) + slaveEntries[i] = new MobSpawnerSlaveEntry(); + + return slaveEntries; // For the base class to use + } + + public override void InitializeSlaveEntry(Actor slave, BaseSpawnerSlaveEntry entry) + { + var se = entry as MobSpawnerSlaveEntry; + base.InitializeSlaveEntry(slave, se); + + se.SpawnerSlave = slave.Trait(); + se.Health = slave.Trait(); + } + + public void ResolveOrder(Actor self, Order order) + { + if (Info.SlavesHaveFreeWill) + return; + + switch (order.OrderString) + { + case "Stop": + StopSlaves(); + break; + default: + break; + } + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + // Mob Master only pause attack when trait is Disabled + // HACK: If Armament hits instantly and kills the target, the target will become invalid + if (target.Type == TargetType.Invalid || (Info.ArmamentNames.Count > 0 && !Info.ArmamentNames.Contains(a.Info.Name)) || Info.SlavesHaveFreeWill || IsTraitDisabled) + return; + + AssignTargetsToSlaves(target); + } + + void ITick.Tick(Actor self) + { + if (spawnReplaceTicks > 0 && !IsTraitDisabled) + { + spawnReplaceTicks--; + + // Time to respawn someting. + if (spawnReplaceTicks <= 0) + { + Replenish(self, slaveEntries); + + SpawnReplenishedSlaves(self); + + // If there's something left to spawn, restart the timer. + if (SelectEntryToSpawn(slaveEntries) != null) + spawnReplaceTicks = Util.ApplyPercentageModifiers(Info.RespawnTicks, reloadModifiers.Select(rm => rm.GetReloadModifier(Info.Name))); + } + } + + // I'm a virtual mob spawning nexus. + if (Info.AggregateHealth) + { + SetNexusPosition(self); + SetNexusHealth(self); + } + + if (!Info.SlavesHaveFreeWill) + AssignSlaveActivity(self); + } + + void SpawnReplenishedSlaves(Actor self) + { + var centerPosition = WPos.Zero; + if (!hasSpawnedInitialLoad || !Info.ExitByBudding) + { + // Spawning from a solid actor... + centerPosition = self.CenterPosition; + } + else + { + // Spawning from a virtual nexus: exit by an existing member. + var se = slaveEntries.FirstOrDefault(s => s.IsValid && s.Actor.IsInWorld); + if (se != null) + centerPosition = se.Actor.CenterPosition; + } + + // WPos.Zero implies this mob spawner master is dead or something. + if (centerPosition == WPos.Zero) + return; + + foreach (var se in slaveEntries) + if (se.IsValid && !se.Actor.IsInWorld) + SpawnIntoWorld(self, se.Actor, centerPosition + se.Offset.Rotate(self.Orientation)); + } + + public override void OnSlaveKilled(Actor self, Actor slave) + { + // No need to update mobs entry because Actor.IsDead marking is done automatically by the engine. + // However, we need to check if all are dead when AggregateHealth. + if (Info.AggregateHealth && slaveEntries.All(m => !m.IsValid)) + self.Dispose(); + + if (spawnReplaceTicks <= 0) + spawnReplaceTicks = Info.RespawnTicks; + } + + void AssignTargetsToSlaves(Target target) + { + foreach (var se in slaveEntries) + { + if (!se.IsValid) + continue; + + se.SpawnerSlave.Attack(se.Actor, target); + } + } + + void MoveSlaves(Actor self) + { + var targets = self.CurrentActivity.GetTargets(self); + if (!targets.Any()) + return; + + var location = self.World.Map.CellContaining(targets.First().CenterPosition); + + foreach (var se in slaveEntries) + { + if (!se.IsValid || !se.Actor.IsInWorld) + continue; + + if (se.Actor.Location == location) + continue; + + if (!se.SpawnerSlave.IsMoving()) + { + se.SpawnerSlave.Stop(se.Actor); + se.SpawnerSlave.Move(se.Actor, location); + } + } + } + + CPos lastAttackMoveLocation; + void AttackMoveSlaves(Actor self) + { + var targets = self.CurrentActivity.GetTargets(self); + if (!targets.Any()) + return; + + var location = self.World.Map.CellContaining(targets.First().CenterPosition); + + if (lastAttackMoveLocation == location) + return; + + lastAttackMoveLocation = location; + + foreach (var se in slaveEntries) + { + if (!se.IsValid || !se.Actor.IsInWorld) + continue; + + se.SpawnerSlave.AttackMove(se.Actor, location); + } + } + + void SetNexusPosition(Actor self) + { + int x = 0, y = 0, cnt = 0; + foreach (var se in slaveEntries) + { + if (!se.IsValid || !se.Actor.IsInWorld) + continue; + + var pos = se.Actor.CenterPosition; + x += pos.X; + y += pos.Y; + cnt++; + } + + if (cnt == 0) + return; + + var newPos = new WPos(x / cnt, y / cnt, aircraft != null ? aircraft.Info.CruiseAltitude.Length : 0); + if (aircraft == null) + position.SetPosition(self, newPos); // breaks arrival detection of the aircraft if we set position. + + position.SetCenterPosition(self, newPos); + } + + int aggregateHealthUpdateTicks = 0; + + void SetNexusHealth(Actor self) + { + if (!Info.AggregateHealth) + return; + + if (aggregateHealthUpdateTicks > 0) + { + aggregateHealthUpdateTicks--; + return; + } + + aggregateHealthUpdateTicks = Info.AggregateHealthUpdateDelay; + + // Time to aggregate health. + var maxHealth = 0; + var h = 0; + + foreach (var se in slaveEntries) + { + maxHealth += se.Health.MaxHP; + + if (!se.IsValid) + continue; + + h += se.Health.HP; + } + + // Apply the aggregate health. + h = h * health.MaxHP / maxHealth; + + if (h > 0) + { + // Only do these when h > 0. + // Nexus kill when wiped out is handled else where. + // We can't set health. Inflict damage instead. + health.InflictDamage(self, self, new Damage(-health.MaxHP), true); // fully heal + health.InflictDamage(self, self, new Damage(health.MaxHP - h), true); // remove some health + } + } + + void AssignSlaveActivity(Actor self) + { + if (self.CurrentActivity is Move || self.CurrentActivity is Fly) + MoveSlaves(self); + else if (self.CurrentActivity is AttackMoveActivity) + AttackMoveSlaves(self); + else if (self.CurrentActivity is AttackOmni.SetTarget) + AssignTargetsToSlaves(self.CurrentActivity.GetTargets(self).First()); + } + + protected override void TraitEnabled(Actor self) + { + if (!Info.EnabledByDefault && !hasSpawnedInitialLoad) + { + var burst = Info.InitialActorCount == -1 ? Info.Actors.Length : Info.InitialActorCount; + for (var i = 0; i < burst; i++) + Replenish(self, SlaveEntries); + + SpawnReplenishedSlaves(self); + hasSpawnedInitialLoad = true; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/MobSpawnerSlave.cs b/OpenRA.Mods.AS/Traits/MobSpawnerSlave.cs new file mode 100644 index 000000000000..7bcecda49fd7 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/MobSpawnerSlave.cs @@ -0,0 +1,106 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +/* + * Needs base engine modification. (Becaus MobSpawner.cs mods it) + */ + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Can be slaved to a Mob spawner.")] + public class MobSpawnerSlaveInfo : BaseSpawnerSlaveInfo + { + public override object Create(ActorInitializer init) { return new MobSpawnerSlave(this); } + } + + public class MobSpawnerSlave : BaseSpawnerSlave, INotifySelected + { + public IMove[] Moves { get; private set; } + public IPositionable Positionable { get; private set; } + + MobSpawnerMaster spawnerMaster; + + public bool IsMoving() + { + return Moves.Any(m => m.IsTraitEnabled() && (m.CurrentMovementTypes.HasFlag(MovementType.Horizontal) || m.CurrentMovementTypes.HasFlag(MovementType.Vertical))); + } + + public MobSpawnerSlave(MobSpawnerSlaveInfo info) + : base(info) { } + + protected override void Created(Actor self) + { + base.Created(self); + + Moves = self.TraitsImplementing().ToArray(); + + var positionables = self.TraitsImplementing(); + if (positionables.Count() != 1) + throw new InvalidOperationException($"Actor {self} has multiple (or no) traits implementing IPositionable."); + + Positionable = positionables.First(); + } + + public override void LinkMaster(Actor self, Actor master, BaseSpawnerMaster spawnerMaster) + { + base.LinkMaster(self, master, spawnerMaster); + this.spawnerMaster = spawnerMaster as MobSpawnerMaster; + } + + public void Move(Actor self, CPos location) + { + // And tell attack bases to stop attacking. + if (Moves.Length == 0) + return; + + foreach (var mv in Moves) + if (mv.IsTraitEnabled()) + { + self.QueueActivity(mv.MoveTo(location, 2)); + break; + } + } + + public void AttackMove(Actor self, CPos location) + { + // And tell attack bases to stop attacking. + if (Moves.Length == 0) + return; + + foreach (var mv in Moves) + if (mv.IsTraitEnabled()) + { + // Must cancel before queueing as the master's attack move order is + // issued multiple times on multiple points along the attack move path. + self.CancelActivity(); + self.QueueActivity(new AttackMoveActivity(self, () => mv.MoveTo(location, 1))); + break; + } + } + + void INotifySelected.Selected(Actor self) + { + if (spawnerMaster.Info.SlavesHaveFreeWill) + return; + + // I'm assuming these guys are selectable, both slave and the nexus. + // self.World.Selection.Remove(self.World, self); No need to remove when you don't wee the selection decoration. + // -SelectionDecorations: is all you need. + // Also use RejectsOrder if necessary. + self.World.Selection.Add(Master); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Multipliers/BuildPaletteOrderModifier.cs b/OpenRA.Mods.AS/Traits/Multipliers/BuildPaletteOrderModifier.cs new file mode 100644 index 000000000000..0c83ede84622 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Multipliers/BuildPaletteOrderModifier.cs @@ -0,0 +1,41 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Modifies the build palette order of this actor for a specific queue or when a prerequisite is granted.")] + public class BuildPaletteOrderModifierInfo : TraitInfo, IBuildPaletteOrderModifierInfo + { + [Desc("Additive modifier to apply.")] + public readonly int Modifier = 1; + + [Desc("Only apply this order change if owner has these prerequisites.")] + public readonly string[] Prerequisites = Array.Empty(); + + [Desc("Queues that this order will apply.")] + public readonly HashSet Queue = new(); + + int IBuildPaletteOrderModifierInfo.GetBuildPaletteOrderModifier(TechTree techTree, string queue) + { + if ((Queue.Count == 0 || Queue.Contains(queue)) && (Prerequisites.Length == 0 || techTree.HasPrerequisites(Prerequisites))) + return Modifier; + + return 0; + } + } + + public class BuildPaletteOrderModifier { } +} diff --git a/OpenRA.Mods.AS/Traits/Multipliers/GattlingReloadDelayMultiplier.cs b/OpenRA.Mods.AS/Traits/Multipliers/GattlingReloadDelayMultiplier.cs new file mode 100644 index 000000000000..9cde79ecfa91 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Multipliers/GattlingReloadDelayMultiplier.cs @@ -0,0 +1,126 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Modifies the reload time of weapons fired by this actor as the weapon firing.")] + public class GatlingReloadDelayMultiplierInfo : PausableConditionalTraitInfo + { + [Desc("Maximum Percentage modifier to apply.")] + public readonly int MaxModifier = 100; + + [Desc("Minimum Percentage modifier to apply.")] + public readonly int MinModifier = 25; + + [Desc("How many time trigger the Cool Down when not attack.")] + public readonly int CoolDownDelay = 20; + + [Desc("The change on reload modifier when not attack.")] + public readonly int CoolDownChange = 1; + + [Desc("The change on reload modifier when attack.")] + public readonly int HeatUpChange = -1; + + [Desc("Should an instance be revoked if the actor changes target?")] + public readonly bool RevokeOnNewTarget = false; + + [Desc("Weapon types to applies to. Leave empty to apply to all weapons.")] + public readonly HashSet Types = new(); + + public override object Create(ActorInitializer init) { return new GatlingReloadDelayMultiplier(this); } + } + + public class GatlingReloadDelayMultiplier : PausableConditionalTrait, IReloadModifier, INotifyAttack, ITick + { + int currentModifier; + int cooldown; + + // Only tracked when RevokeOnNewTarget is true. + Target lastTarget = Target.Invalid; + + public GatlingReloadDelayMultiplier(GatlingReloadDelayMultiplierInfo info) + : base(info) + { + currentModifier = info.MaxModifier; + } + + static bool TargetChanged(in Target lastTarget, in Target target) + { + // Invalidate reveal changing the target. + if (lastTarget.Type == TargetType.FrozenActor && target.Type == TargetType.Actor) + if (lastTarget.FrozenActor.Actor == target.Actor) + return false; + + if (lastTarget.Type == TargetType.Actor && target.Type == TargetType.FrozenActor) + if (target.FrozenActor.Actor == lastTarget.Actor) + return false; + + if (lastTarget.Type != target.Type) + return true; + + // Invalidate attacking different targets with shared target types. + if (lastTarget.Type == TargetType.Actor && target.Type == TargetType.Actor) + if (lastTarget.Actor != target.Actor) + return true; + + if (lastTarget.Type == TargetType.FrozenActor && target.Type == TargetType.FrozenActor) + if (lastTarget.FrozenActor != target.FrozenActor) + return true; + + if (lastTarget.Type == TargetType.Terrain && target.Type == TargetType.Terrain) + if (lastTarget.CenterPosition != target.CenterPosition) + return true; + + return false; + } + + void ITick.Tick(Actor self) + { + if (cooldown <= 0) + currentModifier += Info.CoolDownChange; + else + { + currentModifier += Info.HeatUpChange; + cooldown--; + } + + if (currentModifier > Info.MaxModifier) currentModifier = Info.MaxModifier; + else if (currentModifier < Info.MinModifier) currentModifier = Info.MinModifier; + } + + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + if (IsTraitDisabled || IsTraitPaused || (Info.Types.Count > 0 && !Info.Types.Contains(a.Info.Name))) + return; + + if (Info.RevokeOnNewTarget) + { + if (TargetChanged(lastTarget, target)) + currentModifier = Info.MaxModifier; + + lastTarget = target; + } + + cooldown = Info.CoolDownDelay; + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + + int IReloadModifier.GetReloadModifier(string armamentName) + { + return !IsTraitDisabled && (Info.Types.Count == 0 || (!string.IsNullOrEmpty(armamentName) && Info.Types.Contains(armamentName))) ? currentModifier : 100; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Multipliers/HealthPercentageFirepowerMultiplier.cs b/OpenRA.Mods.AS/Traits/Multipliers/HealthPercentageFirepowerMultiplier.cs new file mode 100644 index 000000000000..4ac496bc1a04 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Multipliers/HealthPercentageFirepowerMultiplier.cs @@ -0,0 +1,45 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Allow the actor to use it's health percentage", + "as a firepower multiplier.")] + class HealthPercentageFirepowerMultiplierInfo : ConditionalTraitInfo, Requires + { + [Desc("Weapon types to applies to. Leave empty to apply to all weapons.")] + public readonly HashSet Types = new(); + + public override object Create(ActorInitializer init) + { + return new HealthPercentageFirepowerMultiplier(init.Self, this); + } + } + + class HealthPercentageFirepowerMultiplier : ConditionalTrait, IFirepowerModifier + { + readonly Health health; + + public HealthPercentageFirepowerMultiplier(Actor self, HealthPercentageFirepowerMultiplierInfo info) + : base(info) + { + health = self.Trait(); + } + + int IFirepowerModifier.GetFirepowerModifier(string armamentName) + { + return !IsTraitDisabled && (Info.Types.Count == 0 || (!string.IsNullOrEmpty(armamentName) && Info.Types.Contains(armamentName))) ? 100 * health.HP / health.MaxHP : 100; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/PeriodicExplosion.cs b/OpenRA.Mods.AS/Traits/PeriodicExplosion.cs new file mode 100644 index 000000000000..0b73627c95c0 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/PeriodicExplosion.cs @@ -0,0 +1,174 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Explodes a weapon at the actor's position when enabled." + + "Reload/BurstDelays are used as explosion intervals.")] + public class PeriodicExplosionInfo : ConditionalTraitInfo, IRulesetLoaded + { + [WeaponReference] + [FieldLoader.Require] + [Desc("Weapon to be used for explosion.")] + public readonly string Weapon = null; + + public readonly string WeaponName = "primary"; + + public readonly bool ResetReloadWhenEnabled = true; + + [Desc("Which limited ammo pool should this weapon be assigned to.")] + public readonly string AmmoPoolName = ""; + + public WeaponInfo WeaponInfo { get; private set; } + + [Desc("Explosion offset relative to actor's position.")] + public readonly WVec LocalOffset = WVec.Zero; + + public override object Create(ActorInitializer init) { return new PeriodicExplosion(init.Self, this); } + + void IRulesetLoaded.RulesetLoaded(Ruleset rules, ActorInfo info) + { + var weaponToLower = Weapon.ToLowerInvariant(); + if (!rules.Weapons.TryGetValue(weaponToLower, out var weaponInfo)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{weaponToLower}'"); + + WeaponInfo = weaponInfo; + } + } + + class PeriodicExplosion : ConditionalTrait, ITick, INotifyCreated + { + readonly PeriodicExplosionInfo info; + readonly WeaponInfo weapon; + readonly BodyOrientation body; + + int fireDelay; + int burst; + AmmoPool ammoPool; + readonly List<(int Delay, Action Action)> delayedActions = new(); + + public PeriodicExplosion(Actor self, PeriodicExplosionInfo info) + : base(info) + { + this.info = info; + + weapon = info.WeaponInfo; + burst = weapon.Burst; + body = self.TraitOrDefault(); + } + + protected override void Created(Actor self) + { + ammoPool = self.TraitsImplementing().FirstOrDefault(la => la.Info.Name == Info.AmmoPoolName); + } + + void ITick.Tick(Actor self) + { + for (var i = 0; i < delayedActions.Count; i++) + { + var x = delayedActions[i]; + if (--x.Delay <= 0) + x.Action(); + delayedActions[i] = x; + } + + delayedActions.RemoveAll(a => a.Delay <= 0); + + if (!self.IsInWorld || IsTraitDisabled) + return; + + if (--fireDelay < 0) + { + if (ammoPool != null && !ammoPool.TakeAmmo(self, 1)) + return; + + var localoffset = body != null + ? body.LocalToWorld(info.LocalOffset.Rotate(body.QuantizeOrientation(self.Orientation))) + : info.LocalOffset; + + var args = new WarheadArgs + { + Weapon = weapon, + DamageModifiers = self.TraitsImplementing().Select(a => a.GetFirepowerModifier(info.WeaponName)).ToArray(), + Source = self.CenterPosition, + SourceActor = self, + WeaponTarget = Target.FromPos(self.CenterPosition + localoffset) + }; + + weapon.Impact(Target.FromPos(self.CenterPosition + localoffset), args); + + if (weapon.Report != null && weapon.Report.Any()) + { + var pos = self.CenterPosition; + if (weapon.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, weapon.Report, self.World, pos, null, weapon.SoundVolume); + } + + if (burst == weapon.Burst && weapon.StartBurstReport != null && weapon.StartBurstReport.Any()) + { + var pos = self.CenterPosition; + if (weapon.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, weapon.StartBurstReport, self.World, pos, null, weapon.SoundVolume); + } + + if (--burst > 0) + { + if (weapon.BurstDelays.Length == 1) + fireDelay = weapon.BurstDelays[0]; + else + fireDelay = weapon.BurstDelays[weapon.Burst - (burst + 1)]; + } + else + { + var modifiers = self.TraitsImplementing() + .Select(m => m.GetReloadModifier(info.WeaponName)); + fireDelay = Util.ApplyPercentageModifiers(weapon.ReloadDelay, modifiers); + burst = weapon.Burst; + + if (weapon.AfterFireSound != null && weapon.AfterFireSound.Any()) + { + ScheduleDelayedAction(weapon.AfterFireSoundDelay, () => + { + var pos = self.CenterPosition; + if (weapon.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, weapon.AfterFireSound, self.World, pos, null, weapon.SoundVolume); + }); + } + } + } + } + + protected override void TraitEnabled(Actor self) + { + if (info.ResetReloadWhenEnabled) + { + burst = weapon.Burst; + fireDelay = 0; + } + } + + protected void ScheduleDelayedAction(int t, Action a) + { + if (t > 0) + delayedActions.Add((t, a)); + else + a(); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/PeriodicProducer.cs b/OpenRA.Mods.AS/Traits/PeriodicProducer.cs new file mode 100644 index 000000000000..7de6e356c087 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/PeriodicProducer.cs @@ -0,0 +1,139 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Produces an actor without using the standard production queue.")] + public class PeriodicProducerInfo : PausableConditionalTraitInfo + { + [ActorReference] + [FieldLoader.Require] + [Desc("Actors to produce.")] + public readonly string[] Actors = null; + + [FieldLoader.Require] + [Desc("Production queue type to use")] + public readonly string Type = null; + + [Desc("Notification played when production is activated.", + "The filename of the audio is defined per faction in notifications.yaml.")] + public readonly string ReadyAudio = null; + + [Desc("Notification played when the exit is jammed.", + "The filename of the audio is defined per faction in notifications.yaml.")] + public readonly string BlockedAudio = null; + + [Desc("Duration between productions.")] + public readonly int ChargeDuration = 1000; + + public readonly bool ResetTraitOnEnable = false; + + public readonly bool ShowSelectionBar = false; + public readonly Color ChargeColor = Color.DarkOrange; + + [Desc("Defines to which players the bar is to be shown.")] + public readonly PlayerRelationship SelectionBarDisplayRelationships = PlayerRelationship.Ally; + + public override object Create(ActorInitializer init) { return new PeriodicProducer(init, this); } + } + + public class PeriodicProducer : PausableConditionalTrait, ISelectionBar, ITick, ISync + { + readonly PeriodicProducerInfo info; + readonly Actor self; + + [Sync] + public int Ticks { get; private set; } + + public PeriodicProducer(ActorInitializer init, PeriodicProducerInfo info) + : base(info) + { + this.info = info; + self = init.Self; + Ticks = info.ChargeDuration; + } + + void ITick.Tick(Actor self) + { + if (IsTraitPaused) + return; + + if (!IsTraitDisabled && --Ticks < 0) + { + var sp = self.TraitsImplementing() + .FirstOrDefault(p => !p.IsTraitDisabled && !p.IsTraitPaused && p.Info.Produces.Contains(info.Type)); + + var activated = false; + + if (sp != null) + { + foreach (var name in info.Actors) + { + var inits = new TypeDictionary + { + new OwnerInit(self.Owner), + new FactionInit(sp.Faction) + }; + + activated |= sp.Produce(self, self.World.Map.Rules.Actors[name.ToLowerInvariant()], info.Type, inits, 0); + } + } + + if (activated) + Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", info.ReadyAudio, self.Owner.Faction.InternalName); + else + Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", info.BlockedAudio, self.Owner.Faction.InternalName); + + Ticks = info.ChargeDuration; + } + } + + protected override void TraitEnabled(Actor self) + { + if (info.ResetTraitOnEnable) + Ticks = info.ChargeDuration; + } + + float ISelectionBar.GetValue() + { + if (!info.ShowSelectionBar || IsTraitDisabled) + return 0f; + + var viewer = self.World.RenderPlayer ?? self.World.LocalPlayer; + if (viewer != null && !Info.SelectionBarDisplayRelationships.HasRelationship(self.Owner.RelationshipWith(viewer))) + return 0f; + + return (float)(info.ChargeDuration - Ticks) / info.ChargeDuration; + } + + Color ISelectionBar.GetColor() + { + return info.ChargeColor; + } + + bool ISelectionBar.DisplayWhenEmpty + { + get + { + var viewer = self.World.RenderPlayer ?? self.World.LocalPlayer; + if (viewer != null && !Info.SelectionBarDisplayRelationships.HasRelationship(self.Owner.RelationshipWith(viewer))) + return false; + + return info.ShowSelectionBar && !IsTraitDisabled; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Player/GpsASWatcher.cs b/OpenRA.Mods.AS/Traits/Player/GpsASWatcher.cs new file mode 100644 index 000000000000..83e4a3023b91 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Player/GpsASWatcher.cs @@ -0,0 +1,99 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Required for AS GPS-related logic to function. Attach this to the player actor.")] + class GpsASWatcherInfo : TraitInfo + { + public override object Create(ActorInitializer init) { return new GpsASWatcher(init.Self.Owner); } + } + + interface IOnGpsASRefreshed { void OnGpsASRefresh(Actor self, Player player); } + + class GpsASWatcher : ISync, IPreventsShroudReset + { + [Sync] + public bool GrantedAllies { get; private set; } + + [Sync] + public bool Granted { get; private set; } + + readonly Player owner; + + public readonly List Providers = new(); + readonly HashSet> notifyOnRefresh = new(); + + public GpsASWatcher(Player owner) + { + this.owner = owner; + } + + public void DeactivateGps(GpsASProvider trait, Player owner) + { + if (!Providers.Contains(trait)) + return; + + Providers.Remove(trait); + RefreshGps(owner); + } + + public void ActivateGps(GpsASProvider trait, Player owner) + { + if (Providers.Contains(trait)) + return; + + Providers.Add(trait); + RefreshGps(owner); + } + + public void RefreshGps(Player launcher) + { + RefreshGranted(); + + foreach (var i in launcher.World.ActorsWithTrait()) + i.Trait.RefreshGranted(); + } + + void RefreshGranted() + { + var wasGranted = Granted; + var wasGrantedAllies = GrantedAllies; + var allyWatchers = owner.World.ActorsWithTrait().Where(kv => kv.Actor.Owner.IsAlliedWith(owner)); + + Granted = Providers.Count > 0; + GrantedAllies = allyWatchers.Any(w => w.Trait.Granted); + + if (wasGranted != Granted || wasGrantedAllies != GrantedAllies) + foreach (var tp in notifyOnRefresh.ToList()) + tp.Trait.OnGpsASRefresh(tp.Actor, owner); + } + + bool IPreventsShroudReset.PreventShroudReset(Actor self) + { + return Granted || GrantedAllies; + } + + public void RegisterForOnGpsRefreshed(Actor actor, IOnGpsASRefreshed toBeNotified) + { + notifyOnRefresh.Add(new TraitPair(actor, toBeNotified)); + } + + public void UnregisterForOnGpsRefreshed(Actor actor, IOnGpsASRefreshed toBeNotified) + { + notifyOnRefresh.Remove(new TraitPair(actor, toBeNotified)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Player/PlayerSilo.cs b/OpenRA.Mods.AS/Traits/Player/PlayerSilo.cs new file mode 100644 index 000000000000..3b693f487128 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Player/PlayerSilo.cs @@ -0,0 +1,108 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Used for silos defined on the player actor.")] + class PlayerSiloInfo : TraitInfo, IStoresResourcesInfo + { + [FieldLoader.Require] + [Desc("The amounts of resources that can be stored.")] + public readonly int Capacity = 28; + + [Desc("Which resources can be stored.")] + public readonly string[] Resources = Array.Empty(); + + string[] IStoresResourcesInfo.ResourceTypes => Resources; + + public override object Create(ActorInitializer init) { return new PlayerSilo(this); } + } + + class PlayerSilo : IStoresResources, ISync + { + readonly PlayerSiloInfo info; + + readonly Dictionary contents = new(); + + [Sync] + public int ContentHash + { + get + { + var value = 0; + foreach (var c in contents) + value += c.Value << c.Key.Length; + + return value; + } + } + + public int ContentsSum { get; private set; } = 0; + public IReadOnlyDictionary Contents { get; } + int IStoresResources.Capacity => info.Capacity; + + public PlayerSilo(PlayerSiloInfo info) + { + this.info = info; + + foreach (var r in info.Resources) + contents[r] = 0; + + Contents = new ReadOnlyDictionary(contents); + } + + public bool HasType(string resourceType) + { + return info.Resources.Contains(resourceType); + } + + int IStoresResources.AddResource(string resourceType, int value) + { + if (!HasType(resourceType)) + return value; + + if (ContentsSum + value > info.Capacity) + { + var added = info.Capacity - ContentsSum; + contents[resourceType] += added; + ContentsSum = info.Capacity; + return value - added; + } + + contents[resourceType] += value; + ContentsSum += value; + return 0; + } + + int IStoresResources.RemoveResource(string resourceType, int value) + { + if (!HasType(resourceType)) + return value; + + if (contents[resourceType] < value) + { + var leftover = value - contents[resourceType]; + ContentsSum -= contents[resourceType]; + contents[resourceType] = 0; + return leftover; + } + + contents[resourceType] -= value; + ContentsSum -= value; + return 0; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Player/RangedGpsWatcher.cs b/OpenRA.Mods.AS/Traits/Player/RangedGpsWatcher.cs new file mode 100644 index 000000000000..89ee28c0a234 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Player/RangedGpsWatcher.cs @@ -0,0 +1,99 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Required for Ranged GPS-related logic to function. Attach this to the player actor.")] + public class RangedGpsWatcherInfo : TraitInfo + { + public override object Create(ActorInitializer init) { return new RangedGpsWatcher(init.Self.Owner); } + } + + public interface IOnRangedGpsRefreshed { void OnRangedGpsRefresh(Actor self, Player player); } + + public class RangedGpsWatcher : ISync, IPreventsShroudReset + { + [Sync] + public bool GrantedAllies { get; private set; } + + [Sync] + public bool Granted { get; private set; } + + readonly Player owner; + + public readonly List Providers = new(); + readonly HashSet> notifyOnRefresh = new(); + + public RangedGpsWatcher(Player owner) + { + this.owner = owner; + } + + public void DeactivateGps(RangedGpsProvider trait, Player owner) + { + if (!Providers.Contains(trait)) + return; + + Providers.Remove(trait); + RefreshGps(owner); + } + + public void ActivateGps(RangedGpsProvider trait, Player owner) + { + if (Providers.Contains(trait)) + return; + + Providers.Add(trait); + RefreshGps(owner); + } + + public void RefreshGps(Player launcher) + { + RefreshGranted(); + + foreach (var i in launcher.World.ActorsWithTrait()) + i.Trait.RefreshGranted(); + } + + void RefreshGranted() + { + var wasGranted = Granted; + var wasGrantedAllies = GrantedAllies; + var allyWatchers = owner.World.ActorsWithTrait().Where(kv => kv.Actor.Owner.IsAlliedWith(owner)); + + Granted = Providers.Count > 0; + GrantedAllies = allyWatchers.Any(w => w.Trait.Granted); + + if (wasGranted != Granted || wasGrantedAllies != GrantedAllies) + foreach (var tp in notifyOnRefresh.ToList()) + tp.Trait.OnRangedGpsRefresh(tp.Actor, owner); + } + + bool IPreventsShroudReset.PreventShroudReset(Actor self) + { + return Granted || GrantedAllies; + } + + public void RegisterForOnGpsRefreshed(Actor actor, IOnRangedGpsRefreshed toBeNotified) + { + notifyOnRefresh.Add(new TraitPair(actor, toBeNotified)); + } + + public void UnregisterForOnGpsRefreshed(Actor actor, IOnRangedGpsRefreshed toBeNotified) + { + notifyOnRefresh.Remove(new TraitPair(actor, toBeNotified)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Player/SharedCargoManager.cs b/OpenRA.Mods.AS/Traits/Player/SharedCargoManager.cs new file mode 100644 index 000000000000..7dc864efc46c --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Player/SharedCargoManager.cs @@ -0,0 +1,61 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Manages the contents of shared cargos like GLA Tunnel Networks")] + public class SharedCargoManagerInfo : TraitInfo + { + [Desc("Type of shared cargo")] + public readonly string Type = "tunnel"; + + [Desc("The maximum sum of Passenger.Weight that this actor can support.")] + public readonly int MaxWeight = 0; + + public override object Create(ActorInitializer init) { return new SharedCargoManager(this); } + } + + public class SharedCargoManager + { + public SharedCargoManagerInfo Info; + public List Cargo = new(); + public HashSet Reserves = new(); + + public IEnumerable Passengers { get { return Cargo; } } + public int PassengerCount { get { return Cargo.Count; } } + + public int TotalWeight = 0; + public int ReservedWeight = 0; + + public SharedCargoManager(SharedCargoManagerInfo info) + { + Info = info; + } + + public bool HasSpace(int weight) { return TotalWeight + ReservedWeight + weight <= Info.MaxWeight; } + public bool IsEmpty() { return Cargo.Count == 0; } + + public void Clear(AttackInfo e = null) + { + foreach (var passenger in Cargo) + if (e != null) + passenger.Kill(e.Attacker, e.Damage.DamageTypes); + else + passenger.Dispose(); + + Cargo.Clear(); + TotalWeight = 0; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Player/Taunts.cs b/OpenRA.Mods.AS/Traits/Player/Taunts.cs new file mode 100644 index 000000000000..dc36ccc1b142 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Player/Taunts.cs @@ -0,0 +1,83 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Attach this to the player actor.")] + public class TauntsInfo : TraitInfo, ILobbyOptions + { + [Desc("Descriptive label for the taunts checkbox in the lobby.")] + public readonly string CheckboxLabel = "Taunts"; + + [Desc("Tooltip description for the taunts checkbox in the lobby.")] + public readonly string CheckboxDescription = "Enables /taunt command to play taunts to other players"; + + [Desc("Default value of the taunts checkbox in the lobby.")] + public readonly bool CheckboxEnabled = true; + + [Desc("Prevent the taunts checkbox state from being changed in the lobby.")] + public readonly bool CheckboxLocked = false; + + [Desc("Whether to display the taunts checkbox in the lobby.")] + public readonly bool CheckboxVisible = true; + + [Desc("Display order for the taunts checkbox in the lobby.")] + public readonly int CheckboxDisplayOrder = 0; + + IEnumerable ILobbyOptions.LobbyOptions(MapPreview map) + { + yield return new LobbyBooleanOption(map, "taunts", CheckboxLabel, CheckboxDescription, CheckboxVisible, CheckboxDisplayOrder, CheckboxEnabled, CheckboxLocked); + } + + public override object Create(ActorInitializer init) { return new Taunts(this); } + } + + public class Taunts : IResolveOrder, INotifyCreated + { + readonly TauntsInfo info; + public bool Enabled { get; private set; } + + public Taunts(TauntsInfo info) + { + this.info = info; + } + + void INotifyCreated.Created(Actor self) + { + Enabled = self.World.LobbyInfo.GlobalSettings.OptionOrDefault("taunts", info.CheckboxEnabled); + } + + public void ResolveOrder(Actor self, Order order) + { + if (!Enabled) + return; + + switch (order.OrderString) + { + case "Taunt": + { + if (self.World.LocalPlayer != null) + { + var rules = self.World.Map.Rules; + if (rules.Notifications["taunts"].NotificationsPools.Value.ContainsKey(order.TargetString)) + Game.Sound.PlayNotification(rules, self.World.LocalPlayer, "Taunts", order.TargetString, self.Owner.Faction.InternalName); + else + TextNotificationsManager.Debug("{0} is not a valid taunt.", order.TargetString); + } + + break; + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Player/TeleportNetworkManager.cs b/OpenRA.Mods.AS/Traits/Player/TeleportNetworkManager.cs new file mode 100644 index 000000000000..97757f73ea41 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Player/TeleportNetworkManager.cs @@ -0,0 +1,36 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This must be attached to player in order for TeleportNetwork to work.")] + public class TeleportNetworkManagerInfo : TraitInfo + { + [FieldLoader.Require] + [Desc("Type of TeleportNetwork that pairs up, in order for it to work.")] + public string Type; + + public override object Create(ActorInitializer init) { return new TeleportNetworkManager(this); } + } + + public class TeleportNetworkManager + { + public readonly string Type; + public int Count = 0; + public Actor PrimaryActor = null; + + public TeleportNetworkManager(TeleportNetworkManagerInfo info) + { + Type = info.Type; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/PointDefense.cs b/OpenRA.Mods.AS/Traits/PointDefense.cs new file mode 100644 index 000000000000..8a77aa99d376 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/PointDefense.cs @@ -0,0 +1,74 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor can destroy weaponry.")] + public class PointDefenseInfo : ConditionalTraitInfo, Requires + { + [FieldLoader.Require] + [Desc("Weapon used to shoot the projectile. Caution: make sure that this is an insta-hit weapon, otherwise will look very odd!")] + public readonly string Armament; + + [FieldLoader.Require] + [Desc("What kind of projectiles can this actor shoot at.")] + public readonly BitSet PointDefenseTypes = default; + + [Desc("What diplomatic stances are affected.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Neutral | PlayerRelationship.Enemy; + + public override object Create(ActorInitializer init) { return new PointDefense(init.Self, this); } + } + + public class PointDefense : ConditionalTrait, IPointDefense + { + readonly Actor self; + readonly PointDefenseInfo info; + readonly Armament armament; + + public PointDefense(Actor self, PointDefenseInfo info) + : base(info) + { + this.self = self; + this.info = info; + armament = self.TraitsImplementing().First(a => a.Info.Name == info.Armament); + } + + bool IPointDefense.Destroy(WPos position, Player attacker, BitSet types) + { + if (IsTraitDisabled || armament.IsTraitDisabled || armament.IsTraitPaused) + return false; + + if (!info.ValidRelationships.HasRelationship(self.Owner.RelationshipWith(attacker))) + return false; + + if (armament.IsReloading) + return false; + + if (!info.PointDefenseTypes.Overlaps(types)) + return false; + + if ((self.CenterPosition - position).HorizontalLengthSquared > armament.MaxRange().LengthSquared) + return false; + + self.World.AddFrameEndTask(w => + { + if (!self.IsDead) + armament.CheckFire(self, null, Target.FromPos(position), true); + }); + return true; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/ProximityBounty.cs b/OpenRA.Mods.AS/Traits/ProximityBounty.cs new file mode 100644 index 000000000000..6fc207f1fdc1 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/ProximityBounty.cs @@ -0,0 +1,181 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class ProximityBountyInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [Desc("The range within bounty gets collected.")] + public readonly WDist Range; + + [Desc("The maximum vertical range above terrain within bounty gets collected.", + "Ignored if 0 (actors are upgraded regardless of vertical distance).")] + public readonly WDist MaximumVerticalOffset = WDist.Zero; + + [Desc("What killer diplomatic stances gathers bounty.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Ally | PlayerRelationship.Neutral | PlayerRelationship.Enemy; + + [Desc("Delay between awarding the bounty.")] + public readonly int Delay = 50; + + [Desc("The type which allows the actor to collect nearby bounty.")] + public readonly BitSet BountyType = default; + + [Desc("Whether to show a floating text announcing the won bounty.")] + public readonly bool ShowBounty = true; + + public readonly string EnableSound = null; + public readonly string DisableSound = null; + + public override object Create(ActorInitializer init) { return new ProximityBounty(init.Self, this); } + } + + public class ProximityBounty : ConditionalTrait, ITick, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOtherProduction + { + readonly Actor self; + + int proximityTrigger; + WPos cachedPosition; + WDist cachedRange; + WDist desiredRange; + WDist cachedVRange; + WDist desiredVRange; + + bool cachedDisabled = true; + int currentBounty; + int ticks; + + public ProximityBounty(Actor self, ProximityBountyInfo info) + : base(info) + { + this.self = self; + cachedRange = info.Range; + cachedVRange = info.MaximumVerticalOffset; + ticks = Info.Delay; + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + cachedPosition = self.CenterPosition; + proximityTrigger = self.World.ActorMap.AddProximityTrigger(cachedPosition, cachedRange, cachedVRange, ActorEntered, ActorExited); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + self.World.ActorMap.RemoveProximityTrigger(proximityTrigger); + GrantBounty(); + } + + void ITick.Tick(Actor self) + { + var disabled = IsTraitDisabled; + + if (cachedDisabled != disabled) + { + Game.Sound.Play(SoundType.World, disabled ? Info.DisableSound : Info.EnableSound, self.CenterPosition); + desiredRange = disabled ? WDist.Zero : Info.Range; + desiredVRange = disabled ? WDist.Zero : Info.MaximumVerticalOffset; + cachedDisabled = disabled; + } + + if (self.CenterPosition != cachedPosition || desiredRange != cachedRange || desiredVRange != cachedVRange) + { + cachedPosition = self.CenterPosition; + cachedRange = desiredRange; + cachedVRange = desiredVRange; + self.World.ActorMap.UpdateProximityTrigger(proximityTrigger, cachedPosition, cachedRange, cachedVRange); + } + + if (--ticks < 0) + { + GrantBounty(); + + ticks = Info.Delay; + } + } + + void GrantBounty() + { + if (currentBounty > 0) + { + var grantedBounty = currentBounty; + + if (Info.ShowBounty && self.Owner.IsAlliedWith(self.World.RenderPlayer)) + self.World.AddFrameEndTask(w => w.Add(new FloatingText(self.CenterPosition, self.Owner.Color, FloatingText.FormatCashTick(grantedBounty), 30))); + + self.Owner.PlayerActor.Trait().GiveCash(currentBounty); + + currentBounty = 0; + } + } + + public void AddBounty(int bounty) + { + currentBounty += bounty; + } + + void ActorEntered(Actor a) + { + if (a == self || a.Disposed || self.Disposed) + return; + + var relationship = self.Owner.RelationshipWith(a.Owner); + if (!Info.ValidRelationships.HasRelationship(relationship)) + return; + + var gpbs = a.TraitsImplementing(); + foreach (var gpb in gpbs) + gpb.Collectors.Add(this); + } + + void INotifyOtherProduction.UnitProducedByOther(Actor self, Actor producer, Actor produced, string productionType, TypeDictionary init) + { + // If the produced Actor doesn't occupy space, it can't be in range + if (produced.OccupiesSpace == null) + return; + + // We don't grant upgrades when disabled + if (IsTraitDisabled) + return; + + // Work around for actors produced within the region not triggering until the second tick + if ((produced.CenterPosition - self.CenterPosition).HorizontalLengthSquared <= Info.Range.LengthSquared) + { + var relationship = self.Owner.RelationshipWith(produced.Owner); + if (!Info.ValidRelationships.HasRelationship(relationship)) + return; + + var gpbs = produced.TraitsImplementing(); + foreach (var gpb in gpbs) + gpb.Collectors.Add(this); + } + } + + void ActorExited(Actor a) + { + if (a == self || a.Disposed || self.Disposed) + return; + + var relationship = self.Owner.RelationshipWith(a.Owner); + if (!Info.ValidRelationships.HasRelationship(relationship)) + return; + + var gpbs = a.TraitsImplementing(); + foreach (var gpb in gpbs) + gpb.Collectors.Remove(this); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/RangedGpsDot.cs b/OpenRA.Mods.AS/Traits/RangedGpsDot.cs new file mode 100644 index 000000000000..65f70f6c621d --- /dev/null +++ b/OpenRA.Mods.AS/Traits/RangedGpsDot.cs @@ -0,0 +1,61 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Mods.AS.Effects; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Show an indicator revealing the actor underneath the fog when a RangedGPSProvider is activated.")] + class RangedGpsDotInfo : ConditionalTraitInfo + { + [Desc("Sprite collection for symbols.")] + public readonly string Image = "gpsdot"; + + [SequenceReference(nameof(Image))] + [Desc("Sprite used for this actor.")] + public readonly string Sequence = "idle"; + + [PaletteReference(true)] + public readonly string IndicatorPalettePrefix = "player"; + + public readonly bool VisibleInShroud = true; + + public override object Create(ActorInitializer init) { return new RangedGpsDot(this); } + } + + class RangedGpsDot : ConditionalTrait, INotifyAddedToWorld, INotifyRemovedFromWorld + { + RangedGpsDotEffect effect; + public readonly List Providers = new(); + + public RangedGpsDot(RangedGpsDotInfo info) + : base(info) { } + + protected override void Created(Actor self) + { + effect = new RangedGpsDotEffect(self, this); + + base.Created(self); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + self.World.AddFrameEndTask(w => w.Add(effect)); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + self.World.AddFrameEndTask(w => w.Remove(effect)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/RangedGpsProvider.cs b/OpenRA.Mods.AS/Traits/RangedGpsProvider.cs new file mode 100644 index 000000000000..4e9908be1f53 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/RangedGpsProvider.cs @@ -0,0 +1,125 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor provides Ranged GPS.")] + public class RangedGpsProviderInfo : ConditionalTraitInfo + { + [Desc("Range for the GPS effect to apply.")] + public readonly WDist Range = WDist.FromCells(5); + + public override object Create(ActorInitializer init) { return new RangedGpsProvider(init.Self, this); } + } + + public class RangedGpsProvider : ConditionalTrait, ITick, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOwnerChanged, INotifyKilled, INotifyActorDisposing + { + readonly Actor self; + readonly List actorsInRange = new(); + protected RangedGpsWatcher Watcher { get; private set; } + int proximityTrigger; + WPos prevPosition; + + public RangedGpsProvider(Actor self, RangedGpsProviderInfo info) + : base(info) + { + this.self = self; + Watcher = self.Owner.PlayerActor.Trait(); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + if (!IsTraitDisabled) + TraitEnabled(self); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + if (!IsTraitDisabled) + TraitDisabled(self); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (!IsTraitDisabled) + TraitDisabled(self); + } + + void INotifyActorDisposing.Disposing(Actor self) + { + if (!IsTraitDisabled) + TraitDisabled(self); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + if (!IsTraitDisabled) + TraitDisabled(self); + + Watcher = newOwner.PlayerActor.Trait(); + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled || !self.IsInWorld || self.CenterPosition == prevPosition) + return; + + self.World.ActorMap.UpdateProximityTrigger(proximityTrigger, self.CenterPosition, Info.Range, WDist.Zero); + prevPosition = self.CenterPosition; + } + + void ActorEntered(Actor other) + { + var dot = other.TraitOrDefault(); + if (dot != null) + { + actorsInRange.Add(other); + dot.Providers.Add(self); + } + } + + void ActorLeft(Actor other) + { + if (other.IsDead) + { + actorsInRange.Remove(other); + return; + } + + var dot = other.TraitOrDefault(); + if (dot != null) + { + actorsInRange.Remove(other); + dot.Providers.Remove(self); + } + } + + protected override void TraitEnabled(Actor self) + { + Watcher.ActivateGps(this, self.Owner); + proximityTrigger = self.World.ActorMap.AddProximityTrigger(self.CenterPosition, Info.Range, WDist.Zero, ActorEntered, ActorLeft); + } + + protected override void TraitDisabled(Actor self) + { + Watcher.DeactivateGps(this, self.Owner); + self.World.ActorMap.RemoveProximityTrigger(proximityTrigger); + foreach (var a in actorsInRange) + if (!a.IsDead) + a.Trait().Providers.Remove(self); + + actorsInRange.Clear(); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Render/WithExitOverlay.cs b/OpenRA.Mods.AS/Traits/Render/WithExitOverlay.cs new file mode 100644 index 000000000000..7f99479af17c --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Render/WithExitOverlay.cs @@ -0,0 +1,78 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Renders an animation when when the actor is leaving from a production building.")] + public class WithExitOverlayInfo : PausableConditionalTraitInfo, Requires, Requires + { + [SequenceReference] + [Desc("Sequence name to use")] + public readonly string Sequence = "exit-overlay"; + + [Desc("Position relative to body")] + public readonly WVec Offset = WVec.Zero; + + [PaletteReference(nameof(IsPlayerPalette))] + [Desc("Custom palette name")] + public readonly string Palette = null; + + [Desc("Custom palette is a player palette BaseName")] + public readonly bool IsPlayerPalette = false; + + public override object Create(ActorInitializer init) { return new WithExitOverlay(init.Self, this); } + } + + public class WithExitOverlay : PausableConditionalTrait, INotifyDamageStateChanged, INotifyProduction, ITick + { + readonly Animation overlay; + bool enable; + CPos exit; + + public WithExitOverlay(Actor self, WithExitOverlayInfo info) + : base(info) + { + var rs = self.Trait(); + var body = self.Trait(); + + overlay = new Animation(self.World, rs.GetImage(self), () => IsTraitPaused); + overlay.PlayRepeating(info.Sequence); + + var anim = new AnimationWithOffset(overlay, + () => body.LocalToWorld(info.Offset.Rotate(body.QuantizeOrientation(self.Orientation))), + () => IsTraitDisabled || !enable); + + rs.Add(anim, info.Palette, info.IsPlayerPalette); + } + + void INotifyDamageStateChanged.DamageStateChanged(Actor self, AttackInfo e) + { + overlay.ReplaceAnim(RenderSprites.NormalizeSequence(overlay, e.DamageState, overlay.CurrentSequence.Name)); + } + + public void UnitProduced(Actor self, Actor other, CPos exit) + { + this.exit = exit; + enable = true; + } + + void ITick.Tick(Actor self) + { + if (enable) + enable = self.World.ActorMap.GetActorsAt(exit).Any(a => a != self); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Render/WithGarrisonPipsDecoration.cs b/OpenRA.Mods.AS/Traits/Render/WithGarrisonPipsDecoration.cs new file mode 100644 index 000000000000..4684457716d5 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Render/WithGarrisonPipsDecoration.cs @@ -0,0 +1,100 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits.Render +{ + public class WithGarrisonPipsDecorationInfo : WithDecorationBaseInfo, Requires + { + [Desc("Number of pips to display. Defaults to Cargo.MaxWeight.")] + public readonly int PipCount = -1; + + [Desc("If non-zero, override the spacing between adjacent pips.")] + public readonly int2 PipStride = int2.Zero; + + [Desc("Image that defines the pip sequences.")] + public readonly string Image = "pips"; + + [SequenceReference(nameof(Image))] + [Desc("Sequence used for empty pips.")] + public readonly string EmptySequence = "pip-empty"; + + [SequenceReference(nameof(Image))] + [Desc("Sequence used for full pips that aren't defined in CustomPipSequences.")] + public readonly string FullSequence = "pip-green"; + + // TODO: [SequenceReference] isn't smart enough to use Dictionaries. + [Desc("Pip sequence to use for specific passenger actors.")] + public readonly Dictionary CustomPipSequences = new(); + + [PaletteReference] + public readonly string Palette = "chrome"; + + public override object Create(ActorInitializer init) { return new WithGarrisonPipsDecoration(init.Self, this); } + } + + public class WithGarrisonPipsDecoration : WithDecorationBase + { + readonly Garrisonable garrisonable; + readonly Animation pips; + readonly int pipCount; + + public WithGarrisonPipsDecoration(Actor self, WithGarrisonPipsDecorationInfo info) + : base(self, info) + { + garrisonable = self.Trait(); + pipCount = info.PipCount > 0 ? info.PipCount : garrisonable.Info.MaxWeight; + pips = new Animation(self.World, info.Image); + } + + string GetPipSequence(int i) + { + var n = i * garrisonable.Info.MaxWeight / pipCount; + + foreach (var g in garrisonable.Garrisoners) + { + var pi = g.Info.TraitInfo(); + if (n < pi.Weight) + { + if (pi.CustomPipType != null && Info.CustomPipSequences.TryGetValue(pi.CustomPipType, out var sequence)) + return sequence; + + return Info.FullSequence; + } + + n -= pi.Weight; + } + + return Info.EmptySequence; + } + + protected override IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos) + { + pips.PlayRepeating(Info.EmptySequence); + + var palette = wr.Palette(Info.Palette); + var pipSize = pips.Image.Size.XY.ToInt2(); + var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0); + + screenPos -= pipSize / 2; + for (var i = 0; i < pipCount; i++) + { + pips.PlayRepeating(GetPipSequence(i)); + yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f); + + screenPos += pipStride; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Render/WithIdleOverlayOnGround.cs b/OpenRA.Mods.AS/Traits/Render/WithIdleOverlayOnGround.cs new file mode 100644 index 000000000000..438c801d4143 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Render/WithIdleOverlayOnGround.cs @@ -0,0 +1,96 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits.Render +{ + [Desc("Plays an idle overlay on the ground position under the actor (regardless of it's actual height).")] + public class WithIdleOverlayOnGroundInfo : WithIdleOverlayInfo + { + public override object Create(ActorInitializer init) { return new WithIdleOverlayOnGround(init.Self, this); } + + public new IEnumerable RenderPreviewSprites(ActorPreviewInitializer init, string image, int facings, PaletteReference p) + { + if (!EnabledByDefault) + yield break; + + if (Palette != null) + p = init.WorldRenderer.Palette(Palette); + + Func facing; + var dynamicfacingInit = init.GetOrDefault(this); + if (dynamicfacingInit != null) + facing = dynamicfacingInit.Value; + else + { + var f = init.GetValue(this, WAngle.Zero); + facing = () => f; + } + + var anim = new Animation(init.World, image, facing); + anim.PlayRepeating(RenderSprites.NormalizeSequence(anim, init.GetDamageState(), Sequence)); + + var body = init.Actor.TraitInfo(); + WRot Orientation() => body.QuantizeOrientation(WRot.FromYaw(facing()), facings); + WVec Offset() => body.LocalToWorld(this.Offset.Rotate(Orientation())); + int ZOffset() + { + var tmpOffset = Offset(); + return tmpOffset.Y + tmpOffset.Z + 1; + } + + yield return new SpriteActorPreview(anim, Offset, ZOffset, p); + } + } + + public class WithIdleOverlayOnGround : PausableConditionalTrait, INotifyDamageStateChanged + { + readonly Animation overlay; + + public WithIdleOverlayOnGround(Actor self, WithIdleOverlayOnGroundInfo info) + : base(info) + { + var rs = self.Trait(); + var body = self.Trait(); + + var image = info.Image ?? rs.GetImage(self); + overlay = new Animation(self.World, image, () => IsTraitPaused) + { + IsDecoration = info.IsDecoration + }; + if (info.StartSequence != null) + overlay.PlayThen(RenderSprites.NormalizeSequence(overlay, self.GetDamageState(), info.StartSequence), + () => overlay.PlayRepeating(RenderSprites.NormalizeSequence(overlay, self.GetDamageState(), info.Sequence))); + else + overlay.PlayRepeating(RenderSprites.NormalizeSequence(overlay, self.GetDamageState(), info.Sequence)); + + var anim = new AnimationWithOffset(overlay, + () => body.LocalToWorld(info.Offset.Rotate(body.QuantizeOrientation(self.Orientation))) + - new WVec(WDist.Zero, WDist.Zero, self.World.Map.DistanceAboveTerrain(self.CenterPosition)), + () => IsTraitDisabled, + p => RenderUtils.ZOffsetFromCenter(self, p, 1)); + + rs.Add(anim, info.Palette, info.IsPlayerPalette); + } + + void INotifyDamageStateChanged.DamageStateChanged(Actor self, AttackInfo e) + { + overlay.ReplaceAnim(RenderSprites.NormalizeSequence(overlay, e.DamageState, overlay.CurrentSequence.Name)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Render/WithMindControlArc.cs b/OpenRA.Mods.AS/Traits/Render/WithMindControlArc.cs new file mode 100644 index 000000000000..cf9bd05ac95a --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Render/WithMindControlArc.cs @@ -0,0 +1,90 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Graphics; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class WithMindControlArcInfo : TraitInfo + { + [Desc("Color of the arc")] + public readonly Color Color = Color.Red; + + public readonly bool UsePlayerColor = false; + + public readonly int Transparency = 255; + + [Desc("Drawing from self.CenterPosition draws the curve from the foot. Add this much for better looks.")] + public readonly WVec Offset = new(0, 0, 0); + + [Desc("The angle of the arc of the beam.")] + public readonly WAngle Angle = new(64); + + [Desc("Controls how fine-grained the resulting arc should be.")] + public readonly int QuantizedSegments = 16; + + [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")] + public readonly int ZOffset = 0; + + [Desc("The width of the zap.")] + public readonly WDist Width = new(43); + + public override object Create(ActorInitializer init) { return new WithMindControlArc(this); } + } + + public class WithMindControlArc : IRenderAboveShroudWhenSelected, INotifySelected, INotifyCreated + { + readonly WithMindControlArcInfo info; + MindController mindController; + MindControllable mindControllable; + + public WithMindControlArc(WithMindControlArcInfo info) + { + this.info = info; + } + + void INotifyCreated.Created(Actor self) + { + mindController = self.TraitOrDefault(); + mindControllable = self.TraitOrDefault(); + } + + void INotifySelected.Selected(Actor a) { } + + IEnumerable IRenderAboveShroudWhenSelected.RenderAboveShroud(Actor self, WorldRenderer wr) + { + var color = Color.FromArgb(info.Transparency, info.UsePlayerColor ? self.Owner.Color : info.Color); + + if (mindController != null) + { + foreach (var s in mindController.Slaves) + yield return new ArcRenderable( + self.CenterPosition + info.Offset, + s.CenterPosition + info.Offset, + info.ZOffset, info.Angle, color, info.Width, info.QuantizedSegments); + yield break; + } + + if (mindControllable == null || mindControllable.Master == null || !mindControllable.Master.IsInWorld) + yield break; + + yield return new ArcRenderable( + mindControllable.Master.CenterPosition + info.Offset, + self.CenterPosition + info.Offset, + info.ZOffset, info.Angle, color, info.Width, info.QuantizedSegments); + } + + bool IRenderAboveShroudWhenSelected.SpatiallyPartitionable { get { return false; } } + } +} diff --git a/OpenRA.Mods.AS/Traits/Render/WithMindControllerPipsDecoration.cs b/OpenRA.Mods.AS/Traits/Render/WithMindControllerPipsDecoration.cs new file mode 100644 index 000000000000..b477339fcc91 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Render/WithMindControllerPipsDecoration.cs @@ -0,0 +1,73 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits.Render +{ + public class WithMindControllerPipsDecorationInfo : WithDecorationBaseInfo, Requires + { + [Desc("If non-zero, override the spacing between adjacent pips.")] + public readonly int2 PipStride = int2.Zero; + + [Desc("Image that defines the pip sequences.")] + public readonly string Image = "pips"; + + [SequenceReference(nameof(Image))] + [Desc("Sequence used for pips marking controlled actors.")] + public readonly string FullSequence = "pip-green"; + + [SequenceReference(nameof(Image))] + [Desc("Sequence used for empty pips.")] + public readonly string EmptySequence = "pip-empty"; + + [PaletteReference] + public readonly string Palette = "chrome"; + + public override object Create(ActorInitializer init) { return new WithMindControllerPipsDecoration(init.Self, this); } + } + + public class WithMindControllerPipsDecoration : WithDecorationBase + { + readonly MindController mindController; + readonly Animation pips; + readonly int pipCount; + + public WithMindControllerPipsDecoration(Actor self, WithMindControllerPipsDecorationInfo info) + : base(self, info) + { + mindController = self.Trait(); + pipCount = mindController.Info.Capacity; + pips = new Animation(self.World, info.Image); + } + + protected override IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos) + { + pips.PlayRepeating(Info.EmptySequence); + + var palette = wr.Palette(Info.Palette); + var pipSize = pips.Image.Size.XY.ToInt2(); + var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0); + + screenPos -= pipSize / 2; + for (var i = 0; i < pipCount; i++) + { + pips.PlayRepeating(i < mindController.Slaves.Count() ? Info.FullSequence : Info.EmptySequence); + yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f); + + screenPos += pipStride; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Render/WithPrismChargeAnimation.cs b/OpenRA.Mods.AS/Traits/Render/WithPrismChargeAnimation.cs new file mode 100644 index 000000000000..f1b44438cbca --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Render/WithPrismChargeAnimation.cs @@ -0,0 +1,47 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits.Render +{ + [Desc("This actor displays a charge-up animation before firing.")] + public class WithPrismChargeAnimationInfo : TraitInfo, Requires, Requires + { + [SequenceReference] + [Desc("Sequence to use for charge animation.")] + public readonly string ChargeSequence = "active"; + + [Desc("Which sprite body to play the animation on.")] + public readonly string Body = "body"; + + public override object Create(ActorInitializer init) { return new WithPrismChargeAnimation(init, this); } + } + + public class WithPrismChargeAnimation : INotifyPrismCharging + { + readonly WithPrismChargeAnimationInfo info; + readonly WithSpriteBody wsb; + + public WithPrismChargeAnimation(ActorInitializer init, WithPrismChargeAnimationInfo info) + { + this.info = info; + wsb = init.Self.TraitsImplementing().Single(w => w.Info.Name == info.Body); + } + + void INotifyPrismCharging.Charging(Actor self, in Target target) + { + wsb.PlayCustomAnimation(self, info.ChargeSequence, () => wsb.CancelCustomAnimation(self)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Render/WithSharedCargoPipsDecoration.cs b/OpenRA.Mods.AS/Traits/Render/WithSharedCargoPipsDecoration.cs new file mode 100644 index 000000000000..abf106463e7a --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Render/WithSharedCargoPipsDecoration.cs @@ -0,0 +1,101 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits.Render +{ + public class WithSharedCargoPipsDecorationInfo : WithDecorationBaseInfo, Requires + { + [Desc("Number of pips to display. Defaults to Cargo.MaxWeight.")] + public readonly int PipCount = -1; + + [Desc("If non-zero, override the spacing between adjacent pips.")] + public readonly int2 PipStride = int2.Zero; + + [Desc("Image that defines the pip sequences.")] + public readonly string Image = "pips"; + + [SequenceReference(nameof(Image))] + [Desc("Sequence used for empty pips.")] + public readonly string EmptySequence = "pip-empty"; + + [SequenceReference(nameof(Image))] + [Desc("Sequence used for full pips that aren't defined in CustomPipSequences.")] + public readonly string FullSequence = "pip-green"; + + // TODO: [SequenceReference] isn't smart enough to use Dictionaries. + [Desc("Pip sequence to use for specific passenger actors.")] + public readonly Dictionary CustomPipSequences = new(); + + [PaletteReference] + public readonly string Palette = "chrome"; + + public override object Create(ActorInitializer init) { return new WithSharedCargoPipsDecoration(init.Self, this); } + } + + public class WithSharedCargoPipsDecoration : WithDecorationBase + { + readonly SharedCargo cargo; + readonly Animation pips; + readonly int pipCount; + + public WithSharedCargoPipsDecoration(Actor self, WithSharedCargoPipsDecorationInfo info) + : base(self, info) + { + cargo = self.Trait(); + pipCount = info.PipCount > 0 ? info.PipCount : cargo.Manager.Info.MaxWeight; + pips = new Animation(self.World, info.Image); + } + + string GetPipSequence(int i) + { + var n = i * cargo.Manager.Info.MaxWeight / pipCount; + + foreach (var c in cargo.Manager.Passengers) + { + var pi = c.Info.TraitInfo(); + if (n < pi.Weight) + { + if (pi.CustomPipType != null && Info.CustomPipSequences.TryGetValue(pi.CustomPipType, out var sequence)) + return sequence; + + return Info.FullSequence; + } + + n -= pi.Weight; + } + + return Info.EmptySequence; + } + + protected override IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos) + { + pips.PlayRepeating(Info.EmptySequence); + + var palette = wr.Palette(Info.Palette); + var pipSize = pips.Image.Size.XY.ToInt2(); + var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0); + + screenPos -= pipSize / 2; + for (var i = 0; i < pipCount; i++) + { + pips.PlayRepeating(GetPipSequence(i)); + yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f); + + screenPos += pipStride; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Render/WithSpawnerMasterPipsDecoration.cs b/OpenRA.Mods.AS/Traits/Render/WithSpawnerMasterPipsDecoration.cs new file mode 100644 index 000000000000..ee245de50094 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Render/WithSpawnerMasterPipsDecoration.cs @@ -0,0 +1,94 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits.Render +{ + public class WithSpawnerMasterPipsDecorationInfo : WithDecorationBaseInfo, Requires + { + [Desc("If non-zero, override the spacing between adjacent pips.")] + public readonly int2 PipStride = int2.Zero; + + [Desc("Image that defines the pip sequences.")] + public readonly string Image = "pips"; + + [SequenceReference(nameof(Image))] + [Desc("Sequence used for spawnees stored in the spawner actor.")] + public readonly string StoredSequence = "pip-green"; + + [SequenceReference(nameof(Image))] + [Desc("Sequence used for spawnees on the field.")] + public readonly string SpawnedSequence = "pip-yellow"; + + [SequenceReference(nameof(Image))] + [Desc("Sequence used for lost spawnees.")] + public readonly string EmptySequence = "pip-empty"; + + [PaletteReference] + public readonly string Palette = "chrome"; + + public override object Create(ActorInitializer init) { return new WithSpawnerMasterPipsDecoration(init.Self, this); } + } + + public class WithSpawnerMasterPipsDecoration : WithDecorationBase + { + readonly Animation pips; + readonly BaseSpawnerMaster spawner; + /* + readonly int pipCount; + */ + + public WithSpawnerMasterPipsDecoration(Actor self, WithSpawnerMasterPipsDecorationInfo info) + : base(self, info) + { + pips = new Animation(self.World, info.Image); + spawner = self.Trait(); + } + + protected override IEnumerable RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos) + { + pips.PlayRepeating(Info.EmptySequence); + + var palette = wr.Palette(Info.Palette); + var pipSize = pips.Image.Size.XY.ToInt2(); + var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0); + screenPos -= pipSize / 2; + + foreach (var item in spawner.SlaveEntries.Where(x => x.IsValid && !x.IsLaunched)) + { + pips.PlayRepeating(Info.StoredSequence); + yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f); + + screenPos += pipStride; + } + + foreach (var item in spawner.SlaveEntries.Where(x => x.IsValid && x.IsLaunched)) + { + pips.PlayRepeating(Info.SpawnedSequence); + yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f); + + screenPos += pipStride; + } + + foreach (var item in spawner.SlaveEntries.Where(x => !x.IsValid)) + { + pips.PlayRepeating(Info.EmptySequence); + yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f); + + screenPos += pipStride; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Render/WithStatIconOverlay.cs b/OpenRA.Mods.AS/Traits/Render/WithStatIconOverlay.cs new file mode 100644 index 000000000000..b47bd014bd47 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Render/WithStatIconOverlay.cs @@ -0,0 +1,59 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Shows an overlay over the cameo on the ActorIconWidget.")] + public class WithStatIconOverlayInfo : ConditionalTraitInfo + { + [Desc("Image used for this overlay. Defaults to the actor's type.")] + public readonly string Image = null; + + [FieldLoader.Require] + [SequenceReference(nameof(Image), allowNullImage: true)] + [Desc("Sequence name to use")] + public readonly string Sequence; + + [PaletteReference(nameof(IsPlayerPalette))] + [Desc("Custom palette name")] + public readonly string Palette = "chrome"; + + [Desc("Custom palette is a player palette BaseName")] + public readonly bool IsPlayerPalette = false; + + public override object Create(ActorInitializer init) { return new WithStatIconOverlay(init.Self, this); } + } + + public class WithStatIconOverlay : ConditionalTrait + { + public readonly Sprite Sprite; + + public WithStatIconOverlay(Actor self, WithStatIconOverlayInfo info) + : base(info) + { + var image = info.Image ?? self.Info.Name; + var anim = new Animation(self.World, image); + anim.Play(info.Sequence); + Sprite = anim.Image; + } + + public float2 GetOffset(int2 iconSize, float iconScale = 1f) + { + var x = (Sprite.Size.X * iconScale - iconSize.X) / 2; + var y = (Sprite.Size.Y * iconScale - iconSize.Y) / 2; + return new float2(x, y); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Render/WithVoxelAnimatedBody.cs b/OpenRA.Mods.AS/Traits/Render/WithVoxelAnimatedBody.cs new file mode 100644 index 000000000000..81a01102c6fe --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Render/WithVoxelAnimatedBody.cs @@ -0,0 +1,110 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Mods.Cnc.Traits.Render; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Render an animated voxel.")] + public class WithVoxelAnimatedBodyInfo : ConditionalTraitInfo, IRenderActorPreviewVoxelsInfo, Requires + { + public readonly string Sequence = "idle"; + + [Desc("The rate of the voxel animation.")] + public readonly int TickRate = 5; + + [Desc("Defines if the Voxel should have a shadow.")] + public readonly bool ShowShadow = true; + + [Desc("Reset the frames to first frame when the trait is disabled.")] + public readonly bool ResetFramesWhenDisabled = false; + + public override object Create(ActorInitializer init) { return new WithVoxelAnimatedBody(init.Self, this); } + + public IEnumerable RenderPreviewVoxels(IModelCache cache, + ActorPreviewInitializer init, RenderVoxelsInfo rv, string image, Func orientation, int facings, PaletteReference p) + { + var voxel = cache.GetModelSequence(image, Sequence); + var body = init.Actor.TraitInfo(); + var frame = init.GetValue(this, 0); + + yield return new ModelAnimation(voxel, () => WVec.Zero, + () => body.QuantizeOrientation(orientation(), facings), + () => false, () => frame, ShowShadow); + } + } + + public class WithVoxelAnimatedBody : ConditionalTrait, ITick, IAutoMouseBounds, IActorPreviewInitModifier + { + readonly WithVoxelAnimatedBodyInfo info; + readonly RenderVoxels rv; + readonly ModelAnimation modelAnimation; + uint tick, frame; + readonly uint frames; + + public WithVoxelAnimatedBody(Actor self, WithVoxelAnimatedBodyInfo info) + : base(info) + { + this.info = info; + + var body = self.Trait(); + rv = self.Trait(); + + var voxel = rv.Renderer.ModelCache.GetModelSequence(rv.Image, info.Sequence); + frames = voxel.Frames; + modelAnimation = new ModelAnimation(voxel, () => WVec.Zero, + () => body.QuantizeOrientation(self.Orientation), + () => IsTraitDisabled, () => frame, info.ShowShadow); + + rv.Add(modelAnimation); + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled) + return; + + tick++; + + if (tick < info.TickRate) + return; + + tick = 0; + if (++frame == frames) + frame = 0; + } + + void IActorPreviewInitModifier.ModifyActorPreviewInit(Actor self, TypeDictionary inits) + { + inits.Add(new BodyAnimationFrameInit(frame)); + } + + Rectangle IAutoMouseBounds.AutoMouseoverBounds(Actor self, WorldRenderer wr) + { + return modelAnimation.ScreenBounds(self.CenterPosition, wr, rv.Info.Scale); + } + + protected override void TraitDisabled(Actor self) + { + if (Info.ResetFramesWhenDisabled) + { + tick = 0; + frame = 0; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Render/WithVoxelHelicopterBody.cs b/OpenRA.Mods.AS/Traits/Render/WithVoxelHelicopterBody.cs new file mode 100644 index 000000000000..ae64228840b6 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Render/WithVoxelHelicopterBody.cs @@ -0,0 +1,111 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Mods.Cnc.Traits.Render; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Render an animated voxel based upon the voxel being inair.")] + public class WithVoxelHelicopterBodyInfo : ConditionalTraitInfo, IRenderActorPreviewVoxelsInfo, Requires + { + public readonly string Sequence = "idle"; + + [Desc("The rate of the voxel animation.")] + public readonly int TickRate = 5; + + [Desc("Defines if the Voxel should have a shadow.")] + public readonly bool ShowShadow = true; + + [Desc("Reset the frames to first frame when the trait is disabled.")] + public readonly bool ResetFramesWhenDisabled = false; + + public override object Create(ActorInitializer init) { return new WithVoxelHelicopterBody(init.Self, this); } + + public IEnumerable RenderPreviewVoxels(IModelCache cache, + ActorPreviewInitializer init, RenderVoxelsInfo rv, string image, Func orientation, int facings, PaletteReference p) + { + var voxel = cache.GetModelSequence(image, Sequence); + var body = init.Actor.TraitInfo(); + var frame = init.GetValue(this, 0); + + yield return new ModelAnimation(voxel, () => WVec.Zero, + () => body.QuantizeOrientation(orientation(), facings), + () => false, () => frame, ShowShadow); + } + } + + public class WithVoxelHelicopterBody : ConditionalTrait, IAutoMouseBounds, ITick, IActorPreviewInitModifier + { + readonly WithVoxelHelicopterBodyInfo info; + readonly RenderVoxels rv; + readonly ModelAnimation modelAnimation; + uint tick, frame; + readonly uint frames; + + public WithVoxelHelicopterBody(Actor self, WithVoxelHelicopterBodyInfo info) + : base(info) + { + this.info = info; + + var body = self.Trait(); + rv = self.Trait(); + + var voxel = rv.Renderer.ModelCache.GetModelSequence(rv.Image, info.Sequence); + frames = voxel.Frames; + modelAnimation = new ModelAnimation(voxel, () => WVec.Zero, + () => body.QuantizeOrientation(self.Orientation), + () => IsTraitDisabled, () => frame, info.ShowShadow); + + rv.Add(modelAnimation); + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled) + return; + + if (self.World.Map.DistanceAboveTerrain(self.CenterPosition) > WDist.Zero) + tick++; + + if (tick < info.TickRate) + return; + + tick = 0; + if (++frame == frames) + frame = 0; + } + + void IActorPreviewInitModifier.ModifyActorPreviewInit(Actor self, TypeDictionary inits) + { + inits.Add(new BodyAnimationFrameInit(frame)); + } + + Rectangle IAutoMouseBounds.AutoMouseoverBounds(Actor self, WorldRenderer wr) + { + return modelAnimation.ScreenBounds(self.CenterPosition, wr, rv.Info.Scale); + } + + protected override void TraitDisabled(Actor self) + { + if (Info.ResetFramesWhenDisabled) + { + tick = 0; + frame = 0; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/ResourcePurifier.cs b/OpenRA.Mods.AS/Traits/ResourcePurifier.cs new file mode 100644 index 000000000000..32387adb179f --- /dev/null +++ b/OpenRA.Mods.AS/Traits/ResourcePurifier.cs @@ -0,0 +1,78 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Gives additional cash when resources are delivered to refineries.")] + public class ResourcePurifierInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [Desc("Percentage value of the resource to grant as cash.")] + public readonly int Modifier = 25; + + public readonly bool ShowTicks = true; + public readonly int TickLifetime = 30; + public readonly int TickVelocity = 2; + public readonly int TickRate = 10; + + public override object Create(ActorInitializer init) { return new ResourcePurifier(init.Self, this); } + } + + public class ResourcePurifier : ConditionalTrait, ITick, IResourcePurifier, INotifyOwnerChanged + { + readonly ResourcePurifierInfo info; + + PlayerResources playerResources; + int currentDisplayTick = 0; + int currentDisplayValue = 0; + + public ResourcePurifier(Actor self, ResourcePurifierInfo info) + : base(info) + { + this.info = info; + + playerResources = self.Owner.PlayerActor.Trait(); + } + + void IResourcePurifier.RefineAmount(int amount) + { + if (IsTraitDisabled) + return; + + var cash = Util.ApplyPercentageModifiers(amount, new int[] { info.Modifier }); + playerResources.GiveCash(cash); + + if (info.ShowTicks) + currentDisplayValue += cash; + } + + void ITick.Tick(Actor self) + { + if (info.ShowTicks && currentDisplayValue > 0 && --currentDisplayTick <= 0) + { + var temp = currentDisplayValue; + if (self.Owner.IsAlliedWith(self.World.RenderPlayer)) + self.World.AddFrameEndTask(w => w.Add(new FloatingText(self.CenterPosition, self.Owner.Color, FloatingText.FormatCashTick(temp), 30))); + currentDisplayTick = info.TickRate; + currentDisplayValue = 0; + } + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + playerResources = newOwner.PlayerActor.Trait(); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/RevealsShroudToIntelligenceOwner.cs b/OpenRA.Mods.AS/Traits/RevealsShroudToIntelligenceOwner.cs new file mode 100644 index 000000000000..d20a03ae077a --- /dev/null +++ b/OpenRA.Mods.AS/Traits/RevealsShroudToIntelligenceOwner.cs @@ -0,0 +1,176 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class RevealsShroudToIntelligenceOwnerInfo : RevealsShroudInfo + { + [FieldLoader.Require] + [Desc("Types of intelligence this trait requires.")] + public readonly HashSet Types = new(); + + public override object Create(ActorInitializer init) { return new RevealsShroudToIntelligenceOwner(this); } + } + + public class RevealsShroudToIntelligenceOwner : RevealsShroud, INotifyAddedToWorld, INotifyMoving, INotifyCenterPositionChanged, ITick + { + public readonly RevealsShroudToIntelligenceOwnerInfo RSTIOInfo; + public List IntelOwners = new(); + + readonly Shroud.SourceType rstiotype; + + public RevealsShroudToIntelligenceOwner(RevealsShroudToIntelligenceOwnerInfo info) + : base(info) + { + RSTIOInfo = info; + rstiotype = info.RevealGeneratedShroud ? Shroud.SourceType.Visibility + : Shroud.SourceType.PassiveVisibility; + } + + protected override void AddCellsToPlayerShroud(Actor self, Player p, PPos[] uv) + { + p.Shroud.AddSource(this, rstiotype, uv); + } + + void INotifyCenterPositionChanged.CenterPositionChanged(Actor self, byte oldLayer, byte newLayer) + { + if (!self.IsInWorld) + return; + + if (self.Owner.NonCombatant) + return; + + if (IntelOwners.Count == 0) + return; + + var centerPosition = self.CenterPosition; + var projectedPos = centerPosition - new WVec(0, centerPosition.Z, centerPosition.Z); + var projectedLocation = self.World.Map.CellContaining(projectedPos); + var pos = self.CenterPosition; + + var dirty = Info.MoveRecalculationThreshold.Length > 0 && (pos - cachedPos).LengthSquared > Info.MoveRecalculationThreshold.LengthSquared; + if (!dirty && cachedLocation == projectedLocation) + return; + + cachedLocation = projectedLocation; + cachedPos = pos; + + UpdateIntelligenceShroudCells(self); + } + + void ITick.Tick(Actor self) + { + if (!self.IsInWorld) + return; + + if (self.Owner.NonCombatant) + return; + + if (IntelOwners.Count == 0) + return; + + var traitDisabled = IsTraitDisabled; + var range = Range; + + if (cachedRange == range && traitDisabled == cachedTraitDisabled) + return; + + cachedRange = range; + cachedTraitDisabled = traitDisabled; + + UpdateIntelligenceShroudCells(self); + } + + void UpdateIntelligenceShroudCells(Actor self) + { + var cells = ProjectedCells(self); + foreach (var p in self.World.Players) + { + RemoveCellsFromPlayerShroud(self, p); + if (IntelOwners.Contains(p)) + AddCellsToPlayerShroud(self, p, cells); + } + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + if (!self.IsInWorld) + return; + + if (self.Owner.NonCombatant) + return; + + var centerPosition = self.CenterPosition; + var projectedPos = centerPosition - new WVec(0, centerPosition.Z, centerPosition.Z); + cachedLocation = self.World.Map.CellContaining(projectedPos); + cachedPos = centerPosition; + cachedTraitDisabled = IsTraitDisabled; + var cells = ProjectedCells(self); + + foreach (var p in self.World.Players) + { + var hasIntel = self.World.ActorsWithTrait() + .Any(t => t.Actor.Owner == p && t.Trait.Info.Types.Overlaps(RSTIOInfo.Types) && !t.Trait.IsTraitDisabled); + + if (hasIntel) + { + RemoveCellsFromPlayerShroud(self, p); + AddCellsToPlayerShroud(self, p, cells); + + if (!IntelOwners.Contains(p)) + IntelOwners.Add(p); + } + } + } + + void INotifyMoving.MovementTypeChanged(Actor self, MovementType type) + { + if (self.Owner.NonCombatant) + return; + + if (IntelOwners.Count == 0) + return; + + // Recalculate the visiblity at our final stop position + if (type == MovementType.None && self.IsInWorld) + { + var centerPosition = self.CenterPosition; + var projectedPos = centerPosition - new WVec(0, centerPosition.Z, centerPosition.Z); + var projectedLocation = self.World.Map.CellContaining(projectedPos); + var pos = self.CenterPosition; + + cachedLocation = projectedLocation; + cachedPos = pos; + + UpdateIntelligenceShroudCells(self); + } + } + + public void AddCellsToIntelligenceOwnerShroud(Actor self, Player p, PPos[] uv) + { + AddCellsToPlayerShroud(self, p, uv); + } + + public void RemoveCellsFromIntelligenceOwnerShroud(Actor self, Player p) + { + RemoveCellsFromPlayerShroud(self, p); + } + + public PPos[] GetIntelligenceProjectedCells(Actor self) + { + return ProjectedCells(self); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SharedCargo.cs b/OpenRA.Mods.AS/Traits/SharedCargo.cs new file mode 100644 index 000000000000..bdfba9d6fd27 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SharedCargo.cs @@ -0,0 +1,409 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Orders; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor can transport Passenger actors.")] + public class SharedCargoInfo : PausableConditionalTraitInfo, Requires + { + [Desc("Number of pips to display when this actor is selected.")] + public readonly int PipCount = 0; + + [Desc("`SharedPassenger.CargoType`s that can be loaded into this actor.")] + public readonly HashSet Types = new(); + + [Desc("`SharedCargoManager.Type` thar this actor shares its passengers.")] + public readonly string ShareType = "tunnel"; + + [Desc("Terrain types that this actor is allowed to eject actors onto. Leave empty for all terrain types.")] + public readonly HashSet UnloadTerrainTypes = new(); + + [VoiceReference] + [Desc("Voice to play when ordered to unload the passengers.")] + public readonly string UnloadVoice = "Action"; + + [Desc("Radius to search for a load/unload location if the ordered cell is blocked.")] + public readonly WDist LoadRange = WDist.FromCells(5); + + [Desc("Which direction the passenger will face (relative to the transport) when unloading.")] + public readonly WAngle PassengerFacing = new(512); + + [Desc("Delay (in ticks) before continuing after loading a passenger.")] + public readonly int AfterLoadDelay = 8; + + [Desc("Delay (in ticks) before unloading the first passenger.")] + public readonly int BeforeUnloadDelay = 8; + + [Desc("Delay (in ticks) before continuing after unloading a passenger.")] + public readonly int AfterUnloadDelay = 25; + + [Desc("Cursor to display when able to unload the passengers.")] + public readonly string UnloadCursor = "deploy"; + + [Desc("Cursor to display when unable to unload the passengers.")] + public readonly string UnloadBlockedCursor = "deploy-blocked"; + + [GrantedConditionReference] + [Desc("The condition to grant to self while waiting for cargo to load.")] + public readonly string LoadingCondition = null; + + [GrantedConditionReference] + [Desc("The condition to grant to self while passengers are loaded.", + "Condition can stack with multiple passengers.")] + public readonly string LoadedCondition = null; + + [Desc("Conditions to grant when specified actors are loaded inside the transport.", + "A dictionary of [actor id]: [condition].")] + public readonly Dictionary PassengerConditions = new(); + + [GrantedConditionReference] + public IEnumerable LinterPassengerConditions { get { return PassengerConditions.Values; } } + + public override object Create(ActorInitializer init) { return new SharedCargo(init, this); } + } + + public class SharedCargo : PausableConditionalTrait, IIssueOrder, IResolveOrder, IOrderVoice, INotifyCreated, + INotifyAddedToWorld, ITick, IIssueDeployOrder, INotifyKilled, INotifyActorDisposing, INotifyPassengersDamage + { + readonly Actor self; + public readonly SharedCargoManager Manager; + readonly Dictionary> passengerTokens = new(); + readonly Lazy facing; + readonly bool checkTerrainType; + + Aircraft aircraft; + int loadingToken = Actor.InvalidConditionToken; + readonly Stack loadedTokens = new(); + bool takeOffAfterLoad; + bool initialized; + + CPos currentCell; + public IEnumerable CurrentAdjacentCells { get; private set; } + + enum State { Free, Locked } + State state = State.Free; + + public SharedCargo(ActorInitializer init, SharedCargoInfo info) + : base(info) + { + self = init.Self; + Manager = self.Owner.PlayerActor.TraitsImplementing().First(m => m.Info.Type == Info.ShareType); + checkTerrainType = info.UnloadTerrainTypes.Count > 0; + facing = Exts.Lazy(self.TraitOrDefault); + } + + protected override void Created(Actor self) + { + base.Created(self); + aircraft = self.TraitOrDefault(); + } + + static int GetWeight(Actor a) { return a.Info.TraitInfo().Weight; } + + public IEnumerable Orders + { + get + { + if (IsTraitDisabled) + yield break; + + yield return new DeployOrderTargeter("UnloadShared", 10, () => CanUnload() ? Info.UnloadCursor : Info.UnloadBlockedCursor); + } + } + + public Order IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) + { + if (order.OrderID == "UnloadShared") + return new Order(order.OrderID, self, queued); + + return null; + } + + Order IIssueDeployOrder.IssueDeployOrder(Actor self, bool queued) + { + return new Order("UnloadShared", self, queued); + } + + bool IIssueDeployOrder.CanIssueDeployOrder(Actor self, bool queued) { return true; } + + public void ResolveOrder(Actor self, Order order) + { + if (order.OrderString == "UnloadShared") + { + if (!order.Queued && !CanUnload()) + return; + + self.QueueActivity(new UnloadSharedCargo(self, Info.LoadRange)); + } + } + + IEnumerable GetAdjacentCells() + { + return Util.AdjacentCells(self.World, Target.FromActor(self)).Where(c => self.Location != c); + } + + public bool CanUnload(BlockedByActor check = BlockedByActor.None) + { + if (checkTerrainType) + { + if (!self.World.Map.Contains(self.Location)) + return false; + + if (!Info.UnloadTerrainTypes.Contains(self.World.Map.GetTerrainInfo(self.Location).Type)) + return false; + } + + return !Manager.IsEmpty() && (aircraft == null || aircraft.CanLand(self.Location)) && !IsTraitPaused + && CurrentAdjacentCells != null && CurrentAdjacentCells.Any(c => Manager.Passengers.Any(p => p.Trait().CanEnterCell(c, null, check))); + } + + public bool CanLoad(Actor self, Actor a) + { + return (Manager.Reserves.Contains(a) || Manager.HasSpace(GetWeight(a))) && self.IsAtGroundLevel() && !IsTraitPaused; + } + + internal bool ReserveSpace(Actor a) + { + if (Manager.Reserves.Contains(a)) + return true; + + var w = GetWeight(a); + if (!Manager.HasSpace(w)) + return false; + + if (loadingToken == Actor.InvalidConditionToken && !string.IsNullOrEmpty(Info.LoadingCondition)) + loadingToken = self.GrantCondition(Info.LoadingCondition); + + Manager.Reserves.Add(a); + Manager.ReservedWeight += w; + LockForPickup(self); + + return true; + } + + internal void UnreserveSpace(Actor a) + { + if (!Manager.Reserves.Contains(a)) + return; + + Manager.ReservedWeight -= GetWeight(a); + Manager.Reserves.Remove(a); + ReleaseLock(self); + + if (loadingToken != Actor.InvalidConditionToken) + loadingToken = self.RevokeCondition(loadingToken); + } + + // Prepare for transport pickup + void LockForPickup(Actor self) + { + if (state == State.Locked) + return; + + state = State.Locked; + + self.CancelActivity(); + + var air = self.TraitOrDefault(); + if (air != null && !air.AtLandAltitude) + { + takeOffAfterLoad = true; + self.QueueActivity(new Land(self)); + } + + self.QueueActivity(new WaitFor(() => state != State.Locked, false)); + } + + void ReleaseLock(Actor self) + { + if (Manager.ReservedWeight != 0) + return; + + state = State.Free; + + self.QueueActivity(new Wait(Info.AfterLoadDelay, false)); + if (takeOffAfterLoad) + self.QueueActivity(new TakeOff(self)); + + takeOffAfterLoad = false; + } + + public string CursorForOrder(Order order) + { + if (order.OrderString != "UnloadShared") + return null; + + return CanUnload() ? Info.UnloadCursor : Info.UnloadBlockedCursor; + } + + public string VoicePhraseForOrder(Actor self, Order order) + { + if (order.OrderString != "UnloadShared" || Manager.IsEmpty() || !self.HasVoice(Info.UnloadVoice)) + return null; + + return Info.UnloadVoice; + } + + public Actor Peek() { return Manager.Cargo.Last(); } + + public Actor Unload(Actor self, Actor passenger = null) + { + passenger ??= Manager.Cargo.Last(); + if (!Manager.Cargo.Remove(passenger)) + throw new ArgumentException("Attempted to unload an actor that is not a passenger."); + + Manager.TotalWeight -= GetWeight(passenger); + + SetPassengerFacing(passenger); + + foreach (var npe in self.TraitsImplementing()) + npe.OnPassengerExited(self, passenger); + + foreach (var nec in passenger.TraitsImplementing()) + nec.OnExitedSharedCargo(passenger, self); + + var p = passenger.Trait(); + p.Transport = null; + + if (passengerTokens.TryGetValue(passenger.Info.Name, out var passengerToken) && passengerToken.Any()) + self.RevokeCondition(passengerToken.Pop()); + + if (loadedTokens.Count > 0) + self.RevokeCondition(loadedTokens.Pop()); + + return passenger; + } + + void SetPassengerFacing(Actor passenger) + { + if (facing.Value == null) + return; + + var passengerFacing = passenger.TraitOrDefault(); + if (passengerFacing != null) + passengerFacing.Facing = facing.Value.Facing + Info.PassengerFacing; + } + + public void Load(Actor self, Actor a) + { + Manager.Cargo.Add(a); + var w = GetWeight(a); + Manager.TotalWeight += w; + if (Manager.Reserves.Contains(a)) + { + Manager.ReservedWeight -= w; + Manager.Reserves.Remove(a); + ReleaseLock(self); + + if (loadingToken != Actor.InvalidConditionToken) + loadingToken = self.RevokeCondition(loadingToken); + } + + // Don't initialise (effectively twice) if this runs before the FrameEndTask from Created + if (initialized) + { + a.Trait().Transport = self; + + foreach (var nec in a.TraitsImplementing()) + nec.OnEnteredSharedCargo(a, self); + + foreach (var npe in self.TraitsImplementing()) + npe.OnPassengerEntered(self, a); + } + + if (Info.PassengerConditions.TryGetValue(a.Info.Name, out var passengerCondition)) + passengerTokens.GetOrAdd(a.Info.Name).Push(self.GrantCondition(passengerCondition)); + + if (!string.IsNullOrEmpty(Info.LoadedCondition)) + loadedTokens.Push(self.GrantCondition(Info.LoadedCondition)); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + // Force location update to avoid issues when initial spawn is outside map + currentCell = self.Location; + CurrentAdjacentCells = GetAdjacentCells(); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (!self.World.ActorsWithTrait().Any(a => a.Trait.Info.ShareType == Info.ShareType && a.Actor.Owner == self.Owner && a.Actor != self)) + Manager.Clear(e); + } + + void INotifyActorDisposing.Disposing(Actor self) + { + if (!self.World.ActorsWithTrait().Any(a => a.Trait.Info.ShareType == Info.ShareType && a.Actor.Owner == self.Owner && a.Actor != self)) + Manager.Clear(); + } + + void ITick.Tick(Actor self) + { + // Notify initial cargo load + if (!initialized) + { + foreach (var c in Manager.Cargo) + { + c.Trait().Transport = self; + + foreach (var npe in self.TraitsImplementing()) + npe.OnPassengerEntered(self, c); + + foreach (var nec in c.TraitsImplementing()) + nec.OnEnteredSharedCargo(c, self); + } + + initialized = true; + } + + var cell = self.World.Map.CellContaining(self.CenterPosition); + if (currentCell != cell) + { + currentCell = cell; + CurrentAdjacentCells = GetAdjacentCells(); + } + } + + static int DamageVersus(Actor victim, Dictionary versus) + { + // If no Versus values are defined, DamageVersus would return 100 anyway, so we might as well do that early. + if (versus.Count == 0 || victim.IsDead) + return 100; + + var armor = victim.TraitsImplementing() + .Where(a => !a.IsTraitDisabled && a.Info.Type != null && versus.ContainsKey(a.Info.Type)) + .Select(a => versus[a.Info.Type]); + + return Util.ApplyPercentageModifiers(100, armor); + } + + void INotifyPassengersDamage.DamagePassengers(int damage, Actor attacker, int amount, Dictionary versus, BitSet damageTypes, IEnumerable damageModifiers) + { + var passengersToDamage = amount > 0 && amount < Manager.Cargo.ToArray().Length ? Manager.Cargo.Shuffle(self.World.SharedRandom).Take(amount).ToArray() : Manager.Cargo.ToArray(); + foreach (var passenger in passengersToDamage) + { + var d = Util.ApplyPercentageModifiers(damage, damageModifiers.Append(DamageVersus(passenger, versus))); + passenger.InflictDamage(attacker, new Damage(d, damageTypes)); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SharedPassenger.cs b/OpenRA.Mods.AS/Traits/SharedPassenger.cs new file mode 100644 index 000000000000..61c9b5d2455f --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SharedPassenger.cs @@ -0,0 +1,214 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common.Orders; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor can enter SharedCargo actors.")] + public class SharedPassengerInfo : TraitInfo + { + public readonly string CargoType = null; + + [Desc("If defined, use a custom pip type defined on the transport's WithCargoPipsDecoration.CustomPipSequences list.")] + public readonly string CustomPipType = null; + + public readonly int Weight = 1; + + [GrantedConditionReference] + [Desc("The condition to grant to when this actor is loaded inside any transport.")] + public readonly string CargoCondition = null; + + [Desc("Conditions to grant when this actor is loaded inside specified transport.", + "A dictionary of [actor id]: [condition].")] + public readonly Dictionary CargoConditions = new(); + + [GrantedConditionReference] + public IEnumerable LinterCargoConditions { get { return CargoConditions.Values; } } + + [VoiceReference] + public readonly string Voice = "Action"; + + [Desc("Color to use for the target line.")] + public readonly Color TargetLineColor = Color.Green; + + [ConsumedConditionReference] + [Desc("Boolean expression defining the condition under which the regular (non-force) enter cursor is disabled.")] + public readonly BooleanExpression RequireForceMoveCondition = null; + + [Desc("Cursor to display when able to enter target actor.")] + public readonly string EnterCursor = "enter"; + + [Desc("Cursor to display when unable to enter target actor.")] + public readonly string EnterBlockedCursor = "enter-blocked"; + + public override object Create(ActorInitializer init) { return new SharedPassenger(this); } + } + + public class SharedPassenger : IIssueOrder, IResolveOrder, IOrderVoice, INotifyRemovedFromWorld, INotifyEnteredSharedCargo, INotifyExitedSharedCargo, INotifyKilled, IObservesVariables + { + public readonly SharedPassengerInfo Info; + public Actor Transport; + bool requireForceMove; + + int anyCargoToken = Actor.InvalidConditionToken; + int specificCargoToken = Actor.InvalidConditionToken; + + public SharedPassenger(SharedPassengerInfo info) + { + Info = info; + } + + public SharedCargo ReservedCargo { get; private set; } + + IEnumerable IIssueOrder.Orders + { + get + { + yield return new EnterAlliedActorTargeter( + "EnterSharedTransport", + 5, + Info.EnterCursor, + Info.EnterBlockedCursor, + IsCorrectCargoType, + CanEnter); + } + } + + public Order IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) + { + if (order.OrderID == "EnterSharedTransport") + return new Order(order.OrderID, self, target, queued); + + return null; + } + + bool IsCorrectCargoType(Actor target, TargetModifiers modifiers) + { + if (requireForceMove && !modifiers.HasModifier(TargetModifiers.ForceMove)) + return false; + + return IsCorrectCargoType(target); + } + + bool IsCorrectCargoType(Actor target) + { + var ci = target.Info.TraitInfo(); + return ci.Types.Contains(Info.CargoType); + } + + bool CanEnter(SharedCargo cargo) + { + return cargo != null && cargo.Manager.HasSpace(Info.Weight) && !cargo.IsTraitPaused; + } + + bool CanEnter(Actor target) + { + return CanEnter(target.TraitOrDefault()); + } + + public string VoicePhraseForOrder(Actor self, Order order) + { + if (order.OrderString != "EnterSharedTransport") + return null; + + if (order.Target.Type != TargetType.Actor || !CanEnter(order.Target.Actor)) + return null; + + return Info.Voice; + } + + void INotifyEnteredSharedCargo.OnEnteredSharedCargo(Actor self, Actor cargo) + { + if (anyCargoToken == Actor.InvalidConditionToken) + anyCargoToken = self.GrantCondition(Info.CargoCondition); + + if (specificCargoToken == Actor.InvalidConditionToken && Info.CargoConditions.TryGetValue(cargo.Info.Name, out var specificCargoCondition)) + specificCargoToken = self.GrantCondition(specificCargoCondition); + } + + void INotifyExitedSharedCargo.OnExitedSharedCargo(Actor self, Actor cargo) + { + if (anyCargoToken != Actor.InvalidConditionToken) + anyCargoToken = self.RevokeCondition(anyCargoToken); + + if (specificCargoToken != Actor.InvalidConditionToken) + specificCargoToken = self.RevokeCondition(specificCargoToken); + } + + public void ResolveOrder(Actor self, Order order) + { + if (order.OrderString != "EnterSharedTransport") + return; + + // Enter orders are only valid for own/allied actors, + // which are guaranteed to never be frozen. + if (order.Target.Type != TargetType.Actor) + return; + + var targetActor = order.Target.Actor; + if (!CanEnter(targetActor)) + return; + + if (!IsCorrectCargoType(targetActor)) + return; + + self.QueueActivity(order.Queued, new RideSharedTransport(self, order.Target, Info.TargetLineColor)); + self.ShowTargetLines(); + } + + public bool Reserve(Actor self, SharedCargo cargo) + { + Unreserve(self); + if (!cargo.ReserveSpace(self)) + return false; + ReservedCargo = cargo; + return true; + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) { Unreserve(self); } + + public void Unreserve(Actor self) + { + if (ReservedCargo == null) + return; + ReservedCargo.UnreserveSpace(self); + ReservedCargo = null; + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (Transport == null) + return; + + // Something killed us, but it wasn't our transport blowing up. Remove us from the cargo. + if (!Transport.IsDead) + Transport.Trait().Unload(Transport, self); + } + + IEnumerable IObservesVariables.GetVariableObservers() + { + if (Info.RequireForceMoveCondition != null) + yield return new VariableObserver(RequireForceMoveConditionChanged, Info.RequireForceMoveCondition.Variables); + } + + void RequireForceMoveConditionChanged(Actor self, IReadOnlyDictionary conditions) + { + requireForceMove = Info.RequireForceMoveCondition.Evaluate(conditions); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Shielded.cs b/OpenRA.Mods.AS/Traits/Shielded.cs new file mode 100644 index 000000000000..e8fb9efedd41 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Shielded.cs @@ -0,0 +1,191 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Grants a shield with its own health pool. Main health pool is unaffected by damage until the shield is broken.")] + public class ShieldedInfo : PausableConditionalTraitInfo + { + [Desc("The strength of the shield (amount of damage it will absorb).")] + public readonly int MaxStrength = 1000; + + [Desc("Strength of the shield when the trait is enabled.")] + public readonly int InitialStrength = 1000; + + [Desc("Delay in ticks before shield regenerate for the first time after trait is enabled.")] + public readonly int InitialRegenDelay = 0; + + [Desc("Delay in ticks after absorbing damage before the shield will regenerate.")] + public readonly int DamageRegenDelay = 0; + + [Desc("Amount to recharge at each interval.")] + public readonly int RegenAmount = 0; + + [Desc("Number of ticks between recharging.")] + public readonly int RegenInterval = 25; + + [Desc("Block the remaining damage after shield breaks.")] + public readonly bool BlockExcessDamage = false; + + [Desc("Damage types that ignore this shield.")] + public readonly BitSet IgnoreShieldDamageTypes = default; + + [GrantedConditionReference] + [Desc("Condition to grant when shields are active.")] + public readonly string ShieldsUpCondition = null; + + [Desc("Hides selection bar when shield is at max strength.")] + public readonly bool HideBarWhenFull = false; + + public readonly bool ShowSelectionBar = true; + public readonly Color SelectionBarColor = Color.FromArgb(128, 200, 255); + + public override object Create(ActorInitializer init) { return new Shielded(init, this); } + } + + public class Shielded : PausableConditionalTrait, ITick, ISync, ISelectionBar, IDamageModifier, INotifyDamage + { + int conditionToken = Actor.InvalidConditionToken; + readonly Actor self; + + [Sync] + public int Strength; + int ticks; + + public Shielded(ActorInitializer init, ShieldedInfo info) + : base(info) + { + self = init.Self; + } + + protected override void Created(Actor self) + { + base.Created(self); + Strength = Info.InitialStrength; + ticks = Info.InitialRegenDelay; + } + + void ITick.Tick(Actor self) + { + Regenerate(self); + } + + protected void Regenerate(Actor self) + { + if (IsTraitDisabled || IsTraitPaused) + return; + + if (Strength == Info.MaxStrength) + return; + + if (--ticks > 0) + return; + + Strength += Info.RegenAmount; + + if (Strength > Info.MaxStrength) + Strength = Info.MaxStrength; + + if (Strength > 0 && conditionToken == Actor.InvalidConditionToken) + conditionToken = self.GrantCondition(Info.ShieldsUpCondition); + + ticks = Info.RegenInterval; + } + + void INotifyDamage.Damaged(Actor self, AttackInfo e) + { + if (IsTraitDisabled) + return; + + if (e.Damage.Value < 0 || (!Info.IgnoreShieldDamageTypes.IsEmpty && e.Damage.DamageTypes.Overlaps(Info.IgnoreShieldDamageTypes))) + return; + + if (ticks < Info.DamageRegenDelay) + ticks = Info.DamageRegenDelay; + + if (Strength == 0 || e.Damage.Value == 0 || e.Attacker == self) + return; + + var damageAmt = Convert.ToInt32(e.Damage.Value / 0.01); + var damageTypes = e.Damage.DamageTypes; + var excessDamage = damageAmt - Strength; + Strength = Math.Max(Strength - damageAmt, 0); + + var health = self.TraitOrDefault(); + + if (health != null) + { + var absorbedDamage = new Damage(-e.Damage.Value, damageTypes); + health.InflictDamage(self, self, absorbedDamage, true); + } + + if (Strength == 0 && conditionToken != Actor.InvalidConditionToken) + conditionToken = self.RevokeCondition(conditionToken); + + if (excessDamage > 0 && !Info.BlockExcessDamage) + { + var hullDamage = new Damage(excessDamage, damageTypes); + + health?.InflictDamage(self, e.Attacker, hullDamage, true); + } + } + + float ISelectionBar.GetValue() + { + if (IsTraitDisabled || !Info.ShowSelectionBar || Strength == 0 || (Strength == Info.MaxStrength && Info.HideBarWhenFull)) + return 0; + + var selected = self.World.Selection.Contains(self); + var rollover = self.World.Selection.RolloverContains(self); + var regularWorld = self.World.Type == WorldType.Regular; + var statusBars = Game.Settings.Game.StatusBars; + + var displayHealth = selected || rollover || (regularWorld && statusBars == StatusBarsType.AlwaysShow) + || (regularWorld && statusBars == StatusBarsType.DamageShow && Strength < Info.MaxStrength); + + if (!displayHealth) + return 0; + + return (float)Strength / Info.MaxStrength; + } + + bool ISelectionBar.DisplayWhenEmpty { get { return false; } } + + Color ISelectionBar.GetColor() { return Info.SelectionBarColor; } + + int IDamageModifier.GetDamageModifier(Actor attacker, Damage damage) + { + return IsTraitDisabled || Strength == 0 || (!Info.IgnoreShieldDamageTypes.IsEmpty && damage.DamageTypes.Overlaps(Info.IgnoreShieldDamageTypes)) ? 100 : 1; + } + + protected override void TraitEnabled(Actor self) + { + ticks = Info.InitialRegenDelay; + Strength = Info.InitialStrength; + + if (conditionToken == Actor.InvalidConditionToken && Strength > 0) + conditionToken = self.GrantCondition(Info.ShieldsUpCondition); + } + + protected override void TraitDisabled(Actor self) + { + if (conditionToken == Actor.InvalidConditionToken) + return; + + conditionToken = self.RevokeCondition(conditionToken); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SlaveMinerHarvester.cs b/OpenRA.Mods.AS/Traits/SlaveMinerHarvester.cs new file mode 100644 index 000000000000..02fa4a72d6de --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SlaveMinerHarvester.cs @@ -0,0 +1,375 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public enum MiningState + { + Scan, + Moving, + TryDeploy, + Deploying, + Mining, + Packaging, + Undeploy, + } + + public class SpawnerHarvestResourceInfo : BaseSpawnerMasterInfo + { + [Desc("Which resources it can harvest. Make sure slaves can mine these too!")] + public readonly HashSet Resources = new(); + } + + [Desc("This actor is a harvester that uses its spawns to indirectly harvest resources. i.e., Slave Miner.")] + public class SlaveMinerHarvesterInfo : SpawnerHarvestResourceInfo, Requires, Requires + { + [VoiceReference] + public readonly string HarvestVoice = "Action"; + + [Desc("Automatically search for resources on creation?")] + public readonly bool SearchOnCreation = true; + + [Desc("When deployed, use this scan radius.")] + public readonly int ShortScanRadius = 8; + + [Desc("Look this far when Searching for Ore (in Cells)")] + public readonly int LongScanRadius = 24; + + [Desc("Look this far when trying to find a deployable position from the target resource patch")] + public readonly int DeployScanRadius = 8; + + [Desc("If no resource within range at each kick, move.")] + public readonly int KickScanRadius = 5; + + [Desc("If the SlaveMiner is idle for this long, he'll try to look for ore again at SlaveMinerShortScan range to find ore and wake up (in ticks)")] + public readonly int KickDelay = 301; + + [Desc("Play this sound when the slave is freed")] + public readonly string FreeSound = null; + + public override object Create(ActorInitializer init) { return new SlaveMinerHarvester(init, this); } + } + + public class SlaveMinerHarvester : BaseSpawnerMaster, + ITick, IIssueOrder, IResolveOrder, IOrderVoice, INotifyDeployComplete, INotifyTransform + { + const string OrderID = "SlaveMinerHarvest"; + + readonly SlaveMinerHarvesterInfo info; + readonly Actor self; + readonly IResourceLayer resLayer; + readonly Mobile mobile; + + // Because activities don't remember states, we remember states here for them. + public CPos? LastOrderLocation = null; + public MiningState MiningState = MiningState.Scan; + + public IEnumerable Orders + { + get { yield return new SlaveMinerHarvestOrderTargeter(OrderID); } + } + + int respawnTicks; + int kickTicks; + bool allowKicks = true; + + public SlaveMinerHarvester(ActorInitializer init, SlaveMinerHarvesterInfo info) + : base(init, info) + { + self = init.Self; + this.info = info; + + mobile = self.Trait(); + resLayer = self.World.WorldActor.Trait(); + + kickTicks = info.KickDelay; + } + + static void AssignTargetForSpawned(Actor slave, CPos targetLocation) + { + /* var harvest = slave.Trait(); */ + + // set target spot to mine + slave.QueueActivity(new FindAndDeliverResources(slave, targetLocation)); + } + + /*void Launch(Actor self, BaseSpawnerSlaveEntry se, CPos targetLocation) + { + var slave = se.Actor; + + SpawnIntoWorld(self, slave, self.CenterPosition); + + self.World.AddFrameEndTask(w => + { + var move = se.Actor.Trait().MoveToTarget(slave, Target.FromPos(self.CenterPosition)); + if (move != null) + slave.QueueActivity(move); + + AssignTargetForSpawned(slave, targetLocation); + }); + }*/ + + public override void OnSlaveKilled(Actor self, Actor slave) + { + if (respawnTicks <= 0) + respawnTicks = Info.RespawnTicks; + } + + void ITick.Tick(Actor self) + { + respawnTicks--; + if (respawnTicks > 0) + return; + + if (MiningState != MiningState.Mining) + return; + + Replenish(self, SlaveEntries); + + /* + var destination = LastOrderLocation.HasValue ? LastOrderLocation.Value : self.Location; + */ + + // Launch whatever we can. + var hasInvalidEntry = false; + foreach (var se in SlaveEntries) + { + if (!se.IsValid) + { + hasInvalidEntry = true; + } + + /*else if (!se.Actor.IsInWorld) + { + Launch(self, se, destination); + }*/ + } + + if (hasInvalidEntry) + { + respawnTicks = info.RespawnTicks; + } + } + + public Order IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) + { + if (order.OrderID == OrderID) + return new Order(order.OrderID, self, target, queued); + + return null; + } + + CPos ResolveHarvestLocation(Actor self, Order order) + { + if (self.World.Map.CellContaining(order.Target.CenterPosition) == CPos.Zero) + return self.Location; + + var loc = self.World.Map.CellContaining(order.Target.CenterPosition); + + var territory = self.World.WorldActor.TraitOrDefault(); + if (territory != null) + { + // Find the nearest claimable cell to the order location (useful for group-select harvest): + return mobile.NearestCell(loc, p => mobile.CanEnterCell(p), 1, 6); + } + + // Find the nearest cell to the order location (useful for group-select harvest): + return mobile.NearestCell(loc, p => mobile.CanEnterCell(p), 1, 6); + } + + void HandleSpawnerHarvest(Actor self, Order order) + { + allowKicks = true; + + // state == Deploying implies order string of SpawnerHarvestDeploying + // and must not cancel deploy activity! + if (MiningState != MiningState.Deploying) + { + self.CancelActivity(); + } + + MiningState = MiningState.Scan; + + LastOrderLocation = ResolveHarvestLocation(self, order); + self.QueueActivity(new SlaveMinerHarvesterHarvest(self)); + self.ShowTargetLines(); + + // Assign new targets for slaves too. + foreach (var se in SlaveEntries) + { + if (se.IsValid && se.Actor.IsInWorld) + { + AssignTargetForSpawned(se.Actor, LastOrderLocation.Value); + } + } + } + + public void ResolveOrder(Actor self, Order order) + { + if (order.OrderString == OrderID) + HandleSpawnerHarvest(self, order); + else if (order.OrderString == "Stop" || order.OrderString == "Move") + { + // Disable "smart idle" + allowKicks = false; + MiningState = MiningState.Scan; + } + } + + public string VoicePhraseForOrder(Actor self, Order order) + { + return order.OrderString == OrderID ? info.HarvestVoice : null; + } + + public void TickIdle(Actor self) + { + // wake up on idle for long (to find new resource patch. i.e., kick) + if (allowKicks && self.IsIdle) + kickTicks--; + else + kickTicks = info.KickDelay; + + if (kickTicks <= 0) + { + kickTicks = info.KickDelay; + MiningState = MiningState.Packaging; + self.QueueActivity(new SlaveMinerHarvesterHarvest(self)); + } + } + + void INotifyDeployComplete.FinishedDeploy(Actor self) + { + allowKicks = true; + + // rescan from where we are + MiningState = MiningState.Scan; + + // Tell harvesters to unload and restart mining. + foreach (var se in SlaveEntries) + { + if (!se.IsValid || !se.Actor.IsInWorld) + continue; + + var s = se.Actor; + se.SpawnerSlave.Stop(s); + AssignTargetForSpawned(s, self.Location); + s.QueueActivity(new FindAndDeliverResources(s)); + } + } + + void INotifyDeployComplete.FinishedUndeploy(Actor self) + { + allowKicks = false; + + // Interrupt harvesters and order them to follow me. + foreach (var se in SlaveEntries) + { + se.SpawnerSlave.Stop(se.Actor); + se.Actor.QueueActivity(new Follow(se.Actor, Target.FromActor(self), WDist.FromCells(1), WDist.FromCells(3), null)); + } + } + + public bool CanHarvestCell(CPos cell) + { + // Resources only exist in the ground layer + if (cell.Layer != 0) + return false; + + var resType = resLayer.GetResource(cell).Type; + if (resType == null) + return false; + + // Can the harvester collect this kind of resource? + return info.Resources.Contains(resType); + } + + void INotifyTransform.BeforeTransform(Actor self) { } + + void INotifyTransform.OnTransform(Actor self) { } + + void INotifyTransform.AfterTransform(Actor toActor) + { + // When transform complete, assign the slaves to the transform actor + var refineryMaster = toActor.Trait(); + foreach (var se in SlaveEntries) + { + var slave = se.Actor; + se.SpawnerSlave.LinkMaster(slave, toActor, refineryMaster); + se.SpawnerSlave.Stop(slave); + if (!slave.IsDead) + slave.QueueActivity(new FindAndDeliverResources(slave)); + } + + refineryMaster.SlaveEntries = SlaveEntries; + toActor.QueueActivity(new SlaveMinerMasterHarvest(toActor)); + } + + protected override void Killed(Actor self, AttackInfo e) + { + base.Killed(self, e); + + if (!string.IsNullOrEmpty(info.FreeSound)) + { + Game.Sound.Play(SoundType.World, info.FreeSound, self.CenterPosition); + } + } + } + + class SlaveMinerHarvestOrderTargeter : IOrderTargeter where T : SpawnerHarvestResourceInfo + { + public SlaveMinerHarvestOrderTargeter(string orderID) + { + OrderID = orderID; + } + + public string OrderID { get; } + public int OrderPriority { get { return 10; } } + public bool IsQueued { get; protected set; } + /* + public static bool TargetOverridesSelection(TargetModifiers modifiers) { return true; } + */ + + public bool CanTarget(Actor self, in Target target, ref TargetModifiers modifiers, ref string cursor) + { + if (target.Type != TargetType.Terrain) + return false; + + if (modifiers.HasModifier(TargetModifiers.ForceMove)) + return false; + + var location = self.World.Map.CellContaining(target.CenterPosition); + + // Don't leak info about resources under the shroud + if (!self.Owner.Shroud.IsExplored(location)) + return false; + + var res = self.World.WorldActor.Trait().GetRenderedResourceType(location); + var info = self.Info.TraitInfo(); + if (res == null || !info.Resources.Contains(res)) + return false; + + cursor = "harvest"; + IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue); + + return true; + } + + public bool TargetOverridesSelection(Actor self, in Target target, List actorsAt, CPos xy, TargetModifiers modifiers) + { + return true; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SlaveMinerMaster.cs b/OpenRA.Mods.AS/Traits/SlaveMinerMaster.cs new file mode 100644 index 000000000000..cec243e72301 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SlaveMinerMaster.cs @@ -0,0 +1,241 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class SlaveMinerMasterInfo : SpawnerHarvestResourceInfo, Requires + { + [Desc("When deployed, use this scan radius.")] + public readonly int ShortScanRadius = 8; + + [Desc("Look this far when Searching for Ore (in Cells)")] + public readonly int LongScanRadius = 24; + + [Desc("Look this far when trying to find a deployable position from the target resource patch")] + public readonly int DeployScanRadius = 8; + + [Desc("If no resource within range at each kick, move.")] + public readonly int KickScanRadius = 5; + + [Desc("If the SlaveMiner is idle for this long, he'll try to look for ore again at SlaveMinerShortScan range to find ore and wake up (in ticks)")] + public readonly int KickDelay = 20; + + [Desc("Play this sound when the slave is freed")] + public readonly string FreeSound = null; + + public override object Create(ActorInitializer init) + { + return new SlaveMinerMaster(init, this); + } + } + + public class SlaveMinerMaster : BaseSpawnerMaster, INotifyTransform, + INotifyBuildingPlaced, ITick, IIssueOrder, IResolveOrder + { + const string OrderID = "SlaveMinerMasterHarvest"; + + public MiningState MiningState = MiningState.Mining; + public CPos? LastOrderLocation = null; + readonly SlaveMinerMasterInfo info; + readonly IResourceLayer resLayer; + readonly bool allowKicks = true; // allow kicks? + readonly Transforms transforms; + int respawnTicks = 0; + int kickTicks; + bool force = false; + CPos? forceMovePos = null; + + public IEnumerable Orders + { + get { yield return new SlaveMinerHarvestOrderTargeter(OrderID); } + } + + public SlaveMinerMaster(ActorInitializer init, SlaveMinerMasterInfo info) + : base(init, info) + { + this.info = info; + resLayer = init.Self.World.WorldActor.Trait(); + transforms = init.Self.Trait(); + } + + #region Transform + public void AfterTransform(Actor toActor) + { + // When transform complete, assign the slaves to this transform actor + var harvesterMaster = toActor.Trait(); + foreach (var se in SlaveEntries) + { + var slave = se.Actor; + se.SpawnerSlave.LinkMaster(slave, toActor, harvesterMaster); + se.SpawnerSlave.Stop(slave); + if (!slave.IsDead) + slave.QueueActivity(new Follow(slave, Target.FromActor(toActor), WDist.FromCells(1), WDist.FromCells(3), null)); + } + + harvesterMaster.SlaveEntries = SlaveEntries; + if (force) + { + harvesterMaster.LastOrderLocation = forceMovePos; + toActor.QueueActivity(new SlaveMinerHarvesterHarvest(toActor)); + } + else + { + toActor.QueueActivity(new SlaveMinerHarvesterHarvest(toActor)); + } + } + + public void BeforeTransform(Actor self) { } + + public void OnTransform(Actor self) { } + + #endregion + + public bool CanHarvestCell(CPos cell) + { + // Resources only exist in the ground layer + if (cell.Layer != 0) + return false; + + var resType = resLayer.GetResource(cell).Type; + if (resType == null) + return false; + + // Can the harvester collect this kind of resource? + return info.Resources.Contains(resType); + } + + void Launch(Actor master, BaseSpawnerSlaveEntry slaveEntry) + { + var slave = slaveEntry.Actor; + + SpawnIntoWorld(master, slave, master.CenterPosition); + } + + public override void SpawnIntoWorld(Actor self, Actor slave, WPos centerPosition) + { + base.SpawnIntoWorld(self, slave, centerPosition); + + self.World.AddFrameEndTask(w => + { + if (self.IsDead) + return; + + slave.QueueActivity(new FindAndDeliverResources(slave, self.Location)); + }); + } + + void HandleSpawnerHarvest(Actor self, Order order) + { + // Maybe player have a better idea, let's move + ForceMove(self.World.Map.CellContaining(order.Target.CenterPosition)); + } + + public void ForceMove(CPos pos) + { + force = true; + forceMovePos = pos; + transforms.DeployTransform(false); + } + + public override void OnSlaveKilled(Actor self, Actor slave) + { + // Set clock so that regen happens. + if (respawnTicks <= 0) // Don't interrupt an already running timer! + respawnTicks = Info.RespawnTicks; + } + + protected override void Killed(Actor self, AttackInfo e) + { + base.Killed(self, e); + + if (!string.IsNullOrEmpty(info.FreeSound)) + { + Game.Sound.Play(SoundType.World, info.FreeSound, self.CenterPosition); + } + } + + public void BuildingPlaced(Actor self) { } + + public void ResolveOrder(Actor self, Order order) + { + if (order.OrderString == OrderID) + { + HandleSpawnerHarvest(self, order); + } + else if (order.OrderString == "Stop" || order.OrderString == "Move") + { + MiningState = MiningState.Scan; + } + } + + public Order IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) + { + if (order.OrderID == OrderID) + return new Order(order.OrderID, self, target, queued); + return null; + } + + public void TickIdle(Actor self) + { + if (allowKicks && self.IsIdle) + kickTicks--; + else + kickTicks = info.KickDelay; + + if (kickTicks <= 0) + { + kickTicks = info.KickDelay; + MiningState = MiningState.Packaging; + self.QueueActivity(new SlaveMinerMasterHarvest(self)); + } + } + + public BaseSpawnerSlaveEntry[] GetSlaves() + { + return SlaveEntries; + } + + void ITick.Tick(Actor self) + { + respawnTicks--; + if (respawnTicks > 0) + return; + + if (MiningState != MiningState.Mining) + return; + + Replenish(self, SlaveEntries); + + var hasInvalidEntry = false; + foreach (var slaveEntry in SlaveEntries) + { + if (!slaveEntry.IsValid) + { + hasInvalidEntry = true; + } + else if (!slaveEntry.Actor.IsInWorld) + { + Launch(self, slaveEntry); + } + } + + if (hasInvalidEntry) + { + respawnTicks = Info.RespawnTicks; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SlaveMinerSlave.cs b/OpenRA.Mods.AS/Traits/SlaveMinerSlave.cs new file mode 100644 index 000000000000..8ba7e04aacac --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SlaveMinerSlave.cs @@ -0,0 +1,86 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public enum SlaveState + { + Free, + Idle, + } + + public class SlaveMinerSlaveInfo : BaseSpawnerSlaveInfo, Requires + { + [Desc("What will happen when master was killed?")] + public readonly SlaveState OnMasterKilled = SlaveState.Idle; + + [Desc("What will happen when master is changed owner?")] + public readonly SlaveState OnMasterOwnerChanged = SlaveState.Idle; + + public override object Create(ActorInitializer init) { return new SlaveMinerSlave(this); } + } + + class SlaveMinerSlave : BaseSpawnerSlave, ITick + { + readonly SlaveMinerSlaveInfo info; + + public SlaveMinerSlave(SlaveMinerSlaveInfo info) + : base(info) + { + this.info = info; + } + + public override void LinkMaster(Actor self, Actor master, BaseSpawnerMaster spawnerMaster) + { + base.LinkMaster(self, master, spawnerMaster); + } + + public override void OnMasterKilled(Actor self, Actor attacker, SpawnerSlaveDisposal disposal) + { + switch (info.OnMasterKilled) + { + case SlaveState.Free: + self.ChangeOwner(attacker.Owner); + break; + } + } + + public override void OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + switch (info.OnMasterOwnerChanged) + { + case SlaveState.Free: + self.ChangeOwner(newOwner); + break; + } + } + + void ITick.Tick(Actor self) + {/* + // Compensate for bug #13879 (upstream). + // https://github.com/OpenRA/OpenRA/issues/13879 + // Follow activity sometimes fails to cancel and the slaves get busy locked by WaitFor activity. + if (spawnerHarvesterMaster?.MiningState == MiningState.Mining && self.CurrentActivity is WaitFor) + { + self.CancelActivity(); + + /// No need to run this here, since it already happened. + /// This slave is just bugged out by Follow activity not canceling properly. + /// AssignTargetForSpawned(s, self.Location); + + self.QueueActivity(new FindAndDeliverResources(self)); + } + */ + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SmokeParticleEmitter.cs b/OpenRA.Mods.AS/Traits/SmokeParticleEmitter.cs new file mode 100644 index 000000000000..0c37b41ab6be --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SmokeParticleEmitter.cs @@ -0,0 +1,192 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using OpenRA.GameRules; +using OpenRA.Mods.AS.Effects; +using OpenRA.Mods.Common.Traits; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class SmokeParticleEmitterInfo : ConditionalTraitInfo, ISmokeParticleInfo, IRulesetLoaded + { + [FieldLoader.Require] + [Desc("The duration of an individual particle. Two values mean actual lifetime will vary between them.")] + public readonly int[] Duration; + + [Desc("Offset for the particle emitter.")] + public readonly WVec[] Offset = { WVec.Zero }; + + [Desc("Randomize particle forward movement.")] + public readonly WDist[] Speed = { WDist.Zero }; + + [Desc("Randomize particle gravity.")] + public readonly WDist[] Gravity = { WDist.Zero }; + + [Desc("Randomize particle facing.")] + public readonly bool RandomFacing = true; + + [Desc("Randomize particle turnrate.")] + public readonly int TurnRate = 0; + + [Desc("Rate to reset particle movement properties.")] + public readonly int RandomRate = 4; + + [Desc("How many particles should spawn.")] + public readonly int[] SpawnFrequency = { 100, 150 }; + + [Desc("Which image to use.")] + public readonly string Image = "particles"; + + [SequenceReference(nameof(Image))] + [Desc("Which sequence to use when the smoke starts.")] + public readonly string[] StartSequences = Array.Empty(); + + [FieldLoader.Require] + [SequenceReference(nameof(Image))] + [Desc("Which sequence to use while smoke is active.")] + public readonly string[] Sequences = Array.Empty(); + + [SequenceReference(nameof(Image))] + [Desc("Which sequence to use when the smoke ends.")] + public readonly string[] EndSequences = Array.Empty(); + + [PaletteReference(nameof(IsPlayerPalette))] + [Desc("Which palette to use.")] + public readonly string Palette = null; + + public readonly bool IsPlayerPalette = false; + + [WeaponReference] + [Desc("Has to be defined in weapons.yaml, if defined, as well.")] + public readonly string Weapon = null; + + public WeaponInfo WeaponInfo { get; private set; } + + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + base.RulesetLoaded(rules, ai); + + if (string.IsNullOrEmpty(Weapon)) + return; + + var weaponToLower = Weapon.ToLowerInvariant(); + if (!rules.Weapons.TryGetValue(weaponToLower, out var weaponInfo)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{weaponToLower}'"); + + WeaponInfo = weaponInfo; + } + + public override object Create(ActorInitializer init) { return new SmokeParticleEmitter(init.Self, this); } + + string ISmokeParticleInfo.Image + { + get { return Image; } + } + + string[] ISmokeParticleInfo.StartSequences + { + get { return StartSequences; } + } + + string[] ISmokeParticleInfo.Sequences + { + get { return Sequences; } + } + + string[] ISmokeParticleInfo.EndSequences + { + get { return EndSequences; } + } + + string ISmokeParticleInfo.Palette + { + get { return Palette; } + } + + bool ISmokeParticleInfo.IsPlayerPalette + { + get { return IsPlayerPalette; } + } + + WDist[] ISmokeParticleInfo.Speed + { + get { return Speed; } + } + + WDist[] ISmokeParticleInfo.Gravity + { + get { return Gravity; } + } + + int[] ISmokeParticleInfo.Duration + { + get { return Duration; } + } + + WeaponInfo ISmokeParticleInfo.Weapon + { + get { return WeaponInfo; } + } + + int ISmokeParticleInfo.TurnRate + { + get { return TurnRate; } + } + + int ISmokeParticleInfo.RandomRate + { + get { return RandomRate; } + } + } + + public class SmokeParticleEmitter : ConditionalTrait, ITick + { + readonly MersenneTwister random; + readonly WVec offset; + + IFacing facing; + int ticks; + + public SmokeParticleEmitter(Actor self, SmokeParticleEmitterInfo info) + : base(info) + { + random = self.World.SharedRandom; + + offset = Info.Offset.Length == 2 + ? new WVec(random.Next(Info.Offset[0].X, Info.Offset[1].X), random.Next(Info.Offset[0].Y, Info.Offset[1].Y), random.Next(Info.Offset[0].Z, Info.Offset[1].Z)) + : Info.Offset[0]; + } + + protected override void Created(Actor self) + { + facing = self.TraitOrDefault(); + + base.Created(self); + } + + void ITick.Tick(Actor self) + { + if (!self.IsInWorld || IsTraitDisabled) + return; + + if (--ticks < 0) + { + ticks = Info.SpawnFrequency.Length == 2 ? random.Next(Info.SpawnFrequency[0], Info.SpawnFrequency[1]) : Info.SpawnFrequency[0]; + + var spawnFacing = (!Info.RandomFacing && facing != null) ? facing.Facing.Facing : -1; + + self.World.AddFrameEndTask(w => w.Add(new SmokeParticle(self, Info, self.CenterPosition + offset, spawnFacing))); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Sound/NotificationAnnouncement.cs b/OpenRA.Mods.AS/Traits/Sound/NotificationAnnouncement.cs new file mode 100644 index 000000000000..4fbcffb4c753 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Sound/NotificationAnnouncement.cs @@ -0,0 +1,87 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits.Sound +{ + [Desc("Plays a notification clip when the trait is enabled.")] + public class NotificationAnnouncementInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [NotificationReference("Speech")] + [Desc("Speech notification to play.")] + public readonly string Notification = null; + + [TranslationReference(optional: true)] + [Desc("Text notification to display.")] + public readonly string TextNotification = null; + + [Desc("Player relationships who can hear this notification.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Ally | PlayerRelationship.Neutral | PlayerRelationship.Enemy; + + [Desc("Ping radar on the actor's location to affected players along with the notification.")] + public readonly bool PingRadar = false; + + [Desc("Play the notification to the owning player even if Stance.Ally is not included in ValidStances.")] + public readonly bool PlayToOwner = true; + + [Desc("Disable the announcement after it has been triggered.")] + public readonly bool OneShot = false; + + public override object Create(ActorInitializer init) { return new NotificationAnnouncement(init.Self, this); } + } + + public class NotificationAnnouncement : ConditionalTrait + { + bool triggered; + readonly Lazy radarPings; + + public NotificationAnnouncement(Actor self, NotificationAnnouncementInfo info) + : base(info) + { + radarPings = Exts.Lazy(() => self.World.WorldActor.Trait()); + } + + protected override void TraitEnabled(Actor self) + { + if (IsTraitDisabled) + return; + + if (Info.OneShot && triggered) + return; + + triggered = true; + var player = self.World.LocalPlayer ?? self.World.RenderPlayer; + if (player == null) + return; + + if (Info.ValidRelationships.HasRelationship(self.Owner.RelationshipWith(player))) + { + Game.Sound.PlayNotification(self.World.Map.Rules, player, "Speech", Info.Notification, player.Faction.InternalName); + TextNotificationsManager.AddTransientLine(player, Info.TextNotification); + + if (Info.PingRadar) + radarPings.Value?.Add(() => true, self.CenterPosition, Color.Red, 50); + } + else if (Info.PlayToOwner && self.Owner == player) + { + Game.Sound.PlayNotification(self.World.Map.Rules, player, "Speech", Info.Notification, player.Faction.InternalName); + TextNotificationsManager.AddTransientLine(player, Info.TextNotification); + + if (Info.PingRadar) + radarPings.Value?.Add(() => true, self.CenterPosition, Color.Red, 50); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Sound/NotificationOnPowerTransition.cs b/OpenRA.Mods.AS/Traits/Sound/NotificationOnPowerTransition.cs new file mode 100644 index 000000000000..7f138d3132a6 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Sound/NotificationOnPowerTransition.cs @@ -0,0 +1,65 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits.Sound +{ + [Desc("Play notifications when the player enters or exits low power.")] + public class NotificationOnPowerTransitionInfo : TraitInfo + { + [NotificationReference("Speech")] + public readonly string EnterLowPowerNotification = null; + + [NotificationReference("Speech")] + public readonly string ExitLowPowerNotification = null; + + public override object Create(ActorInitializer init) { return new NotificationOnPowerTransition(this); } + } + + public class NotificationOnPowerTransition : INotifyPowerLevelChanged, INotifyCreated + { + readonly NotificationOnPowerTransitionInfo info; + PowerManager playerPower; + bool wasLowPower; + + public NotificationOnPowerTransition(NotificationOnPowerTransitionInfo info) + { + this.info = info; + } + + void INotifyCreated.Created(Actor self) + { + // Special case handling is required for the Player actor. + // Created is called before Player.PlayerActor is assigned, + // so we must query other player traits from self, knowing that + // it refers to the same actor as self.Owner.PlayerActor + var playerActor = self.Info.Name == "player" ? self : self.Owner.PlayerActor; + + playerPower = playerActor.Trait(); + } + + void INotifyPowerLevelChanged.PowerLevelChanged(Actor self) + { + var lowPower = playerPower.PowerState != PowerState.Normal; + + if (lowPower != wasLowPower) + { + if (lowPower) + Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", info.EnterLowPowerNotification, self.Owner.Faction.InternalName); + else + Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", info.ExitLowPowerNotification, self.Owner.Faction.InternalName); + } + + wasLowPower = lowPower; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/Sound/WithCargoSounds.cs b/OpenRA.Mods.AS/Traits/Sound/WithCargoSounds.cs new file mode 100644 index 000000000000..9a39b8f5aa2d --- /dev/null +++ b/OpenRA.Mods.AS/Traits/Sound/WithCargoSounds.cs @@ -0,0 +1,69 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using OpenRA.Mods.Common.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class WithCargoSoundsInfo : ConditionalTraitInfo + { + [Desc("Speech notification played when an actor enters this cargo.")] + public readonly string EnterNotification = null; + + [Desc("Speech notification played when an actor leaves this cargo.")] + public readonly string ExitNotification = null; + + [Desc("List of sounds to be randomly played when an actor enters this cargo.")] + public readonly string[] EnterSounds = Array.Empty(); + + [Desc("List of sounds to be randomly played when an actor exits this cargo.")] + public readonly string[] ExitSounds = Array.Empty(); + + [Desc("Does the sound play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the EnterSounds and ExitSounds played at.")] + public readonly float SoundVolume = 1f; + + public override object Create(ActorInitializer init) { return new WithCargoSounds(init.Self, this); } + } + + public class WithCargoSounds : ConditionalTrait, INotifyPassengerEntered, INotifyPassengerExited + { + public WithCargoSounds(Actor self, WithCargoSoundsInfo info) + : base(info) { } + + void INotifyPassengerEntered.OnPassengerEntered(Actor self, Actor passenger) + { + if (Info.EnterSounds.Length > 0) + { + var pos = self.CenterPosition; + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, Info.EnterSounds, self.World, pos, null, Info.SoundVolume); + } + + Game.Sound.PlayNotification(self.World.Map.Rules, passenger.Owner, "Speech", Info.EnterNotification, passenger.Owner.Faction.InternalName); + } + + void INotifyPassengerExited.OnPassengerExited(Actor self, Actor passenger) + { + if (Info.ExitSounds.Length > 0) + { + var pos = self.CenterPosition; + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, Info.ExitSounds, self.World, pos, null, Info.SoundVolume); + } + + Game.Sound.PlayNotification(self.World.Map.Rules, passenger.Owner, "Speech", Info.ExitNotification, passenger.Owner.Faction.InternalName); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SpawnActorOnOwnerChange.cs b/OpenRA.Mods.AS/Traits/SpawnActorOnOwnerChange.cs new file mode 100644 index 000000000000..6996b4c81771 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SpawnActorOnOwnerChange.cs @@ -0,0 +1,48 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Spawns a proxy actor when this actor changes ownership.")] + public class SpawnProxyActorOnOwnerChangeInfo : ConditionalTraitInfo + { + [ActorReference] + [FieldLoader.Require] + public readonly string ProxyActor = null; + + public override object Create(ActorInitializer init) { return new SpawnProxyActorOnOwnerChange(this); } + } + + public class SpawnProxyActorOnOwnerChange : ConditionalTrait, INotifyOwnerChanged + { + readonly SpawnProxyActorOnOwnerChangeInfo info; + + public SpawnProxyActorOnOwnerChange(SpawnProxyActorOnOwnerChangeInfo info) + : base(info) + { + this.info = info; + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + if (IsTraitDisabled) + return; + + self.World.AddFrameEndTask(w => w.CreateActor(info.ProxyActor, new TypeDictionary + { + new OwnerInit(newOwner) + })); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SpawnNeighboringActors.cs b/OpenRA.Mods.AS/Traits/SpawnNeighboringActors.cs new file mode 100644 index 000000000000..43afda5de5a1 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SpawnNeighboringActors.cs @@ -0,0 +1,100 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("This actor places other actors around itself, which keep connected as in they get removed when the parent is sold or destroyed.")] + public class SpawnNeighboringActorsInfo : TraitInfo + { + [FieldLoader.Require] + [ActorReference] + [Desc("Types of actors to place. If multiple are defined, a random one will be selected for each actor spawned.")] + public readonly HashSet ActorTypes = new(); + + [FieldLoader.Require] + [Desc("Locations to spawn the actors relative to the origin (top-left for buildings) of this actor.")] + public readonly CVec[] Locations = Array.Empty(); + + public override object Create(ActorInitializer init) { return new SpawnNeighboringActors(this, init.Self); } + } + + public class SpawnNeighboringActors : INotifyKilled, INotifyOwnerChanged, INotifyActorDisposing, INotifyAddedToWorld, INotifySold + { + readonly SpawnNeighboringActorsInfo info; + readonly List actors = new(); + + public SpawnNeighboringActors(SpawnNeighboringActorsInfo info, Actor self) + { + this.info = info; + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + SpawnActors(self); + } + + public void SpawnActors(Actor self) + { + foreach (var offset in info.Locations) + { + self.World.AddFrameEndTask(w => + { + var actorType = info.ActorTypes.Random(self.World.SharedRandom).ToLowerInvariant(); + var cell = self.Location + offset; + + var actor = w.CreateActor(true, actorType, new TypeDictionary + { + new OwnerInit(self.Owner), + new LocationInit(cell) + }); + + actors.Add(actor); + }); + } + } + + public void RemoveActors() + { + foreach (var actor in actors) + actor.Dispose(); + + actors.Clear(); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + foreach (var actor in actors) + actor.ChangeOwnerSync(newOwner); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + RemoveActors(); + } + + void INotifyActorDisposing.Disposing(Actor self) + { + RemoveActors(); + } + + void INotifySold.Selling(Actor self) { } + void INotifySold.Sold(Actor self) + { + RemoveActors(); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SpawnSmokeParticleOnDeath.cs b/OpenRA.Mods.AS/Traits/SpawnSmokeParticleOnDeath.cs new file mode 100644 index 000000000000..627c2805e460 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SpawnSmokeParticleOnDeath.cs @@ -0,0 +1,182 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using OpenRA.GameRules; +using OpenRA.Mods.AS.Effects; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Spawn smoke particles when this actor is killed.")] + public class SpawnSmokeParticleOnDeathInfo : ConditionalTraitInfo, ISmokeParticleInfo, IRulesetLoaded + { + [Desc("How many particles should spawn.")] + public readonly int[] Amount = { 1 }; + + [Desc("DeathType(s) that trigger spawning. Leave empty to always spawn.")] + public readonly BitSet DeathTypes = default; + + [FieldLoader.Require] + [Desc("The duration of an individual particle. Two values mean actual lifetime will vary between them.")] + public readonly int[] Duration; + + [Desc("Offset for the particle emitter.")] + public readonly WVec[] Offset = { WVec.Zero }; + + [Desc("Randomize particle forward movement.")] + public readonly WDist[] Speed = { WDist.Zero }; + + [Desc("Randomize particle gravity.")] + public readonly WDist[] Gravity = { WDist.Zero }; + + [Desc("Randomize particle turnrate.")] + public readonly int TurnRate = 0; + + [Desc("Rate to reset particle movement properties.")] + public readonly int RandomRate = 4; + + [Desc("Which image to use.")] + public readonly string Image = "particles"; + + [SequenceReference(nameof(Image))] + [Desc("Which sequence to use when the smoke starts.")] + public readonly string[] StartSequences = Array.Empty(); + + [FieldLoader.Require] + [SequenceReference(nameof(Image))] + [Desc("Which sequence to use while smoke is active.")] + public readonly string[] Sequences = Array.Empty(); + + [SequenceReference(nameof(Image))] + [Desc("Which sequence to use when the smoke ends.")] + public readonly string[] EndSequences = Array.Empty(); + + [PaletteReference(nameof(IsPlayerPalette))] + [Desc("Which palette to use.")] + public readonly string Palette = null; + + public readonly bool IsPlayerPalette = false; + + [WeaponReference] + [Desc("Has to be defined in weapons.yaml, if defined, as well.")] + public readonly string Weapon = null; + + public WeaponInfo WeaponInfo { get; private set; } + + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + base.RulesetLoaded(rules, ai); + + if (string.IsNullOrEmpty(Weapon)) + return; + + var weaponToLower = Weapon.ToLowerInvariant(); + if (!rules.Weapons.TryGetValue(weaponToLower, out var weaponInfo)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{weaponToLower}'"); + + WeaponInfo = weaponInfo; + } + + public override object Create(ActorInitializer init) { return new SpawnSmokeParticleOnDeath(this); } + + string ISmokeParticleInfo.Image + { + get { return Image; } + } + + string[] ISmokeParticleInfo.StartSequences + { + get { return StartSequences; } + } + + string[] ISmokeParticleInfo.Sequences + { + get { return Sequences; } + } + + string[] ISmokeParticleInfo.EndSequences + { + get { return EndSequences; } + } + + string ISmokeParticleInfo.Palette + { + get { return Palette; } + } + + bool ISmokeParticleInfo.IsPlayerPalette + { + get { return IsPlayerPalette; } + } + + WDist[] ISmokeParticleInfo.Speed + { + get { return Speed; } + } + + WDist[] ISmokeParticleInfo.Gravity + { + get { return Gravity; } + } + + int[] ISmokeParticleInfo.Duration + { + get { return Duration; } + } + + WeaponInfo ISmokeParticleInfo.Weapon + { + get { return WeaponInfo; } + } + + int ISmokeParticleInfo.TurnRate + { + get { return TurnRate; } + } + + int ISmokeParticleInfo.RandomRate + { + get { return RandomRate; } + } + } + + public class SpawnSmokeParticleOnDeath : ConditionalTrait, INotifyKilled + { + public SpawnSmokeParticleOnDeath(SpawnSmokeParticleOnDeathInfo info) + : base(info) { } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (IsTraitDisabled) + return; + + if (!Info.DeathTypes.IsEmpty && !e.Damage.DamageTypes.Overlaps(Info.DeathTypes)) + return; + + var random = self.World.SharedRandom; + + var amount = Info.Amount.Length == 2 + ? random.Next(Info.Amount[0], Info.Amount[1]) + : Info.Amount[0]; + + for (var i = 0; i < amount; i++) + { + var offset = Info.Offset.Length == 2 + ? new WVec(random.Next(Info.Offset[0].X, Info.Offset[1].X), random.Next(Info.Offset[0].Y, Info.Offset[1].Y), random.Next(Info.Offset[0].Z, Info.Offset[1].Z)) + : Info.Offset[0]; + + self.World.AddFrameEndTask(w => w.Add(new SmokeParticle(e.Attacker, Info, self.CenterPosition + offset))); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SpawnSurvivors.cs b/OpenRA.Mods.AS/Traits/SpawnSurvivors.cs new file mode 100644 index 000000000000..80ffb6461db1 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SpawnSurvivors.cs @@ -0,0 +1,68 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Spawns survivors when an actor is destroyed.")] + public class SpawnSurvivorsInfo : ConditionalTraitInfo + { + [ActorReference] + [FieldLoader.Require] + [Desc("The actors spawned.")] + public readonly string[] Actors = Array.Empty(); + + [Desc("DeathType(s) that trigger spawning. Leave empty to always spawn.")] + public readonly BitSet DeathTypes = default; + + public override object Create(ActorInitializer actor) { return new SpawnSurvivors(this); } + } + + public class SpawnSurvivors : ConditionalTrait, INotifyKilled + { + public SpawnSurvivors(SpawnSurvivorsInfo info) + : base(info) { } + + void INotifyKilled.Killed(Actor self, AttackInfo attack) + { + if (IsTraitDisabled) + return; + + if (!Info.DeathTypes.IsEmpty && !attack.Damage.DamageTypes.Overlaps(Info.DeathTypes)) + return; + + var buildingInfo = self.Info.TraitInfoOrDefault(); + var eligibleLocations = buildingInfo != null + ? buildingInfo.Tiles(self.Location).ToList() + : new List() { self.World.Map.CellContaining(self.CenterPosition) }; + + self.World.AddFrameEndTask(w => + { + foreach (var actorType in Info.Actors) + { + var td = new TypeDictionary + { + new OwnerInit(self.Owner), + new LocationInit(eligibleLocations.Random(w.SharedRandom)) + }; + + var unit = w.CreateActor(true, actorType.ToLowerInvariant(), td); + unit.QueueActivity(false, new Nudge(unit)); + } + }); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SupportPowers/AirstrikePowerAS.cs b/OpenRA.Mods.AS/Traits/SupportPowers/AirstrikePowerAS.cs new file mode 100644 index 000000000000..1afeccef09ad --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SupportPowers/AirstrikePowerAS.cs @@ -0,0 +1,124 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Mods.AS.Effects; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public enum AirstrikeMission { Attack, Guard } + + public class AirstrikePowerASInfo : SupportPowerInfo + { + [ActorReference(typeof(AircraftInfo))] + public readonly string UnitType = "badr.bomber"; + public readonly int SquadSize = 1; + public readonly WVec SquadOffset = new(-1536, 1536, 0); + + public readonly int QuantizedFacings = 32; + public readonly WDist Cordon = new(5120); + + [ActorReference] + [Desc("Actor to spawn when the aircrafts arrive.")] + public readonly string CameraActor = null; + + [Desc("Amount of time to keep the camera alive after the aircraft have left the area.")] + public readonly int CameraRemoveDelay = 25; + + [Desc("Weapon range offset to apply during the beacon clock calculation.")] + public readonly WDist BeaconDistanceOffset = WDist.FromCells(6); + + public readonly AirstrikeMission Mission = AirstrikeMission.Attack; + + public readonly int GuardDuration = 150; + + public override object Create(ActorInitializer init) { return new AirstrikePowerAS(init.Self, this); } + } + + public class AirstrikePowerAS : SupportPower + { + public AirstrikePowerAS(Actor self, AirstrikePowerASInfo info) + : base(self, info) { } + + public override void Activate(Actor self, Order order, SupportPowerManager manager) + { + base.Activate(self, order, manager); + + SendAirstrike(self, order.Target.CenterPosition); + } + + public void SendAirstrike(Actor self, WPos target, bool randomize = true, int attackFacing = 0) + { + var info = Info as AirstrikePowerASInfo; + + if (randomize) + attackFacing = 256 * self.World.SharedRandom.Next(info.QuantizedFacings) / info.QuantizedFacings; + + var altitude = self.World.Map.Rules.Actors[info.UnitType].TraitInfo().CruiseAltitude.Length; + var attackRotation = WRot.FromFacing(attackFacing); + var delta = new WVec(0, -1024, 0).Rotate(attackRotation); + target += new WVec(0, 0, altitude); + + var startPos = target - (self.World.Map.DistanceToEdge(target, -delta) + info.Cordon).Length * delta / 1024; + + self.World.AddFrameEndTask(w => + { + PlayLaunchSounds(); + + var aircrafts = new HashSet(); + + for (var i = -info.SquadSize / 2; i <= info.SquadSize / 2; i++) + { + // Even-sized squads skip the lead plane + if (i == 0 && (info.SquadSize & 1) == 0) + continue; + + // Includes the 90 degree rotation between body and world coordinates + var so = info.SquadOffset; + var spawnOffset = new WVec(i * so.Y, -Math.Abs(i) * so.X, 0).Rotate(attackRotation); + + var a = w.CreateActor(info.UnitType, new TypeDictionary + { + new CenterPositionInit(startPos + spawnOffset), + new OwnerInit(self.Owner), + new FacingInit(WAngle.FromFacing(attackFacing)), + }); + + delta = new WVec(WDist.Zero, info.BeaconDistanceOffset, WDist.Zero).Rotate(attackRotation); + + if (info.Mission == AirstrikeMission.Attack) + { + var height = self.World.Map.DistanceAboveTerrain(target + spawnOffset); + a.QueueActivity(new FlyAttack(a, AttackSource.Default, Target.FromPos(target + spawnOffset - new WVec(WDist.Zero, WDist.Zero, height)), true, Color.OrangeRed)); + } + else + { + a.QueueActivity(new Fly(a, Target.FromPos(target + spawnOffset))); + a.QueueActivity(new AttackMoveActivity(a, () => new FlyIdle(a, info.GuardDuration, false))); + } + + a.QueueActivity(new FlyOffMap(a)); + a.QueueActivity(new RemoveSelf()); + + aircrafts.Add(a); + } + + var effect = new AirstrikePowerASEffect(self.World, self.Owner, target, aircrafts, this, info); + self.World.Add(effect); + }); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SupportPowers/AirstrikePowerRV.cs b/OpenRA.Mods.AS/Traits/SupportPowers/AirstrikePowerRV.cs new file mode 100644 index 000000000000..f7778fdc659c --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SupportPowers/AirstrikePowerRV.cs @@ -0,0 +1,199 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.AS.Effects; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class AirstrikePowerRVInfo : SupportPowerInfo + { + [FieldLoader.Require] + public readonly Dictionary UnitTypes = new(); + + [FieldLoader.Require] + public readonly Dictionary SquadSizes = new(); + + public readonly WVec SquadOffset = new(-1536, 1536, 0); + + public readonly int QuantizedFacings = 32; + public readonly WDist Cordon = new(5120); + + [Desc("Delay between activation and aircraft spawning")] + public readonly int ActivationDelay = 0; + + [ActorReference] + [Desc("Actor to spawn when the aircraft start attacking")] + public readonly string CameraActor = null; + + [Desc("Amount of time to keep the camera alive after the aircraft have finished attacking")] + public readonly int CameraRemoveDelay = 25; + + [Desc("Enables the player directional targeting")] + public readonly bool UseDirectionalTarget = false; + + [Desc("Animation used to render the direction arrows.")] + public readonly string DirectionArrowAnimation = null; + + [Desc("Palette for direction cursor animation.")] + public readonly string DirectionArrowPalette = "chrome"; + + [Desc("Weapon range offset to apply during the beacon clock calculation")] + public readonly WDist BeaconDistanceOffset = WDist.FromCells(6); + + public override object Create(ActorInitializer init) { return new AirstrikePowerRV(init.Self, this); } + } + + public class AirstrikePowerRV : SupportPower + { + readonly AirstrikePowerRVInfo info; + + public AirstrikePowerRV(Actor self, AirstrikePowerRVInfo info) + : base(self, info) + { + this.info = info; + } + + public override void SelectTarget(Actor self, string order, SupportPowerManager manager) + { + if (info.UseDirectionalTarget) + { + Game.Sound.PlayToPlayer(SoundType.UI, manager.Self.Owner, Info.SelectTargetSound); + Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", + Info.SelectTargetSpeechNotification, self.Owner.Faction.InternalName); + + self.World.OrderGenerator = new SelectDirectionalTarget(self.World, order, manager, Info.Cursor, info.DirectionArrowAnimation, info.DirectionArrowPalette); + } + else + base.SelectTarget(self, order, manager); + } + + public override void Activate(Actor self, Order order, SupportPowerManager manager) + { + base.Activate(self, order, manager); + + var facing = info.UseDirectionalTarget && order.ExtraData != uint.MaxValue ? (WAngle?)WAngle.FromFacing((int)order.ExtraData) : null; + SendAirstrike(self, order.Target.CenterPosition, facing); + } + + public Actor[] SendAirstrike(Actor self, WPos target, WAngle? facing = null) + { + var aircraft = new List(); + if (!facing.HasValue) + facing = new WAngle(1024 * self.World.SharedRandom.Next(info.QuantizedFacings) / info.QuantizedFacings); + + var altitude = self.World.Map.Rules.Actors[info.UnitTypes.First(ut => ut.Key == GetLevel()).Value].TraitInfo().CruiseAltitude.Length; + var attackRotation = WRot.FromYaw(facing.Value); + var delta = new WVec(0, -1024, 0).Rotate(attackRotation); + target += new WVec(0, 0, altitude); + var startEdge = target - (self.World.Map.DistanceToEdge(target, -delta) + info.Cordon).Length * delta / 1024; + var finishEdge = target + (self.World.Map.DistanceToEdge(target, delta) + info.Cordon).Length * delta / 1024; + + Actor camera = null; + var aircraftInRange = new Dictionary(); + + void OnEnterRange(Actor a) + { + // Spawn a camera and remove the beacon when the first plane enters the target area + if (info.CameraActor != null && camera == null && !aircraftInRange.Any(kv => kv.Value)) + { + self.World.AddFrameEndTask(w => + { + camera = w.CreateActor(info.CameraActor, new TypeDictionary + { + new LocationInit(self.World.Map.CellContaining(target)), + new OwnerInit(self.Owner), + }); + }); + } + + aircraftInRange[a] = true; + } + + void OnExitRange(Actor a) + { + aircraftInRange[a] = false; + + // Remove the camera when the final plane leaves the target area + if (!aircraftInRange.Any(kv => kv.Value)) + RemoveCamera(camera); + } + + void OnRemovedFromWorld(Actor a) + { + aircraftInRange[a] = false; + + // Checking for attack range is not relevant here because + // aircraft may be shot down before entering the range. + // If at the map's edge, they may be removed from world before leaving. + if (aircraftInRange.All(kv => !kv.Key.IsInWorld)) + { + RemoveCamera(camera); + } + } + + // Create the actors immediately so they can be returned + var squadSize = info.SquadSizes.First(ss => ss.Key == GetLevel()).Value; + for (var i = -squadSize / 2; i <= squadSize / 2; i++) + { + // Even-sized squads skip the lead plane + if (i == 0 && (squadSize & 1) == 0) + continue; + + // Includes the 90 degree rotation between body and world coordinates + var so = info.SquadOffset; + var spawnOffset = new WVec(i * so.Y, -Math.Abs(i) * so.X, 0).Rotate(attackRotation); + var targetOffset = new WVec(i * so.Y, 0, 0).Rotate(attackRotation); + var a = self.World.CreateActor(false, info.UnitTypes.First(ut => ut.Key == GetLevel()).Value, new TypeDictionary + { + new CenterPositionInit(startEdge + spawnOffset), + new OwnerInit(self.Owner), + new FacingInit(facing.Value), + }); + + aircraft.Add(a); + aircraftInRange.Add(a, false); + + var attack = a.Trait(); + attack.SetTarget(target + targetOffset); + attack.OnEnteredAttackRange += OnEnterRange; + attack.OnExitedAttackRange += OnExitRange; + attack.OnRemovedFromWorld += OnRemovedFromWorld; + } + + self.World.AddFrameEndTask(w => + { + PlayLaunchSounds(); + + var effect = new AirstrikePowerRVEffect(self.World, self.Owner, target, startEdge, finishEdge, attackRotation, altitude, GetLevel(), aircraft.ToArray(), this, info); + self.World.Add(effect); + }); + + return aircraft.ToArray(); + } + + void RemoveCamera(Actor camera) + { + if (camera == null) + return; + + camera.QueueActivity(new Wait(info.CameraRemoveDelay)); + camera.QueueActivity(new RemoveSelf()); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SupportPowers/DetonateWeaponPower.cs b/OpenRA.Mods.AS/Traits/SupportPowers/DetonateWeaponPower.cs new file mode 100644 index 000000000000..a994dfd4a547 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SupportPowers/DetonateWeaponPower.cs @@ -0,0 +1,233 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Effects; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Orders; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Support power for detonating a weapon at the target position.")] + public class DetonateWeaponPowerInfo : SupportPowerInfo, IRulesetLoaded + { + [FieldLoader.Require] + public readonly Dictionary Weapons = new(); + + [Desc("Delay between activation and explosion")] + public readonly int ActivationDelay = 10; + + [Desc("Amount of time before detonation to remove the beacon")] + public readonly int BeaconRemoveAdvance = 5; + + [Desc("Range of cells the camera should reveal around target cell.")] + public readonly WDist CameraRange = WDist.Zero; + + [Desc("Can the camera reveal shroud generated by the GeneratesShroud trait?")] + public readonly bool RevealGeneratedShroud = true; + + [Desc("Ignore AirburstAltitude for where to spawn the camera.")] + public readonly bool SpawnCameraAtGround = true; + + [Desc("Reveal cells to players with these stances only.")] + public readonly PlayerRelationship CameraRelationships = PlayerRelationship.Ally; + + [Desc("Amount of time before detonation to spawn the camera")] + public readonly int CameraSpawnAdvance = 5; + + [Desc("Amount of time after detonation to remove the camera")] + public readonly int CameraRemoveDelay = 5; + + [SequenceReference] + [Desc("Sequence the launching actor should play when activating this power.")] + public readonly string ActivationSequence = "active"; + + [Desc("Altitude above terrain below which to explode. Zero effectively deactivates airburst.")] + public readonly WDist AirburstAltitude = WDist.Zero; + + public readonly Dictionary TargetCircleRanges; + public readonly Color TargetCircleColor = Color.White; + public readonly bool TargetCircleUsePlayerColor = false; + public readonly float TargetCircleWidth = 1; + public readonly Color TargetCircleBorderColor = Color.FromArgb(96, Color.Black); + public readonly float TargetCircleBorderWidth = 3; + + public readonly Dictionary WeaponInfos = new(); + + public override object Create(ActorInitializer init) { return new DetonateWeaponPower(init.Self, this); } + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + foreach (var weapon in Weapons) + { + var weaponToLower = weapon.Value.ToLowerInvariant(); + if (!rules.Weapons.TryGetValue(weaponToLower, out var weaponInfo)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{weaponToLower}'"); + + if (!WeaponInfos.ContainsKey(weapon.Key)) + WeaponInfos.Add(weapon.Key, rules.Weapons[weaponToLower]); + } + + base.RulesetLoaded(rules, ai); + } + } + + public class DetonateWeaponPower : SupportPower, ITick + { + public new readonly DetonateWeaponPowerInfo Info; + int ticks; + + public DetonateWeaponPower(Actor self, DetonateWeaponPowerInfo info) + : base(self, info) + { + Info = info; + } + + public override void Activate(Actor self, Order order, SupportPowerManager manager) + { + base.Activate(self, order, manager); + PlayLaunchSounds(); + + if (self.Owner.IsAlliedWith(self.World.RenderPlayer)) + Game.Sound.Play(SoundType.World, Info.LaunchSound); + else + Game.Sound.Play(SoundType.World, Info.IncomingSound); + + if (!string.IsNullOrEmpty(Info.ActivationSequence)) + { + var wsb = self.Trait(); + wsb.PlayCustomAnimation(self, Info.ActivationSequence); + } + + var targetPosition = order.Target.CenterPosition + new WVec(WDist.Zero, WDist.Zero, Info.AirburstAltitude); + + self.World.AddFrameEndTask(w => w.Add(new DelayedAction(Info.ActivationDelay, () => self.World.AddFrameEndTask(w => Info.WeaponInfos.First(wi => wi.Key == GetLevel()).Value.Impact(Target.FromPos(targetPosition), self))))); + + if (Info.CameraRange != WDist.Zero) + { + var cameraPosition = Info.SpawnCameraAtGround ? order.Target.CenterPosition : targetPosition; + var type = Info.RevealGeneratedShroud ? Shroud.SourceType.Visibility + : Shroud.SourceType.PassiveVisibility; + + self.World.AddFrameEndTask(w => w.Add(new RevealShroudEffect(cameraPosition, Info.CameraRange, type, self.Owner, Info.CameraRelationships, + Info.ActivationDelay - Info.CameraSpawnAdvance, Info.CameraSpawnAdvance + Info.CameraRemoveDelay))); + } + + if (Info.DisplayBeacon) + { + var beacon = new Beacon( + order.Player, + targetPosition, + Info.BeaconPaletteIsPlayerPalette, + Info.BeaconPalette, + Info.BeaconImage, + Info.BeaconPosters.First(bp => bp.Key == GetLevel()).Value, + Info.BeaconPosterPalette, + Info.BeaconSequence, + Info.ArrowSequence, + Info.CircleSequence, + Info.ClockSequence, + () => FractionComplete); + + self.World.AddFrameEndTask(w => + { + w.Add(beacon); + w.Add(new DelayedAction(Info.ActivationDelay - Info.BeaconRemoveAdvance, () => self.World.AddFrameEndTask(w => + { + w.Remove(beacon); + beacon = null; + }))); + }); + } + } + + void ITick.Tick(Actor self) + { + ticks++; + } + + public override void SelectTarget(Actor self, string order, SupportPowerManager manager) + { + Game.Sound.PlayToPlayer(SoundType.UI, manager.Self.Owner, Info.SelectTargetSound); + self.World.OrderGenerator = new SelectDetonateWeaponPowerTarget(order, manager, this); + } + + float FractionComplete { get { return ticks * 1f / Info.ActivationDelay; } } + } + + public class SelectDetonateWeaponPowerTarget : OrderGenerator + { + readonly SupportPowerManager manager; + readonly string order; + readonly DetonateWeaponPower power; + + public SelectDetonateWeaponPowerTarget(string order, SupportPowerManager manager, DetonateWeaponPower power) + { + // Clear selection if using Left-Click Orders + if (Game.Settings.Game.UseClassicMouseStyle) + manager.Self.World.Selection.Clear(); + + this.manager = manager; + this.order = order; + this.power = power; + } + + protected override IEnumerable OrderInner(World world, CPos cell, int2 worldPixel, MouseInput mi) + { + world.CancelInputMode(); + if (mi.Button == MouseButton.Left && world.Map.Contains(cell)) + yield return new Order(order, manager.Self, Target.FromCell(world, cell), false) { SuppressVisualFeedback = true }; + } + + protected override void Tick(World world) + { + // Cancel the OG if we can't use the power + if (!manager.Powers.ContainsKey(order)) + world.CancelInputMode(); + } + + protected override IEnumerable Render(WorldRenderer wr, World world) { yield break; } + + protected override IEnumerable RenderAboveShroud(WorldRenderer wr, World world) { yield break; } + + protected override IEnumerable RenderAnnotations(WorldRenderer wr, World world) + { + var xy = wr.Viewport.ViewToWorld(Viewport.LastMousePos); + + if (power.Info.TargetCircleRanges == null || !power.Info.TargetCircleRanges.Any() || power.GetLevel() == 0) + { + yield break; + } + else + { + yield return new RangeCircleAnnotationRenderable( + world.Map.CenterOfCell(xy), + power.Info.TargetCircleRanges[power.GetLevel()], + 0, + power.Info.TargetCircleUsePlayerColor ? power.Self.Owner.Color : power.Info.TargetCircleColor, + power.Info.TargetCircleWidth, + power.Info.TargetCircleBorderColor, + power.Info.TargetCircleBorderWidth); + } + } + + protected override string GetCursor(World world, CPos cell, int2 worldPixel, MouseInput mi) + { + return world.Map.Contains(cell) ? power.Info.Cursor : "generic-blocked"; + } + } +} diff --git a/OpenRA.Mods.AS/Traits/SupportPowers/FireArmamentPower.cs b/OpenRA.Mods.AS/Traits/SupportPowers/FireArmamentPower.cs new file mode 100644 index 000000000000..16cb7f0db29b --- /dev/null +++ b/OpenRA.Mods.AS/Traits/SupportPowers/FireArmamentPower.cs @@ -0,0 +1,371 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Effects; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Orders; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Support power type to fire a burst of armaments.")] + public class FireArmamentPowerInfo : SupportPowerInfo + { + [Desc("The `Name` of the armaments this support power is allowed to fire.")] + public readonly string ArmamentName = "superweapon"; + + [Desc("If `AllowMultiple` is `false`, how many instances of this support power are allowed to fire.", + "Actual instances might end up less due to range/etc.")] + public readonly int MaximumFiringInstances = 1; + + [Desc("Amount of time before detonation to remove the beacon.")] + public readonly int BeaconRemoveAdvance = 25; + + [Desc("Range of cells the camera should reveal around target cell.")] + public readonly WDist CameraRange = WDist.Zero; + + [Desc("Can the camera reveal shroud generated by the GeneratesShroud trait?")] + public readonly bool RevealGeneratedShroud = true; + + [Desc("Reveal cells to players with these stances only.")] + public readonly PlayerRelationship CameraStances = PlayerRelationship.Ally; + + [Desc("Amount of time before firing to spawn the camera.")] + public readonly int CameraSpawnAdvance = 25; + + [Desc("Amount of time after firing to remove the camera.")] + public readonly int CameraRemoveDelay = 25; + + public readonly WDist TargetCircleRange = WDist.Zero; + public readonly Color TargetCircleColor = Color.White; + public readonly bool TargetCircleUsePlayerColor = false; + public readonly float TargetCircleWidth = 1; + public readonly Color TargetCircleBorderColor = Color.FromArgb(96, Color.Black); + public readonly float TargetCircleBorderWidth = 3; + + public override object Create(ActorInitializer init) { return new FireArmamentPower(init.Self, this); } + } + + public class FireArmamentPower : SupportPower, ITick, INotifyBurstComplete, IResolveOrder + { + public readonly FireArmamentPowerInfo FireArmamentPowerInfo; + + IFacing facing; + HashSet activeArmaments; + + bool turreted; + HashSet turrets; + + bool enabled; + int ticks; + int estimatedTicks; + Target target; + + public Armament[] Armaments; + + public FireArmamentPower(Actor self, FireArmamentPowerInfo info) + : base(self, info) + { + FireArmamentPowerInfo = info; + enabled = false; + } + + protected override void Created(Actor self) + { + facing = self.TraitOrDefault(); + Armaments = self.TraitsImplementing().Where(t => t.Info.Name.Contains(FireArmamentPowerInfo.ArmamentName)).ToArray(); + activeArmaments = new HashSet(); + + var armamentturrets = Armaments.Select(x => x.Info.Turret).ToHashSet(); + turreted = self.TraitsImplementing().Any(x => armamentturrets.Contains(x.Name)); + + base.Created(self); + } + + public override void Activate(Actor self, Order order, SupportPowerManager manager) + { + base.Activate(self, order, manager); + + if (FireArmamentPowerInfo.MaximumFiringInstances > 1) + return; + + Activation(self, order); + } + + void IResolveOrder.ResolveOrder(Actor self, Order order) + { + if (order.OrderString.Contains(Info.OrderName)) + Activation(self, order); + } + + void Activation(Actor self, Order order) + { + activeArmaments = Armaments.Where(x => !x.IsTraitDisabled).ToHashSet(); + + if (turreted) + { + var armamentturrets = activeArmaments.Select(x => x.Info.Turret).ToHashSet(); + + // TODO: Fix this when upgradable Turreteds arrive. + turrets = self.TraitsImplementing().Where(x => armamentturrets.Contains(x.Name)).ToHashSet(); + } + + PlayLaunchSounds(); + if (self.Owner.IsAlliedWith(self.World.RenderPlayer)) + Game.Sound.Play(SoundType.World, FireArmamentPowerInfo.LaunchSound); + else + Game.Sound.Play(SoundType.World, FireArmamentPowerInfo.IncomingSound); + + target = order.Target; + + enabled = true; + + // TODO: Estimate the projectile travel time somehow + estimatedTicks = activeArmaments.Max(x => x.FireDelay); + + if (FireArmamentPowerInfo.CameraRange != WDist.Zero) + { + var type = FireArmamentPowerInfo.RevealGeneratedShroud ? Shroud.SourceType.Visibility + : Shroud.SourceType.PassiveVisibility; + + self.World.AddFrameEndTask(w => w.Add(new RevealShroudEffect(target.CenterPosition, FireArmamentPowerInfo.CameraRange, type, self.Owner, + FireArmamentPowerInfo.CameraStances, estimatedTicks - FireArmamentPowerInfo.CameraSpawnAdvance, + FireArmamentPowerInfo.CameraSpawnAdvance + FireArmamentPowerInfo.CameraRemoveDelay))); + } + + if (FireArmamentPowerInfo.DisplayBeacon) + { + var beacon = new Beacon( + order.Player, + target.CenterPosition, + FireArmamentPowerInfo.BeaconPaletteIsPlayerPalette, + FireArmamentPowerInfo.BeaconPalette, + FireArmamentPowerInfo.BeaconImage, + FireArmamentPowerInfo.BeaconPosters.First(bp => bp.Key == GetLevel()).Value, + FireArmamentPowerInfo.BeaconPosterPalette, + FireArmamentPowerInfo.BeaconSequence, + FireArmamentPowerInfo.ArrowSequence, + FireArmamentPowerInfo.CircleSequence, + FireArmamentPowerInfo.ClockSequence, + () => FractionComplete); + + self.World.AddFrameEndTask(w => + { + w.Add(beacon); + w.Add(new DelayedAction(estimatedTicks - FireArmamentPowerInfo.BeaconRemoveAdvance, () => self.World.AddFrameEndTask(w => + { + w.Remove(beacon); beacon = null; + }))); + }); + } + + ticks = 0; + } + + public override void SelectTarget(Actor self, string order, SupportPowerManager manager) + { + Game.Sound.PlayToPlayer(SoundType.UI, manager.Self.Owner, FireArmamentPowerInfo.SelectTargetSound); + self.World.OrderGenerator = new SelectArmamentPowerTarget(self, order, manager, this); + } + + void ITick.Tick(Actor self) + { + if (!enabled) + return; + + if (turreted) + { + foreach (var t in turrets) + { + // HACK HACK HACK HACK + // FireArmamentPower does not set AttackTurreted.IsAiming which means that Turreted.Tick will try to realign against. + // Duplicating the FaceTarget call here ensures that there is a step towards the target direction. + if (!t.FaceTarget(self, target) && !t.FaceTarget(self, target)) + return; + } + } + + foreach (var a in activeArmaments) + a.CheckFire(self, facing, target, true); + + ticks++; + + if (!activeArmaments.Any()) + enabled = false; + } + + void INotifyBurstComplete.FiredBurst(Actor self, in Target target, Armament a) + { + if (activeArmaments.Contains(a)) + self.World.AddFrameEndTask(w => activeArmaments.Remove(a)); + } + + public bool IsActive(Actor self) + { + var activeArmaments = Armaments.Where(x => !x.IsTraitDisabled).ToHashSet(); + + var armamentTurrets = activeArmaments.Select(x => x.Info.Turret).ToHashSet(); + + // TODO: Fix this when upgradable Turreteds arrive. + turrets = self.TraitsImplementing().Where(x => armamentTurrets.Contains(x.Name)).ToHashSet(); + + return activeArmaments.Count > 0 && (!turreted || turrets.Count > 0); + } + + float FractionComplete { get { return ticks * 1f / estimatedTicks; } } + } + + public class SelectArmamentPowerTarget : OrderGenerator + { + readonly Actor self; + readonly SupportPowerManager manager; + readonly string order; + readonly FireArmamentPower power; + + readonly IEnumerable> instances; + + public SelectArmamentPowerTarget(Actor self, string order, SupportPowerManager manager, FireArmamentPower power) + { + // Clear selection if using Left-Click Orders + if (Game.Settings.Game.UseClassicMouseStyle) + manager.Self.World.Selection.Clear(); + + this.self = self; + this.manager = manager; + this.order = order; + this.power = power; + + instances = GetActualInstances(self, power); + } + + static IEnumerable> GetActualInstances(Actor self, FireArmamentPower power) + { + if (!power.Info.AllowMultiple) + { + var actorswithpower = self.World.ActorsWithTrait() + .Where(x => x.Actor.Owner == self.Owner + && x.Trait.FireArmamentPowerInfo.OrderName.Contains(power.FireArmamentPowerInfo.OrderName) + && x.Trait.IsActive(x.Actor)); + foreach (var a in actorswithpower) + { + yield return Tuple.Create(a.Trait, + a.Trait.Armaments.Where(x => !x.IsTraitDisabled).Min(x => x.Weapon.MinRange), + a.Trait.Armaments.Where(x => !x.IsTraitDisabled).Max(x => x.Weapon.Range)); + } + } + else + { + yield return Tuple.Create(power, + power.Armaments.Where(x => !x.IsTraitDisabled).Min(a => a.Weapon.MinRange), + power.Armaments.Where(x => !x.IsTraitDisabled).Max(a => a.Weapon.Range)); + } + + yield break; + } + + protected override IEnumerable OrderInner(World world, CPos xy, int2 worldpixel, MouseInput mi) + { + var pos = world.Map.CenterOfCell(xy); + + world.CancelInputMode(); + if (mi.Button == MouseButton.Left && IsValidTargetCell(xy)) + { + yield return new Order(order, manager.Self, Target.FromCell(world, xy), false) { SuppressVisualFeedback = true }; + + var actors = instances.Where(x => !x.Item1.IsTraitPaused && !x.Item1.IsTraitDisabled + && (x.Item1.Self.CenterPosition - pos).HorizontalLengthSquared < x.Item3.LengthSquared) + .OrderBy(x => (x.Item1.Self.CenterPosition - pos).HorizontalLengthSquared).Select(x => x.Item1.Self).Take(power.FireArmamentPowerInfo.MaximumFiringInstances); + + foreach (var a in actors) + { + yield return new Order(order, a, Target.FromCell(world, xy), false) { SuppressVisualFeedback = true }; + } + } + } + + protected override void Tick(World world) + { + // Cancel the OG if we can't use the power + if (!manager.Powers.ContainsKey(order)) + world.CancelInputMode(); + } + + protected override IEnumerable Render(WorldRenderer wr, World world) { yield break; } + + protected override IEnumerable RenderAboveShroud(WorldRenderer wr, World world) { yield break; } + + protected override IEnumerable RenderAnnotations(WorldRenderer wr, World world) + { + foreach (var i in instances) + { + if (!i.Item1.IsTraitPaused && !i.Item1.IsTraitDisabled) + { + yield return new RangeCircleAnnotationRenderable( + i.Item1.Self.CenterPosition, + i.Item2, + 0, + Color.Red, + 1, + Color.FromArgb(96, Color.Black), + 3); + + yield return new RangeCircleAnnotationRenderable( + i.Item1.Self.CenterPosition, + i.Item3, + 0, + Color.Red, + 1, + Color.FromArgb(96, Color.Black), + 3); + } + } + + if (power.FireArmamentPowerInfo.TargetCircleRange > WDist.Zero) + { + var xy = wr.Viewport.ViewToWorld(Viewport.LastMousePos); + + var targetRangeColor = power.FireArmamentPowerInfo.TargetCircleUsePlayerColor + ? power.Self.Owner.Color : power.FireArmamentPowerInfo.TargetCircleColor; + + yield return new RangeCircleAnnotationRenderable( + world.Map.CenterOfCell(xy), + power.FireArmamentPowerInfo.TargetCircleRange, + 0, + targetRangeColor, + power.FireArmamentPowerInfo.TargetCircleWidth, + power.FireArmamentPowerInfo.TargetCircleBorderColor, + power.FireArmamentPowerInfo.TargetCircleBorderWidth); + } + } + + protected override string GetCursor(World world, CPos cell, int2 worldPixel, MouseInput mi) + { + return IsValidTargetCell(cell) ? power.FireArmamentPowerInfo.Cursor : "generic-blocked"; + } + + bool IsValidTargetCell(CPos xy) + { + if (!self.World.Map.Contains(xy)) + return false; + + var tc = Target.FromCell(self.World, xy); + + return instances.Any(x => !x.Item1.IsTraitPaused && !x.Item1.IsTraitDisabled + && tc.IsInRange(x.Item1.Self.CenterPosition, x.Item3) && !tc.IsInRange(x.Item1.Self.CenterPosition, x.Item2)); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/TeleportNetwork.cs b/OpenRA.Mods.AS/Traits/TeleportNetwork.cs new file mode 100644 index 000000000000..32c48c616e97 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/TeleportNetwork.cs @@ -0,0 +1,94 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + // TO-DO: Create a proper check for Types of TeleportNetwork and TeleportNetworkManager or lint rule. + [Desc("This actor can teleport actors like Nydus canels in SC1. Assuming static object.")] + public class TeleportNetworkInfo : TraitInfo + { + [FieldLoader.Require] + [Desc("Type of TeleportNetwork that pairs up, in order for it to work.")] + public string Type; + + [Desc("Stances requirement that targeted TeleportNetwork has to meet in order to teleport units.")] + public PlayerRelationship ValidRelationships = PlayerRelationship.Ally; + + public override object Create(ActorInitializer init) { return new TeleportNetwork(this); } + } + + // The teleport network canal does nothing. The actor teleports itself, upon entering. + public class TeleportNetwork : INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOwnerChanged + { + public TeleportNetworkInfo Info; + TeleportNetworkManager tnm; + + public TeleportNetwork(TeleportNetworkInfo info) + { + Info = info; + } + + void IncreaseTeleportNetworkCount(Actor self) + { + // Assign itself as primary, when first one. + if (tnm.Count == 0) + { + var pri = self.TraitOrDefault(); + + if (pri == null) + return; + + pri.SetPrimary(self); + } + + tnm.Count++; + } + + void DecreaseTeleportNetworkCount(Actor self) + { + tnm.Count--; + + if (self.IsPrimaryTeleportNetworkExit()) + { + var actors = self.World.ActorsWithTrait() + .Where(a => a.Actor.Owner == self.Owner && a.Actor != self); + + if (!actors.Any()) + tnm.PrimaryActor = null; + else + { + var pri = actors.First().Actor; + pri.Trait().SetPrimary(pri); + } + } + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + tnm = self.Owner.PlayerActor.TraitsImplementing().First(x => x.Type == Info.Type); + IncreaseTeleportNetworkCount(self); + } + + public void OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + DecreaseTeleportNetworkCount(self); + tnm = newOwner.PlayerActor.TraitsImplementing().First(x => x.Type == Info.Type); + IncreaseTeleportNetworkCount(self); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + DecreaseTeleportNetworkCount(self); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/TeleportNetworkPrimaryExit.cs b/OpenRA.Mods.AS/Traits/TeleportNetworkPrimaryExit.cs new file mode 100644 index 000000000000..f1bfd2b4db3c --- /dev/null +++ b/OpenRA.Mods.AS/Traits/TeleportNetworkPrimaryExit.cs @@ -0,0 +1,119 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Orders; +using OpenRA.Traits; + +/* Works without base engine modification */ +namespace OpenRA.Mods.AS.Traits +{ + static class TeleportNetworkPrimaryExitExts + { + public static bool IsValidTeleportNetworkUser(this Actor networkactor, Actor useractor) + { + var trait = networkactor.TraitOrDefault(); + if (trait == null) + return false; + + var exit = networkactor.TraitOrDefault(); + if (exit != null && exit.IsPrimary) + return false; + + return networkactor.Owner.RelationshipWith(useractor.Owner).HasFlag(trait.Info.ValidRelationships); + } + + public static bool IsPrimaryTeleportNetworkExit(this Actor networkactor) + { + var exit = networkactor.TraitOrDefault(); + + if (exit == null) + return false; + + return exit.IsPrimary; + } + } + + [Desc("Used with TeleportNetwork trait for primary exit designation.")] + public class TeleportNetworkPrimaryExitInfo : TraitInfo, Requires + { + [GrantedConditionReference] + [Desc("The condition to grant to self while this is the primary building.")] + public readonly string PrimaryCondition = "primary"; + + [Desc("The speech notification to play when selecting a primary exit.")] + public readonly string SelectionNotification = "PrimaryBuildingSelected"; + + [CursorReference] + [Desc("Cursor to display when setting the primary building.")] + public readonly string Cursor = "deploy"; + + public override object Create(ActorInitializer init) { return new TeleportNetworkPrimaryExit(init.Self, this); } + } + + public class TeleportNetworkPrimaryExit : IIssueOrder, IResolveOrder + { + readonly TeleportNetworkPrimaryExitInfo info; + readonly TeleportNetworkManager manager; + int primaryToken = Actor.InvalidConditionToken; + + public bool IsPrimary { get; private set; } + + public TeleportNetworkPrimaryExit(Actor self, TeleportNetworkPrimaryExitInfo info) + { + this.info = info; + var trait = self.Info.TraitInfoOrDefault(); + manager = self.Owner.PlayerActor.TraitsImplementing().First(x => x.Type == trait.Type); + } + + public IEnumerable Orders + { + get { yield return new DeployOrderTargeter("TeleportNetworkPrimaryExit", 1, () => info.Cursor); } + } + + public Order IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) + { + if (order.OrderID == "TeleportNetworkPrimaryExit") + return new Order(order.OrderID, self, false); + + return null; + } + + public void ResolveOrder(Actor self, Order order) + { + // You can NEVER unselect a primary teleport network building, unlike primary productions buildings in RA1. + if (order.OrderString == "TeleportNetworkPrimaryExit") + SetPrimary(self); + } + + public void RevokePrimary(Actor self) + { + IsPrimary = false; + + if (primaryToken != Actor.InvalidConditionToken) + primaryToken = self.RevokeCondition(primaryToken); + } + + public void SetPrimary(Actor self) + { + IsPrimary = true; + + var pri = manager.PrimaryActor; + if (pri != null && !pri.IsDead) + pri.Trait().RevokePrimary(pri); + + manager.PrimaryActor = self; + + primaryToken = self.GrantCondition(info.PrimaryCondition); + Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", info.SelectionNotification, self.Owner.Faction.InternalName); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/TeleportNetworkTransportable.cs b/OpenRA.Mods.AS/Traits/TeleportNetworkTransportable.cs new file mode 100644 index 000000000000..9693db00982e --- /dev/null +++ b/OpenRA.Mods.AS/Traits/TeleportNetworkTransportable.cs @@ -0,0 +1,141 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common.Orders; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Can move actors instantly to primary designated teleport network canal actor.")] + class TeleportNetworkTransportableInfo : TraitInfo + { + [VoiceReference] + public readonly string Voice = "Action"; + public readonly string EnterCursor = "enter"; + public readonly string EnterBlockedCursor = "enter-blocked"; + public override object Create(ActorInitializer init) { return new TeleportNetworkTransportable(this); } + } + + class TeleportNetworkTransportable : IIssueOrder, IResolveOrder, IOrderVoice + { + readonly TeleportNetworkTransportableInfo info; + + public TeleportNetworkTransportable(TeleportNetworkTransportableInfo info) + { + this.info = info; + } + + public IEnumerable Orders + { + get { yield return new TeleportNetworkTransportOrderTargeter(info); } + } + + public Order IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) + { + if (order.OrderID != "TeleportNetworkTransport") + return null; + + return new Order(order.OrderID, self, target, queued) { }; + } + + // Checks if targeted actor's owner has enough canals (more than 1) of provided type + static bool HasEnoughCanals(Actor targetactor, string type) + { + var counter = targetactor.Owner.PlayerActor.TraitsImplementing().First(x => x.Type == type); + + if (counter == null) + return false; + + return counter.Count > 1; + } + + static bool IsValidOrder(Order order) + { + // Not targeting a frozen actor + if (order.Target.Actor == null) + return false; + + var teleporttrait = order.Target.Actor.TraitOrDefault(); + + if (teleporttrait == null) + return false; + + if (!HasEnoughCanals(order.Target.Actor, teleporttrait.Info.Type)) + return false; + + return !order.Target.Actor.IsPrimaryTeleportNetworkExit(); + } + + public string VoicePhraseForOrder(Actor self, Order order) + { + return order.OrderString == "TeleportNetworkTransport" && IsValidOrder(order) + ? info.Voice : null; + } + + public void ResolveOrder(Actor self, Order order) + { + if (order.OrderString != "TeleportNetworkTransport" || !IsValidOrder(order)) + return; + + if (order.Target.Type != TargetType.Actor) + return; + + var targettrait = order.Target.Actor.TraitOrDefault(); + + if (targettrait == null) + return; + + if (!order.Queued) + self.CancelActivity(); + + self.QueueActivity(new EnterTeleportNetwork(self, order.Target, targettrait.Info.Type)); + } + + class TeleportNetworkTransportOrderTargeter : UnitOrderTargeter + { + readonly TeleportNetworkTransportableInfo info; + + public TeleportNetworkTransportOrderTargeter(TeleportNetworkTransportableInfo info) + : base("TeleportNetworkTransport", 6, info.EnterCursor, true, true) + { + this.info = info; + } + + public override bool CanTargetActor(Actor self, Actor target, TargetModifiers modifiers, ref string cursor) + { + if (modifiers.HasFlag(TargetModifiers.ForceAttack)) + return false; + + // Valid enemy TeleportNetwork entrances should still be offered to be destroyed first. + if (self.Owner.RelationshipWith(target.Owner) == PlayerRelationship.Enemy && !modifiers.HasFlag(TargetModifiers.ForceMove)) + return false; + + var trait = target.TraitOrDefault(); + if (trait == null) + return false; + + if (!target.IsValidTeleportNetworkUser(self)) // block, if primary exit. + cursor = info.EnterBlockedCursor; + + return true; + } + + public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor) + { + // You can't enter frozen actor. + return false; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/TriggersDelayedWeapon.cs b/OpenRA.Mods.AS/Traits/TriggersDelayedWeapon.cs new file mode 100644 index 000000000000..54dc1aedd82e --- /dev/null +++ b/OpenRA.Mods.AS/Traits/TriggersDelayedWeapon.cs @@ -0,0 +1,46 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("When enabled, pre-emptively triggers delayed weapons on the actor.")] + public class TriggersDelayedWeaponInfo : ConditionalTraitInfo, Requires + { + [FieldLoader.Require] + [Desc("Type of DelayedWeapons that can be triggered by this trait.")] + public readonly string Type = "bomb"; + + public override object Create(ActorInitializer init) { return new TriggersDelayedWeapon(init.Self, this); } + } + + public class TriggersDelayedWeapon : ConditionalTrait + { + readonly DelayedWeaponAttachable[] attachables = Array.Empty(); + + public TriggersDelayedWeapon(Actor self, TriggersDelayedWeaponInfo info) + : base(info) + { + attachables = self.TraitsImplementing().Where(dwa => dwa.Info.Type == info.Type).ToArray(); + } + + protected override void TraitEnabled(Actor self) + { + foreach (var attachable in attachables) + foreach (var trigger in attachable.Container) + if (trigger.IsValid) + trigger.Activate(self); + } + } +} diff --git a/OpenRA.Mods.AS/Traits/World/DevOffsetOverlayManager.cs b/OpenRA.Mods.AS/Traits/World/DevOffsetOverlayManager.cs new file mode 100644 index 000000000000..8e092ab6e3fc --- /dev/null +++ b/OpenRA.Mods.AS/Traits/World/DevOffsetOverlayManager.cs @@ -0,0 +1,67 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.Graphics; +using OpenRA.Mods.Common.Commands; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public class DevOffsetOverlayManagerInfo : TraitInfo + { + [Desc("The font used to draw cell vectors. Should match the value as-is in the Fonts section of the mod manifest (do not convert to lowercase).")] + public readonly string Font = "TinyBold"; + + public override object Create(ActorInitializer init) { return new DevOffsetOverlayManager(init.Self); } + } + + public class DevOffsetOverlayManager : IWorldLoaded, IChatCommand + { + const string CommandName = "dev-offset"; + const string CommandHelp = "Commands the DevOffsetOverlay trait. See the trait documentation for controls."; + + readonly Actor self; + + public DevOffsetOverlayManager(Actor self) + { + this.self = self; + } + + void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr) + { + var console = self.TraitOrDefault(); + var help = self.TraitOrDefault(); + + if (console == null || help == null) + return; + + console.RegisterCommand(CommandName, this); + help.RegisterHelp(CommandName, CommandHelp); + } + + void IChatCommand.InvokeCommand(string command, string arg) + { + if (command != CommandName) + return; + + foreach (var actor in self.World.Selection.Actors) + { + if (actor.IsDead) + continue; + + var devOffset = actor.TraitOrDefault(); + if (devOffset == null) + continue; + + devOffset.ParseCommand(actor, arg); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/World/LobbySystemActorConditionCheckbox.cs b/OpenRA.Mods.AS/Traits/World/LobbySystemActorConditionCheckbox.cs new file mode 100644 index 000000000000..5da26e48f145 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/World/LobbySystemActorConditionCheckbox.cs @@ -0,0 +1,95 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [TraitLocation(SystemActors.World)] + [Desc("Enables a condition on the world or player actors if the checkbox is enabled.")] + public class LobbySystemActorConditionCheckboxInfo : TraitInfo, ILobbyOptions + { + [FieldLoader.Require] + [Desc("Internal id for this checkbox.")] + public readonly string ID = null; + + [FieldLoader.Require] + [TranslationReference] + [Desc("Display name for this checkbox.")] + public readonly string Label = null; + + [TranslationReference] + [Desc("Description name for this checkbox.")] + public readonly string Description = null; + + [Desc("Default value of the checkbox in the lobby.")] + public readonly bool Enabled = false; + + [Desc("Prevent the checkbox from being changed from its default value.")] + public readonly bool Locked = false; + + [Desc("Display the checkbox in the lobby.")] + public readonly bool Visible = true; + + [Desc("Display order for the checkbox in the lobby.")] + public readonly int DisplayOrder = 0; + + [Desc("System actors to grant condition to. Only supports: World, Player")] + public readonly SystemActors Actors = SystemActors.World; + + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant when this checkbox is enabled.")] + public readonly string Condition = ""; + + IEnumerable ILobbyOptions.LobbyOptions(MapPreview map) + { + yield return new LobbyBooleanOption(map, ID, Label, Description, + Visible, DisplayOrder, Enabled, Locked); + } + + public override object Create(ActorInitializer init) { return new LobbySystemActorConditionCheckbox(this); } + } + + public class LobbySystemActorConditionCheckbox : INotifyCreated, ITick + { + readonly LobbySystemActorConditionCheckboxInfo info; + bool grantToPlayer; + + public LobbySystemActorConditionCheckbox(LobbySystemActorConditionCheckboxInfo info) + { + this.info = info; + grantToPlayer = info.Actors.HasFlag(SystemActors.Player); + } + + void INotifyCreated.Created(Actor self) + { + var enabled = self.World.LobbyInfo.GlobalSettings.OptionOrDefault(info.ID, info.Enabled); + + if (info.Actors.HasFlag(SystemActors.World) && enabled) + self.GrantCondition(info.Condition); + + grantToPlayer &= enabled; + } + + void ITick.Tick(Actor self) + { + // World actor is created before Player actors, so this doesn't work in Created. + if (grantToPlayer) + { + foreach (var player in self.World.Players) + player.PlayerActor.GrantCondition(info.Condition); + + grantToPlayer = false; + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/World/ResourceTwinkleLayer.cs b/OpenRA.Mods.AS/Traits/World/ResourceTwinkleLayer.cs new file mode 100644 index 000000000000..445670fbdd52 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/World/ResourceTwinkleLayer.cs @@ -0,0 +1,100 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("Allows to play twinkle animations on resources.", "Attach this to the world actor.")] + public class ResourceTwinkleLayerInfo : TraitInfo + { + [FieldLoader.Require] + [Desc("Resource types to twinkle.")] + public readonly HashSet Types = null; + + [Desc("The percentage of resource cells to play the twinkle animation on.", "Use two values to randomize between them.")] + public readonly int[] Ratio = { 5 }; + + [Desc("Tick interval between two twinkle animation spawning.", "Use two values to randomize between them.")] + public readonly int[] Interval = { 50 }; + + [FieldLoader.Require] + [Desc("Twinkle animation image.")] + public readonly string Image = null; + + [SequenceReference(nameof(Image))] + [Desc("Twinkle animation sequences.")] + public readonly string[] Sequences = new string[] { "idle" }; + + [PaletteReference] + [Desc("Twinkle animation palette.")] + public readonly string Palette = null; + + public override object Create(ActorInitializer init) { return new ResourceTwinkleLayer(init.Self, this); } + } + + class ResourceTwinkleLayer : ITick, IResourceLogicLayer + { + readonly ResourceTwinkleLayerInfo info; + + readonly World world; + readonly HashSet cells; + + int ticks; + + public ResourceTwinkleLayer(Actor self, ResourceTwinkleLayerInfo info) + { + world = self.World; + this.info = info; + cells = new HashSet(); + + ticks = info.Interval.Length == 2 + ? world.SharedRandom.Next(info.Interval[0], info.Interval[1]) + : info.Interval[0]; + } + + void ITick.Tick(Actor self) + { + if (--ticks > 0) + return; + + var twinkleable = cells.Shuffle(world.SharedRandom); + var ratio = info.Ratio.Length == 2 + ? world.SharedRandom.Next(info.Ratio[0], info.Ratio[1]) + : info.Ratio[0]; + + var twinkamount = twinkleable.Count() * ratio / 100; + var twinkpositions = twinkleable.Take(twinkamount).Select(x => world.Map.CenterOfCell(x)); + + foreach (var pos in twinkpositions) + world.AddFrameEndTask(w => w.Add(new SpriteEffect(pos, w, info.Image, info.Sequences.Random(w.SharedRandom), info.Palette))); + + ticks = info.Interval.Length == 2 + ? world.SharedRandom.Next(info.Interval[0], info.Interval[1]) + : info.Interval[0]; + } + + void IResourceLogicLayer.UpdatePosition(CPos cell, string resourceType, int density) + { + if (info.Types.Contains(resourceType)) + { + if (density == 0) + cells.Remove(cell); + else + cells.Add(cell); + } + } + } +} diff --git a/OpenRA.Mods.AS/Traits/World/TintedCellsLayer.cs b/OpenRA.Mods.AS/Traits/World/TintedCellsLayer.cs new file mode 100644 index 000000000000..c6a799fe4f49 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/World/TintedCellsLayer.cs @@ -0,0 +1,208 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Graphics; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + public enum FadeoutType + { + Linear, + Logarithmic + } + + [Desc("Has to be attached to world actor. ")] + public class TintedCellsLayerInfo : TraitInfo + { + [Desc("Color of cells")] + public readonly Color Color = Color.FromArgb(0, 255, 0); + + [Desc("Maximum radiation allowable in a cell.The cell can actually have more radiation but it will only damage as if it had the maximum level.")] + public readonly int MaxLevel = 500; + + [Desc("Delay in ticks between level decrements. The level updates this often, although the whole lifetime will be as defined by half-life.")] + public readonly int UpdateDelay = 15; + + [Desc("The alpha value for displaying level for cells with level == 1")] + public readonly int Darkest = 4; + + [Desc("The alpha value for displaying level for cells with level == MaxLevel")] + public readonly int Brightest = 64; + + [Desc("Delay of half life, in ticks")] + public readonly int FadeoutDelay = 150; + + [Desc("Z offset of the visualization.")] + public readonly int ZOffset = 512; + + [Desc("Name of the layer.")] + public readonly string Name = "radioactivity"; + + [Desc("How shall level decay, can be Linear or Logarithmic.")] + public readonly FadeoutType FadeoutType = FadeoutType.Logarithmic; + + public override object Create(ActorInitializer init) { return new TintedCellsLayer(init.Self, this); } + } + + public class TintedCellsLayer : INotifyActorDisposing, ITick, ITickRender + { + readonly World world; + public readonly TintedCellsLayerInfo Info; + + // In the following, I think dictionary is better than array, as radioactivity has similar affecting area as smudges. + + // Tiles without considering fog of war. + readonly Dictionary tiles = new(); + + // What's visible to the player. + readonly Dictionary renderedTiles = new(); + + // Dirty, as in cache dirty bits. + readonly HashSet dirty = new(); + + // There's LERP function but the problem is, it is better to reuse these constants than computing + // related constants (in LERP) every time. + // half life constant, to be computed at init. + public readonly int FalloutScale; + public readonly int TintLevel; + + public TintedCellsLayer(Actor self, TintedCellsLayerInfo info) + { + world = self.World; + Info = info; + + switch (info.FadeoutType) + { + case FadeoutType.Linear: + FalloutScale = info.UpdateDelay * 500 / info.FadeoutDelay; + break; + case FadeoutType.Logarithmic: + // (693 is 1000*ln(2) so we must divide by 1000 later on.) + FalloutScale = info.UpdateDelay * 693 / info.FadeoutDelay; + break; + } + + TintLevel = 255 * (info.Brightest - info.Darkest) / (info.MaxLevel - 1); + } + + void ITick.Tick(Actor self) + { + var remove = new List(); + + // Apply half life to each cell. + foreach (var kv in tiles) + { + if (!Decay(kv.Value, Info.UpdateDelay)) + continue; + + if (kv.Value.Level <= 0) + remove.Add(kv.Key); + + dirty.Add(kv.Key); + } + + foreach (var r in remove) + tiles.Remove(r); + } + + void ITickRender.TickRender(WorldRenderer wr, Actor self) + { + var remove = new List(); + foreach (var c in dirty) + { + if (self.World.FogObscures(c)) + continue; + + if (renderedTiles.TryGetValue(c, out var renderedTile)) + { + world.Remove(renderedTile); + renderedTiles.Remove(c); + } + + // synchronize observations with true value. + if (tiles.TryGetValue(c, out var tile)) + { + renderedTiles[c] = new TintedCell(tile); + world.Add(renderedTiles[c]); + } + + remove.Add(c); + } + + foreach (var r in remove) + dirty.Remove(r); + } + + public int GetLevel(CPos cell) + { + if (!tiles.TryGetValue(cell, out var tile)) + return 0; + + // The damage is constrained by MaxLevel + var level = tile.Level; + if (level > Info.MaxLevel) + return Info.MaxLevel; + else + return level; + } + + public void IncreaseLevel(CPos cell, int level, int max_level) + { + // Initialize, on fresh impact. + if (!tiles.ContainsKey(cell)) + tiles[cell] = new TintedCell(this, cell, world.Map.CenterOfCell(cell)); + + tiles[cell].Ticks = Info.UpdateDelay; + var new_level = tiles[cell].Level + level; + if (new_level > max_level) + new_level = max_level; + + // the given weapon can't saturate the cell anymore. + if (tiles[cell].Level > new_level) + return; + + tiles[cell].SetLevel(new_level); + + dirty.Add(cell); + } + + // Returns true when it decays. + public bool Decay(TintedCell tc, int updateDelay) + { + tc.Ticks--; + if (tc.Ticks > 0) + return false; + + tc.Ticks = updateDelay; + + var dlevel = FalloutScale * tc.Level / 1000; + + // has to be decreased by at least 1 so that it disappears eventually. + if (dlevel < 1) + dlevel = 1; + + tc.SetLevel(tc.Level - dlevel); + return true; + } + + bool disposed = false; + void INotifyActorDisposing.Disposing(Actor self) + { + if (disposed) + return; + + disposed = true; + } + } +} diff --git a/OpenRA.Mods.Common/Traits/PaletteEffects/GlobalLightingPaletteEffect.cs b/OpenRA.Mods.AS/Traits/World/WeaponStorm.cs similarity index 50% rename from OpenRA.Mods.Common/Traits/PaletteEffects/GlobalLightingPaletteEffect.cs rename to OpenRA.Mods.AS/Traits/World/WeaponStorm.cs index fc9831d038cb..07c7a3882956 100644 --- a/OpenRA.Mods.Common/Traits/PaletteEffects/GlobalLightingPaletteEffect.cs +++ b/OpenRA.Mods.AS/Traits/World/WeaponStorm.cs @@ -1,25 +1,30 @@ #region Copyright & License Information /* - * Copyright (c) The OpenRA Developers and Contributors - * This file is part of OpenRA, which is free software. It is made - * available to you under the terms of the GNU General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. For more - * information, see COPYING. + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. */ #endregion using System.Collections.Generic; using System.Linq; +using OpenRA.GameRules; using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits; using OpenRA.Traits; -namespace OpenRA.Mods.Common.Traits +namespace OpenRA.Mods.AS.Traits { - [Desc("Used for day/night effects.")] - [TraitLocation(SystemActors.World | SystemActors.EditorWorld)] - public class GlobalLightingPaletteEffectInfo : TraitInfo, ILobbyCustomRulesIgnore + [Desc("Create a map-wide weapon storm.")] + class WeaponStormInfo : ConditionalTraitInfo, IRulesetLoaded { + [WeaponReference] + [FieldLoader.Require] + [Desc("Has to be defined in weapons.yaml as well.")] + public readonly string Weapon = null; + [Desc("Do not modify graphics that use any palette in this list.")] public readonly HashSet ExcludePalettes = new() { "cursor", "chrome", "colorpicker", "fog", "shroud", "alpha" }; @@ -31,35 +36,84 @@ public class GlobalLightingPaletteEffectInfo : TraitInfo, ILobbyCustomRulesIgnor public readonly float Blue = 1f; public readonly float Ambient = 1f; - public override object Create(ActorInitializer init) { return new GlobalLightingPaletteEffect(this); } + [Desc("How many weapons should be fired per 1000 map cells (on average).")] + public readonly int[] Density = { 1 }; + + public readonly WDist Altitude = WDist.Zero; + + [Desc("Should this storm be associated with an enemy (the Owner player)?")] + public readonly bool Enemy = true; + + public readonly string Owner = "Creeps"; + + public WeaponInfo WeaponInfo { get; private set; } + + void IRulesetLoaded.RulesetLoaded(Ruleset rules, ActorInfo info) + { + var weaponToLower = Weapon.ToLowerInvariant(); + if (!rules.Weapons.TryGetValue(weaponToLower, out var weaponInfo)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{weaponToLower}'"); + + WeaponInfo = weaponInfo; + } + + public override object Create(ActorInitializer init) { return new WeaponStorm(this); } } - public class GlobalLightingPaletteEffect : IPaletteModifier + class WeaponStorm : ConditionalTrait, IPaletteModifier, ISync, ITick, IWorldLoaded { - readonly GlobalLightingPaletteEffectInfo info; + readonly WeaponStormInfo info; - public float Red; - public float Green; - public float Blue; - public float Ambient; + readonly uint ar, ag, ab; - public GlobalLightingPaletteEffect(GlobalLightingPaletteEffectInfo info) + World world; + int mapsize; + + public WeaponStorm(WeaponStormInfo info) + : base(info) { this.info = info; - Red = info.Red; - Green = info.Green; - Blue = info.Blue; - Ambient = info.Ambient; + // Calculate ambient color multipliers as integers for speed. To handle fractional ambiance, we'll increase + // the magnitude of the result by 8 bits. + ar = (uint)((1 << 8) * info.Ambient * info.Red); + ag = (uint)((1 << 8) * info.Ambient * info.Green); + ab = (uint)((1 << 8) * info.Ambient * info.Blue); + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled) + return; + + var density = info.Density.Length == 2 + ? world.SharedRandom.Next(info.Density[0], info.Density[1]) + : info.Density[0]; + + var weapons = mapsize * density / 1000; + var firer = info.Enemy ? world.Players.First(x => x.PlayerName == info.Owner).PlayerActor : world.WorldActor; + + for (var i = 0; i < weapons; i++) + { + var tpos = world.Map.CenterOfCell(world.Map.ChooseRandomCell(world.SharedRandom)) + + new WVec(WDist.Zero, WDist.Zero, info.Altitude); + + var args = new WarheadArgs + { + Weapon = info.WeaponInfo, + Source = tpos, + SourceActor = firer, + WeaponTarget = Target.FromPos(tpos) + }; + + info.WeaponInfo.Impact(Target.FromPos(tpos), args); + } } public void AdjustPalette(IReadOnlyDictionary palettes) { - // Calculate ambient color multipliers as integers for speed. To handle fractional ambiance, we'll increase - // the magnitude of the result by 8 bits. - var ar = (uint)((1 << 8) * Ambient * Red); - var ag = (uint)((1 << 8) * Ambient * Green); - var ab = (uint)((1 << 8) * Ambient * Blue); + if (IsTraitDisabled) + return; foreach (var kvp in palettes) { @@ -107,5 +161,12 @@ public void AdjustPalette(IReadOnlyDictionary palettes) } } } + + void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr) + { + world = w; + + mapsize = world.Map.MapSize.X * world.Map.MapSize.Y; + } } } diff --git a/OpenRA.Mods.AS/Traits/World/WeaponTriggerCells.cs b/OpenRA.Mods.AS/Traits/World/WeaponTriggerCells.cs new file mode 100644 index 000000000000..4a05c73867bf --- /dev/null +++ b/OpenRA.Mods.AS/Traits/World/WeaponTriggerCells.cs @@ -0,0 +1,150 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Effects; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("A layer that support weapon like Inferno Cannon like in CnC: Generals, used by TA")] + [TraitLocation(SystemActors.World | SystemActors.EditorWorld)] + public sealed class WeaponTriggerCellsInfo : TraitInfo + { + [Desc("Name of the layer type")] + public readonly string Name = ""; + + [Desc("Speed of restore per tick to 0")] + public readonly int RestorePerTick = 4; + + [Desc("Show debug overlay of this WeaponTriggerCells")] + public readonly bool ShowDebugOverlay = false; + + public override object Create(ActorInitializer init) { return new WeaponTriggerCells(init.Self, this); } + } + + sealed class TriggerCell + { + public int Level; + } + + public sealed class WeaponTriggerCells : INotifyActorDisposing, ITick, ITickRender + { + readonly World world; + public readonly WeaponTriggerCellsInfo Info; + + readonly Dictionary tiles = new(); + + public WeaponTriggerCells(Actor self, WeaponTriggerCellsInfo info) + { + world = self.World; + Info = info; + } + + void ITick.Tick(Actor self) + { + var remove = new List(); + + // Apply half life to each cell. + foreach (var kv in tiles) + { + // has to be decreased by at least 1 so that it disappears eventually. + var level = kv.Value.Level; + if (level > 0) + { + if ((level -= Info.RestorePerTick) <= 0) + remove.Add(kv.Key); + else + kv.Value.Level = level; + } + else + { + if ((level += Info.RestorePerTick) >= 0) + remove.Add(kv.Key); + else + kv.Value.Level = level; + } + } + + foreach (var r in remove) + tiles.Remove(r); + } + + public int GetLevel(CPos cell) + { + if (!tiles.ContainsKey(cell)) + return 0; + + return tiles[cell].Level; + } + + public void SetLevel(CPos cell, int level) + { + if (!tiles.ContainsKey(cell)) + return; + + tiles[cell].Level = level; + } + + public void IncreaseLevel(CPos cell, int add_level, int max_level) + { + if (add_level == 0) + return; + + var currentLevel = 0; + + // Initialize, on fresh impact. + if (tiles.ContainsKey(cell)) + currentLevel = tiles[cell].Level; + + // the given weapon can't saturate the cell anymore. + if ((add_level > 0 && currentLevel >= max_level) || + (add_level < 0 && currentLevel <= max_level)) + return; + + var new_level = currentLevel + add_level; + + if ((add_level > 0 && new_level > max_level) || + (add_level < 0 && new_level < max_level)) + return; + + currentLevel = new_level; + + if (!tiles.ContainsKey(cell)) + tiles[cell] = new TriggerCell() { Level = currentLevel }; + else + tiles[cell].Level = currentLevel; + } + + bool disposed = false; + void INotifyActorDisposing.Disposing(Actor self) + { + if (disposed) + return; + + disposed = true; + } + + // Debug only, require enabling the `ITickRender` interface in this class + void ITickRender.TickRender(WorldRenderer wr, Actor self) + { + if (Info.ShowDebugOverlay) + { + foreach (var kv in tiles) + { + var i = new FloatingText(world.Map.CenterOfCell(kv.Key), Color.Gold, kv.Value.Level.ToStringInvariant(), 1); + world.Add(i); + } + } + } + } +} diff --git a/OpenRA.Mods.AS/TraitsInterfaces.cs b/OpenRA.Mods.AS/TraitsInterfaces.cs new file mode 100644 index 000000000000..634ecb7b0148 --- /dev/null +++ b/OpenRA.Mods.AS/TraitsInterfaces.cs @@ -0,0 +1,59 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.GameRules; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [RequireExplicitImplementation] + public interface ISmokeParticleInfo + { + string Image { get; } + string[] StartSequences { get; } + string[] Sequences { get; } + string[] EndSequences { get; } + string Palette { get; } + + bool IsPlayerPalette { get; } + + int[] Duration { get; } + + WDist[] Speed { get; } + + WDist[] Gravity { get; } + + WeaponInfo Weapon { get; } + + int TurnRate { get; } + + int RandomRate { get; } + } + + [RequireExplicitImplementation] + public interface INotifyEnteredGarrison { void OnEnteredGarrison(Actor self, Actor garrison); } + + [RequireExplicitImplementation] + public interface INotifyExitedGarrison { void OnExitedGarrison(Actor self, Actor garrison); } + + [RequireExplicitImplementation] + public interface INotifyGarrisonerEntered { void OnGarrisonerEntered(Actor self, Actor garrisoner); } + + [RequireExplicitImplementation] + public interface INotifyGarrisonerExited { void OnGarrisonerExited(Actor self, Actor garrisoner); } + + [RequireExplicitImplementation] + public interface INotifyEnteredSharedCargo { void OnEnteredSharedCargo(Actor self, Actor cargo); } + + [RequireExplicitImplementation] + public interface INotifyExitedSharedCargo { void OnExitedSharedCargo(Actor self, Actor cargo); } + + public interface INotifyPrismCharging { void Charging(Actor self, in Target target); } +} diff --git a/OpenRA.Mods.AS/UtilityCommands/ExtractLegacyRulesValues.cs b/OpenRA.Mods.AS/UtilityCommands/ExtractLegacyRulesValues.cs new file mode 100644 index 000000000000..cd905212b835 --- /dev/null +++ b/OpenRA.Mods.AS/UtilityCommands/ExtractLegacyRulesValues.cs @@ -0,0 +1,85 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OpenRA.Mods.Common.FileFormats; + +namespace OpenRA.Mods.AS.UtilityCommands +{ + class ExtractLegacyRulesValues : IUtilityCommand + { + bool IUtilityCommand.ValidateArguments(string[] args) + { + return args.Length >= 3; + } + + string IUtilityCommand.Name { get { return "--extract-from-ini"; } } + + IniFile rulesIni; + string[] tags; + + [Desc("RULES.INI", "Rule tags", "Extract listed tags from a TS/RA2 INI to a YAML format.")] + void IUtilityCommand.Run(Utility utility, string[] args) + { + // HACK: The engine code assumes that Game.modData is set. + Game.ModData = utility.ModData; + + rulesIni = new IniFile(File.Open(args[1], FileMode.Open)); + tags = args[2].Split(','); + + var technoTypes = rulesIni.GetSection("BuildingTypes").Select(b => b.Value).Distinct(); + Console.WriteLine("# Buildings"); + Console.WriteLine(); + ImportValues(technoTypes); + + technoTypes = rulesIni.GetSection("InfantryTypes").Select(b => b.Value).Distinct(); + Console.WriteLine("# Infantry"); + Console.WriteLine(); + ImportValues(technoTypes); + + technoTypes = rulesIni.GetSection("VehicleTypes").Select(b => b.Value).Distinct(); + Console.WriteLine("# Vehicles"); + Console.WriteLine(); + ImportValues(technoTypes); + + technoTypes = rulesIni.GetSection("AircraftTypes").Select(b => b.Value).Distinct(); + Console.WriteLine("# Aircraft"); + Console.WriteLine(); + ImportValues(technoTypes); + } + + void ImportValues(IEnumerable technoTypes) + { + foreach (var technoType in technoTypes) + { + var rulesSection = rulesIni.GetSection(technoType, allowFail: true); + if (rulesSection == null) + continue; + + Console.WriteLine(rulesSection.Name + ":"); + + foreach (var tag in tags) + { + var results = rulesSection.Where(x => x.Key.StartsWith(tag, StringComparison.Ordinal)); + foreach (var result in results) + { + if (!string.IsNullOrEmpty(result.Key)) + Console.WriteLine("\t" + result.Key + ": " + result.Value); + } + } + + Console.WriteLine(); + } + } + } +} diff --git a/OpenRA.Mods.AS/UtilityCommands/ImportASMapCommand.cs b/OpenRA.Mods.AS/UtilityCommands/ImportASMapCommand.cs new file mode 100644 index 000000000000..fed53189982a --- /dev/null +++ b/OpenRA.Mods.AS/UtilityCommands/ImportASMapCommand.cs @@ -0,0 +1,302 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Mods.Cnc.UtilityCommands; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.UtilityCommands +{ + class ImportTSMapCommand : ImportGen2MapCommand, IUtilityCommand + { + string IUtilityCommand.Name { get { return "--import-as-map"; } } + bool IUtilityCommand.ValidateArguments(string[] args) { return args.Length >= 2; } + + [Desc("FILENAME", "AUTHOR (optional, defaults to Westwood Studios)", "Convert an Attacque Supérior/Valiant Shades map to the OpenRA format.")] + void IUtilityCommand.Run(Utility utility, string[] args) + { + Run(utility, args); + } + + #region Mod-specific data + + protected override Dictionary OverlayToActor { get; } = new() + { + { 0x00, "gasand" }, + { 0x02, "gawall" }, + /* + { 0x18, "bridge1" }, + { 0x19, "bridge2" }, + */ + { 0x1A, "nawall" }, + /* + { 0x27, "tracks01" }, + { 0x28, "tracks02" }, + { 0x29, "tracks03" }, + { 0x2A, "tracks04" }, + { 0x2B, "tracks05" }, + { 0x2C, "tracks06" }, + { 0x2D, "tracks07" }, + { 0x2E, "tracks08" }, + { 0x2F, "tracks09" }, + { 0x30, "tracks10" }, + { 0x31, "tracks11" }, + { 0x32, "tracks12" }, + { 0x33, "tracks13" }, + { 0x34, "tracks14" }, + { 0x35, "tracks15" }, + { 0x36, "tracks16" }, + { 0x37, "tracktunnel01" }, + { 0x38, "tracktunnel02" }, + { 0x39, "tracktunnel03" }, + { 0x3A, "tracktunnel04" }, + */ + /* + { 0x3B, "railbrdg1" }, + { 0x3C, "railbrdg2" }, + */ + /* + { 0x3D, "crat01" }, + { 0x3E, "crat02" }, + { 0x3F, "crat03" }, + { 0x40, "crat04" }, + { 0x41, "crat0A" }, + { 0x42, "crat0B" }, + { 0x43, "crat0C" }, + { 0x44, "drum01" }, + { 0x45, "drum02" }, + { 0x46, "palet01" }, + { 0x47, "palet02" }, + { 0x48, "palet03" }, + { 0x49, "palet04" }, + */ + + /* + { 0x4A, "lobrdg_b" }, // lobrdg01 + { 0x4B, "lobrdg_b" }, // lobrdg02 + { 0x4C, "lobrdg_b" }, // lobrdg03 + { 0x4D, "lobrdg_b" }, // lobrdg04 + { 0x4E, "lobrdg_b" }, // lobrdg05 + { 0x4F, "lobrdg_b" }, // lobrdg06 + { 0x50, "lobrdg_b" }, // lobrdg07 + { 0x51, "lobrdg_b" }, // lobrdg08 + { 0x52, "lobrdg_b" }, // lobrdg09 + { 0x53, "lobrdg_a" }, // lobrdg10 + { 0x54, "lobrdg_a" }, // lobrdg11 + { 0x55, "lobrdg_a" }, // lobrdg12 + { 0x56, "lobrdg_a" }, // lobrdg13 + { 0x57, "lobrdg_a" }, // lobrdg14 + { 0x58, "lobrdg_a" }, // lobrdg15 + { 0x59, "lobrdg_a" }, // lobrdg16 + { 0x5A, "lobrdg_a" }, // lobrdg17 + { 0x5B, "lobrdg_a" }, // lobrdg18 + { 0x5C, "lobrdg_r_se" }, // lobrdg19 + { 0x5D, "lobrdg_r_se" }, // lobrdg20 + { 0x5E, "lobrdg_r_nw" }, // lobrdg21 + { 0x5F, "lobrdg_r_nw" }, // lobrdg22 + { 0x60, "lobrdg_r_ne" }, // lobrdg23 + { 0x61, "lobrdg_r_ne" }, // lobrdg24 + { 0x62, "lobrdg_r_sw" }, // lobrdg25 + { 0x63, "lobrdg_r_sw" }, // lobrdg26 + { 0x64, "lobrdg_b_d" }, // lobrdg27 + { 0x65, "lobrdg_a_d" }, // lobrdg28 + + { 0x7A, "lobrdg_r_se" }, // lobrdg1 + { 0x7B, "lobrdg_r_nw" }, // lobrdg2 + { 0x7C, "lobrdg_r_ne" }, // lobrdg3 + { 0x7D, "lobrdg_r_sw" }, // lobrdg4 + */ + + // Terrain objects + { 0xA8, "srock01" }, + { 0xA9, "srock02" }, + { 0xAA, "srock03" }, + { 0xAB, "srock04" }, + { 0xAC, "srock05" }, + { 0xAD, "trock01" }, + { 0xAE, "trock02" }, + { 0xAF, "trock03" }, + { 0xB0, "trock04" }, + { 0xB1, "trock05" }, + { 0xB3, "crate" }, + { 0xF2, "crate" }, // wcrate (water crate) + + // YR terrain objects + { 0xF4, "lunrk1" }, + { 0xF5, "lunrk2" }, + { 0xF6, "lunrk3" }, + { 0xF7, "lunrk4" }, + { 0xF8, "lunrk5" }, + { 0xF9, "lunrk6" }, + { 0xCB, "cafncb" }, + { 0xCC, "cafncw" }, + { 0xF1, "cafncp" }, + { 0xF0, "cakrmw" }, + { 0xF3, "gafwll" } + + // AS additions + // { 0xF4, "yawall" }, + // { 0xFB, "pawall" }, + // { 0xFC, "bawall" }, + // { 0xCB, "fawall" } + }; + + protected override Dictionary OverlayShapes { get; } = new() + { + { 0x4A, new Size(1, 3) }, + { 0x4B, new Size(1, 3) }, + { 0x4C, new Size(1, 3) }, + { 0x4D, new Size(1, 3) }, + { 0x4E, new Size(1, 3) }, + { 0x4F, new Size(1, 3) }, + { 0x50, new Size(1, 3) }, + { 0x51, new Size(1, 3) }, + { 0x52, new Size(1, 3) }, + { 0x53, new Size(3, 1) }, + { 0x54, new Size(3, 1) }, + { 0x55, new Size(3, 1) }, + { 0x56, new Size(3, 1) }, + { 0x57, new Size(3, 1) }, + { 0x58, new Size(3, 1) }, + { 0x59, new Size(3, 1) }, + { 0x5A, new Size(3, 1) }, + { 0x5B, new Size(3, 1) }, + { 0x5C, new Size(1, 3) }, + { 0x5D, new Size(1, 3) }, + { 0x5E, new Size(1, 3) }, + { 0x5F, new Size(1, 3) }, + { 0x60, new Size(3, 1) }, + { 0x61, new Size(3, 1) }, + { 0x62, new Size(3, 1) }, + { 0x63, new Size(3, 1) }, + { 0x64, new Size(1, 3) }, + { 0x65, new Size(3, 1) }, + { 0x7A, new Size(1, 3) }, + { 0x7B, new Size(1, 3) }, + { 0x7C, new Size(3, 1) }, + { 0x7D, new Size(3, 1) }, + }; + + protected override Dictionary OverlayToHealth { get; } = new() + { + // 1,3 bridge tiles + { 0x4A, DamageState.Undamaged }, + { 0x4B, DamageState.Undamaged }, + { 0x4C, DamageState.Undamaged }, + { 0x4D, DamageState.Undamaged }, + { 0x4E, DamageState.Heavy }, + { 0x4F, DamageState.Heavy }, + { 0x50, DamageState.Heavy }, + { 0x51, DamageState.Critical }, + { 0x52, DamageState.Critical }, + + // 3,1 bridge tiles + { 0x53, DamageState.Undamaged }, + { 0x54, DamageState.Undamaged }, + { 0x55, DamageState.Undamaged }, + { 0x56, DamageState.Undamaged }, + { 0x57, DamageState.Heavy }, + { 0x58, DamageState.Heavy }, + { 0x59, DamageState.Heavy }, + { 0x5A, DamageState.Critical }, + { 0x5B, DamageState.Critical }, + + // Ramps + { 0x5C, DamageState.Undamaged }, + { 0x5D, DamageState.Heavy }, + { 0x5E, DamageState.Undamaged }, + { 0x5F, DamageState.Heavy }, + { 0x60, DamageState.Undamaged }, + { 0x61, DamageState.Heavy }, + { 0x62, DamageState.Undamaged }, + { 0x63, DamageState.Heavy }, + + // Ramp duplicates + { 0x7A, DamageState.Undamaged }, + { 0x7B, DamageState.Undamaged }, + { 0x7C, DamageState.Undamaged }, + { 0x7D, DamageState.Undamaged }, + + // actually dead, placeholders for resurrection + { 0x64, DamageState.Undamaged }, + { 0x65, DamageState.Undamaged }, + }; + + protected override Dictionary ResourceFromOverlay { get; } = new() + { + // "tib" - Regular Tiberium + { + 0x01, new byte[] + { + 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, + + // Should be "tib2" - RA2 mappers were stupid and used third ore as first on maps, yay + 0x7F, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, + 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, + } + }, + + // "btib" - Blue Tiberium + { + 0x02, new byte[] + { + 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, + + // Should be "tib3" + 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, + 0x9D, 0x9E, 0x9F, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6 + } + } + + // Veins + // { 0x03, new byte[] { 0x7E } } + }; + + protected override Dictionary DeployableActors { get; } = new() + { + // { "gadpsa", "lpst" }, + // { "gatick", "ttnk" }, + // { "gaarty", "art2" }, + // { "djugg", "jugg" }, + + // Not yet implemented actors: + // { "gaicbm", "icbm" }, + // { "dlimpet", "limpet" }, + // { "dgweap", "mobwarg" }, + // { "dnweap", "mobwarn" }, + // { "mstl", "sgen" }, + // { "ddefd", "defender" }, + }; + + protected override Dictionary ReplaceActors { get; } = new() + { + { "tibtre02", "tibtre01" }, + { "tibtre03", "tibtre01" }, + { "amradr", "gaairc" }, + { "wwlf", "mumy" }, + { "adog", "dog" }, + { "catech01", "capad" } + }; + + protected override string[] LampActors { get; } = + { + "GALITE", "INGALITE", "NEGLAMP", "REDLAMP", "NEGRED", "GRENLAMP", "BLUELAMP", "YELWLAMP", + "INYELWLAMP", "PURPLAMP", "INPURPLAMP", "INORANLAMP", "INGRNLMP", "INREDLMP", "INBLULMP" + }; + + protected override string[] CreepActors { get; } = Array.Empty(); + + #endregion + } +} diff --git a/OpenRA.Mods.AS/Warheads/AttachDelayedWeaponWarhead.cs b/OpenRA.Mods.AS/Warheads/AttachDelayedWeaponWarhead.cs new file mode 100644 index 000000000000..cca1fadabb0e --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/AttachDelayedWeaponWarhead.cs @@ -0,0 +1,83 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("This warhead can attach a DelayedWeapon to the target. Requires an appropriate type of DelayedWeaponAttachable trait to function properly.")] + public class AttachDelayedWeaponWarhead : WarheadAS, IRulesetLoaded + { + [WeaponReference] + [FieldLoader.Require] + public readonly string Weapon = ""; + + [FieldLoader.Require] + [Desc("Type of the DelayedWeapon.")] + public readonly string Type = ""; + + [Desc("Range of targets to be attached.")] + public readonly WDist Range = WDist.FromCells(1); + + [Desc("Trigger the DelayedWeapon after these amount of ticks. Use -1 to not trigger by time.")] + public readonly int TriggerTime = 30; + + [Desc("DeathType(s) that trigger the DelayedWeapon to activate. Leave empty to always trigger the DelayedWeapon on death.")] + public readonly BitSet DeathTypes = default; + + public WeaponInfo WeaponInfo; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out WeaponInfo)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var pos = target.CenterPosition; + + if (!IsValidImpact(pos, firedBy)) + return; + + var availableActors = firedBy.World.FindActorsOnCircle(pos, Range); + foreach (var actor in availableActors) + { + if (!IsValidAgainst(actor, firedBy)) + continue; + + if (actor.IsDead) + continue; + + var activeShapes = actor.TraitsImplementing().Where(Exts.IsTraitEnabled); + if (!activeShapes.Any()) + continue; + + var distance = activeShapes.Min(t => t.DistanceFromEdge(actor, pos)); + + if (distance > Range) + continue; + + var attachable = actor.TraitsImplementing().FirstOrDefault(a => a.CanAttach(Type)); + attachable?.Attach(new DelayedWeaponTrigger(this, args)); + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/BackFireShrapnelWarhead.cs b/OpenRA.Mods.AS/Warheads/BackFireShrapnelWarhead.cs new file mode 100644 index 000000000000..4f4d6898c9f1 --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/BackFireShrapnelWarhead.cs @@ -0,0 +1,92 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("A creative warhead in MW, made by CombinE88")] + public class BackFireShrapnelWarhead : WarheadAS, IRulesetLoaded + { + [WeaponReference] + [FieldLoader.Require] + [Desc("Has to be defined in weapons.yaml as well.")] + public readonly string Weapon = null; + + public readonly string WeaponName = "primary"; + + WeaponInfo weapon; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var shrapnelTarget = Target.Invalid; + + shrapnelTarget = Target.FromActor(firedBy); + + if (shrapnelTarget.Type != TargetType.Invalid) + { + var pargs = new ProjectileArgs + { + Weapon = weapon, + Facing = (shrapnelTarget.CenterPosition - target.CenterPosition).Yaw, + + DamageModifiers = !firedBy.IsDead + ? firedBy.TraitsImplementing() + .Select(a => a.GetFirepowerModifier(WeaponName)).ToArray() + : Array.Empty(), + + InaccuracyModifiers = !firedBy.IsDead + ? firedBy.TraitsImplementing() + .Select(a => a.GetInaccuracyModifier()).ToArray() + : Array.Empty(), + + RangeModifiers = !firedBy.IsDead + ? firedBy.TraitsImplementing() + .Select(a => a.GetRangeModifier()).ToArray() + : Array.Empty(), + + Source = target.CenterPosition, + SourceActor = firedBy, + GuidedTarget = shrapnelTarget, + PassiveTarget = shrapnelTarget.CenterPosition + }; + + if (pargs.Weapon.Projectile != null) + { + var projectile = pargs.Weapon.Projectile.Create(pargs); + if (projectile != null) + firedBy.World.AddFrameEndTask(w => w.Add(projectile)); + + if (pargs.Weapon.Report != null && pargs.Weapon.Report.Any()) + { + var pos = target.CenterPosition; + if (pargs.Weapon.AudibleThroughFog || (!firedBy.World.ShroudObscures(pos) && !firedBy.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, pargs.Weapon.Report, firedBy.World, pos, null, pargs.Weapon.SoundVolume); + } + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/CaptureActorWarhead.cs b/OpenRA.Mods.AS/Warheads/CaptureActorWarhead.cs new file mode 100644 index 000000000000..c2036b3eabbe --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/CaptureActorWarhead.cs @@ -0,0 +1,133 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("Allows the firer to capture targets. This warhead interacts with the Capturable trait.")] + public class CaptureActorWarhead : WarheadAS + { + [Desc("Range of targets to be captured.")] + public readonly WDist Range = new(64); + + [Desc("Types of actors that it can capture, as long as the type also exists in the Capturable Type: trait.")] + public readonly BitSet CaptureTypes = default; + + [Desc("Targets with health above this percentage will be sabotaged instead of captured.", + "Set to 0 to disable sabotaging.")] + public readonly int SabotageThreshold = 0; + + [Desc("Sabotage damage expressed as a percentage of maximum target health.")] + public readonly int SabotageHPRemoval = 50; + + [Desc("Experience granted to the capturing actor.")] + public readonly int Experience = 0; + + [Desc("PlayerRelationship that the structure's previous owner needs to have for the capturing actor to receive Experience.")] + public readonly PlayerRelationship ExperiencePlayerRelationships = PlayerRelationship.Enemy; + + [Desc("Experience granted to the capturing player.")] + public readonly int PlayerExperience = 0; + + [Desc("PlayerRelationship that the structure's previous owner needs to have for the capturing player to receive Experience.")] + public readonly PlayerRelationship PlayerExperienceStances = PlayerRelationship.Enemy; + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var pos = target.CenterPosition; + + if (!IsValidImpact(pos, firedBy)) + return; + + var availableActors = firedBy.World.FindActorsOnCircle(pos, Range); + + foreach (var a in availableActors) + { + if (!IsValidAgainst(a, firedBy)) + continue; + + var activeShapes = a.TraitsImplementing().Where(Exts.IsTraitEnabled); + if (!activeShapes.Any()) + continue; + + var distance = activeShapes.Min(t => t.DistanceFromEdge(a, pos)); + + if (distance > Range) + continue; + + var capturable = a.TraitsImplementing() + .FirstOrDefault(c => !c.IsTraitDisabled && c.Info.Types.Overlaps(CaptureTypes)); + + if (a.IsDead || capturable == null) + continue; + + firedBy.World.AddFrameEndTask(w => + { + if (a.IsDead) + return; + + if (SabotageThreshold > 0 && !a.Owner.NonCombatant) + { + var health = a.Trait(); + + // Cast to long to avoid overflow when multiplying by the health + if (100 * (long)health.HP > SabotageThreshold * (long)health.MaxHP) + { + var damage = (int)((long)health.MaxHP * SabotageHPRemoval / 100); + a.InflictDamage(firedBy, new Damage(damage)); + + return; + } + } + + var oldOwner = a.Owner; + + a.ChangeOwner(firedBy.Owner); + + foreach (var t in a.TraitsImplementing()) + t.OnCapture(a, firedBy, oldOwner, a.Owner, CaptureTypes); + + if (!firedBy.IsDead && firedBy.Owner.RelationshipWith(oldOwner).HasRelationship(ExperiencePlayerRelationships)) + { + var exp = firedBy.TraitOrDefault(); + exp?.GiveExperience(Experience); + } + + if (firedBy.Owner.RelationshipWith(oldOwner).HasRelationship(PlayerExperienceStances)) + { + var exp = firedBy.Owner.PlayerActor.TraitOrDefault(); + exp?.GiveExperience(PlayerExperience); + } + }); + } + } + + public override bool IsValidAgainst(Actor victim, Actor firedBy) + { + var capturable = victim.TraitsImplementing() + .FirstOrDefault(c => !c.IsTraitDisabled && c.Info.Types.Overlaps(CaptureTypes)); + + if (capturable == null || !ValidRelationships.HasRelationship(victim.Owner.RelationshipWith(firedBy.Owner))) + return false; + + return base.IsValidAgainst(victim, firedBy); + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/CreateTintedCellsWarhead.cs b/OpenRA.Mods.AS/Warheads/CreateTintedCellsWarhead.cs new file mode 100644 index 000000000000..c94b24ad7501 --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/CreateTintedCellsWarhead.cs @@ -0,0 +1,112 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Warheads; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + public class CreateTintedCellsWarhead : DamageWarhead, IRulesetLoaded + { + [Desc("Range between falloff steps in cells.")] + public readonly WDist Spread = new(1024); + + [Desc("Level percentage at each range step.")] + public readonly int[] Falloff = { 100, 37, 14, 5, 0 }; + + [Desc("The name of the layer we want to increase the level of.")] + public readonly string LayerName = "radioactivity"; + + [Desc("Determins whether you can go beyond Falloff[step] * MaxLevel for cells.")] + public readonly bool ApplyFalloffToLevel = true; + + [Desc("Ranges at which each Falloff step is defined (in cells). Overrides Spread.")] + public WDist[] Range = null; + + [Desc("Level this weapon puts on the ground. Accumulates over previously contaminated area.")] + public int Level = 100; + + [Desc("It saturates at this level, by this weapon.")] + public int MaxLevel = 500; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (Range == null) + Range = Exts.MakeArray(Falloff.Length, i => i * Spread); + else + { + if (Range.Length != 1 && Range.Length != Falloff.Length) + throw new YamlException("Number of range values must be 1 or equal to the number of Falloff values."); + + for (var i = 0; i < Range.Length - 1; i++) + if (Range[i] > Range[i + 1]) + throw new YamlException("Range values must be specified in an increasing order."); + } + } + + protected override void DoImpact(WPos pos, Actor firedBy, WarheadArgs args) + { + var world = firedBy.World; + + if (world.LocalPlayer != null) + { + var devMode = world.LocalPlayer.PlayerActor.TraitOrDefault(); + if (devMode != null && devMode.CombatGeometry) + { + var rng = Exts.MakeArray(Range.Length, i => WDist.FromCells(Range[i].Length)); + world.WorldActor.Trait().AddImpact(pos, rng, DebugOverlayColor); + } + } + + var targetTile = world.Map.CellContaining(pos); + for (var i = 0; i < Range.Length; i++) + { + var affectedCells = world.Map.FindTilesInCircle(targetTile, (int)Math.Ceiling((decimal)Range[i].Length / 1024)); + + var raLayer = world.WorldActor.TraitsImplementing() + .First(l => l.Info.Name == LayerName); + + foreach (var cell in affectedCells) + { + var mul = GetIntensityFalloff((pos - world.Map.CenterOfCell(cell)).Length); + IncreaseTintedCellLevel(cell, mul, Falloff[i], raLayer); + } + } + } + + void IncreaseTintedCellLevel(CPos pos, int mul, int foff, TintedCellsLayer tcLayer) + { + if (ApplyFalloffToLevel) + tcLayer.IncreaseLevel(pos, Level * mul / 100, MaxLevel * foff / 100); + else + tcLayer.IncreaseLevel(pos, Level * mul / 100, MaxLevel); + } + + int GetIntensityFalloff(int distance) + { + var inner = Range[0].Length; + for (var i = 1; i < Range.Length; i++) + { + var outer = Range[i].Length; + if (outer > distance) + return int2.Lerp(Falloff[i - 1], Falloff[i], distance - inner, outer - inner); + + inner = outer; + } + + return 0; + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/DetachDelayedWeaponWarhead.cs b/OpenRA.Mods.AS/Warheads/DetachDelayedWeaponWarhead.cs new file mode 100644 index 000000000000..731a9d5581af --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/DetachDelayedWeaponWarhead.cs @@ -0,0 +1,68 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("This warhead can detach a DelayedWeapon from the target. Requires an appropriate type of DelayedWeaponAttachable trait to function properly.")] + public class DetachDelayedWeaponWarhead : WarheadAS + { + [Desc("Types of DelayedWeapons that it can detach.")] + public readonly HashSet Types = new() { "bomb" }; + + [Desc("Range of targets to be attached.")] + public readonly WDist Range = new(1024); + + [Desc("Defines how many DelayedWeapons can be detached per impact.")] + public readonly int DetachLimit = 1; + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var pos = target.CenterPosition; + + if (!IsValidImpact(pos, firedBy)) + return; + + var availableActors = firedBy.World.FindActorsOnCircle(pos, Range); + foreach (var actor in availableActors) + { + if (!IsValidAgainst(actor, firedBy)) + continue; + + if (actor.IsDead) + continue; + + var activeShapes = actor.TraitsImplementing().Where(Exts.IsTraitEnabled); + if (!activeShapes.Any()) + continue; + + var distance = activeShapes.Min(t => t.DistanceFromEdge(actor, pos)); + + if (distance > Range) + continue; + + var attachables = actor.TraitsImplementing(); + var triggers = attachables.Where(a => Types.Any(at => at == a.Info.Type)).SelectMany(a => a.Container); + triggers.OrderBy(t => t.RemainingTime).Take(DetachLimit).ToList().ForEach(t => t.Deactivate()); + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/FireClusterASWarhead.cs b/OpenRA.Mods.AS/Warheads/FireClusterASWarhead.cs new file mode 100644 index 000000000000..2607eb2c00b5 --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/FireClusterASWarhead.cs @@ -0,0 +1,130 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + public class FireClusterASWarhead : WarheadAS, IRulesetLoaded + { + [WeaponReference] + [FieldLoader.Require] + [Desc("Has to be defined in weapons.yaml as well.")] + public readonly string Weapon = null; + + [Desc("Number of weapons fired at random 'x' cells. Negative values will result in a number equal to 'x' footprint cells fired.")] + public readonly int RandomClusterCount = -1; + + [FieldLoader.Require] + [Desc("Size of the cluster footprint")] + public readonly CVec Dimensions = CVec.Zero; + + [FieldLoader.Require] + [Desc("Cluster footprint. Cells marked as x will be attacked.")] + public readonly string Footprint = string.Empty; + + [Desc("Should the weapons be fired around the intended target or at the explosion's epicenter.")] + public readonly bool AroundTarget = false; + + WeaponInfo weapon; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var map = firedBy.World.Map; + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var targetCell = AroundTarget && args.WeaponTarget.Type != TargetType.Invalid + ? map.CellContaining(args.WeaponTarget.CenterPosition) + : map.CellContaining(target.CenterPosition); + + var targetCells = CellsMatching(targetCell, false); + + foreach (var c in targetCells) + FireProjectileAtCell(map, firedBy, target, c, args); + + if (RandomClusterCount != 0) + { + var randomTargetCells = CellsMatching(targetCell, true); + var clusterCount = RandomClusterCount < 0 ? randomTargetCells.Count() : RandomClusterCount; + if (randomTargetCells.Any()) + for (var i = 0; i < clusterCount; i++) + FireProjectileAtCell(map, firedBy, target, randomTargetCells.Random(firedBy.World.SharedRandom), args); + } + } + + void FireProjectileAtCell(Map map, Actor firedBy, Target target, CPos targetCell, WarheadArgs args) + { + var tc = Target.FromCell(firedBy.World, targetCell); + + if (!weapon.IsValidAgainst(tc, firedBy.World, firedBy)) + return; + + var projectileArgs = new ProjectileArgs + { + Weapon = weapon, + Facing = (map.CenterOfCell(targetCell) - target.CenterPosition).Yaw, + CurrentMuzzleFacing = () => (map.CenterOfCell(targetCell) - target.CenterPosition).Yaw, + + DamageModifiers = args.DamageModifiers, + InaccuracyModifiers = Array.Empty(), + RangeModifiers = Array.Empty(), + + Source = target.CenterPosition, + CurrentSource = () => target.CenterPosition, + SourceActor = firedBy, + PassiveTarget = map.CenterOfCell(targetCell), + GuidedTarget = tc + }; + + if (projectileArgs.Weapon.Projectile != null) + { + var projectile = projectileArgs.Weapon.Projectile.Create(projectileArgs); + if (projectile != null) + firedBy.World.AddFrameEndTask(w => w.Add(projectile)); + + if (projectileArgs.Weapon.Report != null && projectileArgs.Weapon.Report.Any()) + { + var pos = target.CenterPosition; + if (projectileArgs.Weapon.AudibleThroughFog || (!firedBy.World.ShroudObscures(pos) && !firedBy.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, projectileArgs.Weapon.Report, firedBy.World, pos, null, projectileArgs.Weapon.SoundVolume); + } + } + } + + IEnumerable CellsMatching(CPos location, bool random) + { + var cellType = !random ? 'X' : 'x'; + var index = 0; + var footprint = Footprint.Where(c => !char.IsWhiteSpace(c)).ToArray(); + var x = location.X - (Dimensions.X - 1) / 2; + var y = location.Y - (Dimensions.Y - 1) / 2; + for (var j = 0; j < Dimensions.Y; j++) + for (var i = 0; i < Dimensions.X; i++) + if (footprint[index++] == cellType) + yield return new CPos(x + i, y + j); + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/FireFragmentWarhead.cs b/OpenRA.Mods.AS/Warheads/FireFragmentWarhead.cs new file mode 100644 index 000000000000..d9244f41e264 --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/FireFragmentWarhead.cs @@ -0,0 +1,127 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("Allows to fire a weapon to a directly specified target position relative to the warhead explosion.")] + public class FireFragmentWarhead : WarheadAS, IRulesetLoaded + { + [WeaponReference] + [FieldLoader.Require] + [Desc("Has to be defined in weapons.yaml as well.")] + public readonly string Weapon = null; + + [Desc("Percentual chance the fragment is fired.")] + public readonly int Chance = 100; + + [Desc("Target offsets relative to warhead explosion.")] + public readonly WVec[] Offsets = { new WVec(0, 0, 0) }; + + [Desc("If set, Offset's Z value will be used as absolute height instead of explosion height.")] + public readonly bool UseZOffsetAsAbsoluteHeight = false; + + [Desc("Should the weapons be fired around the intended target or at the explosion's epicenter.")] + public readonly bool AroundTarget = false; + + [Desc("Rotate the fragment weapon based on the impact orientation.")] + public readonly bool Rotate = false; + + WeaponInfo weapon; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var world = firedBy.World; + var map = world.Map; + + if (Chance < world.SharedRandom.Next(100)) + return; + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var epicenter = AroundTarget && args.WeaponTarget.Type != TargetType.Invalid + ? args.WeaponTarget.CenterPosition + : target.CenterPosition; + + foreach (var offset in Offsets) + { + var targetVector = offset; + + if (Rotate && args.ImpactOrientation != WRot.None) + targetVector = targetVector.Rotate(args.ImpactOrientation); + + var fragmentTargetPosition = epicenter + targetVector; + + if (UseZOffsetAsAbsoluteHeight) + { + fragmentTargetPosition = new WPos(fragmentTargetPosition.X, fragmentTargetPosition.Y, + world.Map.CenterOfCell(world.Map.CellContaining(fragmentTargetPosition)).Z + offset.Z); + } + + var fragmentTarget = Target.FromPos(fragmentTargetPosition); + var fragmentFacing = (fragmentTargetPosition - target.CenterPosition).Yaw; + + // Lambdas can't use 'in' variables, so capture a copy for later + var centerPosition = target.CenterPosition; + + var projectileArgs = new ProjectileArgs + { + Weapon = weapon, + Facing = fragmentFacing, + CurrentMuzzleFacing = () => fragmentFacing, + + DamageModifiers = args.DamageModifiers, + + InaccuracyModifiers = !firedBy.IsDead ? firedBy.TraitsImplementing() + .Select(a => a.GetInaccuracyModifier()).ToArray() : Array.Empty(), + + RangeModifiers = !firedBy.IsDead ? firedBy.TraitsImplementing() + .Select(a => a.GetRangeModifier()).ToArray() : Array.Empty(), + + Source = target.CenterPosition, + CurrentSource = () => centerPosition, + SourceActor = firedBy, + GuidedTarget = fragmentTarget, + PassiveTarget = fragmentTargetPosition + }; + + if (projectileArgs.Weapon.Projectile != null) + { + var projectile = projectileArgs.Weapon.Projectile.Create(projectileArgs); + if (projectile != null) + firedBy.World.AddFrameEndTask(w => w.Add(projectile)); + + if (projectileArgs.Weapon.Report != null && projectileArgs.Weapon.Report.Any()) + { + var pos = target.CenterPosition; + if (projectileArgs.Weapon.AudibleThroughFog || (!firedBy.World.ShroudObscures(pos) && !firedBy.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, projectileArgs.Weapon.Report, firedBy.World, pos, null, projectileArgs.Weapon.SoundVolume); + } + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/FireRadiusWarhead.cs b/OpenRA.Mods.AS/Warheads/FireRadiusWarhead.cs new file mode 100644 index 000000000000..addea99f3ddd --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/FireRadiusWarhead.cs @@ -0,0 +1,116 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("Fires a defined amount of weapons with their maximum range in a wave pattern.")] + public class FireRadiusWarhead : WarheadAS, IRulesetLoaded + { + [WeaponReference] + [FieldLoader.Require] + [Desc("Has to be defined in weapons.yaml as well.")] + public readonly string Weapon = null; + + [Desc("Amount of weapons fired.")] + public readonly int[] Amount = { 1 }; + + [Desc("Should the weapons be fired around the intended target or at the explosion's epicenter.")] + public readonly bool AroundTarget = false; + + WeaponInfo weapon; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var world = firedBy.World; + var map = world.Map; + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var epicenter = AroundTarget && args.WeaponTarget.Type != TargetType.Invalid + ? args.WeaponTarget.CenterPosition + : target.CenterPosition; + + var amount = Amount.Length == 2 + ? world.SharedRandom.Next(Amount[0], Amount[1]) + : Amount[0]; + + var offset = 1024 / amount; + + for (var i = 0; i < amount; i++) + { + var radiusTarget = Target.Invalid; + + var rotation = WRot.FromYaw(new WAngle(i * offset)); + var targetpos = epicenter + new WVec(weapon.Range.Length, 0, 0).Rotate(rotation); + var tpos = Target.FromPos(new WPos(targetpos.X, targetpos.Y, map.CenterOfCell(map.CellContaining(targetpos)).Z)); + if (weapon.IsValidAgainst(tpos, firedBy.World, firedBy)) + radiusTarget = tpos; + + if (radiusTarget.Type == TargetType.Invalid) + continue; + + // Lambdas can't use 'in' variables, so capture a copy for later + var centerPosition = target.CenterPosition; + + var projectileArgs = new ProjectileArgs + { + Weapon = weapon, + Facing = (radiusTarget.CenterPosition - target.CenterPosition).Yaw, + CurrentMuzzleFacing = () => (radiusTarget.CenterPosition - centerPosition).Yaw, + + DamageModifiers = args.DamageModifiers, + + InaccuracyModifiers = !firedBy.IsDead ? firedBy.TraitsImplementing() + .Select(a => a.GetInaccuracyModifier()).ToArray() : Array.Empty(), + + RangeModifiers = !firedBy.IsDead ? firedBy.TraitsImplementing() + .Select(a => a.GetRangeModifier()).ToArray() : Array.Empty(), + + Source = target.CenterPosition, + CurrentSource = () => centerPosition, + SourceActor = firedBy, + GuidedTarget = radiusTarget, + PassiveTarget = radiusTarget.CenterPosition + }; + + if (projectileArgs.Weapon.Projectile != null) + { + var projectile = projectileArgs.Weapon.Projectile.Create(projectileArgs); + if (projectile != null) + firedBy.World.AddFrameEndTask(w => w.Add(projectile)); + + if (projectileArgs.Weapon.Report != null && projectileArgs.Weapon.Report.Any()) + { + var pos = target.CenterPosition; + if (projectileArgs.Weapon.AudibleThroughFog || (!firedBy.World.ShroudObscures(pos) && !firedBy.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, projectileArgs.Weapon.Report, firedBy.World, pos, null, projectileArgs.Weapon.SoundVolume); + } + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/FireReverseRadiusWarhead.cs b/OpenRA.Mods.AS/Warheads/FireReverseRadiusWarhead.cs new file mode 100644 index 000000000000..a660d54b3e2d --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/FireReverseRadiusWarhead.cs @@ -0,0 +1,114 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("Fires a defined amount of weapons with their maximum range in a reverse wave pattern.")] + public class FireReverseRadiusWarhead : WarheadAS, IRulesetLoaded + { + [WeaponReference] + [FieldLoader.Require] + [Desc("Has to be defined in weapons.yaml as well.")] + public readonly string Weapon = null; + + [Desc("Amount of weapons fired.")] + public readonly int[] Amount = { 1 }; + + [Desc("Should the weapons be fired around the intended target or at the explosion's epicenter.")] + public readonly bool AroundTarget = false; + + WeaponInfo weapon; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var world = firedBy.World; + var map = world.Map; + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var epicenter = AroundTarget && args.WeaponTarget.Type != TargetType.Invalid + ? args.WeaponTarget.CenterPosition + : target.CenterPosition; + + var amount = Amount.Length == 2 + ? world.SharedRandom.Next(Amount[0], Amount[1]) + : Amount[0]; + + var offset = 1024 / amount; + + for (var i = 0; i < amount; i++) + { + var radiusSource = Target.Invalid; + + var rotation = WRot.FromYaw(new WAngle(i * offset)); + var targetpos = epicenter + new WVec(weapon.Range.Length, 0, 0).Rotate(rotation); + radiusSource = Target.FromPos(new WPos(targetpos.X, targetpos.Y, map.CenterOfCell(map.CellContaining(targetpos)).Z)); + + if (radiusSource.Type == TargetType.Invalid) + continue; + + // Lambdas can't use 'in' variables, so capture a copy for later + var centerPosition = target.CenterPosition; + + var projectileArgs = new ProjectileArgs + { + Weapon = weapon, + Facing = (target.CenterPosition - radiusSource.CenterPosition).Yaw, + CurrentMuzzleFacing = () => (centerPosition - radiusSource.CenterPosition).Yaw, + + DamageModifiers = args.DamageModifiers, + + InaccuracyModifiers = !firedBy.IsDead ? firedBy.TraitsImplementing() + .Select(a => a.GetInaccuracyModifier()).ToArray() : Array.Empty(), + + RangeModifiers = !firedBy.IsDead ? firedBy.TraitsImplementing() + .Select(a => a.GetRangeModifier()).ToArray() : Array.Empty(), + + Source = radiusSource.CenterPosition, + CurrentSource = () => radiusSource.CenterPosition, + SourceActor = firedBy, + GuidedTarget = target, + PassiveTarget = target.CenterPosition + }; + + if (projectileArgs.Weapon.Projectile != null) + { + var projectile = projectileArgs.Weapon.Projectile.Create(projectileArgs); + if (projectile != null) + firedBy.World.AddFrameEndTask(w => w.Add(projectile)); + + if (projectileArgs.Weapon.Report != null && projectileArgs.Weapon.Report.Any()) + { + var pos = target.CenterPosition; + if (projectileArgs.Weapon.AudibleThroughFog || (!firedBy.World.ShroudObscures(pos) && !firedBy.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, projectileArgs.Weapon.Report, firedBy.World, pos, null, projectileArgs.Weapon.SoundVolume); + } + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/FireShrapnelWarhead.cs b/OpenRA.Mods.AS/Warheads/FireShrapnelWarhead.cs new file mode 100644 index 000000000000..3b0af2445f62 --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/FireShrapnelWarhead.cs @@ -0,0 +1,174 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + public class FireShrapnelWarhead : WarheadAS, IRulesetLoaded + { + [WeaponReference] + [FieldLoader.Require] + [Desc("Has to be defined in weapons.yaml as well.")] + public readonly string Weapon = null; + + public readonly string WeaponName = "primary"; + + [Desc("Amount of shrapnels thrown.")] + public readonly int[] Amount = { 1 }; + + [Desc("The percentage of aiming this shrapnel to a suitable target actor.")] + public readonly int AimChance = 0; + + [Desc("What diplomatic stances can be targeted by the shrapnel.")] + public readonly PlayerRelationship AimTargetStances = PlayerRelationship.Ally | PlayerRelationship.Neutral | PlayerRelationship.Enemy; + + [Desc("Allow this shrapnel to be thrown randomly when no targets found.")] + public readonly bool ThrowWithoutTarget = true; + + [Desc("Should the shrapnel hit the direct target?")] + public readonly bool AllowDirectHit = false; + + [Desc("Should the weapons be fired around the intended target or at the explosion's epicenter.")] + public readonly bool AroundTarget = false; + + WeaponInfo weapon; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var world = firedBy.World; + var map = world.Map; + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var epicenter = AroundTarget && args.WeaponTarget.Type != TargetType.Invalid + ? args.WeaponTarget.CenterPosition + : target.CenterPosition; + + var directActors = world.FindActorsOnCircle(epicenter, WDist.Zero) + .Where(a => + { + var activeShapes = a.TraitsImplementing().Where(Exts.IsTraitEnabled); + if (!activeShapes.Any()) + return false; + + var distance = activeShapes.Min(t => t.DistanceFromEdge(a, epicenter)); + + if (distance != WDist.Zero) + return false; + + return true; + }); + + var availableTargetActors = world.FindActorsOnCircle(epicenter, weapon.Range) + .Where(x => (AllowDirectHit || !directActors.Contains(x)) + && weapon.IsValidAgainst(Target.FromActor(x), firedBy.World, firedBy) + && AimTargetStances.HasRelationship(firedBy.Owner.RelationshipWith(x.Owner))) + .Where(x => + { + var activeShapes = x.TraitsImplementing().Where(Exts.IsTraitEnabled); + if (!activeShapes.Any()) + return false; + + var distance = activeShapes.Min(t => t.DistanceFromEdge(x, epicenter)); + + if (distance < weapon.Range) + return true; + + return false; + }) + .Shuffle(world.SharedRandom); + + var targetActor = availableTargetActors.GetEnumerator(); + + var amount = Amount.Length == 2 + ? world.SharedRandom.Next(Amount[0], Amount[1]) + : Amount[0]; + + for (var i = 0; i < amount; i++) + { + var shrapnelTarget = Target.Invalid; + + if (world.SharedRandom.Next(100) < AimChance && targetActor.MoveNext()) + shrapnelTarget = Target.FromActor(targetActor.Current); + + if (ThrowWithoutTarget && shrapnelTarget.Type == TargetType.Invalid) + { + var rotation = WRot.FromFacing(world.SharedRandom.Next(256)); + var range = world.SharedRandom.Next(weapon.MinRange.Length, weapon.Range.Length); + var targetpos = epicenter + new WVec(range, 0, 0).Rotate(rotation); + var tpos = Target.FromPos(new WPos(targetpos.X, targetpos.Y, map.CenterOfCell(map.CellContaining(targetpos)).Z)); + if (weapon.IsValidAgainst(tpos, firedBy.World, firedBy)) + shrapnelTarget = tpos; + } + + if (shrapnelTarget.Type == TargetType.Invalid) + continue; + + var shrapnelFacing = (shrapnelTarget.CenterPosition - epicenter).Yaw; + + // Lambdas can't use 'in' variables, so capture a copy for later + var centerPosition = target.CenterPosition; + + var projectileArgs = new ProjectileArgs + { + Weapon = weapon, + Facing = shrapnelFacing, + CurrentMuzzleFacing = () => shrapnelFacing, + + DamageModifiers = !firedBy.IsDead ? firedBy.TraitsImplementing() + .Select(a => a.GetFirepowerModifier(WeaponName)).ToArray() : Array.Empty(), + + InaccuracyModifiers = !firedBy.IsDead ? firedBy.TraitsImplementing() + .Select(a => a.GetInaccuracyModifier()).ToArray() : Array.Empty(), + + RangeModifiers = !firedBy.IsDead ? firedBy.TraitsImplementing() + .Select(a => a.GetRangeModifier()).ToArray() : Array.Empty(), + + Source = target.CenterPosition, + CurrentSource = () => centerPosition, + SourceActor = firedBy, + GuidedTarget = shrapnelTarget, + PassiveTarget = shrapnelTarget.CenterPosition + }; + + if (projectileArgs.Weapon.Projectile != null) + { + var projectile = projectileArgs.Weapon.Projectile.Create(projectileArgs); + if (projectile != null) + firedBy.World.AddFrameEndTask(w => w.Add(projectile)); + + if (projectileArgs.Weapon.Report != null && projectileArgs.Weapon.Report.Any()) + { + var pos = target.CenterPosition; + if (projectileArgs.Weapon.AudibleThroughFog || (!firedBy.World.ShroudObscures(pos) && !firedBy.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, projectileArgs.Weapon.Report, firedBy.World, pos, null, projectileArgs.Weapon.SoundVolume); + } + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/FlashTargetWarhead.cs b/OpenRA.Mods.AS/Warheads/FlashTargetWarhead.cs new file mode 100644 index 000000000000..38e674716551 --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/FlashTargetWarhead.cs @@ -0,0 +1,49 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.GameRules; +using OpenRA.Mods.Common.Effects; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + public class FlashTargetWarhead : WarheadAS + { + [Desc("Color of the flash.")] + public readonly Color FlashColor = Color.White; + + [Desc("Use color of the firer for the flash, instead of `Color` value.")] + public readonly bool UsePlayerColor = true; + + [Desc("Range of targets to be affected.")] + public readonly WDist Range = new(64); + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var pos = target.CenterPosition; + if (!IsValidImpact(pos, firedBy)) + return; + + var availableActors = firedBy.World.FindActorsInCircle(pos, Range); + foreach (var a in availableActors) + { + if (!IsValidAgainst(a, firedBy)) + continue; + + firedBy.World.AddFrameEndTask(w => w.Add(new FlashTarget(a, UsePlayerColor ? firedBy.Owner.Color : FlashColor))); + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/GlobalExplodeWeaponWarhead.cs b/OpenRA.Mods.AS/Warheads/GlobalExplodeWeaponWarhead.cs new file mode 100644 index 000000000000..4831d1cced45 --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/GlobalExplodeWeaponWarhead.cs @@ -0,0 +1,58 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("Triggers a weapon explosion on all appropriate actors on the map.", + "Note that this warhead is a HUGE HACK. Use it on your own risk!")] + public class GlobalExplodeWeaponWarhead : WarheadAS, IRulesetLoaded + { + [WeaponReference] + [FieldLoader.Require] + [Desc("Has to be defined in weapons.yaml as well.")] + public readonly string Weapon = null; + + WeaponInfo weapon; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var allowedActors = firedBy.World.Actors.Where(a => a.IsInWorld && !a.IsDead && IsValidAgainst(a, firedBy)); + + foreach (var actor in allowedActors) + { + weapon.Impact(Target.FromActor(actor), args); + + if (weapon.Report != null && weapon.Report.Any()) + { + var pos = actor.CenterPosition; + if (weapon.AudibleThroughFog || (!firedBy.World.ShroudObscures(pos) && !firedBy.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, weapon.Report.Random(firedBy.World.SharedRandom), pos, weapon.SoundVolume); + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/IgnoreHeightDamageWarhad.cs b/OpenRA.Mods.AS/Warheads/IgnoreHeightDamageWarhad.cs new file mode 100644 index 000000000000..3ee958d4bedf --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/IgnoreHeightDamageWarhad.cs @@ -0,0 +1,63 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.GameRules; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Warheads; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("Apply fixed damage in a specified range ignore height. used for superweapon instant kill for air unit")] + sealed class IgnoreHeightDamageWarhead : TargetDamageWarhead + { + protected override void DoImpact(WPos pos, Actor firedBy, WarheadArgs args) + { + if (Spread == WDist.Zero) + return; + + var debugVis = firedBy.World.WorldActor.TraitOrDefault(); + if (debugVis != null && debugVis.CombatGeometry) + firedBy.World.WorldActor.Trait().AddImpact(pos, new[] { WDist.Zero, Spread }, DebugOverlayColor); + + foreach (var victim in firedBy.World.FindActorsInCircle(pos, Spread)) + { + if (!IsValidAgainst(victim, firedBy)) + continue; + + HitShape closestActiveShape = null; + var closestDistance = int.MaxValue; + + // PERF: Avoid using TraitsImplementing that needs to find the actor in the trait dictionary. + foreach (var targetPos in victim.EnabledTargetablePositions) + { + if (targetPos is HitShape hitshape) + { + var distance = hitshape.DistanceFromEdge(victim, pos).Length; + if (distance < closestDistance) + { + closestDistance = distance; + closestActiveShape = hitshape; + } + } + } + + // Cannot be damaged without an active HitShape. + if (closestActiveShape == null) + continue; + + // Summary: when find victim actors, OpenRA ignores height, + // but when calculate hitshape, most of damage warhead will + // consider height. + InflictDamage(victim, firedBy, closestActiveShape, args); + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/OpenToppedDamageWarhead.cs b/OpenRA.Mods.AS/Warheads/OpenToppedDamageWarhead.cs new file mode 100644 index 000000000000..7fdd5ed1f8ee --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/OpenToppedDamageWarhead.cs @@ -0,0 +1,32 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.GameRules; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Warheads; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("Deals damage to the passengers/garrisoners of the target actors.")] + public class OpenToppedDamageWarhead : TargetDamageWarhead + { + [Desc("Amount of garrisoners that will be affected, use -1 to affect all.")] + public readonly int Amount = -1; + + protected override void InflictDamage(Actor victim, Actor firedBy, HitShape shape, WarheadArgs args) + { + var validTraits = victim.TraitsImplementing(); + foreach (var trait in validTraits) + { + trait.DamagePassengers(Damage, firedBy, Amount, Versus, DamageTypes, args.DamageModifiers); + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/PromotionWarhead.cs b/OpenRA.Mods.AS/Warheads/PromotionWarhead.cs new file mode 100644 index 000000000000..fba68a538767 --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/PromotionWarhead.cs @@ -0,0 +1,66 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("Grants promotion to actors.")] + public class PromotionWarhead : WarheadAS + { + [Desc("Range of targets to be promoted.")] + public readonly WDist Range = new(2048); + + [Desc("Levels of promotion granted.")] + public readonly int Levels = 1; + + [Desc("Suppress levelup effects?")] + public readonly bool SuppressEffects = false; + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var pos = target.CenterPosition; + + if (!IsValidImpact(pos, firedBy)) + return; + + var availableActors = firedBy.World.FindActorsOnCircle(pos, Range); + + foreach (var a in availableActors) + { + if (!IsValidAgainst(a, firedBy)) + continue; + + var activeShapes = a.TraitsImplementing().Where(Exts.IsTraitEnabled); + if (!activeShapes.Any()) + continue; + + var distance = activeShapes.Min(t => t.DistanceFromEdge(a, pos)); + + if (distance > Range) + continue; + + var xp = a.TraitOrDefault(); + if (xp == null) + continue; + + xp.GiveLevels(Levels, SuppressEffects); + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/RevealShroudWarhead.cs b/OpenRA.Mods.AS/Warheads/RevealShroudWarhead.cs new file mode 100644 index 000000000000..331e60496edf --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/RevealShroudWarhead.cs @@ -0,0 +1,51 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using OpenRA.GameRules; +using OpenRA.Mods.Common.Effects; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + public class RevealShroudWarhead : WarheadAS + { + [Desc("PlayerRelationships relative to the firer which the warhead affects.")] + public readonly PlayerRelationship RevealStances = PlayerRelationship.Ally; + + [Desc("Duration of the reveal.")] + public readonly int Duration = 25; + + [Desc("Radius of the reveal around the detonation.")] + public readonly WDist Radius = new(1536); + + [Desc("Can this warhead reveal shroud generated by the GeneratesShroud trait?")] + public readonly bool RevealGeneratedShroud = false; + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + // Lambdas can't use 'in' variables, so capture a copy for later + var centerPosition = target.CenterPosition; + + if (!firedBy.IsDead) + { + firedBy.World.AddFrameEndTask(w => w.Add(new RevealShroudEffect(centerPosition, Radius, + RevealGeneratedShroud ? Shroud.SourceType.Visibility : Shroud.SourceType.PassiveVisibility, + firedBy.Owner, RevealStances, duration: Duration))); + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/SendAirstrikeWarhead.cs b/OpenRA.Mods.AS/Warheads/SendAirstrikeWarhead.cs new file mode 100644 index 000000000000..e2943a2ccb5f --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/SendAirstrikeWarhead.cs @@ -0,0 +1,95 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using OpenRA.GameRules; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +// TODO: Mix this with Spawner logics when those arrive for the airstrike to grant experience to the initiator. +namespace OpenRA.Mods.AS.Warheads +{ + public enum AirstrikeTarget { Target, Position } + + [Desc("This warhead sends an airstrike.")] + public class SendAirstrikeWarhead : WarheadAS + { + [Desc("The mode the airstrike should behave. Available options are Target (where the plane attacks the weapon's target) and Position.")] + public readonly AirstrikeTarget Mode = AirstrikeTarget.Target; + + [Desc("Should the aircraft fly in from a random edge of the map or use the firer's facing?")] + public readonly bool RandomizeAircraftFacing = false; + + [ActorReference(typeof(AircraftInfo))] + [FieldLoader.Require] + public readonly string UnitType = null; + public readonly int SquadSize = 1; + public readonly WVec SquadOffset = new(-1536, 1536, 0); + + public readonly int QuantizedFacings = 32; + public readonly WDist Cordon = new(5120); + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + if (!IsValidImpact(target.CenterPosition, firedBy) || firedBy.IsDead) + return; + + var attackFacing = RandomizeAircraftFacing || !firedBy.Info.HasTraitInfo() + ? new WAngle(1024 * firedBy.World.SharedRandom.Next(QuantizedFacings) / QuantizedFacings) + : firedBy.Trait().Facing; + + var attackRotation = WRot.FromYaw(attackFacing); + + var altitude = new WVec(0, 0, firedBy.World.Map.Rules.Actors[UnitType.ToLowerInvariant()].TraitInfo().CruiseAltitude.Length); + var delta = new WVec(0, -1024, 0).Rotate(attackRotation); + + var startPos = target.CenterPosition + altitude - (firedBy.World.Map.DistanceToEdge(target.CenterPosition, -delta) + Cordon).Length * delta / 1024; + + // Lambdas can't use 'in' variables, so capture a copy for later + var delayedTarget = target; + + firedBy.World.AddFrameEndTask(w => + { + for (var i = -SquadSize / 2; i <= SquadSize / 2; i++) + { + // Even-sized squads skip the lead plane + if (i == 0 && (SquadSize & 1) == 0) + continue; + + // Includes the 90 degree rotation between body and world coordinates + var so = SquadOffset; + var spawnOffset = new WVec(i * so.Y, -Math.Abs(i) * so.X, 0).Rotate(attackRotation); + + var a = w.CreateActor(UnitType, new TypeDictionary + { + new CenterPositionInit(startPos + spawnOffset), + new OwnerInit(firedBy.Owner), + new FacingInit(attackFacing), + }); + + if (Mode == AirstrikeTarget.Target) + a.QueueActivity(new FlyAttack(a, AttackSource.Default, delayedTarget, true, Color.OrangeRed)); + else + a.QueueActivity(new FlyAttack(a, AttackSource.Default, Target.FromPos(delayedTarget.CenterPosition + spawnOffset), true, Color.OrangeRed)); + + a.QueueActivity(new FlyOffMap(a)); + a.QueueActivity(new RemoveSelf()); + } + }); + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/SpawnActorWarhead.cs b/OpenRA.Mods.AS/Warheads/SpawnActorWarhead.cs new file mode 100644 index 000000000000..366b73fd9f6f --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/SpawnActorWarhead.cs @@ -0,0 +1,204 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.AS.Activities; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + public enum ASOwnerType { Attacker, InternalName } + + [Desc("Spawn actors upon explosion. Don't use this with buildings.")] + public class SpawnActorWarhead : WarheadAS, IRulesetLoaded + { + [Desc("The cell range to try placing the actors within.")] + public readonly int Range = 10; + + [Desc("Actors to spawn.")] + public readonly string[] Actors = Array.Empty(); + + [Desc("Should this actor link to the actor who create them? This will pass firer as the Parent Actor to spawned.")] + public readonly bool LinkToParent = false; + + [Desc("Try to parachute the actors. When unset, actors will just fall down visually using FallRate." + + " Requires the Parachutable trait on all actors if set.")] + public readonly bool Paradrop = false; + + public readonly int FallRate = 130; + + [Desc("Always spawn the actors on the ground.")] + public readonly bool ForceGround = false; + + [Desc("Owner of the spawned actor. Allowed keywords:" + + "'Attacker' and 'InternalName'.")] + public readonly ASOwnerType OwnerType = ASOwnerType.Attacker; + + [Desc("Map player to use when 'InternalName' is defined on 'OwnerType'.")] + public readonly string InternalOwner = "Neutral"; + + [Desc("Defines the image of an optional animation played at the spawning location.")] + public readonly string Image = null; + + [SequenceReference(nameof(Image), allowNullImage: true)] + [Desc("Defines the sequence of an optional animation played at the spawning location.")] + public readonly string Sequence = "idle"; + + [PaletteReference] + [Desc("Defines the palette of an optional animation played at the spawning location.")] + public readonly string Palette = "effect"; + + [Desc("List of sounds that can be played at the spawning location.")] + public readonly string[] Sounds = Array.Empty(); + + public readonly bool UsePlayerPalette = false; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + foreach (var a in Actors) + { + var actorInfo = rules.Actors[a.ToLowerInvariant()]; + var buildingInfo = actorInfo.TraitInfoOrDefault(); + + if (buildingInfo != null) + throw new YamlException($"SpawnActorWarhead cannot be used to spawn building actor '{a}'!"); + } + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var map = firedBy.World.Map; + var targetCell = map.CellContaining(target.CenterPosition); + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var targetCells = map.FindTilesInCircle(targetCell, Range); + var cell = targetCells.GetEnumerator(); + + foreach (var a in Actors) + { + var placed = false; + var td = new TypeDictionary(); + var ai = map.Rules.Actors[a.ToLowerInvariant()]; + + if (OwnerType == ASOwnerType.Attacker) + td.Add(new OwnerInit(firedBy.Owner)); + else + td.Add(new OwnerInit(firedBy.World.Players.First(p => p.InternalName == InternalOwner))); + + if (LinkToParent) + td.Add(new ParentActorInit(firedBy)); + + // HACK HACK HACK + // Immobile does not offer a check directly if the actor can exist in a position. + // It also crashes the game if it's actor's created without a LocationInit. + // See AS/Engine#84. + if (ai.HasTraitInfo()) + { + var immobileInfo = ai.TraitInfo(); + + while (cell.MoveNext()) + { + if (!immobileInfo.OccupiesSpace || !firedBy.World.ActorMap.GetActorsAt(cell.Current).Any()) + { + td.Add(new LocationInit(cell.Current)); + var immobileunit = firedBy.World.CreateActor(false, a.ToLowerInvariant(), td); + + firedBy.World.AddFrameEndTask(w => + { + w.Add(immobileunit); + + var palette = Palette; + if (UsePlayerPalette) + palette += immobileunit.Owner.InternalName; + + var immobilespawnpos = firedBy.World.Map.CenterOfCell(cell.Current); + + if (Image != null) + w.Add(new SpriteEffect(immobilespawnpos, w, Image, Sequence, palette)); + + var sound = Sounds.RandomOrDefault(firedBy.World.LocalRandom); + if (sound != null) + Game.Sound.Play(SoundType.World, sound, immobilespawnpos); + }); + + break; + } + } + + continue; + } + + // Lambdas can't use 'in' variables, so capture a copy for later + var delayedTarget = target; + + firedBy.World.AddFrameEndTask(w => + { + var unit = firedBy.World.CreateActor(false, a.ToLowerInvariant(), td); + var positionable = unit.TraitOrDefault(); + cell = targetCells.GetEnumerator(); + + while (cell.MoveNext() && !placed) + { + var subCell = positionable.GetAvailableSubCell(cell.Current); + + if (ai.HasTraitInfo() + && ai.TraitInfo().CanEnterCell(firedBy.World, unit, cell.Current)) + subCell = SubCell.FullCell; + + if (subCell != SubCell.Invalid) + { + positionable.SetPosition(unit, cell.Current, subCell); + + var pos = unit.CenterPosition; + if (!ForceGround) + pos += new WVec(WDist.Zero, WDist.Zero, firedBy.World.Map.DistanceAboveTerrain(delayedTarget.CenterPosition)); + + positionable.SetCenterPosition(unit, pos); + w.Add(unit); + + if (Paradrop) + unit.QueueActivity(new Parachute(unit)); + else + unit.QueueActivity(new FallDown(unit, pos, FallRate)); + + var palette = Palette; + if (UsePlayerPalette) + palette += unit.Owner.InternalName; + + if (Image != null) + w.Add(new SpriteEffect(pos, w, Image, Sequence, palette)); + + var sound = Sounds.RandomOrDefault(firedBy.World.LocalRandom); + if (sound != null) + Game.Sound.Play(SoundType.World, sound, pos); + + placed = true; + } + } + + if (!placed) + unit.Dispose(); + }); + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/SpawnBuildingWarhead.cs b/OpenRA.Mods.AS/Warheads/SpawnBuildingWarhead.cs new file mode 100644 index 000000000000..b10bc9dcb7f7 --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/SpawnBuildingWarhead.cs @@ -0,0 +1,135 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("Spawn buildings upon explosion.")] + public class SpawnBuildingWarhead : WarheadAS, IRulesetLoaded + { + [Desc("The cell range to try placing the buildings within.")] + public readonly int Range = 10; + + [Desc("Actors to spawn.")] + public readonly string[] Buildings = Array.Empty(); + + [Desc("Should this building link to the actor who create them?")] + public readonly bool LinkToParent = false; + + public readonly bool SkipMakeAnims = false; + + [Desc("Owner of the spawned actor. Allowed keywords:" + + "'Attacker' and 'InternalName'.")] + public readonly ASOwnerType OwnerType = ASOwnerType.Attacker; + + [Desc("Map player to use when 'InternalName' is defined on 'OwnerType'.")] + public readonly string InternalOwner = "Neutral"; + + [Desc("Defines the image of an optional animation played at the spawning location.")] + public readonly string Image = null; + + [SequenceReference(nameof(Image), allowNullImage: true)] + [Desc("Defines the sequence of an optional animation played at the spawning location.")] + public readonly string Sequence = "idle"; + + [PaletteReference] + [Desc("Defines the palette of an optional animation played at the spawning location.")] + public readonly string Palette = "effect"; + + [Desc("List of sounds that can be played at the spawning location.")] + public readonly string[] Sounds = Array.Empty(); + + public readonly bool UsePlayerPalette = false; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + foreach (var b in Buildings) + { + var actorInfo = rules.Actors[b]; + var buildingInfo = actorInfo.TraitInfoOrDefault(); + + if (buildingInfo == null) + throw new YamlException($"SpawnBuildingWarhead cannot be used to spawn nonbuilding actor '{b}'"); + } + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var map = firedBy.World.Map; + var targetCell = map.CellContaining(target.CenterPosition); + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var targetCells = map.FindTilesInCircle(targetCell, Range); + var cell = targetCells.GetEnumerator(); + var alreadyusedcells = new HashSet(); + + foreach (var b in Buildings) + { + var actorInfo = firedBy.World.Map.Rules.Actors[b]; + var buildingInfo = actorInfo.TraitInfo(); + + var td = new TypeDictionary(); + if (OwnerType == ASOwnerType.Attacker) + td.Add(new OwnerInit(firedBy.Owner)); + else + td.Add(new OwnerInit(firedBy.World.Players.First(p => p.InternalName == InternalOwner))); + + if (LinkToParent) + td.Add(new ParentActorInit(firedBy)); + + while (cell.MoveNext()) + { + if (!buildingInfo.Tiles(cell.Current).Any(c => alreadyusedcells.Contains(c)) && + firedBy.World.CanPlaceBuilding(cell.Current, actorInfo, buildingInfo, null)) + { + td.Add(new LocationInit(cell.Current)); + + if (SkipMakeAnims) + td.Add(new SkipMakeAnimsInit()); + + alreadyusedcells.Concat(buildingInfo.Tiles(cell.Current)); + + firedBy.World.AddFrameEndTask(w => + { + var building = w.CreateActor(b, td); + + var palette = Palette; + if (UsePlayerPalette) + palette += building.Owner.InternalName; + + if (Image != null) + w.Add(new SpriteEffect(building.CenterPosition, w, Image, Sequence, palette)); + + var sound = Sounds.RandomOrDefault(firedBy.World.LocalRandom); + if (sound != null) + Game.Sound.Play(SoundType.World, sound, building.CenterPosition); + }); + + break; + } + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/SpawnSmokeParticleWarhead.cs b/OpenRA.Mods.AS/Warheads/SpawnSmokeParticleWarhead.cs new file mode 100644 index 000000000000..b223526171e0 --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/SpawnSmokeParticleWarhead.cs @@ -0,0 +1,160 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using OpenRA.GameRules; +using OpenRA.Mods.AS.Effects; +using OpenRA.Mods.AS.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + public class SpawnSmokeParticleWarhead : WarheadAS, IRulesetLoaded, ISmokeParticleInfo + { + [Desc("Amount of particles spawned. Two values mean actual amount will vary between them.")] + public readonly int[] Count = { 1 }; + + [FieldLoader.Require] + [Desc("The duration of an individual particle. Two values mean actual lifetime will vary between them.")] + public readonly int[] Duration; + + [Desc("Randomize particle forward movement.")] + public readonly WDist[] Speed = { WDist.Zero }; + + [Desc("Randomize particle gravity.")] + public readonly WDist[] Gravity = { WDist.Zero }; + + [Desc("Randomize particle turnrate.")] + public readonly int TurnRate = 0; + + [Desc("Rate to reset particle movement properties.")] + public readonly int RandomRate = 4; + + [Desc("Which image to use.")] + public readonly string Image = "particles"; + + [SequenceReference(nameof(Image))] + [Desc("Which sequence to use when the smoke starts.")] + public readonly string[] StartSequences = Array.Empty(); + + [FieldLoader.Require] + [SequenceReference(nameof(Image))] + [Desc("Which sequence to use while smoke is active.")] + public readonly string[] Sequences = Array.Empty(); + + [SequenceReference(nameof(Image))] + [Desc("Which sequence to use when the smoke ends.")] + public readonly string[] EndSequences = Array.Empty(); + + [PaletteReference(nameof(IsPlayerPalette))] + [Desc("Which palette to use.")] + public readonly string Palette = null; + + public readonly bool IsPlayerPalette = false; + + [Desc("Defines particle ownership (invoker if unset).")] + public readonly bool Neutral = false; + + [WeaponReference] + [Desc("Has to be defined in weapons.yaml, if defined, as well.")] + public readonly string Weapon = null; + + WeaponInfo weapon; + + string ISmokeParticleInfo.Image + { + get { return Image; } + } + + string[] ISmokeParticleInfo.StartSequences + { + get { return StartSequences; } + } + + string[] ISmokeParticleInfo.Sequences + { + get { return Sequences; } + } + + string[] ISmokeParticleInfo.EndSequences + { + get { return EndSequences; } + } + + string ISmokeParticleInfo.Palette + { + get { return Palette; } + } + + bool ISmokeParticleInfo.IsPlayerPalette + { + get { return IsPlayerPalette; } + } + + WDist[] ISmokeParticleInfo.Speed + { + get { return Speed; } + } + + WDist[] ISmokeParticleInfo.Gravity + { + get { return Gravity; } + } + + int[] ISmokeParticleInfo.Duration + { + get { return Duration; } + } + + WeaponInfo ISmokeParticleInfo.Weapon + { + get { return weapon; } + } + + int ISmokeParticleInfo.TurnRate + { + get { return TurnRate; } + } + + int ISmokeParticleInfo.RandomRate + { + get { return RandomRate; } + } + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (string.IsNullOrEmpty(Weapon)) + return; + + if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var count = Count.Length == 2 + ? firedBy.World.SharedRandom.Next(Count[0], Count[1]) + : Count[0]; + + // Lambdas can't use 'in' variables, so capture a copy for later + var delayedTarget = target; + + for (var i = 0; i < count; i++) + firedBy.World.AddFrameEndTask(w => w.Add(new SmokeParticle(Neutral || firedBy.IsDead ? firedBy.World.WorldActor : firedBy, this, delayedTarget.CenterPosition))); + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/TriggerLayerWeaponWarhead.cs b/OpenRA.Mods.AS/Warheads/TriggerLayerWeaponWarhead.cs new file mode 100644 index 000000000000..6bc0665d5a51 --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/TriggerLayerWeaponWarhead.cs @@ -0,0 +1,145 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("Works like Inferno Canon like in CnC Generals:, used by TA.")] + public sealed class TriggerLayerWeaponWarhead : WarheadAS, IRulesetLoaded + { + [Desc("Range between falloff steps in cells.")] + public readonly WDist Spread = new(1024); + + [Desc("Level percentage at each range step.")] + public readonly int[] Falloff = { 100, 75, 50 }; + + [Desc("The name of the layer we want to increase the level of.")] + public readonly string LayerName = ""; + + [Desc("Ranges at which each Falloff step is defined (in cells). Overrides Spread.")] + public WDist[] Range = null; + + [Desc("Level this weapon puts on the ground. Accumulates over previously trigger area.")] + public int Level = 200; + + [Desc("It saturates at this level, by this weapon.")] + public int MaxLevel = 600; + + [Desc("Allow triggering effects when the impacted cell has the value in [TriggerAtLevelMax, TriggerAtLevelMin]")] + public bool AllowTriggerLevel = true; + + [Desc("Allows a triggering effect: cells (affected by Falloff and Range) set to a specific level defined by TriggerSetLevel")] + public bool AllowSetLevelWhenTrigger = true; + + [Desc("Allows a triggering effect: impacted cell explode a weapon")] + public bool AllowTriggerWeaponWhenTrigger = true; + + [Desc("Impacted cell has the value in [TriggerAtLevelMax, TriggerAtLevelMin] to trigger effect. Requires \"AllowTriggerLevel = true\".")] + public int TriggerAtLevelMax = int.MaxValue; + + [Desc("Impacted cell has the value in [TriggerAtLevelMax, TriggerAtLevelMin] to trigger effect. Requires \"AllowTriggerLevel = true\".")] + public int TriggerAtLevelMin = int.MinValue; + + [Desc("Cells (affected by Falloff and Range) set to this level when trigger. Requires \"AllowTriggerLevel = true\" and \"AllowSetLevelWhenTrigger = true\"")] + public int TriggerSetLevel = 0; + + [WeaponReference] + [Desc("Impacted cell explode a weapon when trigger. Has to be defined in weapons.yaml as well.")] + public readonly string TriggerWeapon = null; + + WeaponInfo weapon; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (Range == null) + Range = Exts.MakeArray(Falloff.Length, i => i * Spread); + else + { + if (Range.Length != 1 && Range.Length != Falloff.Length) + throw new YamlException("Number of range values must be 1 or equal to the number of Falloff values."); + + for (var i = 0; i < Range.Length - 1; i++) + if (Range[i] > Range[i + 1]) + throw new YamlException("Range values must be specified in an increasing order."); + } + + if (AllowTriggerLevel && AllowTriggerWeaponWhenTrigger && !rules.Weapons.TryGetValue(TriggerWeapon.ToLowerInvariant(), out weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{TriggerWeapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + var world = firedBy.World; + + if (world.LocalPlayer != null) + { + var devMode = world.LocalPlayer.PlayerActor.TraitOrDefault(); + if (devMode != null && devMode.CombatGeometry) + { + var rng = Exts.MakeArray(Range.Length, i => WDist.FromCells(Range[i].Length)); + world.WorldActor.Trait().AddImpact(target.CenterPosition, rng, Primitives.Color.Gold); + } + } + + var targetTile = world.Map.CellContaining(target.CenterPosition); + var raLayer = world.WorldActor.TraitsImplementing() + .First(l => l.Info.Name == LayerName); + + var triggeredSetLevel = false; + if (AllowTriggerLevel && + raLayer.GetLevel(targetTile) >= TriggerAtLevelMin && + raLayer.GetLevel(targetTile) <= TriggerAtLevelMax) + { + if (AllowTriggerWeaponWhenTrigger) + weapon.Impact(Target.FromPos(target.CenterPosition), firedBy); + + var affectedCells = world.Map.FindTilesInCircle(targetTile, (int)Math.Ceiling((decimal)Range[^1].Length / 1024)); + if (AllowSetLevelWhenTrigger) + { + triggeredSetLevel = true; + foreach (var cell in affectedCells) + raLayer.SetLevel(cell, TriggerSetLevel); + } + } + + if (!triggeredSetLevel && Level != 0) + { + var affectedCells = world.Map.FindTilesInCircle(targetTile, (int)Math.Ceiling((decimal)Range[^1].Length / 1024)); + foreach (var cell in affectedCells) + { + var mul = GetIntensityFalloff((target.CenterPosition - world.Map.CenterOfCell(cell)).Length); + raLayer.IncreaseLevel(cell, Level * mul / 100, MaxLevel); + } + } + } + + int GetIntensityFalloff(int distance) + { + var inner = Range[0].Length; + for (var i = 1; i < Range.Length; i++) + { + var outer = Range[i].Length; + if (outer > distance) + return int2.Lerp(Falloff[i - 1], Falloff[i], distance - inner, outer - inner); + + inner = outer; + } + + return 0; + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/WarheadAS.cs b/OpenRA.Mods.AS/Warheads/WarheadAS.cs new file mode 100644 index 000000000000..64d53051c71d --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/WarheadAS.cs @@ -0,0 +1,85 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Warheads; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Warheads +{ + [Desc("AS warhead extension class." + + "These warheads check for the Air TargetType when detonated inair!")] + public abstract class WarheadAS : Warhead + { + [Desc("Whether to consider actors in determining whether the explosion should happen. If false, only terrain will be considered.")] + public readonly bool ImpactActors = true; + + static readonly BitSet TargetTypeAir = new("Air"); + + protected enum ImpactActorType + { + None, + Invalid, + Valid, + } + + /// Checks if there are any actors at impact position and if the warhead is valid against any of them. + protected ImpactActorType ActorTypeAtImpact(World world, WPos pos, Actor firedBy) + { + var anyInvalidActor = false; + + // Check whether the impact position overlaps with an actor's hitshape + var potentialVictims = world.FindActorsOnCircle(pos, WDist.Zero); + foreach (var victim in potentialVictims) + { + if (!AffectsParent && victim == firedBy) + continue; + + var activeShapes = victim.TraitsImplementing().Where(Exts.IsTraitEnabled); + if (!activeShapes.Any(s => s.DistanceFromEdge(victim, pos).Length <= 0)) + continue; + + if (IsValidAgainst(victim, firedBy)) + return ImpactActorType.Valid; + + anyInvalidActor = true; + } + + return anyInvalidActor ? ImpactActorType.Invalid : ImpactActorType.None; + } + + /// Checks if the warhead is valid against the terrain at impact position. + protected bool IsValidAgainstTerrain(World world, WPos pos) + { + var cell = world.Map.CellContaining(pos); + if (!world.Map.Contains(cell)) + return false; + + var dat = world.Map.DistanceAboveTerrain(pos); + return IsValidTarget(dat > AirThreshold ? TargetTypeAir : world.Map.GetTerrainInfo(cell).TargetTypes); + } + + protected bool IsValidImpact(WPos pos, Actor firedBy) + { + var actorAtImpact = ImpactActors ? ActorTypeAtImpact(firedBy.World, pos, firedBy) : ImpactActorType.None; + + // If there's either a) an invalid actor, or b) no actor and invalid terrain, we don't trigger the effect(s). + if (actorAtImpact == ImpactActorType.Invalid) + return false; + else if (actorAtImpact == ImpactActorType.None && !IsValidAgainstTerrain(firedBy.World, pos)) + return false; + + return true; + } + } +} diff --git a/OpenRA.Mods.AS/Widgets/ActorIconWidget.cs b/OpenRA.Mods.AS/Widgets/ActorIconWidget.cs new file mode 100644 index 000000000000..9889ce1ee1ff --- /dev/null +++ b/OpenRA.Mods.AS/Widgets/ActorIconWidget.cs @@ -0,0 +1,322 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Mods.Common.Widgets; +using OpenRA.Traits; +using OpenRA.Widgets; + +namespace OpenRA.Mods.AS.Widgets +{ + public class BasicUnit + { + public readonly Actor Actor; + public readonly ActorInfo ActorInfo; + public readonly Tooltip[] Tooltips; + public readonly TooltipDescription[] TooltipDescriptions; + public readonly BuildableInfo BuildableInfo; + + public BasicUnit(Actor actor, ActorInfo actorInfo) + { + Actor = actor; + ActorInfo = actorInfo ?? actor.Info; + + Tooltips = actor?.TraitsImplementing().ToArray(); + TooltipDescriptions = actor?.TraitsImplementing().ToArray(); + BuildableInfo = ActorInfo.TraitInfos().FirstOrDefault(); + } + } + + public class ActorIconWidget : Widget + { + public readonly int2 IconSize; + public readonly int2 IconPos; + public readonly float IconScale = 1f; + public readonly string NoIconImage = "icon"; + public readonly string NoIconSequence = "xxicon"; + public readonly string NoIconPalette = "chrome"; + public readonly string DefaultIconImage = "icon"; + public readonly string DefaultIconSequence = "xxicon"; + public readonly string DefaultIconPalette = "chrome"; + public readonly string DisabledOverlayImage = "clock"; + public readonly string DisabledOverlaySequence = "idle"; + public readonly string DisabledOverlayPalette = "chrome"; + + public readonly string TooltipTemplate = "ACTOR_ICON_TOOLTIP"; + public readonly string TooltipContainer; + + public readonly string ClickSound = ChromeMetrics.Get("ClickSound"); + public readonly string ClickDisabledSound = ChromeMetrics.Get("ClickDisabledSound"); + + public BasicUnit TooltipUnit { get; private set; } + public Func GetTooltipUnit; + + readonly ModData modData; + readonly WorldRenderer worldRenderer; + Animation icon; + readonly Animation disabledOverlay; + ActorStatValues stats; + readonly Lazy tooltipContainer; + + Player player; + readonly World world; + /* readonly float2 iconOffset;*/ + + public Func GetActor = () => null; + Actor actor = null; + + public Func GetActorInfo = () => null; + ActorInfo actorInfo = null; + + public Func GetDisabled = () => false; + bool isDisabled = false; + + string currentPalette; + bool currentPaletteIsPlayerPalette; + readonly ISelection selection; + + [ObjectCreator.UseCtor] + public ActorIconWidget(ModData modData, World world, WorldRenderer worldRenderer) + { + this.modData = modData; + this.world = world; + this.worldRenderer = worldRenderer; + selection = world.WorldActor.Trait(); + + /*iconOffset = 0.5f * IconSize.ToFloat2() + IconPos;*/ + + currentPalette = NoIconPalette; + currentPaletteIsPlayerPalette = false; + icon = new Animation(world, NoIconImage); + icon.Play(NoIconSequence); + + GetTooltipUnit = () => TooltipUnit; + tooltipContainer = Exts.Lazy(() => + Ui.Root.Get(TooltipContainer)); + + disabledOverlay = new Animation(world, DisabledOverlayImage); + disabledOverlay.PlayFetchIndex(DisabledOverlaySequence, () => 0); + } + + protected ActorIconWidget(ActorIconWidget other) + : base(other) + { + modData = other.modData; + world = other.world; + worldRenderer = other.worldRenderer; + selection = other.selection; + + IconSize = other.IconSize; + IconPos = other.IconPos; + IconScale = other.IconScale; + NoIconImage = other.NoIconImage; + NoIconSequence = other.NoIconSequence; + NoIconPalette = other.NoIconPalette; + DefaultIconImage = other.DefaultIconImage; + DefaultIconSequence = other.DefaultIconSequence; + DefaultIconPalette = other.DefaultIconPalette; + DisabledOverlayImage = other.DisabledOverlayImage; + DisabledOverlaySequence = other.DisabledOverlaySequence; + DisabledOverlayPalette = other.DisabledOverlayPalette; + + ClickSound = other.ClickSound; + ClickDisabledSound = other.ClickDisabledSound; + + icon = other.icon; + disabledOverlay = other.disabledOverlay; + + TooltipUnit = other.TooltipUnit; + GetTooltipUnit = () => TooltipUnit; + + TooltipTemplate = other.TooltipTemplate; + TooltipContainer = other.TooltipContainer; + + tooltipContainer = Exts.Lazy(() => + Ui.Root.Get(TooltipContainer)); + } + + public override void Initialize(WidgetArgs args) + { + base.Initialize(args); + } + + public void RefreshIcons() + { + actor = GetActor(); + actorInfo = GetActorInfo(); + isDisabled = GetDisabled(); + if ((actor == null || !actor.IsInWorld || actor.IsDead || actor.Disposed) && actorInfo == null) + { + currentPalette = NoIconPalette; + currentPaletteIsPlayerPalette = false; + icon = new Animation(world, NoIconImage); + icon.Play(NoIconSequence); + player = null; + TooltipUnit = null; + stats = null; + return; + } + + if (actorInfo == null) + { + player = actor.Owner; + var rs = actor.TraitOrDefault(); + if (rs == null) + { + currentPalette = DefaultIconPalette; + currentPaletteIsPlayerPalette = false; + icon = new Animation(world, DefaultIconImage); + icon.Play(DefaultIconSequence); + return; + } + + stats = actor.TraitOrDefault(); + if (!string.IsNullOrEmpty(stats.Icon)) + { + currentPaletteIsPlayerPalette = stats.IconPaletteIsPlayerPalette; + currentPalette = currentPaletteIsPlayerPalette ? stats.IconPalette + player.InternalName : stats.IconPalette; + icon = new Animation(world, stats.DisguiseImage ?? rs.GetImage(actor)); + icon.Play(stats.Icon); + } + else + { + currentPalette = DefaultIconPalette; + currentPaletteIsPlayerPalette = false; + icon = new Animation(world, DefaultIconImage); + icon.Play(DefaultIconSequence); + } + + TooltipUnit = new BasicUnit(actor, stats.TooltipActor); + } + else + { + var rsi = actorInfo.TraitInfoOrDefault(); + var bi = actorInfo.TraitInfos().FirstOrDefault(); + if (rsi == null || bi == null) + { + currentPalette = DefaultIconPalette; + currentPaletteIsPlayerPalette = false; + icon = new Animation(world, DefaultIconImage); + icon.Play(DefaultIconSequence); + return; + } + + currentPaletteIsPlayerPalette = bi.IconPaletteIsPlayerPalette; + currentPalette = currentPaletteIsPlayerPalette ? bi.IconPalette + player.InternalName : bi.IconPalette; + icon = new Animation(world, rsi.GetImage(actorInfo, "default")); + icon.Play(bi.Icon); + + TooltipUnit = new BasicUnit(null, actorInfo); + } + } + + public override void Draw() + { + Game.Renderer.EnableAntialiasingFilter(); + + if (icon.Image != null) + WidgetUtils.DrawSpriteCentered(icon.Image, worldRenderer.Palette(currentPalette), IconPos + 0.5f * IconSize.ToFloat2() + RenderBounds.Location, IconScale); + + if (stats != null) + { + foreach (var iconOverlay in stats.IconOverlays.Where(io => !io.IsTraitDisabled)) + { + var palette = iconOverlay.Info.IsPlayerPalette ? iconOverlay.Info.Palette + player.InternalName : iconOverlay.Info.Palette; + WidgetUtils.DrawSpriteCentered(iconOverlay.Sprite, worldRenderer.Palette(palette), IconPos + 0.5f * IconSize.ToFloat2() + RenderBounds.Location + iconOverlay.GetOffset(IconSize, IconScale), IconScale); + } + } + + if (isDisabled) + WidgetUtils.DrawSpriteCentered(disabledOverlay.Image, worldRenderer.Palette(DisabledOverlayPalette), IconPos + 0.5f * IconSize.ToFloat2() + RenderBounds.Location, IconScale); + + Game.Renderer.DisableAntialiasingFilter(); + } + + public override void Tick() + { + RefreshIcons(); + } + + public override void MouseEntered() + { + if (TooltipContainer != null) + { + tooltipContainer.Value.SetTooltip(TooltipTemplate, + new WidgetArgs() { { "player", world.LocalPlayer }, { "getTooltipUnit", GetTooltipUnit }, { "world", world } }); + } + } + + public override void MouseExited() + { + if (TooltipContainer != null) + tooltipContainer.Value.RemoveTooltip(); + } + + public override bool HandleMouseInput(MouseInput mi) + { + if (mi.Event == MouseInputEvent.Down) + { + if (actor == null) + { + Game.Sound.PlayNotification(world.Map.Rules, null, "Sounds", ClickDisabledSound, null); + } + else + { + if (mi.Button == MouseButton.Left) + { + if (mi.Modifiers.HasModifier(Modifiers.Ctrl)) + { + foreach (var selected in selection.Actors) + { + if (TooltipUnit.ActorInfo.Name != selected.Info.Name) + selection.Remove(selected); + } + } + else if (mi.Modifiers.HasModifier(Modifiers.Shift)) + { + selection.Clear(); + selection.Add(actor); + } + else + { + worldRenderer.Viewport.Center(actor.CenterPosition); + } + + Game.Sound.PlayNotification(world.Map.Rules, null, "Sounds", ClickSound, null); + } + else if (mi.Button == MouseButton.Right) + { + selection.Remove(actor); + if (mi.Modifiers.HasModifier(Modifiers.Ctrl)) + { + foreach (var selected in selection.Actors) + { + if (TooltipUnit.ActorInfo.Name == selected.Info.Name) + selection.Remove(selected); + } + } + + Game.Sound.PlayNotification(world.Map.Rules, null, "Sounds", ClickSound, null); + } + } + } + + return true; + } + + public override Widget Clone() { return new ActorIconWidget(this); } + } +} diff --git a/OpenRA.Mods.AS/Widgets/HealthBarWidget.cs b/OpenRA.Mods.AS/Widgets/HealthBarWidget.cs new file mode 100644 index 000000000000..02faaa6b4d34 --- /dev/null +++ b/OpenRA.Mods.AS/Widgets/HealthBarWidget.cs @@ -0,0 +1,128 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Globalization; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Widgets; +using OpenRA.Primitives; +using OpenRA.Traits; +using OpenRA.Widgets; + +namespace OpenRA.Mods.AS.Widgets +{ + public class HealthBarWidget : Widget + { + public string Background = "progressbar-bg"; + public string EmptyHealthBar = "progressbar-thumb-empty"; + public string RedHealthBar = "progressbar-thumb-red"; + public string YellowHealthBar = "progressbar-thumb-yellow"; + public string GreenHealthBar = "progressbar-thumb-green"; + public Size BarMargin = new(2, 2); + public int HealthDivisor = 1; + + public Func GetHealth = () => null; + IHealth health; + + public Func GetScale = () => 1f; + float scale; + + LabelWidget label; + bool labelChecked; + + public HealthBarWidget() { } + + protected HealthBarWidget(HealthBarWidget other) + : base(other) + { + Background = other.Background; + EmptyHealthBar = other.EmptyHealthBar; + RedHealthBar = other.RedHealthBar; + YellowHealthBar = other.YellowHealthBar; + GreenHealthBar = other.GreenHealthBar; + BarMargin = other.BarMargin; + HealthDivisor = other.HealthDivisor; + + GetHealth = other.GetHealth; + health = other.health; + } + + public override void Draw() + { + var rb = RenderBounds; + WidgetUtils.DrawPanel(Background, rb); + + var percentage = GetPercentage(); + var bar = GetBar(percentage); + + var minBarWidth = ChromeProvider.GetMinimumPanelSize(bar).Width; + var maxBarWidth = rb.Width - BarMargin.Width * 2; + var barWidth = percentage * maxBarWidth / 100; + barWidth = Math.Max(barWidth, minBarWidth); + + var barRect = new Rectangle(rb.X + BarMargin.Width, rb.Y + BarMargin.Height, barWidth, rb.Height - 2 * BarMargin.Height); + WidgetUtils.DrawPanel(bar, barRect); + } + + public override void Tick() + { + health = GetHealth(); + scale = GetScale(); + + if (!labelChecked) + { + label = GetOrNull("HEALTH_LABEL"); + if (label != null) + label.GetText = () => GetText(); + + labelChecked = true; + } + } + + string GetBar(int percentage) + { + var bar = EmptyHealthBar; + if (health != null) + { + if (percentage <= 25) + return RedHealthBar; + else if (percentage <= 50) + return YellowHealthBar; + else + return GreenHealthBar; + } + + return bar; + } + + int GetPercentage() + { + if (health == null) + return 0; + + var healthValue = health.HP; + var maxHealthValue = health.MaxHP; + return 100 - (int)((float)(maxHealthValue - healthValue) / maxHealthValue * 100); + } + + string GetText() + { + if (health == null) + return ""; + + var healthValue = Math.Round(health.HP * scale / HealthDivisor, 0); + var maxHealthValue = Math.Round(health.MaxHP * scale / HealthDivisor, 0); + return healthValue.ToString(NumberFormatInfo.CurrentInfo) + " / " + maxHealthValue.ToString(NumberFormatInfo.CurrentInfo); + } + + public override Widget Clone() { return new HealthBarWidget(this); } + } +} diff --git a/OpenRA.Mods.AS/Widgets/Logic/ASCreditsLogic.cs b/OpenRA.Mods.AS/Widgets/Logic/ASCreditsLogic.cs new file mode 100644 index 000000000000..5a2de1d01e38 --- /dev/null +++ b/OpenRA.Mods.AS/Widgets/Logic/ASCreditsLogic.cs @@ -0,0 +1,113 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OpenRA.Mods.Common.Widgets; +using OpenRA.Widgets; + +namespace OpenRA.Mods.AS.Widgets.Logic +{ + public enum ASCreditsState + { + Engine, + AS, + Mod + } + + public class ASCreditsLogic : ChromeLogic + { + readonly ScrollPanelWidget scrollPanel; + readonly LabelWidget template; + + readonly IEnumerable modLines; + readonly IEnumerable engineLines; + readonly IEnumerable asEngineLines; + ASCreditsState tabState; + + [ObjectCreator.UseCtor] + public ASCreditsLogic(Widget widget, ModData modData, Action onExit) + { + var panel = widget.Get("CREDITS_PANEL"); + + panel.Get("BACK_BUTTON").OnClick = () => + { + Ui.CloseWindow(); + onExit(); + }; + + engineLines = ParseLines(File.OpenRead(Platform.ResolvePath("./AUTHORS"))); + asEngineLines = ParseLines(File.OpenRead(Platform.ResolvePath("./AUTHORS.AS"))); + + var tabContainer = panel.Get("TAB_CONTAINER"); + var modTab = tabContainer.Get("MOD_TAB"); + modTab.IsHighlighted = () => tabState == ASCreditsState.Mod; + modTab.OnClick = () => ShowCredits(ASCreditsState.Mod); + + var engineTab = tabContainer.Get("ENGINE_TAB"); + engineTab.IsHighlighted = () => tabState == ASCreditsState.Engine; + engineTab.OnClick = () => ShowCredits(ASCreditsState.Engine); + + var asTab = tabContainer.Get("ASENGINE_TAB"); + asTab.IsHighlighted = () => tabState == ASCreditsState.AS; + asTab.OnClick = () => ShowCredits(ASCreditsState.AS); + + scrollPanel = panel.Get("CREDITS_DISPLAY"); + template = scrollPanel.Get("CREDITS_TEMPLATE"); + + // Make space to show the tabs + tabContainer.IsVisible = () => true; + scrollPanel.Bounds.Y += tabContainer.Bounds.Height; + scrollPanel.Bounds.Height -= tabContainer.Bounds.Height; + + var hasModCredits = modData.Manifest.Contains(); + if (hasModCredits) + { + var modCredits = modData.Manifest.Get(); + modLines = ParseLines(modData.DefaultFileSystem.Open(modCredits.ModCreditsFile)); + modTab.GetText = () => modCredits.ModTabTitle; + } + + if (hasModCredits) + ShowCredits(ASCreditsState.Mod); + else + ShowCredits(ASCreditsState.AS); + } + + void ShowCredits(ASCreditsState credits) + { + tabState = credits; + + scrollPanel.RemoveChildren(); + + IEnumerable lines; + if (credits == ASCreditsState.Engine) + lines = engineLines; + else if (credits == ASCreditsState.AS) + lines = asEngineLines; + else + lines = modLines; + + foreach (var line in lines) + { + var label = template.Clone() as LabelWidget; + label.GetText = () => line; + scrollPanel.AddChild(label); + } + } + + static IEnumerable ParseLines(Stream file) + { + return file.ReadAllLines().Select(l => l.Replace("\t", " ").Replace("*", "\u2022").Replace(">", "\u2023")).ToList(); + } + } +} diff --git a/OpenRA.Mods.AS/Widgets/Logic/Ingame/ActorIconTooltipLogic.cs b/OpenRA.Mods.AS/Widgets/Logic/Ingame/ActorIconTooltipLogic.cs new file mode 100644 index 000000000000..30bf53bbb804 --- /dev/null +++ b/OpenRA.Mods.AS/Widgets/Logic/Ingame/ActorIconTooltipLogic.cs @@ -0,0 +1,97 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Widgets; +using OpenRA.Traits; +using OpenRA.Widgets; + +namespace OpenRA.Mods.AS.Widgets.Logic +{ + public class ActorIconTooltipLogic : ChromeLogic + { + [ObjectCreator.UseCtor] + public ActorIconTooltipLogic(Widget widget, TooltipContainerWidget tooltipContainer, Func getTooltipUnit) + { + widget.IsVisible = () => getTooltipUnit() != null; + var nameLabel = widget.Get("NAME"); + var descLabel = widget.Get("DESC"); + + var font = Game.Renderer.Fonts[nameLabel.Font]; + var descFont = Game.Renderer.Fonts[descLabel.Font]; + + BasicUnit lastUnit = null; + var descLabelPadding = descLabel.Bounds.Height; + + tooltipContainer.BeforeRender = () => + { + var unit = getTooltipUnit(); + + if (unit == null || unit == lastUnit) + return; + + var world = unit.Actor?.World; + var stance = world?.RenderPlayer == null ? PlayerRelationship.None : unit.Actor?.Owner.RelationshipWith(world.RenderPlayer); + var tooltip = unit.Tooltips?.FirstEnabledTraitOrDefault(); + var name = tooltip?.TooltipInfo.TooltipForPlayerStance(stance.Value) ?? unit.ActorInfo.TraitInfos().FirstOrDefault().Name; + name ??= unit.Actor?.Info.Name ?? unit.ActorInfo.Name; + var buildable = unit.BuildableInfo; + var tooltipDescs = unit.TooltipDescriptions?.Where(td => td.IsTooltipVisible(world.RenderPlayer ?? world.LocalPlayer)); + + nameLabel.Text = name; + + var nameSize = font.Measure(name); + + var descSize = new int2(0, 0); + if (tooltipDescs != null && tooltipDescs.Any()) + { + var descText = ""; + foreach (var tooltipDesc in tooltipDescs) + { + if (!string.IsNullOrEmpty(descText)) + descText += "\n"; + + descText += tooltipDesc.TooltipText.Replace("\\n", "\n"); + } + + descLabel.Text = descText; + descSize = descFont.Measure(descLabel.Text); + descLabel.Bounds.Width = descSize.X; + descLabel.Bounds.Height = descSize.Y + descLabelPadding; + } + else if (buildable != null && !string.IsNullOrEmpty(buildable.Description)) + { + descLabel.Text = buildable.Description.Replace("\\n", "\n"); + descSize = descFont.Measure(descLabel.Text); + descLabel.Bounds.Width = descSize.X; + descLabel.Bounds.Height = descSize.Y + descLabelPadding; + } + else + { + descLabel.Bounds.Height = 0; + } + + var leftWidth = Math.Max(nameSize.X, descSize.X); + + widget.Bounds.Width = leftWidth + 2 * nameLabel.Bounds.X; + + // Set the bottom margin to match the left margin + var leftHeight = descLabel.Bounds.Bottom + descLabel.Bounds.X; + + widget.Bounds.Height = leftHeight; + + lastUnit = unit; + }; + } + } +} diff --git a/OpenRA.Mods.AS/Widgets/Logic/Ingame/CollapsableWidgetLogic.cs b/OpenRA.Mods.AS/Widgets/Logic/Ingame/CollapsableWidgetLogic.cs new file mode 100644 index 000000000000..8c43791c666c --- /dev/null +++ b/OpenRA.Mods.AS/Widgets/Logic/Ingame/CollapsableWidgetLogic.cs @@ -0,0 +1,46 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Widgets; +using OpenRA.Widgets; + +namespace OpenRA.Mods.AS.Widgets.Logic +{ + class CollapsableWidgetLogic : ChromeLogic + { + [ObjectCreator.UseCtor] + public CollapsableWidgetLogic(Widget widget) + { + var closeButton = widget.Get("CLOSE_BUTTON"); + var openButton = widget.Get("OPEN_BUTTON"); + var closedBackground = widget.GetOrNull("CLOSED_BACKGROUND"); + var openedBackground = widget.Get("OPENED_BACKGROUND"); + + closeButton.OnClick = () => + { + openButton.Visible = true; + openedBackground.Visible = false; + closeButton.Visible = false; + if (closedBackground != null) + closedBackground.Visible = true; + }; + + openButton.OnClick = () => + { + openButton.Visible = false; + openedBackground.Visible = true; + closeButton.Visible = true; + if (closedBackground != null) + closedBackground.Visible = false; + }; + } + } +} diff --git a/OpenRA.Mods.AS/Widgets/Logic/Ingame/IngameActorStatsLogic.cs b/OpenRA.Mods.AS/Widgets/Logic/Ingame/IngameActorStatsLogic.cs new file mode 100644 index 000000000000..168862455856 --- /dev/null +++ b/OpenRA.Mods.AS/Widgets/Logic/Ingame/IngameActorStatsLogic.cs @@ -0,0 +1,395 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.AS.Widgets; +using OpenRA.Traits; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets.Logic +{ + public class IngameActorStatsLogic : ChromeLogic + { + [ObjectCreator.UseCtor] + public IngameActorStatsLogic(Widget widget, World world, Dictionary logicArgs) + { + var selection = world.WorldActor.Trait(); + + var largeIcons = new List { widget.Get("STAT_ICON") }; + var largeHealthBars = new List { widget.Get("STAT_HEALTH_BAR") }; + var largeIconCount = 1; + var largeIconSpacing = new int2(2, 2); + if (logicArgs.TryGetValue("LargeIconCount", out var largeIconCountEntry)) + largeIconCount = FieldLoader.GetValue("LargeIconCount", largeIconCountEntry.Value); + if (logicArgs.TryGetValue("LargeIconSpacing", out var largeIconSpacingEntry)) + largeIconSpacing = FieldLoader.GetValue("LargeIconSpacing", largeIconSpacingEntry.Value); + if (largeIconCount > 1) + { + for (var i = 1; i < largeIconCount; i++) + { + var iconClone = largeIcons[0].Clone() as ActorIconWidget; + iconClone.Bounds.X += (iconClone.IconSize.X + largeIconSpacing.X) * i; + + widget.AddChild(iconClone); + largeIcons.Add(iconClone); + + var healthBarClone = largeHealthBars[0].Clone() as HealthBarWidget; + healthBarClone.Bounds.X += (healthBarClone.Bounds.Width + largeIconSpacing.X) * i; + + widget.AddChild(healthBarClone); + largeHealthBars.Add(healthBarClone); + } + } + + var smallIcons = new List(); + var smallHealthBars = new List(); + var smallIconCount = 0; + var smallIconSpacing = new int2(0, 5); + var smallIconRows = 6; + if (logicArgs.TryGetValue("SmallIconCount", out var smallIconCountEntry)) + smallIconCount = FieldLoader.GetValue("SmallIconCount", smallIconCountEntry.Value); + if (logicArgs.TryGetValue("SmallIconSpacing", out var smallIconSpacingEntry)) + smallIconSpacing = FieldLoader.GetValue("SmallIconSpacing", smallIconSpacingEntry.Value); + if (logicArgs.TryGetValue("SmallIconRows", out var smallIconRowsEntry)) + smallIconRows = FieldLoader.GetValue("SmallIconRows", smallIconRowsEntry.Value); + if (smallIconCount > 0) + { + smallIcons.Add(widget.Get("STAT_ICON_SMALL")); + smallHealthBars.Add(widget.Get("STAT_HEALTH_BAR_SMALL")); + for (var i = 1; i < largeIconCount + smallIconCount; i++) + { + var iconClone = smallIcons[0].Clone() as ActorIconWidget; + iconClone.Bounds.X += (iconClone.IconSize.X + smallIconSpacing.X) * (i % smallIconRows); + iconClone.Bounds.Y += (iconClone.IconSize.Y + smallIconSpacing.Y) * (i / smallIconRows); + + widget.AddChild(iconClone); + smallIcons.Add(iconClone); + + var healthBarClone = smallHealthBars[0].Clone() as HealthBarWidget; + healthBarClone.Bounds.X += (iconClone.IconSize.X + smallIconSpacing.X) * (i % smallIconRows); + healthBarClone.Bounds.Y += (iconClone.IconSize.Y + smallIconSpacing.Y) * (i / smallIconRows); + + widget.AddChild(healthBarClone); + smallHealthBars.Add(healthBarClone); + } + } + + var upgradeIcons = new List { widget.GetOrNull("STAT_ICON_UPGRADE") }; + if (upgradeIcons[0] != null) + { + var upgradeIconCount = 5; + var upgradeIconSpacing = new int2(0, 5); + + if (logicArgs.TryGetValue("UpgradeIconCount", out var upgradeIconCountEntry)) + upgradeIconCount = FieldLoader.GetValue("UpgradeIconCount", upgradeIconCountEntry.Value); + if (logicArgs.TryGetValue("UpgradeIconSpacing", out var upgradeIconSpacingEntry)) + upgradeIconSpacing = FieldLoader.GetValue("UpgradeIconSpacing", upgradeIconSpacingEntry.Value); + + if (upgradeIconCount > 1) + { + for (var i = 1; i < upgradeIconCount; i++) + { + var iconClone = upgradeIcons[0].Clone() as ActorIconWidget; + iconClone.Bounds.X += (iconClone.IconSize.X + upgradeIconSpacing.X) * i; + + widget.AddChild(iconClone); + upgradeIcons.Add(iconClone); + } + } + + var upgIconID = 0; + foreach (var icon in upgradeIcons) + { + var index = ++upgIconID; + icon.IsVisible = () => + { + var validActors = selection.Actors.Where(a => a.Info.HasTraitInfo()); + return validActors.Count() <= 1; + }; + + icon.GetActorInfo = () => + { + var validActors = selection.Actors.Where(a => a.Info.HasTraitInfo()).ToArray(); + if (validActors.Length > 1) + return null; + + var unit = validActors.FirstOrDefault(); + if (unit != null && !unit.IsDead) + { + var usv = unit.Trait(); + if (usv.Disguised) + { + if (usv.DisguiseUpgrades.Count >= index) + return unit.World.Map.Rules.Actors[usv.DisguiseCurrentUpgrades[index - 1]]; + + return null; + } + else if (usv.Upgrades.Count >= index) + return unit.World.Map.Rules.Actors[usv.CurrentUpgrades[index - 1]]; + + return null; + } + + return null; + }; + + icon.GetDisabled = () => + { + var validActors = selection.Actors.Where(a => a.Info.HasTraitInfo()).ToArray(); + if (validActors.Length > 1) + return false; + + var unit = validActors.FirstOrDefault(); + if (unit != null && !unit.IsDead) + { + var usv = unit.Trait(); + if (usv.Disguised) + { + if (usv.DisguiseUpgrades.Count >= index) + return !usv.DisguiseUpgrades[usv.DisguiseCurrentUpgrades[index - 1]]; + + return false; + } + else if (usv.Upgrades.Count >= index) + return !usv.Upgrades[usv.CurrentUpgrades[index - 1]]; + + return false; + } + + return false; + }; + } + } + + var name = widget.Get("STAT_NAME"); + var more = widget.GetOrNull("STAT_MORE"); + + var extraStatLabels = new List(); + var labelID = 1; + while (widget.GetOrNull("STAT_LABEL_" + labelID.ToStringInvariant()) != null) + { + extraStatLabels.Add(widget.Get("STAT_LABEL_" + labelID.ToStringInvariant())); + labelID++; + } + + var extraStatIcons = new List(); + var iconID = 1; + while (widget.GetOrNull("STAT_ICON_" + iconID.ToStringInvariant()) != null) + { + extraStatIcons.Add(widget.Get("STAT_ICON_" + iconID.ToStringInvariant())); + iconID++; + } + + name.GetText = () => + { + var validActors = selection.Actors.Where(a => a.Info.HasTraitInfo()); + if (largeIconCount > 1 && validActors.Count() != 1) + return ""; + + var unit = validActors.FirstOrDefault(); + if (unit != null && !unit.IsDead) + { + var usv = unit.Trait(); + if (usv.Tooltips.Any()) + { + var stance = world.RenderPlayer == null ? PlayerRelationship.None : unit.Owner.RelationshipWith(world.RenderPlayer); + var actorName = usv.Tooltips.FirstEnabledTraitOrDefault().TooltipInfo.TooltipForPlayerStance(stance); + return actorName; + } + } + + return ""; + }; + + iconID = 0; + foreach (var icon in largeIcons) + { + var index = ++iconID; + icon.IsVisible = () => + { + var validActors = selection.Actors.Where(a => a.Info.HasTraitInfo()); + if (smallIconCount > 0 && validActors.Count() > largeIconCount) + return false; + + return index == 1 || validActors.Count() >= index; + }; + + icon.GetActor = () => + { + var validActors = selection.Actors.Where(a => a.Info.HasTraitInfo()).ToArray(); + if (validActors.Length >= index) + return validActors[index - 1]; + else + return null; + }; + } + + iconID = 0; + foreach (var icon in smallIcons) + { + var index = ++iconID; + icon.IsVisible = () => + { + var validActors = selection.Actors.Where(a => a.Info.HasTraitInfo()); + return validActors.Count() > largeIconCount && validActors.Count() >= index; + }; + + icon.GetActor = () => + { + var validActors = selection.Actors.Where(a => a.Info.HasTraitInfo()).ToArray(); + if (validActors.Length >= index) + return validActors[index - 1]; + else + return null; + }; + } + + if (more != null) + { + more.GetText = () => + { + var validActors = selection.Actors.Where(a => a.Info.HasTraitInfo()); + if (validActors.Count() <= largeIconCount + smallIconCount) + return ""; + else + return "+" + (validActors.Count() - (largeIconCount + smallIconCount)).ToString(NumberFormatInfo.CurrentInfo); + }; + } + + for (var i = 0; i < largeHealthBars.Count; i++) + { + var index = i; + largeHealthBars[index].IsVisible = () => + { + var validActors = selection.Actors.Where(a => a.Info.HasTraitInfo()); + if (smallIconCount > 0 && validActors.Count() > largeIconCount) + return false; + + return index == 0 || validActors.Count() >= index + 1; + }; + + largeHealthBars[index].GetScale = () => + { + var validActors = selection.Actors.Where(a => !a.IsDead && a.Info.HasTraitInfo()).ToArray(); + if (validActors.Length >= index + 1) + { + var usv = validActors[index].Trait(); + if (usv.Disguised) + return (float)usv.DisguiseMaxHealth / usv.Health.MaxHP; + + return (float)usv.CurrentMaxHealth / usv.Health.MaxHP; + } + + return 1f; + }; + + largeHealthBars[index].GetHealth = () => + { + var validActors = selection.Actors.Where(a => !a.IsDead && a.Info.HasTraitInfo()).ToArray(); + if (validActors.Length >= index + 1) + return validActors[index].Trait().Health; + + return null; + }; + } + + for (var i = 0; i < smallHealthBars.Count; i++) + { + var index = i; + smallHealthBars[index].IsVisible = () => + { + var validActors = selection.Actors.Where(a => a.Info.HasTraitInfo()); + return validActors.Count() > largeIconCount && validActors.Count() >= index + 1; + }; + + smallHealthBars[index].GetHealth = () => + { + var validActors = selection.Actors.Where(a => !a.IsDead && a.Info.HasTraitInfo()).ToArray(); + if (validActors.Length >= index + 1) + return validActors[index].Trait().Health; + else + return null; + }; + } + + labelID = 0; + foreach (var statLabel in extraStatLabels) + { + var index = ++labelID; + statLabel.GetText = () => + { + var validActors = selection.Actors.Where(a => !a.IsDead && a.Info.HasTraitInfo()).ToArray(); + if (largeIconCount > 1 && validActors.Length > 1) + return ""; + + var unit = validActors.FirstOrDefault(); + if (unit != null) + { + var usv = unit.Trait(); + var labelText = ""; + if (usv.Disguised) + labelText = usv.DisguiseStats[index]; + else + labelText = usv.GetValueFor(index); + + return string.IsNullOrEmpty(labelText) ? "" : statLabel.Text + labelText; + } + + return statLabel.Text; + }; + } + + iconID = 0; + foreach (var statIcon in extraStatIcons) + { + var index = ++iconID; + statIcon.IsVisible = () => + { + var validActors = selection.Actors.Where(a => !a.IsDead && a.Info.HasTraitInfo()).ToArray(); + if (largeIconCount > 1 && validActors.Length > 1) + return false; + + var unit = validActors.FirstOrDefault(); + if (unit != null) + { + var usv = unit.Trait(); + if (usv.Disguised) + return usv.DisguiseStatIcons[index] != null; + + return usv.GetIconFor(index) != null; + } + + return true; + }; + statIcon.GetImageName = () => + { + var unit = selection.Actors.FirstOrDefault(a => a.Info.HasTraitInfo()); + if (unit != null && !unit.IsDead) + { + var usv = unit.Trait(); + var iconName = ""; + if (usv.Disguised) + iconName = usv.DisguiseStatIcons[index]; + else + iconName = usv.GetIconFor(index); + + return string.IsNullOrEmpty(iconName) ? statIcon.ImageName : iconName; + } + + return statIcon.ImageName; + }; + } + } + } +} diff --git a/OpenRA.Mods.Cnc/Activities/Leap.cs b/OpenRA.Mods.Cnc/Activities/Leap.cs index 6a8fe6243f47..561974036975 100644 --- a/OpenRA.Mods.Cnc/Activities/Leap.cs +++ b/OpenRA.Mods.Cnc/Activities/Leap.cs @@ -38,6 +38,7 @@ public class Leap : Activity public Leap(in Target target, Mobile mobile, Mobile targetMobile, int speed, AttackLeap attack, EdibleByLeap edible) { + ActivityType = ActivityType.Move; this.mobile = mobile; this.targetMobile = targetMobile; this.attack = attack; diff --git a/OpenRA.Mods.Cnc/Activities/LeapAttack.cs b/OpenRA.Mods.Cnc/Activities/LeapAttack.cs index 2ab0f27b51b5..694f4622ae21 100644 --- a/OpenRA.Mods.Cnc/Activities/LeapAttack.cs +++ b/OpenRA.Mods.Cnc/Activities/LeapAttack.cs @@ -40,6 +40,7 @@ public class LeapAttack : Activity, IActivityNotifyStanceChanged public LeapAttack(Actor self, in Target target, bool allowMovement, bool forceAttack, AttackLeap attack, AttackLeapInfo info, Color? targetLineColor = null) { + ActivityType = ActivityType.Attack; this.target = target; this.targetLineColor = targetLineColor; this.info = info; diff --git a/OpenRA.Mods.Cnc/Activities/Teleport.cs b/OpenRA.Mods.Cnc/Activities/Teleport.cs index 988486011012..d1c32e28fe72 100644 --- a/OpenRA.Mods.Cnc/Activities/Teleport.cs +++ b/OpenRA.Mods.Cnc/Activities/Teleport.cs @@ -34,6 +34,7 @@ public Teleport(Actor teleporter, CPos destination, int? maximumDistance, bool killCargo, bool screenFlash, string sound, bool interruptable = true, bool killOnFailure = false, BitSet killDamageTypes = default) { + ActivityType = ActivityType.Move; var max = teleporter.World.Map.Grid.MaximumTileSearchRange; if (maximumDistance > max) throw new InvalidOperationException($"Teleport distance cannot exceed the value of MaximumTileSearchRange ({max})."); @@ -101,7 +102,7 @@ public override bool Tick(Actor self) // Trigger screen desaturate effect if (screenFlash) - foreach (var a in self.World.ActorsWithTrait()) + foreach (var a in self.World.ActorsWithTrait()) a.Trait.Enable(); if (teleporter != null && self != teleporter && !teleporter.Disposed) @@ -128,7 +129,7 @@ public override bool Tick(Actor self) if (pos.CanEnterCell(destination) && teleporter.Owner.Shroud.IsExplored(destination)) return destination; - var max = maximumDistance != null ? maximumDistance.Value : teleporter.World.Map.Grid.MaximumTileSearchRange; + var max = maximumDistance ?? teleporter.World.Map.Grid.MaximumTileSearchRange; foreach (var tile in self.World.Map.FindTilesInCircle(destination, max)) { if (teleporter.Owner.Shroud.IsExplored(tile) diff --git a/OpenRA.Mods.Cnc/Effects/ConyardChronoVortex.cs b/OpenRA.Mods.Cnc/Effects/ConyardChronoVortex.cs new file mode 100644 index 000000000000..0995a94aa0ec --- /dev/null +++ b/OpenRA.Mods.Cnc/Effects/ConyardChronoVortex.cs @@ -0,0 +1,63 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Effects; +using OpenRA.Graphics; +using OpenRA.Mods.Cnc.Graphics; +using OpenRA.Mods.Cnc.Traits; +using OpenRA.Primitives; + +namespace OpenRA.Mods.Cnc.Effects +{ + sealed class ConyardChronoVortex : IEffect, ISpatiallyPartitionable + { + static readonly Size Size = new(64, 64); + static readonly WVec Offset = new(171, 0, 0); + readonly ChronoVortexRenderer renderer; + readonly WPos center; + readonly Action onCompletion; + WPos pos; + WAngle angle; + int loops = 3; + int frame; + + public ConyardChronoVortex(Actor launcher, Action onCompletion) + { + this.onCompletion = onCompletion; + renderer = launcher.World.WorldActor.Trait(); + center = launcher.CenterPosition; + pos = center + Offset.Rotate(WRot.FromYaw(angle)); + launcher.World.ScreenMap.Add(this, pos, Size); + } + + public void Tick(World world) + { + // First 16 frames are the vortex opening + // Next 16 frames are loopable + // Final 16 frames are the vortex closing + if (++frame == 32 && --loops > 0) + frame = 16; + + angle += new WAngle(42); + pos = center + Offset.Rotate(WRot.FromYaw(angle)); + world.ScreenMap.Update(this, pos, Size); + if (frame == 48) + world.AddFrameEndTask(w => { w.Remove(this); w.ScreenMap.Remove(this); onCompletion(); }); + } + + public IEnumerable Render(WorldRenderer wr) + { + yield return new ChronoVortexRenderable(renderer, pos, frame); + } + } +} diff --git a/OpenRA.Mods.Cnc/FileFormats/BlowfishKeyProvider.cs b/OpenRA.Mods.Cnc/FileFormats/BlowfishKeyProvider.cs index 3c68af388467..a683e189ed9c 100644 --- a/OpenRA.Mods.Cnc/FileFormats/BlowfishKeyProvider.cs +++ b/OpenRA.Mods.Cnc/FileFormats/BlowfishKeyProvider.cs @@ -149,7 +149,7 @@ static void ShrBigNum(uint[] n, int bits, int len) if (bits == 0) return; for (i = 0; i < len - 1; i++) n[i] = (n[i] >> bits) | (n[i + 1] << (32 - bits)); - n[i] = n[i] >> bits; + n[i] >>= bits; } static void ShlBigNum(uint[] n, int bits, int len) @@ -284,24 +284,21 @@ void InitTwoDw(uint[] n, uint len) static unsafe void MulBignumWord(ushort* pn1, uint[] n2, uint mul, uint len) { uint i, tmp; - unsafe + fixed (uint* tempPn2 = &n2[0]) { - fixed (uint* tempPn2 = &n2[0]) - { - var pn2 = (ushort*)tempPn2; + var pn2 = (ushort*)tempPn2; - tmp = 0; - for (i = 0; i < len; i++) - { - tmp = mul * *pn2 + *pn1 + tmp; - *pn1 = (ushort)tmp; - pn1++; - pn2++; - tmp >>= 16; - } - - *pn1 += (ushort)tmp; + tmp = 0; + for (i = 0; i < len; i++) + { + tmp = mul * *pn2 + *pn1 + tmp; + *pn1 = (ushort)tmp; + pn1++; + pn2++; + tmp >>= 16; } + + *pn1 += (ushort)tmp; } } diff --git a/OpenRA.Mods.Cnc/Graphics/ChronoVortexRenderable.cs b/OpenRA.Mods.Cnc/Graphics/ChronoVortexRenderable.cs new file mode 100644 index 000000000000..c1f5f741b363 --- /dev/null +++ b/OpenRA.Mods.Cnc/Graphics/ChronoVortexRenderable.cs @@ -0,0 +1,67 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Mods.Cnc.Traits; +using OpenRA.Primitives; + +namespace OpenRA.Mods.Cnc.Graphics +{ + public class ChronoVortexRenderable : IRenderable, IFinalizedRenderable + { + public static readonly IEnumerable None = Array.Empty(); + readonly ChronoVortexRenderer renderer; + public WPos Pos { get; } + readonly int frame; + + public ChronoVortexRenderable(ChronoVortexRenderer renderer, WPos pos, int frame) + { + if (frame < 0 || frame >= 48) + throw new ArgumentException("frame must be in the range 0-47", nameof(frame)); + + this.renderer = renderer; + Pos = pos; + this.frame = frame; + } + + public int ZOffset => 0; + public bool IsDecoration => false; + + public IRenderable WithZOffset(int newOffset) => this; + public IRenderable OffsetBy(in WVec offset) => this; + public IRenderable AsDecoration() => this; + + public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; } + + public void Render(WorldRenderer wr) + { + renderer.DrawVortex(wr.Screen3DPxPosition(Pos), frame); + } + + public void RenderDebugGeometry(WorldRenderer wr) + { + var pos = wr.Screen3DPxPosition(Pos); + var tl = wr.Viewport.WorldToViewPx(pos); + var br = wr.Viewport.WorldToViewPx(pos + new float3(64, 64, 0)); + Game.Renderer.RgbaColorRenderer.DrawRect(tl, br, 1, Color.Red); + } + + public Rectangle ScreenBounds(WorldRenderer wr) + { + var pos = wr.Screen3DPxPosition(Pos); + var tl = wr.Viewport.WorldToViewPx(pos); + var br = wr.Viewport.WorldToViewPx(pos + new float3(64, 64, 0)); + return new Rectangle(tl.X, tl.Y, br.X - tl.X, br.Y - tl.Y); + } + } +} diff --git a/OpenRA.Mods.Cnc/Projectiles/DropPodImpact.cs b/OpenRA.Mods.Cnc/Projectiles/DropPodImpact.cs index ab77d32f19f1..ee7bf8a355d1 100644 --- a/OpenRA.Mods.Cnc/Projectiles/DropPodImpact.cs +++ b/OpenRA.Mods.Cnc/Projectiles/DropPodImpact.cs @@ -42,7 +42,8 @@ public DropPodImpact(Player firedBy, WeaponInfo weapon, World world, WPos launch entryAnimation.PlayThen(entrySequence, () => Finish(world)); if (weapon.Report != null && weapon.Report.Length > 0) - Game.Sound.Play(SoundType.World, weapon.Report, world, launchPos); + if (weapon.AudibleThroughFog || (!world.ShroudObscures(launchPos) && !world.FogObscures(launchPos))) + Game.Sound.Play(SoundType.World, weapon.Report, world, launchPos, null, weapon.SoundVolume); } public void Tick(World world) diff --git a/OpenRA.Mods.Cnc/Projectiles/IonCannon.cs b/OpenRA.Mods.Cnc/Projectiles/IonCannon.cs index 53ba8391fa7f..22ac14224dae 100644 --- a/OpenRA.Mods.Cnc/Projectiles/IonCannon.cs +++ b/OpenRA.Mods.Cnc/Projectiles/IonCannon.cs @@ -38,7 +38,8 @@ public IonCannon(Player firedBy, WeaponInfo weapon, World world, WPos launchPos, anim.PlayThen(sequence, () => Finish(world)); if (weapon.Report != null && weapon.Report.Length > 0) - Game.Sound.Play(SoundType.World, weapon.Report, world, launchPos); + if (weapon.AudibleThroughFog || (!world.ShroudObscures(launchPos) && !world.FogObscures(launchPos))) + Game.Sound.Play(SoundType.World, weapon.Report, world, launchPos, null, weapon.SoundVolume); } public void Tick(World world) diff --git a/OpenRA.Mods.Cnc/SpriteLoaders/ShpTDLoader.cs b/OpenRA.Mods.Cnc/SpriteLoaders/ShpTDLoader.cs index 1bb58c7899b8..a989e718d956 100644 --- a/OpenRA.Mods.Cnc/SpriteLoaders/ShpTDLoader.cs +++ b/OpenRA.Mods.Cnc/SpriteLoaders/ShpTDLoader.cs @@ -232,7 +232,7 @@ public static void Write(Stream s, Size size, IEnumerable frames) var eof = new ImageHeader { FileOffset = (uint)dataOffset }; eof.WriteTo(bw); - var allZeroes = new ImageHeader { }; + var allZeroes = new ImageHeader(); allZeroes.WriteTo(bw); foreach (var f in compressedFrames) diff --git a/OpenRA.Mods.Cnc/Traits/Attack/AttackTDGunboatTurreted.cs b/OpenRA.Mods.Cnc/Traits/Attack/AttackTDGunboatTurreted.cs index bbdad94d1dec..3c72c22eed5f 100644 --- a/OpenRA.Mods.Cnc/Traits/Attack/AttackTDGunboatTurreted.cs +++ b/OpenRA.Mods.Cnc/Traits/Attack/AttackTDGunboatTurreted.cs @@ -44,6 +44,7 @@ sealed class AttackTDGunboatTurretedActivity : Activity public AttackTDGunboatTurretedActivity(Actor self, in Target target, bool forceAttack, Color? targetLineColor = null) { + ActivityType = ActivityType.Attack; attack = self.Trait(); this.target = target; this.forceAttack = forceAttack; diff --git a/OpenRA.Mods.Cnc/Traits/Attack/AttackTesla.cs b/OpenRA.Mods.Cnc/Traits/Attack/AttackTesla.cs index 498a9eb4008c..36b76e7ed330 100644 --- a/OpenRA.Mods.Cnc/Traits/Attack/AttackTesla.cs +++ b/OpenRA.Mods.Cnc/Traits/Attack/AttackTesla.cs @@ -36,6 +36,12 @@ sealed class AttackTeslaInfo : AttackBaseInfo [Desc("Sound to play when actor charges.")] public readonly string ChargeAudio = null; + [Desc("Do the charge audio play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the sounds played at.")] + public readonly float SoundVolume = 1f; + public override object Create(ActorInitializer init) { return new AttackTesla(init.Self, this); } } @@ -92,6 +98,7 @@ sealed class ChargeAttack : Activity, IActivityNotifyStanceChanged public ChargeAttack(AttackTesla attack, in Target target, bool forceAttack, Color? targetLineColor = null) { + ActivityType = ActivityType.Attack; this.attack = attack; this.target = target; this.forceAttack = forceAttack; @@ -110,7 +117,11 @@ public override bool Tick(Actor self) notify.Charging(self, target); if (!string.IsNullOrEmpty(attack.info.ChargeAudio)) - Game.Sound.Play(SoundType.World, attack.info.ChargeAudio, self.CenterPosition); + { + var pos = self.CenterPosition; + if (attack.info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, attack.info.ChargeAudio, pos, attack.info.SoundVolume); + } QueueChild(new Wait(attack.info.InitialChargeDelay)); QueueChild(new ChargeFire(attack, target)); @@ -151,6 +162,7 @@ sealed class ChargeFire : Activity public ChargeFire(AttackTesla attack, in Target target) { + ActivityType = ActivityType.Attack; this.attack = attack; this.target = target; } diff --git a/OpenRA.Mods.Cnc/Traits/Buildings/ClonesProducedUnits.cs b/OpenRA.Mods.Cnc/Traits/Buildings/ClonesProducedUnits.cs index 73d685809e58..fb63a263a574 100644 --- a/OpenRA.Mods.Cnc/Traits/Buildings/ClonesProducedUnits.cs +++ b/OpenRA.Mods.Cnc/Traits/Buildings/ClonesProducedUnits.cs @@ -47,7 +47,7 @@ public void UnitProducedByOther(Actor self, Actor producer, Actor produced, stri return; // No recursive cloning! - if (producer.Owner != self.Owner || producer.Info.HasTraitInfo()) + if (producer.Owner != self.Owner || productionType == Info.ProductionType) return; var ci = produced.Info.TraitInfoOrDefault(); diff --git a/OpenRA.Mods.Cnc/Traits/Chronoshiftable.cs b/OpenRA.Mods.Cnc/Traits/Chronoshiftable.cs index 7f92aaf3f654..b2fbfaa75562 100644 --- a/OpenRA.Mods.Cnc/Traits/Chronoshiftable.cs +++ b/OpenRA.Mods.Cnc/Traits/Chronoshiftable.cs @@ -36,11 +36,8 @@ public class ChronoshiftableInfo : ConditionalTraitInfo [Desc("The color the bar of the 'return-to-origin' logic has.")] public readonly Color TimeBarColor = Color.White; - public override void RulesetLoaded(Ruleset rules, ActorInfo ai) - { - if (!ai.HasTraitInfo() && !ai.HasTraitInfo()) - throw new YamlException("Chronoshiftable requires actors to have the Mobile or Husk traits."); - } + [Desc("Should parasites be teleported along?")] + public readonly bool ExposeInfectors = true; public override object Create(ActorInitializer init) { return new Chronoshiftable(init, this); } } @@ -52,8 +49,11 @@ public class Chronoshiftable : ConditionalTrait, ITick, ISy Actor chronosphere; bool killCargo; int duration; + CPos targetLocation; IPositionable iPositionable; + int teleportingToken = Actor.InvalidConditionToken; + // Return-to-origin logic [Sync] public CPos Origin; @@ -61,6 +61,12 @@ public class Chronoshiftable : ConditionalTrait, ITick, ISy [Sync] public int ReturnTicks = 0; + [Sync] + public int BeforeTeleportTicks = 0; + + [Sync] + public int AfterTeleportTicks = 0; + public Chronoshiftable(ActorInitializer init, ChronoshiftableInfo info) : base(info) { @@ -81,27 +87,46 @@ public Chronoshiftable(ActorInitializer init, ChronoshiftableInfo info) void ITick.Tick(Actor self) { - if (IsTraitDisabled || !Info.ReturnToOrigin || ReturnTicks <= 0) + if (IsTraitDisabled) return; - // Return to original location - if (--ReturnTicks == 0) + if (Info.ReturnToOrigin && ReturnTicks > 0) + { + // Return to original location + if (--ReturnTicks == 0) + { + // The Move activity is not immediately cancelled, which, combined + // with Activity.Cancel discarding NextActivity without checking the + // IsInterruptable flag, means that a well timed order can cancel the + // Teleport activity queued below - an exploit / cheat of the return mechanic. + // The Teleport activity queued below is guaranteed to either complete + // (force-resetting the actor to the middle of the target cell) or kill + // the actor. It is therefore safe to force-erase the Move activity to + // work around the cancellation bug. + // HACK: this is manipulating private internal actor state + if (self.CurrentActivity is Move) + typeof(Actor).GetProperty(nameof(Actor.CurrentActivity)).SetValue(self, null); + + if (Info.ExposeInfectors) + foreach (var i in self.TraitsImplementing()) + i.RemoveInfector(self, false); + + // The actor is killed using Info.DamageTypes if the teleport fails + self.QueueActivity(false, new Teleport(chronosphere ?? self, Origin, null, true, killCargo, Info.ChronoshiftSound, + false, true, Info.DamageTypes)); + } + } + + if (BeforeTeleportTicks > 0) + { + if (--BeforeTeleportTicks == 0) + Teleport(self, targetLocation, duration, killCargo, chronosphere); + } + else if (AfterTeleportTicks > 0) { - // The Move activity is not immediately cancelled, which, combined - // with Activity.Cancel discarding NextActivity without checking the - // IsInterruptable flag, means that a well timed order can cancel the - // Teleport activity queued below - an exploit / cheat of the return mechanic. - // The Teleport activity queued below is guaranteed to either complete - // (force-resetting the actor to the middle of the target cell) or kill - // the actor. It is therefore safe to force-erase the Move activity to - // work around the cancellation bug. - // HACK: this is manipulating private internal actor state - if (self.CurrentActivity is Move) - typeof(Actor).GetProperty(nameof(Actor.CurrentActivity)).SetValue(self, null); - - // The actor is killed using Info.DamageTypes if the teleport fails - self.QueueActivity(false, new Teleport(chronosphere ?? self, Origin, null, true, killCargo, Info.ChronoshiftSound, - false, true, Info.DamageTypes)); + if (--AfterTeleportTicks == 0) + if (teleportingToken != Actor.InvalidConditionToken) + teleportingToken = self.RevokeCondition(teleportingToken); } } @@ -118,6 +143,33 @@ public virtual bool CanChronoshiftTo(Actor self, CPos targetLocation) return !IsTraitDisabled && iPositionable != null && iPositionable.CanEnterCell(targetLocation); } + public virtual bool Teleport(Actor self, CPos targetLocation, int duration, bool killCargo, Actor chronosphere, int beforeDelay, int afterDelay, string condition) + { + if (IsTraitDisabled) + return false; + + if (beforeDelay == 0) + { + Teleport(self, targetLocation, duration, killCargo, chronosphere); + } + else + { + BeforeTeleportTicks = beforeDelay; + + this.duration = duration; + this.chronosphere = chronosphere; + this.killCargo = killCargo; + this.targetLocation = targetLocation; + } + + AfterTeleportTicks = afterDelay; + + if (beforeDelay != 0 && afterDelay != 0 && teleportingToken == Actor.InvalidConditionToken) + teleportingToken = self.GrantCondition(condition); + + return true; + } + public virtual bool Teleport(Actor self, CPos targetLocation, int duration, bool killCargo, Actor chronosphere) { if (IsTraitDisabled) @@ -148,9 +200,25 @@ public virtual bool Teleport(Actor self, CPos targetLocation, int duration, bool this.chronosphere = chronosphere; this.killCargo = killCargo; + if (Info.ExposeInfectors) + foreach (var i in self.TraitsImplementing()) + i.RemoveInfector(self, false); + // Set up the teleport self.QueueActivity(false, new Teleport(chronosphere, targetLocation, null, killCargo, true, Info.ChronoshiftSound)); + // AllowImpassable was true, but we can't enter here, kill the unit. + if (iPositionable == null || !iPositionable.CanExistInCell(targetLocation)) + { + self.World.AddFrameEndTask(w => + { + // Damage is inflicted by the chronosphere + if (!self.Disposed) + self.Kill(chronosphere, Info.DamageTypes); + }); + return true; + } + return true; } diff --git a/OpenRA.Mods.Cnc/Traits/ConyardChronoReturn.cs b/OpenRA.Mods.Cnc/Traits/ConyardChronoReturn.cs index 6629e347ab53..62f8f0cc31da 100644 --- a/OpenRA.Mods.Cnc/Traits/ConyardChronoReturn.cs +++ b/OpenRA.Mods.Cnc/Traits/ConyardChronoReturn.cs @@ -10,7 +10,7 @@ #endregion using System.Collections.Generic; -using System.Linq; +using OpenRA.Mods.Cnc.Effects; using OpenRA.Mods.Common; using OpenRA.Mods.Common.Traits; using OpenRA.Mods.Common.Traits.Render; @@ -25,13 +25,6 @@ namespace OpenRA.Mods.Cnc.Traits "Otherwise, a vortex animation is played and damage is dealt each tick, ignoring modifiers.")] public class ConyardChronoReturnInfo : TraitInfo, Requires, Requires, IObservesVariablesInfo { - [SequenceReference] - [Desc("Sequence name with the baked-in vortex animation")] - public readonly string Sequence = "pdox"; - - [Desc("Sprite body to play the vortex animation on.")] - public readonly string Body = "body"; - [GrantedConditionReference] [Desc("Condition to grant while the vortex animation plays.")] public readonly string Condition = null; @@ -65,7 +58,6 @@ public class ConyardChronoReturn : ITick, ISync, IObservesVariables, ISelectionB IDeathActorInitModifier, ITransformActorInitModifier { readonly ConyardChronoReturnInfo info; - readonly WithSpriteBody wsb; readonly Health health; readonly Actor self; readonly string faction; @@ -92,8 +84,6 @@ public ConyardChronoReturn(ActorInitializer init, ConyardChronoReturnInfo info) self = init.Self; health = self.Trait(); - - wsb = self.TraitsImplementing().Single(w => w.Info.Name == info.Body); faction = init.GetValue(self.Owner.Faction.InternalName); var returnInit = init.GetOrDefault(); @@ -127,16 +117,12 @@ void TriggerVortex() triggered = true; - // Don't override the selling animation - if (selling) - return; - - wsb.PlayCustomAnimation(self, info.Sequence, () => + self.World.AddFrameEndTask(w => w.Add(new ConyardChronoVortex(self, () => { triggered = false; - if (conditionToken != Actor.InvalidConditionToken) + if (conditionToken != Actor.InvalidConditionToken && !self.Disposed) conditionToken = self.RevokeCondition(conditionToken); - }); + }))); } CPos? ChooseBestDestinationCell(MobileInfo mobileInfo, CPos destination) @@ -213,7 +199,7 @@ void ITick.Tick(Actor self) TriggerVortex(); // Trigger screen desaturate effect - foreach (var cpa in self.World.ActorsWithTrait()) + foreach (var cpa in self.World.ActorsWithTrait()) cpa.Trait.Enable(); Game.Sound.Play(SoundType.World, info.ChronoshiftSound, self.CenterPosition); diff --git a/OpenRA.Mods.Cnc/Traits/Disguise.cs b/OpenRA.Mods.Cnc/Traits/Disguise.cs index 3649c62c6ba9..807ea1ca6702 100644 --- a/OpenRA.Mods.Cnc/Traits/Disguise.cs +++ b/OpenRA.Mods.Cnc/Traits/Disguise.cs @@ -14,18 +14,19 @@ using System.Linq; using OpenRA.Mods.Common.Orders; using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Traits.Render; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Cnc.Traits { [Desc("Overrides the default Tooltip when this actor is disguised (aids in deceiving enemy players).")] - sealed class DisguiseTooltipInfo : TooltipInfo, Requires + public sealed class DisguiseTooltipInfo : TooltipInfo, Requires { public override object Create(ActorInitializer init) { return new DisguiseTooltip(init.Self, this); } } - sealed class DisguiseTooltip : ConditionalTrait, ITooltip + public sealed class DisguiseTooltip : ConditionalTrait, ITooltip { readonly Actor self; readonly Disguise disguise; @@ -65,7 +66,7 @@ public enum RevealDisguiseType } [Desc("Provides access to the disguise command, which makes the actor appear to be another player's actor.")] - sealed class DisguiseInfo : TraitInfo + public sealed class DisguiseInfo : TraitInfo { [VoiceReference] public readonly string Voice = "Action"; @@ -99,36 +100,41 @@ sealed class DisguiseInfo : TraitInfo public override object Create(ActorInitializer init) { return new Disguise(init.Self, this); } } - sealed class Disguise : IEffectiveOwner, IIssueOrder, IResolveOrder, IOrderVoice, IRadarColorModifier, INotifyAttack, + public sealed class Disguise : IEffectiveOwner, IIssueOrder, IResolveOrder, IOrderVoice, IRadarColorModifier, INotifyAttack, INotifyDamage, INotifyLoadCargo, INotifyUnloadCargo, INotifyDemolition, INotifyInfiltration, ITick { public ActorInfo AsActor { get; private set; } public Player AsPlayer { get; private set; } + public string AsSprite { get; private set; } public ITooltipInfo AsTooltipInfo { get; private set; } + public List TurretOffsets = new() { WVec.Zero }; public bool Disguised => AsPlayer != null; public Player Owner => AsPlayer; readonly Actor self; - readonly DisguiseInfo info; + public readonly DisguiseInfo Info; int disguisedToken = Actor.InvalidConditionToken; int disguisedAsToken = Actor.InvalidConditionToken; CPos? lastPos; + readonly INotifyDisguised[] notifiers; + public Disguise(Actor self, DisguiseInfo info) { this.self = self; - this.info = info; + Info = info; AsActor = self.Info; + notifiers = self.TraitsImplementing().ToArray(); } IEnumerable IIssueOrder.Orders { get { - yield return new DisguiseOrderTargeter(info); + yield return new DisguiseOrderTargeter(Info); } } @@ -155,7 +161,7 @@ void IResolveOrder.ResolveOrder(Actor self, Order order) string IOrderVoice.VoicePhraseForOrder(Actor self, Order order) { - return order.OrderString == "Disguise" ? info.Voice : null; + return order.OrderString == "Disguise" ? Info.Voice : null; } Color IRadarColorModifier.RadarColorOverride(Actor self, Color color) @@ -179,6 +185,7 @@ public void DisguiseAs(Actor target) var targetDisguise = target.TraitOrDefault(); if (targetDisguise != null && targetDisguise.Disguised) { + AsSprite = targetDisguise.AsSprite; AsPlayer = targetDisguise.AsPlayer; AsActor = targetDisguise.AsActor; AsTooltipInfo = targetDisguise.AsTooltipInfo; @@ -189,9 +196,26 @@ public void DisguiseAs(Actor target) if (tooltip == null) throw new ArgumentNullException("tooltip", "Missing tooltip or invalid target."); + AsSprite = target.Trait().GetImage(target); AsPlayer = tooltip.Owner; AsActor = target.Info; AsTooltipInfo = tooltip.TooltipInfo; + + var targetTurreted = target.TraitsImplementing(); + if (targetTurreted != null) + { + TurretOffsets.Clear(); + foreach (var t in targetTurreted) + TurretOffsets.Add(t.Offset); + } + else + { + TurretOffsets.Clear(); + TurretOffsets.Add(WVec.Zero); + } + + foreach (var nd in notifiers) + nd.DisguiseChanged(self, target); } } else @@ -199,6 +223,13 @@ public void DisguiseAs(Actor target) AsTooltipInfo = null; AsPlayer = null; AsActor = self.Info; + AsSprite = null; + + TurretOffsets.Clear(); + TurretOffsets.Add(WVec.Zero); + + foreach (var nd in notifiers) + nd.DisguiseChanged(self, self); } HandleDisguise(oldEffectiveActor, oldEffectiveOwner, oldDisguiseSetting); @@ -210,10 +241,25 @@ public void DisguiseAs(ActorInfo actorInfo, Player newOwner) var oldEffectiveOwner = AsPlayer; var oldDisguiseSetting = Disguised; + var renderSprites = actorInfo.TraitInfoOrDefault(); + AsSprite = renderSprites?.GetImage(actorInfo, newOwner.Faction.InternalName); AsPlayer = newOwner; AsActor = actorInfo; AsTooltipInfo = actorInfo.TraitInfos().FirstOrDefault(info => info.EnabledByDefault); + var targetTurreted = actorInfo.TraitInfos(); + if (targetTurreted != null) + { + TurretOffsets.Clear(); + foreach (var t in targetTurreted) + TurretOffsets.Add(t.Offset); + } + else + { + TurretOffsets.Clear(); + TurretOffsets.Add(WVec.Zero); + } + HandleDisguise(oldEffectiveActor, oldEffectiveOwner, oldDisguiseSetting); } @@ -225,7 +271,7 @@ void HandleDisguise(ActorInfo oldEffectiveActor, Player oldEffectiveOwner, bool if (Disguised != oldDisguiseSetting) { if (Disguised && disguisedToken == Actor.InvalidConditionToken) - disguisedToken = self.GrantCondition(info.DisguisedCondition); + disguisedToken = self.GrantCondition(Info.DisguisedCondition); else if (!Disguised && disguisedToken != Actor.InvalidConditionToken) disguisedToken = self.RevokeCondition(disguisedToken); } @@ -235,7 +281,7 @@ void HandleDisguise(ActorInfo oldEffectiveActor, Player oldEffectiveOwner, bool if (disguisedAsToken != Actor.InvalidConditionToken) disguisedAsToken = self.RevokeCondition(disguisedAsToken); - if (info.DisguisedAsConditions.TryGetValue(AsActor.Name, out var disguisedAsCondition)) + if (Info.DisguisedAsConditions.TryGetValue(AsActor.Name, out var disguisedAsCondition)) disguisedAsToken = self.GrantCondition(disguisedAsCondition); } } @@ -244,50 +290,50 @@ void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Bar void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) { - if (info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Attack)) + if (Info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Attack)) DisguiseAs(null); } void INotifyDamage.Damaged(Actor self, AttackInfo e) { - if (info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Damaged) && e.Damage.Value > 0) + if (Info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Damaged) && e.Damage.Value > 0) DisguiseAs(null); } void INotifyLoadCargo.Loading(Actor self) { - if (info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Load)) + if (Info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Load)) DisguiseAs(null); } void INotifyUnloadCargo.Unloading(Actor self) { - if (info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Unload)) + if (Info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Unload)) DisguiseAs(null); } void INotifyDemolition.Demolishing(Actor self) { - if (info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Demolish)) + if (Info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Demolish)) DisguiseAs(null); } void INotifyInfiltration.Infiltrating(Actor self) { - if (info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Infiltrate)) + if (Info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Infiltrate)) DisguiseAs(null); } void ITick.Tick(Actor self) { - if (info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Move) && lastPos != null && lastPos.Value != self.Location) + if (Info.RevealDisguiseOn.HasFlag(RevealDisguiseType.Move) && lastPos != null && lastPos.Value != self.Location) DisguiseAs(null); lastPos = self.Location; } } - sealed class DisguiseOrderTargeter : UnitOrderTargeter + public sealed class DisguiseOrderTargeter : UnitOrderTargeter { readonly DisguiseInfo info; diff --git a/OpenRA.Mods.Cnc/Traits/DisguisingTurreted.cs b/OpenRA.Mods.Cnc/Traits/DisguisingTurreted.cs new file mode 100644 index 000000000000..63ac98e3dbe7 --- /dev/null +++ b/OpenRA.Mods.Cnc/Traits/DisguisingTurreted.cs @@ -0,0 +1,46 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.Cnc.Traits.Render +{ + sealed class DisguisingTurretedInfo : TurretedInfo, Requires + { + public override object Create(ActorInitializer init) { return new DisguisingTurreted(init, this); } + } + + sealed class DisguisingTurreted : Turreted + { + readonly Disguise disguise; + WVec intendedTurretOffset; + + public DisguisingTurreted(ActorInitializer init, DisguisingTurretedInfo info) + : base(init, info) + { + disguise = init.Self.Trait(); + intendedTurretOffset = disguise.TurretOffsets.First(); + } + + protected override void Tick(Actor self) + { + if (disguise.TurretOffsets.FirstOrDefault() != intendedTurretOffset) + { + intendedTurretOffset = disguise.TurretOffsets.FirstOrDefault(); + localOffset = intendedTurretOffset; + } + + base.Tick(self); + } + } +} diff --git a/OpenRA.Mods.Cnc/Traits/MadTank.cs b/OpenRA.Mods.Cnc/Traits/MadTank.cs index 66847f57347c..2daa66ac9b46 100644 --- a/OpenRA.Mods.Cnc/Traits/MadTank.cs +++ b/OpenRA.Mods.Cnc/Traits/MadTank.cs @@ -157,6 +157,7 @@ sealed class DetonationSequence : Activity public DetonationSequence(Actor self, MadTank mad) : this(self, mad, Target.Invalid) { + ActivityType = ActivityType.Attack; assignTargetOnFirstRun = true; } diff --git a/OpenRA.Mods.Cnc/Traits/PaletteEffects/ChronoshiftPaletteEffect.cs b/OpenRA.Mods.Cnc/Traits/PaletteEffects/ChronoshiftPostProcessEffect.cs similarity index 55% rename from OpenRA.Mods.Cnc/Traits/PaletteEffects/ChronoshiftPaletteEffect.cs rename to OpenRA.Mods.Cnc/Traits/PaletteEffects/ChronoshiftPostProcessEffect.cs index 6e8dd5a71eaa..363248564206 100644 --- a/OpenRA.Mods.Cnc/Traits/PaletteEffects/ChronoshiftPaletteEffect.cs +++ b/OpenRA.Mods.Cnc/Traits/PaletteEffects/ChronoshiftPostProcessEffect.cs @@ -9,29 +9,29 @@ */ #endregion -using System.Collections.Generic; using OpenRA.Graphics; -using OpenRA.Primitives; +using OpenRA.Mods.Common.Traits; using OpenRA.Traits; namespace OpenRA.Mods.Cnc.Traits { [TraitLocation(SystemActors.World | SystemActors.EditorWorld)] [Desc("Apply palette full screen rotations during chronoshifts. Add this to the world actor.")] - public class ChronoshiftPaletteEffectInfo : TraitInfo + public class ChronoshiftPostProcessEffectInfo : TraitInfo { [Desc("Measured in ticks.")] public readonly int ChronoEffectLength = 60; - public override object Create(ActorInitializer init) { return new ChronoshiftPaletteEffect(this); } + public override object Create(ActorInitializer init) { return new ChronoshiftPostProcessEffect(this); } } - public class ChronoshiftPaletteEffect : IPaletteModifier, ITick + public class ChronoshiftPostProcessEffect : RenderPostProcessPassBase, ITick { - readonly ChronoshiftPaletteEffectInfo info; + readonly ChronoshiftPostProcessEffectInfo info; int remainingFrames; - public ChronoshiftPaletteEffect(ChronoshiftPaletteEffectInfo info) + public ChronoshiftPostProcessEffect(ChronoshiftPostProcessEffectInfo info) + : base("chronoshift", PostProcessPassType.AfterWorld) { this.info = info; } @@ -47,23 +47,10 @@ void ITick.Tick(Actor self) remainingFrames--; } - void IPaletteModifier.AdjustPalette(IReadOnlyDictionary palettes) + protected override bool Enabled => remainingFrames > 0; + protected override void PrepareRender(WorldRenderer wr, IShader shader) { - if (remainingFrames == 0) - return; - - var frac = (float)remainingFrames / info.ChronoEffectLength; - - foreach (var pal in palettes) - { - for (var x = 0; x < Palette.Size; x++) - { - var orig = pal.Value.GetColor(x); - var lum = (int)(255 * orig.GetBrightness()); - var desat = Color.FromArgb(orig.A, lum, lum, lum); - pal.Value.SetColor(x, Exts.ColorLerp(frac, orig, desat)); - } - } + shader.SetVec("Blend", (float)remainingFrames / info.ChronoEffectLength); } } } diff --git a/OpenRA.Mods.Cnc/Traits/PortableChrono.cs b/OpenRA.Mods.Cnc/Traits/PortableChrono.cs index daffc1d53b0c..8a39ceccfe1c 100644 --- a/OpenRA.Mods.Cnc/Traits/PortableChrono.cs +++ b/OpenRA.Mods.Cnc/Traits/PortableChrono.cs @@ -204,8 +204,6 @@ public bool CanTarget(Actor self, in Target target, ref TargetModifiers modifier cursor = targetCursor; return true; } - - return false; } return false; @@ -252,7 +250,6 @@ protected override void Tick(World world) if (portableChrono.IsTraitDisabled || portableChrono.IsTraitPaused) { world.CancelInputMode(); - return; } } diff --git a/OpenRA.Mods.Cnc/Traits/Render/WithDisguisingFacingSpriteBody.cs b/OpenRA.Mods.Cnc/Traits/Render/WithDisguisingFacingSpriteBody.cs new file mode 100644 index 000000000000..050899284782 --- /dev/null +++ b/OpenRA.Mods.Cnc/Traits/Render/WithDisguisingFacingSpriteBody.cs @@ -0,0 +1,46 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.Cnc.Traits.Render +{ + sealed class WithDisguisingFacingSpriteBodyInfo : WithFacingSpriteBodyInfo, Requires + { + public override object Create(ActorInitializer init) { return new WithDisguisingFacingSpriteBody(init, this); } + } + + sealed class WithDisguisingFacingSpriteBody : WithFacingSpriteBody, ITick + { + readonly Disguise disguise; + readonly RenderSprites rs; + string intendedSprite; + + public WithDisguisingFacingSpriteBody(ActorInitializer init, WithDisguisingFacingSpriteBodyInfo info) + : base(init, info) + { + rs = init.Self.Trait(); + disguise = init.Self.Trait(); + intendedSprite = disguise.AsSprite; + } + + void ITick.Tick(Actor self) + { + if (disguise.AsSprite != intendedSprite) + { + intendedSprite = disguise.AsSprite; + DefaultAnimation.ChangeImage(intendedSprite ?? rs.GetImage(self), DefaultAnimation.CurrentSequence.Name); + rs.UpdatePalette(); + } + } + } +} diff --git a/OpenRA.Mods.Cnc/Traits/Render/WithDisguisingInfantryBody.cs b/OpenRA.Mods.Cnc/Traits/Render/WithDisguisingInfantryBody.cs index 478827ffc4f6..ebcac1da0ab3 100644 --- a/OpenRA.Mods.Cnc/Traits/Render/WithDisguisingInfantryBody.cs +++ b/OpenRA.Mods.Cnc/Traits/Render/WithDisguisingInfantryBody.cs @@ -24,16 +24,15 @@ sealed class WithDisguisingInfantryBody : WithInfantryBody { readonly Disguise disguise; readonly RenderSprites rs; - ActorInfo disguiseActor; - Player disguisePlayer; WithInfantryBodyInfo disguiseInfantryBody; - string disguiseImage; + string intendedSprite; public WithDisguisingInfantryBody(ActorInitializer init, WithDisguisingInfantryBodyInfo info) : base(init, info) { rs = init.Self.Trait(); disguise = init.Self.Trait(); + intendedSprite = disguise.AsSprite; } protected override WithInfantryBodyInfo GetDisplayInfo() @@ -43,31 +42,17 @@ protected override WithInfantryBodyInfo GetDisplayInfo() protected override void Tick(Actor self) { - if (disguise.AsActor != disguiseActor || disguise.AsPlayer != disguisePlayer) + if (disguise.AsSprite != intendedSprite) { - // Force actor back to the stand state to avoid mismatched sequences - PlayStandAnimation(self); - - disguiseActor = disguise.AsActor; - disguisePlayer = disguise.AsPlayer; - disguiseImage = null; - disguiseInfantryBody = null; - - if (disguisePlayer != null) - { - var renderSprites = disguiseActor.TraitInfoOrDefault(); - var infantryBody = disguiseActor.TraitInfos() - .FirstOrDefault(t => t.EnabledByDefault); - if (renderSprites != null && infantryBody != null) - { - disguiseImage = renderSprites.GetImage(disguiseActor, disguisePlayer.Faction.InternalName); - disguiseInfantryBody = infantryBody; - } - } + var infantryBody = disguise.AsActor.TraitInfos() + .FirstOrDefault(t => t.EnabledByDefault); + if (infantryBody != null) + disguiseInfantryBody = infantryBody; + intendedSprite = disguise.AsSprite; var sequence = DefaultAnimation.GetRandomExistingSequence(GetDisplayInfo().StandSequences, Game.CosmeticRandom); if (sequence != null) - DefaultAnimation.ChangeImage(disguiseImage ?? rs.GetImage(self), sequence); + DefaultAnimation.ChangeImage(intendedSprite ?? rs.GetImage(self), sequence); rs.UpdatePalette(); } diff --git a/OpenRA.Mods.Cnc/Traits/Render/WithDisguisingSpriteTurret.cs b/OpenRA.Mods.Cnc/Traits/Render/WithDisguisingSpriteTurret.cs new file mode 100644 index 000000000000..a84637cd4f5a --- /dev/null +++ b/OpenRA.Mods.Cnc/Traits/Render/WithDisguisingSpriteTurret.cs @@ -0,0 +1,54 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Traits.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.Cnc.Traits.Render +{ + sealed class WithDisguisingSpriteTurretInfo : WithSpriteTurretInfo, Requires + { + public override object Create(ActorInitializer init) { return new WithDisguisingSpriteTurret(init.Self, this); } + } + + sealed class WithDisguisingSpriteTurret : WithSpriteTurret, ITick + { + readonly Disguise disguise; + readonly RenderSprites rs; + readonly Turreted t; + string intendedSprite; + + public WithDisguisingSpriteTurret(Actor self, WithDisguisingSpriteTurretInfo info) + : base(self, info) + { + rs = self.Trait(); + t = self.TraitsImplementing() + .First(tt => tt.Name == info.Turret); + disguise = self.Trait(); + intendedSprite = disguise.AsSprite; + } + + void ITick.Tick(Actor self) + { + if (disguise.AsSprite != intendedSprite) + { + intendedSprite = disguise.AsSprite; + DefaultAnimation.ChangeImage(intendedSprite ?? rs.GetImage(self), DefaultAnimation.CurrentSequence.Name); + rs.UpdatePalette(); + + // Restrict turret facings to match the sprite + t.QuantizedFacings = DefaultAnimation.CurrentSequence.Facings; + } + } + } +} diff --git a/OpenRA.Mods.Cnc/Traits/Render/WithEmbeddedTurretSpriteBody.cs b/OpenRA.Mods.Cnc/Traits/Render/WithEmbeddedTurretSpriteBody.cs index f180ef5a23b5..2ba3819c9562 100644 --- a/OpenRA.Mods.Cnc/Traits/Render/WithEmbeddedTurretSpriteBody.cs +++ b/OpenRA.Mods.Cnc/Traits/Render/WithEmbeddedTurretSpriteBody.cs @@ -33,8 +33,8 @@ public override IEnumerable RenderPreviewSprites(ActorPreviewInit if (!EnabledByDefault) yield break; - var t = init.Actor.TraitInfos().FirstOrDefault(); - var wsb = init.Actor.TraitInfos().FirstOrDefault(); + var t = init.Actor.TraitInfos().First(); + var wsb = init.Actor.TraitInfos().First(); // Show the correct turret facing var anim = new Animation(init.World, image, t.WorldFacingFromInit(init)); @@ -52,15 +52,15 @@ public class WithEmbeddedTurretSpriteBody : WithSpriteBody static Func MakeTurretFacingFunc(Actor self) { // Turret artwork is baked into the sprite, so only the first turret makes sense. - var turreted = self.TraitsImplementing().FirstOrDefault(); - return () => turreted.WorldOrientation.Yaw; + var firstTurret = self.TraitsImplementing().First(); + return () => firstTurret.WorldOrientation.Yaw; } public WithEmbeddedTurretSpriteBody(ActorInitializer init, WithEmbeddedTurretSpriteBodyInfo info) : base(init, info, MakeTurretFacingFunc(init.Self)) { this.info = info; - turreted = init.Self.TraitsImplementing().FirstOrDefault(); + turreted = init.Self.TraitsImplementing().First(); } protected override void TraitEnabled(Actor self) diff --git a/OpenRA.Mods.Cnc/Traits/Render/WithVoxelUnloadBody.cs b/OpenRA.Mods.Cnc/Traits/Render/WithVoxelUnloadBody.cs index b66ef6ee691a..ade826060428 100644 --- a/OpenRA.Mods.Cnc/Traits/Render/WithVoxelUnloadBody.cs +++ b/OpenRA.Mods.Cnc/Traits/Render/WithVoxelUnloadBody.cs @@ -20,7 +20,7 @@ namespace OpenRA.Mods.Cnc.Traits.Render { // TODO: This trait is hacky and should go away as soon as we support granting a condition on docking, in favor of toggling two regular WithVoxelBodies - public class WithVoxelUnloadBodyInfo : TraitInfo, IRenderActorPreviewVoxelsInfo, Requires + public class WithVoxelUnloadBodyInfo : ConditionalTraitInfo, IRenderActorPreviewVoxelsInfo, Requires { [Desc("Voxel sequence name to use when docked to a refinery.")] public readonly string UnloadSequence = "unload"; @@ -44,7 +44,7 @@ public IEnumerable RenderPreviewVoxels(IModelCache cache, } } - public class WithVoxelUnloadBody : IAutoMouseBounds, IDockClientBody + public class WithVoxelUnloadBody : ConditionalTrait, IAutoMouseBounds, IDockClientBody { bool docked; @@ -52,6 +52,7 @@ public class WithVoxelUnloadBody : IAutoMouseBounds, IDockClientBody readonly RenderVoxels rv; public WithVoxelUnloadBody(Actor self, WithVoxelUnloadBodyInfo info) + : base(info) { var body = self.Trait(); rv = self.Trait(); @@ -59,16 +60,16 @@ public WithVoxelUnloadBody(Actor self, WithVoxelUnloadBodyInfo info) var idleModel = rv.Renderer.ModelCache.GetModelSequence(rv.Image, info.IdleSequence); modelAnimation = new ModelAnimation(idleModel, () => WVec.Zero, () => body.QuantizeOrientation(self.Orientation), - () => docked, - () => 0, info.ShowShadow); + () => docked || IsTraitDisabled, + () => 0, Info.ShowShadow); rv.Add(modelAnimation); var unloadModel = rv.Renderer.ModelCache.GetModelSequence(rv.Image, info.UnloadSequence); rv.Add(new ModelAnimation(unloadModel, () => WVec.Zero, () => body.QuantizeOrientation(self.Orientation), - () => !docked, - () => 0, info.ShowShadow)); + () => !docked || IsTraitDisabled, + () => 0, Info.ShowShadow)); } void IDockClientBody.PlayDockAnimation(Actor self, Action after) diff --git a/OpenRA.Mods.Cnc/Traits/SupportPowers/AttackOrderPower.cs b/OpenRA.Mods.Cnc/Traits/SupportPowers/AttackOrderPower.cs index c069637167ed..99d023ee2295 100644 --- a/OpenRA.Mods.Cnc/Traits/SupportPowers/AttackOrderPower.cs +++ b/OpenRA.Mods.Cnc/Traits/SupportPowers/AttackOrderPower.cs @@ -34,13 +34,22 @@ sealed class AttackOrderPowerInfo : SupportPowerInfo, Requires [Desc("Range circle border width.")] public readonly float CircleBorderWidth = 3; + [Desc("Condition set to the unit that will execute the attack while the support power is in targeting mode.")] + [GrantedConditionReference] + public readonly string SupportPowerTargetingCondition = "support-targeting"; + + [Desc("Condition set to the unit is executing the attack while the support power attack is in progress.")] + [GrantedConditionReference] + public readonly string SupportPowerAttackingCondition = "support-attacking"; + public override object Create(ActorInitializer init) { return new AttackOrderPower(init.Self, this); } } - sealed class AttackOrderPower : SupportPower, INotifyCreated, INotifyBurstComplete + sealed class AttackOrderPower : SupportPower, INotifyCreated, INotifyBurstComplete, INotifyBecomingIdle { readonly AttackOrderPowerInfo info; AttackBase attack; + int attackingToken = 0; public AttackOrderPower(Actor self, AttackOrderPowerInfo info) : base(self, info) @@ -58,6 +67,7 @@ public override void Activate(Actor self, Order order, SupportPowerManager manag base.Activate(self, order, manager); PlayLaunchSounds(); + attackingToken = self.GrantCondition(info.SupportPowerAttackingCondition); attack.AttackTarget(order.Target, AttackSource.Default, false, false, true); } @@ -72,6 +82,11 @@ void INotifyBurstComplete.FiredBurst(Actor self, in Target target, Armament a) { self.World.IssueOrder(new Order("Stop", self, false)); } + + void INotifyBecomingIdle.OnBecomingIdle(Actor self) + { + self.RevokeCondition(attackingToken); + } } public class SelectAttackPowerTarget : OrderGenerator @@ -83,14 +98,21 @@ public class SelectAttackPowerTarget : OrderGenerator readonly string cursorBlocked; readonly MouseButton expectedButton; readonly AttackBase attack; + readonly Actor self; + readonly int targetingToken = 0; + bool isFiring; public SelectAttackPowerTarget(Actor self, string order, SupportPowerManager manager, string cursor, MouseButton button, AttackBase attack) { + this.self = self; + // Clear selection if using Left-Click Orders if (Game.Settings.Game.UseClassicMouseStyle) manager.Self.World.Selection.Clear(); instance = manager.GetPowersForActor(self).FirstOrDefault(); + targetingToken = self.GrantCondition((instance.Info as AttackOrderPowerInfo).SupportPowerTargetingCondition); + this.manager = manager; this.order = order; this.cursor = cursor; @@ -109,12 +131,21 @@ bool IsValidTarget(World world, CPos cell) protected override IEnumerable OrderInner(World world, CPos cell, int2 worldPixel, MouseInput mi) { + // CancelInputMode will call Deactivate before we could validate the target, so we need to delay clearing the targeting flag + isFiring = true; world.CancelInputMode(); if (mi.Button == expectedButton && IsValidTarget(world, cell)) + { + self.RevokeCondition(targetingToken); yield return new Order(order, manager.Self, Target.FromCell(world, cell), false) { SuppressVisualFeedback = true }; + } + else + { + self.RevokeCondition(targetingToken); + } } protected override void Tick(World world) @@ -124,6 +155,14 @@ protected override void Tick(World world) world.CancelInputMode(); } + protected override void Deactivate() + { + if (!isFiring) + { + self.RevokeCondition(targetingToken); + } + } + protected override IEnumerable Render(WorldRenderer wr, World world) { yield break; } protected override IEnumerable RenderAboveShroud(WorldRenderer wr, World world) { yield break; } diff --git a/OpenRA.Mods.Cnc/Traits/SupportPowers/ChronoshiftPower.cs b/OpenRA.Mods.Cnc/Traits/SupportPowers/ChronoshiftPower.cs index 926d6c6d79e4..477c00321312 100644 --- a/OpenRA.Mods.Cnc/Traits/SupportPowers/ChronoshiftPower.cs +++ b/OpenRA.Mods.Cnc/Traits/SupportPowers/ChronoshiftPower.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Linq; using OpenRA.Graphics; +using OpenRA.Mods.Common.Effects; using OpenRA.Mods.Common.Orders; using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; @@ -23,14 +24,14 @@ sealed class ChronoshiftPowerInfo : SupportPowerInfo { [FieldLoader.Require] [Desc("Size of the footprint of the affected area.")] - public readonly CVec Dimensions = CVec.Zero; + public readonly Dictionary Dimensions = new(); [FieldLoader.Require] [Desc("Actual footprint. Cells marked as x will be affected.")] - public readonly string Footprint = string.Empty; + public readonly Dictionary Footprints = new(); [Desc("Ticks until returning after teleportation.")] - public readonly int Duration = 750; + public readonly Dictionary Durations = new(); [PaletteReference] public readonly string TargetOverlayPalette = TileSet.TerrainPaletteInternalName; @@ -48,6 +49,32 @@ sealed class ChronoshiftPowerInfo : SupportPowerInfo public readonly bool KillCargo = true; + public readonly string EffectImage = null; + + [SequenceReference(nameof(EffectImage))] + public readonly string SelectionStartSequence = null; + + [SequenceReference(nameof(EffectImage))] + public readonly string SelectionLoopSequence = null; + + [SequenceReference(nameof(EffectImage))] + public readonly string SelectionEndSequence = null; + + [SequenceReference(nameof(EffectImage))] + public readonly string TeleportTargetSequence = null; + + [PaletteReference] + public readonly string EffectPalette = null; + + [Desc("Condition to grant while teleportation is happening.")] + public readonly string TeleportingCondition = null; + + [Desc("Delay after application of the ability before teleportation happens.")] + public readonly int BeforeTeleportDelay = 0; + + [Desc("Delay after teleportation that TeleoprtingCondition is kept.")] + public readonly int AfterTeleportDelay = 0; + [CursorReference] [Desc("Cursor to display when selecting targets for the chronoshift.")] public readonly string SelectionCursor = "chrono-select"; @@ -60,19 +87,27 @@ sealed class ChronoshiftPowerInfo : SupportPowerInfo [Desc("Cursor to display when the targeted area is blocked.")] public readonly string TargetBlockedCursor = "move-blocked"; + [Desc("Can we teleport to units to places they can't enter to kill them.")] + public readonly bool AllowImpassable = false; + public override object Create(ActorInitializer init) { return new ChronoshiftPower(init.Self, this); } } sealed class ChronoshiftPower : SupportPower { - readonly char[] footprint; - readonly CVec dimensions; + readonly Dictionary footprints = new(); + readonly Dictionary dimensions; + + public readonly bool AllowImpassable; public ChronoshiftPower(Actor self, ChronoshiftPowerInfo info) : base(self, info) { - footprint = info.Footprint.Where(c => !char.IsWhiteSpace(c)).ToArray(); + foreach (var pair in info.Footprints) + footprints.Add(pair.Key, pair.Value.Where(c => !char.IsWhiteSpace(c)).ToArray()); + dimensions = info.Dimensions; + AllowImpassable = info.AllowImpassable; } public override void SelectTarget(Actor self, string order, SupportPowerManager manager) @@ -86,6 +121,9 @@ public override void Activate(Actor self, Order order, SupportPowerManager manag PlayLaunchSounds(); var info = (ChronoshiftPowerInfo)Info; + if (!string.IsNullOrEmpty(info.TeleportTargetSequence) && !string.IsNullOrEmpty(info.EffectPalette)) + self.World.Add(new SpriteEffect(order.Target.CenterPosition, self.World, info.EffectImage, info.TeleportTargetSequence, info.EffectPalette)); + var targetDelta = self.World.Map.CellContaining(order.Target.CenterPosition) - order.ExtraLocation; foreach (var target in UnitsInRange(order.ExtraLocation)) { @@ -97,14 +135,15 @@ public override void Activate(Actor self, Order order, SupportPowerManager manag var targetCell = target.Location + targetDelta; - if (self.Owner.Shroud.IsExplored(targetCell) && cs.CanChronoshiftTo(target, targetCell)) - cs.Teleport(target, targetCell, info.Duration, info.KillCargo, self); + if (self.Owner.Shroud.IsExplored(targetCell) && (cs.CanChronoshiftTo(target, targetCell) || AllowImpassable)) + cs.Teleport(target, targetCell, info.Durations.First(d => d.Key == GetLevel()).Value, info.KillCargo, self, info.BeforeTeleportDelay, info.AfterTeleportDelay, info.TeleportingCondition); } } public IEnumerable UnitsInRange(CPos xy) { - var tiles = CellsMatching(xy, footprint, dimensions); + var level = GetLevel(); + var tiles = CellsMatching(xy, footprints.First(f => f.Key == level).Value, dimensions.First(d => d.Key == level).Value); var units = new HashSet(); foreach (var t in tiles) units.UnionWith(Self.World.ActorMap.GetActorsAt(t)); @@ -117,9 +156,11 @@ public bool SimilarTerrain(CPos xy, CPos sourceLocation) if (!Self.Owner.Shroud.IsExplored(xy)) return false; - var sourceTiles = CellsMatching(xy, footprint, dimensions); - var destTiles = CellsMatching(sourceLocation, footprint, dimensions); - + var level = GetLevel(); + var footprint = footprints.First(f => f.Key == level).Value; + var dimension = dimensions.First(f => f.Key == level).Value; + var sourceTiles = CellsMatching(xy, footprint, dimension); + var destTiles = CellsMatching(sourceLocation, footprint, dimension); if (!sourceTiles.Any() || !destTiles.Any()) return false; @@ -143,8 +184,8 @@ public bool SimilarTerrain(CPos xy, CPos sourceLocation) sealed class SelectChronoshiftTarget : OrderGenerator { readonly ChronoshiftPower power; - readonly char[] footprint; - readonly CVec dimensions; + readonly Dictionary footprints = new(); + readonly Dictionary dimensions; readonly Sprite tile; readonly float alpha; readonly SupportPowerManager manager; @@ -162,7 +203,9 @@ public SelectChronoshiftTarget(World world, string order, SupportPowerManager ma var info = (ChronoshiftPowerInfo)power.Info; var s = world.Map.Sequences.GetSequence(info.FootprintImage, info.SourceFootprintSequence); - footprint = info.Footprint.Where(c => !char.IsWhiteSpace(c)).ToArray(); + foreach (var pair in info.Footprints) + footprints.Add(pair.Key, pair.Value.Where(c => !char.IsWhiteSpace(c)).ToArray()); + dimensions = info.Dimensions; tile = s.GetSprite(0); alpha = s.GetAlpha(0); @@ -206,7 +249,8 @@ protected override IEnumerable RenderAnnotations(WorldRenderer wr, protected override IEnumerable Render(WorldRenderer wr, World world) { var xy = wr.Viewport.ViewToWorld(Viewport.LastMousePos); - var tiles = power.CellsMatching(xy, footprint, dimensions); + var level = power.GetLevel(); + var tiles = power.CellsMatching(xy, footprints.First(f => f.Key == level).Value, dimensions.First(d => d.Key == level).Value); var palette = wr.Palette(((ChronoshiftPowerInfo)power.Info).TargetOverlayPalette); foreach (var t in tiles) yield return new SpriteRenderable(tile, wr.World.Map.CenterOfCell(t), WVec.Zero, -511, palette, 1f, alpha, float3.Ones, TintModifiers.IgnoreWorldTint, true); @@ -222,11 +266,12 @@ sealed class SelectDestination : OrderGenerator { readonly ChronoshiftPower power; readonly CPos sourceLocation; - readonly char[] footprint; - readonly CVec dimensions; + readonly Dictionary footprints = new(); + readonly Dictionary dimensions; readonly Sprite validTile, invalidTile, sourceTile; readonly float validAlpha, invalidAlpha, sourceAlpha; readonly SupportPowerManager manager; + readonly Animation overlay; readonly string order; public SelectDestination(World world, string order, SupportPowerManager manager, ChronoshiftPower power, CPos sourceLocation) @@ -237,10 +282,24 @@ public SelectDestination(World world, string order, SupportPowerManager manager, this.sourceLocation = sourceLocation; var info = (ChronoshiftPowerInfo)power.Info; - footprint = info.Footprint.Where(c => !char.IsWhiteSpace(c)).ToArray(); - dimensions = info.Dimensions; + if (info.EffectImage != null) + { + overlay = new Animation(world, info.EffectImage); + + var powerInfo = (ChronoshiftPowerInfo)power.Info; + if (powerInfo.SelectionStartSequence != null) + overlay.PlayThen(powerInfo.SelectionStartSequence, + () => overlay.PlayRepeating(powerInfo.SelectionLoopSequence)); + else + overlay.PlayRepeating(powerInfo.SelectionLoopSequence); + } + + foreach (var pair in info.Footprints) + footprints.Add(pair.Key, pair.Value.Where(c => !char.IsWhiteSpace(c)).ToArray()); + dimensions = info.Dimensions; var sequences = world.Map.Sequences; + var tilesetValid = info.ValidFootprintSequence + "-" + world.Map.Tileset.ToLowerInvariant(); if (sequences.HasSequence(info.FootprintImage, tilesetValid)) { @@ -264,8 +323,17 @@ public SelectDestination(World world, string order, SupportPowerManager manager, sourceAlpha = sourceSequence.GetAlpha(0); } + void PlayCancelAnim(World world) + { + var info = (ChronoshiftPowerInfo)power.Info; + if (!string.IsNullOrEmpty(info.SelectionEndSequence) && !string.IsNullOrEmpty(info.EffectPalette)) + world.Add(new SpriteEffect(world.Map.CenterOfCell(sourceLocation), world, info.EffectImage, info.SelectionEndSequence, info.EffectPalette)); + } + protected override IEnumerable OrderInner(World world, CPos cell, int2 worldPixel, MouseInput mi) { + PlayCancelAnim(world); + if (mi.Button == MouseButton.Right) { world.CancelInputMode(); @@ -295,7 +363,13 @@ protected override void Tick(World world) { // Cancel the OG if we can't use the power if (!manager.Powers.TryGetValue(order, out var p) || !p.Active || !p.Ready) + { + PlayCancelAnim(world); + world.CancelInputMode(); + } + + overlay?.Tick(); } protected override IEnumerable RenderAboveShroud(WorldRenderer wr, World world) @@ -305,7 +379,8 @@ protected override IEnumerable RenderAboveShroud(WorldRenderer wr, // Destination tiles var delta = xy - sourceLocation; - foreach (var t in power.CellsMatching(sourceLocation, footprint, dimensions)) + var level = power.GetLevel(); + foreach (var t in power.CellsMatching(sourceLocation, footprints.First(f => f.Key == level).Value, dimensions.First(d => d.Key == level).Value)) { var isValid = manager.Self.Owner.Shroud.IsExplored(t + delta); var tile = isValid ? validTile : invalidTile; @@ -320,7 +395,7 @@ protected override IEnumerable RenderAboveShroud(WorldRenderer wr, { var targetCell = unit.Location + (xy - sourceLocation); var canEnter = manager.Self.Owner.Shroud.IsExplored(targetCell) && - unit.Trait().CanChronoshiftTo(unit, targetCell); + (unit.Trait().CanChronoshiftTo(unit, targetCell) || power.AllowImpassable); var tile = canEnter ? validTile : invalidTile; var alpha = canEnter ? validAlpha : invalidAlpha; yield return new SpriteRenderable(tile, wr.World.Map.CenterOfCell(targetCell), WVec.Zero, -511, palette, 1f, alpha, float3.Ones, TintModifiers.IgnoreWorldTint, true); @@ -349,10 +424,17 @@ protected override IEnumerable RenderAnnotations(WorldRenderer wr, protected override IEnumerable Render(WorldRenderer wr, World world) { - var palette = wr.Palette(power.Info.IconPalette); + if (overlay != null) + { + var powerInfo = (ChronoshiftPowerInfo)power.Info; + foreach (var r in overlay.Render(world.Map.CenterOfCell(sourceLocation), wr.Palette(powerInfo.EffectPalette))) + yield return r; + } // Source tiles - foreach (var t in power.CellsMatching(sourceLocation, footprint, dimensions)) + var palette = wr.Palette(power.Info.IconPalette); + var level = power.GetLevel(); + foreach (var t in power.CellsMatching(sourceLocation, footprints.First(f => f.Key == level).Value, dimensions.First(d => d.Key == level).Value)) yield return new SpriteRenderable(sourceTile, wr.World.Map.CenterOfCell(t), WVec.Zero, -511, palette, 1f, sourceAlpha, float3.Ones, TintModifiers.IgnoreWorldTint, true); } @@ -364,7 +446,7 @@ bool IsValidTarget(CPos xy) { anyUnitsInRange = true; var targetCell = unit.Location + (xy - sourceLocation); - if (manager.Self.Owner.Shroud.IsExplored(targetCell) && unit.Trait().CanChronoshiftTo(unit, targetCell)) + if (manager.Self.Owner.Shroud.IsExplored(targetCell) && (unit.Trait().CanChronoshiftTo(unit, targetCell) || power.AllowImpassable)) { canTeleport = true; break; diff --git a/OpenRA.Mods.Cnc/Traits/World/ChronoVortexRenderer.cs b/OpenRA.Mods.Cnc/Traits/World/ChronoVortexRenderer.cs new file mode 100644 index 000000000000..b62f711456f8 --- /dev/null +++ b/OpenRA.Mods.Cnc/Traits/World/ChronoVortexRenderer.cs @@ -0,0 +1,109 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Cnc.Traits +{ + [TraitLocation(SystemActors.World | SystemActors.EditorWorld)] + [Desc("Render chrono vortex")] + public class ChronoVortexRendererInfo : TraitInfo + { + public override object Create(ActorInitializer init) { return new ChronoVortexRenderer(init.Self); } + } + + public sealed class ChronoVortexRenderer : IRenderPostProcessPass + { + readonly Renderer renderer; + readonly IShader shader; + readonly IVertexBuffer vortexBuffer; + readonly Sheet vortexSheet; + readonly List<(float3, int)> vortices = new(); + + public ChronoVortexRenderer(Actor self) + { + renderer = Game.Renderer; + shader = renderer.CreateShader(new RenderPostProcessPassTexturedShaderBindings("vortex")); + + vortexSheet = new Sheet(SheetType.BGRA, new Size(512, 512)); + vortexBuffer = renderer.CreateVertexBuffer(288); + var vertices = new RenderPostProcessPassTexturedVertex[288]; + + var data = vortexSheet.GetData(); + var j = 0; + for (var f = 0; f < 48; f++) + { + var row = f / 8; + var col = f % 8; + + using (var stream = self.World.Map.Open($"hole{f:D04}.lut")) + { + for (var y = 0; y < 64; y++) + { + var i = 2048 * (64 * row + y) + 256 * col; + for (var x = 0; x < 64; x++) + { + data[i++] = (byte)(stream.ReadUInt8() + 128 - x); + data[i++] = (byte)(stream.ReadUInt8() + 128 - y); + data[i++] = stream.ReadUInt8(); + data[i++] = 255; + } + } + } + + var tl = new float2(col, row) / 8; + var br = new float2(col + 1, row + 1) / 8; + vertices[j++] = new RenderPostProcessPassTexturedVertex(-32, -32, tl.X, tl.Y); + vertices[j++] = new RenderPostProcessPassTexturedVertex(32, -32, br.X, tl.Y); + vertices[j++] = new RenderPostProcessPassTexturedVertex(32, 32, br.X, br.Y); + vertices[j++] = new RenderPostProcessPassTexturedVertex(32, 32, br.X, br.Y); + vertices[j++] = new RenderPostProcessPassTexturedVertex(-32, 32, tl.X, br.Y); + vertices[j++] = new RenderPostProcessPassTexturedVertex(-32, -32, tl.X, tl.Y); + } + + vortexBuffer.SetData(ref vertices, 288); + vortexSheet.CommitBufferedData(); + } + + public void DrawVortex(float3 pos, int frame) + { + vortices.Add((pos, frame)); + } + + PostProcessPassType IRenderPostProcessPass.Type => PostProcessPassType.AfterWorld; + bool IRenderPostProcessPass.Enabled => vortices.Count > 0; + + void IRenderPostProcessPass.Draw(WorldRenderer wr, ITexture worldTexture) + { + var scroll = wr.Viewport.TopLeft; + var size = renderer.WorldFrameBufferSize; + var width = 2f / (renderer.WorldDownscaleFactor * size.Width); + var height = 2f / (renderer.WorldDownscaleFactor * size.Height); + + shader.SetVec("Scroll", scroll.X, scroll.Y); + shader.SetVec("p1", width, height); + shader.SetVec("p2", -1, -1); + shader.SetTexture("WorldTexture", worldTexture); + shader.SetTexture("VortexTexture", vortexSheet.GetTexture()); + shader.PrepareRender(); + foreach (var (pos, frame) in vortices) + { + shader.SetVec("Pos", pos.X, pos.Y); + renderer.DrawBatch(vortexBuffer, shader, 6 * frame, 6, PrimitiveType.TriangleList); + } + + vortices.Clear(); + } + } +} diff --git a/OpenRA.Mods.Cnc/Traits/World/ModelRenderer.cs b/OpenRA.Mods.Cnc/Traits/World/ModelRenderer.cs index 7bd6c98753af..57d6a66c4eba 100644 --- a/OpenRA.Mods.Cnc/Traits/World/ModelRenderer.cs +++ b/OpenRA.Mods.Cnc/Traits/World/ModelRenderer.cs @@ -65,9 +65,10 @@ public sealed class ModelRenderer : IDisposable, IRenderer, INotifyActorDisposin SheetBuilder sheetBuilderForFrame; bool isInFrame; - public void SetPalette(ITexture palette) + public void SetPalette(HardwarePalette palette) { - shader.SetTexture("Palette", palette); + shader.SetTexture("Palette", palette.Texture); + shader.SetVec("PaletteRows", palette.Height); } public ModelRenderer(ModelRendererInfo info, Actor self) @@ -226,12 +227,12 @@ public ModelRenderProxy RenderAsync( var lightDirection = ExtractRotationVector(Util.MatrixMultiply(it, lightTransform)); Render(rd, ModelCache, Util.MatrixMultiply(transform, t), lightDirection, - lightAmbientColor, lightDiffuseColor, color.TextureMidIndex, normals.TextureMidIndex); + lightAmbientColor, lightDiffuseColor, color.TextureIndex, normals.TextureIndex); // Disable shadow normals by forcing zero diffuse and identity ambient light if (m.ShowShadow) Render(rd, ModelCache, Util.MatrixMultiply(shadow, t), lightDirection, - ShadowAmbient, ShadowDiffuse, shadowPalette.TextureMidIndex, normals.TextureMidIndex); + ShadowAmbient, ShadowDiffuse, shadowPalette.TextureIndex, normals.TextureIndex); } } })); @@ -279,10 +280,10 @@ void Render( IModelCache cache, float[] t, float[] lightDirection, float[] ambientLight, float[] diffuseLight, - float colorPaletteTextureMidIndex, float normalsPaletteTextureMidIndex) + float colorPaletteTextureIndex, float normalsPaletteTextureIndex) { shader.SetTexture("DiffuseTexture", renderData.Sheet.GetTexture()); - shader.SetVec("PaletteRows", colorPaletteTextureMidIndex, normalsPaletteTextureMidIndex); + shader.SetVec("Palettes", colorPaletteTextureIndex, normalsPaletteTextureIndex); shader.SetMatrix("TransformMatrix", t); shader.SetVec("LightDirection", lightDirection, 4); shader.SetVec("AmbientLight", ambientLight, 3); diff --git a/OpenRA.Mods.Cnc/Traits/World/TSEditorResourceLayer.cs b/OpenRA.Mods.Cnc/Traits/World/TSEditorResourceLayer.cs index 162c347e5a16..3b3e7ed294e2 100644 --- a/OpenRA.Mods.Cnc/Traits/World/TSEditorResourceLayer.cs +++ b/OpenRA.Mods.Cnc/Traits/World/TSEditorResourceLayer.cs @@ -23,7 +23,7 @@ sealed class TSEditorResourceLayerInfo : EditorResourceLayerInfo, Requires VeinholeActors = new() { }; + public readonly HashSet VeinholeActors = new(); public override object Create(ActorInitializer init) { return new TSEditorResourceLayer(init.Self, this); } } diff --git a/OpenRA.Mods.Cnc/Traits/World/TSResourceLayer.cs b/OpenRA.Mods.Cnc/Traits/World/TSResourceLayer.cs index 0e3177c1b90f..c99e33230402 100644 --- a/OpenRA.Mods.Cnc/Traits/World/TSResourceLayer.cs +++ b/OpenRA.Mods.Cnc/Traits/World/TSResourceLayer.cs @@ -25,7 +25,7 @@ sealed class TSResourceLayerInfo : ResourceLayerInfo [ActorReference] [Desc("Actor types that should be treated as veins for adjacency.")] - public readonly HashSet VeinholeActors = new() { }; + public readonly HashSet VeinholeActors = new(); public override object Create(ActorInitializer init) { return new TSResourceLayer(init.Self, this); } } diff --git a/OpenRA.Mods.Cnc/Traits/World/TSVeinsRenderer.cs b/OpenRA.Mods.Cnc/Traits/World/TSVeinsRenderer.cs index 8230e89fb7f9..776fdfb49ce3 100644 --- a/OpenRA.Mods.Cnc/Traits/World/TSVeinsRenderer.cs +++ b/OpenRA.Mods.Cnc/Traits/World/TSVeinsRenderer.cs @@ -44,7 +44,7 @@ public class TSVeinsRendererInfo : TraitInfo, Requires, IMap [ActorReference] [Desc("Actor types that should be treated as veins for adjacency.")] - public readonly HashSet VeinholeActors = new() { }; + public readonly HashSet VeinholeActors = new(); void IMapPreviewSignatureInfo.PopulateMapPreviewSignatureCells(Map map, ActorInfo ai, ActorReference s, List<(MPos Uv, Color Color)> destinationBuffer) { diff --git a/OpenRA.Mods.Cnc/TraitsInterfaces.cs b/OpenRA.Mods.Cnc/TraitsInterfaces.cs index e8d2cb07ec79..fd3c6c5aed3c 100644 --- a/OpenRA.Mods.Cnc/TraitsInterfaces.cs +++ b/OpenRA.Mods.Cnc/TraitsInterfaces.cs @@ -15,4 +15,7 @@ namespace OpenRA.Mods.Cnc.Traits { [RequireExplicitImplementation] public interface INotifyTeslaCharging { void Charging(Actor self, in Target target); } + + [RequireExplicitImplementation] + public interface INotifyDisguised { void DisguiseChanged(Actor self, Actor target); } } diff --git a/OpenRA.Mods.Cnc/UtilityCommands/Glob.cs b/OpenRA.Mods.Cnc/UtilityCommands/Glob.cs index 79821d2d5cc9..652da65ec6cd 100644 --- a/OpenRA.Mods.Cnc/UtilityCommands/Glob.cs +++ b/OpenRA.Mods.Cnc/UtilityCommands/Glob.cs @@ -65,7 +65,7 @@ public static IEnumerable Expand(string filePath) if (parts.Count == 0 || (parts[0][0] != Path.DirectorySeparatorChar && parts[0][0] != Path.AltDirectorySeparatorChar - && parts[0].Contains(':') == false + && !parts[0].Contains(':') && parts[0] != "." + Path.DirectorySeparatorChar && parts[0] != "." + Path.AltDirectorySeparatorChar && parts[0] != ".." + Path.DirectorySeparatorChar diff --git a/OpenRA.Mods.Cnc/UtilityCommands/ImportGen2MapCommand.cs b/OpenRA.Mods.Cnc/UtilityCommands/ImportGen2MapCommand.cs index 921b756dffcb..c6cf9c2272b6 100644 --- a/OpenRA.Mods.Cnc/UtilityCommands/ImportGen2MapCommand.cs +++ b/OpenRA.Mods.Cnc/UtilityCommands/ImportGen2MapCommand.cs @@ -49,6 +49,8 @@ public abstract class ImportGen2MapCommand protected abstract Dictionary DeployableActors { get; } + protected abstract Dictionary ReplaceActors { get; } + protected abstract string[] LampActors { get; } protected abstract string[] CreepActors { get; } @@ -65,6 +67,9 @@ protected void Run(Utility utility, string[] args) var tileset = mapSection.GetValue("Theater", ""); var iniSize = mapSection.GetValue("Size", "0, 0, 0, 0").Split(',').Select(int.Parse).ToArray(); var iniBounds = mapSection.GetValue("LocalSize", "0, 0, 0, 0").Split(',').Select(int.Parse).ToArray(); + var author = args.Length > 2 + ? args[2] + : "Westwood Studios"; if (!utility.ModData.DefaultTerrainInfo.TryGetValue(tileset, out var terrainInfo)) throw new InvalidDataException($"Unknown tileset {tileset}"); @@ -75,7 +80,7 @@ protected void Run(Utility utility, string[] args) var map = new Map(Game.ModData, terrainInfo, mapCanvasSize.Width, mapCanvasSize.Height) { Title = basic.GetValue("Name", Path.GetFileNameWithoutExtension(filename)), - Author = "Westwood Studios", + Author = author, RequiresMod = utility.ModData.Manifest.Id }; @@ -236,6 +241,9 @@ protected virtual void ReadTerrainActors(Map map, IniFile file, int2 fullSize) var cell = ToMPos(rx, ry, fullSize.X).ToCPos(map); var name = kv.Value.ToLowerInvariant(); + if (ReplaceActors.TryGetValue(name, out var replacement)) + name = replacement; + var ar = new ActorReference(name) { new LocationInit(cell), @@ -268,6 +276,9 @@ protected virtual void ReadActors(Map map, IniFile file, string type, int2 fullS isDeployed = true; } + if (ReplaceActors.TryGetValue(name, out var replacement)) + name = replacement; + var health = Exts.ParseInt16Invariant(entries[2]); var rx = Exts.ParseInt32Invariant(entries[3]); var ry = Exts.ParseInt32Invariant(entries[4]); diff --git a/OpenRA.Mods.Cnc/UtilityCommands/ImportRedAlertMapCommand.cs b/OpenRA.Mods.Cnc/UtilityCommands/ImportRedAlertMapCommand.cs index 4e57805876d6..cc1b550404d0 100644 --- a/OpenRA.Mods.Cnc/UtilityCommands/ImportRedAlertMapCommand.cs +++ b/OpenRA.Mods.Cnc/UtilityCommands/ImportRedAlertMapCommand.cs @@ -34,10 +34,7 @@ public ImportRedAlertMapCommand() public override void ValidateMapFormat(int format) { if (format < 2) - { Console.WriteLine($"ERROR: Detected NewINIFormat {format}. Are you trying to import a Tiberian Dawn map?"); - return; - } } // Mapping from RA95 overlay index to type string @@ -146,46 +143,46 @@ public override void LoadPlayer(IniFile file, string section) string faction; switch (section) { - case "Spain": - color = "gold"; - faction = "allies"; - break; - case "England": - color = "green"; - faction = "allies"; - break; - case "Ukraine": - color = "orange"; - faction = "soviet"; - break; - case "Germany": - color = "black"; - faction = "allies"; - break; - case "France": - color = "teal"; - faction = "allies"; - break; - case "Turkey": - color = "salmon"; - faction = "allies"; - break; - case "Greece": - case "GoodGuy": - color = "blue"; - faction = "allies"; - break; - case "USSR": - case "BadGuy": - color = "red"; - faction = "soviet"; - break; - case "Special": - case "Neutral": - default: - color = "neutral"; - faction = "allies"; - break; + case "Spain": + color = "gold"; + faction = "allies"; + break; + case "England": + color = "green"; + faction = "allies"; + break; + case "Ukraine": + color = "orange"; + faction = "soviet"; + break; + case "Germany": + color = "black"; + faction = "allies"; + break; + case "France": + color = "teal"; + faction = "allies"; + break; + case "Turkey": + color = "salmon"; + faction = "allies"; + break; + case "Greece": + case "GoodGuy": + color = "blue"; + faction = "allies"; + break; + case "USSR": + case "BadGuy": + color = "red"; + faction = "soviet"; + break; + case "Special": + case "Neutral": + default: + color = "neutral"; + faction = "allies"; + break; } SetMapPlayers(section, faction, color, file, Players, MapPlayers); diff --git a/OpenRA.Mods.Cnc/UtilityCommands/ImportTiberianDawnMapCommand.cs b/OpenRA.Mods.Cnc/UtilityCommands/ImportTiberianDawnMapCommand.cs index 73ee5f8e4c5c..b775ec3b5d08 100644 --- a/OpenRA.Mods.Cnc/UtilityCommands/ImportTiberianDawnMapCommand.cs +++ b/OpenRA.Mods.Cnc/UtilityCommands/ImportTiberianDawnMapCommand.cs @@ -32,10 +32,7 @@ public ImportTiberianDawnMapCommand() public override void ValidateMapFormat(int format) { if (format > 1) - { Console.WriteLine($"ERROR: Detected NewINIFormat {format}. Are you trying to import a Red Alert map?"); - return; - } } static readonly Dictionary OverlayResourceMapping = new() @@ -145,20 +142,20 @@ public override void LoadPlayer(IniFile file, string section) string faction; switch (section) { - case "GoodGuy": - color = "gold"; - faction = "gdi"; - break; - case "BadGuy": - color = "red"; - faction = "nod"; - break; - case "Special": - case "Neutral": - default: - color = "neutral"; - faction = "gdi"; - break; + case "GoodGuy": + color = "gold"; + faction = "gdi"; + break; + case "BadGuy": + color = "red"; + faction = "nod"; + break; + case "Special": + case "Neutral": + default: + color = "neutral"; + faction = "gdi"; + break; } SetMapPlayers(section, faction, color, file, Players, MapPlayers); diff --git a/OpenRA.Mods.Cnc/UtilityCommands/ImportTiberianSunMapCommand.cs b/OpenRA.Mods.Cnc/UtilityCommands/ImportTiberianSunMapCommand.cs index b04cd6bb3a44..d8717172fef3 100644 --- a/OpenRA.Mods.Cnc/UtilityCommands/ImportTiberianSunMapCommand.cs +++ b/OpenRA.Mods.Cnc/UtilityCommands/ImportTiberianSunMapCommand.cs @@ -255,6 +255,8 @@ void IUtilityCommand.Run(Utility utility, string[] args) // { "ddefd", "defender" }, }; + protected override Dictionary ReplaceActors { get; } = new() { }; + protected override string[] LampActors { get; } = { "GALITE", "INGALITE", "NEGLAMP", "REDLAMP", "NEGRED", "GRENLAMP", "BLUELAMP", "YELWLAMP", diff --git a/OpenRA.Mods.Cnc/Widgets/ModelWidget.cs b/OpenRA.Mods.Cnc/Widgets/ModelWidget.cs index 1ed7b67fc5d5..8c1070914382 100644 --- a/OpenRA.Mods.Cnc/Widgets/ModelWidget.cs +++ b/OpenRA.Mods.Cnc/Widgets/ModelWidget.cs @@ -95,19 +95,16 @@ public override Widget Clone() return new ModelWidget(this); } - IModel cachedVoxel; string cachedPalette; string cachedPlayerPalette; string cachedNormalsPalette; string cachedShadowPalette; - float cachedScale; - WRot cachedRotation; - float[] cachedLightAmbientColor = new float[] { 0, 0, 0 }; - float[] cachedLightDiffuseColor = new float[] { 0, 0, 0 }; int cachedLightPitch; int cachedLightYaw; + WRot cachedLightSource; WAngle cachedCameraAngle; PaletteReference paletteReference; + WRot cachedCameraRotation; PaletteReference paletteReferencePlayer; PaletteReference paletteReferenceNormals; PaletteReference paletteReferenceShadow; @@ -123,8 +120,14 @@ public override void Draw() public override void PrepareRenderables() { var voxel = GetVoxel(); + if (voxel == null) + return; + var palette = GetPalette(); var playerPalette = GetPlayerPalette(); + if (string.IsNullOrEmpty(palette) && string.IsNullOrEmpty(playerPalette)) + return; + var normalsPalette = GetNormalsPalette(); var shadowPalette = GetShadowPalette(); var scale = GetScale(); @@ -135,20 +138,10 @@ public override void PrepareRenderables() var lightYaw = GetLightYaw(); var cameraAngle = GetCameraAngle(); - if (voxel == null || palette == null) - return; - - if (voxel != cachedVoxel) - cachedVoxel = voxel; - if (palette != cachedPalette) { - if (string.IsNullOrEmpty(palette) && string.IsNullOrEmpty(playerPalette)) - return; - - var paletteName = string.IsNullOrEmpty(palette) ? playerPalette : palette; - paletteReference = WorldRenderer.Palette(paletteName); - cachedPalette = paletteName; + paletteReference = WorldRenderer.Palette(playerPalette); + cachedPalette = palette; } if (playerPalette != cachedPlayerPalette) @@ -169,65 +162,39 @@ public override void PrepareRenderables() cachedShadowPalette = shadowPalette; } - if (scale != cachedScale) - cachedScale = scale; - - if (rotation != cachedRotation) - cachedRotation = rotation; - - if (lightPitch != cachedLightPitch) + if (lightPitch != cachedLightPitch || lightYaw != cachedLightYaw) + { cachedLightPitch = lightPitch; - - if (lightYaw != cachedLightYaw) cachedLightYaw = lightYaw; - - if (cachedLightAmbientColor[0] != lightAmbientColor[0] || cachedLightAmbientColor[1] != lightAmbientColor[1] || cachedLightAmbientColor[2] != lightAmbientColor[2]) - cachedLightAmbientColor = lightAmbientColor; - - if (cachedLightDiffuseColor[0] != lightDiffuseColor[0] || cachedLightDiffuseColor[1] != lightDiffuseColor[1] || cachedLightDiffuseColor[2] != lightDiffuseColor[2]) - cachedLightDiffuseColor = lightDiffuseColor; + cachedLightSource = new WRot(WAngle.Zero, new WAngle(256 - lightPitch), new WAngle(lightYaw)); + } if (cameraAngle != cachedCameraAngle) + { cachedCameraAngle = cameraAngle; - - if (cachedVoxel == null) - return; + cachedCameraRotation = new WRot(WAngle.Zero, cameraAngle - new WAngle(256), new WAngle(256)); + } var animation = new ModelAnimation( - cachedVoxel, + voxel, () => WVec.Zero, - () => cachedRotation, + () => rotation, () => false, () => 0, true); var animations = new ModelAnimation[] { animation }; - var renderer = WorldRenderer.World.WorldActor.Trait(); - - var preview = new ModelPreview( - renderer, - new ModelAnimation[] { animation }, WVec.Zero, 0, - cachedScale, - new WAngle(cachedLightPitch), - new WAngle(cachedLightYaw), - cachedLightAmbientColor, - cachedLightDiffuseColor, - cachedCameraAngle, - paletteReference, - paletteReferenceNormals, - paletteReferenceShadow); - var screenBounds = animation.ScreenBounds(WPos.Zero, WorldRenderer, scale); IdealPreviewSize = new int2(screenBounds.Width, screenBounds.Height); var origin = RenderOrigin + new int2(RenderBounds.Size.Width / 2, RenderBounds.Size.Height / 2); - var camera = new WRot(WAngle.Zero, cachedCameraAngle - new WAngle(256), new WAngle(256)); + var renderer = WorldRenderer.World.WorldActor.Trait(); var modelRenderable = new UIModelRenderable( renderer, - animations, WPos.Zero, origin, 0, camera, scale, - WRot.None, cachedLightAmbientColor, cachedLightDiffuseColor, - paletteReferencePlayer, paletteReferenceNormals, paletteReferenceShadow); + animations, WPos.Zero, origin, 0, cachedCameraRotation, scale, + cachedLightSource, lightAmbientColor, lightDiffuseColor, + paletteReferencePlayer ?? paletteReference, paletteReferenceNormals, paletteReferenceShadow); renderable = modelRenderable.PrepareRender(WorldRenderer); } diff --git a/OpenRA.Mods.Common/AIUtils.cs b/OpenRA.Mods.Common/AIUtils.cs index ec90bed68897..75eaffcb69ab 100644 --- a/OpenRA.Mods.Common/AIUtils.cs +++ b/OpenRA.Mods.Common/AIUtils.cs @@ -22,6 +22,24 @@ public enum WaterCheck { NotChecked, EnoughWater, NotEnoughWater, DontCheck } public static class AIUtils { + public static bool PathExist(Actor unit, CPos destination, Actor ignoreActor, BlockedByActor blockedByActor = BlockedByActor.Immovable) + { + var mobile = unit.TraitOrDefault(); + if (mobile == null) + { + // We consider other IMove ignore all blockers + if (unit.TraitsImplementing().Any()) + return true; + else + return false; + } + + if (mobile.PathFinder.FindPathToTargetCell(unit, new List { unit.Location }, destination, blockedByActor, ignoreActor: ignoreActor, laneBias: false).Count > 0) + return true; + else + return false; + } + public static bool IsAreaAvailable(World world, Player player, Map map, int radius, HashSet terrainTypes) { var cells = world.ActorsHavingTrait().Where(a => a.Owner == player); @@ -74,7 +92,7 @@ public static void BotDebug(string format, params object[] args) TextNotificationsManager.Debug(format, args); } - public static IEnumerable ClearBlockersOrders(IEnumerable tiles, Player owner, Actor ignoreActor = null) + public static IEnumerable ClearBlockersOrders(List tiles, Player owner, Actor ignoreActor = null) { var world = owner.World; var adjacentTiles = Util.ExpandFootprint(tiles, true).Except(tiles) @@ -107,5 +125,33 @@ public static IEnumerable ClearBlockersOrders(IEnumerable tiles, Pl }; } } + + public static bool CanPlaceBuildingWithSpaceAround(World world, CPos cell, ActorInfo ai, BuildingInfo bi, Actor toIgnore, int cellDist) + { + if (cellDist > 0) + { + var buildingInfluence = world.WorldActor.Trait(); + var left = -cellDist; + var right = bi.Dimensions.X + cellDist - 1; + var top = -cellDist; + var bottom = bi.Dimensions.Y + cellDist - 1; + (int RowStart, int RowEnd)[] rowProcessIndexPairs = { (left, left), (right, right), (left, right), (left, right) }; + (int ColStart, int ColEnd)[] colProcessIndexPairs = { (top, bottom), (top, bottom), (top, top), (bottom, bottom) }; + + for (var i = 0; i < rowProcessIndexPairs.Length; i++) + for (var rowIndex = rowProcessIndexPairs[i].RowStart; rowIndex <= rowProcessIndexPairs[i].RowEnd; rowIndex++) + for (var colIndex = colProcessIndexPairs[i].ColStart; colIndex <= colProcessIndexPairs[i].ColEnd; colIndex++) + { + var cellchecking = cell + new CVec(rowIndex, colIndex); + if (!world.Map.Contains(cellchecking)) + continue; + + if (buildingInfluence.AnyBuildingAt(cellchecking)) + return false; + } + } + + return world.CanPlaceBuilding(cell, ai, bi, toIgnore); + } } } diff --git a/OpenRA.Mods.Common/Activities/Air/FallToEarth.cs b/OpenRA.Mods.Common/Activities/Air/FallToEarth.cs index 8e59779d4d82..48b3b80c4c51 100644 --- a/OpenRA.Mods.Common/Activities/Air/FallToEarth.cs +++ b/OpenRA.Mods.Common/Activities/Air/FallToEarth.cs @@ -26,6 +26,7 @@ public class FallToEarth : Activity public FallToEarth(Actor self, FallsToEarthInfo info) { + ActivityType = ActivityType.Move; this.info = info; IsInterruptible = false; aircraft = self.Trait(); diff --git a/OpenRA.Mods.Common/Activities/Air/Fly.cs b/OpenRA.Mods.Common/Activities/Air/Fly.cs index 482432ed4059..3138562c1d97 100644 --- a/OpenRA.Mods.Common/Activities/Air/Fly.cs +++ b/OpenRA.Mods.Common/Activities/Air/Fly.cs @@ -34,11 +34,13 @@ public class Fly : Activity public Fly(Actor self, in Target t, WDist nearEnough, WPos? initialTargetPosition = null, Color? targetLineColor = null) : this(self, t, initialTargetPosition, targetLineColor) { + ActivityType = ActivityType.Move; this.nearEnough = nearEnough; } public Fly(Actor self, in Target t, WPos? initialTargetPosition = null, Color? targetLineColor = null) { + ActivityType = ActivityType.Move; aircraft = self.Trait(); target = t; this.targetLineColor = targetLineColor; @@ -56,6 +58,7 @@ public Fly(Actor self, in Target t, WDist minRange, WDist maxRange, WPos? initialTargetPosition = null, Color? targetLineColor = null) : this(self, t, initialTargetPosition, targetLineColor) { + ActivityType = ActivityType.Move; this.maxRange = maxRange; this.minRange = minRange; } @@ -126,7 +129,7 @@ public override bool Tick(Actor self) // HACK: Prevent paused (for example, EMP'd) aircraft from taking off. // This is necessary until the TODOs in the IsCanceling block below are addressed. if (isLanded && aircraft.IsTraitPaused) - return false; + return true; if (IsCanceling) { diff --git a/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs b/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs index f2546fab6274..72ec04c829a9 100644 --- a/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs +++ b/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs @@ -39,6 +39,7 @@ public class FlyAttack : Activity, IActivityNotifyStanceChanged public FlyAttack(Actor self, AttackSource source, in Target target, bool forceAttack, Color? targetLineColor) { + ActivityType = ActivityType.Attack; this.source = source; this.target = target; this.forceAttack = forceAttack; @@ -173,7 +174,7 @@ public override bool Tick(Actor self) else if (attackAircraft.Info.AttackType == AirAttackType.Strafe) QueueChild(new StrafeAttackRun(attackAircraft, aircraft, target, strafeDistance != WDist.Zero ? strafeDistance : lastVisibleMaximumRange)); else if (attackAircraft.Info.AttackType == AirAttackType.Default && !aircraft.Info.CanHover) - QueueChild(new FlyAttackRun(target, lastVisibleMaximumRange, attackAircraft)); + QueueChild(new FlyAttackRun(target, lastVisibleMaximumRange, attackAircraft, rearmable)); // Turn to face the target if required. else if (!attackAircraft.TargetInFiringArc(self, target, attackAircraft.Info.FacingTolerance)) @@ -215,16 +216,19 @@ public override IEnumerable TargetLineNodes(Actor self) sealed class FlyAttackRun : Activity { readonly AttackAircraft attack; + readonly Rearmable rearmable; readonly WDist exitRange; Target target; bool targetIsVisibleActor; - public FlyAttackRun(in Target t, WDist exitRange, AttackAircraft attack) + public FlyAttackRun(in Target t, WDist exitRange, AttackAircraft attack, Rearmable rearmable) { + ActivityType = ActivityType.Attack; ChildHasPriority = false; target = t; + this.rearmable = rearmable; this.exitRange = exitRange; this.attack = attack; } @@ -249,6 +253,10 @@ public override bool Tick(Actor self) if (TickChild(self) || IsCanceling) return true; + // Return as soon as we run out of ammo. + if (rearmable != null && attack.Armaments.All(x => x.IsTraitPaused || !x.Weapon.IsValidAgainst(target, self.World, self))) + return true; + // Cancel the run if the target become invalid (e.g. killed) while visible var targetWasVisibleActor = targetIsVisibleActor; target = target.Recalculate(self.Owner, out var targetIsHiddenActor); @@ -271,6 +279,7 @@ sealed class StrafeAttackRun : Activity public StrafeAttackRun(AttackAircraft attackAircraft, Aircraft aircraft, in Target t, WDist exitRange) { + ActivityType = ActivityType.Attack; ChildHasPriority = false; target = t; diff --git a/OpenRA.Mods.Common/Activities/Air/FlyFollow.cs b/OpenRA.Mods.Common/Activities/Air/FlyFollow.cs index 96f40c3a387d..064a42fae872 100644 --- a/OpenRA.Mods.Common/Activities/Air/FlyFollow.cs +++ b/OpenRA.Mods.Common/Activities/Air/FlyFollow.cs @@ -31,6 +31,7 @@ public class FlyFollow : Activity public FlyFollow(Actor self, in Target target, WDist minRange, WDist maxRange, WPos? initialTargetPosition, Color? targetLineColor = null) { + ActivityType = ActivityType.Move; this.target = target; aircraft = self.Trait(); this.minRange = minRange; diff --git a/OpenRA.Mods.Common/Activities/Air/FlyForward.cs b/OpenRA.Mods.Common/Activities/Air/FlyForward.cs index b5076870b12c..973154357cda 100644 --- a/OpenRA.Mods.Common/Activities/Air/FlyForward.cs +++ b/OpenRA.Mods.Common/Activities/Air/FlyForward.cs @@ -24,6 +24,7 @@ public class FlyForward : Activity FlyForward(Actor self) { + ActivityType = ActivityType.Move; aircraft = self.Trait(); cruiseAltitude = aircraft.Info.CruiseAltitude; } @@ -31,12 +32,14 @@ public class FlyForward : Activity public FlyForward(Actor self, int ticks = -1) : this(self) { + ActivityType = ActivityType.Move; flyTicks = ticks; } public FlyForward(Actor self, WDist distance) : this(self) { + ActivityType = ActivityType.Move; remainingDistance = distance.Length; } diff --git a/OpenRA.Mods.Common/Activities/Air/FlyIdle.cs b/OpenRA.Mods.Common/Activities/Air/FlyIdle.cs index 5bd10f503f32..2411d455caad 100644 --- a/OpenRA.Mods.Common/Activities/Air/FlyIdle.cs +++ b/OpenRA.Mods.Common/Activities/Air/FlyIdle.cs @@ -26,6 +26,7 @@ public class FlyIdle : Activity public FlyIdle(Actor self, int ticks = -1, bool idleTurn = true) { + ActivityType = ActivityType.Move; aircraft = self.Trait(); isIdleTurner = aircraft.Info.IdleSpeed > 0 || (!aircraft.Info.CanHover && aircraft.Info.IdleSpeed < 0); remainingTicks = ticks; diff --git a/OpenRA.Mods.Common/Activities/Air/FlyOffMap.cs b/OpenRA.Mods.Common/Activities/Air/FlyOffMap.cs index 012d33e41cfc..a4df9fdf191e 100644 --- a/OpenRA.Mods.Common/Activities/Air/FlyOffMap.cs +++ b/OpenRA.Mods.Common/Activities/Air/FlyOffMap.cs @@ -24,6 +24,7 @@ public class FlyOffMap : Activity public FlyOffMap(Actor self, int endingDelay = 25) { + ActivityType = ActivityType.Move; aircraft = self.Trait(); ChildHasPriority = false; this.endingDelay = endingDelay; @@ -32,6 +33,7 @@ public FlyOffMap(Actor self, int endingDelay = 25) public FlyOffMap(Actor self, in Target target, int endingDelay = 25) : this(self, endingDelay) { + ActivityType = ActivityType.Move; this.target = target; hasTarget = true; } diff --git a/OpenRA.Mods.Common/Activities/Air/Land.cs b/OpenRA.Mods.Common/Activities/Air/Land.cs index a4fc45a79ce5..bc0ef30fb1bf 100644 --- a/OpenRA.Mods.Common/Activities/Air/Land.cs +++ b/OpenRA.Mods.Common/Activities/Air/Land.cs @@ -38,20 +38,22 @@ public class Land : Activity public Land(Actor self, WAngle? facing = null, Color? targetLineColor = null) : this(self, Target.Invalid, new WDist(-1), WVec.Zero, facing, targetLineColor: targetLineColor) { + ActivityType = ActivityType.Move; assignTargetOnFirstRun = true; } public Land(Actor self, in Target target, WAngle? facing = null, Color? targetLineColor = null) - : this(self, target, new WDist(-1), WVec.Zero, facing, targetLineColor: targetLineColor) { } + : this(self, target, new WDist(-1), WVec.Zero, facing, targetLineColor: targetLineColor) { ActivityType = ActivityType.Move; } public Land(Actor self, in Target target, WDist landRange, WAngle? facing = null, Color? targetLineColor = null) - : this(self, target, landRange, WVec.Zero, facing, targetLineColor: targetLineColor) { } + : this(self, target, landRange, WVec.Zero, facing, targetLineColor: targetLineColor) { ActivityType = ActivityType.Move; } public Land(Actor self, in Target target, in WVec offset, WAngle? facing = null, Color? targetLineColor = null) - : this(self, target, WDist.Zero, offset, facing, targetLineColor: targetLineColor) { } + : this(self, target, WDist.Zero, offset, facing, targetLineColor: targetLineColor) { ActivityType = ActivityType.Move; } public Land(Actor self, in Target target, WDist landRange, in WVec offset, WAngle? facing = null, CPos[] clearCells = null, Color? targetLineColor = null) { + ActivityType = ActivityType.Move; aircraft = self.Trait(); this.target = target; this.offset = offset; @@ -225,7 +227,11 @@ public override bool Tick(Actor self) } if (aircraft.Info.LandingSounds.Length > 0) - Game.Sound.Play(SoundType.World, aircraft.Info.LandingSounds, self.World, aircraft.CenterPosition); + { + var centerPos = aircraft.CenterPosition; + if (aircraft.Info.AudibleThroughFog || (!self.World.ShroudObscures(centerPos) && !self.World.FogObscures(centerPos))) + Game.Sound.Play(SoundType.World, aircraft.Info.LandingSounds, self.World, centerPos, null, aircraft.Info.SoundVolume); + } foreach (var notify in self.TraitsImplementing()) notify.Landing(self); diff --git a/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs b/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs index 924ae1ee5658..cd24863f244d 100644 --- a/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs +++ b/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs @@ -29,6 +29,7 @@ public class ReturnToBase : Activity public ReturnToBase(Actor self, Actor dest = null, bool alwaysLand = false) { + ActivityType = ActivityType.Move; this.dest = dest; this.alwaysLand = alwaysLand; aircraft = self.Trait(); @@ -110,7 +111,7 @@ public override bool Tick(Actor self) if (ShouldLandAtBuilding(self, dest)) { - var exit = dest.NearestExitOrDefault(self.CenterPosition); + var exit = dest.FirstExitOrDefault(); var offset = WVec.Zero; if (exit != null) { diff --git a/OpenRA.Mods.Common/Activities/Air/TakeOff.cs b/OpenRA.Mods.Common/Activities/Air/TakeOff.cs index 8da4ebb30fd0..051512af7933 100644 --- a/OpenRA.Mods.Common/Activities/Air/TakeOff.cs +++ b/OpenRA.Mods.Common/Activities/Air/TakeOff.cs @@ -20,6 +20,7 @@ public class TakeOff : Activity public TakeOff(Actor self) { + ActivityType = ActivityType.Move; aircraft = self.Trait(); } @@ -38,7 +39,11 @@ protected override void OnFirstRun(Actor self) return; if (aircraft.Info.TakeoffSounds.Length > 0) - Game.Sound.Play(SoundType.World, aircraft.Info.TakeoffSounds, self.World, aircraft.CenterPosition); + { + var pos = aircraft.CenterPosition; + if (aircraft.Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, aircraft.Info.TakeoffSounds, self.World, pos, null, aircraft.Info.SoundVolume); + } foreach (var notify in self.TraitsImplementing()) notify.TakeOff(self); diff --git a/OpenRA.Mods.Common/Activities/Attack.cs b/OpenRA.Mods.Common/Activities/Attack.cs index ea062da4a056..c1a630b0602d 100644 --- a/OpenRA.Mods.Common/Activities/Attack.cs +++ b/OpenRA.Mods.Common/Activities/Attack.cs @@ -48,6 +48,7 @@ protected enum AttackStatus { UnableToAttack, NeedsToTurn, NeedsToMove, Attackin public Attack(Actor self, in Target target, bool allowMovement, bool forceAttack, Color? targetLineColor = null) { + ActivityType = ActivityType.Attack; this.target = target; this.targetLineColor = targetLineColor; this.forceAttack = forceAttack; @@ -226,7 +227,7 @@ protected virtual AttackStatus TickAttack(Actor self, AttackFrontal attack) if (!attack.TargetInFiringArc(self, target, attack.Info.FacingTolerance)) { // Mirror Turn activity checks. - if (mobile == null || (!mobile.IsTraitDisabled && !mobile.IsTraitPaused)) + if (mobile == null || (!mobile.IsTraitDisabled && !mobile.IsTraitPaused) || mobile.Info.CanTurnWhileDisabled) { // Don't queue a Turn activity: Executing a child takes an additional tick during which the target may have moved again. facing.Facing = Util.TickFacing(facing.Facing, (attack.GetTargetPosition(pos, target) - pos).Yaw, facing.TurnSpeed); diff --git a/OpenRA.Mods.Common/Activities/CaptureActor.cs b/OpenRA.Mods.Common/Activities/CaptureActor.cs index 7c03ee9eb6b0..ff76739e1835 100644 --- a/OpenRA.Mods.Common/Activities/CaptureActor.cs +++ b/OpenRA.Mods.Common/Activities/CaptureActor.cs @@ -124,6 +124,9 @@ void DoCapture(Actor self, Captures captures) foreach (var t in enterActor.TraitsImplementing()) t.OnCapture(enterActor, self, oldOwner, self.Owner, captures.Info.CaptureTypes); + if (captures.Info.CaptureCompleteVoice != null && self.Owner == self.World.LocalPlayer && self.IsInWorld) + self.PlayVoice(captures.Info.CaptureCompleteVoice); + if (self.Owner.RelationshipWith(oldOwner).HasRelationship(captures.Info.PlayerExperienceRelationships)) self.Owner.PlayerActor.TraitOrDefault()?.GiveExperience(captures.Info.PlayerExperience); diff --git a/OpenRA.Mods.Common/Activities/DeliverUnit.cs b/OpenRA.Mods.Common/Activities/DeliverUnit.cs index 352b3bfb0e09..fd0f341d83ed 100644 --- a/OpenRA.Mods.Common/Activities/DeliverUnit.cs +++ b/OpenRA.Mods.Common/Activities/DeliverUnit.cs @@ -29,11 +29,13 @@ public class DeliverUnit : Activity public DeliverUnit(Actor self, WDist deliverRange, Color? targetLineColor) : this(self, Target.Invalid, deliverRange, targetLineColor) { + ActivityType = ActivityType.Move; assignTargetOnFirstRun = true; } public DeliverUnit(Actor self, in Target destination, WDist deliverRange, Color? targetLineColor) { + ActivityType = ActivityType.Move; this.destination = destination; this.deliverRange = deliverRange; this.targetLineColor = targetLineColor; @@ -72,6 +74,7 @@ sealed class ReleaseUnit : Activity public ReleaseUnit(Actor self) { + ActivityType = ActivityType.Move; facing = self.Trait(); carryall = self.Trait(); body = self.Trait(); diff --git a/OpenRA.Mods.Common/Activities/Demolish.cs b/OpenRA.Mods.Common/Activities/Demolish.cs index 5b55a61ac98c..fe4f7691bd41 100644 --- a/OpenRA.Mods.Common/Activities/Demolish.cs +++ b/OpenRA.Mods.Common/Activities/Demolish.cs @@ -17,7 +17,7 @@ namespace OpenRA.Mods.Common.Activities { - sealed class Demolish : Enter + public sealed class Demolish : Enter { readonly int delay; readonly int flashes; diff --git a/OpenRA.Mods.Common/Activities/DeployForGrantedCondition.cs b/OpenRA.Mods.Common/Activities/DeployForGrantedCondition.cs index 31c777ee6541..73121048a12b 100644 --- a/OpenRA.Mods.Common/Activities/DeployForGrantedCondition.cs +++ b/OpenRA.Mods.Common/Activities/DeployForGrantedCondition.cs @@ -24,6 +24,7 @@ public class DeployForGrantedCondition : Activity public DeployForGrantedCondition(Actor self, GrantConditionOnDeploy deploy, bool moving = false) { + ActivityType = ActivityType.Move; this.deploy = deploy; this.moving = moving; canTurn = self.Info.HasTraitInfo(); @@ -50,8 +51,6 @@ public override IEnumerable TargetLineNodes(Actor self) if (NextActivity != null) foreach (var n in NextActivity.TargetLineNodes(self)) yield return n; - - yield break; } } @@ -62,6 +61,7 @@ public class DeployInner : Activity public DeployInner(GrantConditionOnDeploy deployment) { + ActivityType = ActivityType.Move; this.deployment = deployment; // Once deployment animation starts, the animation must finish. diff --git a/OpenRA.Mods.Common/Activities/Enter.cs b/OpenRA.Mods.Common/Activities/Enter.cs index 4ee523795576..b43783e96651 100644 --- a/OpenRA.Mods.Common/Activities/Enter.cs +++ b/OpenRA.Mods.Common/Activities/Enter.cs @@ -33,6 +33,7 @@ enum EnterState { Approaching, Entering, Exiting, Finished } protected Enter(Actor self, in Target target, Color? targetLineColor = null) { + ActivityType = ActivityType.Move; move = self.Trait(); this.target = target; this.targetLineColor = targetLineColor; diff --git a/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs b/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs index 7a61cfbaea8b..fabea08282ae 100644 --- a/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs +++ b/OpenRA.Mods.Common/Activities/FindAndDeliverResources.cs @@ -34,6 +34,7 @@ public class FindAndDeliverResources : Activity public FindAndDeliverResources(Actor self, CPos? orderLocation = null) { + ActivityType = ActivityType.Move; harv = self.Trait(); harvInfo = self.Info.TraitInfo(); mobile = self.Trait(); diff --git a/OpenRA.Mods.Common/Activities/GenericDockSequence.cs b/OpenRA.Mods.Common/Activities/GenericDockSequence.cs index 7742a0b83eb6..f66d9a2519e1 100644 --- a/OpenRA.Mods.Common/Activities/GenericDockSequence.cs +++ b/OpenRA.Mods.Common/Activities/GenericDockSequence.cs @@ -43,6 +43,7 @@ protected enum DockingState { Wait, Drag, Dock, Loop, Undock, Complete } public GenericDockSequence(Actor self, DockClientManager client, Actor hostActor, IDockHost host) { + ActivityType = ActivityType.Move; dockingState = DockingState.Drag; DockClient = client; diff --git a/OpenRA.Mods.Common/Activities/HarvestResource.cs b/OpenRA.Mods.Common/Activities/HarvestResource.cs index 85537bdb6158..a3d97d6bbb11 100644 --- a/OpenRA.Mods.Common/Activities/HarvestResource.cs +++ b/OpenRA.Mods.Common/Activities/HarvestResource.cs @@ -31,6 +31,7 @@ public class HarvestResource : Activity public HarvestResource(Actor self, CPos targetCell) { + ActivityType = ActivityType.Move; harv = self.Trait(); harvInfo = self.Info.TraitInfo(); facing = self.Trait(); diff --git a/OpenRA.Mods.Common/Activities/Hunt.cs b/OpenRA.Mods.Common/Activities/Hunt.cs index 219e2fc3b296..76d0485da4f3 100644 --- a/OpenRA.Mods.Common/Activities/Hunt.cs +++ b/OpenRA.Mods.Common/Activities/Hunt.cs @@ -24,11 +24,12 @@ public class Hunt : Activity public Hunt(Actor self) { + ActivityType = ActivityType.Move; move = self.Trait(); - var attack = self.Trait(); + var attacks = self.TraitsImplementing(); targets = self.World.ActorsHavingTrait().Where( a => self != a && !a.IsDead && a.IsInWorld && a.AppearsHostileTo(self) - && a.IsTargetableBy(self) && attack.HasAnyValidWeapons(Target.FromActor(a))); + && a.IsTargetableBy(self) && attacks.Any(attack => attack.HasAnyValidWeapons(Target.FromActor(a)))); } public override bool Tick(Actor self) diff --git a/OpenRA.Mods.Common/Activities/InstantRepair.cs b/OpenRA.Mods.Common/Activities/InstantRepair.cs index c415ed143462..c0b13b60f3f1 100644 --- a/OpenRA.Mods.Common/Activities/InstantRepair.cs +++ b/OpenRA.Mods.Common/Activities/InstantRepair.cs @@ -69,7 +69,11 @@ protected override void OnEnterComplete(Actor self, Actor targetActor) enterActor.InflictDamage(self, new Damage(-enterHealth.MaxHP)); if (!string.IsNullOrEmpty(info.RepairSound)) - Game.Sound.Play(SoundType.World, info.RepairSound, enterActor.CenterPosition); + { + var pos = enterActor.CenterPosition; + if (info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, info.RepairSound, pos, info.SoundVolume); + } if (info.EnterBehaviour == EnterBehaviour.Dispose) self.Dispose(); diff --git a/OpenRA.Mods.Common/Activities/LayMines.cs b/OpenRA.Mods.Common/Activities/LayMines.cs index fb5740af16e3..4a36b8618c37 100644 --- a/OpenRA.Mods.Common/Activities/LayMines.cs +++ b/OpenRA.Mods.Common/Activities/LayMines.cs @@ -34,6 +34,7 @@ public class LayMines : Activity public LayMines(Actor self, List minefield = null) { + ActivityType = ActivityType.Ability; minelayer = self.Trait(); ammoPools = self.TraitsImplementing().ToArray(); movement = self.Trait(); @@ -212,6 +213,7 @@ bool LayMine(Actor self) { new LocationInit(self.Location), new OwnerInit(self.Owner), + new ParentActorInit(self) }); foreach (var t in self.TraitsImplementing()) diff --git a/OpenRA.Mods.Common/Activities/Move/AttackMoveActivity.cs b/OpenRA.Mods.Common/Activities/Move/AttackMoveActivity.cs index f9cf4bfed476..f86c82b589a9 100644 --- a/OpenRA.Mods.Common/Activities/Move/AttackMoveActivity.cs +++ b/OpenRA.Mods.Common/Activities/Move/AttackMoveActivity.cs @@ -30,6 +30,7 @@ public class AttackMoveActivity : Activity public AttackMoveActivity(Actor self, Func getMove, bool assaultMoving = false) { + ActivityType = ActivityType.Move; this.getMove = getMove; autoTarget = self.TraitOrDefault(); attackMove = self.TraitOrDefault(); @@ -103,8 +104,6 @@ public override IEnumerable TargetLineNodes(Actor self) { foreach (var n in getMove().TargetLineNodes(self)) yield return n; - - yield break; } } } diff --git a/OpenRA.Mods.Common/Activities/Move/Drag.cs b/OpenRA.Mods.Common/Activities/Move/Drag.cs index 62de46b20037..ee9e3d8132df 100644 --- a/OpenRA.Mods.Common/Activities/Move/Drag.cs +++ b/OpenRA.Mods.Common/Activities/Move/Drag.cs @@ -29,6 +29,7 @@ public class Drag : Activity public Drag(Actor self, WPos start, WPos end, int length, WAngle? facing = null) { + ActivityType = ActivityType.Move; positionable = self.Trait(); disableable = self.TraitOrDefault() as IDisabledTrait; this.start = start; diff --git a/OpenRA.Mods.Common/Activities/Move/Follow.cs b/OpenRA.Mods.Common/Activities/Move/Follow.cs index e70c5809412b..eb193ff99e3c 100644 --- a/OpenRA.Mods.Common/Activities/Move/Follow.cs +++ b/OpenRA.Mods.Common/Activities/Move/Follow.cs @@ -31,6 +31,7 @@ public class Follow : Activity public Follow(Actor self, in Target target, WDist minRange, WDist maxRange, WPos? initialTargetPosition, Color? targetLineColor = null) { + ActivityType = ActivityType.Move; this.target = target; this.minRange = minRange; this.maxRange = maxRange; diff --git a/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs b/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs index 56c5a906c039..aef0615567bb 100644 --- a/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs +++ b/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs @@ -27,6 +27,7 @@ public class LocalMoveIntoTarget : Activity public LocalMoveIntoTarget(Actor self, in Target target, WDist targetMovementThreshold, Color? targetLineColor = null) { + ActivityType = ActivityType.Move; mobile = self.Trait(); this.target = target; this.targetMovementThreshold = targetMovementThreshold; diff --git a/OpenRA.Mods.Common/Activities/Move/Move.cs b/OpenRA.Mods.Common/Activities/Move/Move.cs index 6d63f05af847..47e636d98b24 100644 --- a/OpenRA.Mods.Common/Activities/Move/Move.cs +++ b/OpenRA.Mods.Common/Activities/Move/Move.cs @@ -54,6 +54,8 @@ public class Move : Activity // Ignores lane bias public Move(Actor self, CPos destination, Color? targetLineColor = null) { + ActivityType = ActivityType.Move; + // PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait. mobile = (Mobile)self.OccupiesSpace; @@ -71,6 +73,8 @@ public Move(Actor self, CPos destination, Color? targetLineColor = null) public Move(Actor self, CPos destination, WDist nearEnough, Actor ignoreActor = null, bool evaluateNearestMovableCell = false, Color? targetLineColor = null) { + ActivityType = ActivityType.Move; + // PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait. mobile = (Mobile)self.OccupiesSpace; @@ -94,6 +98,8 @@ public Move(Actor self, CPos destination, WDist nearEnough, Actor ignoreActor = public Move(Actor self, Func> getPath, Color? targetLineColor = null) { + ActivityType = ActivityType.Move; + // PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait. mobile = (Mobile)self.OccupiesSpace; @@ -379,6 +385,7 @@ protected MovePart(Move move, WPos from, WPos to, WAngle fromFacing, WAngle toFa WRot? fromTerrainOrientation, WRot? toTerrainOrientation, int terrainOrientationMargin, int carryoverProgress, bool shouldArc, bool movingOnGroundLayer) { + ActivityType = ActivityType.Move; Move = move; From = from; To = to; diff --git a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs index 24b3d464875a..cb61f4aa026f 100644 --- a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs +++ b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs @@ -31,6 +31,7 @@ public class MoveAdjacentTo : Activity public MoveAdjacentTo(Actor self, in Target target, WPos? initialTargetPosition = null, Color? targetLineColor = null) { + ActivityType = ActivityType.Move; this.target = target; this.targetLineColor = targetLineColor; Mobile = self.Trait(); diff --git a/OpenRA.Mods.Common/Activities/Move/Nudge.cs b/OpenRA.Mods.Common/Activities/Move/Nudge.cs index 0fb90b01afe8..e4c22fc27bb3 100644 --- a/OpenRA.Mods.Common/Activities/Move/Nudge.cs +++ b/OpenRA.Mods.Common/Activities/Move/Nudge.cs @@ -59,8 +59,6 @@ public override IEnumerable TargetLineNodes(Actor self) if (ChildActivity != null) foreach (var n in ChildActivity.TargetLineNodes(self)) yield return n; - - yield break; } } } diff --git a/OpenRA.Mods.Common/Activities/MoveToDock.cs b/OpenRA.Mods.Common/Activities/MoveToDock.cs index 46621adc80b0..c0a7b939d10c 100644 --- a/OpenRA.Mods.Common/Activities/MoveToDock.cs +++ b/OpenRA.Mods.Common/Activities/MoveToDock.cs @@ -26,6 +26,7 @@ public class MoveToDock : Activity public MoveToDock(Actor self, Actor dockHostActor = null, IDockHost dockHost = null) { + ActivityType = ActivityType.Move; dockClient = self.Trait(); this.dockHostActor = dockHostActor; this.dockHost = dockHost; diff --git a/OpenRA.Mods.Common/Activities/Parachute.cs b/OpenRA.Mods.Common/Activities/Parachute.cs index 8ed048f8d646..9f5e151a318b 100644 --- a/OpenRA.Mods.Common/Activities/Parachute.cs +++ b/OpenRA.Mods.Common/Activities/Parachute.cs @@ -23,6 +23,7 @@ public class Parachute : Activity public Parachute(Actor self) { + ActivityType = ActivityType.Move; pos = self.OccupiesSpace as IPositionable; fallVector = new WVec(0, 0, self.Info.TraitInfo().FallRate); IsInterruptible = false; diff --git a/OpenRA.Mods.Common/Activities/PickupUnit.cs b/OpenRA.Mods.Common/Activities/PickupUnit.cs index 1e3f4da1e826..be4caaac5711 100644 --- a/OpenRA.Mods.Common/Activities/PickupUnit.cs +++ b/OpenRA.Mods.Common/Activities/PickupUnit.cs @@ -36,6 +36,7 @@ enum PickupState { Intercept, LockCarryable, Pickup } public PickupUnit(Actor self, Actor cargo, int delay, Color? targetLineColor) { + ActivityType = ActivityType.Move; this.cargo = cargo; this.delay = delay; this.targetLineColor = targetLineColor; @@ -152,6 +153,7 @@ sealed class AttachUnit : Activity public AttachUnit(Actor self, Actor cargo) { + ActivityType = ActivityType.Move; this.cargo = cargo; carryable = cargo.Trait(); carryall = self.Trait(); diff --git a/OpenRA.Mods.Common/Activities/Resupply.cs b/OpenRA.Mods.Common/Activities/Resupply.cs index f8316f08ed75..ccd4f50e8b5e 100644 --- a/OpenRA.Mods.Common/Activities/Resupply.cs +++ b/OpenRA.Mods.Common/Activities/Resupply.cs @@ -46,6 +46,7 @@ public class Resupply : Activity public Resupply(Actor self, Actor host, WDist closeEnough, bool stayOnResupplier = false) { + ActivityType = ActivityType.Move; this.host = Target.FromActor(host); this.closeEnough = closeEnough; this.stayOnResupplier = stayOnResupplier; @@ -74,6 +75,8 @@ public Resupply(Actor self, Actor host, WDist closeEnough, bool stayOnResupplier if (!cannotRepairAtHost) { activeResupplyTypes |= ResupplyType.Repair; + if (repairableNear != null && repairableNear.Info.RepairActors.Contains(host.Info.Name)) + activeResupplyTypes |= ResupplyType.RepairNear; // HACK: Reservable logic can't handle repairs, so force a take-off if resupply included repairs. // TODO: Make reservation logic or future docking logic properly handle this. @@ -104,7 +107,7 @@ public override bool Tick(Actor self) // Otherwise check against host CenterPosition. if (closeEnough < WDist.Zero) isCloseEnough = true; - else if (repairableNear != null) + else if (activeResupplyTypes.HasFlag(ResupplyType.RepairNear)) isCloseEnough = host.IsInRange(self.CenterPosition, closeEnough); else isCloseEnough = (host.CenterPosition - self.CenterPosition).HorizontalLengthSquared <= closeEnough.LengthSquared; @@ -129,13 +132,13 @@ public override bool Tick(Actor self) return true; } - else if (activeResupplyTypes != 0 && aircraft == null && !isCloseEnough) + else if (activeResupplyTypes != 0 && (aircraft == null || activeResupplyTypes.HasFlag(ResupplyType.RepairNear)) && !isCloseEnough) { var targetCell = self.World.Map.CellContaining(host.Actor.CenterPosition); // HACK: Repairable needs the actor to move to host center. // TODO: Get rid of this or at least replace it with something less hacky. - if (repairableNear == null) + if (!activeResupplyTypes.HasFlag(ResupplyType.RepairNear)) QueueChild(move.MoveOntoTarget(self, host, WVec.Zero, null, moveInfo.GetTargetLineColor())); else QueueChild(move.MoveWithinRange(host, closeEnough, targetLineColor: moveInfo.GetTargetLineColor())); @@ -260,7 +263,10 @@ void RepairTick(Actor self) if (repairsUnits == null) { if (!allRepairsUnits.Any(r => r.IsTraitPaused)) + { activeResupplyTypes &= ~ResupplyType.Repair; + activeResupplyTypes &= ~ResupplyType.RepairNear; + } return; } @@ -274,6 +280,7 @@ void RepairTick(Actor self) TextNotificationsManager.AddTransientLine(self.Owner, repairsUnits.Info.FinishRepairingTextNotification); activeResupplyTypes &= ~ResupplyType.Repair; + activeResupplyTypes &= ~ResupplyType.RepairNear; return; } diff --git a/OpenRA.Mods.Common/Activities/Sell.cs b/OpenRA.Mods.Common/Activities/Sell.cs index 825aabebc0d3..6f2cb7556fb4 100644 --- a/OpenRA.Mods.Common/Activities/Sell.cs +++ b/OpenRA.Mods.Common/Activities/Sell.cs @@ -25,6 +25,7 @@ sealed class Sell : Activity public Sell(Actor self, bool showTicks) { + ActivityType = ActivityType.Ability; this.showTicks = showTicks; health = self.TraitOrDefault(); sellableInfo = self.Info.TraitInfo(); diff --git a/OpenRA.Mods.Common/Activities/SimpleTeleport.cs b/OpenRA.Mods.Common/Activities/SimpleTeleport.cs index 5ca2c0016d8d..6abc4dc0f4fd 100644 --- a/OpenRA.Mods.Common/Activities/SimpleTeleport.cs +++ b/OpenRA.Mods.Common/Activities/SimpleTeleport.cs @@ -18,7 +18,7 @@ public class SimpleTeleport : Activity { readonly CPos destination; - public SimpleTeleport(CPos destination) { this.destination = destination; } + public SimpleTeleport(CPos destination) { ActivityType = ActivityType.Move; this.destination = destination; } public override bool Tick(Actor self) { diff --git a/OpenRA.Mods.Common/Activities/Transform.cs b/OpenRA.Mods.Common/Activities/Transform.cs index dd78143d3a93..c378288cd68e 100644 --- a/OpenRA.Mods.Common/Activities/Transform.cs +++ b/OpenRA.Mods.Common/Activities/Transform.cs @@ -23,23 +23,26 @@ public class Transform : Activity { public readonly string ToActor; public CVec Offset = CVec.Zero; - public WAngle Facing = new(384); + public WAngle? Facing = null; public string[] Sounds = Array.Empty(); public string Notification = null; public string TextNotification = null; + public bool AudibleThroughFog = false; + public float SoundVolume = 1f; public int ForceHealthPercentage = 0; public bool SkipMakeAnims = false; public string Faction = null; public Transform(string toActor) { + ActivityType = ActivityType.Ability; ToActor = toActor; } protected override void OnFirstRun(Actor self) { - if (self.Info.HasTraitInfo()) - QueueChild(new Turn(self, Facing)); + if (self.Info.HasTraitInfo() && Facing != null) + QueueChild(new Turn(self, Facing.Value)); if (self.Info.HasTraitInfo()) QueueChild(new Land(self)); @@ -58,16 +61,19 @@ public override bool Tick(Actor self) foreach (var nt in self.TraitsImplementing()) nt.BeforeTransform(self); - var makeAnimation = self.TraitOrDefault(); - if (!SkipMakeAnims && makeAnimation != null) + if (!SkipMakeAnims) { - // Once the make animation starts the activity must not be stopped anymore. - IsInterruptible = false; + var makeAnimation = self.TraitOrDefault(); + if (makeAnimation != null) + { + // Once the make animation starts the activity must not be stopped anymore. + IsInterruptible = false; - // Wait forever - QueueChild(new WaitFor(() => false)); - makeAnimation.Reverse(self, () => DoTransform(self, transforms, makeAnimation)); - return false; + // Wait forever + QueueChild(new WaitFor(() => false)); + makeAnimation.Reverse(self, () => DoTransform(self, transforms, makeAnimation)); + return false; + } } DoTransform(self, transforms, null); @@ -106,8 +112,10 @@ void DoTransform(Actor self, Transforms transforms, WithMakeAnimation makeAnimat var controlgroup = w.ControlGroups.GetControlGroupForActor(self); self.Dispose(); - foreach (var s in Sounds) - Game.Sound.PlayToPlayer(SoundType.World, self.Owner, s, self.CenterPosition); + var pos = self.CenterPosition; + if (AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + foreach (var s in Sounds) + Game.Sound.Play(SoundType.World, s, pos, SoundVolume); Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", Notification, self.Owner.Faction.InternalName); TextNotificationsManager.AddTransientLine(self.Owner, TextNotification); @@ -116,9 +124,11 @@ void DoTransform(Actor self, Transforms transforms, WithMakeAnimation makeAnimat { new LocationInit(self.Location + Offset), new OwnerInit(self.Owner), - new FacingInit(Facing), }; + if (Facing != null || self.TraitOrDefault() != null) + init.Add(new FacingInit(Facing != null ? Facing.Value : self.Trait().Facing)); + if (SkipMakeAnims) init.Add(new SkipMakeAnimsInit()); @@ -170,6 +180,7 @@ sealed class IssueOrderAfterTransform : Activity public IssueOrderAfterTransform(string orderString, in Target target, Color? targetLineColor = null) { + ActivityType = ActivityType.Ability; this.orderString = orderString; this.target = target; this.targetLineColor = targetLineColor; diff --git a/OpenRA.Mods.Common/Activities/Turn.cs b/OpenRA.Mods.Common/Activities/Turn.cs index 9f5069ed6c59..fb9a3f31a8e3 100644 --- a/OpenRA.Mods.Common/Activities/Turn.cs +++ b/OpenRA.Mods.Common/Activities/Turn.cs @@ -23,6 +23,7 @@ public class Turn : Activity public Turn(Actor self, WAngle desiredFacing) { + ActivityType = ActivityType.Move; mobile = self.TraitOrDefault(); facing = self.Trait(); this.desiredFacing = desiredFacing; @@ -33,13 +34,14 @@ public override bool Tick(Actor self) if (IsCanceling) return true; - if (mobile != null && (mobile.IsTraitDisabled || mobile.IsTraitPaused)) + if (mobile != null && (mobile.IsTraitDisabled || mobile.IsTraitPaused) && !mobile.Info.CanTurnWhileDisabled) return false; if (desiredFacing == facing.Facing) return true; - facing.Facing = Util.TickFacing(facing.Facing, desiredFacing, facing.TurnSpeed); + var turnSpeed = mobile != null ? new WAngle(Util.ApplyPercentageModifiers(facing.TurnSpeed.Angle, mobile.TurnSpeedModifiers)) : facing.TurnSpeed; + facing.Facing = Util.TickFacing(facing.Facing, desiredFacing, turnSpeed); return false; } diff --git a/OpenRA.Mods.Common/Activities/UnloadCargo.cs b/OpenRA.Mods.Common/Activities/UnloadCargo.cs index 14308d2ae66a..59216239503b 100644 --- a/OpenRA.Mods.Common/Activities/UnloadCargo.cs +++ b/OpenRA.Mods.Common/Activities/UnloadCargo.cs @@ -34,11 +34,13 @@ public class UnloadCargo : Activity public UnloadCargo(Actor self, WDist unloadRange, bool unloadAll = true) : this(self, Target.Invalid, unloadRange, unloadAll) { + ActivityType = ActivityType.Move; assignTargetOnFirstRun = true; } public UnloadCargo(Actor self, in Target destination, WDist unloadRange, bool unloadAll = true) { + ActivityType = ActivityType.Move; this.self = self; cargo = self.Trait(); notifiers = self.TraitsImplementing().ToArray(); @@ -49,17 +51,6 @@ public UnloadCargo(Actor self, in Target destination, WDist unloadRange, bool un this.unloadRange = unloadRange; } - public (CPos Cell, SubCell SubCell)? ChooseExitSubCell(Actor passenger) - { - var pos = passenger.Trait(); - - return cargo.CurrentAdjacentCells - .Shuffle(self.World.SharedRandom) - .Select(c => (c, pos.GetAvailableSubCell(c))) - .Cast<(CPos, SubCell SubCell)?>() - .FirstOrDefault(s => s.Value.SubCell != SubCell.Invalid); - } - IEnumerable BlockedExitCells(Actor passenger) { var pos = passenger.Trait(); @@ -103,7 +94,7 @@ public override bool Tick(Actor self) var actor = cargo.Peek(); var spawn = self.CenterPosition; - var exitSubCell = ChooseExitSubCell(actor); + var exitSubCell = cargo.ChooseExitSubCell(actor); if (exitSubCell == null) { self.NotifyBlocker(BlockedExitCells(actor)); @@ -119,11 +110,12 @@ public override bool Tick(Actor self) var move = actor.Trait(); var pos = actor.Trait(); + var passenger = actor.Trait(); pos.SetPosition(actor, exitSubCell.Value.Cell, exitSubCell.Value.SubCell); pos.SetCenterPosition(actor, spawn); - actor.CancelActivity(); + passenger.OnBeforeAddedToWorld(actor); w.Add(actor); }); } diff --git a/OpenRA.Mods.Common/Activities/Wait.cs b/OpenRA.Mods.Common/Activities/Wait.cs index 4d8ac55c600c..f8704b93707a 100644 --- a/OpenRA.Mods.Common/Activities/Wait.cs +++ b/OpenRA.Mods.Common/Activities/Wait.cs @@ -18,9 +18,10 @@ public class Wait : Activity { int remainingTicks; - public Wait(int period) { remainingTicks = period; } + public Wait(int period) { ActivityType = ActivityType.Undefined; remainingTicks = period; } public Wait(int period, bool interruptible) { + ActivityType = ActivityType.Undefined; remainingTicks = period; IsInterruptible = interruptible; } @@ -38,9 +39,10 @@ public class WaitFor : Activity { readonly Func f; - public WaitFor(Func f) { this.f = f; } + public WaitFor(Func f) { this.f = f; ActivityType = ActivityType.Undefined; } public WaitFor(Func f, bool interruptible) { + ActivityType = ActivityType.Undefined; this.f = f; IsInterruptible = interruptible; } diff --git a/OpenRA.Mods.Common/Commands/DevCommands.cs b/OpenRA.Mods.Common/Commands/DevCommands.cs index 43144f8a1f75..dc86d13a997b 100644 --- a/OpenRA.Mods.Common/Commands/DevCommands.cs +++ b/OpenRA.Mods.Common/Commands/DevCommands.cs @@ -33,6 +33,9 @@ public class DevCommands : IChatCommand, IWorldLoaded [TranslationReference] const string ToggleVisiblityDescription = "description-toggle-visibility"; + [TranslationReference] + const string ToggleVisiblityAllDescription = "description-toggle-visibility-all"; + [TranslationReference] const string GiveCashDescription = "description-give-cash"; @@ -42,21 +45,39 @@ public class DevCommands : IChatCommand, IWorldLoaded [TranslationReference] const string InstantBuildingDescription = "description-instant-building"; + [TranslationReference] + const string InstantBuildingAllDescription = "description-instant-building-all"; + [TranslationReference] const string BuildAnywhereDescription = "description-build-anywhere"; + [TranslationReference] + const string BuildAnywhereAllDescription = "description-build-anywhere-all"; + [TranslationReference] const string UnlimitedPowerDescription = "description-unlimited-power"; + [TranslationReference] + const string UnlimitedPowerAllDescription = "description-unlimited-power-all"; + [TranslationReference] const string EnableTechDescription = "description-enable-tech"; + [TranslationReference] + const string EnableTechAllDescription = "description-enable-tech-all"; + [TranslationReference] const string FastChargeDescription = "description-fast-charge"; + [TranslationReference] + const string FastChargeAllDescription = "description-fast-charge-all"; + [TranslationReference] const string DevCheatAllDescription = "description-dev-cheat-all"; + [TranslationReference] + const string DevCheatAllForAllDescription = "description-dev-cheat-all-for-all"; + [TranslationReference] const string DevCrashDescription = "description-dev-crash"; @@ -75,23 +96,38 @@ public class DevCommands : IChatCommand, IWorldLoaded [TranslationReference] const string DisposeSelectedActorsDescription = "description-dispose-selected-actors"; + [TranslationReference] + const string ProduceFromSelectedActorsDescription = "description-produce-from-selected-actors"; + + [TranslationReference] + const string ClearResourcesDescription = "description-clear-resources"; + readonly IDictionary Handler)> commandHandlers = new Dictionary)> { { "visibility", (ToggleVisiblityDescription, Visibility) }, + { "visibility-all", (ToggleVisiblityAllDescription, VisibilityAll) }, { "give-cash", (GiveCashDescription, GiveCash) }, { "give-cash-all", (GiveCashAllDescription, GiveCashAll) }, { "instant-build", (InstantBuildingDescription, InstantBuild) }, + { "instant-build-all", (InstantBuildingAllDescription, InstantBuildAll) }, { "build-anywhere", (BuildAnywhereDescription, BuildAnywhere) }, + { "build-anywhere-all", (BuildAnywhereAllDescription, BuildAnywhereAll) }, { "unlimited-power", (UnlimitedPowerDescription, UnlimitedPower) }, + { "unlimited-power-all", (UnlimitedPowerAllDescription, UnlimitedPowerAll) }, { "enable-tech", (EnableTechDescription, EnableTech) }, + { "enable-tech-all", (EnableTechAllDescription, EnableTechAll) }, { "fast-charge", (FastChargeDescription, FastCharge) }, + { "fast-charge-all", (FastChargeAllDescription, FastChargeAll) }, { "all", (DevCheatAllDescription, All) }, + { "all-for-all", (DevCheatAllForAllDescription, AllForAll) }, { "crash", (DevCrashDescription, Crash) }, { "levelup", (LevelUpActorDescription, LevelUp) }, { "player-experience", (PlayerExperienceDescription, PlayerExperience) }, { "power-outage", (PowerOutageDescription, PowerOutage) }, { "kill", (KillSelectedActorsDescription, Kill) }, - { "dispose", (DisposeSelectedActorsDescription, Dispose) } + { "dispose", (DisposeSelectedActorsDescription, Dispose) }, + { "produce", (ProduceFromSelectedActorsDescription, Produce) }, + { "clear-resources", (ClearResourcesDescription, ClearResources) } }; World world; @@ -161,36 +197,78 @@ static void Visibility(string arg, World world) IssueDevCommand(world, "DevVisibility"); } + static void VisibilityAll(string arg, World world) + { + foreach (var player in world.Players.Where(p => !p.NonCombatant)) + world.IssueOrder(new Order("DevVisibility", player.PlayerActor, false)); + } + static void InstantBuild(string arg, World world) { IssueDevCommand(world, "DevFastBuild"); } + static void InstantBuildAll(string arg, World world) + { + foreach (var player in world.Players.Where(p => !p.NonCombatant)) + world.IssueOrder(new Order("DevFastBuild", player.PlayerActor, false)); + } + static void BuildAnywhere(string arg, World world) { IssueDevCommand(world, "DevBuildAnywhere"); } + static void BuildAnywhereAll(string arg, World world) + { + foreach (var player in world.Players.Where(p => !p.NonCombatant)) + world.IssueOrder(new Order("DevBuildAnywhere", player.PlayerActor, false)); + } + static void UnlimitedPower(string arg, World world) { IssueDevCommand(world, "DevUnlimitedPower"); } + static void UnlimitedPowerAll(string arg, World world) + { + foreach (var player in world.Players.Where(p => !p.NonCombatant)) + world.IssueOrder(new Order("DevUnlimitedPower", player.PlayerActor, false)); + } + static void EnableTech(string arg, World world) { IssueDevCommand(world, "DevEnableTech"); } + static void EnableTechAll(string arg, World world) + { + foreach (var player in world.Players.Where(p => !p.NonCombatant)) + world.IssueOrder(new Order("DevEnableTech", player.PlayerActor, false)); + } + static void FastCharge(string arg, World world) { IssueDevCommand(world, "DevFastCharge"); } + static void FastChargeAll(string arg, World world) + { + foreach (var player in world.Players.Where(p => !p.NonCombatant)) + world.IssueOrder(new Order("DevFastCharge", player.PlayerActor, false)); + } + static void All(string arg, World world) { IssueDevCommand(world, "DevAll"); } + static void AllForAll(string arg, World world) + { + foreach (var player in world.Players.Where(p => !p.NonCombatant)) + world.IssueOrder(new Order("DevAll", player.PlayerActor, false)); + } + static void Crash(string arg, World world) { throw new DevException(); @@ -249,6 +327,22 @@ static void Dispose(string arg, World world) } } + static void Produce(string arg, World world) + { + foreach (var actor in world.Selection.Actors) + { + if (actor.IsDead) + continue; + + world.IssueOrder(new Order("DevProduce", world.LocalPlayer.PlayerActor, Target.FromActor(actor), false) { TargetString = arg }); + } + } + + static void ClearResources(string arg, World world) + { + IssueDevCommand(world, "DevClearResources"); + } + static void IssueDevCommand(World world, string command) { world.IssueOrder(new Order(command, world.LocalPlayer.PlayerActor, false)); diff --git a/OpenRA.Mods.Common/Effects/RallyPointIndicator.cs b/OpenRA.Mods.Common/Effects/RallyPointIndicator.cs index 102d0f62b3fc..8ce304d8b806 100644 --- a/OpenRA.Mods.Common/Effects/RallyPointIndicator.cs +++ b/OpenRA.Mods.Common/Effects/RallyPointIndicator.cs @@ -24,7 +24,7 @@ public class RallyPointIndicator : IEffect, IEffectAboveShroud, IEffectAnnotatio readonly Animation flag; readonly Animation circles; - readonly List targetLineNodes = new() { }; + readonly List targetLineNodes = new(); List cachedLocations; public RallyPointIndicator(Actor building, RallyPoint rp) diff --git a/OpenRA.Mods.Common/FileFormats/Blast.cs b/OpenRA.Mods.Common/FileFormats/Blast.cs index 8c5f8a4766a7..cf1ee7b1681d 100644 --- a/OpenRA.Mods.Common/FileFormats/Blast.cs +++ b/OpenRA.Mods.Common/FileFormats/Blast.cs @@ -214,8 +214,7 @@ public BitReader(Stream stream) public int ReadBits(int count) { var ret = 0; - var filled = 0; - while (filled < count) + for (var filled = 0; filled < count; filled++) { if (bitCount == 0) { @@ -226,7 +225,6 @@ public int ReadBits(int count) ret |= (bitBuffer & 1) << filled; bitBuffer >>= 1; bitCount--; - filled++; } return ret; diff --git a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs index 5fbc5cb0c8aa..fbea95a53716 100644 --- a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs +++ b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs @@ -392,6 +392,11 @@ public DefaultSpriteSequence(SpriteCache cache, ISpriteSequenceLoader loader, st facings = -facings; } + // Facings must be an integer factor of 1024 (i.e. 1024 / facings is an integer) to allow the frames to be + // mapped uniformly over the full rotation range. This implies that it is a power of 2. + if (facings == 0 || facings > 1024 || !Exts.IsPowerOf2(facings)) + throw new YamlException($"{facingsLocation}: {Facings.Key} must be within the (positive or negative) range of 1 to 1024, and a power of 2."); + if (interpolatedFacings != null && (interpolatedFacings < 2 || interpolatedFacings <= facings || interpolatedFacings > 1024 || !Exts.IsPowerOf2(interpolatedFacings.Value))) throw new YamlException($"{interpolatedFacingsLocation}: {InterpolatedFacings.Key} must be greater than {Facings.Key}, within the range of 2 to 1024, and a power of 2."); diff --git a/OpenRA.Mods.Common/Lint/CheckTooltips.cs b/OpenRA.Mods.Common/Lint/CheckTooltips.cs index c864b7495df0..e47e41bd7b96 100644 --- a/OpenRA.Mods.Common/Lint/CheckTooltips.cs +++ b/OpenRA.Mods.Common/Lint/CheckTooltips.cs @@ -35,8 +35,8 @@ static void Run(Action emitError, Ruleset rules) // Catch TypeDictionary errors. try { - var buildable = actorInfo.Value.TraitInfoOrDefault(); - if (buildable == null) + var buildable = actorInfo.Value.TraitInfos().ToArray(); + if (buildable.Length == 0) continue; var tooltip = actorInfo.Value.TraitInfos().FirstOrDefault(info => info.EnabledByDefault); diff --git a/OpenRA.Mods.Common/Lint/CheckTranslationReference.cs b/OpenRA.Mods.Common/Lint/CheckTranslationReference.cs index f759bab05925..ed252e878b11 100644 --- a/OpenRA.Mods.Common/Lint/CheckTranslationReference.cs +++ b/OpenRA.Mods.Common/Lint/CheckTranslationReference.cs @@ -17,6 +17,7 @@ using Linguini.Syntax.Ast; using Linguini.Syntax.Parser; using OpenRA.Traits; +using OpenRA.Widgets; namespace OpenRA.Mods.Common.Lint { @@ -111,7 +112,7 @@ void ILintPass.Run(Action emitError, Action emitWarning, ModData { foreach (var fieldInfo in modType.GetFields(Binding).Where(m => Utility.HasAttribute(m))) { - if (fieldInfo.IsInitOnly) + if (fieldInfo.IsInitOnly || !fieldInfo.IsStatic) continue; if (fieldInfo.FieldType != typeof(string)) @@ -120,7 +121,7 @@ void ILintPass.Run(Action emitError, Action emitWarning, ModData continue; } - var key = (string)fieldInfo.GetValue(string.Empty); + var key = (string)fieldInfo.GetValue(null); if (referencedKeys.Contains(key)) continue; @@ -135,6 +136,23 @@ void ILintPass.Run(Action emitError, Action emitWarning, ModData } } + var translatableFields = modData.ObjectCreator.GetTypes() + .Where(t => t.Name.EndsWith("Widget", StringComparison.InvariantCulture) && t.IsSubclassOf(typeof(Widget))) + .ToDictionary( + t => t.Name[..^6], + t => t.GetFields().Where(f => f.HasAttribute()).ToArray()) + .Where(t => t.Value.Length > 0) + .ToDictionary( + t => t.Key, + t => t.Value.Select(f => (f.Name, f, Utility.GetCustomAttributes(f, true)[0])).ToArray()); + + foreach (var filename in modData.Manifest.ChromeLayout) + { + var nodes = MiniYaml.FromStream(modData.DefaultFileSystem.Open(filename)); + foreach (var node in nodes) + CheckChrome(node, translation, language, emitError, emitWarning, translatableFields); + } + foreach (var file in modData.Manifest.Translations) { var stream = modData.DefaultFileSystem.Open(file); @@ -179,6 +197,47 @@ void ILintPass.Run(Action emitError, Action emitWarning, ModData } } + void CheckChrome(MiniYamlNode node, Translation translation, string language, Action emitError, Action emitWarning, + Dictionary translatables) + { + var nodeType = node.Key.Split('@')[0]; + foreach (var childNode in node.Value.Nodes) + { + if (!translatables.TryGetValue(nodeType, out var translationNodes)) + continue; + + var childType = childNode.Key.Split('@')[0]; + var field = Array.Find(translationNodes, t => t.Name == childType); + if (field.Name == null) + continue; + + var key = childNode.Value.Value; + if (key == null) + { + if (!field.Attribute.Optional) + emitError($"Widget `{node.Key}` in field `{childType}` has an empty translation reference."); + + continue; + } + + if (referencedKeys.Contains(key)) + continue; + + if (!key.Any(char.IsLetter)) + continue; + + if (!translation.HasMessage(key)) + emitWarning($"`{key}` defined by `{node.Key}` is not present in `{language}` translation."); + + referencedKeys.Add(key); + } + + foreach (var childNode in node.Value.Nodes) + if (childNode.Key == "Children") + foreach (var n in childNode.Value.Nodes) + CheckChrome(n, translation, language, emitError, emitWarning, translatables); + } + void CheckUnusedKey(string key, string attribute, Action emitWarning, string file) { var isAttribute = !string.IsNullOrEmpty(attribute); diff --git a/OpenRA.Mods.Common/Lint/LintBuildablePrerequisites.cs b/OpenRA.Mods.Common/Lint/LintBuildablePrerequisites.cs index 8570def42664..c4925adee61a 100644 --- a/OpenRA.Mods.Common/Lint/LintBuildablePrerequisites.cs +++ b/OpenRA.Mods.Common/Lint/LintBuildablePrerequisites.cs @@ -38,8 +38,8 @@ static void Run(Action emitError, Ruleset rules) // Catch TypeDictionary errors. try { - var bi = actorInfo.Value.TraitInfoOrDefault(); - if (bi != null) + var bis = actorInfo.Value.TraitInfos(); + foreach (var bi in bis) foreach (var prereq in bi.Prerequisites) if (!prereq.StartsWith("~disabled", StringComparison.Ordinal) && !providedPrereqs.Contains(prereq.Replace("!", "").Replace("~", ""))) emitError($"Buildable actor `{actorInfo.Key}` has prereq `{prereq}` not provided by anything."); diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 55ef9e46f9fb..035750d6773e 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -5,7 +5,7 @@ - + diff --git a/OpenRA.Mods.Common/Orders/OrderGenerator.cs b/OpenRA.Mods.Common/Orders/OrderGenerator.cs index 271aa90d931f..cca2d014f417 100644 --- a/OpenRA.Mods.Common/Orders/OrderGenerator.cs +++ b/OpenRA.Mods.Common/Orders/OrderGenerator.cs @@ -31,7 +31,7 @@ public virtual IEnumerable Order(World world, CPos cell, int2 worldPixel, IEnumerable IOrderGenerator.RenderAboveShroud(WorldRenderer wr, World world) { return RenderAboveShroud(wr, world); } IEnumerable IOrderGenerator.RenderAnnotations(WorldRenderer wr, World world) { return RenderAnnotations(wr, world); } string IOrderGenerator.GetCursor(World world, CPos cell, int2 worldPixel, MouseInput mi) { return GetCursor(world, cell, worldPixel, mi); } - void IOrderGenerator.Deactivate() { } + void IOrderGenerator.Deactivate() { Deactivate(); } bool IOrderGenerator.HandleKeyPress(KeyInput e) { return false; } void IOrderGenerator.SelectionChanged(World world, IEnumerable selected) { SelectionChanged(world, selected); } @@ -42,5 +42,6 @@ protected virtual void Tick(World world) { } protected abstract string GetCursor(World world, CPos cell, int2 worldPixel, MouseInput mi); protected abstract IEnumerable OrderInner(World world, CPos cell, int2 worldPixel, MouseInput mi); protected virtual void SelectionChanged(World world, IEnumerable selected) { } + protected virtual void Deactivate() { } } } diff --git a/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs b/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs index 90151ed09185..c8cc916ce4d6 100644 --- a/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs +++ b/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs @@ -46,6 +46,7 @@ sealed class VariantWrapper { public readonly ActorInfo ActorInfo; public readonly BuildingInfo BuildingInfo; + public readonly BuildableInfo BuildableInfo; public readonly PlugInfo PlugInfo; public readonly LineBuildInfo LineBuildInfo; public readonly IPlaceBuildingPreview Preview; @@ -54,6 +55,7 @@ public VariantWrapper(WorldRenderer wr, ProductionQueue queue, ActorInfo ai) { ActorInfo = ai; BuildingInfo = ActorInfo.TraitInfo(); + BuildableInfo = BuildableInfo.GetTraitForQueue(ActorInfo, queue.Info.Type); PlugInfo = ActorInfo.TraitInfoOrDefault(); LineBuildInfo = ActorInfo.TraitInfoOrDefault(); @@ -61,9 +63,8 @@ public VariantWrapper(WorldRenderer wr, ProductionQueue queue, ActorInfo ai) if (previewGeneratorInfo != null) { string faction; - var buildableInfo = ActorInfo.TraitInfoOrDefault(); - if (buildableInfo != null && buildableInfo.ForceFaction != null) - faction = buildableInfo.ForceFaction; + if (BuildableInfo != null && BuildableInfo.ForceFaction != null) + faction = BuildableInfo.ForceFaction; else { var mostLikelyProducer = queue.MostLikelyProducer(); @@ -107,7 +108,7 @@ public PlaceBuildingOrderGenerator(ProductionQueue queue, string name, WorldRend var variants = new List() { - new VariantWrapper(worldRenderer, queue, world.Map.Rules.Actors[name]) + new(worldRenderer, queue, world.Map.Rules.Actors[name]) }; foreach (var v in variants[0].ActorInfo.TraitInfos()) @@ -168,7 +169,13 @@ protected virtual IEnumerable InnerOrder(World world, CPos cell, MouseInp var owner = Queue.Actor.Owner; var ai = variants[variant].ActorInfo; var bi = variants[variant].BuildingInfo; - var notification = Queue.Info.CannotPlaceAudio ?? placeBuildingInfo.CannotPlaceNotification; + var buildableInfo = variants[variant].BuildableInfo; + var notification = buildableInfo?.CannotPlaceAudio; + notification ??= Queue.Info.CannotPlaceAudio; + notification ??= placeBuildingInfo.CannotPlaceNotification; + var textNotification = buildableInfo?.CannotPlaceTextNotification; + textNotification ??= Queue.Info.CannotPlaceTextNotification; + textNotification ??= placeBuildingInfo.CannotPlaceTextNotification; if (mi.Button == MouseButton.Left) { @@ -182,7 +189,7 @@ protected virtual IEnumerable InnerOrder(World world, CPos cell, MouseInp if (!AcceptsPlug(topLeft, plugInfo)) { Game.Sound.PlayNotification(world.Map.Rules, owner, "Speech", notification, owner.Faction.InternalName); - TextNotificationsManager.AddTransientLine(owner, placeBuildingInfo.CannotPlaceTextNotification); + TextNotificationsManager.AddTransientLine(owner, textNotification); yield break; } @@ -190,13 +197,13 @@ protected virtual IEnumerable InnerOrder(World world, CPos cell, MouseInp else { if (!world.CanPlaceBuilding(topLeft, ai, bi, null) - || !bi.IsCloseEnoughToBase(world, owner, ai, topLeft)) + || !bi.IsCloseEnoughToBase(world, owner, ai, Queue.Actor, topLeft)) { foreach (var order in ClearBlockersOrders(topLeft)) yield return order; Game.Sound.PlayNotification(world.Map.Rules, owner, "Speech", notification, owner.Faction.InternalName); - TextNotificationsManager.AddTransientLine(owner, placeBuildingInfo.CannotPlaceTextNotification); + TextNotificationsManager.AddTransientLine(owner, textNotification); yield break; } @@ -280,21 +287,21 @@ IEnumerable IOrderGenerator.RenderAboveShroud(WorldRenderer wr, Wor foreach (var t in BuildingUtils.GetLineBuildCells(world, topLeft, actorInfo, buildingInfo, owner)) { - var lineBuildable = world.IsCellBuildable(t.Cell, segmentInfo, segmentBuildingInfo); - var lineCloseEnough = segmentBuildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, segmentInfo, t.Cell); + var lineBuildable = world.IsCellBuildable(t.Cell, topLeft, segmentInfo, segmentBuildingInfo); + var lineCloseEnough = segmentBuildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, segmentInfo, Queue.Actor, t.Cell); footprint.Add(t.Cell, MakeCellType(lineBuildable && lineCloseEnough, true)); } } - var buildable = world.IsCellBuildable(topLeft, actorInfo, buildingInfo); - var closeEnough = buildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, actorInfo, topLeft); + var buildable = world.IsCellBuildable(topLeft, topLeft, actorInfo, buildingInfo); + var closeEnough = buildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, actorInfo, Queue.Actor, topLeft); footprint[topLeft] = MakeCellType(buildable && closeEnough); } else { - var isCloseEnough = buildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, actorInfo, topLeft); + var isCloseEnough = buildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, actorInfo, Queue.Actor, topLeft); foreach (var t in buildingInfo.Tiles(topLeft)) - footprint.Add(t, MakeCellType(isCloseEnough && world.IsCellBuildable(t, actorInfo, buildingInfo) && (resourceLayer == null || resourceLayer.GetResource(t).Type == null))); + footprint.Add(t, MakeCellType(isCloseEnough && world.IsCellBuildable(t, topLeft, actorInfo, buildingInfo) && (resourceLayer == null || resourceLayer.GetResource(t).Type == null))); } return preview?.Render(wr, topLeft, footprint) ?? Enumerable.Empty(); diff --git a/OpenRA.Mods.Common/Orders/RepairOrderGenerator.cs b/OpenRA.Mods.Common/Orders/RepairOrderGenerator.cs index 9a50f357442b..0c61e716a6a1 100644 --- a/OpenRA.Mods.Common/Orders/RepairOrderGenerator.cs +++ b/OpenRA.Mods.Common/Orders/RepairOrderGenerator.cs @@ -57,13 +57,16 @@ static IEnumerable OrderInner(World world, MouseInput mi) var repairable = underCursor.TraitOrDefault(); if (repairable != null) repairBuilding = repairable.FindRepairBuilding(underCursor); - else + + var repairableNear = underCursor.TraitOrDefault(); + if (repairableNear != null) { - var repairableNear = underCursor.TraitOrDefault(); - if (repairableNear != null) + var reairableNearBuilding = repairableNear.FindRepairBuilding(underCursor); + if (reairableNearBuilding != null && + (repairBuilding == null || (underCursor.Location - reairableNearBuilding.Location).LengthSquared < (underCursor.Location - repairBuilding.Location).LengthSquared)) { orderId = "RepairNear"; - repairBuilding = repairableNear.FindRepairBuilding(underCursor); + repairBuilding = reairableNearBuilding; } } diff --git a/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs b/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs index 9dcde2ad8ff4..cbb24ea12f13 100644 --- a/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs +++ b/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs @@ -632,20 +632,29 @@ void RequireProjectionRefreshInCell(CPos cell) } /// + /// /// defines immovability based on the mobile trait. The blocking rules /// in allow units /// to pass these immovable actors if they are temporary blockers (e.g. gates) or crushable by the locomotor. /// Since our abstract graph must work for any actor, we have to be conservative and can only consider a subset /// of the immovable actors in the graph - ones we know cannot be passed by some actors due to these rules. /// Both this and must be true for a cell to be blocked. - /// + /// + /// /// This method is dependant on the logic in /// and /// . This method must be kept in sync with changes in the locomotor /// rules. + /// /// bool ActorIsBlocking(Actor actor) { + if (locomotor.Info.BlockerActors.Length > 0 && !locomotor.Info.BlockerActors.Contains(actor.Info.Name)) + return false; + + if (locomotor.Info.NonBlockerActors.Length > 0 && locomotor.Info.NonBlockerActors.Contains(actor.Info.Name)) + return false; + var isMovable = actor.OccupiesSpace is Mobile mobile && !mobile.IsTraitDisabled && !mobile.IsTraitPaused && !mobile.IsImmovable; if (isMovable) return false; diff --git a/OpenRA.Mods.Common/PlayerExtensions.cs b/OpenRA.Mods.Common/PlayerExtensions.cs index a25f5118c877..afd79479ed06 100644 --- a/OpenRA.Mods.Common/PlayerExtensions.cs +++ b/OpenRA.Mods.Common/PlayerExtensions.cs @@ -19,8 +19,8 @@ public static class PlayerExtensions public static bool HasNoRequiredUnits(this Player player, bool shortGame) { if (shortGame) - return !player.World.ActorsHavingTrait(t => t.Info.RequiredForShortGame).Any(a => a.Owner == player); - return !player.World.ActorsHavingTrait().Any(a => a.Owner == player && a.IsInWorld); + return !player.World.ActorsHavingTrait(t => t.Info.RequiredForShortGame && !t.IsTraitDisabled).Any(a => a.Owner == player); + return !player.World.ActorsHavingTrait(t => !t.IsTraitDisabled).Any(a => a.Owner == player && a.IsInWorld); } } } diff --git a/OpenRA.Mods.Common/Projectiles/GravityBomb.cs b/OpenRA.Mods.Common/Projectiles/GravityBomb.cs index c6086a93a385..b35baaf2c419 100644 --- a/OpenRA.Mods.Common/Projectiles/GravityBomb.cs +++ b/OpenRA.Mods.Common/Projectiles/GravityBomb.cs @@ -10,8 +10,10 @@ #endregion using System.Collections.Generic; +using System.Linq; using OpenRA.GameRules; using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; using OpenRA.Traits; @@ -49,6 +51,9 @@ public class GravityBombInfo : IProjectileInfo [Desc("Value added to Velocity every tick.")] public readonly WVec Acceleration = new(0, 0, -15); + [Desc("Types of point defense weapons that can target this projectile.")] + public readonly BitSet PointDefenseTypes = default; + public IProjectile Create(ProjectileArgs args) { return new GravityBomb(this, args); } } @@ -96,6 +101,23 @@ public void Tick(World world) pos += velocity; velocity += acceleration; + if (info.PointDefenseTypes.Any()) + { + var shouldExplode = world.ActorsWithTrait().Any(x => x.Trait.Destroy(pos, args.SourceActor.Owner, info.PointDefenseTypes)); + if (shouldExplode) + { + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, Util.GetVerticalAngle(lastPos, pos), args.Facing), + ImpactPosition = pos, + }; + + args.Weapon.Impact(Target.FromPos(pos), warheadArgs); + world.AddFrameEndTask(w => w.Remove(this)); + return; + } + } + if (pos.Z <= args.PassiveTarget.Z) { pos += new WVec(0, 0, args.PassiveTarget.Z - pos.Z); @@ -110,6 +132,16 @@ public void Tick(World world) args.Weapon.Impact(Target.FromPos(pos), warheadArgs); } + if (info.PointDefenseTypes.Any()) + { + var shouldExplode = world.ActorsWithTrait().Any(x => x.Trait.Destroy(pos, args.SourceActor.Owner, info.PointDefenseTypes)); + if (shouldExplode) + { + args.Weapon.Impact(Target.FromPos(pos), new WarheadArgs(args)); + world.AddFrameEndTask(w => w.Remove(this)); + } + } + anim?.Tick(); } diff --git a/OpenRA.Mods.Common/Projectiles/Missile.cs b/OpenRA.Mods.Common/Projectiles/Missile.cs index d83d122c9011..36193950e91a 100644 --- a/OpenRA.Mods.Common/Projectiles/Missile.cs +++ b/OpenRA.Mods.Common/Projectiles/Missile.cs @@ -169,6 +169,9 @@ public class MissileInfo : IProjectileInfo [Desc("Range of facings by which jammed missiles can stray from current path.")] public readonly int JammedDiversionRange = 20; + [Desc("Types of point defense weapons that can target this projectile.")] + public readonly BitSet PointDefenseTypes = default; + [Desc("Explodes when leaving the following terrain type, e.g., Water for torpedoes.")] public readonly string BoundToTerrainType = ""; @@ -886,6 +889,10 @@ public void Tick(World world) pos = blockedPos; shouldExplode = true; } + else if (info.PointDefenseTypes.Any() && world.ActorsWithTrait().Any(a => a.Trait.Destroy(pos, args.SourceActor.Owner, info.PointDefenseTypes))) + { + shouldExplode = true; + } // Create the sprite trail effect if (!string.IsNullOrEmpty(info.TrailImage) && --ticksToNextSmoke < 0 && (state != States.Freefall || info.TrailWhenDeactivated)) diff --git a/OpenRA.Mods.Common/Projectiles/NukeLaunch.cs b/OpenRA.Mods.Common/Projectiles/NukeLaunch.cs index ea5e0a981739..4e7d9a901b47 100644 --- a/OpenRA.Mods.Common/Projectiles/NukeLaunch.cs +++ b/OpenRA.Mods.Common/Projectiles/NukeLaunch.cs @@ -92,7 +92,8 @@ public void Tick(World world) if (!isLaunched) { if (weapon.Report != null && weapon.Report.Length > 0) - Game.Sound.Play(SoundType.World, weapon.Report, world, pos); + if (weapon.AudibleThroughFog || (!world.ShroudObscures(pos) && !world.FogObscures(pos))) + Game.Sound.Play(SoundType.World, weapon.Report, world, pos, null, weapon.SoundVolume); if (anim != null) { @@ -121,8 +122,9 @@ public void Tick(World world) { var trailPos = !isDescending ? WPos.LerpQuadratic(ascendSource, ascendTarget, WAngle.Zero, ticks - trailDelay, turn) : WPos.LerpQuadratic(descendSource, descendTarget, WAngle.Zero, ticks - turn - trailDelay, impactDelay - turn); + var trailFacing = isDescending ? new WAngle(512) : WAngle.Zero; - world.AddFrameEndTask(w => w.Add(new SpriteEffect(trailPos, w, trailImage, trailSequences.Random(world.SharedRandom), + world.AddFrameEndTask(w => w.Add(new SpriteEffect(trailPos, trailFacing, w, trailImage, trailSequences.Random(world.SharedRandom), trailPalette))); trailTicks = trailInterval; diff --git a/OpenRA.Mods.Common/Scripting/CallLuaFunc.cs b/OpenRA.Mods.Common/Scripting/CallLuaFunc.cs index f3d060d8cc01..b26753605a7b 100644 --- a/OpenRA.Mods.Common/Scripting/CallLuaFunc.cs +++ b/OpenRA.Mods.Common/Scripting/CallLuaFunc.cs @@ -23,6 +23,7 @@ public sealed class CallLuaFunc : Activity, IDisposable public CallLuaFunc(LuaFunction function, ScriptContext context) { + ActivityType = ActivityType.Undefined; this.function = (LuaFunction)function.CopyReference(); this.context = context; } @@ -46,7 +47,6 @@ public override void Cancel(Actor self, bool keepQueue = false) { base.Cancel(self, keepQueue); Dispose(); - return; } public void Dispose() diff --git a/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs index 9dc5d0c130db..c7ef348ea235 100644 --- a/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/ActorGlobal.cs @@ -125,7 +125,7 @@ public int BuildTime(string type, string queue = null) if (!Context.World.Map.Rules.Actors.TryGetValue(type, out var ai)) throw new LuaException($"Unknown actor type '{type}'"); - var bi = ai.TraitInfoOrDefault(); + var bi = BuildableInfo.GetTraitForQueue(ai, queue); if (bi == null) return 0; diff --git a/OpenRA.Mods.Common/Scripting/Global/DateTimeGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/DateTimeGlobal.cs index cc0fcf77b9ab..234fda4fc739 100644 --- a/OpenRA.Mods.Common/Scripting/Global/DateTimeGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/DateTimeGlobal.cs @@ -32,6 +32,7 @@ public DateGlobal(ScriptContext context) } [Desc("True on the 31st of October.")] + [Obsolete("Use CurrentMonth and CurrentDay instead.")] public bool IsHalloween => DateTime.Today.Month == 10 && DateTime.Today.Day == 31; [Desc("Get the current game time (in ticks).")] @@ -43,6 +44,13 @@ public int Seconds(int seconds) return seconds * ticksPerSecond; } + public int CurrentYear => DateTime.Now.Year; + public int CurrentMonth => DateTime.Now.Month; + public int CurrentDay => DateTime.Now.Day; + public int CurrentHour => DateTime.Now.Hour; + public int CurrentMinute => DateTime.Now.Minute; + public int CurrentSecond => DateTime.Now.Second; + [Desc("Converts the number of minutes into game time (ticks).")] public int Minutes(int minutes) { diff --git a/OpenRA.Mods.Common/Scripting/Global/LightingGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/LightingGlobal.cs index 140b62fa3a3b..d9bc2da36f49 100644 --- a/OpenRA.Mods.Common/Scripting/Global/LightingGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/LightingGlobal.cs @@ -18,48 +18,46 @@ namespace OpenRA.Mods.Common.Scripting [ScriptGlobal("Lighting")] public class LightingGlobal : ScriptGlobal { - readonly IEnumerable flashPaletteEffects; - readonly GlobalLightingPaletteEffect lighting; - readonly bool hasLighting; + readonly IEnumerable flashEffects; + readonly TintPostProcessEffect tintEffect; public LightingGlobal(ScriptContext context) : base(context) { - flashPaletteEffects = context.World.WorldActor.TraitsImplementing(); - lighting = context.World.WorldActor.TraitOrDefault(); - hasLighting = lighting != null; + flashEffects = context.World.WorldActor.TraitsImplementing(); + tintEffect = context.World.WorldActor.TraitOrDefault(); } - [Desc("Controls the `" + nameof(FlashPaletteEffect) + "` trait.")] + [Desc("Controls the `" + nameof(FlashPostProcessEffect) + "` trait.")] public void Flash(string type = null, int ticks = -1) { - foreach (var effect in flashPaletteEffects) + foreach (var effect in flashEffects) if (effect.Info.Type == type) effect.Enable(ticks); } public double Red { - get => hasLighting ? lighting.Red : 1d; - set { if (hasLighting) lighting.Red = (float)value; } + get => tintEffect?.Red ?? 1; + set { if (tintEffect != null) tintEffect.Red = (float)value; } } public double Green { - get => hasLighting ? lighting.Green : 1d; - set { if (hasLighting) lighting.Green = (float)value; } + get => tintEffect?.Green ?? 1; + set { if (tintEffect != null) tintEffect.Green = (float)value; } } public double Blue { - get => hasLighting ? lighting.Blue : 1d; - set { if (hasLighting) lighting.Blue = (float)value; } + get => tintEffect?.Blue ?? 1; + set { if (tintEffect != null) tintEffect.Blue = (float)value; } } public double Ambient { - get => hasLighting ? lighting.Ambient : 1d; - set { if (hasLighting) lighting.Ambient = (float)value; } + get => tintEffect?.Ambient ?? 1; + set { if (tintEffect != null) tintEffect.Ambient = (float)value; } } } } diff --git a/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs index 0b607f15bdc9..e4a1a95499db 100644 --- a/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs @@ -159,11 +159,10 @@ public void FloatingText(string text, WPos position, int duration = 30, Color? c Action WrapOnPlayComplete(LuaFunction onPlayComplete) { - Action onComplete; if (onPlayComplete != null) { var f = (LuaFunction)onPlayComplete.CopyReference(); - onComplete = () => + return () => { try { @@ -177,9 +176,7 @@ Action WrapOnPlayComplete(LuaFunction onPlayComplete) }; } else - onComplete = () => { }; - - return onComplete; + return () => { }; } } } diff --git a/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs index 1f6a970d8110..eaa6e9ac5130 100644 --- a/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs @@ -157,7 +157,7 @@ public LuaTable ReinforceWithTransport(Player owner, string actorType, string[] // Scripted cargo aircraft must turn to default position before unloading. // TODO: pass facing through UnloadCargo instead. if (aircraft != null) - transport.QueueActivity(new Land(transport, Target.FromCell(transport.World, entryPath.Last()), WDist.FromCells(dropRange), aircraft.Info.InitialFacing)); + transport.QueueActivity(new Land(transport, Target.FromCell(transport.World, entryPath.Last()), WDist.FromCells(dropRange))); if (cargo != null) transport.QueueActivity(new UnloadCargo(transport, WDist.FromCells(dropRange))); diff --git a/OpenRA.Mods.Common/Scripting/LuaScript.cs b/OpenRA.Mods.Common/Scripting/LuaScript.cs index c9a7a6833a6d..253b4b28ccaf 100644 --- a/OpenRA.Mods.Common/Scripting/LuaScript.cs +++ b/OpenRA.Mods.Common/Scripting/LuaScript.cs @@ -20,7 +20,7 @@ namespace OpenRA.Mods.Common.Scripting { [TraitLocation(SystemActors.World)] [Desc("Part of the new Lua API.")] - public class LuaScriptInfo : TraitInfo, Requires + public class LuaScriptInfo : TraitInfo, Requires, NotBefore { [Desc("File names with location relative to the map.")] public readonly HashSet Scripts = new(); diff --git a/OpenRA.Mods.Common/Scripting/Properties/ProductionProperties.cs b/OpenRA.Mods.Common/Scripting/Properties/ProductionProperties.cs index df75f81d338e..792f156c8603 100644 --- a/OpenRA.Mods.Common/Scripting/Properties/ProductionProperties.cs +++ b/OpenRA.Mods.Common/Scripting/Properties/ProductionProperties.cs @@ -41,7 +41,7 @@ public void Produce(string actorType, string factionVariant = null, string produ if (!Self.World.Map.Rules.Actors.TryGetValue(actorType, out var actorInfo)) throw new LuaException($"Unknown actor type '{actorType}'"); - var bi = actorInfo.TraitInfo(); + var bi = actorInfo.TraitInfos().FirstOrDefault(); Self.QueueActivity(new WaitFor(() => { // Go through all available traits and see which one successfully produces @@ -138,7 +138,7 @@ public bool Build(string[] actorTypes, LuaFunction actionFunc = null) if (triggers.HasAnyCallbacksFor(Trigger.OnProduction)) return false; - var queue = queues.Where(q => actorTypes.All(t => GetBuildableInfo(t).Queue.Contains(q.Info.Type))) + var queue = queues.Where(q => actorTypes.All(t => GetBuildableInfo(t, q.Info.Type) != null)) .FirstOrDefault(q => !q.AllQueued().Any()); if (queue == null) @@ -187,19 +187,20 @@ public bool IsProducing(string actorType) if (triggers.HasAnyCallbacksFor(Trigger.OnProduction)) return true; - return queues.Where(q => GetBuildableInfo(actorType).Queue.Contains(q.Info.Type)) + return queues.Where(q => GetBuildableInfo(actorType, q.Info.Type) != null) .Any(q => q.AllQueued().Any()); } - BuildableInfo GetBuildableInfo(string actorType) + BuildableInfo GetBuildableInfo(string actorType, string queue) { var ri = Self.World.Map.Rules.Actors[actorType]; - var bi = ri.TraitInfoOrDefault(); + var bi = ri.TraitInfos().FirstOrDefault(); + // Error if there are no Buildable traits, return correct one otherwise. if (bi == null) throw new LuaException($"Actor of type {actorType} cannot be produced"); else - return bi; + return BuildableInfo.GetTraitForQueue(ri, queue); } } @@ -301,7 +302,7 @@ public bool IsProducing(string actorType) BuildableInfo GetBuildableInfo(string actorType) { var ri = Player.World.Map.Rules.Actors[actorType]; - var bi = ri.TraitInfoOrDefault(); + var bi = ri.TraitInfos().FirstOrDefault(); if (bi == null) throw new LuaException($"Actor of type {actorType} cannot be produced"); diff --git a/OpenRA.Mods.Common/Traits/AcceptsDeliveredExperience.cs b/OpenRA.Mods.Common/Traits/AcceptsDeliveredExperience.cs index 366978946e65..2b159180fa83 100644 --- a/OpenRA.Mods.Common/Traits/AcceptsDeliveredExperience.cs +++ b/OpenRA.Mods.Common/Traits/AcceptsDeliveredExperience.cs @@ -26,8 +26,5 @@ public class AcceptsDeliveredExperienceInfo : TraitInfo, Requires Types = new() { }; + public readonly HashSet Types = new(); public override object Create(ActorInitializer init) { return new ActorSpawner(this); } } diff --git a/OpenRA.Mods.Common/Traits/AffectsShroud.cs b/OpenRA.Mods.Common/Traits/AffectsShroud.cs index a0e152dc60ea..08677722b3e5 100644 --- a/OpenRA.Mods.Common/Traits/AffectsShroud.cs +++ b/OpenRA.Mods.Common/Traits/AffectsShroud.cs @@ -41,15 +41,15 @@ public abstract class AffectsShroud : ConditionalTrait, ISync readonly HashSet footprint; [Sync] - CPos cachedLocation; + protected CPos cachedLocation; [Sync] - WDist cachedRange; + protected WDist cachedRange; [Sync] - protected bool CachedTraitDisabled { get; private set; } + protected bool cachedTraitDisabled; - WPos cachedPos; + protected WPos cachedPos; protected abstract void AddCellsToPlayerShroud(Actor self, Player player, PPos[] uv); protected abstract void RemoveCellsFromPlayerShroud(Actor self, Player player); @@ -61,7 +61,7 @@ protected AffectsShroud(AffectsShroudInfo info) footprint = new HashSet(); } - PPos[] ProjectedCells(Actor self) + protected PPos[] ProjectedCells(Actor self) { var map = self.World.Map; var minRange = Info.MinRange; @@ -115,11 +115,11 @@ void ITick.Tick(Actor self) var traitDisabled = IsTraitDisabled; var range = Range; - if (cachedRange == range && traitDisabled == CachedTraitDisabled) + if (cachedRange == range && traitDisabled == cachedTraitDisabled) return; cachedRange = range; - CachedTraitDisabled = traitDisabled; + cachedTraitDisabled = traitDisabled; UpdateShroudCells(self); } @@ -140,7 +140,7 @@ void INotifyAddedToWorld.AddedToWorld(Actor self) var projectedPos = centerPosition - new WVec(0, centerPosition.Z, centerPosition.Z); cachedLocation = self.World.Map.CellContaining(projectedPos); cachedPos = centerPosition; - CachedTraitDisabled = IsTraitDisabled; + cachedTraitDisabled = IsTraitDisabled; var cells = ProjectedCells(self); foreach (var p in self.World.Players) @@ -153,7 +153,7 @@ void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) RemoveCellsFromPlayerShroud(self, p); } - public virtual WDist Range => CachedTraitDisabled ? WDist.Zero : Info.Range; + public virtual WDist Range => cachedTraitDisabled ? WDist.Zero : Info.Range; void INotifyMoving.MovementTypeChanged(Actor self, MovementType type) { diff --git a/OpenRA.Mods.Common/Traits/Air/Aircraft.cs b/OpenRA.Mods.Common/Traits/Air/Aircraft.cs index d1438a37d62f..95cf18d290b8 100644 --- a/OpenRA.Mods.Common/Traits/Air/Aircraft.cs +++ b/OpenRA.Mods.Common/Traits/Air/Aircraft.cs @@ -135,6 +135,9 @@ public class AircraftInfo : PausableConditionalTraitInfo, IPositionableInfo, IFa [Desc("Range to search for an alternative landing location if the ordered cell is blocked.")] public readonly WDist LandRange = WDist.FromCells(5); + [Desc("Take up space on terrain when landing.")] + public readonly bool TakeUpCellWhenLand = true; + [Desc("How fast this actor ascends or descends during horizontal movement.")] public readonly WAngle MaximumPitch = WAngle.FromDegrees(10); @@ -147,6 +150,12 @@ public class AircraftInfo : PausableConditionalTraitInfo, IPositionableInfo, IFa [Desc("Sounds to play when the actor is landing.")] public readonly string[] LandingSounds = Array.Empty(); + [Desc("Do the take off or landing sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the TakeoffSounds and LandingSounds played at.")] + public readonly float SoundVolume = 1f; + [Desc("The distance of the resupply base that the aircraft will wait for its turn.")] public readonly WDist WaitDistanceFromResupplyBase = new(3072); @@ -289,6 +298,7 @@ public WAngle GetTurnSpeed(bool isIdleTurn) readonly CPos[] creationRallyPoint; bool notify = true; + bool repulseEnabled = true; public static WPos GroundPosition(Actor self) { @@ -453,9 +463,13 @@ protected virtual void Tick(Actor self) Pitch = Util.TickFacing(Pitch, WAngle.Zero, Info.PitchSpeed); } - Repulse(); + if (repulseEnabled) + Repulse(); } + public void EnableRepulse() { repulseEnabled = true; } + public void DisableRepulse() { repulseEnabled = false; } + public void Repulse() { var repulsionForce = GetRepulsionForce(); @@ -495,7 +509,7 @@ public virtual WVec GetRepulsionForce() continue; var ai = actor.Info.TraitInfoOrDefault(); - if (ai == null || !ai.Repulsable || ai.CruiseAltitude != Info.CruiseAltitude) + if (ai == null || !ai.Repulsable || ai.CruiseAltitude != Info.CruiseAltitude || !actor.AppearsFriendlyTo(self)) continue; repulsionForce += GetRepulsionForce(actor); @@ -814,7 +828,8 @@ public void SetPosition(Actor self, WPos pos) { self.World.ActorMap.RemoveInfluence(self, this); landingCells = currentPos; - self.World.ActorMap.AddInfluence(self, this); + if (Info.TakeUpCellWhenLand) + self.World.ActorMap.AddInfluence(self, this); } } @@ -868,6 +883,9 @@ void CrushAction(Actor self, Func 0; + return Info.TakeUpCellWhenLand && landingCells.Length > 0; } #endregion @@ -1221,7 +1240,10 @@ void IActorPreviewInitModifier.ModifyActorPreviewInit(Actor self, TypeDictionary Activity ICreationActivity.GetCreationActivity() { - return new AssociateWithAirfieldActivity(self, creationActivityDelay, creationRallyPoint); + if (creationRallyPoint != null || creationActivityDelay > 0) + return new AssociateWithAirfieldActivity(self, creationActivityDelay, creationRallyPoint); + + return null; } sealed class AssociateWithAirfieldActivity : Activity diff --git a/OpenRA.Mods.Common/Traits/Armament.cs b/OpenRA.Mods.Common/Traits/Armament.cs index b8c7496d7ccd..aa6c1bc7ea84 100644 --- a/OpenRA.Mods.Common/Traits/Armament.cs +++ b/OpenRA.Mods.Common/Traits/Armament.cs @@ -20,6 +20,8 @@ namespace OpenRA.Mods.Common.Traits public class Barrel { public WVec Offset; + public WVec CasingOffset; + public WVec CasingTargetOffset; public WAngle Yaw; } @@ -64,7 +66,21 @@ public class ArmamentInfo : PausableConditionalTraitInfo, Requires(); + + [Desc("Casing target position relative to turret or body, (forward, right, up) triples.")] + public readonly WVec[] CasingTargetOffset = Array.Empty(); + + [Desc("Casing target position will be modified to ground level.")] + public readonly bool CasingHitGroundLevel = true; + public WeaponInfo WeaponInfo { get; private set; } + public WeaponInfo CasingWeaponInfo { get; private set; } public WDist ModifiedRange { get; private set; } public readonly PlayerRelationship TargetRelationships = PlayerRelationship.Enemy; @@ -85,6 +101,21 @@ public class ArmamentInfo : PausableConditionalTraitInfo, Requires().Select(m => m.GetRangeModifierDefault()))); + if (CasingWeapon != null) + { + var casingweaponToLower = CasingWeapon.ToLowerInvariant(); + if (!rules.Weapons.TryGetValue(casingweaponToLower, out var casingweaponInfo)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{weaponToLower}'"); + + CasingWeaponInfo = casingweaponInfo; + } + if (WeaponInfo.Burst > 1 && WeaponInfo.BurstDelays.Length > 1 && (WeaponInfo.BurstDelays.Length != WeaponInfo.Burst - 1)) throw new YamlException($"Weapon '{weaponToLower}' has an invalid number of BurstDelays, must be single entry or Burst - 1."); @@ -111,6 +151,7 @@ public override void RulesetLoaded(Ruleset rules, ActorInfo ai) public class Armament : PausableConditionalTrait, ITick { public readonly WeaponInfo Weapon; + public readonly WeaponInfo CasingWeapon; public readonly Barrel[] Barrels; Turreted turret; BodyOrientation coords; @@ -140,6 +181,7 @@ public Armament(Actor self, ArmamentInfo info) Actor = self; Weapon = info.WeaponInfo; + CasingWeapon = info.CasingWeaponInfo; Burst = Weapon.Burst; var barrels = new List(); @@ -148,12 +190,14 @@ public Armament(Actor self, ArmamentInfo info) barrels.Add(new Barrel { Offset = info.LocalOffset[i], - Yaw = info.LocalYaw.Length > i ? info.LocalYaw[i] : WAngle.Zero + Yaw = info.LocalYaw.Length > i ? info.LocalYaw[i] : WAngle.Zero, + CasingOffset = info.CasingSpawnLocalOffset.Length > i ? info.CasingSpawnLocalOffset[i] : WVec.Zero, + CasingTargetOffset = info.CasingTargetOffset.Length > i ? info.CasingTargetOffset[i] : WVec.Zero }); } if (barrels.Count == 0) - barrels.Add(new Barrel { Offset = WVec.Zero, Yaw = WAngle.Zero }); + barrels.Add(new Barrel { Offset = WVec.Zero, Yaw = WAngle.Zero, CasingOffset = WVec.Zero, CasingTargetOffset = WVec.Zero }); barrelCount = barrels.Count; @@ -173,8 +217,8 @@ protected override void Created(Actor self) notifyAttacks = self.TraitsImplementing().ToArray(); rangeModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetRangeModifier()); - reloadModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetReloadModifier()); - damageModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetFirepowerModifier()); + reloadModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetReloadModifier(Info.Name)); + damageModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetFirepowerModifier(Info.Name)); inaccuracyModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetInaccuracyModifier()); base.Created(self); @@ -327,6 +371,35 @@ protected virtual void FireBarrel(Actor self, IFacing facing, in Target target, GuidedTarget = target }; + ProjectileArgs argsCasing = null; + if (CasingWeapon != null) + { + WPos CasingSpawnPosition() => self.CenterPosition + CasingSpawnOffset(self, barrel); + + var casingHitPosition = self.CenterPosition + CasingHitOffset(self, barrel); + casingHitPosition = Info.CasingHitGroundLevel ? casingHitPosition - new WVec(0, 0, self.World.Map.DistanceAboveTerrain(casingHitPosition).Length) : casingHitPosition; + + WAngle CasingFireFacing() => (casingHitPosition - CasingSpawnPosition()).Yaw; + + argsCasing = new ProjectileArgs + { + Weapon = CasingWeapon, + Facing = CasingFireFacing(), + CurrentMuzzleFacing = CasingFireFacing, + + DamageModifiers = damageModifiers.ToArray(), + + InaccuracyModifiers = inaccuracyModifiers.ToArray(), + + RangeModifiers = rangeModifiers.ToArray(), + + Source = CasingSpawnPosition(), + CurrentSource = CasingSpawnPosition, + SourceActor = self, + PassiveTarget = casingHitPosition + }; + } + // Lambdas can't use 'in' variables, so capture a copy for later var delayedTarget = target; ScheduleDelayedAction(Info.FireDelay, Burst, (burst) => @@ -338,10 +411,25 @@ protected virtual void FireBarrel(Actor self, IFacing facing, in Target target, self.World.Add(projectile); if (args.Weapon.Report != null && args.Weapon.Report.Length > 0) - Game.Sound.Play(SoundType.World, args.Weapon.Report, self.World, self.CenterPosition); + { + var pos = self.CenterPosition; + if (args.Weapon.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, args.Weapon.Report, self.World, pos, null, args.Weapon.SoundVolume); + } if (burst == args.Weapon.Burst && args.Weapon.StartBurstReport != null && args.Weapon.StartBurstReport.Length > 0) - Game.Sound.Play(SoundType.World, args.Weapon.StartBurstReport, self.World, self.CenterPosition); + { + var pos = self.CenterPosition; + if (args.Weapon.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, args.Weapon.StartBurstReport, self.World, pos, null, args.Weapon.SoundVolume); + } + + if (argsCasing != null) + { + var projectileCasing = argsCasing.Weapon.Projectile.Create(argsCasing); + if (projectileCasing != null) + self.World.Add(projectileCasing); + } foreach (var na in notifyAttacks) na.Attacking(self, delayedTarget, this, barrel); @@ -364,10 +452,20 @@ protected virtual void UpdateBurst(Actor self, in Target target) { var modifiers = reloadModifiers.ToArray(); FireDelay = Util.ApplyPercentageModifiers(Weapon.ReloadDelay, modifiers); + if (FireDelay <= 0) + FireDelay = 1; + Burst = Weapon.Burst; if (Weapon.AfterFireSound != null && Weapon.AfterFireSound.Length > 0) - ScheduleDelayedAction(Weapon.AfterFireSoundDelay, Burst, (burst) => Game.Sound.Play(SoundType.World, Weapon.AfterFireSound, self.World, self.CenterPosition)); + { + ScheduleDelayedAction(Weapon.AfterFireSoundDelay, Burst, (burst) => + { + var pos = self.CenterPosition; + if (Weapon.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, Weapon.AfterFireSound, self.World, pos, null, Weapon.SoundVolume); + }); + } foreach (var nbc in notifyBurstComplete) nbc.FiredBurst(self, target, this); @@ -378,23 +476,33 @@ protected virtual void UpdateBurst(Actor self, in Target target) public WVec MuzzleOffset(Actor self, Barrel b) { - return CalculateMuzzleOffset(self, b); + return CalculateFireEffectOffset(self, b.Offset); + } + + public WVec CasingSpawnOffset(Actor self, Barrel b) + { + return CalculateFireEffectOffset(self, b.CasingOffset); + } + + public WVec CasingHitOffset(Actor self, Barrel b) + { + return CalculateFireEffectOffset(self, b.CasingTargetOffset); } - protected virtual WVec CalculateMuzzleOffset(Actor self, Barrel b) + protected virtual WVec CalculateFireEffectOffset(Actor self, WVec offset) { // Weapon offset in turret coordinates - var localOffset = b.Offset + new WVec(-Recoil, WDist.Zero, WDist.Zero); + var effectOffset = offset + new WVec(-Recoil, WDist.Zero, WDist.Zero); // Turret coordinates to body coordinates var bodyOrientation = coords.QuantizeOrientation(self.Orientation); if (turret != null) - localOffset = localOffset.Rotate(turret.WorldOrientation) + turret.Offset.Rotate(bodyOrientation); + effectOffset = effectOffset.Rotate(turret.WorldOrientation) + turret.Offset.Rotate(bodyOrientation); else - localOffset = localOffset.Rotate(bodyOrientation); + effectOffset = effectOffset.Rotate(bodyOrientation); // Body coordinates to world coordinates - return coords.LocalToWorld(localOffset); + return coords.LocalToWorld(effectOffset); } public WRot MuzzleOrientation(Actor self, Barrel b) diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs index ed75f2997adc..76e309782457 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs @@ -55,6 +55,9 @@ public abstract class AttackBaseInfo : PausableConditionalTraitInfo [Desc("Tolerance for attack angle. Range [0, 512], 512 covers 360 degrees.")] public readonly WAngle FacingTolerance = new(512); + [Desc("When enabled, show the target cursor on terrain cells even without force-fire.")] + public readonly bool TargetTerrainWithoutForceFire = false; + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) { base.RulesetLoaded(rules, ai); @@ -80,6 +83,7 @@ public abstract class AttackBase : PausableConditionalTrait, ITi protected IPositionable positionable; protected INotifyAiming[] notifyAiming; protected Func> getArmaments; + protected RejectsMoveToAttack[] rmta; readonly Actor self; @@ -96,6 +100,7 @@ protected override void Created(Actor self) facing = self.TraitOrDefault(); positionable = self.OccupiesSpace as IPositionable; notifyAiming = self.TraitsImplementing().ToArray(); + rmta = self.TraitsImplementing().ToArray(); getArmaments = InitializeGetArmaments(self); @@ -361,7 +366,7 @@ public IEnumerable ChooseArmamentsForTarget(Target t, bool forceAttack { // If force-fire is not used, and the target requires force-firing or the target is // terrain or invalid, no armaments can be used - if (!forceAttack && (t.Type == TargetType.Terrain || t.Type == TargetType.Invalid || t.RequiresForceFire)) + if (!forceAttack && ((t.Type == TargetType.Terrain && !Info.TargetTerrainWithoutForceFire) || t.Type == TargetType.Invalid || t.RequiresForceFire)) return Enumerable.Empty(); // Get target's owner; in case of terrain or invalid target there will be no problems @@ -387,6 +392,7 @@ public void AttackTarget(in Target target, AttackSource source, bool queued, boo if (!target.IsValidFor(self)) return; + allowMove &= !rmta.Any(t => !t.IsTraitDisabled); var activity = GetAttackActivity(self, source, target, allowMove, forceAttack, targetLineColor); self.QueueActivity(queued, activity); OnResolveAttackOrder(self, activity, target, queued, forceAttack); @@ -477,8 +483,9 @@ bool CanTargetLocation(Actor self, CPos location, TargetModifiers modifiers, ref IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue); - // Targeting the terrain is only possible with force-attack modifier - if (modifiers.HasModifier(TargetModifiers.ForceMove) || !modifiers.HasModifier(TargetModifiers.ForceAttack)) + // Targeting the terrain is only possible with force-attack modifier or when TargetTerrainWithoutForceFire is set + if (modifiers.HasModifier(TargetModifiers.ForceMove) || + !(ab.Info.TargetTerrainWithoutForceFire || modifiers.HasModifier(TargetModifiers.ForceAttack))) return false; var target = Target.FromCell(self.World, location); diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs b/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs index 1d2030c8c53e..c53ccd9762d3 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs @@ -226,9 +226,9 @@ void INotifyStanceChanged.StanceChanged(Actor self, AutoTarget autoTarget, UnitS } } - sealed class AttackActivity : Activity, IActivityNotifyStanceChanged + public class AttackActivity : Activity, IActivityNotifyStanceChanged { - readonly AttackFollow attack; + readonly AttackFollow[] attacks; readonly RevealsShroud[] revealsShroud; readonly IMove move; readonly bool forceAttack; @@ -250,7 +250,8 @@ sealed class AttackActivity : Activity, IActivityNotifyStanceChanged public AttackActivity(Actor self, AttackSource source, in Target target, bool allowMove, bool forceAttack, Color? targetLineColor = null) { - attack = self.Trait(); + ActivityType = ActivityType.Attack; + attacks = self.TraitsImplementing().ToArray(); move = allowMove ? self.TraitOrDefault() : null; revealsShroud = self.TraitsImplementing().ToArray(); rearmable = self.TraitOrDefault(); @@ -267,8 +268,8 @@ public AttackActivity(Actor self, AttackSource source, in Target target, bool al || target.Type == TargetType.FrozenActor || target.Type == TargetType.Terrain) { lastVisibleTarget = Target.FromPos(target.CenterPosition); - lastVisibleMaximumRange = attack.GetMaximumRangeVersusTarget(target); - lastVisibleMinimumRange = attack.GetMinimumRangeVersusTarget(target); + lastVisibleMaximumRange = attacks.Max(af => af.GetMaximumRangeVersusTarget(this.target)); + lastVisibleMinimumRange = attacks.Min(af => af.GetMinimumRangeVersusTarget(this.target)); if (target.Type == TargetType.Actor) { @@ -292,25 +293,27 @@ public override bool Tick(Actor self) // Check that AttackFollow hasn't cancelled the target by modifying attack.Target // Having both this and AttackFollow modify that field is a horrible hack. - if (hasTicked && attack.RequestedTarget.Type == TargetType.Invalid) + if (hasTicked && attacks.All(a => a.RequestedTarget.Type == TargetType.Invalid)) return true; - if (attack.IsTraitPaused) + if (attacks.All(a => a.IsTraitPaused)) return false; target = target.Recalculate(self.Owner, out var targetIsHiddenActor); - attack.SetRequestedTarget(target, forceAttack); + foreach (var attack in attacks) + attack.SetRequestedTarget(target, forceAttack); + hasTicked = true; if (!targetIsHiddenActor && target.Type == TargetType.Actor) { lastVisibleTarget = Target.FromTargetPositions(target); - lastVisibleMaximumRange = attack.GetMaximumRangeVersusTarget(target); - lastVisibleMinimumRange = attack.GetMinimumRange(); + lastVisibleMaximumRange = attacks.Max(af => af.GetMaximumRangeVersusTarget(target)); + lastVisibleMinimumRange = attacks.Min(af => af.GetMinimumRange()); lastVisibleOwner = target.Actor.Owner; lastVisibleTargetTypes = target.Actor.GetEnabledTargetTypes(); - var leeway = attack.Info.RangeMargin.Length; + var leeway = attacks.Min(af => af.Info.RangeMargin.Length); if (leeway != 0 && move != null && target.Actor.Info.HasTraitInfo()) { var preferMinRange = Math.Min(lastVisibleMinimumRange.Length + leeway, lastVisibleMaximumRange.Length); @@ -325,7 +328,7 @@ public override bool Tick(Actor self) else if (target.Type == TargetType.FrozenActor && !lastVisibleTarget.IsValidFor(self)) { lastVisibleTarget = Target.FromTargetPositions(target); - lastVisibleMaximumRange = attack.GetMaximumRangeVersusTarget(target); + lastVisibleMaximumRange = attacks.Max(af => af.GetMaximumRangeVersusTarget(target)); lastVisibleOwner = target.FrozenActor.Owner; lastVisibleTargetTypes = target.FrozenActor.TargetTypes; } @@ -335,7 +338,7 @@ public override bool Tick(Actor self) useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self); // Most actors want to be able to see their target before shooting - if (target.Type == TargetType.FrozenActor && !attack.Info.TargetFrozenActors && !forceAttack) + if (target.Type == TargetType.FrozenActor && !attacks.Any(a => a.Info.TargetFrozenActors) && !forceAttack) { var rs = revealsShroud .Where(t => !t.IsTraitDisabled) @@ -358,14 +361,14 @@ public override bool Tick(Actor self) // If all valid weapons have depleted their ammo and Rearmable trait exists, return to RearmActor to reload // and resume the activity after reloading if AbortOnResupply is set to 'false' - if (rearmable != null && !useLastVisibleTarget && attack.Armaments.All(x => x.IsTraitPaused || !x.Weapon.IsValidAgainst(target, self.World, self))) + if (rearmable != null && !useLastVisibleTarget && attacks.All(a => a.Armaments.All(x => x.IsTraitPaused || !x.Weapon.IsValidAgainst(target, self.World, self)))) { // Attack moves never resupply if (source == AttackSource.AttackMove) return true; // AbortOnResupply cancels the current activity (after resupplying) plus any queued activities - if (attack.Info.AbortOnResupply) + if (attacks.All(a => a.Info.AbortOnResupply)) NextActivity?.Cancel(self); if (isAircraft) @@ -385,7 +388,7 @@ public override bool Tick(Actor self) } returnToBase = true; - return attack.Info.AbortOnResupply; + return attacks.All(a => a.Info.AbortOnResupply); } var pos = self.CenterPosition; @@ -413,7 +416,8 @@ public override bool Tick(Actor self) protected override void OnLastRun(Actor self) { // Cancel the requested target, but keep firing on it while in range - attack.ClearRequestedTarget(); + foreach (var attack in attacks) + attack.ClearRequestedTarget(); } void IActivityNotifyStanceChanged.StanceChanged(Actor self, AutoTarget autoTarget, UnitStance oldStance, UnitStance newStance) @@ -424,7 +428,8 @@ void IActivityNotifyStanceChanged.StanceChanged(Actor self, AutoTarget autoTarge // If lastVisibleTarget is invalid we could never view the target in the first place, so we just drop it here too if (!lastVisibleTarget.IsValidFor(self) || !autoTarget.HasValidTargetPriority(self, lastVisibleOwner, lastVisibleTargetTypes)) - attack.ClearRequestedTarget(); + foreach (var attack in attacks) + attack.ClearRequestedTarget(); } public override IEnumerable TargetLineNodes(Actor self) @@ -434,7 +439,7 @@ public override IEnumerable TargetLineNodes(Actor self) if (returnToBase) foreach (var n in ChildActivity.TargetLineNodes(self)) yield return n; - if (!returnToBase || !attack.Info.AbortOnResupply) + if (!returnToBase || attacks.Any(a => !a.Info.AbortOnResupply)) yield return new TargetLineNode(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value); } } diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackOmni.cs b/OpenRA.Mods.Common/Traits/Attack/AttackOmni.cs index c87ef23e40de..e6bb63b8d0c3 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackOmni.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackOmni.cs @@ -42,6 +42,7 @@ public class SetTarget : Activity, IActivityNotifyStanceChanged public SetTarget(AttackOmni attack, in Target target, bool allowMove, bool forceAttack, Color? targetLineColor = null) { + ActivityType = ActivityType.Attack; this.target = target; this.targetLineColor = targetLineColor; this.attack = attack; @@ -86,6 +87,12 @@ public override IEnumerable TargetLineNodes(Actor self) if (targetLineColor != null) yield return new TargetLineNode(target, targetLineColor.Value); } + + // Added for the Angry Mob logic + public override IEnumerable GetTargets(Actor self) + { + yield return target; + } } } } diff --git a/OpenRA.Mods.Common/Traits/AutoCrusher.cs b/OpenRA.Mods.Common/Traits/AutoCrusher.cs index aa07c4e6fd52..3b5de4100888 100644 --- a/OpenRA.Mods.Common/Traits/AutoCrusher.cs +++ b/OpenRA.Mods.Common/Traits/AutoCrusher.cs @@ -36,11 +36,12 @@ sealed class AutoCrusherInfo : PausableConditionalTraitInfo, Requires public override object Create(ActorInitializer init) { return new AutoCrusher(init.Self, this); } } - sealed class AutoCrusher : PausableConditionalTrait, INotifyIdle + sealed class AutoCrusher : ConditionalTrait, INotifyIdle { int nextScanTime; readonly IMoveInfo moveInfo; readonly bool isAircraft; + readonly bool ignoresDisguise; readonly IMove move; public AutoCrusher(Actor self, AutoCrusherInfo info) @@ -48,21 +49,17 @@ public AutoCrusher(Actor self, AutoCrusherInfo info) { move = self.Trait(); moveInfo = self.Info.TraitInfo(); - nextScanTime = self.World.SharedRandom.Next(Info.MinimumScanTimeInterval, Info.MaximumScanTimeInterval); isAircraft = move is Aircraft; + ignoresDisguise = self.Info.HasTraitInfo(); } void INotifyIdle.TickIdle(Actor self) { - if (nextScanTime-- > 0) + if (IsTraitDisabled || nextScanTime-- > 0) return; - // TODO: Add a proper Cloak and Disguise detection here. var crushableActor = self.World.FindActorsInCircle(self.CenterPosition, Info.ScanRadius) - .Where(a => a != self && !a.IsDead && a.IsInWorld && - self.Location != a.Location && a.IsAtGroundLevel() && - Info.TargetRelationships.HasRelationship(self.Owner.RelationshipWith(a.Owner)) && - a.TraitsImplementing().Any(c => c.CrushableBy(a, self, Info.CrushClasses))) + .Where(a => IsValidCrushTarget(self, a)) .ClosestToWithPathFrom(self); // TODO: Make it use shortest pathfinding distance instead if (crushableActor == null) @@ -75,5 +72,32 @@ void INotifyIdle.TickIdle(Actor self) nextScanTime = self.World.SharedRandom.Next(Info.MinimumScanTimeInterval, Info.MaximumScanTimeInterval); } + + bool IsValidCrushTarget(Actor self, Actor target) + { + if (target == self || target.IsDead || !target.IsInWorld || self.Location == target.Location || !target.IsAtGroundLevel()) + return false; + + var targetRelationship = self.Owner.RelationshipWith(target.Owner); + var effectiveOwner = target.EffectiveOwner?.Owner; + if (effectiveOwner != null && !ignoresDisguise && targetRelationship != PlayerRelationship.Ally) + { + // Check effective relationships if the target is disguised and we cannot see through the disguise. (By ignoring it or by being an ally.) + if (!Info.TargetRelationships.HasRelationship(self.Owner.RelationshipWith(effectiveOwner))) + return false; + } + else if (!Info.TargetRelationships.HasRelationship(targetRelationship)) + return false; + + if (target.TraitsImplementing().Any(c => !c.IsTraitDisabled && !c.IsVisible(target, self.Owner))) + return false; + + return target.TraitsImplementing().Any(c => c.CrushableBy(target, self, Info.CrushClasses)); + } + + protected override void TraitEnabled(Actor self) + { + nextScanTime = self.World.SharedRandom.Next(Info.MinimumScanTimeInterval, Info.MaximumScanTimeInterval); + } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs index 67ba202ec081..1ffd320a1069 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs @@ -28,6 +28,9 @@ public class BaseBuilderBotModuleInfo : ConditionalTraitInfo [Desc("Tells the AI what building types are considered refineries.")] public readonly HashSet RefineryTypes = new(); + [Desc("Tells the AI to build refineries near these actors.")] + public readonly HashSet SupplyDockTypes = new(); + [Desc("Tells the AI what building types are considered power plants.")] public readonly HashSet PowerTypes = new(); @@ -152,20 +155,21 @@ public CPos GetRandomBaseCenter() public CPos DefenseCenter { get; private set; } - // Actor, ActorCount. + // Actor, ActorCount public Dictionary BuildingsBeingProduced = new(); readonly World world; readonly Player player; - PowerManager playerPower; PlayerResources playerResources; IResourceLayer resourceLayer; IBotPositionsUpdated[] positionsUpdatedModules; CPos initialBaseCenter; - readonly BaseBuilderQueueManager[] builders; int currentBuilderIndex = 0; + public PowerManager PlayerPower { get; private set; } + public int ExcessPower { get; private set; } + public BaseBuilderBotModule(Actor self, BaseBuilderBotModuleInfo info) : base(info) { @@ -176,7 +180,7 @@ public BaseBuilderBotModule(Actor self, BaseBuilderBotModuleInfo info) protected override void Created(Actor self) { - playerPower = self.Owner.PlayerActor.TraitOrDefault(); + PlayerPower = self.Owner.PlayerActor.TraitOrDefault(); playerResources = self.Owner.PlayerActor.Trait(); resourceLayer = self.World.WorldActor.TraitOrDefault(); positionsUpdatedModules = self.Owner.PlayerActor.TraitsImplementing().ToArray(); @@ -184,10 +188,10 @@ protected override void Created(Actor self) var i = 0; foreach (var building in Info.BuildingQueues) - builders[i++] = new BaseBuilderQueueManager(this, building, player, playerPower, playerResources, resourceLayer); + builders[i++] = new BaseBuilderQueueManager(this, building, player, playerResources, resourceLayer); foreach (var defense in Info.DefenseQueues) - builders[i++] = new BaseBuilderQueueManager(this, defense, player, playerPower, playerResources, resourceLayer); + builders[i++] = new BaseBuilderQueueManager(this, defense, player, playerResources, resourceLayer); } void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) @@ -205,13 +209,14 @@ void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) void IBotTick.BotTick(IBot bot) { // TODO: this causes pathfinding lag when AI's gets blocked in - SetRallyPointsForNewProductionBuildings(bot); + /* SetRallyPointsForNewProductionBuildings(bot); */ BuildingsBeingProduced.Clear(); // PERF: We tick only one type of valid queue at a time // if AI gets enough cash, it can fill all of its queues with enough ticks var findQueue = false; + ExcessPower = PlayerPower != null ? PlayerPower.ExcessPower : 0; for (int i = 0, builderIndex = currentBuilderIndex; i < builders.Length; i++) { if (++builderIndex >= builders.Length) @@ -228,19 +233,24 @@ void IBotTick.BotTick(IBot bot) findQueue = true; } - // Refresh "BuildingsBeingProduced" only when AI can produce + // Record buildings being produced only when AI can produce, + // and record their power only when AI can produce if (playerResources.GetCashAndResources() >= Info.ProductionMinCashRequirement) { foreach (var queue in queues) { + // Record the number of the buildings. var producing = queue.AllQueued().FirstOrDefault(); if (producing == null) continue; - if (BuildingsBeingProduced.TryGetValue(producing.Item, out var number)) - BuildingsBeingProduced[producing.Item] = number + 1; + if (BuildingsBeingProduced.ContainsKey(producing.Item)) + BuildingsBeingProduced[producing.Item] = BuildingsBeingProduced[producing.Item] + 1; else BuildingsBeingProduced.Add(producing.Item, 1); + + // Record the power of the building. + ExcessPower += producing.ActorInfo.TraitInfos().Where(p => p.EnabledByDefault).Sum(pi => pi.Amount); } } } @@ -266,7 +276,7 @@ void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) n.UpdatedDefenseCenter(e.Attacker.Location); } - void SetRallyPointsForNewProductionBuildings(IBot bot) + /* void SetRallyPointsForNewProductionBuildings(IBot bot) { foreach (var rp in world.ActorsWithTrait()) { @@ -301,8 +311,8 @@ CPos ChooseRallyLocationNear(Actor producer) bool IsRallyPointValid(CPos x, BuildingInfo info) { - return info != null && world.IsCellBuildable(x, null, info); - } + return info != null && world.IsCellBuildable(x, x, null, info); + } */ // Require at least one refinery, unless we can't build it. public bool HasAdequateRefineryCount => diff --git a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs index f91f20c44ade..0b4484a64623 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs @@ -24,7 +24,6 @@ sealed class BaseBuilderQueueManager readonly BaseBuilderBotModule baseBuilder; readonly World world; readonly Player player; - readonly PowerManager playerPower; readonly PlayerResources playerResources; readonly IResourceLayer resourceLayer; @@ -40,13 +39,11 @@ sealed class BaseBuilderQueueManager WaterCheck waterState = WaterCheck.NotChecked; - public BaseBuilderQueueManager(BaseBuilderBotModule baseBuilder, string category, Player p, PowerManager pm, - PlayerResources pr, IResourceLayer rl) + public BaseBuilderQueueManager(BaseBuilderBotModule baseBuilder, string category, Player p, PlayerResources pr, IResourceLayer rl) { this.baseBuilder = baseBuilder; world = p.World; player = p; - playerPower = pm; playerResources = pr; resourceLayer = rl; Category = category; @@ -172,7 +169,7 @@ bool TickQueue(IBot bot, ProductionQueue queue) else if (baseBuilder.Info.RefineryTypes.Contains(actorInfo.Name)) type = BuildingType.Refinery; - (location, actorVariant) = ChooseBuildLocation(currentBuilding.Item, true, type); + (location, actorVariant) = ChooseBuildLocation(currentBuilding.Item, true, queue.Actor, type); } if (location == null) @@ -223,7 +220,7 @@ ActorInfo GetProducibleBuilding(HashSet actors, IEnumerable b if (!baseBuilder.Info.BuildingLimits.ContainsKey(actor.Name)) return true; - return playerBuildings.Count(a => a.Info.Name == actor.Name) < baseBuilder.Info.BuildingLimits[actor.Name]; + return playerBuildings.Count(a => a.Info.Name == actor.Name) + (baseBuilder.BuildingsBeingProduced.TryGetValue(actor.Name, out var beingProduced) ? beingProduced : 0) < baseBuilder.Info.BuildingLimits[actor.Name]; }); if (orderBy != null) @@ -234,8 +231,8 @@ ActorInfo GetProducibleBuilding(HashSet actors, IEnumerable b bool HasSufficientPowerForActor(ActorInfo actorInfo) { - return playerPower == null || actorInfo.TraitInfos().Where(i => i.EnabledByDefault) - .Sum(p => p.Amount) + playerPower.ExcessPower >= baseBuilder.Info.MinimumExcessPower; + return baseBuilder.PlayerPower == null || actorInfo.TraitInfos().Where(i => i.EnabledByDefault) + .Sum(p => p.Amount) + baseBuilder.ExcessPower >= baseBuilder.Info.MinimumExcessPower; } ActorInfo ChooseBuildingToBuild(ProductionQueue queue) @@ -247,7 +244,7 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) a => a.TraitInfos().Where(i => i.EnabledByDefault).Sum(p => p.Amount)); // First priority is to get out of a low power situation - if (playerPower != null && playerPower.ExcessPower < minimumExcessPower) + if (baseBuilder.PlayerPower != null && baseBuilder.ExcessPower < minimumExcessPower) { if (power != null && power.TraitInfos().Where(i => i.EnabledByDefault).Sum(p => p.Amount) > 0) { @@ -363,14 +360,25 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) || !AIUtils.IsAreaAvailable(world, player, world.Map, baseBuilder.Info.CheckForWaterRadius, baseBuilder.Info.WaterTerrainTypes))) continue; - // Will this put us into low power? + // Maybe we can't queue this because of InstantCashDrain logic? var actor = world.Map.Rules.Actors[name]; - if (playerPower != null && (playerPower.ExcessPower < minimumExcessPower || !HasSufficientPowerForActor(actor))) + if (playerResources != null) + { + if (queue.Info.InstantCashDrain) + { + var cost = queue.GetProductionCost(actor); + if (playerResources.GetCashAndResources() < cost) + continue; + } + } + + // Will this put us into low power? + if (baseBuilder.PlayerPower != null && (baseBuilder.ExcessPower < minimumExcessPower || !HasSufficientPowerForActor(actor))) { // Try building a power plant instead if (power != null && power.TraitInfos().Where(i => i.EnabledByDefault).Sum(pi => pi.Amount) > 0) { - if (playerPower.PowerOutageRemainingTicks > 0) + if (baseBuilder.PlayerPower.PowerOutageRemainingTicks > 0) AIUtils.BotDebug("{0} decided to build {1}: Priority override (is low power)", queue.Actor.Owner, power.Name); else AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); @@ -390,7 +398,7 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) return null; } - (CPos? Location, int Variant) ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type) + (CPos? Location, int Variant) ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, Actor producer, BuildingType type) { var actorInfo = world.Map.Rules.Actors[actorType]; var bi = actorInfo.TraitInfoOrDefault(); @@ -460,10 +468,11 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) foreach (var cell in cells) { - if (!world.CanPlaceBuilding(cell, variantActorInfo, vbi, null)) + // AI need to check the building place with an additional 1 cell bounds for not enclosing units. + if (!AIUtils.CanPlaceBuildingWithSpaceAround(world, cell, variantActorInfo, vbi, null, 1)) continue; - if (distanceToBaseIsImportant && !vbi.IsCloseEnoughToBase(world, player, variantActorInfo, cell)) + if (distanceToBaseIsImportant && !vbi.IsCloseEnoughToBase(world, player, variantActorInfo, producer, cell)) continue; return (cell, actorVariant); @@ -489,16 +498,34 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) case BuildingType.Refinery: - // Try and place the refinery near a resource field - if (resourceLayer != null) + // Don't check for resources if the mod has docks + if (!baseBuilder.Info.SupplyDockTypes.Any()) + { + // Try and place the refinery near a resource field + if (resourceLayer != null) + { + var nearbyResources = world.Map.FindTilesInAnnulus(baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius) + .Where(a => resourceLayer.GetResource(a).Type != null) + .Shuffle(world.LocalRandom).Take(baseBuilder.Info.MaxResourceCellsToCheck); + + foreach (var r in nearbyResources) + { + var found = FindPos(baseCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); + if (found.Location != null) + return found; + } + } + } + else { - var nearbyResources = world.Map.FindTilesInAnnulus(baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius) - .Where(a => resourceLayer.GetResource(a).Type != null) + // Try and place the refinery near a supply dock + var nearbyDocks = world.FindActorsInCircle(world.Map.CenterOfCell(baseCenter), WDist.FromCells(baseBuilder.Info.MaxBaseRadius)) + .Where(a => baseBuilder.Info.SupplyDockTypes.Contains(a.Info.Name)) .Shuffle(world.LocalRandom).Take(baseBuilder.Info.MaxResourceCellsToCheck); - foreach (var r in nearbyResources) + foreach (var r in nearbyDocks) { - var found = FindPos(baseCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); + var found = FindPos(baseCenter, world.Map.CellContaining(r.CenterPosition), baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); if (found.Location != null) return found; } diff --git a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/MinelayerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/MinelayerBotModule.cs index ddd95266564b..be738f7d001c 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/MinelayerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/MinelayerBotModule.cs @@ -149,7 +149,7 @@ void IBotTick.BotTick(IBot bot) foreach (var minelayer in minelayers) { var cells = pathFinder.FindPathToTargetCell(minelayer.Actor, new[] { minelayer.Actor.Location }, enemy.Location, BlockedByActor.Immovable, laneBias: false); - if (cells != null && !(cells.Count == 0)) + if (cells != null && cells.Count != 0) { AIUtils.BotDebug($"{player}: try find a location to lay mine."); EnqueueConflictPosition(cells[cells.Count / 2]); @@ -193,7 +193,7 @@ void IBotTick.BotTick(IBot bot) foreach (var minelayer in minelayers) { var cells = pathFinder.FindPathToTargetCell(minelayer.Actor, new[] { minelayer.Actor.Location }, minelayingPosition, BlockedByActor.Immovable, laneBias: false); - if (cells != null && !(cells.Count == 0)) + if (cells != null && cells.Count != 0) { orderedActors.Add(minelayer.Actor); diff --git a/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs index ca5588f8b340..1e3258056e58 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs @@ -12,13 +12,13 @@ using System; using System.Collections.Generic; using System.Linq; +using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Traits.BotModules.Squads; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { - [TraitLocation(SystemActors.Player)] [Desc("Manages AI squads.")] public class SquadManagerBotModuleInfo : ConditionalTraitInfo { @@ -34,6 +34,10 @@ public class SquadManagerBotModuleInfo : ConditionalTraitInfo [Desc("Actor types that should generally be excluded from attack squads.")] public readonly HashSet ExcludeFromSquadsTypes = new(); + [ActorReference] + [Desc("Actor types that are randomly sent around the base after their production.")] + public readonly HashSet DozerTypes = new(); + [ActorReference] [Desc("Actor types that are considered construction yards (base builders).")] public readonly HashSet ConstructionYardTypes = new(); @@ -46,8 +50,12 @@ public class SquadManagerBotModuleInfo : ConditionalTraitInfo [Desc("Own actor types that are prioritized when defending.")] public readonly HashSet ProtectionTypes = new(); + [ActorReference] + [Desc("Units that form a guerrilla squad.")] + public readonly HashSet GuerrillaTypes = new(); + [Desc("Target types are used for identifying aircraft.")] - public readonly BitSet AircraftTargetType = new("Air"); + public readonly BitSet AircraftTargetType = new("Air", "AirborneActor"); [Desc("Minimum number of units AI must have before attacking.")] public readonly int SquadSize = 8; @@ -55,11 +63,11 @@ public class SquadManagerBotModuleInfo : ConditionalTraitInfo [Desc("Random number of up to this many units is added to squad size when creating an attack squad.")] public readonly int SquadSizeRandomBonus = 30; - [Desc("Delay (in ticks) between giving out orders to units.")] - public readonly int AssignRolesInterval = 50; + [Desc("Possibility of units in GuerrillaTypes to join Guerrilla.")] + public readonly int JoinGuerrilla = 50; - [Desc("Delay (in ticks) between attempting rush attacks.")] - public readonly int RushInterval = 600; + [Desc("Max number of units AI has in guerrilla squad")] + public readonly int MaxGuerrillaSize = 10; [Desc("Delay (in ticks) between updating squads.")] public readonly int AttackForceInterval = 75; @@ -67,12 +75,15 @@ public class SquadManagerBotModuleInfo : ConditionalTraitInfo [Desc("Minimum delay (in ticks) between creating squads.")] public readonly int MinimumAttackForceDelay = 0; - [Desc("Radius in cells around enemy BaseBuilder (Construction Yard) where AI scans for targets to rush.")] - public readonly int RushAttackScanRadius = 15; - [Desc("Radius in cells around the base that should be scanned for units to be protected.")] public readonly int ProtectUnitScanRadius = 15; + [Desc("Minimum radius in cells around base center to send dozer after building it.")] + public readonly int MinDozerSendingRadius = 4; + + [Desc("Maximum radius in cells around base center to send dozer after building it.")] + public readonly int MaxDozerSendingRadius = 16; + [Desc("Maximum distance in cells from center of the base when checking for MCV deployment location.", "Only applies if RestrictMCVDeploymentFallbackToBase is enabled and there's at least one construction yard.")] public readonly int MaxBaseRadius = 20; @@ -92,6 +103,12 @@ public class SquadManagerBotModuleInfo : ConditionalTraitInfo [Desc("Enemy target types to never target.")] public readonly BitSet IgnoredEnemyTargetTypes = default; + [Desc("Locomotor used by pathfinding leader for squads")] + public readonly HashSet SuggestedGroundLeaderLocomotor = new(); + + [Desc("Locomotor used by pathfinding leader for squads")] + public readonly HashSet SuggestedNavyLeaderLocomotor = new(); + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) { base.RulesetLoaded(rules, ai); @@ -116,12 +133,13 @@ public CPos GetRandomBaseCenter() public readonly World World; public readonly Player Player; + public readonly int RepeatedAltertTicks = 15; - readonly Predicate unitCannotBeOrdered; - readonly List unitsHangingAroundTheBase = new(); + public readonly Predicate UnitCannotBeOrdered; + readonly List unitsHangingAroundTheBase = new(); // Units that the bot already knows about. Any unit not on this list needs to be given a role. - readonly HashSet activeUnits = new(); + readonly List activeUnits = new(); public List Squads = new(); @@ -130,19 +148,29 @@ public CPos GetRandomBaseCenter() IBotNotifyIdleBaseUnits[] notifyIdleBaseUnits; CPos initialBaseCenter; + Actor airStrikeTarget; - int rushTicks; int assignRolesTicks; - int attackForceTicks; + + // int attackForceTicks; + int protectionForceTicks; + int guerrillaForceTicks; + int airForceTicks; + int navyForceTicks; + int groundForceTicks; + int minAttackForceDelayTicks; + int alertedTicks; + public SquadManagerBotModule(Actor self, SquadManagerBotModuleInfo info) : base(info) { World = self.World; Player = self.Owner; + alertedTicks = 0; - unitCannotBeOrdered = a => a == null || a.Owner != Player || a.IsDead || !a.IsInWorld; + UnitCannotBeOrdered = a => a == null || a.Owner != Player || a.IsDead || !a.IsInWorld || a.CurrentActivity is Enter; } // Use for proactive targeting. @@ -152,13 +180,10 @@ public bool IsPreferredEnemyUnit(Actor a) return false; var targetTypes = a.GetEnabledTargetTypes(); - if (targetTypes.IsEmpty || targetTypes.Overlaps(Info.IgnoredEnemyTargetTypes)) - return false; - - return IsNotHiddenUnit(a); + return !targetTypes.IsEmpty && !targetTypes.Overlaps(Info.IgnoredEnemyTargetTypes); } - bool IsNotHiddenUnit(Actor a) + public bool IsNotHiddenUnit(Actor a) { var hasModifier = false; var visModifiers = a.TraitsImplementing(); @@ -173,6 +198,21 @@ bool IsNotHiddenUnit(Actor a) return !hasModifier; } + public bool IsNotUnseenUnit(Actor a) + { + var isUnseen = false; + var visModifiers = a.TraitsImplementing(); + foreach (var v in visModifiers) + { + if (v.IsVisible(a, Player)) + return true; + + isUnseen = true; + } + + return !isUnseen; + } + protected override void Created(Actor self) { notifyPositionsUpdated = self.Owner.PlayerActor.TraitsImplementing().ToArray(); @@ -181,13 +221,15 @@ protected override void Created(Actor self) protected override void TraitEnabled(Actor self) { - // Avoid all AIs trying to rush in the same tick, randomize their initial rush a little. - var smallFractionOfRushInterval = Info.RushInterval / 20; - rushTicks = World.LocalRandom.Next(Info.RushInterval - smallFractionOfRushInterval, Info.RushInterval + smallFractionOfRushInterval); + var attackForceTicks = World.LocalRandom.Next(0, Info.AttackForceInterval); + + protectionForceTicks = attackForceTicks; + guerrillaForceTicks = attackForceTicks + 1; + airForceTicks = attackForceTicks + 2; + navyForceTicks = attackForceTicks + 3; + groundForceTicks = attackForceTicks + 4; + assignRolesTicks = attackForceTicks + 5; - // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. - assignRolesTicks = World.LocalRandom.Next(0, Info.AssignRolesInterval); - attackForceTicks = World.LocalRandom.Next(0, Info.AttackForceInterval); minAttackForceDelayTicks = World.LocalRandom.Next(0, Info.MinimumAttackForceDelay); } @@ -198,107 +240,57 @@ void IBotEnabled.BotEnabled(IBot bot) void IBotTick.BotTick(IBot bot) { + if (!IsPreferredEnemyUnit(airStrikeTarget) || !IsNotHiddenUnit(airStrikeTarget)) + airStrikeTarget = null; + AssignRolesToIdleUnits(bot); + if (alertedTicks > 0) + alertedTicks--; } - internal static Actor ClosestTo(IEnumerable ownActors, Actor targetActor) + internal Actor FindClosestEnemy(Actor sourceActor, WDist radius) { - // Return actors that can get within weapons range of the target. - // First, let's determine the max weapons range for each of the actors. - var target = Target.FromActor(targetActor); - var ownActorsAndTheirAttackRanges = ownActors - .Select(a => (Actor: a, AttackBases: a.TraitsImplementing().Where(Exts.IsTraitEnabled) - .Where(ab => ab.HasAnyValidWeapons(target)).ToList())) - .Where(x => x.AttackBases.Count > 0) - .Select(x => (x.Actor, Range: x.AttackBases.Max(ab => ab.GetMaximumRangeVersusTarget(target)))) - .ToDictionary(x => x.Actor, x => x.Range); - - // Now determine if each actor can either path directly to the target, - // or if it can path to a nearby location at the edge of its weapon range to the target - // A thorough check would check each position within the circle, but for performance - // we'll only check 8 positions around the edge of the circle. - // We need to account for the weapons range here to account for units such as boats. - // They can't path directly to a land target, - // but might be able to get close enough to shore to attack the target from range. - return ownActorsAndTheirAttackRanges.Keys - .ClosestToWithPathToAny(targetActor.World, a => - { - var range = ownActorsAndTheirAttackRanges[a].Length; - var rangeDiag = Exts.MultiplyBySqrtTwoOverTwo(range); - return new[] - { - targetActor.CenterPosition, - targetActor.CenterPosition + new WVec(range, 0, 0), - targetActor.CenterPosition + new WVec(-range, 0, 0), - targetActor.CenterPosition + new WVec(0, range, 0), - targetActor.CenterPosition + new WVec(0, -range, 0), - targetActor.CenterPosition + new WVec(rangeDiag, rangeDiag, 0), - targetActor.CenterPosition + new WVec(-rangeDiag, rangeDiag, 0), - targetActor.CenterPosition + new WVec(-rangeDiag, -rangeDiag, 0), - targetActor.CenterPosition + new WVec(rangeDiag, -rangeDiag, 0), - }; - }); + return World.FindActorsInCircle(sourceActor.CenterPosition, radius).Where(a => IsPreferredEnemyUnit(a) && IsNotHiddenUnit(a) && IsNotUnseenUnit(a)).ClosestToWithPathFrom(sourceActor); } - internal IEnumerable<(Actor Actor, WVec Offset)> FindEnemies(IEnumerable actors, Actor sourceActor) + internal Actor FindClosestEnemy(Actor sourceActor) { - // Check units are in fact enemies and not hidden. - // Then check which are in weapons range of the source. - var activeAttackBases = sourceActor.TraitsImplementing().Where(Exts.IsTraitEnabled).ToArray(); - var enemiesAndSourceAttackRanges = actors - .Where(IsPreferredEnemyUnit) - .Select(a => (Actor: a, AttackBases: activeAttackBases.Where(ab => ab.HasAnyValidWeapons(Target.FromActor(a))).ToList())) - .Where(x => x.AttackBases.Count > 0) - .Select(x => (x.Actor, Range: x.AttackBases.Max(ab => ab.GetMaximumRangeVersusTarget(Target.FromActor(x.Actor))))) - .ToDictionary(x => x.Actor, x => x.Range); - - // Now determine if the source actor can path directly to the target, - // or if it can path to a nearby location at the edge of its weapon range to the target - // A thorough check would check each position within the circle, but for performance - // we'll only check 8 positions around the edge of the circle. - // We need to account for the weapons range here to account for units such as boats. - // They can't path directly to a land target, - // but might be able to get close enough to shore to attack the target from range. - return enemiesAndSourceAttackRanges.Keys - .WithPathFrom(sourceActor, a => + var findVisible = false; + var bestDist = long.MaxValue; + Actor bestTarget = null; + foreach (var a in World.Actors.Where(a => IsPreferredEnemyUnit(a))) + { + var dist = (a.CenterPosition - sourceActor.CenterPosition).LengthSquared; + + if (findVisible) { - var range = enemiesAndSourceAttackRanges[a].Length; - var rangeDiag = Exts.MultiplyBySqrtTwoOverTwo(range); - return new[] + if (IsNotHiddenUnit(a) && dist < bestDist) { - WVec.Zero, - new WVec(range, 0, 0), - new WVec(-range, 0, 0), - new WVec(0, range, 0), - new WVec(0, -range, 0), - new WVec(rangeDiag, rangeDiag, 0), - new WVec(-rangeDiag, rangeDiag, 0), - new WVec(-rangeDiag, -rangeDiag, 0), - new WVec(rangeDiag, -rangeDiag, 0), - }; - }) - .Select(x => (x.Actor, x.ReachableOffsets.MinBy(o => o.LengthSquared))); - } - - internal (Actor Actor, WVec Offset) FindClosestEnemy(Actor sourceActor) - { - return FindClosestEnemy(World.Actors, sourceActor); - } - - internal (Actor Actor, WVec Offset) FindClosestEnemy(Actor sourceActor, WDist radius) - { - return FindClosestEnemy(World.FindActorsInCircle(sourceActor.CenterPosition, radius), sourceActor); - } + bestTarget = a; + bestDist = dist; + } + } + else + { + if (IsNotHiddenUnit(a)) + { + findVisible = true; + bestTarget = a; + bestDist = dist; + } + else if (dist < bestDist) + { + bestTarget = a; + bestDist = dist; + } + } + } - (Actor Actor, WVec Offset) FindClosestEnemy(IEnumerable actors, Actor sourceActor) - { - return WorldUtils.ClosestToIgnoringPath(FindEnemies(actors, sourceActor), x => x.Actor, sourceActor); + return bestTarget; } void CleanSquads() { - foreach (var s in Squads) - s.Units.RemoveWhere(unitCannotBeOrdered); Squads.RemoveAll(s => !s.IsValid); } @@ -308,54 +300,95 @@ Squad GetSquadOfType(SquadType type) return Squads.FirstOrDefault(s => s.Type == type); } - Squad RegisterNewSquad(IBot bot, SquadType type, (Actor Actor, WVec Offset) target = default) + IEnumerable GetSquadsOfType(SquadType type) + { + return Squads.Where(s => s.Type == type); + } + + Squad RegisterNewSquad(IBot bot, SquadType type, Actor target = null) { var ret = new Squad(bot, this, type, target); Squads.Add(ret); return ret; } - internal void UnregisterSquad(Squad squad) + public void DismissSquad(Squad squad) { - activeUnits.ExceptWith(squad.Units); - squad.Units.Clear(); + foreach (var unit in squad.Units) + { + unitsHangingAroundTheBase.Add(unit); + } - // CleanSquads will remove the squad from the Squads list. - // We can't do that here as this is designed to be called from within Squad.Update - // and thus would mutate the Squads list we are iterating over. + squad.Units.Clear(); } void AssignRolesToIdleUnits(IBot bot) { CleanSquads(); - activeUnits.RemoveWhere(unitCannotBeOrdered); - unitsHangingAroundTheBase.RemoveAll(unitCannotBeOrdered); - foreach (var n in notifyIdleBaseUnits) - n.UpdatedIdleBaseUnits(unitsHangingAroundTheBase); + // Ticks squads + if (--protectionForceTicks <= 0) + { + protectionForceTicks = Info.AttackForceInterval; + foreach (var s in GetSquadsOfType(SquadType.Protection)) + { + s.Units.RemoveAll(u => UnitCannotBeOrdered(u.Actor)); + s.Update(); + } + } + + if (--guerrillaForceTicks <= 0) + { + guerrillaForceTicks = Info.AttackForceInterval; + foreach (var s in GetSquadsOfType(SquadType.Assault)) + { + s.Units.RemoveAll(u => UnitCannotBeOrdered(u.Actor)); + s.Update(); + } + } - if (--rushTicks <= 0) + if (--airForceTicks <= 0) { - rushTicks = Info.RushInterval; - TryToRushAttack(bot); + airForceTicks = Info.AttackForceInterval; + foreach (var s in GetSquadsOfType(SquadType.Air)) + { + s.Units.RemoveAll(u => UnitCannotBeOrdered(u.Actor)); + s.Update(); + } } - if (--attackForceTicks <= 0) + if (--navyForceTicks <= 0) { - attackForceTicks = Info.AttackForceInterval; - foreach (var s in Squads) + navyForceTicks = Info.AttackForceInterval; + foreach (var s in GetSquadsOfType(SquadType.Naval)) + { + s.Units.RemoveAll(u => UnitCannotBeOrdered(u.Actor)); + s.Update(); + } + } + + if (--groundForceTicks <= 0) + { + groundForceTicks = Info.AttackForceInterval; + foreach (var s in GetSquadsOfType(SquadType.Rush)) + { + s.Units.RemoveAll(u => UnitCannotBeOrdered(u.Actor)); s.Update(); + } } if (--assignRolesTicks <= 0) { - assignRolesTicks = Info.AssignRolesInterval; + assignRolesTicks = Info.AttackForceInterval; + unitsHangingAroundTheBase.RemoveAll(u => UnitCannotBeOrdered(u.Actor)); + activeUnits.RemoveAll(UnitCannotBeOrdered); FindNewUnits(bot); } if (--minAttackForceDelayTicks <= 0) { minAttackForceDelayTicks = Info.MinimumAttackForceDelay; + unitsHangingAroundTheBase.RemoveAll(u => UnitCannotBeOrdered(u.Actor)); CreateAttackForce(bot); } } @@ -367,24 +400,43 @@ void FindNewUnits(IBot bot) !Info.ExcludeFromSquadsTypes.Contains(a.Info.Name) && !activeUnits.Contains(a)); + var guerrillaForce = GetSquadOfType(SquadType.Assault); + var guerrillaUpdate = guerrillaForce == null || (guerrillaForce.Units.Count <= Info.MaxGuerrillaSize && (World.LocalRandom.Next(100) >= Info.JoinGuerrilla)); + foreach (var a in newUnits) { - if (Info.AirUnitsTypes.Contains(a.Info.Name)) + var baseCenter = GetRandomBaseCenter(); + var mobile = a.TraitOrDefault(); + if (Info.DozerTypes.Contains(a.Info.Name) && mobile != null) + { + var dozerTargetPos = World.Map.FindTilesInAnnulus(baseCenter, Info.MinDozerSendingRadius, Info.MaxDozerSendingRadius) + .Where(c => mobile.CanEnterCell(c)).Random(World.LocalRandom); + + AIUtils.BotDebug($"AI: {a.Owner} has chosen {dozerTargetPos} to move its Dozer ({a})"); + bot.QueueOrder(new Order("Move", a, Target.FromCell(World, dozerTargetPos), true)); + } + else if (Info.GuerrillaTypes.Contains(a.Info.Name) && guerrillaUpdate) + { + guerrillaForce ??= RegisterNewSquad(bot, SquadType.Assault); + + guerrillaForce.Units.Add(new UnitWposWrapper(a)); + } + else if (Info.AirUnitsTypes.Contains(a.Info.Name)) { var air = GetSquadOfType(SquadType.Air); air ??= RegisterNewSquad(bot, SquadType.Air); - air.Units.Add(a); + air.Units.Add(new UnitWposWrapper(a)); } else if (Info.NavalUnitsTypes.Contains(a.Info.Name)) { var ships = GetSquadOfType(SquadType.Naval); ships ??= RegisterNewSquad(bot, SquadType.Naval); - ships.Units.Add(a); + ships.Units.Add(new UnitWposWrapper(a)); } else - unitsHangingAroundTheBase.Add(a); + unitsHangingAroundTheBase.Add(new UnitWposWrapper(a)); activeUnits.Add(a); } @@ -392,6 +444,16 @@ void FindNewUnits(IBot bot) // Notifying here rather than inside the loop, should be fine and saves a bunch of notification calls foreach (var n in notifyIdleBaseUnits) n.UpdatedIdleBaseUnits(unitsHangingAroundTheBase); + + var protectSq = GetSquadOfType(SquadType.Protection); + if (protectSq != null) + { + protectSq.Units = unitsHangingAroundTheBase; + return; + } + + protectSq = RegisterNewSquad(bot, SquadType.Protection, null); + protectSq.Units = unitsHangingAroundTheBase; } void CreateAttackForce(IBot bot) @@ -402,9 +464,10 @@ void CreateAttackForce(IBot bot) if (unitsHangingAroundTheBase.Count >= randomizedSquadSize) { - var attackForce = RegisterNewSquad(bot, SquadType.Assault); + var attackForce = RegisterNewSquad(bot, SquadType.Rush); - attackForce.Units.UnionWith(unitsHangingAroundTheBase); + foreach (var a in unitsHangingAroundTheBase) + attackForce.Units.Add(a); unitsHangingAroundTheBase.Clear(); foreach (var n in notifyIdleBaseUnits) @@ -412,71 +475,17 @@ void CreateAttackForce(IBot bot) } } - void TryToRushAttack(IBot bot) + void ProtectOwn(Actor attacker) { - var ownUnits = activeUnits - .Where(unit => - unit.IsIdle - && unit.Info.HasTraitInfo() - && !Info.AirUnitsTypes.Contains(unit.Info.Name) - && !Info.NavalUnitsTypes.Contains(unit.Info.Name) - && !Info.ExcludeFromSquadsTypes.Contains(unit.Info.Name)) - .ToList(); - - if (ownUnits.Count < Info.SquadSize) - return; - - var allEnemyBaseBuilder = FindEnemies( - World.Actors.Where(a => Info.ConstructionYardTypes.Contains(a.Info.Name)), - ownUnits.First()) - .ToList(); - - if (allEnemyBaseBuilder.Count == 0 || ownUnits.Count < Info.SquadSize) - return; - - foreach (var enemyBaseBuilder in allEnemyBaseBuilder) + foreach (var s in Squads.Where(s => s.IsValid)) { - // Don't rush enemy aircraft! - var enemies = FindEnemies( - World.FindActorsInCircle(enemyBaseBuilder.Actor.CenterPosition, WDist.FromCells(Info.RushAttackScanRadius)) - .Where(unit => - unit.Info.HasTraitInfo() - && !Info.AirUnitsTypes.Contains(unit.Info.Name) - && !Info.NavalUnitsTypes.Contains(unit.Info.Name)), - ownUnits.First()) - .ToList(); - - if (AttackOrFleeFuzzy.Rush.CanAttack(ownUnits, enemies.Select(x => x.Actor).ToList())) + if (s.Type != SquadType.Protection) { - var target = enemies.Count > 0 ? enemies.Random(World.LocalRandom) : enemyBaseBuilder; - var rush = GetSquadOfType(SquadType.Rush); - rush ??= RegisterNewSquad(bot, SquadType.Rush, target); - - rush.Units.UnionWith(ownUnits); - - return; + if ((s.CenterPosition - attacker.CenterPosition).LengthSquared > WDist.FromCells(Info.ProtectUnitScanRadius).LengthSquared) + continue; } - } - } - void ProtectOwn(IBot bot, Actor attacker) - { - var protectSq = GetSquadOfType(SquadType.Protection); - protectSq ??= RegisterNewSquad(bot, SquadType.Protection, (attacker, WVec.Zero)); - - if (protectSq.IsValid && !protectSq.IsTargetValid(protectSq.CenterUnit())) - protectSq.SetActorToTarget((attacker, WVec.Zero)); - - if (!protectSq.IsValid) - { - var ownUnits = World.FindActorsInCircle(World.Map.CenterOfCell(GetRandomBaseCenter()), WDist.FromCells(Info.ProtectUnitScanRadius)) - .Where(unit => - unit.Owner == Player - && !Info.ProtectionTypes.Contains(unit.Info.Name) - && unit.Info.HasTraitInfo()) - .WithPathTo(World, attacker.CenterPosition); - - protectSq.Units.UnionWith(ownUnits); + s.TargetActor = attacker; } } @@ -489,18 +498,32 @@ void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) { - if (!IsPreferredEnemyUnit(e.Attacker)) + if (alertedTicks > 0 || !IsPreferredEnemyUnit(e.Attacker)) return; + alertedTicks = RepeatedAltertTicks; + if (Info.ProtectionTypes.Contains(self.Info.Name)) { foreach (var n in notifyPositionsUpdated) n.UpdatedDefenseCenter(e.Attacker.Location); - ProtectOwn(bot, e.Attacker); + ProtectOwn(e.Attacker); } } + public void SetAirStrikeTarget(Actor target) + { + airStrikeTarget = target; + } + + public Actor PopAirStrikeTarget() + { + var target = airStrikeTarget; + airStrikeTarget = null; + return target; + } + List IGameSaveTraitData.IssueTraitData(Actor self) { if (IsTraitDisabled) @@ -511,16 +534,19 @@ List IGameSaveTraitData.IssueTraitData(Actor self) new MiniYamlNode("Squads", "", Squads.Select(s => new MiniYamlNode("Squad", s.Serialize())).ToList()), new MiniYamlNode("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)), new MiniYamlNode("UnitsHangingAroundTheBase", FieldSaver.FormatValue(unitsHangingAroundTheBase - .Where(a => !unitCannotBeOrdered(a)) - .Select(a => a.ActorID) + .Where(u => !UnitCannotBeOrdered(u.Actor)) + .Select(u => u.Actor.ActorID) .ToArray())), new MiniYamlNode("ActiveUnits", FieldSaver.FormatValue(activeUnits - .Where(a => !unitCannotBeOrdered(a)) + .Where(a => !UnitCannotBeOrdered(a)) .Select(a => a.ActorID) .ToArray())), - new MiniYamlNode("RushTicks", FieldSaver.FormatValue(rushTicks)), new MiniYamlNode("AssignRolesTicks", FieldSaver.FormatValue(assignRolesTicks)), - new MiniYamlNode("AttackForceTicks", FieldSaver.FormatValue(attackForceTicks)), + new MiniYamlNode("protectionForceTicks", FieldSaver.FormatValue(protectionForceTicks)), + new MiniYamlNode("guerrillaForceTicks", FieldSaver.FormatValue(guerrillaForceTicks)), + new MiniYamlNode("airForceTicks", FieldSaver.FormatValue(airForceTicks)), + new MiniYamlNode("navyForceTicks", FieldSaver.FormatValue(navyForceTicks)), + new MiniYamlNode("groundForceTicks", FieldSaver.FormatValue(groundForceTicks)), new MiniYamlNode("MinAttackForceDelayTicks", FieldSaver.FormatValue(minAttackForceDelayTicks)), }; } @@ -538,25 +564,38 @@ void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) if (nodes.TryGetValue("UnitsHangingAroundTheBase", out var unitsHangingAroundTheBaseNode)) { unitsHangingAroundTheBase.Clear(); - unitsHangingAroundTheBase.AddRange(FieldLoader.GetValue("UnitsHangingAroundTheBase", unitsHangingAroundTheBaseNode.Value) - .Select(a => self.World.GetActorById(a)).Where(a => a != null)); + + foreach (var a in FieldLoader.GetValue("UnitsHangingAroundTheBase", unitsHangingAroundTheBaseNode.Value) + .Select(a => self.World.GetActorById(a)).Where(a => a != null)) + { + unitsHangingAroundTheBase.Add(new UnitWposWrapper(a)); + } } if (nodes.TryGetValue("ActiveUnits", out var activeUnitsNode)) { activeUnits.Clear(); - activeUnits.UnionWith(FieldLoader.GetValue("ActiveUnits", activeUnitsNode.Value) + activeUnits.AddRange(FieldLoader.GetValue("ActiveUnits", activeUnitsNode.Value) .Select(a => self.World.GetActorById(a)).Where(a => a != null)); } - if (nodes.TryGetValue("RushTicks", out var rushTicksNode)) - rushTicks = FieldLoader.GetValue("RushTicks", rushTicksNode.Value); - if (nodes.TryGetValue("AssignRolesTicks", out var assignRolesTicksNode)) assignRolesTicks = FieldLoader.GetValue("AssignRolesTicks", assignRolesTicksNode.Value); - if (nodes.TryGetValue("AttackForceTicks", out var attackForceTicksNode)) - attackForceTicks = FieldLoader.GetValue("AttackForceTicks", attackForceTicksNode.Value); + if (nodes.TryGetValue("protectionForceTicks", out var protectionForceTicksNode)) + protectionForceTicks = FieldLoader.GetValue("protectionForceTicks", protectionForceTicksNode.Value); + + if (nodes.TryGetValue("guerrillaForceTicks", out var guerrillaForceTicksNode)) + guerrillaForceTicks = FieldLoader.GetValue("guerrillaForceTicks", guerrillaForceTicksNode.Value); + + if (nodes.TryGetValue("airForceTicks", out var airForceTicksNode)) + airForceTicks = FieldLoader.GetValue("airForceTicks", airForceTicksNode.Value); + + if (nodes.TryGetValue("navyForceTicks", out var navyForceTicksNode)) + navyForceTicks = FieldLoader.GetValue("navyForceTicks", navyForceTicksNode.Value); + + if (nodes.TryGetValue("groundForceTicks", out var groundForceTicksNode)) + groundForceTicks = FieldLoader.GetValue("groundForceTicks", groundForceTicksNode.Value); if (nodes.TryGetValue("MinAttackForceDelayTicks", out var minAttackForceDelayTicksNode)) minAttackForceDelayTicks = FieldLoader.GetValue("MinAttackForceDelayTicks", minAttackForceDelayTicksNode.Value); diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs index 7afe09e31b93..45615130aec0 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs @@ -20,44 +20,37 @@ public enum SquadType { Assault, Air, Rush, Protection, Naval } public class Squad { - public HashSet Units = new(); + public List Units = new(); public SquadType Type; internal IBot Bot; internal World World; internal SquadManagerBotModule SquadManager; internal MersenneTwister Random; - internal StateMachine FuzzyStateMachine; - - /// - /// Target location to attack. This will be either the targeted actor, - /// or a position close to that actor sufficient to get within weapons range. - /// - internal Target Target { get; set; } - /// - /// Actor that is targeted, for any actor based checks. Use for a targeting location. - /// - internal Actor TargetActor; + internal Target Target; + internal StateMachine FuzzyStateMachine; + internal CPos BaseLocation; public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type) - : this(bot, squadManager, type, default) { } + : this(bot, squadManager, type, null) { } - public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type, (Actor Actor, WVec Offset) target) + public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type, Actor target) { Bot = bot; SquadManager = squadManager; World = bot.Player.PlayerActor.World; Random = World.LocalRandom; Type = type; - SetActorToTarget(target); + Target = Target.FromActor(target); FuzzyStateMachine = new StateMachine(); switch (type) { case SquadType.Assault: + FuzzyStateMachine.ChangeState(this, new GuerrillaUnitsIdleState(), true); + break; case SquadType.Rush: - case SquadType.Naval: FuzzyStateMachine.ChangeState(this, new GroundUnitsIdleState(), true); break; case SquadType.Air: @@ -66,6 +59,9 @@ public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type, (Acto case SquadType.Protection: FuzzyStateMachine.ChangeState(this, new UnitsForProtectionIdleState(), true); break; + case SquadType.Naval: + FuzzyStateMachine.ChangeState(this, new NavyUnitsIdleState(), true); + break; } } @@ -77,75 +73,27 @@ public void Update() public bool IsValid => Units.Count > 0; - public void SetActorToTarget((Actor Actor, WVec Offset) target) - { - TargetActor = target.Actor; - if (TargetActor == null) - { - Target = Target.Invalid; - return; - } - - if (target.Offset == WVec.Zero) - Target = Target.FromActor(TargetActor); - else - Target = Target.FromPos(TargetActor.CenterPosition + target.Offset); - } - - /// - /// Checks the target is still valid, and updates the location if it is still valid. - /// - public bool IsTargetValid(Actor squadUnit) + public Actor TargetActor { - var valid = - TargetActor != null && - TargetActor.IsInWorld && - Units.Any(Target.IsValidFor) && - !TargetActor.Info.HasTraitInfo(); - if (!valid) - return false; - - // Refresh the target location. - // If the actor moved out of reach then we'll mark it invalid. - // e.g. a ship targeting a land unit that moves inland out of weapons range. - // or the target crossed a bridge which is then destroyed. - // If it is still in range but we have to target a nearby location, we can update that location. - // e.g. a ship targeting a land unit, but the land unit moved north. - // We need to update our location to move north as well. - // If we can reach the actor directly, we'll just target it directly. - var target = SquadManager.FindEnemies(new[] { TargetActor }, squadUnit).FirstOrDefault(); - SetActorToTarget(target); - return target.Actor != null; + get => Target.Actor; + set => Target = Target.FromActor(value); } - public bool IsTargetVisible => - TargetActor != null && - TargetActor.CanBeViewedByPlayer(Bot.Player); + public bool IsTargetValid => Target.IsValidFor(Units.FirstOrDefault().Actor) && !Target.Actor.Info.HasTraitInfo(); - public WPos CenterPosition() - { - return Units.Select(a => a.CenterPosition).Average(); - } + public bool IsTargetVisible => TargetActor.CanBeViewedByPlayer(Bot.Player); - public Actor CenterUnit() - { - var centerPosition = CenterPosition(); - return Units.MinByOrDefault(a => (a.CenterPosition - centerPosition).LengthSquared); - } + public WPos CenterPosition { get { return Units.First().Actor.CenterPosition; } } public MiniYaml Serialize() { var nodes = new List() { new MiniYamlNode("Type", FieldSaver.FormatValue(Type)), - new MiniYamlNode("Units", FieldSaver.FormatValue(Units.Select(a => a.ActorID).ToArray())) + new MiniYamlNode("Units", FieldSaver.FormatValue(Units.Where(a => !SquadManager.UnitCannotBeOrdered(a.Actor)).Select(a => a.Actor.ActorID).ToArray())), }; - - if (Target != Target.Invalid) - { - nodes.Add(new MiniYamlNode("ActorToTarget", FieldSaver.FormatValue(TargetActor.ActorID))); - nodes.Add(new MiniYamlNode("TargetOffset", FieldSaver.FormatValue(Target.CenterPosition - TargetActor.CenterPosition))); - } + if (Target.Type == TargetType.Actor) + nodes.Add(new MiniYamlNode("Target", FieldSaver.FormatValue(Target.Actor.ActorID))); return new MiniYaml("", nodes); } @@ -153,27 +101,27 @@ public MiniYaml Serialize() public static Squad Deserialize(IBot bot, SquadManagerBotModule squadManager, MiniYaml yaml) { var type = SquadType.Rush; - var target = ((Actor)null, WVec.Zero); + Actor targetActor = null; var typeNode = yaml.NodeWithKeyOrDefault("Type"); if (typeNode != null) type = FieldLoader.GetValue("Type", typeNode.Value.Value); - var actorToTargetNode = yaml.NodeWithKeyOrDefault("ActorToTarget"); - var targetOffsetNode = yaml.NodeWithKeyOrDefault("TargetOffset"); - if (actorToTargetNode != null && targetOffsetNode != null) - { - var actorToTarget = squadManager.World.GetActorById(FieldLoader.GetValue("ActorToTarget", actorToTargetNode.Value.Value)); - var targetOffset = FieldLoader.GetValue("TargetOffset", targetOffsetNode.Value.Value); - target = (actorToTarget, targetOffset); - } + var targetNode = yaml.NodeWithKeyOrDefault("Target"); + if (targetNode != null) + targetActor = squadManager.World.GetActorById(FieldLoader.GetValue("Target", targetNode.Value.Value)); - var squad = new Squad(bot, squadManager, type, target); + var squad = new Squad(bot, squadManager, type, targetActor); var unitsNode = yaml.NodeWithKeyOrDefault("Units"); if (unitsNode != null) - squad.Units.UnionWith(FieldLoader.GetValue("Units", unitsNode.Value.Value) - .Select(a => squadManager.World.GetActorById(a))); + { + foreach (var a in FieldLoader.GetValue("Units", unitsNode.Value.Value) + .Select(a => squadManager.World.GetActorById(a))) + { + squad.Units.Add(new UnitWposWrapper(a)); + } + } return squad; } diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/StateMachine.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/StateMachine.cs index 8c40f0995eb8..c755e8f80ed7 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/StateMachine.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/StateMachine.cs @@ -38,6 +38,11 @@ public void RevertToPreviousState(Squad squad, bool saveCurrentState) { ChangeState(squad, previousState, saveCurrentState); } + + public bool HasPreviousState() + { + return previousState != null; + } } interface IState diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs index a17668366d43..c251720b0c32 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs @@ -17,8 +17,6 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads { abstract class AirStateBase : StateBase { - protected const int MissileUnitMultiplier = 3; - protected static int CountAntiAirUnits(Squad owner, IReadOnlyCollection units) { if (units.Count == 0) @@ -27,7 +25,7 @@ protected static int CountAntiAirUnits(Squad owner, IReadOnlyCollection u var missileUnitsCount = 0; foreach (var unit in units) { - if (unit == null || unit.Info.HasTraitInfo()) + if (unit == null) continue; foreach (var ab in unit.TraitsImplementing()) @@ -39,7 +37,10 @@ protected static int CountAntiAirUnits(Squad owner, IReadOnlyCollection u { if (a.Weapon.IsValidTarget(owner.SquadManager.Info.AircraftTargetType)) { - missileUnitsCount++; + if (unit.Info.HasTraitInfo()) + missileUnitsCount += 1; + else + missileUnitsCount += 3; break; } } @@ -49,38 +50,6 @@ protected static int CountAntiAirUnits(Squad owner, IReadOnlyCollection u return missileUnitsCount; } - protected static Actor FindDefenselessTarget(Squad owner) - { - FindSafePlace(owner, out var target, true); - return target; - } - - protected static CPos? FindSafePlace(Squad owner, out Actor detectedEnemyTarget, bool needTarget) - { - var map = owner.World.Map; - var dangerRadius = owner.SquadManager.Info.DangerScanRadius; - detectedEnemyTarget = null; - - var columnCount = (map.MapSize.X + dangerRadius - 1) / dangerRadius; - var rowCount = (map.MapSize.Y + dangerRadius - 1) / dangerRadius; - - var checkIndices = Exts.MakeArray(columnCount * rowCount, i => i).Shuffle(owner.World.LocalRandom); - foreach (var i in checkIndices) - { - var pos = new MPos(i % columnCount * dangerRadius + dangerRadius / 2, i / columnCount * dangerRadius + dangerRadius / 2).ToCPos(map); - - if (NearToPosSafely(owner, map.CenterOfCell(pos), out detectedEnemyTarget)) - { - if (needTarget && detectedEnemyTarget == null) - continue; - - return pos; - } - } - - return null; - } - protected static bool NearToPosSafely(Squad owner, WPos loc) { return NearToPosSafely(owner, loc, out _); @@ -89,6 +58,7 @@ protected static bool NearToPosSafely(Squad owner, WPos loc) protected static bool NearToPosSafely(Squad owner, WPos loc, out Actor detectedEnemyTarget) { detectedEnemyTarget = null; + var dangerRadius = owner.SquadManager.Info.DangerScanRadius; var unitsAroundPos = owner.World.FindActorsInCircle(loc, WDist.FromCells(dangerRadius)) .Where(owner.SquadManager.IsPreferredEnemyUnit).ToList(); @@ -96,7 +66,7 @@ protected static bool NearToPosSafely(Squad owner, WPos loc, out Actor detectedE if (unitsAroundPos.Count == 0) return true; - if (CountAntiAirUnits(owner, unitsAroundPos) * MissileUnitMultiplier < owner.Units.Count) + if (CountAntiAirUnits(owner, unitsAroundPos) < owner.Units.Count) { detectedEnemyTarget = unitsAroundPos.Random(owner.Random); return true; @@ -106,32 +76,79 @@ protected static bool NearToPosSafely(Squad owner, WPos loc, out Actor detectedE } // Checks the number of anti air enemies around units - protected virtual bool ShouldFlee(Squad owner) + protected virtual bool ShouldFlee(Squad owner, Actor leader) { - return ShouldFlee(owner, enemies => CountAntiAirUnits(owner, enemies) * MissileUnitMultiplier > owner.Units.Count); + return ShouldFlee(owner, enemies => CountAntiAirUnits(owner, enemies) > owner.Units.Count); } } sealed class AirIdleState : AirStateBase, IState { - public void Activate(Squad owner) { } + const int MaxCheckTimesPerTick = 2; + Map map; + int dangerRadius; + int columnCount; + int rowCount; + + int[] airStrikeCheckIndices = null; + int checkedIndex = 0; + + public void Activate(Squad owner) + { + dangerRadius = owner.SquadManager.Info.DangerScanRadius; + map = owner.World.Map; + columnCount = (map.MapSize.X + dangerRadius - 1) / dangerRadius; + rowCount = (map.MapSize.Y + dangerRadius - 1) / dangerRadius; + airStrikeCheckIndices ??= Exts.MakeArray(columnCount * rowCount, i => i).Shuffle(owner.World.LocalRandom).ToArray(); + } + + Actor FindDefenselessTarget(Squad owner) + { + for (var checktime = 0; checktime <= MaxCheckTimesPerTick; checkedIndex++, checktime++) + { + if (checkedIndex >= airStrikeCheckIndices.Length) + checkedIndex = 0; + + var pos = new MPos(airStrikeCheckIndices[checkedIndex] % columnCount * dangerRadius + dangerRadius / 2, airStrikeCheckIndices[checkedIndex] / columnCount * dangerRadius + dangerRadius / 2).ToCPos(map); + + if (NearToPosSafely(owner, map.CenterOfCell(pos), out var detectedEnemyTarget)) + { + if (detectedEnemyTarget == null) + continue; + + checkedIndex = owner.World.LocalRandom.Next(airStrikeCheckIndices.Length); + return detectedEnemyTarget; + } + } + + return null; + } public void Tick(Squad owner) { if (!owner.IsValid) return; - if (ShouldFlee(owner)) + if (ShouldFlee(owner, owner.Units.First().Actor)) { owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), true); return; } - var e = FindDefenselessTarget(owner); - if (e == null) + if (!owner.IsTargetValid) + { + var e = owner.SquadManager.PopAirStrikeTarget(); + e ??= FindDefenselessTarget(owner); + + owner.TargetActor = e; + } + + if (!owner.IsTargetValid) + { + Retreat(owner, flee: false, rearm: true, repair: true); return; + } - owner.SetActorToTarget((e, WVec.Zero)); owner.FuzzyStateMachine.ChangeState(owner, new AirAttackState(), true); } @@ -147,45 +164,82 @@ public void Tick(Squad owner) if (!owner.IsValid) return; - var leader = owner.CenterUnit(); - if (!owner.IsTargetValid(leader)) + if (!owner.IsTargetValid) { - var closestEnemy = owner.SquadManager.FindClosestEnemy(leader); - owner.SetActorToTarget(closestEnemy); - if (closestEnemy.Actor == null) + var u = owner.Units.Random(owner.Random); + var closestEnemy = owner.SquadManager.FindClosestEnemy(u.Actor); + if (closestEnemy != null) + owner.TargetActor = closestEnemy; + else { - owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), true); + owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), false); return; } } - if (!NearToPosSafely(owner, owner.Target.CenterPosition)) + var leader = owner.Units.Select(u => u.Actor).ClosestToIgnoringPath(owner.TargetActor.CenterPosition); + + var unitsAroundPos = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.SquadManager.Info.DangerScanRadius)) + .Where(a => owner.SquadManager.IsPreferredEnemyUnit(a) && owner.SquadManager.IsNotHiddenUnit(a)).ToList(); + + // Check if get ambushed. + if (CountAntiAirUnits(owner, unitsAroundPos) > owner.Units.Count) { - owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), true); + owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), false); return; } - foreach (var a in owner.Units) + var cannotRetaliate = true; + var resupplyingUnits = new List(); + var backingoffUnits = new List(); + var attackingUnits = new List(); + foreach (var u in owner.Units) { - if (BusyAttack(a)) + if (IsAttackingAndTryAttack(u.Actor).TryAttacking) + { + cannotRetaliate = false; continue; + } - var ammoPools = a.TraitsImplementing().ToArray(); - if (!ReloadsAutomatically(ammoPools, a.TraitOrDefault())) + var ammoPools = u.Actor.TraitsImplementing().ToArray(); + if (!ReloadsAutomatically(ammoPools, u.Actor.TraitOrDefault())) { - if (IsRearming(a)) + if (IsRearming(u.Actor)) continue; if (!HasAmmo(ammoPools)) { - owner.Bot.QueueOrder(new Order("ReturnToBase", a, false)); + resupplyingUnits.Add(u.Actor); + continue; + } + } + + if (CanAttackTarget(u.Actor, owner.TargetActor)) + { + cannotRetaliate = false; + attackingUnits.Add(u.Actor); + } + else + { + if (!FullAmmo(ammoPools)) + { + resupplyingUnits.Add(u.Actor); continue; } + + backingoffUnits.Add(u.Actor); } + } - if (CanAttackTarget(a, owner.TargetActor)) - owner.Bot.QueueOrder(new Order("Attack", a, owner.Target, false)); + if (cannotRetaliate) + { + owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), false); + return; } + + owner.Bot.QueueOrder(new Order("ReturnToBase", null, false, groupedActors: resupplyingUnits.ToArray())); + owner.Bot.QueueOrder(new Order("Attack", null, Target.FromActor(owner.TargetActor), false, groupedActors: attackingUnits.ToArray())); + owner.Bot.QueueOrder(new Order("Move", null, Target.FromCell(owner.World, RandomBuildingLocation(owner)), false, groupedActors: backingoffUnits.ToArray())); } public void Deactivate(Squad owner) { } @@ -197,25 +251,17 @@ public void Activate(Squad owner) { } public void Tick(Squad owner) { + owner.TargetActor = null; + if (!owner.IsValid) return; - foreach (var a in owner.Units) - { - var ammoPools = a.TraitsImplementing().ToArray(); - if (!ReloadsAutomatically(ammoPools, a.TraitOrDefault()) && !FullAmmo(ammoPools)) - { - if (IsRearming(a)) - continue; - - owner.Bot.QueueOrder(new Order("ReturnToBase", a, false)); - continue; - } - - owner.Bot.QueueOrder(new Order("Move", a, Target.FromCell(owner.World, RandomBuildingLocation(owner)), false)); - } + Retreat(owner, flee: true, rearm: true, repair: true); - owner.FuzzyStateMachine.ChangeState(owner, new AirIdleState(), true); + if (owner.FuzzyStateMachine.HasPreviousState()) + owner.FuzzyStateMachine.RevertToPreviousState(owner, false); + else + owner.FuzzyStateMachine.ChangeState(owner, new AirIdleState(), false); } public void Deactivate(Squad owner) { } diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs index 9f72c58f7cb6..f3356b7c3843 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs @@ -15,71 +15,12 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads { - abstract class GroundStateBase : StateBase - { - Actor leader; - - /// - /// Elects a unit to lead the squad, other units in the squad will regroup to the leader if they start to spread out. - /// The leader remains the same unless a new one is forced or the leader is no longer part of the squad. - /// - protected Actor Leader(Squad owner) - { - if (leader == null || !owner.Units.Contains(leader)) - leader = NewLeader(owner); - return leader; - } - - static Actor NewLeader(Squad owner) - { - IEnumerable units = owner.Units; - - // Identify the Locomotor with the most restrictive passable terrain list. For squads with mixed - // locomotors, we hope to choose the most restrictive option. This means we won't nominate a leader who has - // more options. This avoids situations where we would nominate a hovercraft as the leader and tanks would - // fail to follow it because they can't go over water. By forcing us to choose a unit with limited movement - // options, we maximise the chance other units will be able to follow it. We could still be screwed if the - // squad has a mix of units with disparate movement, e.g. land units and naval units. We must trust the - // squad has been formed from a set of units that don't suffer this problem. - var leastCommonDenominator = units - .Select(a => a.TraitOrDefault()?.Locomotor) - .Where(l => l != null) - .MinByOrDefault(l => l.Info.TerrainSpeeds.Count) - ?.Info.TerrainSpeeds.Count; - if (leastCommonDenominator != null) - units = units.Where(a => a.TraitOrDefault()?.Locomotor.Info.TerrainSpeeds.Count == leastCommonDenominator).ToList(); - - // Choosing a unit in the center reduces the need for an immediate regroup. - var centerPosition = units.Select(a => a.CenterPosition).Average(); - return units.MinBy(a => (a.CenterPosition - centerPosition).LengthSquared); - } - - protected virtual bool ShouldFlee(Squad owner) - { - return ShouldFlee(owner, enemies => !AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemies)); - } - - protected (Actor Actor, WVec Offset) NewLeaderAndFindClosestEnemy(Squad owner) - { - leader = null; // Force a new leader to be elected, useful if we are targeting a new enemy. - return owner.SquadManager.FindClosestEnemy(Leader(owner)); - } - - protected IEnumerable<(Actor Actor, WVec Offset)> FindEnemies(Squad owner, IEnumerable actors) - { - return owner.SquadManager.FindEnemies( - actors, - Leader(owner)); - } - - protected static Actor ClosestToEnemy(Squad owner) - { - return SquadManagerBotModule.ClosestTo(owner.Units, owner.TargetActor); - } - } + abstract class GroundStateBase : StateBase { } sealed class GroundUnitsIdleState : GroundStateBase, IState { + Actor leader; + public void Activate(Squad owner) { } public void Tick(Squad owner) @@ -87,109 +28,245 @@ public void Tick(Squad owner) if (!owner.IsValid) return; - if (!owner.IsTargetValid(Leader(owner))) + if (owner.SquadManager.UnitCannotBeOrdered(leader)) + leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedGroundLeaderLocomotor).Actor; + + if (!owner.IsTargetValid) { - var closestEnemy = NewLeaderAndFindClosestEnemy(owner); - owner.SetActorToTarget(closestEnemy); - if (closestEnemy.Actor == null) + var closestEnemy = owner.SquadManager.FindClosestEnemy(leader); + if (closestEnemy == null) return; + + owner.TargetActor = closestEnemy; } - var enemyUnits = - FindEnemies(owner, - owner.World.FindActorsInCircle(owner.Target.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius))) - .Select(x => x.Actor) - .ToList(); + var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius)) + .Where(owner.SquadManager.IsPreferredEnemyUnit).ToList(); if (enemyUnits.Count == 0) - return; - - if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemyUnits)) { - owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray())); - - // We have gathered sufficient units. Attack the nearest enemy unit. - owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackMoveState(), true); + Retreat(owner, flee: false, rearm: true, repair: true); + return; } + + if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units.Select(u => u.Actor).ToList(), enemyUnits)) + owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackMoveState(), false); else - owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); + Retreat(owner, flee: true, rearm: true, repair: true); } public void Deactivate(Squad owner) { } } + // This version AI forcus on solving pathfinding problem for AI + // 1. use a leader to guide the entire squad to target, solve stuck on twisted road and saving performance on pathfinding + // 2. have two methods to solve entire squad stuck. First, try make way for leader. Second, kick stuck units sealed class GroundUnitsAttackMoveState : GroundStateBase, IState { - int lastUpdatedTick; - CPos? lastLeaderLocation; - Actor lastTarget; + const int MaxMakeWayPossibility = 4; + const int MaxSquadStuckPossibility = 6; + const int MakeWayTicks = 3; + const int KickStuckTicks = 4; + + // Give tolerance for AI grouping team at start + int shouldMakeWayPossibility = -(MaxMakeWayPossibility * 6); + int shouldKickStuckPossibility = -(MaxSquadStuckPossibility * 6); + int makeWay = 0; + int kickStuck = 0; + + UnitWposWrapper leader = new(null); public void Activate(Squad owner) { } public void Tick(Squad owner) { + // Basic check if (!owner.IsValid) return; - if (!owner.IsTargetValid(Leader(owner))) + // Initialize leader. Optimize pathfinding by using a leader with specific locomotor. + if (owner.SquadManager.UnitCannotBeOrdered(leader.Actor)) + leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedGroundLeaderLocomotor); + + if (!owner.IsTargetValid || !CheckReachability(leader.Actor, owner.TargetActor)) { - var closestEnemy = NewLeaderAndFindClosestEnemy(owner); - owner.SetActorToTarget(closestEnemy); - if (closestEnemy.Actor == null) + var targetActor = owner.SquadManager.FindClosestEnemy(leader.Actor); + if (targetActor != null) + owner.TargetActor = targetActor; + else { - owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); + owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), false); return; } } - var leader = Leader(owner); - if (leader.Location != lastLeaderLocation) + // Switch to "GroundUnitsAttackState" if we encounter enemy units. + var attackScanRadius = WDist.FromCells(owner.SquadManager.Info.AttackScanRadius); + + var enemyActor = owner.SquadManager.FindClosestEnemy(leader.Actor, attackScanRadius); + if (enemyActor != null) { - lastLeaderLocation = leader.Location; - lastUpdatedTick = owner.World.WorldTick; + owner.TargetActor = enemyActor; + owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackState(), false); + return; } - if (owner.TargetActor != lastTarget) + // Since units have different movement speeds, they get separated while approaching the target. + // Let them regroup into tighter formation towards "leader". + // + // "occupiedArea" means the space the squad units will occupy (if 1 per Cell). + // leader only stop when scope of "lookAround" is not covered all units; + // units in "unitsHurryUp" will catch up, which keep the team tight while not stuck. + // + // Imagining "occupiedArea" takes up a a place shape like square, + // we need to draw a circle to cover the the enitire circle. + var occupiedArea = (long)WDist.FromCells(owner.Units.Count).Length * 1024; + + // Kick stuck units: Kick stuck units that is blocked + if (kickStuck > 0) { - lastTarget = owner.TargetActor; - lastUpdatedTick = owner.World.WorldTick; + var stopUnits = new List(); + var otherUnits = new List(); + + // Check if it is the leader stuck + if (leader.Actor.CenterPosition == leader.WPos && !IsAttackingAndTryAttack(leader.Actor).IsFiring) + { + stopUnits.Add(leader.Actor); + owner.Units.Remove(leader); + AIUtils.BotDebug("AI ({0}): Kick leader from squad.", owner.Bot.Player.ClientIndex); + } + + // Check if it is the units stuck + else + { + for (var i = 0; i < owner.Units.Count; i++) + { + var u = owner.Units[i]; + + if (u.Actor == leader.Actor) + continue; + + // If unit that is not in valid distance from leader nor firing at enemy, + // we will check if it can reach the leader, or stuck due to unknow reason + if ((u.Actor.CenterPosition - leader.Actor.CenterPosition).HorizontalLengthSquared >= 5 * occupiedArea + && (u.Actor.CenterPosition == u.WPos + || !AIUtils.PathExist(u.Actor, leader.Actor.Location, leader.Actor))) + { + stopUnits.Add(u.Actor); + owner.Units.RemoveAt(i); + i--; + } + else + { + u.WPos = u.Actor.CenterPosition; + otherUnits.Add(u.Actor); + } + } + + if (stopUnits.Count > 0) + AIUtils.BotDebug("AI ({0}): Kick ({1}) from squad.", owner.Bot.Player.ClientIndex, stopUnits.Count); + } + + if (owner.Units.Count == 0) + return; + + if (kickStuck > 1) + { + leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedGroundLeaderLocomotor); + leader.WPos = leader.Actor.CenterPosition; + owner.Bot.QueueOrder(new Order("AttackMove", leader.Actor, Target.FromCell(owner.World, owner.TargetActor.Location), false)); + owner.Bot.QueueOrder(new Order("Stop", null, false, groupedActors: stopUnits.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Actor.Location), false, groupedActors: otherUnits.ToArray())); + kickStuck--; + } + else if (kickStuck == 1) + { + shouldMakeWayPossibility = 0; + shouldKickStuckPossibility = 0; + leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedGroundLeaderLocomotor); + + // The end of "kickStuck": stop the leader for position record next tick + owner.Bot.QueueOrder(new Order("Stop", leader.Actor, false)); + kickStuck = 0; + } + + return; } - // HACK: Drop back to the idle state if we haven't moved in 2.5 seconds - // This works around the squad being stuck trying to attack-move to a location - // that they cannot path to, generating expensive pathfinding calls each tick. - if (owner.World.WorldTick > lastUpdatedTick + 63) + // Make way for leader: Make sure the guide unit has not been blocked by the rest of the squad. + if (makeWay > 0) { - owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState(), true); + if (makeWay > 1) + { + var others = owner.Units.Where(u => u.Actor != leader.Actor).Select(u => u.Actor); + owner.Bot.QueueOrder(new Order("Scatter", null, false, groupedActors: others.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", leader.Actor, Target.FromCell(owner.World, owner.TargetActor.Location), false)); + makeWay--; + } + else if (makeWay == 1) + { + shouldMakeWayPossibility = 0; + shouldKickStuckPossibility = MaxSquadStuckPossibility / 2; + + // The end of "makeWay": stop the leader for position record next tick + // set "makeWay" to -1 to inform that squad just make way for leader + owner.Bot.QueueOrder(new Order("Stop", leader.Actor, false)); + makeWay = -1; + } + return; } - var ownUnits = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.Units.Count) / 3) - .Where(owner.Units.Contains).ToHashSet(); - - if (ownUnits.Count < owner.Units.Count) + // "leaderStopCheck" to see if leader move. + // "leaderWaitCheck" to see if leader should wait squad members that left behind. + var leaderStopCheck = leader.Actor.CenterPosition == leader.WPos; + var leaderWaitCheck = owner.Units.Any(u => (u.Actor.CenterPosition - leader.Actor.CenterPosition).HorizontalLengthSquared > occupiedArea * 5); + + // To find out the stuck problem of the squad and deal with it. + // 1. If leader cannot move and leader should wait, there may be squad members stuck. + // 2. If leader cannot move but leader should go, leader is stuck. + // -- Try make way for leader + // -- If make way cannot solve this problem, we kick stuck unit + // 3. If leader can move and leader should go, we consider this squad has no problem on stuck. + if (leaderStopCheck && leaderWaitCheck) + shouldKickStuckPossibility++; + else if (leaderStopCheck && !leaderWaitCheck) { - // Since units have different movement speeds, they get separated while approaching the target. - // Let them regroup into tighter formation. - owner.Bot.QueueOrder(new Order("Stop", leader, false)); + if (makeWay != -1) + shouldMakeWayPossibility++; + else + shouldKickStuckPossibility++; + } + else if (!leaderStopCheck && !leaderWaitCheck) + { + shouldMakeWayPossibility = 0; + shouldKickStuckPossibility = 0; + } - var units = owner.Units.Where(a => !ownUnits.Contains(a)).ToArray(); - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Location), false, groupedActors: units)); + // Check if we need to make way for leader or kick stuck units + if (shouldMakeWayPossibility >= MaxMakeWayPossibility) + { + AIUtils.BotDebug("AI ({0}): Make way for squad leader.", owner.Bot.Player.ClientIndex); + makeWay = MakeWayTicks; } - else + else if (shouldKickStuckPossibility >= MaxSquadStuckPossibility) { - var target = owner.SquadManager.FindClosestEnemy(leader, WDist.FromCells(owner.SquadManager.Info.AttackScanRadius)); - if (target.Actor != null) - { - owner.SetActorToTarget(target); - owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackState(), true); - } - else - owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray())); + AIUtils.BotDebug("AI ({0}): Kick stuck units from squad.", owner.Bot.Player.ClientIndex); + kickStuck = KickStuckTicks; } - if (ShouldFlee(owner)) - owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); + // Record current position of the squad leader + leader.WPos = leader.Actor.CenterPosition; + + // Leader will wait squad members that left behind, unless + // next tick is kick stuck unit (we need leader move in advance). + if (leaderWaitCheck && kickStuck <= 0) + owner.Bot.QueueOrder(new Order("Stop", leader.Actor, false)); + else + owner.Bot.QueueOrder(new Order("AttackMove", leader.Actor, Target.FromCell(owner.World, owner.TargetActor.Location), false)); + + var unitsHurryUp = owner.Units.Where(u => (u.Actor.CenterPosition - leader.Actor.CenterPosition).HorizontalLengthSquared >= occupiedArea * 2).Select(u => u.Actor).ToArray(); + owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Actor.Location), false, groupedActors: unitsHurryUp)); } public void Deactivate(Squad owner) { } @@ -197,56 +274,113 @@ public void Deactivate(Squad owner) { } sealed class GroundUnitsAttackState : GroundStateBase, IState { - int lastUpdatedTick; - CPos? lastLeaderLocation; - Actor lastTarget; + // Use it to find if entire squad cannot reach the attack position + int tryAttackTick; - public void Activate(Squad owner) { } + Actor leader; + int tryAttack = 0; + + public void Activate(Squad owner) + { + tryAttackTick = owner.SquadManager.Info.AttackScanRadius; + } public void Tick(Squad owner) { + // Basic check if (!owner.IsValid) return; - if (!owner.IsTargetValid(Leader(owner))) + if (owner.SquadManager.UnitCannotBeOrdered(leader)) + leader = owner.Units.First().Actor; + + owner.SquadManager.SetAirStrikeTarget(owner.TargetActor); + var isDefaultLeader = true; + + // Rescan target to prevent being ambushed and die without fight + // If there is no threat around, return to AttackMove state for formation + var attackScanRadius = WDist.FromCells(owner.SquadManager.Info.AttackScanRadius); + var closestEnemy = owner.SquadManager.FindClosestEnemy(leader, attackScanRadius); + + // Becuase MoveWithinRange can cause huge lag when stuck + // we only allow free attack behaivour within TryAttackTick + // then the squad will gather to a certain leader + if (closestEnemy == null) { - var closestEnemy = NewLeaderAndFindClosestEnemy(owner); - owner.SetActorToTarget(closestEnemy); - if (closestEnemy.Actor == null) - { - owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); - return; - } + owner.TargetActor = owner.SquadManager.FindClosestEnemy(leader); + owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackMoveState(), false); + return; } - - var leader = Leader(owner); - if (leader.Location != lastLeaderLocation) + else if (owner.TargetActor != closestEnemy) { - lastLeaderLocation = leader.Location; - lastUpdatedTick = owner.World.WorldTick; + // Refresh tryAttack when target switched + tryAttack = 0; + owner.TargetActor = closestEnemy; } - if (owner.TargetActor != lastTarget) + var cannotRetaliate = true; + var followingUnits = new List(); + var attackingUnits = new List(); + + foreach (var u in owner.Units) { - lastTarget = owner.TargetActor; - lastUpdatedTick = owner.World.WorldTick; + var (isFiring, tryAttacking) = IsAttackingAndTryAttack(u.Actor); + + if ((tryAttacking || isFiring) && + (u.Actor.CenterPosition - owner.TargetActor.CenterPosition).HorizontalLengthSquared < + (leader.CenterPosition - owner.TargetActor.CenterPosition).HorizontalLengthSquared) + { + isDefaultLeader = false; + leader = u.Actor; + } + + if (isFiring && tryAttack != 0) + { + // Make there is at least one follow and attack target, AFTER first trying on attack + if (isDefaultLeader) + { + leader = u.Actor; + isDefaultLeader = false; + } + + cannotRetaliate = false; + } + else if (CanAttackTarget(u.Actor, owner.TargetActor)) + { + if (tryAttack > tryAttackTick && tryAttacking) + { + // Make there is at least one follow and attack target even when approach max tryAttackTick + if (isDefaultLeader) + { + leader = u.Actor; + isDefaultLeader = false; + attackingUnits.Add(u.Actor); + continue; + } + + followingUnits.Add(u.Actor); + continue; + } + + attackingUnits.Add(u.Actor); + cannotRetaliate = false; + } + else + followingUnits.Add(u.Actor); } - // HACK: Drop back to the idle state if we haven't moved in 2.5 seconds - // This works around the squad being stuck trying to attack-move to a location - // that they cannot path to, generating expensive pathfinding calls each tick. - if (owner.World.WorldTick > lastUpdatedTick + 63) + // Because ShouldFlee(owner) cannot retreat units while they cannot even fight + // a unit that they cannot target. Therefore, use `cannotRetaliate` here to solve this bug. + if (ShouldFleeSimple(owner) || cannotRetaliate) { - owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState(), true); + owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), false); return; } - foreach (var a in owner.Units) - if (!BusyAttack(a)) - owner.Bot.QueueOrder(new Order("AttackMove", a, owner.Target, false)); + tryAttack++; - if (ShouldFlee(owner)) - owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); + owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Location), false, groupedActors: followingUnits.ToArray())); + owner.Bot.QueueOrder(new Order("Attack", null, Target.FromActor(owner.TargetActor), false, groupedActors: attackingUnits.ToArray())); } public void Deactivate(Squad owner) { } @@ -261,10 +395,10 @@ public void Tick(Squad owner) if (!owner.IsValid) return; - GoToRandomOwnBuilding(owner); - owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState(), true); + Retreat(owner, flee: true, rearm: true, repair: true); + owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState(), false); } - public void Deactivate(Squad owner) { owner.SquadManager.UnregisterSquad(owner); } + public void Deactivate(Squad owner) { owner.SquadManager.DismissSquad(owner); } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GuerrillaStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GuerrillaStates.cs new file mode 100644 index 000000000000..6717b4df76d5 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GuerrillaStates.cs @@ -0,0 +1,460 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits.BotModules.Squads +{ + abstract class GuerrillaStatesBase : GroundStateBase { } + + class GuerrillaUnitsIdleState : GuerrillaStatesBase, IState + { + Actor leader; + int squadsize; + + public void Activate(Squad owner) { } + + public void Tick(Squad owner) + { + if (!owner.IsValid) + return; + + if (owner.SquadManager.UnitCannotBeOrdered(leader) || squadsize != owner.Units.Count) + { + leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedGroundLeaderLocomotor).Actor; + squadsize = owner.Units.Count; + } + + if (!owner.IsTargetValid) + { + var closestEnemy = owner.SquadManager.FindClosestEnemy(leader); + if (closestEnemy == null) + return; + + owner.TargetActor = closestEnemy; + } + + var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius)) + .Where(owner.SquadManager.IsPreferredEnemyUnit).ToList(); + + if (enemyUnits.Count == 0) + { + Retreat(owner, false, true, true); + return; + } + + if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units.Select(u => u.Actor).ToList(), enemyUnits)) + { + // We have gathered sufficient units. Attack the nearest enemy unit. + owner.BaseLocation = RandomBuildingLocation(owner); + owner.FuzzyStateMachine.ChangeState(owner, new GuerrillaUnitsAttackMoveState(), false); + } + else + Retreat(owner, true, true, true); + } + + public void Deactivate(Squad owner) { } + } + + // See detailed comments at GroundStates.cs + // There is many in common + class GuerrillaUnitsAttackMoveState : GuerrillaStatesBase, IState + { + const int MaxMakeWayPossibility = 4; + const int MaxSquadStuckPossibility = 6; + const int MakeWayTicks = 3; + const int KickStuckTicks = 4; + + // Give tolerance for AI grouping team at start + int shouldMakeWayPossibility = -(MaxMakeWayPossibility * 6); + int shouldKickStuckPossibility = -(MaxSquadStuckPossibility * 6); + int makeWay = 0; + int kickStuck = 0; + + UnitWposWrapper leader = new(null); + int squadsize = 0; + + public void Activate(Squad owner) { } + + public void Tick(Squad owner) + { + // Basic check + if (!owner.IsValid) + return; + + // Initialize leader. Optimize pathfinding by using leader. + if (owner.SquadManager.UnitCannotBeOrdered(leader.Actor) || squadsize != owner.Units.Count) + { + leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedGroundLeaderLocomotor); + squadsize = owner.Units.Count; + } + + if (!owner.IsTargetValid || !CheckReachability(leader.Actor, owner.TargetActor)) + { + var targetActor = owner.SquadManager.FindClosestEnemy(leader.Actor); + if (targetActor != null) + owner.TargetActor = targetActor; + else + { + owner.FuzzyStateMachine.ChangeState(owner, new GuerrillaUnitsFleeState(), false); + return; + } + } + + // Switch to attack state if we encounter enemy units like ground squad + var attackScanRadius = WDist.FromCells(owner.SquadManager.Info.AttackScanRadius); + + var enemyActor = owner.SquadManager.FindClosestEnemy(leader.Actor, attackScanRadius); + if (enemyActor != null) + { + owner.TargetActor = enemyActor; + owner.FuzzyStateMachine.ChangeState(owner, new GuerrillaUnitsHitState(), false); + return; + } + + var occupiedArea = (long)WDist.FromCells(owner.Units.Count).Length * 1024; + + // Kick stuck units: Kick stuck units that is blocked + if (kickStuck > 0) + { + var stopUnits = new List(); + var otherUnits = new List(); + + // Check if it is the leader stuck + if (leader.Actor.CenterPosition == leader.WPos && !IsAttackingAndTryAttack(leader.Actor).IsFiring) + { + stopUnits.Add(leader.Actor); + owner.Units.Remove(leader); + AIUtils.BotDebug("AI ({0}): Kick leader from squad.", owner.Bot.Player.ClientIndex); + } + + // Check if it is the units stuck + else + { + for (var i = 0; i < owner.Units.Count; i++) + { + var u = owner.Units[i]; + + if (u.Actor == leader.Actor) + continue; + + // If unit that is not in valid distance from leader nor firing at enemy, + // we will check if it can reach the leader, or stuck due to unknow reason + if ((u.Actor.CenterPosition - leader.Actor.CenterPosition).HorizontalLengthSquared >= 5 * occupiedArea + && (u.Actor.CenterPosition == u.WPos + || !AIUtils.PathExist(u.Actor, leader.Actor.Location, leader.Actor))) + { + stopUnits.Add(u.Actor); + owner.Units.RemoveAt(i); + i--; + } + else + { + u.WPos = u.Actor.CenterPosition; + otherUnits.Add(u.Actor); + } + } + + if (stopUnits.Count > 0) + AIUtils.BotDebug("AI ({0}): Kick ({1}) from squad.", owner.Bot.Player.ClientIndex, stopUnits.Count); + } + + if (owner.Units.Count == 0) + return; + + if (kickStuck > 1) + { + leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedGroundLeaderLocomotor); + leader.WPos = leader.Actor.CenterPosition; + owner.Bot.QueueOrder(new Order("AttackMove", leader.Actor, Target.FromCell(owner.World, owner.TargetActor.Location), false)); + owner.Bot.QueueOrder(new Order("Stop", null, false, groupedActors: stopUnits.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Actor.Location), false, groupedActors: otherUnits.ToArray())); + kickStuck--; + } + else if (kickStuck == 1) + { + shouldMakeWayPossibility = 0; + shouldKickStuckPossibility = 0; + leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedGroundLeaderLocomotor); + + // The end of "kickStuck": stop the leader for position record next tick + owner.Bot.QueueOrder(new Order("Stop", leader.Actor, false)); + kickStuck = 0; + } + + return; + } + + // Make way for leader: Make sure the guide unit has not been blocked by the rest of the squad. + if (makeWay > 0) + { + if (makeWay > 1) + { + var others = owner.Units.Where(u => u.Actor != leader.Actor).Select(u => u.Actor); + owner.Bot.QueueOrder(new Order("Scatter", null, false, groupedActors: others.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", leader.Actor, Target.FromCell(owner.World, owner.TargetActor.Location), false)); + makeWay--; + } + else if (makeWay == 1) + { + shouldMakeWayPossibility = 0; + shouldKickStuckPossibility = MaxSquadStuckPossibility / 2; + + // The end of "makeWay": stop the leader for position record next tick + // set "makeWay" to -1 to inform that squad just make way for leader + owner.Bot.QueueOrder(new Order("Stop", leader.Actor, false)); + makeWay = -1; + } + + return; + } + + // "leaderStopCheck" to see if leader move. + // "leaderWaitCheck" to see if leader should wait squad members that left behind. + var leaderStopCheck = leader.Actor.CenterPosition == leader.WPos; + var leaderWaitCheck = owner.Units.Any(u => (u.Actor.CenterPosition - leader.Actor.CenterPosition).HorizontalLengthSquared > occupiedArea * 5); + + // To find out the stuck problem of the squad and deal with it. + // 1. If leader cannot move and leader should wait, there may be squad members stuck. + // 2. If leader cannot move but leader should go, leader is stuck. + // -- Try make way for leader + // -- If make way cannot solve this problem, we kick stuck unit + // 3. If leader can move and leader should go, we consider this squad has no problem on stuck. + if (leaderStopCheck && leaderWaitCheck) + shouldKickStuckPossibility++; + else if (leaderStopCheck && !leaderWaitCheck) + { + if (makeWay != -1) + shouldMakeWayPossibility++; + else + shouldKickStuckPossibility++; + } + else if (!leaderStopCheck && !leaderWaitCheck) + { + shouldMakeWayPossibility = 0; + shouldKickStuckPossibility = 0; + } + + // Check if we need to make way for leader or kick stuck units + if (shouldMakeWayPossibility >= MaxMakeWayPossibility) + { + AIUtils.BotDebug("AI ({0}): Make way for squad leader.", owner.Bot.Player.ClientIndex); + makeWay = MakeWayTicks; + } + else if (shouldKickStuckPossibility >= MaxSquadStuckPossibility) + { + AIUtils.BotDebug("AI ({0}): Kick stuck units from squad.", owner.Bot.Player.ClientIndex); + kickStuck = KickStuckTicks; + } + + // Record current position of the squad leader + leader.WPos = leader.Actor.CenterPosition; + + // Leader will wait squad members that left behind, unless + // next tick is kick stuck unit (we need leader move in advance). + if (leaderWaitCheck && kickStuck <= 0) + owner.Bot.QueueOrder(new Order("Stop", leader.Actor, false)); + else + owner.Bot.QueueOrder(new Order("AttackMove", leader.Actor, Target.FromCell(owner.World, owner.TargetActor.Location), false)); + + var unitsHurryUp = owner.Units.Where(u => (u.Actor.CenterPosition - leader.Actor.CenterPosition).HorizontalLengthSquared >= occupiedArea * 2).Select(u => u.Actor).ToArray(); + owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Actor.Location), false, groupedActors: unitsHurryUp)); + } + + public void Deactivate(Squad owner) { } + } + + // See detailed comments at GroundStates.cs + // There are many in common + class GuerrillaUnitsHitState : GuerrillaStatesBase, IState + { + // Use it to find if entire squad cannot reach the attack position + int tryAttackTick; + + Actor leader; + int tryAttack = 0; + bool isFirstTick = true; // Only record HP and do not retreat at first tick + int squadsize = 0; + + public void Activate(Squad owner) + { + tryAttackTick = owner.SquadManager.Info.AttackScanRadius; + } + + public void Tick(Squad owner) + { + // Basic check + if (!owner.IsValid) + return; + + if (owner.SquadManager.UnitCannotBeOrdered(leader)) + leader = owner.Units.FirstOrDefault().Actor; + + owner.SquadManager.SetAirStrikeTarget(owner.TargetActor); + var isDefaultLeader = true; + + // Rescan target to prevent being ambushed and die without fight + // If there is no threat around, return to AttackMove state for formation + var attackScanRadius = WDist.FromCells(owner.SquadManager.Info.AttackScanRadius); + var closestEnemy = owner.SquadManager.FindClosestEnemy(leader, attackScanRadius); + + var healthChange = false; + var cannotRetaliate = true; + var followingUnits = new List(); + var attackingUnits = new List(); + + if (closestEnemy == null) + { + owner.TargetActor = owner.SquadManager.FindClosestEnemy(leader); + owner.FuzzyStateMachine.ChangeState(owner, new GuerrillaUnitsAttackMoveState(), false); + return; + } + else + { + if (owner.TargetActor != closestEnemy) + { + // Refresh tryAttack when target switched + tryAttack = 0; + owner.TargetActor = closestEnemy; + } + + for (var i = 0; i < owner.Units.Count; i++) + { + var u = owner.Units[i]; + var (isFiring, tryAttacking) = IsAttackingAndTryAttack(u.Actor); + + var health = u.Actor.TraitOrDefault(); + + if (health != null) + { + var healthWPos = new WPos(0, 0, (int)health.DamageState); // HACK: use WPos.Z storage HP + if (u.WPos.Z != healthWPos.Z) + { + if (u.WPos.Z < healthWPos.Z) + healthChange = true; + u.WPos = healthWPos; + } + } + + if ((tryAttacking || isFiring) && + (u.Actor.CenterPosition - owner.TargetActor.CenterPosition).HorizontalLengthSquared < + (leader.CenterPosition - owner.TargetActor.CenterPosition).HorizontalLengthSquared) + { + isDefaultLeader = false; + leader = u.Actor; + } + + if (isFiring && tryAttack != 0) + { + // Make there is at least one follow and attack target, AFTER first trying on attack + if (isDefaultLeader) + { + leader = u.Actor; + isDefaultLeader = false; + } + + cannotRetaliate = false; + } + else if (CanAttackTarget(u.Actor, owner.TargetActor)) + { + if (tryAttack > tryAttackTick && tryAttacking) + { + // Make there is at least one follow and attack target even when approach max tryAttackTick + if (isDefaultLeader) + { + leader = u.Actor; + isDefaultLeader = false; + attackingUnits.Add(u.Actor); + continue; + } + + followingUnits.Add(u.Actor); + continue; + } + + attackingUnits.Add(u.Actor); + cannotRetaliate = false; + } + else + followingUnits.Add(u.Actor); + } + } + + // Because ShouldFlee(owner) cannot retreat units while they cannot even fight + // a unit that they cannot target. Therefore, use `cannotRetaliate` here to solve this bug. + if (cannotRetaliate) + owner.FuzzyStateMachine.ChangeState(owner, new GuerrillaUnitsFleeState(), true); + + tryAttack++; + + var unitlost = squadsize > owner.Units.Count; + squadsize = owner.Units.Count; + + if ((healthChange || unitlost) && !isFirstTick) + owner.FuzzyStateMachine.ChangeState(owner, new GuerrillaUnitsRunState(), true); + + owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Location), false, groupedActors: followingUnits.ToArray())); + owner.Bot.QueueOrder(new Order("Attack", null, Target.FromActor(owner.TargetActor), false, groupedActors: attackingUnits.ToArray())); + + isFirstTick = false; + } + + public void Deactivate(Squad owner) { } + } + + class GuerrillaUnitsRunState : GuerrillaStatesBase, IState + { + public const int HitTicks = 2; + internal int Hit = HitTicks; + bool ordered; + + public void Activate(Squad owner) { ordered = false; } + + public void Tick(Squad owner) + { + if (!owner.IsValid) + return; + + if (Hit-- <= 0) + { + Hit = HitTicks; + owner.FuzzyStateMachine.ChangeState(owner, new GuerrillaUnitsHitState(), true); + return; + } + + if (!ordered) + { + owner.Bot.QueueOrder(new Order("Move", null, Target.FromCell(owner.World, owner.BaseLocation), false, groupedActors: owner.Units.Select(u => u.Actor).ToArray())); + ordered = true; + } + } + + public void Deactivate(Squad owner) { } + } + + class GuerrillaUnitsFleeState : GuerrillaStatesBase, IState + { + public void Activate(Squad owner) { } + + public void Tick(Squad owner) + { + if (!owner.IsValid) + return; + + Retreat(owner, true, true, true); + owner.FuzzyStateMachine.ChangeState(owner, new GuerrillaUnitsIdleState(), false); + } + + public void Deactivate(Squad owner) { } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs new file mode 100644 index 000000000000..c5b346562cfb --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs @@ -0,0 +1,415 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits.BotModules.Squads +{ + abstract class NavyStateBase : StateBase + { + protected static Actor FindClosestEnemy(Squad owner, Actor sourceActor) + { + // Navy squad AI can exploit enemy naval production to find path, if any. + // (Way better than finding a nearest target which is likely to be on Ground) + // You might be tempted to move these lookups into Activate() but that causes null reference exception. + var mobile = sourceActor.Trait(); + + var navalProductions = owner.World.ActorsHavingTrait().Where(a + => owner.SquadManager.Info.NavalProductionTypes.Contains(a.Info.Name) + && mobile.PathFinder.PathExistsForLocomotor(mobile.Locomotor, sourceActor.Location, a.Location) + && a.AppearsHostileTo(sourceActor)); + + var nearest = navalProductions.ClosestToWithPathFrom(sourceActor); + if (nearest != null) + { + // Return nearest when it is FAR enough. + // If the naval production is within MaxBaseRadius, it implies that + // this squad is close to enemy territory and they should expect a naval combat; + // closest enemy makes more sense in that case. + if ((nearest.Location - sourceActor.Location).LengthSquared > owner.SquadManager.Info.MaxBaseRadius * owner.SquadManager.Info.MaxBaseRadius) + return nearest; + } + + return owner.SquadManager.FindClosestEnemy(sourceActor); + } + } + + sealed class NavyUnitsIdleState : NavyStateBase, IState + { + Actor leader; + + public void Activate(Squad owner) { } + + public void Tick(Squad owner) + { + if (!owner.IsValid) + return; + + leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedNavyLeaderLocomotor).Actor; + + if (!owner.IsTargetValid) + { + var closestEnemy = FindClosestEnemy(owner, leader); + if (closestEnemy == null) + return; + + owner.TargetActor = closestEnemy; + } + + var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius)) + .Where(owner.SquadManager.IsPreferredEnemyUnit).ToList(); + + if (enemyUnits.Count == 0) + { + Retreat(owner, flee: false, rearm: true, repair: true); + return; + } + + if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units.Select(u => u.Actor).ToList(), enemyUnits)) + { + // We have gathered sufficient units. Attack the nearest enemy unit. + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsAttackMoveState(), false); + } + else + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), false); + } + + public void Deactivate(Squad owner) { } + } + + // See detailed comments at GroundStates.cs + // There is many in common + sealed class NavyUnitsAttackMoveState : NavyStateBase, IState + { + const int MaxMakeWayPossibility = 4; + const int MaxSquadStuckPossibility = 6; + const int MakeWayTicks = 3; + const int KickStuckTicks = 4; + + // Give tolerance for AI grouping team at start + int shouldMakeWayPossibility = -(MaxMakeWayPossibility * 6); + int shouldKickStuckPossibility = -(MaxSquadStuckPossibility * 6); + int makeWay = 0; + int kickStuck = 0; + + public void Activate(Squad owner) { } + + public void Tick(Squad owner) + { + // Basic check + if (!owner.IsValid) + return; + + // Initialize leader. Optimize pathfinding by using leader. + var leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedNavyLeaderLocomotor); + + if (!owner.IsTargetValid || !CheckReachability(leader.Actor, owner.TargetActor)) + { + var targetActor = FindClosestEnemy(owner, leader.Actor); + if (targetActor != null) + owner.TargetActor = targetActor; + else + { + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), false); + return; + } + } + + // Switch to attack state if we encounter enemy units like ground squad + var attackScanRadius = WDist.FromCells(owner.SquadManager.Info.AttackScanRadius); + + var enemyActor = owner.SquadManager.FindClosestEnemy(leader.Actor, attackScanRadius); + if (enemyActor != null) + { + owner.TargetActor = enemyActor; + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsAttackState(), false); + return; + } + + var occupiedArea = (long)WDist.FromCells(owner.Units.Count).Length * 1024; + + // Try kick units in squad that cannot move at all. + if (kickStuck > 0) + { + var stopUnits = new List(); + var otherUnits = new List(); + + // Check if it is the leader stuck + if (leader.Actor.CenterPosition == leader.WPos && !IsAttackingAndTryAttack(leader.Actor).IsFiring) + { + stopUnits.Add(leader.Actor); + owner.Units.Remove(leader); + AIUtils.BotDebug("AI ({0}): Kick leader from squad.", owner.Bot.Player.ClientIndex); + } + + // Check if it is the units stuck + else + { + for (var i = 0; i < owner.Units.Count; i++) + { + var u = owner.Units[i]; + + if (u.Actor == leader.Actor) + continue; + + // If unit that is not in valid distance from leader nor firing at enemy, + // we will check if it can reach the leader, or stuck due to unknow reason + if ((u.Actor.CenterPosition - leader.Actor.CenterPosition).HorizontalLengthSquared >= 5 * occupiedArea + && (u.Actor.CenterPosition == u.WPos + || !AIUtils.PathExist(u.Actor, leader.Actor.Location, leader.Actor))) + { + stopUnits.Add(u.Actor); + owner.Units.RemoveAt(i); + i--; + } + else + { + u.WPos = u.Actor.CenterPosition; + otherUnits.Add(u.Actor); + } + } + + if (stopUnits.Count > 0) + AIUtils.BotDebug("AI ({0}): Kick ({1}) from squad.", owner.Bot.Player.ClientIndex, stopUnits.Count); + } + + if (owner.Units.Count == 0) + return; + + if (kickStuck > 1) + { + leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedNavyLeaderLocomotor); + leader.WPos = leader.Actor.CenterPosition; + owner.Bot.QueueOrder(new Order("AttackMove", leader.Actor, Target.FromCell(owner.World, owner.TargetActor.Location), false)); + owner.Bot.QueueOrder(new Order("Stop", null, false, groupedActors: stopUnits.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Actor.Location), false, groupedActors: otherUnits.ToArray())); + kickStuck--; + } + else if (kickStuck == 1) + { + shouldMakeWayPossibility = 0; + shouldKickStuckPossibility = 0; + leader = GetPathfindLeader(owner, owner.SquadManager.Info.SuggestedNavyLeaderLocomotor); + + // The end of "kickStuck": stop the leader for position record next tick + owner.Bot.QueueOrder(new Order("Stop", leader.Actor, false)); + kickStuck = 0; + } + + return; + } + + // Make way for leader: Make sure the guide unit has not been blocked by the rest of the squad. + if (makeWay > 0) + { + if (makeWay > 1) + { + var others = owner.Units.Where(u => u.Actor != leader.Actor).Select(u => u.Actor); + owner.Bot.QueueOrder(new Order("Scatter", null, false, groupedActors: others.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", leader.Actor, Target.FromCell(owner.World, owner.TargetActor.Location), false)); + makeWay--; + } + else if (makeWay == 1) + { + shouldMakeWayPossibility = 0; + shouldKickStuckPossibility = MaxSquadStuckPossibility / 2; + + // The end of "makeWay": stop the leader for position record next tick + // set "makeWay" to -1 to inform that squad just make way for leader + owner.Bot.QueueOrder(new Order("Stop", leader.Actor, false)); + makeWay = -1; + } + + return; + } + + // "leaderStopCheck" to see if leader move. + // "leaderWaitCheck" to see if leader should wait squad members that left behind. + var leaderStopCheck = leader.Actor.CenterPosition == leader.WPos; + var leaderWaitCheck = owner.Units.Any(u => (u.Actor.CenterPosition - leader.Actor.CenterPosition).HorizontalLengthSquared > occupiedArea * 5); + + // To find out the stuck problem of the squad and deal with it. + // 1. If leader cannot move and leader should wait, there may be squad members stuck. + // 2. If leader cannot move but leader should go, leader is stuck. + // -- Try make way for leader + // -- If make way cannot solve this problem, we kick stuck unit + // 3. If leader can move and leader should go, we consider this squad has no problem on stuck. + if (leaderStopCheck && leaderWaitCheck) + shouldKickStuckPossibility++; + else if (leaderStopCheck && !leaderWaitCheck) + { + if (makeWay != -1) + shouldMakeWayPossibility++; + else + shouldKickStuckPossibility++; + } + else if (!leaderStopCheck && !leaderWaitCheck) + { + shouldMakeWayPossibility = 0; + shouldKickStuckPossibility = 0; + } + + // Check if we need to make way for leader or kick stuck units + if (shouldMakeWayPossibility >= MaxMakeWayPossibility) + { + AIUtils.BotDebug("AI ({0}): Make way for squad leader.", owner.Bot.Player.ClientIndex); + makeWay = MakeWayTicks; + } + else if (shouldKickStuckPossibility >= MaxSquadStuckPossibility) + { + AIUtils.BotDebug("AI ({0}): Kick stuck units from squad.", owner.Bot.Player.ClientIndex); + kickStuck = KickStuckTicks; + } + + // Record current position of the squad leader + leader.WPos = leader.Actor.CenterPosition; + + // Leader will wait squad members that left behind, unless + // next tick is kick stuck unit (we need leader move in advance). + if (leaderWaitCheck && kickStuck <= 0) + owner.Bot.QueueOrder(new Order("Stop", leader.Actor, false)); + else + owner.Bot.QueueOrder(new Order("AttackMove", leader.Actor, Target.FromCell(owner.World, owner.TargetActor.Location), false)); + + var unitsHurryUp = owner.Units.Where(u => (u.Actor.CenterPosition - leader.Actor.CenterPosition).HorizontalLengthSquared >= occupiedArea * 2).Select(u => u.Actor); + owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Actor.Location), false, groupedActors: unitsHurryUp.ToArray())); + } + + public void Deactivate(Squad owner) { } + } + + // See detailed comments at GroundStates.cs + // There are many in common + sealed class NavyUnitsAttackState : NavyStateBase, IState + { + // Use it to find if entire squad cannot reach the attack position + int tryAttackTick; + int tryAttack = 0; + + public void Activate(Squad owner) + { + tryAttackTick = owner.SquadManager.Info.AttackScanRadius; + } + + public void Tick(Squad owner) + { + // Basic check + if (!owner.IsValid) + return; + + owner.SquadManager.SetAirStrikeTarget(owner.TargetActor); + var leader = owner.Units.First().Actor; + var isDefaultLeader = true; + + // Rescan target to prevent being ambushed and die without fight + // If there is no threat around, return to AttackMove state for formation + var attackScanRadius = WDist.FromCells(owner.SquadManager.Info.AttackScanRadius); + var closestEnemy = owner.SquadManager.FindClosestEnemy(leader, attackScanRadius); + + if (closestEnemy == null) + { + owner.TargetActor = FindClosestEnemy(owner, leader); + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsAttackMoveState(), false); + return; + } + else if (owner.TargetActor != closestEnemy) + { + // Refresh tryAttack when target switched + tryAttack = 0; + owner.TargetActor = closestEnemy; + } + + var cannotRetaliate = true; + var followingUnits = new List(); + var attackingUnits = new List(); + + foreach (var u in owner.Units) + { + var (isFiring, tryAttacking) = IsAttackingAndTryAttack(u.Actor); + + if ((tryAttacking || isFiring) && + (u.Actor.CenterPosition - owner.TargetActor.CenterPosition).HorizontalLengthSquared < + (leader.CenterPosition - owner.TargetActor.CenterPosition).HorizontalLengthSquared) + { + isDefaultLeader = false; + leader = u.Actor; + } + + if (isFiring && tryAttack != 0) + { + // Make there is at least one follow and attack target, AFTER first trying on attack + if (isDefaultLeader) + { + leader = u.Actor; + isDefaultLeader = false; + } + + cannotRetaliate = false; + } + else if (CanAttackTarget(u.Actor, owner.TargetActor)) + { + if (tryAttack > tryAttackTick && tryAttacking) + { + // Make there is at least one follow and attack target even when approach max tryAttackTick + if (isDefaultLeader) + { + leader = u.Actor; + isDefaultLeader = false; + attackingUnits.Add(u.Actor); + continue; + } + + followingUnits.Add(u.Actor); + continue; + } + + attackingUnits.Add(u.Actor); + cannotRetaliate = false; + } + else + followingUnits.Add(u.Actor); + } + + // Because ShouldFlee(owner) cannot retreat units while they cannot even fight + // a unit that they cannot target. Therefore, use `cannotRetaliate` here to solve this bug. + if (ShouldFleeSimple(owner) || cannotRetaliate) + { + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), false); + return; + } + + tryAttack++; + + owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Location), false, groupedActors: followingUnits.ToArray())); + owner.Bot.QueueOrder(new Order("Attack", null, Target.FromActor(owner.TargetActor), false, groupedActors: attackingUnits.ToArray())); + } + + public void Deactivate(Squad owner) { } + } + + sealed class NavyUnitsFleeState : NavyStateBase, IState + { + public void Activate(Squad owner) { } + + public void Tick(Squad owner) + { + if (!owner.IsValid) + return; + + Retreat(owner, flee: true, rearm: true, repair: true); + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsIdleState(), false); + } + + public void Deactivate(Squad owner) { } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs index 9b4bd3c900cc..81ae3fc90de9 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs @@ -9,46 +9,81 @@ */ #endregion +using System.Collections.Generic; using System.Linq; +using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits.BotModules.Squads { - sealed class UnitsForProtectionIdleState : GroundStateBase, IState + abstract class ProtectionStateBase : GroundStateBase { } + + sealed class UnitsForProtectionIdleState : ProtectionStateBase, IState { public void Activate(Squad owner) { } - public void Tick(Squad owner) { owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionAttackState(), true); } + public void Tick(Squad owner) + { + if (!owner.IsValid) + return; + + if (!owner.IsTargetValid) + { + Retreat(owner, flee: false, rearm: true, repair: true); + return; + } + else + owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionAttackState(), false); + } + public void Deactivate(Squad owner) { } } - sealed class UnitsForProtectionAttackState : GroundStateBase, IState + sealed class UnitsForProtectionAttackState : ProtectionStateBase, IState { public const int BackoffTicks = 4; + int tryAttackTick; + internal int Backoff = BackoffTicks; + int tryAttack = 0; - public void Activate(Squad owner) { } + public void Activate(Squad owner) + { + tryAttackTick = owner.SquadManager.Info.ProtectionScanRadius; + } public void Tick(Squad owner) { if (!owner.IsValid) return; - var leader = Leader(owner); - if (!owner.IsTargetValid(leader)) + var leader = owner.Units.FirstOrDefault().Actor; + + // rescan target to prevent being ambushed and die without fight + // return to AttackMove state for formation + var protectionScanRadius = WDist.FromCells(owner.SquadManager.Info.ProtectionScanRadius); + var closestEnemy = owner.SquadManager.FindClosestEnemy(leader, protectionScanRadius); + + if (closestEnemy == null && !owner.IsTargetValid) { - var target = owner.SquadManager.FindClosestEnemy(leader, WDist.FromCells(owner.SquadManager.Info.ProtectionScanRadius)); - owner.SetActorToTarget(target); - if (target.Actor == null) - { - owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionFleeState(), true); - return; - } + owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionFleeState(), false); + return; + } + else if (closestEnemy != null && owner.TargetActor != closestEnemy) + { + // Refresh tryAttack when target switched + tryAttack = 0; + owner.TargetActor = closestEnemy; } + var cannotRetaliate = false; + var resupplyingUnits = new List(); + var followingUnits = new List(); + var attackingUnits = new List(); + if (!owner.IsTargetVisible) { if (Backoff < 0) { - owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionFleeState(), true); + owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionFleeState(), false); Backoff = BackoffTicks; return; } @@ -56,25 +91,107 @@ public void Tick(Squad owner) Backoff--; } else - owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray())); + { + cannotRetaliate = true; + + for (var i = 0; i < owner.Units.Count; i++) + { + var u = owner.Units[i]; + + // Air units control: + var ammoPools = u.Actor.TraitsImplementing().ToArray(); + if (u.Actor.Info.HasTraitInfo() && ammoPools.Any()) + { + if (IsAttackingAndTryAttack(u.Actor).TryAttacking) + { + cannotRetaliate = false; + continue; + } + + if (!ReloadsAutomatically(ammoPools, u.Actor.TraitOrDefault())) + { + if (IsRearming(u.Actor)) + continue; + + if (!HasAmmo(ammoPools)) + { + resupplyingUnits.Add(u.Actor); + continue; + } + } + + if (CanAttackTarget(u.Actor, owner.TargetActor)) + { + attackingUnits.Add(u.Actor); + cannotRetaliate = false; + } + else + followingUnits.Add(u.Actor); + } + + // Ground/naval units control: + // Becuase MoveWithinRange can cause huge lag when stuck + // we only allow free attack behaivour within TryAttackTick + // then the squad will gather to a certain leader + else + { + var (isFiring, tryAttacking) = IsAttackingAndTryAttack(u.Actor); + + if ((tryAttacking || isFiring) && + (u.Actor.CenterPosition - owner.TargetActor.CenterPosition).HorizontalLengthSquared < + (leader.CenterPosition - owner.TargetActor.CenterPosition).HorizontalLengthSquared) + leader = u.Actor; + + if (isFiring && tryAttack != 0) + cannotRetaliate = false; + else if (CanAttackTarget(u.Actor, owner.TargetActor)) + { + if (tryAttack > tryAttackTick && tryAttacking) + { + followingUnits.Add(u.Actor); + continue; + } + + attackingUnits.Add(u.Actor); + cannotRetaliate = false; + } + else + followingUnits.Add(u.Actor); + } + } + } + + if (cannotRetaliate) + { + owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionFleeState(), false); + return; + } + + tryAttack++; + + owner.Bot.QueueOrder(new Order("ReturnToBase", null, false, groupedActors: resupplyingUnits.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Location), false, groupedActors: followingUnits.ToArray())); + owner.Bot.QueueOrder(new Order("Attack", null, Target.FromActor(owner.TargetActor), false, groupedActors: attackingUnits.ToArray())); } public void Deactivate(Squad owner) { } } - sealed class UnitsForProtectionFleeState : GroundStateBase, IState + sealed class UnitsForProtectionFleeState : ProtectionStateBase, IState { public void Activate(Squad owner) { } public void Tick(Squad owner) { + owner.TargetActor = null; + if (!owner.IsValid) return; - GoToRandomOwnBuilding(owner); - owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionIdleState(), true); + Retreat(owner, flee: true, rearm: true, repair: true); + owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionIdleState(), false); } - public void Deactivate(Squad owner) { owner.SquadManager.UnregisterSquad(owner); } + public void Deactivate(Squad owner) { } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/StateBase.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/StateBase.cs index 9fbd4608f8a1..4041d54f244f 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/StateBase.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/StateBase.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; +using OpenRA.Activities; using OpenRA.Mods.Common.Activities; using OpenRA.Traits; @@ -22,8 +23,8 @@ abstract class StateBase protected static void GoToRandomOwnBuilding(Squad squad) { var loc = RandomBuildingLocation(squad); - foreach (var a in squad.Units) - squad.Bot.QueueOrder(new Order("Move", a, Target.FromCell(squad.World, loc), false)); + foreach (var u in squad.Units) + squad.Bot.QueueOrder(new Order("Move", u.Actor, Target.FromCell(squad.World, loc), false)); } protected static CPos RandomBuildingLocation(Squad squad) @@ -37,27 +38,90 @@ protected static CPos RandomBuildingLocation(Squad squad) return location; } + // Deprecated old method protected static bool BusyAttack(Actor a) { if (a.IsIdle) return false; + var isAttacking = false; var activity = a.CurrentActivity; var type = activity.GetType(); + if (type == typeof(Attack) || type == typeof(FlyAttack)) - return true; + isAttacking = true; + else + { + var next = activity.NextActivity; + if (next == null) + return false; + + var nextType = next.GetType(); + if (nextType == typeof(Attack) || nextType == typeof(FlyAttack)) + isAttacking = true; + } - var next = activity.NextActivity; - if (next == null) + if (!isAttacking) return false; - var nextType = next.GetType(); - if (nextType == typeof(Attack) || nextType == typeof(FlyAttack)) - return true; + var arms = a.TraitsImplementing(); + foreach (var arm in arms) + { + if (arm.IsTraitDisabled) + continue; + + if ((arm.Info.TargetRelationships & PlayerRelationship.Enemy) != 0) + return true; + } return false; } + protected static (bool IsFiring, bool TryAttacking) IsAttackingAndTryAttack(Actor a) + { + if (a.IsIdle) + return (false, false); + + var isFiring = false; + var tryAttacking = false; + var activity = a.CurrentActivity; + var type = activity.ActivityType; + + var arms = a.TraitsImplementing(); + var isValid = false; + foreach (var arm in arms) + { + if (arm.IsTraitDisabled) + continue; + + if ((arm.Info.TargetRelationships & PlayerRelationship.Enemy) != 0) + { + isValid = true; + break; + } + } + + if (!isValid) + return (false, false); + + if (type == ActivityType.Attack) + { + tryAttacking = true; + + var childActivity = activity.ChildActivity; + if (childActivity == null) + isFiring = true; + else + { + var childType = childActivity.ActivityType; + if (childType != ActivityType.Move) + isFiring = true; + } + } + + return (isFiring, tryAttacking); + } + protected static bool CanAttackTarget(Actor a, Actor target) { if (!a.Info.HasTraitInfo()) @@ -73,6 +137,9 @@ protected static bool CanAttackTarget(Actor a, Actor target) if (arm.IsTraitDisabled) continue; + if ((arm.Info.TargetRelationships & PlayerRelationship.Enemy) == 0) + continue; + if (arm.Weapon.IsValidTarget(targetTypes)) return true; } @@ -85,8 +152,9 @@ protected virtual bool ShouldFlee(Squad squad, Func, if (!squad.IsValid) return false; + var randomSquadUnit = squad.Units.Random(squad.Random); var dangerRadius = squad.SquadManager.Info.DangerScanRadius; - var units = squad.World.FindActorsInCircle(squad.CenterPosition(), WDist.FromCells(dangerRadius)).ToList(); + var units = squad.World.FindActorsInCircle(randomSquadUnit.Actor.CenterPosition, WDist.FromCells(dangerRadius)).ToList(); // If there are any own buildings within the DangerRadius, don't flee // PERF: Avoid LINQ @@ -103,6 +171,34 @@ protected virtual bool ShouldFlee(Squad squad, Func, return flee(enemyAroundUnit); } + // Note: There is a simple check without using costy AttackOrFleeFuzzy + protected virtual bool ShouldFleeSimple(Squad squad) + { + if (!squad.IsValid) + return false; + + var squadUnit = squad.Units.First().Actor; + var dangerRadius = squad.SquadManager.Info.DangerScanRadius; + var units = squad.World.FindActorsInCircle(squadUnit.CenterPosition, WDist.FromCells(dangerRadius)).ToList(); + + var enemyAroundUnit = units.Where(unit => squad.SquadManager.IsPreferredEnemyUnit(unit) && unit.Info.HasTraitInfo()).ToList(); + if (enemyAroundUnit.Count == 0) + return false; + + var panic = (enemyAroundUnit.Count + squad.Units.Count - units.Count) * (int)DamageState.Critical; + foreach (var u in squad.Units) + { + var health = u.Actor.TraitOrDefault(); + if (health != null) + panic += (int)health.DamageState; + } + + if (panic > squad.Units.Count * (int)DamageState.Medium) + return true; + + return false; + } + protected static bool IsRearming(Actor a) { return !a.IsIdle && (a.CurrentActivity.ActivitiesImplementing().Any() || a.CurrentActivity.ActivitiesImplementing().Any()); @@ -137,5 +233,112 @@ protected static bool ReloadsAutomatically(IEnumerable ammoPools, Rear return true; } + + // Retreat units from combat, or for supply only in idle + protected static void Retreat(Squad squad, bool flee, bool rearm, bool repair) + { + // HACK: "alreadyRepair" is to solve AI repair orders performance, + // which is only allow one goes to repairpad at the same time to avoid queueing too many orders. + // if repairpad logic is better we can just drop it. + var alreadyRepair = false; + + var rearmingUnits = new List(); + var fleeingUnits = new List(); + + foreach (var u in squad.Units) + { + if (IsRearming(u.Actor) || IsAttackingAndTryAttack(u.Actor).IsFiring) + continue; + + var orderQueued = false; + + // Units need to rearm will be added to rearming group. + if (rearm) + { + var ammoPools = u.Actor.TraitsImplementing().ToArray(); + if (!ReloadsAutomatically(ammoPools, u.Actor.TraitOrDefault()) && !FullAmmo(ammoPools)) + { + rearmingUnits.Add(u.Actor); + orderQueued = true; + } + } + + // Try repair units. + // Don't use grounp order here becuase we have 2 kinds of repaid orders and we need to find repair building for both traits. + if (repair && !alreadyRepair) + { + Actor repairBuilding = null; + var orderId = "Repair"; + var health = u.Actor.TraitOrDefault(); + + if (health != null && health.DamageState > DamageState.Undamaged) + { + var repairable = u.Actor.TraitOrDefault(); + if (repairable != null) + repairBuilding = repairable.FindRepairBuilding(u.Actor); + else + { + var repairableNear = u.Actor.TraitOrDefault(); + if (repairableNear != null) + { + orderId = "RepairNear"; + repairBuilding = repairableNear.FindRepairBuilding(u.Actor); + } + } + + if (repairBuilding != null) + { + squad.Bot.QueueOrder(new Order(orderId, u.Actor, Target.FromActor(repairBuilding), orderQueued)); + orderQueued = true; + alreadyRepair = true; + } + } + } + + // If there is no order in queue and units should flee, add unit to fleeing group. + if (flee && !orderQueued) + fleeingUnits.Add(u.Actor); + } + + if (rearmingUnits.Count > 0) + squad.Bot.QueueOrder(new Order("ReturnToBase", null, true, groupedActors: rearmingUnits.ToArray())); + + if (fleeingUnits.Count > 0) + squad.Bot.QueueOrder(new Order("Move", null, Target.FromCell(squad.World, RandomBuildingLocation(squad)), false, groupedActors: fleeingUnits.ToArray())); + } + + protected static UnitWposWrapper GetPathfindLeader(Squad squad, HashSet locomotorTypes) + { + var nonAircraft = new UnitWposWrapper(null); // HACK: Becuase Mobile is always affected by terrain, so we always select a nonAircraft as leader + foreach (var u in squad.Units) + { + var mt = u.Actor.TraitsImplementing().FirstOrDefault(t => !t.IsTraitDisabled && !t.IsTraitPaused); + if (mt == null) + continue; + else + { + nonAircraft = u; + if (locomotorTypes.Contains(mt.Info.Locomotor)) + return u; + } + } + + if (nonAircraft.Actor != null) + return nonAircraft; + + return squad.Units.FirstOrDefault(); + } + + protected static bool CheckReachability(Actor sourceActor, Actor targetActor) + { + var mobile = sourceActor.TraitOrDefault(); + if (mobile == null) + return false; + else + { + var locomotor = mobile.Locomotor; + return mobile.PathFinder.PathExistsForLocomotor(locomotor, sourceActor.Location, targetActor.Location); + } + } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs index 079fc3c7fdc7..b3764882e62f 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs @@ -45,12 +45,14 @@ public class SupportPowerBotModule : ConditionalTrait readonly Dictionary powerDecisions = new(); readonly List stalePowers = new(); SupportPowerManager supportPowerManager; + PlayerResources playerResource; public SupportPowerBotModule(Actor self, SupportPowerBotModuleInfo info) : base(info) { world = self.World; player = self.Owner; + self.World.AddFrameEndTask(w => playerResource = player.PlayerActor.Trait()); } protected override void Created(Actor self) @@ -88,6 +90,14 @@ void IBotTick.BotTick(IBot bot) continue; } + if (sp.Info.Cost != 0 && playerResource.Cash + playerResource.Resources < sp.Info.Cost) + { + AIUtils.BotDebug("AI: {1} can't afford the activation of support power {0}. Delaying rescan.", sp.Info.OrderName, player.PlayerName); + waitingPowers[sp] += powerDecision.GetNextScanTime(world); + + continue; + } + var attackLocation = FindCoarseAttackLocationToSupportPower(sp); if (attackLocation == null) { diff --git a/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs index 7964b320f469..c67ceca50628 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs @@ -15,7 +15,6 @@ namespace OpenRA.Mods.Common.Traits { - [TraitLocation(SystemActors.Player)] [Desc("Controls AI unit production.")] public class UnitBuilderBotModuleInfo : ConditionalTraitInfo { @@ -38,8 +37,15 @@ public class UnitBuilderBotModuleInfo : ConditionalTraitInfo [Desc("When should the AI start train specific units.")] public readonly Dictionary UnitDelays = null; + [Desc("Limit of queue instances to build from at the same time.")] + public readonly Dictionary QueueLimits = null; + [Desc("Only queue construction of a new unit when above this requirement.")] - public readonly int ProductionMinCashRequirement = 500; + public readonly int ProductionMinCashRequirement = 501; + + [ActorReference] + [Desc("What units can the AI not build if there are no supplies on the map.")] + public readonly HashSet SupplyCollectorTypes = new(); public override object Create(ActorInitializer init) { return new UnitBuilderBotModule(init.Self, this); } } @@ -73,7 +79,7 @@ protected override void Created(Actor self) playerResources = self.Owner.PlayerActor.Trait(); } - void IBotNotifyIdleBaseUnits.UpdatedIdleBaseUnits(List idleUnits) + void IBotNotifyIdleBaseUnits.UpdatedIdleBaseUnits(List idleUnits) { idleUnitCount = idleUnits.Count; } @@ -130,7 +136,7 @@ void BuildRandomUnit(IBot bot, string category) return; // Pick a free queue - var queue = AIUtils.FindQueues(player, category).FirstOrDefault(q => !q.AllQueued().Any()); + var queue = FindQueue(player, category); if (queue == null) return; @@ -149,16 +155,19 @@ void BuildUnit(IBot bot, string name) if (actorInfo == null) return; - var buildableInfo = actorInfo.TraitInfoOrDefault(); - if (buildableInfo == null) + var buildableInfos = actorInfo.TraitInfos().ToArray(); + if (buildableInfos.Length == 0) return; ProductionQueue queue = null; - foreach (var pq in buildableInfo.Queue) + foreach (var bi in buildableInfos) { - queue = AIUtils.FindQueues(player, pq).FirstOrDefault(q => !q.AllQueued().Any()); - if (queue != null) - break; + foreach (var pq in bi.Queue) + { + queue = FindQueue(player, pq, true); + if (queue != null) + break; + } } if (queue != null) @@ -175,6 +184,7 @@ ActorInfo ChooseRandomUnitToBuild(ProductionQueue queue) return null; var allUnits = world.Actors.Where(a => a.Owner == player && Info.UnitsToBuild.ContainsKey(a.Info.Name) && !a.IsDead).ToArray(); + var hasSupply = Info.SupplyCollectorTypes.Count == 0 || world.ActorsWithTrait().Any(d => !d.Trait.IsEmpty()); ActorInfo desiredUnit = null; var desiredError = int.MaxValue; @@ -183,13 +193,16 @@ ActorInfo ChooseRandomUnitToBuild(ProductionQueue queue) if (!Info.UnitsToBuild.ContainsKey(unit.Name) || (Info.UnitDelays != null && Info.UnitDelays.TryGetValue(unit.Name, out var delay) && delay > world.WorldTick)) continue; + if (!hasSupply && Info.SupplyCollectorTypes.Contains(unit.Name)) + continue; + var unitCount = allUnits.Count(a => a.Info.Name == unit.Name); if (Info.UnitLimits != null && Info.UnitLimits.TryGetValue(unit.Name, out var count) && unitCount >= count) continue; var error = allUnits.Length > 0 ? unitCount * 100 / allUnits.Length - Info.UnitsToBuild[unit.Name] : -1; if (error < 0) - return HasAdequateAirUnitReloadBuildings(unit) ? unit : null; + return unit; if (error < desiredError) { @@ -198,27 +211,24 @@ ActorInfo ChooseRandomUnitToBuild(ProductionQueue queue) } } - return desiredUnit != null ? (HasAdequateAirUnitReloadBuildings(desiredUnit) ? desiredUnit : null) : null; + return desiredUnit; } - // For mods like RA (number of RearmActors must match the number of aircraft) - bool HasAdequateAirUnitReloadBuildings(ActorInfo actorInfo) + public ProductionQueue FindQueue(Player player, string category, bool ignoreQueueLimit = false) { - var aircraftInfo = actorInfo.TraitInfoOrDefault(); - if (aircraftInfo == null) - return true; + var queues = AIUtils.FindQueues(player, category); - // If actor isn't Rearmable, it doesn't need a RearmActor to reload - var rearmableInfo = actorInfo.TraitInfoOrDefault(); - if (rearmableInfo == null) - return true; + var usedQueues = queues.Where(q => q.AllQueued().Any()); + if (!ignoreQueueLimit && Info.QueueLimits != null && + Info.QueueLimits.ContainsKey(category) && + usedQueues.Count() >= Info.QueueLimits[category]) + return null; - var countOwnAir = AIUtils.CountActorsWithTrait(actorInfo.Name, player); - var countBuildings = rearmableInfo.RearmActors.Sum(b => AIUtils.CountActorsWithTrait(b, player)); - if (countOwnAir >= countBuildings) - return false; + var freeQueues = queues.Where(q => !q.AllQueued().Any()); + if (!freeQueues.Any()) + return null; - return true; + return freeQueues.RandomOrDefault(world.LocalRandom); } List IGameSaveTraitData.IssueTraitData(Actor self) diff --git a/OpenRA.Mods.Common/Traits/Buildable.cs b/OpenRA.Mods.Common/Traits/Buildable.cs index 8c0042d80642..b9e84cde2f56 100644 --- a/OpenRA.Mods.Common/Traits/Buildable.cs +++ b/OpenRA.Mods.Common/Traits/Buildable.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; +using System.Linq; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -32,9 +33,15 @@ public class BuildableInfo : TraitInfo [Desc("Disable production when there are more than this many of this actor on the battlefield. Set to 0 to disable.")] public readonly int BuildLimit = 0; + [Desc("Build this many of the actor at once.")] + public readonly int BuildAmount = 1; + [Desc("Force a specific faction variant, overriding the faction of the producing actor.")] public readonly string ForceFaction = null; + [Desc("Show a tooltip when hovered over my icon.")] + public readonly bool ShowTooltip = true; + [SequenceReference] [Desc("Sequence of the actor that contains the icon.")] public readonly string Icon = "icon"; @@ -55,12 +62,106 @@ public class BuildableInfo : TraitInfo [Desc("Sort order for the production palette. Smaller numbers are presented earlier.")] public readonly int BuildPaletteOrder = 9999; + [Desc("Place the icon on the slot number of BuildPaletteOwner, even if there are free slots before it.")] + public readonly bool ForceIconLocation = false; + [Desc("Text shown in the production tooltip.")] public readonly string Description = ""; + [NotificationReference("Speech")] + [Desc("Notification played when production is complete.", + "The filename of the audio is defined per faction in notifications.yaml.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string ReadyAudio = null; + + [Desc("Notification displayed when production is complete.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string ReadyTextNotification = null; + + [NotificationReference("Speech")] + [Desc("Notification played when you can't queue another actor", + "when the queue length limit is exceeded.", + "The filename of the audio is defined per faction in notifications.yaml.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string LimitedAudio = null; + + [Desc("Notification displayed when you can't queue another actor", + "when the queue length limit is exceeded.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string LimitedTextNotification = null; + + [NotificationReference("Speech")] + [Desc("Notification played when you can't place a building.", + "The filename of the audio is defined per faction in notifications.yaml.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string CannotPlaceAudio = null; + + [Desc("Notification displayed when you can't place a building.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string CannotPlaceTextNotification = null; + + [NotificationReference("Speech")] + [Desc("Notification played when user clicks on the build palette icon.", + "The filename of the audio is defined per faction in notifications.yaml.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string QueuedAudio = null; + + [Desc("Notification displayed when user clicks on the build palette icon.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string QueuedTextNotification = null; + + [NotificationReference("Speech")] + [Desc("Notification played when player right-clicks on the build palette icon.", + "The filename of the audio is defined per faction in notifications.yaml.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string OnHoldAudio = null; + + [Desc("Notification displayed when player right-clicks on the build palette icon.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string OnHoldTextNotification = null; + + [NotificationReference("Speech")] + [Desc("Notification played when player right-clicks on a build palette icon that is already on hold.", + "The filename of the audio is defined per faction in notifications.yaml.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string CancelledAudio = null; + + [Desc("Notification displayed when player right-clicks on a build palette icon that is already on hold.", + "Defaults to what is set for the Queue actor built from.")] + public readonly string CancelledTextNotification = null; + + public int GetBuildPaletteOrder(ActorInfo ai, ProductionQueue queue) + { + var paletteOrder = BuildPaletteOrder; + if (queue == null) + return paletteOrder; + + var modifiers = ai.TraitInfos() + .Select(t => t.GetBuildPaletteOrderModifier(queue.TechTree, queue.Info.Type)); + foreach (var modifier in modifiers) + paletteOrder += modifier; + + return paletteOrder; + } + + public static BuildableInfo GetTraitForQueue(ActorInfo ai, string queue) + { + var buildables = ai.TraitInfos(); + if (!string.IsNullOrEmpty(queue)) + { + foreach (var bi in buildables) + if (bi.Queue.Contains(queue)) + return bi; + + return null; + } + + return buildables.FirstOrDefault(); + } + public static string GetInitialFaction(ActorInfo ai, string defaultFaction) { - return ai.TraitInfoOrDefault()?.ForceFaction ?? defaultFaction; + return GetTraitForQueue(ai, null)?.ForceFaction ?? defaultFaction; } } diff --git a/OpenRA.Mods.Common/Traits/Buildings/ActorPreviewPlaceBuildingPreview.cs b/OpenRA.Mods.Common/Traits/Buildings/ActorPreviewPlaceBuildingPreview.cs index a36e4ffa0666..7a3d1b5089f5 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/ActorPreviewPlaceBuildingPreview.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/ActorPreviewPlaceBuildingPreview.cs @@ -33,6 +33,9 @@ public class ActorPreviewPlaceBuildingPreviewInfo : FootprintPlaceBuildingPrevie [Desc("Footprint types to draw above the actor preview.")] public readonly PlaceBuildingCellType FootprintOverPreview = PlaceBuildingCellType.Invalid; + [Desc("Custom ZOffset of the rendered building preview.")] + public readonly int ZOffset = 0; + protected override IPlaceBuildingPreview CreatePreview(WorldRenderer wr, ActorInfo ai, TypeDictionary init) { return new ActorPreviewPlaceBuildingPreviewPreview(wr, ai, this, init); @@ -82,10 +85,14 @@ protected override IEnumerable RenderInner(WorldRenderer wr, CPos t foreach (var r in previewRenderables.OrderBy(WorldRenderer.RenderableZPositionComparisonKey)) { + var renderable = r; if (info.PreviewAlpha < 1f && r is IModifyableRenderable mr) - yield return mr.WithAlpha(mr.Alpha * info.PreviewAlpha); - else - yield return r; + renderable = mr.WithAlpha(mr.Alpha * info.PreviewAlpha); + + if (info.ZOffset != 0) + renderable = renderable.WithZOffset(info.ZOffset); + + yield return renderable; } if (info.FootprintOverPreview != PlaceBuildingCellType.None) diff --git a/OpenRA.Mods.Common/Traits/Buildings/Building.cs b/OpenRA.Mods.Common/Traits/Buildings/Building.cs index bf037e72d65d..3f623af8653a 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/Building.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/Building.cs @@ -31,6 +31,9 @@ public class BuildingInfo : TraitInfo, IOccupySpaceInfo, IPlaceBuildingDecoratio [Desc("Where you are allowed to place the building (Water, Clear, ...)")] public readonly HashSet TerrainTypes = new(); + [Desc("Terrain that the building can be placed on, but not at the same time with ones defined under `TerrainTypes`.")] + public readonly HashSet SecondaryTerrainTypes = new(); + [Desc("x means cell is blocked, capital X means blocked but not counting as targetable, ", "= means part of the footprint but passable, _ means completely empty.")] [FieldLoader.LoadUsing(nameof(LoadFootprint))] @@ -60,6 +63,12 @@ public class BuildingInfo : TraitInfo, IOccupySpaceInfo, IPlaceBuildingDecoratio public readonly string[] UndeploySounds = Array.Empty(); + [Desc("Do the sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the BuildSounds and UndeploySounds played at.")] + public readonly float SoundVolume = 1f; + public override object Create(ActorInitializer init) { return new Building(init, this); } protected static object LoadFootprint(MiniYaml yaml) @@ -180,7 +189,7 @@ public BaseProvider FindBaseProvider(World world, Player p, CPos topLeft) return null; } - static bool AnyGivesBuildableArea(IEnumerable actors, Player p, bool allyBuildEnabled, RequiresBuildableAreaInfo rba) + static bool AnyGivesBuildableArea(IEnumerable actors, Actor producer, Player p, bool allyBuildEnabled, RequiresBuildableAreaInfo rba) { foreach (var a in actors) { @@ -191,6 +200,7 @@ static bool AnyGivesBuildableArea(IEnumerable actors, Player p, bool ally continue; var overlaps = rba.AreaTypes.Overlaps(a.TraitsImplementing() + .Where(gba => !gba.Info.OnlyAllowPlacementFromSelf || a == producer) .SelectMany(gba => gba.AreaTypes)); if (overlaps) @@ -200,7 +210,7 @@ static bool AnyGivesBuildableArea(IEnumerable actors, Player p, bool ally return false; } - public virtual bool IsCloseEnoughToBase(World world, Player p, ActorInfo ai, CPos topLeft) + public virtual bool IsCloseEnoughToBase(World world, Player p, ActorInfo ai, Actor producer, CPos topLeft) { var requiresBuildableArea = ai.TraitInfoOrDefault(); var mapBuildRadius = world.WorldActor.TraitOrDefault(); @@ -226,7 +236,7 @@ public virtual bool IsCloseEnoughToBase(World world, Player p, ActorInfo ai, CPo for (var x = scanStart.X; x < scanEnd.X; x++) { var c = new CPos(x, y); - if (AnyGivesBuildableArea(world.ActorMap.GetActorsAt(c), p, allyBuildEnabled, requiresBuildableArea)) + if (AnyGivesBuildableArea(world.ActorMap.GetActorsAt(c), producer, p, allyBuildEnabled, requiresBuildableArea)) { nearnessCandidates.Add(c); continue; @@ -234,7 +244,7 @@ public virtual bool IsCloseEnoughToBase(World world, Player p, ActorInfo ai, CPo // Building bibs and pathable footprint cells are not included in the ActorMap // TODO: Allow ActorMap to track these and finally remove the BuildingInfluence layer completely - if (AnyGivesBuildableArea(bi.GetBuildingsAt(c), p, allyBuildEnabled, requiresBuildableArea)) + if (AnyGivesBuildableArea(bi.GetBuildingsAt(c), producer, p, allyBuildEnabled, requiresBuildableArea)) nearnessCandidates.Add(c); } } @@ -338,8 +348,10 @@ void INotifyTransform.BeforeTransform(Actor self) if (Info.RemoveSmudgesOnTransform) RemoveSmudges(); - foreach (var s in Info.UndeploySounds) - Game.Sound.PlayToPlayer(SoundType.World, self.Owner, s, self.CenterPosition); + var pos = self.CenterPosition; + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + foreach (var s in Info.UndeploySounds) + Game.Sound.Play(SoundType.World, s, pos, Info.SoundVolume); } void INotifyTransform.OnTransform(Actor self) { } diff --git a/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs b/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs index 175c069558e6..c669ccc114ff 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs @@ -16,7 +16,7 @@ namespace OpenRA.Mods.Common.Traits { public static class BuildingUtils { - public static bool IsCellBuildable(this World world, CPos cell, ActorInfo ai, BuildingInfo bi, Actor toIgnore = null) + public static bool IsCellBuildable(this World world, CPos cell, CPos topLeft, ActorInfo ai, BuildingInfo bi, Actor toIgnore = null) { if (!world.Map.Contains(cell)) return false; @@ -76,7 +76,13 @@ public static bool IsCellBuildable(this World world, CPos cell, ActorInfo ai, Bu } // Buildings can never be placed on ramps - return world.Map.Ramp[cell] == 0 && bi.TerrainTypes.Contains(world.Map.GetTerrainInfo(cell).Type); + if (world.Map.Ramp[cell] != 0) + return false; + + if (bi.SecondaryTerrainTypes.Count == 0) + return bi.TerrainTypes.Contains(world.Map.GetTerrainInfo(cell).Type); + + return (bi.TerrainTypes.Contains(world.Map.GetTerrainInfo(cell).Type) && !bi.SecondaryTerrainTypes.Contains(world.Map.GetTerrainInfo(topLeft).Type)) || (!bi.TerrainTypes.Contains(world.Map.GetTerrainInfo(cell).Type) && bi.SecondaryTerrainTypes.Contains(world.Map.GetTerrainInfo(topLeft).Type)); } public static bool CanPlaceBuilding(this World world, CPos cell, ActorInfo ai, BuildingInfo bi, Actor toIgnore) @@ -87,7 +93,7 @@ public static bool CanPlaceBuilding(this World world, CPos cell, ActorInfo ai, B var resourceLayer = world.WorldActor.TraitOrDefault(); return bi.Tiles(cell).All(t => world.Map.Contains(t) && (bi.AllowPlacementOnResources || resourceLayer == null || resourceLayer.GetResource(t).Type == null) && - world.IsCellBuildable(t, ai, bi, toIgnore)); + world.IsCellBuildable(t, cell, ai, bi, toIgnore)); } public static IEnumerable<(CPos Cell, Actor Actor)> GetLineBuildCells(World world, CPos cell, ActorInfo ai, BuildingInfo bi, Player owner) @@ -95,7 +101,7 @@ public static bool CanPlaceBuilding(this World world, CPos cell, ActorInfo ai, B var lbi = ai.TraitInfo(); var topLeft = cell; // 1x1 assumption! - if (world.IsCellBuildable(topLeft, ai, bi)) + if (world.IsCellBuildable(topLeft, topLeft, ai, bi)) yield return (topLeft, null); // Start at place location, search outwards @@ -121,7 +127,7 @@ public static bool CanPlaceBuilding(this World world, CPos cell, ActorInfo ai, B // Continue the search if the cell is empty or not visible var c = topLeft + i * vecs[d]; - if (world.IsCellBuildable(c, segmentInfo, segmentBuildingInfo) || !owner.Shroud.IsExplored(c)) + if (world.IsCellBuildable(c, topLeft, segmentInfo, segmentBuildingInfo) || !owner.Shroud.IsExplored(c)) continue; // Cell contains an actor. Is it the type we want? diff --git a/OpenRA.Mods.Common/Traits/Buildings/Exit.cs b/OpenRA.Mods.Common/Traits/Buildings/Exit.cs index 72ccf77fd0f5..59f6abb39333 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/Exit.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/Exit.cs @@ -46,6 +46,18 @@ public Exit(ExitInfo info) public static class ExitExts { + public static Exit FirstExitOrDefault(this Actor actor, string productionType = null) + { + var all = actor.TraitsImplementing() + .Where(Exts.IsTraitEnabled) + .OrderBy(e => e.Info.Priority); + + if (string.IsNullOrEmpty(productionType)) + return all.FirstOrDefault(); + + return all.FirstOrDefault(e => e.Info.ProductionTypes.Count == 0 || e.Info.ProductionTypes.Contains(productionType)); + } + public static Exit NearestExitOrDefault(this Actor actor, WPos pos, string productionType = null, Func p = null) { // The .ToList() is required to work around a bug/unexpected behaviour in mono, where diff --git a/OpenRA.Mods.Common/Traits/Buildings/FreeActor.cs b/OpenRA.Mods.Common/Traits/Buildings/FreeActor.cs index 6ab2e642d959..36115a070d46 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/FreeActor.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/FreeActor.cs @@ -70,16 +70,17 @@ protected override void TraitEnabled(Actor self) allowSpawn = Info.AllowRespawn; - self.World.AddFrameEndTask(w => + var td = new TypeDictionary { - w.CreateActor(Info.Actor, new TypeDictionary - { - new ParentActorInit(self), - new LocationInit(self.Location + Info.SpawnOffset), - new OwnerInit(self.Owner), - new FacingInit(Info.Facing), - }); - }); + new ParentActorInit(self), + new OwnerInit(self.Owner), + new FacingInit(Info.Facing), + }; + + if (self.TraitOrDefault() != null) + td.Add(new LocationInit(self.Location + Info.SpawnOffset)); + + self.World.AddFrameEndTask(w => w.CreateActor(Info.Actor, td)); } } diff --git a/OpenRA.Mods.Common/Traits/Buildings/Gate.cs b/OpenRA.Mods.Common/Traits/Buildings/Gate.cs index 0569cb354f33..fc1699c823b6 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/Gate.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/Gate.cs @@ -21,6 +21,12 @@ public class GateInfo : PausableConditionalTraitInfo, ITemporaryBlockerInfo, IBl public readonly string OpeningSound = null; public readonly string ClosingSound = null; + [Desc("Do the sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the OpeningSound and ClosingSound played at.")] + public readonly float SoundVolume = 1f; + [Desc("Ticks until the gate closes.")] public readonly int CloseDelay = 150; @@ -72,7 +78,10 @@ void ITick.Tick(Actor self) // Gate was fully open if (Position == OpenPosition) { - Game.Sound.Play(SoundType.World, Info.ClosingSound, self.CenterPosition); + var pos = self.CenterPosition; + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, Info.ClosingSound, pos, Info.SoundVolume); + self.World.ActorMap.AddInfluence(self, building); } @@ -82,7 +91,11 @@ void ITick.Tick(Actor self) { // Gate was fully closed if (Position == 0) - Game.Sound.Play(SoundType.World, Info.OpeningSound, self.CenterPosition); + { + var pos = self.CenterPosition; + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, Info.OpeningSound, pos, Info.SoundVolume); + } Position++; diff --git a/OpenRA.Mods.Common/Traits/Buildings/GivesBuildableArea.cs b/OpenRA.Mods.Common/Traits/Buildings/GivesBuildableArea.cs index b979c4bc52d9..1289de286c24 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/GivesBuildableArea.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/GivesBuildableArea.cs @@ -20,6 +20,9 @@ public class GivesBuildableAreaInfo : ConditionalTraitInfo [Desc("Types of buildable area this actor gives.")] public readonly HashSet AreaTypes = new(); + [Desc("Is this buildable area is only valid for buildings built from the this actor.")] + public readonly bool OnlyAllowPlacementFromSelf = false; + public override object Create(ActorInitializer init) { return new GivesBuildableArea(this); } } diff --git a/OpenRA.Mods.Common/Traits/Buildings/ProductionAirdrop.cs b/OpenRA.Mods.Common/Traits/Buildings/ProductionAirdrop.cs index 731112118253..90adb74d475c 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/ProductionAirdrop.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/ProductionAirdrop.cs @@ -39,6 +39,15 @@ public class ProductionAirdropInfo : ProductionInfo [Desc("Direction the aircraft should face to land.")] public readonly WAngle Facing = new(256); + [Desc("Tick that aircraft should wait before producing.")] + public readonly int WaitTickBeforeProduce = 0; + + [Desc("Tick that aircraft should wait after producing.")] + public readonly int WaitTickAfterProduce = 0; + + [Desc("Offset the aircraft used for landing.")] + public readonly WVec LandOffset = WVec.Zero; + public override object Create(ActorInitializer init) { return new ProductionAirdrop(init, this); } } @@ -103,7 +112,9 @@ public override bool Produce(Actor self, ActorInfo producee, string productionTy }); var exitCell = self.Location + exit.ExitCell; - actor.QueueActivity(new Land(actor, Target.FromActor(self), WDist.Zero, WVec.Zero, info.Facing, clearCells: new CPos[1] { exitCell })); + actor.QueueActivity(new Land(actor, Target.FromActor(self), WDist.Zero, info.LandOffset, info.Facing, clearCells: new CPos[1] { exitCell })); + if (info.WaitTickBeforeProduce > 0) + actor.QueueActivity(new Wait(info.WaitTickBeforeProduce)); actor.QueueActivity(new CallFunc(() => { if (!self.IsInWorld || self.IsDead) @@ -119,7 +130,8 @@ public override bool Produce(Actor self, ActorInfo producee, string productionTy Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", info.ReadyAudio, self.Owner.Faction.InternalName); TextNotificationsManager.AddTransientLine(self.Owner, info.ReadyTextNotification); })); - + if (info.WaitTickAfterProduce > 0) + actor.QueueActivity(new Wait(info.WaitTickAfterProduce)); actor.QueueActivity(new FlyOffMap(actor, Target.FromCell(w, endPos))); actor.QueueActivity(new RemoveSelf()); }); diff --git a/OpenRA.Mods.Common/Traits/Buildings/Refinery.cs b/OpenRA.Mods.Common/Traits/Buildings/Refinery.cs index 10db8db5ffb6..afa38eea5476 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/Refinery.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/Refinery.cs @@ -38,6 +38,8 @@ public class Refinery : IAcceptResources, INotifyCreated, ITick, INotifyOwnerCha PlayerResources playerResources; IEnumerable resourceValueModifiers; + IRefineryResourceDelivered[] refineryResourceDelivereds; + int currentDisplayTick = 0; int currentDisplayValue = 0; public Refinery(Actor self, RefineryInfo info) @@ -50,6 +52,7 @@ public Refinery(Actor self, RefineryInfo info) void INotifyCreated.Created(Actor self) { resourceValueModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetResourceValueModifier()); + refineryResourceDelivereds = self.TraitsImplementing().ToArray(); } int IAcceptResources.AcceptResources(Actor self, string resourceType, int count) @@ -84,6 +87,13 @@ int IAcceptResources.AcceptResources(Actor self, string resourceType, int count) notify.Trait.OnResourceAccepted(notify.Actor, self, resourceType, count, value); } + foreach (var rrd in refineryResourceDelivereds) + rrd.ResourceGiven(self, value); + + var purifiers = self.World.ActorsWithTrait().Where(x => x.Actor.Owner == self.Owner).Select(x => x.Trait); + foreach (var p in purifiers) + p.RefineAmount(value); + if (info.ShowTicks) currentDisplayValue += value; diff --git a/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoAircraft.cs b/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoAircraft.cs index 031e37808dd6..7d4333c418f4 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoAircraft.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoAircraft.cs @@ -26,7 +26,7 @@ public class TransformsIntoAircraftInfo : ConditionalTraitInfo, Requires DockActors = new() { }; + public readonly HashSet DockActors = new(); [VoiceReference] public readonly string Voice = "Action"; diff --git a/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoRepairable.cs b/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoRepairable.cs index 1435cd0554b2..e569dce00259 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoRepairable.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoRepairable.cs @@ -23,7 +23,7 @@ public class TransformsIntoRepairableInfo : ConditionalTraitInfo, Requires RepairActors = new() { }; + public readonly HashSet RepairActors = new(); [VoiceReference] public readonly string Voice = "Action"; diff --git a/OpenRA.Mods.Common/Traits/CapturableProgressBlink.cs b/OpenRA.Mods.Common/Traits/CapturableProgressBlink.cs index d7900d285546..7e8a21f7de3a 100644 --- a/OpenRA.Mods.Common/Traits/CapturableProgressBlink.cs +++ b/OpenRA.Mods.Common/Traits/CapturableProgressBlink.cs @@ -22,6 +22,15 @@ sealed class CapturableProgressBlinkInfo : ConditionalTraitInfo, Requires= Info.Interval) diff --git a/OpenRA.Mods.Common/Traits/CaptureManager.cs b/OpenRA.Mods.Common/Traits/CaptureManager.cs index c66f1bffe3e2..6e93847faea7 100644 --- a/OpenRA.Mods.Common/Traits/CaptureManager.cs +++ b/OpenRA.Mods.Common/Traits/CaptureManager.cs @@ -38,6 +38,14 @@ public class CaptureManagerInfo : TraitInfo [Desc("Should units friendly to the capturing actor auto-target this actor while it is being captured?")] public readonly bool PreventsAutoTarget = true; + [NotificationReference("Speech")] + [Desc("Notification to play when this actor is being captured.")] + public readonly string BeingCapturedNotification = null; + + [TranslationReference(optional: true)] + [Desc("Text notification to display when this actor is being captured.")] + public readonly string BeingCapturedTextNotification = null; + public override object Create(ActorInitializer init) { return new CaptureManager(init.Self, this); } } @@ -52,7 +60,10 @@ public class CaptureManager : INotifyCreated, INotifyCapture, ITick, IDisableEne BitSet allyCapturesTypes; BitSet neutralCapturesTypes; BitSet enemyCapturesTypes; - BitSet capturableTypes; + BitSet allyForceCapturesTypes; + BitSet neutralForceCapturesTypes; + BitSet enemyForceCapturesTypes; + public BitSet CapturableTypes; IEnumerable enabledCapturable; IEnumerable enabledCaptures; @@ -64,6 +75,7 @@ public class CaptureManager : INotifyCreated, INotifyCapture, ITick, IDisableEne int currentTargetTotal; int capturingToken = Actor.InvalidConditionToken; int beingCapturedToken = Actor.InvalidConditionToken; + bool beingCapturedNotificationPlayed = false; bool enteringCurrentTarget; readonly HashSet currentCaptors = new(); @@ -96,22 +108,29 @@ void INotifyCreated.Created(Actor self) public void RefreshCaptures() { allyCapturesTypes = neutralCapturesTypes = enemyCapturesTypes = default; + allyForceCapturesTypes = neutralForceCapturesTypes = enemyForceCapturesTypes = default; foreach (var c in enabledCaptures) { if (c.Info.ValidRelationships.HasRelationship(PlayerRelationship.Ally)) allyCapturesTypes = allyCapturesTypes.Union(c.Info.CaptureTypes); + if (c.Info.ForceTargetRelationships.HasRelationship(PlayerRelationship.Ally)) + allyForceCapturesTypes = allyForceCapturesTypes.Union(c.Info.CaptureTypes); if (c.Info.ValidRelationships.HasRelationship(PlayerRelationship.Neutral)) neutralCapturesTypes = neutralCapturesTypes.Union(c.Info.CaptureTypes); + if (c.Info.ForceTargetRelationships.HasRelationship(PlayerRelationship.Neutral)) + neutralForceCapturesTypes = neutralForceCapturesTypes.Union(c.Info.CaptureTypes); if (c.Info.ValidRelationships.HasRelationship(PlayerRelationship.Enemy)) enemyCapturesTypes = enemyCapturesTypes.Union(c.Info.CaptureTypes); + if (c.Info.ForceTargetRelationships.HasRelationship(PlayerRelationship.Enemy)) + enemyForceCapturesTypes = enemyForceCapturesTypes.Union(c.Info.CaptureTypes); } } public void RefreshCapturable() { - capturableTypes = enabledCapturable.Aggregate( + CapturableTypes = enabledCapturable.Aggregate( default(BitSet), (a, b) => a.Union(b.Info.Types)); } @@ -119,11 +138,17 @@ public void RefreshCapturable() /// Should only be called from the captor's CaptureManager. public bool CanTarget(CaptureManager target) { - return CanTarget(target.self.Owner, target.capturableTypes); + return CanTarget(target, false) || CanTarget(target, true); + } + + /// Should only be called from the captor's CaptureManager. + public bool CanTarget(CaptureManager target, bool forceCapture) + { + return CanTarget(target.self.Owner, target.CapturableTypes, forceCapture); } /// Should only be called from the captor CaptureManager. - public bool CanTarget(FrozenActor target) + public bool CanTarget(FrozenActor target, bool forceCapture) { if (!target.Info.HasTraitInfo()) return false; @@ -135,20 +160,20 @@ public bool CanTarget(FrozenActor target) default(BitSet), (a, b) => a.Union(b.Types)); - return CanTarget(target.Owner, targetTypes); + return CanTarget(target.Owner, targetTypes, forceCapture); } - bool CanTarget(Player target, BitSet captureTypes) + bool CanTarget(Player target, BitSet captureTypes, bool forceCapture) { var relationship = self.Owner.RelationshipWith(target); if (relationship.HasRelationship(PlayerRelationship.Enemy)) - return captureTypes.Overlaps(enemyCapturesTypes); + return captureTypes.Overlaps(forceCapture ? enemyForceCapturesTypes : enemyCapturesTypes); if (relationship.HasRelationship(PlayerRelationship.Neutral)) - return captureTypes.Overlaps(neutralCapturesTypes); + return captureTypes.Overlaps(forceCapture ? neutralForceCapturesTypes : neutralCapturesTypes); if (relationship.HasRelationship(PlayerRelationship.Ally)) - return captureTypes.Overlaps(allyCapturesTypes); + return captureTypes.Overlaps(forceCapture ? allyForceCapturesTypes : allyCapturesTypes); return false; } @@ -160,7 +185,7 @@ public Captures ValidCapturesWithLowestSabotageThreshold(CaptureManager target) var relationship = self.Owner.RelationshipWith(target.self.Owner); foreach (var c in enabledCaptures.OrderBy(c => c.Info.SabotageThreshold).ThenBy(c => c.Info.CaptureDelay)) - if (c.Info.ValidRelationships.HasRelationship(relationship) && target.capturableTypes.Overlaps(c.Info.CaptureTypes)) + if (c.Info.ValidRelationships.HasRelationship(relationship) && target.CapturableTypes.Overlaps(c.Info.CaptureTypes)) return c; return null; @@ -214,6 +239,16 @@ public bool StartCapture(CaptureManager targetManager, out Captures captures) if (enterMobile != null && enterMobile.IsMovingBetweenCells) return false; + if (!targetManager.beingCapturedNotificationPlayed) + { + TextNotificationsManager.AddTransientLine(target.Owner, targetManager.info.BeingCapturedTextNotification); + + if (targetManager.info.BeingCapturedNotification != null) + Game.Sound.PlayNotification(self.World.Map.Rules, target.Owner, "Speech", targetManager.info.BeingCapturedNotification, target.Owner.Faction.InternalName); + + targetManager.beingCapturedNotificationPlayed = true; + } + if (progressWatchers.Length > 0 || targetManager.progressWatchers.Length > 0) { currentTargetTotal = captures.Info.CaptureDelay; @@ -260,6 +295,7 @@ public void CancelCapture(CaptureManager targetManager) if (targetManager.beingCapturedToken != Actor.InvalidConditionToken) targetManager.beingCapturedToken = target.RevokeCondition(targetManager.beingCapturedToken); + targetManager.beingCapturedNotificationPlayed = false; targetManager.currentCaptors.Remove(self); } diff --git a/OpenRA.Mods.Common/Traits/Captures.cs b/OpenRA.Mods.Common/Traits/Captures.cs index 2b5f3513e252..56269f27036e 100644 --- a/OpenRA.Mods.Common/Traits/Captures.cs +++ b/OpenRA.Mods.Common/Traits/Captures.cs @@ -10,6 +10,7 @@ #endregion using System.Collections.Generic; +using System.Linq; using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Orders; using OpenRA.Primitives; @@ -46,6 +47,9 @@ public class CapturesInfo : ConditionalTraitInfo, Requires [Desc("What player relationships the target's owner needs to be captured by this actor.")] public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Neutral | PlayerRelationship.Enemy; + [Desc("What player relationships the target's owner needs to be captured by this actor by force-fire.")] + public readonly PlayerRelationship ForceTargetRelationships = PlayerRelationship.Neutral | PlayerRelationship.Enemy; + [Desc("Relationships that the structure's previous owner needs to have for the capturing player to receive Experience.")] public readonly PlayerRelationship PlayerExperienceRelationships = PlayerRelationship.Enemy; @@ -64,6 +68,9 @@ public class CapturesInfo : ConditionalTraitInfo, Requires [VoiceReference] public readonly string Voice = "Action"; + [VoiceReference] + public readonly string CaptureCompleteVoice = null; + [Desc("Color to use for the target line.")] public readonly Color TargetLineColor = Color.Crimson; @@ -113,6 +120,33 @@ public void ResolveOrder(Actor self, Order order) self.ShowTargetLines(); } + bool CanTarget(Actor self, Player target, BitSet captureTypes, bool forceCapture) + { + if (!captureTypes.Overlaps(Info.CaptureTypes)) + return false; + + var validRelationships = forceCapture ? Info.ForceTargetRelationships : Info.ValidRelationships; + if (!validRelationships.HasRelationship(self.Owner.RelationshipWith(target))) + return false; + + return true; + } + + public bool CanTarget(Actor self, FrozenActor target, bool forceCapture) + { + if (!target.Info.HasTraitInfo()) + return false; + + // TODO: FrozenActors don't yet have a way of caching conditions, so we can't filter disabled traits + // This therefore assumes that all Capturable traits are enabled, which is probably wrong. + // Actors with FrozenUnderFog should therefore not disable the Capturable trait. + var targetTypes = target.Info.TraitInfos().Aggregate( + default(BitSet), + (a, b) => a.Union(b.Types)); + + return CanTarget(self, target.Owner, targetTypes, forceCapture); + } + protected override void TraitEnabled(Actor self) { CaptureManager.RefreshCaptures(); } protected override void TraitDisabled(Actor self) { CaptureManager.RefreshCaptures(); } @@ -129,7 +163,7 @@ public CaptureOrderTargeter(Captures captures) public override bool CanTargetActor(Actor self, Actor target, TargetModifiers modifiers, ref string cursor) { var targetManager = target.TraitOrDefault(); - if (targetManager == null || !captures.CaptureManager.CanTarget(targetManager)) + if (targetManager == null || !captures.CanTarget(self, target.Owner, targetManager.CapturableTypes, modifiers.HasModifier(TargetModifiers.ForceAttack))) { cursor = captures.Info.EnterBlockedCursor; return false; @@ -150,7 +184,7 @@ public override bool CanTargetActor(Actor self, Actor target, TargetModifiers mo public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor) { - if (!captures.CaptureManager.CanTarget(target)) + if (!captures.CanTarget(self, target, modifiers.HasModifier(TargetModifiers.ForceAttack))) { cursor = captures.Info.EnterBlockedCursor; return false; diff --git a/OpenRA.Mods.Common/Traits/Cargo.cs b/OpenRA.Mods.Common/Traits/Cargo.cs index fc975eeb4043..5431f2904f96 100644 --- a/OpenRA.Mods.Common/Traits/Cargo.cs +++ b/OpenRA.Mods.Common/Traits/Cargo.cs @@ -14,14 +14,13 @@ using System.Linq; using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Orders; -using OpenRA.Mods.Common.Widgets; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("This actor can transport Passenger actors.")] - public class CargoInfo : ConditionalTraitInfo, Requires + public class CargoInfo : PausableConditionalTraitInfo, Requires { [Desc("The maximum sum of Passenger.Weight that this actor can support.")] public readonly int MaxWeight = 0; @@ -32,11 +31,14 @@ public class CargoInfo : ConditionalTraitInfo, Requires [Desc("A list of actor types that are initially spawned into this actor.")] public readonly string[] InitialUnits = Array.Empty(); + [Desc("When cargo is full, unload the first passenger instead of disabling loading.")] + public readonly bool ReplaceFirstWhenFull = false; + [Desc("When this actor is sold should all of its passengers be unloaded?")] public readonly bool EjectOnSell = true; - [Desc("When this actor dies should all of its passengers be unloaded?")] - public readonly bool EjectOnDeath = false; + [Desc("When this actor dies this much percent of passengers total health is dealt to them.")] + public readonly int EjectOnDeathDamage = 100; [Desc("Terrain types that this actor is allowed to eject actors onto. Leave empty for all terrain types.")] public readonly HashSet UnloadTerrainTypes = new(); @@ -82,15 +84,18 @@ public class CargoInfo : ConditionalTraitInfo, Requires "A dictionary of [actor name]: [condition].")] public readonly Dictionary PassengerConditions = new(); + [Desc("Change the passengers owner if transport owner changed")] + public readonly bool OwnerChangedAffectsPassengers = true; + [GrantedConditionReference] public IEnumerable LinterPassengerConditions => PassengerConditions.Values; public override object Create(ActorInitializer init) { return new Cargo(init, this); } } - public class Cargo : ConditionalTrait, IIssueOrder, IResolveOrder, IOrderVoice, - INotifyOwnerChanged, INotifySold, INotifyActorDisposing, IIssueDeployOrder, - INotifyCreated, INotifyKilled, ITransformActorInitModifier + public class Cargo : PausableConditionalTrait, IIssueOrder, IResolveOrder, IOrderVoice, + INotifyOwnerChanged, INotifySold, INotifyActorDisposing, IIssueDeployOrder, INotifyAddedToWorld, + INotifyCreated, INotifyKilled, ITransformActorInitModifier, INotifyPassengersDamage, ITick { readonly Actor self; readonly List cargo = new(); @@ -99,7 +104,7 @@ public class Cargo : ConditionalTrait, IIssueOrder, IResolveOrder, IO readonly Lazy facing; readonly bool checkTerrainType; - int totalWeight = 0; + public int TotalWeight = 0; int reservedWeight = 0; Aircraft aircraft; int loadingToken = Actor.InvalidConditionToken; @@ -107,10 +112,8 @@ public class Cargo : ConditionalTrait, IIssueOrder, IResolveOrder, IO bool takeOffAfterLoad; bool initialised; - readonly CachedTransform currentAdjacentCells; - - public CPos[] CurrentAdjacentCells => currentAdjacentCells.Update(self.Location); - + CPos currentCell; + public IEnumerable CurrentAdjacentCells { get; private set; } public IEnumerable Passengers => cargo; public int PassengerCount => cargo.Count; @@ -123,15 +126,12 @@ public Cargo(ActorInitializer init, CargoInfo info) self = init.Self; checkTerrainType = info.UnloadTerrainTypes.Count > 0; - currentAdjacentCells = new CachedTransform(loc => - Util.AdjacentCells(self.World, Target.FromActor(self)).Where(c => loc != c).ToArray()); - var runtimeCargoInit = init.GetOrDefault(info); var cargoInit = init.GetOrDefault(info); if (runtimeCargoInit != null) { cargo = runtimeCargoInit.Value.ToList(); - totalWeight = cargo.Sum(c => GetWeight(c)); + TotalWeight = cargo.Sum(c => GetWeight(c)); } else if (cargoInit != null) { @@ -143,7 +143,7 @@ public Cargo(ActorInitializer init, CargoInfo info) cargo.Add(unit); } - totalWeight = cargo.Sum(c => GetWeight(c)); + TotalWeight = cargo.Sum(c => GetWeight(c)); } else { @@ -155,7 +155,7 @@ public Cargo(ActorInitializer init, CargoInfo info) cargo.Add(unit); } - totalWeight = cargo.Sum(c => GetWeight(c)); + TotalWeight = cargo.Sum(c => GetWeight(c)); } facing = Exts.Lazy(self.TraitOrDefault); @@ -234,6 +234,11 @@ public void ResolveOrder(Actor self, Order order) } } + IEnumerable GetAdjacentCells() + { + return Util.AdjacentCells(self.World, Target.FromActor(self)).Where(c => self.Location != c); + } + public bool CanUnload(BlockedByActor check = BlockedByActor.None) { if (IsTraitDisabled) @@ -248,13 +253,13 @@ public bool CanUnload(BlockedByActor check = BlockedByActor.None) return false; } - return !IsEmpty() && (aircraft == null || aircraft.CanLand(self.Location, blockedByMobile: false)) + return !IsEmpty() && (aircraft == null || aircraft.CanLand(self.Location, blockedByMobile: false)) && !IsTraitPaused && CurrentAdjacentCells != null && CurrentAdjacentCells.Any(c => Passengers.Any(p => !p.IsDead && p.Trait().CanEnterCell(c, null, check))); } public bool CanLoad(Actor a) { - return !IsTraitDisabled && (reserves.Contains(a) || HasSpace(GetWeight(a))); + return !IsTraitDisabled && !IsTraitPaused && (reserves.Contains(a) || HasSpace(GetWeight(a))); } internal bool ReserveSpace(Actor a) @@ -331,7 +336,7 @@ public string VoicePhraseForOrder(Actor self, Order order) return Info.UnloadVoice; } - public bool HasSpace(int weight) { return totalWeight + reservedWeight + weight <= Info.MaxWeight; } + public bool HasSpace(int weight) { return Info.ReplaceFirstWhenFull || TotalWeight + reservedWeight + weight <= Info.MaxWeight; } public bool IsEmpty() { return cargo.Count == 0; } public Actor Peek() { return cargo.Last(); } @@ -342,7 +347,7 @@ public Actor Unload(Actor self, Actor passenger = null) if (!cargo.Remove(passenger)) throw new ArgumentException("Attempted to unload an actor that is not a passenger."); - totalWeight -= GetWeight(passenger); + TotalWeight -= GetWeight(passenger); SetPassengerFacing(passenger); @@ -374,11 +379,46 @@ void SetPassengerFacing(Actor passenger) passengerFacing.Facing = facing.Value.Facing + Info.PassengerFacing; } + public (CPos Cell, SubCell SubCell)? ChooseExitSubCell(Actor passenger) + { + var pos = passenger.Trait(); + + return CurrentAdjacentCells.Shuffle(self.World.SharedRandom) + .Select(c => (c, pos.GetAvailableSubCell(c))) + .Cast<(CPos, SubCell SubCell)?>() + .FirstOrDefault(s => s.Value.SubCell != SubCell.Invalid); + } + public void Load(Actor self, Actor a) { - cargo.Add(a); var w = GetWeight(a); - totalWeight += w; + TotalWeight += w; + + while (Info.ReplaceFirstWhenFull && TotalWeight > Info.MaxWeight) + { + var passenger = Unload(self, cargo.First()); + var cp = self.CenterPosition; + var inAir = self.World.Map.DistanceAboveTerrain(cp).Length != 0; + var positionable = passenger.Trait(); + var exitSubCell = ChooseExitSubCell(passenger); + if (exitSubCell != null) + positionable.SetPosition(passenger, exitSubCell.Value.Cell, exitSubCell.Value.SubCell); + else + positionable.SetPosition(passenger, self.Location); + positionable.SetCenterPosition(passenger, self.CenterPosition); + + if (self.Owner.WinState != WinState.Lost && !inAir && positionable.CanEnterCell(self.Location, self, BlockedByActor.None)) + { + self.World.AddFrameEndTask(world => world.Add(passenger)); + var nbms = passenger.TraitsImplementing(); + foreach (var nbm in nbms) + nbm.OnNotifyBlockingMove(passenger, passenger); + } + else + passenger.Kill(self); + } + + cargo.Add(a); if (reserves.Contains(a)) { reservedWeight -= w; @@ -409,9 +449,9 @@ public void Load(Actor self, Actor a) } void INotifyKilled.Killed(Actor self, AttackInfo e) - { + { // IsAtGroundLevel contains Map.Contains(self.Location) check. - if (Info.EjectOnDeath && self.IsAtGroundLevel() && (!checkTerrainType || Info.UnloadTerrainTypes.Contains(self.World.Map.GetTerrainInfo(self.Location).Type))) + if (Info.EjectOnDeathDamage < 100 && self.IsAtGroundLevel() && (!checkTerrainType || Info.UnloadTerrainTypes.Contains(self.World.Map.GetTerrainInfo(self.Location).Type))) { while (!IsEmpty()) { @@ -421,15 +461,26 @@ void INotifyKilled.Killed(Actor self, AttackInfo e) var positionable = passenger.Trait(); if (positionable.CanEnterCell(self.Location, self, BlockedByActor.All)) { - positionable.SetPosition(passenger, self.Location); + var exitSubCell = ChooseExitSubCell(passenger); + if (exitSubCell != null) + positionable.SetPosition(passenger, exitSubCell.Value.Cell, exitSubCell.Value.SubCell); + else + positionable.SetPosition(passenger, self.Location); + positionable.SetCenterPosition(passenger, self.CenterPosition); w.Add(passenger); var nbms = passenger.TraitsImplementing(); foreach (var nbm in nbms) nbm.OnNotifyBlockingMove(passenger, passenger); - // For show. - passenger.QueueActivity(new Nudge(passenger)); + var health = passenger.TraitOrDefault(); + if (Info.EjectOnDeathDamage > 0 && health != null) + { + var damage = health.MaxHP * Info.EjectOnDeathDamage / 100; + health.InflictDamage(passenger, e.Attacker, new Damage(damage, e.Damage.DamageTypes), true); + } + + passenger.Trait().OnEjectedFromKilledCargo(passenger); } else passenger.Kill(e.Attacker); @@ -474,17 +525,57 @@ void SpawnPassenger(Actor passenger) void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) { - if (cargo == null) + if (!Info.OwnerChangedAffectsPassengers || cargo == null) return; foreach (var p in Passengers) p.ChangeOwner(newOwner); } + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + // Force location update to avoid issues when initial spawn is outside map + currentCell = self.Location; + CurrentAdjacentCells = GetAdjacentCells(); + } + + void ITick.Tick(Actor self) + { + var cell = self.World.Map.CellContaining(self.CenterPosition); + if (currentCell != cell) + { + currentCell = cell; + CurrentAdjacentCells = GetAdjacentCells(); + } + } + void ITransformActorInitModifier.ModifyTransformActorInit(Actor self, TypeDictionary init) { init.Add(new RuntimeCargoInit(Info, Passengers.ToArray())); } + + static int DamageVersus(Actor victim, Dictionary versus) + { + // If no Versus values are defined, DamageVersus would return 100 anyway, so we might as well do that early. + if (versus.Count == 0 || victim.IsDead) + return 100; + + var armor = victim.TraitsImplementing() + .Where(a => !a.IsTraitDisabled && a.Info.Type != null && versus.ContainsKey(a.Info.Type)) + .Select(a => versus[a.Info.Type]); + + return Util.ApplyPercentageModifiers(100, armor); + } + + void INotifyPassengersDamage.DamagePassengers(int damage, Actor attacker, int amount, Dictionary versus, BitSet damageTypes, IEnumerable damageModifiers) + { + var passengersToDamage = amount > 0 && amount < cargo.Count ? cargo.Shuffle(self.World.SharedRandom).Take(amount).ToArray() : cargo.ToArray(); + foreach (var passenger in passengersToDamage) + { + var d = Util.ApplyPercentageModifiers(damage, damageModifiers.Append(DamageVersus(passenger, versus))); + passenger.InflictDamage(attacker, new Damage(d, damageTypes)); + } + } } public class RuntimeCargoInit : ValueActorInit, ISuppressInitExport diff --git a/OpenRA.Mods.Common/Traits/CashTrickler.cs b/OpenRA.Mods.Common/Traits/CashTrickler.cs index 75c6dc499835..0c6745fc6e5b 100644 --- a/OpenRA.Mods.Common/Traits/CashTrickler.cs +++ b/OpenRA.Mods.Common/Traits/CashTrickler.cs @@ -27,6 +27,9 @@ public class CashTricklerInfo : PausableConditionalTraitInfo, IRulesetLoaded [Desc("Amount of money to give each time.")] public readonly int Amount = 15; + [Desc("Only shows the cash ticks, but does not actually give the cash.")] + public readonly bool Fake = false; + [Desc("Whether to show the cash tick indicators rising from the actor.")] public readonly bool ShowTicks = true; @@ -96,14 +99,17 @@ void AddCashTick(Actor self, int amount) void ModifyCash(Actor self, int amount) { - if (info.UseResourceStorage) + if (!info.Fake) { - var initialAmount = resources.Resources; - resources.GiveResources(amount); - amount = resources.Resources - initialAmount; + if (info.UseResourceStorage) + { + var initialAmount = resources.Resources; + resources.GiveResources(amount); + amount = resources.Resources - initialAmount; + } + else + amount = resources.ChangeCash(amount); } - else - amount = resources.ChangeCash(amount); if (info.ShowTicks && amount != 0) AddCashTick(self, amount); diff --git a/OpenRA.Mods.Common/Traits/Cloak.cs b/OpenRA.Mods.Common/Traits/Cloak.cs index 9f99043060e7..2b5cb4d88bff 100644 --- a/OpenRA.Mods.Common/Traits/Cloak.cs +++ b/OpenRA.Mods.Common/Traits/Cloak.cs @@ -57,6 +57,12 @@ public class CloakInfo : PausableConditionalTraitInfo public readonly string CloakSound = null; public readonly string UncloakSound = null; + [Desc("Do the sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the CloakSound and UncloakSound played at.")] + public readonly float SoundVolume = 1f; + [PaletteReference(nameof(IsPlayerPalette))] public readonly string Palette = "cloak"; public readonly bool IsPlayerPalette = false; @@ -91,12 +97,17 @@ public class CloakInfo : PausableConditionalTraitInfo [Desc("Should the effect track the actor.")] public readonly bool EffectTracksActor = true; + [Desc("This unit uncloaks when ForceUncloakManager asked to. Needs ForceUncloakManager on Player Actor.")] + public readonly bool CanBeForcedUncloak = false; + public override object Create(ActorInitializer init) { return new Cloak(this); } } public class Cloak : PausableConditionalTrait, IRenderModifier, INotifyDamage, INotifyUnloadCargo, INotifyLoadCargo, INotifyDemolition, INotifyInfiltration, INotifyAttack, ITick, IVisibilityModifier, IRadarColorModifier, INotifyCreated, INotifyDockClient, INotifySupportPower { + ForceUncloakManager forceUncloakManager; + [Sync] int remainingTime; @@ -116,6 +127,8 @@ public Cloak(CloakInfo info) protected override void Created(Actor self) { + forceUncloakManager = self.Owner.PlayerActor.TraitsImplementing().FirstOrDefault(); + if (Info.CloakType != null) { otherCloaks = self.TraitsImplementing() @@ -202,7 +215,8 @@ void ITick.Tick(Actor self) if (!(firstTick && Info.InitialDelay == 0) && (otherCloaks == null || !otherCloaks.Any(a => a.Cloaked))) { var pos = self.CenterPosition; - Game.Sound.Play(SoundType.World, Info.CloakSound, self.CenterPosition); + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, Info.CloakSound, pos, Info.SoundVolume); Func posfunc = () => self.CenterPosition + Info.EffectOffset; if (!Info.EffectTracksActor) @@ -227,7 +241,8 @@ void ITick.Tick(Actor self) if (!(firstTick && Info.InitialDelay == 0) && (otherCloaks == null || !otherCloaks.Any(a => a.Cloaked))) { var pos = self.CenterPosition; - Game.Sound.Play(SoundType.World, Info.CloakSound, pos); + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, Info.UncloakSound, pos, Info.SoundVolume); Func posfunc = () => self.CenterPosition + Info.EffectOffset; if (!Info.EffectTracksActor) @@ -261,8 +276,11 @@ public bool IsVisible(Actor self, Player viewer) if (!Cloaked || self.Owner.IsAlliedWith(viewer)) return true; - return self.World.ActorsWithTrait().Any(a => a.Actor.Owner.IsAlliedWith(viewer) - && Info.DetectionTypes.Overlaps(a.Trait.Info.DetectionTypes) + if (Info.CanBeForcedUncloak && forceUncloakManager != null && forceUncloakManager.ForcedUncloak && !forceUncloakManager.IsTraitDisabled) + return true; + + return self.World.ActorsWithTrait().Any(a => a.Actor.IsInWorld + && a.Actor.Owner.IsAlliedWith(viewer) && Info.DetectionTypes.Overlaps(a.Trait.Info.DetectionTypes) && (self.CenterPosition - a.Actor.CenterPosition).LengthSquared <= a.Trait.Range.LengthSquared); } @@ -313,7 +331,7 @@ void INotifyInfiltration.Infiltrating(Actor self) Uncloak(); } - void INotifySupportPower.Charged(Actor self) { return; } + void INotifySupportPower.Charged(Actor self) { } void INotifySupportPower.Activated(Actor self) { diff --git a/OpenRA.Mods.Common/Traits/ConditionPrerequisite.cs b/OpenRA.Mods.Common/Traits/ConditionPrerequisite.cs new file mode 100644 index 000000000000..f7ccf402f674 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/ConditionPrerequisite.cs @@ -0,0 +1,106 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("To produce some specific actors, this trait should be enabled on the actor.")] + public class ConditionPrerequisiteInfo : PausableConditionalTraitInfo, Requires + { + [ActorReference] + [FieldLoader.Require] + [Desc("Actor that this condition will apply.")] + public readonly string Actor = null; + + [FieldLoader.Require] + [Desc("Queues that this condition will apply.")] + public readonly HashSet Queue = new(); + + public override object Create(ActorInitializer init) { return new ConditionPrerequisite(init.Self, this); } + } + + public class ConditionPrerequisite : PausableConditionalTrait, INotifyCreated + { + readonly ProductionQueue[] queues; + + public ConditionPrerequisite(Actor self, ConditionPrerequisiteInfo info) + : base(info) + { + queues = self.TraitsImplementing().Where(t => Info.Queue.Contains(t.Info.Type)).ToArray(); + } + + protected override void Created(Actor self) + { + if (Info.RequiresCondition == null) + { + foreach (var queue in queues.Where(t => t.Enabled)) + { + queue.CacheProducibles(); + queue.Producible[self.World.Map.Rules.Actors[Info.Actor]].Visible = true; + if (!IsTraitPaused) + queue.Producible[self.World.Map.Rules.Actors[Info.Actor]].Buildable = true; + } + } + + if (IsTraitDisabled) + { + foreach (var queue in queues.Where(t => t.Enabled)) + { + queue.CacheProducibles(); + queue.Producible[self.World.Map.Rules.Actors[Info.Actor]].Visible = false; + } + } + + base.Created(self); + } + + protected override void TraitEnabled(Actor self) + { + foreach (var queue in queues.Where(t => t.Enabled)) + { + queue.CacheProducibles(); + queue.Producible[self.World.Map.Rules.Actors[Info.Actor]].Visible = true; + if (!IsTraitPaused) + queue.Producible[self.World.Map.Rules.Actors[Info.Actor]].Buildable = true; + } + } + + protected override void TraitDisabled(Actor self) + { + foreach (var queue in queues.Where(t => t.Enabled)) + { + queue.CacheProducibles(); + queue.Producible[self.World.Map.Rules.Actors[Info.Actor]].Visible = false; + } + } + + protected override void TraitPaused(Actor self) + { + foreach (var queue in queues.Where(t => t.Enabled)) + { + queue.CacheProducibles(); + queue.Producible[self.World.Map.Rules.Actors[Info.Actor]].Buildable = false; + } + } + + protected override void TraitResumed(Actor self) + { + foreach (var queue in queues.Where(t => t.Enabled)) + { + queue.CacheProducibles(); + queue.Producible[self.World.Map.Rules.Actors[Info.Actor]].Buildable = true; + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnAttack.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnAttack.cs index 490bdaaa2410..3a171784b1f8 100644 --- a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnAttack.cs +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnAttack.cs @@ -10,6 +10,7 @@ #endregion using System.Collections.Generic; +using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -43,21 +44,28 @@ public class GrantConditionOnAttackInfo : PausableConditionalTraitInfo [Desc("Should all instances be revoked instead of only one?")] public readonly bool RevokeAll = false; + public readonly bool ShowSelectionBar = false; + public readonly Color SelectionBarColor = Color.Magenta; + public override object Create(ActorInitializer init) { return new GrantConditionOnAttack(this); } } - public class GrantConditionOnAttack : PausableConditionalTrait, INotifyCreated, ITick, INotifyAttack + public class GrantConditionOnAttack : PausableConditionalTrait, INotifyCreated, ITick, INotifyAttack, ISelectionBar { readonly Stack tokens = new(); int cooldown = 0; int shotsFired = 0; + int requiredShots = 0; // Only tracked when RevokeOnNewTarget is true. Target lastTarget = Target.Invalid; public GrantConditionOnAttack(GrantConditionOnAttackInfo info) - : base(info) { } + : base(info) + { + requiredShots = Info.RequiredShotsPerInstance[0]; + } void GrantInstance(Actor self, string cond) { @@ -83,7 +91,7 @@ void RevokeInstance(Actor self, bool revokeAll) void ITick.Tick(Actor self) { - if (tokens.Count > 0 && --cooldown == 0) + if ((shotsFired > 0 || tokens.Count > 0) && --cooldown == 0) { cooldown = Info.RevokeDelay; RevokeInstance(self, Info.RevokeAll); @@ -142,7 +150,7 @@ void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel ba return; shotsFired++; - var requiredShots = tokens.Count < Info.RequiredShotsPerInstance.Length + requiredShots = tokens.Count < Info.RequiredShotsPerInstance.Length ? Info.RequiredShotsPerInstance[tokens.Count] : Info.RequiredShotsPerInstance[^1]; @@ -159,6 +167,21 @@ void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel ba void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + float ISelectionBar.GetValue() + { + if (IsTraitDisabled || !Info.ShowSelectionBar) + return 0f; + + if (1f - (float)shotsFired / requiredShots > 1f) + return 1f; + + return (float)shotsFired / requiredShots; + } + + bool ISelectionBar.DisplayWhenEmpty { get { return false; } } + + Color ISelectionBar.GetColor() { return Info.SelectionBarColor; } + protected override void TraitDisabled(Actor self) { RevokeInstance(self, true); diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnClientDock.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnClientDock.cs new file mode 100644 index 000000000000..08e2442130cd --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnClientDock.cs @@ -0,0 +1,86 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public sealed class GrantConditionOnClientDockInfo : TraitInfo + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant to self")] + public readonly string Condition = null; + + [Desc("How long condition is applied even after undock. Use -1 for infinite.")] + public readonly int AfterDockDuration = 0; + + [Desc("Host actor type(s) leading to the condition being granted. Leave empty for allowing all hosts by default.")] + public readonly HashSet DockHostNames = null; + + public override object Create(ActorInitializer init) { return new GrantConditionOnClientDock(this); } + } + + public sealed class GrantConditionOnClientDock : INotifyDockClient, ITick, ISync + { + readonly GrantConditionOnClientDockInfo info; + int token; + int delayedtoken; + + [Sync] + public int Duration { get; private set; } + + public GrantConditionOnClientDock(GrantConditionOnClientDockInfo info) + { + this.info = info; + token = Actor.InvalidConditionToken; + delayedtoken = Actor.InvalidConditionToken; + } + + void INotifyDockClient.Docked(Actor self, Actor host) + { + if (info.Condition != null && (info.DockHostNames == null || info.DockHostNames.Contains(host.Info.Name))) + { + if (token == Actor.InvalidConditionToken) + { + if (delayedtoken == Actor.InvalidConditionToken) + token = self.GrantCondition(info.Condition); + else + { + token = delayedtoken; + delayedtoken = Actor.InvalidConditionToken; + } + } + } + } + + void INotifyDockClient.Undocked(Actor self, Actor host) + { + if (token == Actor.InvalidConditionToken || info.AfterDockDuration < 0) + return; + if (info.AfterDockDuration == 0) + token = self.RevokeCondition(token); + else + { + delayedtoken = token; + token = Actor.InvalidConditionToken; + Duration = info.AfterDockDuration; + } + } + + void ITick.Tick(Actor self) + { + if (delayedtoken != Actor.InvalidConditionToken && --Duration <= 0) + delayedtoken = self.RevokeCondition(delayedtoken); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDeploy.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDeploy.cs index 9a42a7073a24..8ff3ce2e90d1 100644 --- a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDeploy.cs +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDeploy.cs @@ -37,6 +37,9 @@ public class GrantConditionOnDeployInfo : PausableConditionalTraitInfo, IEditorA [Desc("Can this actor deploy on slopes?")] public readonly bool CanDeployOnRamps = false; + [Desc("Does this actor need to synchronize it's deployment with other actors?")] + public readonly bool SmartDeploy = false; + [CursorReference] [Desc("Cursor to display when able to (un)deploy the actor.")] public readonly string DeployCursor = "deploy"; @@ -54,6 +57,9 @@ public class GrantConditionOnDeployInfo : PausableConditionalTraitInfo, IEditorA [Desc("Play a randomly selected sound from this list when undeploying.")] public readonly string[] UndeploySounds = null; + [Desc("Do the deploy and undeploy sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + [Desc("Skip make/deploy animation?")] public readonly bool SkipMakeAnimation = false; @@ -66,6 +72,10 @@ public class GrantConditionOnDeployInfo : PausableConditionalTraitInfo, IEditorA [VoiceReference] public readonly string Voice = "Action"; + [VoiceReference] + [Desc("Override the voicelines used for undeploying.")] + public readonly string UndeployVoice = null; + [Desc("Display order for the deployed checkbox in the map editor")] public readonly int EditorDeployedDisplayOrder = 4; @@ -183,7 +193,45 @@ Order IIssueDeployOrder.IssueDeployOrder(Actor self, bool queued) return new Order("GrantConditionOnDeploy", self, queued); } - bool IIssueDeployOrder.CanIssueDeployOrder(Actor self, bool queued) { return !IsTraitPaused && !IsTraitDisabled; } + bool IIssueDeployOrder.CanIssueDeployOrder(Actor self, bool queued) + { + return !IsTraitPaused && !IsTraitDisabled && IsGroupDeployNeeded(self); + } + + bool IsGroupDeployNeeded(Actor self) + { + if (!Info.SmartDeploy) + return true; + + var actors = self.World.Selection.Actors; + + var hasDeployedActors = false; + var hasUndeployedActors = false; + + foreach (var a in actors) + { + GrantConditionOnDeploy gcod = null; + if (!a.IsDead && a.IsInWorld) + gcod = a.TraitOrDefault(); + + if (!hasDeployedActors && gcod != null && (gcod.DeployState == DeployState.Deploying || gcod.DeployState == DeployState.Deployed)) + hasDeployedActors = true; + + if (!hasUndeployedActors && gcod != null && (gcod.DeployState == DeployState.Undeploying || gcod.DeployState == DeployState.Undeployed)) + hasUndeployedActors = true; + + if (!self.IsDead && !self.Disposed && hasDeployedActors && hasUndeployedActors) + { + var self_gcod = self.TraitOrDefault(); + if (self_gcod.DeployState == DeployState.Undeploying || self_gcod.DeployState == DeployState.Undeployed) + return true; + + return false; + } + } + + return true; + } public void ResolveOrder(Actor self, Order order) { @@ -198,7 +246,13 @@ public void ResolveOrder(Actor self, Order order) public string VoicePhraseForOrder(Actor self, Order order) { - return order.OrderString == "GrantConditionOnDeploy" ? Info.Voice : null; + if (order.OrderString != "GrantConditionOnDeploy") + return null; + + if (Info.UndeployVoice != null && DeployState == DeployState.Deployed) + return Info.UndeployVoice; + + return Info.Voice; } bool CanDeploy() @@ -258,7 +312,11 @@ void Deploy(bool init) return; if (Info.DeploySounds != null && Info.DeploySounds.Length > 0) - Game.Sound.Play(SoundType.World, Info.DeploySounds, self.World, self.CenterPosition); + { + var pos = self.CenterPosition; + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, Info.DeploySounds, self.World, pos); + } // Revoke condition that is applied while undeployed. if (!init) @@ -282,7 +340,11 @@ void Undeploy(bool init) return; if (Info.UndeploySounds != null && Info.UndeploySounds.Length > 0) - Game.Sound.Play(SoundType.World, Info.UndeploySounds, self.World, self.CenterPosition); + { + var pos = self.CenterPosition; + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, Info.UndeploySounds, self.World, pos); + } if (!init) OnUndeployStarted(); diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDeployWithCharge.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDeployWithCharge.cs index 4aabf3163b30..ed718caeb2d5 100644 --- a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDeployWithCharge.cs +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDeployWithCharge.cs @@ -18,7 +18,7 @@ namespace OpenRA.Mods.Common.Traits { [Desc("Allow deploying on specified charge to grant a condition for a specified duration.")] - public class GrantConditionOnDeployWithChargeInfo : PausableConditionalTraitInfo, Requires, IRulesetLoaded + public class GrantConditionOnDeployWithChargeInfo : PausableConditionalTraitInfo, IRulesetLoaded { [FieldLoader.Require] [GrantedConditionReference] @@ -256,8 +256,6 @@ public override IEnumerable TargetLineNodes(Actor self) if (NextActivity != null) foreach (var n in NextActivity.TargetLineNodes(self)) yield return n; - - yield break; } } } diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnHostDock.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnHostDock.cs new file mode 100644 index 000000000000..cb4c9f627564 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnHostDock.cs @@ -0,0 +1,86 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public sealed class GrantConditionOnHostDockInfo : TraitInfo + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant to self")] + public readonly string Condition = null; + + [Desc("How long condition is applied even after undock. Use -1 for infinite.")] + public readonly int AfterDockDuration = 0; + + [Desc("Client actor type(s) leading to the condition being granted. Leave empty for allowing all clients by default.")] + public readonly HashSet DockClientNames = null; + + public override object Create(ActorInitializer init) { return new GrantConditionOnHostDock(this); } + } + + public sealed class GrantConditionOnHostDock : INotifyDockHost, ITick, ISync + { + readonly GrantConditionOnHostDockInfo info; + int token; + int delayedtoken; + + [Sync] + public int Duration { get; private set; } + + public GrantConditionOnHostDock(GrantConditionOnHostDockInfo info) + { + this.info = info; + token = Actor.InvalidConditionToken; + delayedtoken = Actor.InvalidConditionToken; + } + + void INotifyDockHost.Docked(Actor self, Actor client) + { + if (info.Condition != null && (info.DockClientNames == null || info.DockClientNames.Contains(client.Info.Name))) + { + if (token == Actor.InvalidConditionToken) + { + if (delayedtoken == Actor.InvalidConditionToken) + token = self.GrantCondition(info.Condition); + else + { + token = delayedtoken; + delayedtoken = Actor.InvalidConditionToken; + } + } + } + } + + void INotifyDockHost.Undocked(Actor self, Actor client) + { + if (token == Actor.InvalidConditionToken || info.AfterDockDuration < 0) + return; + if (info.AfterDockDuration == 0) + token = self.RevokeCondition(token); + else + { + delayedtoken = token; + token = Actor.InvalidConditionToken; + Duration = info.AfterDockDuration; + } + } + + void ITick.Tick(Actor self) + { + if (delayedtoken != Actor.InvalidConditionToken && --Duration <= 0) + delayedtoken = self.RevokeCondition(delayedtoken); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnSubterraneanLayer.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnSubterraneanLayer.cs index 028c4f54d5dd..9d14d559eb5d 100644 --- a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnSubterraneanLayer.cs +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnSubterraneanLayer.cs @@ -10,6 +10,7 @@ #endregion using OpenRA.Mods.Common.Effects; +using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -17,6 +18,13 @@ namespace OpenRA.Mods.Common.Traits [Desc("Grants Condition on subterranean layer. Also plays transition audio-visuals.")] public class GrantConditionOnSubterraneanLayerInfo : GrantConditionOnLayerInfo { + [GrantedConditionReference] + [Desc("The condition to grant to self when getting out of the subterranean layer.")] + public readonly string ResurfaceCondition = null; + + [Desc("How long to grant ResurfaceCondition for.")] + public readonly int ResurfaceConditionDuration = 25; + [Desc("Dig animation image to play when transitioning.")] public readonly string SubterraneanTransitionImage = null; @@ -30,6 +38,18 @@ public class GrantConditionOnSubterraneanLayerInfo : GrantConditionOnLayerInfo [Desc("Dig sound to play when transitioning.")] public readonly string SubterraneanTransitionSound = null; + [Desc("Do the sound play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Ignore fog checks for following relationships.")] + public readonly PlayerRelationship AlwaysPlayFor = PlayerRelationship.Ally; + + [Desc("Volume the SubterraneanTransitionSound played at.")] + public readonly float SoundVolume = 1f; + + public readonly bool ShowSelectionBar = true; + public readonly Color SelectionBarColor = Color.Red; + public override object Create(ActorInitializer init) { return new GrantConditionOnSubterraneanLayer(this); } public override void RulesetLoaded(Ruleset rules, ActorInfo ai) @@ -42,9 +62,13 @@ public override void RulesetLoaded(Ruleset rules, ActorInfo ai) } } - public class GrantConditionOnSubterraneanLayer : GrantConditionOnLayer, INotifyCenterPositionChanged + public class GrantConditionOnSubterraneanLayer : GrantConditionOnLayer, INotifyCenterPositionChanged, ITick, ISync, ISelectionBar { WDist transitionDepth; + protected int resurfaceConditionToken = Actor.InvalidConditionToken; + + [Sync] + int resurfaceTicks; public GrantConditionOnSubterraneanLayer(GrantConditionOnSubterraneanLayerInfo info) : base(info, CustomMovementLayerType.Subterranean) { } @@ -57,6 +81,12 @@ protected override void Created(Actor self) base.Created(self); } + void ITick.Tick(Actor self) + { + if (--resurfaceTicks <= 0 && resurfaceConditionToken != Actor.InvalidConditionToken) + resurfaceConditionToken = self.RevokeCondition(resurfaceConditionToken); + } + void PlayTransitionAudioVisuals(Actor self, CPos fromCell) { if (!string.IsNullOrEmpty(Info.SubterraneanTransitionSequence)) @@ -64,8 +94,12 @@ void PlayTransitionAudioVisuals(Actor self, CPos fromCell) Info.SubterraneanTransitionImage, Info.SubterraneanTransitionSequence, Info.SubterraneanTransitionPalette))); - if (!string.IsNullOrEmpty(Info.SubterraneanTransitionSound)) - Game.Sound.Play(SoundType.World, Info.SubterraneanTransitionSound); + var pos = self.CenterPosition; + var viewver = self.World.RenderPlayer ?? self.World.LocalPlayer; + if (!string.IsNullOrEmpty(Info.SubterraneanTransitionSound) && + (Info.AudibleThroughFog || viewver == null || Info.AlwaysPlayFor.HasRelationship(viewver.RelationshipWith(self.Owner)) || + (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos)))) + Game.Sound.Play(SoundType.World, Info.SubterraneanTransitionSound, pos, Info.SoundVolume); } void INotifyCenterPositionChanged.CenterPositionChanged(Actor self, byte oldLayer, byte newLayer) @@ -80,6 +114,11 @@ void INotifyCenterPositionChanged.CenterPositionChanged(Actor self, byte oldLaye { conditionToken = self.RevokeCondition(conditionToken); PlayTransitionAudioVisuals(self, self.Location); + + if (resurfaceConditionToken == Actor.InvalidConditionToken) + resurfaceConditionToken = self.GrantCondition(Info.ResurfaceCondition); + + resurfaceTicks = Info.ResurfaceConditionDuration; } } @@ -89,5 +128,17 @@ protected override void UpdateConditions(Actor self, byte oldLayer, byte newLaye if (newLayer == ValidLayerType && oldLayer != ValidLayerType) PlayTransitionAudioVisuals(self, self.Location); } + + float ISelectionBar.GetValue() + { + if (IsTraitDisabled || !Info.ShowSelectionBar || resurfaceTicks <= 0) + return 0f; + + return (float)resurfaceTicks / Info.ResurfaceConditionDuration; + } + + bool ISelectionBar.DisplayWhenEmpty { get { return false; } } + + Color ISelectionBar.GetColor() { return Info.SelectionBarColor; } } } diff --git a/OpenRA.Mods.Common/Traits/Crates/Crate.cs b/OpenRA.Mods.Common/Traits/Crates/Crate.cs index f2acb8c30374..46e02c845042 100644 --- a/OpenRA.Mods.Common/Traits/Crates/Crate.cs +++ b/OpenRA.Mods.Common/Traits/Crates/Crate.cs @@ -75,6 +75,7 @@ public class Crate : ITick, IPositionable, ICrushable, ISync, INotifyCreated, { readonly Actor self; readonly CrateInfo info; + readonly CrateSpawner spawner; bool collected; INotifyCenterPositionChanged[] notifyCenterPositionChanged; @@ -89,6 +90,11 @@ public Crate(ActorInitializer init, CrateInfo info) self = init.Self; this.info = info; + var crateSpawnerInit = init.GetOrDefault(); + if (crateSpawnerInit != null) + if (init.Contains()) + spawner = crateSpawnerInit.Value; + var locationInit = init.GetOrDefault(); if (locationInit != null) SetPosition(self, locationInit.Value); @@ -252,7 +258,7 @@ void INotifyAddedToWorld.AddedToWorld(Actor self) { self.World.AddToMaps(self, this); - self.World.WorldActor.TraitOrDefault()?.IncrementCrates(); + spawner?.IncrementCrates(); if (self.World.Map.DistanceAboveTerrain(CenterPosition) > WDist.Zero && self.TraitOrDefault() != null) self.QueueActivity(new Parachute(self)); @@ -262,7 +268,7 @@ void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) { self.World.RemoveFromMaps(self, this); - self.World.WorldActor.TraitOrDefault()?.DecrementCrates(); + spawner?.DecrementCrates(); } } } diff --git a/OpenRA.Mods.Common/Traits/CreatesShroud.cs b/OpenRA.Mods.Common/Traits/CreatesShroud.cs index f7761bd528a8..a0d9a841c440 100644 --- a/OpenRA.Mods.Common/Traits/CreatesShroud.cs +++ b/OpenRA.Mods.Common/Traits/CreatesShroud.cs @@ -55,7 +55,7 @@ public override WDist Range { get { - if (CachedTraitDisabled) + if (cachedTraitDisabled) return WDist.Zero; var range = Util.ApplyPercentageModifiers(Info.Range.Length, rangeModifiers); diff --git a/OpenRA.Mods.Common/Traits/Crushable.cs b/OpenRA.Mods.Common/Traits/Crushable.cs index 579c4b6baa3f..76205398744c 100644 --- a/OpenRA.Mods.Common/Traits/Crushable.cs +++ b/OpenRA.Mods.Common/Traits/Crushable.cs @@ -18,6 +18,10 @@ sealed class CrushableInfo : ConditionalTraitInfo { [Desc("Sound to play when being crushed.")] public readonly string CrushSound = null; + [Desc("Do the sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + [Desc("Volume the CrushSound played at.")] + public readonly float SoundVolume = 1f; [Desc("Which crush classes does this actor belong to.")] public readonly BitSet CrushClasses = new("infantry"); [Desc("Probability of mobile actors noticing and evading a crush attempt.")] @@ -53,7 +57,9 @@ void INotifyCrushed.OnCrush(Actor self, Actor crusher, BitSet crushC if (!CrushableInner(crushClasses, crusher.Owner)) return; - Game.Sound.Play(SoundType.World, Info.CrushSound, crusher.CenterPosition); + var pos = crusher.CenterPosition; + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, Info.CrushSound, pos, Info.SoundVolume); var crusherMobile = crusher.TraitOrDefault(); self.Kill(crusher, crusherMobile != null ? crusherMobile.Info.LocomotorInfo.CrushDamageTypes : default); diff --git a/OpenRA.Mods.Common/Traits/Demolition.cs b/OpenRA.Mods.Common/Traits/Demolition.cs index 1cca0808965b..fa9818e27dfd 100644 --- a/OpenRA.Mods.Common/Traits/Demolition.cs +++ b/OpenRA.Mods.Common/Traits/Demolition.cs @@ -18,7 +18,7 @@ namespace OpenRA.Mods.Common.Traits { - sealed class DemolitionInfo : ConditionalTraitInfo + public sealed class DemolitionInfo : ConditionalTraitInfo { [Desc("Delay to demolish the target once the explosive device is planted. " + "Measured in game ticks. Default is 1.8 seconds.")] @@ -57,7 +57,7 @@ sealed class DemolitionInfo : ConditionalTraitInfo public override object Create(ActorInitializer init) { return new Demolition(this); } } - sealed class Demolition : ConditionalTrait, IIssueOrder, IResolveOrder, IOrderVoice + public sealed class Demolition : ConditionalTrait, IIssueOrder, IResolveOrder, IOrderVoice { public Demolition(DemolitionInfo info) : base(info) { } diff --git a/OpenRA.Mods.Common/Traits/Explodes.cs b/OpenRA.Mods.Common/Traits/Explodes.cs index 87a32384b7bd..e7d3a671d68a 100644 --- a/OpenRA.Mods.Common/Traits/Explodes.cs +++ b/OpenRA.Mods.Common/Traits/Explodes.cs @@ -118,7 +118,11 @@ void INotifyKilled.Killed(Actor self, AttackInfo e) var source = Info.DamageSource == DamageSource.Self ? self : e.Attacker; if (weapon.Report != null && weapon.Report.Length > 0) - Game.Sound.Play(SoundType.World, weapon.Report, self.World, self.CenterPosition); + { + var pos = self.CenterPosition; + if (weapon.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, weapon.Report, self.World, pos, null, weapon.SoundVolume); + } if (Info.Type == ExplosionType.Footprint && buildingInfo != null) { diff --git a/OpenRA.Mods.Common/Traits/ExplosionOnDamageTransition.cs b/OpenRA.Mods.Common/Traits/ExplosionOnDamageTransition.cs index 589f50989000..b7aa998cf3f4 100644 --- a/OpenRA.Mods.Common/Traits/ExplosionOnDamageTransition.cs +++ b/OpenRA.Mods.Common/Traits/ExplosionOnDamageTransition.cs @@ -25,8 +25,8 @@ public class ExplosionOnDamageTransitionInfo : ConditionalTraitInfo, IRulesetLoa [Desc("At which damage state explosion will trigger.")] public readonly DamageState DamageState = DamageState.Heavy; - [Desc("Should the explosion only be triggered once?")] - public readonly bool TriggerOnlyOnce = false; + [Desc("The cooldown of the explosion. Set to -1 to trigger only once.")] + public readonly int CoolDown = 0; public WeaponInfo WeaponInfo { get; private set; } @@ -49,29 +49,29 @@ public override void RulesetLoaded(Ruleset rules, ActorInfo ai) public class ExplosionOnDamageTransition : ConditionalTrait, INotifyDamageStateChanged { - bool triggered; + int lastWorldTick = 0; public ExplosionOnDamageTransition(ExplosionOnDamageTransitionInfo info) : base(info) { } void INotifyDamageStateChanged.DamageStateChanged(Actor self, AttackInfo e) { - if (!self.IsInWorld) + if (!self.IsInWorld || IsTraitDisabled) return; - if (triggered) - return; - - if (IsTraitDisabled) + if (lastWorldTick != 0 && Info.CoolDown < 0) return; if (e.DamageState >= Info.DamageState && e.PreviousDamageState < Info.DamageState) { - if (Info.TriggerOnlyOnce) - triggered = true; - - // Use .FromPos since the actor might have been killed, don't use Target.FromActor - Info.WeaponInfo.Impact(Target.FromPos(self.CenterPosition), e.Attacker); + var worldtick = self.World.WorldTick; + + if (worldtick - lastWorldTick > Info.CoolDown) + { + // Use .FromPos since the actor might have been killed, don't use Target.FromActor + Info.WeaponInfo.Impact(Target.FromPos(self.CenterPosition), e.Attacker); + lastWorldTick = worldtick; + } } } } diff --git a/OpenRA.Mods.Common/Traits/FireWarheads.cs b/OpenRA.Mods.Common/Traits/FireWarheads.cs index 2ab3107f2a77..be819967acd7 100644 --- a/OpenRA.Mods.Common/Traits/FireWarheads.cs +++ b/OpenRA.Mods.Common/Traits/FireWarheads.cs @@ -17,7 +17,7 @@ namespace OpenRA.Mods.Common.Traits { [Desc("Detonate defined warheads at the current location at a set interval.")] - public class FireWarheadsInfo : PausableConditionalTraitInfo, Requires, IRulesetLoaded + public class FireWarheadsInfo : PausableConditionalTraitInfo, Requires, IRulesetLoaded { [WeaponReference] [FieldLoader.Require] @@ -72,11 +72,8 @@ void ITick.Tick(Actor self) foreach (var wep in Info.WeaponInfos) { wep.Impact(Target.FromPos(self.CenterPosition), self); - self.World.AddFrameEndTask(world => - { - if (wep.Report != null && wep.Report.Length > 0) - Game.Sound.Play(SoundType.World, wep.Report, world, self.CenterPosition); - }); + if (wep.Report != null && wep.Report.Length > 0) + Game.Sound.Play(SoundType.World, wep.Report, self.World, self.CenterPosition); } } } diff --git a/OpenRA.Mods.Common/Traits/GivesBounty.cs b/OpenRA.Mods.Common/Traits/GivesBounty.cs index 3c58d0fc155b..097dcd2607d1 100644 --- a/OpenRA.Mods.Common/Traits/GivesBounty.cs +++ b/OpenRA.Mods.Common/Traits/GivesBounty.cs @@ -20,8 +20,8 @@ namespace OpenRA.Mods.Common.Traits [Desc("When killed, this actor causes the attacking player to receive money.")] sealed class GivesBountyInfo : ConditionalTraitInfo { - [Desc("Percentage of the killed actor's Cost or CustomSellValue to be given.")] - public readonly int Percentage = 10; + [Desc("Type of bounty. Used for targerting along with 'TakesBounty' trait on actors.")] + public readonly string Type = "Bounty"; [Desc("Player relationships the attacking player needs to receive the bounty.")] public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Neutral | PlayerRelationship.Enemy; @@ -33,6 +33,9 @@ sealed class GivesBountyInfo : ConditionalTraitInfo "Use an empty list (the default) to allow all DeathTypes.")] public readonly BitSet DeathTypes = default; + [Desc("Allow bounty for passenger inside this actor being showed.")] + public readonly bool ShowPassengerBounties = true; + public override object Create(ActorInitializer init) { return new GivesBounty(this); } } @@ -43,18 +46,21 @@ sealed class GivesBounty : ConditionalTrait, INotifyKilled, INo public GivesBounty(GivesBountyInfo info) : base(info) { } - int GetBountyValue(Actor self) + static int GetBountyValue(Actor self, TakesBounty activeAttackerTakesBounty) { - return self.GetSellValue() * Info.Percentage / 100; + return self.GetSellValue() * activeAttackerTakesBounty.Info.Percentage / 100; } - int GetDisplayedBountyValue(Actor self) + int GetDisplayedBountyValue(Actor self, TakesBounty activeAttackerTakesBounty) { - var bounty = GetBountyValue(self); - foreach (var pb in passengerBounties) - foreach (var b in pb.Value) - if (!b.IsTraitDisabled) - bounty += b.GetDisplayedBountyValue(pb.Key); + var bounty = GetBountyValue(self, activeAttackerTakesBounty); + if (Info.ShowPassengerBounties) + { + foreach (var pb in passengerBounties) + foreach (var b in pb.Value) + if (!b.IsTraitDisabled) + bounty += b.GetDisplayedBountyValue(pb.Key, activeAttackerTakesBounty); + } return bounty; } @@ -70,21 +76,28 @@ void INotifyKilled.Killed(Actor self, AttackInfo e) if (!Info.DeathTypes.IsEmpty && !e.Damage.DamageTypes.Overlaps(Info.DeathTypes)) return; - var displayedBounty = GetDisplayedBountyValue(self); - if (Info.ShowBounty && self.IsInWorld && displayedBounty != 0 && e.Attacker.Owner.IsAlliedWith(self.World.RenderPlayer)) + var attackerTakesBounty = e.Attacker.TraitsImplementing().ToArray(); + var activeAttackerTakesBounty = attackerTakesBounty.FirstOrDefault(tb => !tb.IsTraitDisabled && tb.Info.ValidTypes.Contains(Info.Type)); + if (activeAttackerTakesBounty == null) + return; + + var displayedBounty = GetDisplayedBountyValue(self, activeAttackerTakesBounty); + if (Info.ShowBounty && self.IsInWorld && displayedBounty > 0 && e.Attacker.Owner.IsAlliedWith(self.World.RenderPlayer)) e.Attacker.World.AddFrameEndTask(w => w.Add(new FloatingText(self.CenterPosition, e.Attacker.Owner.Color, FloatingText.FormatCashTick(displayedBounty), 30))); - e.Attacker.Owner.PlayerActor.Trait().ChangeCash(GetBountyValue(self)); + e.Attacker.Owner.PlayerActor.Trait().ChangeCash(GetBountyValue(self, activeAttackerTakesBounty)); } void INotifyPassengerEntered.OnPassengerEntered(Actor self, Actor passenger) { - passengerBounties.Add(passenger, passenger.TraitsImplementing().ToArray()); + if (Info.ShowPassengerBounties && !passengerBounties.ContainsKey(passenger)) // We need this to keep SharedCargo stable. + passengerBounties.Add(passenger, passenger.TraitsImplementing().ToArray()); } void INotifyPassengerExited.OnPassengerExited(Actor self, Actor passenger) { - passengerBounties.Remove(passenger); + if (Info.ShowPassengerBounties) + passengerBounties.Remove(passenger); } } } diff --git a/OpenRA.Mods.Common/Traits/Harvester.cs b/OpenRA.Mods.Common/Traits/Harvester.cs index eac5a1e91d2a..470683ae390c 100644 --- a/OpenRA.Mods.Common/Traits/Harvester.cs +++ b/OpenRA.Mods.Common/Traits/Harvester.cs @@ -115,7 +115,7 @@ protected override void Created(Actor self) UpdateCondition(self); // Note: This is queued in a FrameEndTask because otherwise the activity is dropped/overridden while moving out of a factory. - if (Info.SearchOnCreation) + if (Info.SearchOnCreation && !IsTraitDisabled) self.World.AddFrameEndTask(w => self.QueueActivity(new FindAndDeliverResources(self))); base.Created(self); diff --git a/OpenRA.Mods.Common/Traits/Immobile.cs b/OpenRA.Mods.Common/Traits/Immobile.cs index 28db3d94e49c..ce8a59cf421e 100644 --- a/OpenRA.Mods.Common/Traits/Immobile.cs +++ b/OpenRA.Mods.Common/Traits/Immobile.cs @@ -15,7 +15,7 @@ namespace OpenRA.Mods.Common.Traits { - sealed class ImmobileInfo : TraitInfo, IOccupySpaceInfo + public class ImmobileInfo : TraitInfo, IOccupySpaceInfo { public readonly bool OccupiesSpace = true; public override object Create(ActorInitializer init) { return new Immobile(init, this); } @@ -29,7 +29,7 @@ public IReadOnlyDictionary OccupiedCells(ActorInfo info, CPos loc bool IOccupySpaceInfo.SharesCell => false; } - sealed class Immobile : IOccupySpace, ISync, INotifyAddedToWorld, INotifyRemovedFromWorld + public class Immobile : IOccupySpace, ISync, INotifyAddedToWorld, INotifyRemovedFromWorld { [Sync] readonly CPos location; diff --git a/OpenRA.Mods.Common/Traits/InstantlyRepairs.cs b/OpenRA.Mods.Common/Traits/InstantlyRepairs.cs index cd67e36d131b..15074b306059 100644 --- a/OpenRA.Mods.Common/Traits/InstantlyRepairs.cs +++ b/OpenRA.Mods.Common/Traits/InstantlyRepairs.cs @@ -39,6 +39,12 @@ public class InstantlyRepairsInfo : ConditionalTraitInfo [Desc("Sound to play when repairing is done.")] public readonly string RepairSound = null; + [Desc("Do the sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the RepairSound played at.")] + public readonly float SoundVolume = 1f; + [CursorReference] [Desc("Cursor to display when hovering over a valid actor to repair.")] public readonly string Cursor = "goldwrench"; diff --git a/OpenRA.Mods.Common/Traits/Minelayer.cs b/OpenRA.Mods.Common/Traits/Minelayer.cs index 324e10d8b848..f28c93665e67 100644 --- a/OpenRA.Mods.Common/Traits/Minelayer.cs +++ b/OpenRA.Mods.Common/Traits/Minelayer.cs @@ -20,7 +20,7 @@ namespace OpenRA.Mods.Common.Traits { - public class MinelayerInfo : TraitInfo, Requires + public class MinelayerInfo : TraitInfo { [ActorReference] public readonly string Mine = "minv"; diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs index 78a87c4b8468..08de7e11811e 100644 --- a/OpenRA.Mods.Common/Traits/Mobile.cs +++ b/OpenRA.Mods.Common/Traits/Mobile.cs @@ -79,6 +79,9 @@ public class MobileInfo : PausableConditionalTraitInfo, IMoveInfo, IPositionable "If set to -1 the unit will be allowed to move backwards without range limit.")] public readonly int MaxBackwardCells = 15; + [Desc("Can the actor turn, even when the trait is disabled")] + public readonly bool CanTurnWhileDisabled = false; + [ConsumedConditionReference] [Desc("Boolean expression defining the condition under which the regular (non-force) move cursor is disabled.")] public readonly BooleanExpression RequireForceMoveCondition = null; @@ -173,6 +176,7 @@ public class Mobile : PausableConditionalTrait, IIssueOrder, IResolv { readonly Actor self; readonly Lazy> speedModifiers; + public readonly IEnumerable TurnSpeedModifiers; readonly bool returnToCellOnCreation; readonly bool returnToCellOnCreationRecalculateSubCell = true; @@ -269,6 +273,7 @@ public Mobile(ActorInitializer init, MobileInfo info) self = init.Self; speedModifiers = Exts.Lazy(() => self.TraitsImplementing().ToArray().Select(x => x.GetSpeedModifier())); + TurnSpeedModifiers = self.TraitsImplementing().ToArray().Select(x => x.GetTurnSpeedModifier()); ToSubCell = FromSubCell = info.LocomotorInfo.SharesCell ? init.World.Map.Grid.DefaultSubCell : SubCell.FullCell; @@ -649,6 +654,7 @@ public class ReturnToCellActivity : Activity public ReturnToCellActivity(Actor self, int delay = 0, bool recalculateSubCell = false) { + ActivityType = ActivityType.Move; mobile = self.Trait(); IsInterruptible = false; this.delay = delay; @@ -1000,7 +1006,11 @@ protected override void OnFirstRun(Actor self) Activity ICreationActivity.GetCreationActivity() { - return new LeaveProductionActivity(self, creationActivityDelay, creationRallypoint, returnToCellOnCreation ? new ReturnToCellActivity(self, creationActivityDelay, returnToCellOnCreationRecalculateSubCell) : null); + if (returnToCellOnCreation || creationRallypoint != null || creationActivityDelay > 0) + return new LeaveProductionActivity(self, creationActivityDelay, creationRallypoint, + returnToCellOnCreation ? new ReturnToCellActivity(self, creationActivityDelay, returnToCellOnCreationRecalculateSubCell) : null); + + return null; } sealed class MoveOrderTargeter : IOrderTargeter diff --git a/OpenRA.Mods.Common/Traits/Multipliers/FirepowerMultiplier.cs b/OpenRA.Mods.Common/Traits/Multipliers/FirepowerMultiplier.cs index 79b91cab7356..5081f7a317e9 100644 --- a/OpenRA.Mods.Common/Traits/Multipliers/FirepowerMultiplier.cs +++ b/OpenRA.Mods.Common/Traits/Multipliers/FirepowerMultiplier.cs @@ -9,6 +9,8 @@ */ #endregion +using System.Collections.Generic; + namespace OpenRA.Mods.Common.Traits { [Desc("Modifies the damage applied by this actor.")] @@ -18,6 +20,9 @@ public class FirepowerMultiplierInfo : ConditionalTraitInfo [Desc("Percentage modifier to apply.")] public readonly int Modifier = 100; + [Desc("Weapon types to applies to. Leave empty to apply to all weapons.")] + public readonly HashSet Types = new(); + public override object Create(ActorInitializer init) { return new FirepowerMultiplier(this); } } @@ -26,6 +31,9 @@ public class FirepowerMultiplier : ConditionalTrait, IF public FirepowerMultiplier(FirepowerMultiplierInfo info) : base(info) { } - int IFirepowerModifier.GetFirepowerModifier() { return IsTraitDisabled ? 100 : Info.Modifier; } + int IFirepowerModifier.GetFirepowerModifier(string armamentName) + { + return !IsTraitDisabled && (Info.Types.Count == 0 || (!string.IsNullOrEmpty(armamentName) && Info.Types.Contains(armamentName))) ? Info.Modifier : 100; + } } } diff --git a/OpenRA.Mods.Common/Traits/Multipliers/HandicapFirepowerMultiplier.cs b/OpenRA.Mods.Common/Traits/Multipliers/HandicapFirepowerMultiplier.cs index ef71993f5837..03e6701a6b39 100644 --- a/OpenRA.Mods.Common/Traits/Multipliers/HandicapFirepowerMultiplier.cs +++ b/OpenRA.Mods.Common/Traits/Multipliers/HandicapFirepowerMultiplier.cs @@ -28,7 +28,7 @@ public HandicapFirepowerMultiplier(Actor self) this.self = self; } - int IFirepowerModifier.GetFirepowerModifier() + int IFirepowerModifier.GetFirepowerModifier(string armamentName) { // Equivalent to the firepower handicap from C&C3: // 5% handicap = 95% firepower diff --git a/OpenRA.Mods.Common/Traits/Multipliers/ReloadDelayMultiplier.cs b/OpenRA.Mods.Common/Traits/Multipliers/ReloadDelayMultiplier.cs index 802a1f6b1ba3..ee690ba7da5a 100644 --- a/OpenRA.Mods.Common/Traits/Multipliers/ReloadDelayMultiplier.cs +++ b/OpenRA.Mods.Common/Traits/Multipliers/ReloadDelayMultiplier.cs @@ -9,6 +9,8 @@ */ #endregion +using System.Collections.Generic; + namespace OpenRA.Mods.Common.Traits { [Desc("Modifies the reload time of weapons fired by this actor.")] @@ -18,6 +20,9 @@ public class ReloadDelayMultiplierInfo : ConditionalTraitInfo [Desc("Percentage modifier to apply.")] public readonly int Modifier = 100; + [Desc("Weapon types to applies to. Leave empty to apply to all weapons.")] + public readonly HashSet Types = new(); + public override object Create(ActorInitializer init) { return new ReloadDelayMultiplier(this); } } @@ -26,6 +31,9 @@ public class ReloadDelayMultiplier : ConditionalTrait public ReloadDelayMultiplier(ReloadDelayMultiplierInfo info) : base(info) { } - int IReloadModifier.GetReloadModifier() { return IsTraitDisabled ? 100 : Info.Modifier; } + int IReloadModifier.GetReloadModifier(string armamentName) + { + return !IsTraitDisabled && (Info.Types.Count == 0 || (!string.IsNullOrEmpty(armamentName) && Info.Types.Contains(armamentName))) ? Info.Modifier : 100; + } } } diff --git a/OpenRA.Mods.Common/Traits/Multipliers/TurnSpeedMultiplier.cs b/OpenRA.Mods.Common/Traits/Multipliers/TurnSpeedMultiplier.cs new file mode 100644 index 000000000000..ed229449fe83 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Multipliers/TurnSpeedMultiplier.cs @@ -0,0 +1,31 @@ +#region Copyright & License Information +/* + * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Modifies the turn movement speed of this actor.")] + public class TurnSpeedMultiplierInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [Desc("Percentage modifier to apply.")] + public readonly int Modifier = 100; + + public override object Create(ActorInitializer init) { return new TurnSpeedMultiplier(this); } + } + + public class TurnSpeedMultiplier : ConditionalTrait, ITurnSpeedModifier + { + public TurnSpeedMultiplier(TurnSpeedMultiplierInfo info) + : base(info) { } + + int ITurnSpeedModifier.GetTurnSpeedModifier() { return IsTraitDisabled ? 100 : Info.Modifier; } + } +} diff --git a/OpenRA.Mods.Common/Traits/MustBeDestroyed.cs b/OpenRA.Mods.Common/Traits/MustBeDestroyed.cs index 8cd2b75eaac0..5df64d70cb7a 100644 --- a/OpenRA.Mods.Common/Traits/MustBeDestroyed.cs +++ b/OpenRA.Mods.Common/Traits/MustBeDestroyed.cs @@ -9,12 +9,10 @@ */ #endregion -using OpenRA.Traits; - namespace OpenRA.Mods.Common.Traits { [Desc("Actors with this trait must be destroyed for a game to end.")] - public class MustBeDestroyedInfo : TraitInfo + public class MustBeDestroyedInfo : ConditionalTraitInfo { [Desc("In a short game only actors that have this value set to true need to be destroyed.")] public readonly bool RequiredForShortGame = false; @@ -22,13 +20,9 @@ public class MustBeDestroyedInfo : TraitInfo public override object Create(ActorInitializer init) { return new MustBeDestroyed(this); } } - public class MustBeDestroyed + public class MustBeDestroyed : ConditionalTrait { - public readonly MustBeDestroyedInfo Info; - public MustBeDestroyed(MustBeDestroyedInfo info) - { - Info = info; - } + : base(info) { } } } diff --git a/OpenRA.Mods.Common/Traits/PaletteEffects/FlashPaletteEffect.cs b/OpenRA.Mods.Common/Traits/PaletteEffects/FlashPostProcessEffect.cs similarity index 53% rename from OpenRA.Mods.Common/Traits/PaletteEffects/FlashPaletteEffect.cs rename to OpenRA.Mods.Common/Traits/PaletteEffects/FlashPostProcessEffect.cs index 489031d0dfff..9e16edce947d 100644 --- a/OpenRA.Mods.Common/Traits/PaletteEffects/FlashPaletteEffect.cs +++ b/OpenRA.Mods.Common/Traits/PaletteEffects/FlashPostProcessEffect.cs @@ -9,21 +9,17 @@ */ #endregion -using System.Collections.Generic; +using System; using OpenRA.Graphics; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { - using GUtil = OpenRA.Graphics.Util; - [TraitLocation(SystemActors.World | SystemActors.EditorWorld)] [Desc("Used for bursted one-colored whole screen effects. Add this to the world actor.")] - public class FlashPaletteEffectInfo : TraitInfo + public class FlashPostProcessEffectInfo : TraitInfo { - public readonly HashSet ExcludePalettes = new() { "cursor", "chrome", "colorpicker", "fog", "shroud" }; - [Desc("Measured in ticks.")] public readonly int Length = 20; @@ -32,20 +28,21 @@ public class FlashPaletteEffectInfo : TraitInfo [Desc("Set this when using multiple independent flash effects.")] public readonly string Type = null; - public override object Create(ActorInitializer init) { return new FlashPaletteEffect(this); } + public override object Create(ActorInitializer init) { return new FlashPostProcessEffect(this); } } - public class FlashPaletteEffect : IPaletteModifier, ITick + public class FlashPostProcessEffect : RenderPostProcessPassBase, ITick { - public readonly FlashPaletteEffectInfo Info; + public readonly FlashPostProcessEffectInfo Info; + int remainingFrames; + float blend; - public FlashPaletteEffect(FlashPaletteEffectInfo info) + public FlashPostProcessEffect(FlashPostProcessEffectInfo info) + : base("flash", PostProcessPassType.AfterWorld) { Info = info; } - int remainingFrames; - public void Enable(int ticks) { if (ticks == -1) @@ -57,27 +54,14 @@ public void Enable(int ticks) void ITick.Tick(Actor self) { if (remainingFrames > 0) - remainingFrames--; + blend = Math.Min((float)--remainingFrames / Info.Length, 1); } - public void AdjustPalette(IReadOnlyDictionary palettes) + protected override bool Enabled => remainingFrames > 0; + protected override void PrepareRender(WorldRenderer wr, IShader shader) { - if (remainingFrames == 0) - return; - - var frac = (float)remainingFrames / Info.Length; - - foreach (var pal in palettes) - { - for (var x = 0; x < Palette.Size; x++) - { - var orig = pal.Value.GetColor(x); - var c = Info.Color; - var color = Color.FromArgb(orig.A, ((int)c.R).Clamp(0, 255), ((int)c.G).Clamp(0, 255), ((int)c.B).Clamp(0, 255)); - var final = GUtil.PremultipliedColorLerp(frac, orig, GUtil.PremultiplyAlpha(Color.FromArgb(orig.A, color))); - pal.Value.SetColor(x, final); - } - } + shader.SetVec("Blend", blend); + shader.SetVec("Color", (float)Info.Color.B / 255, (float)Info.Color.G / 255, (float)Info.Color.R / 255); } } } diff --git a/OpenRA.Mods.Common/Traits/PaletteEffects/MenuPaletteEffect.cs b/OpenRA.Mods.Common/Traits/PaletteEffects/MenuPostProcessEffect.cs similarity index 51% rename from OpenRA.Mods.Common/Traits/PaletteEffects/MenuPaletteEffect.cs rename to OpenRA.Mods.Common/Traits/PaletteEffects/MenuPostProcessEffect.cs index 8693e4630caa..1aedffde0785 100644 --- a/OpenRA.Mods.Common/Traits/PaletteEffects/MenuPaletteEffect.cs +++ b/OpenRA.Mods.Common/Traits/PaletteEffects/MenuPostProcessEffect.cs @@ -9,9 +9,7 @@ */ #endregion -using System.Collections.Generic; using OpenRA.Graphics; -using OpenRA.Primitives; using OpenRA.Traits; using OpenRA.Widgets; @@ -19,97 +17,56 @@ namespace OpenRA.Mods.Common.Traits { [TraitLocation(SystemActors.World | SystemActors.EditorWorld)] [Desc("Fades the world from/to black at the start/end of the game, and can (optionally) desaturate the world")] - public class MenuPaletteEffectInfo : TraitInfo + public class MenuPostProcessEffectInfo : TraitInfo { [Desc("Time (in ticks) to fade between states")] public readonly int FadeLength = 10; [Desc("Effect style to fade to during gameplay. Accepts values of None or Desaturated.")] - public readonly MenuPaletteEffect.EffectType Effect = MenuPaletteEffect.EffectType.None; + public readonly MenuPostProcessEffect.EffectType Effect = MenuPostProcessEffect.EffectType.None; [Desc("Effect style to fade to when opening the in-game menu. Accepts values of None, Black or Desaturated.")] - public readonly MenuPaletteEffect.EffectType MenuEffect = MenuPaletteEffect.EffectType.None; + public readonly MenuPostProcessEffect.EffectType MenuEffect = MenuPostProcessEffect.EffectType.None; - public override object Create(ActorInitializer init) { return new MenuPaletteEffect(this); } + public override object Create(ActorInitializer init) { return new MenuPostProcessEffect(this); } } - public class MenuPaletteEffect : IPaletteModifier, IRender, IWorldLoaded, INotifyGameLoaded + public class MenuPostProcessEffect : RenderPostProcessPassBase, IWorldLoaded, INotifyGameLoaded { public enum EffectType { None, Black, Desaturated } - public readonly MenuPaletteEffectInfo Info; + public readonly MenuPostProcessEffectInfo Info; EffectType from = EffectType.Black; EffectType to = EffectType.Black; - float frac = 0; long startTime; long endTime; - public MenuPaletteEffect(MenuPaletteEffectInfo info) { Info = info; } + public MenuPostProcessEffect(MenuPostProcessEffectInfo info) + : base("menufade", PostProcessPassType.AfterShroud) + { + Info = info; + } public void Fade(EffectType type) { startTime = Game.RunTime; endTime = startTime + Ui.Timestep * Info.FadeLength; - frac = 1; from = to; to = type; } - IEnumerable IRender.Render(Actor self, WorldRenderer wr) - { - if (endTime == 0) - yield break; - - frac = (endTime - Game.RunTime) * 1f / (endTime - startTime); - if (frac < 0) - frac = startTime = endTime = 0; - - yield break; - } - - IEnumerable IRender.ScreenBounds(Actor self, WorldRenderer wr) + protected override bool Enabled => to != EffectType.None || endTime != 0; + protected override void PrepareRender(WorldRenderer wr, IShader shader) { - yield break; - } - - static Color ColorForEffect(EffectType t, Color orig) - { - switch (t) - { - case EffectType.Black: - return Color.FromArgb(orig.A, Color.Black); - case EffectType.Desaturated: - var lum = (int)(255 * orig.GetBrightness()); - return Color.FromArgb(orig.A, lum, lum, lum); - default: - case EffectType.None: - return orig; - } - } - - public void AdjustPalette(IReadOnlyDictionary palettes) - { - if (to == EffectType.None && endTime == 0) - return; - - foreach (var pal in palettes.Values) - { - for (var x = 0; x < Palette.Size; x++) - { - var orig = pal.GetColor(x); - var t = ColorForEffect(to, orig); + var blend = (endTime - Game.RunTime) * 1f / (endTime - startTime); + if (blend < 0) + blend = startTime = endTime = 0; - if (endTime == 0) - pal.SetColor(x, t); - else - { - var f = ColorForEffect(from, orig); - pal.SetColor(x, Exts.ColorLerp(frac, t, f)); - } - } - } + shader.SetVec("From", (int)from); + shader.SetVec("To", (int)to); + shader.SetVec("Blend", blend); } void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr) diff --git a/OpenRA.Mods.Common/Traits/PaletteEffects/TintPostProcessEffect.cs b/OpenRA.Mods.Common/Traits/PaletteEffects/TintPostProcessEffect.cs new file mode 100644 index 000000000000..1b27d1f80df0 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/PaletteEffects/TintPostProcessEffect.cs @@ -0,0 +1,51 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Used for day/night effects.")] + [TraitLocation(SystemActors.World | SystemActors.EditorWorld)] + public class TintPostProcessEffectInfo : TraitInfo, ILobbyCustomRulesIgnore + { + public readonly float Red = 1f; + public readonly float Green = 1f; + public readonly float Blue = 1f; + public readonly float Ambient = 1f; + + public override object Create(ActorInitializer init) { return new TintPostProcessEffect(this); } + } + + public class TintPostProcessEffect : RenderPostProcessPassBase + { + public float Red; + public float Green; + public float Blue; + public float Ambient; + + public TintPostProcessEffect(TintPostProcessEffectInfo info) + : base("tint", PostProcessPassType.AfterActors) + { + Red = info.Red; + Green = info.Green; + Blue = info.Blue; + Ambient = info.Ambient; + } + + protected override bool Enabled => true; + protected override void PrepareRender(WorldRenderer wr, IShader shader) + { + shader.SetVec("Tint", Ambient * Red, Ambient * Green, Ambient * Blue); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Passenger.cs b/OpenRA.Mods.Common/Traits/Passenger.cs index c72544fbe423..0cd8e6f583cc 100644 --- a/OpenRA.Mods.Common/Traits/Passenger.cs +++ b/OpenRA.Mods.Common/Traits/Passenger.cs @@ -109,13 +109,13 @@ bool IsCorrectCargoType(Actor target, TargetModifiers modifiers) bool IsCorrectCargoType(Actor target) { - var cargo = target.Trait(); - return !cargo.IsTraitDisabled && cargo.Info.Types.Contains(Info.CargoType); + var cargo = target.TraitOrDefault(); + return cargo != null && !cargo.IsTraitDisabled && cargo.Info.Types.Contains(Info.CargoType); } bool CanEnter(Cargo cargo) { - return cargo != null && !cargo.IsTraitDisabled && cargo.HasSpace(Info.Weight); + return cargo != null && !cargo.IsTraitDisabled && !cargo.IsTraitPaused && cargo.HasSpace(Info.Weight); } bool CanEnter(Actor target) @@ -207,6 +207,19 @@ public void Unreserve(Actor self) ReservedCargo = null; } + public virtual void OnBeforeAddedToWorld(Actor actor) + { + actor.CancelActivity(); + } + + public virtual void OnEjectedFromKilledCargo(Actor self) + { + // Cancel all other activities to keep consistent behavior with the one in UnloadCargo. + self.CurrentActivity?.Cancel(self); + + self.QueueActivity(new Nudge(self)); + } + void INotifyKilled.Killed(Actor self, AttackInfo e) { if (Transport == null) diff --git a/OpenRA.Mods.Common/Traits/Player/BaseAttackNotifier.cs b/OpenRA.Mods.Common/Traits/Player/BaseAttackNotifier.cs index 89c80bb74290..d0d6236878a5 100644 --- a/OpenRA.Mods.Common/Traits/Player/BaseAttackNotifier.cs +++ b/OpenRA.Mods.Common/Traits/Player/BaseAttackNotifier.cs @@ -22,6 +22,9 @@ public class BaseAttackNotifierInfo : TraitInfo [Desc("Minimum duration (in milliseconds) between notification events.")] public readonly int NotifyInterval = 30000; + [Desc("Ping radar on the damaged actor's location.")] + public readonly bool PingRadar = true; + public readonly Color RadarPingColor = Color.Red; [Desc("Length of time (in ticks) to display a location ping in the minimap.")] @@ -44,6 +47,9 @@ public class BaseAttackNotifierInfo : TraitInfo [Desc("Text notification to display to allies when under attack.")] public readonly string AllyTextNotification = null; + [Desc("Trigger the notification for non-buildings only.")] + public readonly bool RevertUnitTypes = false; + public override object Create(ActorInitializer init) { return new BaseAttackNotifier(init.Self, this); } } @@ -77,8 +83,8 @@ void INotifyDamage.Damaged(Actor self, AttackInfo e) if (e.Attacker == self.World.WorldActor) return; - // Only track last hit against our base - if (!self.Info.HasTraitInfo()) + if ((!info.RevertUnitTypes && !self.Info.HasTraitInfo()) + || (info.RevertUnitTypes && self.Info.HasTraitInfo())) return; if (e.Attacker.Owner.IsAlliedWith(self.Owner) && e.Damage.Value <= 0) @@ -90,16 +96,19 @@ void INotifyDamage.Damaged(Actor self, AttackInfo e) if (self.Owner == localPlayer) { - Game.Sound.PlayNotification(rules, self.Owner, "Speech", info.Notification, self.Owner.Faction.InternalName); + if (!string.IsNullOrEmpty(info.Notification)) + Game.Sound.PlayNotification(rules, self.Owner, "Speech", info.Notification, self.Owner.Faction.InternalName); TextNotificationsManager.AddTransientLine(self.Owner, info.TextNotification); } else if (localPlayer.IsAlliedWith(self.Owner) && localPlayer != e.Attacker.Owner) { - Game.Sound.PlayNotification(rules, localPlayer, "Speech", info.AllyNotification, localPlayer.Faction.InternalName); + if (!string.IsNullOrEmpty(info.AllyNotification)) + Game.Sound.PlayNotification(rules, localPlayer, "Speech", info.AllyNotification, localPlayer.Faction.InternalName); TextNotificationsManager.AddTransientLine(localPlayer, info.AllyTextNotification); } - radarPings?.Add(() => self.Owner.IsAlliedWith(self.World.RenderPlayer), self.CenterPosition, info.RadarPingColor, info.RadarPingDuration); + if (info.PingRadar) + radarPings?.Add(() => self.Owner.IsAlliedWith(self.World.RenderPlayer), self.CenterPosition, info.RadarPingColor, info.RadarPingDuration); lastAttackTime = Game.RunTime; } diff --git a/OpenRA.Mods.Common/Traits/Player/ClassicParallelProductionQueue.cs b/OpenRA.Mods.Common/Traits/Player/ClassicParallelProductionQueue.cs index b4013132f8ff..12d3c63b7700 100644 --- a/OpenRA.Mods.Common/Traits/Player/ClassicParallelProductionQueue.cs +++ b/OpenRA.Mods.Common/Traits/Player/ClassicParallelProductionQueue.cs @@ -147,7 +147,7 @@ public override TraitPair MostLikelyProducer() protected override bool BuildUnit(ActorInfo unit) { // Find a production structure to build this actor - var bi = unit.TraitInfo(); + var bi = BuildableInfo.GetTraitForQueue(unit, Info.Type); // Some units may request a specific production type, which is ignored if the AllTech cheat is enabled var type = developerMode.AllTech ? Info.Type : (bi.BuildAtProductionType ?? Info.Type); @@ -181,10 +181,7 @@ protected override bool BuildUnit(ActorInfo unit) } if (!anyProducers) - { CancelProduction(unit.Name, 1); - return false; - } return false; } diff --git a/OpenRA.Mods.Common/Traits/Player/ClassicProductionQueue.cs b/OpenRA.Mods.Common/Traits/Player/ClassicProductionQueue.cs index 13f1a7be3d70..d8de4c6d2422 100644 --- a/OpenRA.Mods.Common/Traits/Player/ClassicProductionQueue.cs +++ b/OpenRA.Mods.Common/Traits/Player/ClassicProductionQueue.cs @@ -97,7 +97,7 @@ public override TraitPair MostLikelyProducer() protected override bool BuildUnit(ActorInfo unit) { // Find a production structure to build this actor - var bi = unit.TraitInfo(); + var bi = BuildableInfo.GetTraitForQueue(unit, Info.Type); // Some units may request a specific production type, which is ignored if the AllTech cheat is enabled var type = developerMode.AllTech ? Info.Type : (bi.BuildAtProductionType ?? Info.Type); @@ -131,10 +131,7 @@ protected override bool BuildUnit(ActorInfo unit) } if (!anyProducers) - { CancelProduction(unit.Name, 1); - return false; - } return false; } diff --git a/OpenRA.Mods.Common/Traits/Player/DeveloperMode.cs b/OpenRA.Mods.Common/Traits/Player/DeveloperMode.cs index 347f3da870f2..5d49ee20d63a 100644 --- a/OpenRA.Mods.Common/Traits/Player/DeveloperMode.cs +++ b/OpenRA.Mods.Common/Traits/Player/DeveloperMode.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Linq; +using OpenRA.Mods.Common.Activities; using OpenRA.Primitives; using OpenRA.Traits; @@ -77,6 +78,12 @@ public class DeveloperMode : IResolveOrder, ISync, INotifyCreated, IUnlocksRende [TranslationReference("cheat", "player", "suffix")] const string CheatUsed = "notification-cheat-used"; + [TranslationReference("actor")] + const string InvalidActorName = "notification-invalid-actor-name"; + + [TranslationReference("actor")] + const string UnbuildableActorName = "notification-unbuildable-actor-name"; + readonly DeveloperModeInfo info; public bool Enabled { get; private set; } @@ -274,6 +281,47 @@ public void ResolveOrder(Actor self, Order order) break; } + case "DevProduce": + { + if (order.Target.Type != TargetType.Actor) + break; + + var args = order.TargetString.Split(' '); + var producer = order.Target.Actor; + var production = producer.TraitsImplementing().FirstOrDefault(p => !p.IsTraitDisabled && !p.IsTraitPaused); + var actors = self.World.Map.Rules.Actors; + var actorToProduce = actors.Keys.Contains(args[0]) ? actors[args[0]] : null; + var buildable = actorToProduce?.TraitInfos().Count > 0; + + if (production != null && actorToProduce != null && buildable) + { + var faction = args.Length > 1 ? args[1] : BuildableInfo.GetInitialFaction(actorToProduce, production.Faction); + var inits = new TypeDictionary + { + new OwnerInit(producer.Owner), + new FactionInit(faction) + }; + + producer.QueueActivity(new WaitFor(() => production.Produce(producer, actorToProduce, null, inits, 0))); + } + + if (actorToProduce == null) + TextNotificationsManager.Debug(TranslationProvider.GetString(InvalidActorName, Translation.Arguments("actor", args[0]))); + else if (!buildable) + TextNotificationsManager.Debug(TranslationProvider.GetString(UnbuildableActorName, Translation.Arguments("actor", args[0]))); + + break; + } + + case "DevClearResources": + { + var resLayer = self.World.WorldActor.TraitOrDefault(); + foreach (var cell in self.World.Map.ProjectedCells.ToArray()) + resLayer.ClearResources(((MPos)cell).ToCPos(self.World.Map.Grid.Type)); + + break; + } + default: return; } diff --git a/OpenRA.Mods.Common/Traits/Player/ForceUncloakManager.cs b/OpenRA.Mods.Common/Traits/Player/ForceUncloakManager.cs new file mode 100644 index 000000000000..90200fd08cca --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Player/ForceUncloakManager.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Attach this to the player actor to allow cloak actors can be forced reveal, if player only has actors can be force uncloack.")] + public sealed class ForceUncloakManagerInfo : ConditionalTraitInfo + { + [Desc("Scan interval. Set it longer for performace.")] + public readonly int ScanInterval = 51; + + [Desc("Does force uncloak reversible?")] + public readonly bool Irreversible = true; + + [Desc("Ignore those actors when checking, like some of the proxy and dummy actors")] + public readonly HashSet IgnoreActors = new(); + + [Desc("The duration before force uncloak when there are only units can be forced uncloak. Set to < 0 can skip warning.")] + public readonly int DurationBeforeForceUncloak = 2000; + + [NotificationReference("Speech")] + [Desc("Sound the perpetrator will hear after successful infiltration.")] + public readonly string ForceUncloakNotification = null; + + [TranslationReference(optional: true)] + [Desc("Text notification the perpetrator will see after successful infiltration.")] + public readonly string ForceUncloakTextNotification = null; + + [NotificationReference("Speech")] + [Desc("Sound the perpetrator will hear after successful infiltration.")] + public readonly string ForceUncloakWarningNotification = null; + + [TranslationReference(optional: true)] + [Desc("Text notification the perpetrator will see after successful infiltration.")] + public readonly string ForceUncloakWarningTextNotification = null; + + public override object Create(ActorInitializer init) { return new ForceUncloakManager(this, init.Self); } + } + + public sealed class ForceUncloakManager : ConditionalTrait, ITick, INotifyCreated + { + readonly World world; + readonly ForceUncloakManagerInfo info; + bool forcedUncloakWarning; + int scanInterval; + int remainingWarningtime; + + public bool ForcedUncloak { get; private set; } + + public ForceUncloakManager(ForceUncloakManagerInfo info, Actor self) + : base(info) + { + this.info = info; + world = self.World; + } + + protected override void Created(Actor self) + { + scanInterval = world.SharedRandom.Next(1, info.ScanInterval); + base.Created(self); + } + + void ITick.Tick(Actor self) + { + if (IsTraitDisabled || (info.Irreversible && ForcedUncloak) || --scanInterval > 0) + return; + + // When there are only actors that can be forced uncloak, first warning then force uncloak + if (world.ActorsHavingTrait().Where(a => a.Owner == self.Owner && !info.IgnoreActors.Contains(a.Info.Name) && a.IsInWorld && !a.IsDead).All(a => a.TraitsImplementing().Any(c => c.Info.CanBeForcedUncloak && !c.IsTraitDisabled))) + { + if (!forcedUncloakWarning) + { + forcedUncloakWarning = true; + remainingWarningtime = info.DurationBeforeForceUncloak; + + // Show warning notification if we can show warning + if (info.DurationBeforeForceUncloak > 0) + { + if (info.ForceUncloakWarningNotification != null) + Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", info.ForceUncloakWarningNotification, self.Owner.Faction.InternalName); + + TextNotificationsManager.AddTransientLine(self.Owner, info.ForceUncloakWarningTextNotification); + + scanInterval = Math.Min(remainingWarningtime, info.ScanInterval); + } + else + scanInterval = 0; + } + else if (!ForcedUncloak) + { + remainingWarningtime -= info.ScanInterval; + if (remainingWarningtime < 0) + { + ForcedUncloak = true; + + if (info.ForceUncloakNotification != null) + Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", info.ForceUncloakNotification, self.Owner.Faction.InternalName); + + TextNotificationsManager.AddTransientLine(self.Owner, info.ForceUncloakTextNotification); + return; + } + + scanInterval = Math.Min(remainingWarningtime, info.ScanInterval); + } + else + scanInterval = info.ScanInterval; + } + + // When there is any actors cannot be forced uncloak, restore the check + else + { + scanInterval = info.ScanInterval; + remainingWarningtime = info.DurationBeforeForceUncloak; + forcedUncloakWarning = false; + ForcedUncloak = false; + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs b/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs index 949d1c8c67c0..01bcd179a7fb 100644 --- a/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs +++ b/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs @@ -104,7 +104,7 @@ void IResolveOrder.ResolveOrder(Actor self, Order order) var faction = producer.Trait?.Faction ?? self.Owner.Faction.InternalName; var buildingInfo = actorInfo.TraitInfo(); - var buildableInfo = actorInfo.TraitInfoOrDefault(); + var buildableInfo = BuildableInfo.GetTraitForQueue(actorInfo, queue.Info.Type); if (buildableInfo != null && buildableInfo.ForceFaction != null) faction = buildableInfo.ForceFaction; @@ -129,8 +129,10 @@ void IResolveOrder.ResolveOrder(Actor self, Order order) new PlaceBuildingInit() }); - foreach (var s in buildingInfo.BuildSounds) - Game.Sound.PlayToPlayer(SoundType.World, order.Player, s, placed.CenterPosition); + var pos = placed.CenterPosition; + if (buildingInfo.AudibleThroughFog || (!w.ShroudObscures(pos) && !w.FogObscures(pos))) + foreach (var s in buildingInfo.BuildSounds) + Game.Sound.Play(SoundType.World, s, pos, buildingInfo.SoundVolume); // Build the connection segments var segmentType = actorInfo.TraitInfo().SegmentType; @@ -182,14 +184,16 @@ void IResolveOrder.ResolveOrder(Actor self, Order order) return; pluggable.EnablePlug(a, plugInfo.Type); - foreach (var s in buildingInfo.BuildSounds) - Game.Sound.PlayToPlayer(SoundType.World, order.Player, s, a.CenterPosition); + var pos = a.CenterPosition; + if (buildingInfo.AudibleThroughFog || (!w.ShroudObscures(pos) && !w.FogObscures(pos))) + foreach (var s in buildingInfo.BuildSounds) + Game.Sound.Play(SoundType.World, s, pos, buildingInfo.SoundVolume); } } else { if (!self.World.CanPlaceBuilding(targetLocation, actorInfo, buildingInfo, null) - || !buildingInfo.IsCloseEnoughToBase(self.World, order.Player, actorInfo, targetLocation)) + || !buildingInfo.IsCloseEnoughToBase(self.World, order.Player, actorInfo, queue.Actor, targetLocation)) return; var building = w.CreateActor(actorInfo.Name, new TypeDictionary @@ -200,8 +204,10 @@ void IResolveOrder.ResolveOrder(Actor self, Order order) new PlaceBuildingInit() }); - foreach (var s in buildingInfo.BuildSounds) - Game.Sound.PlayToPlayer(SoundType.World, order.Player, s, building.CenterPosition); + var pos = building.CenterPosition; + if (buildingInfo.AudibleThroughFog || (!w.ShroudObscures(pos) && !w.FogObscures(pos))) + foreach (var s in buildingInfo.BuildSounds) + Game.Sound.Play(SoundType.World, s, pos, buildingInfo.SoundVolume); } if (producer.Actor != null) diff --git a/OpenRA.Mods.Common/Traits/Player/PlayerStatistics.cs b/OpenRA.Mods.Common/Traits/Player/PlayerStatistics.cs index e559df2ac74b..c6c7f4d291d6 100644 --- a/OpenRA.Mods.Common/Traits/Player/PlayerStatistics.cs +++ b/OpenRA.Mods.Common/Traits/Player/PlayerStatistics.cs @@ -143,6 +143,7 @@ public class ArmyUnit public readonly int BuildPaletteOrder; public readonly TooltipInfo TooltipInfo; public readonly BuildableInfo BuildableInfo; + public readonly bool Upgrade; public int Count { get; set; } @@ -153,7 +154,7 @@ public ArmyUnit(ActorInfo actorInfo, Player owner) var queues = owner.World.Map.Rules.Actors.Values .SelectMany(a => a.TraitInfos()); - BuildableInfo = actorInfo.TraitInfoOrDefault(); + BuildableInfo = actorInfo.TraitInfos().FirstOrDefault(); TooltipInfo = actorInfo.TraitInfos().FirstOrDefault(info => info.EnabledByDefault); var rsi = actorInfo.TraitInfoOrDefault(); @@ -170,6 +171,12 @@ public ArmyUnit(ActorInfo actorInfo, Player owner) .Select(q => q.DisplayOrder) .MinByOrDefault(o => o); } + + var upsi = actorInfo.TraitInfoOrDefault(); + if (upsi != null) + Upgrade = upsi.AddToUpgradesTab; + else + Upgrade = false; } } @@ -186,6 +193,9 @@ public class UpdatesPlayerStatisticsInfo : TraitInfo [Desc("Count this actor as a different type in the spectator army display.")] public readonly string OverrideActor = null; + [Desc("Show this actor in the upgrades display.")] + public bool AddToUpgradesTab = false; + public override object Create(ActorInitializer init) { return new UpdatesPlayerStatistics(this, init.Self); } } @@ -214,8 +224,10 @@ void INotifyKilled.Killed(Actor self, AttackInfo e) return; if (includedInArmyValue) - { playerStats.ArmyValue -= cost; + + if (includedInArmyValue || info.AddToUpgradesTab) + { includedInArmyValue = false; playerStats.Units[actorName].Count--; } @@ -255,10 +267,10 @@ void INotifyCreated.Created(Actor self) { includedInArmyValue = info.AddToArmyValue; if (includedInArmyValue) - { playerStats.ArmyValue += cost; + + if (includedInArmyValue || info.AddToUpgradesTab) playerStats.Units[actorName].Count++; - } includedInAssetsValue = info.AddToAssetsValue; if (includedInAssetsValue) @@ -272,6 +284,10 @@ void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newO { playerStats.ArmyValue -= cost; newOwnerStats.ArmyValue += cost; + } + + if (includedInArmyValue || info.AddToUpgradesTab) + { playerStats.Units[actorName].Count--; newOwnerStats.Units[actorName].Count++; } @@ -288,8 +304,10 @@ void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newO void INotifyActorDisposing.Disposing(Actor self) { if (includedInArmyValue) - { playerStats.ArmyValue -= cost; + + if (includedInArmyValue || info.AddToUpgradesTab) + { includedInArmyValue = false; playerStats.Units[actorName].Count--; } diff --git a/OpenRA.Mods.Common/Traits/Player/ProductionQueue.cs b/OpenRA.Mods.Common/Traits/Player/ProductionQueue.cs index fa328a0316f3..4916f5ebdc18 100644 --- a/OpenRA.Mods.Common/Traits/Player/ProductionQueue.cs +++ b/OpenRA.Mods.Common/Traits/Player/ProductionQueue.cs @@ -35,12 +35,18 @@ public class ProductionQueueInfo : TraitInfo, IRulesetLoaded [Desc("Only enable this queue for certain factions.")] public readonly HashSet Factions = new(); + [Desc("Show the queue for these factions, even if it doesn't have any buildable unit in it.")] + public readonly HashSet AlwaysShowForFactions = new(); + [Desc("Should the prerequisite remain enabled if the owner changes?")] public readonly bool Sticky = true; [Desc("Should right clicking on the icon instantly cancel the production instead of putting it on hold?")] public readonly bool DisallowPaused = false; + [Desc("Drain the cost of actors instantly at the start of production.")] + public readonly bool InstantCashDrain = false; + [Desc("This percentage value is multiplied with actor cost to translate into build time (lower means faster).")] public readonly int BuildDurationModifier = 100; @@ -93,6 +99,10 @@ public class ProductionQueueInfo : TraitInfo, IRulesetLoaded "The filename of the audio is defined per faction in notifications.yaml.")] public readonly string CannotPlaceAudio = null; + [Desc("Notification displayed when you can't place a building.", + "Overrides PlaceBuilding.CannotPlaceTextNotification for this queue.")] + public readonly string CannotPlaceTextNotification = null; + [NotificationReference("Speech")] [Desc("Notification played when user clicks on the build palette icon.", "The filename of the audio is defined per faction in notifications.yaml.")] @@ -134,18 +144,18 @@ public class ProductionQueue : IResolveOrder, ITick, ITechTreeElement, INotifyOw public readonly ProductionQueueInfo Info; // A list of things we could possibly build - protected readonly Dictionary Producible = new(); + public readonly Dictionary Producible = new(); protected readonly List Queue = new(); readonly IEnumerable allProducibles; readonly IEnumerable buildableProducibles; protected Production[] productionTraits; + protected ConditionPrerequisiteInfo[] conditionPrerequisites; // Will change if the owner changes PowerManager playerPower; protected PlayerResources playerResources; protected DeveloperMode developerMode; - protected TechTree techTree; public Actor Actor { get; } @@ -154,6 +164,10 @@ public class ProductionQueue : IResolveOrder, ITick, ITechTreeElement, INotifyOw public string Faction { get; private set; } + public TechTree TechTree { get; private set; } + + public bool AlwaysVisible { get; private set; } + [Sync] public bool IsValidFaction { get; private set; } @@ -164,6 +178,7 @@ public ProductionQueue(ActorInitializer init, ProductionQueueInfo info) Faction = init.GetValue(Actor.Owner.Faction.InternalName); IsValidFaction = info.Factions.Count == 0 || info.Factions.Contains(Faction); + AlwaysVisible = info.AlwaysShowForFactions.Contains(Faction); Enabled = IsValidFaction; allProducibles = Producible.Where(a => a.Value.Buildable || a.Value.Visible).Select(a => a.Key); @@ -175,9 +190,10 @@ void INotifyCreated.Created(Actor self) playerPower = self.Owner.PlayerActor.TraitOrDefault(); playerResources = self.Owner.PlayerActor.Trait(); developerMode = self.Owner.PlayerActor.Trait(); - techTree = self.Owner.PlayerActor.Trait(); + TechTree = self.Owner.PlayerActor.Trait(); productionTraits = self.TraitsImplementing().Where(p => p.Info.Produces.Contains(Info.Type)).ToArray(); + conditionPrerequisites = self.Info.TraitInfos().ToArray(); CacheProducibles(); } @@ -196,18 +212,19 @@ void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newO playerPower = newOwner.PlayerActor.TraitOrDefault(); playerResources = newOwner.PlayerActor.Trait(); developerMode = newOwner.PlayerActor.Trait(); - techTree = newOwner.PlayerActor.Trait(); + TechTree = newOwner.PlayerActor.Trait(); if (!Info.Sticky) { Faction = self.Owner.Faction.InternalName; IsValidFaction = Info.Factions.Count == 0 || Info.Factions.Contains(Faction); + AlwaysVisible = Info.AlwaysShowForFactions.Contains(Faction); } // Regenerate the producibles and tech tree state oldOwner.PlayerActor.Trait().Remove(this); CacheProducibles(); - techTree.Update(); + TechTree.Update(); } void INotifyKilled.Killed(Actor killed, AttackInfo e) { if (killed == Actor) { ClearQueue(); Enabled = false; } } @@ -218,18 +235,24 @@ void INotifySold.Sold(Actor self) { } void INotifyTransform.OnTransform(Actor self) { } void INotifyTransform.AfterTransform(Actor self) { } - void CacheProducibles() + public void CacheProducibles() { - Producible.Clear(); + foreach (var a in Actor.World.Map.Rules.Actors.Values) + { + if (!Actor.Info.TraitInfos().Any(t => t.Queue.Contains(Info.Type) && t.Actor == a.Name)) + Producible.Remove(a); + } + if (!Enabled) return; foreach (var a in AllBuildables(Info.Type)) { - var bi = a.TraitInfo(); + var bi = BuildableInfo.GetTraitForQueue(a, Info.Type); - Producible.Add(a, new ProductionState()); - techTree.Add(a.Name, bi.Prerequisites, bi.BuildLimit, this); + if (!Producible.ContainsKey(a)) + Producible.Add(a, new ProductionState()); + TechTree.Add(a.Name, bi.Prerequisites, bi.BuildLimit, this); } } @@ -239,26 +262,38 @@ IEnumerable AllBuildables(string category) .Where(x => x.Name[0] != '^' && x.HasTraitInfo() && - x.TraitInfo().Queue.Contains(category)); + BuildableInfo.GetTraitForQueue(x, category) != null); } public void PrerequisitesAvailable(string key) { + if (conditionPrerequisites.Where(t => t.Queue.Contains(Info.Type) && t.Actor == key).Any()) + return; + Producible[Actor.World.Map.Rules.Actors[key]].Buildable = true; } public void PrerequisitesUnavailable(string key) { + if (conditionPrerequisites.Where(t => t.Queue.Contains(Info.Type) && t.Actor == key).Any()) + return; + Producible[Actor.World.Map.Rules.Actors[key]].Buildable = false; } public void PrerequisitesItemHidden(string key) { + if (conditionPrerequisites.Where(t => t.Queue.Contains(Info.Type) && t.Actor == key).Any()) + return; + Producible[Actor.World.Map.Rules.Actors[key]].Visible = false; } public void PrerequisitesItemVisible(string key) { + if (conditionPrerequisites.Where(t => t.Queue.Contains(Info.Type) && t.Actor == key).Any()) + return; + Producible[Actor.World.Map.Rules.Actors[key]].Visible = true; } @@ -363,24 +398,36 @@ public bool CanQueue(ActorInfo actor, out string notificationAudio, out string n notificationAudio = Info.BlockedAudio; notificationText = Info.BlockedTextNotification; - var bi = actor.TraitInfoOrDefault(); + var bi = BuildableInfo.GetTraitForQueue(actor, Info.Type); if (bi == null) return false; + if (Info.InstantCashDrain) + { + var cost = GetProductionCost(actor); + if (playerResources.Cash + playerResources.Resources < cost) + { + notificationAudio = playerResources.Info.InsufficientFundsNotification; + notificationText = playerResources.Info.InsufficientFundsTextNotification; + + return false; + } + } + if (!developerMode.AllTech) { if (Info.QueueLimit > 0 && Queue.Count >= Info.QueueLimit) { - notificationAudio = Info.LimitedAudio; - notificationText = Info.LimitedTextNotification; + notificationAudio = bi.LimitedAudio ?? Info.LimitedAudio; + notificationText = bi.LimitedTextNotification ?? Info.LimitedTextNotification; return false; } var queueCount = Queue.Count(i => i.Item == actor.Name); if (Info.ItemLimit > 0 && queueCount >= Info.ItemLimit) { - notificationAudio = Info.LimitedAudio; - notificationText = Info.LimitedTextNotification; + notificationAudio = bi.LimitedAudio ?? Info.LimitedAudio; + notificationText = bi.LimitedTextNotification ?? Info.LimitedTextNotification; return false; } @@ -393,8 +440,8 @@ public bool CanQueue(ActorInfo actor, out string notificationAudio, out string n } } - notificationAudio = Info.QueuedAudio; - notificationText = Info.QueuedTextNotification; + notificationAudio = bi.QueuedAudio ?? Info.QueuedAudio; + notificationText = bi.QueuedTextNotification ?? Info.QueuedTextNotification; return true; } @@ -408,11 +455,7 @@ public void ResolveOrder(Actor self, Order order) { case "StartProduction": var unit = rules.Actors[order.TargetString]; - var bi = unit.TraitInfo(); - - // Not built by this queue - if (!bi.Queue.Contains(Info.Type)) - return; + var bi = BuildableInfo.GetTraitForQueue(unit, Info.Type); // You can't build that if (BuildableItems().All(b => b.Name != order.TargetString)) @@ -444,6 +487,9 @@ public void ResolveOrder(Actor self, Order order) var amountToBuild = Math.Min(fromLimit, order.ExtraData); for (var n = 0; n < amountToBuild; n++) { + if (Info.InstantCashDrain && !playerResources.TakeCash(cost, true)) + return; + var hasPlayedSound = false; BeginProduction(new ProductionItem(this, order.TargetString, cost, playerPower, () => self.World.AddFrameEndTask(_ => { @@ -452,17 +498,19 @@ public void ResolveOrder(Actor self, Order order) return; var isBuilding = unit.HasTraitInfo(); + var readyAudio = bi.ReadyAudio ?? Info.ReadyAudio; + var readyTextNotification = bi.ReadyTextNotification ?? Info.ReadyTextNotification; if (isBuilding && !hasPlayedSound) { - hasPlayedSound = Game.Sound.PlayNotification(rules, self.Owner, "Speech", Info.ReadyAudio, self.Owner.Faction.InternalName); - TextNotificationsManager.AddTransientLine(self.Owner, Info.ReadyTextNotification); + hasPlayedSound = Game.Sound.PlayNotification(rules, self.Owner, "Speech", readyAudio, self.Owner.Faction.InternalName); + TextNotificationsManager.AddTransientLine(self.Owner, readyTextNotification); } else if (!isBuilding) { if (BuildUnit(unit)) { - Game.Sound.PlayNotification(rules, self.Owner, "Speech", Info.ReadyAudio, self.Owner.Faction.InternalName); - TextNotificationsManager.AddTransientLine(self.Owner, Info.ReadyTextNotification); + Game.Sound.PlayNotification(rules, self.Owner, "Speech", readyAudio, self.Owner.Faction.InternalName); + TextNotificationsManager.AddTransientLine(self.Owner, readyTextNotification); } else if (!hasPlayedSound && time > 0) { @@ -494,7 +542,7 @@ public virtual int GetBuildTime(ActorInfo unit, BuildableInfo bi) time = GetProductionCost(unit); var modifiers = unit.TraitInfos() - .Select(t => t.GetProductionTimeModifier(techTree, Info.Type)) + .Select(t => t.GetProductionTimeModifier(TechTree, Info.Type)) .Append(bi.BuildDurationModifier) .Append(Info.BuildDurationModifier); @@ -508,7 +556,7 @@ public virtual int GetProductionCost(ActorInfo unit) return 0; var modifiers = unit.TraitInfos() - .Select(t => t.GetProductionCostModifier(techTree, Info.Type)); + .Select(t => t.GetProductionCostModifier(TechTree, Info.Type)); return Util.ApplyPercentageModifiers(valued.Cost, modifiers); } @@ -620,7 +668,7 @@ protected virtual bool BuildUnit(ActorInfo unit) new FactionInit(BuildableInfo.GetInitialFaction(unit, Faction)) }; - var bi = unit.TraitInfo(); + var bi = BuildableInfo.GetTraitForQueue(unit, Info.Type); var type = developerMode.AllTech ? Info.Type : (bi.BuildAtProductionType ?? Info.Type); var item = Queue.First(i => i.Done && i.Item == unit.Name); if (!mostLikelyProducerTrait.IsTraitPaused && mostLikelyProducerTrait.Produce(Actor, unit, type, inits, item.TotalCost)) @@ -660,21 +708,22 @@ public class ProductionItem public bool Infinite { get; set; } public int BuildPaletteOrder { get; } - readonly ActorInfo ai; - readonly BuildableInfo bi; + public readonly ActorInfo ActorInfo; + public readonly BuildableInfo BuildableInfo; readonly PowerManager pm; public ProductionItem(ProductionQueue queue, string item, int cost, PowerManager pm, Action onComplete) { Item = item; RemainingTime = TotalTime = 1; - RemainingCost = TotalCost = cost; + TotalCost = cost; + RemainingCost = queue.Info.InstantCashDrain ? 0 : cost; OnComplete = onComplete; Queue = queue; this.pm = pm; - ai = Queue.Actor.World.Map.Rules.Actors[Item]; - bi = ai.TraitInfo(); - BuildPaletteOrder = bi.BuildPaletteOrder; + ActorInfo = Queue.Actor.World.Map.Rules.Actors[Item]; + BuildableInfo = BuildableInfo.GetTraitForQueue(ActorInfo, Queue.Info.Type); + BuildPaletteOrder = BuildableInfo.BuildPaletteOrder; Infinite = false; } @@ -682,7 +731,7 @@ public void Tick(PlayerResources pr) { if (!Started) { - var time = Queue.GetBuildTime(ai, bi); + var time = Queue.GetBuildTime(ActorInfo, BuildableInfo); if (time > 0) RemainingTime = TotalTime = time; @@ -708,12 +757,16 @@ public void Tick(PlayerResources pr) return; } - var expectedRemainingCost = RemainingTime == 1 ? 0 : TotalCost * RemainingTime / Math.Max(1, TotalTime); - var costThisFrame = RemainingCost - expectedRemainingCost; - if (costThisFrame != 0 && !pr.TakeCash(costThisFrame, true)) - return; + if (!Queue.Info.InstantCashDrain) + { + var expectedRemainingCost = RemainingTime == 1 ? 0 : TotalCost * RemainingTime / Math.Max(1, TotalTime); + var costThisFrame = RemainingCost - expectedRemainingCost; + if (costThisFrame != 0 && !pr.TakeCash(costThisFrame, true)) + return; + + RemainingCost -= costThisFrame; + } - RemainingCost -= costThisFrame; RemainingTime -= 1; if (RemainingTime > 0) return; diff --git a/OpenRA.Mods.Common/Traits/Player/TechTree.cs b/OpenRA.Mods.Common/Traits/Player/TechTree.cs index facb3a57b856..e243e0a5de5f 100644 --- a/OpenRA.Mods.Common/Traits/Player/TechTree.cs +++ b/OpenRA.Mods.Common/Traits/Player/TechTree.cs @@ -35,8 +35,8 @@ public TechTree(ActorInitializer init) public void ActorChanged(Actor a) { - var bi = a.Info.TraitInfoOrDefault(); - if (a.Owner == Owner && (a.Info.HasTraitInfo() || (bi != null && bi.BuildLimit > 0))) + var bis = a.Info.TraitInfos(); + if (a.Owner == Owner && (a.Info.HasTraitInfo() || bis.Any(bi => bi.BuildLimit > 0))) Update(); } @@ -99,7 +99,7 @@ static Dictionary GatherOwnedPrerequisites(Player player) a.Actor.IsInWorld && !a.Actor.IsDead && !ret.ContainsKey(a.Actor.Info.Name) && - a.Actor.Info.TraitInfo().BuildLimit > 0); + a.Actor.Info.TraitInfos().Any(bi => bi.BuildLimit > 0)); foreach (var buildable in buildables) { @@ -171,7 +171,7 @@ public void Update(Dictionary ownedPrerequisites) var nowHasPrerequisites = !hasReachedLimit && HasPrerequisites(ownedPrerequisites); var nowHidden = IsHidden(ownedPrerequisites); - if (initialized == false) + if (!initialized) { initialized = true; hasPrerequisites = !nowHasPrerequisites; diff --git a/OpenRA.Mods.Common/Traits/ProducibleWithLevel.cs b/OpenRA.Mods.Common/Traits/ProducibleWithLevel.cs index 2212ec224648..0901451c3cc1 100644 --- a/OpenRA.Mods.Common/Traits/ProducibleWithLevel.cs +++ b/OpenRA.Mods.Common/Traits/ProducibleWithLevel.cs @@ -10,6 +10,7 @@ #endregion using System; +using System.Collections.Generic; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -20,27 +21,45 @@ public class ProducibleWithLevelInfo : TraitInfo, Requires { public readonly string[] Prerequisites = Array.Empty(); + [Desc("Only grant this level for certain factions.")] + public readonly HashSet Factions = new(); + + [Desc("Should it recheck everything when it is captured?")] + public readonly bool ResetOnOwnerChange = false; + [Desc("Number of levels to give to the actor on creation.")] public readonly int InitialLevels = 1; [Desc("Should the level-up animation be suppressed when actor is created?")] public readonly bool SuppressLevelupAnimation = true; - public override object Create(ActorInitializer init) { return new ProducibleWithLevel(this); } + public override object Create(ActorInitializer init) { return new ProducibleWithLevel(init, this); } } - public class ProducibleWithLevel : INotifyCreated + public class ProducibleWithLevel : INotifyCreated, INotifyOwnerChanged { readonly ProducibleWithLevelInfo info; + string faction; - public ProducibleWithLevel(ProducibleWithLevelInfo info) + public ProducibleWithLevel(ActorInitializer init, ProducibleWithLevelInfo info) { this.info = info; + + faction = init.GetValue(init.Self.Owner.Faction.InternalName); + } + + public void OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + if (info.ResetOnOwnerChange) + faction = newOwner.Faction.InternalName; } void INotifyCreated.Created(Actor self) { - if (!self.Owner.PlayerActor.Trait().HasPrerequisites(info.Prerequisites)) + if (info.Factions.Count > 0 && !info.Factions.Contains(faction)) + return; + + if (info.Prerequisites.Length > 0 && !self.Owner.PlayerActor.Trait().HasPrerequisites(info.Prerequisites)) return; var ge = self.Trait(); diff --git a/OpenRA.Mods.Common/Traits/Production.cs b/OpenRA.Mods.Common/Traits/Production.cs index 7d6f7f202fd3..7446a2fce066 100644 --- a/OpenRA.Mods.Common/Traits/Production.cs +++ b/OpenRA.Mods.Common/Traits/Production.cs @@ -122,7 +122,13 @@ public virtual bool Produce(Actor self, ActorInfo producee, string productionTyp var exit = SelectExit(self, producee, productionType); if (exit != null || self.OccupiesSpace == null || !producee.HasTraitInfo()) { - DoProduction(self, producee, exit?.Info, productionType, inits); + var buildable = BuildableInfo.GetTraitForQueue(producee, productionType); + if (buildable != null) + for (var n = 0; n < buildable.BuildAmount; n++) + DoProduction(self, producee, exit?.Info, productionType, inits); + else + DoProduction(self, producee, exit?.Info, productionType, inits); + return true; } diff --git a/OpenRA.Mods.Common/Traits/ProductionQueueFromSelection.cs b/OpenRA.Mods.Common/Traits/ProductionQueueFromSelection.cs index 1c86547be483..8707b8776a81 100644 --- a/OpenRA.Mods.Common/Traits/ProductionQueueFromSelection.cs +++ b/OpenRA.Mods.Common/Traits/ProductionQueueFromSelection.cs @@ -63,7 +63,7 @@ void INotifySelection.SelectionChanged() .FirstOrDefault(q => q.Enabled && types.Contains(q.Info.Type)); } - if (queue == null || !queue.BuildableItems().Any()) + if (queue == null || (!queue.BuildableItems().Any() && !queue.AlwaysVisible)) return; if (tabsWidget.Value != null) diff --git a/OpenRA.Mods.Common/Traits/Rearmable.cs b/OpenRA.Mods.Common/Traits/Rearmable.cs index c3031564a527..f6b9ed9f6688 100644 --- a/OpenRA.Mods.Common/Traits/Rearmable.cs +++ b/OpenRA.Mods.Common/Traits/Rearmable.cs @@ -20,7 +20,7 @@ public class RearmableInfo : TraitInfo [ActorReference] [FieldLoader.Require] [Desc("Actors that this actor can dock to and get rearmed by.")] - public readonly HashSet RearmActors = new() { }; + public readonly HashSet RearmActors = new(); [Desc("Name(s) of AmmoPool(s) that use this trait to rearm.")] public readonly HashSet AmmoPools = new() { "primary" }; diff --git a/OpenRA.Mods.Common/Traits/RejectsMoveToAttack.cs b/OpenRA.Mods.Common/Traits/RejectsMoveToAttack.cs new file mode 100644 index 000000000000..3cbd35cd71ca --- /dev/null +++ b/OpenRA.Mods.Common/Traits/RejectsMoveToAttack.cs @@ -0,0 +1,25 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("When enabled actor can't move within range to attack a target.")] + public class RejectsMoveToAttackInfo : ConditionalTraitInfo + { + public override object Create(ActorInitializer init) { return new RejectsMoveToAttack(this); } + } + + public class RejectsMoveToAttack : ConditionalTrait + { + public RejectsMoveToAttack(RejectsMoveToAttackInfo info) + : base(info) { } + } +} diff --git a/OpenRA.Mods.Common/Traits/Render/FloatingSpriteEmitter.cs b/OpenRA.Mods.Common/Traits/Render/FloatingSpriteEmitter.cs index 641797795191..1a8cb149b2b9 100644 --- a/OpenRA.Mods.Common/Traits/Render/FloatingSpriteEmitter.cs +++ b/OpenRA.Mods.Common/Traits/Render/FloatingSpriteEmitter.cs @@ -92,7 +92,7 @@ protected override void TraitEnabled(Actor self) void ITick.Tick(Actor self) { - if (IsTraitDisabled) + if (!self.IsInWorld || IsTraitDisabled) return; if (Info.Duration > 0 && --duration < 0) diff --git a/OpenRA.Mods.Common/Traits/Render/LeavesTrails.cs b/OpenRA.Mods.Common/Traits/Render/LeavesTrails.cs index ed75b4b401a7..1e9a28a17f7f 100644 --- a/OpenRA.Mods.Common/Traits/Render/LeavesTrails.cs +++ b/OpenRA.Mods.Common/Traits/Render/LeavesTrails.cs @@ -64,7 +64,7 @@ public class LeavesTrailsInfo : ConditionalTraitInfo public override object Create(ActorInitializer init) { return new LeavesTrails(this); } } - public class LeavesTrails : ConditionalTrait, ITick + public class LeavesTrails : ConditionalTrait, ITick, INotifyAddedToWorld { BodyOrientation body; IFacing facing; @@ -99,7 +99,7 @@ protected override void Created(Actor self) void ITick.Tick(Actor self) { - if (IsTraitDisabled) + if (!self.IsInWorld || IsTraitDisabled) return; wasStationary = !isMoving; @@ -156,5 +156,10 @@ protected override void TraitEnabled(Actor self) { cachedPosition = self.CenterPosition; } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + cachedPosition = self.CenterPosition; + } } } diff --git a/OpenRA.Mods.Common/Traits/Render/ProductionBar.cs b/OpenRA.Mods.Common/Traits/Render/ProductionBar.cs index c22c1454aacc..a3f2a1b8e85e 100644 --- a/OpenRA.Mods.Common/Traits/Render/ProductionBar.cs +++ b/OpenRA.Mods.Common/Traits/Render/ProductionBar.cs @@ -77,7 +77,7 @@ void ITick.Tick(Actor self) return; var current = queue.AllQueued().Where(i => i.Started).MinByOrDefault(i => i.RemainingTime); - value = current != null ? 1 - (float)current.RemainingCost / current.TotalCost : 0; + value = current != null ? 1 - (float)current.RemainingTime / current.TotalTime : 0; } float ISelectionBar.GetValue() diff --git a/OpenRA.Mods.Common/Traits/Render/ProductionIconOverlayManager.cs b/OpenRA.Mods.Common/Traits/Render/ProductionIconOverlayManager.cs index 1cf2f64d4fd4..08908ef0495d 100644 --- a/OpenRA.Mods.Common/Traits/Render/ProductionIconOverlayManager.cs +++ b/OpenRA.Mods.Common/Traits/Render/ProductionIconOverlayManager.cs @@ -89,7 +89,7 @@ float2 IProductionIconOverlay.Offset(float2 iconSize) return new float2(x, y); } - bool IProductionIconOverlay.IsOverlayActive(ActorInfo ai) + bool IProductionIconOverlay.IsOverlayActive(ActorInfo ai, Actor producer) { if (!overlayActive.TryGetValue(ai, out var isActive)) return false; diff --git a/OpenRA.Mods.Common/Traits/Render/RenderDebugState.cs b/OpenRA.Mods.Common/Traits/Render/RenderDebugState.cs index bea697c060e7..7059c3f04721 100644 --- a/OpenRA.Mods.Common/Traits/Render/RenderDebugState.cs +++ b/OpenRA.Mods.Common/Traits/Render/RenderDebugState.cs @@ -87,7 +87,7 @@ IEnumerable IRenderAnnotationsWhenSelected.RenderAnnotations(Actor yield break; var squads = squadManagerModules.FirstEnabledConditionalTraitOrDefault()?.Squads; - var squad = squads?.FirstOrDefault(x => x.Units.Contains(self)); + var squad = squads?.FirstOrDefault(x => x.Units.Any(u => u.Actor == self)); if (squad == null) yield break; diff --git a/OpenRA.Mods.Common/Traits/Render/RenderDetectionCircle.cs b/OpenRA.Mods.Common/Traits/Render/RenderDetectionCircle.cs index 27865fcb60c7..e62ed9381c0b 100644 --- a/OpenRA.Mods.Common/Traits/Render/RenderDetectionCircle.cs +++ b/OpenRA.Mods.Common/Traits/Render/RenderDetectionCircle.cs @@ -40,6 +40,9 @@ public class RenderDetectionCircleInfo : TraitInfo, Requires [Desc("Range circle border width.")] public readonly float BorderWidth = 3; + [Desc("Render the circle on the ground regardless of actors height.")] + public readonly bool RenderOnGround = false; + [Desc("When to show the detection circle. Valid values are `Always`, and `WhenSelected`")] public readonly DetectionCircleVisibility Visible = DetectionCircleVisibility.WhenSelected; @@ -70,8 +73,9 @@ IEnumerable RenderCircle(Actor self, DetectionCircleVisibility visi if (range == WDist.Zero) yield break; + var position = self.CenterPosition - new WVec(WDist.Zero, WDist.Zero, info.RenderOnGround ? self.World.Map.DistanceAboveTerrain(self.CenterPosition) : WDist.Zero); yield return new DetectionCircleAnnotationRenderable( - self.CenterPosition, + position, range, 0, info.TrailCount, diff --git a/OpenRA.Mods.Common/Traits/Render/SelectionDecorationsBase.cs b/OpenRA.Mods.Common/Traits/Render/SelectionDecorationsBase.cs index c4cc242ed8c7..f50d104bd37c 100644 --- a/OpenRA.Mods.Common/Traits/Render/SelectionDecorationsBase.cs +++ b/OpenRA.Mods.Common/Traits/Render/SelectionDecorationsBase.cs @@ -20,6 +20,9 @@ namespace OpenRA.Mods.Common.Traits.Render public abstract class SelectionDecorationsBaseInfo : TraitInfo { public readonly Color SelectionBoxColor = Color.White; + + [Desc("Minimum zoom level to render the selection decorations.")] + public readonly float MinimumZoom = 1f; } public abstract class SelectionDecorationsBase : ISelectionDecorations, IRenderAnnotations, INotifyCreated @@ -109,7 +112,7 @@ IEnumerable DrawDecorations(Actor self, WorldRenderer wr) // Hide decorations for spectators that zoom out further than the normal minimum level // This avoids graphical glitches with pip rows and icons overlapping the selection box - if (wr.Viewport.Zoom < wr.Viewport.MinZoom) + if (wr.Viewport.Zoom < Info.MinimumZoom) yield break; var renderDecorations = selected ? selectedDecorations : decorations; diff --git a/OpenRA.Mods.Common/Traits/Render/WithInfantryBody.cs b/OpenRA.Mods.Common/Traits/Render/WithInfantryBody.cs index e492814b3d67..fee8b04a48bb 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithInfantryBody.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithInfantryBody.cs @@ -20,6 +20,9 @@ namespace OpenRA.Mods.Common.Traits.Render { public class WithInfantryBodyInfo : ConditionalTraitInfo, IRenderActorPreviewSpritesInfo, Requires, Requires { + [Desc("Identifier used to assign modifying traits to this sprite body.")] + public readonly string Name = "body"; + public readonly int MinIdleDelay = 30; public readonly int MaxIdleDelay = 110; diff --git a/OpenRA.Mods.Common/Traits/Render/WithProductionDoorOverlay.cs b/OpenRA.Mods.Common/Traits/Render/WithProductionDoorOverlay.cs index 3f13b954575d..8613f4113953 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithProductionDoorOverlay.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithProductionDoorOverlay.cs @@ -76,6 +76,9 @@ void INotifyDamageStateChanged.DamageStateChanged(Actor self, AttackInfo e) void INotifyProduction.UnitProduced(Actor self, Actor other, CPos exit) { + if (other.TraitOrDefault() == null) + return; + openExit = exit; exitingActor = other; desiredFrame = door.CurrentSequence.Length - 1; diff --git a/OpenRA.Mods.Common/Traits/Render/WithProductionOverlay.cs b/OpenRA.Mods.Common/Traits/Render/WithProductionOverlay.cs index e2837f88f0bf..2cabc01f53e5 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithProductionOverlay.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithProductionOverlay.cs @@ -76,16 +76,18 @@ void CacheQueues(Actor self) { // Per-actor production queues = self.TraitsImplementing() - .Where(q => productionInfos.Any(p => p.Produces.Contains(q.Info.Type))) - .Where(q => Info.Queues.Count == 0 || Info.Queues.Contains(q.Info.Type)) + .Where(q => + productionInfos.Any(p => p.Produces.Contains(q.Info.Type)) && + (Info.Queues.Count == 0 || Info.Queues.Contains(q.Info.Type))) .ToArray(); if (queues.Length == 0) { // Player-wide production queues = self.Owner.PlayerActor.TraitsImplementing() - .Where(q => productionInfos.Any(p => p.Produces.Contains(q.Info.Type))) - .Where(q => Info.Queues.Count == 0 || Info.Queues.Contains(q.Info.Type)) + .Where(q => + productionInfos.Any(p => p.Produces.Contains(q.Info.Type)) && + (Info.Queues.Count == 0 || Info.Queues.Contains(q.Info.Type))) .ToArray(); } } diff --git a/OpenRA.Mods.Common/Traits/Render/WithRangeCircle.cs b/OpenRA.Mods.Common/Traits/Render/WithRangeCircle.cs index 1b6ae5c77b1c..06d63175338f 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithRangeCircle.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithRangeCircle.cs @@ -50,12 +50,16 @@ sealed class WithRangeCircleInfo : ConditionalTraitInfo, IPlaceBuildingDecoratio [Desc("Range of the circle")] public readonly WDist Range = WDist.Zero; + [Desc("Render the circle on the ground regardless of actors height.")] + public readonly bool RenderOnGround = false; + public IEnumerable RenderAnnotations(WorldRenderer wr, World w, ActorInfo ai, WPos centerPosition) { + var position = centerPosition - new WVec(WDist.Zero, WDist.Zero, RenderOnGround ? w.Map.DistanceAboveTerrain(centerPosition) : WDist.Zero); if (EnabledByDefault) { yield return new RangeCircleAnnotationRenderable( - centerPosition, + position, Range, 0, Color, @@ -97,9 +101,10 @@ bool Visible public IEnumerable RenderRangeCircle(Actor self, RangeCircleVisibility visibility) { + var position = self.CenterPosition - new WVec(WDist.Zero, WDist.Zero, Info.RenderOnGround ? self.World.Map.DistanceAboveTerrain(self.CenterPosition) : WDist.Zero); if (Info.Visible == visibility && Visible) yield return new RangeCircleAnnotationRenderable( - self.CenterPosition, + position, Info.Range, 0, Info.UsePlayerColor ? self.Owner.Color : Info.Color, diff --git a/OpenRA.Mods.Common/Traits/Repairable.cs b/OpenRA.Mods.Common/Traits/Repairable.cs index 7fe00bc9ea78..ca00f9872009 100644 --- a/OpenRA.Mods.Common/Traits/Repairable.cs +++ b/OpenRA.Mods.Common/Traits/Repairable.cs @@ -23,7 +23,7 @@ public class RepairableInfo : TraitInfo, Requires, Requires RepairActors = new() { }; + public readonly HashSet RepairActors = new(); [VoiceReference] public readonly string Voice = "Action"; diff --git a/OpenRA.Mods.Common/Traits/RepairableNear.cs b/OpenRA.Mods.Common/Traits/RepairableNear.cs index 86726f580bbd..616b67739f24 100644 --- a/OpenRA.Mods.Common/Traits/RepairableNear.cs +++ b/OpenRA.Mods.Common/Traits/RepairableNear.cs @@ -22,7 +22,7 @@ public class RepairableNearInfo : TraitInfo, Requires, Requires RepairActors = new() { }; + public readonly HashSet RepairActors = new(); public readonly WDist CloseEnough = WDist.FromCells(4); diff --git a/OpenRA.Mods.Common/Traits/RevealsShroud.cs b/OpenRA.Mods.Common/Traits/RevealsShroud.cs index f19f4487639d..cfba1394ff3b 100644 --- a/OpenRA.Mods.Common/Traits/RevealsShroud.cs +++ b/OpenRA.Mods.Common/Traits/RevealsShroud.cs @@ -61,7 +61,7 @@ public override WDist Range { get { - if (CachedTraitDisabled) + if (cachedTraitDisabled) return WDist.Zero; var range = Util.ApplyPercentageModifiers(Info.Range.Length, rangeModifiers); diff --git a/OpenRA.Mods.Common/Traits/Sound/AmbientSound.cs b/OpenRA.Mods.Common/Traits/Sound/AmbientSound.cs index 7efd75e6ade9..872baeee4409 100644 --- a/OpenRA.Mods.Common/Traits/Sound/AmbientSound.cs +++ b/OpenRA.Mods.Common/Traits/Sound/AmbientSound.cs @@ -28,6 +28,12 @@ sealed class AmbientSoundInfo : ConditionalTraitInfo "Two values indicate a random delay range.")] public readonly int[] Interval = { 0 }; + [Desc("Do the sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the sounds played at.")] + public readonly float Volume = 1f; + public override object Create(ActorInitializer init) { return new AmbientSound(init.Self, this); } } @@ -64,6 +70,15 @@ void ITick.Tick(Actor self) } } + foreach (var s in currentSounds) + { + if (!Info.AudibleThroughFog) + if (self.World.ShroudObscures(cachedPosition) || self.World.FogObscures(cachedPosition)) + s.Volume = 0f; + else + s.Volume = Info.Volume * Game.Settings.Sound.SoundVolume; + } + if (delay < 0) return; @@ -80,15 +95,16 @@ void StartSound(Actor self) var sound = Info.SoundFiles.RandomOrDefault(Game.CosmeticRandom); ISound s; + var shouldStart = Info.AudibleThroughFog || (!self.World.ShroudObscures(cachedPosition) && !self.World.FogObscures(cachedPosition)); if (self.OccupiesSpace != null) { cachedPosition = self.CenterPosition; - s = loop ? Game.Sound.PlayLooped(SoundType.World, sound, cachedPosition) : - Game.Sound.Play(SoundType.World, sound, self.CenterPosition); + s = loop ? Game.Sound.PlayLooped(SoundType.World, sound, cachedPosition, shouldStart ? Info.Volume : 0f) : + Game.Sound.Play(SoundType.World, sound, self.CenterPosition, shouldStart ? Info.Volume : 0f); } else - s = loop ? Game.Sound.PlayLooped(SoundType.World, sound) : - Game.Sound.Play(SoundType.World, sound); + s = loop ? Game.Sound.PlayLooped(SoundType.World, sound, shouldStart ? Info.Volume : 0f) : + Game.Sound.Play(SoundType.World, sound, shouldStart ? Info.Volume : 0f); currentSounds.Add(s); } diff --git a/OpenRA.Mods.Common/Traits/Sound/DeathSounds.cs b/OpenRA.Mods.Common/Traits/Sound/DeathSounds.cs index a71cf4b1a183..d4cefe6fba2e 100644 --- a/OpenRA.Mods.Common/Traits/Sound/DeathSounds.cs +++ b/OpenRA.Mods.Common/Traits/Sound/DeathSounds.cs @@ -21,6 +21,9 @@ public class DeathSoundsInfo : ConditionalTraitInfo [Desc("Death notification voice.")] public readonly string Voice = "Die"; + [Desc("Do the voices play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + [Desc("Multiply volume with this factor.")] public readonly float VolumeMultiplier = 1f; @@ -42,7 +45,11 @@ void INotifyKilled.Killed(Actor self, AttackInfo e) return; if (Info.DeathTypes.IsEmpty || e.Damage.DamageTypes.Overlaps(Info.DeathTypes)) - self.PlayVoiceLocal(Info.Voice, Info.VolumeMultiplier); + { + var pos = self.CenterPosition; + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + self.PlayVoiceLocal(Info.Voice, Info.VolumeMultiplier); + } } } } diff --git a/OpenRA.Mods.Common/Traits/Sound/SoundOnDamageTransition.cs b/OpenRA.Mods.Common/Traits/Sound/SoundOnDamageTransition.cs index 5d85345aaac3..3b91056a5f31 100644 --- a/OpenRA.Mods.Common/Traits/Sound/SoundOnDamageTransition.cs +++ b/OpenRA.Mods.Common/Traits/Sound/SoundOnDamageTransition.cs @@ -26,6 +26,12 @@ public class SoundOnDamageTransitionInfo : TraitInfo [Desc("DamageType(s) that trigger the sounds. Leave empty to always trigger a sound.")] public readonly BitSet DamageTypes = default; + [Desc("Do the sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the DamagedSounds and DestroyedSounds played at.")] + public readonly float SoundVolume = 1f; + public override object Create(ActorInitializer init) { return new SoundOnDamageTransition(this); } } @@ -44,16 +50,20 @@ void INotifyDamageStateChanged.DamageStateChanged(Actor self, AttackInfo e) return; var rand = Game.CosmeticRandom; + var pos = self.CenterPosition; - if (e.DamageState == DamageState.Dead) - { - var sound = info.DestroyedSounds.RandomOrDefault(rand); - Game.Sound.Play(SoundType.World, sound, self.CenterPosition); - } - else if (e.DamageState >= DamageState.Heavy && e.PreviousDamageState < DamageState.Heavy) + if (info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) { - var sound = info.DamagedSounds.RandomOrDefault(rand); - Game.Sound.Play(SoundType.World, sound, self.CenterPosition); + if (e.DamageState == DamageState.Dead) + { + var sound = info.DestroyedSounds.RandomOrDefault(rand); + Game.Sound.Play(SoundType.World, sound, pos, info.SoundVolume); + } + else if (e.DamageState >= DamageState.Heavy && e.PreviousDamageState < DamageState.Heavy) + { + var sound = info.DamagedSounds.RandomOrDefault(rand); + Game.Sound.Play(SoundType.World, sound, pos, info.SoundVolume); + } } } } diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs index 0a8c23192e41..1d3cea2c4462 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/AirstrikePower.cs @@ -21,9 +21,12 @@ namespace OpenRA.Mods.Common.Traits { public class AirstrikePowerInfo : SupportPowerInfo { - [ActorReference(typeof(AircraftInfo))] - public readonly string UnitType = "badr.bomber"; - public readonly int SquadSize = 1; + [FieldLoader.Require] + public readonly Dictionary UnitTypes = new(); + + [FieldLoader.Require] + public readonly Dictionary SquadSizes = new(); + public readonly WVec SquadOffset = new(-1536, 1536, 0); public readonly int QuantizedFacings = 32; @@ -83,7 +86,7 @@ public Actor[] SendAirstrike(Actor self, WPos target, WAngle? facing = null) if (!facing.HasValue) facing = new WAngle(1024 * self.World.SharedRandom.Next(info.QuantizedFacings) / info.QuantizedFacings); - var altitude = self.World.Map.Rules.Actors[info.UnitType].TraitInfo().CruiseAltitude.Length; + var altitude = self.World.Map.Rules.Actors[info.UnitTypes.First(ut => ut.Key == GetLevel()).Value].TraitInfo().CruiseAltitude.Length; var attackRotation = WRot.FromYaw(facing.Value); var delta = new WVec(0, -1024, 0).Rotate(attackRotation); target += new WVec(0, 0, altitude); @@ -138,17 +141,18 @@ void OnRemovedFromWorld(Actor a) } // Create the actors immediately so they can be returned - for (var i = -info.SquadSize / 2; i <= info.SquadSize / 2; i++) + var squadSize = info.SquadSizes.First(ss => ss.Key == GetLevel()).Value; + for (var i = -squadSize / 2; i <= squadSize / 2; i++) { // Even-sized squads skip the lead plane - if (i == 0 && (info.SquadSize & 1) == 0) + if (i == 0 && (squadSize & 1) == 0) continue; // Includes the 90 degree rotation between body and world coordinates var so = info.SquadOffset; var spawnOffset = new WVec(i * so.Y, -Math.Abs(i) * so.X, 0).Rotate(attackRotation); var targetOffset = new WVec(i * so.Y, 0, 0).Rotate(attackRotation); - var a = self.World.CreateActor(false, info.UnitType, new TypeDictionary + var a = self.World.CreateActor(false, info.UnitTypes.First(ut => ut.Key == GetLevel()).Value, new TypeDictionary { new CenterPositionInit(startEdge + spawnOffset), new OwnerInit(self.Owner), @@ -171,10 +175,10 @@ void OnRemovedFromWorld(Actor a) var j = 0; Actor distanceTestActor = null; - for (var i = -info.SquadSize / 2; i <= info.SquadSize / 2; i++) + for (var i = -squadSize / 2; i <= squadSize / 2; i++) { // Even-sized squads skip the lead plane - if (i == 0 && (info.SquadSize & 1) == 0) + if (i == 0 && (squadSize & 1) == 0) continue; // Includes the 90 degree rotation between body and world coordinates @@ -200,7 +204,7 @@ void OnRemovedFromWorld(Actor a) Info.BeaconPaletteIsPlayerPalette, Info.BeaconPalette, Info.BeaconImage, - Info.BeaconPoster, + Info.BeaconPosters.First(bp => bp.Key == GetLevel()).Value, Info.BeaconPosterPalette, Info.BeaconSequence, Info.ArrowSequence, diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/GrantExternalConditionPower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/GrantExternalConditionPower.cs index d8bf57bd68e8..e5abaaa18379 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/GrantExternalConditionPower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/GrantExternalConditionPower.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Linq; using OpenRA.Graphics; +using OpenRA.Mods.Common.Effects; using OpenRA.Mods.Common.Orders; using OpenRA.Mods.Common.Traits.Render; using OpenRA.Primitives; @@ -23,18 +24,19 @@ public class GrantExternalConditionPowerInfo : SupportPowerInfo { [FieldLoader.Require] [Desc("The condition to apply. Must be included in the target actor's ExternalConditions list.")] - public readonly string Condition = null; + public readonly Dictionary Conditions = new(); + [FieldLoader.Require] [Desc("Duration of the condition (in ticks). Set to 0 for a permanent condition.")] - public readonly int Duration = 0; + public readonly Dictionary Durations = new(); [FieldLoader.Require] [Desc("Size of the footprint of the affected area.")] - public readonly CVec Dimensions = CVec.Zero; + public readonly Dictionary Dimensions = new(); [FieldLoader.Require] [Desc("Actual footprint. Cells marked as x will be affected.")] - public readonly string Footprint = string.Empty; + public readonly Dictionary Footprints = new(); [Desc("Sound to instantly play at the targeted area.")] public readonly string OnFireSound = null; @@ -47,6 +49,14 @@ public class GrantExternalConditionPowerInfo : SupportPowerInfo "This requires the actor to have the WithSpriteBody trait or one of its derivatives.")] public readonly string Sequence = "active"; + public readonly string EffectImage = null; + + [SequenceReference(nameof(EffectImage), allowNullImage: true)] + public readonly string EffectSequence = null; + + [PaletteReference] + public readonly string EffectPalette = null; + [CursorReference] [Desc("Cursor to display when there are no units to apply the condition in range.")] public readonly string BlockedCursor = "move-blocked"; @@ -62,13 +72,14 @@ public class GrantExternalConditionPowerInfo : SupportPowerInfo public class GrantExternalConditionPower : SupportPower { readonly GrantExternalConditionPowerInfo info; - readonly char[] footprint; + readonly Dictionary footprints = new(); public GrantExternalConditionPower(Actor self, GrantExternalConditionPowerInfo info) : base(self, info) { this.info = info; - footprint = info.Footprint.Where(c => !char.IsWhiteSpace(c)).ToArray(); + foreach (var pair in info.Footprints) + footprints.Add(pair.Key, pair.Value.Where(c => !char.IsWhiteSpace(c)).ToArray()); } public override void SelectTarget(Actor self, string order, SupportPowerManager manager) @@ -81,40 +92,46 @@ public override void Activate(Actor self, Order order, SupportPowerManager manag base.Activate(self, order, manager); PlayLaunchSounds(); + var position = order.Target.CenterPosition; + if (!string.IsNullOrEmpty(info.EffectSequence) && !string.IsNullOrEmpty(info.EffectPalette)) + self.World.Add(new SpriteEffect(position, self.World, info.EffectImage, info.EffectSequence, info.EffectPalette)); + var wsb = self.TraitOrDefault(); if (wsb != null && wsb.DefaultAnimation.HasSequence(info.Sequence)) wsb.PlayCustomAnimation(self, info.Sequence); - Game.Sound.Play(SoundType.World, info.OnFireSound, order.Target.CenterPosition); + Game.Sound.Play(SoundType.World, info.OnFireSound, position); - foreach (var a in UnitsInRange(self.World.Map.CellContaining(order.Target.CenterPosition))) + foreach (var a in UnitsInRange(self.World.Map.CellContaining(position))) a.TraitsImplementing() - .FirstOrDefault(t => t.Info.Condition == info.Condition && t.CanGrantCondition(self)) - ?.GrantCondition(a, self, info.Duration); + .FirstOrDefault(t => t.Info.Condition == info.Conditions.First(c => c.Key == GetLevel()).Value && t.CanGrantCondition(self)) + ?.GrantCondition(a, self, info.Durations.First(d => d.Key == GetLevel()).Value); } public IEnumerable UnitsInRange(CPos xy) { - var tiles = CellsMatching(xy, footprint, info.Dimensions); + var level = GetLevel(); + var tiles = CellsMatching(xy, footprints.First(f => f.Key == level).Value, info.Dimensions.First(d => d.Key == level).Value); var units = new List(); foreach (var t in tiles) units.AddRange(Self.World.ActorMap.GetActorsAt(t)); + var condition = info.Conditions.First(c => c.Key == level).Value; return units.Distinct().Where(a => { if (!info.ValidRelationships.HasRelationship(Self.Owner.RelationshipWith(a.Owner))) return false; return a.TraitsImplementing() - .Any(t => t.Info.Condition == info.Condition && t.CanGrantCondition(Self)); + .Any(t => t.Info.Condition == condition && t.CanGrantCondition(Self)); }); } sealed class SelectConditionTarget : OrderGenerator { readonly GrantExternalConditionPower power; - readonly char[] footprint; - readonly CVec dimensions; + readonly Dictionary footprints = new(); + readonly Dictionary dimensions; readonly Sprite tile; readonly float alpha; readonly SupportPowerManager manager; @@ -129,7 +146,9 @@ public SelectConditionTarget(World world, string order, SupportPowerManager mana this.manager = manager; this.order = order; this.power = power; - footprint = power.info.Footprint.Where(c => !char.IsWhiteSpace(c)).ToArray(); + foreach (var pair in power.info.Footprints) + footprints.Add(pair.Key, pair.Value.Where(c => !char.IsWhiteSpace(c)).ToArray()); + dimensions = power.info.Dimensions; var sequence = world.Map.Sequences.GetSequence(power.info.FootprintImage, power.info.FootprintSequence); @@ -170,7 +189,8 @@ protected override IEnumerable Render(WorldRenderer wr, World world var xy = wr.Viewport.ViewToWorld(Viewport.LastMousePos); var pal = wr.Palette(TileSet.TerrainPaletteInternalName); - foreach (var t in power.CellsMatching(xy, footprint, dimensions)) + var level = power.GetLevel(); + foreach (var t in power.CellsMatching(xy, footprints.First(f => f.Key == level).Value, dimensions.First(d => d.Key == level).Value)) yield return new SpriteRenderable(tile, wr.World.Map.CenterOfCell(t), WVec.Zero, -511, pal, 1f, alpha, float3.Ones, TintModifiers.IgnoreWorldTint, true); } diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/NukePower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/NukePower.cs index 55830e35e539..2e08bb47e652 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/NukePower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/NukePower.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; +using System.Linq; using OpenRA.GameRules; using OpenRA.Graphics; using OpenRA.Mods.Common.Effects; @@ -22,10 +23,10 @@ namespace OpenRA.Mods.Common.Traits { public class NukePowerInfo : SupportPowerInfo { - [WeaponReference] + // [WeaponReference] [FieldLoader.Require] [Desc("Weapon to use for the impact.")] - public readonly string MissileWeapon = ""; + public readonly Dictionary MissileWeapons = new(); [Desc("Delay (in ticks) after launch until the missile is spawned.")] public readonly int MissileDelay = 0; @@ -33,11 +34,11 @@ public class NukePowerInfo : SupportPowerInfo [Desc("Image to use for the missile.")] public readonly string MissileImage = null; - [SequenceReference(nameof(MissileImage))] + [SequenceReference(nameof(MissileImage), allowNullImage: true)] [Desc("Sprite sequence for the ascending missile.")] public readonly string MissileUp = "up"; - [SequenceReference(nameof(MissileImage))] + [SequenceReference(nameof(MissileImage), allowNullImage: true)] [Desc("Sprite sequence for the descending missile.")] public readonly string MissileDown = "down"; @@ -108,6 +109,9 @@ public class NukePowerInfo : SupportPowerInfo [Desc("Range circle color.")] public readonly Color CircleColor = Color.FromArgb(128, Color.Red); + [Desc("Use player color for circle rather than `CircleColor`.")] + public readonly bool CircleUsePlayerColor = false; + [Desc("Range circle width in pixel.")] public readonly float CircleWidth = 1; @@ -118,9 +122,9 @@ public class NukePowerInfo : SupportPowerInfo public readonly float CircleBorderWidth = 3; [Desc("Render circles based on these distance ranges while targeting.")] - public readonly WDist[] CircleRanges = null; + public readonly Dictionary CircleRanges; - public WeaponInfo WeaponInfo { get; private set; } + public readonly Dictionary WeaponInfos = new(); public override object Create(ActorInitializer init) { return new NukePower(init.Self, this); } public override void RulesetLoaded(Ruleset rules, ActorInfo ai) @@ -128,25 +132,29 @@ public override void RulesetLoaded(Ruleset rules, ActorInfo ai) if (!string.IsNullOrEmpty(TrailImage) && TrailSequences.Length == 0) throw new YamlException("At least one entry in TrailSequences must be defined when TrailImage is defined."); - var weaponToLower = (MissileWeapon ?? string.Empty).ToLowerInvariant(); - if (!rules.Weapons.TryGetValue(weaponToLower, out var weapon)) - throw new YamlException($"Weapons Ruleset does not contain an entry '{weaponToLower}'"); + foreach (var missileWeapon in MissileWeapons) + { + var weaponToLower = missileWeapon.Value.ToLowerInvariant(); + if (!rules.Weapons.TryGetValue(weaponToLower, out var weaponInfo)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{weaponToLower}'"); - WeaponInfo = weapon; + if (!WeaponInfos.ContainsKey(missileWeapon.Key)) + WeaponInfos.Add(missileWeapon.Key, rules.Weapons[weaponToLower]); + } base.RulesetLoaded(rules, ai); } } - sealed class NukePower : SupportPower + public class NukePower : SupportPower { - readonly NukePowerInfo info; + public new readonly NukePowerInfo Info; BodyOrientation body; public NukePower(Actor self, NukePowerInfo info) : base(self, info) { - this.info = info; + Info = info; } protected override void Created(Actor self) @@ -165,25 +173,26 @@ public override void Activate(Actor self, Order order, SupportPowerManager manag public void Activate(Actor self, WPos targetPosition) { - var palette = info.IsPlayerPalette ? info.MissilePalette + self.Owner.InternalName : info.MissilePalette; - var skipAscent = info.SkipAscent || body == null; - var launchPos = skipAscent ? WPos.Zero : self.CenterPosition + body.LocalToWorld(info.SpawnOffset); + var palette = Info.IsPlayerPalette ? Info.MissilePalette + self.Owner.InternalName : Info.MissilePalette; + var skipAscent = Info.SkipAscent || body == null; + var launchPos = skipAscent ? WPos.Zero : self.CenterPosition + body.LocalToWorld(Info.SpawnOffset); - var missile = new NukeLaunch(self.Owner, info.MissileImage, info.WeaponInfo, palette, info.MissileUp, info.MissileDown, + var weaponInfo = Info.WeaponInfos.First(wi => wi.Key == GetLevel()).Value; + var missile = new NukeLaunch(self.Owner, Info.MissileImage, weaponInfo, palette, Info.MissileUp, Info.MissileDown, launchPos, - targetPosition, info.DetonationAltitude, info.RemoveMissileOnDetonation, - info.FlightVelocity, info.MissileDelay, info.FlightDelay, skipAscent, - info.TrailImage, info.TrailSequences, info.TrailPalette, info.TrailUsePlayerPalette, info.TrailDelay, info.TrailInterval); + targetPosition, Info.DetonationAltitude, Info.RemoveMissileOnDetonation, + Info.FlightVelocity, Info.MissileDelay, Info.FlightDelay, skipAscent, + Info.TrailImage, Info.TrailSequences, Info.TrailPalette, Info.TrailUsePlayerPalette, Info.TrailDelay, Info.TrailInterval); self.World.AddFrameEndTask(w => w.Add(missile)); - if (info.CameraRange != WDist.Zero) + if (Info.CameraRange != WDist.Zero) { - var type = info.RevealGeneratedShroud ? Shroud.SourceType.Visibility + var type = Info.RevealGeneratedShroud ? Shroud.SourceType.Visibility : Shroud.SourceType.PassiveVisibility; - self.World.AddFrameEndTask(w => w.Add(new RevealShroudEffect(targetPosition, info.CameraRange, type, self.Owner, info.CameraRelationships, - info.FlightDelay - info.CameraSpawnAdvance, info.CameraSpawnAdvance + info.CameraRemoveDelay))); + self.World.AddFrameEndTask(w => w.Add(new RevealShroudEffect(targetPosition, Info.CameraRange, type, self.Owner, Info.CameraRelationships, + Info.FlightDelay - Info.CameraSpawnAdvance, Info.CameraSpawnAdvance + Info.CameraRemoveDelay))); } if (Info.DisplayBeacon) @@ -194,7 +203,7 @@ public void Activate(Actor self, WPos targetPosition) Info.BeaconPaletteIsPlayerPalette, Info.BeaconPalette, Info.BeaconImage, - Info.BeaconPoster, + Info.BeaconPosters.First(bp => bp.Key == GetLevel()).Value, Info.BeaconPosterPalette, Info.BeaconSequence, Info.ArrowSequence, @@ -202,7 +211,7 @@ public void Activate(Actor self, WPos targetPosition) Info.ClockSequence, () => missile.FractionComplete, Info.BeaconDelay, - info.FlightDelay - info.BeaconRemoveAdvance); + Info.FlightDelay - Info.BeaconRemoveAdvance); self.World.AddFrameEndTask(w => w.Add(beacon)); } @@ -210,35 +219,39 @@ public void Activate(Actor self, WPos targetPosition) public override void SelectTarget(Actor self, string order, SupportPowerManager manager) { - self.World.OrderGenerator = new SelectNukePowerTarget(order, manager, info, MouseButton.Left); + self.World.OrderGenerator = new SelectNukePowerTarget(order, manager, this, MouseButton.Left); } } public class SelectNukePowerTarget : SelectGenericPowerTarget { - readonly NukePowerInfo info; + readonly NukePower power; - public SelectNukePowerTarget(string order, SupportPowerManager manager, NukePowerInfo info, MouseButton button) - : base(order, manager, info.Cursor, button) + public SelectNukePowerTarget(string order, SupportPowerManager manager, NukePower power, MouseButton button) + : base(order, manager, power.Info.Cursor, button) { - this.info = info; + this.power = power; } protected override IEnumerable RenderAnnotations(WorldRenderer wr, World world) { - if (info.CircleRanges == null) + if (power.Info.CircleRanges == null) + yield break; + + var level = power.GetLevel(); + if (level == 0) yield break; var centerPosition = wr.World.Map.CenterOfCell(wr.Viewport.ViewToWorld(Viewport.LastMousePos)); - foreach (var range in info.CircleRanges) + foreach (var range in power.Info.CircleRanges[level]) yield return new RangeCircleAnnotationRenderable( centerPosition, range, 0, - info.CircleColor, - info.CircleWidth, - info.CircleBorderColor, - info.CircleBorderWidth); + power.Info.CircleUsePlayerColor ? power.Self.Owner.Color : power.Info.CircleColor, + power.Info.CircleWidth, + power.Info.CircleBorderColor, + power.Info.CircleBorderWidth); } } } diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/ParatroopersPower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/ParatroopersPower.cs index 80e5d5960cad..742ad44f3567 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/ParatroopersPower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/ParatroopersPower.cs @@ -21,9 +21,12 @@ namespace OpenRA.Mods.Common.Traits { public class ParatroopersPowerInfo : SupportPowerInfo { - [ActorReference(typeof(AircraftInfo))] - public readonly string UnitType = "badr"; - public readonly int SquadSize = 1; + [FieldLoader.Require] + public readonly Dictionary UnitTypes = new(); + + [FieldLoader.Require] + public readonly Dictionary SquadSizes = new(); + public readonly WVec SquadOffset = new(-1536, 1536, 0); [NotificationReference("Speech")] @@ -40,9 +43,9 @@ public class ParatroopersPowerInfo : SupportPowerInfo [Desc("Spawn and remove the plane this far outside the map.")] public readonly WDist Cordon = new(5120); - [ActorReference(typeof(PassengerInfo))] + [FieldLoader.Require] [Desc("Troops to be delivered. They will be distributed between the planes if SquadSize > 1.")] - public readonly string[] DropItems = Array.Empty(); + public readonly Dictionary DropItems = new(); [Desc("Risks stuck units when they don't have the Paratrooper trait.")] public readonly bool AllowImpassableCells = false; @@ -100,12 +103,10 @@ public override void Activate(Actor self, Order order, SupportPowerManager manag var aircraft = new List(); var units = new List(); - var info = Info as ParatroopersPowerInfo; - if (!facing.HasValue) facing = new WAngle(1024 * self.World.SharedRandom.Next(info.QuantizedFacings) / info.QuantizedFacings); - var utLower = info.UnitType.ToLowerInvariant(); + var utLower = info.UnitTypes.First(ut => ut.Key == GetLevel()).Value.ToLowerInvariant(); if (!self.World.Map.Rules.Actors.TryGetValue(utLower, out var unitType)) throw new YamlException($"Actors ruleset does not include the entry '{utLower}'"); @@ -172,17 +173,18 @@ void OnRemovedFromWorld(Actor a) } // Create the actors immediately so they can be returned - for (var i = -info.SquadSize / 2; i <= info.SquadSize / 2; i++) + var squadSize = info.SquadSizes.First(ss => ss.Key == GetLevel()).Value; + for (var i = -squadSize / 2; i <= squadSize / 2; i++) { // Even-sized squads skip the lead plane - if (i == 0 && (info.SquadSize & 1) == 0) + if (i == 0 && (squadSize & 1) == 0) continue; // Includes the 90 degree rotation between body and world coordinates var so = info.SquadOffset; var spawnOffset = new WVec(i * so.Y, -Math.Abs(i) * so.X, 0).Rotate(dropRotation); - aircraft.Add(self.World.CreateActor(false, info.UnitType, new TypeDictionary + aircraft.Add(self.World.CreateActor(false, utLower, new TypeDictionary { new CenterPositionInit(startEdge + spawnOffset), new OwnerInit(self.Owner), @@ -190,7 +192,8 @@ void OnRemovedFromWorld(Actor a) })); } - foreach (var p in info.DropItems) + var dropItems = info.DropItems.First(di => di.Key == GetLevel()).Value; + foreach (var p in dropItems) { units.Add(self.World.CreateActor(false, p.ToLowerInvariant(), new TypeDictionary { @@ -204,13 +207,13 @@ void OnRemovedFromWorld(Actor a) Actor distanceTestActor = null; - var passengersPerPlane = (info.DropItems.Length + info.SquadSize - 1) / info.SquadSize; + var passengersPerPlane = (dropItems.Length + squadSize - 1) / squadSize; var added = 0; var j = 0; - for (var i = -info.SquadSize / 2; i <= info.SquadSize / 2; i++) + for (var i = -squadSize / 2; i <= squadSize / 2; i++) { // Even-sized squads skip the lead plane - if (i == 0 && (info.SquadSize & 1) == 0) + if (i == 0 && (squadSize & 1) == 0) continue; // Includes the 90 degree rotation between body and world coordinates @@ -254,7 +257,7 @@ void OnRemovedFromWorld(Actor a) Info.BeaconPaletteIsPlayerPalette, Info.BeaconPalette, Info.BeaconImage, - Info.BeaconPoster, + Info.BeaconPosters.First(bp => bp.Key == GetLevel()).Value, Info.BeaconPosterPalette, Info.BeaconSequence, Info.ArrowSequence, diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/ProduceActorPower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/ProduceActorPower.cs index 1ee55900e254..23b4f9668ad8 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/ProduceActorPower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/ProduceActorPower.cs @@ -45,17 +45,26 @@ public class ProduceActorPowerInfo : SupportPowerInfo [Desc("Text notification displayed when the exit is jammed.")] public readonly string BlockedTextNotification = null; + [Desc("Allows the actors to be produced immediately when charged.")] + public readonly bool AutoFire = false; + public override object Create(ActorInitializer init) { return new ProduceActorPower(init, this); } } - public class ProduceActorPower : SupportPower + public class ProduceActorPower : SupportPower, ITick { readonly string faction; + readonly string key; + readonly bool autoFire; + + int ticks; public ProduceActorPower(ActorInitializer init, ProduceActorPowerInfo info) : base(init.Self, info) { faction = init.GetValue(init.Self.Owner.Faction.InternalName); + autoFire = info.AutoFire; + key = info.AllowMultiple ? info.OrderName + "_" + init.Self.ActorID : info.OrderName; } public override void SelectTarget(Actor self, string order, SupportPowerManager manager) @@ -63,6 +72,17 @@ public override void SelectTarget(Actor self, string order, SupportPowerManager self.World.IssueOrder(new Order(order, manager.Self, false)); } + public override void Charged(Actor self, string key) + { + base.Charged(self, key); + + if (autoFire) + { + self.Owner.PlayerActor.Trait().Powers[key].Activate(new Order()); + ticks = 10; + } + } + public override void Activate(Actor self, Order order, SupportPowerManager manager) { base.Activate(self, order, manager); @@ -109,5 +129,11 @@ public override void Activate(Actor self, Order order, SupportPowerManager manag TextNotificationsManager.AddTransientLine(manager.Self.Owner, info.BlockedTextNotification); } } + + void ITick.Tick(Actor self) + { + if (autoFire && self.Owner.PlayerActor.Trait().Powers[key].Ready && --ticks < 0) + self.Owner.PlayerActor.Trait().Powers[key].Activate(new Order()); + } } } diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/SpawnActorPower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/SpawnActorPower.cs index 737f1fbada78..15c967112f15 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/SpawnActorPower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/SpawnActorPower.cs @@ -14,6 +14,7 @@ using OpenRA.Graphics; using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Graphics; using OpenRA.Mods.Common.Orders; using OpenRA.Primitives; using OpenRA.Traits; @@ -23,10 +24,9 @@ namespace OpenRA.Mods.Common.Traits [Desc("Spawns an actor that stays for a limited amount of time.")] public class SpawnActorPowerInfo : SupportPowerInfo { - [ActorReference] [FieldLoader.Require] - [Desc("Actor to spawn.")] - public readonly string Actor = null; + [Desc("Actors to spawn for each level.")] + public readonly Dictionary Actors = new(); [Desc("Amount of time to keep the actor alive in ticks. Value < 0 means this actor will not remove itself.")] public readonly int LifeTime = 250; @@ -40,7 +40,7 @@ public class SpawnActorPowerInfo : SupportPowerInfo public readonly string EffectImage = null; - [SequenceReference(nameof(EffectImage))] + [SequenceReference(nameof(EffectImage), allowNullImage: true)] public readonly string EffectSequence = null; [PaletteReference] @@ -49,21 +49,32 @@ public class SpawnActorPowerInfo : SupportPowerInfo [Desc("Cursor to display when the location is unsuitable.")] public readonly string BlockedCursor = "move-blocked"; + public readonly Dictionary TargetCircleRanges; + public readonly Color TargetCircleColor = Color.White; + public readonly bool TargetCircleUsePlayerColor = false; + public readonly float TargetCircleWidth = 1; + public readonly Color TargetCircleBorderColor = Color.FromArgb(96, Color.Black); + public readonly float TargetCircleBorderWidth = 3; + public override object Create(ActorInitializer init) { return new SpawnActorPower(init.Self, this); } } public class SpawnActorPower : SupportPower { + public new readonly SpawnActorPowerInfo Info; + public SpawnActorPower(Actor self, SpawnActorPowerInfo info) - : base(self, info) { } + : base(self, info) + { + Info = info; + } public override void Activate(Actor self, Order order, SupportPowerManager manager) { - var info = Info as SpawnActorPowerInfo; var position = order.Target.CenterPosition; var cell = self.World.Map.CellContaining(position); - if (!Validate(self.World, info, cell)) + if (!Validate(self.World, Info, cell)) return; base.Activate(self, order, manager); @@ -71,20 +82,20 @@ public override void Activate(Actor self, Order order, SupportPowerManager manag self.World.AddFrameEndTask(w => { PlayLaunchSounds(); - Game.Sound.Play(SoundType.World, info.DeploySound, position); + Game.Sound.Play(SoundType.World, Info.DeploySound, position); - if (!string.IsNullOrEmpty(info.EffectSequence) && !string.IsNullOrEmpty(info.EffectPalette)) - w.Add(new SpriteEffect(position, w, info.EffectImage, info.EffectSequence, info.EffectPalette)); + if (!string.IsNullOrEmpty(Info.EffectSequence) && !string.IsNullOrEmpty(Info.EffectPalette)) + w.Add(new SpriteEffect(position, w, Info.EffectImage, Info.EffectSequence, Info.EffectPalette)); - var actor = w.CreateActor(info.Actor, new TypeDictionary + var actor = w.CreateActor(Info.Actors.First(a => a.Key == GetLevel()).Value, new TypeDictionary { new LocationInit(cell), new OwnerInit(self.Owner), }); - if (info.LifeTime > -1) + if (Info.LifeTime > -1) { - actor.QueueActivity(new Wait(info.LifeTime)); + actor.QueueActivity(new Wait(Info.LifeTime)); actor.QueueActivity(new RemoveSelf()); } }); @@ -136,7 +147,7 @@ public SelectSpawnActorPowerTarget(string order, SupportPowerManager manager, Sp OrderKey = order; expectedButton = button; - info = (SpawnActorPowerInfo)power.Info; + info = power.Info; } protected override IEnumerable OrderInner(World world, CPos cell, int2 worldPixel, MouseInput mi) @@ -158,8 +169,30 @@ protected override void Tick(World world) } protected override IEnumerable Render(WorldRenderer wr, World world) { yield break; } + protected override IEnumerable RenderAboveShroud(WorldRenderer wr, World world) { yield break; } - protected override IEnumerable RenderAnnotations(WorldRenderer wr, World world) { yield break; } + + protected override IEnumerable RenderAnnotations(WorldRenderer wr, World world) + { + var xy = wr.Viewport.ViewToWorld(Viewport.LastMousePos); + + if (power.Info.TargetCircleRanges == null || !power.Info.TargetCircleRanges.Any() || power.GetLevel() == 0) + { + yield break; + } + else + { + yield return new RangeCircleAnnotationRenderable( + world.Map.CenterOfCell(xy), + power.Info.TargetCircleRanges[power.GetLevel()], + 0, + power.Info.TargetCircleUsePlayerColor ? power.Self.Owner.Color : power.Info.TargetCircleColor, + power.Info.TargetCircleWidth, + power.Info.TargetCircleBorderColor, + power.Info.TargetCircleBorderWidth); + } + } + protected override string GetCursor(World world, CPos cell, int2 worldPixel, MouseInput mi) { return power.Validate(world, info, cell) ? info.Cursor : info.BlockedCursor; diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/SupportPower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/SupportPower.cs index e5be5a18c5c5..c8bdb35e530d 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/SupportPower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/SupportPower.cs @@ -9,8 +9,8 @@ */ #endregion -using System; using System.Collections.Generic; +using System.Linq; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -22,22 +22,23 @@ public abstract class SupportPowerInfo : PausableConditionalTraitInfo public readonly string IconImage = "icon"; - [SequenceReference(nameof(IconImage))] + // [SequenceReference(nameof(IconImage))] [Desc("Icon sprite displayed in the support power palette.")] - public readonly string Icon = null; + public readonly Dictionary Icons = new(); [PaletteReference] [Desc("Palette used for the icon.")] public readonly string IconPalette = "chrome"; - public readonly string Name = ""; - public readonly string Description = ""; + public readonly Dictionary Names = new(); + public readonly Dictionary Descriptions = new(); [Desc("Allow multiple instances of the same support power.")] public readonly bool AllowMultiple = false; [Desc("Allow this to be used only once.")] public readonly bool OneShot = false; + public readonly int Cost = 0; [CursorReference] [Desc("Cursor to display for using this support power.")] @@ -46,7 +47,14 @@ public abstract class SupportPowerInfo : PausableConditionalTraitInfo [Desc("If set to true, the support power will be fully charged when it becomes available. " + "Normal rules apply for subsequent charges.")] public readonly bool StartFullyCharged = false; - public readonly string[] Prerequisites = Array.Empty(); + + [Desc("If set to true, the support power will be fully charged when the first time player obtain it. " + + "Overrided by `StartFullyCharged`." + + "Note: it depends on `OrderName` and the first name in `Names` to indentify different support power." + + "Normal rules apply for subsequent charges.")] + public readonly bool StartFullyChargedForTheFirstTime = false; + + public readonly Dictionary Prerequisites = new(); public readonly string DetectedSound = null; @@ -117,8 +125,8 @@ public abstract class SupportPowerInfo : PausableConditionalTraitInfo public readonly string BeaconImage = "beacon"; - [SequenceReference(nameof(BeaconImage))] - public readonly string BeaconPoster = null; + // [SequenceReference(nameof(BeaconImage))] + public readonly Dictionary BeaconPosters = new(); [PaletteReference] public readonly string BeaconPosterPalette = "chrome"; @@ -151,17 +159,43 @@ public abstract class SupportPowerInfo : PausableConditionalTraitInfo protected SupportPowerInfo() { OrderName = GetType().Name + "Order"; } } - public class SupportPower : PausableConditionalTrait + public class SupportPower : PausableConditionalTrait, INotifyOwnerChanged { public readonly Actor Self; readonly SupportPowerInfo info; protected RadarPing ping; + DeveloperMode developerMode; + TechTree techTree; + public SupportPower(Actor self, SupportPowerInfo info) : base(info) { Self = self; this.info = info; + + // Special case handling is required for the Player actor. + // Created is called before Player.PlayerActor is assigned, + // so we must query other player traits from self, knowing that + // it refers to the same actor as self.Owner.PlayerActor + var playerActor = self.Info.Name == "player" ? self : self.Owner.PlayerActor; + + techTree = playerActor.Trait(); + developerMode = playerActor.Trait(); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + techTree = newOwner.PlayerActor.Trait(); + developerMode = newOwner.PlayerActor.Trait(); + } + + public int GetLevel() + { + var availables = Info.Prerequisites.Where(p => techTree.HasPrerequisites(p.Value)); + var level = availables.Any() ? availables.Max(p => p.Key) : 0; + + return developerMode.AllTech ? Info.Prerequisites.Max(p => p.Key) : level; } protected override void Created(Actor self) diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/SupportPowerManager.cs b/OpenRA.Mods.Common/Traits/SupportPowers/SupportPowerManager.cs index 987200a5151a..56bf7337ce73 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/SupportPowerManager.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/SupportPowerManager.cs @@ -29,6 +29,7 @@ public class SupportPowerManager : ITick, IResolveOrder, ITechTreeElement { public readonly Actor Self; public readonly Dictionary Powers = new(); + public readonly HashSet ObtainedSupportPower = new() { string.Empty }; public readonly DeveloperMode DevMode; public readonly TechTree TechTree; @@ -59,18 +60,22 @@ void ActorAdded(Actor a) { var key = MakeKey(t); - if (!Powers.ContainsKey(key)) - { + var hasKey = Powers.ContainsKey(key); + if (!hasKey) Powers.Add(key, t.CreateInstance(key, this)); - if (t.Info.Prerequisites.Length > 0) + Powers[key].Instances.Add(t); + + if (!hasKey) + { + foreach (var prerequisite in t.Info.Prerequisites) { - TechTree.Add(key, t.Info.Prerequisites, 0, this); - TechTree.Update(); + var techKey = key + prerequisite.Key; + TechTree.Add(techKey, prerequisite.Value, 0, this); } - } - Powers[key].Instances.Add(t); + TechTree.Update(); + } } } @@ -84,10 +89,16 @@ void ActorRemoved(Actor a) var key = MakeKey(t); Powers[key].Instances.Remove(t); - if (Powers[key].Instances.Count == 0 && !Powers[key].Disabled) + if (Powers[key].Instances.Count == 0) { Powers.Remove(key); - TechTree.Remove(key); + + foreach (var prerequisite in t.Info.Prerequisites) + { + var techKey = key + prerequisite.Key; + TechTree.Remove(techKey); + } + TechTree.Update(); } } @@ -118,24 +129,24 @@ public IEnumerable GetPowersForActor(Actor a) .Where(p => p.Instances.Any(i => !i.IsTraitDisabled && i.Self == a)); } - public void PrerequisitesAvailable(string key) + void ITechTreeElement.PrerequisitesAvailable(string key) { - if (!Powers.TryGetValue(key, out var sp)) + if (!Powers.TryGetValue(key.Remove(key.Length - 1), out var sp)) return; - sp.PrerequisitesAvailable(true); + sp.CheckPrerequisites(false); } - public void PrerequisitesUnavailable(string key) + void ITechTreeElement.PrerequisitesUnavailable(string key) { - if (!Powers.TryGetValue(key, out var sp)) + if (!Powers.TryGetValue(key.Remove(key.Length - 1), out var sp)) return; - sp.PrerequisitesAvailable(false); + sp.CheckPrerequisites(false); } - public void PrerequisitesItemHidden(string key) { } - public void PrerequisitesItemVisible(string key) { } + void ITechTreeElement.PrerequisitesItemHidden(string key) { } + void ITechTreeElement.PrerequisitesItemVisible(string key) { } } public class SupportPowerInstance @@ -174,17 +185,25 @@ public SupportPowerInstance(string key, SupportPowerInfo info, SupportPowerManag { Key = key; TotalTicks = info.ChargeInterval; - remainingSubTicks = info.StartFullyCharged ? 0 : TotalTicks * 100; + + var supportpowerID = info.StartFullyChargedForTheFirstTime ? info.Names[info.Names.Keys.Min()] + info.OrderName : string.Empty; + if (!manager.ObtainedSupportPower.Contains(supportpowerID)) + { + remainingSubTicks = 0; + manager.ObtainedSupportPower.Add(supportpowerID); + } + else + remainingSubTicks = info.StartFullyCharged ? 0 : TotalTicks * 100; Manager = manager; } - public virtual void PrerequisitesAvailable(bool available) + public void CheckPrerequisites(bool disable) { - prereqsAvailable = available; - - if (!available) - remainingSubTicks = TotalTicks * 100; + if (disable) + prereqsAvailable = false; + else + prereqsAvailable = GetLevel() != 0; } public virtual void Tick() @@ -227,6 +246,9 @@ public virtual void Target() if (power == null) return; + if (!HasSufficientFunds(power)) + return; + Game.Sound.PlayToPlayer(SoundType.UI, Manager.Self.Owner, Info.SelectTargetSound); Game.Sound.PlayNotification(power.Self.World.Map.Rules, power.Self.Owner, "Speech", Info.SelectTargetSpeechNotification, power.Self.Owner.Faction.InternalName); @@ -253,6 +275,9 @@ public virtual void Activate(Order order) if (power == null) return; + if (!HasSufficientFunds(power, true)) + return; + // Note: order.Subject is the *player* actor power.Activate(power.Self, order, Manager); remainingSubTicks = TotalTicks * 100; @@ -260,11 +285,42 @@ public virtual void Activate(Order order) if (Info.OneShot) { - PrerequisitesAvailable(false); + CheckPrerequisites(true); oneShotFired = true; } } + bool HasSufficientFunds(SupportPower power, bool activate = false) + { + if (power.Info.Cost != 0) + { + var player = Manager.Self; + var pr = player.Trait(); + if (pr.Cash + pr.Resources < power.Info.Cost) + { + Game.Sound.PlayNotification(player.World.Map.Rules, player.Owner, "Speech", + pr.Info.InsufficientFundsNotification, player.Owner.Faction.InternalName); + return false; + } + + if (activate) + pr.TakeCash(power.Info.Cost); + } + + return true; + } + + public int GetLevel() + { + if (Info == null) + return 0; + + var availables = Info.Prerequisites.Where(p => Manager.TechTree.HasPrerequisites(p.Value)); + var level = availables.Any() ? availables.Max(p => p.Key) : 0; + + return Manager.DevMode.AllTech ? Info.Prerequisites.Max(p => p.Key) : level; + } + public virtual string IconOverlayTextOverride() { return null; diff --git a/OpenRA.Mods.Common/Traits/TakesBounty.cs b/OpenRA.Mods.Common/Traits/TakesBounty.cs new file mode 100644 index 000000000000..e020845f47b7 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/TakesBounty.cs @@ -0,0 +1,36 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("This actor takes bounty from actors with Gives Bounty trait with same Type.")] + public class TakesBountyInfo : ConditionalTraitInfo + { + [Desc("Percentage of the killed actor's Cost or CustomSellValue to be given.")] + public readonly int Percentage = 10; + + [Desc("Scale bounty based on the veterancy of the killed unit. The value is given in percent.")] + public readonly int LevelMod = 125; + + [Desc("Accepted `Gives Bounty` types. Leave empty to accept all types.")] + public readonly HashSet ValidTypes = new() { "Bounty" }; + + public override object Create(ActorInitializer init) { return new TakesBounty(this); } + } + + public class TakesBounty : ConditionalTrait + { + public TakesBounty(TakesBountyInfo info) + : base(info) { } + } +} diff --git a/OpenRA.Mods.Common/Traits/ThrowsShrapnel.cs b/OpenRA.Mods.Common/Traits/ThrowsShrapnel.cs index e0fc1c2a2fb6..0463306c9e03 100644 --- a/OpenRA.Mods.Common/Traits/ThrowsShrapnel.cs +++ b/OpenRA.Mods.Common/Traits/ThrowsShrapnel.cs @@ -24,12 +24,17 @@ public class ThrowsShrapnelInfo : ConditionalTraitInfo, IRulesetLoaded [Desc("The weapons used for shrapnel.")] public readonly string[] Weapons = Array.Empty(); + public readonly string WeaponName = "primary"; + [Desc("The amount of pieces of shrapnel to expel. Two values indicate a range.")] public readonly int[] Pieces = { 3, 10 }; [Desc("The minimum and maximum distances the shrapnel may travel.")] public readonly WDist[] Range = { WDist.FromCells(2), WDist.FromCells(5) }; + [Desc("Throw the projectile to where actor is facing.")] + public readonly bool ConsiderFacing = false; + public WeaponInfo[] WeaponInfos { get; private set; } public override object Create(ActorInitializer actor) { return new ThrowsShrapnel(this); } @@ -64,15 +69,16 @@ public void Killed(Actor self, AttackInfo attack) for (var i = 0; pieces > i; i++) { - var rotation = WRot.FromYaw(new WAngle(self.World.SharedRandom.Next(1024))); + var myFacing = self.TraitOrDefault(); + var rotation = WRot.FromYaw(myFacing != null && Info.ConsiderFacing ? myFacing.Facing + new WAngle(256) : new WAngle(self.World.SharedRandom.Next(1024))); var args = new ProjectileArgs { Weapon = wep, - Facing = new WAngle(self.World.SharedRandom.Next(1024)), + Facing = myFacing != null && Info.ConsiderFacing ? myFacing.Facing : new WAngle(self.World.SharedRandom.Next(1024)), CurrentMuzzleFacing = () => WAngle.Zero, DamageModifiers = self.TraitsImplementing() - .Select(a => a.GetFirepowerModifier()).ToArray(), + .Select(a => a.GetFirepowerModifier(Info.WeaponName)).ToArray(), InaccuracyModifiers = self.TraitsImplementing() .Select(a => a.GetInaccuracyModifier()).ToArray(), @@ -95,7 +101,11 @@ public void Killed(Actor self, AttackInfo attack) self.World.Add(projectile); if (args.Weapon.Report != null && args.Weapon.Report.Length > 0) - Game.Sound.Play(SoundType.World, args.Weapon.Report, self.World, self.CenterPosition); + { + var pos = self.CenterPosition; + if (args.Weapon.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, args.Weapon.Report, self.World, pos, null, args.Weapon.SoundVolume); + } } }); } diff --git a/OpenRA.Mods.Common/Traits/Transforms.cs b/OpenRA.Mods.Common/Traits/Transforms.cs index f96ee6be5a56..4dcea706ca2e 100644 --- a/OpenRA.Mods.Common/Traits/Transforms.cs +++ b/OpenRA.Mods.Common/Traits/Transforms.cs @@ -30,8 +30,8 @@ public class TransformsInfo : PausableConditionalTraitInfo [Desc("Offset to spawn the transformed actor relative to the current cell.")] public readonly CVec Offset = CVec.Zero; - [Desc("Facing that the actor must face before transforming.")] - public readonly WAngle Facing = new(384); + [Desc("Facing that the actor must face before transforming. Leave undefined to deploy regardless of facing.")] + public readonly WAngle? Facing = null; [Desc("Sounds to play when transforming.")] public readonly string[] TransformSounds = Array.Empty(); @@ -39,6 +39,12 @@ public class TransformsInfo : PausableConditionalTraitInfo [Desc("Sounds to play when the transformation is blocked.")] public readonly string[] NoTransformSounds = Array.Empty(); + [Desc("Do the sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the TransformSounds and NoTransformSounds played at.")] + public readonly float SoundVolume = 1f; + [NotificationReference("Speech")] [Desc("Speech notification to play when transforming.")] public readonly string TransformNotification = null; @@ -112,6 +118,8 @@ public Activity GetTransformActivity() Sounds = Info.TransformSounds, Notification = Info.TransformNotification, TextNotification = Info.TransformTextNotification, + AudibleThroughFog = Info.AudibleThroughFog, + SoundVolume = Info.SoundVolume, Faction = faction }; } @@ -150,8 +158,10 @@ public void DeployTransform(bool queued) // Only play the "Cannot deploy here" audio // for non-queued orders - foreach (var s in Info.NoTransformSounds) - Game.Sound.PlayToPlayer(SoundType.World, self.Owner, s); + var pos = self.CenterPosition; + if (Info.AudibleThroughFog || (!self.World.ShroudObscures(pos) && !self.World.FogObscures(pos))) + foreach (var s in Info.NoTransformSounds) + Game.Sound.Play(SoundType.World, s, pos, Info.SoundVolume); Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech", Info.NoTransformNotification, self.Owner.Faction.InternalName); TextNotificationsManager.AddTransientLine(self.Owner, Info.NoTransformTextNotification); diff --git a/OpenRA.Mods.Common/Traits/World/ActorSpawnManager.cs b/OpenRA.Mods.Common/Traits/World/ActorSpawnManager.cs index 35d00497dd50..37ce2f94d405 100644 --- a/OpenRA.Mods.Common/Traits/World/ActorSpawnManager.cs +++ b/OpenRA.Mods.Common/Traits/World/ActorSpawnManager.cs @@ -39,7 +39,7 @@ public class ActorSpawnManagerInfo : ConditionalTraitInfo, Requires Types = new() { }; + public readonly HashSet Types = new(); public override void RulesetLoaded(Ruleset rules, ActorInfo ai) { diff --git a/OpenRA.Mods.Common/Traits/World/CrateSpawner.cs b/OpenRA.Mods.Common/Traits/World/CrateSpawner.cs index 1c100ae473ad..d02cbede0ef7 100644 --- a/OpenRA.Mods.Common/Traits/World/CrateSpawner.cs +++ b/OpenRA.Mods.Common/Traits/World/CrateSpawner.cs @@ -18,29 +18,9 @@ namespace OpenRA.Mods.Common.Traits { - [TraitLocation(SystemActors.World)] - public class CrateSpawnerInfo : TraitInfo, ILobbyOptions + [TraitLocation(SystemActors.World | SystemActors.Player)] + public class CrateSpawnerInfo : PausableConditionalTraitInfo { - [TranslationReference] - [Desc("Descriptive label for the crates checkbox in the lobby.")] - public readonly string CheckboxLabel = "checkbox-crates.label"; - - [TranslationReference] - [Desc("Tooltip description for the crates checkbox in the lobby.")] - public readonly string CheckboxDescription = "checkbox-crates.description"; - - [Desc("Default value of the crates checkbox in the lobby.")] - public readonly bool CheckboxEnabled = true; - - [Desc("Prevent the crates state from being changed in the lobby.")] - public readonly bool CheckboxLocked = false; - - [Desc("Whether to display the crates checkbox in the lobby.")] - public readonly bool CheckboxVisible = true; - - [Desc("Display order for the crates checkbox in the lobby.")] - public readonly int CheckboxDisplayOrder = 0; - [Desc("Minimum number of crates.")] public readonly int Minimum = 1; @@ -79,47 +59,34 @@ public class CrateSpawnerInfo : TraitInfo, ILobbyOptions [Desc("Spawn and remove the plane this far outside the map.")] public readonly WDist Cordon = new(5120); - IEnumerable ILobbyOptions.LobbyOptions(MapPreview map) - { - yield return new LobbyBooleanOption(map, "crates", CheckboxLabel, CheckboxDescription, CheckboxVisible, CheckboxDisplayOrder, CheckboxEnabled, CheckboxLocked); - } - public override object Create(ActorInitializer init) { return new CrateSpawner(init.Self, this); } } - public class CrateSpawner : ITick, INotifyCreated + public class CrateSpawner : PausableConditionalTrait, ITick { readonly Actor self; - readonly CrateSpawnerInfo info; - bool enabled; int crates; int ticks; public CrateSpawner(Actor self, CrateSpawnerInfo info) + : base(info) { this.self = self; - this.info = info; ticks = info.InitialSpawnDelay; } - void INotifyCreated.Created(Actor self) - { - enabled = self.World.LobbyInfo.GlobalSettings - .OptionOrDefault("crates", info.CheckboxEnabled); - } - void ITick.Tick(Actor self) { - if (!enabled) + if (IsTraitDisabled || IsTraitPaused) return; if (--ticks <= 0) { - ticks = info.SpawnInterval; + ticks = Info.SpawnInterval; - var toSpawn = Math.Max(0, info.Minimum - crates) - + (crates < info.Maximum && info.Maximum > info.Minimum ? 1 : 0); + var toSpawn = Math.Max(0, Info.Minimum - crates) + + (crates < Info.Maximum && Info.Maximum > Info.Minimum ? 1 : 0); for (var n = 0; n < toSpawn; n++) SpawnCrate(self); @@ -128,7 +95,7 @@ void ITick.Tick(Actor self) void SpawnCrate(Actor self) { - var inWater = self.World.SharedRandom.Next(100) < info.WaterChance; + var inWater = self.World.SharedRandom.Next(100) < Info.WaterChance; var pp = ChooseDropCell(self, inWater, 100); if (pp == null) @@ -139,21 +106,21 @@ void SpawnCrate(Actor self) self.World.AddFrameEndTask(w => { - if (info.DeliveryAircraft != null) + if (Info.DeliveryAircraft != null) { - var crate = w.CreateActor(false, crateActor, new TypeDictionary { new OwnerInit(w.WorldActor.Owner) }); - var dropFacing = new WAngle(1024 * self.World.SharedRandom.Next(info.QuantizedFacings) / info.QuantizedFacings); + var crate = w.CreateActor(false, crateActor, new TypeDictionary { new OwnerInit(w.WorldActor.Owner), new CrateSpawnerTraitInit(this) }); + var dropFacing = new WAngle(1024 * self.World.SharedRandom.Next(Info.QuantizedFacings) / Info.QuantizedFacings); var delta = new WVec(0, -1024, 0).Rotate(WRot.FromYaw(dropFacing)); - var altitude = self.World.Map.Rules.Actors[info.DeliveryAircraft].TraitInfo().CruiseAltitude.Length; + var altitude = self.World.Map.Rules.Actors[Info.DeliveryAircraft].TraitInfo().CruiseAltitude.Length; var target = self.World.Map.CenterOfCell(p) + new WVec(0, 0, altitude); - var startEdge = target - (self.World.Map.DistanceToEdge(target, -delta) + info.Cordon).Length * delta / 1024; - var finishEdge = target + (self.World.Map.DistanceToEdge(target, delta) + info.Cordon).Length * delta / 1024; + var startEdge = target - (self.World.Map.DistanceToEdge(target, -delta) + Info.Cordon).Length * delta / 1024; + var finishEdge = target + (self.World.Map.DistanceToEdge(target, delta) + Info.Cordon).Length * delta / 1024; - var plane = w.CreateActor(info.DeliveryAircraft, new TypeDictionary + var plane = w.CreateActor(Info.DeliveryAircraft, new TypeDictionary { new CenterPositionInit(startEdge), - new OwnerInit(self.Owner), + new OwnerInit(w.WorldActor.Owner), new FacingInit(dropFacing), }); @@ -165,7 +132,7 @@ void SpawnCrate(Actor self) plane.QueueActivity(new RemoveSelf()); } else - w.CreateActor(crateActor, new TypeDictionary { new OwnerInit(w.WorldActor.Owner), new LocationInit(p) }); + w.CreateActor(crateActor, new TypeDictionary { new OwnerInit(w.WorldActor.Owner), new LocationInit(p), new CrateSpawnerTraitInit(this) }); }); } @@ -177,7 +144,7 @@ void SpawnCrate(Actor self) // Is this valid terrain? var terrainType = self.World.Map.GetTerrainInfo(p).Type; - if (!(inWater ? info.ValidWater : info.ValidGround).Contains(terrainType)) + if (!(inWater ? Info.ValidWater : Info.ValidGround).Contains(terrainType)) continue; // Don't drop on any actors @@ -192,7 +159,7 @@ void SpawnCrate(Actor self) string ChooseCrateActor() { - var crateShares = info.CrateActorShares; + var crateShares = Info.CrateActorShares; var n = self.World.SharedRandom.Next(crateShares.Sum()); var cumulativeShares = 0; @@ -200,7 +167,7 @@ string ChooseCrateActor() { cumulativeShares += crateShares[i]; if (n <= cumulativeShares) - return info.CrateActors[i]; + return Info.CrateActors[i]; } return null; @@ -215,5 +182,16 @@ public void DecrementCrates() { crates--; } + + protected override void TraitDisabled(Actor self) + { + ticks = Info.SpawnInterval; + } + } + + public class CrateSpawnerTraitInit : ValueActorInit, ISingleInstanceInit + { + public CrateSpawnerTraitInit(CrateSpawner value) + : base(value) { } } } diff --git a/OpenRA.Mods.Common/Traits/World/Locomotor.cs b/OpenRA.Mods.Common/Traits/World/Locomotor.cs index 53c99f284909..79688872062c 100644 --- a/OpenRA.Mods.Common/Traits/World/Locomotor.cs +++ b/OpenRA.Mods.Common/Traits/World/Locomotor.cs @@ -72,6 +72,14 @@ public class LocomotorInfo : TraitInfo, NotBefore [Desc("Can the actor be ordered to move in to shroud?")] public readonly bool MoveIntoShroud = true; + [ActorReference] + [Desc("If this list is not empty, these actors can't block me.")] + public readonly string[] NonBlockerActors = Array.Empty(); + + [ActorReference] + [Desc("If this list is not empty, only these actors can block me.")] + public readonly string[] BlockerActors = Array.Empty(); + [Desc("e.g. crate, wall, infantry")] public readonly BitSet Crushes = default; @@ -283,11 +291,30 @@ bool CanMoveFreelyInto(Actor actor, CPos cell, SubCell subCell, BlockedByActor c } var otherActors = subCell == SubCell.FullCell ? world.ActorMap.GetActorsAt(cell) : world.ActorMap.GetActorsAt(cell, subCell); - foreach (var otherActor in otherActors) - if (IsBlockedBy(actor, otherActor, ignoreActor, ignoreSelf, cell, check, cellFlag)) - return false; - return true; + if (ignoreSelf) + { + // Any actor blocking us will prevent our movement, *unless* we are one of those actors. + var isBlocked = false; + foreach (var otherActor in otherActors) + { + if (actor == otherActor) + return true; + + isBlocked = isBlocked || IsBlockedBy(actor, otherActor, ignoreActor, cell, check, cellFlag); + } + + return !isBlocked; + } + else + { + // Any actor blocking us will prevent our movement. + foreach (var otherActor in otherActors) + if (IsBlockedBy(actor, otherActor, ignoreActor, cell, check, cellFlag)) + return false; + + return true; + } } public bool CanStayInCell(CPos cell) @@ -305,7 +332,7 @@ public SubCell GetAvailableSubCell(Actor self, CPos cell, BlockedByActor check, if (check > BlockedByActor.None) { - bool CheckTransient(Actor otherActor) => IsBlockedBy(self, otherActor, ignoreActor, false, cell, check, GetCache(cell).CellFlag); + bool CheckTransient(Actor otherActor) => IsBlockedBy(self, otherActor, ignoreActor, cell, check, GetCache(cell).CellFlag); if (!sharesCell) return world.ActorMap.AnyActorsAt(cell, SubCell.FullCell, CheckTransient) ? SubCell.Invalid : SubCell.FullCell; @@ -322,9 +349,9 @@ public SubCell GetAvailableSubCell(Actor self, CPos cell, BlockedByActor check, /// This logic is replicated in and /// . If this method is updated please update those as /// well. - bool IsBlockedBy(Actor actor, Actor otherActor, Actor ignoreActor, bool ignoreSelf, CPos cell, BlockedByActor check, CellFlag cellFlag) + bool IsBlockedBy(Actor actor, Actor otherActor, Actor ignoreActor, CPos cell, BlockedByActor check, CellFlag cellFlag) { - if (otherActor == ignoreActor || (ignoreSelf && otherActor == actor)) + if (otherActor == ignoreActor) return false; var otherMobile = otherActor.OccupiesSpace as Mobile; @@ -471,6 +498,12 @@ void UpdateCellBlocking(CPos cell) foreach (var actor in actorMap.GetActorsAt(cell)) { + if (Info.BlockerActors.Length > 0 && !Info.BlockerActors.Contains(actor.Info.Name)) + continue; + + if (Info.NonBlockerActors.Length > 0 && Info.NonBlockerActors.Contains(actor.Info.Name)) + continue; + var actorImmovablePlayers = world.AllPlayersMask; var actorCrushablePlayers = world.NoPlayersMask; diff --git a/OpenRA.Mods.Common/Traits/World/MapStartingUnits.cs b/OpenRA.Mods.Common/Traits/World/MapStartingUnits.cs index 8682de6e92e3..6e83c2cdcb88 100644 --- a/OpenRA.Mods.Common/Traits/World/MapStartingUnits.cs +++ b/OpenRA.Mods.Common/Traits/World/MapStartingUnits.cs @@ -40,12 +40,24 @@ public class StartingUnitsInfo : TraitInfo [ActorReference] public readonly string[] SupportActors = Array.Empty(); + [Desc("A group of buildings ready to work.")] + public readonly string[] SupportBuildings = Array.Empty(); + + [Desc("A group of proxy actors that will be at the start.")] + public readonly string[] SupportProxyActors = Array.Empty(); + [Desc("Inner radius for spawning support actors")] public readonly int InnerSupportRadius = 2; [Desc("Outer radius for spawning support actors")] public readonly int OuterSupportRadius = 4; + [Desc("Inner radius for spawning support buildings")] + public readonly int InnerBuildingRadius = 3; + + [Desc("Outer radius for spawning support buildings")] + public readonly int OuterBuildingRadius = 5; + [Desc("Initial facing of BaseActor. Leave undefined for random facings.")] public readonly WAngle? BaseActorFacing = new WAngle(512); diff --git a/OpenRA.Mods.Common/Traits/World/RenderPostProcessPassBase.cs b/OpenRA.Mods.Common/Traits/World/RenderPostProcessPassBase.cs new file mode 100644 index 000000000000..2658694e44d2 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/World/RenderPostProcessPassBase.cs @@ -0,0 +1,56 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public abstract class RenderPostProcessPassBase : IRenderPostProcessPass + { + readonly Renderer renderer; + readonly IShader shader; + readonly IVertexBuffer buffer; + readonly PostProcessPassType type; + + protected RenderPostProcessPassBase(string name, PostProcessPassType type) + { + this.type = type; + renderer = Game.Renderer; + shader = renderer.CreateShader(new RenderPostProcessPassShaderBindings(name)); + var vertices = new RenderPostProcessPassVertex[] + { + new(-1, -1), + new(1, -1), + new(1, 1), + new(1, 1), + new(-1, 1), + new(-1, -1) + }; + + buffer = renderer.CreateVertexBuffer(6); + buffer.SetData(ref vertices, 6); + } + + PostProcessPassType IRenderPostProcessPass.Type => type; + bool IRenderPostProcessPass.Enabled => Enabled; + void IRenderPostProcessPass.Draw(WorldRenderer wr, ITexture worldTexture) + { + shader.SetTexture("WorldTexture", worldTexture); + PrepareRender(wr, shader); + shader.PrepareRender(); + renderer.DrawBatch(buffer, shader, 0, 6, PrimitiveType.TriangleList); + } + + protected abstract bool Enabled { get; } + protected abstract void PrepareRender(WorldRenderer wr, IShader shader); + } +} diff --git a/OpenRA.Mods.Common/Traits/World/ResourceLayer.cs b/OpenRA.Mods.Common/Traits/World/ResourceLayer.cs index 5266eb40e3a4..ab6f3ca51426 100644 --- a/OpenRA.Mods.Common/Traits/World/ResourceLayer.cs +++ b/OpenRA.Mods.Common/Traits/World/ResourceLayer.cs @@ -102,7 +102,7 @@ bool IResourceLayerInfo.TryGetResourceIndex(string resourceType, out byte index) public override object Create(ActorInitializer init) { return new ResourceLayer(init.Self, this); } } - public class ResourceLayer : IResourceLayer, IWorldLoaded + public class ResourceLayer : IResourceLayer, IWorldLoaded, INotifyCreated { readonly ResourceLayerInfo info; readonly World world; @@ -111,6 +111,8 @@ public class ResourceLayer : IResourceLayer, IWorldLoaded protected readonly CellLayer Content; protected readonly Dictionary ResourceTypesByIndex; + IResourceLogicLayer[] resourceLogicLayers; + int resCells; public event Action CellChanged; @@ -127,6 +129,11 @@ public ResourceLayer(Actor self, ResourceLayerInfo info) kv => kv.Key); } + void INotifyCreated.Created(Actor self) + { + resourceLogicLayers = self.TraitsImplementing().ToArray(); + } + protected virtual void WorldLoaded(World w, WorldRenderer wr) { foreach (var cell in w.Map.AllCells) @@ -163,6 +170,9 @@ protected virtual void WorldLoaded(World w, WorldRenderer wr) // Adjacent includes the current cell, so is always >= 1 var density = Math.Max(int2.Lerp(0, resourceInfo.MaxDensity, adjacent, 9), 1); Content[cell] = new ResourceLayerContents(resource.Type, density); + + foreach (var rl in resourceLogicLayers) + rl.UpdatePosition(cell, resource.Type, density); } } @@ -235,6 +245,9 @@ int AddResource(string resourceType, CPos cell, int amount = 1) CellChanged?.Invoke(cell, content.Type); + foreach (var rl in resourceLogicLayers) + rl.UpdatePosition(cell, content.Type, density); + return density - oldDensity; } @@ -257,11 +270,17 @@ int RemoveResource(string resourceType, CPos cell, int amount = 1) --resCells; CellChanged?.Invoke(cell, null); + + foreach (var rl in resourceLogicLayers) + rl.UpdatePosition(cell, content.Type, 0); } else { Content[cell] = new ResourceLayerContents(content.Type, density); CellChanged?.Invoke(cell, content.Type); + + foreach (var rl in resourceLogicLayers) + rl.UpdatePosition(cell, content.Type, density); } return oldDensity - density; @@ -281,6 +300,9 @@ void ClearResources(CPos cell) Map.CustomTerrain[cell] = byte.MaxValue; --resCells; + foreach (var rl in resourceLogicLayers) + rl.UpdatePosition(cell, content.Type, 0); + CellChanged?.Invoke(cell, null); } diff --git a/OpenRA.Mods.Common/Traits/World/ResourceRenderer.cs b/OpenRA.Mods.Common/Traits/World/ResourceRenderer.cs index 0a79f976b33d..f6ef0bf597b9 100644 --- a/OpenRA.Mods.Common/Traits/World/ResourceRenderer.cs +++ b/OpenRA.Mods.Common/Traits/World/ResourceRenderer.cs @@ -232,7 +232,7 @@ void ITickRender.TickRender(WorldRenderer wr, Actor self) dirty.Remove(cleanDirty.Dequeue()); } - protected virtual void UpdateRenderedSprite(CPos cell, RendererCellContents content) + public virtual void UpdateRenderedSprite(CPos cell, RendererCellContents content) { if (content.Density > 0) { @@ -337,34 +337,34 @@ bool IRadarTerrainLayer.TryGetTerrainColorPair(MPos uv, out (Color Left, Color R value = (info.Color, info.Color); return true; } + } - public readonly struct RendererCellContents - { - public readonly string Type; - public readonly ResourceRendererInfo.ResourceTypeInfo Info; - public readonly ISpriteSequence Sequence; - public readonly PaletteReference Palette; - public readonly int Density; + public readonly struct RendererCellContents + { + public readonly string Type; + public readonly ResourceRendererInfo.ResourceTypeInfo Info; + public readonly ISpriteSequence Sequence; + public readonly PaletteReference Palette; + public readonly int Density; - public static readonly RendererCellContents Empty = default; + public static readonly RendererCellContents Empty = default; - public RendererCellContents(string resourceType, int density, ResourceRendererInfo.ResourceTypeInfo info, ISpriteSequence sequence, PaletteReference palette) - { - Type = resourceType; - Density = density; - Info = info; - Sequence = sequence; - Palette = palette; - } + public RendererCellContents(string resourceType, int density, ResourceRendererInfo.ResourceTypeInfo info, ISpriteSequence sequence, PaletteReference palette) + { + Type = resourceType; + Density = density; + Info = info; + Sequence = sequence; + Palette = palette; + } - public RendererCellContents(RendererCellContents contents, int density) - { - Type = contents.Type; - Density = density; - Info = contents.Info; - Sequence = contents.Sequence; - Palette = contents.Palette; - } + public RendererCellContents(RendererCellContents contents, int density) + { + Type = contents.Type; + Density = density; + Info = contents.Info; + Sequence = contents.Sequence; + Palette = contents.Palette; } } } diff --git a/OpenRA.Mods.Common/Traits/World/SpawnStartingUnits.cs b/OpenRA.Mods.Common/Traits/World/SpawnStartingUnits.cs index c4337113a03b..0973de887053 100644 --- a/OpenRA.Mods.Common/Traits/World/SpawnStartingUnits.cs +++ b/OpenRA.Mods.Common/Traits/World/SpawnStartingUnits.cs @@ -97,8 +97,30 @@ void SpawnUnitsForPlayer(World w, Player p) }); } - if (unitGroup.SupportActors.Length == 0) - return; + var buildingSpawnCells = w.Map.FindTilesInAnnulus(p.HomeLocation, unitGroup.InnerBuildingRadius + 1, unitGroup.OuterBuildingRadius); + + foreach (var b in unitGroup.SupportBuildings) + { + var actorRules = w.Map.Rules.Actors[b.ToLowerInvariant()]; + var building = actorRules.TraitInfo(); + var validCells = buildingSpawnCells.Where(c => w.CanPlaceBuilding(c, actorRules, building, null)); + if (!validCells.Any()) + { + Log.Write("debug", $"No cells available to spawn starting building {b} for player {p}"); + continue; + } + + var cell = validCells.Random(w.SharedRandom); + var facing = unitGroup.SupportActorsFacing ?? new WAngle(w.SharedRandom.Next(1024)); + + w.CreateActor(b.ToLowerInvariant(), new TypeDictionary + { + new OwnerInit(p), + new LocationInit(cell), + new SkipMakeAnimsInit(), + new FacingInit(facing) + }); + } var supportSpawnCells = w.Map.FindTilesInAnnulus(p.HomeLocation, unitGroup.InnerSupportRadius + 1, unitGroup.OuterSupportRadius); @@ -125,6 +147,14 @@ void SpawnUnitsForPlayer(World w, Player p) new FacingInit(facing), }); } + + foreach (var pa in unitGroup.SupportProxyActors) + { + w.CreateActor(pa.ToLowerInvariant(), new TypeDictionary + { + new OwnerInit(p) + }); + } } } } diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index df9fd2ea34fa..3e8a2f74a256 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -31,7 +31,8 @@ public enum ResupplyType { None = 0, Rearm = 1, - Repair = 2 + Repair = 2, + RepairNear = 4 } public interface IQuantizeBodyOrientationInfo : ITraitInfoInterface @@ -330,7 +331,7 @@ public interface IProductionIconOverlay Sprite Sprite { get; } string Palette { get; } float2 Offset(float2 iconSize); - bool IsOverlayActive(ActorInfo ai); + bool IsOverlayActive(ActorInfo ai, Actor producer); } public interface INotifyTransform @@ -452,10 +453,13 @@ public interface IDamageModifier { int GetDamageModifier(Actor attacker, Damage public interface ISpeedModifier { int GetSpeedModifier(); } [RequireExplicitImplementation] - public interface IFirepowerModifier { int GetFirepowerModifier(); } + public interface ITurnSpeedModifier { int GetTurnSpeedModifier(); } [RequireExplicitImplementation] - public interface IReloadModifier { int GetReloadModifier(); } + public interface IFirepowerModifier { int GetFirepowerModifier(string armamentName); } + + [RequireExplicitImplementation] + public interface IReloadModifier { int GetReloadModifier(string armamentName); } [RequireExplicitImplementation] public interface IReloadAmmoModifier { int GetReloadAmmoModifier(); } @@ -623,7 +627,19 @@ public interface IBotPositionsUpdated [RequireExplicitImplementation] public interface IBotNotifyIdleBaseUnits { - void UpdatedIdleBaseUnits(List idleUnits); + void UpdatedIdleBaseUnits(List unitsHangingAroundTheBase); + } + + public class UnitWposWrapper + { + public Actor Actor; + public WPos WPos; + + public UnitWposWrapper(Actor a) + { + Actor = a; + WPos = WPos.Zero; + } } [RequireExplicitImplementation] diff --git a/OpenRA.Mods.Common/TraitsInterfacesExternal.cs b/OpenRA.Mods.Common/TraitsInterfacesExternal.cs new file mode 100644 index 000000000000..00bb5c2fb73d --- /dev/null +++ b/OpenRA.Mods.Common/TraitsInterfacesExternal.cs @@ -0,0 +1,70 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [RequireExplicitImplementation] + public interface IBuildPaletteOrderModifierInfo : ITraitInfoInterface { int GetBuildPaletteOrderModifier(TechTree techTree, string queue); } + + [RequireExplicitImplementation] + public interface IResourcePurifier + { + void RefineAmount(int amount); + } + + [RequireExplicitImplementation] + public interface IResourceLogicLayer + { + void UpdatePosition(CPos cell, string resourceType, int density); + } + + [RequireExplicitImplementation] + public interface IRefineryResourceDelivered + { + void ResourceGiven(Actor self, int amount); + } + + [RequireExplicitImplementation] + public interface IRemoveInfector + { + void RemoveInfector(Actor self, bool kill, AttackInfo e = null); + } + + [RequireExplicitImplementation] + public interface IPointDefense + { + bool Destroy(WPos position, Player attacker, BitSet types); + } + + [RequireExplicitImplementation] + public interface INotifyPassengersDamage + { + void DamagePassengers(int damage, Actor attacker, int amount, Dictionary versus, BitSet damageTypes, IEnumerable damageModifiers); + } + + [RequireExplicitImplementation] + public interface ISupplyDock + { + bool IsFull(); + bool IsEmpty(); + int Fullness(); + } + + [RequireExplicitImplementation] + public interface ISupplyCollector + { + int Amount(); + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20200503/RemoveLaysTerrain.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20200503/RemoveLaysTerrain.cs index aa5369a8b1e2..dff309c7ef53 100644 --- a/OpenRA.Mods.Common/UpdateRules/Rules/20200503/RemoveLaysTerrain.cs +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20200503/RemoveLaysTerrain.cs @@ -23,8 +23,6 @@ public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNod { if (actorNode.RemoveNodes("LaysTerrain") > 0) yield return $"'LaysTerrain' was removed from {actorNode.Key} ({actorNode.Location.Filename}) without replacement.\n"; - - yield break; } } } diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20210321/ChangeBackwardDurationDefaultValue.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20210321/ChangeBackwardDurationDefaultValue.cs new file mode 100644 index 000000000000..f29b2c0a3943 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20210321/ChangeBackwardDurationDefaultValue.cs @@ -0,0 +1,37 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + class ChangeBackwardDurationDefaultValue : UpdateRule + { + public override string Name => "BackwardDuration default value changed."; + + public override string Description => "BackwardDuration default value changed, old default value has to be defined in the rules now"; + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNodeBuilder actorNode) + { + foreach (var mobile in actorNode.ChildrenMatching("mobile", includeRemovals: false)) + { + var backwardDuration = mobile.LastChildMatching("BackwardDuration"); + if (backwardDuration != null) + continue; + + var backwardDurationNode = new MiniYamlNodeBuilder("BackwardDuration", FieldSaver.FormatValue(40)); + backwardDurationNode.AddNode(backwardDurationNode); + } + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20230801/ExtractResourceStorageFromHarvester.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/ExtractResourceStorageFromHarvester.cs index c7f66be92869..96d0d3b9c163 100644 --- a/OpenRA.Mods.Common/UpdateRules/Rules/20230801/ExtractResourceStorageFromHarvester.cs +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/ExtractResourceStorageFromHarvester.cs @@ -42,8 +42,6 @@ public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNod storesResources.AddNode(resources); actorNode.AddNode(storesResources); - - yield break; } } } diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20230801/RemoveConyardChronoReturnAnimation.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/RemoveConyardChronoReturnAnimation.cs new file mode 100644 index 000000000000..d838da2be3c2 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/RemoveConyardChronoReturnAnimation.cs @@ -0,0 +1,33 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class RemoveConyardChronoReturnAnimation : UpdateRule + { + public override string Name => "Remove Sequence and Body properties from ConyardChronoReturn."; + + public override string Description => "These properties have been replaced with a dynamic vortex renderable."; + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNodeBuilder actorNode) + { + foreach (var trait in actorNode.ChildrenMatching("ConyardChronoReturn")) + { + trait.RemoveNodes("Sequence"); + trait.RemoveNodes("Body"); + } + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20230801/RemoveValidRelationsFromCapturable.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/RemoveValidRelationsFromCapturable.cs index 6434cd3a7c36..104a791925b7 100644 --- a/OpenRA.Mods.Common/UpdateRules/Rules/20230801/RemoveValidRelationsFromCapturable.cs +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/RemoveValidRelationsFromCapturable.cs @@ -10,7 +10,6 @@ #endregion using System.Collections.Generic; -using System.Linq; namespace OpenRA.Mods.Common.UpdateRules.Rules { @@ -24,7 +23,7 @@ public class RemoveValidRelationsFromCapturable : UpdateRule public override IEnumerable AfterUpdate(ModData modData) { - if (locations.Any()) + if (locations.Count > 0) yield return Description + "\n" + "ValidRelations have been removed from:\n" + UpdateUtils.FormatMessageList(locations); diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20230801/ReplacePaletteModifiers.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/ReplacePaletteModifiers.cs new file mode 100644 index 000000000000..fba6cc6b3d4c --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20230801/ReplacePaletteModifiers.cs @@ -0,0 +1,45 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + public class ReplacePaletteModifiers : UpdateRule + { + public override string Name => "Replace palette modifiers with post-processing shaders."; + + public override string Description => + "MenuPaletteEffect is renamed to MenuPostProcessEffect\n" + + "ChronoshiftPaletteEffect is renamed to ChronoshiftPostProcessEffect\n" + + "FlashPaletteEffect is renamed to FlashPostProcessEffect\n" + + "GlobalLightingPaletteEffect is renamed to TintPostProcessEffect"; + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNodeBuilder actorNode) + { + actorNode.RenameChildrenMatching("MenuPaletteEffect", "MenuPostProcessEffect"); + actorNode.RenameChildrenMatching("ChronoshiftPaletteEffect", "ChronoshiftPostProcessEffect"); + actorNode.RenameChildrenMatching("FlashPaletteEffect", "FlashPostProcessEffect"); + actorNode.RenameChildrenMatching("GlobalLightingPaletteEffect", "TintPostProcessEffect"); + + yield break; + } + + public override IEnumerable UpdateWeaponNode(ModData modData, MiniYamlNodeBuilder weaponNode) + { + foreach (var warheadNode in weaponNode.ChildrenMatching("Warhead")) + if (warheadNode.Value.Value == "FlashPaletteEffect") + warheadNode.Value.Value = "FlashEffect"; + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs index 7c8970e450b5..1ef750ee719e 100644 --- a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs +++ b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs @@ -15,7 +15,7 @@ namespace OpenRA.Mods.Common.UpdateRules { - public class UpdatePath + public sealed class UpdatePath { // Define known update paths from stable tags to the current bleed tip // @@ -80,6 +80,7 @@ public class UpdatePath new ReplaceSequenceEmbeddedPalette(), new UnhardcodeVeteranProductionIconOverlay(), new RenameContrailProperties(), + new ChangeBackwardDurationDefaultValue(), new RemoveDomainIndex(), new AddControlGroups(), @@ -110,6 +111,8 @@ public class UpdatePath // bleed only changes here. new RemoveValidRelationsFromCapturable(), new ExtractResourceStorageFromHarvester(), + new ReplacePaletteModifiers(), + new RemoveConyardChronoReturnAnimation(), // Execute these rules last to avoid premature yaml merge crashes. new AbstractDocking(), diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractEmmyLuaAPI.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractEmmyLuaAPI.cs index 7f4a703c5190..f438942474ef 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractEmmyLuaAPI.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractEmmyLuaAPI.cs @@ -58,7 +58,6 @@ void IUtilityCommand.Run(Utility utility, string[] args) Console.WriteLine(); var globalTables = utility.ModData.ObjectCreator.GetTypesImplementing().OrderBy(t => t.Name); WriteGlobals(globalTables); - Console.WriteLine(); var actorProperties = utility.ModData.ObjectCreator.GetTypesImplementing(); WriteScriptProperties(typeof(Actor), actorProperties); @@ -70,8 +69,10 @@ void IUtilityCommand.Run(Utility utility, string[] args) static void WriteDiagnosticsDisabling() { Console.WriteLine("--- This file only lists function \"signatures\", causing Lua Diagnostics errors: \"Annotations specify that a return value is required here.\""); - Console.WriteLine("--- Disable that specific error for the entire file."); + Console.WriteLine("--- and Lua Diagnostics warnings \"Unused local\" for the functions' parameters."); + Console.WriteLine("--- Disable those specific errors for the entire file."); Console.WriteLine("---@diagnostic disable: missing-return"); + Console.WriteLine("---@diagnostic disable: unused-local"); } static void WriteManual() @@ -149,7 +150,13 @@ static void WriteActorInits(IEnumerable actorInits, out IEnumerable .Distinct()); if (!string.IsNullOrEmpty(parameterString)) + { + // OwnerInit is special as it is the only "required" init. All others are optional. + if (init.Name != nameof(OwnerInit)) + parameterString += '?'; + Console.WriteLine($"---@field {name} {parameterString}"); + } } usedEnums = localEnums.Distinct(); @@ -239,43 +246,60 @@ static void WriteGlobals(IEnumerable globalTables) static void WriteScriptProperties(Type type, IEnumerable implementingTypes) { var className = type.Name.ToLowerInvariant(); - var tableName = $"__{type.Name.ToLowerInvariant()}"; + var tableName = $"__{className}"; Console.WriteLine($"---@class {className}"); - Console.WriteLine("local " + tableName + " = {"); - var properties = implementingTypes.SelectMany(t => + var members = implementingTypes.SelectMany(t => { var requiredTraits = ScriptMemberWrapper.RequiredTraitNames(t); return ScriptMemberWrapper.WrappableMembers(t).Select(memberInfo => (memberInfo, requiredTraits)); }); - var duplicateProperties = properties + var duplicateMembers = members .GroupBy(x => x.memberInfo.Name) .Where(x => x.Count() > 1) .Select(x => x.Key) .ToHashSet(); - foreach (var (memberInfo, requiredTraits) in properties) + foreach (var (memberInfo, requiredTraits) in members) { - Console.WriteLine(); + // Properties are supposed to be defined as @fields on the class. + // They can be defined as keys inside the tables, but then are treated as readonly by the Lua extension. + if (memberInfo is PropertyInfo propertyInfo && propertyInfo.CanWrite) + { + WriteMemberDescription(memberInfo, requiredTraits, 0); - var isActivity = Utility.HasAttribute(memberInfo); + if (duplicateMembers.Contains(memberInfo.Name)) + Console.WriteLine(" ---@diagnostic disable-next-line: duplicate-index"); - if (Utility.HasAttribute(memberInfo)) - { - var lines = Utility.GetCustomAttributes(memberInfo, true).First().Lines; - foreach (var line in lines) - Console.WriteLine($" --- {line}"); + Console.WriteLine($"---@field {propertyInfo.Name} {propertyInfo.PropertyType.EmmyLuaString()}"); } + } - if (isActivity) - Console.WriteLine(" --- *Queued Activity*"); + Console.WriteLine("local " + tableName + " = {"); - if (requiredTraits.Length != 0) - Console.WriteLine($" --- **Requires {(requiredTraits.Length == 1 ? "Trait" : "Traits")}:** {requiredTraits.Select(GetDocumentationUrl).JoinWith(", ")}"); + foreach (var (memberInfo, requiredTraits) in members) + { + // Properties are supposed to be defined as @fields on the class, + // but if they are defined as keys inside the table, they are treated as readonly by the Lua extension. + if (memberInfo is PropertyInfo propertyInfo && !propertyInfo.CanWrite) + { + Console.WriteLine(); + WriteMemberDescription(memberInfo, requiredTraits, 1); + + if (duplicateMembers.Contains(memberInfo.Name)) + Console.WriteLine(" ---@diagnostic disable-next-line: duplicate-index"); + + Console.WriteLine($" ---@type {propertyInfo.PropertyType.EmmyLuaString()}"); + Console.WriteLine($" {propertyInfo.Name} = nil;"); + } + // Functions are defined as keys inside the table. if (memberInfo is MethodInfo methodInfo) { + Console.WriteLine(); + WriteMemberDescription(memberInfo, requiredTraits, 1); + var attributes = methodInfo.GetCustomAttributes(false); foreach (var obsolete in attributes.OfType()) Console.WriteLine($" ---@deprecated {obsolete.Message}"); @@ -290,25 +314,33 @@ static void WriteScriptProperties(Type type, IEnumerable implementingTypes if (returnType != "Void") Console.WriteLine($" ---@return {returnType}"); - if (duplicateProperties.Contains(methodInfo.Name)) + if (duplicateMembers.Contains(methodInfo.Name)) Console.WriteLine(" ---@diagnostic disable-next-line: duplicate-index"); Console.WriteLine($" {methodInfo.Name} = function({parameterString}) end;"); } + } - if (memberInfo is PropertyInfo propertyInfo) - { - Console.WriteLine($" ---@type {propertyInfo.PropertyType.EmmyLuaString()}"); + Console.WriteLine("}"); + Console.WriteLine(); - if (duplicateProperties.Contains(propertyInfo.Name)) - Console.WriteLine(" ---@diagnostic disable-next-line: duplicate-index"); + static void WriteMemberDescription(MemberInfo memberInfo, string[] requiredTraits, int indentation) + { + var isActivity = Utility.HasAttribute(memberInfo); - Console.WriteLine(" " + propertyInfo.Name + " = nil;"); + if (Utility.HasAttribute(memberInfo)) + { + var lines = Utility.GetCustomAttributes(memberInfo, true).First().Lines; + foreach (var line in lines) + Console.WriteLine($"{new string(' ', indentation * 4)}--- {line}"); } - } - Console.WriteLine("}"); - Console.WriteLine(); + if (isActivity) + Console.WriteLine($"{new string(' ', indentation * 4)}--- *Queued Activity*"); + + if (requiredTraits.Length != 0) + Console.WriteLine($"{new string(' ', indentation * 4)}--- **Requires {(requiredTraits.Length == 1 ? "Trait" : "Traits")}:** {requiredTraits.Select(GetDocumentationUrl).JoinWith(", ")}"); + } } static string GetDocumentationUrl(string trait) diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractLuaDocsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractLuaDocsCommand.cs index 88d83a9f9049..8a071ba2d51b 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractLuaDocsCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractLuaDocsCommand.cs @@ -74,10 +74,18 @@ void IUtilityCommand.Run(Utility utility, string[] args) Console.WriteLine(); Console.WriteLine("| Function | Description |"); Console.WriteLine("|---------:|-------------|"); + foreach (var m in members.OrderBy(m => m.Name)) { + var memberString = m.LuaDocString(); var desc = Utility.HasAttribute(m) ? Utility.GetCustomAttributes(m, true).First().Lines.JoinWith("
") : ""; - Console.WriteLine($"| **{m.LuaDocString()}** | {desc} |"); + if (Utility.HasAttribute(m)) + { + memberString = $"{memberString}"; + desc += $"
**Deprecated: {Utility.GetCustomAttributes(m, true).First().Message}**"; + } + + Console.WriteLine($"| **{memberString}** | {desc} |"); } } @@ -105,12 +113,17 @@ void IUtilityCommand.Run(Utility utility, string[] args) foreach (var property in kv.OrderBy(p => p.mi.Name)) { var mi = property.mi; + var memberString = mi.LuaDocString(); var required = property.required; var hasDesc = Utility.HasAttribute(mi); var hasRequires = required.Length > 0; var isActivity = Utility.HasAttribute(mi); + var isDeprecated = Utility.HasAttribute(mi); - Console.Write($"| **{mi.LuaDocString()}**"); + if (isDeprecated) + Console.Write($"| **{memberString}**"); + else + Console.Write($"| **{memberString}**"); if (isActivity) Console.Write("
*Queued Activity*"); @@ -126,6 +139,9 @@ void IUtilityCommand.Run(Utility utility, string[] args) if (hasRequires) Console.Write($"**Requires {(required.Length == 1 ? "Trait" : "Traits")}:** {required.JoinWith(", ")}"); + if (isDeprecated) + Console.Write($"**Deprecated: {Utility.GetCustomAttributes(mi, true).First().Message}**"); + Console.WriteLine(" |"); } } @@ -154,12 +170,17 @@ void IUtilityCommand.Run(Utility utility, string[] args) foreach (var property in kv.OrderBy(p => p.mi.Name)) { var mi = property.mi; + var memberString = mi.LuaDocString(); var required = property.required; var hasDesc = Utility.HasAttribute(mi); var hasRequires = required.Length > 0; var isActivity = Utility.HasAttribute(mi); + var isDeprecated = Utility.HasAttribute(mi); - Console.Write($"| **{mi.LuaDocString()}**"); + if (isDeprecated) + Console.Write($"| **{memberString}**"); + else + Console.Write($"| **{memberString}**"); if (isActivity) Console.Write("
*Queued Activity*"); @@ -175,6 +196,9 @@ void IUtilityCommand.Run(Utility utility, string[] args) if (hasRequires) Console.Write($"**Requires {(required.Length == 1 ? "Trait" : "Traits")}:** {required.JoinWith(", ")}"); + if (isDeprecated) + Console.Write($"**Deprecated: {Utility.GetCustomAttributes(mi, true).First().Message}**"); + Console.WriteLine(" |"); } diff --git a/OpenRA.Mods.Common/Warheads/ChangeOwnerWarhead.cs b/OpenRA.Mods.Common/Warheads/ChangeOwnerWarhead.cs index 16deb0bfb51a..a1328aaf0b4e 100644 --- a/OpenRA.Mods.Common/Warheads/ChangeOwnerWarhead.cs +++ b/OpenRA.Mods.Common/Warheads/ChangeOwnerWarhead.cs @@ -9,18 +9,28 @@ */ #endregion +using System.Linq; using OpenRA.GameRules; using OpenRA.Mods.Common.Traits; using OpenRA.Traits; namespace OpenRA.Mods.Common.Warheads { + public enum OwnerChangeType { Firer, InternalName } + [Desc("Interacts with the `" + nameof(TemporaryOwnerManager) + "` trait.")] public class ChangeOwnerWarhead : Warhead { [Desc("Duration of the owner change (in ticks). Set to 0 to make it permanent.")] public readonly int Duration = 0; + [Desc("Owner to change to. Allowed keywords:" + + "'Firer' and 'InternalName'.")] + public readonly OwnerChangeType OwnerType = OwnerChangeType.Firer; + + [Desc("Map player to use when 'InternalName' is defined on 'OwnerType'.")] + public readonly string InternalOwner = "Neutral"; + public readonly WDist Range = WDist.FromCells(1); public override void DoImpact(in Target target, WarheadArgs args) @@ -34,19 +44,23 @@ public override void DoImpact(in Target target, WarheadArgs args) if (!IsValidAgainst(a, firedBy)) continue; - // Don't do anything on friendly fire - if (a.Owner == firedBy.Owner) + var owner = firedBy.Owner; + if (OwnerType == OwnerChangeType.InternalName) + owner = firedBy.World.Players.First(p => p.InternalName == InternalOwner); + + // Don't do anything on if already target owner + if (a.Owner == owner) continue; if (Duration == 0) - a.ChangeOwner(firedBy.Owner); // Permanent + a.ChangeOwner(owner); // Permanent else { var tempOwnerManager = a.TraitOrDefault(); if (tempOwnerManager == null) continue; - tempOwnerManager.ChangeOwner(a, firedBy.Owner, Duration); + tempOwnerManager.ChangeOwner(a, owner, Duration); } // Stop shooting, you have new enemies diff --git a/OpenRA.Mods.Common/Warheads/CreateEffectWarhead.cs b/OpenRA.Mods.Common/Warheads/CreateEffectWarhead.cs index 664398e729bc..f74f419f9039 100644 --- a/OpenRA.Mods.Common/Warheads/CreateEffectWarhead.cs +++ b/OpenRA.Mods.Common/Warheads/CreateEffectWarhead.cs @@ -51,6 +51,12 @@ public class CreateEffectWarhead : Warhead [Desc("The maximum inaccuracy of the effect spawn position relative to actual impact position.")] public readonly WDist Inaccuracy = WDist.Zero; + [Desc("Do the impact sounds play under shroud or fog.")] + public readonly bool AudibleThroughFog = false; + + [Desc("Volume the impact sounds played at.")] + public readonly float Volume = 1f; + static readonly BitSet TargetTypeAir = new("Air"); /// Checks if there are any actors at impact position and if the warhead is valid against any of them. @@ -131,8 +137,8 @@ public override void DoImpact(in Target target, WarheadArgs args) } var impactSound = ImpactSounds.RandomOrDefault(world.LocalRandom); - if (impactSound != null && world.LocalRandom.Next(0, 100) < ImpactSoundChance) - Game.Sound.Play(SoundType.World, impactSound, pos); + if (impactSound != null && world.LocalRandom.Next(0, 100) < ImpactSoundChance && (AudibleThroughFog || (!world.ShroudObscures(pos) && !world.FogObscures(pos)))) + Game.Sound.Play(SoundType.World, impactSound, pos, Volume); } /// Checks if the warhead is valid against the terrain at impact position. diff --git a/OpenRA.Mods.Common/Warheads/FireClusterWarhead.cs b/OpenRA.Mods.Common/Warheads/FireClusterWarhead.cs index b80ae5e0db31..b387547eb1fb 100644 --- a/OpenRA.Mods.Common/Warheads/FireClusterWarhead.cs +++ b/OpenRA.Mods.Common/Warheads/FireClusterWarhead.cs @@ -99,7 +99,11 @@ void FireProjectileAtCell(Map map, Actor firedBy, Target target, CPos targetCell firedBy.World.AddFrameEndTask(w => w.Add(projectile)); if (projectileArgs.Weapon.Report != null && projectileArgs.Weapon.Report.Length > 0) - Game.Sound.Play(SoundType.World, projectileArgs.Weapon.Report, firedBy.World, target.CenterPosition); + { + var pos = target.CenterPosition; + if (projectileArgs.Weapon.AudibleThroughFog || (!firedBy.World.ShroudObscures(pos) && !firedBy.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, projectileArgs.Weapon.Report, firedBy.World, pos, null, projectileArgs.Weapon.SoundVolume); + } } } diff --git a/OpenRA.Mods.Common/Warheads/FlashPaletteEffectWarhead.cs b/OpenRA.Mods.Common/Warheads/FlashEffectWarhead.cs similarity index 74% rename from OpenRA.Mods.Common/Warheads/FlashPaletteEffectWarhead.cs rename to OpenRA.Mods.Common/Warheads/FlashEffectWarhead.cs index da96b5677eb3..2ef36b9df704 100644 --- a/OpenRA.Mods.Common/Warheads/FlashPaletteEffectWarhead.cs +++ b/OpenRA.Mods.Common/Warheads/FlashEffectWarhead.cs @@ -15,19 +15,19 @@ namespace OpenRA.Mods.Common.Warheads { - [Desc("Used to trigger a FlashPaletteEffect trait on the world actor.")] - public class FlashPaletteEffectWarhead : Warhead + [Desc("Used to trigger a FlashPostProcessEffect trait on the world actor.")] + public class FlashEffectWarhead : Warhead { - [Desc("Corresponds to `Type` from `FlashPaletteEffect` on the world actor.")] + [Desc("Corresponds to `Type` from `FlashPostProcessEffect` on the world actor.")] public readonly string FlashType = null; [FieldLoader.Require] - [Desc("Duration of the flashing, measured in ticks. Set to -1 to default to the `Length` of the `FlashPaletteEffect`.")] + [Desc("Duration of the flashing, measured in ticks. Set to -1 to default to the `Length` of the `FlashPostProcessEffect`.")] public readonly int Duration = 0; public override void DoImpact(in Target target, WarheadArgs args) { - foreach (var flash in args.SourceActor.World.WorldActor.TraitsImplementing()) + foreach (var flash in args.SourceActor.World.WorldActor.TraitsImplementing()) if (flash.Info.Type == FlashType) flash.Enable(Duration); } diff --git a/OpenRA.Mods.Common/Warheads/GrantExternalConditionWarhead.cs b/OpenRA.Mods.Common/Warheads/GrantExternalConditionWarhead.cs index 753812cbd29f..881bbcef5b7f 100644 --- a/OpenRA.Mods.Common/Warheads/GrantExternalConditionWarhead.cs +++ b/OpenRA.Mods.Common/Warheads/GrantExternalConditionWarhead.cs @@ -30,6 +30,9 @@ public class GrantExternalConditionWarhead : Warhead public override void DoImpact(in Target target, WarheadArgs args) { + if (target.Type == TargetType.Invalid) + return; + var firedBy = args.SourceActor; if (target.Type == TargetType.Invalid) diff --git a/OpenRA.Mods.Common/Warheads/SpreadDamageWarhead.cs b/OpenRA.Mods.Common/Warheads/SpreadDamageWarhead.cs index 88ce7d710737..01f5a24d0323 100644 --- a/OpenRA.Mods.Common/Warheads/SpreadDamageWarhead.cs +++ b/OpenRA.Mods.Common/Warheads/SpreadDamageWarhead.cs @@ -17,6 +17,7 @@ namespace OpenRA.Mods.Common.Warheads { public enum DamageCalculationType { HitShape, ClosestTargetablePosition, CenterPosition } + public enum UnintendedFriendlyFireType { None, Actor, All } [Desc("Apply damage in a specified range.")] public class SpreadDamageWarhead : DamageWarhead, IRulesetLoaded @@ -33,8 +34,13 @@ public class SpreadDamageWarhead : DamageWarhead, IRulesetLoaded [Desc("Controls the way damage is calculated. Possible values are 'HitShape', 'ClosestTargetablePosition' and 'CenterPosition'.")] public readonly DamageCalculationType DamageCalculationType = DamageCalculationType.HitShape; + [Desc("If attacker doesn't intent, this warhead won't hurt friendly actor.")] + public readonly UnintendedFriendlyFireType NoUnintendedFriendlyFire = UnintendedFriendlyFireType.None; + WDist[] effectiveRange; + bool noFriendlyFire = false; + void IRulesetLoaded.RulesetLoaded(Ruleset rules, WeaponInfo info) { if (Range != null) @@ -58,9 +64,27 @@ protected override void DoImpact(WPos pos, Actor firedBy, WarheadArgs args) if (debugVis != null && debugVis.CombatGeometry) firedBy.World.WorldActor.Trait().AddImpact(pos, effectiveRange, DebugOverlayColor); + if (NoUnintendedFriendlyFire != UnintendedFriendlyFireType.None) + { + var intentTarget = args.WeaponTarget; + switch (intentTarget.Type) + { + case TargetType.Actor: + noFriendlyFire = intentTarget.Actor.Owner.RelationshipWith(firedBy.Owner) != PlayerRelationship.Ally; + break; + case TargetType.FrozenActor: + noFriendlyFire = intentTarget.FrozenActor.Owner.RelationshipWith(firedBy.Owner) != PlayerRelationship.Ally; + break; + default: + if (NoUnintendedFriendlyFire == UnintendedFriendlyFireType.All) + noFriendlyFire = true; + break; + } + } + foreach (var victim in firedBy.World.FindActorsOnCircle(pos, effectiveRange[^1])) { - if (!IsValidAgainst(victim, firedBy)) + if (!IsValidAgainst(victim, firedBy) || (noFriendlyFire && victim.Owner.RelationshipWith(firedBy.Owner) == PlayerRelationship.Ally)) continue; HitShape closestActiveShape = null; diff --git a/OpenRA.Mods.Common/Widgets/ButtonWidget.cs b/OpenRA.Mods.Common/Widgets/ButtonWidget.cs index 43a9deb8ad83..43724700ce7b 100644 --- a/OpenRA.Mods.Common/Widgets/ButtonWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ButtonWidget.cs @@ -25,6 +25,7 @@ public class ButtonWidget : InputWidget public bool DisableKeyRepeat = false; public bool DisableKeySound = false; + [TranslationReference] public string Text = ""; public TextAlign Align = TextAlign.Center; public int LeftMargin = 5; @@ -54,9 +55,11 @@ public class ButtonWidget : InputWidget protected Lazy tooltipContainer; + [TranslationReference] public string TooltipText; public Func GetTooltipText; + [TranslationReference] public string TooltipDesc; public Func GetTooltipDesc; @@ -74,7 +77,11 @@ public ButtonWidget(ModData modData) { ModRules = modData.DefaultRules; - GetText = () => Text; + var textCache = new CachedTransform(s => !string.IsNullOrEmpty(s) ? TranslationProvider.GetString(s) : ""); + var tooltipTextCache = new CachedTransform(s => !string.IsNullOrEmpty(s) ? TranslationProvider.GetString(s) : ""); + var tooltipDescCache = new CachedTransform(s => !string.IsNullOrEmpty(s) ? TranslationProvider.GetString(s) : ""); + + GetText = () => textCache.Update(Text); GetColor = () => TextColor; GetColorDisabled = () => TextColorDisabled; GetContrastColorDark = () => ContrastColorDark; @@ -82,8 +89,8 @@ public ButtonWidget(ModData modData) OnMouseUp = _ => OnClick(); OnKeyPress = _ => OnClick(); IsHighlighted = () => Highlighted; - GetTooltipText = () => TooltipText; - GetTooltipDesc = () => TooltipDesc; + GetTooltipText = () => tooltipTextCache.Update(TooltipText); + GetTooltipDesc = () => tooltipDescCache.Update(TooltipDesc); tooltipContainer = Exts.Lazy(() => Ui.Root.Get(TooltipContainer)); } diff --git a/OpenRA.Mods.Common/Widgets/ExponentialSliderWidget.cs b/OpenRA.Mods.Common/Widgets/ExponentialSliderWidget.cs index 89c47bd54059..cd5fd013547a 100644 --- a/OpenRA.Mods.Common/Widgets/ExponentialSliderWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ExponentialSliderWidget.cs @@ -19,8 +19,7 @@ public class ExponentialSliderWidget : SliderWidget public double ExpA = 1.0e-3; public double ExpB = 6.908; - public ExponentialSliderWidget() - : base() { } + public ExponentialSliderWidget() { } public ExponentialSliderWidget(ExponentialSliderWidget other) : base(other) { } diff --git a/OpenRA.Mods.Common/Widgets/HotkeyEntryWidget.cs b/OpenRA.Mods.Common/Widgets/HotkeyEntryWidget.cs index 440b4c6f224e..b6aa9a0ed395 100644 --- a/OpenRA.Mods.Common/Widgets/HotkeyEntryWidget.cs +++ b/OpenRA.Mods.Common/Widgets/HotkeyEntryWidget.cs @@ -44,11 +44,6 @@ protected HotkeyEntryWidget(HotkeyEntryWidget widget) VisualHeight = widget.VisualHeight; } - public override bool TakeKeyboardFocus() - { - return base.TakeKeyboardFocus(); - } - public override bool YieldKeyboardFocus() { OnLoseFocus(); diff --git a/OpenRA.Mods.Common/Widgets/ImageWidget.cs b/OpenRA.Mods.Common/Widgets/ImageWidget.cs index 1046d3cd2551..255808108736 100644 --- a/OpenRA.Mods.Common/Widgets/ImageWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ImageWidget.cs @@ -27,6 +27,7 @@ public class ImageWidget : Widget public Func GetImageCollection; public Func GetSprite; + [TranslationReference] public string TooltipText; readonly Lazy tooltipContainer; @@ -39,7 +40,8 @@ public ImageWidget() { GetImageName = () => ImageName; GetImageCollection = () => ImageCollection; - GetTooltipText = () => TooltipText; + var tooltipCache = new CachedTransform(s => !string.IsNullOrEmpty(s) ? TranslationProvider.GetString(s) : ""); + GetTooltipText = () => tooltipCache.Update(TooltipText); tooltipContainer = Exts.Lazy(() => Ui.Root.Get(TooltipContainer)); diff --git a/OpenRA.Mods.Common/Widgets/LabelForInputWidget.cs b/OpenRA.Mods.Common/Widgets/LabelForInputWidget.cs index 940355b0d008..9ac4c36049e4 100644 --- a/OpenRA.Mods.Common/Widgets/LabelForInputWidget.cs +++ b/OpenRA.Mods.Common/Widgets/LabelForInputWidget.cs @@ -24,8 +24,8 @@ public class LabelForInputWidget : LabelWidget readonly CachedTransform textColor; [ObjectCreator.UseCtor] - public LabelForInputWidget() - : base() + public LabelForInputWidget(ModData modData) + : base(modData) { inputWidget = Exts.Lazy(() => Parent.Get(For)); textColor = new CachedTransform(disabled => disabled ? TextDisabledColor : TextColor); diff --git a/OpenRA.Mods.Common/Widgets/LabelWidget.cs b/OpenRA.Mods.Common/Widgets/LabelWidget.cs index 1444b2dadb4b..e98dbb973d76 100644 --- a/OpenRA.Mods.Common/Widgets/LabelWidget.cs +++ b/OpenRA.Mods.Common/Widgets/LabelWidget.cs @@ -21,6 +21,7 @@ public enum TextVAlign { Top, Middle, Bottom } public class LabelWidget : Widget { + [TranslationReference] public string Text = null; public TextAlign Align = TextAlign.Left; public TextVAlign VAlign = TextVAlign.Middle; @@ -37,9 +38,11 @@ public class LabelWidget : Widget public Func GetContrastColorDark; public Func GetContrastColorLight; - public LabelWidget() + [ObjectCreator.UseCtor] + public LabelWidget(ModData modData) { - GetText = () => Text; + var textCache = new CachedTransform(s => !string.IsNullOrEmpty(s) ? TranslationProvider.GetString(s) : ""); + GetText = () => textCache.Update(Text); GetColor = () => TextColor; GetContrastColorDark = () => ContrastColorDark; GetContrastColorLight = () => ContrastColorLight; diff --git a/OpenRA.Mods.Common/Widgets/LabelWithHighlightWidget.cs b/OpenRA.Mods.Common/Widgets/LabelWithHighlightWidget.cs index 932d1e921fc8..9914c851858c 100644 --- a/OpenRA.Mods.Common/Widgets/LabelWithHighlightWidget.cs +++ b/OpenRA.Mods.Common/Widgets/LabelWithHighlightWidget.cs @@ -23,8 +23,8 @@ public class LabelWithHighlightWidget : LabelWidget readonly CachedTransform textComponents; [ObjectCreator.UseCtor] - public LabelWithHighlightWidget() - : base() + public LabelWithHighlightWidget(ModData modData) + : base(modData) { textComponents = new CachedTransform(MakeComponents); } diff --git a/OpenRA.Mods.Common/Widgets/LabelWithTooltipWidget.cs b/OpenRA.Mods.Common/Widgets/LabelWithTooltipWidget.cs index d740399f2f50..7b93a5050ba0 100644 --- a/OpenRA.Mods.Common/Widgets/LabelWithTooltipWidget.cs +++ b/OpenRA.Mods.Common/Widgets/LabelWithTooltipWidget.cs @@ -23,8 +23,8 @@ public class LabelWithTooltipWidget : LabelWidget public Func GetTooltipText = () => ""; [ObjectCreator.UseCtor] - public LabelWithTooltipWidget() - : base() + public LabelWithTooltipWidget(ModData modData) + : base(modData) { tooltipContainer = Exts.Lazy(() => Ui.Root.Get(TooltipContainer)); diff --git a/OpenRA.Mods.Common/Widgets/Logic/ButtonTooltipLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ButtonTooltipLogic.cs index e30b891b4375..65933d696cb3 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ButtonTooltipLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ButtonTooltipLogic.cs @@ -50,7 +50,7 @@ public ButtonTooltipLogic(Widget widget, ButtonWidget button) var descFont = Game.Renderer.Fonts[descTemplate.Font]; var descWidth = 0; var descOffset = descTemplate.Bounds.Y; - foreach (var line in desc.Split(new[] { "\\n" }, StringSplitOptions.None)) + foreach (var line in desc.Split(new[] { "\n" }, StringSplitOptions.None)) { descWidth = Math.Max(descWidth, descFont.Measure(line).X); var lineLabel = (LabelWidget)descTemplate.Clone(); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/ActorSelectorLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/ActorSelectorLogic.cs index 5e945aeac4f5..e285c4dabec2 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/ActorSelectorLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/ActorSelectorLogic.cs @@ -225,7 +225,6 @@ protected override void InitializePreviews() { Log.Write("debug", $"Map editor ignoring actor {actor.Name}, " + $"because of missing sprites for tileset {World.Map.Rules.TerrainInfo.Id}."); - continue; } } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs index a8debba51ddd..0b5566bb63dd 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs @@ -326,7 +326,7 @@ public static void SaveMapInner(Map map, IReadWritePackage package, World world, static void SaveMapFailed(Exception e, ModData modData, World world) { - Log.Write("debug", $"Failed to save map."); + Log.Write("debug", "Failed to save map."); Log.Write("debug", e); var actionManager = world.WorldActor.TraitOrDefault(); diff --git a/OpenRA.Mods.Common/Widgets/Logic/EncyclopediaLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/EncyclopediaLogic.cs index eec50c2e2217..c3d93b04f656 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/EncyclopediaLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/EncyclopediaLogic.cs @@ -146,7 +146,7 @@ void SelectActor(ActorInfo actor) var text = ""; - var buildable = actor.TraitInfoOrDefault(); + var buildable = actor.TraitInfos().FirstOrDefault(); if (buildable != null) { var prerequisites = buildable.Prerequisites diff --git a/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs index 65d4e8d10d88..6e689e1ff399 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs @@ -105,13 +105,8 @@ public GameSaveBrowserLogic(Widget widget, ModData modData, Action onExit, Actio defaultSaveFilename = world.Map.Title; var filenameAttempt = 0; - while (true) - { - if (!File.Exists(Path.Combine(baseSavePath, defaultSaveFilename + ".orasav"))) - break; - + while (File.Exists(Path.Combine(baseSavePath, defaultSaveFilename + ".orasav"))) defaultSaveFilename = world.Map.Title + $" ({++filenameAttempt})"; - } var saveButton = panel.Get("SAVE_BUTTON"); saveButton.IsDisabled = () => string.IsNullOrWhiteSpace(saveTextField.Text); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/ClassicProductionLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ClassicProductionLogic.cs index b09ce9ded800..d66b22be0950 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/ClassicProductionLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ClassicProductionLogic.cs @@ -42,7 +42,7 @@ void SelectTab(bool reverse) palette.PickUpCompletedBuilding(); } - button.IsDisabled = () => !queues.Any(q => q.BuildableItems().Any()); + button.IsDisabled = () => !queues.Any(q => q.BuildableItems().Any() || q.AlwaysVisible); button.OnMouseUp = mi => SelectTab(mi.Modifiers.HasModifier(Modifiers.Shift)); button.OnKeyPress = e => SelectTab(e.Modifiers.HasModifier(Modifiers.Shift)); button.OnClick = () => SelectTab(false); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameMenuLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameMenuLogic.cs index 44b863de005c..7f4886ff7d11 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameMenuLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameMenuLogic.cs @@ -148,7 +148,7 @@ public class IngameMenuLogic : ChromeLogic readonly Action onExit; readonly World world; readonly WorldRenderer worldRenderer; - readonly MenuPaletteEffect mpe; + readonly MenuPostProcessEffect mpe; readonly bool isSinglePlayer; readonly bool hasError; bool leaving; @@ -184,7 +184,7 @@ public IngameMenuLogic(Widget widget, ModData modData, World world, Action onExi isSinglePlayer = !world.LobbyInfo.GlobalSettings.Dedicated && world.LobbyInfo.NonBotClients.Count() == 1; menu = widget.Get("INGAME_MENU"); - mpe = world.WorldActor.TraitOrDefault(); + mpe = world.WorldActor.TraitOrDefault(); mpe?.Fade(mpe.Info.MenuEffect); menu.Get("VERSION_LABEL").Text = modData.Manifest.Metadata.Version; @@ -249,13 +249,13 @@ public static void OnQuit(World world) var iop = world.WorldActor.TraitsImplementing().FirstOrDefault(); var exitDelay = iop?.ExitDelay ?? 0; - var mpe = world.WorldActor.TraitOrDefault(); + var mpe = world.WorldActor.TraitOrDefault(); if (mpe != null) { Game.RunAfterDelay(exitDelay, () => { if (Game.IsCurrentWorld(world)) - mpe.Fade(MenuPaletteEffect.EffectType.Black); + mpe.Fade(MenuPostProcessEffect.EffectType.Black); }); exitDelay += 40 * mpe.Info.FadeLength; } @@ -280,7 +280,7 @@ void ShowMenu() void CloseMenu() { Ui.CloseWindow(); - mpe?.Fade(MenuPaletteEffect.EffectType.None); + mpe?.Fade(MenuPostProcessEffect.EffectType.None); onExit(); Ui.ResetTooltips(); } @@ -342,7 +342,7 @@ void OnRestart() if (mpe != null) { if (Game.IsCurrentWorld(world)) - mpe.Fade(MenuPaletteEffect.EffectType.Black); + mpe.Fade(MenuPostProcessEffect.EffectType.Black); exitDelay += 40 * mpe.Info.FadeLength; } @@ -536,7 +536,7 @@ void CreatePlayMapButton() Ui.ResetTooltips(); void CloseMenu() { - mpe?.Fade(MenuPaletteEffect.EffectType.None); + mpe?.Fade(MenuPostProcessEffect.EffectType.None); onExit(); } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/ProductionTabsLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ProductionTabsLogic.cs index aebe9eb3f72b..c7fb06ee5d68 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/ProductionTabsLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ProductionTabsLogic.cs @@ -34,7 +34,7 @@ void SelectTab(bool reverse) tabs.PickUpCompletedBuilding(); } - button.IsDisabled = () => !tabs.Groups[button.ProductionGroup].Tabs.Any(t => t.Queue.BuildableItems().Any()); + button.IsDisabled = () => !tabs.Groups[button.ProductionGroup].Tabs.Any(t => t.Queue.BuildableItems().Any() || t.Queue.AlwaysVisible); button.OnMouseUp = mi => SelectTab(mi.Modifiers.HasModifier(Modifiers.Shift)); button.OnKeyPress = e => SelectTab(e.Modifiers.HasModifier(Modifiers.Shift)); button.IsHighlighted = () => tabs.QueueGroup == button.ProductionGroup; diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/ProductionTooltipLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ProductionTooltipLogic.cs index 0014edf3fb94..2e4d0c3c924c 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/ProductionTooltipLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ProductionTooltipLogic.cs @@ -31,7 +31,7 @@ public ProductionTooltipLogic(Widget widget, TooltipContainerWidget tooltipConta var pm = player.PlayerActor.TraitOrDefault(); var pr = player.PlayerActor.Trait(); - widget.IsVisible = () => getTooltipIcon() != null && getTooltipIcon().Actor != null; + widget.IsVisible = () => getTooltipIcon() != null && getTooltipIcon().Actor != null && BuildableInfo.GetTraitForQueue(getTooltipIcon().Actor, getTooltipIcon().ProductionQueue?.Info.Type).ShowTooltip; var nameLabel = widget.Get("NAME"); var hotkeyLabel = widget.Get("HOTKEY"); var requiresLabel = widget.Get("REQUIRES"); @@ -70,7 +70,7 @@ public ProductionTooltipLogic(Widget widget, TooltipContainerWidget tooltipConta var tooltip = actor.TraitInfos().FirstOrDefault(info => info.EnabledByDefault); var name = tooltip?.Name ?? actor.Name; - var buildable = actor.TraitInfo(); + var buildable = BuildableInfo.GetTraitForQueue(actor, tooltipIcon.ProductionQueue?.Info.Type); var cost = 0; if (tooltipIcon.ProductionQueue != null) @@ -132,6 +132,8 @@ public ProductionTooltipLogic(Widget widget, TooltipContainerWidget tooltipConta timeLabel.Text = formatBuildTime.Update(buildTime * timeModifier / 100); timeLabel.TextColor = (pm != null && pm.PowerState != PowerState.Normal && tooltipIcon.ProductionQueue.Info.LowPowerModifier > 100) ? Color.Red : Color.White; var timeSize = font.Measure(timeLabel.Text); + costLabel.IsVisible = () => cost != 0; + costIcon.IsVisible = () => cost != 0; costLabel.Text = cost.ToString(NumberFormatInfo.CurrentInfo); costLabel.GetColor = () => pr.GetCashAndResources() >= cost ? Color.White : Color.Red; diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/SupportPowerTooltipLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/SupportPowerTooltipLogic.cs index 99ed5b509e38..f27130689bea 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/SupportPowerTooltipLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/SupportPowerTooltipLogic.cs @@ -10,6 +10,8 @@ #endregion using System; +using System.Globalization; +using System.Linq; using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; using OpenRA.Widgets; @@ -19,19 +21,23 @@ namespace OpenRA.Mods.Common.Widgets.Logic public class SupportPowerTooltipLogic : ChromeLogic { [ObjectCreator.UseCtor] - public SupportPowerTooltipLogic(Widget widget, TooltipContainerWidget tooltipContainer, Func getTooltipIcon, World world) + public SupportPowerTooltipLogic(Widget widget, TooltipContainerWidget tooltipContainer, Func getTooltipIcon, + World world, PlayerResources playerResources) { widget.IsVisible = () => getTooltipIcon() != null && getTooltipIcon().Power.Info != null; var nameLabel = widget.Get("NAME"); var hotkeyLabel = widget.Get("HOTKEY"); var timeLabel = widget.Get("TIME"); var descLabel = widget.Get("DESC"); + var costLabel = widget.Get("COST"); var nameFont = Game.Renderer.Fonts[nameLabel.Font]; var hotkeyFont = Game.Renderer.Fonts[hotkeyLabel.Font]; var timeFont = Game.Renderer.Fonts[timeLabel.Font]; var descFont = Game.Renderer.Fonts[descLabel.Font]; + var costFont = Game.Renderer.Fonts[costLabel.Font]; var baseHeight = widget.Bounds.Height; var timeOffset = timeLabel.Bounds.X; + var costOffset = costLabel.Bounds.X; SupportPowerInstance lastPower = null; var lastHotkey = Hotkey.Invalid; @@ -53,10 +59,19 @@ public SupportPowerTooltipLogic(Widget widget, TooltipContainerWidget tooltipCon if (sp == lastPower && hotkey == lastHotkey && lastRemainingSeconds == remainingSeconds) return; - nameLabel.Text = sp.Info.Name; + var cost = sp.Info.Cost; + var costString = costLabel.Text + cost.ToString(NumberFormatInfo.CurrentInfo); + costLabel.GetText = () => costString; + costLabel.GetColor = () => playerResources.Cash + playerResources.Resources >= cost + ? Color.White : Color.Red; + costLabel.Visible = cost != 0; + var costSize = costFont.Measure(costString); + + var level = sp.GetLevel(); + nameLabel.Text = sp.Info.Names.First(ld => ld.Key == level).Value; var nameSize = nameFont.Measure(nameLabel.Text); - descLabel.Text = sp.Info.Description.Replace("\\n", "\n"); + descLabel.Text = sp.Info.Descriptions.First(ld => ld.Key == level).Value.Replace("\\n", "\n"); var descSize = descFont.Measure(descLabel.Text); var customLabel = sp.TooltipTimeTextOverride(); @@ -82,11 +97,22 @@ public SupportPowerTooltipLogic(Widget widget, TooltipContainerWidget tooltipCon } var timeWidth = timeSize.X; + var costWidth = costSize.X; var topWidth = nameSize.X + hotkeyWidth + timeWidth + timeOffset; + + if (cost != 0) + topWidth += costWidth + costOffset; + widget.Bounds.Width = 2 * nameLabel.Bounds.X + Math.Max(topWidth, descSize.X); widget.Bounds.Height = baseHeight + descSize.Y; timeLabel.Bounds.X = widget.Bounds.Width - nameLabel.Bounds.X - timeWidth; + if (cost != 0) + { + timeLabel.Bounds.X -= costWidth + costOffset; + costLabel.Bounds.X = widget.Bounds.Width - nameLabel.Bounds.X - costWidth; + } + lastPower = sp; lastHotkey = hotkey; lastRemainingSeconds = remainingSeconds; diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index ff8e57a60066..b9e9e0cc1748 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -218,7 +218,7 @@ internal LobbyLogic(Widget widget, ModData modData, WorldRenderer worldRenderer, colorManager = modRules.Actors[SystemActors.World].TraitInfo(); foreach (var f in modRules.Actors[SystemActors.World].TraitInfos()) - factions.Add(f.InternalName, new LobbyFaction { Selectable = f.Selectable, Name = f.Name, Side = f.Side, Description = f.Description }); + factions.Add(f.InternalName, new LobbyFaction { Selectable = f.Selectable, Name = f.Name, Side = f.Side, Description = f.Description?.Replace(@"\n", "\n") }); var gameStarting = false; Func configurationDisabled = () => !Game.IsHost || gameStarting || diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyOptionsLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyOptionsLogic.cs index e56e9ef1d10b..25de1151c5bc 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyOptionsLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyOptionsLogic.cs @@ -107,7 +107,11 @@ void RebuildOptions() checkbox.GetText = () => option.Name; if (option.Description != null) - checkbox.GetTooltipText = () => option.Description; + { + var (text, desc) = LobbyUtils.SplitOnFirstToken(option.Description); + checkbox.GetTooltipText = () => text; + checkbox.GetTooltipDesc = () => desc; + } checkbox.IsVisible = () => true; checkbox.IsChecked = () => optionEnabled.Update(orderManager.LobbyInfo.GlobalSettings); @@ -148,7 +152,12 @@ void RebuildOptions() dropdown.GetText = () => getOptionLabel.Update(optionValue.Update(orderManager.LobbyInfo.GlobalSettings).Value); if (option.Description != null) - dropdown.GetTooltipText = () => option.Description; + { + var (text, desc) = LobbyUtils.SplitOnFirstToken(option.Description); + dropdown.GetTooltipText = () => text; + dropdown.GetTooltipDesc = () => desc; + } + dropdown.IsVisible = () => true; dropdown.IsDisabled = () => configurationDisabled() || optionValue.Update(orderManager.LobbyInfo.GlobalSettings).IsLocked; diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs index 4bc2ce552500..970ccb5ff728 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs @@ -200,7 +200,7 @@ ScrollItemWidget SetupItem(int ii, ScrollItemWidget itemTemplate) } /// Splits a string into two parts on the first instance of a given token. - static (string First, string Second) SplitOnFirstToken(string input, string token = "\\n") + public static (string First, string Second) SplitOnFirstToken(string input, string token = "\n") { if (string.IsNullOrEmpty(input)) return (null, null); diff --git a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs index 7864cc6fdd8c..61f6db1bd99d 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs @@ -323,11 +323,11 @@ void EnumerateMaps(MapClassification tab, ScrollItemWidget template) playerCountFilter = -1; var maps = tabMaps[tab] - .Where(m => category == null || m.Categories.Contains(category)) - .Where(m => mapFilter == null || + .Where(m => (category == null || m.Categories.Contains(category)) && + (mapFilter == null || (m.Title != null && m.Title.Contains(mapFilter, StringComparison.CurrentCultureIgnoreCase)) || (m.Author != null && m.Author.Contains(mapFilter, StringComparison.CurrentCultureIgnoreCase)) || - m.PlayerCount == playerCountFilter); + m.PlayerCount == playerCountFilter)); if (orderByFunc == null) maps = maps.OrderBy(m => m.Title); diff --git a/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs index 022460238496..546c8c56c0b6 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs @@ -17,6 +17,7 @@ using OpenRA.Graphics; using OpenRA.Mods.Common.Traits; using OpenRA.Network; +using OpenRA.Traits; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic @@ -24,6 +25,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic public class MissionBrowserLogic : ChromeLogic { enum PlayingVideo { None, Info, Briefing, GameStart } + enum PanelType { MissionInfo, Options } [TranslationReference] const string NoVideoTitle = "dialog-no-video.title"; @@ -43,16 +45,15 @@ enum PlayingVideo { None, Info, Briefing, GameStart } [TranslationReference] const string CantPlayCancel = "dialog-cant-play-video.cancel"; - [TranslationReference] - const string DifficultyNormal = "options-difficulty.normal"; - readonly ModData modData; readonly Action onStart; + readonly Widget missionDetail; + readonly Widget optionsContainer; + readonly Widget checkboxRowTemplate; + readonly Widget dropdownRowTemplate; readonly ScrollPanelWidget descriptionPanel; readonly LabelWidget description; readonly SpriteFont descriptionFont; - readonly DropDownButtonWidget difficultyButton; - readonly DropDownButtonWidget gameSpeedButton; readonly ButtonWidget startBriefingVideoButton; readonly ButtonWidget stopBriefingVideoButton; readonly ButtonWidget startInfoVideoButton; @@ -66,9 +67,8 @@ enum PlayingVideo { None, Info, Briefing, GameStart } MapPreview selectedMap; PlayingVideo playingVideo; - - string difficulty; - string gameSpeed; + readonly Dictionary missionOptions = new(); + PanelType panel = PanelType.MissionInfo; [ObjectCreator.UseCtor] public MissionBrowserLogic(Widget widget, ModData modData, World world, Action onStart, Action onExit, string initialMap) @@ -84,7 +84,10 @@ public MissionBrowserLogic(Widget widget, ModData modData, World world, Action o var title = widget.GetOrNull("MISSIONBROWSER_TITLE"); if (title != null) - title.GetText = () => playingVideo != PlayingVideo.None ? selectedMap.Title : title.Text; + { + var label = TranslationProvider.GetString(title.Text); + title.GetText = () => playingVideo != PlayingVideo.None ? selectedMap.Title : label; + } widget.Get("MISSION_INFO").IsVisible = () => selectedMap != null; @@ -96,13 +99,18 @@ public MissionBrowserLogic(Widget widget, ModData modData, World world, Action o widget.Get("MISSION_BIN").IsVisible = () => playingVideo != PlayingVideo.None; fullscreenVideoPlayer = Ui.LoadWidget("FULLSCREEN_PLAYER", Ui.Root, new WidgetArgs { { "world", world } }); - descriptionPanel = widget.Get("MISSION_DESCRIPTION_PANEL"); + missionDetail = widget.Get("MISSION_DETAIL"); + + descriptionPanel = missionDetail.Get("MISSION_DESCRIPTION_PANEL"); + descriptionPanel.IsVisible = () => panel == PanelType.MissionInfo; description = descriptionPanel.Get("MISSION_DESCRIPTION"); descriptionFont = Game.Renderer.Fonts[description.Font]; - difficultyButton = widget.Get("DIFFICULTY_DROPDOWNBUTTON"); - gameSpeedButton = widget.GetOrNull("GAMESPEED_DROPDOWNBUTTON"); + optionsContainer = missionDetail.Get("MISSION_OPTIONS"); + optionsContainer.IsVisible = () => panel == PanelType.Options; + checkboxRowTemplate = optionsContainer.Get("CHECKBOX_ROW_TEMPLATE"); + dropdownRowTemplate = optionsContainer.Get("DROPDOWN_ROW_TEMPLATE"); startBriefingVideoButton = widget.Get("START_BRIEFING_VIDEO_BUTTON"); stopBriefingVideoButton = widget.Get("STOP_BRIEFING_VIDEO_BUTTON"); @@ -191,6 +199,19 @@ public MissionBrowserLogic(Widget widget, ModData modData, World world, Action o Ui.CloseWindow(); onExit(); }; + + var tabContainer = widget.Get("MISSION_TABS"); + tabContainer.IsVisible = () => true; + + var optionsTab = tabContainer.Get("OPTIONS_TAB"); + optionsTab.IsHighlighted = () => panel == PanelType.Options; + optionsTab.IsDisabled = () => false; + optionsTab.OnClick = () => panel = PanelType.Options; + + var missionTab = tabContainer.Get("MISSIONINFO_TAB"); + missionTab.IsHighlighted = () => panel == PanelType.MissionInfo; + missionTab.IsDisabled = () => false; + missionTab.OnClick = () => panel = PanelType.MissionInfo; } void OnGameStart() @@ -238,28 +259,16 @@ void SelectMap(MapPreview preview) { selectedMap = preview; - // Cache the rules on a background thread to avoid jank - var difficultyDisabled = true; - var difficulties = new Dictionary(); - var briefingVideo = ""; var briefingVideoVisible = false; var infoVideo = ""; var infoVideoVisible = false; + panel = PanelType.MissionInfo; + new Thread(() => { - var mapDifficulty = preview.WorldActorInfo.TraitInfos() - .FirstOrDefault(sld => sld.ID == "difficulty"); - - if (mapDifficulty != null) - { - difficulty = mapDifficulty.Default; - difficulties = mapDifficulty.Values; - difficultyDisabled = mapDifficulty.Locked; - } - var missionData = preview.WorldActorInfo.TraitInfoOrDefault(); if (missionData != null) { @@ -291,58 +300,116 @@ void SelectMap(MapPreview preview) descriptionPanel.ScrollToTop(); - if (difficultyButton != null) + RebuildOptions(); + } + + void RebuildOptions() + { + if (selectedMap == null || selectedMap.WorldActorInfo == null) + return; + + missionOptions.Clear(); + optionsContainer.RemoveChildren(); + + var allOptions = selectedMap.PlayerActorInfo.TraitInfos() + .Concat(selectedMap.WorldActorInfo.TraitInfos()) + .SelectMany(t => t.LobbyOptions(selectedMap)) + .Where(o => o.IsVisible) + .OrderBy(o => o.DisplayOrder).ToArray(); + + Widget row = null; + var checkboxColumns = new Queue(); + var dropdownColumns = new Queue(); + + var yOffset = 0; + foreach (var option in allOptions.Where(o => o is LobbyBooleanOption)) { - var difficultyName = new CachedTransform(id => preview.GetLocalisedString( - id == null || !difficulties.ContainsKey(id) ? DifficultyNormal : difficulties[id])); + missionOptions[option.Id] = option.DefaultValue; - difficultyButton.IsDisabled = () => difficultyDisabled; - difficultyButton.GetText = () => difficultyName.Update(difficulty); - difficultyButton.OnMouseDown = _ => + if (checkboxColumns.Count == 0) { - var options = difficulties.Select(kv => new DropDownOption - { - Title = preview.GetLocalisedString(kv.Value), - IsSelected = () => difficulty == kv.Key, - OnClick = () => difficulty = kv.Key - }); + row = checkboxRowTemplate.Clone(); + row.Bounds.Y = yOffset; + yOffset += row.Bounds.Height; + foreach (var child in row.Children) + if (child is CheckboxWidget childCheckbox) + checkboxColumns.Enqueue(childCheckbox); + + optionsContainer.AddChild(row); + } - ScrollItemWidget SetupItem(DropDownOption option, ScrollItemWidget template) - { - var item = ScrollItemWidget.Setup(template, option.IsSelected, option.OnClick); - item.Get("LABEL").GetText = () => option.Title; - return item; - } + var checkbox = checkboxColumns.Dequeue(); + + checkbox.GetText = () => option.Name; + if (option.Description != null) + { + var (text, desc) = LobbyUtils.SplitOnFirstToken(option.Description); + checkbox.GetTooltipText = () => text; + checkbox.GetTooltipDesc = () => desc; + } - difficultyButton.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count() * 30, options, SetupItem); + checkbox.IsVisible = () => true; + checkbox.IsChecked = () => missionOptions[option.Id] == "True"; + checkbox.IsDisabled = () => option.IsLocked; + checkbox.OnClick = () => + { + if (missionOptions[option.Id] == "True") + missionOptions[option.Id] = "False"; + else + missionOptions[option.Id] = "True"; }; } - if (gameSpeedButton != null) + foreach (var option in allOptions.Where(o => o is not LobbyBooleanOption)) { - var speeds = modData.Manifest.Get().Speeds; - gameSpeed = "default"; + missionOptions[option.Id] = option.DefaultValue; - var speedText = new CachedTransform(s => TranslationProvider.GetString(speeds[s].Name)); - gameSpeedButton.GetText = () => speedText.Update(gameSpeed); - gameSpeedButton.OnMouseDown = _ => + if (dropdownColumns.Count == 0) { - var options = speeds.Select(s => new DropDownOption - { - Title = TranslationProvider.GetString(s.Value.Name), - IsSelected = () => gameSpeed == s.Key, - OnClick = () => gameSpeed = s.Key - }); + row = dropdownRowTemplate.Clone(); + row.Bounds.Y = yOffset; + yOffset += row.Bounds.Height; + foreach (var child in row.Children) + if (child is DropDownButtonWidget dropDown) + dropdownColumns.Enqueue(dropDown); + + optionsContainer.AddChild(row); + } + + var dropdown = dropdownColumns.Dequeue(); - ScrollItemWidget SetupItem(DropDownOption option, ScrollItemWidget template) + dropdown.GetText = () => option.Values[missionOptions[option.Id]]; + if (option.Description != null) + { + var (text, desc) = LobbyUtils.SplitOnFirstToken(option.Description); + dropdown.GetTooltipText = () => text; + dropdown.GetTooltipDesc = () => desc; + } + + dropdown.IsVisible = () => true; + dropdown.IsDisabled = () => option.IsLocked; + + dropdown.OnMouseDown = _ => + { + ScrollItemWidget SetupItem(KeyValuePair c, ScrollItemWidget template) { - var item = ScrollItemWidget.Setup(template, option.IsSelected, option.OnClick); - item.Get("LABEL").GetText = () => option.Title; + bool IsSelected() => missionOptions[option.Id] == c.Key; + void OnClick() => missionOptions[option.Id] = c.Key; + + var item = ScrollItemWidget.Setup(template, IsSelected, OnClick); + item.Get("LABEL").GetText = () => c.Value; return item; } - gameSpeedButton.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count() * 30, options, SetupItem); + dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", option.Values.Count * 30, option.Values, SetupItem); }; + + var label = row.GetOrNull(dropdown.Id + "_DESC"); + if (label != null) + { + label.GetText = () => option.Name + ":"; + label.IsVisible = () => true; + } } } @@ -432,10 +499,10 @@ void StartMissionClicked(Action onExit) selectedMap = modData.MapCache[map]; var orders = new List(); - if (difficulty != null) - orders.Add(Order.Command($"option difficulty {difficulty}")); - orders.Add(Order.Command($"option gamespeed {gameSpeed}")); + foreach (var option in missionOptions) + orders.Add(Order.Command($"option {option.Key} {option.Value}")); + orders.Add(Order.Command($"state {Session.ClientState.Ready}")); var missionData = selectedMap.WorldActorInfo.TraitInfoOrDefault(); @@ -449,12 +516,5 @@ void StartMissionClicked(Action onExit) else Game.CreateAndStartLocalServer(selectedMap.Uid, orders); } - - sealed class DropDownOption - { - public string Title; - public Func IsSelected; - public Action OnClick; - } } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs index 7284d49a4e10..b6926cbdf38e 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs @@ -570,7 +570,6 @@ void RenameReplay(ReplayMetadata replay, string newFilenameWithoutExtension) catch (Exception ex) { Log.Write("debug", ex.ToString()); - return; } } @@ -688,7 +687,7 @@ void ApplyFilter() foreach (var replay in replays) replayState[replay].Visible = EvaluateReplayVisibility(replay); - if (selectedReplay == null || replayState[selectedReplay].Visible == false) + if (selectedReplay == null || !replayState[selectedReplay].Visible) SelectFirstVisibleReplay(); replayList.Layout.AdjustChildren(); diff --git a/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs index 2b6e7e89ace4..074450242fad 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs @@ -235,7 +235,7 @@ void CreateAndJoin() if (e.ErrorCode == 10048) message += "\n" + TranslationProvider.GetString(ServerCreationFailedPortUsed); else - message += $"\n" + TranslationProvider.GetString(ServerCreationFailedError, + message += "\n" + TranslationProvider.GetString(ServerCreationFailedError, Translation.Arguments("message", e.Message, "code", e.ErrorCode)); ConfirmationDialogs.ButtonPrompt(modData, ServerCreationFailedTitle, message, diff --git a/OpenRA.Mods.Common/Widgets/ObserverArmyIconsWidget.cs b/OpenRA.Mods.Common/Widgets/ObserverArmyIconsWidget.cs index d366726ff172..6c5a3aa2a1a8 100644 --- a/OpenRA.Mods.Common/Widgets/ObserverArmyIconsWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ObserverArmyIconsWidget.cs @@ -93,7 +93,7 @@ public override void Draw() var playerStatistics = stats.Update(player); var items = playerStatistics.Units.Values - .Where(u => u.Count > 0 && u.Icon != null) + .Where(u => u.Count > 0 && u.Icon != null && !u.Upgrade) .OrderBy(u => u.ProductionQueueOrder) .ThenBy(u => u.BuildPaletteOrder); diff --git a/OpenRA.Mods.Common/Widgets/ObserverProductionIconsWidget.cs b/OpenRA.Mods.Common/Widgets/ObserverProductionIconsWidget.cs index e62ff665d7e5..8b904b9c53cd 100644 --- a/OpenRA.Mods.Common/Widgets/ObserverProductionIconsWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ObserverProductionIconsWidget.cs @@ -137,7 +137,7 @@ public override void Draw() var rsi = actor.TraitInfo(); var icon = new Animation(world, rsi.GetImage(actor, faction)); - var bi = actor.TraitInfo(); + var bi = BuildableInfo.GetTraitForQueue(actor, queue.Info.Type); icon.Play(bi.Icon); var topLeftOffset = new float2(queueCol * (IconWidth + IconSpacing), 0); @@ -159,9 +159,9 @@ public override void Draw() productionIconsBounds.Add(rect); - var pios = queue.Actor.Owner.PlayerActor.TraitsImplementing(); + var pios = queue.Actor.World.ActorsWithTrait().Where(a => a.Actor.Owner == queue.Actor.Owner).Select(a => a.Trait).ToArray(); - foreach (var pio in pios.Where(p => p.IsOverlayActive(actor))) + foreach (var pio in pios.Where(p => p.IsOverlayActive(actor, queue.Actor))) WidgetUtils.DrawSpriteCentered(pio.Sprite, worldRenderer.Palette(pio.Palette), centerPosition + pio.Offset(iconSize), 0.5f); diff --git a/OpenRA.Mods.Common/Widgets/ObserverSupportPowerIconsWidget.cs b/OpenRA.Mods.Common/Widgets/ObserverSupportPowerIconsWidget.cs index a6afbc4fb701..97ecb276fa09 100644 --- a/OpenRA.Mods.Common/Widgets/ObserverSupportPowerIconsWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ObserverSupportPowerIconsWidget.cs @@ -95,7 +95,7 @@ public override void Draw() return; var powers = player.PlayerActor.Trait().Powers - .Where(x => !x.Value.Disabled) + .Where(x => !x.Value.Disabled && x.Value.GetLevel() != 0) .OrderBy(p => p.Value.Info.SupportPowerPaletteOrder) .Select((a, i) => new { a, i }) .ToList(); @@ -114,11 +114,12 @@ public override void Draw() foreach (var power in powers) { var item = power.a.Value; - if (item == null || item.Info == null || item.Info.Icon == null) + if (item == null || item.Info == null || item.Info.Icons == null) continue; + var level = item.GetLevel(); icon = new Animation(worldRenderer.World, item.Info.IconImage); - icon.Play(item.Info.Icon); + icon.Play(item.Info.Icons.First(i => i.Key == level).Value); var location = new float2(RenderBounds.Location) + new float2(power.i * (IconWidth + IconSpacing), 0); supportPowerIconsIcons.Add(new SupportPowersWidget.SupportPowerIcon { Power = item, Pos = location }); @@ -186,7 +187,7 @@ public override void Tick() lastIconIdx = i; TooltipIcon = supportPowerIconsIcons[i]; currentTooltipToken = tooltipContainer.Value.SetTooltip(TooltipTemplate, - new WidgetArgs() { { "world", worldRenderer.World }, { "player", GetPlayer() }, { "getTooltipIcon", GetTooltipIcon } }); + new WidgetArgs() { { "world", worldRenderer.World }, { "player", GetPlayer() }, { "getTooltipIcon", GetTooltipIcon }, { "playerResources", GetPlayer().PlayerActor.Trait() } }); return; } diff --git a/OpenRA.Mods.Common/Widgets/ProductionPaletteWidget.cs b/OpenRA.Mods.Common/Widgets/ProductionPaletteWidget.cs index d63a3af8bbd8..a9eabb83e6ed 100644 --- a/OpenRA.Mods.Common/Widgets/ProductionPaletteWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ProductionPaletteWidget.cs @@ -75,9 +75,11 @@ public enum ReadyTextStyleOptions { Solid, AlternatingColor, Blinking } public readonly bool DrawTime = true; - public readonly string ReadyText = ""; + [TranslationReference] + public string ReadyText = ""; - public readonly string HoldText = ""; + [TranslationReference] + public string HoldText = ""; public readonly string InfiniteSymbol = "\u221E"; @@ -176,7 +178,9 @@ public override void Initialize(WidgetArgs args) Game.Renderer.Fonts.TryGetValue(SymbolsFont, out symbolFont); iconOffset = 0.5f * IconSize.ToFloat2() + IconSpriteOffset; + HoldText = TranslationProvider.GetString(HoldText); holdOffset = iconOffset - overlayFont.Measure(HoldText) / 2; + ReadyText = TranslationProvider.GetString(ReadyText); readyOffset = iconOffset - overlayFont.Measure(ReadyText) / 2; if (ChromeMetrics.TryGet("InfiniteOffset", out infiniteOffset)) @@ -221,13 +225,24 @@ public IEnumerable AllBuildables if (CurrentQueue == null) return Enumerable.Empty(); - return CurrentQueue.AllItems().OrderBy(a => a.TraitInfo().BuildPaletteOrder); + return CurrentQueue.AllItems().OrderBy(a => BuildableInfo.GetTraitForQueue(a, CurrentQueue.Info.Type).GetBuildPaletteOrder(a, CurrentQueue)); } } public override void Tick() { - TotalIconCount = AllBuildables.Count(); + var forcedIcons = AllBuildables.Where(a => BuildableInfo.GetTraitForQueue(a, CurrentQueue.Info.Type).ForceIconLocation); + + var largestForcedIconOrder = 0; + if (forcedIcons.Any()) + largestForcedIconOrder = forcedIcons.Max(a => BuildableInfo.GetTraitForQueue(a, CurrentQueue.Info.Type).GetBuildPaletteOrder(a, CurrentQueue)); + + var totalIconCount = AllBuildables.Count(); + + if (largestForcedIconOrder > totalIconCount) + TotalIconCount = largestForcedIconOrder; + else + TotalIconCount = totalIconCount; if (CurrentQueue != null && !CurrentQueue.Actor.IsInWorld) CurrentQueue = null; @@ -322,9 +337,11 @@ bool HandleLeftClick(ProductionItem item, ProductionIcon icon, int handleCount, if (item != null && item.Paused) { // Resume a paused item + var notification = item.BuildableInfo.QueuedAudio ?? CurrentQueue.Info.QueuedAudio; + var textNotification = item.BuildableInfo.QueuedTextNotification ?? CurrentQueue.Info.QueuedTextNotification; Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null); - Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", CurrentQueue.Info.QueuedAudio, World.LocalPlayer.Faction.InternalName); - TextNotificationsManager.AddTransientLine(World.LocalPlayer, CurrentQueue.Info.QueuedTextNotification); + Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", notification, World.LocalPlayer.Faction.InternalName); + TextNotificationsManager.AddTransientLine(World.LocalPlayer, textNotification); World.IssueOrder(Order.PauseProduction(CurrentQueue.Actor, icon.Name, false)); return true; @@ -338,7 +355,7 @@ bool HandleLeftClick(ProductionItem item, ProductionIcon icon, int handleCount, Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null); var canQueue = CurrentQueue.CanQueue(buildable, out var notification, out var textNotification); - if (!CurrentQueue.AllQueued().Any()) + if (!canQueue || !CurrentQueue.AllQueued().Any()) { Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", notification, World.LocalPlayer.Faction.InternalName); TextNotificationsManager.AddTransientLine(World.LocalPlayer, textNotification); @@ -365,16 +382,20 @@ bool HandleRightClick(ProductionItem item, ProductionIcon icon, int handleCount) if (CurrentQueue.Info.DisallowPaused || item.Paused || item.Done || item.TotalCost == item.RemainingCost) { // Instantly cancel items that haven't started, have finished, or if the queue doesn't support pausing - Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", CurrentQueue.Info.CancelledAudio, World.LocalPlayer.Faction.InternalName); - TextNotificationsManager.AddTransientLine(World.LocalPlayer, CurrentQueue.Info.CancelledTextNotification); + var notification = item.BuildableInfo.CancelledAudio ?? CurrentQueue.Info.CancelledAudio; + var textNotification = item.BuildableInfo.CancelledTextNotification ?? CurrentQueue.Info.CancelledTextNotification; + Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", notification, World.LocalPlayer.Faction.InternalName); + TextNotificationsManager.AddTransientLine(World.LocalPlayer, textNotification); World.IssueOrder(Order.CancelProduction(CurrentQueue.Actor, icon.Name, handleCount)); } else { // Pause an existing item - Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", CurrentQueue.Info.OnHoldAudio, World.LocalPlayer.Faction.InternalName); - TextNotificationsManager.AddTransientLine(World.LocalPlayer, CurrentQueue.Info.OnHoldTextNotification); + var notification = item.BuildableInfo.OnHoldAudio ?? CurrentQueue.Info.OnHoldAudio; + var textNotification = item.BuildableInfo.OnHoldTextNotification ?? CurrentQueue.Info.OnHoldTextNotification; + Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", notification, World.LocalPlayer.Faction.InternalName); + TextNotificationsManager.AddTransientLine(World.LocalPlayer, textNotification); World.IssueOrder(Order.PauseProduction(CurrentQueue.Actor, icon.Name, true)); } @@ -388,9 +409,11 @@ bool HandleMiddleClick(ProductionItem item, ProductionIcon icon, int handleCount return false; // Directly cancel, skipping "on-hold" + var notification = item.BuildableInfo.CancelledAudio ?? CurrentQueue.Info.CancelledAudio; + var textNotification = item.BuildableInfo.CancelledTextNotification ?? CurrentQueue.Info.CancelledTextNotification; Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null); - Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", CurrentQueue.Info.CancelledAudio, World.LocalPlayer.Faction.InternalName); - TextNotificationsManager.AddTransientLine(World.LocalPlayer, CurrentQueue.Info.CancelledTextNotification); + Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Speech", notification, World.LocalPlayer.Faction.InternalName); + TextNotificationsManager.AddTransientLine(World.LocalPlayer, textNotification); World.IssueOrder(Order.CancelProduction(CurrentQueue.Actor, icon.Name, handleCount)); @@ -455,7 +478,7 @@ bool SelectProductionBuilding() void UpdateCachedProductionIconOverlays() { cachedQueueOwner = CurrentQueue.Actor.Owner; - pios = cachedQueueOwner.PlayerActor.TraitsImplementing().ToArray(); + pios = cachedQueueOwner.World.ActorsWithTrait().Where(a => a.Actor.Owner == cachedQueueOwner).Select(a => a.Trait).ToArray(); } public void RefreshIcons() @@ -481,13 +504,15 @@ public void RefreshIcons() foreach (var item in AllBuildables.Skip(IconRowOffset * Columns).Take(MaxIconRowOffset * Columns)) { - var x = DisplayedIconCount % Columns; - var y = DisplayedIconCount / Columns; + var bi = BuildableInfo.GetTraitForQueue(item, CurrentQueue.Info.Type); + var iconLocation = bi.ForceIconLocation ? bi.GetBuildPaletteOrder(item, CurrentQueue) : DisplayedIconCount; + + var x = iconLocation % Columns; + var y = iconLocation / Columns; var rect = new Rectangle(rb.X + x * (IconSize.X + IconMargin.X), rb.Y + y * (IconSize.Y + IconMargin.Y), IconSize.X, IconSize.Y); var rsi = item.TraitInfo(); var icon = new Animation(World, rsi.GetImage(item, faction)); - var bi = item.TraitInfo(); icon.Play(bi.Icon); var palette = bi.IconPaletteIsPlayerPalette ? bi.IconPalette + producer.Actor.Owner.InternalName : bi.IconPalette; @@ -496,7 +521,7 @@ public void RefreshIcons() { Actor = item, Name = item.Name, - Hotkey = DisplayedIconCount < HotkeyCount ? hotkeys[DisplayedIconCount] : null, + Hotkey = iconLocation < HotkeyCount ? hotkeys[iconLocation] : null, Sprite = icon.Image, Palette = worldRenderer.Palette(palette), IconClockPalette = worldRenderer.Palette(ClockPalette), @@ -506,8 +531,13 @@ public void RefreshIcons() ProductionQueue = currentQueue }; - icons.Add(rect, pi); - DisplayedIconCount++; + if (!icons.ContainsKey(rect)) + icons.Add(rect, pi); + + if (iconLocation > DisplayedIconCount) + DisplayedIconCount = iconLocation + 1; + else + DisplayedIconCount++; } eventBounds = icons.Keys.Union(); @@ -532,7 +562,7 @@ public override void Draw() WidgetUtils.DrawSpriteCentered(icon.Sprite, icon.Palette, icon.Pos + iconOffset); // Draw the ProductionIconOverlay's sprites - foreach (var pio in pios.Where(p => p.IsOverlayActive(icon.Actor))) + foreach (var pio in pios.Where(p => p.IsOverlayActive(icon.Actor, icon.ProductionQueue.Actor))) WidgetUtils.DrawSpriteCentered(pio.Sprite, worldRenderer.Palette(pio.Palette), icon.Pos + iconOffset + pio.Offset(IconSize)); // Build progress diff --git a/OpenRA.Mods.Common/Widgets/ProductionTabsWidget.cs b/OpenRA.Mods.Common/Widgets/ProductionTabsWidget.cs index e1f3e83e3f76..a4c8e0a5808b 100644 --- a/OpenRA.Mods.Common/Widgets/ProductionTabsWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ProductionTabsWidget.cs @@ -35,7 +35,7 @@ public class ProductionTabGroup public void Update(IEnumerable allQueues) { - var queues = allQueues.Where(q => q.Info.Group == Group).ToList(); + var queues = allQueues.Where(q => q.Info.Group == Group && (q.BuildableItems().Any() || q.AlwaysVisible)).ToList(); var tabs = new List(); var largestUsedName = 0; @@ -183,7 +183,7 @@ public ProductionQueue CurrentQueue public override void Draw() { - var tabs = Groups[queueGroup].Tabs.Where(t => t.Queue.BuildableItems().Any()).ToList(); + var tabs = Groups[queueGroup].Tabs.Where(t => t.Queue.BuildableItems().Any() || t.Queue.AlwaysVisible).ToList(); if (tabs.Count == 0) return; @@ -258,6 +258,15 @@ public void ActorChanged(Actor a) else if (!Groups[queueGroup].Tabs.Select(t => t.Queue).Contains(CurrentQueue)) SelectNextTab(false); } + else if (a.Info.HasTraitInfo()) + { + var allQueues = a.World.ActorsWithTrait() + .Where(p => p.Actor.Owner == p.Actor.World.LocalPlayer && p.Actor.IsInWorld && p.Trait.Enabled) + .Select(p => p.Trait).ToList(); + + foreach (var g in Groups.Values) + g.Update(allQueues); + } } public override void Tick() diff --git a/OpenRA.Mods.Common/Widgets/SupportPowerTimerWidget.cs b/OpenRA.Mods.Common/Widgets/SupportPowerTimerWidget.cs index e3517777b53e..6ac1baf16851 100644 --- a/OpenRA.Mods.Common/Widgets/SupportPowerTimerWidget.cs +++ b/OpenRA.Mods.Common/Widgets/SupportPowerTimerWidget.cs @@ -55,9 +55,10 @@ public override void Tick() texts = displayedPowers.Select(p => { + var level = p.GetLevel(); var self = p.Instances[0].Self; var time = WidgetUtils.FormatTime(p.RemainingTicks, false, self.World.Timestep); - var text = Format.FormatCurrent(self.Owner.PlayerName, p.Info.Name, time); + var text = Format.FormatCurrent(self.Owner.PlayerName, p.Info.Names.First(ld => ld.Key == level).Value, time); var playerColor = self.Owner.Color; if (Game.Settings.Game.UsePlayerStanceColors) diff --git a/OpenRA.Mods.Common/Widgets/SupportPowersWidget.cs b/OpenRA.Mods.Common/Widgets/SupportPowersWidget.cs index 2dc70f7364af..9a843ef52c55 100644 --- a/OpenRA.Mods.Common/Widgets/SupportPowersWidget.cs +++ b/OpenRA.Mods.Common/Widgets/SupportPowersWidget.cs @@ -22,9 +22,11 @@ namespace OpenRA.Mods.Common.Widgets { public class SupportPowersWidget : Widget { - public readonly string ReadyText = ""; + [TranslationReference] + public string ReadyText = ""; - public readonly string HoldText = ""; + [TranslationReference] + public string HoldText = ""; public readonly string OverlayFont = "TinyBold"; @@ -109,7 +111,10 @@ public override void Initialize(WidgetArgs args) overlayFont = Game.Renderer.Fonts[OverlayFont]; iconOffset = 0.5f * IconSize.ToFloat2() + IconSpriteOffset; + + HoldText = TranslationProvider.GetString(HoldText); holdOffset = iconOffset - overlayFont.Measure(HoldText) / 2; + ReadyText = TranslationProvider.GetString(ReadyText); readyOffset = iconOffset - overlayFont.Measure(ReadyText) / 2; clock = new Animation(worldRenderer.World, ClockAnimation); @@ -143,8 +148,12 @@ public void RefreshIcons() else rect = new Rectangle(rb.X, rb.Y + IconCount * (IconSize.Y + IconMargin), IconSize.X, IconSize.Y); + var level = p.GetLevel(); + if (level == 0) + continue; + icon = new Animation(worldRenderer.World, p.Info.IconImage); - icon.Play(p.Info.Icon); + icon.Play(p.Info.Icons.First(i => i.Key == level).Value); var power = new SupportPowerIcon() { @@ -256,7 +265,13 @@ public override void MouseEntered() return; tooltipContainer.Value.SetTooltip(TooltipTemplate, - new WidgetArgs() { { "world", worldRenderer.World }, { "player", spm.Self.Owner }, { "getTooltipIcon", GetTooltipIcon } }); + new WidgetArgs() + { + { "world", worldRenderer.World }, + { "player", spm.Self.Owner }, + { "getTooltipIcon", GetTooltipIcon }, + { "playerResources", worldRenderer.World.LocalPlayer.PlayerActor.Trait() } + }); } public override void MouseExited() diff --git a/OpenRA.Mods.Common/Widgets/WorldLabelWithTooltipWidget.cs b/OpenRA.Mods.Common/Widgets/WorldLabelWithTooltipWidget.cs index 44f0cc0c1752..afade0cee479 100644 --- a/OpenRA.Mods.Common/Widgets/WorldLabelWithTooltipWidget.cs +++ b/OpenRA.Mods.Common/Widgets/WorldLabelWithTooltipWidget.cs @@ -18,8 +18,8 @@ public class WorldLabelWithTooltipWidget : LabelWithTooltipWidget readonly World world; [ObjectCreator.UseCtor] - public WorldLabelWithTooltipWidget(World world) - : base() + public WorldLabelWithTooltipWidget(ModData modData, World world) + : base(modData) { this.world = world; } diff --git a/OpenRA.Mods.D2k/Activities/SwallowActor.cs b/OpenRA.Mods.D2k/Activities/SwallowActor.cs index e0980a2bf238..ce6b9562f3c7 100644 --- a/OpenRA.Mods.D2k/Activities/SwallowActor.cs +++ b/OpenRA.Mods.D2k/Activities/SwallowActor.cs @@ -42,6 +42,7 @@ sealed class SwallowActor : Activity public SwallowActor(Actor self, in Target target, Armament a, IFacing facing) { + ActivityType = ActivityType.Attack; this.target = target; this.facing = facing; armament = a; diff --git a/OpenRA.Mods.D2k/Traits/SpiceBloom.cs b/OpenRA.Mods.D2k/Traits/SpiceBloom.cs index ddc4cfd47b32..d71c7e5f8c25 100644 --- a/OpenRA.Mods.D2k/Traits/SpiceBloom.cs +++ b/OpenRA.Mods.D2k/Traits/SpiceBloom.cs @@ -41,6 +41,8 @@ public class SpiceBloomInfo : TraitInfo, IRenderActorPreviewSpritesInfo, Require [WeaponReference] public readonly string Weapon = null; + public readonly string WeaponName = "primary"; + [Desc("The amount of spice to expel.")] public readonly int[] Pieces = { 2, 12 }; @@ -139,7 +141,7 @@ void SeedResources(Actor self) CurrentMuzzleFacing = () => WAngle.Zero, DamageModifiers = self.TraitsImplementing() - .Select(a => a.GetFirepowerModifier()).ToArray(), + .Select(a => a.GetFirepowerModifier(info.WeaponName)).ToArray(), InaccuracyModifiers = self.TraitsImplementing() .Select(a => a.GetInaccuracyModifier()).ToArray(), diff --git a/OpenRA.Mods.D2k/Traits/World/BuildableTerrainLayer.cs b/OpenRA.Mods.D2k/Traits/World/BuildableTerrainLayer.cs index 0a4c30f5e540..d8c4cf405fd2 100644 --- a/OpenRA.Mods.D2k/Traits/World/BuildableTerrainLayer.cs +++ b/OpenRA.Mods.D2k/Traits/World/BuildableTerrainLayer.cs @@ -82,7 +82,7 @@ public void HitTile(CPos cell, int damage) if (world.ActorMap.GetActorsAt(cell).Any(a => a.TraitOrDefault() != null)) return; - strength[cell] = strength[cell] - damage; + strength[cell] -= damage; if (strength[cell] < 1) RemoveTile(cell); } diff --git a/OpenRA.Mods.D2k/Traits/World/D2kResourceRenderer.cs b/OpenRA.Mods.D2k/Traits/World/D2kResourceRenderer.cs index 53c21b428771..6c6c6763c4c7 100644 --- a/OpenRA.Mods.D2k/Traits/World/D2kResourceRenderer.cs +++ b/OpenRA.Mods.D2k/Traits/World/D2kResourceRenderer.cs @@ -39,7 +39,7 @@ public enum ClearSides : byte BottomLeft = 0x40, BottomRight = 0x80, - All = 0xFF + All = Left | Top | Right | Bottom | TopLeft | TopRight | BottomLeft | BottomRight } public static readonly Dictionary SpriteMap = new() @@ -130,7 +130,7 @@ ClearSides FindClearSides(CPos cell, string resourceType) return ret; } - protected override void UpdateRenderedSprite(CPos cell, RendererCellContents content) + public override void UpdateRenderedSprite(CPos cell, RendererCellContents content) { UpdateRenderedSpriteInner(cell, content); diff --git a/OpenRA.Mods.D2k/UtilityCommands/D2kMapImporter.cs b/OpenRA.Mods.D2k/UtilityCommands/D2kMapImporter.cs index 70d19fdbd262..0eabbd17fa52 100644 --- a/OpenRA.Mods.D2k/UtilityCommands/D2kMapImporter.cs +++ b/OpenRA.Mods.D2k/UtilityCommands/D2kMapImporter.cs @@ -18,7 +18,7 @@ namespace OpenRA.Mods.D2k.UtilityCommands { - public class D2kMapImporter + public sealed class D2kMapImporter { const int MapCordonWidth = 2; @@ -505,7 +505,7 @@ TerrainTile GetTile(int tileIndex) } // Get the first tileset template that contains the Frame ID of the original map's tile with the requested index - var template = tileSetsFromYaml.FirstOrDefault(x => ((DefaultTerrainTemplateInfo)x).Frames.Contains(tileIndex)); + var template = tileSetsFromYaml.Find(x => ((DefaultTerrainTemplateInfo)x).Frames.Contains(tileIndex)); // HACK: The arrakis.yaml tileset file seems to be missing some tiles, so just get a replacement for them // Also used for duplicate tiles that are taken from only tileset diff --git a/OpenRA.Platforms.Default/DefaultPlatform.cs b/OpenRA.Platforms.Default/DefaultPlatform.cs index 472705f24fe3..5c7bc2780e80 100644 --- a/OpenRA.Platforms.Default/DefaultPlatform.cs +++ b/OpenRA.Platforms.Default/DefaultPlatform.cs @@ -16,9 +16,9 @@ namespace OpenRA.Platforms.Default { public class DefaultPlatform : IPlatform { - public IPlatformWindow CreateWindow(Size size, WindowMode windowMode, float scaleModifier, int vertexBatchSize, int indexBatchSize, int videoDisplay, GLProfile profile, bool enableLegacyGL) + public IPlatformWindow CreateWindow(Size size, WindowMode windowMode, float scaleModifier, int vertexBatchSize, int indexBatchSize, int videoDisplay, GLProfile profile) { - return new Sdl2PlatformWindow(size, windowMode, scaleModifier, vertexBatchSize, indexBatchSize, videoDisplay, profile, enableLegacyGL); + return new Sdl2PlatformWindow(size, windowMode, scaleModifier, vertexBatchSize, indexBatchSize, videoDisplay, profile); } public ISoundEngine CreateSound(string device) diff --git a/OpenRA.Platforms.Default/DummySoundEngine.cs b/OpenRA.Platforms.Default/DummySoundEngine.cs index f3a9d0cb4079..33fda4823cf0 100644 --- a/OpenRA.Platforms.Default/DummySoundEngine.cs +++ b/OpenRA.Platforms.Default/DummySoundEngine.cs @@ -27,8 +27,6 @@ public SoundDevice[] AvailableDevices() return defaultDevices; } - public DummySoundEngine() { } - public ISoundSource AddSoundSourceFromMemory(byte[] data, int channels, int sampleBits, int sampleRate) { return new NullSoundSource(); diff --git a/OpenRA.Platforms.Default/OpenAlSoundEngine.cs b/OpenRA.Platforms.Default/OpenAlSoundEngine.cs index 13c4db364926..fd40e5be71b1 100644 --- a/OpenRA.Platforms.Default/OpenAlSoundEngine.cs +++ b/OpenRA.Platforms.Default/OpenAlSoundEngine.cs @@ -49,7 +49,10 @@ sealed class PoolSlot const int MaxInstancesPerFrame = 3; const int GroupDistance = 2730; const int GroupDistanceSqr = GroupDistance * GroupDistance; - const int PoolSize = 32; + + // https://github.com/kcat/openal-soft/issues/580 + // https://github.com/kcat/openal-soft/blob/b6aa73b26004afe63d83097f2f91ecda9bc25cb9/alc/alc.cpp#L3191-L3203 + const int PoolSize = 256; readonly Dictionary sourcePool = new(PoolSize); float volume = 1f; @@ -73,7 +76,7 @@ static string[] QueryDevices(string label, int type) var buffer = new List(); var offset = 0; - while (true) + do { var b = Marshal.ReadByte(devicesPtr, offset++); if (b != 0) @@ -85,11 +88,8 @@ static string[] QueryDevices(string label, int type) // A null indicates termination of that string, so add that to our list. devices.Add(Encoding.UTF8.GetString(buffer.ToArray())); buffer.Clear(); - - // Two successive nulls indicates the end of the list. - if (Marshal.ReadByte(devicesPtr, offset) == 0) - break; } + while (Marshal.ReadByte(devicesPtr, offset) != 0); // Two successive nulls indicates the end of the list. return devices.ToArray(); } diff --git a/OpenRA.Platforms.Default/OpenGL.cs b/OpenRA.Platforms.Default/OpenGL.cs index c204b01226bd..8b1e318a7d96 100644 --- a/OpenRA.Platforms.Default/OpenGL.cs +++ b/OpenRA.Platforms.Default/OpenGL.cs @@ -386,6 +386,10 @@ public delegate void VertexAttribPointer(int index, int size, int type, bool nor int stride, IntPtr pointer); public static VertexAttribPointer glVertexAttribPointer { get; private set; } + public delegate void VertexAttribIPointer(int index, int size, int type, + int stride, IntPtr pointer); + public static VertexAttribIPointer glVertexAttribIPointer { get; private set; } + public delegate void EnableVertexAttribArray(int index); public static EnableVertexAttribArray glEnableVertexAttribArray { get; private set; } @@ -442,6 +446,10 @@ public delegate void TexImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, IntPtr pixels); public static TexImage2D glTexImage2D { get; private set; } + public delegate void CopyTexImage2D(int target, int level, int internalFormat, + int x, int y, int width, int height, int border); + public static CopyTexImage2D glCopyTexImage2D { get; private set; } + public delegate void GetTexImage(int target, int level, int format, int type, IntPtr pixels); public static GetTexImage glGetTexImage { get; private set; } @@ -487,7 +495,7 @@ public delegate void FramebufferRenderbuffer(int target, int attachment, #endregion - public static void Initialize(bool preferLegacyProfile) + public static void Initialize() { try { @@ -504,7 +512,7 @@ public static void Initialize(bool preferLegacyProfile) throw new InvalidProgramException("Failed to initialize low-level OpenGL bindings. GPU information is not available.", e); } - if (!DetectGLFeatures(preferLegacyProfile)) + if (!DetectGLFeatures()) { WriteGraphicsLog("Unsupported OpenGL version: " + glGetString(GL_VERSION)); throw new InvalidProgramException("OpenGL Version Error: See graphics.log for details."); @@ -591,6 +599,7 @@ public static void Initialize(bool preferLegacyProfile) glDeleteBuffers = Bind("glDeleteBuffers"); glBindAttribLocation = Bind("glBindAttribLocation"); glVertexAttribPointer = Bind("glVertexAttribPointer"); + glVertexAttribIPointer = Bind("glVertexAttribIPointer"); glEnableVertexAttribArray = Bind("glEnableVertexAttribArray"); glDisableVertexAttribArray = Bind("glDisableVertexAttribArray"); glDrawArrays = Bind("glDrawArrays"); @@ -607,52 +616,33 @@ public static void Initialize(bool preferLegacyProfile) glBindTexture = Bind("glBindTexture"); glActiveTexture = Bind("glActiveTexture"); glTexImage2D = Bind("glTexImage2D"); + glCopyTexImage2D = Bind("glCopyTexImage2D"); glTexParameteri = Bind("glTexParameteri"); glTexParameterf = Bind("glTexParameterf"); - if (Profile != GLProfile.Legacy) + if (Profile != GLProfile.Embedded) { - if (Profile != GLProfile.Embedded) - { - glGetTexImage = Bind("glGetTexImage"); - glBindFragDataLocation = Bind("glBindFragDataLocation"); - } - else - { - glGetTexImage = null; - glBindFragDataLocation = null; - } - - glGenVertexArrays = Bind("glGenVertexArrays"); - glBindVertexArray = Bind("glBindVertexArray"); - glGenFramebuffers = Bind("glGenFramebuffers"); - glBindFramebuffer = Bind("glBindFramebuffer"); - glFramebufferTexture2D = Bind("glFramebufferTexture2D"); - glDeleteFramebuffers = Bind("glDeleteFramebuffers"); - glGenRenderbuffers = Bind("glGenRenderbuffers"); - glBindRenderbuffer = Bind("glBindRenderbuffer"); - glRenderbufferStorage = Bind("glRenderbufferStorage"); - glDeleteRenderbuffers = Bind("glDeleteRenderbuffers"); - glFramebufferRenderbuffer = Bind("glFramebufferRenderbuffer"); - glCheckFramebufferStatus = Bind("glCheckFramebufferStatus"); + glGetTexImage = Bind("glGetTexImage"); + glBindFragDataLocation = Bind("glBindFragDataLocation"); } else { - glGenVertexArrays = null; - glBindVertexArray = null; + glGetTexImage = null; glBindFragDataLocation = null; - glGetTexImage = Bind("glGetTexImage"); - glGenFramebuffers = Bind("glGenFramebuffersEXT"); - glBindFramebuffer = Bind("glBindFramebufferEXT"); - glFramebufferTexture2D = Bind("glFramebufferTexture2DEXT"); - glDeleteFramebuffers = Bind("glDeleteFramebuffersEXT"); - glGenRenderbuffers = Bind("glGenRenderbuffersEXT"); - glBindRenderbuffer = Bind("glBindRenderbufferEXT"); - glRenderbufferStorage = Bind("glRenderbufferStorageEXT"); - glDeleteRenderbuffers = Bind("glDeleteRenderbuffersEXT"); - glFramebufferRenderbuffer = Bind("glFramebufferRenderbufferEXT"); - glCheckFramebufferStatus = Bind("glCheckFramebufferStatusEXT"); } + + glGenVertexArrays = Bind("glGenVertexArrays"); + glBindVertexArray = Bind("glBindVertexArray"); + glGenFramebuffers = Bind("glGenFramebuffers"); + glBindFramebuffer = Bind("glBindFramebuffer"); + glFramebufferTexture2D = Bind("glFramebufferTexture2D"); + glDeleteFramebuffers = Bind("glDeleteFramebuffers"); + glGenRenderbuffers = Bind("glGenRenderbuffers"); + glBindRenderbuffer = Bind("glBindRenderbuffer"); + glRenderbufferStorage = Bind("glRenderbufferStorage"); + glDeleteRenderbuffers = Bind("glDeleteRenderbuffers"); + glFramebufferRenderbuffer = Bind("glFramebufferRenderbuffer"); + glCheckFramebufferStatus = Bind("glCheckFramebufferStatus"); } catch (Exception e) { @@ -666,7 +656,7 @@ static T Bind(string name) return (T)(object)Marshal.GetDelegateForFunctionPointer(SDL.SDL_GL_GetProcAddress(name), typeof(T)); } - public static bool DetectGLFeatures(bool preferLegacyProfile) + public static bool DetectGLFeatures() { var hasValidConfiguration = false; try @@ -703,15 +693,6 @@ public static bool DetectGLFeatures(bool preferLegacyProfile) var hasDebugMessagesCallback = SDL.SDL_GL_ExtensionSupported("GL_KHR_debug") == SDL.SDL_bool.SDL_TRUE; if (hasDebugMessagesCallback) Features |= GLFeatures.DebugMessagesCallback; - - if (preferLegacyProfile || (major == 2 && minor == 1) || (major == 3 && minor < 2)) - { - if (SDL.SDL_GL_ExtensionSupported("GL_EXT_framebuffer_object") == SDL.SDL_bool.SDL_TRUE) - { - hasValidConfiguration = true; - Profile = GLProfile.Legacy; - } - } } catch (Exception) { } @@ -738,14 +719,9 @@ public static void WriteGraphicsLog(string message) Log.Write("graphics", $"Shader Version: {glGetString(GL_SHADING_LANGUAGE_VERSION)}"); Log.Write("graphics", "Available extensions:"); - if (Profile != GLProfile.Legacy) - { - glGetIntegerv(GL_NUM_EXTENSIONS, out var extensionCount); - for (var i = 0; i < extensionCount; i++) - Log.Write("graphics", glGetStringi(GL_EXTENSIONS, (uint)i)); - } - else - Log.Write("graphics", glGetString(GL_EXTENSIONS)); + glGetIntegerv(GL_NUM_EXTENSIONS, out var extensionCount); + for (var i = 0; i < extensionCount; i++) + Log.Write("graphics", glGetStringi(GL_EXTENSIONS, (uint)i)); } public static void CheckGLError() diff --git a/OpenRA.Platforms.Default/Sdl2GraphicsContext.cs b/OpenRA.Platforms.Default/Sdl2GraphicsContext.cs index d2e56c97bd67..43bea04b868d 100644 --- a/OpenRA.Platforms.Default/Sdl2GraphicsContext.cs +++ b/OpenRA.Platforms.Default/Sdl2GraphicsContext.cs @@ -40,16 +40,13 @@ internal void InitializeOpenGL() if (SDL.SDL_GL_MakeCurrent(window.Window, context) < 0) throw new InvalidOperationException($"Can not bind OpenGL context. (Error: {SDL.SDL_GetError()})"); - OpenGL.Initialize(window.GLProfile == GLProfile.Legacy); + OpenGL.Initialize(); OpenGL.CheckGLError(); - if (OpenGL.Profile != GLProfile.Legacy) - { - OpenGL.glGenVertexArrays(1, out var vao); - OpenGL.CheckGLError(); - OpenGL.glBindVertexArray(vao); - OpenGL.CheckGLError(); - } + OpenGL.glGenVertexArrays(1, out var vao); + OpenGL.CheckGLError(); + OpenGL.glBindVertexArray(vao); + OpenGL.CheckGLError(); } public IVertexBuffer CreateVertexBuffer(int size) where T : struct diff --git a/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs b/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs index 99b2fe62a1ae..606df843f294 100644 --- a/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs +++ b/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs @@ -133,7 +133,7 @@ public GLProfile[] SupportedGLProfiles static extern IntPtr XFlush(IntPtr display); public Sdl2PlatformWindow(Size requestEffectiveWindowSize, WindowMode windowMode, - float scaleModifier, int vertexBatchSize, int indexBatchSize, int videoDisplay, GLProfile requestProfile, bool enableLegacyGL) + float scaleModifier, int vertexBatchSize, int indexBatchSize, int videoDisplay, GLProfile requestProfile) { // Lock the Window/Surface properties until initialization is complete lock (syncObject) @@ -147,9 +147,6 @@ public Sdl2PlatformWindow(Size requestEffectiveWindowSize, WindowMode windowMode // Decide which OpenGL profile to use. // Prefer standard GL over GLES provided by the native driver var testProfiles = new List { GLProfile.ANGLE, GLProfile.Modern, GLProfile.Embedded }; - if (enableLegacyGL) - testProfiles.Add(GLProfile.Legacy); - var errorLog = new List(); supportedProfiles = testProfiles .Where(profile => CanCreateGLWindow(profile, errorLog)) @@ -163,7 +160,7 @@ public Sdl2PlatformWindow(Size requestEffectiveWindowSize, WindowMode windowMode throw new InvalidOperationException("No supported OpenGL profiles were found."); } - profile = supportedProfiles.Contains(requestProfile) ? requestProfile : supportedProfiles.First(); + profile = supportedProfiles.Contains(requestProfile) ? requestProfile : supportedProfiles[0]; // Note: This must be called after the CanCreateGLWindow checks above, // which needs to create and destroy its own SDL contexts as a workaround for specific buggy drivers @@ -537,10 +534,6 @@ static void SetSDLAttributes(GLProfile profile) SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 0); SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL.SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_ES); break; - case GLProfile.Legacy: - SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 2); - SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 1); - break; } } diff --git a/OpenRA.Platforms.Default/Shader.cs b/OpenRA.Platforms.Default/Shader.cs index 5705597332d0..4e0c0223aac8 100644 --- a/OpenRA.Platforms.Default/Shader.cs +++ b/OpenRA.Platforms.Default/Shader.cs @@ -13,13 +13,13 @@ using System.Collections.Generic; using System.IO; using System.Text; +using OpenRA.Graphics; namespace OpenRA.Platforms.Default { sealed class Shader : ThreadAffine, IShader { readonly Dictionary samplers = new(); - readonly Dictionary legacySizeUniforms = new(); readonly Dictionary uniformCache = new(); readonly Dictionary textures = new(); readonly Queue unbindTextures = new(); @@ -28,8 +28,7 @@ sealed class Shader : ThreadAffine, IShader static uint CompileShaderObject(int type, string code, string name) { - var version = OpenGL.Profile == GLProfile.Embedded ? "300 es" : - OpenGL.Profile == GLProfile.Legacy ? "120" : "140"; + var version = OpenGL.Profile == GLProfile.Embedded ? "300 es" : "140"; code = code.Replace("{VERSION}", version); @@ -127,14 +126,6 @@ public Shader(IShaderBindings bindings) OpenGL.glUniform1i(loc, nextTexUnit); OpenGL.CheckGLError(); - - if (OpenGL.Profile == GLProfile.Legacy) - { - var sizeLoc = OpenGL.glGetUniformLocation(program, sampler + "Size"); - if (sizeLoc >= 0) - legacySizeUniforms.Add(nextTexUnit, sizeLoc); - } - nextTexUnit++; } } @@ -145,7 +136,10 @@ public void Bind() for (ushort i = 0; i < bindings.Attributes.Length; i++) { var attribute = bindings.Attributes[i]; - OpenGL.glVertexAttribPointer(i, attribute.Components, OpenGL.GL_FLOAT, false, bindings.Stride, new IntPtr(attribute.Offset)); + if (attribute.Type == ShaderVertexAttributeType.Float) + OpenGL.glVertexAttribPointer(i, attribute.Components, OpenGL.GL_FLOAT, false, bindings.Stride, new IntPtr(attribute.Offset)); + else + OpenGL.glVertexAttribIPointer(i, attribute.Components, (int)attribute.Type, bindings.Stride, new IntPtr(attribute.Offset)); OpenGL.CheckGLError(); } } @@ -166,13 +160,6 @@ public void PrepareRender() { OpenGL.glActiveTexture(OpenGL.GL_TEXTURE0 + kv.Key); OpenGL.glBindTexture(OpenGL.GL_TEXTURE_2D, texture.ID); - - // Work around missing textureSize GLSL function by explicitly tracking sizes in a uniform - if (OpenGL.Profile == GLProfile.Legacy && legacySizeUniforms.TryGetValue(kv.Key, out var param)) - { - OpenGL.glUniform2f(param, texture.Size.Width, texture.Size.Height); - OpenGL.CheckGLError(); - } } else unbindTextures.Enqueue(kv.Key); diff --git a/OpenRA.Platforms.Default/Texture.cs b/OpenRA.Platforms.Default/Texture.cs index 26523fbd4fac..f154281269e0 100644 --- a/OpenRA.Platforms.Default/Texture.cs +++ b/OpenRA.Platforms.Default/Texture.cs @@ -111,6 +111,19 @@ public void SetFloatData(float[] data, int width, int height) } } + public void SetDataFromReadBuffer(Rectangle rect) + { + VerifyThreadAffinity(); + if (!Exts.IsPowerOf2(rect.Width) || !Exts.IsPowerOf2(rect.Height)) + throw new InvalidDataException($"Non-power-of-two rectangle {rect.Width}x{rect.Height}"); + + PrepareTexture(); + + var glInternalFormat = OpenGL.Profile == GLProfile.Embedded ? OpenGL.GL_BGRA : OpenGL.GL_RGBA8; + OpenGL.glCopyTexImage2D(OpenGL.GL_TEXTURE_2D, 0, glInternalFormat, rect.X, rect.Y, rect.Width, rect.Height, 0); + OpenGL.CheckGLError(); + } + public byte[] GetData() { VerifyThreadAffinity(); diff --git a/OpenRA.Platforms.Default/ThreadedGraphicsContext.cs b/OpenRA.Platforms.Default/ThreadedGraphicsContext.cs index cc6c2494ee16..61b3449f099b 100644 --- a/OpenRA.Platforms.Default/ThreadedGraphicsContext.cs +++ b/OpenRA.Platforms.Default/ThreadedGraphicsContext.cs @@ -648,6 +648,7 @@ sealed class ThreadedTexture : ITextureInternal readonly Func setData2; readonly Action setData3; readonly Func setData4; + readonly Action setData5; readonly Action dispose; public ThreadedTexture(ThreadedGraphicsContext device, ITextureInternal texture) @@ -663,6 +664,7 @@ public ThreadedTexture(ThreadedGraphicsContext device, ITextureInternal texture) setData2 = tuple => { setData1(tuple); return null; }; setData3 = tuple => { var t = ((float[], int, int))tuple; texture.SetFloatData(t.Item1, t.Item2, t.Item3); }; setData4 = tuple => { setData3(tuple); return null; }; + setData5 = rect => texture.SetDataFromReadBuffer((Rectangle)rect); dispose = texture.Dispose; } @@ -725,6 +727,11 @@ public void SetFloatData(float[] data, int width, int height) } } + public void SetDataFromReadBuffer(Rectangle rect) + { + device.Post(setData5, rect); + } + public void Dispose() { device.Post(dispose); diff --git a/OpenRA.Test/OpenRA.Game/MiniYamlTest.cs b/OpenRA.Test/OpenRA.Game/MiniYamlTest.cs index 4223eca9ccbb..26dba5694ba1 100644 --- a/OpenRA.Test/OpenRA.Game/MiniYamlTest.cs +++ b/OpenRA.Test/OpenRA.Game/MiniYamlTest.cs @@ -544,11 +544,11 @@ public void TestMergeConflictsMultiSourceSecondParent() [TestCase(TestName = "Comments are correctly separated from values")] public void TestEscapedHashInValues() { - var trailingWhitespace = MiniYaml.FromString(@"key: value # comment", "trailingWhitespace", discardCommentsAndWhitespace: false)[0]; + var trailingWhitespace = MiniYaml.FromString("key: value # comment", "trailingWhitespace", discardCommentsAndWhitespace: false)[0]; Assert.AreEqual("value", trailingWhitespace.Value.Value); Assert.AreEqual(" comment", trailingWhitespace.Comment); - var noWhitespace = MiniYaml.FromString(@"key:value# comment", "noWhitespace", discardCommentsAndWhitespace: false)[0]; + var noWhitespace = MiniYaml.FromString("key:value# comment", "noWhitespace", discardCommentsAndWhitespace: false)[0]; Assert.AreEqual("value", noWhitespace.Value.Value); Assert.AreEqual(" comment", noWhitespace.Comment); @@ -556,15 +556,15 @@ public void TestEscapedHashInValues() Assert.AreEqual("before # after", escapedHashInValue.Value.Value); Assert.AreEqual(" comment", escapedHashInValue.Comment); - var emptyValueAndComment = MiniYaml.FromString(@"key:#", "emptyValueAndComment", discardCommentsAndWhitespace: false)[0]; + var emptyValueAndComment = MiniYaml.FromString("key:#", "emptyValueAndComment", discardCommentsAndWhitespace: false)[0]; Assert.AreEqual(null, emptyValueAndComment.Value.Value); Assert.AreEqual("", emptyValueAndComment.Comment); - var noValue = MiniYaml.FromString(@"key:", "noValue", discardCommentsAndWhitespace: false)[0]; + var noValue = MiniYaml.FromString("key:", "noValue", discardCommentsAndWhitespace: false)[0]; Assert.AreEqual(null, noValue.Value.Value); Assert.AreEqual(null, noValue.Comment); - var emptyKey = MiniYaml.FromString(@" : value", "emptyKey", discardCommentsAndWhitespace: false)[0]; + var emptyKey = MiniYaml.FromString(" : value", "emptyKey", discardCommentsAndWhitespace: false)[0]; Assert.AreEqual(null, emptyKey.Key); Assert.AreEqual("value", emptyKey.Value.Value); Assert.AreEqual(null, emptyKey.Comment); diff --git a/OpenRA.WindowsLauncher/Program.cs b/OpenRA.WindowsLauncher/Program.cs index db09a7584983..6c2220cf2d58 100644 --- a/OpenRA.WindowsLauncher/Program.cs +++ b/OpenRA.WindowsLauncher/Program.cs @@ -52,7 +52,7 @@ static int Main(string[] args) } } - if (args.Any(x => x.StartsWith("Engine.LaunchPath=", StringComparison.Ordinal))) + if (Array.Exists(args, x => x.StartsWith("Engine.LaunchPath=", StringComparison.Ordinal))) return RunGame(args); return RunInnerLauncher(args); @@ -89,10 +89,10 @@ static int RunInnerLauncher(string[] args) var launcherPath = Environment.ProcessPath; var launcherArgs = args.ToList(); - if (!launcherArgs.Any(x => x.StartsWith("Engine.LaunchPath=", StringComparison.Ordinal))) + if (!launcherArgs.Exists(x => x.StartsWith("Engine.LaunchPath=", StringComparison.Ordinal))) launcherArgs.Add("Engine.LaunchPath=\"" + launcherPath + "\""); - if (!launcherArgs.Any(x => x.StartsWith("Game.Mod=", StringComparison.Ordinal))) + if (!launcherArgs.Exists(x => x.StartsWith("Game.Mod=", StringComparison.Ordinal))) launcherArgs.Add("Game.Mod=" + modID); var psi = new ProcessStartInfo(launcherPath, string.Join(" ", launcherArgs)); diff --git a/OpenRA.sln b/OpenRA.sln index 7625a59d005a..cb9c4d5dd1ce 100644 --- a/OpenRA.sln +++ b/OpenRA.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28803.352 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2000 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenRA.Game", "OpenRA.Game\OpenRA.Game.csproj", "{0DFB103F-2962-400F-8C6D-E2C28CCBA633}" EndProject @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenRA.Server", "OpenRA.Ser EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenRA.Mods.D2k", "OpenRA.Mods.D2k\OpenRA.Mods.D2k.csproj", "{C0B0465C-6BE2-409C-8770-3A9BF64C4344}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenRA.Mods.AS", "OpenRA.Mods.AS\OpenRA.Mods.AS.csproj", "{6DEEE499-98E5-4977-AAAE-CEAE165F17CF}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenRA.Platforms.Default", "OpenRA.Platforms.Default\OpenRA.Platforms.Default.csproj", "{33D03738-C154-4028-8EA8-63A3C488A651}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenRA.Mods.Common", "OpenRA.Mods.Common\OpenRA.Mods.Common.csproj", "{FE6C8CC0-2F07-442A-B29F-17617B3B7FC6}" @@ -49,6 +51,10 @@ Global {C0B0465C-6BE2-409C-8770-3A9BF64C4344}.Debug|Any CPU.Build.0 = Debug|Any CPU {C0B0465C-6BE2-409C-8770-3A9BF64C4344}.Release|Any CPU.ActiveCfg = Release|Any CPU {C0B0465C-6BE2-409C-8770-3A9BF64C4344}.Release|Any CPU.Build.0 = Release|Any CPU + {6DEEE499-98E5-4977-AAAE-CEAE165F17CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DEEE499-98E5-4977-AAAE-CEAE165F17CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DEEE499-98E5-4977-AAAE-CEAE165F17CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DEEE499-98E5-4977-AAAE-CEAE165F17CF}.Release|Any CPU.Build.0 = Release|Any CPU {33D03738-C154-4028-8EA8-63A3C488A651}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {33D03738-C154-4028-8EA8-63A3C488A651}.Debug|Any CPU.Build.0 = Debug|Any CPU {33D03738-C154-4028-8EA8-63A3C488A651}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/README.md b/README.md index 06dc2725f003..f5c771e3dae4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ + +# This repository is a customized soft-fork of OpenRA aimed towards Attacque Supérior. + +This repository was made under the guidance of the recent OpenRA soft-fork approving policy and intends to maintain bleed-compatibility as much as possible with upstream OpenRA. All code alterations are under the GPLv3 license and probably will be evaluated by StyleCop to maintain OpenRA coding quality. + +This repository's aim is to be used by other OpenRA modders for their advancements as well. + +* Websites: [http://attsup.swr-productions.com](http://attsup.swr-productions.com) and [http://www.moddb.com/mods/attacque-suprior](http://www.moddb.com/mods/attacque-suprior) +* Discord: [https://discord.gg/7aM7Hm2](https://discord.gg/7aM7Hm2) + +Below you can find the original OpenRA readme unedited. + +*** + # OpenRA A Libre/Free Real Time Strategy game engine supporting early Westwood classics. @@ -46,7 +60,7 @@ Check our [Playing the Game](https://github.com/OpenRA/OpenRA/wiki/Playing-the-g ## Support * Sponsor a [mirror server](https://github.com/OpenRA/OpenRAWebsiteV3/tree/master/packages) if you have some bandwidth to spare. -* You can immediately set up a [Dedicated](https://github.com/OpenRA/OpenRA/wiki/Dedicated) Game Server. +* You can immediately set up a [Dedicated](https://github.com/OpenRA/OpenRA/wiki/Dedicated-Server) Game Server. ## License Copyright (c) OpenRA Developers and Contributors diff --git a/glsl/combined.frag b/glsl/combined.frag index 5ae0a74b10f1..30fb5017f099 100644 --- a/glsl/combined.frag +++ b/glsl/combined.frag @@ -19,42 +19,16 @@ uniform vec2 DepthPreviewParams; uniform float DepthTextureScale; uniform float AntialiasPixelsPerTexel; -#if __VERSION__ == 120 -varying vec4 vTexCoord; -varying vec2 vTexMetadata; -varying vec4 vChannelMask; -varying vec4 vDepthMask; -varying vec2 vTexSampler; - -varying vec4 vColorFraction; -varying vec4 vRGBAFraction; -varying vec4 vPalettedFraction; -varying vec4 vTint; - -uniform vec2 Texture0Size; -uniform vec2 Texture1Size; -uniform vec2 Texture2Size; -uniform vec2 Texture3Size; -uniform vec2 Texture4Size; -uniform vec2 Texture5Size; -uniform vec2 Texture6Size; -uniform vec2 Texture7Size; -#else -in vec4 vColor; - in vec4 vTexCoord; -in vec2 vTexMetadata; -in vec4 vChannelMask; -in vec4 vDepthMask; -in vec2 vTexSampler; - -in vec4 vColorFraction; -in vec4 vRGBAFraction; -in vec4 vPalettedFraction; +flat in float vTexPalette; +flat in vec4 vChannelMask; +flat in uint vChannelSampler; +flat in uint vChannelType; +flat in vec4 vDepthMask; +flat in uint vDepthSampler; in vec4 vTint; out vec4 fragColor; -#endif vec3 rgb2hsv(vec3 c) { @@ -99,89 +73,53 @@ vec4 linear2srgb(vec4 c) return c.a * vec4(linear2srgb(c.r / c.a), linear2srgb(c.g / c.a), linear2srgb(c.b / c.a), 1.0f); } -#if __VERSION__ == 120 -vec2 Size(float samplerIndex) +ivec2 Size(uint samplerIndex) { - if (samplerIndex < 0.5) - return Texture0Size; - else if (samplerIndex < 1.5) - return Texture1Size; - else if (samplerIndex < 2.5) - return Texture2Size; - else if (samplerIndex < 3.5) - return Texture3Size; - else if (samplerIndex < 4.5) - return Texture4Size; - else if (samplerIndex < 5.5) - return Texture5Size; - else if (samplerIndex < 6.5) - return Texture6Size; - - return Texture7Size; -} - -vec4 Sample(float samplerIndex, vec2 pos) -{ - if (samplerIndex < 0.5) - return texture2D(Texture0, pos); - else if (samplerIndex < 1.5) - return texture2D(Texture1, pos); - else if (samplerIndex < 2.5) - return texture2D(Texture2, pos); - else if (samplerIndex < 3.5) - return texture2D(Texture3, pos); - else if (samplerIndex < 4.5) - return texture2D(Texture4, pos); - else if (samplerIndex < 5.5) - return texture2D(Texture5, pos); - else if (samplerIndex < 6.5) - return texture2D(Texture6, pos); - - return texture2D(Texture7, pos); -} -#else -ivec2 Size(float samplerIndex) -{ - if (samplerIndex < 0.5) - return textureSize(Texture0, 0); - else if (samplerIndex < 1.5) - return textureSize(Texture1, 0); - else if (samplerIndex < 2.5) - return textureSize(Texture2, 0); - else if (samplerIndex < 3.5) - return textureSize(Texture3, 0); - else if (samplerIndex < 4.5) - return textureSize(Texture4, 0); - else if (samplerIndex < 5.5) - return textureSize(Texture5, 0); - else if (samplerIndex < 6.5) - return textureSize(Texture6, 0); - - return textureSize(Texture7, 0); + switch (samplerIndex) + { + case 7u: + return textureSize(Texture7, 0); + case 6u: + return textureSize(Texture6, 0); + case 5u: + return textureSize(Texture5, 0); + case 4u: + return textureSize(Texture4, 0); + case 3u: + return textureSize(Texture3, 0); + case 2u: + return textureSize(Texture2, 0); + case 1u: + return textureSize(Texture1, 0); + default: + return textureSize(Texture0, 0); + } } -vec4 Sample(float samplerIndex, vec2 pos) +vec4 Sample(uint samplerIndex, vec2 pos) { - if (samplerIndex < 0.5) - return texture(Texture0, pos); - else if (samplerIndex < 1.5) - return texture(Texture1, pos); - else if (samplerIndex < 2.5) - return texture(Texture2, pos); - else if (samplerIndex < 3.5) - return texture(Texture3, pos); - else if (samplerIndex < 4.5) - return texture(Texture4, pos); - else if (samplerIndex < 5.5) - return texture(Texture5, pos); - else if (samplerIndex < 6.5) - return texture(Texture6, pos); - - return texture(Texture7, pos); + switch (samplerIndex) + { + case 7u: + return texture(Texture7, pos); + case 6u: + return texture(Texture6, pos); + case 5u: + return texture(Texture5, pos); + case 4u: + return texture(Texture4, pos); + case 3u: + return texture(Texture3, pos); + case 2u: + return texture(Texture2, pos); + case 1u: + return texture(Texture1, pos); + default: + return texture(Texture0, pos); + } } -#endif -vec4 SamplePalettedBilinear(float samplerIndex, vec2 coords, vec2 textureSize) +vec4 SamplePalettedBilinear(uint samplerIndex, vec2 coords, vec2 textureSize) { vec2 texPos = (coords * textureSize) - vec2(0.5); vec2 interp = fract(texPos); @@ -193,30 +131,18 @@ vec4 SamplePalettedBilinear(float samplerIndex, vec2 coords, vec2 textureSize) vec4 x3 = Sample(samplerIndex, tl + vec2(0., px.y)); vec4 x4 = Sample(samplerIndex, tl + px); - #if __VERSION__ == 120 - vec4 c1 = texture2D(Palette, vec2(dot(x1, vChannelMask), vTexMetadata.s)); - vec4 c2 = texture2D(Palette, vec2(dot(x2, vChannelMask), vTexMetadata.s)); - vec4 c3 = texture2D(Palette, vec2(dot(x3, vChannelMask), vTexMetadata.s)); - vec4 c4 = texture2D(Palette, vec2(dot(x4, vChannelMask), vTexMetadata.s)); - #else - vec4 c1 = texture(Palette, vec2(dot(x1, vChannelMask), vTexMetadata.s)); - vec4 c2 = texture(Palette, vec2(dot(x2, vChannelMask), vTexMetadata.s)); - vec4 c3 = texture(Palette, vec2(dot(x3, vChannelMask), vTexMetadata.s)); - vec4 c4 = texture(Palette, vec2(dot(x4, vChannelMask), vTexMetadata.s)); - #endif + vec4 c1 = texture(Palette, vec2(dot(x1, vChannelMask), vTexPalette)); + vec4 c2 = texture(Palette, vec2(dot(x2, vChannelMask), vTexPalette)); + vec4 c3 = texture(Palette, vec2(dot(x3, vChannelMask), vTexPalette)); + vec4 c4 = texture(Palette, vec2(dot(x4, vChannelMask), vTexPalette)); return mix(mix(c1, c2, interp.x), mix(c3, c4, interp.x), interp.y); } vec4 ColorShift(vec4 c, float p) { - #if __VERSION__ == 120 - vec4 range = texture2D(ColorShifts, vec2(0.25, p)); - vec4 shift = texture2D(ColorShifts, vec2(0.75, p)); - #else vec4 range = texture(ColorShifts, vec2(0.25, p)); vec4 shift = texture(ColorShifts, vec2(0.75, p)); - #endif vec3 hsv = rgb2hsv(srgb2linear(c).rgb); if (hsv.r > range.r && range.g >= hsv.r) @@ -228,11 +154,13 @@ vec4 ColorShift(vec4 c, float p) void main() { vec2 coords = vTexCoord.st; + bool isPaletted = (vChannelType & 0x01u) != 0u; + bool isColor = vChannelType == 0u; vec4 c; if (AntialiasPixelsPerTexel > 0.0) { - vec2 textureSize = vec2(Size(vTexSampler.s)); + vec2 textureSize = vec2(Size(vChannelSampler)); vec2 offset = fract(coords.st * textureSize); // Offset the sampling point to simulate bilinear intepolation in window coordinates instead of texture coordinates @@ -243,32 +171,33 @@ void main() vec2 interp = clamp(offset * ik * AntialiasPixelsPerTexel, 0.0, .5) + clamp((offset - 1.0) * ik * AntialiasPixelsPerTexel + .5, 0.0, .5); coords = (floor(coords.st * textureSize) + interp) / textureSize; - if (vPalettedFraction.x > 0.0) - c = SamplePalettedBilinear(vTexSampler.s, coords, textureSize); + if (isPaletted) + c = SamplePalettedBilinear(vChannelSampler, coords, textureSize); } - if (!(AntialiasPixelsPerTexel > 0.0 && vPalettedFraction.x > 0.0)) + if (!(AntialiasPixelsPerTexel > 0.0 && isPaletted)) { - vec4 x = Sample(vTexSampler.s, coords); - vec2 p = vec2(dot(x, vChannelMask), vTexMetadata.s); - #if __VERSION__ == 120 - c = vPalettedFraction * texture2D(Palette, p) + vRGBAFraction * x + vColorFraction * vTexCoord; - #else - c = vPalettedFraction * texture(Palette, p) + vRGBAFraction * x + vColorFraction * vTexCoord; - #endif + vec4 x = Sample(vChannelSampler, coords); + vec2 p = vec2(dot(x, vChannelMask), vTexPalette); + if (isPaletted) + c = texture(Palette, p); + else if (isColor) + c = vTexCoord; + else + c = x; } // Discard any transparent fragments (both color and depth) if (c.a == 0.0) discard; - if (vRGBAFraction.r > 0.0 && vTexMetadata.s > 0.0) - c = ColorShift(c, vTexMetadata.s); + if (!isPaletted && vTexPalette > 0.0) + c = ColorShift(c, vTexPalette); float depth = gl_FragCoord.z; if (length(vDepthMask) > 0.0) { - vec4 y = Sample(vTexSampler.t, vTexCoord.pq); + vec4 y = Sample(vDepthSampler, vTexCoord.pq); depth = depth + DepthTextureScale * dot(y, vDepthMask); } @@ -277,12 +206,7 @@ void main() if (EnableDepthPreview) { float intensity = 1.0 - clamp(DepthPreviewParams.x * depth - 0.5 * DepthPreviewParams.x - DepthPreviewParams.y + 0.5, 0.0, 1.0); - - #if __VERSION__ == 120 - gl_FragColor = vec4(vec3(intensity), 1.0); - #else fragColor = vec4(vec3(intensity), 1.0); - #endif } else { @@ -292,10 +216,6 @@ void main() else c *= vTint; - #if __VERSION__ == 120 - gl_FragColor = c; - #else fragColor = c; - #endif } } diff --git a/glsl/combined.vert b/glsl/combined.vert index 1cec86be8b60..b408320eb7f3 100644 --- a/glsl/combined.vert +++ b/glsl/combined.vert @@ -2,130 +2,63 @@ uniform vec3 Scroll; uniform vec3 p1, p2; +uniform float PaletteRows; -#if __VERSION__ == 120 -attribute vec3 aVertexPosition; -attribute vec4 aVertexTexCoord; -attribute vec2 aVertexTexMetadata; -attribute vec4 aVertexTint; - -varying vec4 vTexCoord; -varying vec2 vTexMetadata; -varying vec4 vChannelMask; -varying vec4 vDepthMask; -varying vec2 vTexSampler; - -varying vec4 vColorFraction; -varying vec4 vRGBAFraction; -varying vec4 vPalettedFraction; -varying vec4 vTint; -#else in vec3 aVertexPosition; in vec4 aVertexTexCoord; -in vec2 aVertexTexMetadata; +in uint aVertexAttributes; in vec4 aVertexTint; out vec4 vTexCoord; -out vec2 vTexMetadata; -out vec4 vChannelMask; -out vec4 vDepthMask; -out vec2 vTexSampler; - -out vec4 vColorFraction; -out vec4 vRGBAFraction; -out vec4 vPalettedFraction; +flat out float vTexPalette; +flat out vec4 vChannelMask; +flat out uint vChannelSampler; +flat out uint vChannelType; +flat out vec4 vDepthMask; +flat out uint vDepthSampler; out vec4 vTint; -#endif - -vec4 UnpackChannelAttributes(float x) -{ - // The channel attributes float encodes a set of attributes - // stored as flags in the mantissa of the unnormalized float value. - // Bits 9-11 define the sampler index (0-7) that the secondary texture is bound to - // Bits 6-8 define the sampler index (0-7) that the primary texture is bound to - // Bits 3-5 define the behaviour of the secondary texture channel: - // 000: Channel is not used - // 001, 011, 101, 111: Sample depth sprite from channel R,G,B,A - // Bits 0-2 define the behaviour of the primary texture channel: - // 000: Channel is not used (aVertexTexCoord instead defines a color value) - // 010: Sample RGBA sprite from all four channels - // 001, 011, 101, 111: Sample paletted sprite from channel R,G,B,A - - float secondarySampler = 0.0; - if (x >= 2048.0) { x -= 2048.0; secondarySampler += 4.0; } - if (x >= 1024.0) { x -= 1024.0; secondarySampler += 2.0; } - if (x >= 512.0) { x -= 512.0; secondarySampler += 1.0; } - - float primarySampler = 0.0; - if (x >= 256.0) { x -= 256.0; primarySampler += 4.0; } - if (x >= 128.0) { x -= 128.0; primarySampler += 2.0; } - if (x >= 64.0) { x -= 64.0; primarySampler += 1.0; } - - float secondaryChannel = 0.0; - if (x >= 32.0) { x -= 32.0; secondaryChannel += 4.0; } - if (x >= 16.0) { x -= 16.0; secondaryChannel += 2.0; } - if (x >= 8.0) { x -= 8.0; secondaryChannel += 1.0; } - - float primaryChannel = 0.0; - if (x >= 4.0) { x -= 4.0; primaryChannel += 4.0; } - if (x >= 2.0) { x -= 2.0; primaryChannel += 2.0; } - if (x >= 1.0) { x -= 1.0; primaryChannel += 1.0; } - - return vec4(primaryChannel, secondaryChannel, primarySampler, secondarySampler); -} - -vec4 SelectChannelMask(float x) -{ - if (x >= 7.0) - return vec4(0,0,0,1); - if (x >= 5.0) - return vec4(0,0,1,0); - if (x >= 3.0) - return vec4(0,1,0,0); - if (x >= 2.0) - return vec4(1,1,1,1); - if (x >= 1.0) - return vec4(1,0,0,0); - - return vec4(0, 0, 0, 0); -} - -vec4 SelectColorFraction(float x) -{ - if (x > 0.0) - return vec4(0, 0, 0, 0); - - return vec4(1, 1, 1, 1); -} - -vec4 SelectRGBAFraction(float x) -{ - if (x == 2.0) - return vec4(1, 1, 1, 1); - - return vec4(0, 0, 0, 0); -} - -vec4 SelectPalettedFraction(float x) + +vec4 SelectChannelMask(uint x) { - if (x == 0.0 || x == 2.0) - return vec4(0, 0, 0, 0); - - return vec4(1, 1, 1, 1); + switch (x) + { + case 7u: + return vec4(0.0, 0.0, 0.0, 1.0); + case 5u: + return vec4(0.0, 0.0, 1.0, 0.0); + case 3u: + return vec4(0, 1.0, 0.0, 0.0); + case 2u: + return vec4(1.0, 1.0, 1.0, 1.0); + case 1u: + return vec4(1.0, 0.0, 0.0, 0.0); + default: + return vec4(0.0, 0.0, 0.0, 0.0); + } } void main() { gl_Position = vec4((aVertexPosition - Scroll) * p1 + p2, 1); vTexCoord = aVertexTexCoord; - vTexMetadata = aVertexTexMetadata; - vec4 attrib = UnpackChannelAttributes(aVertexTexMetadata.t); - vChannelMask = SelectChannelMask(attrib.s); - vColorFraction = SelectColorFraction(attrib.s); - vRGBAFraction = SelectRGBAFraction(attrib.s); - vPalettedFraction = SelectPalettedFraction(attrib.s); - vDepthMask = SelectChannelMask(attrib.t); - vTexSampler = attrib.pq; + // aVertexAttributes is a packed bitfield, where: + // Bits 0-2 define the behaviour of the primary texture channel: + // 000: Channel is not used (aVertexTexCoord instead defines a color value) + // 010: Sample RGBA sprite from all four channels + // 001, 011, 101, 111: Sample paletted sprite from channel R,G,B,A + // Bits 3-5 define the behaviour of the secondary texture channel: + // 000: Channel is not used + // 001, 011, 101, 111: Sample depth sprite from channel R,G,B,A + // Bits 6-8 define the sampler index (0-7) that the primary texture is bound to + // Bits 9-11 define the sampler index (0-7) that the secondary texture is bound to + // Bits 16-31 define the palette row for paletted sprites + vChannelType = aVertexAttributes & 0x07u; + vChannelMask = SelectChannelMask(vChannelType); + vDepthMask = SelectChannelMask((aVertexAttributes >> 3) & 0x07u); + vChannelSampler = (aVertexAttributes >> 6) & 0x07u; + vDepthSampler = (aVertexAttributes >> 9) & 0x07u; + vTexPalette = float(aVertexAttributes >> 16) / PaletteRows; + vTint = aVertexTint; } diff --git a/glsl/model.frag b/glsl/model.frag index 33bcfb50a80b..b1f016c24828 100644 --- a/glsl/model.frag +++ b/glsl/model.frag @@ -4,43 +4,26 @@ precision mediump float; #endif uniform sampler2D Palette, DiffuseTexture; -uniform vec2 PaletteRows; +uniform vec2 Palettes; +uniform float PaletteRows; uniform vec4 LightDirection; uniform vec3 AmbientLight, DiffuseLight; -#if __VERSION__ == 120 -varying vec4 vTexCoord; -varying vec4 vChannelMask; -varying vec4 vNormalsMask; -#else in vec4 vTexCoord; in vec4 vChannelMask; in vec4 vNormalsMask; out vec4 fragColor; -#endif void main() { - #if __VERSION__ == 120 - vec4 x = texture2D(DiffuseTexture, vTexCoord.st); - vec4 color = texture2D(Palette, vec2(dot(x, vChannelMask), PaletteRows.x)); - if (color.a < 0.01) - discard; - - vec4 y = texture2D(DiffuseTexture, vTexCoord.pq); - vec4 normal = (2.0 * texture2D(Palette, vec2(dot(y, vNormalsMask), PaletteRows.y)) - 1.0); - vec3 intensity = AmbientLight + DiffuseLight * max(dot(normal, LightDirection), 0.0); - gl_FragColor = vec4(intensity * color.rgb, color.a); - #else vec4 x = texture(DiffuseTexture, vTexCoord.st); - vec4 color = texture(Palette, vec2(dot(x, vChannelMask), PaletteRows.x)); + vec4 color = texture(Palette, vec2(dot(x, vChannelMask), (Palettes.x + 0.5) / PaletteRows)); if (color.a < 0.01) discard; vec4 y = texture(DiffuseTexture, vTexCoord.pq); - vec4 normal = (2.0 * texture(Palette, vec2(dot(y, vNormalsMask), PaletteRows.y)) - 1.0); + vec4 normal = (2.0 * texture(Palette, vec2(dot(y, vNormalsMask), (Palettes.y + 0.5) / PaletteRows)) - 1.0); vec3 intensity = AmbientLight + DiffuseLight * max(dot(normal, LightDirection), 0.0); fragColor = vec4(intensity * color.rgb, color.a); - #endif } diff --git a/glsl/model.vert b/glsl/model.vert index 16aa02496f24..30fdb34a94c4 100644 --- a/glsl/model.vert +++ b/glsl/model.vert @@ -3,21 +3,12 @@ uniform mat4 View; uniform mat4 TransformMatrix; -#if __VERSION__ == 120 -attribute vec4 aVertexPosition; -attribute vec4 aVertexTexCoord; -attribute vec2 aVertexTexMetadata; -varying vec4 vTexCoord; -varying vec4 vChannelMask; -varying vec4 vNormalsMask; -#else in vec4 aVertexPosition; in vec4 aVertexTexCoord; in vec2 aVertexTexMetadata; out vec4 vTexCoord; out vec4 vChannelMask; out vec4 vNormalsMask; -#endif vec4 DecodeMask(float x) { diff --git a/glsl/postprocess.vert b/glsl/postprocess.vert new file mode 100644 index 000000000000..34369579f458 --- /dev/null +++ b/glsl/postprocess.vert @@ -0,0 +1,8 @@ +#version {VERSION} + +in vec2 aVertexPosition; + +void main() +{ + gl_Position = vec4(aVertexPosition, 0, 1); +} diff --git a/glsl/postprocess_chronoshift.frag b/glsl/postprocess_chronoshift.frag new file mode 100644 index 000000000000..87f4f6b340c8 --- /dev/null +++ b/glsl/postprocess_chronoshift.frag @@ -0,0 +1,15 @@ +#version {VERSION} +#ifdef GL_ES +precision mediump float; +#endif + +uniform float Blend; +uniform sampler2D WorldTexture; +out vec4 fragColor; + +void main() +{ + vec4 c = texelFetch(WorldTexture, ivec2(gl_FragCoord.xy), 0); + float lum = 0.5 * (min(c.r, min(c.g, c.b)) + max(c.r, max(c.g, c.b))); + fragColor = mix(c, vec4(lum, lum, lum, c.a), Blend); +} diff --git a/glsl/postprocess_flash.frag b/glsl/postprocess_flash.frag new file mode 100644 index 000000000000..4178a905c533 --- /dev/null +++ b/glsl/postprocess_flash.frag @@ -0,0 +1,15 @@ +#version {VERSION} +#ifdef GL_ES +precision mediump float; +#endif + +uniform float Blend; +uniform vec3 Color; +uniform sampler2D WorldTexture; +out vec4 fragColor; + +void main() +{ + vec4 c = texelFetch(WorldTexture, ivec2(gl_FragCoord.xy), 0); + fragColor = mix(c, vec4(Color, c.a), Blend); +} diff --git a/glsl/postprocess_menufade.frag b/glsl/postprocess_menufade.frag new file mode 100644 index 000000000000..7edaeb7ee3b8 --- /dev/null +++ b/glsl/postprocess_menufade.frag @@ -0,0 +1,32 @@ +#version {VERSION} +#ifdef GL_ES +precision mediump float; +#endif + +uniform float From; +uniform float To; +uniform float Blend; +uniform sampler2D WorldTexture; +out vec4 fragColor; + +vec4 ColorForEffect(float effect, vec4 c) +{ + if (effect > 1.5) + { + float lum = 0.5 * (min(c.r, min(c.g, c.b)) + max(c.r, max(c.g, c.b))); + return vec4(lum, lum, lum, c.a); + } + + if (effect > 0.5) + { + return vec4(0, 0, 0, c.a); + } + + return c; +} + +void main() +{ + vec4 c = texelFetch(WorldTexture, ivec2(gl_FragCoord.xy), 0); + fragColor = mix(ColorForEffect(To, c), ColorForEffect(From, c), Blend); +} diff --git a/glsl/postprocess_textured.vert b/glsl/postprocess_textured.vert new file mode 100644 index 000000000000..e22a9ac19a21 --- /dev/null +++ b/glsl/postprocess_textured.vert @@ -0,0 +1,14 @@ +#version {VERSION} + +uniform vec2 Pos, Scroll; +uniform vec2 p1, p2; + +in vec2 aVertexPosition; +in vec2 aVertexTexCoord; +out vec2 vTexCoord; + +void main() +{ + gl_Position = vec4((aVertexPosition + Pos - Scroll) * p1 + p2, 0, 1); + vTexCoord = aVertexTexCoord; +} diff --git a/glsl/postprocess_textured_vortex.frag b/glsl/postprocess_textured_vortex.frag new file mode 100644 index 000000000000..c5b191faf83e --- /dev/null +++ b/glsl/postprocess_textured_vortex.frag @@ -0,0 +1,22 @@ +#version {VERSION} +#ifdef GL_ES +precision mediump float; +#endif + +uniform sampler2D VortexTexture; +uniform sampler2D WorldTexture; + +in vec2 vTexCoord; +out vec4 fragColor; + +void main() +{ + vec4 vtx = texture(VortexTexture, vTexCoord.xy); + + vec2 delta = (vtx.bg - 0.5) * 256.0; + float frac = 16.0 * vtx.r + 0.0625; + if (vtx.r > 0.055) + discard; + + fragColor = texelFetch(WorldTexture, ivec2(gl_FragCoord.xy + delta), 0) * vec4(frac, frac, frac, 1); +} diff --git a/glsl/postprocess_tint.frag b/glsl/postprocess_tint.frag new file mode 100644 index 000000000000..43b05780d12c --- /dev/null +++ b/glsl/postprocess_tint.frag @@ -0,0 +1,14 @@ +#version {VERSION} +#ifdef GL_ES +precision mediump float; +#endif + +uniform vec3 Tint; +uniform sampler2D WorldTexture; +out vec4 fragColor; + +void main() +{ + vec4 c = texelFetch(WorldTexture, ivec2(gl_FragCoord.xy), 0); + fragColor = vec4(Tint, c.a) * c; +} diff --git a/mods/all/mod.yaml b/mods/all/mod.yaml index 1429b17a5a1c..7e54c41f2e4a 100644 --- a/mods/all/mod.yaml +++ b/mods/all/mod.yaml @@ -14,6 +14,7 @@ Assemblies: ^BinDir|OpenRA.Mods.Common.dll ^BinDir|OpenRA.Mods.Cnc.dll ^BinDir|OpenRA.Mods.D2k.dll + ^BinDir|OpenRA.Mods.AS.dll ChromeLayout: diff --git a/mods/cnc/chrome/assetbrowser.yaml b/mods/cnc/chrome/assetbrowser.yaml index dcf827136d09..6ea68991b94f 100644 --- a/mods/cnc/chrome/assetbrowser.yaml +++ b/mods/cnc/chrome/assetbrowser.yaml @@ -12,7 +12,7 @@ Container@ASSETBROWSER_PANEL: Font: BigBold Contrast: true Align: Center - Text: Asset Browser + Text: label-assetbrowser-panel-title Background@bg: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -25,21 +25,21 @@ Container@ASSETBROWSER_PANEL: Height: 25 Font: TinyBold Align: Center - Text: Select asset source + Text: label-bg-source-selector-desc DropDownButton@SOURCE_SELECTOR: X: 15 Y: 30 Width: 195 Height: 25 Font: Bold - Text: Folders + Text: dropdownbutton-bg-source-selector DropDownButton@ASSET_TYPES_DROPDOWN: X: 15 Y: 65 Width: 195 Height: 25 Font: Bold - Text: Asset types + Text: dropdownbutton-bg-asset-types-dropdown Label@FILENAME_DESC: X: 15 Y: 95 @@ -47,7 +47,7 @@ Container@ASSETBROWSER_PANEL: Height: 25 Font: TinyBold Align: Center - Text: Filter by name + Text: label-bg-filename-desc TextField@FILENAME_INPUT: X: 15 Y: 120 @@ -81,7 +81,7 @@ Container@ASSETBROWSER_PANEL: Height: 25 Font: Bold Align: Left - Text: Scale: + Text: label-bg-sprite-scale Slider@SPRITE_SCALE_SLIDER: X: PARENT_RIGHT - WIDTH - 330 Y: 35 @@ -96,7 +96,7 @@ Container@ASSETBROWSER_PANEL: Height: 25 Font: Bold Align: Right - Text: Palette: + Text: label-bg-palette-desc DropDownButton@PALETTE_SELECTOR: X: PARENT_RIGHT - WIDTH - 110 Y: 30 @@ -133,7 +133,7 @@ Container@ASSETBROWSER_PANEL: Height: PARENT_BOTTOM Align: Center Visible: false - Text: Error displaying file. See assetbrowser.log for details. + Text: label-sprite-bg-error Container@FRAME_SELECTOR: X: 225 Y: PARENT_BOTTOM - 40 @@ -222,7 +222,7 @@ Container@ASSETBROWSER_PANEL: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Back + Text: button-assetbrowser-panel-close TooltipContainer@TOOLTIP_CONTAINER: ScrollPanel@ASSET_TYPES_PANEL: @@ -236,3 +236,4 @@ ScrollPanel@ASSET_TYPES_PANEL: Y: 5 Width: PARENT_RIGHT - 29 Height: 20 + diff --git a/mods/cnc/chrome/color-picker.yaml b/mods/cnc/chrome/color-picker.yaml index 5fe0eea0a316..f4b7c1232a5c 100644 --- a/mods/cnc/chrome/color-picker.yaml +++ b/mods/cnc/chrome/color-picker.yaml @@ -13,13 +13,13 @@ Background@COLOR_CHOOSER: Y: 89 Width: 77 Height: 25 - Text: Random + Text: button-color-chooser-random Button@STORE_BUTTON: X: 229 Y: 118 Width: 77 Height: 25 - Text: Store + Text: button-color-chooser-store Font: Bold ActorPreview@PREVIEW: X: 232 @@ -32,14 +32,14 @@ Background@COLOR_CHOOSER: Y: PARENT_BOTTOM - 30 Height: 25 Width: 80 - Text: Mixer + Text: button-color-chooser-mixer-tab Font: Bold Button@PALETTE_TAB_BUTTON: X: 90 Y: PARENT_BOTTOM - 30 Height: 25 Width: 80 - Text: Palette + Text: button-color-chooser-palette-tab Font: Bold Container@MIXER_TAB: X: 5 @@ -98,7 +98,7 @@ Background@COLOR_CHOOSER: Width: PARENT_RIGHT Height: 13 Align: Center - Text: Preset Colors + Text: label-preset-header Container@PRESET_AREA: Width: PARENT_RIGHT - 4 Height: 58 @@ -124,7 +124,7 @@ Background@COLOR_CHOOSER: Width: PARENT_RIGHT Height: 13 Align: Center - Text: Custom Colors + Text: label-custom-header Container@CUSTOM_AREA: Width: PARENT_RIGHT - 4 Height: 31 @@ -138,3 +138,4 @@ Background@COLOR_CHOOSER: Height: 27 Visible: false ClickSound: ClickSound + diff --git a/mods/cnc/chrome/connection.yaml b/mods/cnc/chrome/connection.yaml index e53182bdf30a..ebd59a5aded4 100644 --- a/mods/cnc/chrome/connection.yaml +++ b/mods/cnc/chrome/connection.yaml @@ -11,7 +11,7 @@ Container@CONNECTING_PANEL: Font: BigBold Contrast: true Align: Center - Text: Connecting + Text: label-connecting-panel-title Background@bg: Width: 370 Height: 90 @@ -21,7 +21,7 @@ Container@CONNECTING_PANEL: Y: (PARENT_BOTTOM - HEIGHT) / 2 Width: PARENT_RIGHT Height: 25 - Text: Connecting... + Text: label-bg-connecting-desc Font: Bold Align: Center Button@ABORT_BUTTON: @@ -29,7 +29,7 @@ Container@CONNECTING_PANEL: Y: 89 Width: 140 Height: 35 - Text: Abort + Text: button-connecting-panel-abort Container@CONNECTIONFAILED_PANEL: Logic: ConnectionFailedLogic @@ -54,7 +54,7 @@ Container@CONNECTIONFAILED_PANEL: Y: 16 Width: PARENT_RIGHT Height: 25 - Text: Failed to connect + Text: label-connection-background-connecting-desc Font: Bold Align: Center Label@CONNECTION_ERROR: @@ -69,33 +69,32 @@ Container@CONNECTIONFAILED_PANEL: Width: 95 Height: 25 Align: Right - Text: Password: + Text: label-connection-background-password Font: Bold PasswordField@PASSWORD: X: 140 Y: 80 Width: 155 - MaxLength: 20 Height: 25 Button@ABORT_BUTTON: Key: escape Y: 84 Width: 140 Height: 35 - Text: Abort + Text: button-connectionfailed-panel-abort Button@QUIT_BUTTON: Key: escape Y: 84 Width: 140 Height: 35 - Text: Quit + Text: button-connectionfailed-panel-quit Button@RETRY_BUTTON: Key: return X: 230 Y: 84 Width: 140 Height: 35 - Text: Retry + Text: button-connectionfailed-panel-retry Container@CONNECTION_SWITCHMOD_PANEL: Logic: ConnectionSwitchModLogic @@ -110,7 +109,7 @@ Container@CONNECTION_SWITCHMOD_PANEL: Font: BigBold Contrast: true Align: Center - Text: Switch Mod + Text: label-connection-switchmod-panel-title Background@CONNECTION_BACKGROUND: Width: 370 Height: 120 @@ -120,7 +119,7 @@ Container@CONNECTION_SWITCHMOD_PANEL: Y: 16 Width: PARENT_RIGHT Height: 25 - Text: This server is running a different mod: + Text: label-connection-background-desc Font: Bold Align: Center Container@MOD_CONTAINER: @@ -150,7 +149,7 @@ Container@CONNECTION_SWITCHMOD_PANEL: Y: 81 Width: PARENT_RIGHT Height: 25 - Text: Switch mods and join server? + Text: label-connection-background-desc2 Font: Bold Align: Center Button@ABORT_BUTTON: @@ -158,11 +157,12 @@ Container@CONNECTION_SWITCHMOD_PANEL: Y: 119 Width: 140 Height: 35 - Text: Abort + Text: button-connection-switchmod-panel-abort Button@SWITCH_BUTTON: Key: return X: 230 Y: 119 Width: 140 Height: 35 - Text: Switch + Text: button-connection-switchmod-panel-switch + diff --git a/mods/cnc/chrome/credits.yaml b/mods/cnc/chrome/credits.yaml index 670bc9038c0d..3bbc815a5aab 100644 --- a/mods/cnc/chrome/credits.yaml +++ b/mods/cnc/chrome/credits.yaml @@ -11,7 +11,7 @@ Container@CREDITS_PANEL: Font: BigBold Contrast: true Align: Center - Text: Credits + Text: label-credits-panel-title Background@bg: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -31,7 +31,7 @@ Container@CREDITS_PANEL: X: 150 Width: 140 Height: 35 - Text: OpenRA + Text: button-tab-container-engine ScrollPanel@CREDITS_DISPLAY: X: 15 Y: 15 @@ -48,5 +48,6 @@ Container@CREDITS_PANEL: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Back + Text: button-credits-panel-back Key: escape + diff --git a/mods/cnc/chrome/dialogs.yaml b/mods/cnc/chrome/dialogs.yaml index d0977f59efd0..004366c44130 100644 --- a/mods/cnc/chrome/dialogs.yaml +++ b/mods/cnc/chrome/dialogs.yaml @@ -178,7 +178,7 @@ Background@THREEBUTTON_PROMPT: Y: 40 Width: 140 Height: 35 - Text: Confirm + Text: button-threebutton-prompt-confirm Visible: false Button@OTHER_BUTTON: Key: r @@ -186,14 +186,14 @@ Background@THREEBUTTON_PROMPT: Y: 40 Width: 140 Height: 35 - Text: Restart + Text: button-threebutton-prompt-other Visible: false Button@CANCEL_BUTTON: Key: escape Y: 40 Width: 140 Height: 35 - Text: Cancel + Text: button-threebutton-prompt-cancel Visible: false Background@TWOBUTTON_PROMPT: @@ -220,7 +220,7 @@ Background@TWOBUTTON_PROMPT: Y: 30 Width: 140 Height: 35 - Text: Cancel + Text: button-twobutton-prompt-cancel Visible: false Button@CONFIRM_BUTTON: Key: return @@ -228,7 +228,7 @@ Background@TWOBUTTON_PROMPT: Y: 30 Width: 140 Height: 35 - Text: Confirm + Text: button-twobutton-prompt-confirm Visible: false Container@TEXT_INPUT_PROMPT: @@ -265,7 +265,7 @@ Container@TEXT_INPUT_PROMPT: Y: PARENT_BOTTOM - 1 Width: 160 Height: 34 - Text: OK + Text: button-text-input-prompt-accept Font: Bold Key: return Button@CANCEL_BUTTON: @@ -273,7 +273,7 @@ Container@TEXT_INPUT_PROMPT: Y: PARENT_BOTTOM - 1 Width: 160 Height: 35 - Text: Cancel + Text: button-text-input-prompt-cancel Font: Bold Key: escape @@ -312,3 +312,4 @@ ScrollPanel@NEWS_PANEL: Height: PARENT_BOTTOM Align: Center VAlign: Middle + diff --git a/mods/cnc/chrome/editor.yaml b/mods/cnc/chrome/editor.yaml index b4d81c91f088..9fa41e362739 100644 --- a/mods/cnc/chrome/editor.yaml +++ b/mods/cnc/chrome/editor.yaml @@ -6,7 +6,7 @@ Container@NEW_MAP_BG: Height: 95 Children: Label@TITLE: - Text: New Map + Text: label-new-map-bg-title Width: PARENT_RIGHT Y: 0 - 22 Font: BigBold @@ -23,7 +23,7 @@ Container@NEW_MAP_BG: Width: 95 Height: 25 Align: Right - Text: Tileset: + Text: label-bg-tileset DropDownButton@TILESET: X: 125 Y: 15 @@ -35,7 +35,7 @@ Container@NEW_MAP_BG: Width: 95 Height: 25 Align: Right - Text: Width: + Text: label-bg-width TextField@WIDTH: X: 125 Y: 50 @@ -50,7 +50,7 @@ Container@NEW_MAP_BG: Width: 95 Height: 25 Align: Right - Text: Height: + Text: label-bg-height TextField@HEIGHT: X: 235 Y: 50 @@ -63,7 +63,7 @@ Container@NEW_MAP_BG: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Cancel + Text: button-new-map-bg-cancel Font: Bold Key: escape Button@CREATE_BUTTON: @@ -71,7 +71,7 @@ Container@NEW_MAP_BG: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Create + Text: button-new-map-bg-create Font: Bold Key: return @@ -83,7 +83,7 @@ Container@SAVE_MAP_PANEL: Height: 195 Children: Label@LABEL_TITLE: - Text: Save Map + Text: label-save-map-panel-title Width: PARENT_RIGHT Y: 0 - 22 Font: BigBold @@ -100,7 +100,7 @@ Container@SAVE_MAP_PANEL: Width: 95 Height: 25 Align: Right - Text: Title: + Text: label-save-map-background-title TextField@TITLE: X: 110 Y: 15 @@ -113,7 +113,7 @@ Container@SAVE_MAP_PANEL: Width: 95 Height: 25 Align: Right - Text: Author: + Text: label-save-map-background-author TextField@AUTHOR: X: 110 Y: 50 @@ -126,13 +126,13 @@ Container@SAVE_MAP_PANEL: Width: 95 Height: 25 Align: Right - Text: Visibility: + Text: label-save-map-background-visibility DropDownButton@VISIBILITY_DROPDOWN: X: 110 Y: 85 Width: 220 Height: 25 - Text: Map Visibility + Text: dropdownbutton-save-map-background-visibility-dropdown Font: Regular Label@DIRECTORY_LABEL: X: 10 @@ -140,7 +140,7 @@ Container@SAVE_MAP_PANEL: Width: 95 Height: 25 Align: Right - Text: Directory: + Text: label-save-map-background-directory DropDownButton@DIRECTORY_DROPDOWN: X: 110 Y: 120 @@ -153,7 +153,7 @@ Container@SAVE_MAP_PANEL: Width: 95 Height: 25 Align: Right - Text: Filename: + Text: label-save-map-background-filename TextField@FILENAME: X: 110 Y: 155 @@ -170,7 +170,7 @@ Container@SAVE_MAP_PANEL: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Cancel + Text: button-save-map-panel-back Font: Bold Key: escape Button@SAVE_BUTTON: @@ -178,7 +178,7 @@ Container@SAVE_MAP_PANEL: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Save + Text: button-save-map-panel Font: Bold ScrollPanel@MAP_SAVE_VISIBILITY_PANEL: @@ -241,7 +241,7 @@ Container@EDITOR_WORLD_ROOT: Y: 30 Width: 55 Height: 24 - Text: ID + Text: label-actor-edit-panel-id Align: Right TextField@ACTOR_ID: X: 67 @@ -310,19 +310,19 @@ Container@EDITOR_WORLD_ROOT: X: 4 Width: 75 Height: 25 - Text: Delete + Text: button-container-delete Font: Bold Button@CANCEL_BUTTON: X: 110 Width: 75 Height: 25 - Text: Cancel + Text: button-container-cancel Font: Bold Button@OK_BUTTON: X: 190 Width: 75 Height: 25 - Text: OK + Text: button-container-ok Font: Bold ViewportController: Width: WINDOW_RIGHT @@ -360,7 +360,7 @@ Container@EDITOR_WORLD_ROOT: Y: 5 Width: 30 Height: 25 - TooltipText: Menu + TooltipText: button-editor-world-root-options-tooltip TooltipContainer: TOOLTIP_CONTAINER Children: Image: @@ -389,14 +389,14 @@ Container@EDITOR_WORLD_ROOT: Y: 1 Width: PARENT_RIGHT - 5 Height: 25 - Text: Search: + Text: label-tiles-bg-search Align: Right Font: TinyBold Label@CATEGORIES_LABEL: Y: 25 Width: PARENT_RIGHT - 5 Height: 25 - Text: Filter: + Text: label-tiles-bg-categories Align: Right Font: TinyBold TextField@SEARCH_TEXTFIELD: @@ -472,21 +472,21 @@ Container@EDITOR_WORLD_ROOT: Y: 1 Width: PARENT_RIGHT - 5 Height: 25 - Text: Search: + Text: label-actors-bg-search Align: Right Font: TinyBold Label@CATEGORIES_LABEL: Y: 25 Width: PARENT_RIGHT - 5 Height: 25 - Text: Filter: + Text: label-actors-bg-categories Align: Right Font: TinyBold Label@OWNERS_LABEL: Y: 49 Width: PARENT_RIGHT - 5 Height: 25 - Text: Owner: + Text: label-actors-bg-owners Align: Right Font: TinyBold TextField@SEARCH_TEXTFIELD: @@ -567,87 +567,87 @@ Container@EDITOR_WORLD_ROOT: Button@TILES_TAB: Width: 71 Height: 25 - Text: Tiles + Text: button-map-editor-tab-container-tiles.label Font: Bold Key: EditorTilesTab TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Tiles + TooltipText: button-map-editor-tab-container-tiles.tooltip TooltipContainer: TOOLTIP_CONTAINER Button@OVERLAYS_TAB: X: 70 Width: 80 Height: 25 - Text: Overlays + Text: button-map-editor-tab-container-overlays.label Font: Bold Key: EditorOverlaysTab TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Overlays + TooltipText: button-map-editor-tab-container-overlays.tooltip TooltipContainer: TOOLTIP_CONTAINER Button@ACTORS_TAB: X: 149 Width: 71 Height: 25 - Text: Actors + Text: button-map-editor-tab-container-actors.label Font: Bold Key: EditorActorsTab TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Actors + TooltipText: button-map-editor-tab-container-actors.tooltip TooltipContainer: TOOLTIP_CONTAINER Button@HISTORY_TAB: X: 219 Width: 71 Height: 25 - Text: History + Text: button-map-editor-tab-container-history.label Font: Bold Key: EditorHistoryTab TooltipTemplate: BUTTON_TOOLTIP - TooltipText: History + TooltipText: button-map-editor-tab-container-history.tooltip TooltipContainer: TOOLTIP_CONTAINER Button@UNDO_BUTTON: X: WINDOW_RIGHT - 800 Y: 5 Height: 25 Width: 100 - Text: Undo + Text: button-editor-world-root-undo.label Font: Bold Key: EditorUndo TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Undo last step + TooltipText: button-editor-world-root-undo.tooltip TooltipContainer: TOOLTIP_CONTAINER Button@REDO_BUTTON: X: WINDOW_RIGHT - 690 Y: 5 Height: 25 Width: 100 - Text: Redo + Text: button-editor-world-root-redo.label Font: Bold Key: EditorRedo TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Redo last step + TooltipText: button-editor-world-root-redo.tooltip TooltipContainer: TOOLTIP_CONTAINER Button@COPYPASTE_BUTTON: X: WINDOW_RIGHT - 580 Y: 5 Width: 96 Height: 25 - Text: Copy/Paste + Text: button-editor-world-root-copypaste.label Key: EditorCopy TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Copy + TooltipText: button-editor-world-root-copypaste.tooltip TooltipContainer: TOOLTIP_CONTAINER DropDownButton@COPYFILTER_BUTTON: X: WINDOW_RIGHT - 475 Y: 5 Width: 140 Height: 25 - Text: Copy Filters + Text: dropdownbutton-editor-world-root-copyfilter-button Font: Bold DropDownButton@OVERLAY_BUTTON: X: WINDOW_RIGHT - 950 Y: 5 Width: 140 Height: 25 - Text: Overlays + Text: dropdownbutton-editor-world-root-overlay-button Font: Bold Label@COORDINATE_LABEL: X: 10 @@ -678,13 +678,13 @@ ScrollPanel@CATEGORY_FILTER_PANEL: Y: 0 - 5 Width: 88 Height: 25 - Text: All + Text: button-select-categories-buttons-all Button@SELECT_NONE: X: 10 + 88 + 10 Y: 0 - 5 Width: 88 Height: 25 - Text: None + Text: button-select-categories-buttons-none Checkbox@CATEGORY_TEMPLATE: X: 5 Width: PARENT_RIGHT - 29 @@ -716,3 +716,4 @@ ScrollPanel@OVERLAY_PANEL: Width: PARENT_RIGHT - 29 Height: 20 Visible: false + diff --git a/mods/cnc/chrome/gamesave-browser.yaml b/mods/cnc/chrome/gamesave-browser.yaml index 75740068e5f0..a58fb69d1e2c 100644 --- a/mods/cnc/chrome/gamesave-browser.yaml +++ b/mods/cnc/chrome/gamesave-browser.yaml @@ -2,7 +2,7 @@ Container@GAMESAVE_BROWSER_PANEL: Logic: GameSaveBrowserLogic X: (WINDOW_RIGHT - WIDTH) / 2 Y: (WINDOW_BOTTOM - HEIGHT) / 2 - Width: 714 + Width: 716 Height: 435 Children: Label@LOAD_TITLE: @@ -11,7 +11,7 @@ Container@GAMESAVE_BROWSER_PANEL: Font: BigBold Contrast: true Align: Center - Text: Load game + Text: label-gamesave-browser-panel-load-title Visible: False Label@SAVE_TITLE: Width: PARENT_RIGHT @@ -19,7 +19,7 @@ Container@GAMESAVE_BROWSER_PANEL: Font: BigBold Contrast: true Align: Center - Text: Save game + Text: label-gamesave-browser-panel-save-title Visible: False Background@bg: Width: PARENT_RIGHT @@ -42,7 +42,7 @@ Container@GAMESAVE_BROWSER_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Align: Center - Text: [CREATE NEW FILE] + Text: label-bg-title ScrollItem@GAME_TEMPLATE: Width: PARENT_RIGHT - 27 Height: 25 @@ -78,26 +78,26 @@ Container@GAMESAVE_BROWSER_PANEL: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Back + Text: button-bg-cancel Button@DELETE_ALL_BUTTON: X: PARENT_RIGHT - 370 - WIDTH Y: PARENT_BOTTOM - 1 Width: 100 Height: 35 - Text: Delete All + Text: button-bg-delete-all Button@DELETE_BUTTON: X: PARENT_RIGHT - 260 - WIDTH Y: PARENT_BOTTOM - 1 Width: 100 Height: 35 - Text: Delete + Text: button-bg-delete Key: Delete Button@RENAME_BUTTON: X: PARENT_RIGHT - 150 - WIDTH Y: PARENT_BOTTOM - 1 Width: 100 Height: 35 - Text: Rename + Text: button-bg-rename Key: F2 Button@LOAD_BUTTON: Key: return @@ -105,7 +105,7 @@ Container@GAMESAVE_BROWSER_PANEL: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Load + Text: button-bg-load Visible: False Button@SAVE_BUTTON: Key: return @@ -113,6 +113,7 @@ Container@GAMESAVE_BROWSER_PANEL: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Save + Text: button-bg-save Visible: False TooltipContainer@GAMESAVE_TOOLTIP_CONTAINER: + diff --git a/mods/cnc/chrome/gamesave-loading.yaml b/mods/cnc/chrome/gamesave-loading.yaml index 754ac425c506..b5bcf603b3db 100644 --- a/mods/cnc/chrome/gamesave-loading.yaml +++ b/mods/cnc/chrome/gamesave-loading.yaml @@ -37,7 +37,7 @@ Container@GAMESAVE_LOADING_SCREEN: Height: 25 Font: Bold Align: Center - Text: Loading Saved Game + Text: label-gamesave-loading-screen-title ProgressBar@PROGRESS: X: (WINDOW_RIGHT - 500) / 2 Y: 3 * WINDOW_BOTTOM / 4 @@ -49,4 +49,5 @@ Container@GAMESAVE_LOADING_SCREEN: Height: 25 Font: Regular Align: Center - Text: Press Escape to cancel loading and return to the main menu + Text: label-gamesave-loading-screen-desc + diff --git a/mods/cnc/chrome/ingame-chat.yaml b/mods/cnc/chrome/ingame-chat.yaml index 10534417d8c0..3d3b6c0fcba0 100644 --- a/mods/cnc/chrome/ingame-chat.yaml +++ b/mods/cnc/chrome/ingame-chat.yaml @@ -30,10 +30,10 @@ Container@CHAT_PANEL: Y: PARENT_BOTTOM - HEIGHT Width: 50 Height: 25 - Text: Team + Text: button-chat-chrome-mode.label Font: Bold Key: ToggleChatMode - TooltipText: Toggle chat mode + TooltipText: button-chat-chrome-mode.tooltip TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX TooltipContainer: TOOLTIP_CONTAINER TextField@CHAT_TEXTFIELD: @@ -63,3 +63,4 @@ Container@CHAT_PANEL: TopBottomSpacing: 3 ItemSpacing: 4 Align: Bottom + diff --git a/mods/cnc/chrome/ingame-debug.yaml b/mods/cnc/chrome/ingame-debug.yaml index e6550a4a8dff..99b62592ba4c 100644 --- a/mods/cnc/chrome/ingame-debug.yaml +++ b/mods/cnc/chrome/ingame-debug.yaml @@ -6,7 +6,7 @@ Container@DEBUG_PANEL: Label@TITLE: Y: 26 Font: Bold - Text: Debug Options + Text: label-debug-panel-title Align: Center Width: PARENT_RIGHT Checkbox@INSTANT_BUILD: @@ -15,70 +15,70 @@ Container@DEBUG_PANEL: Width: 200 Height: 20 Font: Regular - Text: Instant Build Speed + Text: checkbox-debug-panel-instant-build Checkbox@ENABLE_TECH: X: 45 Y: 75 Width: 200 Height: 20 Font: Regular - Text: Build Everything + Text: checkbox-debug-panel-enable-tech Checkbox@BUILD_ANYWHERE: X: 45 Y: 105 Width: 200 Height: 20 Font: Regular - Text: Build Anywhere + Text: checkbox-debug-panel-build-anywhere Checkbox@UNLIMITED_POWER: X: 290 Y: 45 Width: 200 Height: 20 Font: Regular - Text: Unlimited Power + Text: checkbox-debug-panel-unlimited-power Checkbox@INSTANT_CHARGE: X: 290 Y: 75 Width: 200 Height: 20 Font: Regular - Text: Instant Charge Time + Text: checkbox-debug-panel-instant-charge Checkbox@DISABLE_VISIBILITY_CHECKS: X: 290 Y: 105 Height: 20 Width: 200 Font: Regular - Text: Disable Visibility Checks + Text: checkbox-debug-panel-disable-visibility-checks Button@GIVE_CASH: X: 90 Y: 145 Width: 140 Height: 35 - Text: Give $20,000 + Text: button-debug-panel-give-cash Button@GROW_RESOURCES: X: 271 Y: 145 Width: 140 Height: 35 - Text: Grow Resources + Text: button-debug-panel-grow-resources Button@GIVE_EXPLORATION: X: 90 Y: 195 Width: 140 Height: 35 - Text: Clear Shroud + Text: button-debug-panel-give-exploration Button@RESET_EXPLORATION: X: 271 Y: 195 Width: 140 Height: 35 - Text: Reset Shroud + Text: button-debug-panel-reset-exploration Label@VISUALIZATIONS_TITLE: Y: 256 Font: Bold - Text: Visualizations + Text: label-debug-panel-visualizations-title Align: Center Width: PARENT_RIGHT Checkbox@SHOW_UNIT_PATHS: @@ -87,46 +87,47 @@ Container@DEBUG_PANEL: Width: 200 Height: 20 Font: Regular - Text: Show Unit Paths + Text: checkbox-debug-panel-show-unit-paths Checkbox@SHOW_CUSTOMTERRAIN_OVERLAY: X: 45 Y: 305 Height: 20 Width: 200 Font: Regular - Text: Show Custom Terrain + Text: checkbox-debug-panel-show-customterrain-overlay Checkbox@SHOW_ACTOR_TAGS: X: 45 Y: 335 Height: 20 Width: 200 Font: Regular - Text: Show Actor Tags + Text: checkbox-debug-panel-show-actor-tags Checkbox@SHOW_COMBATOVERLAY: X: 290 Y: 275 Height: 20 Width: 200 Font: Regular - Text: Show Combat Geometry + Text: checkbox-debug-panel-show-combatoverlay Checkbox@SHOW_GEOMETRY: X: 290 Y: 305 Height: 20 Width: 200 Font: Regular - Text: Show Render Geometry + Text: checkbox-debug-panel-show-geometry Checkbox@SHOW_TERRAIN_OVERLAY: X: 290 Y: 335 Height: 20 Width: 200 Font: Regular - Text: Show Terrain Geometry + Text: checkbox-debug-panel-show-terrain-overlay Checkbox@SHOW_SCREENMAP: X: 290 Y: 365 Height: 20 Width: 200 Font: Regular - Text: Show Screen Map + Text: checkbox-debug-panel-show-screenmap + diff --git a/mods/cnc/chrome/ingame-debuginfo.yaml b/mods/cnc/chrome/ingame-debuginfo.yaml index fd28c2984473..076e6adc1deb 100644 --- a/mods/cnc/chrome/ingame-debuginfo.yaml +++ b/mods/cnc/chrome/ingame-debuginfo.yaml @@ -12,3 +12,4 @@ Container@DEBUG_WIDGETS: Align: Center Font: Bold Contrast: true + diff --git a/mods/cnc/chrome/ingame-info-lobby-options.yaml b/mods/cnc/chrome/ingame-info-lobby-options.yaml index 38dfaaafcc3e..961effc0a172 100644 --- a/mods/cnc/chrome/ingame-info-lobby-options.yaml +++ b/mods/cnc/chrome/ingame-info-lobby-options.yaml @@ -7,7 +7,7 @@ Container@LOBBY_OPTIONS_PANEL: X: 15 Y: 15 Width: PARENT_RIGHT - 30 - Height: 375 + Height: PARENT_BOTTOM - 30 Children: Container@LOBBY_OPTIONS: Y: 10 @@ -63,3 +63,4 @@ Container@LOBBY_OPTIONS_PANEL: Font: Regular Visible: False TooltipContainer: TOOLTIP_CONTAINER + diff --git a/mods/cnc/chrome/ingame-info.yaml b/mods/cnc/chrome/ingame-info.yaml index 717f30abb9c5..02297da915f0 100644 --- a/mods/cnc/chrome/ingame-info.yaml +++ b/mods/cnc/chrome/ingame-info.yaml @@ -9,7 +9,7 @@ Container@GAME_INFO_PANEL: Label@TITLE: Width: PARENT_RIGHT Y: 0 - 22 - Text: Game Information + Text: label-game-info-panel-title Align: Center Font: BigBold Contrast: true @@ -87,11 +87,14 @@ Container@GAME_INFO_PANEL: Background: panel-black Children: Container@STATS_PANEL: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM Container@MAP_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Container@OBJECTIVES_PANEL: Width: PARENT_RIGHT + Height: PARENT_BOTTOM Container@DEBUG_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -101,3 +104,4 @@ Container@GAME_INFO_PANEL: Container@LOBBY_OPTIONS_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM + diff --git a/mods/cnc/chrome/ingame-infobriefing.yaml b/mods/cnc/chrome/ingame-infobriefing.yaml index c55bd8890f95..566908b4448d 100644 --- a/mods/cnc/chrome/ingame-infobriefing.yaml +++ b/mods/cnc/chrome/ingame-infobriefing.yaml @@ -21,10 +21,11 @@ Container@MAP_PANEL: ScrollPanel@MAP_DESCRIPTION_PANEL: X: 15 Y: 228 - Width: 482 - Height: 162 + Width: PARENT_RIGHT - 30 + Height: PARENT_BOTTOM - 228 - 15 Children: Label@MAP_DESCRIPTION: X: 4 Y: 2 Width: PARENT_RIGHT - 32 + diff --git a/mods/cnc/chrome/ingame-infochat.yaml b/mods/cnc/chrome/ingame-infochat.yaml index 64be9d5f4c3e..fb32f99fb93b 100644 --- a/mods/cnc/chrome/ingame-infochat.yaml +++ b/mods/cnc/chrome/ingame-infochat.yaml @@ -17,10 +17,10 @@ Container@CHAT_CONTAINER: Y: PARENT_BOTTOM - HEIGHT Width: 50 Height: 25 - Text: Team + Text: button-chat-chrome-mode.label Font: Bold Key: ToggleChatMode - TooltipText: Toggle chat mode + TooltipText: button-chat-chrome-mode.tooltip TooltipContainer: TOOLTIP_CONTAINER TextField@CHAT_TEXTFIELD: X: 55 @@ -32,3 +32,4 @@ Container@CHAT_CONTAINER: Height: PARENT_BOTTOM - 30 TopBottomSpacing: 3 ItemSpacing: 2 + diff --git a/mods/cnc/chrome/ingame-infoobjectives.yaml b/mods/cnc/chrome/ingame-infoobjectives.yaml index f7dd2f9a6944..8559ca0b94a0 100644 --- a/mods/cnc/chrome/ingame-infoobjectives.yaml +++ b/mods/cnc/chrome/ingame-infoobjectives.yaml @@ -9,7 +9,7 @@ Container@MISSION_OBJECTIVES: Width: 80 Height: 20 Font: MediumBold - Text: Mission: + Text: label-mission-objectives Label@MISSION_STATUS: X: 95 Y: 17 @@ -19,8 +19,8 @@ Container@MISSION_OBJECTIVES: ScrollPanel@OBJECTIVES_PANEL: X: 15 Y: 50 - Width: 482 - Height: 340 + Width: PARENT_RIGHT - 30 + Height: PARENT_BOTTOM - 65 TopBottomSpacing: 15 ItemSpacing: 15 Children: @@ -41,3 +41,4 @@ Container@MISSION_OBJECTIVES: Height: PARENT_BOTTOM Disabled: True TextColorDisabled: FFFFFF + diff --git a/mods/cnc/chrome/ingame-infostats.yaml b/mods/cnc/chrome/ingame-infostats.yaml index 6273053db1a4..e43edf65d694 100644 --- a/mods/cnc/chrome/ingame-infostats.yaml +++ b/mods/cnc/chrome/ingame-infostats.yaml @@ -12,7 +12,7 @@ Container@SKIRMISH_STATS: Width: 80 Height: 20 Font: MediumBold - Text: Mission: + Text: label-objective-mission Label@STATS_STATUS: X: 95 Y: 17 @@ -25,7 +25,7 @@ Container@SKIRMISH_STATS: Width: 482 Height: 20 Font: Bold - Text: Destroy all opposition! + Text: checkbox-objective-stats Disabled: true TextColorDisabled: FFFFFF Container@STATS_HEADERS: @@ -38,33 +38,33 @@ Container@SKIRMISH_STATS: Y: 1 Width: 210 Height: 25 - Text: Player + Text: label-stats-name Font: Bold Label@FACTION: X: 230 Y: 1 Width: 120 Height: 25 - Text: Faction + Text: label-stats-faction Font: Bold Label@SCORE: X: 392 Y: 1 Width: 60 Height: 25 - Text: Score + Text: label-stats-score Font: Bold Label@ACTIONS: X: 457 Width: 20 Height: 25 - Text: Actions + Text: label-stats-actions Font: Bold ScrollPanel@PLAYER_LIST: X: 15 Y: 105 Width: PARENT_RIGHT - 30 - Height: 285 + Height: PARENT_BOTTOM - 120 ItemSpacing: 5 Children: ScrollItem@TEAM_TEMPLATE: @@ -187,3 +187,4 @@ Container@SKIRMISH_STATS: ImageName: kick X: 7 Y: 7 + diff --git a/mods/cnc/chrome/ingame-menu.yaml b/mods/cnc/chrome/ingame-menu.yaml index 4f6330dd3650..cbdb10328dd5 100644 --- a/mods/cnc/chrome/ingame-menu.yaml +++ b/mods/cnc/chrome/ingame-menu.yaml @@ -32,3 +32,4 @@ Container@INGAME_MENU: Button@BUTTON_TEMPLATE: Width: 120 Height: 35 + diff --git a/mods/cnc/chrome/ingame.yaml b/mods/cnc/chrome/ingame.yaml index da93927ae9ae..855c3a9c9f8e 100644 --- a/mods/cnc/chrome/ingame.yaml +++ b/mods/cnc/chrome/ingame.yaml @@ -135,7 +135,7 @@ Container@OBSERVER_WIDGETS: Width: PARENT_RIGHT - 30 Height: 25 Align: Right - Text: Audio Muted + Text: label-mute-indicator Contrast: true LogicKeyListener@OBSERVER_KEY_LISTENER: MenuButton@OPTIONS_BUTTON: @@ -144,7 +144,7 @@ Container@OBSERVER_WIDGETS: Y: 5 Width: 30 Height: 25 - TooltipText: Menu + TooltipText: button-observer-widgets-options-tooltip TooltipContainer: TOOLTIP_CONTAINER DisableWorldSounds: true Children: @@ -186,7 +186,7 @@ Container@OBSERVER_WIDGETS: Width: 26 Height: 26 Key: Pause - TooltipText: Pause + TooltipText: button-replay-player-pause-tooltip TooltipContainer: TOOLTIP_CONTAINER IgnoreChildMouseOver: true Children: @@ -203,7 +203,7 @@ Container@OBSERVER_WIDGETS: Width: 26 Height: 26 Key: Pause - TooltipText: Play + TooltipText: button-replay-player-play-tooltip TooltipContainer: TOOLTIP_CONTAINER IgnoreChildMouseOver: true Children: @@ -220,10 +220,10 @@ Container@OBSERVER_WIDGETS: Width: 36 Height: 20 Key: ReplaySpeedSlow - TooltipText: Slow speed + TooltipText: button-replay-player-slow.tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 - Text: 50% + Text: button-replay-player-slow.label Font: TinyBold Button@BUTTON_REGULAR: X: 57 + 48 @@ -231,10 +231,10 @@ Container@OBSERVER_WIDGETS: Width: 38 Height: 20 Key: ReplaySpeedRegular - TooltipText: Regular speed + TooltipText: button-replay-player-regular.tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 - Text: 100% + Text: button-replay-player-regular.label Font: TinyBold Button@BUTTON_FAST: X: 57 + 48 * 2 @@ -242,10 +242,10 @@ Container@OBSERVER_WIDGETS: Width: 38 Height: 20 Key: ReplaySpeedFast - TooltipText: Fast speed + TooltipText: button-replay-player-fast.tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 - Text: 200% + Text: button-replay-player-fast.label Font: TinyBold Button@BUTTON_MAXIMUM: X: 57 + 48 * 3 @@ -253,10 +253,10 @@ Container@OBSERVER_WIDGETS: Width: 38 Height: 20 Key: ReplaySpeedMax - TooltipText: Maximum speed + TooltipText: button-replay-player-maximum.tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 - Text: MAX + Text: button-replay-player-maximum.label Font: TinyBold DropDownButton@SHROUD_SELECTOR: Logic: ObserverShroudSelectorLogic @@ -339,7 +339,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-basic-stats-player-header Align: Left Shadow: True Label@CASH_HEADER: @@ -348,7 +348,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Cash + Text: label-basic-stats-cash-header Align: Right Shadow: True Label@POWER_HEADER: @@ -357,7 +357,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Power + Text: label-basic-stats-power-header Align: Center Shadow: True Label@KILLS_HEADER: @@ -366,7 +366,7 @@ Container@OBSERVER_WIDGETS: Width: 40 Height: PARENT_BOTTOM Font: Bold - Text: Kills + Text: label-basic-stats-kills-header Align: Right Shadow: True Label@DEATHS_HEADER: @@ -375,7 +375,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Deaths + Text: label-basic-stats-deaths-header Align: Right Shadow: True Label@ASSETS_DESTROYED_HEADER: @@ -384,7 +384,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Destroyed + Text: label-basic-stats-assets-destroyed-header Align: Right Shadow: True Label@ASSETS_LOST_HEADER: @@ -393,7 +393,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Lost + Text: label-basic-stats-assets-lost-header Align: Right Shadow: True Label@EXPERIENCE_HEADER: @@ -402,7 +402,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Score + Text: label-basic-stats-experience-header Align: Right Shadow: True Label@ACTIONS_MIN_HEADER: @@ -411,7 +411,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: APM + Text: label-basic-stats-actions-min-header Align: Right Shadow: True Container@ECONOMY_STATS_HEADERS: @@ -438,14 +438,14 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-economy-stats-player-header Shadow: True Label@CASH_HEADER: X: 160 Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Cash + Text: label-economy-stats-cash-header Align: Right Shadow: True Label@INCOME_HEADER: @@ -453,7 +453,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Income + Text: label-economy-stats-income-header Align: Right Shadow: True Label@ASSETS_HEADER: @@ -461,7 +461,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Assets + Text: label-economy-stats-assets-header Align: Right Shadow: True Label@EARNED_HEADER: @@ -469,7 +469,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Earned + Text: label-economy-stats-earned-header Align: Right Shadow: True Label@SPENT_HEADER: @@ -477,7 +477,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Spent + Text: label-economy-stats-spent-header Align: Right Shadow: True Label@HARVESTERS_HEADER: @@ -485,7 +485,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Harvesters + Text: label-economy-stats-harvesters-header Align: Right Shadow: True Label@DERRICKS_HEADER: @@ -493,7 +493,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: Oil Derricks + Text: label-economy-stats-derricks-header Align: Right Shadow: True Container@PRODUCTION_STATS_HEADERS: @@ -521,7 +521,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-production-stats-player-header Align: Left Shadow: True Label@PRODUCTION_HEADER: @@ -530,7 +530,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Production + Text: label-production-stats-header Shadow: True Container@SUPPORT_POWERS_HEADERS: X: 0 @@ -557,7 +557,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-support-powers-player-header Align: Left Shadow: True Label@SUPPORT_POWERS_HEADER: @@ -566,7 +566,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Support Powers + Text: label-support-powers-header Shadow: True Container@ARMY_HEADERS: X: 0 @@ -593,7 +593,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-army-player-header Align: Left Shadow: True Label@ARMY_HEADER: @@ -602,7 +602,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Army + Text: label-army-header Shadow: True Container@COMBAT_STATS_HEADERS: X: 0 @@ -629,7 +629,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-combat-stats-player-header Align: Left Shadow: True Label@ASSETS_DESTROYED_HEADER: @@ -638,7 +638,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: Destroyed + Text: label-combat-stats-assets-destroyed-header Align: Right Shadow: True Label@ASSETS_LOST_HEADER: @@ -647,7 +647,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: Lost + Text: label-combat-stats-assets-lost-header Align: Right Shadow: True Label@UNITS_KILLED_HEADER: @@ -656,7 +656,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: U. Killed + Text: label-combat-stats-units-killed-header Align: Right Shadow: True Label@UNITS_DEAD_HEADER: @@ -665,7 +665,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: Units Lost + Text: label-combat-stats-units-dead-header Align: Right Shadow: True Label@BUILDINGS_KILLED_HEADER: @@ -674,7 +674,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: B. Killed + Text: label-combat-stats-buildings-killed-header Align: Right Shadow: True Label@BUILDINGS_DEAD_HEADER: @@ -683,7 +683,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: B. Lost + Text: label-combat-stats-buildings-dead-header Align: Right Shadow: True Label@ARMY_VALUE_HEADER: @@ -692,7 +692,7 @@ Container@OBSERVER_WIDGETS: Width: 90 Height: PARENT_BOTTOM Font: Bold - Text: Army Value + Text: label-combat-stats-army-value-header Align: Right Shadow: True Label@VISION_HEADER: @@ -701,7 +701,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Vision + Text: label-combat-stats-vision-header Align: Right Shadow: True ScrollPanel@PLAYER_STATS_PANEL: @@ -1215,8 +1215,8 @@ Container@PLAYER_WIDGETS: Y: 1 TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: SUPPORT_POWER_TOOLTIP_FACTIONSUFFIX - ReadyText: Ready - HoldText: On Hold + ReadyText: supportpowers-support-powers-palette.ready + HoldText: supportpowers-support-powers-palette.hold HotkeyPrefix: SupportPower HotkeyCount: 6 Image@COMMAND_BAR_BACKGROUND: @@ -1245,8 +1245,8 @@ Container@PLAYER_WIDGETS: Background: chrome-button-background Key: AttackMove DisableKeySound: true - TooltipText: Attack Move - TooltipDesc: Selected units will move to the desired location\nand attack any enemies they encounter en route.\n\nHold <(Ctrl)> while targeting to order an Assault Move\nthat attacks any units or structures encountered en route.\n\nLeft-click icon then right-click on target location. + TooltipText: button-command-bar-attack-move.tooltip + TooltipDesc: button-command-bar-attack-move.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP_FACTIONSUFFIX Children: @@ -1269,8 +1269,8 @@ Container@PLAYER_WIDGETS: Height: 26 Background: chrome-button-background DisableKeySound: true - TooltipText: Force Move - TooltipDesc: Selected units will move to the desired location\n - Default activity for the target is suppressed\n - Vehicles will attempt to crush enemies at the target location\n - Helicopters will land at the target location\n\nLeft-click icon then right-click on target.\nHold <(Alt)> to activate temporarily while commanding units. + TooltipText: button-command-bar-force-move.tooltip + TooltipDesc: button-command-bar-force-move.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP_FACTIONSUFFIX Children: @@ -1293,8 +1293,8 @@ Container@PLAYER_WIDGETS: Height: 26 Background: chrome-button-background DisableKeySound: true - TooltipText: Force Attack - TooltipDesc: Selected units will attack the targeted unit or location\n - Default activity for the target is suppressed\n - Allows targeting of own or ally forces\n - Long-range artillery units will always target the\n location, ignoring units and buildings\n\nLeft-click icon then right-click on target.\nHold <(Ctrl)> to activate temporarily while commanding units. + TooltipText: button-command-bar-force-attack.tooltip + TooltipDesc: button-command-bar-force-attack.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP_FACTIONSUFFIX Children: @@ -1318,8 +1318,8 @@ Container@PLAYER_WIDGETS: Background: chrome-button-background Key: Guard DisableKeySound: true - TooltipText: Guard - TooltipDesc: Selected units will follow the targeted unit.\n\nLeft-click icon then right-click on target unit. + TooltipText: button-command-bar-guard.tooltip + TooltipDesc: button-command-bar-guard.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX Children: @@ -1344,8 +1344,8 @@ Container@PLAYER_WIDGETS: Key: Deploy DisableKeyRepeat: true DisableKeySound: true - TooltipText: Deploy - TooltipDesc: Selected units will perform their default deploy activity\n - MCVs will unpack into a Construction Yard\n - Construction Yards will re-pack into a MCV\n - Transports will unload their passengers\n\nActs immediately on selected units. + TooltipText: button-command-bar-deploy.tooltip + TooltipDesc: button-command-bar-deploy.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX Children: @@ -1370,8 +1370,8 @@ Container@PLAYER_WIDGETS: Key: Scatter DisableKeyRepeat: true DisableKeySound: true - TooltipText: Scatter - TooltipDesc: Selected units will stop their current activity\nand move to a nearby location.\n\nActs immediately on selected units. + TooltipText: button-command-bar-scatter.tooltip + TooltipDesc: button-command-bar-scatter.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX Children: @@ -1396,8 +1396,8 @@ Container@PLAYER_WIDGETS: Key: Stop DisableKeyRepeat: true DisableKeySound: true - TooltipText: Stop - TooltipDesc: Selected units will stop their current activity.\nSelected buildings will reset their rally point.\n\nActs immediately on selected targets. + TooltipText: button-command-bar-stop.tooltip + TooltipDesc: button-command-bar-stop.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX Children: @@ -1420,8 +1420,8 @@ Container@PLAYER_WIDGETS: Height: 26 Background: chrome-button-background DisableKeySound: true - TooltipText: Waypoint Mode - TooltipDesc: Use Waypoint Mode to give multiple linking commands\nto the selected units. Units will execute the commands\nimmediately upon receiving them.\n\nLeft-click icon then give commands in the game world.\nHold <(Shift)> to activate temporarily while commanding units. + TooltipText: button-command-bar-queue-orders.tooltip + TooltipDesc: button-command-bar-queue-orders.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP_FACTIONSUFFIX Children: @@ -1447,8 +1447,8 @@ Container@PLAYER_WIDGETS: Key: StanceAttackAnything DisableKeyRepeat: true DisableKeySound: true - TooltipText: Attack Anything Stance - TooltipDesc: Set the selected units to Attack Anything stance:\n - Units will attack enemy units and structures on sight\n - Units will pursue attackers across the battlefield + TooltipText: button-stance-bar-attackanything.tooltip + TooltipDesc: button-stance-bar-attackanything.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX Children: @@ -1473,8 +1473,8 @@ Container@PLAYER_WIDGETS: Key: StanceDefend DisableKeyRepeat: true DisableKeySound: true - TooltipText: Defend Stance - TooltipDesc: Set the selected units to Defend stance:\n - Units will attack enemy units on sight\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-defend.tooltip + TooltipDesc: button-stance-bar-defend.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX Children: @@ -1499,8 +1499,8 @@ Container@PLAYER_WIDGETS: Key: StanceReturnFire DisableKeyRepeat: true DisableKeySound: true - TooltipText: Return Fire Stance - TooltipDesc: Set the selected units to Return Fire stance:\n - Units will retaliate against enemies that attack them\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-returnfire.tooltip + TooltipDesc: button-stance-bar-returnfire.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX Children: @@ -1525,8 +1525,8 @@ Container@PLAYER_WIDGETS: Key: StanceHoldFire DisableKeyRepeat: true DisableKeySound: true - TooltipText: Hold Fire Stance - TooltipDesc: Set the selected units to Hold Fire stance:\n - Units will not fire upon enemies\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-holdfire.tooltip + TooltipDesc: button-stance-bar-holdfire.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX Children: @@ -1553,7 +1553,7 @@ Container@PLAYER_WIDGETS: Width: PARENT_RIGHT - 30 Height: 25 Align: Right - Text: Audio Muted + Text: label-mute-indicator Contrast: true Image@SIDEBAR_BACKGROUND: X: WINDOW_RIGHT - WIDTH - 5 @@ -1576,7 +1576,7 @@ Container@PLAYER_WIDGETS: Height: 26 Background: chrome-button-background Key: Sell - TooltipText: Sell + TooltipText: button-top-buttons-sell-tooltip TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX VisualHeight: 0 @@ -1598,7 +1598,7 @@ Container@PLAYER_WIDGETS: Height: 26 Background: chrome-button-background Key: Repair - TooltipText: Repair + TooltipText: button-top-buttons-repair-tooltip TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX VisualHeight: 0 @@ -1620,7 +1620,7 @@ Container@PLAYER_WIDGETS: Height: 26 Background: chrome-button-background Key: PlaceBeacon - TooltipText: Place Beacon + TooltipText: button-top-buttons-beacon-tooltip TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX VisualHeight: 0 @@ -1636,7 +1636,7 @@ Container@PLAYER_WIDGETS: Width: 38 Height: 26 Background: chrome-button-background - TooltipText: Options + TooltipText: button-top-buttons-options-tooltip TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX DisableWorldSounds: true @@ -1755,7 +1755,7 @@ Container@PLAYER_WIDGETS: Width: 38 Height: 26 Background: chrome-button-background - TooltipText: Buildings + TooltipText: button-production-types-building-tooltip TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX ProductionGroup: Building @@ -1777,7 +1777,7 @@ Container@PLAYER_WIDGETS: Width: 38 Height: 26 Background: chrome-button-background - TooltipText: Support + TooltipText: button-production-types-defence-tooltip TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX ProductionGroup: Defence @@ -1799,7 +1799,7 @@ Container@PLAYER_WIDGETS: Width: 38 Height: 26 Background: chrome-button-background - TooltipText: Infantry + TooltipText: button-production-types-infantry-tooltip TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX ProductionGroup: Infantry @@ -1821,7 +1821,7 @@ Container@PLAYER_WIDGETS: Width: 38 Height: 26 Background: chrome-button-background - TooltipText: Vehicles + TooltipText: button-production-types-vehicle-tooltip TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX ProductionGroup: Vehicle @@ -1843,7 +1843,7 @@ Container@PLAYER_WIDGETS: Width: 38 Height: 26 Background: chrome-button-background - TooltipText: Aircraft + TooltipText: button-production-types-aircraft-tooltip TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_TOOLTIP_FACTIONSUFFIX ProductionGroup: Aircraft @@ -1868,8 +1868,8 @@ Container@PLAYER_WIDGETS: Width: 192 TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: PRODUCTION_TOOLTIP_FACTIONSUFFIX - ReadyText: Ready - HoldText: On Hold + ReadyText: productionpalette-player-widgets-production-palette.ready + HoldText: productionpalette-player-widgets-production-palette.hold HotkeyPrefix: Production HotkeyCount: 24 SelectProductionBuildingHotkey: SelectProductionBuilding @@ -1895,3 +1895,4 @@ Background@FMVPLAYER: Y: 0 Width: WINDOW_RIGHT Height: WINDOW_BOTTOM + diff --git a/mods/cnc/chrome/lobby-kickdialogs.yaml b/mods/cnc/chrome/lobby-kickdialogs.yaml index 0525fa15aee7..b1079ea98c8d 100644 --- a/mods/cnc/chrome/lobby-kickdialogs.yaml +++ b/mods/cnc/chrome/lobby-kickdialogs.yaml @@ -16,34 +16,34 @@ Background@KICK_CLIENT_DIALOG: Height: 25 Font: Regular Align: Center - Text: You may also apply a temporary ban, preventing + Text: label-kick-client-dialog-texta Label@TEXTB: Y: 86 Width: PARENT_RIGHT Height: 25 Font: Regular Align: Center - Text: them from joining for the remainder of this game. + Text: label-kick-client-dialog-textb Checkbox@PREVENT_REJOINING_CHECKBOX: X: (PARENT_RIGHT - WIDTH) / 2 Y: 120 Width: 150 Height: 20 Font: Regular - Text: Temporarily Ban + Text: checkbox-kick-client-dialog-prevent-rejoining Button@OK_BUTTON: X: (PARENT_RIGHT - WIDTH) / 2 + 75 Y: 155 Width: 120 Height: 25 - Text: Kick + Text: button-kick-client-dialog-ok Font: Bold Button@CANCEL_BUTTON: X: (PARENT_RIGHT - WIDTH) / 2 - 75 Y: 155 Width: 120 Height: 25 - Text: Cancel + Text: button-kick-client-dialog-cancel Font: Bold Background@KICK_SPECTATORS_DIALOG: @@ -58,7 +58,7 @@ Background@KICK_SPECTATORS_DIALOG: Height: 25 Font: Bold Align: Center - Text: Kick Spectators + Text: label-kick-spectators-dialog-title Label@TEXT: Y: 86 Width: PARENT_RIGHT @@ -70,14 +70,14 @@ Background@KICK_SPECTATORS_DIALOG: Y: 155 Width: 120 Height: 25 - Text: Ok + Text: button-kick-spectators-dialog-ok Font: Bold Button@CANCEL_BUTTON: X: (PARENT_RIGHT - WIDTH) / 2 - 75 Y: 155 Width: 120 Height: 25 - Text: Cancel + Text: button-kick-spectators-dialog-cancel Font: Bold Background@FORCE_START_DIALOG: @@ -91,21 +91,21 @@ Background@FORCE_START_DIALOG: Height: 25 Font: Bold Align: Center - Text: Start Game? + Text: label-force-start-dialog-title Label@TEXTA: Y: 68 Width: PARENT_RIGHT Height: 25 Font: Regular Align: Center - Text: One or more players are not yet ready. + Text: label-force-start-dialog-texta Label@TEXTB: Y: 86 Width: PARENT_RIGHT Height: 25 Font: Regular Align: Center - Text: Are you sure that you want to force start the game? + Text: label-force-start-dialog-textb Container@KICK_WARNING: Width: PARENT_RIGHT Children: @@ -116,7 +116,7 @@ Background@FORCE_START_DIALOG: Height: 25 Font: Bold Align: Center - Text: One or more clients are missing the selected + Text: label-kick-warning-a Label@KICK_WARNING_B: X: 0 Y: 124 @@ -124,18 +124,19 @@ Background@FORCE_START_DIALOG: Height: 25 Font: Bold Align: Center - Text: map, and will be kicked from the server. + Text: label-kick-warning-b Button@OK_BUTTON: X: (PARENT_RIGHT - WIDTH) / 2 + 75 Y: 155 Width: 120 Height: 25 - Text: Start + Text: button-force-start-dialog-ok Font: Bold Button@CANCEL_BUTTON: X: (PARENT_RIGHT - WIDTH) / 2 - 75 Y: 155 Width: 120 Height: 25 - Text: Cancel + Text: button-force-start-dialog-cancel Font: Bold + diff --git a/mods/cnc/chrome/lobby-mappreview.yaml b/mods/cnc/chrome/lobby-mappreview.yaml index 2f6f14fb028a..aa43e03ecab4 100644 --- a/mods/cnc/chrome/lobby-mappreview.yaml +++ b/mods/cnc/chrome/lobby-mappreview.yaml @@ -76,7 +76,7 @@ Container@MAP_PREVIEW: Height: 25 Font: Tiny Align: Center - Text: This map is not compatible + Text: label-map-incompatible-status-a IgnoreMouseOver: true Label@MAP_STATUS_B: Y: 201 @@ -84,7 +84,7 @@ Container@MAP_PREVIEW: Height: 25 Font: Tiny Align: Center - Text: with this version of OpenRA + Text: label-map-incompatible-status-b Container@MAP_VALIDATING: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -95,7 +95,7 @@ Container@MAP_PREVIEW: Height: 25 Font: Tiny Align: Center - Text: Validating... + Text: label-map-validating-status IgnoreMouseOver: true ProgressBar@MAP_VALIDATING_BAR: Y: 194 @@ -123,12 +123,12 @@ Container@MAP_PREVIEW: Y: 194 Width: PARENT_RIGHT Height: 25 - Text: Install Map + Text: button-map-download-available-install Button@MAP_UPDATE: Y: 195 Width: PARENT_RIGHT Height: 25 - Text: Update Map + Text: button-map-preview-update Container@MAP_UPDATE_DOWNLOAD_AVAILABLE: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -137,14 +137,14 @@ Container@MAP_PREVIEW: Y: 166 Width: PARENT_RIGHT Height: 25 - Text: Install Map + Text: button-map-update-download-available-install Label@MAP_SEARCHING: Y: 158 Width: PARENT_RIGHT Height: 25 Font: Tiny Align: Center - Text: Searching OpenRA Resource Center... + Text: label-map-preview-searching IgnoreMouseOver: true Container@MAP_UNAVAILABLE: Width: PARENT_RIGHT @@ -155,21 +155,21 @@ Container@MAP_PREVIEW: Height: 25 Font: Tiny Align: Center - Text: This map was not found on the + Text: label-map-unavailable-a Label@b: Y: 171 Width: PARENT_RIGHT Height: 25 Font: Tiny Align: Center - Text: OpenRA Resource Center + Text: label-map-unavailable-b Label@MAP_ERROR: Y: 158 Width: PARENT_RIGHT Height: 25 Font: Tiny Align: Center - Text: An error occurred during installation + Text: label-map-preview-error Container@MAP_DOWNLOADING: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -198,11 +198,12 @@ Container@MAP_PREVIEW: Height: 25 Font: Tiny Align: Center - Text: A new version of the map + Text: label-map-update-available-a Label@b: Y: 171 Width: PARENT_RIGHT Height: 25 Font: Tiny Align: Center - Text: was found on your computer + Text: label-map-update-available-b + diff --git a/mods/cnc/chrome/lobby-music.yaml b/mods/cnc/chrome/lobby-music.yaml index 304040d77f83..2f7e9a14e380 100644 --- a/mods/cnc/chrome/lobby-music.yaml +++ b/mods/cnc/chrome/lobby-music.yaml @@ -11,20 +11,20 @@ Container@LOBBY_MUSIC_BIN: Label@MUSIC: Width: 308 Height: 25 - Text: Music + Text: label-container-music Align: Center Font: Bold Label@TITLE: X: 317 Width: 230 Height: 25 - Text: Track + Text: label-container-title Font: Bold Label@LENGTH: X: PARENT_RIGHT - 80 Height: 25 Width: 50 - Text: Length + Text: label-container-length Font: Bold Align: Right Background@CONTROLS: @@ -125,20 +125,20 @@ Container@LOBBY_MUSIC_BIN: Width: 85 Height: 20 Font: Regular - Text: Shuffle + Text: checkbox-controls-shuffle Checkbox@REPEAT: X: PARENT_RIGHT - 15 - WIDTH Y: 150 Width: 70 Height: 20 Font: Regular - Text: Loop + Text: checkbox-controls-repeat Label@VOLUME_LABEL: Y: 182 Width: 65 Height: 25 Align: Right - Text: Volume: + Text: label-controls-volume ExponentialSlider@MUSIC_SLIDER: X: 70 Y: 186 @@ -179,16 +179,17 @@ Container@LOBBY_MUSIC_BIN: Height: 25 Font: Bold Align: Center - Text: Music Not Installed + Text: label-no-music-title Label@DESCA: Y: 95 Width: PARENT_RIGHT - 24 Height: 25 Align: Center - Text: The game music can be installed + Text: label-no-music-desca Label@DESCB: Y: 115 Width: PARENT_RIGHT - 24 Height: 25 Align: Center - Text: from the "Manage Content" menu. + Text: label-no-music-descb + diff --git a/mods/cnc/chrome/lobby-options.yaml b/mods/cnc/chrome/lobby-options.yaml index 48eaaa945bec..6417813e57b8 100644 --- a/mods/cnc/chrome/lobby-options.yaml +++ b/mods/cnc/chrome/lobby-options.yaml @@ -8,7 +8,7 @@ Container@LOBBY_OPTIONS_BIN: Height: 25 Font: Bold Align: Center - Text: Map Options + Text: label-lobby-options-bin-title ScrollPanel: Logic: LobbyOptionsLogic Width: PARENT_RIGHT @@ -89,3 +89,4 @@ Container@LOBBY_OPTIONS_BIN: Font: Regular Visible: False TooltipContainer: TOOLTIP_CONTAINER + diff --git a/mods/cnc/chrome/lobby-players.yaml b/mods/cnc/chrome/lobby-players.yaml index 0ad22fe1368c..997f62f31f81 100644 --- a/mods/cnc/chrome/lobby-players.yaml +++ b/mods/cnc/chrome/lobby-players.yaml @@ -11,49 +11,49 @@ Container@LOBBY_PLAYER_BIN: Label@LABEL_LOBBY_NAME: Width: 200 Height: 25 - Text: Player + Text: label-container-lobby-name Align: Center Font: Bold Label@LABEL_LOBBY_COLOR: X: 210 Width: 94 Height: 25 - Text: Color + Text: label-container-lobby-color Align: Center Font: Bold Label@LABEL_LOBBY_FACTION: X: 309 Width: 120 Height: 25 - Text: Faction + Text: label-container-lobby-faction Align: Center Font: Bold Label@LABEL_LOBBY_TEAM: X: 460 - 25 Width: 50 Height: 25 - Text: Team + Text: label-container-lobby-team Align: Center Font: Bold Label@LABEL_LOBBY_HANDICAP: X: 491 Width: 75 Height: 25 - Text: Handicap + Text: label-container-lobby-handicap Align: Center Font: Bold Label@LABEL_LOBBY_SPAWN: X: 571 Width: 50 Height: 25 - Text: Spawn + Text: label-container-lobby-spawn Align: Left Font: Bold Label@LABEL_LOBBY_STATUS: X: 627 Width: 20 Height: 25 - Text: Ready + Text: label-container-lobby-status Align: Left Font: Bold ScrollPanel@LOBBY_PLAYERS: @@ -110,7 +110,7 @@ Container@LOBBY_PLAYER_BIN: X: 15 Width: 190 Height: 25 - Text: Name + Text: dropdownbutton-template-editable-player-slot-options Font: Regular Visible: false DropDownButton@COLOR: @@ -143,7 +143,7 @@ Container@LOBBY_PLAYER_BIN: X: 40 Width: 50 Height: 25 - Text: Faction + Text: label-template-editable-player-factionname DropDownButton@TEAM_DROPDOWN: X: 435 Width: 50 @@ -155,7 +155,7 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Font: Regular TooltipContainer: TOOLTIP_CONTAINER - TooltipText: A handicap decreases the combat effectiveness of the player's forces + TooltipText: dropdownbutton-template-editable-player-handicap-dropdown-tooltip DropDownButton@SPAWN_DROPDOWN: X: 571 Width: 50 @@ -254,7 +254,7 @@ Container@LOBBY_PLAYER_BIN: X: 40 Width: 50 Height: 25 - Text: Faction + Text: label-faction-factionname Label@TEAM: X: 435 Width: 25 @@ -277,7 +277,7 @@ Container@LOBBY_PLAYER_BIN: Height: 25 Font: Regular TooltipContainer: TOOLTIP_CONTAINER - TooltipText: A handicap decreases the combat effectiveness of the player's forces + TooltipText: dropdownbutton-template-noneditable-player-handicap-dropdown-tooltip Label@SPAWN: X: 571 Width: 25 @@ -307,7 +307,7 @@ Container@LOBBY_PLAYER_BIN: X: 15 Width: 190 Height: 25 - Text: Name + Text: dropdownbutton-template-empty-slot-options Font: Regular Visible: false Label@NAME: @@ -319,7 +319,7 @@ Container@LOBBY_PLAYER_BIN: X: 210 Width: 438 Height: 25 - Text: Play in this slot + Text: button-template-empty-join Font: Regular Container@TEMPLATE_EDITABLE_SPECTATOR: X: 5 @@ -368,7 +368,7 @@ Container@LOBBY_PLAYER_BIN: X: 210 Width: 441 Height: 25 - Text: Spectator + Text: label-template-editable-spectator Align: Center Font: Bold Image@STATUS_IMAGE: @@ -449,7 +449,7 @@ Container@LOBBY_PLAYER_BIN: X: 210 Width: 441 Height: 25 - Text: Spectator + Text: label-template-noneditable-spectator Align: Center Font: Bold Image@STATUS_IMAGE: @@ -471,10 +471,11 @@ Container@LOBBY_PLAYER_BIN: Width: 190 Height: 20 Font: Regular - Text: Allow Spectators? + Text: checkbox-template-new-spectator-toggle-spectators Button@SPECTATE: X: 210 Width: 438 Height: 25 - Text: Spectate + Text: button-template-new-spectator-spectate Font: Regular + diff --git a/mods/cnc/chrome/lobby-servers.yaml b/mods/cnc/chrome/lobby-servers.yaml index 05525faa03e3..38287ee4d024 100644 --- a/mods/cnc/chrome/lobby-servers.yaml +++ b/mods/cnc/chrome/lobby-servers.yaml @@ -11,26 +11,26 @@ Container@LOBBY_SERVERS_BIN: X: 5 Width: 355 Height: 25 - Text: Server + Text: label-container-name Align: Center Font: Bold Label@PLAYERS: X: 390 Width: 85 Height: 25 - Text: Players + Text: label-container-players Font: Bold Label@LOCATION: X: 480 Width: 110 Height: 25 - Text: Location + Text: label-container-location Font: Bold Label@STATUS: X: 595 Width: 50 Height: 25 - Text: Status + Text: label-container-status Font: Bold LogicTicker@NOTICE_WATCHER: Container@NOTICE_CONTAINER: @@ -47,21 +47,21 @@ Container@LOBBY_SERVERS_BIN: Width: PARENT_RIGHT - 10 Height: 18 Align: Center - Text: You are running an outdated version of OpenRA. Download the latest version from www.openra.net + Text: label-bg-outdated-version Font: TinyBold Label@UNKNOWN_VERSION_LABEL: X: 5 Width: PARENT_RIGHT - 10 Height: 18 Align: Center - Text: You are running an unrecognized version of OpenRA. Download the latest version from www.openra.net + Text: label-bg-unknown-version Font: TinyBold Label@PLAYTEST_AVAILABLE_LABEL: X: 5 Width: PARENT_RIGHT - 10 Height: 18 Align: Center - Text: A preview of the next OpenRA release is available for testing. Download the playtest from www.openra.net + Text: label-bg-playtest-available Font: TinyBold ScrollPanel@SERVER_LIST: Width: PARENT_RIGHT @@ -100,7 +100,7 @@ Container@LOBBY_SERVERS_BIN: ImageCollection: lobby-bits TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP - TooltipText: Requires Password + TooltipText: image-lobby-servers-bin-password-protected-tooltip Image@REQUIRES_AUTHENTICATION: X: 372 Y: 6 @@ -109,7 +109,7 @@ Container@LOBBY_SERVERS_BIN: ImageCollection: lobby-bits TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP - TooltipText: Requires OpenRA forum account + TooltipText: image-lobby-servers-bin-requires-authentication-tooltip LabelWithTooltip@PLAYERS: X: 390 Width: 85 @@ -135,7 +135,7 @@ Container@LOBBY_SERVERS_BIN: Y: PARENT_BOTTOM + 5 Width: 180 Height: 25 - Text: Filter Games + Text: dropdownbutton-lobby-servers-bin-filters Font: Bold Button@RELOAD_BUTTON: X: 185 @@ -201,3 +201,4 @@ Container@LOBBY_SERVERS_BIN: Height: 25 Font: TinyBold Align: Center + diff --git a/mods/cnc/chrome/lobby.yaml b/mods/cnc/chrome/lobby.yaml index 925a48336ed8..9914a4be3bbf 100644 --- a/mods/cnc/chrome/lobby.yaml +++ b/mods/cnc/chrome/lobby.yaml @@ -33,7 +33,7 @@ Container@SERVER_LOBBY: Y: 254 Width: 211 Height: 25 - Text: Slot Admin + Text: dropdownbutton-bg-slots Container@SKIRMISH_TABS: X: 697 - WIDTH Width: 465 @@ -43,19 +43,19 @@ Container@SERVER_LOBBY: Y: 248 Width: 151 Height: 31 - Text: Players + Text: button-skirmish-tabs-players-tab Button@OPTIONS_TAB: X: 157 Y: 248 Width: 151 Height: 31 - Text: Options + Text: button-skirmish-tabs-options-tab Button@MUSIC_TAB: X: 314 Y: 248 Width: 151 Height: 31 - Text: Music + Text: button-skirmish-tabs-music-tab Container@MULTIPLAYER_TABS: X: 697 - WIDTH Width: 465 @@ -65,31 +65,31 @@ Container@SERVER_LOBBY: Y: 248 Width: 112 Height: 31 - Text: Players + Text: button-multiplayer-tabs-players-tab Button@OPTIONS_TAB: X: 118 Y: 248 Width: 112 Height: 31 - Text: Options + Text: button-multiplayer-tabs-options-tab Button@MUSIC_TAB: X: 236 Y: 248 Width: 112 Height: 31 - Text: Music + Text: button-multiplayer-tabs-music-tab Button@SERVERS_TAB: X: 354 Y: 248 Width: 111 Height: 31 - Text: Servers + Text: button-multiplayer-tabs-servers-tab Button@CHANGEMAP_BUTTON: X: PARENT_RIGHT - WIDTH - 15 Y: 254 Width: 174 Height: 25 - Text: Change Map + Text: button-bg-changemap Container@TOP_PANELS_ROOT: X: 15 Y: 30 @@ -110,10 +110,10 @@ Container@SERVER_LOBBY: Y: PARENT_BOTTOM - HEIGHT Width: 50 Height: 25 - Text: Team + Text: button-lobbychat-chat-mode.label Font: Bold Key: ToggleChatMode - TooltipText: Toggle chat mode + TooltipText: button-lobbychat-chat-mode.tooltip TooltipContainer: TOOLTIP_CONTAINER TextField@CHAT_TEXTFIELD: X: 55 @@ -124,12 +124,13 @@ Container@SERVER_LOBBY: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Leave Game + Text: button-server-lobby-disconnect Button@START_GAME_BUTTON: X: PARENT_RIGHT - WIDTH Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Start Game + Text: button-server-lobby-start-game Container@FACTION_DROPDOWN_PANEL_ROOT: TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/cnc/chrome/mainmenu-prompts.yaml b/mods/cnc/chrome/mainmenu-prompts.yaml index 44c456c5ad71..75c3044c83da 100644 --- a/mods/cnc/chrome/mainmenu-prompts.yaml +++ b/mods/cnc/chrome/mainmenu-prompts.yaml @@ -11,7 +11,7 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Font: BigBold Contrast: true Align: Center - Text: Establishing Battlefield Control + Text: label-mainmenu-introduction-prompt-title Background@bg: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -23,14 +23,14 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Height: 16 Font: Regular Align: Center - Text: Welcome back Commander! Initialize combat parameters using the options below. + Text: label-bg-desc-a Label@DESC_B: Width: PARENT_RIGHT Y: 33 Height: 16 Font: Regular Align: Center - Text: Additional options can be configured later from the Settings menu. + Text: label-bg-desc-b ScrollPanel@SETTINGS_SCROLLPANEL: X: 15 Y: 60 @@ -53,7 +53,7 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Profile + Text: label-profile-section-header Container@ROW: Width: PARENT_RIGHT Height: 50 @@ -65,7 +65,7 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Label@PLAYER: Width: PARENT_RIGHT Height: 20 - Text: Player Name: + Text: label-player-container TextField@PLAYERNAME: Y: 25 Width: PARENT_RIGHT @@ -79,7 +79,7 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Label@COLOR: Width: PARENT_RIGHT Height: 20 - Text: Preferred Color: + Text: label-playercolor-container-color DropDownButton@PLAYERCOLOR: Y: 25 Width: 75 @@ -105,7 +105,7 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Input + Text: label-input-section-header Container@ROW: Width: PARENT_RIGHT Height: 50 @@ -118,7 +118,7 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Control Scheme: + Text: label-mouse-control-container DropDownButton@MOUSE_CONTROL_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -131,48 +131,48 @@ Container@MAINMENU_INTRODUCTION_PROMPT: LabelWithHighlight@DESC_SELECTION: Height: 16 Font: Small - Text: - Select units using the mouse button + Text: label-mouse-control-desc-classic-selection LabelWithHighlight@DESC_COMMANDS: Y: 17 Height: 16 Font: Small - Text: - Command units using the mouse button + Text: label-mouse-control-desc-classic-commands LabelWithHighlight@DESC_BUILDIGS: Y: 34 Height: 16 Font: Small - Text: - Place structures using the mouse button + Text: label-mouse-control-desc-classic-buildigs LabelWithHighlight@DESC_SUPPORT: Y: 51 Height: 16 Font: Small - Text: - Target support powers using the mouse button + Text: label-mouse-control-desc-classic-support LabelWithHighlight@DESC_ZOOM: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using the + Text: label-mouse-control-desc-classic-zoom LabelWithHighlight@DESC_ZOOM_MODIFIER: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using + Text: label-mouse-control-desc-classic-zoom-modifier LabelWithHighlight@DESC_SCROLL_RIGHT: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-classic-scroll-right LabelWithHighlight@DESC_SCROLL_MIDDLE: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-classic-scroll-middle Label@DESC_EDGESCROLL: X: 9 Y: 102 Height: 16 Font: Small - Text: or by moving the cursor to the edge of the screen + Text: label-mouse-control-desc-classic-edgescroll Container@MOUSE_CONTROL_DESC_MODERN: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -180,48 +180,48 @@ Container@MAINMENU_INTRODUCTION_PROMPT: LabelWithHighlight@DESC_SELECTION: Height: 16 Font: Small - Text: - Select units using the mouse button + Text: label-mouse-control-desc-modern-selection LabelWithHighlight@DESC_COMMANDS: Y: 17 Height: 16 Font: Small - Text: - Command units using the mouse button + Text: label-mouse-control-desc-modern-commands LabelWithHighlight@DESC_BUILDIGS: Y: 34 Height: 16 Font: Small - Text: - Place structures using the mouse button + Text: label-mouse-control-desc-modern-buildigs LabelWithHighlight@DESC_SUPPORT: Y: 51 Height: 16 Font: Small - Text: - Target support powers using the mouse button + Text: label-mouse-control-desc-modern-support LabelWithHighlight@DESC_ZOOM: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using the + Text: label-mouse-control-desc-modern-zoom LabelWithHighlight@DESC_ZOOM_MODIFIER: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using + Text: label-mouse-control-desc-modern-zoom-modifier LabelWithHighlight@DESC_SCROLL_RIGHT: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-modern-scroll-right LabelWithHighlight@DESC_SCROLL_MIDDLE: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-modern-scroll-middle Label@DESC_EDGESCROLL: X: 9 Y: 102 Height: 16 Font: Small - Text: or by moving the cursor to the edge of the screen + Text: label-mouse-control-desc-modern-edgescroll Container@ROW: Width: PARENT_RIGHT Height: 20 @@ -234,7 +234,7 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Screen Edge Panning + Text: checkbox-edgescroll-container Container@SPACER: Height: 30 Background@DISPLAY_SECTION_HEADER: @@ -249,7 +249,7 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Display + Text: label-display-section-header Container@ROW: Width: PARENT_RIGHT Height: 50 @@ -261,7 +261,7 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Label@BATTLEFIELD_CAMERA: Width: PARENT_RIGHT Height: 20 - Text: Battlefield Camera: + Text: label-battlefield-camera-dropdown-container DropDownButton@BATTLEFIELD_CAMERA_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -274,7 +274,7 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Label@UI_SCALE: Width: PARENT_RIGHT Height: 20 - Text: UI Scale: + Text: label-ui-scale-dropdown-container DropDownButton@UI_SCALE_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -292,13 +292,13 @@ Container@MAINMENU_INTRODUCTION_PROMPT: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Increase Cursor Size + Text: checkbox-cursordouble-container Button@CONTINUE_BUTTON: X: PARENT_RIGHT - WIDTH Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Continue + Text: button-mainmenu-introduction-prompt-continue Font: Bold Key: return @@ -315,7 +315,7 @@ Container@MAINMENU_SYSTEM_INFO_PROMPT: Font: BigBold Contrast: true Align: Center - Text: Establishing Battlefield Control + Text: label-mainmenu-system-info-prompt-title Background@bg: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -327,14 +327,14 @@ Container@MAINMENU_SYSTEM_INFO_PROMPT: Width: PARENT_RIGHT - 30 Height: 16 Align: Center - Text: We would like to collect some details that will help us optimize OpenRA. + Text: label-bg-prompt-text-a Label@PROMPT_TEXT_B: X: 15 Y: 33 Width: PARENT_RIGHT - 30 Height: 16 Align: Center - Text: With your permission, the following anonymous system data will be sent: + Text: label-bg-prompt-text-b ScrollPanel@SYSINFO_DATA: X: 15 Y: 63 @@ -354,12 +354,13 @@ Container@MAINMENU_SYSTEM_INFO_PROMPT: Width: 190 Height: 20 Font: Regular - Text: Send System Information + Text: checkbox-bg-sysinfo Button@CONTINUE_BUTTON: X: PARENT_RIGHT - WIDTH Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Continue + Text: button-mainmenu-system-info-prompt-continue Font: Bold Key: return + diff --git a/mods/cnc/chrome/mainmenu.yaml b/mods/cnc/chrome/mainmenu.yaml index b5c61008c903..67cd5beee014 100644 --- a/mods/cnc/chrome/mainmenu.yaml +++ b/mods/cnc/chrome/mainmenu.yaml @@ -54,7 +54,7 @@ Container@MENU_BACKGROUND: Y: 0 - 28 Width: PARENT_RIGHT Height: 20 - Text: Main Menu + Text: label-main-menu-mainmenu-title Align: Center Font: Bold Contrast: True @@ -63,37 +63,37 @@ Container@MENU_BACKGROUND: Y: 0 Width: 140 Height: 35 - Text: Singleplayer + Text: button-main-menu-singleplayer Button@MULTIPLAYER_BUTTON: X: 150 Y: 0 Width: 140 Height: 35 - Text: Multiplayer + Text: button-main-menu-multiplayer Button@SETTINGS_BUTTON: X: 300 Y: 0 Width: 140 Height: 35 - Text: Settings + Text: button-main-menu-settings Button@EXTRAS_BUTTON: X: 450 Y: 0 Width: 140 Height: 35 - Text: Extras + Text: button-main-menu-extras Button@CONTENT_BUTTON: X: 600 Y: 0 Width: 140 Height: 35 - Text: Manage Content + Text: button-main-menu-content Button@QUIT_BUTTON: X: 750 Y: 0 Width: 140 Height: 35 - Text: Quit + Text: button-main-menu-quit Container@SINGLEPLAYER_MENU: Width: PARENT_RIGHT Visible: False @@ -103,7 +103,7 @@ Container@MENU_BACKGROUND: Y: 0 - 28 Width: PARENT_RIGHT Height: 20 - Text: Singleplayer + Text: label-singleplayer-menu-title Align: Center Font: Bold Contrast: True @@ -112,26 +112,26 @@ Container@MENU_BACKGROUND: Y: 0 Width: 140 Height: 35 - Text: Skirmish + Text: button-singleplayer-menu-skirmish Button@MISSIONS_BUTTON: X: 150 Y: 0 Width: 140 Height: 35 - Text: Missions + Text: button-singleplayer-menu-missions Button@LOAD_BUTTON: X: 300 Y: 0 Width: 140 Height: 35 - Text: Load + Text: button-singleplayer-menu-load Button@BACK_BUTTON: Key: escape X: 450 Y: 0 Width: 140 Height: 35 - Text: Back + Text: button-singleplayer-menu-back Container@EXTRAS_MENU: Width: PARENT_RIGHT Visible: False @@ -141,7 +141,7 @@ Container@MENU_BACKGROUND: Y: 0 - 28 Width: PARENT_RIGHT Height: 20 - Text: Extras + Text: label-extras-menu-title Align: Center Font: Bold Contrast: True @@ -150,39 +150,39 @@ Container@MENU_BACKGROUND: Y: 0 Width: 140 Height: 35 - Text: Replays + Text: button-extras-menu-replays Button@MUSIC_BUTTON: X: 150 Y: 0 Width: 140 Height: 35 - Text: Music + Text: button-extras-menu-music Button@MAP_EDITOR_BUTTON: X: 300 Y: 0 Width: 140 Height: 35 - Text: Map Editor + Text: button-extras-menu-map-editor Font: Bold Button@ASSETBROWSER_BUTTON: X: 450 Y: 0 Width: 140 Height: 35 - Text: Asset Browser + Text: button-extras-menu-assetbrowser Button@CREDITS_BUTTON: X: 600 Y: 0 Width: 140 Height: 35 - Text: Credits + Text: button-extras-menu-credits Button@BACK_BUTTON: Key: escape X: 750 Y: 0 Width: 140 Height: 35 - Text: Back + Text: button-extras-menu-back Container@MAP_EDITOR_MENU: Width: PARENT_RIGHT Visible: False @@ -192,7 +192,7 @@ Container@MENU_BACKGROUND: Y: 0 - 28 Width: PARENT_RIGHT Height: 20 - Text: Map Editor + Text: label-map-editor-menu-title Align: Center Font: Bold Contrast: True @@ -201,21 +201,21 @@ Container@MENU_BACKGROUND: Y: 0 Width: 140 Height: 35 - Text: New Map + Text: button-map-editor-menu-new Font: Bold Button@LOAD_MAP_BUTTON: X: 150 Y: 0 Width: 140 Height: 35 - Text: Load Map + Text: button-map-editor-menu-load Font: Bold Button@BACK_BUTTON: X: 300 Y: 0 Width: 140 Height: 35 - Text: Back + Text: button-map-editor-menu-back Font: Bold Key: escape Container@NEWS_BG: @@ -225,7 +225,7 @@ Container@MENU_BACKGROUND: Y: 50 Width: 400 Height: 25 - Text: Battlefield News + Text: dropdownbutton-news-bg-button Font: Bold Container@UPDATE_NOTICE: X: (WINDOW_RIGHT - WIDTH) / 2 @@ -237,14 +237,14 @@ Container@MENU_BACKGROUND: Height: 25 Align: Center Shadow: true - Text: You are running an outdated version of OpenRA. + Text: label-update-notice-a Label@B: Y: 20 Width: PARENT_RIGHT Height: 25 Align: Center Shadow: true - Text: Download the latest version from www.openra.net + Text: label-update-notice-b Container@PERFORMANCE_INFO: Logic: PerfDebugLogic Children: @@ -269,3 +269,4 @@ Container@MENU_BACKGROUND: Container@PLAYER_PROFILE_CONTAINER: X: 31 Y: 31 + diff --git a/mods/cnc/chrome/mapchooser.yaml b/mods/cnc/chrome/mapchooser.yaml index 55152df7928f..2d294bed1f29 100644 --- a/mods/cnc/chrome/mapchooser.yaml +++ b/mods/cnc/chrome/mapchooser.yaml @@ -11,7 +11,7 @@ Container@MAPCHOOSER_PANEL: Font: BigBold Contrast: true Align: Center - Text: Select Map + Text: label-mapchooser-panel-title Background@bg: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -22,13 +22,13 @@ Container@MAPCHOOSER_PANEL: Y: 15 Height: 31 Width: 135 - Text: Official Maps + Text: button-bg-system-maps-tab Button@USER_MAPS_TAB_BUTTON: X: 155 Y: 15 Height: 31 Width: 135 - Text: Custom Maps + Text: button-bg-user-maps-tab Container@MAP_TAB_PANES: Width: PARENT_RIGHT - 30 Height: PARENT_BOTTOM - 90 @@ -107,7 +107,7 @@ Container@MAPCHOOSER_PANEL: Height: 24 Font: Bold Align: Right - Text: Filter: + Text: label-filter-order-controls-desc TextField@MAPFILTER_INPUT: X: 45 Width: 150 @@ -118,7 +118,7 @@ Container@MAPCHOOSER_PANEL: Height: 24 Font: Bold Align: Center - Text: in + Text: label-filter-order-controls-desc-joiner DropDownButton@GAMEMODE_FILTER: X: 225 Width: 200 @@ -129,7 +129,7 @@ Container@MAPCHOOSER_PANEL: Height: 24 Font: Bold Align: Right - Text: Order by: + Text: label-filter-order-controls-orderby DropDownButton@ORDERBY: X: PARENT_RIGHT - WIDTH Width: 200 @@ -139,31 +139,32 @@ Container@MAPCHOOSER_PANEL: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Back + Text: button-bg-cancel Button@RANDOMMAP_BUTTON: Key: space X: PARENT_RIGHT - 150 - WIDTH Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Random + Text: button-bg-randommap Button@DELETE_MAP_BUTTON: X: PARENT_RIGHT - 300 - WIDTH Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Delete Map + Text: button-bg-delete-map Button@DELETE_ALL_MAPS_BUTTON: X: PARENT_RIGHT - 450 - WIDTH Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Delete All Maps + Text: button-bg-delete-all-maps Button@BUTTON_OK: Key: return X: PARENT_RIGHT - WIDTH Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Ok + Text: button-bg-ok TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/cnc/chrome/missionbrowser.yaml b/mods/cnc/chrome/missionbrowser.yaml index 5953d0185a73..2502a6ff3917 100644 --- a/mods/cnc/chrome/missionbrowser.yaml +++ b/mods/cnc/chrome/missionbrowser.yaml @@ -2,13 +2,13 @@ Container@MISSIONBROWSER_PANEL: Logic: MissionBrowserLogic X: (WINDOW_RIGHT - WIDTH) / 2 Y: (WINDOW_BOTTOM - HEIGHT) / 2 - Width: 714 + Width: 716 Height: 435 Children: Label@MISSIONBROWSER_TITLE: Y: 0 - 22 Width: PARENT_RIGHT - Text: Missions + Text: label-missionbrowser-panel-title Align: Center Contrast: true Font: BigBold @@ -20,7 +20,7 @@ Container@MISSIONBROWSER_PANEL: ScrollPanel@MISSION_LIST: X: 15 Y: 15 - Width: 311 + Width: 313 Height: PARENT_BOTTOM - 30 Children: ScrollItem@HEADER: @@ -49,7 +49,7 @@ Container@MISSIONBROWSER_PANEL: TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP Container@MISSION_INFO: - X: 337 + X: 339 Y: 15 Width: 362 Height: PARENT_BOTTOM - 30 @@ -67,47 +67,94 @@ Container@MISSIONBROWSER_PANEL: IgnoreMouseOver: True IgnoreMouseInput: True ShowSpawnPoints: False - ScrollPanel@MISSION_DESCRIPTION_PANEL: + Container@MISSION_TABS: + Width: PARENT_RIGHT + Y: PARENT_BOTTOM - 31 + Children: + Button@MISSIONINFO_TAB: + Width: 178 + Height: 31 + Font: Bold + Text: button-missionbrowser-panel-mission-info + Button@OPTIONS_TAB: + X: 184 + Width: 178 + Height: 31 + Font: Bold + Text: button-missionbrowser-panel-mission-options + Container@MISSION_DETAIL: Y: 213 Width: PARENT_RIGHT - Height: 157 - TopBottomSpacing: 5 + Height: PARENT_BOTTOM - 213 - 30 Children: - Label@MISSION_DESCRIPTION: - X: 4 - Width: PARENT_RIGHT - 32 - VAlign: Top - Font: Small - Label@DIFFICULTY_DESC: - Y: PARENT_BOTTOM - HEIGHT - Width: 56 - Height: 25 - Text: Difficulty: - Align: Right - DropDownButton@DIFFICULTY_DROPDOWNBUTTON: - X: 61 - Y: PARENT_BOTTOM - HEIGHT - Width: 120 - Height: 25 - Font: Regular - Label@GAMESPEED_DESC: - X: PARENT_RIGHT - WIDTH - 125 - Y: PARENT_BOTTOM - HEIGHT - Width: 120 - Height: 25 - Text: Speed: - Align: Right - DropDownButton@GAMESPEED_DROPDOWNBUTTON: - X: PARENT_RIGHT - WIDTH - Y: PARENT_BOTTOM - HEIGHT - Width: 120 - Height: 25 - Font: Regular + ScrollPanel@MISSION_DESCRIPTION_PANEL: + Height: PARENT_BOTTOM + Width: PARENT_RIGHT + TopBottomSpacing: 5 + Children: + Label@MISSION_DESCRIPTION: + X: 4 + Width: PARENT_RIGHT - 32 + VAlign: Top + Font: Small + ScrollPanel@MISSION_OPTIONS: + Height: PARENT_BOTTOM + Width: PARENT_RIGHT + TopBottomSpacing: 5 + Children: + Container@CHECKBOX_ROW_TEMPLATE: + Width: PARENT_RIGHT + Height: 30 + Children: + Checkbox@A: + X: 10 + Width: PARENT_RIGHT / 2 - 25 + Height: 20 + Visible: False + TooltipContainer: TOOLTIP_CONTAINER + Checkbox@B: + X: PARENT_RIGHT / 2 + 5 + Width: PARENT_RIGHT / 2 - 25 + Height: 20 + Visible: False + TooltipContainer: TOOLTIP_CONTAINER + Container@DROPDOWN_ROW_TEMPLATE: + Height: 60 + Width: PARENT_RIGHT + Children: + LabelForInput@A_DESC: + X: 10 + Width: PARENT_RIGHT / 2 - 35 + Height: 20 + Visible: False + For: A + DropDownButton@A: + X: 10 + Width: PARENT_RIGHT / 2 - 35 + Y: 25 + Height: 25 + Visible: False + PanelRoot: MISSION_DROPDOWN_PANEL_ROOT + TooltipContainer: TOOLTIP_CONTAINER + LabelForInput@B_DESC: + X: PARENT_RIGHT / 2 + 5 + Width: PARENT_RIGHT / 2 - 35 + Height: 20 + Visible: False + For: B + DropDownButton@B: + X: PARENT_RIGHT / 2 + 5 + Width: PARENT_RIGHT / 2 - 35 + Y: 25 + Height: 25 + Visible: False + PanelRoot: MISSION_DROPDOWN_PANEL_ROOT + TooltipContainer: TOOLTIP_CONTAINER Button@BACK_BUTTON: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Back + Text: button-missionbrowser-panel-back Font: Bold Key: escape Button@START_BRIEFING_VIDEO_BUTTON: @@ -115,35 +162,35 @@ Container@MISSIONBROWSER_PANEL: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Watch Briefing + Text: button-missionbrowser-panel-start-briefing-video Font: Bold Button@STOP_BRIEFING_VIDEO_BUTTON: X: PARENT_RIGHT - 290 Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Stop Briefing + Text: button-missionbrowser-panel-stop-briefing-video Font: Bold Button@START_INFO_VIDEO_BUTTON: X: PARENT_RIGHT - 440 Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Watch Info Video + Text: button-missionbrowser-panel-start-info-video Font: Bold Button@STOP_INFO_VIDEO_BUTTON: X: PARENT_RIGHT - 440 Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Stop Info Video + Text: button-missionbrowser-panel-stop-info-video Font: Bold Button@STARTGAME_BUTTON: X: PARENT_RIGHT - 140 Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Play + Text: button-missionbrowser-panel-startgame Font: Bold Background@MISSION_BIN: X: 15 @@ -157,6 +204,7 @@ Container@MISSIONBROWSER_PANEL: Y: 1 Width: 684 Height: 402 + Container@MISSION_DROPDOWN_PANEL_ROOT: TooltipContainer@TOOLTIP_CONTAINER: Background@FULLSCREEN_PLAYER: @@ -170,3 +218,4 @@ Background@FULLSCREEN_PLAYER: Y: 0 Width: WINDOW_RIGHT Height: WINDOW_BOTTOM + diff --git a/mods/cnc/chrome/multiplayer-browser.yaml b/mods/cnc/chrome/multiplayer-browser.yaml index b65a502682d6..3cb6c3560f82 100644 --- a/mods/cnc/chrome/multiplayer-browser.yaml +++ b/mods/cnc/chrome/multiplayer-browser.yaml @@ -6,7 +6,7 @@ Container@MULTIPLAYER_PANEL: Height: 540 Children: Label@TITLE: - Text: Multiplayer + Text: label-multiplayer-panel-title Width: PARENT_RIGHT Y: 0 - 22 Font: BigBold @@ -27,26 +27,26 @@ Container@MULTIPLAYER_PANEL: X: 5 Width: 355 Height: 25 - Text: Server + Text: label-container-name Align: Center Font: Bold Label@PLAYERS: X: 390 Width: 85 Height: 25 - Text: Players + Text: label-container-players Font: Bold Label@LOCATION: X: 480 Width: 110 Height: 25 - Text: Location + Text: label-container-location Font: Bold Label@STATUS: X: 595 Width: 50 Height: 25 - Text: Status + Text: label-container-status Font: Bold LogicTicker@NOTICE_WATCHER: Container@NOTICE_CONTAINER: @@ -65,21 +65,21 @@ Container@MULTIPLAYER_PANEL: Width: PARENT_RIGHT - 10 Height: 20 Align: Center - Text: You are running an outdated version of OpenRA. Download the latest version from www.openra.net + Text: label-bg-outdated-version Font: TinyBold Label@UNKNOWN_VERSION_LABEL: X: 5 Width: PARENT_RIGHT - 10 Height: 20 Align: Center - Text: You are running an unrecognized version of OpenRA. Download the latest version from www.openra.net + Text: label-bg-unknown-version Font: TinyBold Label@PLAYTEST_AVAILABLE_LABEL: X: 5 Width: PARENT_RIGHT - 10 Height: 20 Align: Center - Text: A preview of the next OpenRA release is available for testing. Download the playtest from www.openra.net + Text: label-bg-playtest-available Font: TinyBold ScrollPanel@SERVER_LIST: X: 15 @@ -120,7 +120,7 @@ Container@MULTIPLAYER_PANEL: ImageCollection: lobby-bits TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP - TooltipText: Requires Password + TooltipText: image-bg-password-protected-tooltip Image@REQUIRES_AUTHENTICATION: X: 372 Y: 6 @@ -129,7 +129,7 @@ Container@MULTIPLAYER_PANEL: ImageCollection: lobby-bits TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP - TooltipText: Requires OpenRA forum account + TooltipText: image-bg-requires-authentication-tooltip LabelWithTooltip@PLAYERS: X: 390 Width: 85 @@ -210,13 +210,13 @@ Container@MULTIPLAYER_PANEL: Y: 255 Width: PARENT_RIGHT Height: 25 - Text: Join + Text: button-selected-server-join DropDownButton@FILTERS_DROPDOWNBUTTON: X: 15 Y: PARENT_BOTTOM - 40 Width: 152 Height: 25 - Text: Filter Games + Text: dropdownbutton-bg-filters Button@RELOAD_BUTTON: X: 172 Y: PARENT_BOTTOM - 40 @@ -245,18 +245,19 @@ Container@MULTIPLAYER_PANEL: Y: PARENT_BOTTOM - 40 Width: 100 Height: 25 - Text: Direct IP + Text: button-bg-directconnect Button@CREATE_BUTTON: X: 592 Y: PARENT_BOTTOM - 40 Width: 105 Height: 25 - Text: Create + Text: button-bg-create Button@BACK_BUTTON: Key: escape X: 0 Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Back + Text: button-multiplayer-panel-back TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/cnc/chrome/multiplayer-browserpanels.yaml b/mods/cnc/chrome/multiplayer-browserpanels.yaml index 7199a00157a3..fcbd45bc180f 100644 --- a/mods/cnc/chrome/multiplayer-browserpanels.yaml +++ b/mods/cnc/chrome/multiplayer-browserpanels.yaml @@ -50,7 +50,7 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 5 Width: PARENT_RIGHT - 29 Height: 20 - Text: Waiting + Text: checkbox-multiplayer-filter-panel-waiting-for-players TextColor: 32CD32 Font: Regular Checkbox@EMPTY: @@ -58,14 +58,14 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 30 Width: PARENT_RIGHT - 29 Height: 20 - Text: Empty + Text: checkbox-multiplayer-filter-panel-empty Font: Regular Checkbox@PASSWORD_PROTECTED: X: 5 Y: 55 Width: PARENT_RIGHT - 29 Height: 20 - Text: Protected + Text: checkbox-multiplayer-filter-panel-password-protected TextColor: FF0000 Font: Regular Checkbox@ALREADY_STARTED: @@ -73,7 +73,7 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 80 Width: PARENT_RIGHT - 29 Height: 20 - Text: Started + Text: checkbox-multiplayer-filter-panel-already-started TextColor: FFA500 Font: Regular Checkbox@INCOMPATIBLE_VERSION: @@ -81,6 +81,7 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 105 Width: PARENT_RIGHT - 29 Height: 20 - Text: Incompatible + Text: checkbox-multiplayer-filter-panel-incompatible-version TextColor: BEBEBE Font: Regular + diff --git a/mods/cnc/chrome/multiplayer-createserver.yaml b/mods/cnc/chrome/multiplayer-createserver.yaml index 833d56f9d504..74692d4666c4 100644 --- a/mods/cnc/chrome/multiplayer-createserver.yaml +++ b/mods/cnc/chrome/multiplayer-createserver.yaml @@ -11,7 +11,7 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Font: BigBold Contrast: true Align: Center - Text: Create Server + Text: label-multiplayer-createserver-panel-title Background@bg: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -22,7 +22,7 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Width: 105 Height: 25 Align: Right - Text: Server Name: + Text: label-bg-server-name TextField@SERVER_NAME: X: 110 Y: 15 @@ -35,12 +35,11 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Width: 105 Height: 25 Align: Right - Text: Password: + Text: label-bg-password PasswordField@PASSWORD: X: 110 Y: 50 Width: 145 - MaxLength: 20 Height: 25 Label@AFTER_PASSWORD_LABEL: X: 265 @@ -48,13 +47,13 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Width: 105 Height: 25 Align: Left - Text: (optional) + Text: label-bg-after-password Label@LISTEN_PORT_LABEL: Y: 84 Width: 105 Height: 25 Align: Right - Text: Port: + Text: label-bg-listen-port TextField@LISTEN_PORT: X: 110 Y: 85 @@ -69,7 +68,7 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Width: 150 Height: 20 Font: Regular - Text: Advertise Online + Text: checkbox-bg-advertise Label@NOTICES_HEADER_A: X: 15 Y: 125 @@ -99,21 +98,21 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: - Game will be advertised to the Local Area Network only. + Text: label-notices-lan-advertising Label@FIREWALL: Y: 12 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You must manually configure your firewall to allow connections. + Text: label-notices-lan-firewall Label@PORTFORWARD_A: Y: 24 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - Players can connect using Direct IP from the Internet if you + Text: label-notices-lan-portforward-a Label@PORTFORWARD_B: X: 7 Y: 36 @@ -121,7 +120,7 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: manually configure port forwarding on your router. + Text: label-notices-lan-portforward-b Container@NOTICES_NO_UPNP: X: 20 Y: 145 @@ -133,21 +132,21 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: - Game will be advertised to the Local Area Network and Internet. + Text: label-notices-no-upnp-advertising Label@FIREWALL: Y: 12 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You must manually configure your firewall to allow connections. + Text: label-notices-no-upnp-firewall Label@PORTFORWARD_A: Y: 24 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You must manually configure your router to allow and forward + Text: label-notices-no-upnp-portforward-a Label@PORTFORWARD_B: X: 7 Y: 36 @@ -155,14 +154,14 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: connections to your local IP and Port. + Text: label-notices-no-upnp-portforward-b Label@SETTINGS_A: Y: 48 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You can enable UPnP/NAT-PMP (if supported by your router) + Text: label-notices-no-upnp-settings-a Label@SETTINGS_B: X: 7 Y: 60 @@ -170,7 +169,7 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: in the Advanced tab of the settings menu. + Text: label-notices-no-upnp-settings-b Container@NOTICES_UPNP: X: 20 Y: 145 @@ -182,28 +181,28 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: - Game will be advertised to the Local Area Network and Internet. + Text: label-notices-upnp-advertising Label@FIREWALL: Y: 12 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You must manually configure your firewall to allow connections. + Text: label-notices-upnp-firewall Label@PORTFORWARD_A: Y: 24 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - Game will automatically configure port forwarding. + Text: label-notices-upnp-portforward-a Label@SETTINGS_A: Y: 36 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You can disable UPnP/NAT-PMP in the settings menu. + Text: label-notices-upnp-settings-a Container@MAP_PREVIEW_ROOT: X: PARENT_RIGHT - 189 Y: 15 @@ -214,18 +213,19 @@ Container@MULTIPLAYER_CREATESERVER_PANEL: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Back + Text: button-multiplayer-createserver-panel-back Button@MAP_BUTTON: X: PARENT_RIGHT - WIDTH - 150 Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Change Map + Text: button-multiplayer-createserver-panel-map Button@CREATE_BUTTON: Key: return X: PARENT_RIGHT - WIDTH Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Create + Text: button-multiplayer-createserver-panel-create TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/cnc/chrome/multiplayer-directconnect.yaml b/mods/cnc/chrome/multiplayer-directconnect.yaml index a1fa4d52912e..16ef840ffbae 100644 --- a/mods/cnc/chrome/multiplayer-directconnect.yaml +++ b/mods/cnc/chrome/multiplayer-directconnect.yaml @@ -11,7 +11,7 @@ Container@DIRECTCONNECT_PANEL: Font: BigBold Contrast: true Align: Center - Text: Connect to Server + Text: label-directconnect-panel-title Background@bg: Width: 370 Height: 90 @@ -23,7 +23,7 @@ Container@DIRECTCONNECT_PANEL: Width: 95 Height: 25 Align: Right - Text: Address: + Text: label-bg-address TextField@IP: X: 150 Y: 15 @@ -35,7 +35,7 @@ Container@DIRECTCONNECT_PANEL: Width: 95 Height: 25 Align: Right - Text: Port: + Text: label-bg-port TextField@PORT: X: 150 Y: 50 @@ -48,11 +48,12 @@ Container@DIRECTCONNECT_PANEL: Y: 89 Width: 140 Height: 35 - Text: Back + Text: button-directconnect-panel-back Button@JOIN_BUTTON: Key: return X: 230 Y: 89 Width: 140 Height: 35 - Text: Join + Text: button-directconnect-panel-join + diff --git a/mods/cnc/chrome/music.yaml b/mods/cnc/chrome/music.yaml index 80a5f7e279f1..5d0c9ff1267f 100644 --- a/mods/cnc/chrome/music.yaml +++ b/mods/cnc/chrome/music.yaml @@ -12,7 +12,7 @@ Container@MUSIC_PANEL: Font: BigBold Contrast: true Align: Center - Text: Music Player + Text: label-music-panel-title Background@bg: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -51,14 +51,14 @@ Container@MUSIC_PANEL: Label@TITLE: Width: 100 Height: 25 - Text: Track + Text: label-container-title Align: Center Font: Bold Label@TYPE: X: PARENT_RIGHT - 85 Height: 25 Width: 50 - Text: Length + Text: label-container-type Align: Right Font: Bold Container@BUTTONS: @@ -149,14 +149,14 @@ Container@MUSIC_PANEL: Width: 85 Height: 20 Font: Regular - Text: Shuffle + Text: checkbox-bg-shuffle Checkbox@REPEAT: X: PARENT_RIGHT - 15 - WIDTH Y: PARENT_BOTTOM - HEIGHT - 60 Width: 70 Height: 20 Font: Regular - Text: Loop + Text: checkbox-bg-repeat Container@NO_MUSIC_LABEL: X: 15 Y: (PARENT_BOTTOM - HEIGHT - 60) / 2 @@ -169,26 +169,26 @@ Container@MUSIC_PANEL: Height: 25 Font: Bold Align: Center - Text: Music Not Installed + Text: label-no-music-title Label@DESCA: Y: 20 Width: PARENT_RIGHT - 24 Height: 25 Align: Center - Text: The game music can be installed + Text: label-no-music-desca Label@DESCB: Y: 40 Width: PARENT_RIGHT - 24 Height: 25 Align: Center - Text: from the "Manage Content" menu. + Text: label-no-music-descb Button@BACK_BUTTON: Key: escape X: 0 Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Back + Text: button-music-panel-back Label@MUTE_LABEL: X: 100 Y: PARENT_BOTTOM - 60 - 3 @@ -196,3 +196,4 @@ Container@MUSIC_PANEL: Height: 20 Font: Small TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/cnc/chrome/playerprofile.yaml b/mods/cnc/chrome/playerprofile.yaml index 13d4704502da..00e8e070da8e 100644 --- a/mods/cnc/chrome/playerprofile.yaml +++ b/mods/cnc/chrome/playerprofile.yaml @@ -26,7 +26,7 @@ Container@LOCAL_PROFILE_PANEL: Width: 60 Height: 20 Font: TinyBold - Text: Logout + Text: button-profile-header-destroy-key Background@BADGES_CONTAINER: Width: PARENT_RIGHT Y: 48 @@ -43,28 +43,28 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: Connect to a forum account to identify + Text: label-generate-keys-desc-a Label@DESC_B: Y: 22 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: yourself to other players, join private + Text: label-generate-keys-desc-b Label@DESC_C: Y: 38 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: servers, and display badges. + Text: label-generate-keys-desc-c Button@GENERATE_KEY: X: (PARENT_RIGHT - WIDTH) / 2 Y: 70 Width: 240 Height: 20 Font: TinyBold - Text: Connect to an OpenRA forum account + Text: button-generate-keys-key Background@GENERATING_KEYS: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -76,14 +76,14 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: Generating authentication key pair. + Text: label-generating-keys-desc-a Label@DESC_B: Y: 30 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: This will take several seconds... + Text: label-generating-keys-desc-b ProgressBar: X: (PARENT_RIGHT - WIDTH) / 2 Y: 70 @@ -101,35 +101,35 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: An authentication key has been copied to your + Text: label-register-fingerprint-desc-a Label@DESC_B: Y: 19 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: clipboard. Add this to your User Control Panel + Text: label-register-fingerprint-desc-b Label@DESC_C: Y: 35 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: on the OpenRA forum then press Continue. + Text: label-register-fingerprint-desc-c Button@DELETE_KEY: X: 15 Y: 70 Width: 70 Height: 20 Font: TinyBold - Text: Cancel + Text: button-register-fingerprint-delete-key Button@CHECK_KEY: X: 185 Y: 70 Width: 70 Height: 20 Font: TinyBold - Text: Continue + Text: button-register-fingerprint-check-key Background@CHECKING_FINGERPRINT: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -141,14 +141,14 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: Querying account details from + Text: label-checking-fingerprint-desc-a Label@DESC_B: Y: 30 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: the OpenRA forum... + Text: label-checking-fingerprint-desc-b ProgressBar: X: (PARENT_RIGHT - WIDTH) / 2 Y: 70 @@ -166,21 +166,21 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: Your authentication key is not connected + Text: label-fingerprint-not-found-desc-a Label@DESC_B: Y: 30 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: to an OpenRA forum account. + Text: label-fingerprint-not-found-desc-b Button@FINGERPRINT_NOT_FOUND_CONTINUE: X: 185 Y: 70 Width: 70 Height: 20 Font: TinyBold - Text: Back + Text: button-fingerprint-not-found-continue Background@CONNECTION_ERROR: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -192,21 +192,21 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: Failed to connect to the OpenRA forum. + Text: label-connection-error-desc-a Label@DESC_B: Y: 30 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: Please check your internet connection. + Text: label-connection-error-desc-b Button@CONNECTION_ERROR_RETRY: X: 185 Y: 70 Width: 70 Height: 20 Font: TinyBold - Text: Retry + Text: button-connection-error-retry Container@PLAYER_PROFILE_BADGES_INSERT: Logic: PlayerProfileBadgesLogic @@ -227,3 +227,4 @@ Container@PLAYER_PROFILE_BADGES_INSERT: Width: PARENT_RIGHT - 60 Height: 24 Font: Bold + diff --git a/mods/cnc/chrome/replaybrowser.yaml b/mods/cnc/chrome/replaybrowser.yaml index 2ea36f78e4b0..613cada3404a 100644 --- a/mods/cnc/chrome/replaybrowser.yaml +++ b/mods/cnc/chrome/replaybrowser.yaml @@ -11,7 +11,7 @@ Container@REPLAYBROWSER_PANEL: Font: BigBold Contrast: true Align: Center - Text: Replay Viewer + Text: label-replaybrowser-panel-title Background@bg: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -34,104 +34,104 @@ Container@REPLAYBROWSER_PANEL: Height: 25 Font: Bold Align: Center - Text: Filter + Text: label-filters-title Label@FLT_GAMETYPE_DESC: X: 0 Y: 15 Width: 80 Height: 25 - Text: Type: + Text: label-filters-flt-gametype-desc Align: Right DropDownButton@FLT_GAMETYPE_DROPDOWNBUTTON: X: 85 Y: 15 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-gametype Label@FLT_DATE_DESC: X: 0 Y: 45 Width: 80 Height: 25 - Text: Date: + Text: label-filters-flt-date-desc Align: Right DropDownButton@FLT_DATE_DROPDOWNBUTTON: X: 85 Y: 45 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-date Label@FLT_DURATION_DESC: X: 0 Y: 75 Width: 80 Height: 25 - Text: Duration: + Text: label-filters-flt-duration-desc Align: Right DropDownButton@FLT_DURATION_DROPDOWNBUTTON: X: 85 Y: 75 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-duration Label@FLT_MAPNAME_DESC: X: 0 Y: 105 Width: 80 Height: 25 - Text: Map: + Text: label-filters-flt-mapname-desc Align: Right DropDownButton@FLT_MAPNAME_DROPDOWNBUTTON: X: 85 Y: 105 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-mapname Label@FLT_PLAYER_DESC: X: 0 Y: 135 Width: 80 Height: 25 - Text: Player: + Text: label-filters-flt-player-desc Align: Right DropDownButton@FLT_PLAYER_DROPDOWNBUTTON: X: 85 Y: 135 Width: PARENT_RIGHT - 85 Height: 25 - Text: Anyone + Text: dropdownbutton-filters-flt-player Label@FLT_OUTCOME_DESC: X: 0 Y: 165 Width: 80 Height: 25 - Text: Outcome: + Text: label-filters-flt-outcome-desc Align: Right DropDownButton@FLT_OUTCOME_DROPDOWNBUTTON: X: 85 Y: 165 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-outcome Label@FLT_FACTION_DESC: X: 0 Y: 195 Width: 80 Height: 25 - Text: Faction: + Text: label-filters-flt-faction-desc Align: Right DropDownButton@FLT_FACTION_DROPDOWNBUTTON: X: 85 Y: 195 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-faction Button@FLT_RESET_BUTTON: X: 85 Y: 235 Width: PARENT_RIGHT - 85 Height: 25 - Text: Reset Filters + Text: button-filters-flt-reset Font: Bold Container@MANAGEMENT: X: 85 @@ -144,26 +144,26 @@ Container@REPLAYBROWSER_PANEL: Height: 25 Font: Bold Align: Center - Text: Manage + Text: label-management-manage-title Button@MNG_RENSEL_BUTTON: Y: 30 Width: PARENT_RIGHT Height: 25 - Text: Rename + Text: button-management-mng-rensel Font: Bold Key: F2 Button@MNG_DELSEL_BUTTON: Y: 60 Width: PARENT_RIGHT Height: 25 - Text: Delete + Text: button-management-mng-delsel Font: Bold Key: Delete Button@MNG_DELALL_BUTTON: Y: 90 Width: PARENT_RIGHT Height: 25 - Text: Delete All + Text: button-management-mng-delall Font: Bold Container@REPLAY_LIST_CONTAINER: X: 314 @@ -175,7 +175,7 @@ Container@REPLAYBROWSER_PANEL: Y: 0 - 9 Width: PARENT_RIGHT Height: 25 - Text: Choose Replay + Text: label-replay-list-container-replaybrowser-title Align: Center Font: Bold ScrollPanel@REPLAY_LIST: @@ -261,12 +261,13 @@ Container@REPLAYBROWSER_PANEL: Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Back + Text: button-replaybrowser-panel-cancel Button@WATCH_BUTTON: Key: return X: PARENT_RIGHT - 140 Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Watch + Text: button-replaybrowser-panel-watch TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/cnc/chrome/settings-advanced.yaml b/mods/cnc/chrome/settings-advanced.yaml index 808bd37d9742..07e570cc8990 100644 --- a/mods/cnc/chrome/settings-advanced.yaml +++ b/mods/cnc/chrome/settings-advanced.yaml @@ -22,7 +22,7 @@ Container@ADVANCED_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Advanced + Text: label-network-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -35,7 +35,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Enable UPnP/NAT-PMP Discovery + Text: checkbox-nat-discovery-container Container@FETCH_NEWS_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -44,7 +44,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Fetch Community News + Text: checkbox-fetch-news-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -57,7 +57,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show Performance Graph + Text: checkbox-perfgraph-container Container@CHECK_VERSION_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -66,7 +66,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Check for Updates + Text: checkbox-check-version-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -79,7 +79,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show Performance Text + Text: checkbox-perftext-container Container@SENDSYSINFO_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -88,14 +88,14 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Send System Information + Text: checkbox-sendsysinfo-container Label@SENDSYSINFO_DESC: Y: 15 Width: PARENT_RIGHT Height: 30 Font: Tiny WordWrap: True - Text: Your Operating System, OpenGL and .NET runtime versions, and language settings will be sent along with an anonymous ID to help prioritize future development. + Text: label-sendsysinfo-checkbox-container-desc Container@SPACER: Background@DEBUG_SECTION_HEADER: X: 5 @@ -109,7 +109,7 @@ Container@ADVANCED_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Developer + Text: label-debug-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 40 @@ -121,13 +121,13 @@ Container@ADVANCED_PANEL: Label@A: Width: PARENT_RIGHT Height: 20 - Text: Additional developer-specific options can be enabled via the + Text: label-debug-hidden-container-a Align: Center Label@B: Y: 20 Width: PARENT_RIGHT Height: 20 - Text: Debug.DisplayDeveloperSettings setting or launch flag + Text: label-debug-hidden-container-b Align: Center Container@ROW: Width: PARENT_RIGHT - 24 @@ -141,7 +141,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show Bot Debug Messages + Text: checkbox-botdebug-container Container@CHECKBOTSYNC_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -150,7 +150,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Check Sync around BotModule Code + Text: checkbox-checkbotsync-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -163,7 +163,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show Map Debug Messages + Text: checkbox-luadebug-container Container@CHECKUNSYNCED_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -172,7 +172,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Check Sync around Unsynced Code + Text: checkbox-checkunsynced-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -185,7 +185,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Enable Debug Commands in Replays + Text: checkbox-replay-commands-container Container@PERFLOGGING_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -194,4 +194,5 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Enable Tick Performance Logging + Text: checkbox-perflogging-container + diff --git a/mods/cnc/chrome/settings-audio.yaml b/mods/cnc/chrome/settings-audio.yaml index ae12d9508d07..702066ce7073 100644 --- a/mods/cnc/chrome/settings-audio.yaml +++ b/mods/cnc/chrome/settings-audio.yaml @@ -22,7 +22,7 @@ Container@AUDIO_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Audio + Text: label-audio-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -35,7 +35,7 @@ Container@AUDIO_PANEL: Width: PARENT_RIGHT Height: 20 Align: Center - Text: Audio controls require an active sound device + Text: label-no-audio-device-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -48,7 +48,7 @@ Container@AUDIO_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Cash Ticks + Text: checkbox-cash-ticks-container Container@MUTE_SOUND_CONTAINER: X: 10 Y: 30 @@ -58,7 +58,7 @@ Container@AUDIO_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Mute Sound + Text: checkbox-mute-sound-container Container@SOUND_VOLUME_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -66,7 +66,7 @@ Container@AUDIO_PANEL: Label@SOUND_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Sound Volume: + Text: label-sound-volume-container ExponentialSlider@SOUND_VOLUME: Y: 30 Width: PARENT_RIGHT @@ -84,8 +84,8 @@ Container@AUDIO_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Mute Menu Music - TooltipText: Mute background music when no specific track is playing + Text: checkbox-mute-background-music-container.label + TooltipText: checkbox-mute-background-music-container.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER Container@MUSIC_VOLUME_CONTAINER: X: PARENT_RIGHT / 2 + 10 @@ -94,7 +94,7 @@ Container@AUDIO_PANEL: Label@MUSIC_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Music Volume: + Text: label-music-volume-container ExponentialSlider@MUSIC_VOLUME: Y: 25 Width: PARENT_RIGHT @@ -111,7 +111,7 @@ Container@AUDIO_PANEL: Label@AUDIO_DEVICE_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Audio Device: + Text: label-audio-device-container DropDownButton@AUDIO_DEVICE: Y: 25 Width: PARENT_RIGHT @@ -123,7 +123,7 @@ Container@AUDIO_PANEL: Label@VIDEO_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Video Volume: + Text: label-video-volume-container ExponentialSlider@VIDEO_VOLUME: Y: 25 Width: PARENT_RIGHT @@ -142,4 +142,5 @@ Container@AUDIO_PANEL: Height: 20 Font: Tiny Align: Center - Text: Device changes will be applied after the game is restarted + Text: label-restart-required-container-audio-desc + diff --git a/mods/cnc/chrome/settings-display.yaml b/mods/cnc/chrome/settings-display.yaml index 06753a3d2e7e..82a312efde98 100644 --- a/mods/cnc/chrome/settings-display.yaml +++ b/mods/cnc/chrome/settings-display.yaml @@ -22,7 +22,7 @@ Container@DISPLAY_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Profile + Text: label-profile-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -34,7 +34,7 @@ Container@DISPLAY_PANEL: LabelForInput@PLAYER: Width: PARENT_RIGHT Height: 20 - Text: Player Name: + Text: label-player-container For: PLAYERNAME TextField@PLAYERNAME: Y: 25 @@ -49,7 +49,7 @@ Container@DISPLAY_PANEL: LabelForInput@COLOR: Width: PARENT_RIGHT Height: 20 - Text: Preferred Color: + Text: label-playercolor-container-color For: PLAYERCOLOR DropDownButton@PLAYERCOLOR: Y: 25 @@ -76,7 +76,7 @@ Container@DISPLAY_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Display + Text: label-display-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -88,7 +88,7 @@ Container@DISPLAY_PANEL: Label@BATTLEFIELD_CAMERA: Width: PARENT_RIGHT Height: 20 - Text: Battlefield Camera: + Text: label-battlefield-camera-dropdown-container DropDownButton@BATTLEFIELD_CAMERA_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -101,7 +101,7 @@ Container@DISPLAY_PANEL: Label@TARGET_LINES: Width: PARENT_RIGHT Height: 20 - Text: Target Lines: + Text: label-target-lines-dropdown-container DropDownButton@TARGET_LINES_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -118,7 +118,7 @@ Container@DISPLAY_PANEL: LabelForInput@UI_SCALE: Width: PARENT_RIGHT Height: 20 - Text: UI Scale: + Text: label-ui-scale-dropdown-container For: UI_SCALE_DROPDOWN DropDownButton@UI_SCALE_DROPDOWN: Y: 25 @@ -132,7 +132,7 @@ Container@DISPLAY_PANEL: Label@STATUS_BARS: Width: PARENT_RIGHT Height: 20 - Text: Status Bars: + Text: label-status-bar-dropdown-container-bars DropDownButton@STATUS_BAR_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -150,7 +150,7 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Increase Cursor Size + Text: checkbox-cursordouble-container Container@PLAYER_STANCE_COLORS_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -159,8 +159,8 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Player Relationship Colors - TooltipText: Change minimap and health bar colors based on relationship (own, enemy, ally, neutral) + Text: checkbox-player-stance-colors-container.label + TooltipText: checkbox-player-stance-colors-container.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER Container@ROW: Width: PARENT_RIGHT - 24 @@ -174,8 +174,8 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show UI Feedback Notifications - TooltipText: Show transient text notifications for UI events + Text: checkbox-ui-feedback-container.label + TooltipText: checkbox-ui-feedback-container.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER Container@TRANSIENTS_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 @@ -185,8 +185,8 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show Game Event Notifications - TooltipText: Show transient text notifications for game events + Text: checkbox-transients-container.label + TooltipText: checkbox-transients-container.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER Container@ROW: Width: PARENT_RIGHT - 24 @@ -200,7 +200,7 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Hide Chat in Replays + Text: checkbox-hide-replay-chat-container Container@SPACER: Background@VIDEO_SECTION_HEADER: X: 5 @@ -214,7 +214,7 @@ Container@DISPLAY_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Video + Text: label-video-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -226,13 +226,13 @@ Container@DISPLAY_PANEL: Label@VIDEO_MODE: Width: PARENT_RIGHT Height: 20 - Text: Video Mode: + Text: label-video-mode-dropdown-container DropDownButton@MODE_DROPDOWN: Y: 25 Width: PARENT_RIGHT Height: 25 Font: Regular - Text: Windowed + Text: dropdownbutton-video-mode-dropdown-container Container@WINDOW_RESOLUTION_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -240,7 +240,7 @@ Container@DISPLAY_PANEL: Label@WINDOW_SIZE: Width: PARENT_RIGHT Height: 20 - Text: Window Size: + Text: label-window-resolution-container-size TextField@WINDOW_WIDTH: Y: 25 Width: 55 @@ -250,7 +250,7 @@ Container@DISPLAY_PANEL: Label@X: X: 55 Y: 25 - Text: x + Text: label-window-resolution-container-x Font: Bold Height: 25 Width: 15 @@ -269,13 +269,13 @@ Container@DISPLAY_PANEL: Label@DISPLAY_SELECTION_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Select Display: + Text: label-display-selection-container DropDownButton@DISPLAY_SELECTION_DROPDOWN: Y: 25 Width: PARENT_RIGHT Height: 25 Font: Regular - Text: Standard + Text: dropdownbutton-display-selection-container-dropdown Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -307,7 +307,7 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Enable VSync + Text: checkbox-vsync-container Container@FRAME_LIMIT_GAMESPEED_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Y: 25 @@ -317,7 +317,7 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Limit framerate to game tick rate + Text: checkbox-frame-limit-gamespeed-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -329,7 +329,7 @@ Container@DISPLAY_PANEL: Label@GL_PROFILE: Width: PARENT_RIGHT Height: 20 - Text: OpenGL Profile: + Text: label-gl-profile-dropdown-container DropDownButton@GL_PROFILE_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -347,5 +347,6 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Tiny - Text: Display and OpenGL changes require restart + Text: label-restart-required-container-video-desc Align: Center + diff --git a/mods/cnc/chrome/settings-hotkeys.yaml b/mods/cnc/chrome/settings-hotkeys.yaml index 9a6c67abb489..ccd1c7688317 100644 --- a/mods/cnc/chrome/settings-hotkeys.yaml +++ b/mods/cnc/chrome/settings-hotkeys.yaml @@ -30,7 +30,7 @@ Container@HOTKEYS_PANEL: Width: 100 Height: 25 Font: Bold - Text: Filter by name: + Text: label-hotkeys-panel-filter-input TextField@FILTER_INPUT: X: 108 Width: 180 @@ -40,7 +40,7 @@ Container@HOTKEYS_PANEL: Width: 100 Height: 25 Font: Bold - Text: Context: + Text: label-hotkeys-panel-context-dropdown Align: Right DropDownButton@CONTEXT_DROPDOWN: X: PARENT_RIGHT - WIDTH @@ -92,7 +92,7 @@ Container@HOTKEYS_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Align: Center - Text: No hotkeys match the filter criteria. + Text: label-hotkey-empty-list-message Background@HOTKEY_REMAP_BGND: Y: PARENT_BOTTOM - HEIGHT - 1 Width: PARENT_RIGHT @@ -133,22 +133,22 @@ Container@HOTKEYS_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Font: Tiny - Text: This hotkey cannot be modified + Text: label-notices-readonly-notice Button@OVERRIDE_HOTKEY_BUTTON: X: PARENT_RIGHT - 3 * WIDTH - 30 Y: 20 Width: 70 Height: 25 - Text: Override + Text: button-hotkey-remap-dialog-override Font: Bold Button@CLEAR_HOTKEY_BUTTON: X: PARENT_RIGHT - 2 * WIDTH - 30 Y: 20 Width: 65 Height: 25 - Text: Clear + Text: button-hotkey-remap-dialog-clear.label Font: Bold - TooltipText: Unbind the hotkey + TooltipText: button-hotkey-remap-dialog-clear.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP Button@RESET_HOTKEY_BUTTON: @@ -156,8 +156,9 @@ Container@HOTKEYS_PANEL: Y: 20 Width: 65 Height: 25 - Text: Reset + Text: button-hotkey-remap-dialog-reset.label Font: Bold - TooltipText: Reset to default + TooltipText: button-hotkey-remap-dialog-reset.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP + diff --git a/mods/cnc/chrome/settings-input.yaml b/mods/cnc/chrome/settings-input.yaml index f12788b28a8f..e92e2ed673f6 100644 --- a/mods/cnc/chrome/settings-input.yaml +++ b/mods/cnc/chrome/settings-input.yaml @@ -22,7 +22,7 @@ Container@INPUT_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Input + Text: label-input-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -35,7 +35,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Control Scheme: + Text: label-mouse-control-container DropDownButton@MOUSE_CONTROL_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -49,7 +49,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Zoom Modifier: + Text: label-zoom-modifier-container DropDownButton@ZOOM_MODIFIER: Y: 25 Width: PARENT_RIGHT @@ -63,48 +63,48 @@ Container@INPUT_PANEL: LabelWithHighlight@DESC_SELECTION: Height: 16 Font: Small - Text: - Select units using the mouse button + Text: label-mouse-control-desc-classic-selection LabelWithHighlight@DESC_COMMANDS: Y: 17 Height: 16 Font: Small - Text: - Command units using the mouse button + Text: label-mouse-control-desc-classic-commands LabelWithHighlight@DESC_BUILDIGS: Y: 34 Height: 16 Font: Small - Text: - Place structures using the mouse button + Text: label-mouse-control-desc-classic-buildigs LabelWithHighlight@DESC_SUPPORT: Y: 51 Height: 16 Font: Small - Text: - Target support powers using the mouse button + Text: label-mouse-control-desc-classic-support LabelWithHighlight@DESC_ZOOM: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using the + Text: label-mouse-control-desc-classic-zoom LabelWithHighlight@DESC_ZOOM_MODIFIER: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using + Text: label-mouse-control-desc-classic-zoom-modifier LabelWithHighlight@DESC_SCROLL_RIGHT: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-classic-scroll-right LabelWithHighlight@DESC_SCROLL_MIDDLE: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-classic-scroll-middle Label@DESC_EDGESCROLL: X: 9 Y: 102 Height: 16 Font: Small - Text: or by moving the cursor to the edge of the screen + Text: label-mouse-control-desc-classic-edgescroll Container@MOUSE_CONTROL_DESC_MODERN: X: 10 Y: 55 @@ -113,48 +113,48 @@ Container@INPUT_PANEL: LabelWithHighlight@DESC_SELECTION: Height: 16 Font: Small - Text: - Select units using the mouse button + Text: label-mouse-control-desc-modern-selection LabelWithHighlight@DESC_COMMANDS: Y: 17 Height: 16 Font: Small - Text: - Command units using the mouse button + Text: label-mouse-control-desc-modern-commands LabelWithHighlight@DESC_BUILDIGS: Y: 34 Height: 16 Font: Small - Text: - Place structures using the mouse button + Text: label-mouse-control-desc-modern-buildigs LabelWithHighlight@DESC_SUPPORT: Y: 51 Height: 16 Font: Small - Text: - Target support powers using the mouse button + Text: label-mouse-control-desc-modern-support LabelWithHighlight@DESC_ZOOM: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using the + Text: label-mouse-control-desc-modern-zoom LabelWithHighlight@DESC_ZOOM_MODIFIER: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using + Text: label-mouse-control-desc-modern-zoom-modifier LabelWithHighlight@DESC_SCROLL_RIGHT: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-modern-scroll-right LabelWithHighlight@DESC_SCROLL_MIDDLE: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-modern-scroll-middle Label@DESC_EDGESCROLL: X: 9 Y: 102 Height: 16 Font: Small - Text: or by moving the cursor to the edge of the screen + Text: label-mouse-control-desc-modern-edgescroll Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -167,7 +167,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Screen Edge Panning + Text: checkbox-edgescroll-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -180,7 +180,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Alternate Mouse Panning + Text: checkbox-alternate-scroll-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -193,7 +193,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Lock Mouse to Window + Text: checkbox-lockmouse-container Container@SPACER: Height: 30 Container@ROW: @@ -208,7 +208,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Pan Behaviour: + Text: label-mouse-scroll-type-container DropDownButton@MOUSE_SCROLL_TYPE_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -221,7 +221,7 @@ Container@INPUT_PANEL: Label@SCROLL_SPEED_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Pan Speed: + Text: label-scrollspeed-slider-container-scroll-speed Slider@SCROLLSPEED_SLIDER: Y: 25 Width: PARENT_RIGHT @@ -240,7 +240,7 @@ Container@INPUT_PANEL: Label@ZOOM_SPEED_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Zoom Speed: + Text: label-zoomspeed-slider-container-zoom-speed ExponentialSlider@ZOOMSPEED_SLIDER: Y: 25 Width: PARENT_RIGHT @@ -259,7 +259,7 @@ Container@INPUT_PANEL: Label@UI_SCROLL_SPEED_LABEL: Width: PARENT_RIGHT Height: 20 - Text: UI Scroll Speed: + Text: label-ui-scrollspeed-slider-container-scroll-speed Slider@UI_SCROLLSPEED_SLIDER: Y: 25 Width: PARENT_RIGHT @@ -267,3 +267,4 @@ Container@INPUT_PANEL: Ticks: 7 MinimumValue: 1 MaximumValue: 100 + diff --git a/mods/cnc/chrome/settings.yaml b/mods/cnc/chrome/settings.yaml index b36a7f25ee12..d45e62b096ed 100644 --- a/mods/cnc/chrome/settings.yaml +++ b/mods/cnc/chrome/settings.yaml @@ -18,19 +18,19 @@ Container@SETTINGS_PANEL: Font: BigBold Contrast: true Align: Center - Text: Settings + Text: label-settings-panel-title Button@BACK_BUTTON: Key: escape Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Back + Text: button-settings-panel-back Button@RESET_BUTTON: X: 150 Y: PARENT_BOTTOM - 1 Width: 140 Height: 35 - Text: Reset + Text: button-settings-panel-reset Container@SETTINGS_TAB_CONTAINER: X: 0 - 140 + 1 Children: @@ -48,3 +48,4 @@ Container@SETTINGS_PANEL: Width: PARENT_RIGHT - 30 Height: PARENT_BOTTOM - 30 TooltipContainer@SETTINGS_TOOLTIP_CONTAINER: + diff --git a/mods/cnc/chrome/tooltips.yaml b/mods/cnc/chrome/tooltips.yaml index 970016edbe5f..122ad1af4f4f 100644 --- a/mods/cnc/chrome/tooltips.yaml +++ b/mods/cnc/chrome/tooltips.yaml @@ -350,6 +350,12 @@ Background@SUPPORT_POWER_TOOLTIP_FACTIONSUFFIX: Y: 22 Font: TinyBold VAlign: Top + Label@COST: + X: 5 + Y: 6 + Font: TinyBold + VAlign: Top + Text: $ Background@ARMY_TOOLTIP: Logic: ArmyTooltipLogic @@ -405,7 +411,7 @@ Background@LATENCY_TOOLTIP: Y: 1 Height: 23 Font: Bold - Text: Latency: + Text: label-latency-tooltip-prefix Label@LATENCY: Y: 1 Height: 23 @@ -420,7 +426,7 @@ Background@ANONYMOUS_PLAYER_TOOLTIP: Label@NAME: X: 5 Y: 2 - Text: Anonymous Player + Text: label-anonymous-player-tooltip-name Height: 23 Font: MediumBold Label@LOCATION: @@ -451,7 +457,7 @@ Background@ANONYMOUS_PLAYER_TOOLTIP: Label@LABEL: X: 10 Height: 12 - Text: Game Admin + Text: label-game-admin Font: TinyBold Container@REGISTERED_PLAYER_TOOLTIP: @@ -497,7 +503,7 @@ Container@REGISTERED_PLAYER_TOOLTIP: X: 10 Y: 1 Height: 12 - Text: Game Admin + Text: label-game-admin Font: TinyBold Container@MESSAGE_HEADER: Height: 26 @@ -514,3 +520,4 @@ Container@REGISTERED_PLAYER_TOOLTIP: Y: -1 Visible: false Background: panel-black + diff --git a/mods/cnc/languages/chrome/en.ftl b/mods/cnc/languages/chrome/en.ftl new file mode 100644 index 000000000000..4fc2ce409049 --- /dev/null +++ b/mods/cnc/languages/chrome/en.ftl @@ -0,0 +1,735 @@ +## assetbrowser.yaml +label-assetbrowser-panel-title = Asset Browser +label-bg-source-selector-desc = Select asset source +dropdownbutton-bg-source-selector = Folders +dropdownbutton-bg-asset-types-dropdown = Asset types +label-bg-filename-desc = Filter by name +label-bg-sprite-scale = Scale: +label-bg-palette-desc = Palette: +label-sprite-bg-error = Error displaying file. See assetbrowser.log for details. +button-assetbrowser-panel-close = Back + +## color-picker.yaml +button-color-chooser-random = Random +button-color-chooser-store = Store +button-color-chooser-mixer-tab = Mixer +button-color-chooser-palette-tab = Palette +label-preset-header = Preset Colors +label-custom-header = Custom Colors + +## connection.yaml +label-connecting-panel-title = Connecting +label-bg-connecting-desc = Connecting... +button-connecting-panel-abort = Abort +label-connection-background-connecting-desc = Failed to connect +label-connection-background-password = Password: +button-connectionfailed-panel-abort = Abort +button-connectionfailed-panel-quit = Quit +button-connectionfailed-panel-retry = Retry +label-connection-switchmod-panel-title = Switch Mod +label-connection-background-desc = This server is running a different mod: +label-connection-background-desc2 = Switch mods and join server? +button-connection-switchmod-panel-abort = Abort +button-connection-switchmod-panel-switch = Switch + +## credits.yaml +label-credits-panel-title = Credits +button-tab-container-engine = OpenRA +button-credits-panel-back = Back + +## dialogs.yaml +button-threebutton-prompt-confirm = Confirm +button-threebutton-prompt-other = Restart +button-threebutton-prompt-cancel = Cancel +button-twobutton-prompt-cancel = Cancel +button-twobutton-prompt-confirm = Confirm +button-text-input-prompt-accept = OK +button-text-input-prompt-cancel = Cancel + +## editor.yaml +label-new-map-bg-title = New Map +label-bg-tileset = Tileset: +label-bg-width = Width: +label-bg-height = Height: +button-new-map-bg-cancel = Cancel +button-new-map-bg-create = Create +label-save-map-panel-title = Save Map +label-save-map-background-title = Title: +label-save-map-background-author = Author: +label-save-map-background-visibility = Visibility: +dropdownbutton-save-map-background-visibility-dropdown = Map Visibility +label-save-map-background-directory = Directory: +label-save-map-background-filename = Filename: +button-save-map-panel-back = Cancel +button-save-map-panel = Save +label-actor-edit-panel-id = ID +button-container-delete = Delete +button-container-cancel = Cancel +button-container-ok = OK +button-editor-world-root-options-tooltip = Menu +label-tiles-bg-search = Search: +label-tiles-bg-categories = Filter: +label-actors-bg-search = Search: +label-actors-bg-categories = Filter: +label-actors-bg-owners = Owner: + +button-map-editor-tab-container-tiles = + .label = Tiles + .tooltip = Tiles + +button-map-editor-tab-container-overlays = + .label = Overlays + .tooltip = Overlays + +button-map-editor-tab-container-actors = + .label = Actors + .tooltip = Actors + +button-map-editor-tab-container-history = + .label = History + .tooltip = History + +button-editor-world-root-undo = + .label = Undo + .tooltip = Undo last step + +button-editor-world-root-redo = + .label = Redo + .tooltip = Redo last step + +button-editor-world-root-copypaste = + .label = Copy/Paste + .tooltip = Copy + +dropdownbutton-editor-world-root-copyfilter-button = Copy Filters +dropdownbutton-editor-world-root-overlay-button = Overlays +button-select-categories-buttons-all = All +button-select-categories-buttons-none = None + +## gamesave-browser.yaml +label-gamesave-browser-panel-load-title = Load game +label-gamesave-browser-panel-save-title = Save game +label-bg-title = [CREATE NEW FILE] +button-bg-delete-all = Delete All +button-bg-delete = Delete +button-bg-rename = Rename +button-bg-load = Load +button-bg-save = Save + +## gamesave-loading.yaml +label-gamesave-loading-screen-title = Loading Saved Game +label-gamesave-loading-screen-desc = Press Escape to cancel loading and return to the main menu + +## ingame-chat.yaml, ingame-infochat.yaml +button-chat-chrome-mode = + .label = Team + .tooltip = Toggle chat mode + +## ingame-debug.yaml +label-debug-panel-title = Debug Options +checkbox-debug-panel-instant-build = Instant Build Speed +checkbox-debug-panel-enable-tech = Build Everything +checkbox-debug-panel-build-anywhere = Build Anywhere +checkbox-debug-panel-unlimited-power = Unlimited Power +checkbox-debug-panel-instant-charge = Instant Charge Time +checkbox-debug-panel-disable-visibility-checks = Disable Visibility Checks +button-debug-panel-give-cash = Give $20,000 +button-debug-panel-grow-resources = Grow Resources +button-debug-panel-give-exploration = Clear Shroud +button-debug-panel-reset-exploration = Reset Shroud +label-debug-panel-visualizations-title = Visualizations +checkbox-debug-panel-show-unit-paths = Show Unit Paths +checkbox-debug-panel-show-customterrain-overlay = Show Custom Terrain +checkbox-debug-panel-show-actor-tags = Show Actor Tags +checkbox-debug-panel-show-combatoverlay = Show Combat Geometry +checkbox-debug-panel-show-geometry = Show Render Geometry +checkbox-debug-panel-show-terrain-overlay = Show Terrain Geometry +checkbox-debug-panel-show-screenmap = Show Screen Map + +## ingame-debug-hpf.yaml +dropdownbutton-hpf-overlay-locomotor = Select Locomotor +dropdownbutton-hpf-overlay-check = Select BlockedByActor + +## ingame-info.yaml +label-game-info-panel-title = Game Information + +## ingame-infoobjectives.yaml +label-mission-objectives = Mission: + +## ingame-infoscripterror.yaml +label-script-error-panel-desca = The map script has encountered a fatal error +label-script-error-panel-descb = The details of the error have been saved to lua.log in the logs directory. +label-script-error-panel-descc = Please send this file to the map author so that they can fix this issue. + +## ingame-infostats.yaml +label-objective-mission = Mission: +checkbox-objective-stats = Destroy all opposition! +label-stats-name = Player +label-stats-faction = Faction +label-stats-score = Score +label-stats-actions = Actions + +## ingame.yaml +button-observer-widgets-options-tooltip = Menu +button-replay-player-pause-tooltip = Pause +button-replay-player-play-tooltip = Play + +button-replay-player-slow = + .tooltip = Slow speed + .label = 50% + +button-replay-player-regular = + .tooltip = Regular speed + .label = 100% + +button-replay-player-fast = + .tooltip = Fast speed + .label = 200% + +button-replay-player-maximum = + .tooltip = Maximum speed + .label = MAX + +label-basic-stats-player-header = Player +label-basic-stats-cash-header = Cash +label-basic-stats-power-header = Power +label-basic-stats-kills-header = Kills +label-basic-stats-deaths-header = Deaths +label-basic-stats-assets-destroyed-header = Destroyed +label-basic-stats-assets-lost-header = Lost +label-basic-stats-experience-header = Score +label-basic-stats-actions-min-header = APM +label-economy-stats-player-header = Player +label-economy-stats-cash-header = Cash +label-economy-stats-income-header = Income +label-economy-stats-assets-header = Assets +label-economy-stats-earned-header = Earned +label-economy-stats-spent-header = Spent +label-economy-stats-harvesters-header = Harvesters +label-economy-stats-derricks-header = Oil Derricks +label-production-stats-player-header = Player +label-production-stats-header = Production +label-support-powers-player-header = Player +label-support-powers-header = Support Powers +label-army-player-header = Player +label-army-header = Army +label-combat-stats-player-header = Player +label-combat-stats-assets-destroyed-header = Destroyed +label-combat-stats-assets-lost-header = Lost +label-combat-stats-units-killed-header = U. Killed +label-combat-stats-units-dead-header = Units Lost +label-combat-stats-buildings-killed-header = B. Killed +label-combat-stats-buildings-dead-header = B. Lost +label-combat-stats-army-value-header = Army Value +label-combat-stats-vision-header = Vision + +supportpowers-support-powers-palette = + .ready = Ready + .hold = On Hold + +button-command-bar-attack-move = + .tooltip = Attack Move + .tooltipdesc = Selected units will move to the desired location + and attack any enemies they encounter en route. + + Hold <(Ctrl)> while targeting to order an Assault Move + that attacks any units or structures encountered en route. + + Left-click icon then right-click on target location. + +button-command-bar-force-move = + .tooltip = Force Move + .tooltipdesc = Selected units will move to the desired location + - Default activity for the target is suppressed + - Vehicles will attempt to crush enemies at the target location + - Helicopters will land at the target location + + Left-click icon then right-click on target. + Hold <(Alt)> to activate temporarily while commanding units. + +button-command-bar-force-attack = + .tooltip = Force Attack + .tooltipdesc = Selected units will attack the targeted unit or location + - Default activity for the target is suppressed + - Allows targeting of own or ally forces + - Long-range artillery units will always target the + location, ignoring units and buildings + + Left-click icon then right-click on target. + Hold <(Ctrl)> to activate temporarily while commanding units. + +button-command-bar-guard = + .tooltip = Guard + .tooltipdesc = Selected units will follow the targeted unit. + + Left-click icon then right-click on target unit. + +button-command-bar-deploy = + .tooltip = Deploy + .tooltipdesc = Selected units will perform their default deploy activity + - MCVs will unpack into a Construction Yard + - Construction Yards will re-pack into a MCV + - Transports will unload their passengers + + Acts immediately on selected units. + +button-command-bar-scatter = + .tooltip = Scatter + .tooltipdesc = Selected units will stop their current activity + and move to a nearby location. + + Acts immediately on selected units. + +button-command-bar-stop = + .tooltip = Stop + .tooltipdesc = Selected units will stop their current activity. + Selected buildings will reset their rally point. + + Acts immediately on selected targets. + +button-command-bar-queue-orders = + .tooltip = Waypoint Mode + .tooltipdesc = Use Waypoint Mode to give multiple linking commands + to the selected units. Units will execute the commands + immediately upon receiving them. + + Left-click icon then give commands in the game world. + Hold <(Shift)> to activate temporarily while commanding units. + +button-stance-bar-attackanything = + .tooltip = Attack Anything Stance + .tooltipdesc = Set the selected units to Attack Anything stance: + - Units will attack enemy units and structures on sight + - Units will pursue attackers across the battlefield + +button-stance-bar-defend = + .tooltip = Defend Stance + .tooltipdesc = Set the selected units to Defend stance: + - Units will attack enemy units on sight + - Units will not move or pursue enemies + +button-stance-bar-returnfire = + .tooltip = Return Fire Stance + .tooltipdesc = Set the selected units to Return Fire stance: + - Units will retaliate against enemies that attack them + - Units will not move or pursue enemies + +button-stance-bar-holdfire = + .tooltip = Hold Fire Stance + .tooltipdesc = Set the selected units to Hold Fire stance: + - Units will not fire upon enemies + - Units will not move or pursue enemies + +label-mute-indicator = Audio Muted +button-top-buttons-sell-tooltip = Sell +button-top-buttons-repair-tooltip = Repair +button-top-buttons-beacon-tooltip = Place Beacon +button-top-buttons-options-tooltip = Options +button-production-types-building-tooltip = Buildings +button-production-types-defence-tooltip = Support +button-production-types-infantry-tooltip = Infantry +button-production-types-vehicle-tooltip = Vehicles +button-production-types-aircraft-tooltip = Aircraft + +productionpalette-player-widgets-production-palette = + .ready = Ready + .hold = On Hold + +## lobby-kickdialogs.yaml +label-kick-client-dialog-texta = You may also apply a temporary ban, preventing +label-kick-client-dialog-textb = them from joining for the remainder of this game. +checkbox-kick-client-dialog-prevent-rejoining = Temporarily Ban +button-kick-client-dialog-ok = Kick +button-kick-client-dialog-cancel = Cancel +label-kick-spectators-dialog-title = Kick Spectators +button-kick-spectators-dialog-ok = Ok +button-kick-spectators-dialog-cancel = Cancel +label-force-start-dialog-title = Start Game? +label-force-start-dialog-texta = One or more players are not yet ready. +label-force-start-dialog-textb = Are you sure that you want to force start the game? +label-kick-warning-a = One or more clients are missing the selected +label-kick-warning-b = map, and will be kicked from the server. +button-force-start-dialog-ok = Start +button-force-start-dialog-cancel = Cancel + +## lobby-mappreview.yaml +label-map-incompatible-status-a = This map is not compatible +label-map-incompatible-status-b = with this version of OpenRA +label-map-validating-status = Validating... +button-map-download-available-install = Install Map +button-map-preview-update = Update Map +button-map-update-download-available-install = Install Map +label-map-preview-searching = Searching OpenRA Resource Center... +label-map-unavailable-a = This map was not found on the +label-map-unavailable-b = OpenRA Resource Center +label-map-preview-error = An error occurred during installation +label-map-update-available-a = A new version of the map +label-map-update-available-b = was found on your computer + +## lobby-music.yaml +label-container-music = Music +label-container-length = Length +checkbox-controls-shuffle = Shuffle +checkbox-controls-repeat = Loop +label-controls-volume = Volume: + +## lobby-music.yaml, music.yaml +label-container-title = Track +label-no-music-title = Music Not Installed +label-no-music-desca = The game music can be installed +label-no-music-descb = from the "Manage Content" menu. + +## lobby-options.yaml +label-lobby-options-bin-title = Map Options + +## lobby-players.yaml +label-container-lobby-name = Player +label-container-lobby-color = Color +label-container-lobby-faction = Faction +label-container-lobby-team = Team +label-container-lobby-handicap = Handicap +label-container-lobby-spawn = Spawn +label-container-lobby-status = Ready +dropdownbutton-template-editable-player-slot-options = Name +label-template-editable-player-factionname = Faction +dropdownbutton-template-editable-player-handicap-dropdown-tooltip = A handicap decreases the combat effectiveness of the player's forces +label-faction-factionname = Faction +dropdownbutton-template-noneditable-player-handicap-dropdown-tooltip = A handicap decreases the combat effectiveness of the player's forces +dropdownbutton-template-empty-slot-options = Name +button-template-empty-join = Play in this slot +label-template-editable-spectator = Spectator +label-template-noneditable-spectator = Spectator +checkbox-template-new-spectator-toggle-spectators = Allow Spectators? +button-template-new-spectator-spectate = Spectate + +## lobby-servers.yaml +image-lobby-servers-bin-password-protected-tooltip = Requires Password +image-lobby-servers-bin-requires-authentication-tooltip = Requires OpenRA forum account +dropdownbutton-lobby-servers-bin-filters = Filter Games + +## lobby.yaml +dropdownbutton-bg-slots = Slot Admin +button-skirmish-tabs-players-tab = Players +button-skirmish-tabs-options-tab = Options +button-skirmish-tabs-music-tab = Music +button-multiplayer-tabs-players-tab = Players +button-multiplayer-tabs-options-tab = Options +button-multiplayer-tabs-music-tab = Music +button-multiplayer-tabs-servers-tab = Servers +button-bg-changemap = Change Map + +button-lobbychat-chat-mode = + .label = Team + .tooltip = Toggle chat mode + +button-server-lobby-disconnect = Leave Game +button-server-lobby-start-game = Start Game + +## mainmenu-prompts.yaml +label-mainmenu-introduction-prompt-title = Establishing Battlefield Control +label-bg-desc-a = Welcome back Commander! Initialize combat parameters using the options below. +label-bg-desc-b = Additional options can be configured later from the Settings menu. +button-mainmenu-introduction-prompt-continue = Continue +label-mainmenu-system-info-prompt-title = Establishing Battlefield Control +label-bg-prompt-text-a = We would like to collect some details that will help us optimize OpenRA. +label-bg-prompt-text-b = With your permission, the following anonymous system data will be sent: +checkbox-bg-sysinfo = Send System Information +button-mainmenu-system-info-prompt-continue = Continue + +## mainmenu-prompts.yaml, settings-display.yaml +label-profile-section-header = Profile +label-player-container = Player Name: +label-playercolor-container-color = Preferred Color: +label-display-section-header = Display +label-battlefield-camera-dropdown-container = Battlefield Camera: +label-ui-scale-dropdown-container = UI Scale: +checkbox-cursordouble-container = Increase Cursor Size + +## mainmenu-prompts.yaml, settings-input.yaml +label-input-section-header = Input +label-mouse-control-container = Control Scheme: +label-mouse-control-desc-classic-selection = - Select units using the mouse button +label-mouse-control-desc-classic-commands = - Command units using the mouse button +label-mouse-control-desc-classic-buildigs = - Place structures using the mouse button +label-mouse-control-desc-classic-support = - Target support powers using the mouse button +label-mouse-control-desc-classic-zoom = - Zoom the battlefield using the +label-mouse-control-desc-classic-zoom-modifier = - Zoom the battlefield using +label-mouse-control-desc-classic-scroll-right = - Pan the battlefield using the mouse button +label-mouse-control-desc-classic-scroll-middle = - Pan the battlefield using the mouse button +label-mouse-control-desc-classic-edgescroll = or by moving the cursor to the edge of the screen +label-mouse-control-desc-modern-selection = - Select units using the mouse button +label-mouse-control-desc-modern-commands = - Command units using the mouse button +label-mouse-control-desc-modern-buildigs = - Place structures using the mouse button +label-mouse-control-desc-modern-support = - Target support powers using the mouse button +label-mouse-control-desc-modern-zoom = - Zoom the battlefield using the +label-mouse-control-desc-modern-zoom-modifier = - Zoom the battlefield using +label-mouse-control-desc-modern-scroll-right = - Pan the battlefield using the mouse button +label-mouse-control-desc-modern-scroll-middle = - Pan the battlefield using the mouse button +label-mouse-control-desc-modern-edgescroll = or by moving the cursor to the edge of the screen +checkbox-edgescroll-container = Screen Edge Panning + +## mainmenu.yaml +label-main-menu-mainmenu-title = Main Menu +button-main-menu-singleplayer = Singleplayer +button-main-menu-multiplayer = Multiplayer +button-main-menu-settings = Settings +button-main-menu-extras = Extras +button-main-menu-content = Manage Content +button-main-menu-quit = Quit +label-singleplayer-menu-title = Singleplayer +button-singleplayer-menu-skirmish = Skirmish +button-singleplayer-menu-missions = Missions +button-singleplayer-menu-load = Load +button-singleplayer-menu-back = Back +label-extras-menu-title = Extras +button-extras-menu-replays = Replays +button-extras-menu-music = Music +button-extras-menu-map-editor = Map Editor +button-extras-menu-assetbrowser = Asset Browser +button-extras-menu-credits = Credits +button-extras-menu-back = Back +label-map-editor-menu-title = Map Editor +button-map-editor-menu-new = New Map +button-map-editor-menu-load = Load Map +button-map-editor-menu-back = Back +dropdownbutton-news-bg-button = Battlefield News +label-update-notice-a = You are running an outdated version of OpenRA. +label-update-notice-b = Download the latest version from www.openra.net + +## mapchooser.yaml +label-mapchooser-panel-title = Select Map +button-bg-system-maps-tab = Official Maps +button-bg-user-maps-tab = Custom Maps +label-filter-order-controls-desc = Filter: +label-filter-order-controls-desc-joiner = in +label-filter-order-controls-orderby = Order by: +button-bg-randommap = Random +button-bg-delete-map = Delete Map +button-bg-delete-all-maps = Delete All Maps +button-bg-ok = Ok + +## mapchooser.yaml, gamesave-browser.yaml +button-bg-cancel = Back + +## missionbrowser.yaml +label-missionbrowser-panel-title = Missions +button-missionbrowser-panel-back = Back +button-missionbrowser-panel-mission-info = Mission Info +button-missionbrowser-panel-mission-options = Options +button-missionbrowser-panel-start-briefing-video = Watch Briefing +button-missionbrowser-panel-stop-briefing-video = Stop Briefing +button-missionbrowser-panel-start-info-video = Watch Info Video +button-missionbrowser-panel-stop-info-video = Stop Info Video +button-missionbrowser-panel-startgame = Play + +## multiplayer-browser.yaml +label-multiplayer-panel-title = Multiplayer +image-bg-password-protected-tooltip = Requires Password +image-bg-requires-authentication-tooltip = Requires OpenRA forum account +button-selected-server-join = Join +dropdownbutton-bg-filters = Filter Games +button-bg-directconnect = Direct IP +button-bg-create = Create +button-multiplayer-panel-back = Back + +## multiplayer-browser.yaml, lobby-servers.yaml +label-container-name = Server +label-container-players = Players +label-container-location = Location +label-container-status = Status +label-bg-outdated-version = You are running an outdated version of OpenRA. Download the latest version from www.openra.net +label-bg-unknown-version = You are running an unrecognized version of OpenRA. Download the latest version from www.openra.net +label-bg-playtest-available = A preview of the next OpenRA release is available for testing. Download the playtest from www.openra.net + +## multiplayer-browserpanels.yaml +checkbox-multiplayer-filter-panel-waiting-for-players = Waiting +checkbox-multiplayer-filter-panel-empty = Empty +checkbox-multiplayer-filter-panel-password-protected = Protected +checkbox-multiplayer-filter-panel-already-started = Started +checkbox-multiplayer-filter-panel-incompatible-version = Incompatible + +## multiplayer-createserver.yaml +label-multiplayer-createserver-panel-title = Create Server +label-bg-server-name = Server Name: +label-bg-password = Password: +label-bg-after-password = (optional) +label-bg-listen-port = Port: +checkbox-bg-advertise = Advertise Online +label-notices-lan-advertising = - Game will be advertised to the Local Area Network only. +label-notices-lan-firewall = - You must manually configure your firewall to allow connections. +label-notices-lan-portforward-a = - Players can connect using Direct IP from the Internet if you +label-notices-lan-portforward-b = manually configure port forwarding on your router. +label-notices-no-upnp-advertising = - Game will be advertised to the Local Area Network and Internet. +label-notices-no-upnp-firewall = - You must manually configure your firewall to allow connections. +label-notices-no-upnp-portforward-a = - You must manually configure your router to allow and forward +label-notices-no-upnp-portforward-b = connections to your local IP and Port. +label-notices-no-upnp-settings-a = - You can enable UPnP/NAT-PMP (if supported by your router) +label-notices-no-upnp-settings-b = in the Advanced tab of the settings menu. +label-notices-upnp-advertising = - Game will be advertised to the Local Area Network and Internet. +label-notices-upnp-firewall = - You must manually configure your firewall to allow connections. +label-notices-upnp-portforward-a = - Game will automatically configure port forwarding. +label-notices-upnp-settings-a = - You can disable UPnP/NAT-PMP in the settings menu. +button-multiplayer-createserver-panel-back = Back +button-multiplayer-createserver-panel-map = Change Map +button-multiplayer-createserver-panel-create = Create + +## multiplayer-directconnect.yaml +label-directconnect-panel-title = Connect to Server +label-bg-address = Address: +label-bg-port = Port: +button-directconnect-panel-back = Back +button-directconnect-panel-join = Join + +## music.yaml +label-music-panel-title = Music Player +label-container-type = Length +checkbox-bg-shuffle = Shuffle +checkbox-bg-repeat = Loop +button-music-panel-back = Back + +## playerprofile.yaml +button-profile-header-destroy-key = Logout +label-generate-keys-desc-a = Connect to a forum account to identify +label-generate-keys-desc-b = yourself to other players, join private +label-generate-keys-desc-c = servers, and display badges. +button-generate-keys-key = Connect to an OpenRA forum account +label-generating-keys-desc-a = Generating authentication key pair. +label-generating-keys-desc-b = This will take several seconds... +label-register-fingerprint-desc-a = An authentication key has been copied to your +label-register-fingerprint-desc-b = clipboard. Add this to your User Control Panel +label-register-fingerprint-desc-c = on the OpenRA forum then press Continue. +button-register-fingerprint-delete-key = Cancel +button-register-fingerprint-check-key = Continue +label-checking-fingerprint-desc-a = Querying account details from +label-checking-fingerprint-desc-b = the OpenRA forum... +label-fingerprint-not-found-desc-a = Your authentication key is not connected +label-fingerprint-not-found-desc-b = to an OpenRA forum account. +button-fingerprint-not-found-continue = Back +label-connection-error-desc-a = Failed to connect to the OpenRA forum. +label-connection-error-desc-b = Please check your internet connection. +button-connection-error-retry = Retry + +## replaybrowser.yaml +label-replaybrowser-panel-title = Replay Viewer +label-filters-title = Filter +label-filters-flt-gametype-desc = Type: +dropdownbutton-filters-flt-gametype = Any +label-filters-flt-date-desc = Date: +dropdownbutton-filters-flt-date = Any +label-filters-flt-duration-desc = Duration: +dropdownbutton-filters-flt-duration = Any +label-filters-flt-mapname-desc = Map: +dropdownbutton-filters-flt-mapname = Any +label-filters-flt-player-desc = Player: +dropdownbutton-filters-flt-player = Anyone +label-filters-flt-outcome-desc = Outcome: +dropdownbutton-filters-flt-outcome = Any +label-filters-flt-faction-desc = Faction: +dropdownbutton-filters-flt-faction = Any +button-filters-flt-reset = Reset Filters +label-management-manage-title = Manage +button-management-mng-rensel = Rename +button-management-mng-delsel = Delete +button-management-mng-delall = Delete All +label-replay-list-container-replaybrowser-title = Choose Replay +button-replaybrowser-panel-cancel = Back +button-replaybrowser-panel-watch = Watch + +## settings-advanced.yaml +label-network-section-header = Advanced +checkbox-nat-discovery-container = Enable UPnP/NAT-PMP Discovery +checkbox-fetch-news-container = Fetch Community News +checkbox-perfgraph-container = Show Performance Graph +checkbox-check-version-container = Check for Updates +checkbox-perftext-container = Show Performance Text +checkbox-sendsysinfo-container = Send System Information +label-sendsysinfo-checkbox-container-desc = Your Operating System, OpenGL and .NET runtime versions, and language settings will be sent along with an anonymous ID to help prioritize future development. +label-debug-section-header = Developer +label-debug-hidden-container-a = Additional developer-specific options can be enabled via the +label-debug-hidden-container-b = Debug.DisplayDeveloperSettings setting or launch flag +checkbox-botdebug-container = Show Bot Debug Messages +checkbox-checkbotsync-container = Check Sync around BotModule Code +checkbox-luadebug-container = Show Map Debug Messages +checkbox-checkunsynced-container = Check Sync around Unsynced Code +checkbox-replay-commands-container = Enable Debug Commands in Replays +checkbox-perflogging-container = Enable Tick Performance Logging + +## settings-audio.yaml +label-audio-section-header = Audio +label-no-audio-device-container = Audio controls require an active sound device +checkbox-cash-ticks-container = Cash Ticks +checkbox-mute-sound-container = Mute Sound +label-sound-volume-container = Sound Volume: + +checkbox-mute-background-music-container = + .label = Mute Menu Music + .tooltip = Mute background music when no specific track is playing + +label-music-volume-container = Music Volume: +label-audio-device-container = Audio Device: +label-video-volume-container = Video Volume: +label-restart-required-container-audio-desc = Device changes will be applied after the game is restarted + +## settings-display.yaml +label-target-lines-dropdown-container = Target Lines: +label-status-bar-dropdown-container-bars = Status Bars: + +checkbox-player-stance-colors-container = + .label = Player Relationship Colors + .tooltip = Change minimap and health bar colors based on relationship (own, enemy, ally, neutral) + +checkbox-ui-feedback-container = + .label = Show UI Feedback Notifications + .tooltip = Show transient text notifications for UI events + +checkbox-transients-container = + .label = Show Game Event Notifications + .tooltip = Show transient text notifications for game events + +checkbox-hide-replay-chat-container = Hide Chat in Replays +label-video-section-header = Video +label-video-mode-dropdown-container = Video Mode: +dropdownbutton-video-mode-dropdown-container = Windowed +label-window-resolution-container-size = Window Size: +label-window-resolution-container-x = x +label-display-selection-container = Select Display: +dropdownbutton-display-selection-container-dropdown = Standard +checkbox-vsync-container = Enable VSync +checkbox-frame-limit-gamespeed-container = Limit framerate to game tick rate +label-gl-profile-dropdown-container = OpenGL Profile: +label-restart-required-container-video-desc = Display and OpenGL changes require restart + +## settings-hotkeys.yaml +label-hotkeys-panel-filter-input = Filter by name: +label-hotkeys-panel-context-dropdown = Context: +label-hotkey-empty-list-message = No hotkeys match the filter criteria. +label-notices-readonly-notice = This hotkey cannot be modified +button-hotkey-remap-dialog-override = Override + +button-hotkey-remap-dialog-clear = + .label = Clear + .tooltip = Unbind the hotkey + +button-hotkey-remap-dialog-reset = + .label = Reset + .tooltip = Reset to default + +## settings-input.yaml +label-zoom-modifier-container = Zoom Modifier: +checkbox-alternate-scroll-container = Alternate Mouse Panning +checkbox-lockmouse-container = Lock Mouse to Window +label-mouse-scroll-type-container = Pan Behaviour: +label-scrollspeed-slider-container-scroll-speed = Pan Speed: +label-zoomspeed-slider-container-zoom-speed = Zoom Speed: +label-ui-scrollspeed-slider-container-scroll-speed = UI Scroll Speed: + +## settings.yaml +label-settings-panel-title = Settings +button-settings-panel-back = Back +button-settings-panel-reset = Reset + +## tooltips.yaml +label-latency-tooltip-prefix = Latency: +label-anonymous-player-tooltip-name = Anonymous Player +label-game-admin = Game Admin + diff --git a/mods/cnc/languages/difficulties/en.ftl b/mods/cnc/languages/difficulties/en.ftl index 90ad41139706..5521c17cd52e 100644 --- a/mods/cnc/languages/difficulties/en.ftl +++ b/mods/cnc/languages/difficulties/en.ftl @@ -4,4 +4,5 @@ dropdown-difficulty = options-difficulty = .easy = Easy + .normal = Normal .hard = Hard diff --git a/mods/cnc/languages/rules/en.ftl b/mods/cnc/languages/rules/en.ftl index be758e9ab9c6..a2efaa4f8219 100644 --- a/mods/cnc/languages/rules/en.ftl +++ b/mods/cnc/languages/rules/en.ftl @@ -30,9 +30,6 @@ dropdown-map-creeps = .label = Creep Actors .description = Hostile forces spawn on the battlefield -options-difficulty = - .normal = Normal - ## Structures notification-construction-complete = Construction complete. notification-unit-ready = Unit ready. diff --git a/mods/cnc/maps/desert-rats-cnc/rules.yaml b/mods/cnc/maps/desert-rats-cnc/rules.yaml index 90138dffcdac..cc376d1fc2d9 100644 --- a/mods/cnc/maps/desert-rats-cnc/rules.yaml +++ b/mods/cnc/maps/desert-rats-cnc/rules.yaml @@ -1,5 +1,5 @@ World: - GlobalLightingPaletteEffect: + TintPostProcessEffect: Red: 1.1 Green: 0.95 Blue: 1.051 diff --git a/mods/cnc/maps/gdi06/rules.yaml b/mods/cnc/maps/gdi06/rules.yaml index 2ccae05d42ba..d4536b34a5ba 100644 --- a/mods/cnc/maps/gdi06/rules.yaml +++ b/mods/cnc/maps/gdi06/rules.yaml @@ -16,7 +16,7 @@ World: ParticleColors: 304074, 28386C, 202C60, 182C54 LineTailAlphaValue: 150 ParticleSize: 1, 1 - GlobalLightingPaletteEffect: + TintPostProcessEffect: Red: 0.75 Green: 0.85 Blue: 1.5 diff --git a/mods/cnc/maps/nod06a/languages/en.ftl b/mods/cnc/maps/nod06a/languages/en.ftl index 2088d2bcc64e..309a674b6521 100644 --- a/mods/cnc/maps/nod06a/languages/en.ftl +++ b/mods/cnc/maps/nod06a/languages/en.ftl @@ -4,5 +4,6 @@ dropdown-difficulty = options-difficulty = .easy = Easy + .normal = Normal .hard = Hard .tough = Real tough guy diff --git a/mods/cnc/maps/nod06b/languages/en.ftl b/mods/cnc/maps/nod06b/languages/en.ftl index 2088d2bcc64e..309a674b6521 100644 --- a/mods/cnc/maps/nod06b/languages/en.ftl +++ b/mods/cnc/maps/nod06b/languages/en.ftl @@ -4,5 +4,6 @@ dropdown-difficulty = options-difficulty = .easy = Easy + .normal = Normal .hard = Hard .tough = Real tough guy diff --git a/mods/cnc/maps/nod10b/nod10b.lua b/mods/cnc/maps/nod10b/nod10b.lua index a6326b26a301..f377de4223cc 100644 --- a/mods/cnc/maps/nod10b/nod10b.lua +++ b/mods/cnc/maps/nod10b/nod10b.lua @@ -36,9 +36,9 @@ DeliverCommando = function() end) Trigger.OnPlayerWon(Nod, function(Nod) - if not rambo.IsDead then - Nod.MarkCompletedObjective(KeepRamboAliveObjective) - end + if not rambo.IsDead then + Nod.MarkCompletedObjective(KeepRamboAliveObjective) + end end) end @@ -69,7 +69,7 @@ WorldLoaded = function() Utils.Do(Mammoths, function(mammoth) mammoth.Stance = "HoldFire" - end) + end) Utils.Do(MediumTanks, function(tank) Trigger.OnDamaged(tank, function() @@ -84,7 +84,7 @@ WorldLoaded = function() end end) end) - end) + end) Utils.Do(Grenadiers, function(grenadier) Trigger.OnDamaged(grenadier, function() @@ -99,11 +99,11 @@ WorldLoaded = function() end end) end) - end) + end) Utils.Do(GDIBuildings, function(building) RepairBuilding(GDI, building, 0.75) - end) + end) Trigger.OnEnteredFootprint({ NorthEntrance.Location }, function(a, id) if a.Owner == Nod then @@ -125,7 +125,7 @@ WorldLoaded = function() Utils.Do(Riflemen, function(rifleman) rifleman.Patrol(RiflemenPatrolPath) - end) + end) PatrollingMammoth.Patrol(MammothPatrolPath) end diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index b3b8af0dffe9..1291193f050d 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -146,6 +146,7 @@ ChromeLayout: Translations: common|languages/en.ftl common|languages/rules/en.ftl + cnc|languages/chrome/en.ftl cnc|languages/rules/en.ftl Voices: diff --git a/mods/cnc/rules/aircraft.yaml b/mods/cnc/rules/aircraft.yaml index 42c8bc563232..60ddd477f64c 100644 --- a/mods/cnc/rules/aircraft.yaml +++ b/mods/cnc/rules/aircraft.yaml @@ -22,6 +22,7 @@ TRAN: Type: Light RevealsShroud: Range: 10c0 + MoveRecalculationThreshold: 0 Type: GroundPosition WithIdleOverlay@ROTOR1AIR: Offset: 597,0,85 @@ -73,6 +74,7 @@ HELI: Type: Light RevealsShroud: Range: 10c0 + MoveRecalculationThreshold: 0 Type: GroundPosition Armament@PRIMARY: Weapon: HeliAGGun @@ -144,6 +146,7 @@ ORCA: Type: Light RevealsShroud: Range: 10c0 + MoveRecalculationThreshold: 0 Type: GroundPosition Armament@PRIMARY: Weapon: OrcaAGMissiles @@ -272,6 +275,7 @@ TRAN.Husk: Speed: 140 RevealsShroud: Range: 10c0 + MoveRecalculationThreshold: 0 Type: GroundPosition WithIdleOverlay@ROTOR1: Offset: 597,0,85 @@ -291,6 +295,7 @@ HELI.Husk: Speed: 186 RevealsShroud: Range: 10c0 + MoveRecalculationThreshold: 0 Type: GroundPosition WithIdleOverlay: Offset: 0,0,85 @@ -307,6 +312,7 @@ ORCA.Husk: Speed: 186 RevealsShroud: Range: 10c0 + MoveRecalculationThreshold: 0 Type: GroundPosition RenderSprites: Image: orca diff --git a/mods/cnc/rules/palettes.yaml b/mods/cnc/rules/palettes.yaml index 103bb91e91f4..5677d9259971 100644 --- a/mods/cnc/rules/palettes.yaml +++ b/mods/cnc/rules/palettes.yaml @@ -97,10 +97,10 @@ PlayerColorPalette: BasePalette: terrain RemapIndex: 176, 178, 180, 182, 184, 186, 189, 191, 177, 179, 181, 183, 185, 187, 188, 190 - MenuPaletteEffect: + MenuPostProcessEffect: MenuEffect: Desaturated CloakPaletteEffect: - FlashPaletteEffect: + FlashPostProcessEffect: RotationPaletteEffect@water: ExcludePalettes: effect, chrome RotationBase: 32 diff --git a/mods/cnc/rules/structures.yaml b/mods/cnc/rules/structures.yaml index 1b7dd4aaa81c..4d2bb59eda2b 100644 --- a/mods/cnc/rules/structures.yaml +++ b/mods/cnc/rules/structures.yaml @@ -464,6 +464,7 @@ AFLD: ProductionAirdrop: Produces: Vehicle.Nod ActorType: c17 + LandOffset: -1024,0,0 ReadyTextNotification: notification-reinforcements-have-arrived WithBuildingBib: WithIdleOverlay@DISH: diff --git a/mods/cnc/weapons/superweapons.yaml b/mods/cnc/weapons/superweapons.yaml index a957d7286184..e6a54576572d 100644 --- a/mods/cnc/weapons/superweapons.yaml +++ b/mods/cnc/weapons/superweapons.yaml @@ -92,7 +92,7 @@ Atomic: Duration: 20 Intensity: 5 Multiplier: 1,1 - Warhead@14FlashEffect: FlashPaletteEffect + Warhead@14FlashEffect: FlashEffect Duration: 20 IonCannon: diff --git a/mods/common/chrome/assetbrowser.yaml b/mods/common/chrome/assetbrowser.yaml index 9e691570e01b..408b428f7429 100644 --- a/mods/common/chrome/assetbrowser.yaml +++ b/mods/common/chrome/assetbrowser.yaml @@ -12,7 +12,7 @@ Background@ASSETBROWSER_PANEL: Height: 25 Font: Bold Align: Center - Text: Asset Browser + Text: label-assetbrowser-panel-title Label@SOURCE_SELECTOR_DESC: X: 20 Y: 36 @@ -20,21 +20,21 @@ Background@ASSETBROWSER_PANEL: Height: 25 Font: TinyBold Align: Center - Text: Select asset source + Text: label-assetbrowser-panel-source-selector-desc DropDownButton@SOURCE_SELECTOR: X: 20 Y: 60 Width: 195 Height: 25 Font: Bold - Text: Folders + Text: dropdownbutton-assetbrowser-panel-source-selector DropDownButton@ASSET_TYPES_DROPDOWN: X: 20 Y: 90 Width: 195 Height: 25 Font: Bold - Text: Asset types + Text: dropdownbutton-assetbrowser-panel-asset-types-dropdown Label@FILENAME_DESC: X: 20 Y: 115 @@ -42,7 +42,7 @@ Background@ASSETBROWSER_PANEL: Height: 25 Font: TinyBold Align: Center - Text: Filter by name + Text: label-assetbrowser-panel-filename-desc TextField@FILENAME_INPUT: X: 20 Y: 140 @@ -76,7 +76,7 @@ Background@ASSETBROWSER_PANEL: Height: 25 Font: Bold Align: Left - Text: Scale: + Text: label-assetbrowser-panel-sprite-scale Slider@SPRITE_SCALE_SLIDER: X: PARENT_RIGHT - WIDTH - 330 Y: 62 @@ -84,21 +84,6 @@ Background@ASSETBROWSER_PANEL: Height: 20 MinimumValue: 0.5 MaximumValue: 4 - Label@MODEL_SCALE: - X: PARENT_RIGHT - WIDTH - 440 - Y: 60 - Width: 40 - Height: 25 - Font: Bold - Align: Left - Text: Scale: - Slider@MODEL_SCALE_SLIDER: - X: PARENT_RIGHT - WIDTH - 330 - Y: 62 - Width: 100 - Height: 20 - MinimumValue: 10 - MaximumValue: 64 Label@PALETTE_DESC: X: PARENT_RIGHT - WIDTH - 270 Y: 60 @@ -106,7 +91,7 @@ Background@ASSETBROWSER_PANEL: Height: 25 Font: Bold Align: Right - Text: Palette: + Text: label-assetbrowser-panel-palette-desc DropDownButton@PALETTE_SELECTOR: X: PARENT_RIGHT - WIDTH - 110 Y: 60 @@ -138,17 +123,12 @@ Background@ASSETBROWSER_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM AspectRatio: 1 - Model@VOXEL: - Width: PARENT_RIGHT - Height: PARENT_BOTTOM - Palette: colorpicker - PlayerPalette: colorpicker Label@ERROR: Width: PARENT_RIGHT Height: PARENT_BOTTOM Align: Center Visible: false - Text: Error displaying file. See assetbrowser.log for details. + Text: label-sprite-bg-error Container@FRAME_SELECTOR: X: 226 Y: PARENT_BOTTOM - 75 @@ -222,54 +202,6 @@ Background@ASSETBROWSER_PANEL: Height: 25 Font: TinyBold Align: Left - Container@VOXEL_SELECTOR: - X: 226 - Y: PARENT_BOTTOM - 75 - Children: - Label@ROLL: - Y: 1 - Width: 40 - Height: 25 - Font: TinyBold - Align: Left - Text: Roll - Slider@ROLL_SLIDER: - X: 30 - Y: 3 - Width: 100 - Height: 20 - MinimumValue: 1 - MaximumValue: 1023 - Label@PITCH: - X: 150 - Y: 1 - Width: 40 - Height: 25 - Font: TinyBold - Align: Left - Text: Pitch - Slider@PITCH_SLIDER: - X: 190 - Y: 3 - Width: 100 - Height: 20 - MinimumValue: 1 - MaximumValue: 1023 - Label@YAW: - X: 305 - Y: 1 - Width: 40 - Height: 25 - Font: TinyBold - Align: Left - Text: Yaw - Slider@YAW_SLIDER: - X: 335 - Y: 3 - Width: 100 - Height: 20 - MinimumValue: 1 - MaximumValue: 1023 Button@CLOSE_BUTTON: Key: escape X: PARENT_RIGHT - 180 @@ -277,7 +209,7 @@ Background@ASSETBROWSER_PANEL: Width: 160 Height: 25 Font: Bold - Text: Close + Text: button-assetbrowser-panel-close TooltipContainer@TOOLTIP_CONTAINER: ScrollPanel@ASSET_TYPES_PANEL: @@ -291,3 +223,4 @@ ScrollPanel@ASSET_TYPES_PANEL: Y: 5 Width: PARENT_RIGHT - 29 Height: 20 + diff --git a/mods/common/chrome/color-picker.yaml b/mods/common/chrome/color-picker.yaml index 299125d98fb2..5cde07eacfe6 100644 --- a/mods/common/chrome/color-picker.yaml +++ b/mods/common/chrome/color-picker.yaml @@ -13,14 +13,14 @@ Background@COLOR_CHOOSER: Y: 95 Width: 76 Height: 25 - Text: Random + Text: button-color-chooser-random Font: Bold Button@STORE_BUTTON: X: 245 Y: 124 Width: 76 Height: 25 - Text: Store + Text: button-color-chooser-store Font: Bold ActorPreview@PREVIEW: X: 245 @@ -32,14 +32,14 @@ Background@COLOR_CHOOSER: Y: PARENT_BOTTOM - 30 Height: 25 Width: 80 - Text: Mixer + Text: button-color-chooser-mixer-tab Font: Bold Button@PALETTE_TAB_BUTTON: X: 85 Y: PARENT_BOTTOM - 30 Height: 25 Width: 80 - Text: Palette + Text: button-color-chooser-palette-tab Font: Bold Container@MIXER_TAB: X: 5 @@ -98,7 +98,7 @@ Background@COLOR_CHOOSER: Width: PARENT_RIGHT Height: 13 Align: Center - Text: Preset Colors + Text: label-preset-header Container@PRESET_AREA: Width: PARENT_RIGHT - 4 Height: 58 @@ -124,7 +124,7 @@ Background@COLOR_CHOOSER: Width: PARENT_RIGHT Height: 13 Align: Center - Text: Custom Colors + Text: label-custom-header Container@CUSTOM_AREA: Width: PARENT_RIGHT - 4 Height: 31 @@ -138,3 +138,4 @@ Background@COLOR_CHOOSER: Height: 29 Visible: false ClickSound: ClickSound + diff --git a/mods/common/chrome/confirmation-dialogs.yaml b/mods/common/chrome/confirmation-dialogs.yaml index c6a5ee0b4ef7..4956a337d9d7 100644 --- a/mods/common/chrome/confirmation-dialogs.yaml +++ b/mods/common/chrome/confirmation-dialogs.yaml @@ -21,7 +21,7 @@ Background@THREEBUTTON_PROMPT: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Confirm + Text: button-threebutton-prompt-confirm Font: Bold Key: return Visible: false @@ -30,7 +30,7 @@ Background@THREEBUTTON_PROMPT: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Restart + Text: button-threebutton-prompt-other Font: Bold Visible: false Button@CANCEL_BUTTON: @@ -38,7 +38,7 @@ Background@THREEBUTTON_PROMPT: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Cancel + Text: button-threebutton-prompt-cancel Font: Bold Key: escape Visible: false @@ -66,7 +66,7 @@ Background@TWOBUTTON_PROMPT: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Confirm + Text: button-twobutton-prompt-confirm Font: Bold Key: return Visible: false @@ -75,7 +75,7 @@ Background@TWOBUTTON_PROMPT: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Cancel + Text: button-twobutton-prompt-cancel Font: Bold Key: escape Visible: false @@ -108,7 +108,7 @@ Background@TEXT_INPUT_PROMPT: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: OK + Text: button-text-input-prompt-accept Font: Bold Key: return Button@CANCEL_BUTTON: @@ -116,6 +116,7 @@ Background@TEXT_INPUT_PROMPT: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Cancel + Text: button-text-input-prompt-cancel Font: Bold Key: escape + diff --git a/mods/common/chrome/connection.yaml b/mods/common/chrome/connection.yaml index 4b884d26760b..8e5b4aaaa541 100644 --- a/mods/common/chrome/connection.yaml +++ b/mods/common/chrome/connection.yaml @@ -31,20 +31,19 @@ Background@CONNECTIONFAILED_PANEL: Y: 111 Width: 95 Height: 25 - Text: Password: + Text: label-connectionfailed-panel-password Font: Bold PasswordField@PASSWORD: X: PARENT_RIGHT - 285 Y: 111 Width: 190 - MaxLength: 20 Height: 25 Button@RETRY_BUTTON: X: PARENT_RIGHT - 430 Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Retry + Text: button-connectionfailed-panel-retry Font: Bold Key: return Button@ABORT_BUTTON: @@ -52,7 +51,7 @@ Background@CONNECTIONFAILED_PANEL: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Abort + Text: button-connectionfailed-panel-abort Font: Bold Key: escape Button@QUIT_BUTTON: @@ -60,7 +59,7 @@ Background@CONNECTIONFAILED_PANEL: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Quit + Text: button-connectionfailed-panel-quit Font: Bold Key: escape @@ -76,7 +75,7 @@ Background@CONNECTING_PANEL: Y: 21 Width: 450 Height: 25 - Text: Connecting + Text: label-connecting-panel-title Align: Center Font: Bold Label@CONNECTING_DESC: @@ -90,7 +89,7 @@ Background@CONNECTING_PANEL: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Abort + Text: button-connecting-panel-abort Font: Bold Key: escape @@ -108,12 +107,12 @@ Background@CONNECTION_SWITCHMOD_PANEL: Height: 25 Align: Center Font: Bold - Text: Switch Mod + Text: label-connection-switchmod-panel-title Label@DESC: Y: 46 Width: PARENT_RIGHT Height: 25 - Text: This server is running a different mod: + Text: label-connection-switchmod-panel-desc Font: Bold Align: Center Container@MOD_CONTAINER: @@ -143,7 +142,7 @@ Background@CONNECTION_SWITCHMOD_PANEL: Y: 111 Width: PARENT_RIGHT Height: 25 - Text: Switch mods and join server? + Text: label-connection-switchmod-panel-desc2 Font: Bold Align: Center Button@SWITCH_BUTTON: @@ -151,7 +150,7 @@ Background@CONNECTION_SWITCHMOD_PANEL: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Switch + Text: button-connection-switchmod-panel-switch Font: Bold Key: return Button@ABORT_BUTTON: @@ -159,6 +158,7 @@ Background@CONNECTION_SWITCHMOD_PANEL: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Abort + Text: button-connection-switchmod-panel-abort Font: Bold Key: escape + diff --git a/mods/common/chrome/credits.yaml b/mods/common/chrome/credits.yaml index 486f4c53818c..4084c9f64fcc 100644 --- a/mods/common/chrome/credits.yaml +++ b/mods/common/chrome/credits.yaml @@ -11,7 +11,7 @@ Background@CREDITS_PANEL: Height: 25 Font: Bold Align: Center - Text: Credits + Text: label-credits-panel-title Container@TAB_CONTAINER: Visible: False X: 20 @@ -27,7 +27,7 @@ Background@CREDITS_PANEL: X: 140 Width: 140 Height: 31 - Text: OpenRA + Text: button-tab-container-engine Font: Bold ScrollPanel@CREDITS_DISPLAY: X: 20 @@ -46,6 +46,7 @@ Background@CREDITS_PANEL: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Close + Text: button-credits-panel-back Font: Bold Key: escape + diff --git a/mods/common/chrome/dropdowns.yaml b/mods/common/chrome/dropdowns.yaml index 3edd9d82df3b..0f9ed9de1984 100644 --- a/mods/common/chrome/dropdowns.yaml +++ b/mods/common/chrome/dropdowns.yaml @@ -182,3 +182,4 @@ ScrollPanel@NEWS_PANEL: Height: PARENT_BOTTOM Align: Center VAlign: Middle + diff --git a/mods/common/chrome/editor.yaml b/mods/common/chrome/editor.yaml index 47ecca3aa3ae..c237ef7904f7 100644 --- a/mods/common/chrome/editor.yaml +++ b/mods/common/chrome/editor.yaml @@ -10,7 +10,7 @@ Background@NEW_MAP_BG: Y: 21 Width: 300 Height: 25 - Text: New Map + Text: label-new-map-bg-title Align: Center Font: Bold Label@TILESET_LABEL: @@ -19,7 +19,7 @@ Background@NEW_MAP_BG: Width: 95 Height: 25 Align: Right - Text: Tileset: + Text: label-new-map-bg-tileset DropDownButton@TILESET: X: 120 Y: 60 @@ -31,7 +31,7 @@ Background@NEW_MAP_BG: Width: 95 Height: 25 Align: Right - Text: Width: + Text: label-new-map-bg-width TextField@WIDTH: X: 120 Y: 95 @@ -46,7 +46,7 @@ Background@NEW_MAP_BG: Width: 95 Height: 25 Align: Right - Text: Height: + Text: label-new-map-bg-height TextField@HEIGHT: X: 230 Y: 95 @@ -60,7 +60,7 @@ Background@NEW_MAP_BG: Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Create + Text: button-new-map-bg-create Font: Bold Key: return Button@CANCEL_BUTTON: @@ -68,7 +68,7 @@ Background@NEW_MAP_BG: Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Cancel + Text: button-new-map-bg-cancel Font: Bold Key: escape @@ -84,7 +84,7 @@ Background@SAVE_MAP_PANEL: Y: 21 Width: 250 Height: 25 - Text: Save Map + Text: label-save-map-panel-title.label Align: Center Font: Bold Label@TITLE_LABEL: @@ -93,7 +93,7 @@ Background@SAVE_MAP_PANEL: Width: 95 Height: 25 Align: Right - Text: Title: + Text: label-save-map-panel-title.label TextField@TITLE: X: 110 Y: 60 @@ -106,7 +106,7 @@ Background@SAVE_MAP_PANEL: Width: 95 Height: 25 Align: Right - Text: Author: + Text: label-save-map-panel-author TextField@AUTHOR: X: 110 Y: 95 @@ -119,20 +119,20 @@ Background@SAVE_MAP_PANEL: Width: 95 Height: 25 Align: Right - Text: Visibility: + Text: label-save-map-panel-visibility DropDownButton@VISIBILITY_DROPDOWN: X: 110 Y: 130 Width: 220 Height: 25 - Text: Map Visibility + Text: dropdownbutton-save-map-panel-visibility-dropdown Label@DIRECTORY_LABEL: X: 10 Y: 165 Width: 95 Height: 25 Align: Right - Text: Directory: + Text: label-save-map-panel-directory DropDownButton@DIRECTORY_DROPDOWN: X: 110 Y: 165 @@ -144,7 +144,7 @@ Background@SAVE_MAP_PANEL: Width: 95 Height: 25 Align: Right - Text: Filename: + Text: label-save-map-panel-filename TextField@FILENAME: X: 110 Y: 200 @@ -161,14 +161,14 @@ Background@SAVE_MAP_PANEL: Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Save + Text: button-save-map-panel Font: Bold Button@BACK_BUTTON: X: 210 Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Cancel + Text: button-save-map-panel-back Font: Bold Key: escape @@ -234,7 +234,7 @@ Container@EDITOR_WORLD_ROOT: Y: 45 Width: 55 Height: 25 - Text: ID + Text: label-actor-edit-panel-id Align: Right TextField@ACTOR_ID: X: 84 @@ -305,19 +305,19 @@ Container@EDITOR_WORLD_ROOT: X: 15 Width: 75 Height: 25 - Text: Delete + Text: button-container-delete Font: Bold Button@CANCEL_BUTTON: X: 125 Width: 75 Height: 25 - Text: Cancel + Text: button-container-cancel Font: Bold Button@OK_BUTTON: X: 205 Width: 75 Height: 25 - Text: OK + Text: button-container-ok Font: Bold ViewportController: Width: WINDOW_RIGHT @@ -360,7 +360,7 @@ Container@EDITOR_WORLD_ROOT: Y: 12 Width: 55 Height: 25 - Text: Search: + Text: label-tiles-bg-search Align: Right Font: TinyBold TextField@SEARCH_TEXTFIELD: @@ -372,7 +372,7 @@ Container@EDITOR_WORLD_ROOT: Y: 36 Width: 55 Height: 25 - Text: Filter: + Text: label-tiles-bg-categories Align: Right Font: TinyBold DropDownButton@CATEGORIES_DROPDOWN: @@ -438,7 +438,7 @@ Container@EDITOR_WORLD_ROOT: Y: 12 Width: 55 Height: 25 - Text: Search: + Text: label-actors-bg-search Align: Right Font: TinyBold TextField@SEARCH_TEXTFIELD: @@ -450,7 +450,7 @@ Container@EDITOR_WORLD_ROOT: Y: 36 Width: 55 Height: 25 - Text: Filter: + Text: label-actors-bg-categories Align: Right Font: TinyBold DropDownButton@CATEGORIES_DROPDOWN: @@ -463,7 +463,7 @@ Container@EDITOR_WORLD_ROOT: Y: 60 Width: 55 Height: 25 - Text: Owner: + Text: label-actors-bg-owners Align: Right Font: TinyBold DropDownButton@OWNERS_DROPDOWN: @@ -535,41 +535,41 @@ Container@EDITOR_WORLD_ROOT: X: 0 Width: 70 Height: 25 - Text: Tiles + Text: button-map-editor-tab-container-tiles.label Font: Bold Key: EditorTilesTab TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Tiles + TooltipText: button-map-editor-tab-container-tiles.tooltip TooltipContainer: TOOLTIP_CONTAINER Button@OVERLAYS_TAB: X: 70 Width: 90 Height: 25 - Text: Overlays + Text: button-map-editor-tab-container-overlays.label Font: Bold Key: EditorOverlaysTab TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Overlays + TooltipText: button-map-editor-tab-container-overlays.tooltip TooltipContainer: TOOLTIP_CONTAINER Button@ACTORS_TAB: X: 160 Width: 70 Height: 25 - Text: Actors + Text: button-map-editor-tab-container-actors.label Font: Bold Key: EditorActorsTab TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Actors + TooltipText: button-map-editor-tab-container-actors.tooltip TooltipContainer: TOOLTIP_CONTAINER Button@HISTORY_TAB: X: 230 Width: 70 Height: 25 - Text: History + Text: button-map-editor-tab-container-history.label Font: Bold Key: EditorHistoryTab TooltipTemplate: BUTTON_TOOLTIP - TooltipText: History + TooltipText: button-map-editor-tab-container-history.tooltip TooltipContainer: TOOLTIP_CONTAINER MenuButton@OPTIONS_BUTTON: Logic: MenuButtonsChromeLogic @@ -578,8 +578,8 @@ Container@EDITOR_WORLD_ROOT: Pause: true Width: 60 Height: 25 - Text: Menu - TooltipText: Menu + Text: button-editor-world-root-options.label + TooltipText: button-editor-world-root-options.tooltip TooltipContainer: TOOLTIP_CONTAINER Font: Bold Key: escape @@ -587,9 +587,9 @@ Container@EDITOR_WORLD_ROOT: X: 70 Width: 90 Height: 25 - Text: Copy/Paste + Text: button-editor-world-root-copypaste.label TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Copy + TooltipText: button-editor-world-root-copypaste.tooltip TooltipContainer: TOOLTIP_CONTAINER Font: Bold Key: EditorCopy @@ -597,33 +597,33 @@ Container@EDITOR_WORLD_ROOT: X: 170 Width: 140 Height: 25 - Text: Copy Filters + Text: dropdownbutton-editor-world-root-copyfilter-button Font: Bold Button@UNDO_BUTTON: X: 320 Height: 25 Width: 70 - Text: Undo + Text: button-editor-world-root-undo.label Font: Bold Key: EditorUndo TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Undo last step + TooltipText: button-editor-world-root-undo.tooltip TooltipContainer: TOOLTIP_CONTAINER Button@REDO_BUTTON: X: 400 Height: 25 Width: 70 - Text: Redo + Text: button-editor-world-root-redo.label Font: Bold Key: EditorRedo TooltipTemplate: BUTTON_TOOLTIP - TooltipText: Redo last step + TooltipText: button-editor-world-root-redo.tooltip TooltipContainer: TOOLTIP_CONTAINER DropDownButton@OVERLAY_BUTTON: X: 480 Width: 140 Height: 25 - Text: Overlays + Text: dropdownbutton-editor-world-root-overlay-button Font: Bold Label@COORDINATE_LABEL: X: 630 @@ -654,14 +654,14 @@ ScrollPanel@CATEGORY_FILTER_PANEL: Y: 0 - 5 Width: 93 Height: 25 - Text: All + Text: button-select-categories-buttons-all Font: Bold Button@SELECT_NONE: X: 10 + 93 + 10 Y: 0 - 5 Width: 93 Height: 25 - Text: None + Text: button-select-categories-buttons-none Font: Bold Checkbox@CATEGORY_TEMPLATE: X: 5 @@ -694,3 +694,4 @@ ScrollPanel@OVERLAY_PANEL: Width: PARENT_RIGHT - 29 Height: 20 Visible: false + diff --git a/mods/common/chrome/gamesave-browser.yaml b/mods/common/chrome/gamesave-browser.yaml index dc64e8065170..899c15fdc00e 100644 --- a/mods/common/chrome/gamesave-browser.yaml +++ b/mods/common/chrome/gamesave-browser.yaml @@ -11,7 +11,7 @@ Background@GAMESAVE_BROWSER_PANEL: Height: 25 Font: Bold Align: Center - Text: Load game + Text: label-gamesave-browser-panel-load-title Visible: False Label@SAVE_TITLE: Width: PARENT_RIGHT @@ -19,7 +19,7 @@ Background@GAMESAVE_BROWSER_PANEL: Height: 25 Font: Bold Align: Center - Text: Save game + Text: label-gamesave-browser-panel-save-title Visible: False ScrollPanel@GAME_LIST: X: 20 @@ -37,7 +37,7 @@ Background@GAMESAVE_BROWSER_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Align: Center - Text: [CREATE NEW FILE] + Text: label-gamesave-browser-panel-title ScrollItem@GAME_TEMPLATE: Width: PARENT_RIGHT - 27 Height: 25 @@ -73,21 +73,21 @@ Background@GAMESAVE_BROWSER_PANEL: Y: PARENT_BOTTOM - 45 Width: 100 Height: 25 - Text: Back + Text: button-gamesave-browser-panel-cancel Font: Bold Button@DELETE_ALL_BUTTON: X: PARENT_RIGHT - 350 - WIDTH Y: PARENT_BOTTOM - 45 Width: 100 Height: 25 - Text: Delete All + Text: button-gamesave-browser-panel-delete-all Font: Bold Button@DELETE_BUTTON: X: PARENT_RIGHT - 240 - WIDTH Y: PARENT_BOTTOM - 45 Width: 100 Height: 25 - Text: Delete + Text: button-gamesave-browser-panel-delete Font: Bold Key: Delete Button@RENAME_BUTTON: @@ -95,7 +95,7 @@ Background@GAMESAVE_BROWSER_PANEL: Y: PARENT_BOTTOM - 45 Width: 100 Height: 25 - Text: Rename + Text: button-gamesave-browser-panel-rename Font: Bold Key: F2 Button@LOAD_BUTTON: @@ -104,7 +104,7 @@ Background@GAMESAVE_BROWSER_PANEL: Y: PARENT_BOTTOM - 45 Width: 100 Height: 25 - Text: Load + Text: button-gamesave-browser-panel-load Font: Bold Visible: False Button@SAVE_BUTTON: @@ -113,7 +113,8 @@ Background@GAMESAVE_BROWSER_PANEL: Y: PARENT_BOTTOM - 45 Width: 100 Height: 25 - Text: Save + Text: button-gamesave-browser-panel-save Font: Bold Visible: False TooltipContainer@GAMESAVE_TOOLTIP_CONTAINER: + diff --git a/mods/common/chrome/gamesave-loading.yaml b/mods/common/chrome/gamesave-loading.yaml index c529507542c1..a1e463e792b9 100644 --- a/mods/common/chrome/gamesave-loading.yaml +++ b/mods/common/chrome/gamesave-loading.yaml @@ -20,7 +20,7 @@ Container@GAMESAVE_LOADING_SCREEN: Height: 25 Font: Bold Align: Center - Text: Loading Saved Game + Text: label-gamesave-loading-screen-title ProgressBar@PROGRESS: X: (WINDOW_RIGHT - 500) / 2 Y: 3 * WINDOW_BOTTOM / 4 @@ -32,4 +32,5 @@ Container@GAMESAVE_LOADING_SCREEN: Height: 25 Font: Regular Align: Center - Text: Press Escape to cancel loading and return to the main menu + Text: label-gamesave-loading-screen-desc + diff --git a/mods/common/chrome/ingame-chat.yaml b/mods/common/chrome/ingame-chat.yaml index c2b26a93da0a..53a43c560264 100644 --- a/mods/common/chrome/ingame-chat.yaml +++ b/mods/common/chrome/ingame-chat.yaml @@ -29,10 +29,10 @@ Container@CHAT_PANEL: Y: PARENT_BOTTOM - HEIGHT Width: 50 Height: 25 - Text: Team + Text: button-chat-chrome-mode.label Font: Bold Key: ToggleChatMode - TooltipText: Toggle chat mode + TooltipText: button-chat-chrome-mode.tooltip TooltipContainer: TOOLTIP_CONTAINER TextField@CHAT_TEXTFIELD: X: 55 @@ -58,3 +58,4 @@ Container@CHAT_PANEL: TopBottomSpacing: 3 ItemSpacing: 4 Align: Bottom + diff --git a/mods/common/chrome/ingame-debug-hpf.yaml b/mods/common/chrome/ingame-debug-hpf.yaml index 47721c3aacbf..ddc827fe6b13 100644 --- a/mods/common/chrome/ingame-debug-hpf.yaml +++ b/mods/common/chrome/ingame-debug-hpf.yaml @@ -7,11 +7,12 @@ Container@HPF_OVERLAY: Y: PARENT_TOP Width: PARENT_RIGHT Height: 25 - Text: Select Locomotor + Text: dropdownbutton-hpf-overlay-locomotor Font: Regular DropDownButton@HPF_OVERLAY_CHECK: Y: PARENT_TOP + 35 Width: PARENT_RIGHT Height: 25 - Text: Select BlockedByActor + Text: dropdownbutton-hpf-overlay-check Font: Regular + diff --git a/mods/common/chrome/ingame-debug.yaml b/mods/common/chrome/ingame-debug.yaml index 676a3407725d..43df43d45fa5 100644 --- a/mods/common/chrome/ingame-debug.yaml +++ b/mods/common/chrome/ingame-debug.yaml @@ -7,7 +7,7 @@ Container@DEBUG_PANEL: Label@TITLE: Y: 26 Font: Bold - Text: Debug Options + Text: label-debug-panel-title Align: Center Width: PARENT_RIGHT Checkbox@INSTANT_BUILD: @@ -16,74 +16,74 @@ Container@DEBUG_PANEL: Width: 200 Height: 20 Font: Regular - Text: Instant Build Speed + Text: checkbox-debug-panel-instant-build Checkbox@ENABLE_TECH: X: 45 Y: 75 Width: 200 Height: 20 Font: Regular - Text: Build Everything + Text: checkbox-debug-panel-enable-tech Checkbox@BUILD_ANYWHERE: X: 45 Y: 105 Width: 200 Height: 20 Font: Regular - Text: Build Anywhere + Text: checkbox-debug-panel-build-anywhere Checkbox@UNLIMITED_POWER: X: 290 Y: 45 Width: 200 Height: 20 Font: Regular - Text: Unlimited Power + Text: checkbox-debug-panel-unlimited-power Checkbox@INSTANT_CHARGE: X: 290 Y: 75 Width: 200 Height: 20 Font: Regular - Text: Instant Charge Time + Text: checkbox-debug-panel-instant-charge Checkbox@DISABLE_VISIBILITY_CHECKS: X: 290 Y: 105 Height: 20 Width: 200 Font: Regular - Text: Disable Visibility Checks + Text: checkbox-debug-panel-disable-visibility-checks Button@GIVE_CASH: X: 90 Y: 150 Width: 140 Height: 30 Font: Bold - Text: Give $20,000 + Text: button-debug-panel-give-cash Button@GROW_RESOURCES: X: 271 Y: 150 Width: 140 Height: 30 Font: Bold - Text: Grow Resources + Text: button-debug-panel-grow-resources Button@GIVE_EXPLORATION: X: 90 Y: 200 Width: 140 Height: 30 Font: Bold - Text: Clear Shroud + Text: button-debug-panel-give-exploration Button@RESET_EXPLORATION: X: 271 Y: 200 Width: 140 Height: 30 Font: Bold - Text: Reset Shroud + Text: button-debug-panel-reset-exploration Label@VISUALIZATIONS_TITLE: Y: 256 Font: Bold - Text: Visualizations + Text: label-debug-panel-visualizations-title Align: Center Width: PARENT_RIGHT Checkbox@SHOW_UNIT_PATHS: @@ -92,46 +92,47 @@ Container@DEBUG_PANEL: Width: 200 Height: 20 Font: Regular - Text: Show Unit Paths + Text: checkbox-debug-panel-show-unit-paths Checkbox@SHOW_CUSTOMTERRAIN_OVERLAY: X: 45 Y: 305 Height: 20 Width: 200 Font: Regular - Text: Show Custom Terrain + Text: checkbox-debug-panel-show-customterrain-overlay Checkbox@SHOW_ACTOR_TAGS: X: 45 Y: 335 Height: 20 Width: 200 Font: Regular - Text: Show Actor Tags + Text: checkbox-debug-panel-show-actor-tags Checkbox@SHOW_COMBATOVERLAY: X: 290 Y: 275 Height: 20 Width: 200 Font: Regular - Text: Show Combat Geometry + Text: checkbox-debug-panel-show-combatoverlay Checkbox@SHOW_GEOMETRY: X: 290 Y: 305 Height: 20 Width: 200 Font: Regular - Text: Show Render Geometry + Text: checkbox-debug-panel-show-geometry Checkbox@SHOW_TERRAIN_OVERLAY: X: 290 Y: 335 Height: 20 Width: 200 Font: Regular - Text: Show Terrain Geometry + Text: checkbox-debug-panel-show-terrain-overlay Checkbox@SHOW_SCREENMAP: X: 290 Y: 365 Height: 20 Width: 200 Font: Regular - Text: Show Screen Map + Text: checkbox-debug-panel-show-screenmap + diff --git a/mods/common/chrome/ingame-debuginfo.yaml b/mods/common/chrome/ingame-debuginfo.yaml index fd28c2984473..076e6adc1deb 100644 --- a/mods/common/chrome/ingame-debuginfo.yaml +++ b/mods/common/chrome/ingame-debuginfo.yaml @@ -12,3 +12,4 @@ Container@DEBUG_WIDGETS: Align: Center Font: Bold Contrast: true + diff --git a/mods/common/chrome/ingame-fmvplayer.yaml b/mods/common/chrome/ingame-fmvplayer.yaml index 00cfcdcd30b2..217c9a8b43ab 100644 --- a/mods/common/chrome/ingame-fmvplayer.yaml +++ b/mods/common/chrome/ingame-fmvplayer.yaml @@ -8,3 +8,4 @@ Background@FMVPLAYER: Y: 0 Width: WINDOW_RIGHT Height: WINDOW_BOTTOM + diff --git a/mods/common/chrome/ingame-info-lobby-options.yaml b/mods/common/chrome/ingame-info-lobby-options.yaml index 331b141f5354..9fc926a6e78e 100644 --- a/mods/common/chrome/ingame-info-lobby-options.yaml +++ b/mods/common/chrome/ingame-info-lobby-options.yaml @@ -61,3 +61,4 @@ Container@LOBBY_OPTIONS_PANEL: Font: Regular Visible: False TooltipContainer: TOOLTIP_CONTAINER + diff --git a/mods/common/chrome/ingame-info.yaml b/mods/common/chrome/ingame-info.yaml index b64246380956..7d32d266c803 100644 --- a/mods/common/chrome/ingame-info.yaml +++ b/mods/common/chrome/ingame-info.yaml @@ -140,3 +140,4 @@ Container@GAME_INFO_PANEL: Y: 65 Width: PARENT_RIGHT Height: PARENT_BOTTOM + diff --git a/mods/common/chrome/ingame-infobriefing.yaml b/mods/common/chrome/ingame-infobriefing.yaml index d227858c398e..788b8ab51ad4 100644 --- a/mods/common/chrome/ingame-infobriefing.yaml +++ b/mods/common/chrome/ingame-infobriefing.yaml @@ -28,3 +28,4 @@ Container@MAP_PANEL: X: 4 Y: 2 Width: PARENT_RIGHT - 32 + diff --git a/mods/common/chrome/ingame-infochat.yaml b/mods/common/chrome/ingame-infochat.yaml index 6d681bfa9c3f..53ed447e8013 100644 --- a/mods/common/chrome/ingame-infochat.yaml +++ b/mods/common/chrome/ingame-infochat.yaml @@ -17,10 +17,10 @@ Container@CHAT_CONTAINER: Y: PARENT_BOTTOM - HEIGHT Width: 50 Height: 25 - Text: Team + Text: button-chat-chrome-mode.label Font: Bold Key: ToggleChatMode - TooltipText: Toggle chat mode + TooltipText: button-chat-chrome-mode.tooltip TooltipContainer: TOOLTIP_CONTAINER TextField@CHAT_TEXTFIELD: X: 55 @@ -32,3 +32,4 @@ Container@CHAT_CONTAINER: Height: PARENT_BOTTOM - 30 TopBottomSpacing: 3 ItemSpacing: 2 + diff --git a/mods/common/chrome/ingame-infoobjectives.yaml b/mods/common/chrome/ingame-infoobjectives.yaml index 454210770481..c3046db00e50 100644 --- a/mods/common/chrome/ingame-infoobjectives.yaml +++ b/mods/common/chrome/ingame-infoobjectives.yaml @@ -9,7 +9,7 @@ Container@MISSION_OBJECTIVES: Width: 80 Height: 25 Font: MediumBold - Text: Mission: + Text: label-mission-objectives Label@MISSION_STATUS: X: 100 Y: 21 @@ -41,3 +41,4 @@ Container@MISSION_OBJECTIVES: Height: PARENT_BOTTOM Disabled: True TextColorDisabled: FFFFFF + diff --git a/mods/common/chrome/ingame-infoscripterror.yaml b/mods/common/chrome/ingame-infoscripterror.yaml index 4931a846686d..0ef52fd904b7 100644 --- a/mods/common/chrome/ingame-infoscripterror.yaml +++ b/mods/common/chrome/ingame-infoscripterror.yaml @@ -10,7 +10,7 @@ Container@SCRIPT_ERROR_PANEL: Height: 20 Font: Bold Align: Center - Text: The map script has encountered a fatal error + Text: label-script-error-panel-desca Label@DESCB: X: 15 Y: 46 @@ -18,7 +18,7 @@ Container@SCRIPT_ERROR_PANEL: Height: 20 Font: Regular Align: Center - Text: The details of the error have been saved to lua.log in the logs directory. + Text: label-script-error-panel-descb Label@DESCC: X: 15 Y: 66 @@ -26,7 +26,7 @@ Container@SCRIPT_ERROR_PANEL: Height: 20 Font: Regular Align: Center - Text: Please send this file to the map author so that they can fix this issue. + Text: label-script-error-panel-descc ScrollPanel@SCRIPT_ERROR_MESSAGE_PANEL: X: 20 Y: 96 @@ -37,3 +37,4 @@ Container@SCRIPT_ERROR_PANEL: X: 4 Y: 2 Width: PARENT_RIGHT - 32 + diff --git a/mods/common/chrome/ingame-infostats.yaml b/mods/common/chrome/ingame-infostats.yaml index 77d2c080b11e..5c8129e7c8a5 100644 --- a/mods/common/chrome/ingame-infostats.yaml +++ b/mods/common/chrome/ingame-infostats.yaml @@ -12,7 +12,7 @@ Container@SKIRMISH_STATS: Width: 482 Height: 25 Font: MediumBold - Text: Mission: + Text: label-objective-mission Label@STATS_STATUS: X: 100 Y: 22 @@ -25,7 +25,7 @@ Container@SKIRMISH_STATS: Width: 482 Height: 20 Font: Bold - Text: Destroy all opposition! + Text: checkbox-objective-stats Disabled: true TextColorDisabled: FFFFFF Container@STATS_HEADERS: @@ -37,25 +37,25 @@ Container@SKIRMISH_STATS: X: 10 Width: 210 Height: 25 - Text: Player + Text: label-stats-name Font: Bold Label@FACTION: X: 230 Width: 120 Height: 25 - Text: Faction + Text: label-stats-faction Font: Bold Label@SCORE: X: 397 Width: 60 Height: 25 - Text: Score + Text: label-stats-score Font: Bold Label@ACTIONS: X: 457 Width: 20 Height: 25 - Text: Actions + Text: label-stats-actions Font: Bold ScrollPanel@PLAYER_LIST: X: 20 @@ -184,3 +184,4 @@ Container@SKIRMISH_STATS: ImageName: kick X: 7 Y: 7 + diff --git a/mods/common/chrome/ingame-menu.yaml b/mods/common/chrome/ingame-menu.yaml index c0dd23aca70f..079763cfb90c 100644 --- a/mods/common/chrome/ingame-menu.yaml +++ b/mods/common/chrome/ingame-menu.yaml @@ -36,7 +36,7 @@ Container@INGAME_MENU: Y: 20 Width: 200 Height: 30 - Text: Options + Text: label-menu-buttons-title Align: Center Font: Bold Button@BUTTON_TEMPLATE: @@ -45,3 +45,4 @@ Container@INGAME_MENU: Width: 140 Height: 30 Font: Bold + diff --git a/mods/common/chrome/ingame-perf.yaml b/mods/common/chrome/ingame-perf.yaml index 755123043330..3a201afc3122 100644 --- a/mods/common/chrome/ingame-perf.yaml +++ b/mods/common/chrome/ingame-perf.yaml @@ -20,3 +20,4 @@ Container@PERF_WIDGETS: Y: 5 Width: 200 Height: 200 + diff --git a/mods/common/chrome/ingame-transients.yaml b/mods/common/chrome/ingame-transients.yaml index df5124ec08db..e37d84191e22 100644 --- a/mods/common/chrome/ingame-transients.yaml +++ b/mods/common/chrome/ingame-transients.yaml @@ -11,3 +11,4 @@ Container@TRANSIENTS_PANEL: DisplayDurationMs: 4000 LogLength: 5 HideOverflow: False + diff --git a/mods/common/chrome/ingame.yaml b/mods/common/chrome/ingame.yaml index a4faab3df2c6..ff8f58d88ebc 100644 --- a/mods/common/chrome/ingame.yaml +++ b/mods/common/chrome/ingame.yaml @@ -63,3 +63,4 @@ Container@INGAME_ROOT: Container@MENU_ROOT: TooltipContainer@TOOLTIP_CONTAINER: MouseAttachment@MOUSE_ATTATCHMENT: + diff --git a/mods/common/chrome/lobby-kickdialogs.yaml b/mods/common/chrome/lobby-kickdialogs.yaml index 739caff8e1e1..f145104a4bf4 100644 --- a/mods/common/chrome/lobby-kickdialogs.yaml +++ b/mods/common/chrome/lobby-kickdialogs.yaml @@ -16,33 +16,33 @@ Background@KICK_CLIENT_DIALOG: Height: 25 Font: Regular Align: Center - Text: You may also apply a temporary ban, preventing + Text: label-kick-client-dialog-texta Label@TEXTB: Y: 86 Width: PARENT_RIGHT Height: 25 Font: Regular Align: Center - Text: them from joining for the remainder of this game. + Text: label-kick-client-dialog-textb Checkbox@PREVENT_REJOINING_CHECKBOX: X: (PARENT_RIGHT - WIDTH) / 2 Y: 120 Width: 150 Height: 20 - Text: Temporarily Ban + Text: checkbox-kick-client-dialog-prevent-rejoining Button@OK_BUTTON: X: (PARENT_RIGHT - WIDTH) / 2 + 75 Y: 155 Width: 120 Height: 25 - Text: Kick + Text: button-kick-client-dialog-ok Font: Bold Button@CANCEL_BUTTON: X: (PARENT_RIGHT - WIDTH) / 2 - 75 Y: 155 Width: 120 Height: 25 - Text: Cancel + Text: button-kick-client-dialog-cancel Font: Bold Background@KICK_SPECTATORS_DIALOG: @@ -57,7 +57,7 @@ Background@KICK_SPECTATORS_DIALOG: Height: 25 Font: Bold Align: Center - Text: Kick Spectators + Text: label-kick-spectators-dialog-title Label@TEXT: Y: 86 Width: PARENT_RIGHT @@ -69,14 +69,14 @@ Background@KICK_SPECTATORS_DIALOG: Y: 155 Width: 120 Height: 25 - Text: Ok + Text: button-kick-spectators-dialog-ok Font: Bold Button@CANCEL_BUTTON: X: (PARENT_RIGHT - WIDTH) / 2 - 75 Y: 155 Width: 120 Height: 25 - Text: Cancel + Text: button-kick-spectators-dialog-cancel Font: Bold Background@FORCE_START_DIALOG: @@ -90,21 +90,21 @@ Background@FORCE_START_DIALOG: Height: 25 Font: Bold Align: Center - Text: Start Game? + Text: label-force-start-dialog-title Label@TEXTA: Y: 68 Width: PARENT_RIGHT Height: 25 Font: Regular Align: Center - Text: One or more players are not yet ready. + Text: label-force-start-dialog-texta Label@TEXTB: Y: 86 Width: PARENT_RIGHT Height: 25 Font: Regular Align: Center - Text: Are you sure that you want to force start the game? + Text: label-force-start-dialog-textb Container@KICK_WARNING: Width: PARENT_RIGHT Children: @@ -115,7 +115,7 @@ Background@FORCE_START_DIALOG: Height: 25 Font: Bold Align: Center - Text: One or more clients are missing the selected + Text: label-kick-warning-a Label@KICK_WARNING_B: X: 0 Y: 124 @@ -123,18 +123,19 @@ Background@FORCE_START_DIALOG: Height: 25 Font: Bold Align: Center - Text: map, and will be kicked from the server. + Text: label-kick-warning-b Button@OK_BUTTON: X: (PARENT_RIGHT - WIDTH) / 2 + 75 Y: 155 Width: 120 Height: 25 - Text: Start + Text: button-force-start-dialog-ok Font: Bold Button@CANCEL_BUTTON: X: (PARENT_RIGHT - WIDTH) / 2 - 75 Y: 155 Width: 120 Height: 25 - Text: Cancel + Text: button-force-start-dialog-cancel Font: Bold + diff --git a/mods/common/chrome/lobby-mappreview.yaml b/mods/common/chrome/lobby-mappreview.yaml index 7fc448ebe24c..0908af84adc4 100644 --- a/mods/common/chrome/lobby-mappreview.yaml +++ b/mods/common/chrome/lobby-mappreview.yaml @@ -76,7 +76,7 @@ Container@MAP_PREVIEW: Height: 25 Font: Tiny Align: Center - Text: This map is not compatible + Text: label-map-incompatible-status-a IgnoreMouseOver: true Label@MAP_STATUS_B: Y: 201 @@ -84,7 +84,7 @@ Container@MAP_PREVIEW: Height: 25 Font: Tiny Align: Center - Text: with this version of OpenRA + Text: label-map-incompatible-status-b Container@MAP_VALIDATING: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -95,7 +95,7 @@ Container@MAP_PREVIEW: Height: 25 Font: Tiny Align: Center - Text: Validating... + Text: label-map-validating-status IgnoreMouseOver: true ProgressBar@MAP_VALIDATING_BAR: Y: 194 @@ -124,13 +124,13 @@ Container@MAP_PREVIEW: Width: PARENT_RIGHT Height: 25 Font: Bold - Text: Install Map + Text: button-map-download-available-install Button@MAP_UPDATE: Y: 195 Width: PARENT_RIGHT Height: 25 Font: Bold - Text: Update Map + Text: button-map-preview-update Container@MAP_UPDATE_DOWNLOAD_AVAILABLE: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -140,14 +140,14 @@ Container@MAP_PREVIEW: Width: PARENT_RIGHT Height: 25 Font: Bold - Text: Install Map + Text: button-map-update-download-available-install Label@MAP_SEARCHING: Y: 158 Width: PARENT_RIGHT Height: 25 Font: Tiny Align: Center - Text: Searching OpenRA Resource Center... + Text: label-map-preview-searching IgnoreMouseOver: true Container@MAP_UNAVAILABLE: Width: PARENT_RIGHT @@ -158,21 +158,21 @@ Container@MAP_PREVIEW: Height: 25 Font: Tiny Align: Center - Text: This map was not found on the + Text: label-map-unavailable-a Label@b: Y: 171 Width: PARENT_RIGHT Height: 25 Font: Tiny Align: Center - Text: OpenRA Resource Center + Text: label-map-unavailable-b Label@MAP_ERROR: Y: 158 Width: PARENT_RIGHT Height: 25 Font: Tiny Align: Center - Text: An error occurred during installation + Text: label-map-preview-error Container@MAP_DOWNLOADING: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -202,11 +202,12 @@ Container@MAP_PREVIEW: Height: 25 Font: Tiny Align: Center - Text: A new version of the map + Text: label-map-update-available-a Label@b: Y: 171 Width: PARENT_RIGHT Height: 25 Font: Tiny Align: Center - Text: was found on your computer + Text: label-map-update-available-b + diff --git a/mods/common/chrome/lobby-music.yaml b/mods/common/chrome/lobby-music.yaml index 38889c7c0e92..24fdd96ffd8b 100644 --- a/mods/common/chrome/lobby-music.yaml +++ b/mods/common/chrome/lobby-music.yaml @@ -11,20 +11,20 @@ Container@LOBBY_MUSIC_BIN: Label@MUSIC: Width: 268 Height: 25 - Text: Music + Text: label-container-music Align: Center Font: Bold Label@TITLE: X: 278 Width: 230 Height: 25 - Text: Track + Text: label-container-title Font: Bold Label@LENGTH: X: PARENT_RIGHT - 80 Height: 25 Width: 50 - Text: Length + Text: label-container-length Font: Bold Align: Right Background@CONTROLS: @@ -115,20 +115,20 @@ Container@LOBBY_MUSIC_BIN: Width: 85 Height: 20 Font: Regular - Text: Shuffle + Text: checkbox-controls-shuffle Checkbox@REPEAT: X: PARENT_RIGHT - 15 - WIDTH Y: 150 Width: 70 Height: 20 Font: Regular - Text: Loop + Text: checkbox-controls-repeat Label@VOLUME_LABEL: Y: 181 Width: 65 Height: 25 Align: Right - Text: Volume: + Text: label-controls-volume ExponentialSlider@MUSIC_SLIDER: X: 70 Y: 186 @@ -169,16 +169,17 @@ Container@LOBBY_MUSIC_BIN: Height: 25 Font: Bold Align: Center - Text: Music Not Installed + Text: label-no-music-title Label@DESCA: Y: 96 Width: PARENT_RIGHT - 24 Height: 25 Align: Center - Text: The game music can be installed + Text: label-no-music-desca Label@DESCB: Y: 116 Width: PARENT_RIGHT - 24 Height: 25 Align: Center - Text: from the "Manage Content" menu. + Text: label-no-music-descb + diff --git a/mods/common/chrome/lobby-options.yaml b/mods/common/chrome/lobby-options.yaml index 01073d12f950..d71ee68ed120 100644 --- a/mods/common/chrome/lobby-options.yaml +++ b/mods/common/chrome/lobby-options.yaml @@ -8,7 +8,7 @@ Container@LOBBY_OPTIONS_BIN: Height: 25 Font: Bold Align: Center - Text: Map Options + Text: label-lobby-options-bin-title ScrollPanel: Logic: LobbyOptionsLogic Width: PARENT_RIGHT @@ -83,3 +83,4 @@ Container@LOBBY_OPTIONS_BIN: Height: 25 Visible: False TooltipContainer: TOOLTIP_CONTAINER + diff --git a/mods/common/chrome/lobby-players.yaml b/mods/common/chrome/lobby-players.yaml index caacae59eda9..d105af322de8 100644 --- a/mods/common/chrome/lobby-players.yaml +++ b/mods/common/chrome/lobby-players.yaml @@ -11,49 +11,49 @@ Container@LOBBY_PLAYER_BIN: Label@LABEL_LOBBY_NAME: Width: 180 Height: 25 - Text: Name + Text: label-container-lobby-name Align: Center Font: Bold Label@LABEL_LOBBY_COLOR: X: 190 Width: 70 Height: 25 - Text: Color + Text: label-container-lobby-color Align: Center Font: Bold Label@LABEL_LOBBY_FACTION: X: 270 Width: 140 Height: 25 - Text: Faction + Text: label-container-lobby-faction Align: Center Font: Bold Label@LABEL_LOBBY_TEAM: X: 420 Width: 48 Height: 25 - Text: Team + Text: label-container-lobby-team Align: Center Font: Bold Label@LABEL_LOBBY_HANDICAP: X: 478 Width: 72 Height: 25 - Text: Handicap + Text: label-container-lobby-handicap Align: Center Font: Bold Label@LABEL_LOBBY_SPAWN: X: 560 Width: 48 Height: 25 - Text: Spawn + Text: label-container-lobby-spawn Align: Center Font: Bold Label@LABEL_LOBBY_STATUS: X: 617 Width: 20 Height: 25 - Text: Ready + Text: label-container-lobby-status Align: Left Font: Bold ScrollPanel@LOBBY_PLAYERS: @@ -109,7 +109,7 @@ Container@LOBBY_PLAYER_BIN: X: 15 Width: 165 Height: 25 - Text: Name + Text: dropdownbutton-template-editable-player-slot-options Font: Regular Visible: false DropDownButton@COLOR: @@ -140,23 +140,23 @@ Container@LOBBY_PLAYER_BIN: X: 40 Width: 70 Height: 25 - Text: Faction + Text: label-template-editable-player-factionname DropDownButton@TEAM_DROPDOWN: X: 420 Width: 48 Height: 25 - Text: Team + Text: dropdownbutton-template-editable-player-team-dropdown DropDownButton@HANDICAP_DROPDOWN: X: 478 Width: 72 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - TooltipText: A handicap decreases the combat effectiveness of the player's forces + TooltipText: dropdownbutton-template-editable-player-handicap-dropdown-tooltip DropDownButton@SPAWN_DROPDOWN: X: 560 Width: 48 Height: 25 - Text: Spawn + Text: dropdownbutton-template-editable-player-spawn-dropdown Checkbox@STATUS_CHECKBOX: X: 617 Y: 2 @@ -209,7 +209,7 @@ Container@LOBBY_PLAYER_BIN: X: 39 Width: 146 Height: 25 - Text: Name + Text: label-template-noneditable-player-name DropDownButton@PLAYER_ACTION: X: 15 Width: 165 @@ -250,12 +250,12 @@ Container@LOBBY_PLAYER_BIN: X: 40 Width: 70 Height: 25 - Text: Faction + Text: label-faction-factionname Label@TEAM: X: 420 Width: 23 Height: 25 - Text: Team + Text: label-template-noneditable-player-team Align: Center DropDownButton@TEAM_DROPDOWN: X: 420 @@ -272,7 +272,7 @@ Container@LOBBY_PLAYER_BIN: Width: 72 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - TooltipText: A handicap decreases the combat effectiveness of the player's forces + TooltipText: dropdownbutton-template-noneditable-player-handicap-dropdown-tooltip Label@SPAWN: X: 560 Width: 23 @@ -301,16 +301,16 @@ Container@LOBBY_PLAYER_BIN: Width: 165 Height: 25 X: 20 - Text: Name + Text: label-template-empty-name DropDownButton@SLOT_OPTIONS: X: 15 Width: 165 Height: 25 - Text: Name + Text: dropdownbutton-template-empty-slot-options Visible: false Button@JOIN: X: 190 - Text: Play in this slot + Text: button-template-empty-join Width: 418 Height: 25 Container@TEMPLATE_EDITABLE_SPECTATOR: @@ -359,7 +359,7 @@ Container@LOBBY_PLAYER_BIN: X: 190 Width: 418 Height: 25 - Text: Spectator + Text: label-template-editable-spectator Align: Center Font: Bold Checkbox@STATUS_CHECKBOX: @@ -414,7 +414,7 @@ Container@LOBBY_PLAYER_BIN: X: 39 Width: 179 Height: 25 - Text: Name + Text: label-template-noneditable-spectator-name DropDownButton@PLAYER_ACTION: X: 15 Width: 165 @@ -440,7 +440,7 @@ Container@LOBBY_PLAYER_BIN: X: 190 Width: 418 Height: 25 - Text: Spectator + Text: label-template-noneditable-spectator Align: Center Font: Bold Image@STATUS_IMAGE: @@ -462,12 +462,12 @@ Container@LOBBY_PLAYER_BIN: Width: 165 Height: 20 Font: Regular - Text: Allow Spectators? + Text: checkbox-template-new-spectator-toggle-spectators Button@SPECTATE: X: 190 Width: 418 Height: 25 - Text: Spectate + Text: button-template-new-spectator-spectate Font: Regular ScrollPanel@FACTION_DROPDOWN_TEMPLATE: @@ -503,3 +503,4 @@ ScrollPanel@FACTION_DROPDOWN_TEMPLATE: X: 40 Width: 70 Height: 25 + diff --git a/mods/common/chrome/lobby-servers.yaml b/mods/common/chrome/lobby-servers.yaml index 9d3fac57edc8..3fddf391321a 100644 --- a/mods/common/chrome/lobby-servers.yaml +++ b/mods/common/chrome/lobby-servers.yaml @@ -11,26 +11,26 @@ Container@LOBBY_SERVERS_BIN: X: 5 Width: 347 Height: 25 - Text: Server + Text: label-container-name Align: Center Font: Bold Label@PLAYERS: X: 382 Width: 85 Height: 25 - Text: Players + Text: label-container-players Font: Bold Label@LOCATION: X: 472 Width: 110 Height: 25 - Text: Location + Text: label-container-location Font: Bold Label@STATUS: X: 587 Width: 50 Height: 25 - Text: Status + Text: label-container-status Font: Bold LogicTicker@NOTICE_WATCHER: Background@NOTICE_CONTAINER: @@ -43,21 +43,21 @@ Container@LOBBY_SERVERS_BIN: Width: PARENT_RIGHT - 10 Height: 20 Align: Center - Text: You are running an outdated version of OpenRA. Download the latest version from www.openra.net + Text: label-notice-container-outdated-version Font: TinyBold Label@UNKNOWN_VERSION_LABEL: X: 5 Width: PARENT_RIGHT - 10 Height: 20 Align: Center - Text: You are running an unrecognized version of OpenRA. Download the latest version from www.openra.net + Text: label-notice-container-unknown-version Font: TinyBold Label@PLAYTEST_AVAILABLE_LABEL: X: 5 Width: PARENT_RIGHT - 10 Height: 20 Align: Center - Text: A preview of the next OpenRA release is available for testing. Download the playtest from www.openra.net + Text: label-notice-container-playtest-available Font: TinyBold ScrollPanel@SERVER_LIST: Width: PARENT_RIGHT @@ -95,7 +95,7 @@ Container@LOBBY_SERVERS_BIN: ImageCollection: lobby-bits TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP - TooltipText: Requires Password + TooltipText: image-lobby-servers-bin-password-protected-tooltip Image@REQUIRES_AUTHENTICATION: X: 364 Y: 6 @@ -104,7 +104,7 @@ Container@LOBBY_SERVERS_BIN: ImageCollection: lobby-bits TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP - TooltipText: Requires OpenRA forum account + TooltipText: image-lobby-servers-bin-requires-authentication-tooltip LabelWithTooltip@PLAYERS: X: 382 Width: 85 @@ -130,7 +130,7 @@ Container@LOBBY_SERVERS_BIN: Y: PARENT_BOTTOM + 5 Width: 154 Height: 25 - Text: Filter Games + Text: dropdownbutton-lobby-servers-bin-filters Font: Bold Button@RELOAD_BUTTON: X: 159 @@ -196,3 +196,4 @@ Container@LOBBY_SERVERS_BIN: Height: 25 Font: TinyBold Align: Center + diff --git a/mods/common/chrome/lobby.yaml b/mods/common/chrome/lobby.yaml index ea2e10e6d082..04fb04e9f21e 100644 --- a/mods/common/chrome/lobby.yaml +++ b/mods/common/chrome/lobby.yaml @@ -29,7 +29,7 @@ Background@SERVER_LOBBY: Width: 185 Height: 25 Font: Bold - Text: Slot Admin + Text: dropdownbutton-server-lobby-slots Container@SKIRMISH_TABS: X: 695 - WIDTH Width: 486 @@ -40,21 +40,21 @@ Background@SERVER_LOBBY: Width: 162 Height: 31 Font: Bold - Text: Players + Text: button-skirmish-tabs-players-tab Button@OPTIONS_TAB: X: 162 Y: 285 Width: 162 Height: 31 Font: Bold - Text: Options + Text: button-skirmish-tabs-options-tab Button@MUSIC_TAB: X: 2 * 162 Y: 285 Width: 162 Height: 31 Font: Bold - Text: Music + Text: button-skirmish-tabs-music-tab Container@MULTIPLAYER_TABS: X: 695 - WIDTH Width: 486 @@ -65,28 +65,28 @@ Background@SERVER_LOBBY: Width: 121 Height: 31 Font: Bold - Text: Players + Text: button-multiplayer-tabs-players-tab Button@OPTIONS_TAB: X: 121 Y: 285 Width: 122 Height: 31 Font: Bold - Text: Options + Text: button-multiplayer-tabs-options-tab Button@MUSIC_TAB: X: 243 Y: 285 Width: 121 Height: 31 Font: Bold - Text: Music + Text: button-multiplayer-tabs-music-tab Button@SERVERS_TAB: X: 364 Y: 285 Width: 122 Height: 31 Font: Bold - Text: Servers + Text: button-multiplayer-tabs-servers-tab Container@TOP_PANELS_ROOT: X: 20 Y: 67 @@ -97,7 +97,7 @@ Background@SERVER_LOBBY: Y: 291 Width: 174 Height: 25 - Text: Change Map + Text: button-server-lobby-changemap Font: Bold Container@LOBBYCHAT: X: 20 @@ -114,10 +114,10 @@ Background@SERVER_LOBBY: Y: PARENT_BOTTOM - HEIGHT Width: 50 Height: 25 - Text: Team + Text: button-lobbychat-chat-mode.label Font: Bold Key: ToggleChatMode - TooltipText: Toggle chat mode + TooltipText: button-lobbychat-chat-mode.tooltip TooltipContainer: TOOLTIP_CONTAINER TextField@CHAT_TEXTFIELD: X: 55 @@ -129,14 +129,15 @@ Background@SERVER_LOBBY: Y: PARENT_BOTTOM - HEIGHT - 20 Width: 120 Height: 25 - Text: Start Game + Text: button-server-lobby-start-game Font: Bold Button@DISCONNECT_BUTTON: X: PARENT_RIGHT - WIDTH - 20 Y: PARENT_BOTTOM - HEIGHT - 20 Width: 120 Height: 25 - Text: Leave Game + Text: button-server-lobby-disconnect Font: Bold Container@FACTION_DROPDOWN_PANEL_ROOT: TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/common/chrome/mainmenu-prompts.yaml b/mods/common/chrome/mainmenu-prompts.yaml index 1d941f82db6c..3f7d0b3747d8 100644 --- a/mods/common/chrome/mainmenu-prompts.yaml +++ b/mods/common/chrome/mainmenu-prompts.yaml @@ -11,21 +11,21 @@ Background@MAINMENU_INTRODUCTION_PROMPT: Height: 25 Font: Bold Align: Center - Text: Establishing Battlefield Control + Text: label-mainmenu-introduction-prompt-title Label@DESC_A: Width: PARENT_RIGHT Y: 50 Height: 16 Font: Regular Align: Center - Text: Welcome back Commander! Initialize combat parameters using the options below. + Text: label-mainmenu-introduction-prompt-desc-a Label@DESC_B: Width: PARENT_RIGHT Y: 68 Height: 16 Font: Regular Align: Center - Text: Additional options can be configured later from the Settings menu. + Text: label-mainmenu-introduction-prompt-desc-b ScrollPanel@SETTINGS_SCROLLPANEL: X: 20 Y: 100 @@ -48,7 +48,7 @@ Background@MAINMENU_INTRODUCTION_PROMPT: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Profile + Text: label-profile-section-header Container@ROW: Width: PARENT_RIGHT Height: 50 @@ -60,7 +60,7 @@ Background@MAINMENU_INTRODUCTION_PROMPT: Label@PLAYER: Width: PARENT_RIGHT Height: 20 - Text: Player Name: + Text: label-player-container TextField@PLAYERNAME: Y: 25 Width: PARENT_RIGHT @@ -74,7 +74,7 @@ Background@MAINMENU_INTRODUCTION_PROMPT: Label@COLOR: Width: PARENT_RIGHT Height: 20 - Text: Preferred Color: + Text: label-playercolor-container-color DropDownButton@PLAYERCOLOR: Y: 25 Width: 75 @@ -100,7 +100,7 @@ Background@MAINMENU_INTRODUCTION_PROMPT: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Input + Text: label-input-section-header Container@ROW: Width: PARENT_RIGHT Height: 50 @@ -113,7 +113,7 @@ Background@MAINMENU_INTRODUCTION_PROMPT: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Control Scheme: + Text: label-mouse-control-container DropDownButton@MOUSE_CONTROL_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -126,48 +126,48 @@ Background@MAINMENU_INTRODUCTION_PROMPT: LabelWithHighlight@DESC_SELECTION: Height: 16 Font: Small - Text: - Select units using the mouse button + Text: label-mouse-control-desc-classic-selection LabelWithHighlight@DESC_COMMANDS: Y: 17 Height: 16 Font: Small - Text: - Command units using the mouse button + Text: label-mouse-control-desc-classic-commands LabelWithHighlight@DESC_BUILDIGS: Y: 34 Height: 16 Font: Small - Text: - Place structures using the mouse button + Text: label-mouse-control-desc-classic-buildigs LabelWithHighlight@DESC_SUPPORT: Y: 51 Height: 16 Font: Small - Text: - Target support powers using the mouse button + Text: label-mouse-control-desc-classic-support LabelWithHighlight@DESC_ZOOM: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using the + Text: label-mouse-control-desc-classic-zoom LabelWithHighlight@DESC_ZOOM_MODIFIER: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using + Text: label-mouse-control-desc-classic-zoom-modifier LabelWithHighlight@DESC_SCROLL_RIGHT: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-classic-scroll-right LabelWithHighlight@DESC_SCROLL_MIDDLE: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-classic-scroll-middle Label@DESC_EDGESCROLL: X: 9 Y: 102 Height: 16 Font: Small - Text: or by moving the cursor to the edge of the screen + Text: label-mouse-control-desc-classic-edgescroll Container@MOUSE_CONTROL_DESC_MODERN: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -175,48 +175,48 @@ Background@MAINMENU_INTRODUCTION_PROMPT: LabelWithHighlight@DESC_SELECTION: Height: 16 Font: Small - Text: - Select units using the mouse button + Text: label-mouse-control-desc-modern-selection LabelWithHighlight@DESC_COMMANDS: Y: 17 Height: 16 Font: Small - Text: - Command units using the mouse button + Text: label-mouse-control-desc-modern-commands LabelWithHighlight@DESC_BUILDIGS: Y: 34 Height: 16 Font: Small - Text: - Place structures using the mouse button + Text: label-mouse-control-desc-modern-buildigs LabelWithHighlight@DESC_SUPPORT: Y: 51 Height: 16 Font: Small - Text: - Target support powers using the mouse button + Text: label-mouse-control-desc-modern-support LabelWithHighlight@DESC_ZOOM: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using the + Text: label-mouse-control-desc-modern-zoom LabelWithHighlight@DESC_ZOOM_MODIFIER: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using + Text: label-mouse-control-desc-modern-zoom-modifier LabelWithHighlight@DESC_SCROLL_RIGHT: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-modern-scroll-right LabelWithHighlight@DESC_SCROLL_MIDDLE: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-modern-scroll-middle Label@DESC_EDGESCROLL: X: 9 Y: 102 Height: 16 Font: Small - Text: or by moving the cursor to the edge of the screen + Text: label-mouse-control-desc-modern-edgescroll Container@ROW: Width: PARENT_RIGHT Height: 20 @@ -229,7 +229,7 @@ Background@MAINMENU_INTRODUCTION_PROMPT: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Screen Edge Panning + Text: checkbox-edgescroll-container Container@SPACER: Height: 30 Background@DISPLAY_SECTION_HEADER: @@ -244,7 +244,7 @@ Background@MAINMENU_INTRODUCTION_PROMPT: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Display + Text: label-display-section-header Container@ROW: Width: PARENT_RIGHT Height: 50 @@ -256,7 +256,7 @@ Background@MAINMENU_INTRODUCTION_PROMPT: Label@BATTLEFIELD_CAMERA: Width: PARENT_RIGHT Height: 20 - Text: Battlefield Camera: + Text: label-battlefield-camera-dropdown-container DropDownButton@BATTLEFIELD_CAMERA_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -269,7 +269,7 @@ Background@MAINMENU_INTRODUCTION_PROMPT: Label@UI_SCALE: Width: PARENT_RIGHT Height: 20 - Text: UI Scale: + Text: label-ui-scale-dropdown-container DropDownButton@UI_SCALE_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -287,13 +287,13 @@ Background@MAINMENU_INTRODUCTION_PROMPT: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Increase Cursor Size + Text: checkbox-cursordouble-container Button@CONTINUE_BUTTON: X: PARENT_RIGHT - 180 Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Continue + Text: button-mainmenu-introduction-prompt-continue Font: Bold Key: return @@ -310,21 +310,21 @@ Background@MAINMENU_SYSTEM_INFO_PROMPT: Height: 25 Font: Bold Align: Center - Text: Establishing Battlefield Control + Text: label-mainmenu-system-info-prompt-title Label@PROMPT_TEXT_A: X: 15 Y: 50 Width: PARENT_RIGHT - 30 Height: 16 Align: Center - Text: We would like to collect some system details that will help us optimize OpenRA. + Text: label-mainmenu-system-info-prompt-text-a Label@PROMPT_TEXT_B: X: 15 Y: 68 Width: PARENT_RIGHT - 30 Height: 16 Align: Center - Text: With your permission, the following anonymous data will be sent each game launch: + Text: label-mainmenu-system-info-prompt-text-b ScrollPanel@SYSINFO_DATA: X: 20 Y: 98 @@ -344,12 +344,13 @@ Background@MAINMENU_SYSTEM_INFO_PROMPT: Width: 200 Height: 20 Font: Regular - Text: Send System Information + Text: checkbox-mainmenu-system-info-prompt-sysinfo Button@CONTINUE_BUTTON: X: PARENT_RIGHT - WIDTH - 20 Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Continue + Text: button-mainmenu-system-info-prompt-continue Font: Bold Key: return + diff --git a/mods/common/chrome/mainmenu.yaml b/mods/common/chrome/mainmenu.yaml index 5b5bee04fcfc..3160e5642bcd 100644 --- a/mods/common/chrome/mainmenu.yaml +++ b/mods/common/chrome/mainmenu.yaml @@ -43,7 +43,7 @@ Container@MAINMENU: Y: 22 Width: 200 Height: 30 - Text: OpenRA + Text: label-main-menu-mainmenu-title Align: Center Font: Bold Button@SINGLEPLAYER_BUTTON: @@ -51,42 +51,42 @@ Container@MAINMENU: Y: 60 Width: 140 Height: 30 - Text: Singleplayer + Text: button-main-menu-singleplayer Font: Bold Button@MULTIPLAYER_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 100 Width: 140 Height: 30 - Text: Multiplayer + Text: button-main-menu-multiplayer Font: Bold Button@SETTINGS_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 140 Width: 140 Height: 30 - Text: Settings + Text: button-main-menu-settings Font: Bold Button@EXTRAS_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 180 Width: 140 Height: 30 - Text: Extras + Text: button-main-menu-extras Font: Bold Button@CONTENT_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 220 Width: 140 Height: 30 - Text: Manage Content + Text: button-main-menu-content Font: Bold Button@QUIT_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 260 Width: 140 Height: 30 - Text: Quit + Text: button-main-menu-quit Font: Bold Background@SINGLEPLAYER_MENU: Width: PARENT_RIGHT @@ -97,7 +97,7 @@ Container@MAINMENU: Y: 20 Width: 200 Height: 30 - Text: Singleplayer + Text: label-singleplayer-menu-title Align: Center Font: Bold Button@SKIRMISH_BUTTON: @@ -105,21 +105,21 @@ Container@MAINMENU: Y: 60 Width: 140 Height: 30 - Text: Skirmish + Text: button-singleplayer-menu-skirmish Font: Bold Button@MISSIONS_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 100 Width: 140 Height: 30 - Text: Missions + Text: button-singleplayer-menu-missions Font: Bold Button@LOAD_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 140 Width: 140 Height: 30 - Text: Load + Text: button-singleplayer-menu-load Font: Bold Button@BACK_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 @@ -127,7 +127,7 @@ Container@MAINMENU: Y: 260 Width: 140 Height: 30 - Text: Back + Text: button-singleplayer-menu-back Font: Bold Background@EXTRAS_MENU: Width: PARENT_RIGHT @@ -138,7 +138,7 @@ Container@MAINMENU: Y: 20 Width: 200 Height: 30 - Text: Extras + Text: label-extras-menu-title Align: Center Font: Bold Button@REPLAYS_BUTTON: @@ -146,35 +146,35 @@ Container@MAINMENU: Y: 60 Width: 140 Height: 30 - Text: Replays + Text: button-extras-menu-replays Font: Bold Button@MUSIC_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 100 Width: 140 Height: 30 - Text: Music + Text: button-extras-menu-music Font: Bold Button@MAP_EDITOR_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 140 Width: 140 Height: 30 - Text: Map Editor + Text: button-extras-menu-map-editor Font: Bold Button@ASSETBROWSER_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 180 Width: 140 Height: 30 - Text: Asset Browser + Text: button-extras-menu-assetbrowser Font: Bold Button@CREDITS_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 220 Width: 140 Height: 30 - Text: Credits + Text: button-extras-menu-credits Font: Bold Button@BACK_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 @@ -182,7 +182,7 @@ Container@MAINMENU: Y: 260 Width: 140 Height: 30 - Text: Back + Text: button-extras-menu-back Font: Bold Background@MAP_EDITOR_MENU: Width: PARENT_RIGHT @@ -193,7 +193,7 @@ Container@MAINMENU: Y: 20 Width: 200 Height: 30 - Text: Map Editor + Text: label-map-editor-menu-title Align: Center Font: Bold Button@NEW_MAP_BUTTON: @@ -201,14 +201,14 @@ Container@MAINMENU: Y: 60 Width: 140 Height: 30 - Text: New Map + Text: button-map-editor-menu-new Font: Bold Button@LOAD_MAP_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 100 Width: 140 Height: 30 - Text: Load Map + Text: button-map-editor-menu-load Font: Bold Button@BACK_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 @@ -216,7 +216,7 @@ Container@MAINMENU: Y: 260 Width: 140 Height: 30 - Text: Back + Text: button-map-editor-menu-back Font: Bold Container@PERFORMANCE_INFO: Logic: PerfDebugLogic @@ -252,7 +252,7 @@ Container@MAINMENU: Y: 15 Width: 400 Height: 25 - Text: Battlefield News + Text: dropdownbutton-news-bg-button Font: Bold Container@UPDATE_NOTICE: X: (WINDOW_RIGHT - WIDTH) / 2 @@ -264,14 +264,15 @@ Container@MAINMENU: Height: 25 Align: Center Shadow: true - Text: You are running an outdated version of OpenRA. + Text: label-update-notice-a Label@B: Y: 20 Width: PARENT_RIGHT Height: 25 Align: Center Shadow: true - Text: Download the latest version from www.openra.net + Text: label-update-notice-b Container@PLAYER_PROFILE_CONTAINER: X: 25 Y: 25 + diff --git a/mods/common/chrome/map-chooser.yaml b/mods/common/chrome/map-chooser.yaml index 6b864e198b80..e7fdd6685486 100644 --- a/mods/common/chrome/map-chooser.yaml +++ b/mods/common/chrome/map-chooser.yaml @@ -10,21 +10,21 @@ Background@MAPCHOOSER_PANEL: Align: Center Width: PARENT_RIGHT Height: 20 - Text: Choose Map + Text: label-mapchooser-panel-title Font: Bold Button@SYSTEM_MAPS_TAB_BUTTON: X: 20 Y: 48 Height: 31 Width: 140 - Text: Official Maps + Text: button-mapchooser-panel-system-maps-tab Font: Bold Button@USER_MAPS_TAB_BUTTON: X: 160 Y: 48 Height: 31 Width: 140 - Text: Custom Maps + Text: button-mapchooser-panel-user-maps-tab Font: Bold Container@MAP_TAB_PANES: Width: PARENT_RIGHT - 40 @@ -102,7 +102,7 @@ Background@MAPCHOOSER_PANEL: Height: 24 Font: Bold Align: Right - Text: Filter: + Text: label-filter-order-controls-desc TextField@MAPFILTER_INPUT: X: 45 Width: 150 @@ -113,7 +113,7 @@ Background@MAPCHOOSER_PANEL: Height: 24 Font: Bold Align: Center - Text: in + Text: label-filter-order-controls-desc-joiner DropDownButton@GAMEMODE_FILTER: X: 225 Width: 200 @@ -124,7 +124,7 @@ Background@MAPCHOOSER_PANEL: Height: 24 Font: Bold Align: Right - Text: Order by: + Text: label-filter-order-controls-orderby DropDownButton@ORDERBY: X: PARENT_RIGHT - WIDTH Width: 200 @@ -134,28 +134,28 @@ Background@MAPCHOOSER_PANEL: Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Random Map + Text: button-mapchooser-panel-randommap Font: Bold Button@DELETE_MAP_BUTTON: X: 160 Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Delete Map + Text: button-mapchooser-panel-delete-map Font: Bold Button@DELETE_ALL_MAPS_BUTTON: X: 300 Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Delete All Maps + Text: button-mapchooser-panel-delete-all-maps Font: Bold Button@BUTTON_OK: X: PARENT_RIGHT - 270 Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Ok + Text: button-mapchooser-panel-ok Font: Bold Key: return Button@BUTTON_CANCEL: @@ -163,7 +163,8 @@ Background@MAPCHOOSER_PANEL: Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Back + Text: button-mapchooser-panel-cancel Font: Bold Key: escape TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/common/chrome/missionbrowser.yaml b/mods/common/chrome/missionbrowser.yaml index f2d7949c9912..32cced93b64f 100644 --- a/mods/common/chrome/missionbrowser.yaml +++ b/mods/common/chrome/missionbrowser.yaml @@ -9,7 +9,7 @@ Background@MISSIONBROWSER_PANEL: Y: 21 Width: PARENT_RIGHT Height: 25 - Text: Missions + Text: label-missionbrowser-panel-title Align: Center Font: Bold ScrollPanel@MISSION_LIST: @@ -61,83 +61,130 @@ Background@MISSIONBROWSER_PANEL: IgnoreMouseOver: True IgnoreMouseInput: True ShowSpawnPoints: False - ScrollPanel@MISSION_DESCRIPTION_PANEL: + Container@MISSION_TABS: + Width: PARENT_RIGHT + Y: PARENT_BOTTOM - 31 + Children: + Button@MISSIONINFO_TAB: + Width: PARENT_RIGHT / 2 + Height: 31 + Font: Bold + Text: button-missionbrowser-panel-mission-info + Button@OPTIONS_TAB: + X: PARENT_RIGHT / 2 + Width: PARENT_RIGHT / 2 + Height: 31 + Font: Bold + Text: button-missionbrowser-panel-mission-options + Container@MISSION_DETAIL: Y: 212 Width: PARENT_RIGHT - Height: 143 - TopBottomSpacing: 5 + Height: PARENT_BOTTOM - 212 Children: - Label@MISSION_DESCRIPTION: - X: 4 - Width: PARENT_RIGHT - 32 - VAlign: Top - Font: Small - Label@DIFFICULTY_DESC: - Y: PARENT_BOTTOM - HEIGHT - Width: 56 - Height: 25 - Text: Difficulty: - Align: Right - DropDownButton@DIFFICULTY_DROPDOWNBUTTON: - X: 61 - Y: PARENT_BOTTOM - HEIGHT - Width: 135 - Height: 25 - Font: Regular - Label@GAMESPEED_DESC: - X: PARENT_RIGHT - WIDTH - 115 - Y: PARENT_BOTTOM - HEIGHT - Width: 120 - Height: 25 - Text: Speed: - Align: Right - DropDownButton@GAMESPEED_DROPDOWNBUTTON: - X: PARENT_RIGHT - WIDTH - Y: PARENT_BOTTOM - HEIGHT - Width: 110 - Height: 25 - Font: Regular + ScrollPanel@MISSION_DESCRIPTION_PANEL: + Height: PARENT_BOTTOM - 30 + Width: PARENT_RIGHT + TopBottomSpacing: 5 + Children: + Label@MISSION_DESCRIPTION: + X: 4 + Width: PARENT_RIGHT - 32 + VAlign: Top + Font: Small + ScrollPanel@MISSION_OPTIONS: + Height: PARENT_BOTTOM - 30 + Width: PARENT_RIGHT + TopBottomSpacing: 5 + Children: + Container@CHECKBOX_ROW_TEMPLATE: + Width: PARENT_RIGHT + Height: 30 + Children: + Checkbox@A: + X: 10 + Width: PARENT_RIGHT / 2 - 25 + Height: 20 + Visible: False + TooltipContainer: TOOLTIP_CONTAINER + Checkbox@B: + X: PARENT_RIGHT / 2 + 5 + Width: PARENT_RIGHT / 2 - 25 + Height: 20 + Visible: False + TooltipContainer: TOOLTIP_CONTAINER + Container@DROPDOWN_ROW_TEMPLATE: + Height: 60 + Width: PARENT_RIGHT + Children: + LabelForInput@A_DESC: + X: 10 + Width: PARENT_RIGHT / 2 - 35 + Height: 20 + Visible: False + For: A + DropDownButton@A: + X: 10 + Width: PARENT_RIGHT / 2 - 35 + Y: 25 + Height: 25 + Visible: False + PanelRoot: MISSION_DROPDOWN_PANEL_ROOT + TooltipContainer: TOOLTIP_CONTAINER + LabelForInput@B_DESC: + X: PARENT_RIGHT / 2 + 5 + Width: PARENT_RIGHT / 2 - 35 + Height: 20 + Visible: False + For: B + DropDownButton@B: + X: PARENT_RIGHT / 2 + 5 + Width: PARENT_RIGHT / 2 - 35 + Y: 25 + Height: 25 + Visible: False + PanelRoot: MISSION_DROPDOWN_PANEL_ROOT + TooltipContainer: TOOLTIP_CONTAINER Button@START_BRIEFING_VIDEO_BUTTON: X: 20 Y: PARENT_BOTTOM - 45 Width: 130 Height: 25 - Text: Watch Briefing + Text: button-missionbrowser-panel-start-briefing-video Font: Bold Button@STOP_BRIEFING_VIDEO_BUTTON: X: 20 Y: PARENT_BOTTOM - 45 Width: 130 Height: 25 - Text: Stop Briefing + Text: button-missionbrowser-panel-stop-briefing-video Font: Bold Button@START_INFO_VIDEO_BUTTON: X: 160 Y: PARENT_BOTTOM - 45 Width: 130 Height: 25 - Text: Watch Info Video + Text: button-missionbrowser-panel-start-info-video Font: Bold Button@STOP_INFO_VIDEO_BUTTON: X: 160 Y: PARENT_BOTTOM - 45 Width: 130 Height: 25 - Text: Stop Info Video + Text: button-missionbrowser-panel-stop-info-video Font: Bold Button@STARTGAME_BUTTON: X: PARENT_RIGHT - 140 - 130 Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Play + Text: button-missionbrowser-panel-startgame Font: Bold Button@BACK_BUTTON: X: PARENT_RIGHT - 140 Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Back + Text: button-missionbrowser-panel-back Font: Bold Key: escape Background@MISSION_BIN: @@ -152,6 +199,7 @@ Background@MISSIONBROWSER_PANEL: Y: 1 Width: PARENT_RIGHT - 2 Height: PARENT_BOTTOM - 2 + Container@MISSION_DROPDOWN_PANEL_ROOT: TooltipContainer@TOOLTIP_CONTAINER: Background@FULLSCREEN_PLAYER: @@ -165,3 +213,4 @@ Background@FULLSCREEN_PLAYER: Y: 0 Width: WINDOW_RIGHT Height: WINDOW_BOTTOM + diff --git a/mods/common/chrome/multiplayer-browser.yaml b/mods/common/chrome/multiplayer-browser.yaml index 633cba63f0e4..208864523f35 100644 --- a/mods/common/chrome/multiplayer-browser.yaml +++ b/mods/common/chrome/multiplayer-browser.yaml @@ -9,7 +9,7 @@ Background@MULTIPLAYER_PANEL: Y: 16 Width: PARENT_RIGHT Height: 25 - Text: Multiplayer + Text: label-multiplayer-panel-title Align: Center Font: Bold Container@LABEL_CONTAINER: @@ -22,26 +22,26 @@ Background@MULTIPLAYER_PANEL: X: 5 Width: 347 Height: 25 - Text: Server + Text: label-container-name Align: Center Font: Bold Label@PLAYERS: X: 382 Width: 85 Height: 25 - Text: Players + Text: label-container-players Font: Bold Label@LOCATION: X: 472 Width: 110 Height: 25 - Text: Location + Text: label-container-location Font: Bold Label@STATUS: X: 587 Width: 50 Height: 25 - Text: Status + Text: label-container-status Font: Bold LogicTicker@NOTICE_WATCHER: Background@NOTICE_CONTAINER: @@ -56,21 +56,21 @@ Background@MULTIPLAYER_PANEL: Width: PARENT_RIGHT - 10 Height: 20 Align: Center - Text: You are running an outdated version of OpenRA. Download the latest version from www.openra.net + Text: label-notice-container-outdated-version Font: TinyBold Label@UNKNOWN_VERSION_LABEL: X: 5 Width: PARENT_RIGHT - 10 Height: 20 Align: Center - Text: You are running an unrecognized version of OpenRA. Download the latest version from www.openra.net + Text: label-notice-container-unknown-version Font: TinyBold Label@PLAYTEST_AVAILABLE_LABEL: X: 5 Width: PARENT_RIGHT - 10 Height: 20 Align: Center - Text: A preview of the next OpenRA release is available for testing. Download the playtest from www.openra.net + Text: label-notice-container-playtest-available Font: TinyBold ScrollPanel@SERVER_LIST: X: 20 @@ -111,7 +111,7 @@ Background@MULTIPLAYER_PANEL: ImageCollection: lobby-bits TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP - TooltipText: Requires Password + TooltipText: image-multiplayer-panel-password-protected-tooltip Image@REQUIRES_AUTHENTICATION: X: 364 Y: 6 @@ -120,7 +120,7 @@ Background@MULTIPLAYER_PANEL: ImageCollection: lobby-bits TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP - TooltipText: Requires OpenRA forum account + TooltipText: image-multiplayer-panel-requires-authentication-tooltip LabelWithTooltip@PLAYERS: X: 382 Width: 85 @@ -201,14 +201,14 @@ Background@MULTIPLAYER_PANEL: Y: 255 Width: PARENT_RIGHT Height: 25 - Text: Join + Text: button-selected-server-join Font: Bold DropDownButton@FILTERS_DROPDOWNBUTTON: X: 20 Y: PARENT_BOTTOM - HEIGHT - 20 Width: 158 Height: 25 - Text: Filter Games + Text: dropdownbutton-multiplayer-panel-filters Font: Bold Button@RELOAD_BUTTON: X: 182 @@ -238,14 +238,14 @@ Background@MULTIPLAYER_PANEL: Y: PARENT_BOTTOM - HEIGHT - 20 Width: 100 Height: 25 - Text: Direct IP + Text: button-multiplayer-panel-directconnect Font: Bold Button@CREATE_BUTTON: X: 595 Y: PARENT_BOTTOM - HEIGHT - 20 Width: 100 Height: 25 - Text: Create + Text: button-multiplayer-panel-create Font: Bold Button@BACK_BUTTON: Key: escape @@ -253,6 +253,7 @@ Background@MULTIPLAYER_PANEL: Y: PARENT_BOTTOM - HEIGHT - 20 Width: 174 Height: 25 - Text: Back + Text: button-multiplayer-panel-back Font: Bold TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/common/chrome/multiplayer-browserpanels.yaml b/mods/common/chrome/multiplayer-browserpanels.yaml index 9e4edcaa37d4..b1125a0da62a 100644 --- a/mods/common/chrome/multiplayer-browserpanels.yaml +++ b/mods/common/chrome/multiplayer-browserpanels.yaml @@ -49,7 +49,7 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 5 Width: PARENT_RIGHT - 29 Height: 20 - Text: Waiting + Text: checkbox-multiplayer-filter-panel-waiting-for-players TextColor: 32CD32 Font: Regular Checkbox@EMPTY: @@ -57,14 +57,14 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 30 Width: PARENT_RIGHT - 29 Height: 20 - Text: Empty + Text: checkbox-multiplayer-filter-panel-empty Font: Regular Checkbox@PASSWORD_PROTECTED: X: 5 Y: 55 Width: PARENT_RIGHT - 29 Height: 20 - Text: Protected + Text: checkbox-multiplayer-filter-panel-password-protected TextColor: FF0000 Font: Regular Checkbox@ALREADY_STARTED: @@ -72,7 +72,7 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 80 Width: PARENT_RIGHT - 29 Height: 20 - Text: Started + Text: checkbox-multiplayer-filter-panel-already-started TextColor: FFA500 Font: Regular Checkbox@INCOMPATIBLE_VERSION: @@ -80,6 +80,7 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 105 Width: PARENT_RIGHT - 29 Height: 20 - Text: Incompatible + Text: checkbox-multiplayer-filter-panel-incompatible-version TextColor: BEBEBE Font: Regular + diff --git a/mods/common/chrome/multiplayer-createserver.yaml b/mods/common/chrome/multiplayer-createserver.yaml index 8fca382c1857..f4f70efa0335 100644 --- a/mods/common/chrome/multiplayer-createserver.yaml +++ b/mods/common/chrome/multiplayer-createserver.yaml @@ -9,7 +9,7 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Y: 16 Width: PARENT_RIGHT Height: 25 - Text: Create Server + Text: label-multiplayer-createserver-panel-title Align: Center Font: Bold Label@SERVER_NAME_LABEL: @@ -17,7 +17,7 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Width: 105 Height: 25 Align: Right - Text: Server Name: + Text: label-multiplayer-createserver-panel-server-name TextField@SERVER_NAME: X: 110 Y: 45 @@ -30,12 +30,11 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Width: 105 Height: 25 Align: Right - Text: Password: + Text: label-multiplayer-createserver-panel-password PasswordField@PASSWORD: X: 110 Y: 80 Width: 145 - MaxLength: 20 Height: 25 Label@AFTER_PASSWORD_LABEL: X: 265 @@ -43,13 +42,13 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Width: 95 Height: 25 Align: Left - Text: (optional) + Text: label-multiplayer-createserver-panel-after-password Label@LISTEN_PORT_LABEL: Y: 115 Width: 105 Height: 25 Align: Right - Text: Port: + Text: label-multiplayer-createserver-panel-listen-port TextField@LISTEN_PORT: X: 110 Y: 115 @@ -64,7 +63,7 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Width: 150 Height: 20 Font: Regular - Text: Advertise Online + Text: checkbox-multiplayer-createserver-panel-advertise Label@NOTICES_HEADER_A: X: 20 Y: 156 @@ -94,21 +93,21 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: - Game will be advertised to the Local Area Network only. + Text: label-notices-lan-advertising Label@FIREWALL: Y: 12 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You must manually configure your firewall to allow connections. + Text: label-notices-lan-firewall Label@PORTFORWARD_A: Y: 24 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - Players can connect using Direct IP from the Internet if you + Text: label-notices-lan-portforward-a Label@PORTFORWARD_B: X: 7 Y: 36 @@ -116,7 +115,7 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: manually configure port forwarding on your router. + Text: label-notices-lan-portforward-b Container@NOTICES_NO_UPNP: X: 25 Y: 176 @@ -128,21 +127,21 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: - Game will be advertised to the Local Area Network and Internet. + Text: label-notices-no-upnp-advertising Label@FIREWALL: Y: 12 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You must manually configure your firewall to allow connections. + Text: label-notices-no-upnp-firewall Label@PORTFORWARD_A: Y: 24 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You must manually configure your router to allow and forward + Text: label-notices-no-upnp-portforward-a Label@PORTFORWARD_B: X: 7 Y: 36 @@ -150,14 +149,14 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: connections to your local IP and Port. + Text: label-notices-no-upnp-portforward-b Label@SETTINGS_A: Y: 48 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You can enable UPnP/NAT-PMP (if supported by your router) + Text: label-notices-no-upnp-settings-a Label@SETTINGS_B: X: 7 Y: 60 @@ -165,7 +164,7 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: in the Advanced tab of the settings menu. + Text: label-notices-no-upnp-settings-b Container@NOTICES_UPNP: X: 25 Y: 176 @@ -177,28 +176,28 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Height: 25 Font: Tiny Align: Left - Text: - Game will be advertised to the Local Area Network and Internet. + Text: label-notices-upnp-advertising Label@FIREWALL: Y: 12 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You must manually configure your firewall to allow connections. + Text: label-notices-upnp-firewall Label@PORTFORWARD_A: Y: 24 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - Game will automatically configure port forwarding. + Text: label-notices-upnp-portforward-a Label@SETTINGS_A: Y: 36 Width: 305 Height: 25 Font: Tiny Align: Left - Text: - You can disable UPnP/NAT-PMP in the settings menu. + Text: label-notices-upnp-settings-a Container@MAP_PREVIEW_ROOT: X: PARENT_RIGHT - 194 Y: 45 @@ -209,14 +208,14 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Change Map + Text: button-multiplayer-createserver-panel-map Font: Bold Button@BACK_BUTTON: X: PARENT_RIGHT - WIDTH - 20 Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Back + Text: button-multiplayer-createserver-panel-back Font: Bold Key: escape Button@CREATE_BUTTON: @@ -225,6 +224,7 @@ Background@MULTIPLAYER_CREATESERVER_PANEL: Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Create + Text: button-multiplayer-createserver-panel-create Font: Bold TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/common/chrome/multiplayer-directconnect.yaml b/mods/common/chrome/multiplayer-directconnect.yaml index e5a3568bfe25..ef8884394c93 100644 --- a/mods/common/chrome/multiplayer-directconnect.yaml +++ b/mods/common/chrome/multiplayer-directconnect.yaml @@ -10,7 +10,7 @@ Background@DIRECTCONNECT_PANEL: Y: 21 Width: 450 Height: 25 - Text: Connect to Server + Text: label-directconnect-panel-title Align: Center Font: Bold Label@ADDRESS_LABEL: @@ -19,12 +19,11 @@ Background@DIRECTCONNECT_PANEL: Width: 95 Height: 25 Align: Right - Text: Server Address: + Text: label-directconnect-panel-address TextField@IP: X: 150 Y: 60 Width: 210 - MaxLength: 50 Height: 25 Label@PORT_LABEL: X: 360 @@ -45,7 +44,7 @@ Background@DIRECTCONNECT_PANEL: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Join + Text: button-directconnect-panel-join Font: Bold Key: return Button@BACK_BUTTON: @@ -53,6 +52,7 @@ Background@DIRECTCONNECT_PANEL: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Cancel + Text: button-directconnect-panel-back Font: Bold Key: escape + diff --git a/mods/common/chrome/musicplayer.yaml b/mods/common/chrome/musicplayer.yaml index ca79dbf9f516..ee1a8693934d 100644 --- a/mods/common/chrome/musicplayer.yaml +++ b/mods/common/chrome/musicplayer.yaml @@ -39,14 +39,14 @@ Background@MUSIC_PANEL: Label@TITLE: Width: 100 Height: 25 - Text: Track + Text: label-container-title Align: Center Font: Bold Label@TYPE: X: PARENT_RIGHT - WIDTH Height: 25 Width: 95 - Text: Length + Text: label-container-type Align: Center Font: Bold Container@BUTTONS: @@ -127,13 +127,13 @@ Background@MUSIC_PANEL: Y: PARENT_BOTTOM - HEIGHT - 95 Width: 85 Height: 20 - Text: Shuffle + Text: checkbox-music-panel-shuffle Checkbox@REPEAT: X: PARENT_RIGHT - 15 - WIDTH Y: PARENT_BOTTOM - HEIGHT - 95 Width: 70 Height: 20 - Text: Loop + Text: checkbox-music-panel-repeat Container@NO_MUSIC_LABEL: X: 20 Y: (PARENT_BOTTOM - HEIGHT - 95) / 2 @@ -146,25 +146,25 @@ Background@MUSIC_PANEL: Height: 25 Font: Bold Align: Center - Text: Music Not Installed + Text: label-no-music-title Label@DESCA: Y: 20 Width: PARENT_RIGHT - 24 Height: 25 Align: Center - Text: The game music can be installed + Text: label-no-music-desca Label@DESCB: Y: 40 Width: PARENT_RIGHT - 24 Height: 25 Align: Center - Text: from the "Manage Content" menu. + Text: label-no-music-descb Button@BACK_BUTTON: X: PARENT_RIGHT - WIDTH - 20 Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Close + Text: button-music-panel-back Font: Bold Key: escape Label@MUTE_LABEL: @@ -174,3 +174,4 @@ Background@MUSIC_PANEL: Height: 20 Font: Small TooltipContainer@MUSIC_TOOLTIP_CONTAINER: + diff --git a/mods/common/chrome/playerprofile.yaml b/mods/common/chrome/playerprofile.yaml index 8efdd698251e..38d0f61b17be 100644 --- a/mods/common/chrome/playerprofile.yaml +++ b/mods/common/chrome/playerprofile.yaml @@ -26,7 +26,7 @@ Container@LOCAL_PROFILE_PANEL: Width: 60 Height: 20 Font: TinyBold - Text: Logout + Text: button-profile-header-destroy-key Background@BADGES_CONTAINER: Width: PARENT_RIGHT Y: 48 @@ -43,28 +43,28 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: Connect to a forum account to identify + Text: label-generate-keys-desc-a Label@DESC_B: Y: 21 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: yourself to other players, join private + Text: label-generate-keys-desc-b Label@DESC_C: Y: 37 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: servers, and display badges. + Text: label-generate-keys-desc-c Button@GENERATE_KEY: X: (PARENT_RIGHT - WIDTH) / 2 Y: 70 Width: 240 Height: 20 Font: TinyBold - Text: Connect to an OpenRA forum account + Text: button-generate-keys-key Background@GENERATING_KEYS: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -76,14 +76,14 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: Generating authentication key pair. + Text: label-generating-keys-desc-a Label@DESC_B: Y: 29 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: This will take several seconds... + Text: label-generating-keys-desc-b ProgressBar: X: (PARENT_RIGHT - WIDTH) / 2 Y: 70 @@ -101,35 +101,35 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: An authentication key has been copied to your + Text: label-register-fingerprint-desc-a Label@DESC_B: Y: 18 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: clipboard. Add this to your User Control Panel + Text: label-register-fingerprint-desc-b Label@DESC_C: Y: 34 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: on the OpenRA forum then press Continue. + Text: label-register-fingerprint-desc-c Button@DELETE_KEY: X: 15 Y: 70 Width: 70 Height: 20 Font: TinyBold - Text: Cancel + Text: button-register-fingerprint-delete-key Button@CHECK_KEY: X: 185 Y: 70 Width: 70 Height: 20 Font: TinyBold - Text: Continue + Text: button-register-fingerprint-check-key Background@CHECKING_FINGERPRINT: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -141,14 +141,14 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: Querying account details from + Text: label-checking-fingerprint-desc-a Label@DESC_B: Y: 29 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: the OpenRA forum... + Text: label-checking-fingerprint-desc-b ProgressBar: X: (PARENT_RIGHT - WIDTH) / 2 Y: 70 @@ -166,21 +166,21 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: Your authentication key is not connected + Text: label-fingerprint-not-found-desc-a Label@DESC_B: Y: 29 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: to an OpenRA forum account. + Text: label-fingerprint-not-found-desc-b Button@FINGERPRINT_NOT_FOUND_CONTINUE: X: 185 Y: 70 Width: 70 Height: 20 Font: TinyBold - Text: Back + Text: button-fingerprint-not-found-continue Background@CONNECTION_ERROR: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -192,21 +192,21 @@ Container@LOCAL_PROFILE_PANEL: Height: 25 Font: Small Align: Center - Text: Failed to connect to the OpenRA forum. + Text: label-connection-error-desc-a Label@DESC_B: Y: 29 Width: PARENT_RIGHT Height: 25 Font: Small Align: Center - Text: Please check your internet connection. + Text: label-connection-error-desc-b Button@CONNECTION_ERROR_RETRY: X: 185 Y: 70 Width: 70 Height: 20 Font: TinyBold - Text: Retry + Text: button-connection-error-retry Container@PLAYER_PROFILE_BADGES_INSERT: Logic: PlayerProfileBadgesLogic @@ -228,3 +228,4 @@ Container@PLAYER_PROFILE_BADGES_INSERT: Width: PARENT_RIGHT - 60 Height: 24 Font: Bold + diff --git a/mods/common/chrome/replaybrowser.yaml b/mods/common/chrome/replaybrowser.yaml index 61c83c8ad632..8f1d169e24e1 100644 --- a/mods/common/chrome/replaybrowser.yaml +++ b/mods/common/chrome/replaybrowser.yaml @@ -9,7 +9,7 @@ Background@REPLAYBROWSER_PANEL: Y: 16 Width: PARENT_RIGHT Height: 25 - Text: Replay Viewer + Text: label-replaybrowser-panel-title Align: Center Font: Bold Container@FILTER_AND_MANAGE_CONTAINER: @@ -29,104 +29,104 @@ Background@REPLAYBROWSER_PANEL: Height: 25 Font: Bold Align: Center - Text: Filter + Text: label-filters-title Label@FLT_GAMETYPE_DESC: X: 0 Y: 30 Width: 80 Height: 25 - Text: Type: + Text: label-filters-flt-gametype-desc Align: Right DropDownButton@FLT_GAMETYPE_DROPDOWNBUTTON: X: 85 Y: 30 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-gametype Label@FLT_DATE_DESC: X: 0 Y: 60 Width: 80 Height: 25 - Text: Date: + Text: label-filters-flt-date-desc Align: Right DropDownButton@FLT_DATE_DROPDOWNBUTTON: X: 85 Y: 60 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-date Label@FLT_DURATION_DESC: X: 0 Y: 90 Width: 80 Height: 25 - Text: Duration: + Text: label-filters-flt-duration-desc Align: Right DropDownButton@FLT_DURATION_DROPDOWNBUTTON: X: 85 Y: 90 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-duration Label@FLT_MAPNAME_DESC: X: 0 Y: 120 Width: 80 Height: 25 - Text: Map: + Text: label-filters-flt-mapname-desc Align: Right DropDownButton@FLT_MAPNAME_DROPDOWNBUTTON: X: 85 Y: 120 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-mapname Label@FLT_PLAYER_DESC: X: 0 Y: 150 Width: 80 Height: 25 - Text: Player: + Text: label-filters-flt-player-desc Align: Right DropDownButton@FLT_PLAYER_DROPDOWNBUTTON: X: 85 Y: 150 Width: PARENT_RIGHT - 85 Height: 25 - Text: Anyone + Text: dropdownbutton-filters-flt-player Label@FLT_OUTCOME_DESC: X: 0 Y: 180 Width: 80 Height: 25 - Text: Outcome: + Text: label-filters-flt-outcome-desc Align: Right DropDownButton@FLT_OUTCOME_DROPDOWNBUTTON: X: 85 Y: 180 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-outcome Label@FLT_FACTION_DESC: X: 0 Y: 210 Width: 80 Height: 25 - Text: Faction: + Text: label-filters-flt-faction-desc Align: Right DropDownButton@FLT_FACTION_DROPDOWNBUTTON: X: 85 Y: 210 Width: PARENT_RIGHT - 85 Height: 25 - Text: Any + Text: dropdownbutton-filters-flt-faction Button@FLT_RESET_BUTTON: X: 85 Y: 250 Width: PARENT_RIGHT - 85 Height: 25 - Text: Reset Filters + Text: button-filters-flt-reset Font: Bold Container@MANAGEMENT: X: 85 @@ -139,26 +139,26 @@ Background@REPLAYBROWSER_PANEL: Height: 25 Font: Bold Align: Center - Text: Manage + Text: label-management-manage-title Button@MNG_RENSEL_BUTTON: Y: 30 Width: PARENT_RIGHT Height: 25 - Text: Rename + Text: button-management-mng-rensel Font: Bold Key: F2 Button@MNG_DELSEL_BUTTON: Y: 60 Width: PARENT_RIGHT Height: 25 - Text: Delete + Text: button-management-mng-delsel Font: Bold Key: Delete Button@MNG_DELALL_BUTTON: Y: 90 Width: PARENT_RIGHT Height: 25 - Text: Delete All + Text: button-management-mng-delall Font: Bold Container@REPLAY_LIST_CONTAINER: X: 311 @@ -170,7 +170,7 @@ Background@REPLAYBROWSER_PANEL: Y: 6 Width: PARENT_RIGHT Height: 25 - Text: Choose Replay + Text: label-replay-list-container-replaybrowser-title Align: Center Font: Bold ScrollPanel@REPLAY_LIST: @@ -255,7 +255,7 @@ Background@REPLAYBROWSER_PANEL: Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Watch + Text: button-replaybrowser-panel-watch Font: Bold Key: return Button@CANCEL_BUTTON: @@ -263,7 +263,8 @@ Background@REPLAYBROWSER_PANEL: Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Back + Text: button-replaybrowser-panel-cancel Font: Bold Key: escape TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/common/chrome/settings-advanced.yaml b/mods/common/chrome/settings-advanced.yaml index 808bd37d9742..07e570cc8990 100644 --- a/mods/common/chrome/settings-advanced.yaml +++ b/mods/common/chrome/settings-advanced.yaml @@ -22,7 +22,7 @@ Container@ADVANCED_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Advanced + Text: label-network-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -35,7 +35,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Enable UPnP/NAT-PMP Discovery + Text: checkbox-nat-discovery-container Container@FETCH_NEWS_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -44,7 +44,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Fetch Community News + Text: checkbox-fetch-news-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -57,7 +57,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show Performance Graph + Text: checkbox-perfgraph-container Container@CHECK_VERSION_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -66,7 +66,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Check for Updates + Text: checkbox-check-version-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -79,7 +79,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show Performance Text + Text: checkbox-perftext-container Container@SENDSYSINFO_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -88,14 +88,14 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Send System Information + Text: checkbox-sendsysinfo-container Label@SENDSYSINFO_DESC: Y: 15 Width: PARENT_RIGHT Height: 30 Font: Tiny WordWrap: True - Text: Your Operating System, OpenGL and .NET runtime versions, and language settings will be sent along with an anonymous ID to help prioritize future development. + Text: label-sendsysinfo-checkbox-container-desc Container@SPACER: Background@DEBUG_SECTION_HEADER: X: 5 @@ -109,7 +109,7 @@ Container@ADVANCED_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Developer + Text: label-debug-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 40 @@ -121,13 +121,13 @@ Container@ADVANCED_PANEL: Label@A: Width: PARENT_RIGHT Height: 20 - Text: Additional developer-specific options can be enabled via the + Text: label-debug-hidden-container-a Align: Center Label@B: Y: 20 Width: PARENT_RIGHT Height: 20 - Text: Debug.DisplayDeveloperSettings setting or launch flag + Text: label-debug-hidden-container-b Align: Center Container@ROW: Width: PARENT_RIGHT - 24 @@ -141,7 +141,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show Bot Debug Messages + Text: checkbox-botdebug-container Container@CHECKBOTSYNC_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -150,7 +150,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Check Sync around BotModule Code + Text: checkbox-checkbotsync-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -163,7 +163,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show Map Debug Messages + Text: checkbox-luadebug-container Container@CHECKUNSYNCED_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -172,7 +172,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Check Sync around Unsynced Code + Text: checkbox-checkunsynced-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -185,7 +185,7 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Enable Debug Commands in Replays + Text: checkbox-replay-commands-container Container@PERFLOGGING_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -194,4 +194,5 @@ Container@ADVANCED_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Enable Tick Performance Logging + Text: checkbox-perflogging-container + diff --git a/mods/common/chrome/settings-audio.yaml b/mods/common/chrome/settings-audio.yaml index ae12d9508d07..702066ce7073 100644 --- a/mods/common/chrome/settings-audio.yaml +++ b/mods/common/chrome/settings-audio.yaml @@ -22,7 +22,7 @@ Container@AUDIO_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Audio + Text: label-audio-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -35,7 +35,7 @@ Container@AUDIO_PANEL: Width: PARENT_RIGHT Height: 20 Align: Center - Text: Audio controls require an active sound device + Text: label-no-audio-device-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -48,7 +48,7 @@ Container@AUDIO_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Cash Ticks + Text: checkbox-cash-ticks-container Container@MUTE_SOUND_CONTAINER: X: 10 Y: 30 @@ -58,7 +58,7 @@ Container@AUDIO_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Mute Sound + Text: checkbox-mute-sound-container Container@SOUND_VOLUME_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -66,7 +66,7 @@ Container@AUDIO_PANEL: Label@SOUND_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Sound Volume: + Text: label-sound-volume-container ExponentialSlider@SOUND_VOLUME: Y: 30 Width: PARENT_RIGHT @@ -84,8 +84,8 @@ Container@AUDIO_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Mute Menu Music - TooltipText: Mute background music when no specific track is playing + Text: checkbox-mute-background-music-container.label + TooltipText: checkbox-mute-background-music-container.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER Container@MUSIC_VOLUME_CONTAINER: X: PARENT_RIGHT / 2 + 10 @@ -94,7 +94,7 @@ Container@AUDIO_PANEL: Label@MUSIC_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Music Volume: + Text: label-music-volume-container ExponentialSlider@MUSIC_VOLUME: Y: 25 Width: PARENT_RIGHT @@ -111,7 +111,7 @@ Container@AUDIO_PANEL: Label@AUDIO_DEVICE_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Audio Device: + Text: label-audio-device-container DropDownButton@AUDIO_DEVICE: Y: 25 Width: PARENT_RIGHT @@ -123,7 +123,7 @@ Container@AUDIO_PANEL: Label@VIDEO_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Video Volume: + Text: label-video-volume-container ExponentialSlider@VIDEO_VOLUME: Y: 25 Width: PARENT_RIGHT @@ -142,4 +142,5 @@ Container@AUDIO_PANEL: Height: 20 Font: Tiny Align: Center - Text: Device changes will be applied after the game is restarted + Text: label-restart-required-container-audio-desc + diff --git a/mods/common/chrome/settings-display.yaml b/mods/common/chrome/settings-display.yaml index 18232352537e..b1beb18b5cef 100644 --- a/mods/common/chrome/settings-display.yaml +++ b/mods/common/chrome/settings-display.yaml @@ -22,7 +22,7 @@ Container@DISPLAY_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Profile + Text: label-profile-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -34,7 +34,7 @@ Container@DISPLAY_PANEL: LabelForInput@PLAYER: Width: PARENT_RIGHT Height: 20 - Text: Player Name: + Text: label-player-container For: PLAYERNAME TextField@PLAYERNAME: Y: 25 @@ -49,7 +49,7 @@ Container@DISPLAY_PANEL: LabelForInput@COLOR: Width: PARENT_RIGHT Height: 20 - Text: Preferred Color: + Text: label-playercolor-container-color For: PLAYERCOLOR DropDownButton@PLAYERCOLOR: Y: 25 @@ -76,7 +76,7 @@ Container@DISPLAY_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Display + Text: label-display-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -88,7 +88,7 @@ Container@DISPLAY_PANEL: Label@BATTLEFIELD_CAMERA: Width: PARENT_RIGHT Height: 20 - Text: Battlefield Camera: + Text: label-battlefield-camera-dropdown-container DropDownButton@BATTLEFIELD_CAMERA_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -101,7 +101,7 @@ Container@DISPLAY_PANEL: Label@TARGET_LINES: Width: PARENT_RIGHT Height: 20 - Text: Target Lines: + Text: label-target-lines-dropdown-container DropDownButton@TARGET_LINES_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -118,7 +118,7 @@ Container@DISPLAY_PANEL: LabelForInput@UI_SCALE: Width: PARENT_RIGHT Height: 20 - Text: UI Scale: + Text: label-ui-scale-dropdown-container For: UI_SCALE_DROPDOWN DropDownButton@UI_SCALE_DROPDOWN: Y: 25 @@ -132,7 +132,7 @@ Container@DISPLAY_PANEL: Label@STATUS_BARS: Width: PARENT_RIGHT Height: 20 - Text: Status Bars: + Text: label-status-bar-dropdown-container-bars DropDownButton@STATUS_BAR_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -150,7 +150,7 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Increase Cursor Size + Text: checkbox-cursordouble-container Container@PLAYER_STANCE_COLORS_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -159,8 +159,8 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Player Relationship Colors - TooltipText: Change minimap and health bar colors based on relationship (own, enemy, ally, neutral) + Text: checkbox-player-stance-colors-container.label + TooltipText: checkbox-player-stance-colors-container.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER Container@ROW: Width: PARENT_RIGHT - 24 @@ -174,8 +174,8 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show UI Feedback Notifications - TooltipText: Show transient text notifications for UI events + Text: checkbox-ui-feedback-container.label + TooltipText: checkbox-ui-feedback-container.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER Container@TRANSIENTS_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 @@ -185,8 +185,8 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Show Game Event Notifications - TooltipText: Show transient text notifications for game events + Text: checkbox-transients-container.label + TooltipText: checkbox-transients-container.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER Container@ROW: Width: PARENT_RIGHT - 24 @@ -200,7 +200,7 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Pause Menu Background + Text: checkbox-pause-shellmap-container Container@HIDE_REPLAY_CHAT_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 10 @@ -209,7 +209,7 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Hide Chat in Replays + Text: checkbox-hide-replay-chat-container Container@SPACER: Background@VIDEO_SECTION_HEADER: X: 5 @@ -223,7 +223,7 @@ Container@DISPLAY_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Video + Text: label-video-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -235,13 +235,13 @@ Container@DISPLAY_PANEL: Label@VIDEO_MODE: Width: PARENT_RIGHT Height: 20 - Text: Video Mode: + Text: label-video-mode-dropdown-container DropDownButton@MODE_DROPDOWN: Y: 25 Width: PARENT_RIGHT Height: 25 Font: Regular - Text: Windowed + Text: dropdownbutton-video-mode-dropdown-container Container@WINDOW_RESOLUTION_CONTAINER: X: PARENT_RIGHT / 2 + 10 Width: PARENT_RIGHT / 2 - 20 @@ -249,7 +249,7 @@ Container@DISPLAY_PANEL: Label@WINDOW_SIZE: Width: PARENT_RIGHT Height: 20 - Text: Window Size: + Text: label-window-resolution-container-size TextField@WINDOW_WIDTH: Y: 25 Width: 55 @@ -259,7 +259,7 @@ Container@DISPLAY_PANEL: Label@X: X: 55 Y: 25 - Text: x + Text: label-window-resolution-container-x Font: Bold Height: 25 Width: 15 @@ -278,13 +278,13 @@ Container@DISPLAY_PANEL: Label@DISPLAY_SELECTION_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Select Display: + Text: label-display-selection-container DropDownButton@DISPLAY_SELECTION_DROPDOWN: Y: 25 Width: PARENT_RIGHT Height: 25 Font: Regular - Text: Standard + Text: dropdownbutton-display-selection-container-dropdown Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -316,7 +316,7 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Enable VSync + Text: checkbox-vsync-container Container@FRAME_LIMIT_GAMESPEED_CHECKBOX_CONTAINER: X: PARENT_RIGHT / 2 + 10 Y: 25 @@ -326,7 +326,7 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Limit framerate to game tick rate + Text: checkbox-frame-limit-gamespeed-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -338,7 +338,7 @@ Container@DISPLAY_PANEL: Label@GL_PROFILE: Width: PARENT_RIGHT Height: 20 - Text: OpenGL Profile: + Text: label-gl-profile-dropdown-container DropDownButton@GL_PROFILE_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -356,5 +356,6 @@ Container@DISPLAY_PANEL: Width: PARENT_RIGHT Height: 20 Font: Tiny - Text: Display and OpenGL changes require restart + Text: label-restart-required-container-video-desc Align: Center + diff --git a/mods/common/chrome/settings-hotkeys.yaml b/mods/common/chrome/settings-hotkeys.yaml index f21223ab23c9..bb297deff0a3 100644 --- a/mods/common/chrome/settings-hotkeys.yaml +++ b/mods/common/chrome/settings-hotkeys.yaml @@ -30,7 +30,7 @@ Container@HOTKEYS_PANEL: Width: 100 Height: 25 Font: Bold - Text: Filter by name: + Text: label-hotkeys-panel-filter-input TextField@FILTER_INPUT: X: 108 Width: 180 @@ -40,7 +40,7 @@ Container@HOTKEYS_PANEL: Width: 100 Height: 25 Font: Bold - Text: Context: + Text: label-hotkeys-panel-context-dropdown Align: Right DropDownButton@CONTEXT_DROPDOWN: X: PARENT_RIGHT - WIDTH @@ -92,7 +92,7 @@ Container@HOTKEYS_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Align: Center - Text: No hotkeys match the filter criteria. + Text: label-hotkey-empty-list-message Background@HOTKEY_REMAP_BGND: Y: PARENT_BOTTOM - HEIGHT Width: PARENT_RIGHT @@ -133,22 +133,22 @@ Container@HOTKEYS_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Font: Tiny - Text: This hotkey cannot be modified + Text: label-notices-readonly-notice Button@OVERRIDE_HOTKEY_BUTTON: X: PARENT_RIGHT - 3 * WIDTH - 30 Y: 20 Width: 70 Height: 25 - Text: Override + Text: button-hotkey-remap-dialog-override Font: Bold Button@CLEAR_HOTKEY_BUTTON: X: PARENT_RIGHT - 2 * WIDTH - 30 Y: 20 Width: 65 Height: 25 - Text: Clear + Text: button-hotkey-remap-dialog-clear.label Font: Bold - TooltipText: Unbind the hotkey + TooltipText: button-hotkey-remap-dialog-clear.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP Button@RESET_HOTKEY_BUTTON: @@ -156,8 +156,9 @@ Container@HOTKEYS_PANEL: Y: 20 Width: 65 Height: 25 - Text: Reset + Text: button-hotkey-remap-dialog-reset.label Font: Bold - TooltipText: Reset to default + TooltipText: button-hotkey-remap-dialog-reset.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP + diff --git a/mods/common/chrome/settings-input.yaml b/mods/common/chrome/settings-input.yaml index f12788b28a8f..e92e2ed673f6 100644 --- a/mods/common/chrome/settings-input.yaml +++ b/mods/common/chrome/settings-input.yaml @@ -22,7 +22,7 @@ Container@INPUT_PANEL: Height: PARENT_BOTTOM Font: TinyBold Align: Center - Text: Input + Text: label-input-section-header Container@ROW: Width: PARENT_RIGHT - 24 Height: 50 @@ -35,7 +35,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Control Scheme: + Text: label-mouse-control-container DropDownButton@MOUSE_CONTROL_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -49,7 +49,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Zoom Modifier: + Text: label-zoom-modifier-container DropDownButton@ZOOM_MODIFIER: Y: 25 Width: PARENT_RIGHT @@ -63,48 +63,48 @@ Container@INPUT_PANEL: LabelWithHighlight@DESC_SELECTION: Height: 16 Font: Small - Text: - Select units using the mouse button + Text: label-mouse-control-desc-classic-selection LabelWithHighlight@DESC_COMMANDS: Y: 17 Height: 16 Font: Small - Text: - Command units using the mouse button + Text: label-mouse-control-desc-classic-commands LabelWithHighlight@DESC_BUILDIGS: Y: 34 Height: 16 Font: Small - Text: - Place structures using the mouse button + Text: label-mouse-control-desc-classic-buildigs LabelWithHighlight@DESC_SUPPORT: Y: 51 Height: 16 Font: Small - Text: - Target support powers using the mouse button + Text: label-mouse-control-desc-classic-support LabelWithHighlight@DESC_ZOOM: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using the + Text: label-mouse-control-desc-classic-zoom LabelWithHighlight@DESC_ZOOM_MODIFIER: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using + Text: label-mouse-control-desc-classic-zoom-modifier LabelWithHighlight@DESC_SCROLL_RIGHT: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-classic-scroll-right LabelWithHighlight@DESC_SCROLL_MIDDLE: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-classic-scroll-middle Label@DESC_EDGESCROLL: X: 9 Y: 102 Height: 16 Font: Small - Text: or by moving the cursor to the edge of the screen + Text: label-mouse-control-desc-classic-edgescroll Container@MOUSE_CONTROL_DESC_MODERN: X: 10 Y: 55 @@ -113,48 +113,48 @@ Container@INPUT_PANEL: LabelWithHighlight@DESC_SELECTION: Height: 16 Font: Small - Text: - Select units using the mouse button + Text: label-mouse-control-desc-modern-selection LabelWithHighlight@DESC_COMMANDS: Y: 17 Height: 16 Font: Small - Text: - Command units using the mouse button + Text: label-mouse-control-desc-modern-commands LabelWithHighlight@DESC_BUILDIGS: Y: 34 Height: 16 Font: Small - Text: - Place structures using the mouse button + Text: label-mouse-control-desc-modern-buildigs LabelWithHighlight@DESC_SUPPORT: Y: 51 Height: 16 Font: Small - Text: - Target support powers using the mouse button + Text: label-mouse-control-desc-modern-support LabelWithHighlight@DESC_ZOOM: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using the + Text: label-mouse-control-desc-modern-zoom LabelWithHighlight@DESC_ZOOM_MODIFIER: Y: 68 Height: 16 Font: Small - Text: - Zoom the battlefield using + Text: label-mouse-control-desc-modern-zoom-modifier LabelWithHighlight@DESC_SCROLL_RIGHT: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-modern-scroll-right LabelWithHighlight@DESC_SCROLL_MIDDLE: Y: 85 Height: 16 Font: Small - Text: - Pan the battlefield using the mouse button + Text: label-mouse-control-desc-modern-scroll-middle Label@DESC_EDGESCROLL: X: 9 Y: 102 Height: 16 Font: Small - Text: or by moving the cursor to the edge of the screen + Text: label-mouse-control-desc-modern-edgescroll Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -167,7 +167,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Screen Edge Panning + Text: checkbox-edgescroll-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -180,7 +180,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Alternate Mouse Panning + Text: checkbox-alternate-scroll-container Container@ROW: Width: PARENT_RIGHT - 24 Height: 20 @@ -193,7 +193,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Lock Mouse to Window + Text: checkbox-lockmouse-container Container@SPACER: Height: 30 Container@ROW: @@ -208,7 +208,7 @@ Container@INPUT_PANEL: Width: PARENT_RIGHT Height: 20 Font: Regular - Text: Pan Behaviour: + Text: label-mouse-scroll-type-container DropDownButton@MOUSE_SCROLL_TYPE_DROPDOWN: Y: 25 Width: PARENT_RIGHT @@ -221,7 +221,7 @@ Container@INPUT_PANEL: Label@SCROLL_SPEED_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Pan Speed: + Text: label-scrollspeed-slider-container-scroll-speed Slider@SCROLLSPEED_SLIDER: Y: 25 Width: PARENT_RIGHT @@ -240,7 +240,7 @@ Container@INPUT_PANEL: Label@ZOOM_SPEED_LABEL: Width: PARENT_RIGHT Height: 20 - Text: Zoom Speed: + Text: label-zoomspeed-slider-container-zoom-speed ExponentialSlider@ZOOMSPEED_SLIDER: Y: 25 Width: PARENT_RIGHT @@ -259,7 +259,7 @@ Container@INPUT_PANEL: Label@UI_SCROLL_SPEED_LABEL: Width: PARENT_RIGHT Height: 20 - Text: UI Scroll Speed: + Text: label-ui-scrollspeed-slider-container-scroll-speed Slider@UI_SCROLLSPEED_SLIDER: Y: 25 Width: PARENT_RIGHT @@ -267,3 +267,4 @@ Container@INPUT_PANEL: Ticks: 7 MinimumValue: 1 MaximumValue: 100 + diff --git a/mods/common/chrome/settings.yaml b/mods/common/chrome/settings.yaml index 9fab546045da..e2e6e1c3e552 100644 --- a/mods/common/chrome/settings.yaml +++ b/mods/common/chrome/settings.yaml @@ -16,7 +16,7 @@ Background@SETTINGS_PANEL: Y: 20 Width: PARENT_RIGHT Height: 25 - Text: Settings + Text: label-settings-panel-title Align: Center Font: Bold Button@RESET_BUTTON: @@ -24,7 +24,7 @@ Background@SETTINGS_PANEL: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Reset + Text: button-settings-panel-reset Font: Bold Button@BACK_BUTTON: Key: escape @@ -32,7 +32,7 @@ Background@SETTINGS_PANEL: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Back + Text: button-settings-panel-back Font: Bold Container@SETTINGS_TAB_CONTAINER: X: 20 @@ -54,3 +54,4 @@ Background@SETTINGS_PANEL: Width: PARENT_RIGHT - 190 - 20 Height: PARENT_BOTTOM - 105 TooltipContainer@SETTINGS_TOOLTIP_CONTAINER: + diff --git a/mods/common/chrome/text-notifications.yaml b/mods/common/chrome/text-notifications.yaml index b1a77599ba99..be7967b645df 100644 --- a/mods/common/chrome/text-notifications.yaml +++ b/mods/common/chrome/text-notifications.yaml @@ -53,3 +53,4 @@ Container@TRANSIENT_LINE_TEMPLATE: WordWrap: True Shadow: True TextColor: AFEEEE + diff --git a/mods/common/chrome/tooltips.yaml b/mods/common/chrome/tooltips.yaml index b3181e67f93c..57310607ef82 100644 --- a/mods/common/chrome/tooltips.yaml +++ b/mods/common/chrome/tooltips.yaml @@ -123,7 +123,7 @@ Background@LATENCY_TOOLTIP: Y: 3 Height: 26 Font: Bold - Text: Latency: + Text: label-latency-tooltip-prefix Label@LATENCY: Y: 3 Height: 26 @@ -138,7 +138,7 @@ Background@ANONYMOUS_PLAYER_TOOLTIP: Label@NAME: X: 7 Y: 2 - Text: Anonymous Player + Text: label-anonymous-player-tooltip-name Height: 24 Font: MediumBold Label@LOCATION: @@ -169,7 +169,7 @@ Background@ANONYMOUS_PLAYER_TOOLTIP: Label@LABEL: X: 10 Height: 12 - Text: Game Admin + Text: label-game-admin Font: TinyBold Background@REGISTERED_PLAYER_TOOLTIP: @@ -212,7 +212,7 @@ Background@REGISTERED_PLAYER_TOOLTIP: X: 10 Y: 1 Height: 12 - Text: Game Admin + Text: label-game-admin Font: TinyBold Container@MESSAGE_HEADER: Height: 26 @@ -310,6 +310,7 @@ Background@SUPPORT_POWER_TOOLTIP: TextColor: FFFF00 Font: Bold Label@TIME: + X: 5 Y: 10 Font: TinyBold VAlign: Top @@ -318,6 +319,12 @@ Background@SUPPORT_POWER_TOOLTIP: Y: 24 Font: TinyBold VAlign: Top + Label@COST: + X: 5 + Y: 10 + Font: TinyBold + VAlign: Top + Text: $ Background@ARMY_TOOLTIP: Logic: ArmyTooltipLogic @@ -336,3 +343,4 @@ Background@ARMY_TOOLTIP: Height: 2 Font: TinyBold VAlign: Top + diff --git a/mods/common/languages/chrome/en.ftl b/mods/common/languages/chrome/en.ftl new file mode 100644 index 000000000000..e7dbfbde0b1b --- /dev/null +++ b/mods/common/languages/chrome/en.ftl @@ -0,0 +1,578 @@ +## assetbrowser.yaml +label-assetbrowser-panel-title = Asset Browser +label-assetbrowser-panel-source-selector-desc = Select asset source +dropdownbutton-assetbrowser-panel-source-selector = Folders +dropdownbutton-assetbrowser-panel-asset-types-dropdown = Asset types +label-assetbrowser-panel-filename-desc = Filter by name +label-assetbrowser-panel-sprite-scale = Scale: +label-assetbrowser-panel-palette-desc = Palette: +label-sprite-bg-error = Error displaying file. See assetbrowser.log for details. +button-assetbrowser-panel-close = Close + +## color-picker.yaml +button-color-chooser-random = Random +button-color-chooser-store = Store +button-color-chooser-mixer-tab = Mixer +button-color-chooser-palette-tab = Palette +label-preset-header = Preset Colors +label-custom-header = Custom Colors + +## confirmation-dialogs.yaml +button-threebutton-prompt-confirm = Confirm +button-threebutton-prompt-other = Restart +button-threebutton-prompt-cancel = Cancel +button-twobutton-prompt-confirm = Confirm +button-twobutton-prompt-cancel = Cancel +button-text-input-prompt-accept = OK +button-text-input-prompt-cancel = Cancel + +## connection.yaml +label-connectionfailed-panel-password = Password: +button-connectionfailed-panel-retry = Retry +button-connectionfailed-panel-abort = Abort +button-connectionfailed-panel-quit = Quit +label-connecting-panel-title = Connecting +button-connecting-panel-abort = Abort +label-connection-switchmod-panel-title = Switch Mod +label-connection-switchmod-panel-desc = This server is running a different mod: +label-connection-switchmod-panel-desc2 = Switch mods and join server? +button-connection-switchmod-panel-switch = Switch +button-connection-switchmod-panel-abort = Abort + +## credits.yaml +label-credits-panel-title = Credits +button-tab-container-engine = OpenRA +button-credits-panel-back = Close + +## editor.yaml +label-new-map-bg-title = New Map +label-new-map-bg-tileset = Tileset: +label-new-map-bg-width = Width: +label-new-map-bg-height = Height: +button-new-map-bg-create = Create +button-new-map-bg-cancel = Cancel + +label-save-map-panel-title = + .label = Save Map + .label = Title: + +label-save-map-panel-author = Author: +label-save-map-panel-visibility = Visibility: +dropdownbutton-save-map-panel-visibility-dropdown = Map Visibility +label-save-map-panel-directory = Directory: +label-save-map-panel-filename = Filename: +button-save-map-panel = Save +button-save-map-panel-back = Cancel +label-actor-edit-panel-id = ID +button-container-delete = Delete +button-container-cancel = Cancel +button-container-ok = OK +label-tiles-bg-search = Search: +label-tiles-bg-categories = Filter: +label-actors-bg-search = Search: +label-actors-bg-categories = Filter: +label-actors-bg-owners = Owner: + +button-map-editor-tab-container-tiles = + .label = Tiles + .tooltip = Tiles + +button-map-editor-tab-container-overlays = + .label = Overlays + .tooltip = Overlays + +button-map-editor-tab-container-actors = + .label = Actors + .tooltip = Actors + +button-map-editor-tab-container-history = + .label = History + .tooltip = History + +button-editor-world-root-options = + .label = Menu + .tooltip = Menu + +button-editor-world-root-copypaste = + .label = Copy/Paste + .tooltip = Copy + +dropdownbutton-editor-world-root-copyfilter-button = Copy Filters + +button-editor-world-root-undo = + .label = Undo + .tooltip = Undo last step + +button-editor-world-root-redo = + .label = Redo + .tooltip = Redo last step + +dropdownbutton-editor-world-root-overlay-button = Overlays +button-select-categories-buttons-all = All +button-select-categories-buttons-none = None + +## gamesave-browser.yaml +label-gamesave-browser-panel-load-title = Load game +label-gamesave-browser-panel-save-title = Save game +label-gamesave-browser-panel-title = [CREATE NEW FILE] +button-gamesave-browser-panel-cancel = Back +button-gamesave-browser-panel-delete-all = Delete All +button-gamesave-browser-panel-delete = Delete +button-gamesave-browser-panel-rename = Rename +button-gamesave-browser-panel-load = Load +button-gamesave-browser-panel-save = Save + +## ingame-chat.yaml, ingame-infochat.yaml +button-chat-chrome-mode = + .label = Team + .tooltip = Toggle chat mode + +## ingame-debug-hpf.yaml +dropdownbutton-hpf-overlay-locomotor = Select Locomotor +dropdownbutton-hpf-overlay-check = Select BlockedByActor + +## ingame-debug.yaml +label-debug-panel-title = Debug Options +checkbox-debug-panel-instant-build = Instant Build Speed +checkbox-debug-panel-enable-tech = Build Everything +checkbox-debug-panel-build-anywhere = Build Anywhere +checkbox-debug-panel-unlimited-power = Unlimited Power +checkbox-debug-panel-instant-charge = Instant Charge Time +checkbox-debug-panel-disable-visibility-checks = Disable Visibility Checks +button-debug-panel-give-cash = Give $20,000 +button-debug-panel-grow-resources = Grow Resources +button-debug-panel-give-exploration = Clear Shroud +button-debug-panel-reset-exploration = Reset Shroud +label-debug-panel-visualizations-title = Visualizations +checkbox-debug-panel-show-unit-paths = Show Unit Paths +checkbox-debug-panel-show-customterrain-overlay = Show Custom Terrain +checkbox-debug-panel-show-actor-tags = Show Actor Tags +checkbox-debug-panel-show-combatoverlay = Show Combat Geometry +checkbox-debug-panel-show-geometry = Show Render Geometry +checkbox-debug-panel-show-terrain-overlay = Show Terrain Geometry +checkbox-debug-panel-show-screenmap = Show Screen Map + +## ingame-infoobjectives.yaml +label-mission-objectives = Mission: + +## ingame-infoscripterror.yaml +label-script-error-panel-desca = The map script has encountered a fatal error +label-script-error-panel-descb = The details of the error have been saved to lua.log in the logs directory. +label-script-error-panel-descc = Please send this file to the map author so that they can fix this issue. + +## ingame-infostats.yaml +label-objective-mission = Mission: +checkbox-objective-stats = Destroy all opposition! +label-stats-name = Player +label-stats-faction = Faction +label-stats-score = Score +label-stats-actions = Actions + +## ingame-menu.yaml +label-menu-buttons-title = Options + +## lobby-kickdialogs.yaml +label-kick-client-dialog-texta = You may also apply a temporary ban, preventing +label-kick-client-dialog-textb = them from joining for the remainder of this game. +checkbox-kick-client-dialog-prevent-rejoining = Temporarily Ban +button-kick-client-dialog-ok = Kick +button-kick-client-dialog-cancel = Cancel +label-kick-spectators-dialog-title = Kick Spectators +button-kick-spectators-dialog-ok = Ok +button-kick-spectators-dialog-cancel = Cancel +label-force-start-dialog-title = Start Game? +label-force-start-dialog-texta = One or more players are not yet ready. +label-force-start-dialog-textb = Are you sure that you want to force start the game? +label-kick-warning-a = One or more clients are missing the selected +label-kick-warning-b = map, and will be kicked from the server. +button-force-start-dialog-ok = Start +button-force-start-dialog-cancel = Cancel + +## lobby-mappreview.yaml +label-map-incompatible-status-a = This map is not compatible +label-map-incompatible-status-b = with this version of OpenRA +label-map-validating-status = Validating... +button-map-download-available-install = Install Map +button-map-preview-update = Update Map +button-map-update-download-available-install = Install Map +label-map-preview-searching = Searching OpenRA Resource Center... +label-map-unavailable-a = This map was not found on the +label-map-unavailable-b = OpenRA Resource Center +label-map-preview-error = An error occurred during installation +label-map-update-available-a = A new version of the map +label-map-update-available-b = was found on your computer + +## lobby-music.yaml +label-container-music = Music +label-container-length = Length +checkbox-controls-shuffle = Shuffle +checkbox-controls-repeat = Loop +label-controls-volume = Volume: + +## lobby-music.yaml, musicplayer.yaml +label-container-title = Track +label-no-music-title = Music Not Installed +label-no-music-desca = The game music can be installed +label-no-music-descb = from the "Manage Content" menu. + +## lobby-options.yaml +label-lobby-options-bin-title = Map Options + +## lobby-players.yaml +label-container-lobby-name = Name +label-container-lobby-color = Color +label-container-lobby-faction = Faction +label-container-lobby-team = Team +label-container-lobby-handicap = Handicap +label-container-lobby-spawn = Spawn +label-container-lobby-status = Ready +dropdownbutton-template-editable-player-slot-options = Name +label-template-editable-player-factionname = Faction +dropdownbutton-template-editable-player-team-dropdown = Team +dropdownbutton-template-editable-player-handicap-dropdown-tooltip = A handicap decreases the combat effectiveness of the player's forces +dropdownbutton-template-editable-player-spawn-dropdown = Spawn +label-template-noneditable-player-name = Name +label-faction-factionname = Faction +label-template-noneditable-player-team = Team +dropdownbutton-template-noneditable-player-handicap-dropdown-tooltip = A handicap decreases the combat effectiveness of the player's forces +label-template-empty-name = Name +dropdownbutton-template-empty-slot-options = Name +button-template-empty-join = Play in this slot +label-template-editable-spectator = Spectator +label-template-noneditable-spectator-name = Name +label-template-noneditable-spectator = Spectator +checkbox-template-new-spectator-toggle-spectators = Allow Spectators? +button-template-new-spectator-spectate = Spectate + +## lobby-servers.yaml +image-lobby-servers-bin-password-protected-tooltip = Requires Password +image-lobby-servers-bin-requires-authentication-tooltip = Requires OpenRA forum account +dropdownbutton-lobby-servers-bin-filters = Filter Games + +## lobby-servers.yaml, multiplayer-browser.yaml +label-container-name = Server +label-container-players = Players +label-container-location = Location +label-container-status = Status +label-notice-container-outdated-version = You are running an outdated version of OpenRA. Download the latest version from www.openra.net +label-notice-container-unknown-version = You are running an unrecognized version of OpenRA. Download the latest version from www.openra.net +label-notice-container-playtest-available = A preview of the next OpenRA release is available for testing. Download the playtest from www.openra.net + +## lobby.yaml +dropdownbutton-server-lobby-slots = Slot Admin +button-skirmish-tabs-players-tab = Players +button-skirmish-tabs-options-tab = Options +button-skirmish-tabs-music-tab = Music +button-multiplayer-tabs-players-tab = Players +button-multiplayer-tabs-options-tab = Options +button-multiplayer-tabs-music-tab = Music +button-multiplayer-tabs-servers-tab = Servers +button-server-lobby-changemap = Change Map + +button-lobbychat-chat-mode = + .label = Team + .tooltip = Toggle chat mode + +button-server-lobby-start-game = Start Game +button-server-lobby-disconnect = Leave Game + +## mainmenu-prompts.yaml +label-mainmenu-introduction-prompt-title = Establishing Battlefield Control +label-mainmenu-introduction-prompt-desc-a = Welcome back Commander! Initialize combat parameters using the options below. +label-mainmenu-introduction-prompt-desc-b = Additional options can be configured later from the Settings menu. +button-mainmenu-introduction-prompt-continue = Continue +label-mainmenu-system-info-prompt-title = Establishing Battlefield Control +label-mainmenu-system-info-prompt-text-a = We would like to collect some system details that will help us optimize OpenRA. +label-mainmenu-system-info-prompt-text-b = With your permission, the following anonymous data will be sent each game launch: +checkbox-mainmenu-system-info-prompt-sysinfo = Send System Information +button-mainmenu-system-info-prompt-continue = Continue + +## mainmenu-prompts.yaml, settings-display.yaml +label-profile-section-header = Profile +label-player-container = Player Name: +label-playercolor-container-color = Preferred Color: +label-display-section-header = Display +label-battlefield-camera-dropdown-container = Battlefield Camera: +label-ui-scale-dropdown-container = UI Scale: +checkbox-cursordouble-container = Increase Cursor Size + +## mainmenu-prompts.yaml, settings-input.yaml +label-input-section-header = Input +label-mouse-control-container = Control Scheme: +label-mouse-control-desc-classic-selection = - Select units using the mouse button +label-mouse-control-desc-classic-commands = - Command units using the mouse button +label-mouse-control-desc-classic-buildigs = - Place structures using the mouse button +label-mouse-control-desc-classic-support = - Target support powers using the mouse button +label-mouse-control-desc-classic-zoom = - Zoom the battlefield using the +label-mouse-control-desc-classic-zoom-modifier = - Zoom the battlefield using +label-mouse-control-desc-classic-scroll-right = - Pan the battlefield using the mouse button +label-mouse-control-desc-classic-scroll-middle = - Pan the battlefield using the mouse button +label-mouse-control-desc-classic-edgescroll = or by moving the cursor to the edge of the screen +label-mouse-control-desc-modern-selection = - Select units using the mouse button +label-mouse-control-desc-modern-commands = - Command units using the mouse button +label-mouse-control-desc-modern-buildigs = - Place structures using the mouse button +label-mouse-control-desc-modern-support = - Target support powers using the mouse button +label-mouse-control-desc-modern-zoom = - Zoom the battlefield using the +label-mouse-control-desc-modern-zoom-modifier = - Zoom the battlefield using +label-mouse-control-desc-modern-scroll-right = - Pan the battlefield using the mouse button +label-mouse-control-desc-modern-scroll-middle = - Pan the battlefield using the mouse button +label-mouse-control-desc-modern-edgescroll = or by moving the cursor to the edge of the screen +checkbox-edgescroll-container = Screen Edge Panning + +## mainmenu.yaml +label-main-menu-mainmenu-title = OpenRA +button-main-menu-singleplayer = Singleplayer +button-main-menu-multiplayer = Multiplayer +button-main-menu-settings = Settings +button-main-menu-extras = Extras +button-main-menu-content = Manage Content +button-main-menu-quit = Quit +label-singleplayer-menu-title = Singleplayer +button-singleplayer-menu-skirmish = Skirmish +button-singleplayer-menu-missions = Missions +button-singleplayer-menu-load = Load +button-singleplayer-menu-back = Back +label-extras-menu-title = Extras +button-extras-menu-replays = Replays +button-extras-menu-music = Music +button-extras-menu-map-editor = Map Editor +button-extras-menu-assetbrowser = Asset Browser +button-extras-menu-credits = Credits +button-extras-menu-back = Back +label-map-editor-menu-title = Map Editor +button-map-editor-menu-new = New Map +button-map-editor-menu-load = Load Map +button-map-editor-menu-back = Back +dropdownbutton-news-bg-button = Battlefield News +label-update-notice-a = You are running an outdated version of OpenRA. +label-update-notice-b = Download the latest version from www.openra.net + +## map-chooser.yaml +label-mapchooser-panel-title = Choose Map +button-mapchooser-panel-system-maps-tab = Official Maps +button-mapchooser-panel-user-maps-tab = Custom Maps +label-filter-order-controls-desc = Filter: +label-filter-order-controls-desc-joiner = in +label-filter-order-controls-orderby = Order by: +button-mapchooser-panel-randommap = Random Map +button-mapchooser-panel-delete-map = Delete Map +button-mapchooser-panel-delete-all-maps = Delete All Maps +button-mapchooser-panel-ok = Ok +button-mapchooser-panel-cancel = Back + +## missionbrowser.yaml +label-missionbrowser-panel-title = Missions +button-missionbrowser-panel-start-briefing-video = Watch Briefing +button-missionbrowser-panel-stop-briefing-video = Stop Briefing +button-missionbrowser-panel-start-info-video = Watch Info Video +button-missionbrowser-panel-stop-info-video = Stop Info Video +button-missionbrowser-panel-startgame = Play +button-missionbrowser-panel-back = Back +button-missionbrowser-panel-mission-info = Mission Info +button-missionbrowser-panel-mission-options = Options + +## multiplayer-browser.yaml +label-multiplayer-panel-title = Multiplayer +image-multiplayer-panel-password-protected-tooltip = Requires Password +image-multiplayer-panel-requires-authentication-tooltip = Requires OpenRA forum account +button-selected-server-join = Join +dropdownbutton-multiplayer-panel-filters = Filter Games +button-multiplayer-panel-directconnect = Direct IP +button-multiplayer-panel-create = Create +button-multiplayer-panel-back = Back + +## multiplayer-browserpanels.yaml +checkbox-multiplayer-filter-panel-waiting-for-players = Waiting +checkbox-multiplayer-filter-panel-empty = Empty +checkbox-multiplayer-filter-panel-password-protected = Protected +checkbox-multiplayer-filter-panel-already-started = Started +checkbox-multiplayer-filter-panel-incompatible-version = Incompatible + +## multiplayer-createserver.yaml +label-multiplayer-createserver-panel-title = Create Server +label-multiplayer-createserver-panel-server-name = Server Name: +label-multiplayer-createserver-panel-password = Password: +label-multiplayer-createserver-panel-after-password = (optional) +label-multiplayer-createserver-panel-listen-port = Port: +checkbox-multiplayer-createserver-panel-advertise = Advertise Online +label-notices-lan-advertising = - Game will be advertised to the Local Area Network only. +label-notices-lan-firewall = - You must manually configure your firewall to allow connections. +label-notices-lan-portforward-a = - Players can connect using Direct IP from the Internet if you +label-notices-lan-portforward-b = manually configure port forwarding on your router. +label-notices-no-upnp-advertising = - Game will be advertised to the Local Area Network and Internet. +label-notices-no-upnp-firewall = - You must manually configure your firewall to allow connections. +label-notices-no-upnp-portforward-a = - You must manually configure your router to allow and forward +label-notices-no-upnp-portforward-b = connections to your local IP and Port. +label-notices-no-upnp-settings-a = - You can enable UPnP/NAT-PMP (if supported by your router) +label-notices-no-upnp-settings-b = in the Advanced tab of the settings menu. +label-notices-upnp-advertising = - Game will be advertised to the Local Area Network and Internet. +label-notices-upnp-firewall = - You must manually configure your firewall to allow connections. +label-notices-upnp-portforward-a = - Game will automatically configure port forwarding. +label-notices-upnp-settings-a = - You can disable UPnP/NAT-PMP in the settings menu. +button-multiplayer-createserver-panel-map = Change Map +button-multiplayer-createserver-panel-back = Back +button-multiplayer-createserver-panel-create = Create + +## multiplayer-directconnect.yaml +label-directconnect-panel-title = Connect to Server +label-directconnect-panel-address = Server Address: +button-directconnect-panel-join = Join +button-directconnect-panel-back = Cancel + +## musicplayer.yaml +label-container-type = Length +checkbox-music-panel-shuffle = Shuffle +checkbox-music-panel-repeat = Loop +button-music-panel-back = Close + +## playerprofile.yaml +button-profile-header-destroy-key = Logout +label-generate-keys-desc-a = Connect to a forum account to identify +label-generate-keys-desc-b = yourself to other players, join private +label-generate-keys-desc-c = servers, and display badges. +button-generate-keys-key = Connect to an OpenRA forum account +label-generating-keys-desc-a = Generating authentication key pair. +label-generating-keys-desc-b = This will take several seconds... +label-register-fingerprint-desc-a = An authentication key has been copied to your +label-register-fingerprint-desc-b = clipboard. Add this to your User Control Panel +label-register-fingerprint-desc-c = on the OpenRA forum then press Continue. +button-register-fingerprint-delete-key = Cancel +button-register-fingerprint-check-key = Continue +label-checking-fingerprint-desc-a = Querying account details from +label-checking-fingerprint-desc-b = the OpenRA forum... +label-fingerprint-not-found-desc-a = Your authentication key is not connected +label-fingerprint-not-found-desc-b = to an OpenRA forum account. +button-fingerprint-not-found-continue = Back +label-connection-error-desc-a = Failed to connect to the OpenRA forum. +label-connection-error-desc-b = Please check your internet connection. +button-connection-error-retry = Retry + +## replaybrowser.yaml +label-replaybrowser-panel-title = Replay Viewer +label-filters-title = Filter +label-filters-flt-gametype-desc = Type: +dropdownbutton-filters-flt-gametype = Any +label-filters-flt-date-desc = Date: +dropdownbutton-filters-flt-date = Any +label-filters-flt-duration-desc = Duration: +dropdownbutton-filters-flt-duration = Any +label-filters-flt-mapname-desc = Map: +dropdownbutton-filters-flt-mapname = Any +label-filters-flt-player-desc = Player: +dropdownbutton-filters-flt-player = Anyone +label-filters-flt-outcome-desc = Outcome: +dropdownbutton-filters-flt-outcome = Any +label-filters-flt-faction-desc = Faction: +dropdownbutton-filters-flt-faction = Any +button-filters-flt-reset = Reset Filters +label-management-manage-title = Manage +button-management-mng-rensel = Rename +button-management-mng-delsel = Delete +button-management-mng-delall = Delete All +label-replay-list-container-replaybrowser-title = Choose Replay +button-replaybrowser-panel-watch = Watch +button-replaybrowser-panel-cancel = Back + +## settings-advanced.yaml +label-network-section-header = Advanced +checkbox-nat-discovery-container = Enable UPnP/NAT-PMP Discovery +checkbox-fetch-news-container = Fetch Community News +checkbox-perfgraph-container = Show Performance Graph +checkbox-check-version-container = Check for Updates +checkbox-perftext-container = Show Performance Text +checkbox-sendsysinfo-container = Send System Information +label-sendsysinfo-checkbox-container-desc = Your Operating System, OpenGL and .NET runtime versions, and language settings will be sent along with an anonymous ID to help prioritize future development. +label-debug-section-header = Developer +label-debug-hidden-container-a = Additional developer-specific options can be enabled via the +label-debug-hidden-container-b = Debug.DisplayDeveloperSettings setting or launch flag +checkbox-botdebug-container = Show Bot Debug Messages +checkbox-checkbotsync-container = Check Sync around BotModule Code +checkbox-luadebug-container = Show Map Debug Messages +checkbox-checkunsynced-container = Check Sync around Unsynced Code +checkbox-replay-commands-container = Enable Debug Commands in Replays +checkbox-perflogging-container = Enable Tick Performance Logging + +## settings-audio.yaml +label-audio-section-header = Audio +label-no-audio-device-container = Audio controls require an active sound device +checkbox-cash-ticks-container = Cash Ticks +checkbox-mute-sound-container = Mute Sound +label-sound-volume-container = Sound Volume: + +checkbox-mute-background-music-container = + .label = Mute Menu Music + .tooltip = Mute background music when no specific track is playing + +label-music-volume-container = Music Volume: +label-audio-device-container = Audio Device: +label-video-volume-container = Video Volume: +label-restart-required-container-audio-desc = Device changes will be applied after the game is restarted + +## settings-display.yaml +label-target-lines-dropdown-container = Target Lines: +label-status-bar-dropdown-container-bars = Status Bars: + +checkbox-player-stance-colors-container = + .label = Player Relationship Colors + .tooltip = Change minimap and health bar colors based on relationship (own, enemy, ally, neutral) + +checkbox-ui-feedback-container = + .label = Show UI Feedback Notifications + .tooltip = Show transient text notifications for UI events + +checkbox-transients-container = + .label = Show Game Event Notifications + .tooltip = Show transient text notifications for game events + +checkbox-pause-shellmap-container = Pause Menu Background +checkbox-hide-replay-chat-container = Hide Chat in Replays +label-video-section-header = Video +label-video-mode-dropdown-container = Video Mode: +dropdownbutton-video-mode-dropdown-container = Windowed +label-window-resolution-container-size = Window Size: +label-window-resolution-container-x = x +label-display-selection-container = Select Display: +dropdownbutton-display-selection-container-dropdown = Standard +checkbox-vsync-container = Enable VSync +checkbox-frame-limit-gamespeed-container = Limit framerate to game tick rate +label-gl-profile-dropdown-container = OpenGL Profile: +label-restart-required-container-video-desc = Display and OpenGL changes require restart + +## settings-hotkeys.yaml +label-hotkeys-panel-filter-input = Filter by name: +label-hotkeys-panel-context-dropdown = Context: +label-hotkey-empty-list-message = No hotkeys match the filter criteria. +label-notices-readonly-notice = This hotkey cannot be modified +button-hotkey-remap-dialog-override = Override + +button-hotkey-remap-dialog-clear = + .label = Clear + .tooltip = Unbind the hotkey + +button-hotkey-remap-dialog-reset = + .label = Reset + .tooltip = Reset to default + +## settings-input.yaml +label-zoom-modifier-container = Zoom Modifier: +checkbox-alternate-scroll-container = Alternate Mouse Panning +checkbox-lockmouse-container = Lock Mouse to Window +label-mouse-scroll-type-container = Pan Behaviour: +label-scrollspeed-slider-container-scroll-speed = Pan Speed: +label-zoomspeed-slider-container-zoom-speed = Zoom Speed: +label-ui-scrollspeed-slider-container-scroll-speed = UI Scroll Speed: + +## settings.yaml +label-settings-panel-title = Settings +button-settings-panel-reset = Reset +button-settings-panel-back = Back + +## tooltips.yaml +label-latency-tooltip-prefix = Latency: +label-anonymous-player-tooltip-name = Anonymous Player +label-game-admin = Game Admin + +## gamesave-loading.yaml +label-gamesave-loading-screen-title = Loading Saved Game +label-gamesave-loading-screen-desc = Press Escape to cancel loading and return to the main menu + diff --git a/mods/common/languages/en.ftl b/mods/common/languages/en.ftl index c96683536f3d..6337e6f967c8 100644 --- a/mods/common/languages/en.ftl +++ b/mods/common/languages/en.ftl @@ -718,21 +718,32 @@ description-actor-tags-overlay = toggles actor tags overlay. ## DevCommands notification-cheats-disabled = Cheats are disabled. notification-invalid-cash-amount = Invalid amount of cash. +notification-invalid-actor-name = { $actor } is not a valid actor. +notification-unbuildable-actor-name = { $actor } is not a producable actor. description-toggle-visibility = toggles visibility checks and minimap. +description-toggle-visibility-all = toggles visibility checks and minimap for all players. description-give-cash = gives the default or specified amount of money. description-give-cash-all = gives the default or specified amount of money to all players and ai. description-instant-building = toggles instant building. +description-instant-building-all = toggles instant building for all players. description-build-anywhere = toggles the ability to build anywhere. +description-build-anywhere-all = toggles the ability to build anywhere for all players. description-unlimited-power = toggles infinite power. +description-unlimited-power-all = toggles infinite power for all players. description-enable-tech = toggles the ability to build everything. +description-enable-tech-all = toggles the ability to build everything for all players. description-fast-charge = toggles almost instant support power charging. +description-fast-charge-all = toggles almost instant support power charging for all players. description-dev-cheat-all = toggles all cheats and gives you some cash for your trouble. +description-dev-cheat-all-for-all = toggles all cheats for all players and gives everyone some cash for their troubles. description-dev-crash = crashes the game. description-levelup-actor = adds a specified number of levels to the selected actors. description-player-experience = adds a specified amount of player experience to the owner(s) of selected actors. description-power-outage = causes owner(s) of selected actors to have a 5 second power outage. description-kill-selected-actors = kills selected actors. description-dispose-selected-actors = disposes selected actors. +description-produce-from-selected-actors = makes the selected actors produce given actor. +description-clear-resources = removes all resources from the map. ## HelpCommands notification-available-commands = Here are the available commands: diff --git a/mods/d2k/chrome/dropdowns.yaml b/mods/d2k/chrome/dropdowns.yaml index 2a8fa23a5292..e4d6605cdc40 100644 --- a/mods/d2k/chrome/dropdowns.yaml +++ b/mods/d2k/chrome/dropdowns.yaml @@ -146,3 +146,4 @@ ScrollPanel@NEWS_PANEL: Height: PARENT_BOTTOM Align: Center VAlign: Middle + diff --git a/mods/d2k/chrome/encyclopedia.yaml b/mods/d2k/chrome/encyclopedia.yaml index 8fe1afea9c67..66bda6326d08 100644 --- a/mods/d2k/chrome/encyclopedia.yaml +++ b/mods/d2k/chrome/encyclopedia.yaml @@ -14,7 +14,7 @@ Background@ENCYCLOPEDIA_PANEL: Label@ENCYCLOPEDIA_TITLE: Width: PARENT_RIGHT Height: 25 - Text: Mentat + Text: label-encyclopedia-content-title Align: Center Font: Bold ScrollPanel@ACTOR_LIST: @@ -79,7 +79,8 @@ Background@ENCYCLOPEDIA_PANEL: Y: PARENT_BOTTOM - 45 Width: 160 Height: 25 - Text: Back + Text: button-encyclopedia-panel-back Font: Bold Key: escape TooltipContainer@TOOLTIP_CONTAINER: + diff --git a/mods/d2k/chrome/ingame-infostats.yaml b/mods/d2k/chrome/ingame-infostats.yaml index 1f5310011db9..f73024676343 100644 --- a/mods/d2k/chrome/ingame-infostats.yaml +++ b/mods/d2k/chrome/ingame-infostats.yaml @@ -12,7 +12,7 @@ Container@SKIRMISH_STATS: Width: 482 Height: 25 Font: MediumBold - Text: Mission: + Text: label-objective-mission Label@STATS_STATUS: X: 100 Y: 22 @@ -25,7 +25,7 @@ Container@SKIRMISH_STATS: Width: 482 Height: 20 Font: Bold - Text: Destroy all opposition! + Text: checkbox-objective-stats Disabled: true TextColorDisabled: FFFFFF Container@STATS_HEADERS: @@ -38,28 +38,28 @@ Container@SKIRMISH_STATS: Y: 1 Width: 210 Height: 25 - Text: Player + Text: label-stats-name Font: Bold Label@FACTION: X: 230 Y: 1 Width: 120 Height: 25 - Text: Faction + Text: label-stats-faction Font: Bold Label@SCORE: X: 392 Y: 1 Width: 60 Height: 25 - Text: Score + Text: label-stats-score Font: Bold Label@ACTIONS: X: 457 Y: 1 Width: 20 Height: 25 - Text: Actions + Text: label-stats-actions Font: Bold ScrollPanel@PLAYER_LIST: X: 20 @@ -138,7 +138,7 @@ Container@SKIRMISH_STATS: Height: 25 Background: checkbox-toggle TooltipContainer: TOOLTIP_CONTAINER - TooltipText: Kick this player + TooltipText: button-player-template-kick-tooltip Children: Image: ImageCollection: lobby-bits @@ -181,10 +181,11 @@ Container@SKIRMISH_STATS: Height: 25 Background: checkbox-toggle TooltipContainer: TOOLTIP_CONTAINER - TooltipText: Kick this player + TooltipText: button-spectator-template-kick-tooltip Children: Image: ImageCollection: lobby-bits ImageName: kick X: 7 Y: 7 + diff --git a/mods/d2k/chrome/ingame-menu.yaml b/mods/d2k/chrome/ingame-menu.yaml index 92fae414e87f..9160d668d942 100644 --- a/mods/d2k/chrome/ingame-menu.yaml +++ b/mods/d2k/chrome/ingame-menu.yaml @@ -23,7 +23,7 @@ Container@INGAME_MENU: Y: 21 Width: 200 Height: 30 - Text: Options + Text: label-menu-buttons-title Align: Center Font: Bold Button@BUTTON_TEMPLATE: @@ -32,3 +32,4 @@ Container@INGAME_MENU: Width: 140 Height: 30 Font: Bold + diff --git a/mods/d2k/chrome/ingame-observer.yaml b/mods/d2k/chrome/ingame-observer.yaml index 24d4c713a702..9598e17a4f23 100644 --- a/mods/d2k/chrome/ingame-observer.yaml +++ b/mods/d2k/chrome/ingame-observer.yaml @@ -21,14 +21,14 @@ Container@OBSERVER_WIDGETS: Width: PARENT_RIGHT - 30 Height: 25 Align: Right - Text: Audio Muted + Text: label-mute-indicator Contrast: true MenuButton@OPTIONS_BUTTON: X: 5 Y: 5 Width: 160 Height: 25 - Text: Options (Esc) + Text: button-observer-widgets-options Font: Bold Key: escape DisableWorldSounds: true @@ -116,7 +116,7 @@ Container@OBSERVER_WIDGETS: Width: 26 Height: 26 Key: Pause - TooltipText: Pause + TooltipText: button-replay-player-pause-tooltip TooltipContainer: TOOLTIP_CONTAINER IgnoreChildMouseOver: true Children: @@ -132,7 +132,7 @@ Container@OBSERVER_WIDGETS: Height: 26 Key: Pause IgnoreChildMouseOver: true - TooltipText: Play + TooltipText: button-replay-player-play-tooltip TooltipContainer: TOOLTIP_CONTAINER Children: Image@IMAGE_PLAY: @@ -146,9 +146,9 @@ Container@OBSERVER_WIDGETS: Width: 36 Height: 20 Key: ReplaySpeedSlow - TooltipText: Slow speed + TooltipText: button-replay-player-slow.tooltip TooltipContainer: TOOLTIP_CONTAINER - Text: 50% + Text: button-replay-player-slow.label Font: TinyBold Button@BUTTON_REGULAR: X: 55 + 45 @@ -156,9 +156,9 @@ Container@OBSERVER_WIDGETS: Width: 38 Height: 20 Key: ReplaySpeedRegular - TooltipText: Regular speed + TooltipText: button-replay-player-regular.tooltip TooltipContainer: TOOLTIP_CONTAINER - Text: 100% + Text: button-replay-player-regular.label Font: TinyBold Button@BUTTON_FAST: X: 55 + 45 * 2 @@ -166,9 +166,9 @@ Container@OBSERVER_WIDGETS: Width: 38 Height: 20 Key: ReplaySpeedFast - TooltipText: Fast speed + TooltipText: button-replay-player-fast.tooltip TooltipContainer: TOOLTIP_CONTAINER - Text: 200% + Text: button-replay-player-fast.label Font: TinyBold Button@BUTTON_MAXIMUM: X: 55 + 45 * 3 @@ -176,9 +176,9 @@ Container@OBSERVER_WIDGETS: Width: 38 Height: 20 Key: ReplaySpeedMax - TooltipText: Maximum speed + TooltipText: button-replay-player-maximum.tooltip TooltipContainer: TOOLTIP_CONTAINER - Text: MAX + Text: button-replay-player-maximum.label Font: TinyBold Container@INGAME_OBSERVERSTATS_BG: Logic: ObserverStatsLogic @@ -235,7 +235,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-basic-stats-player-header Align: Left Shadow: True Label@CASH_HEADER: @@ -244,7 +244,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Cash + Text: label-basic-stats-cash-header Align: Right Shadow: True Label@POWER_HEADER: @@ -253,7 +253,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Power + Text: label-basic-stats-power-header Align: Center Shadow: True Label@KILLS_HEADER: @@ -262,7 +262,7 @@ Container@OBSERVER_WIDGETS: Width: 40 Height: PARENT_BOTTOM Font: Bold - Text: Kills + Text: label-basic-stats-kills-header Align: Right Shadow: True Label@DEATHS_HEADER: @@ -271,7 +271,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Deaths + Text: label-basic-stats-deaths-header Align: Right Shadow: True Label@ASSETS_DESTROYED_HEADER: @@ -280,7 +280,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Destroyed + Text: label-basic-stats-assets-destroyed-header Align: Right Shadow: True Label@ASSETS_LOST_HEADER: @@ -289,7 +289,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Lost + Text: label-basic-stats-assets-lost-header Align: Right Shadow: True Label@EXPERIENCE_HEADER: @@ -298,7 +298,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Score + Text: label-basic-stats-experience-header Align: Right Shadow: True Label@ACTIONS_MIN_HEADER: @@ -307,7 +307,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: APM + Text: label-basic-stats-actions-min-header Align: Right Shadow: True Container@ECONOMY_STATS_HEADERS: @@ -334,14 +334,14 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-economy-stats-player-header Shadow: True Label@CASH_HEADER: X: 155 Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Cash + Text: label-economy-stats-cash-header Align: Right Shadow: True Label@INCOME_HEADER: @@ -349,7 +349,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Income + Text: label-economy-stats-income-header Align: Right Shadow: True Label@ASSETS_HEADER: @@ -357,7 +357,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Assets + Text: label-economy-stats-assets-header Align: Right Shadow: True Label@EARNED_HEADER: @@ -365,7 +365,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Earned + Text: label-economy-stats-earned-header Align: Right Shadow: True Label@SPENT_HEADER: @@ -373,7 +373,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Spent + Text: label-economy-stats-spent-header Align: Right Shadow: True Label@HARVESTERS_HEADER: @@ -381,7 +381,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Harvesters + Text: label-economy-stats-harvesters-header Align: Right Shadow: True Label@CARRYALLS_HEADER: @@ -389,7 +389,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Carryalls + Text: label-economy-stats-carryalls-header Align: Right Shadow: True Container@PRODUCTION_STATS_HEADERS: @@ -417,7 +417,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-production-stats-player-header Align: Left Shadow: True Label@PRODUCTION_HEADER: @@ -426,7 +426,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Production + Text: label-production-stats-header Shadow: True Container@SUPPORT_POWERS_HEADERS: X: 0 @@ -453,7 +453,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-support-powers-player-header Align: Left Shadow: True Label@SUPPORT_POWERS_HEADER: @@ -462,7 +462,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Support Powers + Text: label-support-powers-header Shadow: True Container@ARMY_HEADERS: X: 0 @@ -489,7 +489,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-army-player-header Align: Left Shadow: True Label@ARMY_HEADER: @@ -498,7 +498,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Army + Text: label-army-header Shadow: True Container@COMBAT_STATS_HEADERS: X: 0 @@ -525,7 +525,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-combat-stats-player-header Align: Left Shadow: True Label@ASSETS_DESTROYED_HEADER: @@ -534,7 +534,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: Destroyed + Text: label-combat-stats-assets-destroyed-header Align: Right Shadow: True Label@ASSETS_LOST_HEADER: @@ -543,7 +543,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: Lost + Text: label-combat-stats-assets-lost-header Align: Right Shadow: True Label@UNITS_KILLED_HEADER: @@ -552,7 +552,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: U. Killed + Text: label-combat-stats-units-killed-header Align: Right Shadow: True Label@UNITS_DEAD_HEADER: @@ -561,7 +561,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: U. Lost + Text: label-combat-stats-units-dead-header Align: Right Shadow: True Label@BUILDINGS_KILLED_HEADER: @@ -570,7 +570,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: B. Killed + Text: label-combat-stats-buildings-killed-header Align: Right Shadow: True Label@BUILDINGS_DEAD_HEADER: @@ -579,7 +579,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: B. Lost + Text: label-combat-stats-buildings-dead-header Align: Right Shadow: True Label@ARMY_VALUE_HEADER: @@ -588,7 +588,7 @@ Container@OBSERVER_WIDGETS: Width: 90 Height: PARENT_BOTTOM Font: Bold - Text: Army Value + Text: label-combat-stats-army-value-header Align: Right Shadow: True Label@VISION_HEADER: @@ -597,7 +597,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Vision + Text: label-combat-stats-vision-header Align: Right Shadow: True ScrollPanel@PLAYER_STATS_PANEL: @@ -1052,3 +1052,4 @@ Container@OBSERVER_WIDGETS: X: WINDOW_RIGHT - WIDTH - 260 Y: 40 Width: 175 + diff --git a/mods/d2k/chrome/ingame-player.yaml b/mods/d2k/chrome/ingame-player.yaml index 19eba2a798e5..3cabea174ded 100644 --- a/mods/d2k/chrome/ingame-player.yaml +++ b/mods/d2k/chrome/ingame-player.yaml @@ -21,8 +21,8 @@ Container@PLAYER_WIDGETS: IconSize: 60, 48 IconSpriteOffset: -1, -1 TooltipContainer: TOOLTIP_CONTAINER - ReadyText: READY - HoldText: ON HOLD + ReadyText: supportpowers-support-powers-palette.ready + HoldText: supportpowers-support-powers-palette.hold HotkeyPrefix: SupportPower HotkeyCount: 6 Image@COMMAND_BAR_BACKGROUND: @@ -47,8 +47,8 @@ Container@PLAYER_WIDGETS: Background: command-button Key: AttackMove DisableKeySound: true - TooltipText: Attack Move - TooltipDesc: Selected units will move to the desired location\nand attack any enemies they encounter en route.\n\nHold <(Ctrl)> while targeting to order an Assault Move\nthat attacks any units or structures encountered en route.\n\nLeft-click icon then right-click on target location. + TooltipText: button-command-bar-attack-move.tooltip + TooltipDesc: button-command-bar-attack-move.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -64,8 +64,8 @@ Container@PLAYER_WIDGETS: VisualHeight: 0 Background: command-button DisableKeySound: true - TooltipText: Force Move - TooltipDesc: Selected units will move to the desired location\n - Default activity for the target is suppressed\n - Vehicles will attempt to crush enemies at the target location\n - Deployed thumpers will undeploy and move to the target location\n\nLeft-click icon then right-click on target.\nHold <(Alt)> to activate temporarily while commanding units. + TooltipText: button-command-bar-force-move.tooltip + TooltipDesc: button-command-bar-force-move.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -81,8 +81,8 @@ Container@PLAYER_WIDGETS: VisualHeight: 0 Background: command-button DisableKeySound: true - TooltipText: Force Attack - TooltipDesc: Selected units will attack the targeted unit or location\n - Default activity for the target is suppressed\n - Allows targeting of own or ally forces\n\nLeft-click icon then right-click on target.\nHold <(Ctrl)> to activate temporarily while commanding units. + TooltipText: button-command-bar-force-attack.tooltip + TooltipDesc: button-command-bar-force-attack.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -99,8 +99,8 @@ Container@PLAYER_WIDGETS: Background: command-button Key: Guard DisableKeySound: true - TooltipText: Guard - TooltipDesc: Selected units will follow the targeted unit.\n\nLeft-click icon then right-click on target unit. + TooltipText: button-command-bar-guard.tooltip + TooltipDesc: button-command-bar-guard.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -117,8 +117,8 @@ Container@PLAYER_WIDGETS: Key: Deploy DisableKeyRepeat: true DisableKeySound: true - TooltipText: Deploy - TooltipDesc: Selected units will perform their default deploy activity\n - MCVs will unpack into a Construction Yard\n - Thumpers will start or stop attracting worms\n - Devastators will become immobilized and explode\n\nActs immediately on selected units. + TooltipText: button-command-bar-deploy.tooltip + TooltipDesc: button-command-bar-deploy.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -135,8 +135,8 @@ Container@PLAYER_WIDGETS: Key: Scatter DisableKeyRepeat: true DisableKeySound: true - TooltipText: Scatter - TooltipDesc: Selected units will stop their current activity\nand move to a nearby location.\n\nActs immediately on selected units. + TooltipText: button-command-bar-scatter.tooltip + TooltipDesc: button-command-bar-scatter.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -153,8 +153,8 @@ Container@PLAYER_WIDGETS: Key: Stop DisableKeyRepeat: true DisableKeySound: true - TooltipText: Stop - TooltipDesc: Selected units will stop their current activity.\nSelected buildings will reset their rally point.\n\nActs immediately on selected targets. + TooltipText: button-command-bar-stop.tooltip + TooltipDesc: button-command-bar-stop.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -169,8 +169,8 @@ Container@PLAYER_WIDGETS: VisualHeight: 0 Background: command-button DisableKeySound: true - TooltipText: Waypoint Mode - TooltipDesc: Use Waypoint Mode to give multiple linking commands\nto the selected units. Units will execute the commands\nimmediately upon receiving them.\n\nLeft-click icon then give commands in the game world.\nHold <(Shift)> to activate temporarily while commanding units. + TooltipText: button-command-bar-queue-orders.tooltip + TooltipDesc: button-command-bar-queue-orders.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -194,8 +194,8 @@ Container@PLAYER_WIDGETS: Key: StanceAttackAnything DisableKeyRepeat: true DisableKeySound: true - TooltipText: Attack Anything Stance - TooltipDesc: Set the selected units to Attack Anything stance:\n - Units will attack enemy units and structures on sight\n - Units will pursue attackers across the battlefield + TooltipText: button-stance-bar-attackanything.tooltip + TooltipDesc: button-stance-bar-attackanything.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -212,8 +212,8 @@ Container@PLAYER_WIDGETS: Key: StanceDefend DisableKeyRepeat: true DisableKeySound: true - TooltipText: Defend Stance - TooltipDesc: Set the selected units to Defend stance:\n - Units will attack enemy units on sight\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-defend.tooltip + TooltipDesc: button-stance-bar-defend.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -230,8 +230,8 @@ Container@PLAYER_WIDGETS: Key: StanceReturnFire DisableKeyRepeat: true DisableKeySound: true - TooltipText: Return Fire Stance - TooltipDesc: Set the selected units to Return Fire stance:\n - Units will retaliate against enemies that attack them\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-returnfire.tooltip + TooltipDesc: button-stance-bar-returnfire.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -248,8 +248,8 @@ Container@PLAYER_WIDGETS: Key: StanceHoldFire DisableKeyRepeat: true DisableKeySound: true - TooltipText: Hold Fire Stance - TooltipDesc: Set the selected units to Hold Fire stance:\n - Units will not fire upon enemies\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-holdfire.tooltip + TooltipDesc: button-stance-bar-holdfire.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -275,7 +275,7 @@ Container@PLAYER_WIDGETS: Width: PARENT_RIGHT - 30 Height: 25 Align: Right - Text: Audio Muted + Text: label-mute-indicator Contrast: true Image@SIDEBAR_BACKGROUND_TOP: X: WINDOW_RIGHT - 226 @@ -298,7 +298,7 @@ Container@PLAYER_WIDGETS: Height: 35 Background: Key: Repair - TooltipText: Repair + TooltipText: button-top-buttons-repair-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -313,7 +313,7 @@ Container@PLAYER_WIDGETS: Height: 35 Background: Key: Sell - TooltipText: Sell + TooltipText: button-top-buttons-sell-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -328,7 +328,7 @@ Container@PLAYER_WIDGETS: Height: 35 Background: Key: PlaceBeacon - TooltipText: Place Beacon + TooltipText: button-top-buttons-beacon-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -343,7 +343,7 @@ Container@PLAYER_WIDGETS: Height: 35 Background: Key: PowerDown - TooltipText: Power Down + TooltipText: button-top-buttons-power-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -358,7 +358,7 @@ Container@PLAYER_WIDGETS: Width: 40 Height: 38 Background: - TooltipText: Options + TooltipText: button-top-buttons-options-tooltip TooltipContainer: TOOLTIP_CONTAINER DisableWorldSounds: true VisualHeight: 0 @@ -490,8 +490,8 @@ Container@PLAYER_WIDGETS: X: 39 Y: 1 TooltipContainer: TOOLTIP_CONTAINER - ReadyText: READY - HoldText: ON HOLD + ReadyText: productionpalette-sidebar-production-palette.ready + HoldText: productionpalette-sidebar-production-palette.hold IconSize: 58, 48 IconMargin: 2, 0 IconSpriteOffset: 0, 0 @@ -512,7 +512,7 @@ Container@PLAYER_WIDGETS: Height: 25 VisualHeight: 0 Background: sidebar-button - TooltipText: Buildings + TooltipText: button-production-types-building-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Building Key: ProductionTypeBuilding @@ -527,7 +527,7 @@ Container@PLAYER_WIDGETS: Height: 25 VisualHeight: 0 Background: sidebar-button - TooltipText: Infantry + TooltipText: button-production-types-infantry-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Infantry Key: ProductionTypeInfantry @@ -542,7 +542,7 @@ Container@PLAYER_WIDGETS: Height: 25 VisualHeight: 0 Background: sidebar-button - TooltipText: Light Vehicles + TooltipText: button-production-types-vehicle-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Vehicle Key: ProductionTypeVehicle @@ -557,7 +557,7 @@ Container@PLAYER_WIDGETS: Height: 25 VisualHeight: 0 Background: sidebar-button - TooltipText: Heavy Vehicles + TooltipText: button-production-types-tanks-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Armor Key: ProductionTypeTank @@ -572,7 +572,7 @@ Container@PLAYER_WIDGETS: Height: 25 VisualHeight: 0 Background: sidebar-button - TooltipText: Aircraft + TooltipText: button-production-types-aircraft-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Aircraft Key: ProductionTypeAircraft @@ -587,7 +587,7 @@ Container@PLAYER_WIDGETS: Height: 25 VisualHeight: 0 Background: sidebar-button - TooltipText: Starport + TooltipText: button-production-types-starport-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Starport Key: ProductionTypeMerchant @@ -602,7 +602,7 @@ Container@PLAYER_WIDGETS: Height: 25 VisualHeight: 0 Background: sidebar-button - TooltipText: Upgrades + TooltipText: button-production-types-upgrade-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Upgrade Key: ProductionTypeUpgrade @@ -617,7 +617,7 @@ Container@PLAYER_WIDGETS: Height: 25 VisualHeight: 0 Background: sidebar-button - TooltipText: Scroll up + TooltipText: button-production-types-scroll-up-tooltip TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -631,7 +631,7 @@ Container@PLAYER_WIDGETS: Height: 25 VisualHeight: 0 Background: sidebar-button - TooltipText: Scroll down + TooltipText: button-production-types-scroll-down-tooltip TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -644,3 +644,4 @@ Container@PLAYER_WIDGETS: X: WINDOW_RIGHT - WIDTH - 231 Y: 40 Width: 175 + diff --git a/mods/d2k/chrome/lobby-players.yaml b/mods/d2k/chrome/lobby-players.yaml index 8e970caa579c..ebeef128edf6 100644 --- a/mods/d2k/chrome/lobby-players.yaml +++ b/mods/d2k/chrome/lobby-players.yaml @@ -11,49 +11,49 @@ Container@LOBBY_PLAYER_BIN: Label@LABEL_LOBBY_NAME: Width: 180 Height: 25 - Text: Name + Text: label-container-lobby-name Align: Center Font: Bold Label@LABEL_LOBBY_COLOR: X: 190 Width: 70 Height: 25 - Text: Color + Text: label-container-lobby-color Align: Center Font: Bold Label@LABEL_LOBBY_FACTION: X: 270 Width: 140 Height: 25 - Text: Faction + Text: label-container-lobby-faction Align: Center Font: Bold Label@LABEL_LOBBY_TEAM: X: 420 Width: 48 Height: 25 - Text: Team + Text: label-container-lobby-team Align: Center Font: Bold Label@LABEL_LOBBY_HANDICAP: X: 478 Width: 72 Height: 25 - Text: Handicap + Text: label-container-lobby-handicap Align: Center Font: Bold Label@LABEL_LOBBY_SPAWN: X: 560 Width: 48 Height: 25 - Text: Spawn + Text: label-container-lobby-spawn Align: Center Font: Bold Label@LABEL_LOBBY_STATUS: X: 617 Width: 20 Height: 25 - Text: Ready + Text: label-container-lobby-status Align: Left Font: Bold ScrollPanel@LOBBY_PLAYERS: @@ -109,7 +109,7 @@ Container@LOBBY_PLAYER_BIN: X: 15 Width: 165 Height: 25 - Text: Name + Text: dropdownbutton-template-editable-player-slot-options Font: Regular Visible: false DropDownButton@COLOR: @@ -140,23 +140,23 @@ Container@LOBBY_PLAYER_BIN: X: 34 Width: 80 Height: 25 - Text: Faction + Text: label-template-editable-player-factionname DropDownButton@TEAM_DROPDOWN: X: 420 Width: 48 Height: 25 - Text: Team + Text: dropdownbutton-template-editable-player-team-dropdown DropDownButton@HANDICAP_DROPDOWN: X: 478 Width: 72 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - TooltipText: A handicap decreases the combat effectiveness of the player's forces + TooltipText: dropdownbutton-template-editable-player-handicap-dropdown-tooltip DropDownButton@SPAWN_DROPDOWN: X: 560 Width: 48 Height: 25 - Text: Spawn + Text: dropdownbutton-template-editable-player-spawn-dropdown Checkbox@STATUS_CHECKBOX: X: 617 Y: 2 @@ -209,7 +209,7 @@ Container@LOBBY_PLAYER_BIN: X: 39 Width: 146 Height: 25 - Text: Name + Text: label-template-noneditable-player-name DropDownButton@PLAYER_ACTION: X: 15 Width: 165 @@ -250,13 +250,13 @@ Container@LOBBY_PLAYER_BIN: X: 34 Width: 80 Height: 25 - Text: Faction + Text: label-faction-factionname Label@TEAM: X: 420 Width: 23 Height: 25 Align: Center - Text: Team + Text: label-template-noneditable-player-team DropDownButton@TEAM_DROPDOWN: X: 420 Width: 48 @@ -272,7 +272,7 @@ Container@LOBBY_PLAYER_BIN: Width: 72 Height: 25 TooltipContainer: TOOLTIP_CONTAINER - TooltipText: A handicap decreases the combat effectiveness of the player's forces + TooltipText: dropdownbutton-template-noneditable-player-handicap-dropdown-tooltip Label@SPAWN: X: 560 Width: 23 @@ -301,18 +301,18 @@ Container@LOBBY_PLAYER_BIN: X: 20 Width: 165 Height: 25 - Text: Name + Text: label-template-empty-name DropDownButton@SLOT_OPTIONS: X: 15 Width: 165 Height: 25 - Text: Name + Text: dropdownbutton-template-empty-slot-options Visible: false Button@JOIN: X: 190 Width: 418 Height: 25 - Text: Play in this slot + Text: button-template-empty-join Container@TEMPLATE_EDITABLE_SPECTATOR: X: 5 Width: 475 @@ -359,7 +359,7 @@ Container@LOBBY_PLAYER_BIN: X: 190 Width: 418 Height: 25 - Text: Spectator + Text: label-template-editable-spectator Align: Center Font: Bold Checkbox@STATUS_CHECKBOX: @@ -414,7 +414,7 @@ Container@LOBBY_PLAYER_BIN: X: 39 Width: 160 Height: 25 - Text: Name + Text: label-template-noneditable-spectator-name DropDownButton@PLAYER_ACTION: X: 15 Width: 165 @@ -440,7 +440,7 @@ Container@LOBBY_PLAYER_BIN: X: 190 Width: 418 Height: 25 - Text: Spectator + Text: label-template-noneditable-spectator Align: Center Font: Bold Image@STATUS_IMAGE: @@ -461,13 +461,13 @@ Container@LOBBY_PLAYER_BIN: X: 15 Width: 165 Height: 20 - Text: Allow Spectators? + Text: checkbox-template-new-spectator-toggle-spectators Font: Regular Button@SPECTATE: X: 190 Width: 418 Height: 25 - Text: Spectate + Text: button-template-new-spectator-spectate Font: Regular ScrollPanel@FACTION_DROPDOWN_TEMPLATE: @@ -503,3 +503,4 @@ ScrollPanel@FACTION_DROPDOWN_TEMPLATE: X: 30 Width: 80 Height: 25 + diff --git a/mods/d2k/chrome/mainmenu.yaml b/mods/d2k/chrome/mainmenu.yaml index 3fca20cf24e4..ebcc9c130166 100644 --- a/mods/d2k/chrome/mainmenu.yaml +++ b/mods/d2k/chrome/mainmenu.yaml @@ -30,7 +30,7 @@ Container@MAINMENU: Y: 21 Width: 200 Height: 30 - Text: OpenRA + Text: label-main-menu-mainmenu-title Align: Center Font: Bold Button@SINGLEPLAYER_BUTTON: @@ -38,42 +38,42 @@ Container@MAINMENU: Y: 60 Width: 140 Height: 30 - Text: Singleplayer + Text: button-main-menu-singleplayer Font: Bold Button@MULTIPLAYER_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 100 Width: 140 Height: 30 - Text: Multiplayer + Text: button-main-menu-multiplayer Font: Bold Button@SETTINGS_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 140 Width: 140 Height: 30 - Text: Settings + Text: button-main-menu-settings Font: Bold Button@EXTRAS_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 180 Width: 140 Height: 30 - Text: Extras + Text: button-main-menu-extras Font: Bold Button@CONTENT_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 220 Width: 140 Height: 30 - Text: Manage Content + Text: button-main-menu-content Font: Bold Button@QUIT_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 260 Width: 140 Height: 30 - Text: Quit + Text: button-main-menu-quit Font: Bold Background@SINGLEPLAYER_MENU: Width: PARENT_RIGHT @@ -84,7 +84,7 @@ Container@MAINMENU: Y: 21 Width: 200 Height: 30 - Text: Singleplayer + Text: label-singleplayer-menu-title Align: Center Font: Bold Button@SKIRMISH_BUTTON: @@ -92,28 +92,28 @@ Container@MAINMENU: Y: 60 Width: 140 Height: 30 - Text: Skirmish + Text: button-singleplayer-menu-skirmish Font: Bold Button@MISSIONS_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 100 Width: 140 Height: 30 - Text: Missions + Text: button-singleplayer-menu-missions Font: Bold Button@LOAD_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 140 Width: 140 Height: 30 - Text: Load + Text: button-singleplayer-menu-load Font: Bold Button@ENCYCLOPEDIA_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 180 Width: 140 Height: 30 - Text: Mentat + Text: button-singleplayer-menu-encyclopedia Font: Bold Button@BACK_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 @@ -121,7 +121,7 @@ Container@MAINMENU: Y: 260 Width: 140 Height: 30 - Text: Back + Text: button-singleplayer-menu-back Font: Bold Background@EXTRAS_MENU: Width: PARENT_RIGHT @@ -132,7 +132,7 @@ Container@MAINMENU: Y: 21 Width: 200 Height: 30 - Text: Extras + Text: label-extras-menu-title Align: Center Font: Bold Button@REPLAYS_BUTTON: @@ -140,35 +140,35 @@ Container@MAINMENU: Y: 60 Width: 140 Height: 30 - Text: Replays + Text: button-extras-menu-replays Font: Bold Button@MUSIC_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 100 Width: 140 Height: 30 - Text: Music + Text: button-extras-menu-music Font: Bold Button@MAP_EDITOR_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 140 Width: 140 Height: 30 - Text: Map Editor + Text: button-extras-menu-map-editor Font: Bold Button@ASSETBROWSER_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 180 Width: 140 Height: 30 - Text: Asset Browser + Text: button-extras-menu-assetbrowser Font: Bold Button@CREDITS_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 220 Width: 140 Height: 30 - Text: Credits + Text: button-extras-menu-credits Font: Bold Button@BACK_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 @@ -176,7 +176,7 @@ Container@MAINMENU: Y: 260 Width: 140 Height: 30 - Text: Back + Text: button-extras-menu-back Font: Bold Background@MAP_EDITOR_MENU: Width: PARENT_RIGHT @@ -187,7 +187,7 @@ Container@MAINMENU: Y: 21 Width: 200 Height: 30 - Text: Map Editor + Text: label-map-editor-menu-title Align: Center Font: Bold Button@NEW_MAP_BUTTON: @@ -195,14 +195,14 @@ Container@MAINMENU: Y: 60 Width: 140 Height: 30 - Text: New Map + Text: button-map-editor-menu-new Font: Bold Button@LOAD_MAP_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 100 Width: 140 Height: 30 - Text: Load Map + Text: button-map-editor-menu-load Font: Bold Button@BACK_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 @@ -210,7 +210,7 @@ Container@MAINMENU: Y: 260 Width: 140 Height: 30 - Text: Back + Text: button-map-editor-menu-back Font: Bold Background@NEWS_BG: X: (WINDOW_RIGHT - WIDTH) / 2 @@ -223,7 +223,7 @@ Container@MAINMENU: Y: 15 Width: 400 Height: 25 - Text: Battlefield News + Text: dropdownbutton-news-bg-button Font: Bold Container@UPDATE_NOTICE: X: (WINDOW_RIGHT - WIDTH) / 2 @@ -235,14 +235,14 @@ Container@MAINMENU: Height: 25 Align: Center Shadow: true - Text: You are running an outdated version of OpenRA. + Text: label-update-notice-a Label@B: Y: 20 Width: PARENT_RIGHT Height: 25 Align: Center Shadow: true - Text: Download the latest version from www.openra.net + Text: label-update-notice-b Container@PERFORMANCE_INFO: Logic: PerfDebugLogic Children: @@ -268,3 +268,4 @@ Container@MAINMENU: Container@PLAYER_PROFILE_CONTAINER: X: 5 Y: 5 + diff --git a/mods/d2k/chrome/missionbrowser.yaml b/mods/d2k/chrome/missionbrowser.yaml index 48cda4d09ea8..9c668373d9c7 100644 --- a/mods/d2k/chrome/missionbrowser.yaml +++ b/mods/d2k/chrome/missionbrowser.yaml @@ -2,21 +2,21 @@ Background@MISSIONBROWSER_PANEL: Logic: MissionBrowserLogic X: (WINDOW_RIGHT - WIDTH) / 2 Y: (WINDOW_BOTTOM - HEIGHT) / 2 - Width: 880 + Width: 682 Height: 591 Children: Label@MISSIONBROWSER_TITLE: Y: 20 Width: PARENT_RIGHT Height: 25 - Text: Missions + Text: label-missionbrowser-panel-title Align: Center Font: Bold ScrollPanel@MISSION_LIST: X: 20 Y: 50 Width: 190 - Height: 405 + Height: 483 Children: ScrollItem@HEADER: Background: scrollheader @@ -42,41 +42,15 @@ Background@MISSIONBROWSER_PANEL: Height: 25 TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP - Label@DIFFICULTY_DESC: - X: 210 - WIDTH - 125 - Y: 468 - Width: 56 - Height: 25 - Text: Difficulty: - Align: Right - DropDownButton@DIFFICULTY_DROPDOWNBUTTON: - X: 210 - WIDTH - Y: 468 - Width: 120 - Height: 25 - Font: Regular - Label@GAMESPEED_DESC: - X: 210 - WIDTH - 125 - Y: 508 - Width: 120 - Height: 25 - Text: Speed: - Align: Right - DropDownButton@GAMESPEED_DROPDOWNBUTTON: - X: 210 - WIDTH - Y: 508 - Width: 120 - Height: 25 - Font: Regular Container@MISSION_INFO: X: 220 Y: 50 - Width: 642 - Height: 800 + Width: PARENT_RIGHT - 240 + Height: 483 Children: Background@MISSION_BG: Width: PARENT_RIGHT - Height: 327 + Height: 225 Background: dialog3 Children: MapPreview@MISSION_PREVIEW: @@ -87,62 +61,134 @@ Background@MISSIONBROWSER_PANEL: IgnoreMouseOver: True IgnoreMouseInput: True ShowSpawnPoints: False - ScrollPanel@MISSION_DESCRIPTION_PANEL: - Y: 337 + Container@MISSION_TABS: Width: PARENT_RIGHT - Height: 146 + Y: PARENT_BOTTOM - 31 Children: - Label@MISSION_DESCRIPTION: - X: 4 - Y: 1 - Width: PARENT_RIGHT - 32 - VAlign: Top - Font: Small + Button@MISSIONINFO_TAB: + Width: PARENT_RIGHT / 2 + Height: 31 + Font: Bold + Text: button-missionbrowser-panel-mission-info + Button@OPTIONS_TAB: + X: PARENT_RIGHT / 2 + Width: PARENT_RIGHT / 2 + Height: 31 + Font: Bold + Text: button-missionbrowser-panel-mission-options + Container@MISSION_DETAIL: + Y: 235 + Width: PARENT_RIGHT + Height: PARENT_BOTTOM - 235 + Children: + ScrollPanel@MISSION_DESCRIPTION_PANEL: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM - 30 + TopBottomSpacing: 5 + Children: + Label@MISSION_DESCRIPTION: + X: 4 + Width: PARENT_RIGHT - 32 + VAlign: Top + Font: Small + ScrollPanel@MISSION_OPTIONS: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM - 30 + TopBottomSpacing: 5 + Children: + Container@CHECKBOX_ROW_TEMPLATE: + Width: PARENT_RIGHT + Height: 30 + Children: + Checkbox@A: + X: 10 + Width: PARENT_RIGHT / 2 - 25 + Height: 20 + Visible: False + TooltipContainer: TOOLTIP_CONTAINER + Checkbox@B: + X: PARENT_RIGHT / 2 + 5 + Width: PARENT_RIGHT / 2 - 25 + Height: 20 + Visible: False + TooltipContainer: TOOLTIP_CONTAINER + Container@DROPDOWN_ROW_TEMPLATE: + Height: 60 + Width: PARENT_RIGHT + Children: + LabelForInput@A_DESC: + X: 10 + Width: PARENT_RIGHT / 2 - 35 + Height: 20 + Visible: False + For: A + DropDownButton@A: + X: 10 + Width: PARENT_RIGHT / 2 - 35 + Y: 25 + Height: 25 + Visible: False + PanelRoot: MISSION_DROPDOWN_PANEL_ROOT + TooltipContainer: TOOLTIP_CONTAINER + LabelForInput@B_DESC: + X: PARENT_RIGHT / 2 + 5 + Width: PARENT_RIGHT / 2 - 35 + Height: 20 + Visible: False + For: B + DropDownButton@B: + X: PARENT_RIGHT / 2 + 5 + Width: PARENT_RIGHT / 2 - 35 + Y: 25 + Height: 25 + Visible: False + PanelRoot: MISSION_DROPDOWN_PANEL_ROOT + TooltipContainer: TOOLTIP_CONTAINER Button@START_BRIEFING_VIDEO_BUTTON: - X: 220 + X: 20 Y: PARENT_BOTTOM - 45 Width: 130 Height: 25 - Text: Watch Briefing + Text: button-missionbrowser-panel-start-briefing-video Font: Bold Button@STOP_BRIEFING_VIDEO_BUTTON: - X: 220 + X: 20 Y: PARENT_BOTTOM - 45 Width: 130 Height: 25 - Text: Stop Briefing + Text: button-missionbrowser-panel-stop-briefing-video Font: Bold Button@START_INFO_VIDEO_BUTTON: - X: 360 + X: 160 Y: PARENT_BOTTOM - 45 Width: 130 Height: 25 - Text: Watch Info Video + Text: button-missionbrowser-panel-start-info-video Font: Bold Button@STOP_INFO_VIDEO_BUTTON: - X: 360 + X: 160 Y: PARENT_BOTTOM - 45 Width: 130 Height: 25 - Text: Stop Info Video + Text: button-missionbrowser-panel-stop-info-video Font: Bold Button@STARTGAME_BUTTON: X: PARENT_RIGHT - 140 - 130 Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Play + Text: button-missionbrowser-panel-startgame Font: Bold Button@BACK_BUTTON: X: PARENT_RIGHT - 140 Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: Back + Text: button-missionbrowser-panel-back Font: Bold Key: escape Background@MISSION_BIN: - X: 220 + X: 20 Y: 50 Width: 642 Height: 483 @@ -155,6 +201,7 @@ Background@MISSIONBROWSER_PANEL: Height: 480 AspectRatio: 1.2 DrawOverlay: False + Container@MISSION_DROPDOWN_PANEL_ROOT: TooltipContainer@TOOLTIP_CONTAINER: Background@FULLSCREEN_PLAYER: @@ -168,3 +215,4 @@ Background@FULLSCREEN_PLAYER: Y: 0 Width: WINDOW_RIGHT Height: WINDOW_BOTTOM + diff --git a/mods/d2k/chrome/multiplayer-browserpanels.yaml b/mods/d2k/chrome/multiplayer-browserpanels.yaml index d2c91522241c..e8039add35fd 100644 --- a/mods/d2k/chrome/multiplayer-browserpanels.yaml +++ b/mods/d2k/chrome/multiplayer-browserpanels.yaml @@ -48,7 +48,7 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 5 Width: PARENT_RIGHT - 29 Height: 20 - Text: Waiting + Text: checkbox-multiplayer-filter-panel-waiting-for-players TextColor: 32CD32 Font: Regular Checkbox@EMPTY: @@ -56,14 +56,14 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 30 Width: PARENT_RIGHT - 29 Height: 20 - Text: Empty + Text: checkbox-multiplayer-filter-panel-empty Font: Regular Checkbox@PASSWORD_PROTECTED: X: 5 Y: 55 Width: PARENT_RIGHT - 29 Height: 20 - Text: Protected + Text: checkbox-multiplayer-filter-panel-password-protected TextColor: FF0000 Font: Regular Checkbox@ALREADY_STARTED: @@ -71,7 +71,7 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 80 Width: PARENT_RIGHT - 29 Height: 20 - Text: Started + Text: checkbox-multiplayer-filter-panel-already-started TextColor: FFA500 Font: Regular Checkbox@INCOMPATIBLE_VERSION: @@ -79,6 +79,7 @@ ScrollPanel@MULTIPLAYER_FILTER_PANEL: Y: 105 Width: PARENT_RIGHT - 29 Height: 20 - Text: Incompatible + Text: checkbox-multiplayer-filter-panel-incompatible-version TextColor: BEBEBE Font: Regular + diff --git a/mods/d2k/chrome/tooltips.yaml b/mods/d2k/chrome/tooltips.yaml index 23b5b076b191..b36ac1aee1b2 100644 --- a/mods/d2k/chrome/tooltips.yaml +++ b/mods/d2k/chrome/tooltips.yaml @@ -124,7 +124,7 @@ Background@LATENCY_TOOLTIP: Y: 4 Height: 23 Font: Bold - Text: Latency: + Text: label-latency-tooltip-prefix Label@LATENCY: Y: 4 Height: 23 @@ -139,7 +139,7 @@ Background@ANONYMOUS_PLAYER_TOOLTIP: Label@NAME: X: 7 Y: 3 - Text: Anonymous Player + Text: label-anonymous-player-tooltip-name Height: 24 Font: MediumBold Label@LOCATION: @@ -170,7 +170,7 @@ Background@ANONYMOUS_PLAYER_TOOLTIP: Label@LABEL: X: 10 Height: 12 - Text: Game Admin + Text: label-game-admin Font: TinyBold Background@REGISTERED_PLAYER_TOOLTIP: @@ -213,7 +213,7 @@ Background@REGISTERED_PLAYER_TOOLTIP: X: 10 Y: 1 Height: 12 - Text: Game Admin + Text: label-game-admin Font: TinyBold Container@MESSAGE_HEADER: Height: 31 @@ -320,6 +320,12 @@ Background@SUPPORT_POWER_TOOLTIP: Y: 30 Font: TinyBold VAlign: Top + Label@COST: + X: 5 + Y: 9 + Font: TinyBold + VAlign: Top + Text: $ Background@ARMY_TOOLTIP: Logic: ArmyTooltipLogic @@ -338,3 +344,4 @@ Background@ARMY_TOOLTIP: Height: 3 Font: TinyBold VAlign: Top + diff --git a/mods/d2k/languages/chrome/en.ftl b/mods/d2k/languages/chrome/en.ftl new file mode 100644 index 000000000000..5fbcd9b35622 --- /dev/null +++ b/mods/d2k/languages/chrome/en.ftl @@ -0,0 +1,266 @@ +## encyclopedia.yaml +label-encyclopedia-content-title = Mentat +button-encyclopedia-panel-back = Back + +## ingame-infostats.yaml +label-objective-mission = Mission: +checkbox-objective-stats = Destroy all opposition! +label-stats-name = Player +label-stats-faction = Faction +label-stats-score = Score +label-stats-actions = Actions +button-player-template-kick-tooltip = Kick this player +button-spectator-template-kick-tooltip = Kick this player + +## ingame-menu.yaml +label-menu-buttons-title = Options + +## ingame-observer.yaml +button-observer-widgets-options = Options (Esc) +button-replay-player-pause-tooltip = Pause +button-replay-player-play-tooltip = Play + +button-replay-player-slow = + .tooltip = Slow speed + .label = 50% + +button-replay-player-regular = + .tooltip = Regular speed + .label = 100% + +button-replay-player-fast = + .tooltip = Fast speed + .label = 200% + +button-replay-player-maximum = + .tooltip = Maximum speed + .label = MAX + +label-basic-stats-player-header = Player +label-basic-stats-cash-header = Cash +label-basic-stats-power-header = Power +label-basic-stats-kills-header = Kills +label-basic-stats-deaths-header = Deaths +label-basic-stats-assets-destroyed-header = Destroyed +label-basic-stats-assets-lost-header = Lost +label-basic-stats-experience-header = Score +label-basic-stats-actions-min-header = APM +label-economy-stats-player-header = Player +label-economy-stats-cash-header = Cash +label-economy-stats-income-header = Income +label-economy-stats-assets-header = Assets +label-economy-stats-earned-header = Earned +label-economy-stats-spent-header = Spent +label-economy-stats-harvesters-header = Harvesters +label-economy-stats-carryalls-header = Carryalls +label-production-stats-player-header = Player +label-production-stats-header = Production +label-support-powers-player-header = Player +label-support-powers-header = Support Powers +label-army-player-header = Player +label-army-header = Army +label-combat-stats-player-header = Player +label-combat-stats-assets-destroyed-header = Destroyed +label-combat-stats-assets-lost-header = Lost +label-combat-stats-units-killed-header = U. Killed +label-combat-stats-units-dead-header = U. Lost +label-combat-stats-buildings-killed-header = B. Killed +label-combat-stats-buildings-dead-header = B. Lost +label-combat-stats-army-value-header = Army Value +label-combat-stats-vision-header = Vision + +## ingame-observer.yaml, ingame-player.yaml +label-mute-indicator = Audio Muted + +## ingame-player.yaml +supportpowers-support-powers-palette = + .ready = READY + .hold = ON HOLD + +button-command-bar-attack-move = + .tooltip = Attack Move + .tooltipdesc = Selected units will move to the desired location + and attack any enemies they encounter en route. + + Hold <(Ctrl)> while targeting to order an Assault Move + that attacks any units or structures encountered en route. + + Left-click icon then right-click on target location. + +button-command-bar-force-move = + .tooltip = Force Move + .tooltipdesc = Selected units will move to the desired location + - Default activity for the target is suppressed + - Vehicles will attempt to crush enemies at the target location + - Deployed thumpers will undeploy and move to the target location + + Left-click icon then right-click on target. + Hold <(Alt)> to activate temporarily while commanding units. + +button-command-bar-force-attack = + .tooltip = Force Attack + .tooltipdesc = Selected units will attack the targeted unit or location + - Default activity for the target is suppressed + - Allows targeting of own or ally forces + + Left-click icon then right-click on target. + Hold <(Ctrl)> to activate temporarily while commanding units. + +button-command-bar-guard = + .tooltip = Guard + .tooltipdesc = Selected units will follow the targeted unit. + + Left-click icon then right-click on target unit. + +button-command-bar-deploy = + .tooltip = Deploy + .tooltipdesc = Selected units will perform their default deploy activity + - MCVs will unpack into a Construction Yard + - Thumpers will start or stop attracting worms + - Devastators will become immobilized and explode + + Acts immediately on selected units. + +button-command-bar-scatter = + .tooltip = Scatter + .tooltipdesc = Selected units will stop their current activity + and move to a nearby location. + + Acts immediately on selected units. + +button-command-bar-stop = + .tooltip = Stop + .tooltipdesc = Selected units will stop their current activity. + Selected buildings will reset their rally point. + + Acts immediately on selected targets. + +button-command-bar-queue-orders = + .tooltip = Waypoint Mode + .tooltipdesc = Use Waypoint Mode to give multiple linking commands + to the selected units. Units will execute the commands + immediately upon receiving them. + + Left-click icon then give commands in the game world. + Hold <(Shift)> to activate temporarily while commanding units. + +button-stance-bar-attackanything = + .tooltip = Attack Anything Stance + .tooltipdesc = Set the selected units to Attack Anything stance: + - Units will attack enemy units and structures on sight + - Units will pursue attackers across the battlefield + +button-stance-bar-defend = + .tooltip = Defend Stance + .tooltipdesc = Set the selected units to Defend stance: + - Units will attack enemy units on sight + - Units will not move or pursue enemies + +button-stance-bar-returnfire = + .tooltip = Return Fire Stance + .tooltipdesc = Set the selected units to Return Fire stance: + - Units will retaliate against enemies that attack them + - Units will not move or pursue enemies + +button-stance-bar-holdfire = + .tooltip = Hold Fire Stance + .tooltipdesc = Set the selected units to Hold Fire stance: + - Units will not fire upon enemies + - Units will not move or pursue enemies + +button-top-buttons-repair-tooltip = Repair +button-top-buttons-sell-tooltip = Sell +button-top-buttons-beacon-tooltip = Place Beacon +button-top-buttons-power-tooltip = Power Down +button-top-buttons-options-tooltip = Options + +productionpalette-sidebar-production-palette = + .ready = READY + .hold = ON HOLD + +button-production-types-building-tooltip = Buildings +button-production-types-infantry-tooltip = Infantry +button-production-types-vehicle-tooltip = Light Vehicles +button-production-types-tanks-tooltip = Heavy Vehicles +button-production-types-aircraft-tooltip = Aircraft +button-production-types-starport-tooltip = Starport +button-production-types-upgrade-tooltip = Upgrades +button-production-types-scroll-up-tooltip = Scroll up +button-production-types-scroll-down-tooltip = Scroll down + +## lobby-players.yaml +label-container-lobby-name = Name +label-container-lobby-color = Color +label-container-lobby-faction = Faction +label-container-lobby-team = Team +label-container-lobby-handicap = Handicap +label-container-lobby-spawn = Spawn +label-container-lobby-status = Ready +dropdownbutton-template-editable-player-slot-options = Name +label-template-editable-player-factionname = Faction +dropdownbutton-template-editable-player-team-dropdown = Team +dropdownbutton-template-editable-player-handicap-dropdown-tooltip = A handicap decreases the combat effectiveness of the player's forces +dropdownbutton-template-editable-player-spawn-dropdown = Spawn +label-template-noneditable-player-name = Name +label-faction-factionname = Faction +label-template-noneditable-player-team = Team +dropdownbutton-template-noneditable-player-handicap-dropdown-tooltip = A handicap decreases the combat effectiveness of the player's forces +label-template-empty-name = Name +dropdownbutton-template-empty-slot-options = Name +button-template-empty-join = Play in this slot +label-template-editable-spectator = Spectator +label-template-noneditable-spectator-name = Name +label-template-noneditable-spectator = Spectator +checkbox-template-new-spectator-toggle-spectators = Allow Spectators? +button-template-new-spectator-spectate = Spectate + +## mainmenu.yaml +label-main-menu-mainmenu-title = OpenRA +button-main-menu-singleplayer = Singleplayer +button-main-menu-multiplayer = Multiplayer +button-main-menu-settings = Settings +button-main-menu-extras = Extras +button-main-menu-content = Manage Content +button-main-menu-quit = Quit +label-singleplayer-menu-title = Singleplayer +button-singleplayer-menu-skirmish = Skirmish +button-singleplayer-menu-missions = Missions +button-singleplayer-menu-load = Load +button-singleplayer-menu-encyclopedia = Mentat +button-singleplayer-menu-back = Back +label-extras-menu-title = Extras +button-extras-menu-replays = Replays +button-extras-menu-music = Music +button-extras-menu-map-editor = Map Editor +button-extras-menu-assetbrowser = Asset Browser +button-extras-menu-credits = Credits +button-extras-menu-back = Back +label-map-editor-menu-title = Map Editor +button-map-editor-menu-new = New Map +button-map-editor-menu-load = Load Map +button-map-editor-menu-back = Back +dropdownbutton-news-bg-button = Battlefield News +label-update-notice-a = You are running an outdated version of OpenRA. +label-update-notice-b = Download the latest version from www.openra.net + +## missionbrowser.yaml +label-missionbrowser-panel-title = Missions +button-missionbrowser-panel-start-briefing-video = Watch Briefing +button-missionbrowser-panel-stop-briefing-video = Stop Briefing +button-missionbrowser-panel-start-info-video = Watch Info Video +button-missionbrowser-panel-stop-info-video = Stop Info Video +button-missionbrowser-panel-startgame = Play +button-missionbrowser-panel-back = Back + +## multiplayer-browserpanels.yaml +checkbox-multiplayer-filter-panel-waiting-for-players = Waiting +checkbox-multiplayer-filter-panel-empty = Empty +checkbox-multiplayer-filter-panel-password-protected = Protected +checkbox-multiplayer-filter-panel-already-started = Started +checkbox-multiplayer-filter-panel-incompatible-version = Incompatible + +## tooltips.yaml +label-latency-tooltip-prefix = Latency: +label-anonymous-player-tooltip-name = Anonymous Player +label-game-admin = Game Admin + diff --git a/mods/d2k/languages/difficulties/en.ftl b/mods/d2k/languages/difficulties/en.ftl index 90ad41139706..5521c17cd52e 100644 --- a/mods/d2k/languages/difficulties/en.ftl +++ b/mods/d2k/languages/difficulties/en.ftl @@ -4,4 +4,5 @@ dropdown-difficulty = options-difficulty = .easy = Easy + .normal = Normal .hard = Hard diff --git a/mods/d2k/languages/rules/en.ftl b/mods/d2k/languages/rules/en.ftl index 3550c79bacad..abd0975b90d8 100644 --- a/mods/d2k/languages/rules/en.ftl +++ b/mods/d2k/languages/rules/en.ftl @@ -31,9 +31,6 @@ options-starting-units = .light-support = Light Support .heavy-support = Heavy Support -options-difficulty = - .normal = Normal - ## Arrakis notification-worm-attack = Worm attack. notification-worm-sign = Worm sign. diff --git a/mods/d2k/maps/shellmap/d2k-shellmap.lua b/mods/d2k/maps/shellmap/d2k-shellmap.lua index 542d964a2e2d..8070bd3d38b3 100644 --- a/mods/d2k/maps/shellmap/d2k-shellmap.lua +++ b/mods/d2k/maps/shellmap/d2k-shellmap.lua @@ -64,25 +64,25 @@ CorCarryHarvWaypoints = { cor_harvcarry_2.Location, cor_harvcarry_1.Location } SmgCarryHarvWaypoints = { smg_harvcarry_2.Location, smg_harvcarry_1.Location } Produce = function(house, units) - if HoldProduction[house.Name] then - Trigger.AfterDelay(DateTime.Minutes(1), function() Produce(house, units) end) - return - end - - local delay = Utils.RandomInteger(AttackDelay[1], AttackDelay[2]) - local toBuild = { Utils.Random(units) } - house.Build(toBuild, function(unit) + if HoldProduction[house.Name] then + Trigger.AfterDelay(DateTime.Minutes(1), function() Produce(house, units) end) + return + end + + local delay = Utils.RandomInteger(AttackDelay[1], AttackDelay[2]) + local toBuild = { Utils.Random(units) } + house.Build(toBuild, function(unit) local unitCount = 1 if IdlingUnits[house.Name] then unitCount = 1 + #IdlingUnits[house.Name] end IdlingUnits[house.Name][unitCount] = unit[1] - Trigger.AfterDelay(delay, function() Produce(house, units) end) + Trigger.AfterDelay(delay, function() Produce(house, units) end) - if unitCount >= (AttackGroupSize[1] * 2) then - SendAttack(house) - end - end) + if unitCount >= (AttackGroupSize[1] * 2) then + SendAttack(house) + end + end) end SetupAttackGroup = function(house) diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index 21513d3c0309..e29d1421b051 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -125,7 +125,9 @@ ChromeLayout: Translations: common|languages/en.ftl + common|languages/chrome/en.ftl common|languages/rules/en.ftl + d2k|languages/chrome/en.ftl d2k|languages/rules/en.ftl Weapons: diff --git a/mods/d2k/rules/aircraft.yaml b/mods/d2k/rules/aircraft.yaml index fce48fd4dd1e..191f64539ce2 100644 --- a/mods/d2k/rules/aircraft.yaml +++ b/mods/d2k/rules/aircraft.yaml @@ -65,10 +65,12 @@ carryall: MinAirborneAltitude: 400 RevealsShroud@lifting_low: Range: 2c512 + MoveRecalculationThreshold: 0 Type: GroundPosition RequiresCondition: !airborne RevealsShroud@lifting_high: Range: 1c256 + MoveRecalculationThreshold: 0 Type: GroundPosition RequiresCondition: !cruising Buildable: diff --git a/mods/d2k/rules/palettes.yaml b/mods/d2k/rules/palettes.yaml index dc7e858c9e97..07d0182bb2f9 100644 --- a/mods/d2k/rules/palettes.yaml +++ b/mods/d2k/rules/palettes.yaml @@ -64,7 +64,8 @@ BasePalette: player Alpha: 0.68 Premultiply: false - FlashPaletteEffect: + MenuPostProcessEffect: + FlashPostProcessEffect: PaletteFromPlayerPaletteWithAlpha@cloak: BaseName: cloak BasePalette: player diff --git a/mods/d2k/rules/structures.yaml b/mods/d2k/rules/structures.yaml index caa61cca50e8..de513b00e8d3 100644 --- a/mods/d2k/rules/structures.yaml +++ b/mods/d2k/rules/structures.yaml @@ -659,6 +659,9 @@ starport: SpawnOffset: 0,-480,0 ExitCell: 0,2 ProductionAirdrop: + WaitTickBeforeProduce: 10 + WaitTickAfterProduce: 15 + LandOffset: 0, -256, 0 Produces: Starport ActorType: frigate ReadyTextNotification: notification-reinforcements-have-arrived diff --git a/mods/ra/bits/factpdox.shp b/mods/ra/bits/factpdox.shp deleted file mode 100644 index d07b69451956..000000000000 Binary files a/mods/ra/bits/factpdox.shp and /dev/null differ diff --git a/mods/ra/chrome/gamesave-loading.yaml b/mods/ra/chrome/gamesave-loading.yaml index 3345e9706ce5..675c2830a22f 100644 --- a/mods/ra/chrome/gamesave-loading.yaml +++ b/mods/ra/chrome/gamesave-loading.yaml @@ -20,7 +20,7 @@ Container@GAMESAVE_LOADING_SCREEN: Height: 25 Font: Bold Align: Center - Text: Loading Saved Game + Text: label-gamesave-loading-screen-title ProgressBar@PROGRESS: X: (WINDOW_RIGHT - 500) / 2 Y: 3 * WINDOW_BOTTOM / 4 @@ -34,4 +34,5 @@ Container@GAMESAVE_LOADING_SCREEN: Height: 25 Font: Regular Align: Center - Text: Press Escape to cancel loading and return to the main menu + Text: label-gamesave-loading-screen-desc + diff --git a/mods/ra/chrome/ingame-observer.yaml b/mods/ra/chrome/ingame-observer.yaml index 6f28966e8bdf..56e838ad4be2 100644 --- a/mods/ra/chrome/ingame-observer.yaml +++ b/mods/ra/chrome/ingame-observer.yaml @@ -20,7 +20,7 @@ Container@OBSERVER_WIDGETS: Width: PARENT_RIGHT - 30 Height: 25 Align: Right - Text: Audio Muted + Text: label-mute-indicator Contrast: true Image@SIDEBAR_BACKGROUND_TOP: X: WINDOW_RIGHT - 250 @@ -59,7 +59,7 @@ Container@OBSERVER_WIDGETS: Width: 28 Height: 28 Background: sidebar-button-observer - TooltipText: Options + TooltipText: button-top-buttons-options-tooltip TooltipContainer: TOOLTIP_CONTAINER DisableWorldSounds: true VisualHeight: 0 @@ -144,7 +144,7 @@ Container@OBSERVER_WIDGETS: Height: 28 Background: sidebar-button-observer Key: Pause - TooltipText: Pause + TooltipText: button-observer-widgets-pause-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -160,7 +160,7 @@ Container@OBSERVER_WIDGETS: Height: 28 Background: sidebar-button-observer Key: Pause - TooltipText: Play + TooltipText: button-observer-widgets-play-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -176,10 +176,10 @@ Container@OBSERVER_WIDGETS: Height: 22 Background: sidebar-button-observer Key: ReplaySpeedSlow - TooltipText: Slow speed + TooltipText: button-observer-widgets-slow.tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 - Text: 50% + Text: button-observer-widgets-slow.label Font: TinyBold Button@BUTTON_REGULAR: X: 95 @@ -188,10 +188,10 @@ Container@OBSERVER_WIDGETS: Height: 22 Background: sidebar-button-observer Key: ReplaySpeedRegular - TooltipText: Regular speed + TooltipText: button-observer-widgets-regular.tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 - Text: 100% + Text: button-observer-widgets-regular.label Font: TinyBold Button@BUTTON_FAST: X: 141 @@ -200,10 +200,10 @@ Container@OBSERVER_WIDGETS: Height: 22 Background: sidebar-button-observer Key: ReplaySpeedFast - TooltipText: Fast speed + TooltipText: button-observer-widgets-fast.tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 - Text: 200% + Text: button-observer-widgets-fast.label Font: TinyBold Button@BUTTON_MAXIMUM: X: 187 @@ -212,10 +212,10 @@ Container@OBSERVER_WIDGETS: Height: 22 Background: sidebar-button-observer Key: ReplaySpeedMax - TooltipText: Maximum speed + TooltipText: button-observer-widgets-maximum.tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 - Text: MAX + Text: button-observer-widgets-maximum.label Font: TinyBold Container@INGAME_OBSERVERSTATS_BG: Logic: ObserverStatsLogic @@ -275,7 +275,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-basic-stats-player-header Align: Left Shadow: True Label@CASH_HEADER: @@ -284,7 +284,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Cash + Text: label-basic-stats-cash-header Align: Right Shadow: True Label@POWER_HEADER: @@ -293,7 +293,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Power + Text: label-basic-stats-power-header Align: Center Shadow: True Label@KILLS_HEADER: @@ -302,7 +302,7 @@ Container@OBSERVER_WIDGETS: Width: 40 Height: PARENT_BOTTOM Font: Bold - Text: Kills + Text: label-basic-stats-kills-header Align: Right Shadow: True Label@DEATHS_HEADER: @@ -311,7 +311,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Deaths + Text: label-basic-stats-deaths-header Align: Right Shadow: True Label@ASSETS_DESTROYED_HEADER: @@ -320,7 +320,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Destroyed + Text: label-basic-stats-assets-destroyed-header Align: Right Shadow: True Label@ASSETS_LOST_HEADER: @@ -329,7 +329,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Lost + Text: label-basic-stats-assets-lost-header Align: Right Shadow: True Label@EXPERIENCE_HEADER: @@ -338,7 +338,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Score + Text: label-basic-stats-experience-header Align: Right Shadow: True Label@ACTIONS_MIN_HEADER: @@ -347,7 +347,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: APM + Text: label-basic-stats-actions-min-header Align: Right Shadow: True Container@ECONOMY_STATS_HEADERS: @@ -374,14 +374,14 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-economy-stats-player-header Shadow: True Label@CASH_HEADER: X: 160 Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Cash + Text: label-economy-stats-cash-header Align: Right Shadow: True Label@INCOME_HEADER: @@ -389,7 +389,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Income + Text: label-economy-stats-income-header Align: Right Shadow: True Label@ASSETS_HEADER: @@ -397,7 +397,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Assets + Text: label-economy-stats-assets-header Align: Right Shadow: True Label@EARNED_HEADER: @@ -405,7 +405,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Earned + Text: label-economy-stats-earned-header Align: Right Shadow: True Label@SPENT_HEADER: @@ -413,7 +413,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Spent + Text: label-economy-stats-spent-header Align: Right Shadow: True Label@HARVESTERS_HEADER: @@ -421,7 +421,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Harvesters + Text: label-economy-stats-harvesters-header Align: Right Shadow: True Label@DERRICKS_HEADER: @@ -429,7 +429,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Oil Derricks + Text: label-economy-stats-derricks-header Align: Right Shadow: True Container@PRODUCTION_STATS_HEADERS: @@ -457,7 +457,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-production-stats-player-header Align: Left Shadow: True Label@PRODUCTION_HEADER: @@ -466,7 +466,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Production + Text: label-production-stats-header Shadow: True Container@SUPPORT_POWERS_HEADERS: X: 0 @@ -493,7 +493,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-support-powers-player-header Align: Left Shadow: True Label@SUPPORT_POWERS_HEADER: @@ -502,7 +502,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Support Powers + Text: label-support-powers-header Shadow: True Container@ARMY_HEADERS: X: 0 @@ -529,7 +529,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-army-player-header Align: Left Shadow: True Label@ARMY_HEADER: @@ -538,7 +538,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Army + Text: label-army-header Shadow: True Container@COMBAT_STATS_HEADERS: X: 0 @@ -565,7 +565,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-combat-stats-player-header Align: Left Shadow: True Label@ASSETS_DESTROYED_HEADER: @@ -574,7 +574,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: Destroyed + Text: label-combat-stats-assets-destroyed-header Align: Right Shadow: True Label@ASSETS_LOST_HEADER: @@ -583,7 +583,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: Lost + Text: label-combat-stats-assets-lost-header Align: Right Shadow: True Label@UNITS_KILLED_HEADER: @@ -592,7 +592,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: U. Killed + Text: label-combat-stats-units-killed-header Align: Right Shadow: True Label@UNITS_DEAD_HEADER: @@ -601,7 +601,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: U. Lost + Text: label-combat-stats-units-dead-header Align: Right Shadow: True Label@BUILDINGS_KILLED_HEADER: @@ -610,7 +610,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: B. Killed + Text: label-combat-stats-buildings-killed-header Align: Right Shadow: True Label@BUILDINGS_DEAD_HEADER: @@ -619,7 +619,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: B. Lost + Text: label-combat-stats-buildings-dead-header Align: Right Shadow: True Label@ARMY_VALUE_HEADER: @@ -628,7 +628,7 @@ Container@OBSERVER_WIDGETS: Width: 90 Height: PARENT_BOTTOM Font: Bold - Text: Army Value + Text: label-combat-stats-army-value-header Align: Right Shadow: True Label@VISION_HEADER: @@ -637,7 +637,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Vision + Text: label-combat-stats-vision-header Align: Right Shadow: True ScrollPanel@PLAYER_STATS_PANEL: @@ -1101,3 +1101,4 @@ Container@OBSERVER_WIDGETS: X: WINDOW_RIGHT - WIDTH - 255 Y: 40 Width: 175 + diff --git a/mods/ra/chrome/ingame-player.yaml b/mods/ra/chrome/ingame-player.yaml index 42b2c8061f9f..7c24c003d5d5 100644 --- a/mods/ra/chrome/ingame-player.yaml +++ b/mods/ra/chrome/ingame-player.yaml @@ -21,8 +21,8 @@ Container@PLAYER_WIDGETS: IconSize: 62, 46 IconSpriteOffset: -1, -1 TooltipContainer: TOOLTIP_CONTAINER - ReadyText: READY - HoldText: ON HOLD + ReadyText: supportpowers-support-powers-palette.ready + HoldText: supportpowers-support-powers-palette.hold HotkeyPrefix: SupportPower HotkeyCount: 6 Container@PALETTE_FOREGROUND: @@ -63,8 +63,8 @@ Container@PLAYER_WIDGETS: Background: command-button Key: AttackMove DisableKeySound: true - TooltipText: Attack Move - TooltipDesc: Selected units will move to the desired location\nand attack any enemies they encounter en route.\n\nHold <(Ctrl)> while targeting to order an Assault Move\nthat attacks any units or structures encountered en route.\n\nLeft-click icon then right-click on target location. + TooltipText: button-command-bar-attack-move.tooltip + TooltipDesc: button-command-bar-attack-move.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -81,8 +81,8 @@ Container@PLAYER_WIDGETS: VisualHeight: 0 Background: command-button DisableKeySound: true - TooltipText: Force Move - TooltipDesc: Selected units will move to the desired location\n - Default activity for the target is suppressed\n - Vehicles will attempt to crush enemies at the target location\n - Helicopters will land at the target location\n - Chrono Tanks will teleport towards the target location\n\nLeft-click icon then right-click on target.\nHold <(Alt)> to activate temporarily while commanding units. + TooltipText: button-command-bar-force-move.tooltip + TooltipDesc: button-command-bar-force-move.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -99,8 +99,8 @@ Container@PLAYER_WIDGETS: VisualHeight: 0 Background: command-button DisableKeySound: true - TooltipText: Force Attack - TooltipDesc: Selected units will attack the targeted unit or location\n - Default activity for the target is suppressed\n - Allows targeting of own or ally forces\n - Long-range artillery units will always target the\n location, ignoring units and buildings\n\nLeft-click icon then right-click on target.\nHold <(Ctrl)> to activate temporarily while commanding units. + TooltipText: button-command-bar-force-attack.tooltip + TooltipDesc: button-command-bar-force-attack.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -118,8 +118,8 @@ Container@PLAYER_WIDGETS: Background: command-button Key: Guard DisableKeySound: true - TooltipText: Guard - TooltipDesc: Selected units will follow the targeted unit.\n\nLeft-click icon then right-click on target unit. + TooltipText: button-command-bar-guard.tooltip + TooltipDesc: button-command-bar-guard.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -137,8 +137,8 @@ Container@PLAYER_WIDGETS: Key: Deploy DisableKeyRepeat: true DisableKeySound: true - TooltipText: Deploy - TooltipDesc: Selected units will perform their default deploy activity\n - MCVs will unpack into a Construction Yard\n - Construction Yards will re-pack into a MCV\n - Transports will unload their passengers\n - Demolition Trucks and MAD Tanks will self-destruct\n - Minelayers will deploy a mine\n - Aircraft will return to base\n\nActs immediately on selected units. + TooltipText: button-command-bar-deploy.tooltip + TooltipDesc: button-command-bar-deploy.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -156,8 +156,8 @@ Container@PLAYER_WIDGETS: Key: Scatter DisableKeyRepeat: true DisableKeySound: true - TooltipText: Scatter - TooltipDesc: Selected units will stop their current activity\nand move to a nearby location.\n\nActs immediately on selected units. + TooltipText: button-command-bar-scatter.tooltip + TooltipDesc: button-command-bar-scatter.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -175,8 +175,8 @@ Container@PLAYER_WIDGETS: Key: Stop DisableKeyRepeat: true DisableKeySound: true - TooltipText: Stop - TooltipDesc: Selected units will stop their current activity.\nSelected buildings will reset their rally point.\n\nActs immediately on selected targets. + TooltipText: button-command-bar-stop.tooltip + TooltipDesc: button-command-bar-stop.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -192,8 +192,8 @@ Container@PLAYER_WIDGETS: VisualHeight: 0 Background: command-button DisableKeySound: true - TooltipText: Waypoint Mode - TooltipDesc: Use Waypoint Mode to give multiple linking commands\nto the selected units. Units will execute the commands\nimmediately upon receiving them.\n\nLeft-click icon then give commands in the game world.\nHold <(Shift)> to activate temporarily while commanding units. + TooltipText: button-command-bar-queue-orders.tooltip + TooltipDesc: button-command-bar-queue-orders.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -218,8 +218,8 @@ Container@PLAYER_WIDGETS: Key: StanceAttackAnything DisableKeyRepeat: true DisableKeySound: true - TooltipText: Attack Anything Stance - TooltipDesc: Set the selected units to Attack Anything stance:\n - Units will attack enemy units and structures on sight\n - Units will pursue attackers across the battlefield + TooltipText: button-stance-bar-attackanything.tooltip + TooltipDesc: button-stance-bar-attackanything.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -237,8 +237,8 @@ Container@PLAYER_WIDGETS: Key: StanceDefend DisableKeyRepeat: true DisableKeySound: true - TooltipText: Defend Stance - TooltipDesc: Set the selected units to Defend stance:\n - Units will attack enemy units on sight\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-defend.tooltip + TooltipDesc: button-stance-bar-defend.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -256,8 +256,8 @@ Container@PLAYER_WIDGETS: Key: StanceReturnFire DisableKeyRepeat: true DisableKeySound: true - TooltipText: Return Fire Stance - TooltipDesc: Set the selected units to Return Fire stance:\n - Units will retaliate against enemies that attack them\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-returnfire.tooltip + TooltipDesc: button-stance-bar-returnfire.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -275,8 +275,8 @@ Container@PLAYER_WIDGETS: Key: StanceHoldFire DisableKeyRepeat: true DisableKeySound: true - TooltipText: Hold Fire Stance - TooltipDesc: Set the selected units to Hold Fire stance:\n - Units will not fire upon enemies\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-holdfire.tooltip + TooltipDesc: button-stance-bar-holdfire.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -302,7 +302,7 @@ Container@PLAYER_WIDGETS: Width: PARENT_RIGHT - 30 Height: 25 Align: Right - Text: Audio Muted + Text: label-mute-indicator Contrast: true Image@SIDEBAR_BACKGROUND_TOP: Logic: AddFactionSuffixLogic @@ -325,7 +325,7 @@ Container@PLAYER_WIDGETS: Height: 28 Background: sidebar-button Key: PlaceBeacon - TooltipText: Place Beacon + TooltipText: button-top-buttons-beacon-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -340,7 +340,7 @@ Container@PLAYER_WIDGETS: Height: 28 Background: sidebar-button Key: Sell - TooltipText: Sell + TooltipText: button-top-buttons-sell-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -355,7 +355,7 @@ Container@PLAYER_WIDGETS: Height: 28 Background: sidebar-button Key: PowerDown - TooltipText: Power Down + TooltipText: button-top-buttons-power-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -370,7 +370,7 @@ Container@PLAYER_WIDGETS: Height: 28 Background: sidebar-button Key: Repair - TooltipText: Repair + TooltipText: button-top-buttons-repair-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -385,7 +385,7 @@ Container@PLAYER_WIDGETS: Height: 28 Background: sidebar-button Key: escape - TooltipText: Options + TooltipText: button-top-buttons-options-tooltip TooltipContainer: TOOLTIP_CONTAINER DisableWorldSounds: true VisualHeight: 0 @@ -450,8 +450,8 @@ Container@PLAYER_WIDGETS: X: 42 Y: 1 TooltipContainer: TOOLTIP_CONTAINER - ReadyText: READY - HoldText: ON HOLD + ReadyText: productionpalette-sidebar-production-palette.ready + HoldText: productionpalette-sidebar-production-palette.hold IconSize: 62, 46 IconMargin: 1, 1 IconSpriteOffset: -1, -1 @@ -479,7 +479,7 @@ Container@PLAYER_WIDGETS: Height: 28 VisualHeight: 0 Background: sidebar-button - TooltipText: Buildings + TooltipText: button-production-types-building-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Building Key: ProductionTypeBuilding @@ -495,7 +495,7 @@ Container@PLAYER_WIDGETS: Height: 28 VisualHeight: 0 Background: sidebar-button - TooltipText: Defense + TooltipText: button-production-types-defense-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Defense Key: ProductionTypeDefense @@ -511,7 +511,7 @@ Container@PLAYER_WIDGETS: Height: 28 VisualHeight: 0 Background: sidebar-button - TooltipText: Infantry + TooltipText: button-production-types-infantry-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Infantry Key: ProductionTypeInfantry @@ -527,7 +527,7 @@ Container@PLAYER_WIDGETS: Height: 28 VisualHeight: 0 Background: sidebar-button - TooltipText: Vehicles + TooltipText: button-production-types-vehicle-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Vehicle Key: ProductionTypeVehicle @@ -543,7 +543,7 @@ Container@PLAYER_WIDGETS: Height: 28 VisualHeight: 0 Background: sidebar-button - TooltipText: Aircraft + TooltipText: button-production-types-aircraft-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Aircraft Key: ProductionTypeAircraft @@ -559,7 +559,7 @@ Container@PLAYER_WIDGETS: Height: 28 VisualHeight: 0 Background: sidebar-button - TooltipText: Naval + TooltipText: button-production-types-naval-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Ship Key: ProductionTypeNaval @@ -575,7 +575,7 @@ Container@PLAYER_WIDGETS: Height: 22 VisualHeight: 0 Background: sidebar-button - TooltipText: Scroll up + TooltipText: button-production-types-scroll-up-tooltip TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -590,7 +590,7 @@ Container@PLAYER_WIDGETS: Height: 22 VisualHeight: 0 Background: sidebar-button - TooltipText: Scroll down + TooltipText: button-production-types-scroll-down-tooltip TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -652,3 +652,4 @@ Container@PLAYER_WIDGETS: X: WINDOW_RIGHT - WIDTH - 260 Y: 40 Width: 175 + diff --git a/mods/ra/languages/chrome/en.ftl b/mods/ra/languages/chrome/en.ftl new file mode 100644 index 000000000000..6820cff21fb9 --- /dev/null +++ b/mods/ra/languages/chrome/en.ftl @@ -0,0 +1,181 @@ +## gamesave-loading.yaml +label-gamesave-loading-screen-title = Loading Saved Game +label-gamesave-loading-screen-desc = Press Escape to cancel loading and return to the main menu + +## ingame-observer.yaml +button-observer-widgets-pause-tooltip = Pause +button-observer-widgets-play-tooltip = Play + +button-observer-widgets-slow = + .tooltip = Slow speed + .label = 50% + +button-observer-widgets-regular = + .tooltip = Regular speed + .label = 100% + +button-observer-widgets-fast = + .tooltip = Fast speed + .label = 200% + +button-observer-widgets-maximum = + .tooltip = Maximum speed + .label = MAX + +label-basic-stats-player-header = Player +label-basic-stats-cash-header = Cash +label-basic-stats-power-header = Power +label-basic-stats-kills-header = Kills +label-basic-stats-deaths-header = Deaths +label-basic-stats-assets-destroyed-header = Destroyed +label-basic-stats-assets-lost-header = Lost +label-basic-stats-experience-header = Score +label-basic-stats-actions-min-header = APM +label-economy-stats-player-header = Player +label-economy-stats-cash-header = Cash +label-economy-stats-income-header = Income +label-economy-stats-assets-header = Assets +label-economy-stats-earned-header = Earned +label-economy-stats-spent-header = Spent +label-economy-stats-harvesters-header = Harvesters +label-economy-stats-derricks-header = Oil Derricks +label-production-stats-player-header = Player +label-production-stats-header = Production +label-support-powers-player-header = Player +label-support-powers-header = Support Powers +label-army-player-header = Player +label-army-header = Army +label-combat-stats-player-header = Player +label-combat-stats-assets-destroyed-header = Destroyed +label-combat-stats-assets-lost-header = Lost +label-combat-stats-units-killed-header = U. Killed +label-combat-stats-units-dead-header = U. Lost +label-combat-stats-buildings-killed-header = B. Killed +label-combat-stats-buildings-dead-header = B. Lost +label-combat-stats-army-value-header = Army Value +label-combat-stats-vision-header = Vision + +## ingame-observer.yaml, ingame-player.yaml +label-mute-indicator = Audio Muted +button-top-buttons-options-tooltip = Options + +## ingame-player.yaml +supportpowers-support-powers-palette = + .ready = READY + .hold = ON HOLD + +button-command-bar-attack-move = + .tooltip = Attack Move + .tooltipdesc = Selected units will move to the desired location + and attack any enemies they encounter en route. + + Hold <(Ctrl)> while targeting to order an Assault Move + that attacks any units or structures encountered en route. + + Left-click icon then right-click on target location. + +button-command-bar-force-move = + .tooltip = Force Move + .tooltipdesc = Selected units will move to the desired location + - Default activity for the target is suppressed + - Vehicles will attempt to crush enemies at the target location + - Helicopters will land at the target location + - Chrono Tanks will teleport towards the target location + + Left-click icon then right-click on target. + Hold <(Alt)> to activate temporarily while commanding units. + +button-command-bar-force-attack = + .tooltip = Force Attack + .tooltipdesc = Selected units will attack the targeted unit or location + - Default activity for the target is suppressed + - Allows targeting of own or ally forces + - Long-range artillery units will always target the + location, ignoring units and buildings + + Left-click icon then right-click on target. + Hold <(Ctrl)> to activate temporarily while commanding units. + +button-command-bar-guard = + .tooltip = Guard + .tooltipdesc = Selected units will follow the targeted unit. + + Left-click icon then right-click on target unit. + +button-command-bar-deploy = + .tooltip = Deploy + .tooltipdesc = Selected units will perform their default deploy activity + - MCVs will unpack into a Construction Yard + - Construction Yards will re-pack into a MCV + - Transports will unload their passengers + - Demolition Trucks and MAD Tanks will self-destruct + - Minelayers will deploy a mine + - Aircraft will return to base + + Acts immediately on selected units. + +button-command-bar-scatter = + .tooltip = Scatter + .tooltipdesc = Selected units will stop their current activity + and move to a nearby location. + + Acts immediately on selected units. + +button-command-bar-stop = + .tooltip = Stop + .tooltipdesc = Selected units will stop their current activity. + Selected buildings will reset their rally point. + + Acts immediately on selected targets. + +button-command-bar-queue-orders = + .tooltip = Waypoint Mode + .tooltipdesc = Use Waypoint Mode to give multiple linking commands + to the selected units. Units will execute the commands + immediately upon receiving them. + + Left-click icon then give commands in the game world. + Hold <(Shift)> to activate temporarily while commanding units. + +button-stance-bar-attackanything = + .tooltip = Attack Anything Stance + .tooltipdesc = Set the selected units to Attack Anything stance: + - Units will attack enemy units and structures on sight + - Units will pursue attackers across the battlefield + +button-stance-bar-defend = + .tooltip = Defend Stance + .tooltipdesc = Set the selected units to Defend stance: + - Units will attack enemy units on sight + - Units will not move or pursue enemies + +button-stance-bar-returnfire = + .tooltip = Return Fire Stance + .tooltipdesc = Set the selected units to Return Fire stance: + - Units will retaliate against enemies that attack them + - Units will not move or pursue enemies + +button-stance-bar-holdfire = + .tooltip = Hold Fire Stance + .tooltipdesc = Set the selected units to Hold Fire stance: + - Units will not fire upon enemies + - Units will not move or pursue enemies + +button-top-buttons-beacon-tooltip = Place Beacon +button-top-buttons-sell-tooltip = Sell +button-top-buttons-power-tooltip = Power Down +button-top-buttons-repair-tooltip = Repair + +productionpalette-sidebar-production-palette = + .ready = READY + .hold = ON HOLD + +button-production-types-building-tooltip = Buildings +button-production-types-defense-tooltip = Defense +button-production-types-infantry-tooltip = Infantry +button-production-types-vehicle-tooltip = Vehicles +button-production-types-aircraft-tooltip = Aircraft +button-production-types-naval-tooltip = Naval +button-production-types-scroll-up-tooltip = Scroll up +button-production-types-scroll-down-tooltip = Scroll down + diff --git a/mods/ra/languages/difficulties/en.ftl b/mods/ra/languages/difficulties/en.ftl index 90ad41139706..5521c17cd52e 100644 --- a/mods/ra/languages/difficulties/en.ftl +++ b/mods/ra/languages/difficulties/en.ftl @@ -4,4 +4,5 @@ dropdown-difficulty = options-difficulty = .easy = Easy + .normal = Normal .hard = Hard diff --git a/mods/ra/languages/rules/en.ftl b/mods/ra/languages/rules/en.ftl index bd9a74e1835b..e7107170377e 100644 --- a/mods/ra/languages/rules/en.ftl +++ b/mods/ra/languages/rules/en.ftl @@ -32,9 +32,6 @@ options-starting-units = .light-support = Light Support .heavy-support = Heavy Support -options-difficulty = - .normal = Normal - ## Structures notification-construction-complete = Construction complete. notification-unit-ready = Unit ready. diff --git a/mods/ra/maps/a-nuclear-winter/rules.yaml b/mods/ra/maps/a-nuclear-winter/rules.yaml index 7eb286864276..2e2a22e74bbd 100644 --- a/mods/ra/maps/a-nuclear-winter/rules.yaml +++ b/mods/ra/maps/a-nuclear-winter/rules.yaml @@ -7,7 +7,7 @@ World: ScatterDirection: -1, 1 ParticleColors: ECECEC, E4E4E4, D0D0D0, BCBCBC LineTailAlphaValue: 0 - GlobalLightingPaletteEffect: + TintPostProcessEffect: Red: 0.88 Green: 0.93 Blue: 1.06 diff --git a/mods/ra/maps/allies-02/languages/en.ftl b/mods/ra/maps/allies-02/languages/en.ftl index 2088d2bcc64e..309a674b6521 100644 --- a/mods/ra/maps/allies-02/languages/en.ftl +++ b/mods/ra/maps/allies-02/languages/en.ftl @@ -4,5 +4,6 @@ dropdown-difficulty = options-difficulty = .easy = Easy + .normal = Normal .hard = Hard .tough = Real tough guy diff --git a/mods/ra/maps/allies-04/languages/en.ftl b/mods/ra/maps/allies-04/languages/en.ftl index 2088d2bcc64e..309a674b6521 100644 --- a/mods/ra/maps/allies-04/languages/en.ftl +++ b/mods/ra/maps/allies-04/languages/en.ftl @@ -4,5 +4,6 @@ dropdown-difficulty = options-difficulty = .easy = Easy + .normal = Normal .hard = Hard .tough = Real tough guy diff --git a/mods/ra/maps/allies-05a/languages/en.ftl b/mods/ra/maps/allies-05a/languages/en.ftl index 2088d2bcc64e..309a674b6521 100644 --- a/mods/ra/maps/allies-05a/languages/en.ftl +++ b/mods/ra/maps/allies-05a/languages/en.ftl @@ -4,5 +4,6 @@ dropdown-difficulty = options-difficulty = .easy = Easy + .normal = Normal .hard = Hard .tough = Real tough guy diff --git a/mods/ra/maps/chernobyl/rules.yaml b/mods/ra/maps/chernobyl/rules.yaml index f2886941427c..702d5c503d22 100644 --- a/mods/ra/maps/chernobyl/rules.yaml +++ b/mods/ra/maps/chernobyl/rules.yaml @@ -1,5 +1,5 @@ World: - GlobalLightingPaletteEffect: + TintPostProcessEffect: Red: 1 Green: 0.90 Blue: 0.83 diff --git a/mods/ra/maps/desert-rats/rules.yaml b/mods/ra/maps/desert-rats/rules.yaml index 1e030c185e11..777354a40226 100644 --- a/mods/ra/maps/desert-rats/rules.yaml +++ b/mods/ra/maps/desert-rats/rules.yaml @@ -1,5 +1,5 @@ World: - GlobalLightingPaletteEffect: + TintPostProcessEffect: Red: 1.1 Green: 0.92 Blue: 1.051 diff --git a/mods/ra/maps/desert-shellmap/desert-shellmap.lua b/mods/ra/maps/desert-shellmap/desert-shellmap.lua index 86c21c65b843..2094a9286420 100644 --- a/mods/ra/maps/desert-shellmap/desert-shellmap.lua +++ b/mods/ra/maps/desert-shellmap/desert-shellmap.lua @@ -6,7 +6,9 @@ the License, or (at your option) any later version. For more information, see COPYING. ]] -if DateTime.IsHalloween then + +-- Halloween easter egg +if DateTime.CurrentMonth == 10 and DateTime.CurrentDay == 31 then UnitTypes = { "ant", "ant", "ant" } BeachUnitTypes = { "ant", "ant" } ProxyType = "powerproxy.parazombies" diff --git a/mods/ra/maps/fall-of-greece-1-personal-war/weapons.yaml b/mods/ra/maps/fall-of-greece-1-personal-war/weapons.yaml index 96b3463b0e8d..aeded73fdc56 100644 --- a/mods/ra/maps/fall-of-greece-1-personal-war/weapons.yaml +++ b/mods/ra/maps/fall-of-greece-1-personal-war/weapons.yaml @@ -57,6 +57,6 @@ ParaBomb: ValidTargets: Ground, Infantry Size: 3 Delay: 10 - Warhead@13FlashEffect: FlashPaletteEffect + Warhead@13FlashEffect: FlashEffect Duration: 20 FlashType: Nuke diff --git a/mods/ra/maps/fort-lonestar/rules.yaml b/mods/ra/maps/fort-lonestar/rules.yaml index aab4eb205610..794fe6cc3d6d 100644 --- a/mods/ra/maps/fort-lonestar/rules.yaml +++ b/mods/ra/maps/fort-lonestar/rules.yaml @@ -17,14 +17,14 @@ World: ParticleColors: 304074, 28386C, 202C60, 182C54 LineTailAlphaValue: 150 ParticleSize: 1, 1 - GlobalLightingPaletteEffect: + TintPostProcessEffect: Red: 0.75 Green: 0.85 Blue: 1.5 Ambient: 0.45 MusicPlaylist: BackgroundMusic: rain - FlashPaletteEffect@LIGHTNINGSTRIKE: + FlashPostProcessEffect@LIGHTNINGSTRIKE: Type: LightningStrike LuaScript: Scripts: campaign.lua, fort-lonestar.lua, fort-lonestar-AI.lua diff --git a/mods/ra/maps/infiltration/rules.yaml b/mods/ra/maps/infiltration/rules.yaml index 3df3b68c6a33..96cc63aba8fd 100644 --- a/mods/ra/maps/infiltration/rules.yaml +++ b/mods/ra/maps/infiltration/rules.yaml @@ -1,5 +1,5 @@ World: - GlobalLightingPaletteEffect@HAZE: + TintPostProcessEffect@HAZE: Red: 1 Green: 0.55 Blue: 0 diff --git a/mods/ra/maps/mousetrap/rules.yaml b/mods/ra/maps/mousetrap/rules.yaml index 9ce276bb1b2a..f76ef1f05581 100644 --- a/mods/ra/maps/mousetrap/rules.yaml +++ b/mods/ra/maps/mousetrap/rules.yaml @@ -22,7 +22,7 @@ World: ClearNoSmudges: 100 ^Palettes: - -ChronoshiftPaletteEffect: + -ChronoshiftPostProcessEffect: # Used in ChronoEffect. 1TNK: diff --git a/mods/ra/maps/ritual-circle.oramap b/mods/ra/maps/ritual-circle.oramap index 9d7054e8dc75..3112cdd55efa 100644 Binary files a/mods/ra/maps/ritual-circle.oramap and b/mods/ra/maps/ritual-circle.oramap differ diff --git a/mods/ra/maps/shattered-mountain/rules.yaml b/mods/ra/maps/shattered-mountain/rules.yaml index fa53327a7f6b..f2f5e07787b1 100644 --- a/mods/ra/maps/shattered-mountain/rules.yaml +++ b/mods/ra/maps/shattered-mountain/rules.yaml @@ -7,7 +7,7 @@ World: ParticleDensityFactor: 8 ParticleColors: ECECEC, E4E4E4, D0D0D0, BCBCBC LineTailAlphaValue: 0 - GlobalLightingPaletteEffect: + TintPostProcessEffect: Red: 0.88 Green: 0.92 Blue: 1.06 diff --git a/mods/ra/maps/situation-critical/weapons.yaml b/mods/ra/maps/situation-critical/weapons.yaml index 53a60036fd7a..0c51bf4402ab 100644 --- a/mods/ra/maps/situation-critical/weapons.yaml +++ b/mods/ra/maps/situation-critical/weapons.yaml @@ -75,6 +75,6 @@ ParaBomb: ValidTargets: Ground, Infantry Size: 3 Delay: 10 - Warhead@13FlashEffect: FlashPaletteEffect + Warhead@13FlashEffect: FlashEffect Duration: 20 FlashType: Nuke diff --git a/mods/ra/maps/snow-town/rules.yaml b/mods/ra/maps/snow-town/rules.yaml index 0c568b5b2eca..ce813387b1d3 100644 --- a/mods/ra/maps/snow-town/rules.yaml +++ b/mods/ra/maps/snow-town/rules.yaml @@ -5,7 +5,7 @@ World: UseSquares: true ParticleColors: ECECEC, E4E4E4, D0D0D0, BCBCBC LineTailAlphaValue: 0 - GlobalLightingPaletteEffect: + TintPostProcessEffect: Red: 0.9 Green: 0.9 Blue: 1.0 diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index c3ba7e977c22..9f4cc4e05734 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -141,7 +141,9 @@ ChromeLayout: Translations: common|languages/en.ftl + common|languages/chrome/en.ftl common|languages/rules/en.ftl + ra|languages/chrome/en.ftl ra|languages/rules/en.ftl Weapons: diff --git a/mods/ra/rules/aircraft.yaml b/mods/ra/rules/aircraft.yaml index d34e4c517f37..ed20f6c0c1da 100644 --- a/mods/ra/rules/aircraft.yaml +++ b/mods/ra/rules/aircraft.yaml @@ -81,10 +81,12 @@ MIG: RevealsShroud: MinRange: 11c0 Range: 13c0 + MoveRecalculationThreshold: 0 Type: GroundPosition RevealGeneratedShroud: False RevealsShroud@GAPGEN: Range: 11c0 + MoveRecalculationThreshold: 0 Type: GroundPosition Armament: Weapon: Maverick @@ -159,10 +161,12 @@ YAK: RevealsShroud: MinRange: 9c0 Range: 11c0 + MoveRecalculationThreshold: 0 Type: GroundPosition RevealGeneratedShroud: False RevealsShroud@GAPGEN: Range: 9c0 + MoveRecalculationThreshold: 0 Type: GroundPosition Armament: Weapon: ChainGun.Yak @@ -236,10 +240,12 @@ TRAN: RevealsShroud: MinRange: 6c0 Range: 8c0 + MoveRecalculationThreshold: 0 Type: GroundPosition RevealGeneratedShroud: False RevealsShroud@GAPGEN: Range: 6c0 + MoveRecalculationThreshold: 0 Type: GroundPosition Aircraft: TurnSpeed: 20 @@ -290,10 +296,12 @@ HELI: RevealsShroud: MinRange: 10c0 Range: 12c0 + MoveRecalculationThreshold: 0 Type: GroundPosition RevealGeneratedShroud: False RevealsShroud@GAPGEN: Range: 10c0 + MoveRecalculationThreshold: 0 Type: GroundPosition Armament@PRIMARY: Weapon: HellfireAA @@ -372,10 +380,12 @@ HIND: RevealsShroud: MinRange: 8c0 Range: 10c0 + MoveRecalculationThreshold: 0 Type: GroundPosition RevealGeneratedShroud: False RevealsShroud@GAPGEN: Range: 8c0 + MoveRecalculationThreshold: 0 Type: GroundPosition Armament: Weapon: ChainGun @@ -490,10 +500,12 @@ MH60: RevealsShroud: MinRange: 8c0 Range: 10c0 + MoveRecalculationThreshold: 0 Type: GroundPosition RevealGeneratedShroud: False RevealsShroud@GAPGEN: Range: 8c0 + MoveRecalculationThreshold: 0 Type: GroundPosition Armament: Weapon: ChainGun diff --git a/mods/ra/rules/palettes.yaml b/mods/ra/rules/palettes.yaml index 19d21dff4d30..9bf7a8fe84d1 100644 --- a/mods/ra/rules/palettes.yaml +++ b/mods/ra/rules/palettes.yaml @@ -84,7 +84,7 @@ BaseName: cloak BasePalette: player Alpha: 0.55 - MenuPaletteEffect: + MenuPostProcessEffect: RotationPaletteEffect@defaultwater: Palettes: terrain ExcludeTilesets: DESERT @@ -99,8 +99,8 @@ RotationBase: 32 LightPaletteRotator: ExcludePalettes: terrain, effect, desert - ChronoshiftPaletteEffect: - FlashPaletteEffect@NUKE: + ChronoshiftPostProcessEffect: + FlashPostProcessEffect@NUKE: Type: Nuke IndexedPalette@CIV2: Name: civilian2 diff --git a/mods/ra/rules/vehicles.yaml b/mods/ra/rules/vehicles.yaml index 4a0850cbbd9f..6cb60b4f4ed8 100644 --- a/mods/ra/rules/vehicles.yaml +++ b/mods/ra/rules/vehicles.yaml @@ -367,6 +367,7 @@ HARV: ResourceSequences: Ore: pip-yellow Gems: pip-red + RequiresCondition: !dome MCV: Inherits: ^Vehicle diff --git a/mods/ra/rules/world.yaml b/mods/ra/rules/world.yaml index 04d6c0b7a863..fac900bf6714 100644 --- a/mods/ra/rules/world.yaml +++ b/mods/ra/rules/world.yaml @@ -92,6 +92,7 @@ Name: immobile TerrainSpeeds: TerrainRenderer: + ChronoVortexRenderer: ShroudRenderer: FogVariants: shroud Index: 255, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 20, 40, 56, 65, 97, 130, 148, 194, 24, 33, 66, 132, 28, 41, 67, 134, 1, 2, 4, 8, 3, 6, 12, 9, 7, 14, 13, 11, 5, 10, 15, 255 diff --git a/mods/ra/sequences/structures.yaml b/mods/ra/sequences/structures.yaml index 4b5c10de3d30..f2f9079d4fef 100644 --- a/mods/ra/sequences/structures.yaml +++ b/mods/ra/sequences/structures.yaml @@ -79,18 +79,11 @@ fact: build: Start: 1 Length: 25 - pdox: - Filename: factpdox.shp - Length: 80 damaged-idle: Start: 26 damaged-build: Start: 27 Length: 25 - damaged-pdox: - Filename: factpdox.shp - Start: 80 - Length: 80 dead: Filename: factdead.shp Tick: 800 diff --git a/mods/ra/weapons/explosions.yaml b/mods/ra/weapons/explosions.yaml index a5fd4d809aba..7c8696d473ba 100644 --- a/mods/ra/weapons/explosions.yaml +++ b/mods/ra/weapons/explosions.yaml @@ -271,7 +271,7 @@ CrateNuke: ValidTargets: Ground, Infantry Size: 4 Delay: 5 - Warhead@7FlashEffect: FlashPaletteEffect + Warhead@7FlashEffect: FlashEffect Duration: 20 FlashType: Nuke @@ -345,6 +345,6 @@ MiniNuke: ValidTargets: Ground, Infantry Size: 4 Delay: 15 - Warhead@14FlashEffect: FlashPaletteEffect + Warhead@14FlashEffect: FlashEffect Duration: 20 FlashType: Nuke diff --git a/mods/ra/weapons/smallcaliber.yaml b/mods/ra/weapons/smallcaliber.yaml index 22192fb1d3da..e8dbfeeaec30 100644 --- a/mods/ra/weapons/smallcaliber.yaml +++ b/mods/ra/weapons/smallcaliber.yaml @@ -330,4 +330,3 @@ Colt45: Range: 7c0 Warhead@1Dam: SpreadDamage Damage: 10000 - diff --git a/mods/ra/weapons/superweapons.yaml b/mods/ra/weapons/superweapons.yaml index 40853bc75238..a5ebea2a50ec 100644 --- a/mods/ra/weapons/superweapons.yaml +++ b/mods/ra/weapons/superweapons.yaml @@ -134,6 +134,6 @@ Atomic: Duration: 20 Intensity: 5 Multiplier: 1,1 - Warhead@22FlashEffect: FlashPaletteEffect + Warhead@22FlashEffect: FlashEffect Duration: 20 FlashType: Nuke diff --git a/mods/ts/chrome/assetbrowser.yaml b/mods/ts/chrome/assetbrowser.yaml new file mode 100644 index 000000000000..61dcdce8432e --- /dev/null +++ b/mods/ts/chrome/assetbrowser.yaml @@ -0,0 +1,296 @@ +Background@ASSETBROWSER_PANEL: + Logic: AssetBrowserLogic + X: (WINDOW_RIGHT - WIDTH) / 2 + Y: (WINDOW_BOTTOM - HEIGHT) / 2 + Width: 900 + Height: 600 + Children: + LogicTicker@ANIMATION_TICKER: + Label@ASSETBROWSER_TITLE: + Y: 16 + Width: PARENT_RIGHT + Height: 25 + Font: Bold + Align: Center + Text: label-assetbrowser-panel-title + Label@SOURCE_SELECTOR_DESC: + X: 20 + Y: 36 + Width: 195 + Height: 25 + Font: TinyBold + Align: Center + Text: label-assetbrowser-panel-source-selector-desc + DropDownButton@SOURCE_SELECTOR: + X: 20 + Y: 60 + Width: 195 + Height: 25 + Font: Bold + Text: dropdownbutton-assetbrowser-panel-source-selector + DropDownButton@ASSET_TYPES_DROPDOWN: + X: 20 + Y: 90 + Width: 195 + Height: 25 + Font: Bold + Text: dropdownbutton-assetbrowser-panel-asset-types-dropdown + Label@FILENAME_DESC: + X: 20 + Y: 115 + Width: 195 + Height: 25 + Font: TinyBold + Align: Center + Text: label-assetbrowser-panel-filename-desc + TextField@FILENAME_INPUT: + X: 20 + Y: 140 + Width: 195 + Height: 25 + Type: Filename + ScrollPanel@ASSET_LIST: + X: 20 + Y: 170 + Width: 195 + Height: PARENT_BOTTOM - 250 + CollapseHiddenChildren: True + Children: + ScrollItem@ASSET_TEMPLATE: + Width: PARENT_RIGHT - 27 + Height: 25 + X: 2 + Visible: false + EnableChildMouseOver: True + Children: + LabelWithTooltip@TITLE: + X: 10 + Width: PARENT_RIGHT - 20 + Height: 25 + TooltipContainer: TOOLTIP_CONTAINER + TooltipTemplate: SIMPLE_TOOLTIP + Label@SPRITE_SCALE: + X: PARENT_RIGHT - WIDTH - 440 + Y: 60 + Width: 40 + Height: 25 + Font: Bold + Align: Left + Text: label-assetbrowser-panel-sprite-scale + Slider@SPRITE_SCALE_SLIDER: + X: PARENT_RIGHT - WIDTH - 330 + Y: 62 + Width: 100 + Height: 20 + MinimumValue: 0.5 + MaximumValue: 4 + Label@MODEL_SCALE: + X: PARENT_RIGHT - WIDTH - 440 + Y: 60 + Width: 40 + Height: 25 + Font: Bold + Align: Left + Text: label-assetbrowser-panel-model-scale + Slider@MODEL_SCALE_SLIDER: + X: PARENT_RIGHT - WIDTH - 330 + Y: 62 + Width: 100 + Height: 20 + MinimumValue: 10 + MaximumValue: 64 + Label@PALETTE_DESC: + X: PARENT_RIGHT - WIDTH - 270 + Y: 60 + Width: 150 + Height: 25 + Font: Bold + Align: Right + Text: label-assetbrowser-panel-palette-desc + DropDownButton@PALETTE_SELECTOR: + X: PARENT_RIGHT - WIDTH - 110 + Y: 60 + Width: 150 + Height: 25 + Font: Bold + DropDownButton@COLOR: + X: PARENT_RIGHT - WIDTH - 20 + Y: 60 + Width: 80 + Height: 25 + Children: + ColorBlock@COLORBLOCK: + X: 5 + Y: 6 + Width: PARENT_RIGHT - 35 + Height: PARENT_BOTTOM - 12 + Background@SPRITE_BG: + X: 226 + Y: 90 + Width: PARENT_RIGHT - 226 - 20 + Height: PARENT_BOTTOM - 170 + Background: dialog3 + Children: + Sprite@SPRITE: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + VideoPlayer@PLAYER: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + AspectRatio: 1 + Model@VOXEL: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Palette: colorpicker + PlayerPalette: colorpicker + LightPitch: 256 + LightYaw: 0 + Label@ERROR: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Align: Center + Visible: false + Text: label-sprite-bg-error + Container@FRAME_SELECTOR: + X: 226 + Y: PARENT_BOTTOM - 75 + Width: PARENT_RIGHT - 226 + Children: + Button@BUTTON_PREV: + Width: 26 + Height: 26 + Key: LEFT + Children: + Image@IMAGE_PREV: + X: 5 + Y: 5 + ImageCollection: music + ImageName: prev + Button@BUTTON_PLAY: + X: 35 + Width: 26 + Height: 26 + Key: SPACE + Children: + Image@IMAGE_PLAY: + X: 5 + Y: 5 + ImageCollection: music + ImageName: play + Button@BUTTON_PAUSE: + Visible: false + X: 35 + Width: 26 + Height: 26 + Key: SPACE + Children: + Image@IMAGE_PAUSE: + X: 5 + Y: 5 + ImageCollection: music + ImageName: pause + Button@BUTTON_STOP: + X: 70 + Width: 26 + Height: 26 + Key: RETURN + Children: + Image@IMAGE_STOP: + X: 5 + Y: 5 + ImageCollection: music + ImageName: stop + Button@BUTTON_NEXT: + X: 105 + Width: 26 + Height: 26 + Key: RIGHT + Children: + Image@IMAGE_NEXT: + X: 5 + Y: 5 + ImageCollection: music + ImageName: next + Slider@FRAME_SLIDER: + X: 140 + Y: 3 + Width: PARENT_RIGHT - 140 - 85 + Height: 20 + MinimumValue: 0 + Label@FRAME_COUNT: + X: PARENT_RIGHT - WIDTH + 5 + Y: 0 + Width: 85 + Height: 25 + Font: TinyBold + Align: Left + Container@VOXEL_SELECTOR: + X: 226 + Y: PARENT_BOTTOM - 75 + Children: + Label@ROLL: + Y: 1 + Width: 40 + Height: 25 + Font: TinyBold + Align: Left + Text: label-voxel-selector-roll + Slider@ROLL_SLIDER: + X: 30 + Y: 3 + Width: 100 + Height: 20 + MinimumValue: 1 + MaximumValue: 1023 + Label@PITCH: + X: 150 + Y: 1 + Width: 40 + Height: 25 + Font: TinyBold + Align: Left + Text: label-voxel-selector-pitch + Slider@PITCH_SLIDER: + X: 190 + Y: 3 + Width: 100 + Height: 20 + MinimumValue: 1 + MaximumValue: 1023 + Label@YAW: + X: 305 + Y: 1 + Width: 40 + Height: 25 + Font: TinyBold + Align: Left + Text: label-voxel-selector-yaw + Slider@YAW_SLIDER: + X: 335 + Y: 3 + Width: 100 + Height: 20 + MinimumValue: 1 + MaximumValue: 1023 + Button@CLOSE_BUTTON: + Key: escape + X: PARENT_RIGHT - 180 + Y: PARENT_BOTTOM - 45 + Width: 160 + Height: 25 + Font: Bold + Text: button-assetbrowser-panel-close + TooltipContainer@TOOLTIP_CONTAINER: + +ScrollPanel@ASSET_TYPES_PANEL: + Width: 195 + Height: 130 + ItemSpacing: 5 + TopBottomSpacing: 0 + Children: + Checkbox@ASSET_TYPE_TEMPLATE: + X: 5 + Y: 5 + Width: PARENT_RIGHT - 29 + Height: 20 + diff --git a/mods/ts/chrome/color-picker.yaml b/mods/ts/chrome/color-picker.yaml index 2de266df87c2..92a94d81d446 100644 --- a/mods/ts/chrome/color-picker.yaml +++ b/mods/ts/chrome/color-picker.yaml @@ -13,14 +13,14 @@ Background@COLOR_CHOOSER: Y: 95 Width: 80 Height: 25 - Text: Random + Text: button-color-chooser-random Font: Bold Button@STORE_BUTTON: X: 245 Y: 124 Width: 80 Height: 25 - Text: Store + Text: button-color-chooser-store Font: Bold ActorPreview@PREVIEW: X: PARENT_RIGHT - 94 @@ -33,14 +33,14 @@ Background@COLOR_CHOOSER: Y: PARENT_BOTTOM - 30 Height: 25 Width: 80 - Text: Mixer + Text: button-color-chooser-mixer-tab Font: Bold Button@PALETTE_TAB_BUTTON: X: 85 Y: PARENT_BOTTOM - 30 Height: 25 Width: 80 - Text: Palette + Text: button-color-chooser-palette-tab Font: Bold Container@MIXER_TAB: X: 5 @@ -99,7 +99,7 @@ Background@COLOR_CHOOSER: Width: PARENT_RIGHT Height: 13 Align: Center - Text: Preset Colors + Text: label-preset-header Container@PRESET_AREA: Width: PARENT_RIGHT - 4 Height: 58 @@ -125,7 +125,7 @@ Background@COLOR_CHOOSER: Width: PARENT_RIGHT Height: 13 Align: Center - Text: Custom Colors + Text: label-custom-header Container@CUSTOM_AREA: Width: PARENT_RIGHT - 4 Height: 31 @@ -139,3 +139,4 @@ Background@COLOR_CHOOSER: Height: 29 Visible: false ClickSound: ClickSound + diff --git a/mods/ts/chrome/dropdowns.yaml b/mods/ts/chrome/dropdowns.yaml index 3d52d689e4bc..c70e03b39f15 100644 --- a/mods/ts/chrome/dropdowns.yaml +++ b/mods/ts/chrome/dropdowns.yaml @@ -146,3 +146,4 @@ ScrollPanel@NEWS_PANEL: Height: PARENT_BOTTOM Align: Center VAlign: Middle + diff --git a/mods/ts/chrome/ingame-debug.yaml b/mods/ts/chrome/ingame-debug.yaml index 013e81d3539c..6f984cd06c17 100644 --- a/mods/ts/chrome/ingame-debug.yaml +++ b/mods/ts/chrome/ingame-debug.yaml @@ -7,7 +7,7 @@ Container@DEBUG_PANEL: Label@TITLE: Y: 26 Font: Bold - Text: Debug Options + Text: label-debug-panel-title Align: Center Width: PARENT_RIGHT Checkbox@INSTANT_BUILD: @@ -16,74 +16,74 @@ Container@DEBUG_PANEL: Width: 200 Height: 20 Font: Regular - Text: Instant Build Speed + Text: checkbox-debug-panel-instant-build Checkbox@ENABLE_TECH: X: 45 Y: 75 Width: 200 Height: 20 Font: Regular - Text: Build Everything + Text: checkbox-debug-panel-enable-tech Checkbox@BUILD_ANYWHERE: X: 45 Y: 105 Width: 200 Height: 20 Font: Regular - Text: Build Anywhere + Text: checkbox-debug-panel-build-anywhere Checkbox@UNLIMITED_POWER: X: 290 Y: 45 Width: 200 Height: 20 Font: Regular - Text: Unlimited Power + Text: checkbox-debug-panel-unlimited-power Checkbox@INSTANT_CHARGE: X: 290 Y: 75 Width: 200 Height: 20 Font: Regular - Text: Instant Charge Time + Text: checkbox-debug-panel-instant-charge Checkbox@DISABLE_VISIBILITY_CHECKS: X: 290 Y: 105 Height: 20 Width: 200 Font: Regular - Text: Disable Visibility Checks + Text: checkbox-debug-panel-disable-visibility-checks Button@GIVE_CASH: X: 90 Y: 150 Width: 140 Height: 30 Font: Bold - Text: Give $20,000 + Text: button-debug-panel-give-cash Button@GROW_RESOURCES: X: 271 Y: 150 Width: 140 Height: 30 Font: Bold - Text: Grow Resources + Text: button-debug-panel-grow-resources Button@GIVE_EXPLORATION: X: 90 Y: 200 Width: 140 Height: 30 Font: Bold - Text: Clear Shroud + Text: button-debug-panel-give-exploration Button@RESET_EXPLORATION: X: 271 Y: 200 Width: 140 Height: 30 Font: Bold - Text: Reset Shroud + Text: button-debug-panel-reset-exploration Label@VISUALIZATIONS_TITLE: Y: 255 Font: Bold - Text: Visualizations + Text: label-debug-panel-visualizations-title Align: Center Width: PARENT_RIGHT Checkbox@SHOW_UNIT_PATHS: @@ -92,53 +92,54 @@ Container@DEBUG_PANEL: Width: 200 Height: 20 Font: Regular - Text: Show Unit Paths + Text: checkbox-debug-panel-show-unit-paths Checkbox@SHOW_CUSTOMTERRAIN_OVERLAY: X: 45 Y: 305 Height: 20 Width: 200 Font: Regular - Text: Show Custom Terrain + Text: checkbox-debug-panel-show-customterrain-overlay Checkbox@SHOW_ACTOR_TAGS: X: 45 Y: 335 Height: 20 Width: 200 Font: Regular - Text: Show Actor Tags + Text: checkbox-debug-panel-show-actor-tags Checkbox@SHOW_DEPTH_PREVIEW: X: 45 Y: 365 Height: 20 Width: 200 Font: Regular - Text: Show Depth Data + Text: checkbox-debug-panel-show-depth-preview Checkbox@SHOW_COMBATOVERLAY: X: 290 Y: 275 Height: 20 Width: 200 Font: Regular - Text: Show Combat Geometry + Text: checkbox-debug-panel-show-combatoverlay Checkbox@SHOW_GEOMETRY: X: 290 Y: 305 Height: 20 Width: 200 Font: Regular - Text: Show Render Geometry + Text: checkbox-debug-panel-show-geometry Checkbox@SHOW_TERRAIN_OVERLAY: X: 290 Y: 335 Height: 20 Width: 200 Font: Regular - Text: Show Terrain Geometry + Text: checkbox-debug-panel-show-terrain-overlay Checkbox@SHOW_SCREENMAP: X: 290 Y: 365 Height: 20 Width: 200 Font: Regular - Text: Show Screen Map + Text: checkbox-debug-panel-show-screenmap + diff --git a/mods/ts/chrome/ingame-observer.yaml b/mods/ts/chrome/ingame-observer.yaml index 636f9a9ae335..107d97bb8747 100644 --- a/mods/ts/chrome/ingame-observer.yaml +++ b/mods/ts/chrome/ingame-observer.yaml @@ -21,14 +21,14 @@ Container@OBSERVER_WIDGETS: Width: PARENT_RIGHT - 30 Height: 25 Align: Right - Text: Audio Muted + Text: label-mute-indicator Contrast: true MenuButton@OPTIONS_BUTTON: X: 5 Y: 5 Width: 160 Height: 25 - Text: Options (Esc) + Text: button-observer-widgets-options Font: Bold Key: escape DisableWorldSounds: true @@ -116,7 +116,7 @@ Container@OBSERVER_WIDGETS: Width: 26 Height: 26 Key: Pause - TooltipText: Pause + TooltipText: button-replay-player-pause-tooltip TooltipContainer: TOOLTIP_CONTAINER IgnoreChildMouseOver: true Children: @@ -131,7 +131,7 @@ Container@OBSERVER_WIDGETS: Width: 26 Height: 26 Key: Pause - TooltipText: Play + TooltipText: button-replay-player-play-tooltip TooltipContainer: TOOLTIP_CONTAINER IgnoreChildMouseOver: true Children: @@ -146,9 +146,9 @@ Container@OBSERVER_WIDGETS: Width: 36 Height: 20 Key: ReplaySpeedSlow - TooltipText: Slow speed + TooltipText: button-replay-player-slow.tooltip TooltipContainer: TOOLTIP_CONTAINER - Text: 50% + Text: button-replay-player-slow.label Font: TinyBold Button@BUTTON_REGULAR: X: 55 + 45 @@ -156,9 +156,9 @@ Container@OBSERVER_WIDGETS: Width: 38 Height: 20 Key: ReplaySpeedRegular - TooltipText: Regular speed + TooltipText: button-replay-player-regular.tooltip TooltipContainer: TOOLTIP_CONTAINER - Text: 100% + Text: button-replay-player-regular.label Font: TinyBold Button@BUTTON_FAST: X: 55 + 45 * 2 @@ -166,9 +166,9 @@ Container@OBSERVER_WIDGETS: Width: 38 Height: 20 Key: ReplaySpeedFast - TooltipText: Fast speed + TooltipText: button-replay-player-fast.tooltip TooltipContainer: TOOLTIP_CONTAINER - Text: 200% + Text: button-replay-player-fast.label Font: TinyBold Button@BUTTON_MAXIMUM: X: 55 + 45 * 3 @@ -176,9 +176,9 @@ Container@OBSERVER_WIDGETS: Width: 38 Height: 20 Key: ReplaySpeedMax - TooltipText: Maximum speed + TooltipText: button-replay-player-maximum.tooltip TooltipContainer: TOOLTIP_CONTAINER - Text: MAX + Text: button-replay-player-maximum.label Font: TinyBold Container@INGAME_OBSERVERSTATS_BG: Logic: ObserverStatsLogic @@ -235,7 +235,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-basic-stats-player-header Align: Left Shadow: True Label@CASH_HEADER: @@ -244,7 +244,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Cash + Text: label-basic-stats-cash-header Align: Right Shadow: True Label@POWER_HEADER: @@ -253,7 +253,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Power + Text: label-basic-stats-power-header Align: Center Shadow: True Label@KILLS_HEADER: @@ -262,7 +262,7 @@ Container@OBSERVER_WIDGETS: Width: 40 Height: PARENT_BOTTOM Font: Bold - Text: Kills + Text: label-basic-stats-kills-header Align: Right Shadow: True Label@DEATHS_HEADER: @@ -271,7 +271,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Deaths + Text: label-basic-stats-deaths-header Align: Right Shadow: True Label@ASSETS_DESTROYED_HEADER: @@ -280,7 +280,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Destroyed + Text: label-basic-stats-assets-destroyed-header Align: Right Shadow: True Label@ASSETS_LOST_HEADER: @@ -289,7 +289,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Lost + Text: label-basic-stats-assets-lost-header Align: Right Shadow: True Label@EXPERIENCE_HEADER: @@ -298,7 +298,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Score + Text: label-basic-stats-experience-header Align: Right Shadow: True Label@ACTIONS_MIN_HEADER: @@ -307,7 +307,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: APM + Text: label-basic-stats-actions-min-header Align: Right Shadow: True Container@ECONOMY_STATS_HEADERS: @@ -334,14 +334,14 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-economy-stats-player-header Shadow: True Label@CASH_HEADER: X: 160 Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Cash + Text: label-economy-stats-cash-header Align: Right Shadow: True Label@INCOME_HEADER: @@ -349,7 +349,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Income + Text: label-economy-stats-income-header Align: Right Shadow: True Label@ASSETS_HEADER: @@ -357,7 +357,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Assets + Text: label-economy-stats-assets-header Align: Right Shadow: True Label@EARNED_HEADER: @@ -365,7 +365,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Earned + Text: label-economy-stats-earned-header Align: Right Shadow: True Label@SPENT_HEADER: @@ -373,7 +373,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Spent + Text: label-economy-stats-spent-header Align: Right Shadow: True Label@HARVESTERS_HEADER: @@ -381,7 +381,7 @@ Container@OBSERVER_WIDGETS: Width: 80 Height: PARENT_BOTTOM Font: Bold - Text: Harvesters + Text: label-economy-stats-harvesters-header Align: Right Shadow: True Container@PRODUCTION_STATS_HEADERS: @@ -409,7 +409,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-production-stats-player-header Align: Left Shadow: True Label@PRODUCTION_HEADER: @@ -418,7 +418,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Production + Text: label-production-stats-header Shadow: True Container@SUPPORT_POWERS_HEADERS: X: 0 @@ -445,7 +445,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-support-powers-player-header Align: Left Shadow: True Label@SUPPORT_POWERS_HEADER: @@ -454,7 +454,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Support Powers + Text: label-support-powers-header Shadow: True Container@ARMY_HEADERS: X: 0 @@ -481,7 +481,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-army-player-header Align: Left Shadow: True Label@ARMY_HEADER: @@ -490,7 +490,7 @@ Container@OBSERVER_WIDGETS: Width: 100 Height: PARENT_BOTTOM Font: Bold - Text: Army + Text: label-army-header Shadow: True Container@COMBAT_STATS_HEADERS: X: 0 @@ -517,7 +517,7 @@ Container@OBSERVER_WIDGETS: Width: 120 Height: PARENT_BOTTOM Font: Bold - Text: Player + Text: label-combat-stats-player-header Align: Left Shadow: True Label@ASSETS_DESTROYED_HEADER: @@ -526,7 +526,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: Destroyed + Text: label-combat-stats-assets-destroyed-header Align: Right Shadow: True Label@ASSETS_LOST_HEADER: @@ -535,7 +535,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: Lost + Text: label-combat-stats-assets-lost-header Align: Right Shadow: True Label@UNITS_KILLED_HEADER: @@ -544,7 +544,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: U. Killed + Text: label-combat-stats-units-killed-header Align: Right Shadow: True Label@UNITS_DEAD_HEADER: @@ -553,7 +553,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: U. Lost + Text: label-combat-stats-units-dead-header Align: Right Shadow: True Label@BUILDINGS_KILLED_HEADER: @@ -562,7 +562,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: B. Killed + Text: label-combat-stats-buildings-killed-header Align: Right Shadow: True Label@BUILDINGS_DEAD_HEADER: @@ -571,7 +571,7 @@ Container@OBSERVER_WIDGETS: Width: 75 Height: PARENT_BOTTOM Font: Bold - Text: B. Lost + Text: label-combat-stats-buildings-dead-header Align: Right Shadow: True Label@ARMY_VALUE_HEADER: @@ -580,7 +580,7 @@ Container@OBSERVER_WIDGETS: Width: 90 Height: PARENT_BOTTOM Font: Bold - Text: Army Value + Text: label-combat-stats-army-value-header Align: Right Shadow: True Label@VISION_HEADER: @@ -589,7 +589,7 @@ Container@OBSERVER_WIDGETS: Width: 60 Height: PARENT_BOTTOM Font: Bold - Text: Vision + Text: label-combat-stats-vision-header Align: Right Shadow: True ScrollPanel@PLAYER_STATS_PANEL: @@ -1046,3 +1046,4 @@ Container@OBSERVER_WIDGETS: X: WINDOW_RIGHT - WIDTH - 260 Y: 40 Width: 175 + diff --git a/mods/ts/chrome/ingame-player.yaml b/mods/ts/chrome/ingame-player.yaml index ee8751870ce8..ba5192bb951c 100644 --- a/mods/ts/chrome/ingame-player.yaml +++ b/mods/ts/chrome/ingame-player.yaml @@ -28,8 +28,8 @@ Container@PLAYER_WIDGETS: IconSize: 64, 48 IconSpriteOffset: -1, -1 TooltipContainer: TOOLTIP_CONTAINER - ReadyText: READY - HoldText: ON HOLD + ReadyText: supportpowers-support-powers-palette.ready + HoldText: supportpowers-support-powers-palette.hold ClockPalette: iconclock HotkeyPrefix: SupportPower HotkeyCount: 6 @@ -58,8 +58,8 @@ Container@PLAYER_WIDGETS: Background: Key: AttackMove DisableKeySound: true - TooltipText: Attack Move - TooltipDesc: Selected units will move to the desired location\nand attack any enemies they encounter en route.\n\nHold <(Ctrl)> while targeting to order an Assault Move\nthat attacks any units or structures encountered en route.\n\nLeft-click icon then right-click on target location. + TooltipText: button-command-bar-attack-move.tooltip + TooltipDesc: button-command-bar-attack-move.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -76,8 +76,8 @@ Container@PLAYER_WIDGETS: VisualHeight: 0 Background: DisableKeySound: true - TooltipText: Force Move - TooltipDesc: Selected units will move to the desired location\n - Default activity for the target is suppressed\n - Vehicles will attempt to crush enemies at the target location\n - Deployed units will undeploy and move to the target location\n - Helicopters will land at the target location\n\nLeft-click icon then right-click on target.\nHold <(Alt)> to activate temporarily while commanding units. + TooltipText: button-command-bar-force-move.tooltip + TooltipDesc: button-command-bar-force-move.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -94,8 +94,8 @@ Container@PLAYER_WIDGETS: VisualHeight: 0 Background: DisableKeySound: true - TooltipText: Force Attack - TooltipDesc: Selected units will attack the targeted unit or location\n - Default activity for the target is suppressed\n - Allows targeting of own or ally forces\n - Long-range artillery units will always target the\n location, ignoring units and buildings\n\nLeft-click icon then right-click on target.\nHold <(Ctrl)> to activate temporarily while commanding units. + TooltipText: button-command-bar-force-attack.tooltip + TooltipDesc: button-command-bar-force-attack.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -113,8 +113,8 @@ Container@PLAYER_WIDGETS: Background: Key: Guard DisableKeySound: true - TooltipText: Guard - TooltipDesc: Selected units will follow the targeted unit.\n\nLeft-click icon then right-click on target unit. + TooltipText: button-command-bar-guard.tooltip + TooltipDesc: button-command-bar-guard.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -132,8 +132,8 @@ Container@PLAYER_WIDGETS: Key: Deploy DisableKeyRepeat: true DisableKeySound: true - TooltipText: Deploy - TooltipDesc: Selected units will perform their default deploy activity\n - MCVs will unpack into a Construction Yard\n - Construction Yards will re-pack into a MCV\n - Transports will unload their passengers\n - Tick Tanks, Artillery, Juggernauts,\n and Mobile Sensor arrays will deploy\n - Aircraft will return to base\n\nActs immediately on selected units. + TooltipText: button-command-bar-deploy.tooltip + TooltipDesc: button-command-bar-deploy.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -151,8 +151,8 @@ Container@PLAYER_WIDGETS: Key: Scatter DisableKeyRepeat: true DisableKeySound: true - TooltipText: Scatter - TooltipDesc: Selected units will stop their current activity\nand move to a nearby location.\n\nActs immediately on selected units. + TooltipText: button-command-bar-scatter.tooltip + TooltipDesc: button-command-bar-scatter.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -170,8 +170,8 @@ Container@PLAYER_WIDGETS: Key: Stop DisableKeyRepeat: true DisableKeySound: true - TooltipText: Stop - TooltipDesc: Selected units will stop their current activity.\nSelected buildings will reset their rally point.\n\nActs immediately on selected targets. + TooltipText: button-command-bar-stop.tooltip + TooltipDesc: button-command-bar-stop.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -187,8 +187,8 @@ Container@PLAYER_WIDGETS: VisualHeight: 0 Background: DisableKeySound: true - TooltipText: Waypoint Mode - TooltipDesc: Use Waypoint Mode to give multiple linking commands\nto the selected units. Units will execute the commands\nimmediately upon receiving them.\n\nLeft-click icon then give commands in the game world.\nHold <(Shift)> to activate temporarily while commanding units. + TooltipText: button-command-bar-queue-orders.tooltip + TooltipDesc: button-command-bar-queue-orders.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER TooltipTemplate: BUTTON_WITH_DESC_HIGHLIGHT_TOOLTIP Children: @@ -213,8 +213,8 @@ Container@PLAYER_WIDGETS: Key: StanceAttackAnything DisableKeyRepeat: true DisableKeySound: true - TooltipText: Attack Anything Stance - TooltipDesc: Set the selected units to Attack Anything stance:\n - Units will attack enemy units and structures on sight\n - Units will pursue attackers across the battlefield + TooltipText: button-stance-bar-attackanything.tooltip + TooltipDesc: button-stance-bar-attackanything.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -232,8 +232,8 @@ Container@PLAYER_WIDGETS: Key: StanceDefend DisableKeyRepeat: true DisableKeySound: true - TooltipText: Defend Stance - TooltipDesc: Set the selected units to Defend stance:\n - Units will attack enemy units on sight\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-defend.tooltip + TooltipDesc: button-stance-bar-defend.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -251,8 +251,8 @@ Container@PLAYER_WIDGETS: Key: StanceReturnFire DisableKeyRepeat: true DisableKeySound: true - TooltipText: Return Fire Stance - TooltipDesc: Set the selected units to Return Fire stance:\n - Units will retaliate against enemies that attack them\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-returnfire.tooltip + TooltipDesc: button-stance-bar-returnfire.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -270,8 +270,8 @@ Container@PLAYER_WIDGETS: Key: StanceHoldFire DisableKeyRepeat: true DisableKeySound: true - TooltipText: Hold Fire Stance - TooltipDesc: Set the selected units to Hold Fire stance:\n - Units will not fire upon enemies\n - Units will not move or pursue enemies + TooltipText: button-stance-bar-holdfire.tooltip + TooltipDesc: button-stance-bar-holdfire.tooltipdesc TooltipContainer: TOOLTIP_CONTAINER Children: Image@ICON: @@ -298,7 +298,7 @@ Container@PLAYER_WIDGETS: Width: PARENT_RIGHT - 30 Height: 25 Align: Right - Text: Audio Muted + Text: label-mute-indicator Contrast: true Image@SIDEBAR_BACKGROUND_TOP: Logic: AddFactionSuffixLogic @@ -322,7 +322,7 @@ Container@PLAYER_WIDGETS: Height: 31 Background: sidebar-button Key: Repair - TooltipText: Repair + TooltipText: button-top-buttons-repair-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -338,7 +338,7 @@ Container@PLAYER_WIDGETS: Height: 31 Background: sidebar-button Key: Sell - TooltipText: Sell + TooltipText: button-top-buttons-sell-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -354,7 +354,7 @@ Container@PLAYER_WIDGETS: Height: 31 Background: sidebar-button Key: PlaceBeacon - TooltipText: Place Beacon + TooltipText: button-top-buttons-beacon-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -370,7 +370,7 @@ Container@PLAYER_WIDGETS: Height: 31 Background: sidebar-button Key: PowerDown - TooltipText: Power Down + TooltipText: button-top-buttons-power-tooltip TooltipContainer: TOOLTIP_CONTAINER VisualHeight: 0 Children: @@ -386,7 +386,7 @@ Container@PLAYER_WIDGETS: Width: 30 Height: 31 Background: sidebar-button - TooltipText: Options + TooltipText: button-top-buttons-options-tooltip TooltipContainer: TOOLTIP_CONTAINER DisableWorldSounds: true VisualHeight: 0 @@ -493,8 +493,8 @@ Container@PLAYER_WIDGETS: X: 24 Y: 1 TooltipContainer: TOOLTIP_CONTAINER - ReadyText: READY - HoldText: ON HOLD + ReadyText: productionpalette-sidebar-production-palette.ready + HoldText: productionpalette-sidebar-production-palette.hold ClockPalette: iconclock NotBuildableAnimation: darken NotBuildablePalette: chromewithshadow @@ -520,7 +520,7 @@ Container@PLAYER_WIDGETS: Height: 31 VisualHeight: 0 Background: sidebar-button - TooltipText: Buildings + TooltipText: button-production-types-building-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Building Key: ProductionTypeBuilding @@ -537,7 +537,7 @@ Container@PLAYER_WIDGETS: Height: 31 VisualHeight: 0 Background: sidebar-button - TooltipText: Support + TooltipText: button-production-types-defense-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Defense Key: ProductionTypeDefense @@ -554,7 +554,7 @@ Container@PLAYER_WIDGETS: Height: 31 VisualHeight: 0 Background: sidebar-button - TooltipText: Infantry + TooltipText: button-production-types-infantry-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Infantry Key: ProductionTypeInfantry @@ -571,7 +571,7 @@ Container@PLAYER_WIDGETS: Height: 31 VisualHeight: 0 Background: sidebar-button - TooltipText: Vehicles + TooltipText: button-production-types-vehicle-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Vehicle Key: ProductionTypeVehicle @@ -588,7 +588,7 @@ Container@PLAYER_WIDGETS: Height: 31 VisualHeight: 0 Background: sidebar-button - TooltipText: Aircraft + TooltipText: button-production-types-aircraft-tooltip TooltipContainer: TOOLTIP_CONTAINER ProductionGroup: Air Key: ProductionTypeAircraft @@ -605,7 +605,7 @@ Container@PLAYER_WIDGETS: Height: 27 VisualHeight: 0 Background: scrollup-buttons - TooltipText: Scroll up + TooltipText: button-production-types-scroll-up-tooltip TooltipContainer: TOOLTIP_CONTAINER Button@SCROLL_DOWN_BUTTON: Logic: AddFactionSuffixLogic @@ -615,10 +615,11 @@ Container@PLAYER_WIDGETS: Height: 27 VisualHeight: 0 Background: scrolldown-buttons - TooltipText: Scroll down + TooltipText: button-production-types-scroll-down-tooltip TooltipContainer: TOOLTIP_CONTAINER Container@HPF_ROOT: Logic: LoadIngameHierarchicalPathFinderOverlayLogic X: WINDOW_RIGHT - WIDTH - 245 Y: 40 Width: 175 + diff --git a/mods/ts/chrome/ingame-transients.yaml b/mods/ts/chrome/ingame-transients.yaml index dbfafeffec23..1f1177ae3471 100644 --- a/mods/ts/chrome/ingame-transients.yaml +++ b/mods/ts/chrome/ingame-transients.yaml @@ -11,3 +11,4 @@ Container@TRANSIENTS_PANEL: DisplayDurationMs: 4000 LogLength: 5 HideOverflow: False + diff --git a/mods/ts/chrome/mainmenu-prerelease-notification.yaml b/mods/ts/chrome/mainmenu-prerelease-notification.yaml index b6669c632b1f..5ac55db00405 100644 --- a/mods/ts/chrome/mainmenu-prerelease-notification.yaml +++ b/mods/ts/chrome/mainmenu-prerelease-notification.yaml @@ -11,35 +11,35 @@ Background@MAINMENU_PRERELEASE_NOTIFICATION: Height: 25 Font: Bold Align: Center - Text: Tiberian Sun developer preview + Text: label-mainmenu-prerelease-notification-prompt-title Label@PROMPT_TEXT_A: X: 15 Y: 50 Width: PARENT_RIGHT - 30 Height: 16 Align: Center - Text: This pre-alpha build of OpenRA's Tiberian Sun mod is made available + Text: label-mainmenu-prerelease-notification-prompt-text-a Label@PROMPT_TEXT_B: X: 15 Y: 68 Width: PARENT_RIGHT - 30 Height: 16 Align: Center - Text: for the community to follow development and as example for modders. + Text: label-mainmenu-prerelease-notification-prompt-text-b Label@PROMPT_TEXT_C: X: 15 Y: 104 Width: PARENT_RIGHT - 30 Height: 16 Align: Center - Text: Many features are missing or incomplete, performance has not been + Text: label-mainmenu-prerelease-notification-prompt-text-c Label@PROMPT_TEXT_D: X: 15 Y: 122 Width: PARENT_RIGHT - 30 Height: 16 Align: Center - Text: optimized, and balance will not be addressed until a future beta. + Text: label-mainmenu-prerelease-notification-prompt-text-d Label@PROMPT_TEXT_E: X: 15 Y: 140 @@ -51,5 +51,6 @@ Background@MAINMENU_PRERELEASE_NOTIFICATION: Y: PARENT_BOTTOM - 45 Width: 120 Height: 25 - Text: I Understand + Text: button-mainmenu-prerelease-notification-continue Font: Bold + diff --git a/mods/ts/chrome/settings-hotkeys.yaml b/mods/ts/chrome/settings-hotkeys.yaml index bba1d2904393..9d018d622477 100644 --- a/mods/ts/chrome/settings-hotkeys.yaml +++ b/mods/ts/chrome/settings-hotkeys.yaml @@ -32,7 +32,7 @@ Container@HOTKEYS_PANEL: Width: 100 Height: 25 Font: Bold - Text: Filter by name: + Text: label-hotkeys-panel-filter-input TextField@FILTER_INPUT: X: 108 Width: 180 @@ -42,7 +42,7 @@ Container@HOTKEYS_PANEL: Width: 100 Height: 25 Font: Bold - Text: Context: + Text: label-hotkeys-panel-context-dropdown Align: Right DropDownButton@CONTEXT_DROPDOWN: X: PARENT_RIGHT - WIDTH @@ -94,7 +94,7 @@ Container@HOTKEYS_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Align: Center - Text: No hotkeys match the filter criteria. + Text: label-hotkey-empty-list-message Background@HOTKEY_REMAP_BGND: Y: PARENT_BOTTOM - HEIGHT Width: PARENT_RIGHT @@ -135,22 +135,22 @@ Container@HOTKEYS_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM Font: Tiny - Text: This hotkey cannot be modified + Text: label-notices-readonly-notice Button@OVERRIDE_HOTKEY_BUTTON: X: PARENT_RIGHT - 3 * WIDTH - 30 Y: 20 Width: 70 Height: 25 - Text: Override + Text: button-hotkey-remap-dialog-override Font: Bold Button@CLEAR_HOTKEY_BUTTON: X: PARENT_RIGHT - 2 * WIDTH - 30 Y: 20 Width: 65 Height: 25 - Text: Clear + Text: button-hotkey-remap-dialog-clear.label Font: Bold - TooltipText: Unbind the hotkey + TooltipText: button-hotkey-remap-dialog-clear.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP Button@RESET_HOTKEY_BUTTON: @@ -158,8 +158,9 @@ Container@HOTKEYS_PANEL: Y: 20 Width: 65 Height: 25 - Text: Reset + Text: button-hotkey-remap-dialog-reset.label Font: Bold - TooltipText: Reset to default + TooltipText: button-hotkey-remap-dialog-reset.tooltip TooltipContainer: SETTINGS_TOOLTIP_CONTAINER TooltipTemplate: SIMPLE_TOOLTIP + diff --git a/mods/ts/cursors.yaml b/mods/ts/cursors.yaml index 420867f0e35b..ea79316d8c37 100644 --- a/mods/ts/cursors.yaml +++ b/mods/ts/cursors.yaml @@ -188,7 +188,7 @@ Cursors: repair-blocked: Start: 190 Length: 1 - sell2: # TODO: unused + sell2: Start: 139 Length: 10 powerdown-blocked: diff --git a/mods/ts/languages/chrome/en.ftl b/mods/ts/languages/chrome/en.ftl new file mode 100644 index 000000000000..1db5b3e34960 --- /dev/null +++ b/mods/ts/languages/chrome/en.ftl @@ -0,0 +1,244 @@ +## assetbrowser.yaml +label-assetbrowser-panel-title = Asset Browser +label-assetbrowser-panel-source-selector-desc = Select asset source +dropdownbutton-assetbrowser-panel-source-selector = Folders +dropdownbutton-assetbrowser-panel-asset-types-dropdown = Asset types +label-assetbrowser-panel-filename-desc = Filter by name +label-assetbrowser-panel-sprite-scale = Scale: +label-assetbrowser-panel-model-scale = Scale: +label-assetbrowser-panel-palette-desc = Palette: +label-sprite-bg-error = Error displaying file. See assetbrowser.log for details. +label-voxel-selector-roll = Roll +label-voxel-selector-pitch = Pitch +label-voxel-selector-yaw = Yaw +button-assetbrowser-panel-close = Close + +## color-picker.yaml +button-color-chooser-random = Random +button-color-chooser-store = Store +button-color-chooser-mixer-tab = Mixer +button-color-chooser-palette-tab = Palette +label-preset-header = Preset Colors +label-custom-header = Custom Colors + +## ingame-debug.yaml +label-debug-panel-title = Debug Options +checkbox-debug-panel-instant-build = Instant Build Speed +checkbox-debug-panel-enable-tech = Build Everything +checkbox-debug-panel-build-anywhere = Build Anywhere +checkbox-debug-panel-unlimited-power = Unlimited Power +checkbox-debug-panel-instant-charge = Instant Charge Time +checkbox-debug-panel-disable-visibility-checks = Disable Visibility Checks +button-debug-panel-give-cash = Give $20,000 +button-debug-panel-grow-resources = Grow Resources +button-debug-panel-give-exploration = Clear Shroud +button-debug-panel-reset-exploration = Reset Shroud +label-debug-panel-visualizations-title = Visualizations +checkbox-debug-panel-show-unit-paths = Show Unit Paths +checkbox-debug-panel-show-customterrain-overlay = Show Custom Terrain +checkbox-debug-panel-show-actor-tags = Show Actor Tags +checkbox-debug-panel-show-depth-preview = Show Depth Data +checkbox-debug-panel-show-combatoverlay = Show Combat Geometry +checkbox-debug-panel-show-geometry = Show Render Geometry +checkbox-debug-panel-show-terrain-overlay = Show Terrain Geometry +checkbox-debug-panel-show-screenmap = Show Screen Map + +## ingame-observer.yaml +button-observer-widgets-options = Options (Esc) +button-replay-player-pause-tooltip = Pause +button-replay-player-play-tooltip = Play + +button-replay-player-slow = + .tooltip = Slow speed + .label = 50% + +button-replay-player-regular = + .tooltip = Regular speed + .label = 100% + +button-replay-player-fast = + .tooltip = Fast speed + .label = 200% + +button-replay-player-maximum = + .tooltip = Maximum speed + .label = MAX + +label-basic-stats-player-header = Player +label-basic-stats-cash-header = Cash +label-basic-stats-power-header = Power +label-basic-stats-kills-header = Kills +label-basic-stats-deaths-header = Deaths +label-basic-stats-assets-destroyed-header = Destroyed +label-basic-stats-assets-lost-header = Lost +label-basic-stats-experience-header = Score +label-basic-stats-actions-min-header = APM +label-economy-stats-player-header = Player +label-economy-stats-cash-header = Cash +label-economy-stats-income-header = Income +label-economy-stats-assets-header = Assets +label-economy-stats-earned-header = Earned +label-economy-stats-spent-header = Spent +label-economy-stats-harvesters-header = Harvesters +label-production-stats-player-header = Player +label-production-stats-header = Production +label-support-powers-player-header = Player +label-support-powers-header = Support Powers +label-army-player-header = Player +label-army-header = Army +label-combat-stats-player-header = Player +label-combat-stats-assets-destroyed-header = Destroyed +label-combat-stats-assets-lost-header = Lost +label-combat-stats-units-killed-header = U. Killed +label-combat-stats-units-dead-header = U. Lost +label-combat-stats-buildings-killed-header = B. Killed +label-combat-stats-buildings-dead-header = B. Lost +label-combat-stats-army-value-header = Army Value +label-combat-stats-vision-header = Vision + +## ingame-observer.yaml, ingame-player.yaml +label-mute-indicator = Audio Muted + +## ingame-player.yaml +supportpowers-support-powers-palette = + .ready = READY + .hold = ON HOLD + +button-command-bar-attack-move = + .tooltip = Attack Move + .tooltipdesc = Selected units will move to the desired location + and attack any enemies they encounter en route. + + Hold <(Ctrl)> while targeting to order an Assault Move + that attacks any units or structures encountered en route. + + Left-click icon then right-click on target location. + +button-command-bar-force-move = + .tooltip = Force Move + .tooltipdesc = Selected units will move to the desired location + - Default activity for the target is suppressed + - Vehicles will attempt to crush enemies at the target location + - Deployed units will undeploy and move to the target location + - Helicopters will land at the target location + + Left-click icon then right-click on target. + Hold <(Alt)> to activate temporarily while commanding units. + +button-command-bar-force-attack = + .tooltip = Force Attack + .tooltipdesc = Selected units will attack the targeted unit or location + - Default activity for the target is suppressed + - Allows targeting of own or ally forces + - Long-range artillery units will always target the + location, ignoring units and buildings + + Left-click icon then right-click on target. + Hold <(Ctrl)> to activate temporarily while commanding units. + +button-command-bar-guard = + .tooltip = Guard + .tooltipdesc = Selected units will follow the targeted unit. + + Left-click icon then right-click on target unit. + +button-command-bar-deploy = + .tooltip = Deploy + .tooltipdesc = Selected units will perform their default deploy activity + - MCVs will unpack into a Construction Yard + - Construction Yards will re-pack into a MCV + - Transports will unload their passengers + - Tick Tanks, Artillery, Juggernauts, + and Mobile Sensor arrays will deploy + - Aircraft will return to base + + Acts immediately on selected units. + +button-command-bar-scatter = + .tooltip = Scatter + .tooltipdesc = Selected units will stop their current activity + and move to a nearby location. + + Acts immediately on selected units. + +button-command-bar-stop = + .tooltip = Stop + .tooltipdesc = Selected units will stop their current activity. + Selected buildings will reset their rally point. + + Acts immediately on selected targets. + +button-command-bar-queue-orders = + .tooltip = Waypoint Mode + .tooltipdesc = Use Waypoint Mode to give multiple linking commands + to the selected units. Units will execute the commands + immediately upon receiving them. + + Left-click icon then give commands in the game world. + Hold <(Shift)> to activate temporarily while commanding units. + +button-stance-bar-attackanything = + .tooltip = Attack Anything Stance + .tooltipdesc = Set the selected units to Attack Anything stance: + - Units will attack enemy units and structures on sight + - Units will pursue attackers across the battlefield + +button-stance-bar-defend = + .tooltip = Defend Stance + .tooltipdesc = Set the selected units to Defend stance: + - Units will attack enemy units on sight + - Units will not move or pursue enemies + +button-stance-bar-returnfire = + .tooltip = Return Fire Stance + .tooltipdesc = Set the selected units to Return Fire stance: + - Units will retaliate against enemies that attack them + - Units will not move or pursue enemies + +button-stance-bar-holdfire = + .tooltip = Hold Fire Stance + .tooltipdesc = Set the selected units to Hold Fire stance: + - Units will not fire upon enemies + - Units will not move or pursue enemies + +button-top-buttons-repair-tooltip = Repair +button-top-buttons-sell-tooltip = Sell +button-top-buttons-beacon-tooltip = Place Beacon +button-top-buttons-power-tooltip = Power Down +button-top-buttons-options-tooltip = Options + +productionpalette-sidebar-production-palette = + .ready = READY + .hold = ON HOLD + +button-production-types-building-tooltip = Buildings +button-production-types-defense-tooltip = Support +button-production-types-infantry-tooltip = Infantry +button-production-types-vehicle-tooltip = Vehicles +button-production-types-aircraft-tooltip = Aircraft +button-production-types-scroll-up-tooltip = Scroll up +button-production-types-scroll-down-tooltip = Scroll down + +## mainmenu-prerelease-notification.yaml +label-mainmenu-prerelease-notification-prompt-title = Tiberian Sun developer preview +label-mainmenu-prerelease-notification-prompt-text-a = This pre-alpha build of OpenRA's Tiberian Sun mod is made available +label-mainmenu-prerelease-notification-prompt-text-b = for the community to follow development and as example for modders. +label-mainmenu-prerelease-notification-prompt-text-c = Many features are missing or incomplete, performance has not been +label-mainmenu-prerelease-notification-prompt-text-d = optimized, and balance will not be addressed until a future beta. +button-mainmenu-prerelease-notification-continue = I Understand + +## settings-hotkeys.yaml +label-hotkeys-panel-filter-input = Filter by name: +label-hotkeys-panel-context-dropdown = Context: +label-hotkey-empty-list-message = No hotkeys match the filter criteria. +label-notices-readonly-notice = This hotkey cannot be modified +button-hotkey-remap-dialog-override = Override + +button-hotkey-remap-dialog-clear = + .label = Clear + .tooltip = Unbind the hotkey + +button-hotkey-remap-dialog-reset = + .label = Reset + .tooltip = Reset to default + diff --git a/mods/ts/languages/rules/en.ftl b/mods/ts/languages/rules/en.ftl index 23d234bc37fe..f193cde92bc9 100644 --- a/mods/ts/languages/rules/en.ftl +++ b/mods/ts/languages/rules/en.ftl @@ -29,15 +29,13 @@ dropdown-map-creeps = .label = Creep Actors .description = Hostile forces spawn on the battlefield -options-difficulty = - .normal = Normal - ## Structures notification-construction-complete = Construction complete. notification-unit-ready = Unit ready. notification-unable-to-comply-building-in-progress = Unable to comply. Building in progress. notification-repairing = Repairing. notification-unit-repaired = Unit repaired. +notification-unit-sold = Unit sold. notification-ion-cannon-ready = Ion cannon ready. notification-select-target = Select target. notification-cluster-missile-ready = Cluster missile ready. diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index d24241a0361b..295aa1774dcd 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -178,7 +178,7 @@ ChromeLayout: ts|chrome/dropdowns.yaml common|chrome/musicplayer.yaml common|chrome/tooltips.yaml - common|chrome/assetbrowser.yaml + ts|chrome/assetbrowser.yaml common|chrome/missionbrowser.yaml common|chrome/confirmation-dialogs.yaml common|chrome/editor.yaml @@ -186,7 +186,9 @@ ChromeLayout: Translations: common|languages/en.ftl + common|languages/chrome/en.ftl common|languages/rules/en.ftl + ts|languages/chrome/en.ftl ts|languages/rules/en.ftl Voices: diff --git a/mods/ts/rules/defaults.yaml b/mods/ts/rules/defaults.yaml index 41a7c18bcb31..5c8a929b75eb 100644 --- a/mods/ts/rules/defaults.yaml +++ b/mods/ts/rules/defaults.yaml @@ -862,6 +862,16 @@ Sequence: vehicle Palette: player IsPlayerPalette: true + Sellable: + SellSounds: cashturn.aud + Cursor: sell2 + Notification: UnitSold + TextNotification: notification-unit-sold + RequiresCondition: unit.sellable + GrantConditionOnClientDock@Sellable: + Condition: unit.sellable + AfterDockDuration: 20 + DockHostNames: gadept ^Tank: Inherits: ^Vehicle @@ -949,6 +959,16 @@ RequiresCondition: cruising BobDistance: -64 InitialHeight: 64 + Sellable: + SellSounds: cashturn.aud + Cursor: sell2 + Notification: UnitSold + TextNotification: notification-unit-sold + RequiresCondition: unit.sellable + GrantConditionOnClientDock@Sellable: + Condition: unit.sellable + AfterDockDuration: 20 + DockHostNames: gadept ^EMPableAircraft: Inherits: ^Aircraft diff --git a/mods/ts/rules/gdi-structures.yaml b/mods/ts/rules/gdi-structures.yaml index 2017e6fe18d0..30367f24b699 100644 --- a/mods/ts/rules/gdi-structures.yaml +++ b/mods/ts/rules/gdi-structures.yaml @@ -356,7 +356,7 @@ GADEPT: BuildPaletteOrder: 70 Prerequisites: factory, ~structures.gdi, ~techlevel.medium Queue: Building - Description: Repairs vehicles. + Description: Repairs or sells vehicles and aircraft. Building: Footprint: =+= x++ x+= Dimensions: 3,3 @@ -431,6 +431,11 @@ GADEPT: FactionImages: gdi: gadept.gdi nod: gadept.nod + GrantConditionOnHostDock: + Condition: serving + AfterDockDuration: 20 + Sellable: + RequiresCondition: !serving && !build-incomplete && !being-demolished GARADR: Inherits: ^Building diff --git a/mods/ts/rules/gdi-vehicles.yaml b/mods/ts/rules/gdi-vehicles.yaml index 7b78764b4183..12e8508ca168 100644 --- a/mods/ts/rules/gdi-vehicles.yaml +++ b/mods/ts/rules/gdi-vehicles.yaml @@ -94,10 +94,8 @@ HVR: RequiresCondition: !empdisable BobDistance: -64 InitialHeight: 384 - Carryable: - CarriedCondition: carried LeavesTrails: - RequiresCondition: !inside-tunnel && !carried + RequiresCondition: !inside-tunnel Image: wake Palette: effect TerrainTypes: Water diff --git a/mods/ts/rules/palettes.yaml b/mods/ts/rules/palettes.yaml index aa424dadf46d..5c644a87538f 100644 --- a/mods/ts/rules/palettes.yaml +++ b/mods/ts/rules/palettes.yaml @@ -160,4 +160,4 @@ BasePalette: terraindecoration Name: terrainalpha Alpha: 0.55 - MenuPaletteEffect: + MenuPostProcessEffect: diff --git a/mods/ts/rules/shared-support.yaml b/mods/ts/rules/shared-support.yaml index d34251b7e7b7..9397a1337570 100644 --- a/mods/ts/rules/shared-support.yaml +++ b/mods/ts/rules/shared-support.yaml @@ -25,7 +25,7 @@ NAPULS: InitialFacing: 896 RealignDelay: -1 AttackTurreted: - RequiresCondition: !build-incomplete && !empdisable && !disabled + RequiresCondition: !build-incomplete && !empdisable && !disabled && ( support-targeting || support-attacking ) Armament: Weapon: EMPulseCannon LocalOffset: 212,0,1768 diff --git a/packaging/functions.sh b/packaging/functions.sh old mode 100755 new mode 100644 index 7f8e42cf0a2d..b205ae61209a --- a/packaging/functions.sh +++ b/packaging/functions.sh @@ -15,6 +15,7 @@ # COPY_GENERIC_LAUNCHER: If set to True the OpenRA.exe will also be copied (True, False) # COPY_CNC_DLL: If set to True the OpenRA.Mods.Cnc.dll will also be copied (True, False) # COPY_D2K_DLL: If set to True the OpenRA.Mods.D2k.dll will also be copied (True, False) +# COPY_AS_DLL: If set to True the OpenRA.Mods.AS.dll will also be copied (True, False) # Used by: # Makefile (install target for local installs and downstream packaging) # Windows packaging @@ -33,6 +34,7 @@ install_assemblies() ( COPY_GENERIC_LAUNCHER="${5}" COPY_CNC_DLL="${6}" COPY_D2K_DLL="${7}" + COPY_AS_DLL="${8}" ORIG_PWD=$(pwd) cd "${SRC_PATH}" @@ -59,6 +61,10 @@ install_assemblies() ( rm "${SRC_PATH}/bin/OpenRA.Mods.D2k.dll" fi + if [ "${COPY_AS_DLL}" != "True" ]; then + rm "${SRC_PATH}/bin/OpenRA.Mods.AS.dll" + fi + cd "${ORIG_PWD}" echo "Installing engine to ${DEST_PATH}" @@ -80,7 +86,7 @@ install_assemblies() ( done fi else - dotnet publish -c Release -p:TargetPlatform="${TARGETPLATFORM}" -p:CopyGenericLauncher="${COPY_GENERIC_LAUNCHER}" -p:CopyCncDll="${COPY_CNC_DLL}" -p:CopyD2kDll="${COPY_D2K_DLL}" -r "${TARGETPLATFORM}" -p:PublishDir="${DEST_PATH}" --self-contained true + dotnet publish -c Release -p:TargetPlatform="${TARGETPLATFORM}" -p:CopyGenericLauncher="${COPY_GENERIC_LAUNCHER}" -p:CopyCncDll="${COPY_CNC_DLL}" -p:CopyD2kDll="${COPY_D2K_DLL}" -p:CopyASDll="${COPY_AS_DLL}" -r "${TARGETPLATFORM}" -p:PublishDir="${DEST_PATH}" --self-contained true fi cd "${ORIG_PWD}" )