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:
committed by
Gustas Kažukauskas
parent
484784bd4d
commit
5f407211ac
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
622
OpenRA.Mods.D2k/Traits/World/D2kMapGenerator.cs
Normal file
622
OpenRA.Mods.D2k/Traits/World/D2kMapGenerator.cs
Normal 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 */ }
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
328
mods/d2k/rules/map-generators.yaml
Normal file
328
mods/d2k/rules/map-generators.yaml
Normal 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
|
||||
@@ -271,3 +271,4 @@ EditorWorld:
|
||||
DefaultStart: RockSmooth
|
||||
DefaultInner: RockSmooth
|
||||
DefaultEnd: RockSmooth
|
||||
Inherits@MapGenerators: ^MapGenerators
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user