Add D2K random map generator

Adds a random map generator tailored for the Dune 2000 mod,
D2kMapGenerator.

This map generator has some limitations compared to the RA/CnC
map generator (ExperimentalMapGenerator):

- Symmetry is far less accurate for non-trivial rotations.
- No circular map shapes.
- Unplayable regions are not obstructed and no symmetry corrections
  are applied. This is partly due to mod limitations (e.g., no
  decorative actors), but I've also considered it less important.
- Somewhat higher map generation failure rates, due to more extreme
  play-space requirements.

In order to support the map generator, some additional features are
added to common map generation utilties:

- A "FromTemplates" MultiBrush collection shorthand.
- Path partitioning logic in Terraformer, use to split paths into
  multiple TilingPaths with different segment types.
This commit is contained in:
Ashley Newson
2025-07-09 21:27:17 +01:00
committed by Gustas Kažukauskas
parent 484784bd4d
commit 5f407211ac
9 changed files with 1554 additions and 17 deletions

View File

@@ -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<TileInfo> 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<MultiBrushInfo>();
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();
}
}

View File

@@ -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;
}
/// <summary>
/// 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.
/// </summary>
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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="path">The path to be divided.</param>
/// <param name="allZones">The full set of zones, in order of preference for ties.</param>
/// <param name="zoneMask">
/// Matrix which assigns zones to matching points in the path.
/// Null values can be used to describe locations with no zoning preference.
/// </param>
/// <param name="brushes">Segmented brushes for TilingPath creation.</param>
/// <param name="minimumStraight">
/// 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.
/// </param>
public List<TilingPath> PartitionPath(
int2[] path,
IReadOnlyList<PathPartitionZone> allZones,
Matrix<PathPartitionZone> zoneMask,
IReadOnlyList<MultiBrush> 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<int> Range(int from, int length)
{
for (var i = 0; i < length; i++)
yield return (from + i) % zones.Length;
}
// from must be >= 0.
IEnumerable<int> 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<PathPartitionZone> majorities = null)
{
var votes = new Dictionary<PathPartitionZone, int>();
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<TilingPath> 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<PathPartitionZone>();
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<int> 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<int> 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<int>(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<int> 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<PathPartitionZone>();
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<TilingPath>();
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;
}
/// <summary>Wrapper around PartitionPath to process multiple paths at once.</summary>
public List<TilingPath> PartitionPaths(
IEnumerable<int2[]> paths,
IReadOnlyList<PathPartitionZone> zones,
Matrix<PathPartitionZone> partitionMask,
IReadOnlyList<MultiBrush> brushes,
int minStraight)
{
return paths
.SelectMany(path => PartitionPath(
path, zones, partitionMask, brushes, minStraight))
.ToList();
}
/// <summary>
/// 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.

View File

@@ -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<D2kMapGenerator>, 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<string> 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<string> 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<byte> PlayableTerrain;
[FieldLoader.Ignore]
public readonly IReadOnlySet<byte> RockZoneableTerrain = default;
[FieldLoader.Ignore]
public readonly IReadOnlySet<byte> 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<MultiBrush> SegmentedBrushes;
[FieldLoader.Ignore]
public readonly IReadOnlyList<MultiBrush> SandDetailBrushes;
[FieldLoader.Ignore]
public readonly IReadOnlyList<MultiBrush> DuneBrushes;
public Parameters(Map map, MiniYaml my)
{
FieldLoader.Load(this, my);
var terrainInfo = (ITemplatedTerrainInfo)map.Rules.TerrainInfo;
IReadOnlySet<byte> ParseTerrainIndexes(string key)
{
return my.NodeWithKey(key).Value.Value
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(terrainInfo.GetTerrainIndex)
.ToImmutableHashSet();
}
var resourceTypes = map.Rules.Actors[SystemActors.World].TraitInfoOrDefault<ResourceLayerInfo>().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<ActorPlan>();
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<Terraformer.Side> 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<Terraformer.ResourceBias>();
// 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 */ }
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -271,3 +271,4 @@ EditorWorld:
DefaultStart: RockSmooth
DefaultInner: RockSmooth
DefaultEnd: RockSmooth
Inherits@MapGenerators: ^MapGenerators

View File

@@ -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