diff --git a/OpenRA.Game/Graphics/MarkerTileRenderable.cs b/OpenRA.Game/Graphics/MarkerTileRenderable.cs new file mode 100644 index 0000000000..c74a236980 --- /dev/null +++ b/OpenRA.Game/Graphics/MarkerTileRenderable.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 System.Linq; +using OpenRA.Primitives; + +namespace OpenRA.Graphics +{ + public class MarkerTileRenderable : IRenderable, IFinalizedRenderable + { + readonly CPos pos; + readonly Color color; + + public MarkerTileRenderable(CPos pos, Color color) + { + this.pos = pos; + this.color = color; + } + + public WPos Pos => WPos.Zero; + public int ZOffset => 0; + public bool IsDecoration => true; + + public IRenderable WithZOffset(int newOffset) { return this; } + + public IRenderable OffsetBy(in WVec vec) + { + return new MarkerTileRenderable(pos, color); + } + + public IRenderable AsDecoration() { return this; } + + public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; } + public void Render(WorldRenderer wr) + { + var map = wr.World.Map; + var r = map.Grid.Ramps[map.Ramp[pos]]; + var wpos = map.CenterOfCell(pos) - new WVec(0, 0, r.CenterHeightOffset); + + var corners = r.Corners.Select(corner => wr.Viewport.WorldToViewPx(wr.Screen3DPosition(wpos + corner))).ToList(); + + Game.Renderer.RgbaColorRenderer.FillRect(corners[0], corners[1], corners[2], corners[3], color); + } + + public void RenderDebugGeometry(WorldRenderer wr) { } + public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; } + } +} diff --git a/OpenRA.Mods.Common/EditorBrushes/EditorMarkerLayerBrush.cs b/OpenRA.Mods.Common/EditorBrushes/EditorMarkerLayerBrush.cs new file mode 100644 index 0000000000..95d1d58d62 --- /dev/null +++ b/OpenRA.Mods.Common/EditorBrushes/EditorMarkerLayerBrush.cs @@ -0,0 +1,234 @@ +#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.Graphics; +using OpenRA.Mods.Common.Traits; + +namespace OpenRA.Mods.Common.Widgets +{ + public sealed class EditorMarkerLayerBrush : IEditorBrush + { + public int? Template; + + readonly WorldRenderer worldRenderer; + readonly World world; + readonly EditorActionManager editorActionManager; + readonly MarkerLayerOverlay markerLayerOverlay; + readonly EditorViewportControllerWidget editorWidget; + + PaintMarkerTileEditorAction action; + bool painting; + + public EditorMarkerLayerBrush(EditorViewportControllerWidget editorWidget, int? id, WorldRenderer wr) + { + this.editorWidget = editorWidget; + worldRenderer = wr; + world = wr.World; + + editorActionManager = world.WorldActor.Trait(); + markerLayerOverlay = world.WorldActor.Trait(); + + Template = id; + worldRenderer = wr; + world = wr.World; + action = new PaintMarkerTileEditorAction(Template, markerLayerOverlay); + } + + public bool HandleMouseInput(MouseInput mi) + { + if (mi.Button != MouseButton.Left && mi.Button != MouseButton.Right) + return false; + + if (mi.Button == MouseButton.Right) + { + if (mi.Event == MouseInputEvent.Up) + { + editorWidget.ClearBrush(); + return true; + } + + return false; + } + + var cell = worldRenderer.Viewport.ViewToWorld(mi.Location); + + if (mi.Button == MouseButton.Left && mi.Event != MouseInputEvent.Up) + { + action.Add(cell); + painting = true; + } + else if (painting && mi.Button == MouseButton.Left && mi.Event == MouseInputEvent.Up) + { + if (action.DidPaintTiles) + editorActionManager.Add(action); + + action = new PaintMarkerTileEditorAction(Template, markerLayerOverlay); + painting = false; + } + + return true; + } + + public void Tick() { } + + public void Dispose() { } + } + + readonly struct PaintMarkerTile + { + public readonly CPos Cell; + public readonly int? Previous; + + public PaintMarkerTile(CPos cell, int? previous) + { + Cell = cell; + Previous = previous; + } + } + + class PaintMarkerTileEditorAction : IEditorAction + { + [TranslationReference("amount", "type")] + const string AddedMarkerTiles = "notification-added-marker-tiles"; + + [TranslationReference("amount")] + const string RemovedMarkerTiles = "notification-removed-marker-tiles"; + + public string Text { get; private set; } + + readonly int? type; + readonly MarkerLayerOverlay markerLayerOverlay; + + readonly List paintTiles = new(); + + public bool DidPaintTiles => paintTiles.Count > 0; + + public PaintMarkerTileEditorAction( + int? type, + MarkerLayerOverlay markerLayerOverlay) + { + this.markerLayerOverlay = markerLayerOverlay; + this.type = type; + } + + public void Execute() + { + } + + public void Do() + { + foreach (var paintTile in paintTiles) + markerLayerOverlay.SetTile(paintTile.Cell, type); + } + + public void Undo() + { + foreach (var paintTile in paintTiles) + markerLayerOverlay.SetTile(paintTile.Cell, paintTile.Previous); + } + + public void Add(CPos target) + { + foreach (var cell in markerLayerOverlay.CalculateMirrorPositions(target)) + { + var existing = markerLayerOverlay.CellLayer[cell]; + if (existing == type) + continue; + + paintTiles.Add(new PaintMarkerTile(cell, existing)); + markerLayerOverlay.SetTile(cell, type); + } + + if (type != null) + Text = TranslationProvider.GetString(AddedMarkerTiles, Translation.Arguments("amount", paintTiles.Count, "type", type)); + else + Text = TranslationProvider.GetString(RemovedMarkerTiles, Translation.Arguments("amount", paintTiles.Count)); + } + } + + class ClearSelectedMarkerTilesEditorAction : IEditorAction + { + [TranslationReference("amount", "type")] + const string ClearedSelectedMarkerTiles = "notification-cleared-selected-marker-tiles"; + + public string Text { get; } + + readonly MarkerLayerOverlay markerLayerOverlay; + readonly HashSet tiles; + readonly int tile; + + public ClearSelectedMarkerTilesEditorAction( + int tile, + MarkerLayerOverlay markerLayerOverlay) + { + this.tile = tile; + this.markerLayerOverlay = markerLayerOverlay; + + tiles = new HashSet(markerLayerOverlay.Tiles[tile]); + + Text = TranslationProvider.GetString(ClearedSelectedMarkerTiles, Translation.Arguments("amount", tiles.Count, "type", tile)); + } + + public void Execute() + { + Do(); + } + + public void Do() + { + markerLayerOverlay.ClearSelected(tile); + } + + public void Undo() + { + markerLayerOverlay.SetSelected(tile, tiles); + } + } + + class ClearAllMarkerTilesEditorAction : IEditorAction + { + [TranslationReference("amount")] + const string ClearedAllMarkerTiles = "notification-cleared-all-marker-tiles"; + + public string Text { get; } + + readonly MarkerLayerOverlay markerLayerOverlay; + readonly Dictionary> tiles; + + public ClearAllMarkerTilesEditorAction( + MarkerLayerOverlay markerLayerOverlay) + { + this.markerLayerOverlay = markerLayerOverlay; + tiles = new Dictionary>(markerLayerOverlay.Tiles); + + var allTilesCount = tiles.Values.Select(x => x.Count).Sum(); + + Text = TranslationProvider.GetString(ClearedAllMarkerTiles, Translation.Arguments("amount", allTilesCount)); + } + + public void Execute() + { + Do(); + } + + public void Do() + { + markerLayerOverlay.ClearAll(); + } + + public void Undo() + { + markerLayerOverlay.SetAll(tiles); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/World/MarkerLayerOverlay.cs b/OpenRA.Mods.Common/Traits/World/MarkerLayerOverlay.cs new file mode 100644 index 0000000000..5ed0ca9d9d --- /dev/null +++ b/OpenRA.Mods.Common/Traits/World/MarkerLayerOverlay.cs @@ -0,0 +1,471 @@ +#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.IO; +using System.Linq; +using Newtonsoft.Json; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Traits; +using Color = OpenRA.Primitives.Color; + +namespace OpenRA.Mods.Common.Traits +{ + [TraitLocation(SystemActors.EditorWorld)] + public class MarkerLayerOverlayInfo : TraitInfo + { + [Desc("A list of colors to be used for drawing.")] + public readonly Color[] Colors = new[] + { + Color.FromArgb(255, 0, 0), + Color.FromArgb(255, 127, 0), + Color.FromArgb(255, 238, 70), + Color.FromArgb(0, 255, 33), + Color.FromArgb(0, 255, 255), + Color.FromArgb(0, 42, 255), + Color.FromArgb(165, 0, 255), + Color.FromArgb(255, 0, 220), + }; + + [Desc("Default alpha blend.")] + public readonly int Alpha = 85; + + [Desc("Color of the axis angle display.")] + public readonly Color AxisAngleColor = Color.Crimson; + + public override object Create(ActorInitializer init) + { + return new MarkerLayerOverlay(init.Self, this); + } + } + + public class MarkerLayerOverlay : IRenderAnnotations, INotifyActorDisposing, IWorldLoaded + { + public class MarkerLayerFile + { + public Dictionary> Tiles { get; set; } + public MarkerTileMirrorMode MirrorMode { get; set; } + public int NumSides { get; set; } + public int AxisAngle { get; set; } + public int TileAlpha { get; set; } + } + + const double DegreesToRadians = Math.PI / 180; + + readonly int[] validFlipModeSides = { 2, 4 }; + + public enum MarkerTileMirrorMode + { + None, + Flip, + Rotate + } + + readonly World world; + readonly WPos mapCenter; + readonly Color[] alphaBlendColors; + + public readonly CellLayer CellLayer; + public readonly Dictionary> Tiles = new(); + + public bool Enabled = true; + public MarkerTileMirrorMode MirrorMode { get; private set; } = MarkerTileMirrorMode.None; + public MarkerLayerOverlayInfo Info { get; } + public int NumSides = 2; + public int AxisAngle; + public int TileAlpha + { + get => tileAlpha; + set + { + tileAlpha = value; + UpdateTileAlpha(); + } + } + + int tileAlpha; + bool disposed; + + public MarkerLayerOverlay(Actor self, MarkerLayerOverlayInfo info) + { + Info = info; + world = self.World; + var map = self.World.Map; + + tileAlpha = info.Alpha; + alphaBlendColors = new Color[info.Colors.Length]; + UpdateTileAlpha(); + + CellLayer = new CellLayer(map); + + mapCenter = GetMapCenterWPos(); + } + + public void WorldLoaded(World w, WorldRenderer wr) + { + try + { + var modData = Game.ModData; + var mod = modData.Manifest.Metadata; + var directory = Path.Combine(Platform.SupportDir, "Editor", modData.Manifest.Id, mod.Version, "MarkerTiles"); + if (!Directory.Exists(directory)) + return; + + if (string.IsNullOrWhiteSpace(world.Map.Package.Name)) + return; + + var markerTileFilename = $"{Path.GetFileNameWithoutExtension(world.Map.Package.Name)}.json"; + var markerTilePath = Path.Combine(directory, markerTileFilename); + if (!File.Exists(markerTilePath)) + return; + + using (var streamReader = new StreamReader(markerTilePath)) + { + var content = streamReader.ReadToEnd(); + var file = JsonConvert.DeserializeObject(content); + + TileAlpha = file.TileAlpha; + MirrorMode = file.MirrorMode; + NumSides = file.NumSides; + AxisAngle = file.AxisAngle; + + var savedTilesHashSetDictionary = file.Tiles.ToDictionary(x => x.Key, x => x.Value.Select(bits => new CPos(bits)).ToHashSet()); + SetAll(savedTilesHashSetDictionary); + } + } + catch (Exception e) + { + Log.Write("debug", "Failed to load map editor marker tiles."); + Log.Write("debug", e); + } + } + + public MarkerLayerFile ToFile() + { + var tilesBitsDictionary = Tiles.ToDictionary(x => x.Key, x => x.Value.Select(cpos => cpos.Bits).ToList()); + return new MarkerLayerFile + { + Tiles = tilesBitsDictionary, + TileAlpha = TileAlpha, + MirrorMode = MirrorMode, + NumSides = NumSides, + AxisAngle = AxisAngle, + }; + } + + void UpdateTileAlpha() + { + for (var i = 0; i < Info.Colors.Length; i++) + alphaBlendColors[i] = Color.FromArgb(tileAlpha, Info.Colors[i]); + } + + public void ClearSelected(int tileType) + { + if (Tiles.TryGetValue(tileType, out var set)) + foreach (var pos in set) + SetTile(pos, null); + } + + public void ClearAll() + { + foreach (var position in Tiles.SelectMany(x => x.Value)) + CellLayer[position] = null; + + Tiles.Clear(); + } + + public void SetAll(Dictionary> newTiles) + { + ClearAll(); + + foreach (var type in newTiles) + { + var set = new HashSet(); + Tiles.Add(type.Key, set); + + foreach (var position in type.Value) + { + if (!world.Map.Contains(position)) + continue; + + set.Add(position); + CellLayer[position] = type.Key; + } + } + } + + public void SetSelected(int tile, HashSet newTiles) + { + var type = Tiles[tile]; + foreach (var pos in type) + SetTile(pos, null); + + type.Clear(); + + foreach (var pos in newTiles) + { + type.Add(pos); + CellLayer[pos] = tile; + } + } + + public void SetMirrorMode(MarkerTileMirrorMode mirrorMode) + { + MirrorMode = mirrorMode; + + if (mirrorMode == MarkerTileMirrorMode.Flip && !validFlipModeSides.Contains(NumSides)) + NumSides = validFlipModeSides[0]; + } + + WPos GetMapCenterWPos() + { + var map = world.Map; + + var boundsWidth = map.AllCells.BottomRight.X - map.AllCells.TopLeft.X; + var boundsHeight = map.AllCells.BottomRight.Y - map.AllCells.TopLeft.Y; + + var xIsOdd = boundsWidth % 2 != 0; + var yIsOdd = boundsHeight % 2 != 0; + + var xCenter = boundsWidth / 2; + var yCenter = boundsHeight / 2; + + var centerWpos = map.CenterOfCell(new CPos(xCenter, yCenter)); + if (xIsOdd) + centerWpos += new WVec(512, 0, 0); + + if (yIsOdd) + centerWpos += new WVec(0, 512, 0); + + return centerWpos; + } + + public CPos[] CalculateMirrorPositions(CPos cell) + { + const int DegreesInCircle = 360; + + var map = world.Map; + + var wpos = map.CenterOfCell(cell); + var wposVec = wpos - mapCenter; + var angle = DegreesInCircle / NumSides; + + var targets = new List(); + + if (map.Contains(cell)) + targets.Add(cell); + + if (MirrorMode == MarkerTileMirrorMode.Flip) + { + var startAxis = new WVec(1024, 0, 0); + var axes = new List(); + for (var i = 0; i < NumSides / 2; i++) + { + var targetAngle = (i * angle + AxisAngle) * DegreesToRadians; + var point = new WVec((int)(startAxis.X * Math.Cos(targetAngle) - startAxis.Y * Math.Sin(targetAngle)), + (int)(startAxis.X * Math.Sin(targetAngle) + startAxis.Y * Math.Cos(targetAngle)), + wpos.Z); + + axes.Add(point); + } + + foreach (var axis in axes) + { + var point = GetAxisMirrorPoint(mapCenter, axis, wpos); + var cellPoint = map.CellContaining(point); + + if (map.Contains(cellPoint)) + targets.Add(cellPoint); + } + + // Mirror twice for both + if (axes.Count == 2) + { + var point = GetAxisMirrorPoint(mapCenter, axes[0], wpos); + point = GetAxisMirrorPoint(mapCenter, axes[1], point); + var cellPoint = map.CellContaining(point); + + if (map.Contains(cellPoint)) + targets.Add(cellPoint); + } + + /////////////// + + static WPos GetAxisMirrorPoint(WPos center, WVec axis, WPos point) + { + var testPoint = center - new WVec(point.X, point.Y, 0); + var a = axis.Y; + var b = -axis.X; + var c = -a * 0 - b * 0; + + var m = Math.Sqrt(a * a + b * b); + var aDash = a / m; + var bDash = b / m; + var cDash = c / m; + + var d = aDash * testPoint.X + bDash * testPoint.Y + cDash; + var pxDash = testPoint.X - 2 * aDash * d; + var pyDash = testPoint.Y - 2 * bDash * d; + + return new WPos((int)pxDash + center.X, (int)pyDash + center.Y, 0); + } + } + else if (MirrorMode == MarkerTileMirrorMode.Rotate) + { + // Rotate + var flipAngleRadians = DegreesToRadians * angle; + + var sidesAreEven = NumSides % 2 == 0; + var oddSideStartIndex = (int)Math.Floor((double)NumSides / 2); + var startIndex = sidesAreEven ? 0 : -oddSideStartIndex; + var count = sidesAreEven ? NumSides : oddSideStartIndex + 1; + + for (var i = startIndex; i < count; i++) + { + var targetAngle = i * flipAngleRadians; + var point = new WPos((int)(wposVec.X * Math.Cos(targetAngle) - wposVec.Y * Math.Sin(targetAngle)), + (int)(wposVec.X * Math.Sin(targetAngle) + wposVec.Y * Math.Cos(targetAngle)), + wpos.Z); + + var cellPoint = map.CellContaining(point + new WVec(mapCenter.X, mapCenter.Y, 0)); + + if (map.Contains(cellPoint)) + targets.Add(cellPoint); + } + } + + return targets.ToArray(); + } + + public void SetTile(CPos target, int? tileType) + { + if (!world.Map.Contains(target)) + return; + + // Maintain map of tile types for selective clearing + var prevTile = CellLayer[target]; + if (prevTile.HasValue && Tiles.TryGetValue(prevTile.Value, out var set)) + set.Remove(target); + + if (tileType.HasValue) + { + if (Tiles.TryGetValue(tileType.Value, out set)) + set.Add(target); + else + Tiles.Add(tileType.Value, new HashSet { target }); + + CellLayer[target] = tileType; + } + else + CellLayer[target] = null; + } + + void INotifyActorDisposing.Disposing(Actor self) + { + if (disposed) + return; + + disposed = true; + } + + readonly struct MapLine + { + public readonly float2 Start; + public readonly float2 End; + + public MapLine(float2 start, float2 end) + { + Start = start; + End = end; + } + } + + IEnumerable IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr) + { + if (!Enabled) + yield break; + + foreach (var cellPair in Tiles) + foreach (var cellPos in cellPair.Value) + yield return new MarkerTileRenderable(cellPos, alphaBlendColors[cellPair.Key]); + + if (MirrorMode != MarkerTileMirrorMode.Flip) + yield break; + + const int LineWidth = 1; + + var color = Info.AxisAngleColor; + var targetAngle = AxisAngle * DegreesToRadians; + + var mapCenterFloat = new float2(mapCenter.X, mapCenter.Y); + var mapBoundsWorldSize = mapCenterFloat * 2; + + // Create our axis lines + var horizontalVec = new float2(1, 0); + var verticalVec = new float2(0, 1); + var edges = new[] + { + new MapLine(mapCenterFloat, mapCenterFloat + horizontalVec), + new MapLine(mapCenterFloat, mapCenterFloat + verticalVec), + }; + + var sourceAxes = new[] { verticalVec, -verticalVec, horizontalVec, -horizontalVec }; + for (var i = 0; i < NumSides; i++) + { + var isOpposite = i % 2 != 0; + var sourceAxis = sourceAxes[i]; + var rotatedAxis = new float2( + (float)(sourceAxis.X * Math.Cos(targetAngle) - sourceAxis.Y * Math.Sin(targetAngle)), + (float)(sourceAxis.X * Math.Sin(targetAngle) + sourceAxis.Y * Math.Cos(targetAngle))); + + var axisLine = new MapLine(float2.Zero, rotatedAxis); + var collisionPoints = FindEdgeCollisionPoints(edges, axisLine); + + var closestCollisionPoint = collisionPoints.OrderBy(x => x.LengthSquared).First(); + if (isOpposite) + closestCollisionPoint *= -1; + + var resultPos = new WVec((int)closestCollisionPoint.X, (int)closestCollisionPoint.Y, 0); + yield return new LineAnnotationRenderable(mapCenter, mapCenter + resultPos, LineWidth, color, color); + } + } + + static float2[] FindEdgeCollisionPoints(MapLine[] mapEdges, MapLine axis) + { + var collisionResults = new List(); + foreach (var mapEdge in mapEdges) + if (FindIntersection(axis.Start, axis.End, mapEdge.Start, mapEdge.End, out var collisionVec)) + collisionResults.Add(collisionVec); + + return collisionResults.ToArray(); + } + + static bool FindIntersection(float2 a1, float2 a2, float2 b1, float2 b2, out float2 result) + { + result = float2.Zero; + var d = (a1.X - a2.X) * (b1.Y - b2.Y) - (a1.Y - a2.Y) * (b1.X - b2.X); + + // check if lines are parallel + if (d == 0) + return false; + + var px = (a1.X * a2.Y - a1.Y * a2.X) * (b1.X - b2.X) - (a1.X - a2.X) * (b1.X * b2.Y - b1.Y * b2.X); + var py = (a1.X * a2.Y - a1.Y * a2.X) * (b1.Y - b2.Y) - (a1.Y - a2.Y) * (b1.X * b2.Y - b1.Y * b2.X); + + result = new float2(px, py) / d; + return true; + } + + bool IRenderAnnotations.SpatiallyPartitionable => false; + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20231010/AddMarkerLayerOverlay.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20231010/AddMarkerLayerOverlay.cs new file mode 100644 index 0000000000..8158c51467 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20231010/AddMarkerLayerOverlay.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 AddMarkerLayerOverlay : UpdateRule + { + public override string Name => "Add MarkerLayerOverlay."; + + public override string Description => + "Add MarkerLayerOverlay to editor."; + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNodeBuilder actorNode) + { + var editorWorldNode = actorNode.LastChildMatching("EditorWorld"); + if (editorWorldNode == null) + yield break; + + var markerLayerOverlayNode = new MiniYamlNodeBuilder("MarkerLayerOverlay", new MiniYamlBuilder("")); + editorWorldNode.AddNode(markerLayerOverlayNode); + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs index 83aa46131e..bb504b32b3 100644 --- a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs +++ b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs @@ -86,6 +86,7 @@ namespace OpenRA.Mods.Common.UpdateRules new ReplacePaletteModifiers(), new RemoveConyardChronoReturnAnimation(), new RemoveEditorSelectionLayerProperties(), + new AddMarkerLayerOverlay(), // Execute these rules last to avoid premature yaml merge crashes. new ReplaceCloakPalette(), diff --git a/OpenRA.Mods.Common/Widgets/EditorViewportControllerWidget.cs b/OpenRA.Mods.Common/Widgets/EditorViewportControllerWidget.cs index de7430b114..0eeb87becf 100644 --- a/OpenRA.Mods.Common/Widgets/EditorViewportControllerWidget.cs +++ b/OpenRA.Mods.Common/Widgets/EditorViewportControllerWidget.cs @@ -23,6 +23,8 @@ namespace OpenRA.Mods.Common.Widgets public readonly string TooltipTemplate; public readonly EditorDefaultBrush DefaultBrush; + public event Action BrushChanged; + readonly Lazy tooltipContainer; readonly WorldRenderer worldRenderer; @@ -46,6 +48,8 @@ namespace OpenRA.Mods.Common.Widgets CurrentBrush?.Dispose(); CurrentBrush = brush ?? DefaultBrush; + + BrushChanged?.Invoke(); } public override void MouseEntered() diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/MapMarkerTilesLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapMarkerTilesLogic.cs new file mode 100644 index 0000000000..dab7e52fdd --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapMarkerTilesLogic.cs @@ -0,0 +1,259 @@ +#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.Traits; +using OpenRA.Widgets; +using static OpenRA.Mods.Common.Traits.MarkerLayerOverlay; + +namespace OpenRA.Mods.Common.Widgets.Logic +{ + public class MapMarkerTilesLogic : ChromeLogic + { + [TranslationReference] + const string MarkerMirrorModeNoneTranslation = "mirror-mode.none"; + + [TranslationReference] + const string MarkerMirrorModeFlipTranslation = "mirror-mode.flip"; + + [TranslationReference] + const string MarkerMirrorModeRotateTranslation = "mirror-mode.rotate"; + + readonly EditorActionManager editorActionManager; + readonly MarkerLayerOverlay markerLayerTrait; + readonly ScrollPanelWidget tileColorPanel; + readonly SliderWidget alphaSlider; + readonly LabelWidget alphaValueLabel; + readonly DropDownButtonWidget modeDropdown; + readonly SliderWidget rotateNumSidesSlider; + readonly DropDownButtonWidget flipNumSidesDropdown; + readonly LabelWidget numSidesLabel; + readonly LabelWidget rotateNumSidesValueLabel; + readonly LabelWidget axisAngleLabel; + readonly SliderWidget axisAngleSlider; + readonly LabelWidget axisAngleValueLabel; + readonly ButtonWidget clearSelectedButtonWidget; + readonly ButtonWidget clearAllButtonWidget; + readonly EditorViewportControllerWidget editor; + + int? markerTile; + + [ObjectCreator.UseCtor] + public MapMarkerTilesLogic(Widget widget, World world, ModData modData, WorldRenderer worldRenderer, Dictionary logicArgs) + { + markerLayerTrait = world.WorldActor.Trait(); + editorActionManager = world.WorldActor.Trait(); + + editor = widget.Parent.Parent.Parent.Parent.Get("MAP_EDITOR"); + editor.BrushChanged += HandleBrushChanged; + + tileColorPanel = widget.Get("TILE_COLOR_PANEL"); + { + tileColorPanel.Layout = new GridLayout(tileColorPanel); + var colorSwatchTemplate = tileColorPanel.Get("TILE_COLOR_TEMPLATE"); + var iconTemplate = tileColorPanel.Get("TILE_ICON_TEMPLATE"); + tileColorPanel.RemoveChildren(); + + var colors = markerLayerTrait.Info.Colors; + for (var colorIndex = 0; colorIndex < colors.Length; colorIndex++) + { + var scrollItem = SetupColorSwatchItem(colorIndex, colorSwatchTemplate); + tileColorPanel.AddChild(scrollItem); + } + + var eraseItem = SetupEraseItem(iconTemplate); + tileColorPanel.AddChild(eraseItem); + + /////// + + ScrollItemWidget SetupColorSwatchItem(int index, ScrollItemWidget template) + { + var item = ScrollItemWidget.Setup(template, + () => markerTile == index, + () => + { + markerTile = index; + editor.SetBrush(new EditorMarkerLayerBrush(editor, index, worldRenderer)); + }); + + var colorWidget = item.Get("TILE_PREVIEW"); + colorWidget.GetColor = () => colors[index]; + + return item; + } + + ScrollItemWidget SetupEraseItem(ScrollItemWidget template) + { + var item = ScrollItemWidget.Setup(template, + () => markerTile == null && editor.CurrentBrush != null && editor.CurrentBrush is EditorMarkerLayerBrush, + () => + { + markerTile = null; + editor.SetBrush(new EditorMarkerLayerBrush(editor, null, worldRenderer)); + }); + + return item; + } + } + + clearSelectedButtonWidget = widget.Get("CLEAR_CURRENT_BUTTON"); + clearSelectedButtonWidget.IsDisabled = () => markerTile == null; + clearSelectedButtonWidget.OnClick = ClearSelected; + + clearAllButtonWidget = widget.Get("CLEAR_ALL_BUTTON"); + clearAllButtonWidget.OnClick = ClearAll; + + alphaSlider = widget.Get("ALPHA_SLIDER"); + alphaSlider.MinimumValue = 1; + alphaSlider.MaximumValue = 255; + alphaSlider.Ticks = 12; + alphaSlider.OnChange += (val) => markerLayerTrait.TileAlpha = (int)val; + alphaSlider.GetValue = () => markerLayerTrait.TileAlpha; + + alphaValueLabel = widget.Get("ALPHA_VALUE"); + alphaValueLabel.GetText = () => markerLayerTrait.TileAlpha.ToString(NumberFormatInfo.InvariantInfo); + + modeDropdown = widget.Get("MODE_DROPDOWN"); + modeDropdown.OnMouseDown = _ => ShowMarkerModeDropDown(modeDropdown); + modeDropdown.GetText = () => + { + switch (markerLayerTrait.MirrorMode) + { + case MarkerTileMirrorMode.None: + return TranslationProvider.GetString(MarkerMirrorModeNoneTranslation); + case MarkerTileMirrorMode.Flip: + return TranslationProvider.GetString(MarkerMirrorModeFlipTranslation); + case MarkerTileMirrorMode.Rotate: + return TranslationProvider.GetString(MarkerMirrorModeRotateTranslation); + default: + throw new ArgumentException($"Couldn't find translation for marker tile mirror mode '{markerLayerTrait.MirrorMode}'"); + } + }; + + bool IsFlipMode() => markerLayerTrait.MirrorMode == MarkerTileMirrorMode.Flip; + bool IsRotateMode() => markerLayerTrait.MirrorMode == MarkerTileMirrorMode.Rotate; + + numSidesLabel = widget.Get("NUM_SIDES_LABEL"); + numSidesLabel.IsVisible = () => IsFlipMode() || IsRotateMode(); + + rotateNumSidesSlider = widget.Get("ROTATE_NUM_SIDES_SLIDER"); + rotateNumSidesSlider.MinimumValue = 2; + rotateNumSidesSlider.MaximumValue = 8; + rotateNumSidesSlider.Ticks = 7; + rotateNumSidesSlider.IsVisible = IsRotateMode; + rotateNumSidesSlider.OnChange += (val) => markerLayerTrait.NumSides = (int)val; + rotateNumSidesSlider.GetValue = () => markerLayerTrait.NumSides; + + rotateNumSidesValueLabel = widget.Get("ROTATE_NUM_SIDES_VALUE"); + rotateNumSidesValueLabel.IsVisible = IsRotateMode; + rotateNumSidesValueLabel.GetText = () => markerLayerTrait.NumSides.ToString(NumberFormatInfo.InvariantInfo); + + flipNumSidesDropdown = widget.Get("FLIP_NUM_SIDES_DROPDOWN"); + flipNumSidesDropdown.OnMouseDown = _ => ShowFlipNumSidesDropDown(flipNumSidesDropdown); + flipNumSidesDropdown.IsVisible = IsFlipMode; + flipNumSidesDropdown.GetText = () => markerLayerTrait.NumSides.ToString(NumberFormatInfo.InvariantInfo); + + axisAngleLabel = widget.Get("AXIS_ANGLE_LABEL"); + axisAngleLabel.IsVisible = IsFlipMode; + + axisAngleSlider = widget.Get("AXIS_ANGLE_SLIDER"); + axisAngleSlider.MinimumValue = 0; + axisAngleSlider.MaximumValue = 11; + axisAngleSlider.Ticks = 12; + axisAngleSlider.IsVisible = IsFlipMode; + axisAngleSlider.OnChange += (val) => markerLayerTrait.AxisAngle = (int)val * 15; + axisAngleSlider.GetValue = () => markerLayerTrait.AxisAngle / 15; + + axisAngleValueLabel = widget.Get("AXIS_ANGLE_VALUE"); + axisAngleValueLabel.IsVisible = IsFlipMode; + axisAngleValueLabel.GetText = () => markerLayerTrait.AxisAngle.ToString(NumberFormatInfo.InvariantInfo); + } + + protected override void Dispose(bool disposing) + { + editor.BrushChanged -= HandleBrushChanged; + base.Dispose(disposing); + } + + void HandleBrushChanged() + { + if (editor.CurrentBrush is not EditorMarkerLayerBrush) + { + markerTile = null; + } + } + + void ClearSelected() + { + if (editor.CurrentBrush is EditorMarkerLayerBrush markerLayerBrush && + markerLayerBrush.Template.HasValue && + markerLayerTrait.Tiles.TryGetValue(markerLayerBrush.Template.Value, out var tiles) && + tiles.Count > 0) + editorActionManager.Add(new ClearSelectedMarkerTilesEditorAction(markerLayerBrush.Template.Value, markerLayerTrait)); + } + + void ClearAll() + { + if (markerLayerTrait.Tiles.Count > 0 && markerLayerTrait.Tiles.Any(x => x.Value.Count > 0)) + editorActionManager.Add(new ClearAllMarkerTilesEditorAction(markerLayerTrait)); + } + + void ShowMarkerModeDropDown(DropDownButtonWidget dropdown) + { + ScrollItemWidget SetupItem(MarkerTileMirrorMode mode, ScrollItemWidget itemTemplate) + { + var item = ScrollItemWidget.Setup(itemTemplate, + () => markerLayerTrait.MirrorMode == mode, + () => markerLayerTrait.SetMirrorMode(mode)); + + item.Get("LABEL").GetText = () => + { + switch (mode) + { + case MarkerTileMirrorMode.None: + return TranslationProvider.GetString(MarkerMirrorModeNoneTranslation); + case MarkerTileMirrorMode.Flip: + return TranslationProvider.GetString(MarkerMirrorModeFlipTranslation); + case MarkerTileMirrorMode.Rotate: + return TranslationProvider.GetString(MarkerMirrorModeRotateTranslation); + default: + throw new ArgumentException($"Couldn't find translation for marker tile mirror mode '{mode}'"); + } + }; + + return item; + } + + var options = new[] { MarkerTileMirrorMode.None, MarkerTileMirrorMode.Flip, MarkerTileMirrorMode.Rotate }; + dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 150, options, SetupItem); + } + + void ShowFlipNumSidesDropDown(DropDownButtonWidget dropdown) + { + ScrollItemWidget SetupItem(int value, ScrollItemWidget itemTemplate) + { + var item = ScrollItemWidget.Setup(itemTemplate, + () => markerLayerTrait.NumSides == value, + () => markerLayerTrait.NumSides = value); + + item.Get("LABEL").GetText = () => value.ToString(NumberFormatInfo.InvariantInfo); + return item; + } + + var options = new[] { 2, 4 }; + dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 150, options, SetupItem); + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/MapOverlaysLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapOverlaysLogic.cs index 430f9e2f5e..3e0a4f81bc 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/MapOverlaysLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapOverlaysLogic.cs @@ -11,13 +11,14 @@ using System; using System.Collections.Generic; +using OpenRA.Graphics; using OpenRA.Mods.Common.Lint; using OpenRA.Mods.Common.Traits; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic { - [ChromeLogicArgsHotkeys("ToggleGridOverlayKey", "ToggleBuildableOverlayKey")] + [ChromeLogicArgsHotkeys("ToggleGridOverlayKey", "ToggleBuildableOverlayKey", "ToggleMarkerOverlayKey")] public class MapOverlaysLogic : ChromeLogic { [Flags] @@ -26,19 +27,19 @@ namespace OpenRA.Mods.Common.Widgets.Logic None = 0, Grid = 1, Buildable = 2, + Marker = 4, } readonly TerrainGeometryOverlay terrainGeometryTrait; readonly BuildableTerrainOverlay buildableTerrainTrait; - readonly Widget widget; + readonly MarkerLayerOverlay markerLayerTrait; [ObjectCreator.UseCtor] - public MapOverlaysLogic(Widget widget, World world, ModData modData, Dictionary logicArgs) + public MapOverlaysLogic(Widget widget, World world, ModData modData, WorldRenderer worldRenderer, Dictionary logicArgs) { - this.widget = widget; - terrainGeometryTrait = world.WorldActor.Trait(); buildableTerrainTrait = world.WorldActor.Trait(); + markerLayerTrait = world.WorldActor.Trait(); var toggleGridKey = new HotkeyReference(); if (logicArgs.TryGetValue("ToggleGridOverlayKey", out var yaml)) @@ -48,6 +49,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (logicArgs.TryGetValue("ToggleBuildableOverlayKey", out yaml)) toggleBuildableKey = modData.Hotkeys[yaml.Value]; + var toggleMarkerKey = new HotkeyReference(); + if (logicArgs.TryGetValue("ToggleMarkerOverlayKey", out yaml)) + toggleMarkerKey = modData.Hotkeys[yaml.Value]; + var keyhandler = widget.Get("OVERLAY_KEYHANDLER"); keyhandler.AddHandler(e => { @@ -66,6 +71,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic return true; } + if (toggleMarkerKey.IsActivatedBy(e)) + { + markerLayerTrait.Enabled ^= true; + return true; + } + return false; }); @@ -84,23 +95,33 @@ namespace OpenRA.Mods.Common.Widgets.Logic Widget CreateOverlaysPanel() { - var categoriesPanel = widget.Get("TOOLS_WIDGETS"); - var showGridCheckbox = categoriesPanel.Get("SHOW_TILE_GRID"); - var showBuildableAreaCheckbox = categoriesPanel.Get("SHOW_BUILDABLE_AREA"); + var categoriesPanel = Ui.LoadWidget("OVERLAY_PANEL", null, new WidgetArgs()); + var categoryTemplate = categoriesPanel.Get("CATEGORY_TEMPLATE"); - MapOverlays[] allCategories = { MapOverlays.Grid, MapOverlays.Buildable }; + MapOverlays[] allCategories = { MapOverlays.Grid, MapOverlays.Buildable, MapOverlays.Marker }; foreach (var cat in allCategories) { + var category = (CheckboxWidget)categoryTemplate.Clone(); + category.GetText = () => cat.ToString(); + category.IsVisible = () => true; + if (cat.HasFlag(MapOverlays.Grid)) { - showGridCheckbox.IsChecked = () => terrainGeometryTrait.Enabled; - showGridCheckbox.OnClick = () => terrainGeometryTrait.Enabled ^= true; + category.IsChecked = () => terrainGeometryTrait.Enabled; + category.OnClick = () => terrainGeometryTrait.Enabled ^= true; } else if (cat.HasFlag(MapOverlays.Buildable)) { - showBuildableAreaCheckbox.IsChecked = () => buildableTerrainTrait.Enabled; - showBuildableAreaCheckbox.OnClick = () => buildableTerrainTrait.Enabled ^= true; + category.IsChecked = () => buildableTerrainTrait.Enabled; + category.OnClick = () => buildableTerrainTrait.Enabled ^= true; } + else if (cat.HasFlag(MapOverlays.Marker)) + { + category.IsChecked = () => markerLayerTrait.Enabled; + category.OnClick = () => markerLayerTrait.Enabled ^= true; + } + + categoriesPanel.AddChild(category); } return categoriesPanel; diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/MapToolsLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapToolsLogic.cs new file mode 100644 index 0000000000..342664eda8 --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapToolsLogic.cs @@ -0,0 +1,82 @@ +#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.Widgets; + +namespace OpenRA.Mods.Common.Widgets.Logic +{ + public class MapToolsLogic : ChromeLogic + { + [TranslationReference] + const string MarkerTiles = "label-tool-marker-tiles"; + + enum MapTool + { + MarkerTiles + } + + readonly DropDownButtonWidget toolsDropdown; + readonly Dictionary toolNames = new() + { + { MapTool.MarkerTiles, MarkerTiles } + }; + + readonly Dictionary toolPanels = new(); + + MapTool selectedTool = MapTool.MarkerTiles; + + [ObjectCreator.UseCtor] + public MapToolsLogic(Widget widget, World world, ModData modData, WorldRenderer worldRenderer, Dictionary logicArgs) + { + toolsDropdown = widget.Get("TOOLS_DROPDOWN"); + + var markerToolPanel = widget.Get("MARKER_TOOL_PANEL"); + toolPanels.Add(MapTool.MarkerTiles, markerToolPanel); + + toolsDropdown.OnMouseDown = _ => ShowToolsDropDown(toolsDropdown); + toolsDropdown.GetText = () => TranslationProvider.GetString(toolNames[selectedTool]); + toolsDropdown.Disabled = true; // TODO: Enable if new tools are added + } + + void ShowToolsDropDown(DropDownButtonWidget dropdown) + { + ScrollItemWidget SetupItem(MapTool tool, ScrollItemWidget itemTemplate) + { + var item = ScrollItemWidget.Setup(itemTemplate, + () => selectedTool == tool, + () => SelectTool(tool)); + + item.Get("LABEL").GetText = () => TranslationProvider.GetString(toolNames[tool]); + + return item; + } + + var options = new[] { MapTool.MarkerTiles }; + dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 150, options, SetupItem); + } + + void SelectTool(MapTool tool) + { + if (tool != selectedTool) + { + var currentToolPanel = toolPanels[selectedTool]; + currentToolPanel.Visible = false; + } + + selectedTool = tool; + + var toolPanel = toolPanels[selectedTool]; + toolPanel.Visible = true; + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs index 5387fcfeaf..7ce757c78f 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Newtonsoft.Json; using OpenRA.FileSystem; using OpenRA.Mods.Common.Traits; using OpenRA.Widgets; @@ -300,6 +301,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic } saveMap(combinedPath); + + SaveMapMarkerTiles(map, modData, world); } public static void SaveMapInner(Map map, IReadWritePackage package, World world, ModData modData) @@ -344,5 +347,33 @@ namespace OpenRA.Mods.Common.Widgets.Logic }, confirmText: SaveMapFailedConfirm); } + + static void SaveMapMarkerTiles(Map map, ModData modData, World world) + { + try + { + var markerLayerOverlay = world.WorldActor.Trait(); + if (markerLayerOverlay.Tiles.Count == 0) + return; + + var mod = modData.Manifest.Metadata; + var directory = Path.Combine(Platform.SupportDir, "Editor", modData.Manifest.Id, mod.Version, "MarkerTiles"); + Directory.CreateDirectory(directory); + + var markerTilesFile = markerLayerOverlay.ToFile(); + var markerTilesContent = JsonConvert.SerializeObject(markerTilesFile); + + var markerTileFilename = $"{Path.GetFileNameWithoutExtension(map.Package.Name)}.json"; + using (var streamWriter = new StreamWriter(Path.Combine(directory, markerTileFilename), false)) + { + streamWriter.Write(markerTilesContent); + } + } + catch (Exception e) + { + Log.Write("debug", "Failed to save map editor marker tiles."); + Log.Write("debug", e); + } + } } } diff --git a/mods/cnc/chrome.yaml b/mods/cnc/chrome.yaml index 10aab40efa..f34cb22405 100644 --- a/mods/cnc/chrome.yaml +++ b/mods/cnc/chrome.yaml @@ -759,3 +759,4 @@ editor: actors: 802, 68, 16, 16 tools: 904, 68, 16, 16 history: 904, 51, 16, 16 + erase: 818, 170, 16, 16 diff --git a/mods/cnc/chrome/editor.yaml b/mods/cnc/chrome/editor.yaml index d611c22624..0b6bba0149 100644 --- a/mods/cnc/chrome/editor.yaml +++ b/mods/cnc/chrome/editor.yaml @@ -218,6 +218,7 @@ Container@EDITOR_WORLD_ROOT: Logic: LoadIngamePerfLogic, MapEditorLogic, ActorEditLogic, MapOverlaysLogic, MapEditorSelectionLogic ToggleGridOverlayKey: EditorToggleGridOverlay ToggleBuildableOverlayKey: EditorToggleBuildableOverlay + ToggleMarkerOverlayKey: EditorToggleMarkerOverlay Children: LogicKeyListener@OVERLAY_KEYHANDLER: Container@PERF_ROOT: @@ -431,25 +432,161 @@ Container@EDITOR_WORLD_ROOT: Width: 290 Height: WINDOW_BOTTOM - 410 ClickThrough: false + Logic: MapToolsLogic Children: Background@TOOLS_EDIT_PANEL: - X: 0 - Y: 0 Width: PARENT_RIGHT + Height: 25 + Background: panel-black + Label@TOOLS_LABEL: + Y: 1 + Width: 55 + Height: 25 + Text: label-tools-bg-categories + Align: Right + Font: TinyBold + DropDownButton@TOOLS_DROPDOWN: + X: 60 + Width: PARENT_RIGHT - 60 + Height: 25 + Font: Bold + ScrollPanel@MARKER_TOOL_PANEL: + Y: 24 + Width: PARENT_RIGHT - 1 Height: PARENT_BOTTOM - Background: scrollpanel-bg - Checkbox@SHOW_TILE_GRID: - X: 6 - Y: 7 - Width: PARENT_RIGHT - 29 - Height: 20 - Text: label-show-tile-grid - Checkbox@SHOW_BUILDABLE_AREA: - X: 6 - Y: 32 - Width: PARENT_RIGHT - 29 - Height: 20 - Text: label-show-buildable-area + Visible: true + ScrollBar: Hidden + ScrollbarWidth: 0 + Logic: MapMarkerTilesLogic + Children: + ScrollPanel@TILE_COLOR_PANEL: + X: 6 + Y: 6 + Width: PARENT_RIGHT - 18 + Height: 31 + TopBottomSpacing: 1 + ItemSpacing: 1 + ScrollBar: Hidden + ScrollbarWidth: 0 + ContentHeight: 31 + Children: + ScrollItem@TILE_COLOR_TEMPLATE: + Visible: false + Height: 29 + Width: 29 + IgnoreChildMouseOver: true + Children: + ColorBlock@TILE_PREVIEW: + X: 2 + Y: 2 + Width: 26 + Height: 26 + ScrollItem@TILE_ICON_TEMPLATE: + Visible: false + Height: 29 + Width: 29 + IgnoreChildMouseOver: true + Children: + Image@TILE_ERASE: + X: 6 + Y: 6 + Width: 26 + Height: 26 + ImageCollection: editor + ImageName: erase + Button@CLEAR_CURRENT_BUTTON: + X: 6 + Y: 42 + Width: 100 + Height: 25 + Text: button-marker-tiles-clear-current + Font: Bold + Button@CLEAR_ALL_BUTTON: + X: 111 + Y: 42 + Width: 75 + Height: 25 + Text: button-marker-tiles-clear-all + Font: Bold + Label@ALPHA_LABEL: + X: 6 + Y: 72 + Width: 265 + Height: 25 + Align: Left + Text: label-marker-alpha + Slider@ALPHA_SLIDER: + X: 130 + Y: 72 + Width: 128 + Height: 25 + Label@ALPHA_VALUE: + X: 260 + Y: 72 + Width: 30 + Height: 25 + Align: Left + Text: 85 + Label@MODE_LABEL: + X: 6 + Y: 102 + Width: 265 + Height: 25 + Align: Left + Text: label-marker-mirror-mode + DropDownButton@MODE_DROPDOWN: + X: 129 + Y: 102 + Width: 157 + Height: 25 + Label@NUM_SIDES_LABEL: + X: 6 + Y: 132 + Width: 256 + Height: 25 + Align: Left + Text: label-marker-layer-num-sides + Slider@ROTATE_NUM_SIDES_SLIDER: + X: 130 + Y: 132 + Width: 128 + Height: 25 + Visible: false + Label@ROTATE_NUM_SIDES_VALUE: + X: 260 + Y: 132 + Width: 30 + Height: 25 + Align: Left + Text: 2 + Visible: false + DropDownButton@FLIP_NUM_SIDES_DROPDOWN: + X: 129 + Y: 132 + Width: 157 + Height: 25 + Label@AXIS_ANGLE_LABEL: + X: 6 + Y: 162 + Width: 256 + Height: 25 + Align: Left + Text: label-marker-axis-angle + Visible: false + Slider@AXIS_ANGLE_SLIDER: + X: 130 + Y: 162 + Width: 128 + Height: 25 + Visible: false + Label@AXIS_ANGLE_VALUE: + X: 260 + Y: 162 + Width: 30 + Height: 25 + Align: Left + Text: 0 + Visible: false Container@HISTORY_WIDGETS: Logic: HistoryLogLogic X: WINDOW_RIGHT - 295 @@ -819,6 +956,13 @@ Container@EDITOR_WORLD_ROOT: TooltipTemplate: BUTTON_TOOLTIP TooltipText: button-editor-world-root-paste.tooltip TooltipContainer: TOOLTIP_CONTAINER + DropDownButton@OVERLAY_BUTTON: + X: WINDOW_RIGHT - 914 + Y: 5 + Width: 140 + Height: 25 + Text: dropdownbutton-editor-world-root-overlay-button + Font: Bold Label@COORDINATE_LABEL: X: 10 Width: 50 @@ -863,7 +1007,7 @@ ScrollPanel@CATEGORY_FILTER_PANEL: ScrollPanel@OVERLAY_PANEL: Width: 140 - Height: 55 + Height: 80 ItemSpacing: 5 TopBottomSpacing: 0 Children: @@ -873,4 +1017,3 @@ ScrollPanel@OVERLAY_PANEL: Width: PARENT_RIGHT - 29 Height: 20 Visible: false - diff --git a/mods/cnc/languages/chrome/en.ftl b/mods/cnc/languages/chrome/en.ftl index 9939dc9e30..0fc85819f9 100644 --- a/mods/cnc/languages/chrome/en.ftl +++ b/mods/cnc/languages/chrome/en.ftl @@ -70,8 +70,13 @@ label-copy-filters = Copy Filters label-filter-terrain = Terrain label-filter-resources = Resources label-filter-actors = Actors -label-show-tile-grid = Show Tile Grid -label-show-buildable-area = Show Buildable Area +label-tools-bg-categories = Tool: +button-marker-tiles-clear-current = Clear Current +button-marker-tiles-clear-all = Clear All +label-marker-layer-num-sides = Number of Sides +label-marker-alpha = Tile Alpha +label-marker-mirror-mode = Mirror Mode +label-marker-axis-angle = Axis Angle button-map-editor-tab-container-select-tooltip = Select button-map-editor-tab-container-tiles-tooltip = Tiles @@ -96,9 +101,12 @@ 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 +label-tool-marker-tiles = Marker Tiles + ## gamesave-browser.yaml label-gamesave-browser-panel-load-title = Load game label-gamesave-browser-panel-save-title = Save game diff --git a/mods/cnc/rules/world.yaml b/mods/cnc/rules/world.yaml index dc831fc885..b72b59c646 100644 --- a/mods/cnc/rules/world.yaml +++ b/mods/cnc/rules/world.yaml @@ -299,3 +299,4 @@ EditorWorld: EditorActionManager: BuildableTerrainOverlay: AllowedTerrainTypes: Clear, Road + MarkerLayerOverlay: diff --git a/mods/cnc/uibits/chrome-2x.png b/mods/cnc/uibits/chrome-2x.png index 7b84da62ff..0fe4453566 100644 Binary files a/mods/cnc/uibits/chrome-2x.png and b/mods/cnc/uibits/chrome-2x.png differ diff --git a/mods/cnc/uibits/chrome-3x.png b/mods/cnc/uibits/chrome-3x.png index b9e2353295..85d6663093 100644 Binary files a/mods/cnc/uibits/chrome-3x.png and b/mods/cnc/uibits/chrome-3x.png differ diff --git a/mods/cnc/uibits/chrome.png b/mods/cnc/uibits/chrome.png index d8142060f9..d214306aaf 100644 Binary files a/mods/cnc/uibits/chrome.png and b/mods/cnc/uibits/chrome.png differ diff --git a/mods/common/chrome/editor.yaml b/mods/common/chrome/editor.yaml index ecc164fde7..8a1226314a 100644 --- a/mods/common/chrome/editor.yaml +++ b/mods/common/chrome/editor.yaml @@ -84,7 +84,7 @@ Background@SAVE_MAP_PANEL: Y: 21 Width: 250 Height: 25 - Text: label-save-map-panel-title.label + Text: label-save-map-panel-heading Align: Center Font: Bold Label@TITLE_LABEL: @@ -93,7 +93,7 @@ Background@SAVE_MAP_PANEL: Width: 95 Height: 25 Align: Right - Text: label-save-map-panel-title.label + Text: label-save-map-panel-title TextField@TITLE: X: 110 Y: 60 @@ -207,6 +207,7 @@ Container@EDITOR_WORLD_ROOT: Logic: LoadIngamePerfLogic, MapEditorLogic, ActorEditLogic, MapOverlaysLogic, MapEditorSelectionLogic ToggleGridOverlayKey: EditorToggleGridOverlay ToggleBuildableOverlayKey: EditorToggleBuildableOverlay + ToggleMarkerOverlayKey: EditorToggleMarkerOverlay Children: LogicKeyListener@OVERLAY_KEYHANDLER: Container@PERF_ROOT: @@ -392,19 +393,159 @@ Container@EDITOR_WORLD_ROOT: Width: 310 Height: WINDOW_BOTTOM - 458 Visible: false + Logic: MapToolsLogic Children: - Checkbox@SHOW_TILE_GRID: - X: 15 - Y: 15 - Width: PARENT_RIGHT - 15 - Height: 20 - Text: label-show-tile-grid - Checkbox@SHOW_BUILDABLE_AREA: - X: 15 - Y: 40 - Width: PARENT_RIGHT - 15 - Height: 20 - Text: label-show-buildable-area + Label@TOOLS_LABEL: + Y: 12 + Width: 55 + Height: 25 + Text: label-tools-bg-categories + Align: Right + Font: TinyBold + DropDownButton@TOOLS_DROPDOWN: + X: 60 + Y: 10 + Width: PARENT_RIGHT - 70 + Height: 25 + Font: Bold + ScrollPanel@MARKER_TOOL_PANEL: + X: 9 + Y: 35 + Width: 290 + Height: WINDOW_BOTTOM - 490 + Visible: true + ScrollBar: Hidden + ScrollbarWidth: 0 + Logic: MapMarkerTilesLogic + Children: + ScrollPanel@TILE_COLOR_PANEL: + X: 6 + Y: 6 + Width: PARENT_RIGHT - 19 + Height: 31 + TopBottomSpacing: 1 + ItemSpacing: 1 + ScrollBar: Hidden + ScrollbarWidth: 0 + ContentHeight: 31 + Children: + ScrollItem@TILE_COLOR_TEMPLATE: + Visible: false + Height: 29 + Width: 29 + IgnoreChildMouseOver: true + Children: + ColorBlock@TILE_PREVIEW: + X: 2 + Y: 2 + Width: 26 + Height: 26 + ScrollItem@TILE_ICON_TEMPLATE: + Visible: false + Height: 29 + Width: 29 + IgnoreChildMouseOver: true + Children: + Image@TILE_ERASE: + X: 6 + Y: 6 + Width: 26 + Height: 26 + ImageCollection: editor + ImageName: erase + Button@CLEAR_CURRENT_BUTTON: + X: 6 + Y: 42 + Width: 100 + Height: 25 + Text: button-marker-tiles-clear-current + Font: Bold + Button@CLEAR_ALL_BUTTON: + X: 111 + Y: 42 + Width: 75 + Height: 25 + Text: button-marker-tiles-clear-all + Font: Bold + Label@ALPHA_LABEL: + X: 6 + Y: 72 + Width: 265 + Height: 25 + Align: Left + Text: label-marker-alpha + Slider@ALPHA_SLIDER: + X: 130 + Y: 72 + Width: 128 + Height: 25 + Label@ALPHA_VALUE: + X: 260 + Y: 72 + Width: 30 + Height: 25 + Align: Left + Text: 85 + Label@MODE_LABEL: + X: 6 + Y: 102 + Width: 265 + Height: 25 + Align: Left + Text: label-marker-mirror-mode + DropDownButton@MODE_DROPDOWN: + X: 129 + Y: 102 + Width: 157 + Height: 25 + Label@NUM_SIDES_LABEL: + X: 6 + Y: 132 + Width: 256 + Height: 25 + Align: Left + Text: label-marker-layer-num-sides + Slider@ROTATE_NUM_SIDES_SLIDER: + X: 130 + Y: 132 + Width: 128 + Height: 25 + Visible: false + Label@ROTATE_NUM_SIDES_VALUE: + X: 260 + Y: 132 + Width: 30 + Height: 25 + Align: Left + Text: 2 + Visible: false + DropDownButton@FLIP_NUM_SIDES_DROPDOWN: + X: 129 + Y: 132 + Width: 157 + Height: 25 + Label@AXIS_ANGLE_LABEL: + X: 6 + Y: 162 + Width: 256 + Height: 25 + Align: Left + Text: label-marker-axis-angle + Visible: false + Slider@AXIS_ANGLE_SLIDER: + X: 130 + Y: 162 + Width: 128 + Height: 25 + Visible: false + Label@AXIS_ANGLE_VALUE: + X: 260 + Y: 162 + Width: 30 + Height: 25 + Align: Left + Text: 0 + Visible: false Container@HISTORY_WIDGETS: X: WINDOW_RIGHT - 320 Y: 354 @@ -768,15 +909,21 @@ Container@EDITOR_WORLD_ROOT: TooltipTemplate: BUTTON_TOOLTIP TooltipText: button-editor-world-root-paste.tooltip TooltipContainer: TOOLTIP_CONTAINER + DropDownButton@OVERLAY_BUTTON: + X: 390 + Width: 140 + Height: 25 + Text: dropdownbutton-editor-world-root-overlay-button + Font: Bold Label@COORDINATE_LABEL: - X: 470 + X: 540 Width: 50 Height: 25 Align: Left Font: Bold Contrast: true Label@CASH_LABEL: - X: 595 + X: 655 Width: 50 Height: 25 Align: Left @@ -812,3 +959,15 @@ ScrollPanel@CATEGORY_FILTER_PANEL: Height: 20 Visible: false +ScrollPanel@OVERLAY_PANEL: + Width: 140 + Height: 80 + ItemSpacing: 5 + TopBottomSpacing: 0 + Children: + Checkbox@CATEGORY_TEMPLATE: + X: 5 + Y: 5 + Width: PARENT_RIGHT - 29 + Height: 20 + Visible: false diff --git a/mods/common/hotkeys/editor.yaml b/mods/common/hotkeys/editor.yaml index d1fbe9cfdc..46d71dc675 100644 --- a/mods/common/hotkeys/editor.yaml +++ b/mods/common/hotkeys/editor.yaml @@ -78,3 +78,8 @@ EditorToggleBuildableOverlay: F2 Description: Buildable Terrain Overlay Types: Editor Contexts: Editor + +EditorToggleMarkerOverlay: F3 + Description: Marker Layer Overlay + Types: Editor + Contexts: Editor diff --git a/mods/common/languages/chrome/en.ftl b/mods/common/languages/chrome/en.ftl index d4b4ea0588..b4c32d86b0 100644 --- a/mods/common/languages/chrome/en.ftl +++ b/mods/common/languages/chrome/en.ftl @@ -43,10 +43,8 @@ label-new-map-bg-width = Width: label-new-map-bg-height = Height: button-new-map-bg-create = Create -label-save-map-panel-title = - .label = Save Map - .label = Title: - +label-save-map-panel-heading = Save Map +label-save-map-panel-title = Title: label-save-map-panel-author = Author: label-save-map-panel-visibility = Visibility: dropdownbutton-save-map-panel-visibility-dropdown = Map Visibility @@ -68,8 +66,13 @@ label-copy-filters = Copy Filters label-filter-terrain = Terrain label-filter-resources = Resources label-filter-actors = Actors -label-show-tile-grid = Show Tile Grid -label-show-buildable-area = Show Buildable Area +label-tools-bg-categories = Tool: +button-marker-tiles-clear-current = Clear Current +button-marker-tiles-clear-all = Clear All +label-marker-layer-num-sides = Number of Sides +label-marker-alpha = Tile Alpha +label-marker-mirror-mode = Mirror Mode +label-marker-axis-angle = Axis Angle button-map-editor-tab-container-select-tooltip = Select button-map-editor-tab-container-tiles-tooltip = Tiles @@ -98,9 +101,12 @@ 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 +label-tool-marker-tiles = Marker Tiles + ## gamesave-browser.yaml label-gamesave-browser-panel-load-title = Load game label-gamesave-browser-panel-save-title = Save game diff --git a/mods/common/languages/en.ftl b/mods/common/languages/en.ftl index 8040df9d3c..1455699bc8 100644 --- a/mods/common/languages/en.ftl +++ b/mods/common/languages/en.ftl @@ -823,9 +823,29 @@ notification-added-resource = notification-added-tile = Added tile { $id } notification-filled-tile = Filled with tile { $id } +## EditorMarkerLayerBrush +notification-added-marker-tiles = + { $amount -> + [one] Added one marker tile of type { $type } + *[other] Added { $amount } marker tiles of type { $type } + } +notification-removed-marker-tiles = + { $amount -> + [one] Removed one marker tile + *[other] Removed { $amount } marker tiles + } +notification-cleared-selected-marker-tiles = Cleared { $amount } marker tiles of type { $type } +notification-cleared-all-marker-tiles = Cleared { $amount } marker tiles + ## EditorActionManager notification-opened = Opened +## MapOverlaysLogic +mirror-mode = + .none = None + .flip = Flip + .rotate = Rotate + ## ActorEditLogic notification-edited-actor = Edited { $name } ({ $id }) diff --git a/mods/d2k/chrome.yaml b/mods/d2k/chrome.yaml index 1e2be3563c..e79c37e56e 100644 --- a/mods/d2k/chrome.yaml +++ b/mods/d2k/chrome.yaml @@ -521,3 +521,4 @@ editor: actors: 34, 68, 16, 16 tools: 34, 144, 16, 16 history: 136, 51, 16, 16 + erase: 67, 144, 16, 16 diff --git a/mods/d2k/rules/world.yaml b/mods/d2k/rules/world.yaml index d64b11439b..34fab1c4a1 100644 --- a/mods/d2k/rules/world.yaml +++ b/mods/d2k/rules/world.yaml @@ -265,3 +265,4 @@ EditorWorld: EditorActionManager: BuildableTerrainOverlay: AllowedTerrainTypes: Rock, Concrete + MarkerLayerOverlay: diff --git a/mods/d2k/uibits/glyphs-2x.png b/mods/d2k/uibits/glyphs-2x.png index 910209f12f..d97be349a4 100644 Binary files a/mods/d2k/uibits/glyphs-2x.png and b/mods/d2k/uibits/glyphs-2x.png differ diff --git a/mods/d2k/uibits/glyphs-3x.png b/mods/d2k/uibits/glyphs-3x.png index 38dfff6518..c0a9e4e58c 100644 Binary files a/mods/d2k/uibits/glyphs-3x.png and b/mods/d2k/uibits/glyphs-3x.png differ diff --git a/mods/d2k/uibits/glyphs.png b/mods/d2k/uibits/glyphs.png index 48889f22e0..50f5d6da21 100644 Binary files a/mods/d2k/uibits/glyphs.png and b/mods/d2k/uibits/glyphs.png differ diff --git a/mods/ra/chrome.yaml b/mods/ra/chrome.yaml index 2799603460..b06c33ad5c 100644 --- a/mods/ra/chrome.yaml +++ b/mods/ra/chrome.yaml @@ -665,3 +665,4 @@ editor: actors: 34, 68, 16, 16 tools: 136, 68, 16, 16 history: 136, 51, 16, 16 + erase: 50, 187, 16, 16 diff --git a/mods/ra/rules/world.yaml b/mods/ra/rules/world.yaml index 799d7881fe..ec5776e336 100644 --- a/mods/ra/rules/world.yaml +++ b/mods/ra/rules/world.yaml @@ -321,3 +321,4 @@ EditorWorld: EditorActionManager: BuildableTerrainOverlay: AllowedTerrainTypes: Clear, Road + MarkerLayerOverlay: diff --git a/mods/ra/uibits/glyphs-2x.png b/mods/ra/uibits/glyphs-2x.png index 451873f7cf..a2dca700d3 100644 Binary files a/mods/ra/uibits/glyphs-2x.png and b/mods/ra/uibits/glyphs-2x.png differ diff --git a/mods/ra/uibits/glyphs-3x.png b/mods/ra/uibits/glyphs-3x.png index aa90dcc057..d25e876c53 100644 Binary files a/mods/ra/uibits/glyphs-3x.png and b/mods/ra/uibits/glyphs-3x.png differ diff --git a/mods/ra/uibits/glyphs.png b/mods/ra/uibits/glyphs.png index 5c88e5899b..e789470c1b 100644 Binary files a/mods/ra/uibits/glyphs.png and b/mods/ra/uibits/glyphs.png differ diff --git a/mods/ts/chrome.yaml b/mods/ts/chrome.yaml index cd6b085d59..d77022fe19 100644 --- a/mods/ts/chrome.yaml +++ b/mods/ts/chrome.yaml @@ -796,3 +796,4 @@ editor: actors: 34, 68, 16, 16 tools: 34, 144, 16, 16 history: 136, 51, 16, 16 + erase: 67, 144, 16, 16 diff --git a/mods/ts/rules/world.yaml b/mods/ts/rules/world.yaml index c4d2e5c2f7..bc863178ac 100644 --- a/mods/ts/rules/world.yaml +++ b/mods/ts/rules/world.yaml @@ -430,3 +430,4 @@ EditorWorld: AllowedTerrainTypes: Clear, Rough, Road, DirtRoad, Green, Sand, Pavement Palette: ra Alpha: 0.35 + MarkerLayerOverlay: diff --git a/mods/ts/uibits/glyphs-2x.png b/mods/ts/uibits/glyphs-2x.png index e2e979f344..b9449041f9 100644 Binary files a/mods/ts/uibits/glyphs-2x.png and b/mods/ts/uibits/glyphs-2x.png differ diff --git a/mods/ts/uibits/glyphs-3x.png b/mods/ts/uibits/glyphs-3x.png index 13d12f9ae4..3259d0a0f5 100644 Binary files a/mods/ts/uibits/glyphs-3x.png and b/mods/ts/uibits/glyphs-3x.png differ diff --git a/mods/ts/uibits/glyphs.png b/mods/ts/uibits/glyphs.png index cfe01e990b..da4fc915f1 100644 Binary files a/mods/ts/uibits/glyphs.png and b/mods/ts/uibits/glyphs.png differ