Updates the random map generator to produce more elaborate roads: - Roads extend beyond map bounds. - Shorter, unviable roads are pruned and merged to create longer roads. - Roads can loop.
1636 lines
58 KiB
C#
1636 lines
58 KiB
C#
#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.Primitives;
|
|
using OpenRA.Support;
|
|
using OpenRA.Traits;
|
|
using static OpenRA.Mods.Common.Traits.ResourceLayerInfo;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
[TraitLocation(SystemActors.World | SystemActors.EditorWorld)]
|
|
public sealed class RaMapGeneratorInfo : TraitInfo, IMapGeneratorInfo
|
|
{
|
|
[FieldLoader.Require]
|
|
public readonly string Type = null;
|
|
|
|
[FieldLoader.Require]
|
|
[FluentReference]
|
|
public readonly string Name = null;
|
|
|
|
// 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;
|
|
|
|
public override object Create(ActorInitializer init) { return new RaMapGenerator(this); }
|
|
|
|
static MiniYaml SettingsLoader(MiniYaml my)
|
|
{
|
|
return my.NodeWithKey("Settings").Value;
|
|
}
|
|
|
|
static List<string> FluentReferencesLoader(MiniYaml my)
|
|
{
|
|
return MapGeneratorSettings.DumpFluent(my.NodeWithKey("Settings").Value);
|
|
}
|
|
}
|
|
|
|
public sealed class RaMapGenerator : IMapGenerator
|
|
{
|
|
sealed class Parameters
|
|
{
|
|
[FieldLoader.Ignore]
|
|
public readonly Map Map;
|
|
[FieldLoader.Ignore]
|
|
public readonly ITemplatedTerrainInfo TemplatedTerrainInfo;
|
|
|
|
[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 float TerrainFeatureSize = default;
|
|
[FieldLoader.Require]
|
|
public readonly float ForestFeatureSize = default;
|
|
[FieldLoader.Require]
|
|
public readonly float ResourceFeatureSize = default;
|
|
[FieldLoader.Require]
|
|
public readonly float Water = default;
|
|
[FieldLoader.Require]
|
|
public readonly float Mountains = default;
|
|
[FieldLoader.Require]
|
|
public readonly float Forests = default;
|
|
[FieldLoader.Require]
|
|
public readonly int ForestCutout = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MaximumCutoutSpacing = default;
|
|
[FieldLoader.Require]
|
|
public readonly int ExternalCircularBias = default;
|
|
[FieldLoader.Require]
|
|
public readonly int TerrainSmoothing = default;
|
|
[FieldLoader.Require]
|
|
public readonly float SmoothingThreshold = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MinimumLandSeaThickness = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MinimumMountainThickness = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MaximumAltitude = default;
|
|
[FieldLoader.Require]
|
|
public readonly int RoughnessRadius = default;
|
|
[FieldLoader.Require]
|
|
public readonly float Roughness = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MinimumTerrainContourSpacing = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MinimumCliffLength = default;
|
|
[FieldLoader.Require]
|
|
public readonly float ForestClumpiness = default;
|
|
[FieldLoader.Require]
|
|
public readonly bool DenyWalledAreas = default;
|
|
[FieldLoader.Require]
|
|
public readonly int EnforceSymmetry = default;
|
|
[FieldLoader.Require]
|
|
public readonly bool Roads = default;
|
|
[FieldLoader.Require]
|
|
public readonly int RoadSpacing = default;
|
|
[FieldLoader.Require]
|
|
public readonly int RoadShrink = default;
|
|
[FieldLoader.Require]
|
|
public readonly bool CreateEntities = default;
|
|
[FieldLoader.Require]
|
|
public readonly float AreaEntityBonus = default;
|
|
[FieldLoader.Require]
|
|
public readonly float PlayerCountEntityBonus = default;
|
|
[FieldLoader.Require]
|
|
public readonly float CentralSpawnReservationFraction = default;
|
|
[FieldLoader.Require]
|
|
public readonly int ResourceSpawnReservation = default;
|
|
[FieldLoader.Require]
|
|
public readonly int SpawnRegionSize = default;
|
|
[FieldLoader.Require]
|
|
public readonly int SpawnBuildSize = default;
|
|
[FieldLoader.Require]
|
|
public readonly int SpawnResourceSpawns = default;
|
|
[FieldLoader.Require]
|
|
public readonly int SpawnReservation = default;
|
|
[FieldLoader.Require]
|
|
public readonly float SpawnResourceBias = default;
|
|
[FieldLoader.Require]
|
|
public readonly int ResourcesPerPlayer = default;
|
|
[FieldLoader.Require]
|
|
public readonly float OreUniformity = default;
|
|
[FieldLoader.Require]
|
|
public readonly float OreClumpiness = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MaximumExpansionResourceSpawns = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MaximumResourceSpawnsPerExpansion = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MinimumExpansionSize = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MaximumExpansionSize = default;
|
|
[FieldLoader.Require]
|
|
public readonly int ExpansionInner = default;
|
|
[FieldLoader.Require]
|
|
public readonly int ExpansionBorder = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MinimumBuildings = default;
|
|
[FieldLoader.Require]
|
|
public readonly int MaximumBuildings = default;
|
|
[FieldLoader.LoadUsing(nameof(BuildingWeightsLoader))]
|
|
public readonly IReadOnlyDictionary<string, float> BuildingWeights = default;
|
|
|
|
[FieldLoader.Require]
|
|
public readonly ushort LandTile = default;
|
|
[FieldLoader.Require]
|
|
public readonly ushort WaterTile = default;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlyList<MultiBrush> ForestObstacles;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlyList<MultiBrush> UnplayableObstacles;
|
|
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlyDictionary<string, ResourceTypeInfo> ResourceTypes;
|
|
[FieldLoader.Ignore]
|
|
public readonly ResourceTypeInfo DefaultResource;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlyDictionary<ResourceTypeInfo, int> ResourceValues;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlySet<(ResourceTypeInfo, byte)> AllowedTerrainResourceCombos;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlyDictionary<string, ResourceTypeInfo> ResourceSpawnSeeds;
|
|
[FieldLoader.LoadUsing(nameof(ResourceSpawnWeightsLoader))]
|
|
public readonly IReadOnlyDictionary<string, float> ResourceSpawnWeights = default;
|
|
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlySet<byte> ClearTerrain;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlySet<byte> PlayableTerrain;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlySet<byte> PartiallyPlayableTerrain;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlySet<byte> UnplayableTerrain;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlySet<byte> DominantTerrain;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlySet<string> PartiallyPlayableCategories;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlyList<string> ClearSegmentTypes;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlyList<string> BeachSegmentTypes;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlyList<string> CliffSegmentTypes;
|
|
[FieldLoader.Ignore]
|
|
public readonly IReadOnlyList<string> RoadSegmentTypes;
|
|
|
|
[FieldLoader.Ignore]
|
|
public readonly int SymmetryCount;
|
|
[FieldLoader.Ignore]
|
|
public readonly int TotalPlayers;
|
|
|
|
public Parameters(Map map, MiniYaml my)
|
|
{
|
|
FieldLoader.Load(this, my);
|
|
Map = map;
|
|
TemplatedTerrainInfo = Map.Rules.TerrainInfo as ITemplatedTerrainInfo;
|
|
ForestObstacles = MultiBrush.LoadCollection(map, my.NodeWithKey("ForestObstacles").Value.Value);
|
|
UnplayableObstacles = MultiBrush.LoadCollection(map, my.NodeWithKey("UnplayableObstacles").Value.Value);
|
|
|
|
ResourceTypes = map.Rules.Actors[SystemActors.World].TraitInfoOrDefault<ResourceLayerInfo>().ResourceTypes;
|
|
if (!ResourceTypes.TryGetValue(my.NodeWithKey("DefaultResource").Value.Value, out DefaultResource))
|
|
throw new YamlException("DefaultResource is not valid");
|
|
var playerResourcesInfo = map.Rules.Actors[SystemActors.Player].TraitInfoOrDefault<PlayerResourcesInfo>();
|
|
ResourceValues = playerResourcesInfo.ResourceValues
|
|
.ToDictionary(kv => ResourceTypes[kv.Key], kv => kv.Value);
|
|
AllowedTerrainResourceCombos = ResourceTypes
|
|
.Values
|
|
.SelectMany(resourceTypeInfo => resourceTypeInfo.AllowedTerrainTypes
|
|
.Select(terrainName => (resourceTypeInfo, TemplatedTerrainInfo.GetTerrainIndex(terrainName))))
|
|
.ToImmutableHashSet();
|
|
try
|
|
{
|
|
ResourceSpawnSeeds = my.NodeWithKey("ResourceSpawnSeeds").Value
|
|
.ToDictionary(subMy => subMy.Value)
|
|
.ToDictionary(kv => kv.Key, kv => ResourceTypes[kv.Value]);
|
|
}
|
|
catch (KeyNotFoundException e)
|
|
{
|
|
throw new YamlException("Bad ResourceSpawnSeeds resource: " + e);
|
|
}
|
|
|
|
switch (Rotations)
|
|
{
|
|
case 1:
|
|
case 2:
|
|
case 4:
|
|
break;
|
|
default:
|
|
EnforceSymmetry = 0;
|
|
break;
|
|
}
|
|
|
|
IReadOnlySet<byte> ParseTerrainIndexes(string key)
|
|
{
|
|
return my.NodeWithKey(key).Value.Value
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(TemplatedTerrainInfo.GetTerrainIndex)
|
|
.ToImmutableHashSet();
|
|
}
|
|
|
|
IReadOnlyList<string> ParseSegmentTypes(string key)
|
|
{
|
|
return my.NodeWithKey(key).Value.Value
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
.ToImmutableArray();
|
|
}
|
|
|
|
ClearTerrain = ParseTerrainIndexes("ClearTerrain");
|
|
PlayableTerrain = ParseTerrainIndexes("PlayableTerrain");
|
|
PartiallyPlayableTerrain = ParseTerrainIndexes("PartiallyPlayableTerrain");
|
|
UnplayableTerrain = ParseTerrainIndexes("UnplayableTerrain");
|
|
DominantTerrain = ParseTerrainIndexes("DominantTerrain");
|
|
|
|
PartiallyPlayableCategories = my.NodeWithKey("PartiallyPlayableCategories").Value.Value
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
.ToImmutableHashSet();
|
|
|
|
ClearSegmentTypes = ParseSegmentTypes("ClearSegmentTypes");
|
|
BeachSegmentTypes = ParseSegmentTypes("BeachSegmentTypes");
|
|
CliffSegmentTypes = ParseSegmentTypes("CliffSegmentTypes");
|
|
RoadSegmentTypes = ParseSegmentTypes("RoadSegmentTypes");
|
|
|
|
SymmetryCount = Symmetry.RotateAndMirrorProjectionCount(Rotations, Mirror);
|
|
TotalPlayers = Players * SymmetryCount;
|
|
|
|
Validate();
|
|
}
|
|
|
|
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}`");
|
|
}
|
|
|
|
static IReadOnlyDictionary<string, float> BuildingWeightsLoader(MiniYaml my)
|
|
{
|
|
return my.NodeWithKey("BuildingWeights").Value.ToDictionary(subMy =>
|
|
{
|
|
if (Exts.TryParseFloatOrPercentInvariant(subMy.Value, out var f))
|
|
return f;
|
|
else
|
|
throw new YamlException($"Invalid building weight `{subMy.Value}`");
|
|
});
|
|
}
|
|
|
|
static IReadOnlyDictionary<string, float> ResourceSpawnWeightsLoader(MiniYaml my)
|
|
{
|
|
return my.NodeWithKey("ResourceSpawnWeights").Value.ToDictionary(subMy =>
|
|
{
|
|
if (Exts.TryParseFloatOrPercentInvariant(subMy.Value, out var f))
|
|
return f;
|
|
else
|
|
throw new YamlException($"Invalid resource spawn weight `{subMy.Value}`");
|
|
});
|
|
}
|
|
|
|
public void Validate()
|
|
{
|
|
if (Rotations < 1)
|
|
throw new MapGenerationException("Rotations must be >= 1");
|
|
if (TerrainFeatureSize < 1.0f)
|
|
throw new MapGenerationException("TerrainFeatureSize must be >= 1.0");
|
|
if (ForestFeatureSize < 1.0f)
|
|
throw new MapGenerationException("ForestFeatureSize must be >= 1.0");
|
|
if (ResourceFeatureSize < 1.0f)
|
|
throw new MapGenerationException("ResourceFeatureSize must be >= 1.0");
|
|
if (TerrainSmoothing < 1)
|
|
throw new MapGenerationException("TerrainSmoothing must be < 1");
|
|
if (SmoothingThreshold < 0.5f || SmoothingThreshold > 1.0f)
|
|
throw new MapGenerationException("SmoothingThreshold must be between 0.5 and 1.0 inclusive");
|
|
if (MinimumLandSeaThickness < 1)
|
|
throw new MapGenerationException("MinimumLandSeaThickness must be >= 1");
|
|
if (MinimumMountainThickness < 1)
|
|
throw new MapGenerationException("MinimumMountainThickness must be >= 1");
|
|
if (Water < 0.0f || Water > 1.0f)
|
|
throw new MapGenerationException("Water must be between 0.0 and 1.0 inclusive");
|
|
if (Forests < 0.0f || Forests > 1.0f)
|
|
throw new MapGenerationException("Forest must be between 0.0 and 1.0 inclusive");
|
|
if (ForestCutout < 0)
|
|
throw new MapGenerationException("ForestCutout must be >= 0");
|
|
if (MaximumCutoutSpacing < 0)
|
|
throw new MapGenerationException("TopologyAugmentationThreshold must be >= 0");
|
|
if (ForestClumpiness < 0.0f)
|
|
throw new MapGenerationException("ForestClumpiness must be >= 0.0");
|
|
if (Mountains < 0.0f || Mountains > 1.0f)
|
|
throw new MapGenerationException("Mountains must be between 0.0 and 1.0 inclusive");
|
|
if (Roughness < 0.0f || Roughness > 1.0f)
|
|
throw new MapGenerationException("Roughness must be between 0.0 and 1.0");
|
|
if (RoughnessRadius < 1)
|
|
throw new MapGenerationException("RoughnessRadius must be >= 1");
|
|
if (MaximumAltitude < 0)
|
|
throw new MapGenerationException("MaximumAltitude must be >= 0");
|
|
if (MinimumTerrainContourSpacing < 0)
|
|
throw new MapGenerationException("MinimumTerrainContourSpacing must be >= 0");
|
|
if (MinimumCliffLength < 1)
|
|
throw new MapGenerationException("MinimumCliffLength must be >= 1");
|
|
if (RoadSpacing < 0)
|
|
throw new MapGenerationException("RoadSpacing must be >= 0");
|
|
if (RoadShrink < 0)
|
|
throw new MapGenerationException("RoadShrink must be >= 0");
|
|
if (Players < 0)
|
|
throw new MapGenerationException("Players must be >= 0");
|
|
if (CentralSpawnReservationFraction < 0.0f)
|
|
throw new MapGenerationException("CentralSpawnReservationFraction must be >= 0.0");
|
|
if (AreaEntityBonus < 0.0f)
|
|
throw new MapGenerationException("PlayableAreaDensityBonus must be >= 0.0");
|
|
if (PlayerCountEntityBonus < 0.0f)
|
|
throw new MapGenerationException("PlayerCountDensityBonus must be >= 0.0");
|
|
if (SpawnRegionSize < 1)
|
|
throw new MapGenerationException("SpawnRegionSize must be >= 1");
|
|
if (SpawnReservation < 1)
|
|
throw new MapGenerationException("SpawnReservation must be >= 1");
|
|
if (SpawnBuildSize < 1)
|
|
throw new MapGenerationException("SpawnBuildSize must be >= 1");
|
|
if (SpawnResourceSpawns < 0)
|
|
throw new MapGenerationException("SpawnResourceSpawns must be >= 0");
|
|
if (ResourceSpawnReservation < 1)
|
|
throw new MapGenerationException("ResourceSpawnReservation must be >= 1");
|
|
if (MaximumExpansionResourceSpawns < 0)
|
|
throw new MapGenerationException("MaximumExpansionResourceSpawns must be >= 0");
|
|
if (MinimumExpansionSize < 1)
|
|
throw new MapGenerationException("MinimumExpansionSize must be >= 1");
|
|
if (MaximumExpansionSize < 1)
|
|
throw new MapGenerationException("MaximumExpansionSize must be >= 1");
|
|
if (MinimumExpansionSize > MaximumExpansionSize)
|
|
throw new MapGenerationException("MinimumExpansionSize must be <= maximumExpansionSize");
|
|
if (ExpansionBorder < 1)
|
|
throw new MapGenerationException("ExpansionBorder must be >= 1");
|
|
if (ExpansionInner < 1)
|
|
throw new MapGenerationException("ExpansionInner must be >= 1");
|
|
if (MaximumResourceSpawnsPerExpansion < 1)
|
|
throw new MapGenerationException("MaximumResourceSpawnsPerExpansion must be >= 1");
|
|
if (MinimumBuildings < 0)
|
|
throw new MapGenerationException("MinimumBuildings must be >= 0");
|
|
if (MaximumBuildings < 0)
|
|
throw new MapGenerationException("MaximumBuildings must be >= 0");
|
|
if (MinimumBuildings > MaximumBuildings)
|
|
throw new MapGenerationException("MinimumBuildings must be <= maximumBuildings");
|
|
if (ResourcesPerPlayer < 0)
|
|
throw new MapGenerationException("ResourcesPerPlayer must be >= 0");
|
|
if (OreUniformity < 0.0f)
|
|
throw new MapGenerationException("OreUniformity must be >= 0.0");
|
|
if (OreClumpiness < 0.0f)
|
|
throw new MapGenerationException("OreClumpiness must be >= 0.0");
|
|
foreach (var kv in BuildingWeights)
|
|
if (kv.Value < 0.0f)
|
|
throw new MapGenerationException("BuildingWeights.* must be >= 0.0");
|
|
foreach (var kv in ResourceSpawnWeights)
|
|
if (kv.Value < 0.0f)
|
|
throw new MapGenerationException("ResourceSpawnWeights.* must be >= 0.0");
|
|
foreach (var kv in ResourceSpawnWeights)
|
|
if (!ResourceSpawnSeeds.ContainsKey(kv.Key))
|
|
throw new MapGenerationException($"ResourceSpawnSeeds does not contain possible resource spawn `{kv.Key}`");
|
|
|
|
if (!(TemplatedTerrainInfo.Templates.TryGetValue(LandTile, out var landTemplate) && landTemplate.Contains(0)))
|
|
throw new MapGenerationException("LandTile is not valid");
|
|
if (!(TemplatedTerrainInfo.Templates.TryGetValue(LandTile, out var waterTemplate) && waterTemplate.Contains(0)))
|
|
throw new MapGenerationException("WaterTile is not valid");
|
|
|
|
if (TotalPlayers > 32)
|
|
throw new MapGenerationException("Total number of players must not exceed 32");
|
|
}
|
|
|
|
public static (T[] Types, float[] Weights) SplitWeights<T>(IReadOnlyDictionary<T, float> typeWeights)
|
|
{
|
|
var types = typeWeights
|
|
.Select(kv => kv.Key)
|
|
.OrderBy(k => k)
|
|
.ToArray();
|
|
var weights = types
|
|
.Select(type => typeWeights[type])
|
|
.ToArray();
|
|
return (types, weights);
|
|
}
|
|
}
|
|
|
|
readonly RaMapGeneratorInfo info;
|
|
|
|
IMapGeneratorInfo IMapGenerator.Info => info;
|
|
|
|
public RaMapGenerator(RaMapGeneratorInfo info)
|
|
{
|
|
this.info = info;
|
|
}
|
|
|
|
public MapGeneratorSettings GetSettings(Map map)
|
|
{
|
|
return MapGeneratorSettings.LoadSettings(info.Settings, map);
|
|
}
|
|
|
|
public void Generate(Map map, MiniYaml settings)
|
|
{
|
|
const float ExternalBias = 1000000.0f;
|
|
|
|
var size = map.MapSize;
|
|
var minSpan = Math.Min(size.X, size.Y);
|
|
var maxSpan = Math.Max(size.X, size.Y);
|
|
var mapCenter = (size.ToFloat2() - new float2(1.0f, 1.0f)) / 2.0f;
|
|
var wMapCenter = CellLayerUtils.Center(map.Tiles);
|
|
var matrixMapCenter = (CellLayerUtils.CellBounds(map).Size.ToInt2().ToFloat2() - new float2(1.0f, 1.0f)) / 2.0f;
|
|
var cellBounds = CellLayerUtils.CellBounds(map);
|
|
var minCSpan = Math.Min(cellBounds.Size.Width, cellBounds.Size.Height);
|
|
var gridType = map.Grid.Type;
|
|
|
|
var actorPlans = new List<ActorPlan>();
|
|
|
|
var param = new Parameters(map, settings);
|
|
|
|
var externalCircleRadius = minCSpan / 2 - (param.MinimumLandSeaThickness + param.MinimumMountainThickness);
|
|
if (externalCircleRadius <= 0)
|
|
throw new MapGenerationException("map is too small for circular shaping");
|
|
|
|
var trivialMirror =
|
|
size.X == size.Y ||
|
|
param.Mirror == Symmetry.Mirror.None ||
|
|
param.Mirror == Symmetry.Mirror.LeftMatchesRight ||
|
|
param.Mirror == Symmetry.Mirror.TopMatchesBottom;
|
|
|
|
var tileset = param.TemplatedTerrainInfo;
|
|
|
|
var beachPermittedTemplates =
|
|
TilingPath.PermittedSegments.FromType(tileset, param.BeachSegmentTypes);
|
|
var beachTiles = beachPermittedTemplates.PossibleTiles().ToImmutableHashSet();
|
|
|
|
var replaceabilityMap = new Dictionary<TerrainTile, MultiBrush.Replaceability>();
|
|
var playabilityMap = new Dictionary<TerrainTile, PlayableSpace.Playability>();
|
|
foreach (var kv in tileset.Templates)
|
|
{
|
|
var id = kv.Key;
|
|
var template = kv.Value;
|
|
for (var ti = 0; ti < template.TilesCount; ti++)
|
|
{
|
|
if (template[ti] == null)
|
|
continue;
|
|
var tile = new TerrainTile(id, (byte)ti);
|
|
var type = tileset.GetTerrainIndex(tile);
|
|
|
|
if (param.PlayableTerrain.Contains(type))
|
|
playabilityMap[tile] = PlayableSpace.Playability.Playable;
|
|
else if (param.PartiallyPlayableTerrain.Contains(type))
|
|
playabilityMap[tile] = PlayableSpace.Playability.Partial;
|
|
else if (param.UnplayableTerrain.Contains(type))
|
|
playabilityMap[tile] = PlayableSpace.Playability.Unplayable;
|
|
else
|
|
throw new MapGenerationException($"Terrain index {type} has unknown playability.");
|
|
|
|
if (id == param.LandTile)
|
|
{
|
|
replaceabilityMap[tile] = MultiBrush.Replaceability.Any;
|
|
}
|
|
else if (id == param.WaterTile)
|
|
{
|
|
replaceabilityMap[tile] = MultiBrush.Replaceability.Tile;
|
|
}
|
|
else
|
|
{
|
|
if (playabilityMap[tile] == PlayableSpace.Playability.Unplayable)
|
|
replaceabilityMap[tile] = MultiBrush.Replaceability.None;
|
|
else
|
|
replaceabilityMap[tile] = MultiBrush.Replaceability.Actor;
|
|
|
|
if (param.PartiallyPlayableCategories.Overlaps(template.Categories)
|
|
&& playabilityMap[tile] == PlayableSpace.Playability.Unplayable)
|
|
{
|
|
playabilityMap[tile] = PlayableSpace.Playability.Partial;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use `random` to derive separate independent random number generators.
|
|
//
|
|
// This prevents changes in one part of the algorithm from affecting randomness in
|
|
// other parts and provides flexibility for future parallel processing.
|
|
//
|
|
// 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 waterRandom = new MersenneTwister(random.Next());
|
|
var beachTilingRandom = new MersenneTwister(random.Next());
|
|
var cliffTilingRandom = new MersenneTwister(random.Next());
|
|
var forestRandom = new MersenneTwister(random.Next());
|
|
var forestTilingRandom = new MersenneTwister(random.Next());
|
|
var symmetryTilingRandom = new MersenneTwister(random.Next());
|
|
var debrisTilingRandom = new MersenneTwister(random.Next());
|
|
var resourceRandom = new MersenneTwister(random.Next());
|
|
var roadTilingRandom = new MersenneTwister(random.Next());
|
|
var playerRandom = new MersenneTwister(random.Next());
|
|
var expansionRandom = new MersenneTwister(random.Next());
|
|
var buildingRandom = new MersenneTwister(random.Next());
|
|
var topologyRandom = new MersenneTwister(random.Next());
|
|
|
|
TerrainTile PickTile(ushort tileType)
|
|
{
|
|
if (tileset.Templates.TryGetValue(tileType, out var template) && template.PickAny)
|
|
return new TerrainTile(tileType, (byte)random.Next(0, template.TilesCount));
|
|
else
|
|
return new TerrainTile(tileType, 0);
|
|
}
|
|
|
|
foreach (var cell in map.AllCells)
|
|
{
|
|
var mpos = cell.ToMPos(gridType);
|
|
map.Tiles[mpos] = PickTile(param.LandTile);
|
|
map.Resources[mpos] = new ResourceTile(0, 0);
|
|
map.Height[mpos] = 0;
|
|
}
|
|
|
|
var elevation = NoiseUtils.SymmetricFractalNoise(
|
|
waterRandom,
|
|
cellBounds.Size.ToInt2(),
|
|
param.Rotations,
|
|
param.Mirror,
|
|
param.TerrainFeatureSize,
|
|
NoiseUtils.PinkAmplitude);
|
|
|
|
if (param.TerrainSmoothing > 0)
|
|
{
|
|
var radius = param.TerrainSmoothing;
|
|
elevation = MatrixUtils.GaussianBlur(elevation, radius, radius);
|
|
}
|
|
|
|
MatrixUtils.CalibrateQuantileInPlace(
|
|
elevation,
|
|
0.0f,
|
|
param.Water);
|
|
|
|
if (param.ExternalCircularBias != 0)
|
|
MatrixUtils.OverCircle(
|
|
matrix: elevation,
|
|
center: mapCenter,
|
|
radius: externalCircleRadius,
|
|
outside: true,
|
|
action: (xy, _) => elevation[xy] = param.ExternalCircularBias * ExternalBias);
|
|
|
|
var landPlan = MatrixUtils.BooleanBlotch(
|
|
elevation.Map(v => v >= 0),
|
|
param.TerrainSmoothing,
|
|
param.SmoothingThreshold,
|
|
param.MinimumLandSeaThickness,
|
|
/*bias=*/param.Water < 0.5);
|
|
|
|
var beaches = CellLayerUtils.FromMatrixPoints(
|
|
MatrixUtils.BordersToPoints(landPlan),
|
|
map.Tiles);
|
|
if (beaches.Length > 0)
|
|
{
|
|
var tiledBeaches = new CPos[beaches.Length][];
|
|
for (var i = 0; i < beaches.Length; i++)
|
|
{
|
|
var beachPath = new TilingPath(
|
|
map,
|
|
beaches[i],
|
|
(param.MinimumLandSeaThickness - 1) / 2,
|
|
param.BeachSegmentTypes[0],
|
|
param.BeachSegmentTypes[0],
|
|
beachPermittedTemplates);
|
|
beachPath
|
|
.ExtendEdge(4)
|
|
.OptimizeLoop();
|
|
tiledBeaches[i] =
|
|
beachPath.Tile(beachTilingRandom)
|
|
?? throw new MapGenerationException("Could not fit tiles for beach");
|
|
}
|
|
|
|
var beachChiralityMatrix = MatrixUtils.PointsChirality(
|
|
landPlan.Size,
|
|
CellLayerUtils.ToMatrixPoints(tiledBeaches, map.Tiles));
|
|
var beachChirality = new CellLayer<int>(map);
|
|
CellLayerUtils.FromMatrix(beachChirality, beachChiralityMatrix);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
{
|
|
// `map.Tiles[mpos].Type == param.LandTile` avoids overwriting beach tiles.
|
|
if (beachChirality[mpos] < 0 && map.Tiles[mpos].Type == param.LandTile)
|
|
map.Tiles[mpos] = PickTile(param.WaterTile);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// There weren't any coastlines
|
|
var tileType = landPlan[0] ? param.LandTile : param.WaterTile;
|
|
foreach (var cell in map.AllCells)
|
|
{
|
|
var mpos = cell.ToMPos(gridType);
|
|
map.Tiles[mpos] = PickTile(tileType);
|
|
}
|
|
}
|
|
|
|
var nonLoopedCliffPermittedTemplates =
|
|
TilingPath.PermittedSegments.FromInnerAndTerminalTypes(
|
|
tileset, param.CliffSegmentTypes, param.ClearSegmentTypes);
|
|
var loopedCliffPermittedTemplates =
|
|
TilingPath.PermittedSegments.FromType(
|
|
tileset, param.CliffSegmentTypes);
|
|
if (param.ExternalCircularBias > 0)
|
|
{
|
|
var cliffRing = new CellLayer<bool>(map);
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: cliffRing,
|
|
wCenter: wMapCenter,
|
|
wRadius: (externalCircleRadius + param.MinimumMountainThickness) * 1024,
|
|
outside: true,
|
|
action: (mpos, _, _, _) => cliffRing[mpos] = true);
|
|
foreach (var cliff in CellLayerUtils.BordersToPoints(cliffRing))
|
|
{
|
|
var isLoop = cliff[0] == cliff[^1];
|
|
TilingPath cliffPath;
|
|
if (isLoop)
|
|
cliffPath = new TilingPath(
|
|
map,
|
|
cliff,
|
|
(param.MinimumMountainThickness - 1) / 2,
|
|
param.CliffSegmentTypes[0],
|
|
param.CliffSegmentTypes[0],
|
|
loopedCliffPermittedTemplates);
|
|
else
|
|
cliffPath = new TilingPath(
|
|
map,
|
|
cliff,
|
|
(param.MinimumMountainThickness - 1) / 2,
|
|
param.ClearSegmentTypes[0],
|
|
param.ClearSegmentTypes[0],
|
|
nonLoopedCliffPermittedTemplates);
|
|
cliffPath
|
|
.ExtendEdge(4)
|
|
.OptimizeLoop();
|
|
if (cliffPath.Tile(cliffTilingRandom) == null)
|
|
throw new MapGenerationException("Could not fit tiles for exterior circle cliffs");
|
|
}
|
|
}
|
|
|
|
if (param.Mountains > 0.0f || param.ExternalCircularBias == 1)
|
|
{
|
|
var roughnessMatrix = MatrixUtils.GridVariance(
|
|
elevation,
|
|
param.RoughnessRadius)
|
|
.Map(v => MathF.Sqrt(v));
|
|
MatrixUtils.CalibrateQuantileInPlace(
|
|
roughnessMatrix,
|
|
0.0f,
|
|
1.0f - param.Roughness);
|
|
var cliffMask = roughnessMatrix.Map(v => v >= 0.0f);
|
|
var mountainElevation = elevation.Clone();
|
|
var cliffPlan = landPlan;
|
|
if (param.ExternalCircularBias > 0)
|
|
MatrixUtils.OverCircle(
|
|
matrix: cliffPlan,
|
|
center: matrixMapCenter,
|
|
radius: externalCircleRadius,
|
|
outside: true,
|
|
action: (xy, _) => cliffPlan[xy] = false);
|
|
|
|
for (var altitude = 1; altitude <= param.MaximumAltitude; altitude++)
|
|
{
|
|
// Limit mountain area to the existing mountain space (starting with all available land)
|
|
var roominess = MatrixUtils.ChebyshevRoom(cliffPlan, true);
|
|
var available = 0;
|
|
var total = size.X * size.Y;
|
|
for (var n = 0; n < mountainElevation.Data.Length; n++)
|
|
{
|
|
if (roominess.Data[n] < param.MinimumTerrainContourSpacing)
|
|
mountainElevation.Data[n] = -1.0f;
|
|
else
|
|
available++;
|
|
|
|
total++;
|
|
}
|
|
|
|
var availableFraction = (float)available / total;
|
|
MatrixUtils.CalibrateQuantileInPlace(
|
|
mountainElevation,
|
|
0.0f,
|
|
1.0f - availableFraction * param.Mountains);
|
|
cliffPlan = MatrixUtils.BooleanBlotch(
|
|
mountainElevation.Map(v => v >= 0),
|
|
param.TerrainSmoothing,
|
|
param.SmoothingThreshold,
|
|
param.MinimumMountainThickness,
|
|
/*bias=*/false);
|
|
var unmaskedCliffs = MatrixUtils.BordersToPoints(cliffPlan);
|
|
var maskedCliffs = MatrixUtils.MaskPathPoints(unmaskedCliffs, cliffMask);
|
|
var cliffs = CellLayerUtils.FromMatrixPoints(maskedCliffs, map.Tiles)
|
|
.Where(cliff => cliff.Length >= param.MinimumCliffLength).ToArray();
|
|
if (cliffs.Length == 0)
|
|
break;
|
|
foreach (var cliff in cliffs)
|
|
{
|
|
var isLoop = cliff[0] == cliff[^1];
|
|
TilingPath cliffPath;
|
|
if (isLoop)
|
|
cliffPath = new TilingPath(
|
|
map,
|
|
cliff,
|
|
(param.MinimumMountainThickness - 1) / 2,
|
|
param.CliffSegmentTypes[0],
|
|
param.CliffSegmentTypes[0],
|
|
loopedCliffPermittedTemplates);
|
|
else
|
|
cliffPath = new TilingPath(
|
|
map,
|
|
cliff,
|
|
(param.MinimumMountainThickness - 1) / 2,
|
|
param.ClearSegmentTypes[0],
|
|
param.ClearSegmentTypes[0],
|
|
nonLoopedCliffPermittedTemplates);
|
|
cliffPath
|
|
.ExtendEdge(4)
|
|
.OptimizeLoop();
|
|
if (cliffPath.Tile(cliffTilingRandom) == null)
|
|
throw new MapGenerationException("Could not fit tiles for cliffs");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (param.Forests > 0.0f)
|
|
{
|
|
var forestNoise = new CellLayer<float>(map);
|
|
NoiseUtils.SymmetricFractalNoiseIntoCellLayer(
|
|
forestRandom,
|
|
forestNoise,
|
|
param.Rotations,
|
|
param.Mirror,
|
|
param.ForestFeatureSize,
|
|
wavelength => MathF.Pow(wavelength, param.ForestClumpiness));
|
|
CellLayerUtils.CalibrateQuantileInPlace(
|
|
forestNoise,
|
|
0.0f,
|
|
1.0f - param.Forests);
|
|
|
|
var forestPlan = new CellLayer<bool>(map);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
if (param.ClearTerrain.Contains(map.GetTerrainIndex(mpos)) && forestNoise[mpos] >= 0.0f)
|
|
forestPlan[mpos] = true;
|
|
|
|
if (param.ForestCutout > 0)
|
|
{
|
|
var space = new CellLayer<bool>(map);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
space[mpos] = param.ClearTerrain.Contains(map.GetTerrainIndex(mpos));
|
|
|
|
// Improve symmetry.
|
|
{
|
|
var newSpace = new CellLayer<bool>(map);
|
|
Symmetry.RotateAndMirrorOverCPos(
|
|
space,
|
|
param.Rotations,
|
|
param.Mirror,
|
|
(sources, destination)
|
|
=> newSpace[destination] =
|
|
sources.All(source => !space.TryGetValue(source, out var value) || value));
|
|
space = newSpace;
|
|
}
|
|
|
|
if (param.MaximumCutoutSpacing > 0)
|
|
{
|
|
var roominess = new CellLayer<int>(map);
|
|
CellLayerUtils.ChebyshevRoom(roominess, space, false);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
roominess[mpos] = Math.Min(
|
|
param.MaximumCutoutSpacing,
|
|
roominess[mpos]);
|
|
|
|
while (true)
|
|
{
|
|
var (chosenMPos, room) = CellLayerUtils.FindRandomBest(
|
|
roominess,
|
|
topologyRandom,
|
|
(a, b) => a.CompareTo(b));
|
|
if (room < param.MaximumCutoutSpacing)
|
|
break;
|
|
|
|
var projections = Symmetry.RotateAndMirrorCPos(
|
|
chosenMPos.ToCPos(map),
|
|
space,
|
|
param.Rotations,
|
|
param.Mirror);
|
|
foreach (var projection in projections)
|
|
{
|
|
if (space.Contains(projection))
|
|
space[projection] = false;
|
|
var minX = projection.X - 2 * param.MaximumCutoutSpacing + 1;
|
|
var minY = projection.Y - 2 * param.MaximumCutoutSpacing + 1;
|
|
var maxX = projection.X + 2 * param.MaximumCutoutSpacing - 1;
|
|
var maxY = projection.Y + 2 * param.MaximumCutoutSpacing - 1;
|
|
for (var y = minY; y <= maxY; y++)
|
|
for (var x = minX; x <= maxX; x++)
|
|
{
|
|
var mpos = new CPos(x, y).ToMPos(map);
|
|
if (roominess.Contains(mpos))
|
|
roominess[mpos] = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var matrixSpace = CellLayerUtils.ToMatrix(space, false);
|
|
|
|
// deflated is grid points, not squares. Has a size of `size + 1`.
|
|
var deflated = MatrixUtils.DeflateSpace(matrixSpace, false);
|
|
var kernel = new Matrix<bool>(2 * param.ForestCutout, 2 * param.ForestCutout).Fill(true);
|
|
var inflated = MatrixUtils.KernelDilateOrErode(deflated.Map(v => v != 0), kernel, new int2(param.ForestCutout - 1, param.ForestCutout - 1), true);
|
|
var cutout = new CellLayer<bool>(map);
|
|
CellLayerUtils.FromMatrix(cutout, inflated, true);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
if (cutout[mpos])
|
|
forestPlan[mpos] = false;
|
|
}
|
|
|
|
var replaceable = IdentifyReplaceableTiles(map, replaceabilityMap, null);
|
|
var forestReplace = new CellLayer<MultiBrush.Replaceability>(map);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
if (forestPlan[mpos])
|
|
forestReplace[mpos] = replaceable[mpos];
|
|
else
|
|
forestReplace[mpos] = MultiBrush.Replaceability.None;
|
|
MultiBrush.PaintArea(map, actorPlans, forestReplace, param.ForestObstacles, forestTilingRandom);
|
|
}
|
|
|
|
if (param.EnforceSymmetry != 0)
|
|
{
|
|
// This is not commutative.
|
|
bool CheckCompatibility(byte main, byte other)
|
|
{
|
|
if (main == other)
|
|
return true;
|
|
else if (param.DominantTerrain.Contains(main))
|
|
return true;
|
|
else if (param.DominantTerrain.Contains(other))
|
|
return false;
|
|
else
|
|
return param.EnforceSymmetry < 2;
|
|
}
|
|
|
|
var replace = new CellLayer<MultiBrush.Replaceability>(map);
|
|
Symmetry.RotateAndMirrorOverCPos(
|
|
replace,
|
|
param.Rotations,
|
|
param.Mirror,
|
|
(CPos[] sources, CPos destination) =>
|
|
{
|
|
var main = tileset.GetTerrainIndex(map.Tiles[destination]);
|
|
var compatible = sources
|
|
.Where(replace.Contains)
|
|
.Select(source => tileset.GetTerrainIndex(map.Tiles[source]))
|
|
.All(source => CheckCompatibility(main, source));
|
|
replace[destination] = compatible ? MultiBrush.Replaceability.None : MultiBrush.Replaceability.Actor;
|
|
});
|
|
MultiBrush.PaintArea(map, actorPlans, replace, param.ForestObstacles, symmetryTilingRandom);
|
|
}
|
|
|
|
var playableArea = new CellLayer<bool>(map);
|
|
{
|
|
var (regions, regionMask, playability) = PlayableSpace.FindPlayableRegions(map, actorPlans, playabilityMap);
|
|
PlayableSpace.Region largest = null;
|
|
var disqualifications = new HashSet<int>();
|
|
|
|
// For circle-in-mountains, the outside is unplayable and should never count as
|
|
// the largest/preferred region.
|
|
if (param.ExternalCircularBias > 0)
|
|
{
|
|
if (map.Grid.Type != MapGridType.Rectangular)
|
|
throw new NotImplementedException();
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: regionMask,
|
|
wCenter: wMapCenter,
|
|
wRadius: (minSpan - 2) * 512,
|
|
outside: true,
|
|
action: (mpos, _, _, _) =>
|
|
{
|
|
if (regionMask[mpos] != PlayableSpace.NullRegion)
|
|
disqualifications.Add(regionMask[mpos]);
|
|
});
|
|
}
|
|
|
|
// Disqualify regions that violate any symmetry requirements.
|
|
{
|
|
var symmetryScore = new int[regions.Length];
|
|
void TestSymmetry(CPos[] sources, CPos destination)
|
|
{
|
|
var id = regionMask[destination];
|
|
if (playability[destination] != PlayableSpace.Playability.Playable)
|
|
return;
|
|
if (sources.All(source => regionMask.TryGetValue(source, out var sourceId) && sourceId == id))
|
|
symmetryScore[id]++;
|
|
}
|
|
|
|
Symmetry.RotateAndMirrorOverCPos(
|
|
regionMask,
|
|
param.Rotations,
|
|
param.Mirror,
|
|
TestSymmetry);
|
|
|
|
for (var id = 0; id < symmetryScore.Length; id++)
|
|
if (symmetryScore[id] < regions[id].PlayableArea / 2)
|
|
disqualifications.Add(id);
|
|
}
|
|
|
|
foreach (var region in regions)
|
|
{
|
|
if (disqualifications.Contains(region.Id))
|
|
continue;
|
|
if (largest == null || region.PlayableArea > largest.PlayableArea)
|
|
largest = region;
|
|
}
|
|
|
|
if (largest == null)
|
|
throw new MapGenerationException("could not find a playable region");
|
|
|
|
var minimumPlayableSpace = (int)(param.TotalPlayers * Math.PI * param.SpawnBuildSize * param.SpawnBuildSize);
|
|
if (largest.PlayableArea < minimumPlayableSpace)
|
|
throw new MapGenerationException("playable space is too small");
|
|
|
|
if (param.DenyWalledAreas)
|
|
{
|
|
// Beach tiles are particularly problematic. If they're for unplayable bodies
|
|
// of water, they should be obliterated. If they're just surrounded by rocks,
|
|
// trees, etc, they should be filled in with actors.
|
|
{
|
|
var unplayableWater = new HashSet<CPos>();
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
if (map.Contains(mpos) &&
|
|
map.Tiles[mpos].Type == param.WaterTile &&
|
|
regionMask[mpos] != largest.Id)
|
|
{
|
|
var cpos = mpos.ToCPos(gridType);
|
|
var projections = Symmetry.RotateAndMirrorCPos(
|
|
cpos, map.Tiles, param.Rotations, param.Mirror);
|
|
foreach (var projection in projections)
|
|
if (map.Tiles.Contains(projection) && map.Tiles[projection].Type == param.WaterTile)
|
|
unplayableWater.Add(projection);
|
|
}
|
|
|
|
bool? ClearWaterBody(CPos cpos, bool _)
|
|
{
|
|
var mpos = cpos.ToMPos(gridType);
|
|
var propagate =
|
|
map.Tiles[mpos].Type == param.WaterTile ||
|
|
beachTiles.Contains(map.Tiles[mpos]);
|
|
map.Tiles[mpos] = PickTile(param.LandTile);
|
|
regionMask[mpos] = PlayableSpace.NullRegion;
|
|
return propagate ? false : null;
|
|
}
|
|
|
|
CellLayerUtils.FloodFill(
|
|
map.Tiles,
|
|
unplayableWater.Select(cpos => (cpos, false)),
|
|
ClearWaterBody,
|
|
Direction.Spread4CVec);
|
|
}
|
|
|
|
var replaceable = IdentifyReplaceableTiles(map, replaceabilityMap, actorPlans);
|
|
var replace = new CellLayer<MultiBrush.Replaceability>(map);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
if (regionMask[mpos] == largest.Id || !map.Contains(mpos))
|
|
replace[mpos] = MultiBrush.Replaceability.None;
|
|
else
|
|
replace[mpos] = replaceable[mpos];
|
|
|
|
MultiBrush.PaintArea(map, actorPlans, replace, param.UnplayableObstacles, debrisTilingRandom);
|
|
}
|
|
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
playableArea[mpos] = playability[mpos] == PlayableSpace.Playability.Playable && regionMask[mpos] == largest.Id;
|
|
}
|
|
|
|
if (param.Roads)
|
|
{
|
|
var space = new CellLayer<bool>(map);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
space[mpos] = param.ClearTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[mpos]));
|
|
|
|
foreach (var actorPlan in actorPlans)
|
|
foreach (var (cpos, _) in actorPlan.Footprint())
|
|
if (space.Contains(cpos))
|
|
space[cpos] = false;
|
|
|
|
// Improve symmetry.
|
|
{
|
|
var newSpace = new CellLayer<bool>(map);
|
|
Symmetry.RotateAndMirrorOverCPos(
|
|
space,
|
|
param.Rotations,
|
|
param.Mirror,
|
|
(sources, destination)
|
|
=> newSpace[destination] =
|
|
sources.All(source => !space.TryGetValue(source, out var value) || value));
|
|
space = newSpace;
|
|
}
|
|
|
|
// TODO: Move to configuration
|
|
const int RoadStraightenShrink = 4;
|
|
const int RoadStraightenGrow = 2;
|
|
const int RoadMinimumShrinkLength = 12;
|
|
const int RoadInertialRange = 8;
|
|
var roadTotalShrink = RoadStraightenShrink + param.RoadShrink;
|
|
|
|
var matrixSpace = CellLayerUtils.ToMatrix(space, true);
|
|
var kernel = new Matrix<bool>(param.RoadSpacing * 2 + 1, param.RoadSpacing * 2 + 1);
|
|
MatrixUtils.OverCircle(
|
|
matrix: kernel,
|
|
center: new float2(param.RoadSpacing, param.RoadSpacing),
|
|
radius: param.RoadSpacing,
|
|
outside: false,
|
|
action: (xy, _) => kernel[xy] = true);
|
|
var dilated = MatrixUtils.KernelDilateOrErode(
|
|
matrixSpace,
|
|
kernel,
|
|
new int2(param.RoadSpacing, param.RoadSpacing),
|
|
false);
|
|
var deflated = MatrixUtils.DeflateSpace(dilated, true);
|
|
var matrixPointArrays = MatrixUtils.DirectionMapToPathsWithPruning(
|
|
input: deflated,
|
|
minimumLength: 20 + 2 * param.RoadShrink,
|
|
minimumJunctionSeparation: 6,
|
|
preserveEdgePaths: true);
|
|
var pointArrays = CellLayerUtils.FromMatrixPoints(matrixPointArrays, space);
|
|
pointArrays = TilingPath.RetainDisjointPaths(pointArrays);
|
|
|
|
var nonLoopedRoadPermittedTemplates =
|
|
TilingPath.PermittedSegments.FromInnerAndTerminalTypes(
|
|
tileset, param.RoadSegmentTypes, param.ClearSegmentTypes);
|
|
var loopedRoadPermittedTemplates =
|
|
TilingPath.PermittedSegments.FromType(
|
|
tileset, param.RoadSegmentTypes);
|
|
|
|
foreach (var pointArray in pointArrays)
|
|
{
|
|
var isLoop = pointArray[0] == pointArray[^1];
|
|
TilingPath path;
|
|
if (isLoop)
|
|
path = new TilingPath(
|
|
map,
|
|
pointArray,
|
|
param.RoadSpacing - 1,
|
|
param.RoadSegmentTypes[0],
|
|
param.RoadSegmentTypes[0],
|
|
loopedRoadPermittedTemplates);
|
|
else
|
|
path = new TilingPath(
|
|
map,
|
|
pointArray,
|
|
param.RoadSpacing - 1,
|
|
param.ClearSegmentTypes[0],
|
|
param.ClearSegmentTypes[0],
|
|
nonLoopedRoadPermittedTemplates);
|
|
|
|
path
|
|
.ChirallyNormalize(cvec => CellLayerUtils.CornerToWPos(cvec, gridType) - wMapCenter)
|
|
.ExtendEdge(2 * roadTotalShrink + RoadMinimumShrinkLength)
|
|
.Shrink(roadTotalShrink, RoadMinimumShrinkLength)
|
|
.InertiallyExtend(RoadStraightenGrow, RoadInertialRange)
|
|
.OptimizeLoop()
|
|
.RetainIfValid();
|
|
|
|
// Shrinking may have deleted the path.
|
|
if (path.Points == null)
|
|
continue;
|
|
|
|
// Roads that are _almost_ vertical or horizontal tile badly. Filter them out.
|
|
var minX = path.Points.Min((p) => p.X);
|
|
var minY = path.Points.Min((p) => p.Y);
|
|
var maxX = path.Points.Max((p) => p.X);
|
|
var maxY = path.Points.Max((p) => p.Y);
|
|
if (maxX - minX < 6 || maxY - minY < 6)
|
|
continue;
|
|
|
|
if (path.Tile(roadTilingRandom) == null)
|
|
throw new MapGenerationException("Could not fit tiles for roads");
|
|
}
|
|
}
|
|
|
|
if (param.CreateEntities)
|
|
{
|
|
var (buildingTypes, buildingWeights) = Parameters.SplitWeights(param.BuildingWeights);
|
|
var (resourceSpawnTypes, resourceSpawnWeights) = Parameters.SplitWeights(param.ResourceSpawnWeights);
|
|
|
|
var projectionSpacing = new CellLayer<int>(map);
|
|
Symmetry.RotateAndMirrorOverCPos(
|
|
projectionSpacing,
|
|
param.Rotations,
|
|
param.Mirror,
|
|
(projections, cpos) =>
|
|
projectionSpacing[cpos] = Symmetry.ProjectionProximity(projections) / 2);
|
|
|
|
var spawnReservationRadius = minSpan * param.CentralSpawnReservationFraction;
|
|
var spawnReservation = new CellLayer<bool>(map);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
spawnReservation[mpos] = Symmetry.IsCPosNearCenter(
|
|
mpos.ToCPos(gridType),
|
|
spawnReservation,
|
|
spawnReservationRadius,
|
|
param.Mirror);
|
|
|
|
var zoneable = new CellLayer<bool>(map);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
zoneable[mpos] = playableArea[mpos] && param.ClearTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[mpos]));
|
|
|
|
foreach (var actorPlan in actorPlans)
|
|
foreach (var cpos in actorPlan.Footprint().Keys)
|
|
if (map.AllCells.Contains(cpos))
|
|
zoneable[cpos] = false;
|
|
|
|
// Improve symmetry.
|
|
{
|
|
var newZoneable = new CellLayer<bool>(map);
|
|
Symmetry.RotateAndMirrorOverCPos(
|
|
zoneable,
|
|
param.Rotations,
|
|
param.Mirror,
|
|
(sources, destination)
|
|
=> newZoneable[destination] =
|
|
sources.All(source => zoneable.TryGetValue(source, out var value) && value));
|
|
zoneable = newZoneable;
|
|
}
|
|
|
|
if (param.Rotations > 1 || param.Mirror != 0)
|
|
{
|
|
// Reserve the center of the map - otherwise it will mess with rotations
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: zoneable,
|
|
wCenter: wMapCenter,
|
|
wRadius: 1024,
|
|
outside: false,
|
|
action: (mpos, _, _, _) => zoneable[mpos] = false);
|
|
}
|
|
|
|
var zoneableArea = zoneable.Count(v => v);
|
|
var entityMultiplier =
|
|
zoneableArea * param.AreaEntityBonus +
|
|
param.TotalPlayers * param.PlayerCountEntityBonus;
|
|
var perSymmetryEntityMultiplier = entityMultiplier / param.SymmetryCount;
|
|
|
|
// Spawn generation
|
|
for (var iteration = 0; iteration < param.Players; iteration++)
|
|
{
|
|
var spawnPreference = new CellLayer<int>(map);
|
|
CellLayerUtils.ChebyshevRoom(spawnPreference, zoneable, false);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
{
|
|
var preference = Math.Min(spawnPreference[mpos], projectionSpacing[mpos]);
|
|
if (spawnReservation[mpos] && preference > 1)
|
|
preference = 1;
|
|
else if (preference > param.SpawnRegionSize)
|
|
preference = param.SpawnRegionSize;
|
|
spawnPreference[mpos] = preference;
|
|
}
|
|
|
|
var (chosenMPos, chosenValue) = CellLayerUtils.FindRandomBest(
|
|
spawnPreference,
|
|
playerRandom,
|
|
(a, b) => a.CompareTo(b));
|
|
|
|
if (chosenValue < 1)
|
|
throw new MapGenerationException("Not enough room for player spawns");
|
|
|
|
var room = chosenValue - 1;
|
|
var spawn = new ActorPlan(map, "mpspawn")
|
|
{
|
|
Location = chosenMPos.ToCPos(gridType),
|
|
};
|
|
|
|
var preferedRange = (param.SpawnBuildSize + param.SpawnRegionSize * 2) / 2;
|
|
var resourceSpawnPreferences = new CellLayer<float>(map);
|
|
CellLayerUtils.WalkingDistances(
|
|
resourceSpawnPreferences,
|
|
zoneable,
|
|
new[] { chosenMPos.ToCPos(gridType) },
|
|
param.SpawnRegionSize);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
{
|
|
var v = resourceSpawnPreferences[mpos];
|
|
resourceSpawnPreferences[mpos] = MathF.Ceiling(
|
|
v > preferedRange ? 2 * preferedRange - v : v);
|
|
}
|
|
|
|
var resourceSpawns = new List<ActorPlan>();
|
|
for (var resourceSpawn = 0; resourceSpawn < param.SpawnResourceSpawns; resourceSpawn++)
|
|
{
|
|
var (mpos, value) = CellLayerUtils.FindRandomBest(
|
|
resourceSpawnPreferences,
|
|
playerRandom,
|
|
(a, b) => a.CompareTo(b));
|
|
if (value <= 1.0f)
|
|
break;
|
|
|
|
var resourceSpawnType = resourceSpawnTypes[playerRandom.PickWeighted(resourceSpawnWeights)];
|
|
var resourceSpawnPlan =
|
|
new ActorPlan(map, resourceSpawnType)
|
|
{
|
|
Location = mpos.ToCPos(gridType)
|
|
};
|
|
resourceSpawns.Add(resourceSpawnPlan);
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: resourceSpawnPreferences,
|
|
wCenter: resourceSpawnPlan.WPosLocation,
|
|
wRadius: 1024,
|
|
outside: false,
|
|
action: (mpos, _, _, _) => resourceSpawnPreferences[mpos] = 0.0f);
|
|
}
|
|
|
|
var projectedSpawns = Symmetry.RotateAndMirrorActorPlan(spawn, param.Rotations, param.Mirror);
|
|
actorPlans.AddRange(projectedSpawns);
|
|
foreach (var projectedSpawn in projectedSpawns)
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: zoneable,
|
|
wCenter: projectedSpawn.WPosLocation,
|
|
wRadius: param.SpawnReservation * 1024,
|
|
outside: false,
|
|
action: (mpos, _, _, _) => zoneable[mpos] = false);
|
|
|
|
var projectedResourceSpawns = Symmetry.RotateAndMirrorActorPlans(resourceSpawns, param.Rotations, param.Mirror);
|
|
actorPlans.AddRange(projectedResourceSpawns);
|
|
foreach (var projectedResourceSpawn in projectedResourceSpawns)
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: zoneable,
|
|
wCenter: projectedResourceSpawn.WPosLocation,
|
|
wRadius: param.SpawnReservation * 1024,
|
|
outside: false,
|
|
action: (mpos, _, _, _) => zoneable[mpos] = false);
|
|
}
|
|
|
|
// Expansions
|
|
{
|
|
var resourceSpawnsRemaining = (int)(param.MaximumExpansionResourceSpawns * perSymmetryEntityMultiplier);
|
|
while (resourceSpawnsRemaining > 0)
|
|
{
|
|
var roominess = new CellLayer<int>(map);
|
|
CellLayerUtils.ChebyshevRoom(roominess, zoneable, false);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
roominess[mpos] = Math.Min(
|
|
param.MaximumExpansionSize + param.ExpansionBorder,
|
|
Math.Min(roominess[mpos], projectionSpacing[mpos]));
|
|
var (chosenMPos, chosenValue) = CellLayerUtils.FindRandomBest(
|
|
roominess,
|
|
expansionRandom,
|
|
(a, b) => a.CompareTo(b));
|
|
var room = chosenValue - 1;
|
|
var radius2 = room - param.ExpansionBorder;
|
|
if (radius2 < param.MinimumExpansionSize)
|
|
break;
|
|
if (radius2 > param.MaximumExpansionSize)
|
|
radius2 = param.MaximumExpansionSize;
|
|
var radius1 = Math.Min(Math.Min(param.ExpansionInner, room), radius2);
|
|
var resourceSpawnCount = Math.Min(resourceSpawnsRemaining, expansionRandom.Next(param.MaximumResourceSpawnsPerExpansion) + 1);
|
|
resourceSpawnsRemaining -= resourceSpawnCount;
|
|
|
|
if (radius1 < 1.0f)
|
|
break;
|
|
|
|
var resourceSpawns = new List<ActorPlan>();
|
|
var resourceSpawnPreferences = new CellLayer<float>(map);
|
|
var wRadius1Sq = (long)radius1 * radius1 * 1024 * 1024;
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: resourceSpawnPreferences,
|
|
wCenter: CellLayerUtils.MPosToWPos(chosenMPos, gridType),
|
|
wRadius: radius2 * 1024,
|
|
outside: false,
|
|
action: (mpos, _, _, rSq) =>
|
|
resourceSpawnPreferences[mpos] =
|
|
rSq >= wRadius1Sq ? rSq : 0.0f);
|
|
for (var resourceSpawn = 0; resourceSpawn < resourceSpawnCount; resourceSpawn++)
|
|
{
|
|
var mpos = CellLayerUtils.PickWeighted(resourceSpawnPreferences, expansionRandom);
|
|
var resourceSpawnType = resourceSpawnTypes[playerRandom.PickWeighted(resourceSpawnWeights)];
|
|
var resourceSpawnPlan =
|
|
new ActorPlan(map, resourceSpawnType)
|
|
{
|
|
Location = mpos.ToCPos(gridType)
|
|
};
|
|
resourceSpawns.Add(resourceSpawnPlan);
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: resourceSpawnPreferences,
|
|
wCenter: resourceSpawnPlan.WPosLocation,
|
|
wRadius: 1024,
|
|
outside: false,
|
|
action: (mpos, _, _, _) => resourceSpawnPreferences[mpos] = 0.0f);
|
|
}
|
|
|
|
var projectedResourceSpawns = Symmetry.RotateAndMirrorActorPlans(resourceSpawns, param.Rotations, param.Mirror);
|
|
actorPlans.AddRange(projectedResourceSpawns);
|
|
foreach (var projectedResourceSpawn in projectedResourceSpawns)
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: zoneable,
|
|
wCenter: projectedResourceSpawn.WPosLocation,
|
|
wRadius: param.ResourceSpawnReservation * 1024,
|
|
outside: false,
|
|
action: (mpos, _, _, _) => zoneable[mpos] = false);
|
|
}
|
|
}
|
|
|
|
// Neutral buildings
|
|
{
|
|
var targetBuildingCount =
|
|
(param.MaximumBuildings != 0)
|
|
? expansionRandom.Next(
|
|
(int)(param.MinimumBuildings * perSymmetryEntityMultiplier),
|
|
(int)(param.MaximumBuildings * perSymmetryEntityMultiplier) + 1)
|
|
: 0;
|
|
for (var i = 0; i < targetBuildingCount; i++)
|
|
{
|
|
var roominess = new CellLayer<int>(map);
|
|
CellLayerUtils.ChebyshevRoom(roominess, zoneable, false);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
roominess[mpos] = Math.Min(
|
|
3,
|
|
Math.Min(roominess[mpos], projectionSpacing[mpos]));
|
|
var (chosenMPos, chosenValue) = CellLayerUtils.FindRandomBest(
|
|
roominess,
|
|
buildingRandom,
|
|
(a, b) => a.CompareTo(b));
|
|
if (chosenValue < 3)
|
|
break;
|
|
var chosenCPos = chosenMPos.ToCPos(gridType);
|
|
var typeChoice = buildingRandom.PickWeighted(buildingWeights);
|
|
var type = buildingTypes[typeChoice];
|
|
var actorPlan = new ActorPlan(map, type)
|
|
{
|
|
WPosCenterLocation = CellLayerUtils.CPosToWPos(chosenCPos, gridType),
|
|
};
|
|
|
|
var projectedBuildings = Symmetry.RotateAndMirrorActorPlan(actorPlan, param.Rotations, param.Mirror);
|
|
actorPlans.AddRange(projectedBuildings);
|
|
foreach (var projectedBuilding in projectedBuildings)
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: zoneable,
|
|
wCenter: projectedBuilding.WPosLocation,
|
|
wRadius: 2048,
|
|
outside: false,
|
|
action: (mpos, _, _, _) => zoneable[mpos] = false);
|
|
}
|
|
}
|
|
|
|
// Grow resources
|
|
{
|
|
var pattern = new CellLayer<float>(map);
|
|
NoiseUtils.SymmetricFractalNoiseIntoCellLayer(
|
|
resourceRandom,
|
|
pattern,
|
|
param.Rotations,
|
|
param.Mirror,
|
|
param.ResourceFeatureSize,
|
|
wavelength => MathF.Pow(wavelength, param.OreClumpiness));
|
|
{
|
|
CellLayerUtils.CalibrateQuantileInPlace(
|
|
pattern,
|
|
0.0f,
|
|
0.0f);
|
|
var max = pattern.Max();
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
{
|
|
pattern[mpos] /= max;
|
|
pattern[mpos] += param.OreUniformity;
|
|
}
|
|
}
|
|
|
|
var strengths = new Dictionary<ResourceTypeInfo, CellLayer<float>>();
|
|
foreach (var actorPlan in actorPlans)
|
|
{
|
|
var type = actorPlan.Reference.Type;
|
|
if (param.ResourceSpawnWeights.ContainsKey(type))
|
|
{
|
|
var resource = param.ResourceSpawnSeeds[type];
|
|
if (!strengths.TryGetValue(resource, out var strength))
|
|
{
|
|
strength = new CellLayer<float>(map);
|
|
strengths.Add(resource, strength);
|
|
}
|
|
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: strength,
|
|
wCenter: actorPlan.WPosLocation,
|
|
wRadius: 16 * 1024,
|
|
outside: false,
|
|
action: (mpos, _, _, rSq) =>
|
|
strength[mpos] +=
|
|
1024.0f / (1024.0f + MathF.Sqrt(rSq)));
|
|
}
|
|
}
|
|
|
|
var maxStrength = new CellLayer<float>(map);
|
|
var bestResource = new CellLayer<ResourceTypeInfo>(map);
|
|
bestResource.Clear(param.DefaultResource);
|
|
foreach (var resourceStrength in strengths)
|
|
{
|
|
var resource = resourceStrength.Key;
|
|
var strength = resourceStrength.Value;
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
if (strength[mpos] > maxStrength[mpos])
|
|
{
|
|
maxStrength[mpos] = strength[mpos];
|
|
bestResource[mpos] = resource;
|
|
}
|
|
}
|
|
|
|
// Closer to +inf means "more preferable" for plan.
|
|
var plan = new CellLayer<float>(map);
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
if (playableArea[mpos] && param.AllowedTerrainResourceCombos.Contains((bestResource[mpos], map.GetTerrainIndex(mpos))))
|
|
plan[mpos] = pattern[mpos] * maxStrength[mpos];
|
|
else
|
|
plan[mpos] = float.NegativeInfinity;
|
|
|
|
var wSpawnBuildSizeSq = (long)param.SpawnBuildSize * param.SpawnBuildSize * 1024 * 1024;
|
|
foreach (var actorPlan in actorPlans)
|
|
if (actorPlan.Reference.Type == "mpspawn")
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: plan,
|
|
wCenter: actorPlan.WPosLocation,
|
|
wRadius: param.SpawnRegionSize * 2 * 1024,
|
|
outside: false,
|
|
action: (mpos, _, _, rSq) =>
|
|
plan[mpos] *= 1.0f + param.SpawnResourceBias * wSpawnBuildSizeSq / rSq);
|
|
|
|
foreach (var actorPlan in actorPlans)
|
|
if (actorPlan.Reference.Type == "mpspawn")
|
|
CellLayerUtils.OverCircle(
|
|
cellLayer: plan,
|
|
wCenter: actorPlan.WPosLocation,
|
|
wRadius: param.SpawnBuildSize * 1024,
|
|
outside: false,
|
|
action: (mpos, _, _, _) => plan[mpos] = float.NegativeInfinity);
|
|
|
|
foreach (var actorPlan in actorPlans)
|
|
foreach (var (cpos, _) in actorPlan.Footprint())
|
|
if (plan.Contains(cpos))
|
|
plan[cpos] = float.NegativeInfinity;
|
|
|
|
// Improve symmetry.
|
|
{
|
|
var newPlan = new CellLayer<float>(map);
|
|
Symmetry.RotateAndMirrorOverCPos(
|
|
plan,
|
|
param.Rotations,
|
|
param.Mirror,
|
|
(sources, destination)
|
|
=> newPlan[destination] =
|
|
sources.Min(source => plan.TryGetValue(source, out var value) ? value : float.NegativeInfinity));
|
|
plan = newPlan;
|
|
}
|
|
|
|
var remaining = param.ResourcesPerPlayer * entityMultiplier;
|
|
|
|
// Closer to -inf means "more preferable" for priorities.
|
|
var priorities = new PriorityArray<float>(
|
|
plan.Size.Width * plan.Size.Height,
|
|
float.PositiveInfinity);
|
|
{
|
|
var i = 0;
|
|
foreach (var v in plan)
|
|
priorities[i++] = -v;
|
|
}
|
|
|
|
int PriorityIndex(MPos mpos) => mpos.V * plan.Size.Width + mpos.U;
|
|
MPos PriorityMPos(int index)
|
|
{
|
|
var v = Math.DivRem(index, plan.Size.Width, out var u);
|
|
return new MPos(u, v);
|
|
}
|
|
|
|
map.Resources.Clear();
|
|
|
|
// Return resource value of a given square.
|
|
// See https://github.com/OpenRA/OpenRA/blob/9302bac6199fbc925a85fd7a08fc2ba4b9317d16/OpenRA.Mods.Common/Traits/World/ResourceLayer.cs#L144-L166
|
|
// https://github.com/OpenRA/OpenRA/blob/9302bac6199fbc925a85fd7a08fc2ba4b9317d16/OpenRA.Mods.Common/Traits/World/EditorResourceLayer.cs#L175-L183
|
|
int CheckValue(CPos cpos)
|
|
{
|
|
if (!map.Resources.Contains(cpos))
|
|
return 0;
|
|
var resource = map.Resources[cpos].Type;
|
|
if (resource == 0)
|
|
return 0;
|
|
var adjacent = 0;
|
|
for (var y = -1; y <= 1; y++)
|
|
for (var x = -1; x <= 1; x++)
|
|
{
|
|
var offsetCpos = cpos + new CVec(x, y);
|
|
if (!map.Resources.Contains(offsetCpos))
|
|
continue;
|
|
if (map.Resources[offsetCpos].Type == resource)
|
|
adjacent++;
|
|
}
|
|
|
|
var resourceType = bestResource[cpos];
|
|
var density = Math.Max(resourceType.MaxDensity * adjacent / /*maxAdjacent=*/9, 1);
|
|
|
|
// density + 1 to mirror a bug that got ossified due to balancing.
|
|
return param.ResourceValues[resourceType] * (density + 1);
|
|
}
|
|
|
|
int CheckValue3By3(CPos cpos)
|
|
{
|
|
var total = 0;
|
|
for (var y = -1; y <= 1; y++)
|
|
for (var x = -1; x <= 1; x++)
|
|
total += CheckValue(cpos + new CVec(x, y));
|
|
|
|
return total;
|
|
}
|
|
|
|
// Set and return change in overall value.
|
|
int AddResource(CPos cpos)
|
|
{
|
|
var mpos = cpos.ToMPos(gridType);
|
|
priorities[PriorityIndex(mpos)] = float.PositiveInfinity;
|
|
|
|
// Generally shouldn't happen, but perhaps a rotation/mirror related inaccuracy.
|
|
if (map.Resources[mpos].Type != 0)
|
|
return 0;
|
|
|
|
var resourceType = bestResource[mpos];
|
|
var oldValue = CheckValue3By3(cpos);
|
|
map.Resources[mpos] = new ResourceTile(
|
|
resourceType.ResourceIndex,
|
|
(byte)resourceType.MaxDensity);
|
|
var newValue = CheckValue3By3(cpos);
|
|
return newValue - oldValue;
|
|
}
|
|
|
|
while (remaining > 0)
|
|
{
|
|
var n = priorities.GetMinIndex();
|
|
if (priorities[n] == float.PositiveInfinity)
|
|
break;
|
|
|
|
var chosenMPos = PriorityMPos(n);
|
|
var chosenCPos = chosenMPos.ToCPos(gridType);
|
|
foreach (var cpos in Symmetry.RotateAndMirrorCPos(chosenCPos, plan, param.Rotations, param.Mirror))
|
|
if (map.Resources.Contains(cpos))
|
|
remaining -= AddResource(cpos);
|
|
}
|
|
}
|
|
}
|
|
|
|
map.PlayerDefinitions = new MapPlayers(map.Rules, 0).ToMiniYaml();
|
|
map.ActorDefinitions = actorPlans
|
|
.Select((plan, i) => new MiniYamlNode($"Actor{i}", plan.Reference.Save()))
|
|
.ToImmutableArray();
|
|
}
|
|
|
|
static CellLayer<MultiBrush.Replaceability> IdentifyReplaceableTiles(
|
|
Map map,
|
|
Dictionary<TerrainTile, MultiBrush.Replaceability> replaceabilityMap,
|
|
IEnumerable<ActorPlan> actorPlans)
|
|
{
|
|
var output = new CellLayer<MultiBrush.Replaceability>(map);
|
|
|
|
foreach (var mpos in map.AllCells.MapCoords)
|
|
{
|
|
var tile = map.Tiles[mpos];
|
|
var replaceability = MultiBrush.Replaceability.Any;
|
|
if (replaceabilityMap.TryGetValue(tile, out var value))
|
|
replaceability = value;
|
|
output[mpos] = replaceability;
|
|
}
|
|
|
|
if (actorPlans != null)
|
|
foreach (var actorPlan in actorPlans)
|
|
foreach (var cpos in actorPlan.Footprint().Keys)
|
|
if (map.AllCells.Contains(cpos))
|
|
output[cpos] = MultiBrush.Replaceability.None;
|
|
|
|
return output;
|
|
}
|
|
}
|
|
}
|