Improved Sector Map

Added Boss Icon
Added Home Icon
Added Legend
Placing bosses at chokepoints
Adjusted home zone placement
Added per-faction areas of influence
This commit is contained in:
Reinier Vens 2021-03-12 05:12:01 +00:00
commit 7f3f198f61
29 changed files with 2936 additions and 308 deletions

View file

@ -41,27 +41,36 @@ public class MegaCorporation : DatabaseEntry, INamedEntry
[InspectableField, JsonProperty("name"), Key(1)]
public string Name;
[InspectableText, JsonProperty("description"), Key(2)]
[InspectableField, JsonProperty("shortName"), Key(2)]
public string ShortName;
[InspectableText, JsonProperty("description"), Key(3)]
public string Description;
[InspectableTexture, JsonProperty("logo"), Key(3)]
[InspectableTexture, JsonProperty("logo"), Key(4)]
public string Logo;
[InspectableDatabaseLink(typeof(PersonalityAttribute)), JsonProperty("personality"), Key(4)]
[InspectableDatabaseLink(typeof(PersonalityAttribute)), JsonProperty("personality"), Key(5)]
public Dictionary<Guid, float> Personality = new Dictionary<Guid, float>();
[InspectableField, JsonProperty("hostile"), Key(5)]
[InspectableField, JsonProperty("hostile"), Key(6)]
public bool PlayerHostile;
[InspectableColor, JsonProperty("primaryColor"), Key(6)]
[InspectableColor, JsonProperty("primaryColor"), Key(7)]
public float3 PrimaryColor;
[InspectableColor, JsonProperty("secondaryColor"), Key(7)]
[InspectableColor, JsonProperty("secondaryColor"), Key(8)]
public float3 SecondaryColor;
[InspectableTextAsset, JsonProperty("geonames"), Key(8)]
[InspectableTextAsset, JsonProperty("geonames"), Key(9)]
public string GeonameFile;
[InspectableDatabaseLink(typeof(HullData)), JsonProperty("bossHull"), Key(10)]
public Guid BossHull;
[InspectableField, JsonProperty("influence"), Key(11)]
public int InfluenceDistance = 4;
[IgnoreMember] public MarkovNameGenerator NameGenerator { get; set; }
[IgnoreMember] public string EntryName

View file

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using static Unity.Mathematics.math;
using Random = Unity.Mathematics.Random;
//Generates random names based on the statistical weight of letter sequences
@ -18,20 +19,40 @@ public class MarkovNameGenerator
private int _minLength;
private int _maxLength;
private Random _random;
public MarkovNameGenerator(ref Random random, string nameData, SectorGenerationSettings settings)
{
var names = new HashSet<string>();
var lines = nameData.Split('\n');
foreach (var line in lines)
{
foreach(var word in line.ToUpperInvariant().Split(' ', ',', '.', '"'))
if (word.Length >= settings.NameGeneratorMinLength && !names.Contains(word))
names.Add(word);
}
_random = random;
_order = max(1, settings.NameGeneratorOrder);
_minLength = max(1, settings.NameGeneratorMinLength);
_maxLength = settings.NameGeneratorMaxLength;
Initialize(names);
}
public MarkovNameGenerator(ref Random random, IEnumerable<string> sampleNames, int order, int minLength, int maxLength)
{
_random = random;
//fix parameter values
if (order < 1)
order = 1;
if (minLength < 1)
minLength = 1;
_order = order;
_minLength = minLength;
_order = max(1, order);
_minLength = max(1, minLength);
_maxLength = maxLength;
Initialize(sampleNames);
}
private void Initialize(IEnumerable<string> sampleNames)
{
//split comma delimited lines
foreach (string line in sampleNames)
{
@ -39,7 +60,7 @@ public class MarkovNameGenerator
foreach (string word in tokens)
{
string upper = word.Trim().ToLowerInvariant();
if (upper.Length < order + 1)
if (upper.Length < _order + 1)
continue;
_samples.Add(upper + '|'); // Add end character
}
@ -48,9 +69,9 @@ public class MarkovNameGenerator
//Build chains
foreach (string word in _samples)
{
for (int letter = 0; letter < word.Length - order; letter++)
for (int letter = 0; letter < word.Length - _order; letter++)
{
string token = word.Substring(letter, order);
string token = word.Substring(letter, _order);
List<char> entry = null;
if (_chains.ContainsKey(token))
entry = _chains[token];
@ -59,7 +80,7 @@ public class MarkovNameGenerator
entry = new List<char>();
_chains[token] = entry;
}
entry.Add(word[letter + order]);
entry.Add(word[letter + _order]);
}
}
}
@ -75,7 +96,6 @@ public class MarkovNameGenerator
{
int n = _random.NextInt(0,_samples.Count);
//int nameLength = _samples[n].Length;
//s = _samples[n].Substring(UnityEngine.Random.Range(0, _samples[n].Length - _order), _order);//get a random token somewhere in middle of sample word
s = _samples[n].Substring(0, _order);//get a random token at the start of a sample word
while (true)//(s.Length < nameLength)
{
@ -86,11 +106,11 @@ public class MarkovNameGenerator
else
break;
}
s = s.Substring(0, 1) + s.Substring(1).ToLower();
s = s.Substring(0, 1).ToUpper() + s.Substring(1);
}
while (_used.Contains(s) || s.Length < _minLength || s.Length > _maxLength);
_used.Add(s);
return s.First().ToString().ToUpper() + s.Substring(1);
return s;
}
}

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DataStructures.ViliWonka.Heap;
using Unity.Mathematics;
using JM.LinqFaster;
using static Unity.Mathematics.math;
@ -7,10 +9,21 @@ using static Unity.Mathematics.math;
public class Sector
{
public List<SectorZone> Zones = new List<SectorZone>();
public Dictionary<MegaCorporation, SectorZone> OccupantSources = new Dictionary<MegaCorporation, SectorZone>();
public Dictionary<MegaCorporation, SectorZone> HomeZones = new Dictionary<MegaCorporation, SectorZone>();
public Dictionary<MegaCorporation, SectorZone> BossZones = new Dictionary<MegaCorporation, SectorZone>();
public SectorZone Entrance;
public SectorZone Exit;
private SectorZone[] _exitPath;
public SectorZone[] ExitPath
{
get
{
return _exitPath ??= FindPath(Entrance, Exit);
}
}
class DijkstraVertex
{
public float Cost;
@ -18,30 +31,29 @@ public class Sector
public SectorZone Zone;
}
public List<SectorZone> ConnectedRegion(SectorZone v, SectorZone ignoreZone = null)
{
List<SectorZone> members = new List<SectorZone>();
members.Add(v);
while (true)
{
var lastCount = members.Count;
// For each member, add all vertices that are connected to it but are not already a member
// Also, if there is a zone being ignored, do not traverse across it
foreach (SectorZone m in members.ToArray())
members.AddRange(m.AdjacentZones
.Where(z => !members.Contains(z) && !(m == v && ignoreZone == z)));
// If we have stopped finding neighbors, stop traversing
if (members.Count == lastCount)
return members;
}
}
// public HashSet<SectorZone> ConnectedRegion(SectorZone v, SectorZone ignoreZone = null)
// {
// var members = new HashSet<SectorZone> {v};
// while (true)
// {
// var lastCount = members.Count;
// // For each member, add all vertices that are connected to it but are not already a member
// // Also, if there is a zone being ignored, do not traverse across it
// foreach (var zone in members.ToArray())
// foreach(var adjacentZone in zone.AdjacentZones)
// if (!members.Contains(adjacentZone) && !(zone == v && ignoreZone == adjacentZone))
// members.Add(adjacentZone);
// // If we have stopped finding neighbors, stop traversing
// if (members.Count == lastCount)
// return members;
// }
// }
//public int TotalDistance(SectorZone v) => ConnectedRegionDistance(v).Sum(x => x.distance);
public Dictionary<SectorZone, int> ConnectedRegionDistance(SectorZone v)
{
Dictionary<SectorZone, int> members = new Dictionary<SectorZone, int>();
members.Add(v,0);
var members = new Dictionary<SectorZone, int> {{v, 0}};
int cost = 1;
while (true)
{
@ -60,31 +72,83 @@ public class Sector
cost++;
}
}
public List<SectorZone> FindPath(SectorZone source, SectorZone target, bool bestFirst = false)
public HashSet<SectorZone> ConnectedRegion(
SectorZone v,
SectorZone ignoreLink1,
SectorZone ignoreLink2,
int maxDistance = int.MaxValue)
{
SortedList<float,DijkstraVertex> members = new SortedList<float,DijkstraVertex>{{0, new DijkstraVertex{Zone = source}}};
List<DijkstraVertex> searched = new List<DijkstraVertex>();
return ConnectedRegion(v, x => x.Item1 != ignoreLink1 || x.Item2 != ignoreLink2, maxDistance);
}
public HashSet<SectorZone> ConnectedRegion(
SectorZone v,
SectorZone ignoreZone,
int maxDistance = int.MaxValue)
{
return ConnectedRegion(v, x => x.Item2 != ignoreZone, maxDistance);
}
public HashSet<SectorZone> ConnectedRegion(
SectorZone v,
int maxDistance = int.MaxValue)
{
return ConnectedRegion(v, x => true, maxDistance);
}
public HashSet<SectorZone> ConnectedRegion(
SectorZone v,
Predicate<(SectorZone,SectorZone)> linkFilter,
int maxDistance = int.MaxValue)
{
var visited = new HashSet<SectorZone> {v};
int cost = 1;
while (true)
{
var s = members.FirstOrDefault(m => !searched.Contains(m.Value)).Value; // Lowest cost unsearched node
if (s == null) return null; // No vertices left unsearched
var lastCount = visited.Count;
// For each member, add all nodes that are connected to it but have not been visited
// Also, if there is a zone being ignored, do not traverse across it
foreach (var zone in visited.ToArray())
foreach (var adjacentZone in zone.AdjacentZones)
{
if(!visited.Contains(adjacentZone) && linkFilter((zone, adjacentZone)))
visited.Add(adjacentZone);
}
// If we have stopped finding neighbors, stop traversing
if (visited.Count == lastCount || cost == maxDistance)
return visited;
cost++;
}
}
public SectorZone[] FindPath(SectorZone source, SectorZone target, bool bestFirst = false)
{
MinHeap<DijkstraVertex> unsearchedNodes = new MinHeap<DijkstraVertex>();
unsearchedNodes.PushObj(new DijkstraVertex{Zone = source}, 0);
//SortedList<float,DijkstraVertex> members = new SortedList<float,DijkstraVertex>{{0, new DijkstraVertex{Zone = source}}};
var searched = new HashSet<SectorZone>();
while (true)
{
if(unsearchedNodes.Count == 0) return null; // No nodes left unsearched
var s = unsearchedNodes.PopObj(); // Lowest cost unsearched node
if (s.Zone == target) // We found the path
{
Stack<DijkstraVertex> path = new Stack<DijkstraVertex>(); // Since we start at the end, use a LIFO collection
path.Push(s);
while(path.Peek().Parent!=null) // Keep pushing until we reach the start, which has no parent
path.Push(path.Peek().Parent);
return path.Select(dv => dv.Zone).ToList();
return path.Select(dv => dv.Zone).ToArray();
}
// For each adjacent star (filter already visited stars unless heuristic is in use)
foreach (var dijkstraStar in s.Zone.AdjacentZones
.WhereSelectF(zone => !bestFirst || members.All(m => m.Value.Zone != zone),
IEnumerable<SectorZone> zonesToSearch = s.Zone.AdjacentZones;
if (!bestFirst) zonesToSearch = zonesToSearch.Where(z => !searched.Contains(z));
foreach (var dijkstraStar in zonesToSearch
// Cost is parent cost plus distance squared
zone => new DijkstraVertex {Parent = s, Zone = zone, Cost = s.Cost + lengthsq(s.Zone.Position - zone.Position)}))
.Select(zone => new DijkstraVertex {Parent = s, Zone = zone, Cost = s.Cost + lengthsq(s.Zone.Position - zone.Position)}))
// Add new member to list, sorted by cost plus optional heuristic
members.Add(bestFirst ? dijkstraStar.Cost + lengthsq(dijkstraStar.Zone.Position - target.Position) : dijkstraStar.Cost, dijkstraStar);
searched.Add(s);
unsearchedNodes.PushObj(dijkstraStar, bestFirst ? dijkstraStar.Cost + lengthsq(dijkstraStar.Zone.Position - target.Position) : dijkstraStar.Cost);
searched.Add(s.Zone);
}
}
}
@ -96,5 +160,7 @@ public class SectorZone
public List<SectorZone> AdjacentZones = new List<SectorZone>();
public int Isolation;
public Dictionary<SectorZone, int> Distance;
public MegaCorporation[] Occupants;
public MegaCorporation[] Megas;
//public List<MegaCorporation> Megas;
public MegaCorporation Owner;
}

View file

@ -5,12 +5,13 @@ using DataStructures.ViliWonka.Heap;
using MIConvexHull;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using Random = Unity.Mathematics.Random;
public static class SectorGenerator
{
public static Sector GenerateSector(SectorGenerationSettings settings, MegaCorporation[] megas, int zoneCount = 32, float linkDensity = .5f, float minLineSeparation = .01f)
public static Sector GenerateSector(SectorGenerationSettings settings, MegaCorporation[] megas, ref Random random, float minLineSeparation = .01f)
{
var outputSamples = WeightedSampleElimination.GeneratePoints(zoneCount,
var outputSamples = WeightedSampleElimination.GeneratePoints(settings.ZoneCount,
settings.CloudDensity,
v => (.2f - lengthsq(v - float2(.5f))) * 4);
var sector = new Sector();
@ -60,10 +61,10 @@ public static class SectorGenerator
var heap = new MaxHeap<(SectorZone, SectorZone)>(links.Count);
foreach(var link in links) heap.PushObj(link, LinkWeight(link));
while (heap.Count > linkDensity * links.Count)
while (heap.Count > settings.LinkDensity * links.Count)
{
var link = heap.PopObj();
if(sector.ConnectedRegion(link.Item1, link.Item2).Contains(link.Item2))
if(sector.ConnectedRegion(link.Item1, link.Item1, link.Item2).Contains(link.Item2))
{
link.Item1.AdjacentZones.Remove(link.Item2);
link.Item2.AdjacentZones.Remove(link.Item1);
@ -83,60 +84,80 @@ public static class SectorGenerator
#endregion
// Cache distance set and calculate isolation for every zone (used extensively for placing stuff)
foreach (var zone in sector.Zones)
{
zone.Distance = sector.ConnectedRegionDistance(zone);
zone.Isolation = zone.Distance.Sum(x => x.Value);
}
// Exit is the most isolated zone (highest total distance to all other zones
sector.Exit = sector.Zones.MaxBy(z => z.Isolation);
// Entrance is the zone furthest from the exit
sector.Entrance = sector.Zones.MaxBy(z => sector.Exit.Distance[z]);
foreach (var mega in megas)
// Store exit path in hash set for querying
var exitPathSet = new HashSet<SectorZone>(sector.ExitPath);
// Find all zones on the exit path where removing that zone would disconnect the entrance from the exit
// Disregard "corridor" zones with only two adjacent zones
var chokePoints = sector.ExitPath
.Where(z => z.AdjacentZones.Count > 2 && !sector.ConnectedRegion(sector.Entrance, z).Contains(sector.Exit));
// Choose some megas to have bosses placed based on whether a boss hull is assigned
var bossMegas = megas
.Where(m => m.BossHull != Guid.Empty)
.Take(settings.BossCount)
.ToArray();
// Place boss zones along the critical path as far apart from each other as possible
foreach (var mega in bossMegas)
{
sector.OccupantSources[mega] = sector.Zones.MaxBy(z =>
sector.BossZones[mega] = chokePoints.MaxBy(z =>
sector.Exit.Distance[z] * sector.Entrance.Distance[z] *
sector.OccupantSources.Values.Aggregate(1, (i, os) => i * os.Distance[z]));
sector.BossZones.Values.Aggregate(1, (i, os) => i * os.Distance[z]));
}
foreach (var zone in sector.Zones)
// Place boss mega headquarters such that their sphere of influence encompasses their boss zone
// While occupying as much territory as possible
foreach (var mega in bossMegas)
{
var sourceDistances = new int[megas.Length];
var minSourceDistance = int.MaxValue;
var occupants = 0;
for (var i = 0; i < megas.Length; i++)
{
sourceDistances[i] = sector.OccupantSources[megas[i]].Distance[zone];
if (sourceDistances[i] == minSourceDistance)
{
occupants++;
}
if (sourceDistances[i] < minSourceDistance)
{
minSourceDistance = sourceDistances[i];
occupants = 1;
}
}
zone.Occupants = new MegaCorporation[occupants];
var occupantIndex = 0;
for (var i = 0; i < megas.Length; i++)
{
if (sourceDistances[i] == minSourceDistance)
{
zone.Occupants[occupantIndex++] = megas[i];
}
}
}
sector.Zones[0].Name = "Hello World!";
foreach (var zone in sector.Zones)
{
zone.Name = zone.Occupants[0].NameGenerator.NextName; // TODO: Merge Naming
sector.HomeZones[mega] = sector
.ConnectedRegion(sector.BossZones[mega], mega.InfluenceDistance)
.MaxBy(z =>
sector.ConnectedRegion(z, mega.InfluenceDistance).Count *
sector.HomeZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])));
}
// Place remaining headquarters away from existing megas while also maximizing territory
foreach (var mega in megas.Where(m=>!bossMegas.Contains(m)))
{
sector.HomeZones[mega] = sector.Zones.MaxBy(z =>
pow(sector.ConnectedRegion(z, mega.InfluenceDistance).Count, sector.HomeZones.Count) *
sector.Exit.Distance[z] * sector.Entrance.Distance[z] *
sector.HomeZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])) *
sector.BossZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])));
}
// Assign faction presence
foreach (var zone in sector.Zones)
{
// All megas for are present in zones within their sphere of influence
zone.Megas = megas
.Where(m => zone.Distance[sector.HomeZones[m]] <= m.InfluenceDistance)
.ToArray();
// Owner of a zone is the one with the nearest headquarters
var nearestMega = megas.MinBy(m => zone.Distance[sector.HomeZones[m]]);
if (zone.Distance[sector.HomeZones[nearestMega]] <= nearestMega.InfluenceDistance)
zone.Owner = nearestMega;
}
// Generate zone name using the owner's name generator, otherwise assign catalogue ID
foreach (var zone in sector.Zones)
zone.Name = zone.Owner?.NameGenerator.NextName ?? $"EAC-{random.NextInt(99999).ToString()}";
return sector;
}
}

View file

@ -46,6 +46,13 @@ public class GalaxyShapeSettings
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
public class SectorGenerationSettings
{
public float LinkDensity = .5f;
public int ZoneCount = 128;
public int NameGeneratorMinLength = 5;
public int NameGeneratorMaxLength = 10;
public int NameGeneratorOrder = 4;
public int MegaCount;
public int BossCount;
public float NoisePosition;
public float CloudExponent;
public float CloudAmplitude;