diff --git a/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs b/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs index 806a9e21f6..cb518eb21d 100644 --- a/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs +++ b/OpenRA.Mods.Common/MapGenerator/MultiBrush.cs @@ -50,6 +50,11 @@ namespace OpenRA.Mods.Common.MapGenerator public readonly ushort Type; public readonly CVec Offset = new(0, 0); + public TemplateInfo(ushort type) + { + Type = type; + } + public TemplateInfo(MiniYaml my) { if (string.IsNullOrEmpty(my.Value)) @@ -88,6 +93,16 @@ namespace OpenRA.Mods.Common.MapGenerator public readonly ImmutableArray Tiles; public readonly MultiBrushSegment Segment; + public MultiBrushInfo(TemplateInfo templateInfo) + { + Weight = MultiBrush.DefaultWeight; + Actors = []; + BackingTile = null; + Templates = [templateInfo]; + Tiles = []; + Segment = null; + } + public MultiBrushInfo(MiniYaml my) { Weight = MultiBrush.DefaultWeight; @@ -99,7 +114,7 @@ namespace OpenRA.Mods.Common.MapGenerator { case "Weight": if (!Exts.TryParseInt32Invariant(node.Value.Value, out Weight)) - throw new YamlException($"Invalid MultiBrush Weight `${node.Value.Value}`"); + throw new YamlException($"Invalid MultiBrush Weight `{node.Value.Value}`"); break; case "Actor": actors.Add(new ActorInfo(node.Value)); @@ -108,7 +123,7 @@ namespace OpenRA.Mods.Common.MapGenerator if (TerrainTile.TryParse(node.Value.Value, out var backingTile)) BackingTile = backingTile; else - throw new YamlException($"Invalid MultiBrush BackingTile `${node.Value.Value}`"); + throw new YamlException($"Invalid MultiBrush BackingTile `{node.Value.Value}`"); break; case "Template": templates.Add(new TemplateInfo(node.Value)); @@ -134,10 +149,26 @@ namespace OpenRA.Mods.Common.MapGenerator { var brushes = new List(); foreach (var node in my.Nodes) - if (node.Key.Split('@')[0] == "MultiBrush") - brushes.Add(new MultiBrushInfo(node.Value)); - else - throw new YamlException($"Expected `MultiBrush@*` but got `{node.Key}`"); + { + switch (node.Key.Split('@')[0]) + { + case "MultiBrush": + brushes.Add(new MultiBrushInfo(node.Value)); + break; + case "FromTemplates": + foreach (var part in node.Value.Value.Split(",")) + { + if (!Exts.TryParseUshortInvariant(part, out var type)) + throw new YamlException($"Invalid MultiBrush Template `{part}`"); + brushes.Add(new MultiBrushInfo(new TemplateInfo(type))); + } + + break; + default: + throw new YamlException($"Invalid MultiBrush collection key `{node.Key}`"); + } + } + return brushes.ToImmutableArray(); } } diff --git a/OpenRA.Mods.Common/MapGenerator/Terraformer.cs b/OpenRA.Mods.Common/MapGenerator/Terraformer.cs index 5c0ea80908..681741460b 100644 --- a/OpenRA.Mods.Common/MapGenerator/Terraformer.cs +++ b/OpenRA.Mods.Common/MapGenerator/Terraformer.cs @@ -74,6 +74,14 @@ namespace OpenRA.Mods.Common.MapGenerator public int Area; } + public sealed class PathPartitionZone + { + public bool ShouldTile = true; + public string SegmentType = null; + public int MinimumLength = 1; + public int MaximumDeviation = 0; + } + public enum Side : sbyte { Out = -1, @@ -1086,6 +1094,433 @@ namespace OpenRA.Mods.Common.MapGenerator return slice; } + /// + /// If given a looped path, normalizes it such that symmetry projected paths should have + /// symmetry projected start/end points. Note that this method isn't meaningful for loops + /// which would overlap with their symmetry projections. For non-looped paths, returns the + /// input unchanged. + /// + public int2[] NormalizeLoopStart(int2[] path) + { + if (path.Length < 2) + throw new ArgumentException("path is too short"); + + if (path[0] != path[^1]) + return path; + + var gridType = Map.Grid.Type; + var center = CellLayerUtils.Center(Map); + + var cpath = CellLayerUtils.FromMatrixPoints([path[0..^1]], Map.Tiles)[0]; + var wpath = cpath + .Select(cpos => CellLayerUtils.CornerToWPos(cpos, gridType)) + .ToList(); + + // Choose the closest to the map center and makes it the start/end of the loop. + // If there are ties, pick the first closest point that follows from the furthest + // point(s), ensuring consistency for symmetries. + var distances = wpath.ConvertAll(w => (w - center).LengthSquared); + var closest = distances.Min(); + var furthest = distances.Max(); + var closestI = distances.IndexOf(furthest); + while (distances[closestI] != closest) + if (++closestI == distances.Count) + closestI = 0; + + return path[closestI..^1].Concat(path[0..(closestI + 1)]).ToArray(); + } + + /// + /// Given a matrix-style path, divide it into a chain of smaller paths and convert them to + /// TilingPaths with segment types that best match a matrix of zones. + /// + /// The path to be divided. + /// The full set of zones, in order of preference for ties. + /// + /// Matrix which assigns zones to matching points in the path. + /// Null values can be used to describe locations with no zoning preference. + /// + /// Segmented brushes for TilingPath creation. + /// + /// If greater than zero, sub-paths are only allowed to change over in straight sections + /// and the starts/ends must be this number of points deep within a straight section. + /// + public List PartitionPath( + int2[] path, + IReadOnlyList allZones, + Matrix zoneMask, + IReadOnlyList brushes, + int minimumStraight) + { + // Algorithmic Overview: + // + // First, find the straight-enough sections that can support changes between sub-paths. + // We then find a best fit for subpaths that change over in these straights, according + // to their minimum lengths and zone matching. + // + // A best fit is found using a Dijkstra's Algorithm-based best-first search. (Bottom-up + // dynamic programming). The sub problems are just spans of the whole path, and are + // built up towards the full path by adding on and scoring sub-paths. + // + // The minimum cost of a sub-path is the minimum possible number of mismatched zones if + // an optimal zone is chosen. + // + // If there are multiple best solutions (with equal costs), there is a preference to + // solutions with more sub-paths. + if (allZones.Count == 0) + throw new ArgumentException("no zones provided"); + + if (path.Length < 2) + throw new ArgumentException("path is too short"); + + if (minimumStraight < 0) + throw new ArgumentException("minimumStraight was not >= 0"); + + var isLoop = path[0] == path[^1]; + + if (isLoop) + path = NormalizeLoopStart(path); + + var zones = new PathPartitionZone[isLoop ? path.Length - 1 : path.Length]; + for (var i = 0; i < zones.Length; i++) + { + if (zoneMask.ContainsXY(path[i])) + zones[i] = zoneMask[path[i]]; + } + + // from must be >= 0. + IEnumerable Range(int from, int length) + { + for (var i = 0; i < length; i++) + yield return (from + i) % zones.Length; + } + + // from must be >= 0. + IEnumerable ReverseRange(int from, int length) + { + for (var i = length - 1; i >= 0; i--) + yield return (from + i) % zones.Length; + } + + // Can also be used to get lengths + int Idx(int i) => (i + zones.Length) % zones.Length; + + int Vote(int from, int length, bool checkMinLength, List majorities = null) + { + var votes = new Dictionary(); + var wildcards = 0; + foreach (var i in Range(from, length)) + { + var zone = zones[i]; + + if (zone == null) + { + wildcards++; + continue; + } + + if (checkMinLength && length < zone.MinimumLength) + continue; + + votes.TryGetValue(zone, out var count); + votes[zone] = count + 1; + } + + if (votes.Count == 0) + { + if (wildcards == 0) + return int.MaxValue; + + if (checkMinLength) + { + var filteredZones = allZones.Where(r => r.MinimumLength >= length).ToList(); + if (filteredZones.Count == 0) + return int.MaxValue; + + majorities?.AddRange(filteredZones); + } + else + { + majorities?.AddRange(allZones); + } + + return 0; + } + + var best = votes.Values.Max(); + majorities?.AddRange( + votes + .Where(kv => kv.Value == best) + .Select(kv => kv.Key)); + return length - best - wildcards; + } + + PathPartitionZone fallbackPath; + + List SinglePath(PathPartitionZone zone) + { + if (zone.ShouldTile) + return [ + new TilingPath( + Map, + CellLayerUtils.FromMatrixPoints([path], Map.Tiles)[0], + zone.MaximumDeviation, + zone.SegmentType, + zone.SegmentType, + TilingPath.PermittedSegments.FromType(brushes, [zone.SegmentType]))]; + else + return []; + } + + { + var majorities = new List(); + if (Vote(0, zones.Length, false, majorities) == 0) + return SinglePath(majorities[0]); + + fallbackPath = majorities[0]; + } + + var minLength = Math.Max(1, allZones.Min(r => r.MinimumLength)); + + if (path.Length < minLength) + return SinglePath(fallbackPath); + + var straight = new bool[zones.Length]; + for (var i = 0; i < zones.Length; i++) + { + var a = path[Idx(i - 1)]; + var b = path[Idx(i)]; + var c = path[Idx(i + 1)]; + straight[i] = DirectionExts.FromInt2(b - a) == DirectionExts.FromInt2(c - b); + } + + if (!isLoop) + straight[0] = straight[^1] = true; + + // Note that loops can't be all straight. + var validTerminal = new bool[zones.Length]; + Array.Fill(validTerminal, true); + { + // Forward run + var run = isLoop + ? ReverseRange(zones.Length - minimumStraight, minimumStraight).TakeWhile(i => straight[i]).Count() + : 0; + foreach (var i in Range(0, zones.Length)) + { + run = straight[i] ? (run + 1) : 0; + validTerminal[i] &= run >= minimumStraight; + } + + // Backward run + run = isLoop + ? Range(zones.Length, minimumStraight).TakeWhile(i => straight[i]).Count() + : 0; + foreach (var i in ReverseRange(0, zones.Length)) + { + run = straight[i] ? (run + 1) : 0; + validTerminal[i] &= run >= minimumStraight; + } + } + + if (!isLoop) + validTerminal[0] = validTerminal[^1] = true; + + List validStarts; + if (isLoop) + validStarts = validTerminal + .Select((v, i) => (Valid: v, Index: i)) + .Where(t => t.Valid) + .Select(t => t.Index) + .ToList(); + else + validStarts = [0]; + + if (validStarts.Count == 0) + return SinglePath(fallbackPath); + + var solutions = new List<(int Cost, List Solution)>(); + + // An optimization would be to include the start point in a combined search. + // This is simpler though. + foreach (var offset in validStarts) + { + var end = path.Length - 1; + var costs = new int[end + 1]; + Array.Fill(costs, int.MaxValue); + + // Find costs + { + var costPriorities = new PriorityArray(end + 1, int.MaxValue); + costPriorities[0] = costs[0] = 0; + while (true) + { + var from = costPriorities.GetMinIndex(); + var fromCost = costPriorities[from]; + if (fromCost == int.MaxValue || from == end) + break; + + costPriorities[from] = int.MaxValue; + var maxLength = end - from; + for (var length = minLength; length <= maxLength; length++) + { + var to = from + length; + if (!validTerminal[Idx(offset + to)]) + continue; + + var mismatch = Vote(offset + from, length, true); + if (mismatch == int.MaxValue) + continue; + + var toCost = fromCost + mismatch; + if (toCost >= costs[to]) + continue; + + costPriorities[to] = costs[to] = toCost; + } + } + } + + if (costs[end] == int.MaxValue) + continue; + + // Work back from costs to solution + { + List solution = [Idx(offset + end)]; + var to = end; + while (to != 0) + { + var toCost = costs[to]; + var maxLength = to; + for (var length = minLength; length <= maxLength; length++) + { + var from = to - length; + if (!validTerminal[Idx(offset + from)]) + continue; + + var mismatch = Vote(offset + from, length, true); + if (mismatch == int.MaxValue) + continue; + + var fromCost = toCost - mismatch; + + // Use the first found solution. + if (fromCost == costs[from]) + { + solution.Add(Idx(offset + from)); + to = from; + break; + } + } + } + + solution.Reverse(); + solutions.Add((costs[end], solution)); + } + } + + if (solutions.Count == 0) + return SinglePath(fallbackPath); + + var bestCost = solutions.Min(t => t.Cost); + var bestCostSolutions = solutions.Where(t => t.Cost == bestCost).ToList(); + var mostBoundaries = bestCostSolutions.Max(t => t.Solution.Count); + var boundaries = bestCostSolutions.First(t => t.Solution.Count == mostBoundaries).Solution; + var ranges = new List<(int Start, int Length, PathPartitionZone Zone)>(); + PathPartitionZone lastZone = null; + for (var i = 0; i < boundaries.Count - 1; i++) + { + var from = boundaries[i]; + var to = boundaries[i + 1]; + if (from == to) + return SinglePath(fallbackPath); + + var length = Idx(to - from); + if (length + 1 == path.Length) + return SinglePath(fallbackPath); + + var possibleZones = new List(); + Vote(from, length, true, possibleZones); + if (possibleZones[0] != lastZone) + ranges.Add((from, length, possibleZones[0])); + else + ranges[^1] = (ranges[^1].Start, ranges[^1].Length + length, lastZone); + + lastZone = possibleZones[0]; + } + + if (isLoop && ranges.Count >= 2 && ranges[0].Zone == ranges[^1].Zone) + { + ranges[0] = (ranges[^1].Start, ranges[^1].Length + ranges[0].Length, ranges[^1].Zone); + ranges.RemoveAt(ranges.Count - 1); + } + + if (ranges.Count == 1) + return SinglePath(fallbackPath); + + var partitions = new List(); + var previousIncludedInterface = isLoop && ranges[^1].Zone.ShouldTile; + for (var rangeI = 0; rangeI < ranges.Count; rangeI++) + { + var (start, length, zone) = ranges[rangeI]; + if (!zone.ShouldTile) + { + previousIncludedInterface = false; + continue; + } + + var innerType = zone.SegmentType; + var startType = (!previousIncludedInterface && (isLoop || rangeI > 0)) + ? ranges[(ranges.Count + rangeI - 1) % ranges.Count].Zone.SegmentType + : innerType; + var endType = (isLoop || rangeI < ranges.Count - 1) + ? ranges[(rangeI + 1) % ranges.Count].Zone.SegmentType + : innerType; + Direction? startDirection = (isLoop || rangeI > 0) + ? DirectionExts.FromInt2( + path[(start + 1) % zones.Length] + - path[start]) + : null; + Direction? endDirection = (isLoop || rangeI < ranges.Count - 1) + ? DirectionExts.FromInt2( + path[(length + start + 1) % zones.Length] + - path[(length + start) % zones.Length]) + : null; + + var points = Range(start, length + 1) + .Select(i => path[i]) + .ToList(); + + var tilingPath = new TilingPath( + Map, + CellLayerUtils.FromMatrixPoints([points.ToArray()], Map.Tiles)[0], + zone.MaximumDeviation, + startType, + endType, + TilingPath.PermittedSegments.FromTypes(brushes, [startType], [innerType], [endType])); + tilingPath.Start.Direction = startDirection; + tilingPath.End.Direction = endDirection; + + partitions.Add(tilingPath); + previousIncludedInterface = true; + } + + return partitions; + } + + /// Wrapper around PartitionPath to process multiple paths at once. + public List PartitionPaths( + IEnumerable paths, + IReadOnlyList zones, + Matrix partitionMask, + IReadOnlyList brushes, + int minStraight) + { + return paths + .SelectMany(path => PartitionPath( + path, zones, partitionMask, brushes, minStraight)) + .ToList(); + } + /// /// Wrapper around InsideOutside which performs both path tiling and side filling, painting /// the result to the map. If tiling fails, returns null without modifying the map. diff --git a/OpenRA.Mods.D2k/Traits/World/D2kMapGenerator.cs b/OpenRA.Mods.D2k/Traits/World/D2kMapGenerator.cs new file mode 100644 index 0000000000..a42eaadf6c --- /dev/null +++ b/OpenRA.Mods.D2k/Traits/World/D2kMapGenerator.cs @@ -0,0 +1,622 @@ +#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.Linq; +using OpenRA.Mods.Common.MapGenerator; +using OpenRA.Mods.Common.Terrain; +using OpenRA.Mods.Common.Traits; +using OpenRA.Support; +using OpenRA.Traits; +using static OpenRA.Mods.Common.Traits.ResourceLayerInfo; + +namespace OpenRA.Mods.D2k.Traits +{ + [TraitLocation(SystemActors.EditorWorld)] + public sealed class D2kMapGeneratorInfo : TraitInfo, IEditorMapGeneratorInfo, IEditorToolInfo + { + [FieldLoader.Require] + public readonly string Type = null; + + [FieldLoader.Require] + [FluentReference] + public readonly string Name = null; + + [FieldLoader.Require] + [Desc("Tilesets that are compatible with this map generator.")] + public readonly string[] Tilesets = null; + + [FluentReference] + [Desc("The title to use for generated maps.")] + public readonly string MapTitle = "label-random-map"; + + [Desc("The widget tree to open when the tool is selected.")] + public readonly string PanelWidget = "MAP_GENERATOR_TOOL_PANEL"; + + // This is purely of interest to the linter. + [FieldLoader.LoadUsing(nameof(FluentReferencesLoader))] + [FluentReference] + public readonly List FluentReferences = null; + + [FieldLoader.LoadUsing(nameof(SettingsLoader))] + public readonly MiniYaml Settings; + + string IMapGeneratorInfo.Type => Type; + string IMapGeneratorInfo.Name => Name; + string IMapGeneratorInfo.MapTitle => MapTitle; + string[] IEditorMapGeneratorInfo.Tilesets => Tilesets; + + static MiniYaml SettingsLoader(MiniYaml my) + { + return my.NodeWithKey("Settings").Value; + } + + static List FluentReferencesLoader(MiniYaml my) + { + return new MapGeneratorSettings(null, my.NodeWithKey("Settings").Value) + .Options.SelectMany(o => o.GetFluentReferences()).ToList(); + } + + const int FractionMax = Terraformer.FractionMax; + const int EntityBonusMax = 1000000; + + sealed class Parameters + { + [FieldLoader.Require] + public readonly int Seed = default; + [FieldLoader.Require] + public readonly int Rotations = default; + [FieldLoader.LoadUsing(nameof(MirrorLoader))] + public readonly Symmetry.Mirror Mirror = default; + + [FieldLoader.Require] + public readonly int Players = default; + [FieldLoader.Require] + public readonly int TerrainFeatureSize = default; + [FieldLoader.Require] + public readonly int SandDetailFeatureSize = default; + [FieldLoader.Require] + public readonly int DuneFeatureSize = default; + [FieldLoader.Require] + public readonly int ResourceFeatureSize = default; + [FieldLoader.Require] + public readonly int TerrainSmoothing = default; + [FieldLoader.Require] + public readonly int DuneSmoothing = default; + [FieldLoader.Require] + public readonly int SmoothingThreshold = default; + [FieldLoader.Require] + public readonly int RockRoughness = default; + [FieldLoader.Require] + public readonly int SandRoughness = default; + [FieldLoader.Require] + public readonly int RoughnessRadius = default; + [FieldLoader.Require] + public readonly int Rock = default; + [FieldLoader.Require] + public readonly int SandCliffs = default; + [FieldLoader.Require] + public readonly int Dunes = default; + [FieldLoader.Require] + public readonly int MinimumRockStraight = default; + [FieldLoader.Require] + public readonly int MinimumSandCliffStraight = default; + [FieldLoader.Require] + public readonly int MinimumRockSandThickness = default; + [FieldLoader.Require] + public readonly int MinimumSandCliffThickness = default; + [FieldLoader.Require] + public readonly int MinimumDuneThickness = default; + [FieldLoader.Require] + public readonly int MinimumRockSmoothLength = default; + [FieldLoader.Require] + public readonly int MinimumSandRockCliffLength = default; + [FieldLoader.Require] + public readonly int MinimumSandSandCliffLength = default; + [FieldLoader.Require] + public readonly int MinimumSandLength = default; + [FieldLoader.Require] + public readonly int SandContourSpacing = default; + [FieldLoader.Require] + public readonly int DuneContourSpacing = default; + [FieldLoader.Require] + public readonly int SandDetail = default; + [FieldLoader.Require] + public readonly int SandDetailClumpiness = default; + [FieldLoader.Require] + public readonly int SandDetailCutout = default; + [FieldLoader.Require] + public readonly int MaximumSandDetailCutoutSpacing = default; + + [FieldLoader.Require] + public readonly bool CreateEntities = default; + [FieldLoader.Require] + public readonly int AreaEntityBonus = default; + [FieldLoader.Require] + public readonly int PlayerCountEntityBonus = default; + [FieldLoader.Require] + public readonly int MinimumSpawnRockArea = default; + [FieldLoader.Require] + public readonly int CentralSpawnReservationFraction = default; + [FieldLoader.Require] + public readonly int SpawnRegionSize = default; + [FieldLoader.Require] + public readonly int MinimumSpawnRadius = default; + [FieldLoader.Require] + public readonly int SpawnReservation = default; + [FieldLoader.Require] + public readonly int BiasedResourceSpawns = default; + [FieldLoader.Require] + public readonly int ResourceSpawnSpacing = default; + [FieldLoader.Require] + public readonly int UnbiasedResourceSpawns = default; + [FieldLoader.Require] + public readonly int ResourceSpawnReservation = default; + [FieldLoader.Require] + public readonly int ResourcesPerPlayer = default; + [FieldLoader.Require] + public readonly int ResourceUniformity = default; + [FieldLoader.Require] + public readonly int ResourceClumpiness = default; + [FieldLoader.Require] + public readonly string ResourceSpawn = default; + [FieldLoader.Ignore] + public readonly ResourceTypeInfo Resource = default; + [FieldLoader.Require] + public readonly string WormSpawn = default; + [FieldLoader.Require] + public readonly int WormSpawns = default; + [FieldLoader.Require] + public readonly int WormSpawnReservation = default; + + [FieldLoader.Require] + public readonly ushort SandTile = default; + [FieldLoader.Require] + public readonly ushort RockTile = default; + [FieldLoader.Ignore] + public readonly IReadOnlySet PlayableTerrain; + [FieldLoader.Ignore] + public readonly IReadOnlySet RockZoneableTerrain = default; + [FieldLoader.Ignore] + public readonly IReadOnlySet SandZoneableTerrain = default; + [FieldLoader.Require] + public readonly string RockSmoothSegmentType = default; + [FieldLoader.Require] + public readonly string SandRockCliffSegmentType = default; + [FieldLoader.Require] + public readonly string SandSandCliffSegmentType = default; + [FieldLoader.Require] + public readonly string SandSegmentType = default; + [FieldLoader.Require] + public readonly string DuneSegmentType = default; + [FieldLoader.Ignore] + public readonly IReadOnlyList SegmentedBrushes; + [FieldLoader.Ignore] + public readonly IReadOnlyList SandDetailBrushes; + [FieldLoader.Ignore] + public readonly IReadOnlyList DuneBrushes; + + public Parameters(Map map, MiniYaml my) + { + FieldLoader.Load(this, my); + + var terrainInfo = (ITemplatedTerrainInfo)map.Rules.TerrainInfo; + + IReadOnlySet ParseTerrainIndexes(string key) + { + return my.NodeWithKey(key).Value.Value + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(terrainInfo.GetTerrainIndex) + .ToImmutableHashSet(); + } + + var resourceTypes = map.Rules.Actors[SystemActors.World].TraitInfoOrDefault().ResourceTypes; + if (!resourceTypes.TryGetValue(my.NodeWithKey("Resource").Value.Value, out Resource)) + throw new YamlException("Resource is not valid"); + + PlayableTerrain = ParseTerrainIndexes("PlayableTerrain"); + RockZoneableTerrain = ParseTerrainIndexes("RockZoneableTerrain"); + SandZoneableTerrain = ParseTerrainIndexes("SandZoneableTerrain"); + SegmentedBrushes = MultiBrush.LoadCollection(map, "Segmented"); + SandDetailBrushes = MultiBrush.LoadCollection(map, my.NodeWithKey("SandDetailBrushes").Value.Value); + DuneBrushes = MultiBrush.LoadCollection(map, my.NodeWithKey("DuneBrushes").Value.Value); + } + + static object MirrorLoader(MiniYaml my) + { + if (Symmetry.TryParseMirror(my.NodeWithKey("Mirror").Value.Value, out var mirror)) + return mirror; + else + throw new YamlException($"Invalid Mirror value `{my.NodeWithKey("Mirror").Value.Value}`"); + } + } + + public IMapGeneratorSettings GetSettings() + { + return new MapGeneratorSettings(this, Settings); + } + + public Map Generate(ModData modData, MapGenerationArgs args) + { + var terrainInfo = modData.DefaultTerrainInfo[args.Tileset]; + var size = args.Size; + + var map = new Map(modData, terrainInfo, size); + var actorPlans = new List(); + + var param = new Parameters(map, args.Settings); + + var terraformer = new Terraformer(args, map, modData, actorPlans, param.Mirror, param.Rotations); + + var sandZone = new Terraformer.PathPartitionZone() + { + ShouldTile = false, + SegmentType = param.SandSegmentType, + MinimumLength = param.MinimumSandLength, + }; + var rockSmoothZone = new Terraformer.PathPartitionZone() + { + SegmentType = param.RockSmoothSegmentType, + MinimumLength = param.MinimumRockSmoothLength, + MaximumDeviation = 10, + }; + var sandRockCliffZone = new Terraformer.PathPartitionZone() + { + SegmentType = param.SandRockCliffSegmentType, + MinimumLength = param.MinimumSandRockCliffLength, + MaximumDeviation = 10, + }; + var sandSandCliffZone = new Terraformer.PathPartitionZone() + { + SegmentType = param.SandSandCliffSegmentType, + MinimumLength = param.MinimumSandSandCliffLength, + MaximumDeviation = 10, + }; + + // Use `random` to derive separate independent random number generators. + // + // This prevents changes in one part of the algorithm from affecting randomness in + // other parts. + // + // In order to maximize stability, additions should be appended only. Disused + // derivatives may be deleted but should be replaced with their unused call to + // random.Next(). All generators should be created unconditionally. + var random = new MersenneTwister(param.Seed); + var pickAnyRandom = new MersenneTwister(random.Next()); + var elevationRandom = new MersenneTwister(random.Next()); + var rockTilingRandom = new MersenneTwister(random.Next()); + var sandSandCliffTilingRandom = new MersenneTwister(random.Next()); + var playerRandom = new MersenneTwister(random.Next()); + var expansionRandom = new MersenneTwister(random.Next()); + var resourceRandom = new MersenneTwister(random.Next()); + var sandDetailRandom = new MersenneTwister(random.Next()); + var topologyRandom = new MersenneTwister(random.Next()); + var sandDetailTilingRandom = new MersenneTwister(random.Next()); + var duneRandom = new MersenneTwister(random.Next()); + var duneTilingRandom = new MersenneTwister(random.Next()); + + terraformer.InitMap(); + + // Clear map to random sand + foreach (var mpos in map.AllCells.MapCoords) + map.Tiles[mpos] = terraformer.PickTile(pickAnyRandom, param.SandTile); + + var elevation = terraformer.ElevationNoiseMatrix( + elevationRandom, + param.TerrainFeatureSize, + param.TerrainSmoothing); + var roughnessMatrix = MatrixUtils.GridVariance( + elevation, + param.RoughnessRadius); + + // Rock generation + CellLayer rockSmoothSand; + { + var cliffMask = MatrixUtils.CalibratedBooleanThreshold( + roughnessMatrix, + param.RockRoughness, FractionMax); + var plan = terraformer.SliceElevation(elevation, null, param.Rock); + plan = MatrixUtils.BooleanBlotch( + plan, + param.TerrainSmoothing, + param.SmoothingThreshold, /*smoothingThresholdOutOf=*/FractionMax, + param.MinimumRockSandThickness, + true); + var contours = MatrixUtils.BordersToPoints(plan); + var partitionMask = cliffMask.Map(masked => masked ? sandRockCliffZone : rockSmoothZone); + var tilingPaths = terraformer.PartitionPaths( + contours, + [rockSmoothZone, sandRockCliffZone], + partitionMask, + param.SegmentedBrushes, + param.MinimumRockStraight); + foreach (var tilingPath in tilingPaths) + tilingPath + .OptimizeLoop() + .ExtendEdge(4); + + rockSmoothSand = terraformer.PaintLoopsAndFill( + rockTilingRandom, + tilingPaths, + plan[0] ? Terraformer.Side.In : Terraformer.Side.Out, + null, + [new MultiBrush().WithTemplate(map, param.RockTile, CVec.Zero)]) + ?? throw new MapGenerationException("Could not fit tiles for rock platforms"); + } + + // Sand cliff generation + if (param.SandCliffs > 0) + { + var inverseElevation = elevation.Map(v => -v); + var cliffMask = MatrixUtils.CalibratedBooleanThreshold( + roughnessMatrix, + param.SandRoughness, FractionMax); + var plan = terraformer.SliceElevation( + inverseElevation, + CellLayerUtils.ToMatrix(rockSmoothSand, Terraformer.Side.Out) + .Map(s => s == Terraformer.Side.Out), + param.SandCliffs, + param.SandContourSpacing); + plan = MatrixUtils.BooleanBlotch( + plan, + param.TerrainSmoothing, + param.SmoothingThreshold, /*smoothingThresholdOutOf=*/FractionMax, + param.MinimumSandCliffThickness, + true); + var contours = MatrixUtils.BordersToPoints(plan); + var partitionMask = cliffMask.Map(masked => masked ? sandSandCliffZone : sandZone); + var tilingPaths = terraformer.PartitionPaths( + contours, + [sandSandCliffZone, sandZone], + partitionMask, + param.SegmentedBrushes, + param.MinimumSandCliffStraight); + foreach (var tilingPath in tilingPaths) + { + var brush = tilingPath + .OptimizeLoop() + .ExtendEdge(4) + .SetAutoEndDeviation() + .Tile(sandSandCliffTilingRandom) + ?? throw new MapGenerationException("Could not fit tiles for sand-sand cliffs"); + terraformer.PaintTiling(pickAnyRandom, brush); + } + } + + // Sand Detail + if (param.SandDetail > 0) + { + var space = terraformer.CheckSpace(param.PlayableTerrain); + var passages = terraformer.PlanPassages( + topologyRandom, + terraformer.ImproveSymmetry(space, true, (a, b) => a && b), + param.SandDetailCutout, + param.MaximumSandDetailCutoutSpacing); + var plan = terraformer.BooleanNoise( + sandDetailRandom, + param.SandDetailFeatureSize, + param.SandDetail, + param.SandDetailClumpiness); + plan = CellLayerUtils.Subtract([ + CellLayerUtils.Intersect([ + plan, + terraformer.CheckSpace(param.SandTile, true)]), + passages]); + terraformer.PaintArea( + sandDetailTilingRandom, + CellLayerUtils.Map(plan, p => p ? MultiBrush.Replaceability.Any : MultiBrush.Replaceability.None), + param.SandDetailBrushes, + true); + } + + // Dunes + if (param.Dunes > 0) + { + var duneNoise = terraformer.ElevationNoiseMatrix( + duneRandom, + param.DuneFeatureSize, + param.DuneSmoothing); + var duneable = terraformer.CheckSpace(param.SandTile, true); + duneable = terraformer.ImproveSymmetry(duneable, true, (a, b) => a && b); + var plan = terraformer.SliceElevation( + duneNoise, + CellLayerUtils.ToMatrix(duneable, true), + param.Dunes, + param.DuneContourSpacing); + plan = MatrixUtils.BooleanBlotch( + plan, + param.DuneSmoothing, + param.SmoothingThreshold, /*smoothingThresholdOutOf=*/FractionMax, + param.MinimumDuneThickness, + false); + var contours = CellLayerUtils.FromMatrixPoints( + MatrixUtils.BordersToPoints(plan), + map.Tiles); + var tilingPaths = contours + .Select(contour => + TilingPath.QuickCreate( + map, + param.SegmentedBrushes, + contour, + (param.MinimumDuneThickness - 1) / 2, + param.DuneSegmentType, + param.DuneSegmentType) + .ExtendEdge(4)) + .ToArray(); + _ = terraformer.PaintLoopsAndFill( + duneTilingRandom, + tilingPaths, + plan[0] ? Terraformer.Side.In : Terraformer.Side.Out, + null, + param.DuneBrushes) + ?? throw new MapGenerationException("Could not fit tiles for rock platforms"); + } + + if (param.CreateEntities) + { + var playable = terraformer.ChoosePlayableRegion( + terraformer.CheckSpace(param.PlayableTerrain, true, false, true), + null) + ?? throw new MapGenerationException("could not find a playable region"); + + var rockZoneable = terraformer.GetZoneable(param.RockZoneableTerrain, playable); + var (regions, regionMask) = terraformer.FindRegions(rockZoneable, DirectionExts.Spread8CVec); + var acceptableRegions = regions + .Where(r => r.Area >= param.MinimumSpawnRockArea) + .Select(r => r.Id) + .ToHashSet(); + if (acceptableRegions.Count == 0) + throw new MapGenerationException("rocks are not big enough for players"); + + rockZoneable = CellLayerUtils.Intersect([ + rockZoneable, + CellLayerUtils.Map(regionMask, acceptableRegions.Contains)]); + + var sandZoneable = terraformer.GetZoneable(param.SandZoneableTerrain, playable); + var spiceZoneable = CellLayerUtils.Clone(sandZoneable); + var sandZoneableArea = sandZoneable.Count(v => v); + + var symmetryCount = Symmetry.RotateAndMirrorProjectionCount(param.Rotations, param.Mirror); + var entityMultiplier = + (long)sandZoneableArea * param.AreaEntityBonus + + (long)param.Players * param.PlayerCountEntityBonus; + var perSymmetryEntityMultiplier = entityMultiplier / symmetryCount; + + // Spawn generation + var symmetryPlayers = param.Players / symmetryCount; + for (var iteration = 0; iteration < symmetryPlayers; iteration++) + { + var chosenCPos = terraformer.ChooseSpawnInZoneable( + playerRandom, + rockZoneable, + param.CentralSpawnReservationFraction, + param.MinimumSpawnRadius, + param.SpawnRegionSize, + param.SpawnReservation) + ?? throw new MapGenerationException("Not enough room for player spawns"); + + var spawn = new ActorPlan(map, "mpspawn") + { + Location = chosenCPos, + }; + + terraformer.ProjectPlaceDezoneActor(spawn, rockZoneable, new WDist(param.SpawnReservation * 1024)); + } + + // Close-to-player spice bloom spawn generation + if (param.BiasedResourceSpawns > 0) + { + // Biased blooms + var walkingDistances = terraformer.TargetWalkingDistance( + playable, + terraformer.ErodeZones(sandZoneable, param.ResourceSpawnSpacing), + terraformer.ActorsOfType("mpspawn").Select(a => a.Location), + new WDist(0), + new WDist(1024000)); + for (var i = 0; i < param.BiasedResourceSpawns; i++) + { + var (chosenMpos, score) = CellLayerUtils.FindRandomBest( + walkingDistances, + expansionRandom, + (a, b) => a.CompareTo(b)); + if (score == -int.MaxValue) + throw new MapGenerationException("failed to place spice blooms near players"); + + terraformer.ProjectPlaceDezoneActor( + new ActorPlan(map, param.ResourceSpawn) + { + Location = chosenMpos.ToCPos(map), + }, + sandZoneable, + new WDist(param.ResourceSpawnReservation * 1024)); + foreach (var mpos in map.AllCells.MapCoords) + if (!sandZoneable[mpos]) + walkingDistances[mpos] = -int.MaxValue; + } + } + + // Unbiased spice bloom spawn generation + { + var targetResourceSpawnCount = (int)(param.UnbiasedResourceSpawns * perSymmetryEntityMultiplier / EntityBonusMax); + for (var i = 0; i < targetResourceSpawnCount; i++) + { + var added = terraformer.AddActor( + expansionRandom, + sandZoneable, + param.ResourceSpawn, + new WDist(param.ResourceSpawnReservation * 1024)); + if (!added) + break; + } + } + + // Worms + { + var targetWormSpawnCount = (int)(param.WormSpawns * perSymmetryEntityMultiplier / EntityBonusMax); + for (var i = 0; i < targetWormSpawnCount; i++) + { + var added = terraformer.AddActor( + expansionRandom, + sandZoneable, + param.WormSpawn, + new WDist(param.WormSpawnReservation * 1024)); + if (!added) + break; + } + } + + // Grow resources + var targetResourceValue = param.ResourcesPerPlayer * entityMultiplier / EntityBonusMax; + if (targetResourceValue > 0) + { + var resourcePattern = terraformer.ResourceNoise( + resourceRandom, + param.ResourceFeatureSize, + param.ResourceClumpiness, + param.ResourceUniformity * 1024 / FractionMax); + + var resourceBiases = new List(); + + // Bias towards resource spawns + resourceBiases.AddRange( + terraformer.ActorsOfType(param.ResourceSpawn) + .Select(a => new Terraformer.ResourceBias(a) + { + BiasRadius = new WDist(16 * 1024), + Bias = (value, rSq) => value + (int)(1024 * 1024 / (1024 + Exts.ISqrt(rSq))), + })); + + var (plan, typePlan) = terraformer.PlanResources( + resourcePattern, + spiceZoneable, + param.Resource, + resourceBiases); + terraformer.GrowResources( + plan, + typePlan, + targetResourceValue); + terraformer.ZoneFromResources(sandZoneable, false); + } + } + + terraformer.BakeMap(); + + return map; + } + + string IEditorToolInfo.Label => Name; + string IEditorToolInfo.PanelWidget => PanelWidget; + } + + public class D2kMapGenerator { /* we're only interested in the Info */ } +} diff --git a/mods/d2k/chrome/dropdowns.yaml b/mods/d2k/chrome/dropdowns.yaml index c663033b7c..014f92d0a3 100644 --- a/mods/d2k/chrome/dropdowns.yaml +++ b/mods/d2k/chrome/dropdowns.yaml @@ -26,6 +26,36 @@ ScrollPanel@LABEL_DROPDOWN_TEMPLATE: Width: PARENT_WIDTH - 20 Height: 25 +ScrollPanel@LABEL_DROPDOWN_WITH_TOOLTIP_TEMPLATE: + Width: DROPDOWN_WIDTH + Children: + ScrollItem@HEADER: + Background: scrollheader + Width: PARENT_WIDTH - 27 + Height: 13 + X: 2 + Y: 0 + Visible: false + Children: + Label@LABEL: + Font: TinyBold + Width: PARENT_WIDTH + Height: 13 + Align: Center + ScrollItem@TEMPLATE: + Width: PARENT_WIDTH - 27 + Height: 25 + X: 2 + Y: 0 + TooltipContainer: TOOLTIP_CONTAINER + TooltipTemplate: BUTTON_TOOLTIP + Visible: false + Children: + Label@LABEL: + X: 10 + Width: PARENT_WIDTH - 20 + Height: 25 + ScrollPanel@PLAYERACTION_DROPDOWN_TEMPLATE: Width: DROPDOWN_WIDTH Children: diff --git a/mods/d2k/fluent/rules.ftl b/mods/d2k/fluent/rules.ftl index 3dd38819ab..2ac1a43b1a 100644 --- a/mods/d2k/fluent/rules.ftl +++ b/mods/d2k/fluent/rules.ftl @@ -103,6 +103,9 @@ faction-smugglers = faction-fremen = .name = Fremen +map-generator-d2k = D2K RMG +map-generator-clear = Clear Terrain + ## defaults.yaml notification-unit-lost = Unit lost. notification-unit-promoted = Unit promoted. @@ -665,3 +668,95 @@ bot-vidius = bot-gladius = .name = Gladius + +## map-generators.yaml +label-random-map = Random Map +label-clear-map-generator-option-tile = Tile +label-clear-map-generator-choice-tile-sand = + .label = Sand +label-clear-map-generator-choice-tile-concrete = + .label = Concrete +label-clear-map-generator-choice-tile-dune = + .label = Dune +label-clear-map-generator-choice-tile-rock = + .label = Rock +label-clear-map-generator-choice-tile-platform = + .label = Platform + +label-d2k-map-generator-option-seed = Seed +label-d2k-map-generator-option-terrain-type = Terrain Type +label-d2k-map-generator-choice-terrain-type-rocky = + .label = Rocky +label-d2k-map-generator-choice-terrain-type-rough = + .label = Rough +label-d2k-map-generator-choice-terrain-type-flat = + .label = Flat +label-d2k-map-generator-choice-terrain-type-pockets = + .label = Pockets +label-d2k-map-generator-option-players = Players + +label-d2k-map-generator-option-symmetry = Symmetry +label-d2k-map-generator-choice-mirror-none = + .label = None +label-d2k-map-generator-choice-symmetry-mirror-horizontal = + .label = Mirror Horizontal +label-d2k-map-generator-choice-symmetry-mirror-vertical = + .label = Mirror Vertical +label-d2k-map-generator-choice-symmetry-mirror-diagonal-tl = + .label = Mirror Diagonal (Top-Left) +label-d2k-map-generator-choice-symmetry-mirror-diagonal-tr = + .label = Mirror Diagonal (Top-Right) +label-d2k-map-generator-choice-symmetry-mirror-2-rotations = + .label = 2 Rotations +label-d2k-map-generator-choice-symmetry-mirror-3-rotations = + .label = 3 Rotations +label-d2k-map-generator-choice-symmetry-mirror-4-rotations = + .label = 4 Rotations +label-d2k-map-generator-choice-symmetry-mirror-5-rotations = + .label = 5 Rotations +label-d2k-map-generator-choice-symmetry-mirror-6-rotations = + .label = 6 Rotations +label-d2k-map-generator-choice-symmetry-mirror-7-rotations = + .label = 7 Rotations +label-d2k-map-generator-choice-symmetry-mirror-8-rotations = + .label = 8 Rotations + +label-d2k-map-generator-option-resources = Resources +label-d2k-map-generator-choice-resources-none = + .label = None +label-d2k-map-generator-choice-resources-low = + .label = Low +label-d2k-map-generator-choice-resources-medium = + .label = Medium +label-d2k-map-generator-choice-resources-high = + .label = High +label-d2k-map-generator-choice-resources-very-high = + .label = Very High +label-d2k-map-generator-choice-resources-full = + .label = Full + +label-d2k-map-generator-option-worms = Worms +label-d2k-map-generator-choice-worms-none = + .label = None +label-d2k-map-generator-choice-worms-low = + .label = Low +label-d2k-map-generator-choice-worms-medium = + .label = Medium +label-d2k-map-generator-choice-worms-high = + .label = High + +label-d2k-map-generator-option-density = Density +label-d2k-map-generator-choice-density-players = + .label = Scale with players +label-d2k-map-generator-choice-density-area-and-players = + .label = Scale with size and players +label-d2k-map-generator-choice-density-area-very-low = + .label = Very Low +label-d2k-map-generator-choice-density-area-low = + .label = Low +label-d2k-map-generator-choice-density-area-medium = + .label = Medium +label-d2k-map-generator-choice-density-area-high = + .label = High +label-d2k-map-generator-choice-density-area-very-high = + .label = Very High diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index 960ece2421..27a7f73895 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -53,6 +53,7 @@ Rules: d2k|rules/starport.yaml d2k|rules/husks.yaml d2k|rules/arrakis.yaml + d2k|rules/map-generators.yaml Sequences: d2k|sequences/aircraft.yaml diff --git a/mods/d2k/rules/map-generators.yaml b/mods/d2k/rules/map-generators.yaml new file mode 100644 index 0000000000..799f51aeda --- /dev/null +++ b/mods/d2k/rules/map-generators.yaml @@ -0,0 +1,328 @@ +^MapGenerators: + D2kMapGenerator@d2k: + Type: d2k + Name: map-generator-d2k + Tilesets: ARRAKIS + Settings: + MultiChoiceOption@hidden_defaults: + Choice@hidden_defaults: + Settings: + Rotations: 1 + Mirror: None + TerrainFeatureSize: 10240 + SandDetailFeatureSize: 10240 + DuneFeatureSize: 10240 + ResourceFeatureSize: 20480 + TerrainSmoothing: 4 + DuneSmoothing: 2 + SmoothingThreshold: 833 + RockRoughness: 350 + SandRoughness: 500 + RoughnessRadius: 5 + Rock: 350 + SandCliffs: 500 + Dunes: 200 + MinimumRockStraight: 3 + MinimumSandCliffStraight: 1 + MinimumRockSandThickness: 6 + MinimumSandCliffThickness: 5 + MinimumDuneThickness: 2 + MinimumRockSmoothLength: 6 + MinimumSandRockCliffLength: 6 + MinimumSandSandCliffLength: 6 + MinimumSandLength: 2 + SandContourSpacing: 5 + DuneContourSpacing: 2 + SandDetail: 200 + SandDetailClumpiness: 1 + SandDetailCutout: 2 + MaximumSandDetailCutoutSpacing: 12 + + CreateEntities: True + AreaEntityBonus: 0 + PlayerCountEntityBonus: 1000000 + MinimumSpawnRockArea: 150 + CentralSpawnReservationFraction: 250 + SpawnRegionSize: 12 + MinimumSpawnRadius: 3 + SpawnReservation: 16 + BiasedResourceSpawns: 2 + ResourceSpawnSpacing: 2 + UnbiasedResourceSpawns: 5 + ResourceSpawnReservation: 8 + ResourcesPerPlayer: 125000 + ResourceUniformity: 250 + ResourceClumpiness: 2 + ResourceSpawn: spicebloom.spawnpoint + Resource: Spice + WormSpawn: wormspawner + WormSpawns: 1 + WormSpawnReservation: 12 + + SandTile: 0 + RockTile: 266 + PlayableTerrain: Clear,Concrete,Dune,Rock,Rough,Sand,Spice,SpiceSand,Transition + RockZoneableTerrain: Rock + SandZoneableTerrain: SpiceSand + RockSmoothSegmentType: RockSmooth + SandRockCliffSegmentType: SandRockCliff + SandSandCliffSegmentType: SandSandCliff + SandSegmentType: Sand + DuneSegmentType: Dune + SandDetailBrushes: Rough-Sand-Detail + DuneBrushes: Dune + IntegerOption@Seed: + Label: label-d2k-map-generator-option-seed + Parameter: Seed + Default: 0 + MultiChoiceOption@TerrainType: + Label: label-d2k-map-generator-option-terrain-type + Priority: 2 + Default: Rocky + Choice@Rocky: + Label: label-d2k-map-generator-choice-terrain-type-rocky + Settings: + TerrainFeatureSize: 10240 + SandDetailFeatureSize: 10240 + DuneFeatureSize: 10240 + RockRoughness: 350 + SandRoughness: 500 + Rock: 350 + SandCliffs: 500 + Dunes: 200 + SandDetail: 200 + Choice@Rough: + Label: label-d2k-map-generator-choice-terrain-type-rough + Settings: + TerrainFeatureSize: 10240 + SandDetailFeatureSize: 10240 + DuneFeatureSize: 10240 + RockRoughness: 300 + SandRoughness: 300 + Rock: 300 + SandCliffs: 500 + Dunes: 200 + SandDetail: 200 + Choice@Flat: + Label: label-d2k-map-generator-choice-terrain-type-flat + Settings: + TerrainFeatureSize: 10240 + SandDetailFeatureSize: 10240 + DuneFeatureSize: 10240 + RockRoughness: 0 + SandRoughness: 0 + Rock: 250 + SandCliffs: 0 + Dunes: 200 + SandDetail: 50 + Choice@Pockets: + Label: label-d2k-map-generator-choice-terrain-type-pockets + Settings: + TerrainFeatureSize: 10240 + SandDetailFeatureSize: 10240 + DuneFeatureSize: 10240 + RockRoughness: 300 + SandRoughness: 250 + Rock: 650 + SandCliffs: 500 + Dunes: 0 + SandDetail: 50 + MultiIntegerChoiceOption@Players: + Label: label-d2k-map-generator-option-players + Parameter: Players + Choices: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 + Default: 2 + Priority: 1 + MultiChoiceOption@Symmetry: + Label: label-d2k-map-generator-option-symmetry + Default: 2Rotations + Priority: 1 + Choice@None: + Label: label-d2k-map-generator-choice-mirror-none + Settings: + Mirror: None + Choice@LeftMatchesRight: + Label: label-d2k-map-generator-choice-symmetry-mirror-horizontal + Players: 2, 4, 6, 8, 10, 12, 14, 16 + Settings: + Mirror: LeftMatchesRight + Choice@TopLeftMatchesBottomRight: + Label: label-d2k-map-generator-choice-symmetry-mirror-diagonal-tl + Players: 2, 4, 6, 8, 10, 12, 14, 16 + Settings: + Mirror: TopLeftMatchesBottomRight + Choice@TopMatchesBottom: + Label: label-d2k-map-generator-choice-symmetry-mirror-vertical + Players: 2, 4, 6, 8, 10, 12, 14, 16 + Settings: + Mirror: TopMatchesBottom + Choice@TopRightMatchesBottomLeft: + Label: label-d2k-map-generator-choice-symmetry-mirror-diagonal-tr + Players: 2, 4, 6, 8, 10, 12, 14, 16 + Settings: + Mirror: TopRightMatchesBottomLeft + Choice@2Rotations: + Label: label-d2k-map-generator-choice-symmetry-mirror-2-rotations + Players: 2, 4, 6, 8, 10, 12, 14, 16 + Settings: + Rotations: 2 + Choice@3Rotations: + Label: label-d2k-map-generator-choice-symmetry-mirror-3-rotations + Players: 3, 6, 9, 12, 15 + Settings: + Rotations: 3 + Choice@4Rotations: + Label: label-d2k-map-generator-choice-symmetry-mirror-4-rotations + Players: 4, 8, 12, 16 + Settings: + Rotations: 4 + Choice@5Rotations: + Label: label-d2k-map-generator-choice-symmetry-mirror-5-rotations + Players: 5, 10, 15 + Settings: + Rotations: 5 + Choice@6Rotations: + Label: label-d2k-map-generator-choice-symmetry-mirror-6-rotations + Players: 6, 12 + Settings: + Rotations: 6 + Choice@7Rotations: + Label: label-d2k-map-generator-choice-symmetry-mirror-7-rotations + Players: 7, 14 + Settings: + Rotations: 7 + Choice@8Rotations: + Label: label-d2k-map-generator-choice-symmetry-mirror-8-rotations + Players: 8, 16 + Settings: + Rotations: 8 + MultiChoiceOption@Resources: + Label: label-d2k-map-generator-option-resources + Default: Medium + Choice@None: + Label: label-d2k-map-generator-choice-resources-none + Settings: + BiasedResourceSpawns: 0 + UnbiasedResourceSpawns: 0 + ResourcesPerPlayer: 0 + Choice@Low: + Label: label-d2k-map-generator-choice-resources-low + Settings: + BiasedResourceSpawns: 1 + UnbiasedResourceSpawns: 2 + ResourcesPerPlayer: 75000 + Choice@Medium: + Label: label-d2k-map-generator-choice-resources-medium + Settings: + BiasedResourceSpawns: 2 + UnbiasedResourceSpawns: 4 + ResourcesPerPlayer: 125000 + Choice@High: + Label: label-d2k-map-generator-choice-resources-high + Settings: + BiasedResourceSpawns: 3 + UnbiasedResourceSpawns: 6 + ResourcesPerPlayer: 150000 + Choice@VeryHigh: + Label: label-d2k-map-generator-choice-resources-very-high + Settings: + BiasedResourceSpawns: 4 + UnbiasedResourceSpawns: 8 + ResourcesPerPlayer: 200000 + Choice@Full: + Label: label-d2k-map-generator-choice-resources-full + Settings: + BiasedResourceSpawns: 1 + UnbiasedResourceSpawns: 1000000 + ResourcesPerPlayer: 1000000000 + MultiChoiceOption@Worms: + Label: label-d2k-map-generator-option-worms + Default: Low + Choice@None: + Label: label-d2k-map-generator-choice-worms-none + Settings: + WormSpawns: 0 + Choice@Low: + Label: label-d2k-map-generator-choice-worms-low + Settings: + WormSpawns: 1 + Choice@Medium: + Label: label-d2k-map-generator-choice-worms-medium + Settings: + WormSpawns: 2 + Choice@High: + Label: label-d2k-map-generator-choice-worms-high + Settings: + WormSpawns: 4 + MultiChoiceOption@Density: + Label: label-d2k-map-generator-option-density + Default: Players + Priority: 1 + Choice@Players: + Label: label-d2k-map-generator-choice-density-players + Settings: + AreaEntityBonus: 0 + PlayerCountEntityBonus: 1000000 + Choice@AreaAndPlayers: + Label: label-d2k-map-generator-choice-density-area-and-players + Settings: + AreaEntityBonus: 200 + PlayerCountEntityBonus: 500000 + Choice@AreaVeryLow: + Label: label-d2k-map-generator-choice-density-area-very-low + Settings: + AreaEntityBonus: 100 + PlayerCountEntityBonus: 0 + Choice@AreaLow: + Label: label-d2k-map-generator-choice-density-area-low + Settings: + AreaEntityBonus: 200 + PlayerCountEntityBonus: 0 + Choice@AreaMedium: + Label: label-d2k-map-generator-choice-density-area-medium + Settings: + AreaEntityBonus: 400 + PlayerCountEntityBonus: 0 + Choice@AreaHigh: + Label: label-d2k-map-generator-choice-density-area-high + Settings: + AreaEntityBonus: 600 + PlayerCountEntityBonus: 0 + Choice@AreaVeryHigh: + Label: label-d2k-map-generator-choice-density-area-very-high + Settings: + AreaEntityBonus: 800 + PlayerCountEntityBonus: 0 + + ClearMapGenerator@clear: + Type: clear + Name: map-generator-clear + Tilesets: ARRAKIS + Settings: + MultiChoiceOption@Tile: + Label: label-clear-map-generator-option-tile + Choice@Sand: + Label: label-clear-map-generator-choice-tile-sand + Tileset: ARRAKIS + Settings: + Tile: 0 + Choice@Concrete: + Label: label-clear-map-generator-choice-tile-concrete + Tileset: ARRAKIS + Settings: + Tile: 88 + Choice@Dune: + Label: label-clear-map-generator-choice-tile-dune + Tileset: ARRAKIS + Settings: + Tile: 241 + Choice@Rock: + Label: label-clear-map-generator-choice-tile-rock + Tileset: ARRAKIS + Settings: + Tile: 266 + Choice@Platform: + Label: label-clear-map-generator-choice-tile-platform + Tileset: ARRAKIS + Settings: + Tile: 1017 diff --git a/mods/d2k/rules/world.yaml b/mods/d2k/rules/world.yaml index dd393160e6..fca6ebb002 100644 --- a/mods/d2k/rules/world.yaml +++ b/mods/d2k/rules/world.yaml @@ -271,3 +271,4 @@ EditorWorld: DefaultStart: RockSmooth DefaultInner: RockSmooth DefaultEnd: RockSmooth + Inherits@MapGenerators: ^MapGenerators diff --git a/mods/d2k/tilesets/arrakis.yaml b/mods/d2k/tilesets/arrakis.yaml index e96b5480f7..a10a6d27a2 100644 --- a/mods/d2k/tilesets/arrakis.yaml +++ b/mods/d2k/tilesets/arrakis.yaml @@ -8529,56 +8529,48 @@ MultiBrushCollections: Template: 181 Segment: Start: SandRockCliff.R - Inner: SandRockCliff End: RockSmooth.D Points: 0,1, 1,1, 1,2 MultiBrush@245: Template: 189 Segment: Start: RockSmooth.U - Inner: SandRockCliff End: SandRockCliff.R - Points: 2,1, 1,1, 2,1 + Points: 1,2, 1,1, 2,1 MultiBrush@246: Template: 190 Segment: Start: SandRockCliff.D - Inner: SandRockCliff End: RockSmooth.L Points: 1,0, 1,1, 0,1 MultiBrush@247: Template: 199 Segment: Start: RockSmooth.R - Inner: SandRockCliff End: SandRockCliff.D Points: 0,1, 1,1 MultiBrush@248: Template: 193 Segment: Start: SandRockCliff.U - Inner: SandRockCliff End: RockSmooth.R Points: 1,2, 1,1, 2,1 MultiBrush@249: Template: 222 Segment: Start: RockSmooth.L - Inner: SandRockCliff End: SandRockCliff.U Points: 2,1, 1,1, 1,0 MultiBrush@250: Template: 195 Segment: Start: SandRockCliff.L - Inner: SandRockCliff End: RockSmooth.U Points: 2,1, 1,1, 1,0 MultiBrush@251: Template: 198 Segment: Start: RockSmooth.D - Inner: SandRockCliff End: SandRockCliff.L Points: 1,0, 1,1, 0,1 MultiBrush@252: # RockRockCliff transition SandRockCliff @@ -8693,5 +8685,7 @@ MultiBrushCollections: Inner: RockRockCliff End: RockRockCliff.L Points: 2,1, 1,1, 0,1 - - + Rough-Sand-Detail: + FromTemplates: 118,119,120,121,122,124,125,214,249,274,283,287,288,289,332,337,338,354,355,356,357,358,385,403,404,405,406,407,408,409,429,431,434,436,477,482,1021,1024,1025,1026,1027,1028,1029,1030,1046,1047,1048,1049,1050,1051,1052,1053,1054,1059,1061,1062,1063,1064,1069,1070,1078,1079 + Dune: + FromTemplates: 224,230,231,241