From 1fda679b9f9a57ae61370fba23a1865453e253e3 Mon Sep 17 00:00:00 2001 From: Ashley Newson Date: Mon, 27 Jan 2025 21:46:19 +0000 Subject: [PATCH] Refine map generator spawn placement rules Spawn generation generally tries to place spawns: - In spacious areas - Away from the center (true center or mirror lines) - Away from a symmetry-projected spawn - Away from previously placed spawns This commit introduces the following adjustments: - The spacing between sequentially placed spawns is relaxed. - Fix spawn mines contributing too much to the space reservation. - Factor rotations into anti-center biasing calculation. - Preserve spacing information in center space fallback decisions. - Use a linear (instead of binary) falloff for anti-center biasing. - Enforce that spawns have a minimum buildable area around them. - Enforce that symmetry-projected spawns have as much separation as sequentially placed spawns would. - Allow spawns on or near roads. --- OpenRA.Mods.Common/MapGenerator/Symmetry.cs | 21 -------- .../Traits/World/RaMapGenerator.cs | 50 ++++++++++++------- mods/cnc/rules/map-generators.yaml | 4 +- mods/ra/rules/map-generators.yaml | 6 ++- 4 files changed, 39 insertions(+), 42 deletions(-) diff --git a/OpenRA.Mods.Common/MapGenerator/Symmetry.cs b/OpenRA.Mods.Common/MapGenerator/Symmetry.cs index 50e0cacdc2..b69ecc82af 100644 --- a/OpenRA.Mods.Common/MapGenerator/Symmetry.cs +++ b/OpenRA.Mods.Common/MapGenerator/Symmetry.cs @@ -278,26 +278,5 @@ namespace OpenRA.Mods.Common.MapGenerator action(projections, original); } } - - /// - /// Returns true iff xy is within reservationRadius of the center of a given CellLayer. If - /// a mirroring is specified, the radius is measured from the mirror line instead of the - /// center point. - /// - public static bool IsCPosNearCenter( - CPos cpos, - CellLayer cellLayer, - int reservationRadius, - Mirror mirror) - { - CPos[] testPoints; - if (mirror == Mirror.None) - testPoints = RotateAndMirrorCPos(cpos, cellLayer, 2, Mirror.None); - else - testPoints = RotateAndMirrorCPos(cpos, cellLayer, 1, mirror); - - var separation = (testPoints[1] - testPoints[0]).LengthSquared; - return separation <= reservationRadius * reservationRadius * 4; - } } } diff --git a/OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs b/OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs index d7de7b5df3..a6f0f7f96d 100644 --- a/OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs +++ b/OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs @@ -139,6 +139,8 @@ namespace OpenRA.Mods.Common.Traits [FieldLoader.Require] public readonly int SpawnBuildSize = default; [FieldLoader.Require] + public readonly int MinimumSpawnRadius = default; + [FieldLoader.Require] public readonly int SpawnResourceSpawns = default; [FieldLoader.Require] public readonly int SpawnReservation = default; @@ -204,6 +206,8 @@ namespace OpenRA.Mods.Common.Traits [FieldLoader.Ignore] public readonly IReadOnlySet DominantTerrain; [FieldLoader.Ignore] + public readonly IReadOnlySet ZoneableTerrain; + [FieldLoader.Ignore] public readonly IReadOnlySet PartiallyPlayableCategories; [FieldLoader.Ignore] public readonly IReadOnlyList ClearSegmentTypes; @@ -290,6 +294,7 @@ namespace OpenRA.Mods.Common.Traits PartiallyPlayableTerrain = ParseTerrainIndexes("PartiallyPlayableTerrain"); UnplayableTerrain = ParseTerrainIndexes("UnplayableTerrain"); DominantTerrain = ParseTerrainIndexes("DominantTerrain"); + ZoneableTerrain = ParseTerrainIndexes("ZoneableTerrain"); PartiallyPlayableCategories = my.NodeWithKey("PartiallyPlayableCategories").Value.Value .Split(',', StringSplitOptions.RemoveEmptyEntries) @@ -394,6 +399,8 @@ namespace OpenRA.Mods.Common.Traits throw new MapGenerationException("SpawnReservation must be >= 1"); if (SpawnBuildSize < 1) throw new MapGenerationException("SpawnBuildSize must be >= 1"); + if (MinimumSpawnRadius < 1) + throw new MapGenerationException("MinimumSpawnRadius must be >= 1"); if (SpawnResourceSpawns < 0) throw new MapGenerationException("SpawnResourceSpawns must be >= 0"); if (ResourceSpawnReservation < 1) @@ -1167,18 +1174,23 @@ namespace OpenRA.Mods.Common.Traits (projections, cpos) => projectionSpacing[cpos] = Symmetry.ProjectionProximity(projections) / 2); - var spawnReservationRadius = minSpan * param.CentralSpawnReservationFraction / FractionMax; - var spawnReservation = new CellLayer(map); + // Spawn bias tries to move spawns away from the map center and their symmetry + // projections. + var spawnBiasRadius = Math.Max(1, minSpan * param.CentralSpawnReservationFraction / FractionMax); + var spawnBias = new CellLayer(map); + spawnBias.Clear(spawnBiasRadius); + CellLayerUtils.OverCircle( + cellLayer: spawnBias, + wCenter: wMapCenter, + wRadius: 1024 * spawnBiasRadius, + outside: false, + action: (mpos, _, _, wrSq) => spawnBias[mpos] = (int)Exts.ISqrt(wrSq) / 1024); foreach (var mpos in map.AllCells.MapCoords) - spawnReservation[mpos] = Symmetry.IsCPosNearCenter( - mpos.ToCPos(gridType), - spawnReservation, - spawnReservationRadius, - param.Mirror); + spawnBias[mpos] = Math.Min(spawnBias[mpos], projectionSpacing[mpos]); var zoneable = new CellLayer(map); foreach (var mpos in map.AllCells.MapCoords) - zoneable[mpos] = playableArea[mpos] && param.ClearTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[mpos])); + zoneable[mpos] = playableArea[mpos] && param.ZoneableTerrain.Contains(tileset.GetTerrainIndex(map.Tiles[mpos])); foreach (var actorPlan in actorPlans) foreach (var cpos in actorPlan.Footprint().Keys) @@ -1198,7 +1210,7 @@ namespace OpenRA.Mods.Common.Traits zoneable = newZoneable; } - if (param.Rotations > 1 || param.Mirror != 0) + if (param.Rotations > 1 || param.Mirror != Symmetry.Mirror.None) { // Reserve the center of the map - otherwise it will mess with rotations CellLayerUtils.OverCircle( @@ -1221,14 +1233,15 @@ namespace OpenRA.Mods.Common.Traits var spawnPreference = new CellLayer(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; - } + if (spawnPreference[mpos] >= param.MinimumSpawnRadius && + projectionSpacing[mpos] * 2 >= param.SpawnReservation + param.MinimumSpawnRadius) + { + spawnPreference[mpos] = spawnBias[mpos] * Math.Min(param.SpawnRegionSize, spawnPreference[mpos]); + } + else + { + spawnPreference[mpos] = 0; + } var (chosenMPos, chosenValue) = CellLayerUtils.FindRandomBest( spawnPreference, @@ -1238,7 +1251,6 @@ namespace OpenRA.Mods.Common.Traits 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), @@ -1299,7 +1311,7 @@ namespace OpenRA.Mods.Common.Traits CellLayerUtils.OverCircle( cellLayer: zoneable, wCenter: projectedResourceSpawn.WPosLocation, - wRadius: param.SpawnReservation * 1024, + wRadius: param.ResourceSpawnReservation * 1024, outside: false, action: (mpos, _, _, _) => zoneable[mpos] = false); } diff --git a/mods/cnc/rules/map-generators.yaml b/mods/cnc/rules/map-generators.yaml index ac9403eceb..cd29a258e3 100644 --- a/mods/cnc/rules/map-generators.yaml +++ b/mods/cnc/rules/map-generators.yaml @@ -34,8 +34,9 @@ ResourceSpawnReservation: 8 SpawnRegionSize: 12 SpawnBuildSize: 8 + MinimumSpawnRadius: 5 SpawnResourceSpawns: 3 - SpawnReservation: 20 + SpawnReservation: 16 SpawnResourceBias: 1050 ResourcesPerPlayer: 50000 OreUniformity: 500 @@ -56,6 +57,7 @@ PartiallyPlayableTerrain: River,Tree,Water UnplayableTerrain: Rock DominantTerrain: River,Rock,Tree,Water + ZoneableTerrain: Clear,Road PartiallyPlayableCategories: Beach,Road ClearSegmentTypes: Clear BeachSegmentTypes: Beach diff --git a/mods/ra/rules/map-generators.yaml b/mods/ra/rules/map-generators.yaml index 1b0b16d4d9..49e2952493 100644 --- a/mods/ra/rules/map-generators.yaml +++ b/mods/ra/rules/map-generators.yaml @@ -34,8 +34,9 @@ ResourceSpawnReservation: 8 SpawnRegionSize: 12 SpawnBuildSize: 8 + MinimumSpawnRadius: 5 SpawnResourceSpawns: 3 - SpawnReservation: 20 + SpawnReservation: 16 SpawnResourceBias: 1150 ResourcesPerPlayer: 50000 OreUniformity: 250 @@ -55,6 +56,7 @@ PartiallyPlayableTerrain: Tree UnplayableTerrain: River,Rock DominantTerrain: River,Rock,Tree,Water + ZoneableTerrain: Clear,Road PartiallyPlayableCategories: Beach,Road ClearSegmentTypes: Clear BeachSegmentTypes: Beach @@ -159,6 +161,7 @@ Settings: Water: 800 Forests: 0 + MinimumSpawnRadius: 4 Choice@LargeIslands: Label: label-ra-map-generator-choice-terrain-type-large-islands Settings: @@ -181,6 +184,7 @@ TerrainFeatureSize: 5120 Forests: 0 SpawnBuildSize: 6 + MinimumSpawnRadius: 4 Option@Rotations: Label: label-ra-map-generator-option-rotations SimpleChoice: Rotations