Merge eureka/aetheria-release-scenarios-menu: every new run comes from a scenario, launched from the New Game submenu
Tutorial Galaxy and Main Galaxy are scenarios; six test arenas show in editor and development builds. RunStart owns new-run staging; Zone.Admit stays the one admission. Galaxy zone linking moves to a double-precision Delaunay (the vendored float hull dropped zones; ~2% of galaxies threw). A fixed seed fixes every fit; a failed start keeps the saved run and says why. Soul s1 and s2 held (cut-scenarios-menu.h1, h2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
a24ffd77db
84 changed files with 1557 additions and 4857 deletions
|
|
@ -915,7 +915,6 @@ MonoBehaviour:
|
|||
m_EditorClassIdentifier:
|
||||
Settings: {fileID: 11400000, guid: cd2344e2001a37544aab0960c164d4ff, type: 2}
|
||||
Dialog: {fileID: 4533226043449861657}
|
||||
InGame: 0
|
||||
PanelPrototype: {fileID: 7329274282942065594}
|
||||
FadeTime: 0.5
|
||||
FadeDistance: 1024
|
||||
|
|
|
|||
|
|
@ -9636,11 +9636,6 @@ PrefabInstance:
|
|||
propertyPath: m_SizeDelta.y
|
||||
value: 90.91
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4533226043457584178, guid: 3d8d9cbf10b12214e98891c1f03c1e99,
|
||||
type: 3}
|
||||
propertyPath: InGame
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4533226043457584178, guid: 3d8d9cbf10b12214e98891c1f03c1e99,
|
||||
type: 3}
|
||||
propertyPath: CloudRenderer
|
||||
|
|
|
|||
|
|
@ -106,7 +106,8 @@ public class ActionGameManager : MonoBehaviour
|
|||
}
|
||||
|
||||
public static Galaxy CurrentGalaxy;
|
||||
public static bool IsTutorial;
|
||||
// The scenario a new run starts from: MainMenu.Launch sets it, and StartGame's new-run branch consumes it once.
|
||||
public static Scenario PendingScenario;
|
||||
|
||||
public GameSettings Settings;
|
||||
//public string StarterShipTemplate = "Longinus";
|
||||
|
|
@ -250,7 +251,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
{
|
||||
if (CurrentGalaxy != null)
|
||||
{
|
||||
var (game, zones) = RunSave.Capture(CultCache, CurrentGalaxy, Zone, DockedEntity ?? CurrentEntity, IsTutorial,
|
||||
var (game, zones) = RunSave.Capture(CultCache, CurrentGalaxy, Zone, DockedEntity ?? CurrentEntity,
|
||||
_actionBarSlots.Select(s => s.Save()).ToArray());
|
||||
RunSave.Commit(CultCache, game, zones, ItemManager.Lots);
|
||||
}
|
||||
|
|
@ -269,7 +270,9 @@ public class ActionGameManager : MonoBehaviour
|
|||
EntityInstance.EffectManagerParent = EffectManagerParent;
|
||||
ConsoleController.MessageReceiver = this;
|
||||
|
||||
ItemManager = new ItemManager(CultCache, RunSave.Lots(CultCache), Settings.GameplaySettings, Debug.Log);
|
||||
// A new run mints into an empty ledger; the saved run's stays in the store until the new run has started.
|
||||
ItemManager = new ItemManager(CultCache, PendingScenario != null ? new ProvenanceLedger() : RunSave.Lots(CultCache),
|
||||
Settings.GameplaySettings, Debug.Log);
|
||||
ZoneRenderer.ItemManager = ItemManager;
|
||||
|
||||
// If hiding minimap asteroids, turn them off to start with
|
||||
|
|
@ -578,19 +581,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
{
|
||||
var nearestFaction = CurrentGalaxy.Factions.MinBy(f => CurrentGalaxy.HomeZones[f].Distance[Zone.GalaxyZone]);
|
||||
|
||||
var loadoutGenerator = IsTutorial ? new LoadoutGenerator(
|
||||
ref ItemManager.Random,
|
||||
ItemManager,
|
||||
CurrentGalaxy,
|
||||
Zone.GalaxyZone,
|
||||
nearestFaction,
|
||||
.5f) : new LoadoutGenerator(
|
||||
ref ItemManager.Random,
|
||||
ItemManager,
|
||||
CurrentGalaxy,
|
||||
Zone.GalaxyZone,
|
||||
nearestFaction,
|
||||
.5f);
|
||||
var loadoutGenerator = new LoadoutGenerator(ref ItemManager.Random, ItemManager, CurrentGalaxy, Zone.GalaxyZone, nearestFaction, .5f);
|
||||
|
||||
var turret = EntitySerializer.Unpack(ItemManager, Zone, loadoutGenerator.GenerateTurretLoadout());
|
||||
turret.Position.xz = _currentEntity.Position.xz +
|
||||
|
|
@ -728,7 +719,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
Settings.ZoneSettings,
|
||||
CurrentGalaxy,
|
||||
galaxyZone,
|
||||
IsTutorial
|
||||
CurrentGalaxy.IsPrelude
|
||||
);
|
||||
galaxyZone.Contents = new Zone(ItemManager, Settings.PlanetSettings, galaxyZone.PackedContents, galaxyZone, CurrentGalaxy);
|
||||
}
|
||||
|
|
@ -806,21 +797,10 @@ public class ActionGameManager : MonoBehaviour
|
|||
{
|
||||
if (CurrentGalaxy != null)
|
||||
{
|
||||
var saved = CultCache.GetGlobal<SavedGame>();
|
||||
if (saved == null)
|
||||
{
|
||||
SectorMap.QueueZoneReveal(CurrentGalaxy.Entrance.AdjacentZones.Prepend(CurrentGalaxy.Entrance));
|
||||
// RunStart owns what a new run starts with (docs/scenarios-cut.md, 2.2); this only enters and binds.
|
||||
RunStart.GenerateArena(ItemManager, Settings.ZoneSettings, CurrentGalaxy, null);
|
||||
PopulateLevel(CurrentGalaxy.Entrance);
|
||||
var failures = new List<string>();
|
||||
var staged = RunStart.Stage(ItemManager, Zone, null, Settings.StartingHullName,
|
||||
IsTutorial ? CurrentGalaxy.ResolveFaction(Settings.TutorialGenerationSettings.ProtagonistFaction) : null, failures)
|
||||
?? throw new InvalidOperationException($"The new run could not stage: {string.Join("; ", failures)}");
|
||||
BindToEntity(staged.Player);
|
||||
}
|
||||
if (PendingScenario != null) StartScenario();
|
||||
else
|
||||
{
|
||||
var saved = CultCache.GetGlobal<SavedGame>();
|
||||
foreach(var group in CurrentGalaxy.DiscoveredZones
|
||||
.GroupBy(dz=>dz.Distance[CurrentGalaxy.Entrance]))
|
||||
SectorMap.QueueZoneReveal(group);
|
||||
|
|
@ -845,6 +825,41 @@ public class ActionGameManager : MonoBehaviour
|
|||
}
|
||||
}
|
||||
|
||||
// RunStart owns what a new run starts with (docs/scenarios-cut.md, R.4); this enters and binds. The saved run is
|
||||
// replaced only once the scenario has staged. A start that fails leaves the saved run as it was and says why on
|
||||
// the main menu, where Continue still resumes the saved run.
|
||||
private void StartScenario()
|
||||
{
|
||||
var scenario = PendingScenario;
|
||||
PendingScenario = null;
|
||||
var failures = new List<string>();
|
||||
RunStart.Staged staged = null;
|
||||
try
|
||||
{
|
||||
staged = RunSave.Replace(CultCache, () =>
|
||||
{
|
||||
RunStart.GenerateArena(ItemManager, Settings.ZoneSettings, CurrentGalaxy, scenario);
|
||||
PopulateLevel(CurrentGalaxy.Entrance);
|
||||
return RunStart.Stage(ItemManager, Zone, scenario, Settings.StartingHullName, Settings.TutorialGenerationSettings, failures);
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
failures.Add(exception.Message);
|
||||
}
|
||||
|
||||
if (staged == null)
|
||||
{
|
||||
CurrentGalaxy = null;
|
||||
MainMenu.gameObject.SetActive(true);
|
||||
MainMenu.Refuse($"{scenario.Name} could not start", string.Join("\n", failures));
|
||||
return;
|
||||
}
|
||||
SectorMap.QueueZoneReveal(CurrentGalaxy.Entrance.AdjacentZones.Prepend(CurrentGalaxy.Entrance));
|
||||
BindToEntity(staged.Player);
|
||||
}
|
||||
|
||||
private IEnumerator IntroCutscene(Ship ship)
|
||||
{
|
||||
ZoneRenderer.PerspectiveEntity = ship;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using GameCult.Caching.MessagePack;
|
|||
// Aetheria's stores: one home store per type, composed here and nowhere else.
|
||||
public static class AetheriaStores
|
||||
{
|
||||
public static readonly Type[] CatalogTypes = { typeof(ItemData), typeof(ShipAuthoring), typeof(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(NameFile), typeof(InputLayout), typeof(Loadout), typeof(Scenario) };
|
||||
public static readonly Type[] CatalogTypes = { typeof(ItemData), typeof(ShipAuthoring), typeof(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(NameFile), typeof(InputLayout), typeof(Loadout) };
|
||||
public static readonly Type[] RunTypes = { typeof(OrbitData), typeof(BodyData), typeof(SavedZone), typeof(SavedGame), typeof(ProvenanceLedger) };
|
||||
public static readonly Type[] PlayerTypes = { typeof(PlayerSettings) };
|
||||
|
||||
|
|
|
|||
97
Assets/Scripts/ServerShared/Delaunay.cs
Normal file
97
Assets/Scripts/ServerShared/Delaunay.cs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CultMath;
|
||||
|
||||
// The Delaunay triangulation of a galaxy's zone positions, as the edges Galaxy links zones along. Bowyer-Watson
|
||||
// (Bowyer 1981, Watson 1981) in double precision: each point is inserted by removing every triangle whose
|
||||
// circumcircle holds it and fanning the cavity's boundary to it. Every point lands in the triangulation, so for two or
|
||||
// more distinct points the edge graph is connected.
|
||||
public static class Delaunay
|
||||
{
|
||||
// Each edge once, as (lower index, higher index), in ascending order.
|
||||
public static List<(int a, int b)> Edges(IReadOnlyList<float2> points)
|
||||
{
|
||||
var n = points.Count;
|
||||
var x = new double[n + 3];
|
||||
var y = new double[n + 3];
|
||||
double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue;
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
x[i] = points[i].x;
|
||||
y[i] = points[i].y;
|
||||
minX = Math.Min(minX, x[i]);
|
||||
minY = Math.Min(minY, y[i]);
|
||||
maxX = Math.Max(maxX, x[i]);
|
||||
maxY = Math.Max(maxY, y[i]);
|
||||
}
|
||||
|
||||
// A super-triangle far outside the points, so its vertices bend no edge between them; its triangles are
|
||||
// dropped at the end.
|
||||
var size = Math.Max(Math.Max(maxX - minX, maxY - minY), 1e-6) * 1000;
|
||||
double midX = (minX + maxX) / 2, midY = (minY + maxY) / 2;
|
||||
x[n] = midX - size; y[n] = midY - size;
|
||||
x[n + 1] = midX + size; y[n + 1] = midY - size;
|
||||
x[n + 2] = midX; y[n + 2] = midY + size;
|
||||
|
||||
var triangles = new List<Triangle> { new Triangle(n, n + 1, n + 2, x, y) };
|
||||
var cavity = new Dictionary<(int, int), int>();
|
||||
for (var p = 0; p < n; p++)
|
||||
{
|
||||
cavity.Clear();
|
||||
for (var t = triangles.Count - 1; t >= 0; t--)
|
||||
{
|
||||
var triangle = triangles[t];
|
||||
var dx = x[p] - triangle.CenterX;
|
||||
var dy = y[p] - triangle.CenterY;
|
||||
if (dx * dx + dy * dy >= triangle.RadiusSquared) continue;
|
||||
Count(cavity, triangle.A, triangle.B);
|
||||
Count(cavity, triangle.B, triangle.C);
|
||||
Count(cavity, triangle.C, triangle.A);
|
||||
triangles[t] = triangles[triangles.Count - 1];
|
||||
triangles.RemoveAt(triangles.Count - 1);
|
||||
}
|
||||
|
||||
// The cavity's boundary is every edge only one removed triangle had.
|
||||
foreach (var edge in cavity)
|
||||
if (edge.Value == 1)
|
||||
triangles.Add(new Triangle(edge.Key.Item1, edge.Key.Item2, p, x, y));
|
||||
}
|
||||
|
||||
var edges = new SortedSet<(int a, int b)>();
|
||||
foreach (var triangle in triangles)
|
||||
{
|
||||
if (triangle.A >= n || triangle.B >= n || triangle.C >= n) continue;
|
||||
edges.Add(Ordered(triangle.A, triangle.B));
|
||||
edges.Add(Ordered(triangle.B, triangle.C));
|
||||
edges.Add(Ordered(triangle.C, triangle.A));
|
||||
}
|
||||
return new List<(int a, int b)>(edges);
|
||||
}
|
||||
|
||||
private static void Count(Dictionary<(int, int), int> cavity, int a, int b)
|
||||
{
|
||||
var key = Ordered(a, b);
|
||||
cavity[key] = cavity.TryGetValue(key, out var seen) ? seen + 1 : 1;
|
||||
}
|
||||
|
||||
private static (int a, int b) Ordered(int a, int b) => a < b ? (a, b) : (b, a);
|
||||
|
||||
private readonly struct Triangle
|
||||
{
|
||||
public readonly int A, B, C;
|
||||
public readonly double CenterX, CenterY, RadiusSquared;
|
||||
|
||||
public Triangle(int a, int b, int c, double[] x, double[] y)
|
||||
{
|
||||
A = a;
|
||||
B = b;
|
||||
C = c;
|
||||
double ax = x[a], ay = y[a], bx = x[b], by = y[b], cx = x[c], cy = y[c];
|
||||
var d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
||||
double a2 = ax * ax + ay * ay, b2 = bx * bx + by * by, c2 = cx * cx + cy * cy;
|
||||
CenterX = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d;
|
||||
CenterY = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
|
||||
RadiusSquared = (ax - CenterX) * (ax - CenterX) + (ay - CenterY) * (ay - CenterY);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ServerShared/Delaunay.cs.meta
Normal file
2
Assets/Scripts/ServerShared/Delaunay.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a05b77dab9114e19b222b79a8403fc21
|
||||
|
|
@ -7,7 +7,6 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using DataStructures.ViliWonka.Heap;
|
||||
using Ink.Runtime;
|
||||
using MIConvexHull;
|
||||
using JM.LinqFaster;
|
||||
using UniRx;
|
||||
using CultMath;
|
||||
|
|
@ -29,6 +28,10 @@ public class Galaxy
|
|||
public Dictionary<Faction, FactionRelationship> FactionRelationships { get; } = new Dictionary<Faction, FactionRelationship>();
|
||||
private Action<string> Log { get; }
|
||||
public bool IsPrelude { get; }
|
||||
|
||||
// The seed this galaxy was generated at, which also seeds its run's item draws (RunStart); zero for a galaxy
|
||||
// restored from a save.
|
||||
public uint Seed { get; }
|
||||
|
||||
private HashSet<CultRecordKey> _containedFactions;
|
||||
private GalaxyZone[] _exitPath;
|
||||
|
|
@ -105,7 +108,8 @@ public class Galaxy
|
|||
Background = background;
|
||||
Log = log;
|
||||
var factions = cache.GetAll<Faction>();
|
||||
var random = new Random(seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed);
|
||||
Seed = seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed;
|
||||
var random = new Random(Seed);
|
||||
Factions = factions.OrderBy(x => random.NextFloat()).Take(settings.MegaCount).ToArray();
|
||||
foreach (var f in Factions) FactionRelationships[f] = FactionRelationship.Neutral;
|
||||
|
||||
|
|
@ -139,6 +143,11 @@ public class Galaxy
|
|||
return _cache.GetAll<Faction>().FirstOrDefault(f => f.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
// How many jumps a faction's influence reaches in this galaxy: its authored distance, halved (rounding up) in a
|
||||
// prelude. Derived, never written back to the catalog's Faction, so one galaxy's generation cannot change the next.
|
||||
public int InfluenceOf(Faction faction) =>
|
||||
IsPrelude ? (faction.InfluenceDistance + 1) / 2 : faction.InfluenceDistance;
|
||||
|
||||
public Galaxy(
|
||||
TutorialGenerationSettings settings,
|
||||
SectorBackgroundSettings background,
|
||||
|
|
@ -155,7 +164,8 @@ public class Galaxy
|
|||
|
||||
Background = background;
|
||||
Log = log;
|
||||
var random = new Random(seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed);
|
||||
Seed = seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed;
|
||||
var random = new Random(Seed);
|
||||
|
||||
var factions = new List<Faction>();
|
||||
|
||||
|
|
@ -177,11 +187,7 @@ public class Galaxy
|
|||
factions.AddRange(neutralFactions);
|
||||
|
||||
Factions = factions.ToArray();
|
||||
foreach (var faction in Factions)
|
||||
{
|
||||
FactionRelationships[faction] = FactionRelationship.Neutral;
|
||||
faction.InfluenceDistance = (faction.InfluenceDistance + 1) / 2;
|
||||
}
|
||||
foreach (var faction in Factions) FactionRelationships[faction] = FactionRelationship.Neutral;
|
||||
|
||||
Zones = GenerateZones(settings.ZoneCount, ref random, progressCallback);
|
||||
|
||||
|
|
@ -190,13 +196,13 @@ public class Galaxy
|
|||
CalculateDistanceMatrix(progressCallback);
|
||||
|
||||
HomeZones[protagonistFaction] = Zones
|
||||
.MaxBy(z => ConnectedRegion(z, protagonistFaction.InfluenceDistance).Count);
|
||||
.MaxBy(z => ConnectedRegion(z, InfluenceOf(protagonistFaction)).Count);
|
||||
|
||||
HomeZones[antagonistFaction] = Zones
|
||||
.MaxBy(z => ConnectedRegion(z, antagonistFaction.InfluenceDistance).Count * z.Distance[HomeZones[protagonistFaction]]);
|
||||
.MaxBy(z => ConnectedRegion(z, InfluenceOf(antagonistFaction)).Count * z.Distance[HomeZones[protagonistFaction]]);
|
||||
|
||||
HomeZones[protagonistFaction] = Zones
|
||||
.MaxBy(z => ConnectedRegion(z, protagonistFaction.InfluenceDistance).Count * sqrt(z.Distance[HomeZones[antagonistFaction]]));
|
||||
.MaxBy(z => ConnectedRegion(z, InfluenceOf(protagonistFaction)).Count * sqrt(z.Distance[HomeZones[antagonistFaction]]));
|
||||
|
||||
// var antagonistRegion = ConnectedRegion(HomeZones[antagonistFaction], antagonistFaction.InfluenceDistance);
|
||||
// var protagonistRegion = ConnectedRegion(HomeZones[protagonistFaction], protagonistFaction.InfluenceDistance);
|
||||
|
|
@ -205,13 +211,13 @@ public class Galaxy
|
|||
var bufferDistance = Zones.Min(z => abs(z.Distance[HomeZones[antagonistFaction]] - z.Distance[HomeZones[protagonistFaction]]));
|
||||
var potentialBufferZones = Zones
|
||||
.Where(z => abs(z.Distance[HomeZones[antagonistFaction]] - z.Distance[HomeZones[protagonistFaction]]) == bufferDistance);
|
||||
HomeZones[bufferFaction] = potentialBufferZones.MaxBy(z => ConnectedRegion(z, bufferFaction.InfluenceDistance).Count);
|
||||
HomeZones[bufferFaction] = potentialBufferZones.MaxBy(z => ConnectedRegion(z, InfluenceOf(bufferFaction)).Count);
|
||||
|
||||
// Place neutral headquarters away from existing factions while also maximizing territory
|
||||
foreach (var faction in neutralFactions)
|
||||
{
|
||||
HomeZones[faction] = Zones.MaxBy(z =>
|
||||
ConnectedRegion(z, faction.InfluenceDistance).Count *
|
||||
ConnectedRegion(z, InfluenceOf(faction)).Count *
|
||||
HomeZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])));
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +227,7 @@ public class Galaxy
|
|||
.Where(z => z.Factions.Contains(antagonistFaction) && z.Factions.Contains(bufferFaction));
|
||||
if (potentialQuestZones.Any())
|
||||
HomeZones[questFaction] = potentialQuestZones
|
||||
.MaxBy(z => z.Distance[HomeZones[antagonistFaction]] * ConnectedRegion(z, questFaction.InfluenceDistance).Count);
|
||||
.MaxBy(z => z.Distance[HomeZones[antagonistFaction]] * ConnectedRegion(z, InfluenceOf(questFaction)).Count);
|
||||
else
|
||||
HomeZones[questFaction] = Zones
|
||||
.Where(z => z.Factions.Contains(antagonistFaction))
|
||||
|
|
@ -287,9 +293,9 @@ public class Galaxy
|
|||
// While occupying as much territory as possible
|
||||
foreach (var mega in bossMegas)
|
||||
{
|
||||
HomeZones[mega] = ConnectedRegion(BossZones[mega], mega.InfluenceDistance)
|
||||
HomeZones[mega] = ConnectedRegion(BossZones[mega], InfluenceOf(mega))
|
||||
.MaxBy(z =>
|
||||
ConnectedRegion(z, mega.InfluenceDistance).Count *
|
||||
ConnectedRegion(z, InfluenceOf(mega)).Count *
|
||||
HomeZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])));
|
||||
}
|
||||
|
||||
|
|
@ -297,7 +303,7 @@ public class Galaxy
|
|||
foreach (var mega in Factions.Where(m => !bossMegas.Contains(m)))
|
||||
{
|
||||
HomeZones[mega] = Zones.MaxBy(z =>
|
||||
pow(ConnectedRegion(z, mega.InfluenceDistance).Count, HomeZones.Count) *
|
||||
pow(ConnectedRegion(z, InfluenceOf(mega)).Count, HomeZones.Count) *
|
||||
Exit.Distance[z] * Entrance.Distance[z] *
|
||||
HomeZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])) *
|
||||
BossZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])));
|
||||
|
|
@ -315,14 +321,14 @@ public class Galaxy
|
|||
// Factions are present in all zones within their sphere of influence
|
||||
zone.Factions = Factions
|
||||
.Where(f => HomeZones.ContainsKey(f))
|
||||
.Where(f => zone.Distance[HomeZones[f]] <= f.InfluenceDistance)
|
||||
.Where(f => zone.Distance[HomeZones[f]] <= InfluenceOf(f))
|
||||
.ToArray();
|
||||
|
||||
// Owner of a zone is the faction with the nearest headquarters
|
||||
var nearestFaction = Factions
|
||||
.Where(f => HomeZones.ContainsKey(f))
|
||||
.MinBy(f => (float)zone.Distance[HomeZones[f]]);
|
||||
if (zone.Distance[HomeZones[nearestFaction]] <= nearestFaction.InfluenceDistance)
|
||||
if (zone.Distance[HomeZones[nearestFaction]] <= InfluenceOf(nearestFaction))
|
||||
zone.Owner = nearestFaction;
|
||||
}
|
||||
}
|
||||
|
|
@ -332,12 +338,13 @@ public class Galaxy
|
|||
ref Random random,
|
||||
Action<string> progressCallback = null)
|
||||
{
|
||||
var used = new HashSet<string>();
|
||||
for (var i = 0; i < Factions.Length; i++)
|
||||
{
|
||||
progressCallback?.Invoke($"Feeding Markov Chains: {i + 1} / {Factions.Length}");
|
||||
//if(progressCallback!=null) Thread.Sleep(250); // Inserting Delay to make it seem like it's doing more work lmao
|
||||
var faction = Factions[i];
|
||||
_nameGenerators[faction] = new MarkovNameGenerator(ref random, cache.Get(faction.GeonameFile).Names, nameGeneratorSettings);
|
||||
_nameGenerators[faction] = new MarkovNameGenerator(ref random, cache.Get(faction.GeonameFile).Names, nameGeneratorSettings, used);
|
||||
}
|
||||
|
||||
// Generate zone name using the owner's name generator, otherwise assign catalogue ID
|
||||
|
|
@ -359,22 +366,10 @@ public class Galaxy
|
|||
progressCallback?.Invoke("Triangulating Zone Positions");
|
||||
if (progressCallback != null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
|
||||
|
||||
// Create a delaunay triangulation to connect adjacent sectors
|
||||
var triangulation = DelaunayTriangulation<Vertex2<GalaxyZone>, Cell2<GalaxyZone>>
|
||||
.Create(Zones.Select(z => new Vertex2<GalaxyZone>(z.Position, z)).ToList(), 1e-7f);
|
||||
var links = new HashSet<(GalaxyZone, GalaxyZone)>();
|
||||
foreach (var cell in triangulation.Cells)
|
||||
{
|
||||
if (!links.Contains((cell.Vertices[0].StoredObject, cell.Vertices[1].StoredObject)) &&
|
||||
!links.Contains((cell.Vertices[1].StoredObject, cell.Vertices[0].StoredObject)))
|
||||
links.Add((cell.Vertices[0].StoredObject, cell.Vertices[1].StoredObject));
|
||||
if (!links.Contains((cell.Vertices[1].StoredObject, cell.Vertices[2].StoredObject)) &&
|
||||
!links.Contains((cell.Vertices[2].StoredObject, cell.Vertices[1].StoredObject)))
|
||||
links.Add((cell.Vertices[1].StoredObject, cell.Vertices[2].StoredObject));
|
||||
if (!links.Contains((cell.Vertices[0].StoredObject, cell.Vertices[2].StoredObject)) &&
|
||||
!links.Contains((cell.Vertices[2].StoredObject, cell.Vertices[0].StoredObject)))
|
||||
links.Add((cell.Vertices[0].StoredObject, cell.Vertices[2].StoredObject));
|
||||
}
|
||||
// The Delaunay triangulation connects adjacent sectors, every zone among them
|
||||
var links = Delaunay.Edges(Zones.Select(z => z.Position).ToArray())
|
||||
.Select(edge => (Zones[edge.a], Zones[edge.b]))
|
||||
.ToList();
|
||||
|
||||
progressCallback?.Invoke("Eliminating Zone Links");
|
||||
if (progressCallback != null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
|
||||
|
|
|
|||
86
Assets/Scripts/ServerShared/GalaxyStage.cs
Normal file
86
Assets/Scripts/ServerShared/GalaxyStage.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
using static CultMath.math;
|
||||
using Random = CultMath.Random;
|
||||
|
||||
// The galaxy half of a scenario's vocabulary (docs/scenarios-cut.md, R.4): the two kinds of galaxy the game generates,
|
||||
// at the scenario's seed. The menu hands it the generation inputs; RunStart.Generate sets the seed and hands it to
|
||||
// Scenario.Generate. The background's noise position derives from the same seed, so a fixed seed fixes the whole
|
||||
// galaxy, and a clock seed makes every launch a new one.
|
||||
public sealed class GalaxyStage
|
||||
{
|
||||
private readonly SectorGenerationSettings _sector;
|
||||
private readonly SectorBackgroundSettings _sectorBackground;
|
||||
private readonly TutorialGenerationSettings _prelude;
|
||||
private readonly SectorBackgroundSettings _preludeBackground;
|
||||
private readonly NameGeneratorSettings _names;
|
||||
private readonly CultCache _cache;
|
||||
private readonly PlayerSettings _playerSettings;
|
||||
private readonly DirectoryInfo _narrativeDirectory;
|
||||
private readonly Action<string> _log;
|
||||
private readonly Action<string> _progress;
|
||||
private readonly Func<uint> _clock;
|
||||
|
||||
public GalaxyStage(
|
||||
SectorGenerationSettings sector,
|
||||
SectorBackgroundSettings sectorBackground,
|
||||
TutorialGenerationSettings prelude,
|
||||
SectorBackgroundSettings preludeBackground,
|
||||
NameGeneratorSettings names,
|
||||
CultCache cache,
|
||||
PlayerSettings playerSettings,
|
||||
DirectoryInfo narrativeDirectory,
|
||||
Action<string> log,
|
||||
Action<string> progress = null,
|
||||
Func<uint> clock = null)
|
||||
{
|
||||
_sector = sector;
|
||||
_sectorBackground = sectorBackground;
|
||||
_prelude = prelude;
|
||||
_preludeBackground = preludeBackground;
|
||||
_names = names;
|
||||
_cache = cache;
|
||||
_playerSettings = playerSettings;
|
||||
_narrativeDirectory = narrativeDirectory;
|
||||
_log = log;
|
||||
_progress = progress;
|
||||
_clock = clock ?? (() => (uint) (DateTime.Now.Ticks % uint.MaxValue));
|
||||
}
|
||||
|
||||
// The seed this generation runs at, never zero: the scenario's, or the clock's when the scenario's is zero.
|
||||
public uint Seed { get; private set; }
|
||||
|
||||
internal void SeedFrom(Scenario scenario)
|
||||
{
|
||||
var clock = _clock();
|
||||
Seed = scenario.Seed != 0 ? scenario.Seed : clock != 0 ? clock : 1;
|
||||
}
|
||||
|
||||
// A main galaxy: the sector settings, factions drawn from the catalog, an exit to reach.
|
||||
public Galaxy Main()
|
||||
{
|
||||
var background = Copy(_sectorBackground);
|
||||
var random = new Random(Seed);
|
||||
background.NoisePosition = random.NextFloat() * 1000;
|
||||
return new Galaxy(_sector, background, _names, _cache, _log, _progress, Seed);
|
||||
}
|
||||
|
||||
// A prelude galaxy: the tutorial settings and their fixed factions, centred in dense cloud.
|
||||
public Galaxy Prelude()
|
||||
{
|
||||
var background = Copy(_preludeBackground);
|
||||
var random = new Random(Seed);
|
||||
var iteration = 1;
|
||||
do
|
||||
{
|
||||
background.NoisePosition = random.NextFloat() * 1000;
|
||||
_progress?.Invoke($"Finding Galaxy Position: iteration {iteration++}");
|
||||
} while (background.CloudDensity(float2(.5f)) < .5f);
|
||||
return new Galaxy(_prelude, background, _names, _cache, _playerSettings, _narrativeDirectory, _log, _progress, Seed);
|
||||
}
|
||||
|
||||
private static SectorBackgroundSettings Copy(SectorBackgroundSettings settings) =>
|
||||
MessagePackSerializer.Deserialize<SectorBackgroundSettings>(MessagePackSerializer.Serialize(settings));
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3969283304824fe4a8eca1eb522fd3a7
|
||||
guid: f5cf458f47074786bf11e72b5d93c545
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -14,14 +14,17 @@ public class MarkovNameGenerator
|
|||
{
|
||||
private Dictionary<string, List<char>> _chains = new Dictionary<string, List<char>>();
|
||||
private List<string> _samples = new List<string>();
|
||||
private static List<string> _used = new List<string>();
|
||||
// The names already given, shared by every generator that must not repeat another's: a galaxy passes one set to
|
||||
// all its factions' generators. Never process-wide, so one galaxy's names cannot change the next galaxy's.
|
||||
private readonly HashSet<string> _used;
|
||||
private int _order;
|
||||
private int _minLength;
|
||||
private int _maxLength;
|
||||
private Random _random;
|
||||
|
||||
public MarkovNameGenerator(ref Random random, IEnumerable<string> sampleNames, NameGeneratorSettings settings)
|
||||
public MarkovNameGenerator(ref Random random, IEnumerable<string> sampleNames, NameGeneratorSettings settings, HashSet<string> used)
|
||||
{
|
||||
_used = used;
|
||||
var names = new HashSet<string>();
|
||||
foreach (var line in sampleNames)
|
||||
{
|
||||
|
|
@ -41,6 +44,7 @@ public class MarkovNameGenerator
|
|||
|
||||
public MarkovNameGenerator(ref Random random, IEnumerable<string> sampleNames, int order, int minLength, int maxLength)
|
||||
{
|
||||
_used = new HashSet<string>();
|
||||
_random = random;
|
||||
|
||||
_order = max(1, order);
|
||||
|
|
@ -113,12 +117,6 @@ public class MarkovNameGenerator
|
|||
}
|
||||
}
|
||||
|
||||
//Reset the used names
|
||||
public void Reset()
|
||||
{
|
||||
_used.Clear();
|
||||
}
|
||||
|
||||
//Get a random letter from the chain
|
||||
private char GetLetter(string token)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
using System.Collections;
|
||||
using System.Linq;
|
||||
using MIConvexHull;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
|
||||
/// <summary>
|
||||
/// A vertex is a simple class that stores the postion of a point, node or vertex.
|
||||
/// </summary>
|
||||
public class Cell2 : TriangulationCell<Vertex2, Cell2>
|
||||
{
|
||||
float2? circumCenter, centroid;
|
||||
|
||||
public float2 Circumcenter {
|
||||
get {
|
||||
circumCenter = circumCenter ?? GetCircumcenter();
|
||||
return circumCenter.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public float2 Centroid {
|
||||
get {
|
||||
centroid = centroid ?? GetCentroid();
|
||||
return centroid.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public Cell2 ()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
double LengthSquared (float[] v)
|
||||
{
|
||||
float norm = 0;
|
||||
for (int i = 0; i < v.Length; i++) {
|
||||
var t = v [i];
|
||||
norm += t * t;
|
||||
}
|
||||
return norm;
|
||||
}
|
||||
|
||||
float Determinant(float[,] m)
|
||||
{
|
||||
float fCofactor00 = m[1,1] * m[2,2] - m[1,2] * m[2,1];
|
||||
float fCofactor10 = m[1,2] * m[2,0] - m[1,0] * m[2,2];
|
||||
float fCofactor20 = m[1,0] * m[2,1] - m[1,1] * m[2,0];
|
||||
|
||||
float fDet = m[0,0] * fCofactor00 + m[0,1] * fCofactor10 + m[0,2] * fCofactor20;
|
||||
|
||||
return fDet;
|
||||
}
|
||||
|
||||
float2 GetCircumcenter ()
|
||||
{
|
||||
// From MathWorld: http://mathworld.wolfram.com/Circumcircle.html
|
||||
|
||||
var points = Vertices;
|
||||
|
||||
float[,] m = new float[3, 3];
|
||||
|
||||
// x, y, 1
|
||||
for (int i = 0; i < 3; i++) {
|
||||
m[i, 0] = points[i].x;
|
||||
m[i, 1] = points[i].y;
|
||||
m[i, 2] = 1;
|
||||
}
|
||||
var a = Determinant(m);
|
||||
|
||||
// size, y, 1
|
||||
for (int i = 0; i < 3; i++) {
|
||||
m[i, 0] = lengthsq(points[i].StoredPosition);
|
||||
}
|
||||
var dx = -Determinant(m);
|
||||
|
||||
// size, x, 1
|
||||
for (int i = 0; i < 3; i++) {
|
||||
m[i, 1] = points[i].x;
|
||||
}
|
||||
var dy = Determinant(m);
|
||||
|
||||
// size, x, y
|
||||
//for (int i = 0; i < 3; i++) {
|
||||
// m[i, 2] = points[i].y;
|
||||
//}
|
||||
//var c = -Det(m);
|
||||
|
||||
var s = -1.0f / (2.0f * a);
|
||||
//var r = System.Math.Abs(s) * System.Math.Sqrt(dx * dx + dy * dy - 4 * a * c);
|
||||
|
||||
return new float2(s * dx, s * dy);
|
||||
}
|
||||
|
||||
float2 GetCentroid ()
|
||||
{
|
||||
return new float2(Vertices.Select(v => v.Position[0]).Average(), Vertices.Select(v => v.Position[1]).Average());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
using System.Collections;
|
||||
using System.Linq;
|
||||
using MIConvexHull;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
|
||||
/// <summary>
|
||||
/// A vertex is a simple class that stores the postion of a point, node or vertex.
|
||||
/// </summary>
|
||||
public class Cell2<T> : TriangulationCell<Vertex2<T>, Cell2<T>>
|
||||
{
|
||||
float2? circumCenter, centroid;
|
||||
|
||||
public float2 Circumcenter {
|
||||
get {
|
||||
circumCenter = circumCenter ?? GetCircumcenter();
|
||||
return circumCenter.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public float2 Centroid {
|
||||
get {
|
||||
centroid = centroid ?? GetCentroid();
|
||||
return centroid.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public Cell2 ()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
double LengthSquared (float[] v)
|
||||
{
|
||||
float norm = 0;
|
||||
for (int i = 0; i < v.Length; i++) {
|
||||
var t = v [i];
|
||||
norm += t * t;
|
||||
}
|
||||
return norm;
|
||||
}
|
||||
|
||||
float Determinant(float[,] m)
|
||||
{
|
||||
float fCofactor00 = m[1,1] * m[2,2] - m[1,2] * m[2,1];
|
||||
float fCofactor10 = m[1,2] * m[2,0] - m[1,0] * m[2,2];
|
||||
float fCofactor20 = m[1,0] * m[2,1] - m[1,1] * m[2,0];
|
||||
|
||||
float fDet = m[0,0] * fCofactor00 + m[0,1] * fCofactor10 + m[0,2] * fCofactor20;
|
||||
|
||||
return fDet;
|
||||
}
|
||||
|
||||
float2 GetCircumcenter ()
|
||||
{
|
||||
// From MathWorld: http://mathworld.wolfram.com/Circumcircle.html
|
||||
|
||||
var points = Vertices;
|
||||
|
||||
float[,] m = new float[3, 3];
|
||||
|
||||
// x, y, 1
|
||||
for (int i = 0; i < 3; i++) {
|
||||
m[i, 0] = points[i].x;
|
||||
m[i, 1] = points[i].y;
|
||||
m[i, 2] = 1;
|
||||
}
|
||||
var a = Determinant(m);
|
||||
|
||||
// size, y, 1
|
||||
for (int i = 0; i < 3; i++) {
|
||||
m[i, 0] = lengthsq(points[i].StoredPosition);
|
||||
}
|
||||
var dx = -Determinant(m);
|
||||
|
||||
// size, x, 1
|
||||
for (int i = 0; i < 3; i++) {
|
||||
m[i, 1] = points[i].x;
|
||||
}
|
||||
var dy = Determinant(m);
|
||||
|
||||
// size, x, y
|
||||
//for (int i = 0; i < 3; i++) {
|
||||
// m[i, 2] = points[i].y;
|
||||
//}
|
||||
//var c = -Det(m);
|
||||
|
||||
var s = -1.0f / (2.0f * a);
|
||||
//var r = System.Math.Abs(s) * System.Math.Sqrt(dx * dx + dy * dy - 4 * a * c);
|
||||
|
||||
return new float2(s * dx, s * dy);
|
||||
}
|
||||
|
||||
float2 GetCentroid ()
|
||||
{
|
||||
return new float2(Vertices.Select(v => v.Position[0]).Average(), Vertices.Select(v => v.Position[1]).Average());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ea854ab9e5204f92828e92ffb1244964
|
||||
timeCreated: 1615420193
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
using System;
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
internal static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// The default plane distance tolerance
|
||||
/// </summary>
|
||||
internal const float DefaultPlaneDistanceTolerance = 1e-10f;
|
||||
/// <summary>
|
||||
/// The starting delta dot product in simplex
|
||||
/// </summary>
|
||||
internal const float StartingDeltaDotProductInSimplex = 0.5f;
|
||||
/// <summary>
|
||||
/// The connector table size
|
||||
/// </summary>
|
||||
internal const int ConnectorTableSize = 2017;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory class for computing convex hulls.
|
||||
/// </summary>
|
||||
public static class ConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a convex hull of the input data.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TFace">The type of the t face.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>
|
||||
/// ConvexHull<TVertex, TFace>.
|
||||
/// </returns>
|
||||
public static ConvexHull<TVertex, TFace> Create<TVertex, TFace>(IList<TVertex> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
where TVertex : IVertex
|
||||
where TFace : ConvexFace<TVertex, TFace>, new()
|
||||
{
|
||||
return ConvexHull<TVertex, TFace>.Create(data, PlaneDistanceTolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a convex hull of the input data.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>
|
||||
/// ConvexHull<TVertex, DefaultConvexFace<TVertex>>.
|
||||
/// </returns>
|
||||
public static ConvexHull<TVertex, DefaultConvexFace<TVertex>> Create<TVertex>(IList<TVertex> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
where TVertex : IVertex
|
||||
{
|
||||
return ConvexHull<TVertex, DefaultConvexFace<TVertex>>.Create(data, PlaneDistanceTolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a convex hull of the input data.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>
|
||||
/// ConvexHull<DefaultVertex, DefaultConvexFace<DefaultVertex>>.
|
||||
/// </returns>
|
||||
public static ConvexHull<DefaultVertex, DefaultConvexFace<DefaultVertex>> Create(IList<float[]> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
{
|
||||
var points = data.Select(p => new DefaultVertex { Position = p }).ToList();
|
||||
return ConvexHull<DefaultVertex, DefaultConvexFace<DefaultVertex>>.Create(points, PlaneDistanceTolerance);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Representation of a convex hull.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TFace">The type of the t face.</typeparam>
|
||||
public class ConvexHull<TVertex, TFace>
|
||||
where TVertex : IVertex
|
||||
where TFace : ConvexFace<TVertex, TFace>, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Can only be created using a factory method.
|
||||
/// </summary>
|
||||
internal ConvexHull()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Points of the convex hull.
|
||||
/// </summary>
|
||||
/// <value>The points.</value>
|
||||
public IEnumerable<TVertex> Points { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Faces of the convex hull.
|
||||
/// </summary>
|
||||
/// <value>The faces.</value>
|
||||
public IEnumerable<TFace> Faces { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates the convex hull.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance.</param>
|
||||
/// <returns>
|
||||
/// ConvexHull<TVertex, TFace>.
|
||||
/// </returns>
|
||||
/// <exception cref="System.ArgumentNullException">The supplied data is null.</exception>
|
||||
/// <exception cref="ArgumentNullException">data</exception>
|
||||
public static ConvexHull<TVertex, TFace> Create(IList<TVertex> data, float PlaneDistanceTolerance)
|
||||
{
|
||||
if (data == null) throw new ArgumentNullException("The supplied data is null.");
|
||||
return ConvexHullAlgorithm.GetConvexHull<TVertex, TFace>(data, PlaneDistanceTolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a6e88469e3b81a541a0bc8a66e60d0c7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,304 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
using System;
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// A more lightweight alternative to List of T.
|
||||
/// On clear, only resets the count and does not clear the references
|
||||
/// => this works because of the ObjectManager.
|
||||
/// Includes a stack functionality.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
internal class SimpleList<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The capacity
|
||||
/// </summary>
|
||||
private int capacity;
|
||||
|
||||
/// <summary>
|
||||
/// The count
|
||||
/// </summary>
|
||||
public int Count;
|
||||
/// <summary>
|
||||
/// The items
|
||||
/// </summary>
|
||||
private T[] items;
|
||||
|
||||
/// <summary>
|
||||
/// Get the i-th element.
|
||||
/// </summary>
|
||||
/// <param name="i">The i.</param>
|
||||
/// <returns>T.</returns>
|
||||
public T this[int i]
|
||||
{
|
||||
get { return items[i]; }
|
||||
set { items[i] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Size matters.
|
||||
/// </summary>
|
||||
private void EnsureCapacity()
|
||||
{
|
||||
if (capacity == 0)
|
||||
{
|
||||
capacity = 32;
|
||||
items = new T[32];
|
||||
}
|
||||
else
|
||||
{
|
||||
var newItems = new T[capacity*2];
|
||||
Array.Copy(items, newItems, capacity);
|
||||
capacity = 2*capacity;
|
||||
items = newItems;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a vertex to the buffer.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Add(T item)
|
||||
{
|
||||
if (Count + 1 > capacity) EnsureCapacity();
|
||||
items[Count++] = item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the value to the back of the list.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public void Push(T item)
|
||||
{
|
||||
if (Count + 1 > capacity) EnsureCapacity();
|
||||
items[Count++] = item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pops the last value from the list.
|
||||
/// </summary>
|
||||
/// <returns>T.</returns>
|
||||
public T Pop()
|
||||
{
|
||||
return items[--Count];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Count to 0, otherwise does nothing.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Class IndexBuffer.
|
||||
/// A fancy name for a list of integers.
|
||||
/// </summary>
|
||||
internal class IndexBuffer : SimpleList<int>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A priority based linked list.
|
||||
/// </summary>
|
||||
internal sealed class FaceList
|
||||
{
|
||||
/// <summary>
|
||||
/// The last
|
||||
/// </summary>
|
||||
private ConvexFaceInternal last;
|
||||
|
||||
/// <summary>
|
||||
/// Get the first element.
|
||||
/// </summary>
|
||||
/// <value>The first.</value>
|
||||
public ConvexFaceInternal First { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the element to the beginning.
|
||||
/// </summary>
|
||||
/// <param name="face">The face.</param>
|
||||
private void AddFirst(ConvexFaceInternal face)
|
||||
{
|
||||
face.InList = true;
|
||||
First.Previous = face;
|
||||
face.Next = First;
|
||||
First = face;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a face to the list.
|
||||
/// </summary>
|
||||
/// <param name="face">The face.</param>
|
||||
public void Add(ConvexFaceInternal face)
|
||||
{
|
||||
if (face.InList)
|
||||
{
|
||||
if (First.VerticesBeyond.Count < face.VerticesBeyond.Count)
|
||||
{
|
||||
Remove(face);
|
||||
AddFirst(face);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
face.InList = true;
|
||||
|
||||
if (First != null && First.VerticesBeyond.Count < face.VerticesBeyond.Count)
|
||||
{
|
||||
First.Previous = face;
|
||||
face.Next = First;
|
||||
First = face;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (last != null)
|
||||
{
|
||||
last.Next = face;
|
||||
}
|
||||
face.Previous = last;
|
||||
last = face;
|
||||
if (First == null)
|
||||
{
|
||||
First = face;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the element from the list.
|
||||
/// </summary>
|
||||
/// <param name="face">The face.</param>
|
||||
public void Remove(ConvexFaceInternal face)
|
||||
{
|
||||
if (!face.InList) return;
|
||||
|
||||
face.InList = false;
|
||||
|
||||
if (face.Previous != null)
|
||||
{
|
||||
face.Previous.Next = face.Next;
|
||||
}
|
||||
else if ( /*first == face*/ face.Previous == null)
|
||||
{
|
||||
First = face.Next;
|
||||
}
|
||||
|
||||
if (face.Next != null)
|
||||
{
|
||||
face.Next.Previous = face.Previous;
|
||||
}
|
||||
else if ( /*last == face*/ face.Next == null)
|
||||
{
|
||||
last = face.Previous;
|
||||
}
|
||||
|
||||
face.Next = null;
|
||||
face.Previous = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connector list.
|
||||
/// </summary>
|
||||
internal sealed class ConnectorList
|
||||
{
|
||||
/// <summary>
|
||||
/// The last
|
||||
/// </summary>
|
||||
private FaceConnector last;
|
||||
|
||||
/// <summary>
|
||||
/// Get the first element.
|
||||
/// </summary>
|
||||
/// <value>The first.</value>
|
||||
public FaceConnector First { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the element to the beginning.
|
||||
/// </summary>
|
||||
/// <param name="connector">The connector.</param>
|
||||
private void AddFirst(FaceConnector connector)
|
||||
{
|
||||
First.Previous = connector;
|
||||
connector.Next = First;
|
||||
First = connector;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a face to the list.
|
||||
/// </summary>
|
||||
/// <param name="element">The element.</param>
|
||||
public void Add(FaceConnector element)
|
||||
{
|
||||
if (last != null)
|
||||
{
|
||||
last.Next = element;
|
||||
}
|
||||
element.Previous = last;
|
||||
last = element;
|
||||
if (First == null)
|
||||
{
|
||||
First = element;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the element from the list.
|
||||
/// </summary>
|
||||
/// <param name="connector">The connector.</param>
|
||||
public void Remove(FaceConnector connector)
|
||||
{
|
||||
if (connector.Previous != null)
|
||||
{
|
||||
connector.Previous.Next = connector.Next;
|
||||
}
|
||||
else if ( /*first == face*/ connector.Previous == null)
|
||||
{
|
||||
First = connector.Next;
|
||||
}
|
||||
|
||||
if (connector.Next != null)
|
||||
{
|
||||
connector.Next.Previous = connector.Previous;
|
||||
}
|
||||
else if ( /*last == face*/ connector.Next == null)
|
||||
{
|
||||
last = connector.Previous;
|
||||
}
|
||||
|
||||
connector.Next = null;
|
||||
connector.Previous = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// A convex face representation containing adjacency information.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TFace">The type of the t face.</typeparam>
|
||||
public abstract class ConvexFace<TVertex, TFace>
|
||||
where TVertex : IVertex
|
||||
where TFace : ConvexFace<TVertex, TFace>
|
||||
{
|
||||
/// <summary>
|
||||
/// Adjacency. Array of length "dimension".
|
||||
/// If F = Adjacency[i] then the vertices shared with F are Vertices[j] where j != i.
|
||||
/// In the context of triangulation, can be null (indicates the cell is at boundary).
|
||||
/// </summary>
|
||||
/// <value>The adjacency.</value>
|
||||
public TFace[] Adjacency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The vertices stored in clockwise order for dimensions 2 - 4, in higher dimensions the order is arbitrary.
|
||||
/// Unless I accidentally switch some index somewhere in which case the order is CCW. Either way, it is consistent.
|
||||
/// 3D Normal = (V[1] - V[0]) x (V[2] - V[1]).
|
||||
/// </summary>
|
||||
/// <value>The vertices.</value>
|
||||
public TVertex[] Vertices { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The normal vector of the face. Null if used in triangulation.
|
||||
/// </summary>
|
||||
/// <value>The normal.</value>
|
||||
public float[] Normal { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A default convex face representation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
public class DefaultConvexFace<TVertex> : ConvexFace<TVertex, DefaultConvexFace<TVertex>>
|
||||
where TVertex : IVertex
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5db1a96860ec6d949930695ae9bedcc0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,721 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/*
|
||||
* This part of the implementation handles initialization of the convex hull algorithm:
|
||||
* - Constructor & Process initiation
|
||||
* - Determine the dimension by looking at length of Position vector of 10 random data points from the input.
|
||||
* - Identify bounding box points in each direction.
|
||||
* - Pick (Dimension + 1) points from the extremes and construct the initial simplex.
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Class ConvexHullAlgorithm.
|
||||
/// </summary>
|
||||
internal partial class ConvexHullAlgorithm
|
||||
{
|
||||
#region Starting functions and constructor
|
||||
/// <summary>
|
||||
/// The main function for the Convex Hull algorithm. It is static, but it creates
|
||||
/// an instantiation of this class in order to allow for parallel execution.
|
||||
/// Following this simple function, the constructor and the main function "FindConvexHull" is listed.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the vertices in the data.</typeparam>
|
||||
/// <typeparam name="TFace">The desired type of the faces.</typeparam>
|
||||
/// <param name="data">The data is the vertices as a collection of IVertices.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance.</param>
|
||||
/// <returns>
|
||||
/// MIConvexHull.ConvexHull<TVertex, TFace>.
|
||||
/// </returns>
|
||||
internal static ConvexHull<TVertex, TFace> GetConvexHull<TVertex, TFace>(IList<TVertex> data,
|
||||
float PlaneDistanceTolerance)
|
||||
where TFace : ConvexFace<TVertex, TFace>, new()
|
||||
where TVertex : IVertex
|
||||
{
|
||||
var ch = new ConvexHullAlgorithm(data.Cast<IVertex>().ToArray(), false, PlaneDistanceTolerance);
|
||||
ch.GetConvexHull();
|
||||
|
||||
if (ch.NumOfDimensions == 2) return ch.Return2DResultInOrder<TVertex, TFace>(data);
|
||||
return new ConvexHull<TVertex, TFace>
|
||||
{
|
||||
Points = ch.GetHullVertices(data),
|
||||
Faces = ch.GetConvexFaces<TVertex, TFace>()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConvexHullAlgorithm" /> class.
|
||||
/// </summary>
|
||||
/// <param name="vertices">The vertices.</param>
|
||||
/// <param name="lift">if set to <c>true</c> [lift].</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance.</param>
|
||||
/// <exception cref="System.InvalidOperationException">Dimension of the input must be 2 or greater.</exception>
|
||||
/// <exception cref="System.ArgumentException">There are too few vertices (m) for the n-dimensional space. (m must be greater +
|
||||
/// than the n, but m is + NumberOfVertices + and n is + NumOfDimensions</exception>
|
||||
/// <exception cref="InvalidOperationException">PointTranslationGenerator cannot be null if PointTranslationType is enabled.
|
||||
/// or
|
||||
/// Dimension of the input must be 2 or greater.</exception>
|
||||
/// <exception cref="ArgumentException">There are too few vertices (m) for the n-dimensional space. (m must be greater " +
|
||||
/// "than the n, but m is " + NumberOfVertices + " and n is " + Dimension</exception>
|
||||
private ConvexHullAlgorithm(IVertex[] vertices, bool lift, float PlaneDistanceTolerance)
|
||||
{
|
||||
IsLifted = lift;
|
||||
Vertices = vertices;
|
||||
NumberOfVertices = vertices.Length;
|
||||
|
||||
NumOfDimensions = DetermineDimension();
|
||||
if (IsLifted) NumOfDimensions++;
|
||||
if (NumOfDimensions < 2) throw new InvalidOperationException("Dimension of the input must be 2 or greater.");
|
||||
if (NumberOfVertices <= NumOfDimensions)
|
||||
throw new ArgumentException(
|
||||
"There are too few vertices (m) for the n-dimensional space. (m must be greater " +
|
||||
"than the n, but m is " + NumberOfVertices + " and n is " + NumOfDimensions);
|
||||
this.PlaneDistanceTolerance = PlaneDistanceTolerance;
|
||||
UnprocessedFaces = new FaceList();
|
||||
ConvexFaces = new IndexBuffer();
|
||||
|
||||
FacePool = new ConvexFaceInternal[(NumOfDimensions + 1) * 10]; // must be initialized before object manager
|
||||
AffectedFaceFlags = new bool[(NumOfDimensions + 1) * 10];
|
||||
ObjectManager = new ObjectManager(this);
|
||||
|
||||
Center = new float[NumOfDimensions];
|
||||
TraverseStack = new IndexBuffer();
|
||||
UpdateBuffer = new int[NumOfDimensions];
|
||||
UpdateIndices = new int[NumOfDimensions];
|
||||
EmptyBuffer = new IndexBuffer();
|
||||
AffectedFaceBuffer = new IndexBuffer();
|
||||
ConeFaceBuffer = new SimpleList<DeferredFace>();
|
||||
SingularVertices = new HashSet<int>();
|
||||
BeyondBuffer = new IndexBuffer();
|
||||
|
||||
ConnectorTable = new ConnectorList[Constants.ConnectorTableSize];
|
||||
for (var i = 0; i < Constants.ConnectorTableSize; i++) ConnectorTable[i] = new ConnectorList();
|
||||
|
||||
VertexVisited = new bool[NumberOfVertices];
|
||||
Positions = new float[NumberOfVertices * NumOfDimensions];
|
||||
boundingBoxPoints = new List<int>[NumOfDimensions];
|
||||
minima = new float[NumOfDimensions];
|
||||
maxima = new float[NumOfDimensions];
|
||||
mathHelper = new MathHelper(NumOfDimensions, Positions);
|
||||
}
|
||||
/// <summary>
|
||||
/// Check the dimensionality of the input data.
|
||||
/// </summary>
|
||||
/// <returns>System.Int32.</returns>
|
||||
/// <exception cref="ArgumentException">Invalid input data (non-uniform dimension).</exception>
|
||||
private int DetermineDimension()
|
||||
{
|
||||
var r = new Random();
|
||||
var dimensions = new List<int>();
|
||||
for (var i = 0; i < 10; i++)
|
||||
dimensions.Add(Vertices[r.Next(NumberOfVertices)].Position.Length);
|
||||
var dimension = dimensions.Min();
|
||||
if (dimension != dimensions.Max())
|
||||
throw new ArgumentException("Invalid input data (non-uniform dimension).");
|
||||
return dimension;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets/calculates the convex hull. This is
|
||||
/// </summary>
|
||||
private void GetConvexHull()
|
||||
{
|
||||
// accessing a 1D array is quicker than a jagged array, so the first step is to make this array
|
||||
SerializeVerticesToPositions();
|
||||
// next the bounding box extremes are found. This is used to shift, scale and find the starting simplex.
|
||||
FindBoundingBoxPoints();
|
||||
// the positions are shifted to avoid divide by zero problems
|
||||
// and if Delaunay or Voronoi, then the parabola terms are scaled back to match the size of the other coords
|
||||
ShiftAndScalePositions();
|
||||
// Find the (dimension+1) initial points and create the simplexes.
|
||||
CreateInitialSimplex();
|
||||
|
||||
// Now, the main loop. These initial faces of a simplex are replaced and expanded
|
||||
// outwards to make the convex hull and faces.
|
||||
while (UnprocessedFaces.First != null)
|
||||
{
|
||||
var currentFace = UnprocessedFaces.First;
|
||||
CurrentVertex = currentFace.FurthestVertex;
|
||||
|
||||
UpdateCenter();
|
||||
|
||||
// The affected faces get tagged
|
||||
TagAffectedFaces(currentFace);
|
||||
|
||||
// Create the cone from the currentVertex and the affected faces horizon.
|
||||
if (!SingularVertices.Contains(CurrentVertex) && CreateCone()) CommitCone();
|
||||
else HandleSingular();
|
||||
|
||||
// Need to reset the tags
|
||||
var count = AffectedFaceBuffer.Count;
|
||||
for (var i = 0; i < count; i++) AffectedFaceFlags[AffectedFaceBuffer[i]] = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the vertices into the 1D array, Positions. The 1D array has much quicker access in C#.
|
||||
/// </summary>
|
||||
private void SerializeVerticesToPositions()
|
||||
{
|
||||
var index = 0;
|
||||
if (IsLifted) // "Lifted" means that the last dimension is the sum of the squares of the others.
|
||||
{
|
||||
foreach (var v in Vertices)
|
||||
{
|
||||
var parabolaTerm = 0.0f; // the lifted term is a sum of squares.
|
||||
var origNumDim = NumOfDimensions - 1;
|
||||
for (var i = 0; i < origNumDim; i++)
|
||||
{
|
||||
var coordinate = v.Position[i];
|
||||
Positions[index++] = coordinate;
|
||||
parabolaTerm += coordinate * coordinate;
|
||||
}
|
||||
Positions[index++] = parabolaTerm;
|
||||
}
|
||||
}
|
||||
else
|
||||
foreach (var v in Vertices)
|
||||
{
|
||||
for (var i = 0; i < NumOfDimensions; i++)
|
||||
Positions[index++] = v.Position[i];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Finds the bounding box points.
|
||||
/// </summary>
|
||||
private void FindBoundingBoxPoints()
|
||||
{
|
||||
indexOfDimensionWithLeastExtremes = -1;
|
||||
var minNumExtremes = int.MaxValue;
|
||||
for (var i = 0; i < NumOfDimensions; i++)
|
||||
{
|
||||
var minIndices = new List<int>();
|
||||
var maxIndices = new List<int>();
|
||||
float min = float.PositiveInfinity, max = float.NegativeInfinity;
|
||||
for (var j = 0; j < NumberOfVertices; j++)
|
||||
{
|
||||
var v = GetCoordinate(j, i);
|
||||
var difference = min - v;
|
||||
if (difference >= PlaneDistanceTolerance)
|
||||
{
|
||||
// you found a better solution than before, clear out the list and store new value
|
||||
min = v;
|
||||
minIndices.Clear();
|
||||
minIndices.Add(j);
|
||||
}
|
||||
else if (difference > 0)
|
||||
{
|
||||
// you found a solution slightly better than before, clear out those that are no longer on the list and store new value
|
||||
min = v;
|
||||
minIndices.RemoveAll(index => min - GetCoordinate(index, i) > PlaneDistanceTolerance);
|
||||
minIndices.Add(j);
|
||||
}
|
||||
else if (difference > -PlaneDistanceTolerance)
|
||||
{
|
||||
//same or almost as good as current limit, so store it
|
||||
minIndices.Add(j);
|
||||
}
|
||||
difference = v - max;
|
||||
if (difference >= PlaneDistanceTolerance)
|
||||
{
|
||||
// you found a better solution than before, clear out the list and store new value
|
||||
max = v;
|
||||
maxIndices.Clear();
|
||||
maxIndices.Add(j);
|
||||
}
|
||||
else if (difference > 0)
|
||||
{
|
||||
// you found a solution slightly better than before, clear out those that are no longer on the list and store new value
|
||||
max = v;
|
||||
maxIndices.RemoveAll(index => min - GetCoordinate(index, i) > PlaneDistanceTolerance);
|
||||
maxIndices.Add(j);
|
||||
}
|
||||
else if (difference > -PlaneDistanceTolerance)
|
||||
{
|
||||
//same or almost as good as current limit, so store it
|
||||
maxIndices.Add(j);
|
||||
}
|
||||
}
|
||||
minima[i] = min;
|
||||
maxima[i] = max;
|
||||
minIndices.AddRange(maxIndices);
|
||||
if (minIndices.Count < minNumExtremes)
|
||||
{
|
||||
minNumExtremes = minIndices.Count;
|
||||
indexOfDimensionWithLeastExtremes = i;
|
||||
}
|
||||
boundingBoxPoints[i] = minIndices;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shifts and scales the Positions to avoid future errors. This does not alter the original data.
|
||||
/// </summary>
|
||||
private void ShiftAndScalePositions()
|
||||
{
|
||||
var positionsLength = Positions.Length;
|
||||
if (IsLifted)
|
||||
{
|
||||
var origNumDim = NumOfDimensions - 1;
|
||||
var parabolaScale = 2 / (minima.Sum(x => Math.Abs(x)) + maxima.Sum(x => Math.Abs(x))
|
||||
- Math.Abs(maxima[origNumDim]) - Math.Abs(minima[origNumDim]));
|
||||
// the parabola scale is 1 / average of the sum of the other dimensions.
|
||||
// multiplying this by the parabola will scale it back to be on near similar size to the
|
||||
// other dimensions. Without this, the term is much larger than the others, which causes
|
||||
// problems for roundoff error and finding the normal of faces.
|
||||
minima[origNumDim] *= parabolaScale; // change the extreme values as well
|
||||
maxima[origNumDim] *= parabolaScale;
|
||||
// it is done here because
|
||||
for (int i = origNumDim; i < positionsLength; i += NumOfDimensions)
|
||||
Positions[i] *= parabolaScale;
|
||||
}
|
||||
var shiftAmount = new float[NumOfDimensions];
|
||||
for (int i = 0; i < NumOfDimensions; i++)
|
||||
// now the entire model is shifted to all positive numbers...plus some more.
|
||||
// why?
|
||||
// 1) to avoid dealing with a point at the origin {0,0,...,0} which causes problems
|
||||
// for future normal finding
|
||||
// 2) note that weird shift that is used (max - min - min). This is to avoid scaling
|
||||
// issues. this shift means that the minima in a dimension will always be a positive
|
||||
// number (no points at zero), and the minima [in a given dimension] will always be
|
||||
// half of the maxima. 'Half' is much preferred to 'thousands of times'
|
||||
// Think of the first term as the range (max - min), then the second term avoids cases
|
||||
// where there are both positive and negative numbers.
|
||||
if (maxima[i] == minima[i]) shiftAmount[i] = 0.0f;
|
||||
else shiftAmount[i] = (maxima[i] - minima[i]) - minima[i];
|
||||
for (int i = 0; i < positionsLength; i++)
|
||||
Positions[i] += shiftAmount[i % NumOfDimensions];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the (dimension+1) initial points and create the simplexes.
|
||||
/// Creates the initial simplex of n+1 vertices by using points from the bounding box.
|
||||
/// Special care is taken to ensure that the vertices chosen do not result in a degenerate shape
|
||||
/// where vertices are collinear (co-planar, etc). This would technically be resolved when additional
|
||||
/// vertices are checked in the main loop, but: 1) a degenerate simplex would not eliminate any other
|
||||
/// vertices (thus no savings there), 2) the creation of the face normal is prone to error.
|
||||
/// </summary>
|
||||
private void CreateInitialSimplex()
|
||||
{
|
||||
var initialPoints = FindInitialPoints();
|
||||
#region Create the first faces from (dimension + 1) vertices.
|
||||
|
||||
var faces = new int[NumOfDimensions + 1];
|
||||
|
||||
for (var i = 0; i < NumOfDimensions + 1; i++)
|
||||
{
|
||||
var vertices = new int[NumOfDimensions];
|
||||
for (int j = 0, k = 0; j <= NumOfDimensions; j++)
|
||||
{
|
||||
if (i != j) vertices[k++] = initialPoints[j];
|
||||
}
|
||||
var newFace = FacePool[ObjectManager.GetFace()];
|
||||
newFace.Vertices = vertices;
|
||||
Array.Sort(vertices);
|
||||
mathHelper.CalculateFacePlane(newFace, Center);
|
||||
faces[i] = newFace.Index;
|
||||
}
|
||||
// update the adjacency (check all pairs of faces)
|
||||
for (var i = 0; i < NumOfDimensions; i++)
|
||||
for (var j = i + 1; j < NumOfDimensions + 1; j++) UpdateAdjacency(FacePool[faces[i]], FacePool[faces[j]]);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Init the vertex beyond buffers.
|
||||
|
||||
foreach (var faceIndex in faces)
|
||||
{
|
||||
var face = FacePool[faceIndex];
|
||||
FindBeyondVertices(face);
|
||||
if (face.VerticesBeyond.Count == 0) ConvexFaces.Add(face.Index); // The face is on the hull
|
||||
else UnprocessedFaces.Add(face);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Set all vertices to false (unvisited).
|
||||
foreach (var vertex in initialPoints) VertexVisited[vertex] = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds (dimension + 1) initial points.
|
||||
/// </summary>
|
||||
/// <returns>List<System.Int32>.</returns>
|
||||
/// <exception cref="System.ArgumentException">The input data is degenerate. It appears to exist in " + NumOfDimensions +
|
||||
/// " dimensions, but it is a " + (NumOfDimensions - 1) + " dimensional set (i.e. the point of collinear,"
|
||||
/// + " coplanar, or co-hyperplanar.)</exception>
|
||||
private List<int> FindInitialPoints()
|
||||
{
|
||||
var bigNumber = maxima.Sum() * NumOfDimensions * NumberOfVertices;
|
||||
// the first two points are taken from the dimension that had the fewest extremes
|
||||
// well, in most cases there will only be 2 in all dimensions: one min and one max
|
||||
// but a lot of engineering part shapes are nice and square and can have hundreds of
|
||||
// parallel vertices at the extremes
|
||||
var vertex1 = boundingBoxPoints[indexOfDimensionWithLeastExtremes].First(); // these are min and max vertices along
|
||||
var vertex2 = boundingBoxPoints[indexOfDimensionWithLeastExtremes].Last(); // the dimension that had the fewest points
|
||||
boundingBoxPoints[indexOfDimensionWithLeastExtremes].RemoveAt(0);
|
||||
boundingBoxPoints[indexOfDimensionWithLeastExtremes].RemoveAt(boundingBoxPoints[indexOfDimensionWithLeastExtremes].Count - 1);
|
||||
var initialPoints = new List<int> { vertex1, vertex2 };
|
||||
VertexVisited[vertex1] = VertexVisited[vertex2] = true;
|
||||
CurrentVertex = vertex1; UpdateCenter();
|
||||
CurrentVertex = vertex2; UpdateCenter();
|
||||
var edgeVectors = new float[NumOfDimensions][];
|
||||
edgeVectors[0] = mathHelper.VectorBetweenVertices(vertex2, vertex1);
|
||||
// now the remaining vertices are just combined in one big list
|
||||
var extremes = boundingBoxPoints.SelectMany(x => x).ToList();
|
||||
// otherwise find the remaining points by maximizing the initial simplex volume
|
||||
var index = 1;
|
||||
while (index < NumOfDimensions && extremes.Any())
|
||||
{
|
||||
var bestVertex = -1;
|
||||
var bestEdgeVector = new float[] { };
|
||||
var maxVolume = Constants.DefaultPlaneDistanceTolerance;
|
||||
for (var i = extremes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
// count backwards in order to remove potential duplicates
|
||||
var vIndex = extremes[i];
|
||||
if (initialPoints.Contains(vIndex)) extremes.RemoveAt(i);
|
||||
else
|
||||
{
|
||||
edgeVectors[index] = mathHelper.VectorBetweenVertices(vIndex, vertex1);
|
||||
var volume = mathHelper.GetSimplexVolume(edgeVectors, index, bigNumber);
|
||||
if (maxVolume < volume)
|
||||
{
|
||||
maxVolume = volume;
|
||||
bestVertex = vIndex;
|
||||
bestEdgeVector = edgeVectors[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
extremes.Remove(bestVertex);
|
||||
if (bestVertex == -1) break;
|
||||
initialPoints.Add(bestVertex);
|
||||
edgeVectors[index++] = bestEdgeVector;
|
||||
CurrentVertex = bestVertex; UpdateCenter();
|
||||
}
|
||||
// hmm, there are not enough points on the bounding box to make a simplex. It is rare but entirely possibly.
|
||||
// As an extreme, the bounding box can be made in n dimensions from only 2 unique points. When we can't find
|
||||
// enough unique points, we start again with ALL the vertices. The following is a near replica of the code
|
||||
// above, but instead of extremes, we consider "allVertices".
|
||||
if (initialPoints.Count <= NumOfDimensions && !IsLifted)
|
||||
{
|
||||
var allVertices = Enumerable.Range(0, NumberOfVertices).ToList();
|
||||
while (index < NumOfDimensions && allVertices.Any())
|
||||
{
|
||||
var bestVertex = -1;
|
||||
var bestEdgeVector = new float[] { };
|
||||
var maxVolume = 0.0;
|
||||
for (var i = allVertices.Count - 1; i >= 0; i--)
|
||||
{
|
||||
// count backwards in order to remove potential duplicates
|
||||
var vIndex = allVertices[i];
|
||||
if (initialPoints.Contains(vIndex)) allVertices.RemoveAt(i);
|
||||
else
|
||||
{
|
||||
edgeVectors[index] = mathHelper.VectorBetweenVertices(vIndex, vertex1);
|
||||
var volume = mathHelper.GetSimplexVolume(edgeVectors, index, bigNumber);
|
||||
if (maxVolume < volume)
|
||||
{
|
||||
maxVolume = volume;
|
||||
bestVertex = vIndex;
|
||||
bestEdgeVector = edgeVectors[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
allVertices.Remove(bestVertex);
|
||||
if (bestVertex == -1) break;
|
||||
initialPoints.Add(bestVertex);
|
||||
edgeVectors[index++] = bestEdgeVector;
|
||||
CurrentVertex = bestVertex; UpdateCenter();
|
||||
}
|
||||
}
|
||||
if (initialPoints.Count <= NumOfDimensions && IsLifted)
|
||||
{
|
||||
var allVertices = Enumerable.Range(0, NumberOfVertices).ToList();
|
||||
while (index < NumOfDimensions && allVertices.Any())
|
||||
{
|
||||
var bestVertex = -1;
|
||||
var bestEdgeVector = new float[] { };
|
||||
var maxVolume = 0.0;
|
||||
for (var i = allVertices.Count - 1; i >= 0; i--)
|
||||
{
|
||||
// count backwards in order to remove potential duplicates
|
||||
var vIndex = allVertices[i];
|
||||
if (initialPoints.Contains(vIndex)) allVertices.RemoveAt(i);
|
||||
else
|
||||
{
|
||||
mathHelper.RandomOffsetToLift(vIndex, maxima.Last()-minima.Last());
|
||||
edgeVectors[index] = mathHelper.VectorBetweenVertices(vIndex, vertex1);
|
||||
var volume = mathHelper.GetSimplexVolume(edgeVectors, index, bigNumber);
|
||||
if (maxVolume < volume)
|
||||
{
|
||||
maxVolume = volume;
|
||||
bestVertex = vIndex;
|
||||
bestEdgeVector = edgeVectors[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
allVertices.Remove(bestVertex);
|
||||
if (bestVertex == -1) break;
|
||||
initialPoints.Add(bestVertex);
|
||||
edgeVectors[index++] = bestEdgeVector;
|
||||
CurrentVertex = bestVertex; UpdateCenter();
|
||||
}
|
||||
}
|
||||
if (initialPoints.Count <= NumOfDimensions && IsLifted)
|
||||
throw new ArgumentException("The input data is degenerate. It appears to exist in " + NumOfDimensions +
|
||||
" dimensions, but it is a " + (NumOfDimensions - 1) + " dimensional set (i.e. the point of collinear,"
|
||||
+ " coplanar, or co-hyperplanar.)");
|
||||
return initialPoints;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if 2 faces are adjacent and if so, update their AdjacentFaces array.
|
||||
/// </summary>
|
||||
/// <param name="l">The l.</param>
|
||||
/// <param name="r">The r.</param>
|
||||
private void UpdateAdjacency(ConvexFaceInternal l, ConvexFaceInternal r)
|
||||
{
|
||||
var lv = l.Vertices;
|
||||
var rv = r.Vertices;
|
||||
int i;
|
||||
|
||||
// reset marks on the 1st face
|
||||
for (i = 0; i < lv.Length; i++) VertexVisited[lv[i]] = false;
|
||||
|
||||
// mark all vertices on the 2nd face
|
||||
for (i = 0; i < rv.Length; i++) VertexVisited[rv[i]] = true;
|
||||
|
||||
// find the 1st false index
|
||||
for (i = 0; i < lv.Length; i++) if (!VertexVisited[lv[i]]) break;
|
||||
|
||||
// no vertex was marked
|
||||
if (i == NumOfDimensions) return;
|
||||
|
||||
// check if only 1 vertex wasn't marked
|
||||
for (var j = i + 1; j < lv.Length; j++) if (!VertexVisited[lv[j]]) return;
|
||||
|
||||
// if we are here, the two faces share an edge
|
||||
l.AdjacentFaces[i] = r.Index;
|
||||
|
||||
// update the adj. face on the other face - find the vertex that remains marked
|
||||
for (i = 0; i < lv.Length; i++) VertexVisited[lv[i]] = false;
|
||||
for (i = 0; i < rv.Length; i++)
|
||||
{
|
||||
if (VertexVisited[rv[i]]) break;
|
||||
}
|
||||
r.AdjacentFaces[i] = l.Index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used in the "initialization" code.
|
||||
/// </summary>
|
||||
/// <param name="face">The face.</param>
|
||||
private void FindBeyondVertices(ConvexFaceInternal face)
|
||||
{
|
||||
var beyondVertices = face.VerticesBeyond;
|
||||
MaxDistance = float.NegativeInfinity;
|
||||
FurthestVertex = 0;
|
||||
for (var i = 0; i < NumberOfVertices; i++)
|
||||
{
|
||||
if (VertexVisited[i]) continue;
|
||||
IsBeyond(face, beyondVertices, i);
|
||||
}
|
||||
|
||||
face.FurthestVertex = FurthestVertex;
|
||||
}
|
||||
#region Fields
|
||||
|
||||
/// <summary>
|
||||
/// Corresponds to the dimension of the data.
|
||||
/// When the "lifted" hull is computed, Dimension is automatically incremented by one.
|
||||
/// </summary>
|
||||
internal readonly int NumOfDimensions;
|
||||
|
||||
/// <summary>
|
||||
/// Are we on a paraboloid?
|
||||
/// </summary>
|
||||
private readonly bool IsLifted;
|
||||
|
||||
/// <summary>
|
||||
/// Explained in ConvexHullComputationConfig.
|
||||
/// </summary>
|
||||
private readonly float PlaneDistanceTolerance;
|
||||
|
||||
/*
|
||||
* Representation of the input vertices.
|
||||
*
|
||||
* - In the algorithm, a vertex is represented by its index in the Vertices array.
|
||||
* This makes the algorithm a lot faster (up to 30%) than using object reference everywhere.
|
||||
* - Positions are stored as a single array of values. Coordinates for vertex with index i
|
||||
* are stored at indices <i * Dimension, (i + 1) * Dimension)
|
||||
* - VertexMarks are used by the algorithm to help identify a set of vertices that is "above" (or "beyond")
|
||||
* a specific face.
|
||||
*/
|
||||
/// <summary>
|
||||
/// The vertices
|
||||
/// </summary>
|
||||
private readonly IVertex[] Vertices;
|
||||
/// <summary>
|
||||
/// The positions
|
||||
/// </summary>
|
||||
private float[] Positions;
|
||||
/// <summary>
|
||||
/// The vertex marks
|
||||
/// </summary>
|
||||
private readonly bool[] VertexVisited;
|
||||
|
||||
private readonly int NumberOfVertices;
|
||||
|
||||
/*
|
||||
* The triangulation faces are represented in a single pool for objects that are being reused.
|
||||
* This allows for represent the faces as integers and significantly speeds up many computations.
|
||||
* - AffectedFaceFlags are used to mark affected faces/
|
||||
*/
|
||||
/// <summary>
|
||||
/// The face pool
|
||||
/// </summary>
|
||||
internal ConvexFaceInternal[] FacePool;
|
||||
/// <summary>
|
||||
/// The affected face flags
|
||||
/// </summary>
|
||||
internal bool[] AffectedFaceFlags;
|
||||
|
||||
/// <summary>
|
||||
/// Used to track the size of the current hull in the Update/RollbackCenter functions.
|
||||
/// </summary>
|
||||
private int ConvexHullSize;
|
||||
|
||||
/// <summary>
|
||||
/// A list of faces that that are not a part of the final convex hull and still need to be processed.
|
||||
/// </summary>
|
||||
private readonly FaceList UnprocessedFaces;
|
||||
|
||||
/// <summary>
|
||||
/// A list of faces that form the convex hull.
|
||||
/// </summary>
|
||||
private readonly IndexBuffer ConvexFaces;
|
||||
|
||||
/// <summary>
|
||||
/// The vertex that is currently being processed.
|
||||
/// </summary>
|
||||
private int CurrentVertex;
|
||||
|
||||
/// <summary>
|
||||
/// A helper variable to determine the furthest vertex for a particular convex face.
|
||||
/// </summary>
|
||||
private double MaxDistance;
|
||||
|
||||
/// <summary>
|
||||
/// A helper variable to help determine the index of the vertex that is furthest from the face that is currently being
|
||||
/// processed.
|
||||
/// </summary>
|
||||
private int FurthestVertex;
|
||||
|
||||
/// <summary>
|
||||
/// The centroid of the currently computed hull.
|
||||
/// </summary>
|
||||
private readonly float[] Center;
|
||||
|
||||
/*
|
||||
* Helper arrays to store faces for adjacency update.
|
||||
* This is just to prevent unnecessary allocations.
|
||||
*/
|
||||
/// <summary>
|
||||
/// The update buffer
|
||||
/// </summary>
|
||||
private readonly int[] UpdateBuffer;
|
||||
/// <summary>
|
||||
/// The update indices
|
||||
/// </summary>
|
||||
private readonly int[] UpdateIndices;
|
||||
|
||||
/// <summary>
|
||||
/// Used to determine which faces need to be updated at each step of the algorithm.
|
||||
/// </summary>
|
||||
private readonly IndexBuffer TraverseStack;
|
||||
|
||||
/// <summary>
|
||||
/// Used for VerticesBeyond for faces that are on the convex hull.
|
||||
/// </summary>
|
||||
private readonly IndexBuffer EmptyBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Used to determine which vertices are "above" (or "beyond") a face
|
||||
/// </summary>
|
||||
private IndexBuffer BeyondBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Stores faces that are visible from the current vertex.
|
||||
/// </summary>
|
||||
private readonly IndexBuffer AffectedFaceBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Stores faces that form a "cone" created by adding new vertex.
|
||||
/// </summary>
|
||||
private readonly SimpleList<DeferredFace> ConeFaceBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Stores a list of "singular" (or "generate", "planar", etc.) vertices that cannot be part of the hull.
|
||||
/// </summary>
|
||||
private readonly HashSet<int> SingularVertices;
|
||||
|
||||
/// <summary>
|
||||
/// The connector table helps to determine the adjacency of convex faces.
|
||||
/// Hashing is used instead of pairwise comparison. This significantly speeds up the computations,
|
||||
/// especially for higher dimensions.
|
||||
/// </summary>
|
||||
private readonly ConnectorList[] ConnectorTable;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the memory allocations and storage of unused objects.
|
||||
/// Saves the garbage collector a lot of work.
|
||||
/// </summary>
|
||||
private readonly ObjectManager ObjectManager;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for handling math related stuff.
|
||||
/// </summary>
|
||||
private readonly MathHelper mathHelper;
|
||||
private readonly List<int>[] boundingBoxPoints;
|
||||
private int indexOfDimensionWithLeastExtremes;
|
||||
private readonly float[] minima;
|
||||
private readonly float[] maxima;
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 48bd00cb6ec8d244f90e1c1608f39074
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,635 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/*
|
||||
* Main part of the algorithm
|
||||
* Basic idea:
|
||||
* - Create the initial hull (done in Initialize.cs)
|
||||
*
|
||||
* For each face there are "vertices beyond" which are "visible" from it.
|
||||
* If there are no such vertices, the face is on the hull.
|
||||
*
|
||||
* - While there is at least one face with at least one "vertex beyond":
|
||||
* * Pick the furthest beyond vertex
|
||||
* * For this vertex:
|
||||
* > find all faces that are visible from it (TagAffectedFaces)
|
||||
* > remove them and replace them with a "cone" created by the vertex and the boundary
|
||||
* of the affected faces, and for each new face, compute "beyond vertices"
|
||||
* (CreateCone + CommitCone)
|
||||
*
|
||||
* + Implement it in way that is fast, but hard to understand and maintain.
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Class ConvexHullAlgorithm.
|
||||
/// </summary>
|
||||
internal partial class ConvexHullAlgorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// Tags all faces seen from the current vertex with 1.
|
||||
/// </summary>
|
||||
/// <param name="currentFace">The current face.</param>
|
||||
private void TagAffectedFaces(ConvexFaceInternal currentFace)
|
||||
{
|
||||
AffectedFaceBuffer.Clear();
|
||||
AffectedFaceBuffer.Add(currentFace.Index);
|
||||
TraverseAffectedFaces(currentFace.Index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively traverse all the relevant faces.
|
||||
/// </summary>
|
||||
/// <param name="currentFace">The current face.</param>
|
||||
private void TraverseAffectedFaces(int currentFace)
|
||||
{
|
||||
TraverseStack.Clear();
|
||||
TraverseStack.Push(currentFace);
|
||||
AffectedFaceFlags[currentFace] = true;
|
||||
|
||||
while (TraverseStack.Count > 0)
|
||||
{
|
||||
var top = FacePool[TraverseStack.Pop()];
|
||||
for (var i = 0; i < NumOfDimensions; i++)
|
||||
{
|
||||
var adjFace = top.AdjacentFaces[i];
|
||||
|
||||
if (!AffectedFaceFlags[adjFace] &&
|
||||
mathHelper.GetVertexDistance(CurrentVertex, FacePool[adjFace]) >= PlaneDistanceTolerance)
|
||||
{
|
||||
AffectedFaceBuffer.Add(adjFace);
|
||||
AffectedFaceFlags[adjFace] = true;
|
||||
TraverseStack.Push(adjFace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new deferred face.
|
||||
/// </summary>
|
||||
/// <param name="face">The face.</param>
|
||||
/// <param name="faceIndex">Index of the face.</param>
|
||||
/// <param name="pivot">The pivot.</param>
|
||||
/// <param name="pivotIndex">Index of the pivot.</param>
|
||||
/// <param name="oldFace">The old face.</param>
|
||||
/// <returns>DeferredFace.</returns>
|
||||
private DeferredFace MakeDeferredFace(ConvexFaceInternal face, int faceIndex, ConvexFaceInternal pivot,
|
||||
int pivotIndex, ConvexFaceInternal oldFace)
|
||||
{
|
||||
var ret = ObjectManager.GetDeferredFace();
|
||||
|
||||
ret.Face = face;
|
||||
ret.FaceIndex = faceIndex;
|
||||
ret.Pivot = pivot;
|
||||
ret.PivotIndex = pivotIndex;
|
||||
ret.OldFace = oldFace;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect faces using a connector.
|
||||
/// </summary>
|
||||
/// <param name="connector">The connector.</param>
|
||||
private void ConnectFace(FaceConnector connector)
|
||||
{
|
||||
var index = connector.HashCode % Constants.ConnectorTableSize;
|
||||
var list = ConnectorTable[index];
|
||||
|
||||
for (var current = list.First; current != null; current = current.Next)
|
||||
{
|
||||
if (FaceConnector.AreConnectable(connector, current, NumOfDimensions))
|
||||
{
|
||||
list.Remove(current);
|
||||
FaceConnector.Connect(current, connector);
|
||||
current.Face = null;
|
||||
connector.Face = null;
|
||||
ObjectManager.DepositConnector(current);
|
||||
ObjectManager.DepositConnector(connector);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
list.Add(connector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the faces "covered" by the current vertex and adds the newly created ones.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
private bool CreateCone()
|
||||
{
|
||||
var currentVertexIndex = CurrentVertex;
|
||||
ConeFaceBuffer.Clear();
|
||||
|
||||
for (var fIndex = 0; fIndex < AffectedFaceBuffer.Count; fIndex++)
|
||||
{
|
||||
var oldFaceIndex = AffectedFaceBuffer[fIndex];
|
||||
var oldFace = FacePool[oldFaceIndex];
|
||||
|
||||
// Find the faces that need to be updated
|
||||
var updateCount = 0;
|
||||
for (var i = 0; i < NumOfDimensions; i++)
|
||||
{
|
||||
var af = oldFace.AdjacentFaces[i];
|
||||
if (!AffectedFaceFlags[af]) // Tag == false when oldFaces does not contain af
|
||||
{
|
||||
UpdateBuffer[updateCount] = af;
|
||||
UpdateIndices[updateCount] = i;
|
||||
++updateCount;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < updateCount; i++)
|
||||
{
|
||||
var adjacentFace = FacePool[UpdateBuffer[i]];
|
||||
|
||||
var oldFaceAdjacentIndex = 0;
|
||||
var adjFaceAdjacency = adjacentFace.AdjacentFaces;
|
||||
for (var j = 0; j < adjFaceAdjacency.Length; j++)
|
||||
{
|
||||
if (oldFaceIndex == adjFaceAdjacency[j])
|
||||
{
|
||||
oldFaceAdjacentIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var forbidden = UpdateIndices[i]; // Index of the face that corresponds to this adjacent face
|
||||
|
||||
int oldVertexIndex;
|
||||
int[] vertices;
|
||||
|
||||
var newFaceIndex = ObjectManager.GetFace();
|
||||
var newFace = FacePool[newFaceIndex];
|
||||
vertices = newFace.Vertices;
|
||||
for (var j = 0; j < NumOfDimensions; j++) vertices[j] = oldFace.Vertices[j];
|
||||
oldVertexIndex = vertices[forbidden];
|
||||
|
||||
int orderedPivotIndex;
|
||||
|
||||
// correct the ordering
|
||||
if (currentVertexIndex < oldVertexIndex)
|
||||
{
|
||||
orderedPivotIndex = 0;
|
||||
for (var j = forbidden - 1; j >= 0; j--)
|
||||
{
|
||||
if (vertices[j] > currentVertexIndex) vertices[j + 1] = vertices[j];
|
||||
else
|
||||
{
|
||||
orderedPivotIndex = j + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
orderedPivotIndex = NumOfDimensions - 1;
|
||||
for (var j = forbidden + 1; j < NumOfDimensions; j++)
|
||||
{
|
||||
if (vertices[j] < currentVertexIndex) vertices[j - 1] = vertices[j];
|
||||
else
|
||||
{
|
||||
orderedPivotIndex = j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vertices[orderedPivotIndex] = CurrentVertex;
|
||||
|
||||
if (!mathHelper.CalculateFacePlane(newFace, Center))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ConeFaceBuffer.Add(MakeDeferredFace(newFace, orderedPivotIndex, adjacentFace, oldFaceAdjacentIndex,
|
||||
oldFace));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commits a cone and adds a vertex to the convex hull.
|
||||
/// </summary>
|
||||
private void CommitCone()
|
||||
{
|
||||
// Fill the adjacency.
|
||||
for (var i = 0; i < ConeFaceBuffer.Count; i++)
|
||||
{
|
||||
var face = ConeFaceBuffer[i];
|
||||
|
||||
var newFace = face.Face;
|
||||
var adjacentFace = face.Pivot;
|
||||
var oldFace = face.OldFace;
|
||||
var orderedPivotIndex = face.FaceIndex;
|
||||
|
||||
newFace.AdjacentFaces[orderedPivotIndex] = adjacentFace.Index;
|
||||
adjacentFace.AdjacentFaces[face.PivotIndex] = newFace.Index;
|
||||
|
||||
// let there be a connection.
|
||||
for (var j = 0; j < NumOfDimensions; j++)
|
||||
{
|
||||
if (j == orderedPivotIndex) continue;
|
||||
var connector = ObjectManager.GetConnector();
|
||||
connector.Update(newFace, j, NumOfDimensions);
|
||||
ConnectFace(connector);
|
||||
}
|
||||
|
||||
// the id adjacent face on the hull? If so, we can use simple method to find beyond vertices.
|
||||
if (adjacentFace.VerticesBeyond.Count == 0)
|
||||
FindBeyondVertices(newFace, oldFace.VerticesBeyond);
|
||||
// it is slightly more effective if the face with the lower number of beyond vertices comes first.
|
||||
else if (adjacentFace.VerticesBeyond.Count < oldFace.VerticesBeyond.Count)
|
||||
FindBeyondVertices(newFace, adjacentFace.VerticesBeyond, oldFace.VerticesBeyond);
|
||||
else
|
||||
FindBeyondVertices(newFace, oldFace.VerticesBeyond, adjacentFace.VerticesBeyond);
|
||||
|
||||
// This face will definitely lie on the hull
|
||||
if (newFace.VerticesBeyond.Count == 0)
|
||||
{
|
||||
ConvexFaces.Add(newFace.Index);
|
||||
UnprocessedFaces.Remove(newFace);
|
||||
ObjectManager.DepositVertexBuffer(newFace.VerticesBeyond);
|
||||
newFace.VerticesBeyond = EmptyBuffer;
|
||||
}
|
||||
else // Add the face to the list
|
||||
{
|
||||
UnprocessedFaces.Add(newFace);
|
||||
}
|
||||
|
||||
// recycle the object.
|
||||
ObjectManager.DepositDeferredFace(face);
|
||||
}
|
||||
|
||||
// Recycle the affected faces.
|
||||
for (var fIndex = 0; fIndex < AffectedFaceBuffer.Count; fIndex++)
|
||||
{
|
||||
var face = AffectedFaceBuffer[fIndex];
|
||||
UnprocessedFaces.Remove(FacePool[face]);
|
||||
ObjectManager.DepositFace(face);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the vertex v is beyond the given face. If so, add it to beyondVertices.
|
||||
/// </summary>
|
||||
/// <param name="face">The face.</param>
|
||||
/// <param name="beyondVertices">The beyond vertices.</param>
|
||||
/// <param name="v">The v.</param>
|
||||
private void IsBeyond(ConvexFaceInternal face, IndexBuffer beyondVertices, int v)
|
||||
{
|
||||
var distance = mathHelper.GetVertexDistance(v, face);
|
||||
if (distance >= PlaneDistanceTolerance)
|
||||
{
|
||||
if (distance > MaxDistance)
|
||||
{
|
||||
// If it's within the tolerance distance, use the lex. larger point
|
||||
if (distance - MaxDistance < PlaneDistanceTolerance)
|
||||
{ // todo: why is this LexCompare necessary. Would seem to favor x over y over z (etc.)?
|
||||
if (LexCompare(v, FurthestVertex) > 0)
|
||||
{
|
||||
MaxDistance = distance;
|
||||
FurthestVertex = v;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MaxDistance = distance;
|
||||
FurthestVertex = v;
|
||||
}
|
||||
}
|
||||
beyondVertices.Add(v);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Compares the values of two vertices. The return value (-1, 0 or +1) are found
|
||||
/// by first checking the first coordinate and then progressing through the rest.
|
||||
/// In this way {2, 8} will be a "-1" (less than) {3, 1}.
|
||||
/// </summary>
|
||||
/// <param name="u">The base vertex index, u.</param>
|
||||
/// <param name="v">The compared vertex index, v.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
private int LexCompare(int u, int v)
|
||||
{
|
||||
int uOffset = u * NumOfDimensions, vOffset = v * NumOfDimensions;
|
||||
for (var i = 0; i < NumOfDimensions; i++)
|
||||
{
|
||||
double x = Positions[uOffset + i], y = Positions[vOffset + i];
|
||||
var comp = x.CompareTo(y);
|
||||
if (comp != 0) return comp;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used by update faces.
|
||||
/// </summary>
|
||||
/// <param name="face">The face.</param>
|
||||
/// <param name="beyond">The beyond.</param>
|
||||
/// <param name="beyond1">The beyond1.</param>
|
||||
private void FindBeyondVertices(ConvexFaceInternal face, IndexBuffer beyond, IndexBuffer beyond1)
|
||||
{
|
||||
var beyondVertices = BeyondBuffer;
|
||||
|
||||
MaxDistance = double.NegativeInfinity;
|
||||
FurthestVertex = 0;
|
||||
int v;
|
||||
|
||||
for (var i = 0; i < beyond1.Count; i++) VertexVisited[beyond1[i]] = true;
|
||||
VertexVisited[CurrentVertex] = false;
|
||||
for (var i = 0; i < beyond.Count; i++)
|
||||
{
|
||||
v = beyond[i];
|
||||
if (v == CurrentVertex) continue;
|
||||
VertexVisited[v] = false;
|
||||
IsBeyond(face, beyondVertices, v);
|
||||
}
|
||||
|
||||
for (var i = 0; i < beyond1.Count; i++)
|
||||
{
|
||||
v = beyond1[i];
|
||||
if (VertexVisited[v]) IsBeyond(face, beyondVertices, v);
|
||||
}
|
||||
|
||||
face.FurthestVertex = FurthestVertex;
|
||||
|
||||
// Pull the old switch a roo (switch the face beyond buffers)
|
||||
var temp = face.VerticesBeyond;
|
||||
face.VerticesBeyond = beyondVertices;
|
||||
if (temp.Count > 0) temp.Clear();
|
||||
BeyondBuffer = temp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the beyond vertices.
|
||||
/// </summary>
|
||||
/// <param name="face">The face.</param>
|
||||
/// <param name="beyond">The beyond.</param>
|
||||
private void FindBeyondVertices(ConvexFaceInternal face, IndexBuffer beyond)
|
||||
{
|
||||
var beyondVertices = BeyondBuffer;
|
||||
|
||||
MaxDistance = double.NegativeInfinity;
|
||||
FurthestVertex = 0;
|
||||
int v;
|
||||
|
||||
for (var i = 0; i < beyond.Count; i++)
|
||||
{
|
||||
v = beyond[i];
|
||||
if (v == CurrentVertex) continue;
|
||||
IsBeyond(face, beyondVertices, v);
|
||||
}
|
||||
|
||||
face.FurthestVertex = FurthestVertex;
|
||||
|
||||
// Pull the old switch a roo (switch the face beyond buffers)
|
||||
var temp = face.VerticesBeyond;
|
||||
face.VerticesBeyond = beyondVertices;
|
||||
if (temp.Count > 0) temp.Clear();
|
||||
BeyondBuffer = temp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recalculates the centroid of the current hull.
|
||||
/// </summary>
|
||||
private void UpdateCenter()
|
||||
{
|
||||
for (var i = 0; i < NumOfDimensions; i++) Center[i] *= ConvexHullSize;
|
||||
ConvexHullSize += 1;
|
||||
var f = 1.0f / ConvexHullSize;
|
||||
var co = CurrentVertex * NumOfDimensions;
|
||||
for (var i = 0; i < NumOfDimensions; i++) Center[i] = f * (Center[i] + Positions[co + i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the last vertex from the center.
|
||||
/// </summary>
|
||||
private void RollbackCenter()
|
||||
{
|
||||
for (var i = 0; i < NumOfDimensions; i++) Center[i] *= ConvexHullSize;
|
||||
ConvexHullSize -= 1;
|
||||
var f = ConvexHullSize > 0 ? 1.0f / ConvexHullSize : 0.0f;
|
||||
var co = CurrentVertex * NumOfDimensions;
|
||||
for (var i = 0; i < NumOfDimensions; i++) Center[i] = f * (Center[i] - Positions[co + i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles singular vertex.
|
||||
/// </summary>
|
||||
private void HandleSingular()
|
||||
{
|
||||
RollbackCenter();
|
||||
SingularVertices.Add(CurrentVertex);
|
||||
|
||||
// This means that all the affected faces must be on the hull and that all their "vertices beyond" are singular.
|
||||
for (var fIndex = 0; fIndex < AffectedFaceBuffer.Count; fIndex++)
|
||||
{
|
||||
var face = FacePool[AffectedFaceBuffer[fIndex]];
|
||||
var vb = face.VerticesBeyond;
|
||||
for (var i = 0; i < vb.Count; i++)
|
||||
{
|
||||
SingularVertices.Add(vb[i]);
|
||||
}
|
||||
|
||||
ConvexFaces.Add(face.Index);
|
||||
UnprocessedFaces.Remove(face);
|
||||
ObjectManager.DepositVertexBuffer(face.VerticesBeyond);
|
||||
face.VerticesBeyond = EmptyBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a vertex coordinate. In order to reduce speed, all vertex coordinates
|
||||
/// have been placed in a single array.
|
||||
/// </summary>
|
||||
/// <param name="vIndex">The vertex index.</param>
|
||||
/// <param name="dimension">The index of the dimension.</param>
|
||||
/// <returns>System.Double.</returns>
|
||||
private float GetCoordinate(int vIndex, int dimension)
|
||||
{
|
||||
return Positions[vIndex * NumOfDimensions + dimension];
|
||||
}
|
||||
|
||||
#region Returning the Results in the proper format
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hull vertices.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>TVertex[].</returns>
|
||||
private TVertex[] GetHullVertices<TVertex>(IList<TVertex> data)
|
||||
{
|
||||
var cellCount = ConvexFaces.Count;
|
||||
var hullVertexCount = 0;
|
||||
|
||||
for (var i = 0; i < NumberOfVertices; i++) VertexVisited[i] = false;
|
||||
|
||||
for (var i = 0; i < cellCount; i++)
|
||||
{
|
||||
var vs = FacePool[ConvexFaces[i]].Vertices;
|
||||
for (var j = 0; j < vs.Length; j++)
|
||||
{
|
||||
var v = vs[j];
|
||||
if (!VertexVisited[v])
|
||||
{
|
||||
VertexVisited[v] = true;
|
||||
hullVertexCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = new TVertex[hullVertexCount];
|
||||
for (var i = 0; i < NumberOfVertices; i++)
|
||||
{
|
||||
if (VertexVisited[i]) result[--hullVertexCount] = data[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the convex hull and creates the TFace objects.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFace">The type of the t face.</typeparam>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <returns>TFace[].</returns>
|
||||
private TFace[] GetConvexFaces<TVertex, TFace>()
|
||||
where TFace : ConvexFace<TVertex, TFace>, new()
|
||||
where TVertex : IVertex
|
||||
{
|
||||
var faces = ConvexFaces;
|
||||
var cellCount = faces.Count;
|
||||
var cells = new TFace[cellCount];
|
||||
|
||||
for (var i = 0; i < cellCount; i++)
|
||||
{
|
||||
var face = FacePool[faces[i]];
|
||||
var vertices = new TVertex[NumOfDimensions];
|
||||
for (var j = 0; j < NumOfDimensions; j++)
|
||||
{
|
||||
vertices[j] = (TVertex)Vertices[face.Vertices[j]];
|
||||
}
|
||||
|
||||
cells[i] = new TFace
|
||||
{
|
||||
Vertices = vertices,
|
||||
Adjacency = new TFace[NumOfDimensions],
|
||||
Normal = IsLifted ? null : face.Normal
|
||||
};
|
||||
face.Tag = i;
|
||||
}
|
||||
|
||||
for (var i = 0; i < cellCount; i++)
|
||||
{
|
||||
var face = FacePool[faces[i]];
|
||||
var cell = cells[i];
|
||||
for (var j = 0; j < NumOfDimensions; j++)
|
||||
{
|
||||
if (face.AdjacentFaces[j] < 0) continue;
|
||||
cell.Adjacency[j] = cells[FacePool[face.AdjacentFaces[j]].Tag];
|
||||
}
|
||||
|
||||
// Fix the vertex orientation.
|
||||
if (face.IsNormalFlipped)
|
||||
{
|
||||
var tempVert = cell.Vertices[0];
|
||||
cell.Vertices[0] = cell.Vertices[NumOfDimensions - 1];
|
||||
cell.Vertices[NumOfDimensions - 1] = tempVert;
|
||||
|
||||
var tempAdj = cell.Adjacency[0];
|
||||
cell.Adjacency[0] = cell.Adjacency[NumOfDimensions - 1];
|
||||
cell.Adjacency[NumOfDimensions - 1] = tempAdj;
|
||||
}
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For 2D only: Returns the result in counter-clockwise order starting with the element with the lowest X value.
|
||||
/// If there are multiple vertices with the same minimum X, then the one with the lowest Y is chosen.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the vertex.</typeparam>
|
||||
/// <typeparam name="TFace">The type of the face.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns></returns>
|
||||
private ConvexHull<TVertex, TFace> Return2DResultInOrder<TVertex, TFace>(IList<TVertex> data)
|
||||
where TVertex : IVertex
|
||||
where TFace : ConvexFace<TVertex, TFace>, new()
|
||||
{
|
||||
TFace[] faces = GetConvexFaces<TVertex, TFace>();
|
||||
var numPoints = faces.Length;
|
||||
var orderDictionary = new Dictionary<TVertex, TFace>();
|
||||
foreach (var face in faces)
|
||||
orderDictionary.Add(face.Vertices[1], face);
|
||||
var firstPoint = faces[0].Vertices[1];
|
||||
var nextPoint = faces[0].Vertices[0];
|
||||
var orderedPointList = new List<TVertex>();
|
||||
orderedPointList.Add(firstPoint);
|
||||
var orderedFaceList = new List<TFace>();
|
||||
orderedFaceList.Add(faces[1]);
|
||||
var lowestXMinIndex = 0;
|
||||
var k = 0;
|
||||
while (!nextPoint.Equals(firstPoint))
|
||||
{
|
||||
orderedPointList.Add(nextPoint);
|
||||
var nextFace = orderDictionary[nextPoint];
|
||||
orderedFaceList.Add(nextFace);
|
||||
if (nextPoint.Position[0] < orderedPointList[lowestXMinIndex].Position[0]
|
||||
|| (nextPoint.Position[0] == orderedPointList[lowestXMinIndex].Position[0]
|
||||
&& nextPoint.Position[1] <= orderedPointList[lowestXMinIndex].Position[1]))
|
||||
lowestXMinIndex = k;
|
||||
k++;
|
||||
nextPoint = nextFace.Vertices[0];
|
||||
}
|
||||
TVertex[] points = new TVertex[numPoints];
|
||||
for (int i = 0; i < numPoints; i++)
|
||||
{
|
||||
var j = (i + lowestXMinIndex) % numPoints;
|
||||
points[i] = orderedPointList[j];
|
||||
faces[i] = orderedFaceList[j];
|
||||
}
|
||||
return new ConvexHull<TVertex, TFace>
|
||||
{
|
||||
Points = points,
|
||||
Faces = faces
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f7f265217d00ea340b87645242a45181
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// For deferred face addition.
|
||||
/// </summary>
|
||||
internal sealed class DeferredFace
|
||||
{
|
||||
/// <summary>
|
||||
/// The faces.
|
||||
/// </summary>
|
||||
public ConvexFaceInternal Face, Pivot, OldFace;
|
||||
|
||||
/// <summary>
|
||||
/// The indices.
|
||||
/// </summary>
|
||||
public int FaceIndex, PivotIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A helper class used to connect faces.
|
||||
/// </summary>
|
||||
internal sealed class FaceConnector
|
||||
{
|
||||
/// <summary>
|
||||
/// The edge to be connected.
|
||||
/// </summary>
|
||||
public int EdgeIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The face.
|
||||
/// </summary>
|
||||
public ConvexFaceInternal Face;
|
||||
|
||||
/// <summary>
|
||||
/// The hash code computed from indices.
|
||||
/// </summary>
|
||||
public uint HashCode;
|
||||
|
||||
/// <summary>
|
||||
/// Next node in the list.
|
||||
/// </summary>
|
||||
public FaceConnector Next;
|
||||
|
||||
/// <summary>
|
||||
/// Prev node in the list.
|
||||
/// </summary>
|
||||
public FaceConnector Previous;
|
||||
|
||||
/// <summary>
|
||||
/// The vertex indices.
|
||||
/// </summary>
|
||||
public int[] Vertices;
|
||||
|
||||
/// <summary>
|
||||
/// Ctor.
|
||||
/// </summary>
|
||||
/// <param name="dimension">The dimension.</param>
|
||||
public FaceConnector(int dimension)
|
||||
{
|
||||
Vertices = new int[dimension - 1];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the connector.
|
||||
/// </summary>
|
||||
/// <param name="face">The face.</param>
|
||||
/// <param name="edgeIndex">Index of the edge.</param>
|
||||
/// <param name="dim">The dim.</param>
|
||||
public void Update(ConvexFaceInternal face, int edgeIndex, int dim)
|
||||
{
|
||||
Face = face;
|
||||
EdgeIndex = edgeIndex;
|
||||
|
||||
uint hashCode = 23;
|
||||
|
||||
unchecked
|
||||
{
|
||||
var vs = face.Vertices;
|
||||
int i, c = 0;
|
||||
for (i = 0; i < edgeIndex; i++)
|
||||
{
|
||||
Vertices[c++] = vs[i];
|
||||
hashCode += 31*hashCode + (uint) vs[i];
|
||||
}
|
||||
for (i = edgeIndex + 1; i < vs.Length; i++)
|
||||
{
|
||||
Vertices[c++] = vs[i];
|
||||
hashCode += 31*hashCode + (uint) vs[i];
|
||||
}
|
||||
}
|
||||
|
||||
HashCode = hashCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can two faces be connected.
|
||||
/// </summary>
|
||||
/// <param name="a">a.</param>
|
||||
/// <param name="b">The b.</param>
|
||||
/// <param name="dim">The dim.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
public static bool AreConnectable(FaceConnector a, FaceConnector b, int dim)
|
||||
{
|
||||
if (a.HashCode != b.HashCode) return false;
|
||||
|
||||
var av = a.Vertices;
|
||||
var bv = b.Vertices;
|
||||
for (var i = 0; i < av.Length; i++)
|
||||
{
|
||||
if (av[i] != bv[i]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect two faces.
|
||||
/// </summary>
|
||||
/// <param name="a">a.</param>
|
||||
/// <param name="b">The b.</param>
|
||||
public static void Connect(FaceConnector a, FaceConnector b)
|
||||
{
|
||||
a.Face.AdjacentFaces[a.EdgeIndex] = b.Face.Index;
|
||||
b.Face.AdjacentFaces[b.EdgeIndex] = a.Face.Index;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This internal class manages the faces of the convex hull. It is a
|
||||
/// separate class from the desired user class.
|
||||
/// </summary>
|
||||
internal sealed class ConvexFaceInternal
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the adjacent face data.
|
||||
/// </summary>
|
||||
public int[] AdjacentFaces;
|
||||
|
||||
/// <summary>
|
||||
/// The furthest vertex.
|
||||
/// </summary>
|
||||
public int FurthestVertex;
|
||||
|
||||
/// <summary>
|
||||
/// Index of the face inside the pool.
|
||||
/// </summary>
|
||||
public int Index;
|
||||
|
||||
/// <summary>
|
||||
/// Is it present in the list.
|
||||
/// </summary>
|
||||
public bool InList;
|
||||
|
||||
/// <summary>
|
||||
/// Is the normal flipped?
|
||||
/// </summary>
|
||||
public bool IsNormalFlipped;
|
||||
|
||||
/// <summary>
|
||||
/// Next node in the list.
|
||||
/// </summary>
|
||||
public ConvexFaceInternal Next;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the normal vector.
|
||||
/// </summary>
|
||||
public float[] Normal;
|
||||
|
||||
/// <summary>
|
||||
/// Face plane constant element.
|
||||
/// </summary>
|
||||
public float Offset;
|
||||
|
||||
//public int UnprocessedIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Prev node in the list.
|
||||
/// </summary>
|
||||
public ConvexFaceInternal Previous;
|
||||
|
||||
/// <summary>
|
||||
/// Used to traverse affected faces and create the Delaunay representation.
|
||||
/// </summary>
|
||||
public int Tag;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertices.
|
||||
/// </summary>
|
||||
public int[] Vertices;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertices beyond.
|
||||
/// </summary>
|
||||
public IndexBuffer VerticesBeyond;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConvexFaceInternal" /> class.
|
||||
/// </summary>
|
||||
/// <param name="dimension">The dimension.</param>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <param name="beyondList">The beyond list.</param>
|
||||
public ConvexFaceInternal(int dimension, int index, IndexBuffer beyondList)
|
||||
{
|
||||
Index = index;
|
||||
AdjacentFaces = new int[dimension];
|
||||
VerticesBeyond = beyondList;
|
||||
Normal = new float[dimension];
|
||||
Vertices = new int[dimension];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 58c58e163f4c86e48a6df3ee6c5056c0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,480 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
using Random = CultMath.Random;
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// A helper class mostly for normal computation. If convex hulls are computed
|
||||
/// in higher dimensions, it might be a good idea to add a specific
|
||||
/// FindNormalVectorND function.
|
||||
/// </summary>
|
||||
internal class MathHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The dimension
|
||||
/// </summary>
|
||||
private readonly int Dimension;
|
||||
/// <summary>
|
||||
/// The matrix pivots
|
||||
/// </summary>
|
||||
private readonly int[] matrixPivots;
|
||||
/// <summary>
|
||||
/// The n d matrix
|
||||
/// </summary>
|
||||
private readonly float[] nDMatrix;
|
||||
/// <summary>
|
||||
/// The n d normal helper vector
|
||||
/// </summary>
|
||||
private readonly float[] nDNormalHelperVector;
|
||||
|
||||
/// <summary>
|
||||
/// The nt x
|
||||
/// </summary>
|
||||
private readonly float[] ntX;
|
||||
/// <summary>
|
||||
/// The nt y
|
||||
/// </summary>
|
||||
private readonly float[] ntY;
|
||||
/// <summary>
|
||||
/// The nt z
|
||||
/// </summary>
|
||||
private readonly float[] ntZ;
|
||||
|
||||
/// <summary>
|
||||
/// The position data
|
||||
/// </summary>
|
||||
private readonly float[] PositionData;
|
||||
|
||||
private static Random _random = new Random((uint) (DateTime.Now.Ticks % uint.MaxValue));
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MathHelper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dimension">The dimension.</param>
|
||||
/// <param name="positions">The positions.</param>
|
||||
internal MathHelper(int dimension, float[] positions)
|
||||
{
|
||||
PositionData = positions;
|
||||
Dimension = dimension;
|
||||
|
||||
ntX = new float[Dimension];
|
||||
ntY = new float[Dimension];
|
||||
ntZ = new float[Dimension];
|
||||
|
||||
nDNormalHelperVector = new float[Dimension];
|
||||
nDMatrix = new float[Dimension * Dimension];
|
||||
matrixPivots = new int[Dimension];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the normal and offset of the hyper-plane given by the face's vertices.
|
||||
/// </summary>
|
||||
/// <param name="face">The face.</param>
|
||||
/// <param name="center">The center.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
internal bool CalculateFacePlane(ConvexFaceInternal face, float[] center)
|
||||
{
|
||||
var vertices = face.Vertices;
|
||||
var normal = face.Normal;
|
||||
FindNormalVector(vertices, normal);
|
||||
|
||||
if (float.IsNaN(normal[0]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = 0.0f;
|
||||
var centerDistance = 0.0f;
|
||||
var fi = vertices[0] * Dimension;
|
||||
for (var i = 0; i < Dimension; i++)
|
||||
{
|
||||
var n = normal[i];
|
||||
offset += n * PositionData[fi + i];
|
||||
centerDistance += n * center[i];
|
||||
}
|
||||
face.Offset = -offset;
|
||||
centerDistance -= offset;
|
||||
|
||||
if (centerDistance > 0)
|
||||
{
|
||||
for (var i = 0; i < Dimension; i++) normal[i] = -normal[i];
|
||||
face.Offset = offset;
|
||||
face.IsNormalFlipped = true;
|
||||
}
|
||||
else face.IsNormalFlipped = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the vertex is "visible" from the face.
|
||||
/// The vertex is "over face" if the return value is > Constants.PlaneDistanceTolerance.
|
||||
/// </summary>
|
||||
/// <param name="v">The v.</param>
|
||||
/// <param name="f">The f.</param>
|
||||
/// <returns>The vertex is "over face" if the result is positive.</returns>
|
||||
internal double GetVertexDistance(int v, ConvexFaceInternal f)
|
||||
{
|
||||
var normal = f.Normal;
|
||||
var x = v * Dimension;
|
||||
var distance = f.Offset;
|
||||
for (var i = 0; i < normal.Length; i++) distance += normal[i] * PositionData[x + i];
|
||||
return distance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the vector the between vertices.
|
||||
/// </summary>
|
||||
/// <param name="toIndex">To index.</param>
|
||||
/// <param name="fromIndex">From index.</param>
|
||||
/// <returns>System.Double[].</returns>
|
||||
internal float[] VectorBetweenVertices(int toIndex, int fromIndex)
|
||||
{
|
||||
var target = new float[Dimension];
|
||||
VectorBetweenVertices(toIndex, fromIndex, target);
|
||||
return target;
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns the vector the between vertices.
|
||||
/// </summary>
|
||||
/// <param name="fromIndex">From index.</param>
|
||||
/// <param name="toIndex">To index.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <returns></returns>
|
||||
private void VectorBetweenVertices(int toIndex, int fromIndex, float[] target)
|
||||
{
|
||||
int u = toIndex * Dimension, v = fromIndex * Dimension;
|
||||
for (var i = 0; i < Dimension; i++)
|
||||
{
|
||||
target[i] = PositionData[u + i] - PositionData[v + i];
|
||||
}
|
||||
}
|
||||
|
||||
internal void RandomOffsetToLift(int index, float maxHeight)
|
||||
{
|
||||
var liftIndex = (index * Dimension) + Dimension - 1;
|
||||
PositionData[liftIndex] += 0.0001f * maxHeight * (_random.NextFloat() - 0.5f);
|
||||
}
|
||||
#region Find the normal vector of the face
|
||||
/// <summary>
|
||||
/// Finds normal vector of a hyper-plane given by vertices.
|
||||
/// Stores the results to normalData.
|
||||
/// </summary>
|
||||
/// <param name="vertices">The vertices.</param>
|
||||
/// <param name="normalData">The normal data.</param>
|
||||
private void FindNormalVector(int[] vertices, float[] normalData)
|
||||
{
|
||||
switch (Dimension)
|
||||
{
|
||||
case 2:
|
||||
FindNormalVector2D(vertices, normalData);
|
||||
break;
|
||||
case 3:
|
||||
FindNormalVector3D(vertices, normalData);
|
||||
break;
|
||||
case 4:
|
||||
FindNormalVector4D(vertices, normalData);
|
||||
break;
|
||||
default:
|
||||
FindNormalVectorND(vertices, normalData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Finds 2D normal vector.
|
||||
/// </summary>
|
||||
/// <param name="vertices">The vertices.</param>
|
||||
/// <param name="normal">The normal.</param>
|
||||
private void FindNormalVector2D(int[] vertices, float[] normal)
|
||||
{
|
||||
VectorBetweenVertices(vertices[1], vertices[0], ntX);
|
||||
|
||||
var nx = -ntX[1];
|
||||
var ny = ntX[0];
|
||||
|
||||
var norm = sqrt(nx * nx + ny * ny);
|
||||
|
||||
var f = 1.0f / norm;
|
||||
normal[0] = f * nx;
|
||||
normal[1] = f * ny;
|
||||
}
|
||||
/// <summary>
|
||||
/// Finds 3D normal vector.
|
||||
/// </summary>
|
||||
/// <param name="vertices">The vertices.</param>
|
||||
/// <param name="normal">The normal.</param>
|
||||
private void FindNormalVector3D(int[] vertices, float[] normal)
|
||||
{
|
||||
VectorBetweenVertices(vertices[1], vertices[0], ntX);
|
||||
VectorBetweenVertices(vertices[2], vertices[1], ntY);
|
||||
|
||||
var nx = ntX[1] * ntY[2] - ntX[2] * ntY[1];
|
||||
var ny = ntX[2] * ntY[0] - ntX[0] * ntY[2];
|
||||
var nz = ntX[0] * ntY[1] - ntX[1] * ntY[0];
|
||||
|
||||
var norm = sqrt(nx * nx + ny * ny + nz * nz);
|
||||
|
||||
var f = 1.0f / norm;
|
||||
normal[0] = f * nx;
|
||||
normal[1] = f * ny;
|
||||
normal[2] = f * nz;
|
||||
}
|
||||
/// <summary>
|
||||
/// Finds 4D normal vector.
|
||||
/// </summary>
|
||||
/// <param name="vertices">The vertices.</param>
|
||||
/// <param name="normal">The normal.</param>
|
||||
private void FindNormalVector4D(int[] vertices, float[] normal)
|
||||
{
|
||||
VectorBetweenVertices(vertices[1], vertices[0], ntX);
|
||||
VectorBetweenVertices(vertices[2], vertices[1], ntY);
|
||||
VectorBetweenVertices(vertices[3], vertices[2], ntZ);
|
||||
|
||||
var x = ntX;
|
||||
var y = ntY;
|
||||
var z = ntZ;
|
||||
|
||||
// This was generated using Mathematica
|
||||
var nx = x[3] * (y[2] * z[1] - y[1] * z[2])
|
||||
+ x[2] * (y[1] * z[3] - y[3] * z[1])
|
||||
+ x[1] * (y[3] * z[2] - y[2] * z[3]);
|
||||
var ny = x[3] * (y[0] * z[2] - y[2] * z[0])
|
||||
+ x[2] * (y[3] * z[0] - y[0] * z[3])
|
||||
+ x[0] * (y[2] * z[3] - y[3] * z[2]);
|
||||
var nz = x[3] * (y[1] * z[0] - y[0] * z[1])
|
||||
+ x[1] * (y[0] * z[3] - y[3] * z[0])
|
||||
+ x[0] * (y[3] * z[1] - y[1] * z[3]);
|
||||
var nw = x[2] * (y[0] * z[1] - y[1] * z[0])
|
||||
+ x[1] * (y[2] * z[0] - y[0] * z[2])
|
||||
+ x[0] * (y[1] * z[2] - y[2] * z[1]);
|
||||
|
||||
var norm = sqrt(nx * nx + ny * ny + nz * nz + nw * nw);
|
||||
|
||||
var f = 1.0f / norm;
|
||||
normal[0] = f * nx;
|
||||
normal[1] = f * ny;
|
||||
normal[2] = f * nz;
|
||||
normal[3] = f * nw;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the normal vector nd.
|
||||
/// </summary>
|
||||
/// <param name="vertices">The vertices.</param>
|
||||
/// <param name="normal">The normal.</param>
|
||||
private void FindNormalVectorND(int[] vertices, float[] normal)
|
||||
{
|
||||
/* We need to solve the matrix A n = B where
|
||||
* - A contains coordinates of vertices as columns
|
||||
* - B is vector with all 1's. Really, it should be the distance of
|
||||
* the plane from the origin, but - since we're not worried about that
|
||||
* here and we will normalize the normal anyway - all 1's suffices.
|
||||
*/
|
||||
var iPiv = matrixPivots;
|
||||
var data = nDMatrix;
|
||||
var norm = 0.0f;
|
||||
|
||||
// Solve determinants by replacing x-th column by all 1.
|
||||
for (var x = 0; x < Dimension; x++)
|
||||
{
|
||||
for (var i = 0; i < Dimension; i++)
|
||||
{
|
||||
var offset = vertices[i] * Dimension;
|
||||
for (var j = 0; j < Dimension; j++)
|
||||
{
|
||||
// maybe I got the i/j mixed up here regarding the representation Math.net uses...
|
||||
// ...but it does not matter since Det(A) = Det(Transpose(A)).
|
||||
data[Dimension * i + j] = j == x ? 1.0f : PositionData[offset + j];
|
||||
}
|
||||
}
|
||||
LUFactor(data, Dimension, iPiv, nDNormalHelperVector);
|
||||
var coord = 1.0f;
|
||||
for (var i = 0; i < Dimension; i++)
|
||||
{
|
||||
if (iPiv[i] != i) coord *= -data[Dimension * i + i]; // the determinant sign changes on row swap.
|
||||
else coord *= data[Dimension * i + i];
|
||||
}
|
||||
normal[x] = coord;
|
||||
norm += coord * coord;
|
||||
}
|
||||
|
||||
// Normalize the result
|
||||
var f = 1.0f / sqrt(norm);
|
||||
for (var i = 0; i < normal.Length; i++) normal[i] *= f;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Simplex Volume
|
||||
/// <summary>
|
||||
/// Gets the simplex volume. Prior to having enough edge vectors, the method pads the remaining with all
|
||||
/// "other numbers". So, yes, this method is not really finding the volume. But a relative volume-like measure. It
|
||||
/// uses the magnitude of the determinant as the volume stand-in following the Cayley-Menger theorem.
|
||||
/// </summary>
|
||||
/// <param name="edgeVectors">The edge vectors.</param>
|
||||
/// <param name="lastIndex">The last index.</param>
|
||||
/// <param name="bigNumber">The big number.</param>
|
||||
/// <returns>System.Double.</returns>
|
||||
internal float GetSimplexVolume(float[][] edgeVectors, int lastIndex, float bigNumber)
|
||||
{
|
||||
var A = new float[Dimension * Dimension];
|
||||
var index = 0;
|
||||
for (int i = 0; i < Dimension; i++)
|
||||
for (int j = 0; j < Dimension; j++)
|
||||
if (i <= lastIndex)
|
||||
A[index++] = edgeVectors[i][j];
|
||||
else A[index] = (pow(-1, index) * index++) / bigNumber;
|
||||
// this last term is used for all the vertices in the comparison for the yet determined vertices
|
||||
// the idea is to come up with sets of numbers that are orthogonal so that an non-zero value will result
|
||||
// and to choose smallish numbers since the choice of vectors will affect what the end volume is.
|
||||
// A better way (todo?) is to solve a smaller matrix. However, cases were found in which the obvious smaller vector
|
||||
// (the upper left) had too many zeros. So, one would need to find the right subset. Indeed choosing a subset
|
||||
// biases the first dimensions of the others. Perhaps a larger volume would be created from a different vertex
|
||||
// if another subset of dimensions were used.
|
||||
return abs(DeterminantDestructive(A));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determinants the destructive.
|
||||
/// </summary>
|
||||
/// <param name="A">a.</param>
|
||||
/// <returns>System.Double.</returns>
|
||||
private float DeterminantDestructive(float[] A)
|
||||
{
|
||||
switch (Dimension)
|
||||
{
|
||||
case 0:
|
||||
return 0.0f;
|
||||
case 1:
|
||||
return A[0];
|
||||
case 2:
|
||||
return A[0] * A[3] - A[1] * A[2];
|
||||
case 3:
|
||||
return A[0] * A[4] * A[8] + A[1] * A[5] * A[6] + A[2] * A[3] * A[7]
|
||||
- A[0] * A[5] * A[7] - A[1] * A[3] * A[8] - A[2] * A[4] * A[6];
|
||||
default:
|
||||
{
|
||||
var iPiv = new int[Dimension];
|
||||
var helper = new float[Dimension];
|
||||
LUFactor(A, Dimension, iPiv, helper);
|
||||
var det = 1.0f;
|
||||
for (var i = 0; i < iPiv.Length; i++)
|
||||
{
|
||||
det *= A[Dimension * i + i];
|
||||
if (iPiv[i] != i) det *= -1; // the determinant sign changes on row swap.
|
||||
}
|
||||
return det;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
// Modified from Math.NET
|
||||
// Copyright (c) 2009-2013 Math.NET
|
||||
/// <summary>
|
||||
/// Lus the factor.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="order">The order.</param>
|
||||
/// <param name="ipiv">The ipiv.</param>
|
||||
/// <param name="vecLUcolj">The vec l ucolj.</param>
|
||||
private static void LUFactor(float[] data, int order, int[] ipiv, float[] vecLUcolj)
|
||||
{
|
||||
// Initialize the pivot matrix to the identity permutation.
|
||||
for (var i = 0; i < order; i++)
|
||||
{
|
||||
ipiv[i] = i;
|
||||
}
|
||||
|
||||
// Outer loop.
|
||||
for (var j = 0; j < order; j++)
|
||||
{
|
||||
var indexj = j * order;
|
||||
var indexjj = indexj + j;
|
||||
|
||||
// Make a copy of the j-th column to localize references.
|
||||
for (var i = 0; i < order; i++)
|
||||
{
|
||||
vecLUcolj[i] = data[indexj + i];
|
||||
}
|
||||
|
||||
// Apply previous transformations.
|
||||
for (var i = 0; i < order; i++)
|
||||
{
|
||||
// Most of the time is spent in the following dot product.
|
||||
var kmax = Math.Min(i, j);
|
||||
var s = 0.0f;
|
||||
for (var k = 0; k < kmax; k++)
|
||||
{
|
||||
s += data[k * order + i] * vecLUcolj[k];
|
||||
}
|
||||
|
||||
data[indexj + i] = vecLUcolj[i] -= s;
|
||||
}
|
||||
|
||||
// Find pivot and exchange if necessary.
|
||||
var p = j;
|
||||
for (var i = j + 1; i < order; i++)
|
||||
{
|
||||
if (abs(vecLUcolj[i]) > abs(vecLUcolj[p]))
|
||||
{
|
||||
p = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (p != j)
|
||||
{
|
||||
for (var k = 0; k < order; k++)
|
||||
{
|
||||
var indexk = k * order;
|
||||
var indexkp = indexk + p;
|
||||
var indexkj = indexk + j;
|
||||
var temp = data[indexkp];
|
||||
data[indexkp] = data[indexkj];
|
||||
data[indexkj] = temp;
|
||||
}
|
||||
|
||||
ipiv[j] = p;
|
||||
}
|
||||
|
||||
// Compute multipliers.
|
||||
if (j < order & data[indexjj] != 0.0f)
|
||||
{
|
||||
for (var i = j + 1; i < order; i++)
|
||||
{
|
||||
data[indexj + i] /= data[indexjj];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8bb3327041efa18479319ec117493f8c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
using System;
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// A helper class for object allocation/storage.
|
||||
/// This helps the GC a lot as it prevents the creation of about 75% of
|
||||
/// new face objects (in the case of ConvexFaceInternal). In the case of
|
||||
/// FaceConnectors and DefferedFaces, the difference is even higher (in most
|
||||
/// cases O(1) vs O(number of created faces)).
|
||||
/// </summary>
|
||||
internal class ObjectManager
|
||||
{
|
||||
/// <summary>
|
||||
/// The dimension
|
||||
/// </summary>
|
||||
private readonly int Dimension;
|
||||
/// <summary>
|
||||
/// The connector stack
|
||||
/// </summary>
|
||||
private FaceConnector ConnectorStack;
|
||||
/// <summary>
|
||||
/// The deferred face stack
|
||||
/// </summary>
|
||||
private readonly SimpleList<DeferredFace> DeferredFaceStack;
|
||||
/// <summary>
|
||||
/// The empty buffer stack
|
||||
/// </summary>
|
||||
private readonly SimpleList<IndexBuffer> EmptyBufferStack;
|
||||
/// <summary>
|
||||
/// The face pool
|
||||
/// </summary>
|
||||
private ConvexFaceInternal[] FacePool;
|
||||
/// <summary>
|
||||
/// The face pool size
|
||||
/// </summary>
|
||||
private int FacePoolSize;
|
||||
/// <summary>
|
||||
/// The face pool capacity
|
||||
/// </summary>
|
||||
private int FacePoolCapacity;
|
||||
/// <summary>
|
||||
/// The free face indices
|
||||
/// </summary>
|
||||
private readonly IndexBuffer FreeFaceIndices;
|
||||
|
||||
/// <summary>
|
||||
/// The hull
|
||||
/// </summary>
|
||||
private readonly ConvexHullAlgorithm Hull;
|
||||
|
||||
/// <summary>
|
||||
/// Create the manager.
|
||||
/// </summary>
|
||||
/// <param name="hull">The hull.</param>
|
||||
public ObjectManager(ConvexHullAlgorithm hull)
|
||||
{
|
||||
Dimension = hull.NumOfDimensions;
|
||||
Hull = hull;
|
||||
FacePool = hull.FacePool;
|
||||
FacePoolSize = 0;
|
||||
FacePoolCapacity = hull.FacePool.Length;
|
||||
FreeFaceIndices = new IndexBuffer();
|
||||
|
||||
EmptyBufferStack = new SimpleList<IndexBuffer>();
|
||||
DeferredFaceStack = new SimpleList<DeferredFace>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the face to the pool for later use.
|
||||
/// </summary>
|
||||
/// <param name="faceIndex">Index of the face.</param>
|
||||
public void DepositFace(int faceIndex)
|
||||
{
|
||||
var face = FacePool[faceIndex];
|
||||
var af = face.AdjacentFaces;
|
||||
for (var i = 0; i < af.Length; i++)
|
||||
{
|
||||
af[i] = -1;
|
||||
}
|
||||
FreeFaceIndices.Push(faceIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reallocate the face pool, including the AffectedFaceFlags
|
||||
/// </summary>
|
||||
private void ReallocateFacePool()
|
||||
{
|
||||
var newPool = new ConvexFaceInternal[2*FacePoolCapacity];
|
||||
var newTags = new bool[2*FacePoolCapacity];
|
||||
Array.Copy(FacePool, newPool, FacePoolCapacity);
|
||||
Buffer.BlockCopy(Hull.AffectedFaceFlags, 0, newTags, 0, FacePoolCapacity*sizeof (bool));
|
||||
FacePoolCapacity = 2*FacePoolCapacity;
|
||||
Hull.FacePool = newPool;
|
||||
FacePool = newPool;
|
||||
Hull.AffectedFaceFlags = newTags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new face and put it in the pool.
|
||||
/// </summary>
|
||||
/// <returns>System.Int32.</returns>
|
||||
private int CreateFace()
|
||||
{
|
||||
var index = FacePoolSize;
|
||||
var face = new ConvexFaceInternal(Dimension, index, GetVertexBuffer());
|
||||
FacePoolSize++;
|
||||
if (FacePoolSize > FacePoolCapacity) ReallocateFacePool();
|
||||
FacePool[index] = face;
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return index of an unused face or creates a new one.
|
||||
/// </summary>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public int GetFace()
|
||||
{
|
||||
if (FreeFaceIndices.Count > 0) return FreeFaceIndices.Pop();
|
||||
return CreateFace();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store a face connector in the "embedded" linked list.
|
||||
/// </summary>
|
||||
/// <param name="connector">The connector.</param>
|
||||
public void DepositConnector(FaceConnector connector)
|
||||
{
|
||||
if (ConnectorStack == null)
|
||||
{
|
||||
connector.Next = null;
|
||||
ConnectorStack = connector;
|
||||
}
|
||||
else
|
||||
{
|
||||
connector.Next = ConnectorStack;
|
||||
ConnectorStack = connector;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an unused face connector. If none is available, create it.
|
||||
/// </summary>
|
||||
/// <returns>FaceConnector.</returns>
|
||||
public FaceConnector GetConnector()
|
||||
{
|
||||
if (ConnectorStack == null) return new FaceConnector(Dimension);
|
||||
|
||||
var ret = ConnectorStack;
|
||||
ConnectorStack = ConnectorStack.Next;
|
||||
ret.Next = null;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deposit the index buffer.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
public void DepositVertexBuffer(IndexBuffer buffer)
|
||||
{
|
||||
buffer.Clear();
|
||||
EmptyBufferStack.Push(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a store index buffer or create a new instance.
|
||||
/// </summary>
|
||||
/// <returns>IndexBuffer.</returns>
|
||||
public IndexBuffer GetVertexBuffer()
|
||||
{
|
||||
return EmptyBufferStack.Count != 0 ? EmptyBufferStack.Pop() : new IndexBuffer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deposit the deferred face.
|
||||
/// </summary>
|
||||
/// <param name="face">The face.</param>
|
||||
public void DepositDeferredFace(DeferredFace face)
|
||||
{
|
||||
DeferredFaceStack.Push(face);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the deferred face.
|
||||
/// </summary>
|
||||
/// <returns>DeferredFace.</returns>
|
||||
public DeferredFace GetDeferredFace()
|
||||
{
|
||||
return DeferredFaceStack.Count != 0 ? DeferredFaceStack.Pop() : new DeferredFace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e8d582e97c6824740ba9e1a41641b372
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using System.Collections;
|
||||
using MIConvexHull;
|
||||
|
||||
public class Face3 : ConvexFace<Vertex3, Face3>
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 30a2635ffe51545499499ce392b234c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface for a structure with nD position.
|
||||
/// </summary>
|
||||
public interface IVertex
|
||||
{
|
||||
/// <summary>
|
||||
/// Position of the vertex.
|
||||
/// </summary>
|
||||
/// <value>The position.</value>
|
||||
float[] Position { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "Default" vertex.
|
||||
/// </summary>
|
||||
/// <seealso cref="MIConvexHull.IVertex" />
|
||||
public class DefaultVertex : IVertex
|
||||
{
|
||||
/// <summary>
|
||||
/// Position of the vertex.
|
||||
/// </summary>
|
||||
/// <value>The position.</value>
|
||||
public float[] Position { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a55f713690988bb48aef7cbd4f01ebc7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple interface to unify different types of triangulations in the future.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TCell">The type of the t cell.</typeparam>
|
||||
public interface ITriangulation<TVertex, TCell>
|
||||
where TCell : TriangulationCell<TVertex, TCell>, new()
|
||||
where TVertex : IVertex
|
||||
{
|
||||
/// <summary>
|
||||
/// Triangulation simplexes. For 2D - triangles, 3D - tetrahedrons, etc ...
|
||||
/// </summary>
|
||||
/// <value>The cells.</value>
|
||||
IEnumerable<TCell> Cells { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory class for creating triangulations.
|
||||
/// </summary>
|
||||
public static class Triangulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the Delaunay triangulation of the input data.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>ITriangulation<TVertex, DefaultTriangulationCell<TVertex>>.</returns>
|
||||
public static ITriangulation<TVertex, DefaultTriangulationCell<TVertex>> CreateDelaunay<TVertex>(
|
||||
IList<TVertex> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
where TVertex : IVertex
|
||||
{
|
||||
return DelaunayTriangulation<TVertex, DefaultTriangulationCell<TVertex>>.Create(data, PlaneDistanceTolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the Delaunay triangulation of the input data.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>ITriangulation<DefaultVertex, DefaultTriangulationCell<DefaultVertex>>.</returns>
|
||||
public static ITriangulation<DefaultVertex, DefaultTriangulationCell<DefaultVertex>> CreateDelaunay(
|
||||
IList<float[]> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
{
|
||||
var points = data.Select(p => new DefaultVertex { Position = p }).ToList();
|
||||
return DelaunayTriangulation<DefaultVertex, DefaultTriangulationCell<DefaultVertex>>.Create(points, PlaneDistanceTolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the Delaunay triangulation of the input data.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TFace">The type of the t face.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>ITriangulation<TVertex, TFace>.</returns>
|
||||
public static ITriangulation<TVertex, TFace> CreateDelaunay<TVertex, TFace>(IList<TVertex> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
where TVertex : IVertex
|
||||
where TFace : TriangulationCell<TVertex, TFace>, new()
|
||||
{
|
||||
return DelaunayTriangulation<TVertex, TFace>.Create(data, PlaneDistanceTolerance);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create the voronoi mesh.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TCell">The type of the t cell.</typeparam>
|
||||
/// <typeparam name="TEdge">The type of the t edge.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>VoronoiMesh<TVertex, TCell, TEdge>.</returns>
|
||||
public static VoronoiMesh<TVertex, TCell, TEdge> CreateVoronoi<TVertex, TCell, TEdge>(IList<TVertex> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
where TCell : TriangulationCell<TVertex, TCell>, new()
|
||||
where TVertex : IVertex
|
||||
where TEdge : VoronoiEdge<TVertex, TCell>, new()
|
||||
{
|
||||
return VoronoiMesh<TVertex, TCell, TEdge>.Create(data, PlaneDistanceTolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the voronoi mesh.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>VoronoiMesh<TVertex, DefaultTriangulationCell<TVertex>, VoronoiEdge<TVertex, DefaultTriangulationCell<TVertex>>>.</returns>
|
||||
public static
|
||||
VoronoiMesh
|
||||
<TVertex, DefaultTriangulationCell<TVertex>, VoronoiEdge<TVertex, DefaultTriangulationCell<TVertex>>>
|
||||
CreateVoronoi<TVertex>(IList<TVertex> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
where TVertex : IVertex
|
||||
{
|
||||
return
|
||||
VoronoiMesh
|
||||
<TVertex, DefaultTriangulationCell<TVertex>, VoronoiEdge<TVertex, DefaultTriangulationCell<TVertex>>
|
||||
>.Create(data, PlaneDistanceTolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the voronoi mesh.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>VoronoiMesh<DefaultVertex, DefaultTriangulationCell<DefaultVertex>, VoronoiEdge<DefaultVertex, DefaultTriangulationCell<DefaultVertex>>>.</returns>
|
||||
public static
|
||||
VoronoiMesh
|
||||
<DefaultVertex, DefaultTriangulationCell<DefaultVertex>,
|
||||
VoronoiEdge<DefaultVertex, DefaultTriangulationCell<DefaultVertex>>>
|
||||
CreateVoronoi(IList<float[]> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
{
|
||||
var points = data.Select(p => new DefaultVertex { Position = p.ToArray() }).ToList();
|
||||
return
|
||||
VoronoiMesh
|
||||
<DefaultVertex, DefaultTriangulationCell<DefaultVertex>,
|
||||
VoronoiEdge<DefaultVertex, DefaultTriangulationCell<DefaultVertex>>>.Create(points, PlaneDistanceTolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the voronoi mesh.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TCell">The type of the t cell.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>VoronoiMesh<TVertex, TCell, VoronoiEdge<TVertex, TCell>>.</returns>
|
||||
public static VoronoiMesh<TVertex, TCell, VoronoiEdge<TVertex, TCell>> CreateVoronoi<TVertex, TCell>(
|
||||
IList<TVertex> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
where TVertex : IVertex
|
||||
where TCell : TriangulationCell<TVertex, TCell>, new()
|
||||
{
|
||||
return VoronoiMesh<TVertex, TCell, VoronoiEdge<TVertex, TCell>>.Create(data, PlaneDistanceTolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0762bfe861ed97749a222fb1c217788a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5bf99a94223c6e1458c243cb68091852
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/*
|
||||
* Code here handles triangulation related stuff.
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Class ConvexHullAlgorithm.
|
||||
/// </summary>
|
||||
internal partial class ConvexHullAlgorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the Delaunay triangulation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TCell">The type of the t cell.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance.</param>
|
||||
/// <returns>TCell[].</returns>
|
||||
internal static TCell[] GetDelaunayTriangulation<TVertex, TCell>(IList<TVertex> data,
|
||||
float PlaneDistanceTolerance )
|
||||
where TCell : TriangulationCell<TVertex, TCell>, new()
|
||||
where TVertex : IVertex
|
||||
{
|
||||
var ch = new ConvexHullAlgorithm(data.Cast<IVertex>().ToArray(), true, PlaneDistanceTolerance);
|
||||
ch.GetConvexHull();
|
||||
ch.RemoveUpperFaces();
|
||||
return ch.GetConvexFaces<TVertex, TCell>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes up facing Tetrahedrons from the triangulation.
|
||||
/// </summary>
|
||||
private void RemoveUpperFaces()
|
||||
{
|
||||
var delaunayFaces = ConvexFaces;
|
||||
var dimension = NumOfDimensions - 1;
|
||||
|
||||
// Remove the "upper" faces
|
||||
for (var i = delaunayFaces.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var candidateIndex = delaunayFaces[i];
|
||||
var candidate = FacePool[candidateIndex];
|
||||
if (candidate.Normal[dimension] >= 0.0)
|
||||
{
|
||||
for (var fi = 0; fi < candidate.AdjacentFaces.Length; fi++)
|
||||
{
|
||||
var af = candidate.AdjacentFaces[fi];
|
||||
if (af >= 0)
|
||||
{
|
||||
var face = FacePool[af];
|
||||
for (var j = 0; j < face.AdjacentFaces.Length; j++)
|
||||
{
|
||||
if (face.AdjacentFaces[j] == candidateIndex)
|
||||
{
|
||||
face.AdjacentFaces[j] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delaunayFaces[i] = delaunayFaces[delaunayFaces.Count - 1];
|
||||
delaunayFaces.Pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: de8661d86f2667647b9dca59c1c97e9a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculation and representation of Delaunay triangulation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TCell">The type of the t cell.</typeparam>
|
||||
/// <seealso cref="MIConvexHull.ITriangulation{TVertex, TCell}" />
|
||||
public class DelaunayTriangulation<TVertex, TCell> : ITriangulation<TVertex, TCell>
|
||||
where TCell : TriangulationCell<TVertex, TCell>, new()
|
||||
where TVertex : IVertex
|
||||
{
|
||||
/// <summary>
|
||||
/// Can only be created using a factory method.
|
||||
/// </summary>
|
||||
private DelaunayTriangulation()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cells of the triangulation.
|
||||
/// </summary>
|
||||
/// <value>The cells.</value>
|
||||
public IEnumerable<TCell> Cells { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates the Delaunay triangulation of the input data.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>DelaunayTriangulation<TVertex, TCell>.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">data</exception>
|
||||
/// <exception cref="ArgumentNullException">data</exception>
|
||||
public static DelaunayTriangulation<TVertex, TCell> Create(IList<TVertex> data,
|
||||
float PlaneDistanceTolerance)// = Constants.DefaultPlaneDistanceTolerance)
|
||||
{
|
||||
if (data == null) throw new ArgumentNullException("data");
|
||||
if (data.Count == 0) return new DelaunayTriangulation<TVertex, TCell> { Cells = new TCell[0] };
|
||||
|
||||
var cells = ConvexHullAlgorithm.GetDelaunayTriangulation<TVertex, TCell>(data, PlaneDistanceTolerance);
|
||||
|
||||
return new DelaunayTriangulation<TVertex, TCell> { Cells = cells };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4b0e872e78d2ab8428b446cfa5c06580
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// Representation of the triangulation cell. Pretty much the same as ConvexFace,
|
||||
/// just wanted to distinguish the two.
|
||||
/// To declare your own face type, use class Face : DelaunayFace(of Vertex, of Face)
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TCell">The type of the t cell.</typeparam>
|
||||
/// <seealso cref="MIConvexHull.ConvexFace{TVertex, TCell}" />
|
||||
public abstract class TriangulationCell<TVertex, TCell> : ConvexFace<TVertex, TCell>
|
||||
where TVertex : IVertex
|
||||
where TCell : ConvexFace<TVertex, TCell>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default triangulation cell.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
public class DefaultTriangulationCell<TVertex> : TriangulationCell<TVertex, DefaultTriangulationCell<TVertex>>
|
||||
where TVertex : IVertex
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 808182249e0e66143b65945d9aeb37fe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing an (undirected) edge of the Voronoi graph.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TCell">The type of the t cell.</typeparam>
|
||||
public class VoronoiEdge<TVertex, TCell>
|
||||
where TVertex : IVertex
|
||||
where TCell : TriangulationCell<TVertex, TCell>
|
||||
{
|
||||
/// <summary>
|
||||
/// Create an instance of the edge.
|
||||
/// </summary>
|
||||
public VoronoiEdge()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an instance of the edge.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
public VoronoiEdge(TCell source, TCell target)
|
||||
{
|
||||
Source = source;
|
||||
Target = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source of the edge.
|
||||
/// </summary>
|
||||
/// <value>The source.</value>
|
||||
public TCell Source { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Target of the edge.
|
||||
/// </summary>
|
||||
/// <value>The target.</value>
|
||||
public TCell Target { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// ...
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current object.</param>
|
||||
/// <returns><c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as VoronoiEdge<TVertex, TCell>;
|
||||
if (other == null) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return (Source == other.Source && Target == other.Target)
|
||||
|| (Source == other.Target && Target == other.Source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ...
|
||||
/// </summary>
|
||||
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = 23;
|
||||
hash = hash*31 + Source.GetHashCode();
|
||||
return hash*31 + Target.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2f5c388c78c121a47b14c2f4ed26af38
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
/******************************************************************************
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MIConvexHull
|
||||
{
|
||||
/// <summary>
|
||||
/// A factory class for creating a Voronoi mesh.
|
||||
/// </summary>
|
||||
public static class VoronoiMesh
|
||||
{
|
||||
/// <summary>
|
||||
/// Create the voronoi mesh.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TCell">The type of the t cell.</typeparam>
|
||||
/// <typeparam name="TEdge">The type of the t edge.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>VoronoiMesh<TVertex, TCell, TEdge>.</returns>
|
||||
public static VoronoiMesh<TVertex, TCell, TEdge> Create<TVertex, TCell, TEdge>(IList<TVertex> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
where TCell : TriangulationCell<TVertex, TCell>, new()
|
||||
where TVertex : IVertex
|
||||
where TEdge : VoronoiEdge<TVertex, TCell>, new()
|
||||
{
|
||||
return VoronoiMesh<TVertex, TCell, TEdge>.Create(data, PlaneDistanceTolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the voronoi mesh.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>VoronoiMesh<TVertex, DefaultTriangulationCell<TVertex>, VoronoiEdge<TVertex, DefaultTriangulationCell<TVertex>>>.</returns>
|
||||
public static
|
||||
VoronoiMesh
|
||||
<TVertex, DefaultTriangulationCell<TVertex>, VoronoiEdge<TVertex, DefaultTriangulationCell<TVertex>>>
|
||||
Create<TVertex>(IList<TVertex> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
where TVertex : IVertex
|
||||
{
|
||||
return
|
||||
VoronoiMesh
|
||||
<TVertex, DefaultTriangulationCell<TVertex>, VoronoiEdge<TVertex, DefaultTriangulationCell<TVertex>>
|
||||
>.Create(data, PlaneDistanceTolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the voronoi mesh.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>VoronoiMesh<DefaultVertex, DefaultTriangulationCell<DefaultVertex>, VoronoiEdge<DefaultVertex, DefaultTriangulationCell<DefaultVertex>>>.</returns>
|
||||
public static
|
||||
VoronoiMesh
|
||||
<DefaultVertex, DefaultTriangulationCell<DefaultVertex>,
|
||||
VoronoiEdge<DefaultVertex, DefaultTriangulationCell<DefaultVertex>>>
|
||||
Create(IList<float[]> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
{
|
||||
var points = data.Select(p => new DefaultVertex { Position = p.ToArray() }).ToList();
|
||||
return
|
||||
VoronoiMesh
|
||||
<DefaultVertex, DefaultTriangulationCell<DefaultVertex>,
|
||||
VoronoiEdge<DefaultVertex, DefaultTriangulationCell<DefaultVertex>>>.Create(points, PlaneDistanceTolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the voronoi mesh.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TCell">The type of the t cell.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>VoronoiMesh<TVertex, TCell, VoronoiEdge<TVertex, TCell>>.</returns>
|
||||
public static VoronoiMesh<TVertex, TCell, VoronoiEdge<TVertex, TCell>> Create<TVertex, TCell>(
|
||||
IList<TVertex> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
where TVertex : IVertex
|
||||
where TCell : TriangulationCell<TVertex, TCell>, new()
|
||||
{
|
||||
return VoronoiMesh<TVertex, TCell, VoronoiEdge<TVertex, TCell>>.Create(data, PlaneDistanceTolerance);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A representation of a voronoi mesh.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEdge">The type of the t edge.</typeparam>
|
||||
/// <typeparam name="TVertex">The type of the t vertex.</typeparam>
|
||||
/// <typeparam name="TCell">The type of the t cell.</typeparam>
|
||||
public class VoronoiMesh<TVertex, TCell, TEdge>
|
||||
where TCell : TriangulationCell<TVertex, TCell>, new()
|
||||
where TVertex : IVertex
|
||||
where TEdge : VoronoiEdge<TVertex, TCell>, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Can only be created using a factory method.
|
||||
/// </summary>
|
||||
private VoronoiMesh()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vertices of the diagram.
|
||||
/// </summary>
|
||||
/// <value>The vertices.</value>
|
||||
public IEnumerable<TCell> Vertices { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Edges connecting the cells.
|
||||
/// The same information can be retrieved Cells' Adjacency.
|
||||
/// </summary>
|
||||
/// <value>The edges.</value>
|
||||
public IEnumerable<TEdge> Edges { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a Voronoi diagram of the input data.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="PlaneDistanceTolerance">The plane distance tolerance (default is 1e-10). If too high, points
|
||||
/// will be missed. If too low, the algorithm may break. Only adjust if you notice problems.</param>
|
||||
/// <returns>VoronoiMesh<TVertex, TCell, TEdge>.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">data</exception>
|
||||
/// <exception cref="ArgumentNullException">data</exception>
|
||||
public static VoronoiMesh<TVertex, TCell, TEdge> Create(IList<TVertex> data,
|
||||
float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance)
|
||||
{
|
||||
if (data == null) throw new ArgumentNullException("data");
|
||||
|
||||
var t = DelaunayTriangulation<TVertex, TCell>.Create(data, PlaneDistanceTolerance);
|
||||
var vertices = t.Cells.ToList();
|
||||
var edges = new HashSet<TEdge>(new EdgeComparer());
|
||||
|
||||
foreach (var f in vertices)
|
||||
{
|
||||
for (var i = 0; i < f.Adjacency.Length; i++)
|
||||
{
|
||||
var af = f.Adjacency[i];
|
||||
if (af != null) edges.Add(new TEdge { Source = f, Target = af });
|
||||
}
|
||||
}
|
||||
|
||||
return new VoronoiMesh<TVertex, TCell, TEdge>
|
||||
{
|
||||
Vertices = vertices,
|
||||
Edges = edges.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is probably not needed, but might make things a tiny bit faster.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.Collections.Generic.IEqualityComparer{TEdge}" />
|
||||
private class EdgeComparer : IEqualityComparer<TEdge>
|
||||
{
|
||||
/// <summary>
|
||||
/// Equals the specified x.
|
||||
/// </summary>
|
||||
/// <param name="x">The x.</param>
|
||||
/// <param name="y">The y.</param>
|
||||
/// <returns>System.Boolean.</returns>
|
||||
public bool Equals(TEdge x, TEdge y)
|
||||
{
|
||||
return (x.Source == y.Source && x.Target == y.Target) || (x.Source == y.Target && x.Target == y.Source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this instance.
|
||||
/// </summary>
|
||||
/// <param name="obj">The <see cref="T:System.Object" /> for which a hash code is to be returned.</param>
|
||||
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
|
||||
public int GetHashCode(TEdge obj)
|
||||
{
|
||||
return obj.Source.GetHashCode() ^ obj.Target.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fbb5750f20479844eb4e504302755db9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using MIConvexHull;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
|
||||
public class Vertex2 : IVertex
|
||||
{
|
||||
public float[] Position => new[] {StoredPosition.x, StoredPosition.y};
|
||||
|
||||
public float2 StoredPosition;
|
||||
|
||||
public float x { get { return StoredPosition.x; } }
|
||||
public float y { get { return StoredPosition.y; } }
|
||||
|
||||
public Vertex2(float x, float y)
|
||||
{
|
||||
StoredPosition = float2(x, y);
|
||||
}
|
||||
|
||||
public Vertex2(float2 pos)
|
||||
{
|
||||
StoredPosition = pos;
|
||||
}
|
||||
|
||||
public float2 ToFloat2() {
|
||||
return StoredPosition;
|
||||
}
|
||||
|
||||
public float Distance(Vertex2 v)
|
||||
{
|
||||
return length(StoredPosition-v.StoredPosition);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 67d3ec22ea52a2642a14ff864e3017b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
using CultMath;
|
||||
|
||||
public class Vertex2<T> : Vertex2
|
||||
{
|
||||
public T StoredObject { get; }
|
||||
|
||||
public Vertex2(float x, float y, T obj) : base(x, y)
|
||||
{
|
||||
StoredObject = obj;
|
||||
}
|
||||
|
||||
public Vertex2(float2 pos, T obj) : base(pos)
|
||||
{
|
||||
StoredObject = obj;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e42612a273ec490380eaa67bed216a6d
|
||||
timeCreated: 1615418804
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
using System.Collections;
|
||||
using MIConvexHull;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
|
||||
public class Vertex3 : IVertex
|
||||
{
|
||||
public float[] Position => new[] {StoredPosition.x, StoredPosition.y};
|
||||
|
||||
public float3 StoredPosition;
|
||||
|
||||
public Vertex3(float x, float y, float z)
|
||||
{
|
||||
StoredPosition = float3(x, y, z);
|
||||
}
|
||||
|
||||
public Vertex3(float3 pos)
|
||||
{
|
||||
StoredPosition = pos;
|
||||
}
|
||||
|
||||
public float3 ToFloat3() {
|
||||
return StoredPosition;
|
||||
}
|
||||
|
||||
public float Distance(Vertex3 v)
|
||||
{
|
||||
return length(StoredPosition-v.StoredPosition);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3553f231414f28347996a803a79425fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -10,7 +10,7 @@ using static CultMath.math;
|
|||
public class PlayerSettings
|
||||
{
|
||||
[Key(0)] public string Name = "Anonymous";
|
||||
[Key(2)] public bool TutorialPassed;
|
||||
// Keys 1 and 2 are retired; an older player.cc that still carries them loads, and they are dropped.
|
||||
[Key(3)] public Dictionary<string, string> HashedStoryFiles = new Dictionary<string, string>();
|
||||
[Key(4)] public PlayerGameplaySettings GameplaySettings = new PlayerGameplaySettings();
|
||||
[Key(5)] public PlayerInputSettings InputSettings = new PlayerInputSettings();
|
||||
|
|
|
|||
|
|
@ -1,131 +1,65 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
using Random = CultMath.Random;
|
||||
|
||||
// The owner of what a new run starts with (docs/scenarios-cut.md, 2.2): how the arena zone is generated, the player
|
||||
// ship, and a scenario's other entities with their stance, pilot and cargo. A plain New Game is the absent-scenario
|
||||
// case of the same path. Nothing else generates, unpacks or admits a new run's player; everything it stages joins the
|
||||
// arena through Zone.Admit. It holds no Unity type.
|
||||
// The owner of a new run (docs/scenarios-cut.md, R.4). Every new run is a scenario: RunStart generates its galaxy,
|
||||
// generates its arena (the galaxy's entrance zone) under the scenario's Ambient rule, and admits what the scenario
|
||||
// stages through Zone.Admit, all of it or none of it. A scenario only declares; nothing else generates a new run's
|
||||
// galaxy, generates or admits its player, or chooses its hull or faction. It holds no Unity type.
|
||||
public static class RunStart
|
||||
{
|
||||
public sealed class Staged
|
||||
{
|
||||
public Ship Player;
|
||||
public List<Entity> Entities; // the scenario's entities, in its order; empty with no scenario
|
||||
public List<Entity> Entities; // what the scenario placed besides the player, in its order
|
||||
}
|
||||
|
||||
// The arena is the galaxy's entrance zone, generated under the scenario's Ambient rule; with no scenario, ambient.
|
||||
public static ZonePack GenerateArena(ItemManager itemManager, ZoneGenerationSettings zoneSettings, Galaxy galaxy, Scenario scenario) =>
|
||||
galaxy.Entrance.PackedContents = ZoneGenerator.GenerateZone(
|
||||
itemManager, zoneSettings, galaxy, galaxy.Entrance, galaxy.IsPrelude, scenario?.Ambient ?? true);
|
||||
|
||||
// The check entry: every failure staging this scenario would hit, found with no zone. With no galaxy there is no
|
||||
// availability to filter by (LoadoutGenerator.IsAvailable), and a scenario's arena is a prelude, which offers every
|
||||
// product anyway. It touches no store, so the menu can call it before it clears the saved run.
|
||||
public static List<string> Check(ItemManager itemManager, Scenario scenario)
|
||||
// The scenario's galaxy, at the scenario's seed or, when that is zero, the clock's.
|
||||
public static Galaxy Generate(Scenario scenario, GalaxyStage stage)
|
||||
{
|
||||
var failures = new List<string>();
|
||||
if (scenario != null) Build(itemManager, null, scenario, _ => true, failures);
|
||||
return failures;
|
||||
stage.SeedFrom(scenario);
|
||||
return scenario.Generate(stage) ?? throw new InvalidOperationException($"Scenario {scenario.Name} generated no galaxy.");
|
||||
}
|
||||
|
||||
// Stages a new run into its arena, all-or-nothing: null, with every failure listed and the zone unchanged, unless
|
||||
// everything builds. With no scenario: a generated ship of the startingHull (any ship hull when it is empty) for
|
||||
// defaultFaction, at the origin. With one: its player and entities, each at its authored position and heading with
|
||||
// its cargo, each entity's stance set both ways with the player, and a Minion for each piloted one.
|
||||
public static ZonePack GenerateArena(ItemManager itemManager, ZoneGenerationSettings zoneSettings, Galaxy galaxy, Scenario scenario)
|
||||
{
|
||||
SeedItems(itemManager, galaxy, 1);
|
||||
return galaxy.Entrance.PackedContents = ZoneGenerator.GenerateZone(
|
||||
itemManager, zoneSettings, galaxy, galaxy.Entrance, galaxy.IsPrelude, scenario.Ambient);
|
||||
}
|
||||
|
||||
// Runs the scenario's Stage, then admits its player and everything it placed: each entity's stance set both ways
|
||||
// with the player, a pilot for each piloted one. Returns null, with every failure listed and the arena unchanged,
|
||||
// unless the whole scenario staged.
|
||||
public static Staged Stage(ItemManager itemManager, Zone arena, Scenario scenario, string startingHull,
|
||||
Faction defaultFaction, List<string> failures)
|
||||
{
|
||||
if (scenario == null)
|
||||
{
|
||||
var generator = new LoadoutGenerator(ref itemManager.Random, itemManager, arena.Galaxy, arena.GalaxyZone, defaultFaction, 2);
|
||||
var pack = generator.GenerateShipLoadout(data => string.IsNullOrEmpty(startingHull) || data.Name == startingHull);
|
||||
if (pack == null)
|
||||
{
|
||||
failures.Add($"player: no ship hull named {startingHull}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var ship = (Ship) EntitySerializer.Unpack(itemManager, arena, pack);
|
||||
ship.IsPlayerShip = true;
|
||||
ship.Position = float3.zero;
|
||||
arena.Admit(ship, piloted: false);
|
||||
return new Staged { Player = ship, Entities = new List<Entity>() };
|
||||
}
|
||||
|
||||
// Presets carry no faction, so availability is the galaxy's alone.
|
||||
var availability = new LoadoutGenerator(ref itemManager.Random, itemManager, arena.Galaxy, arena.GalaxyZone, null, 2);
|
||||
var built = Build(itemManager, arena, scenario, availability.IsAvailable, failures);
|
||||
if (built == null) return null;
|
||||
|
||||
var (player, entities) = built.Value;
|
||||
player.IsPlayerShip = true;
|
||||
arena.Admit(player, piloted: false);
|
||||
for (var i = 0; i < entities.Count; i++)
|
||||
{
|
||||
var hostile = scenario.Entities[i].Stance == ScenarioStance.Hostile;
|
||||
entities[i].SetIff(player, hostile);
|
||||
player.SetIff(entities[i], hostile);
|
||||
arena.Admit(entities[i], scenario.Entities[i].Piloted);
|
||||
}
|
||||
return new Staged { Player = player, Entities = entities };
|
||||
}
|
||||
|
||||
// Everything a scenario places, built and positioned but admitted nowhere; null when anything failed.
|
||||
private static (Ship player, List<Entity> entities)? Build(ItemManager itemManager, Zone zone, Scenario scenario,
|
||||
Predicate<FactionProductData> isAvailable, List<string> failures)
|
||||
TutorialGenerationSettings tutorialGenerationSettings, List<string> failures)
|
||||
{
|
||||
SeedItems(itemManager, arena.Galaxy, 2);
|
||||
var stage = new ScenarioStage(itemManager, arena, startingHull, tutorialGenerationSettings);
|
||||
scenario.Stage(stage);
|
||||
var reported = failures.Count;
|
||||
Entity player = null;
|
||||
if (scenario.Player == null) failures.Add("player: the scenario places no player");
|
||||
else
|
||||
{
|
||||
player = Place(itemManager, zone, scenario.Player, "player", isAvailable, failures);
|
||||
if (player != null && !(player is Ship)) failures.Add($"player: {player.Name} is not a ship hull");
|
||||
}
|
||||
|
||||
var entities = new List<Entity>();
|
||||
for (var i = 0; i < scenario.Entities.Count; i++)
|
||||
{
|
||||
var where = $"entity {i}";
|
||||
var entity = Place(itemManager, zone, scenario.Entities[i], where, isAvailable, failures);
|
||||
if (entity != null && scenario.Entities[i].Piloted && !(entity is Ship))
|
||||
failures.Add($"{where}: {entity.Name} is not a ship hull, so it cannot be piloted");
|
||||
entities.Add(entity);
|
||||
}
|
||||
return failures.Count > reported ? null : ((Ship) player, entities);
|
||||
}
|
||||
|
||||
private static Entity Place(ItemManager itemManager, Zone zone, ScenarioShip placed, string where,
|
||||
Predicate<FactionProductData> isAvailable, List<string> failures)
|
||||
{
|
||||
var loadout = itemManager.ItemData.Get(placed.Loadout);
|
||||
if (loadout == null)
|
||||
{
|
||||
failures.Add($"{where}: preset {placed.Loadout.Key} is not in the catalog");
|
||||
return null;
|
||||
}
|
||||
|
||||
var reported = failures.Count;
|
||||
var materialized = new List<string>();
|
||||
var entity = Loadouts.Materialize(itemManager, zone, loadout, isAvailable, materialized);
|
||||
failures.AddRange(materialized.Select(failure => $"{where} ({loadout.Name}): {failure}"));
|
||||
var cargo = placed.Cargo.Select(design => Loadouts.Resolve(itemManager, design, isAvailable, $"{where} cargo", failures)).ToList();
|
||||
failures.AddRange(stage.Failures);
|
||||
if (!stage.PlayerPlaced)
|
||||
failures.Add("player: the scenario places no player");
|
||||
if (failures.Count > reported) return null;
|
||||
|
||||
entity.Position.xz = placed.Position;
|
||||
if (lengthsq(placed.Direction) > 0) entity.Direction = normalize(placed.Direction);
|
||||
var bay = entity.CargoBays.FirstOrDefault();
|
||||
foreach (var build in cargo)
|
||||
var player = stage.PlayerShip;
|
||||
player.IsPlayerShip = true;
|
||||
arena.Admit(player, piloted: false);
|
||||
foreach (var placed in stage.Placed)
|
||||
{
|
||||
var item = build();
|
||||
if (bay == null || !bay.TryStore(item))
|
||||
failures.Add($"{where} cargo: no room for {itemManager.GetData(item).Name}");
|
||||
var hostile = placed.Stance == ScenarioStance.Hostile;
|
||||
placed.Entity.SetIff(player, hostile);
|
||||
player.SetIff(placed.Entity, hostile);
|
||||
arena.Admit(placed.Entity, placed.Piloted);
|
||||
}
|
||||
return failures.Count > reported ? null : entity;
|
||||
return new Staged { Player = player, Entities = stage.Placed.Select(placed => placed.Entity).ToList() };
|
||||
}
|
||||
|
||||
// A new run's item draws (each lot's quality, each generated fit) come from its galaxy's seed, not the process clock,
|
||||
// so a fixed seed fixes the arena's gear and the player's fit. Each step draws its own stream, so whatever runs
|
||||
// between them (populating the level) cannot shift it.
|
||||
private static void SeedItems(ItemManager itemManager, Galaxy galaxy, uint step) =>
|
||||
itemManager.Random = new Random(galaxy.Seed * 0x9E3779B1u + step);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public static class RunSave
|
|||
{
|
||||
// The live run as plain documents. Writes nothing.
|
||||
public static (SavedGame Game, SavedZone[] Zones) Capture(CultCache cache, Galaxy galaxy, Zone currentZone,
|
||||
Entity currentEntity, bool isTutorial, SavedActionBarBinding[] actionBar)
|
||||
Entity currentEntity, SavedActionBarBinding[] actionBar)
|
||||
{
|
||||
var factions = galaxy.HomeZones.Keys.ToArray();
|
||||
var game = new SavedGame
|
||||
|
|
@ -74,7 +74,7 @@ public static class RunSave
|
|||
CurrentZoneEntity = currentZone.Entities.IndexOf(currentEntity),
|
||||
Entrance = Array.IndexOf(galaxy.Zones, galaxy.Entrance),
|
||||
Exit = Array.IndexOf(galaxy.Zones, galaxy.Exit),
|
||||
IsTutorial = isTutorial,
|
||||
IsTutorial = galaxy.IsPrelude,
|
||||
ActionBarBindings = actionBar
|
||||
};
|
||||
|
||||
|
|
@ -155,17 +155,37 @@ public static class RunSave
|
|||
}
|
||||
|
||||
// Removes every run record (SavedGame, SavedZone, OrbitData, BodyData, ProvenanceLedger) in one Commit.
|
||||
public static void Clear(CultCache cache)
|
||||
public static void Clear(CultCache cache) => Remove(cache, Records(cache));
|
||||
|
||||
// A new run replaces the saved one only once it has started: start runs with the saved run still in the store.
|
||||
// When it returns a value, the saved run's records go. When it returns null or throws, the records it wrote go and
|
||||
// the saved run stays as it was.
|
||||
public static T Replace<T>(CultCache cache, Func<T> start) where T : class
|
||||
{
|
||||
var run = cache.AllStoredDocuments
|
||||
var saved = Records(cache);
|
||||
T started = null;
|
||||
try
|
||||
{
|
||||
started = start();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Remove(cache, started != null ? saved : Records(cache).Except(saved).ToArray());
|
||||
}
|
||||
return started;
|
||||
}
|
||||
|
||||
private static CultRecordKey[] Records(CultCache cache) =>
|
||||
cache.AllStoredDocuments
|
||||
.Where(stored => IsRunRecord(stored.Descriptor.DocumentType))
|
||||
.Select(stored => stored.Key)
|
||||
.ToArray();
|
||||
|
||||
private static void Remove(CultCache cache, CultRecordKey[] records) =>
|
||||
cache.Commit(batch =>
|
||||
{
|
||||
foreach (var key in run) batch.Remove(key);
|
||||
foreach (var key in records) batch.Remove(key);
|
||||
});
|
||||
}
|
||||
|
||||
public static bool IsRunRecord(Type documentType) =>
|
||||
AetheriaStores.RunTypes.Any(home => home.IsAssignableFrom(documentType));
|
||||
|
|
|
|||
|
|
@ -1,38 +1,28 @@
|
|||
using System.Collections.Generic;
|
||||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
using CultMath;
|
||||
|
||||
// A scenario is an arena whose conditions are present at spawn (docs/scenarios-cut.md): the player's ship and every
|
||||
// other entity already fitted and placed. Authored catalog data, read at runtime; RunStart stages it into a new run.
|
||||
// It sets conditions and nothing else: once staged, its entities are ordinary run state.
|
||||
[CultDocument("aetheria.scenario", "1"), MessagePackObject]
|
||||
public class Scenario
|
||||
// A scenario is one way to set up a new run (docs/scenarios-cut.md, R): a script, compiled with the game, that says
|
||||
// which galaxy to generate and what the run starts with. New Game lists them (Scenarios). RunStart runs one: it
|
||||
// generates the galaxy through GalaxyStage, the arena under Ambient, and admits what Stage placed through a
|
||||
// ScenarioStage. A scenario only declares; once staged, everything it placed is ordinary run state.
|
||||
public abstract class Scenario
|
||||
{
|
||||
[CultName, Key(0)] public string Name;
|
||||
[Key(1)] public string Brief; // the conditions in a line or two, shown in the menu
|
||||
[Key(2)] public uint Seed; // the arena galaxy derives from it, so every launch has one layout
|
||||
[Key(3)] public bool Ambient; // false: planets, orbits and stations, but no generated ship or turret
|
||||
[Key(4)] public ScenarioShip Player;
|
||||
[Key(5)] public List<ScenarioEntity> Entities = new List<ScenarioEntity>();
|
||||
}
|
||||
public abstract string Name { get; }
|
||||
|
||||
// A preset placed in the arena. A ship hull stages a Ship; a turret hull an orbital entity with no orbit, which stays
|
||||
// where it is put.
|
||||
[MessagePackObject]
|
||||
public class ScenarioShip
|
||||
{
|
||||
[Key(0)] public CultRecordRef<Loadout> Loadout;
|
||||
[Key(1)] public float2 Position; // zone xz
|
||||
[Key(2)] public float2 Direction; // zero keeps the entity's default heading
|
||||
[Key(3)] public List<CultRecordRef<EquippableItemData>> Cargo = new List<CultRecordRef<EquippableItemData>>(); // first cargo bay
|
||||
}
|
||||
// The conditions and what to verify in them, shown under the scenario's button.
|
||||
public abstract string Brief { get; }
|
||||
|
||||
[MessagePackObject]
|
||||
public class ScenarioEntity : ScenarioShip
|
||||
{
|
||||
[Key(4)] public ScenarioStance Stance; // applied both ways between this entity and the player
|
||||
[Key(5)] public bool Piloted; // a Minion flies it; otherwise it has no agent and stays put
|
||||
// Zero draws the seed from the clock, so each launch is a new galaxy. Anything else fixes the galaxy, its
|
||||
// background, its arena and every fit, each item's maker and workmanship included, so every launch has the same
|
||||
// layout.
|
||||
public virtual uint Seed => 0;
|
||||
|
||||
// False: the arena keeps its planets, orbits and stations but generates no ship or turret, so nothing wanders into
|
||||
// the scenario's conditions.
|
||||
public virtual bool Ambient => true;
|
||||
|
||||
// Which galaxy the run is in: stage.Main() or stage.Prelude().
|
||||
public abstract Galaxy Generate(GalaxyStage stage);
|
||||
|
||||
// What the run starts with: exactly one stage.Player, and anything else the scenario places.
|
||||
public abstract void Stage(ScenarioStage stage);
|
||||
}
|
||||
|
||||
public enum ScenarioStance
|
||||
|
|
|
|||
168
Assets/Scripts/ServerShared/ScenarioStage.cs
Normal file
168
Assets/Scripts/ServerShared/ScenarioStage.cs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
|
||||
// The run half of a scenario's vocabulary (docs/scenarios-cut.md, R.4). A scenario's Stage builds its fits and places
|
||||
// them here; nothing joins the arena until Stage returns with no failure, and then RunStart admits it all at once.
|
||||
// Every verb names what it could not do instead of throwing, so one bad name in a script lists every bad name.
|
||||
// Designs and hulls are named as the catalog names them. Only RunStart builds a stage.
|
||||
public sealed class ScenarioStage
|
||||
{
|
||||
private readonly ItemManager _items;
|
||||
private readonly Predicate<FactionProductData> _available;
|
||||
private readonly List<string> _failures = new List<string>();
|
||||
private readonly List<Placement> _placed = new List<Placement>();
|
||||
private int _placements;
|
||||
|
||||
internal ScenarioStage(ItemManager items, Zone arena, string startingHull, TutorialGenerationSettings tutorialGenerationSettings)
|
||||
{
|
||||
_items = items;
|
||||
Arena = arena;
|
||||
StartingHull = startingHull;
|
||||
TutorialGenerationSettings = tutorialGenerationSettings;
|
||||
// A fit names designs, not manufacturers, so what the galaxy offers decides which product builds each one.
|
||||
_available = new LoadoutGenerator(ref items.Random, items, arena.Galaxy, arena.GalaxyZone, null, 2).IsAvailable;
|
||||
}
|
||||
|
||||
public Galaxy Galaxy => Arena.Galaxy;
|
||||
public Zone Arena { get; }
|
||||
public string StartingHull { get; }
|
||||
public TutorialGenerationSettings TutorialGenerationSettings { get; }
|
||||
|
||||
internal IReadOnlyList<string> Failures => _failures;
|
||||
internal bool PlayerPlaced { get; private set; }
|
||||
internal Ship PlayerShip { get; private set; }
|
||||
internal IReadOnlyList<Placement> Placed => _placed;
|
||||
|
||||
internal sealed class Placement
|
||||
{
|
||||
public Entity Entity;
|
||||
public ScenarioStance Stance;
|
||||
public bool Piloted;
|
||||
}
|
||||
|
||||
// A hull fitted slot by slot: each slot a design, the hull cell it goes in, and its rotation.
|
||||
public ScenarioFit Fit(string hull, params (string design, int2 cell, ItemRotation rotation)[] slots)
|
||||
{
|
||||
var where = $"fit {hull}";
|
||||
var hullData = Named<HullData>(hull, where);
|
||||
var loadout = new Loadout { Name = hull, WeaponGroups = new int[0][] };
|
||||
if (hullData != null) loadout.Hull = _items.ItemData.RefOf(hullData);
|
||||
foreach (var (design, cell, rotation) in slots)
|
||||
{
|
||||
var data = Named<EquippableItemData>(design, where);
|
||||
if (data == null) continue;
|
||||
loadout.Slots.Add(new LoadoutSlot { Design = _items.ItemData.RefOf(data), Position = cell, Rotation = rotation });
|
||||
}
|
||||
return hullData != null && loadout.Slots.Count == slots.Length ? new ScenarioFit(loadout) : null;
|
||||
}
|
||||
|
||||
// A hull with nothing fitted.
|
||||
public ScenarioFit Bare(string hull) => Fit(hull);
|
||||
|
||||
// A fit generated as the game generates its ships, for the faction when one is given, from the run's item draws,
|
||||
// which RunStart seeds from the galaxy's seed. An empty hull means any ship hull.
|
||||
public ScenarioFit Generated(string hull, Faction faction = null)
|
||||
{
|
||||
var generator = new LoadoutGenerator(ref _items.Random, _items, Galaxy, Arena.GalaxyZone, faction, 2);
|
||||
var pack = generator.GenerateShipLoadout(data => string.IsNullOrEmpty(hull) || data.Name == hull);
|
||||
if (pack != null) return new ScenarioFit(pack);
|
||||
_failures.Add($"generated: no ship hull named {hull}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// A loadout preset from the catalog, as capturepreset or Studio wrote it.
|
||||
public ScenarioFit Preset(string name)
|
||||
{
|
||||
var preset = _items.ItemData.GetAll<Loadout>().FirstOrDefault(loadout => loadout.Name == name);
|
||||
if (preset != null) return new ScenarioFit(preset);
|
||||
_failures.Add($"preset {name}: no such preset");
|
||||
return null;
|
||||
}
|
||||
|
||||
// The player's ship. A run has exactly one, and it is a ship hull.
|
||||
public Ship Player(ScenarioFit fit, float2 at, float2 facing = default)
|
||||
{
|
||||
if (PlayerPlaced)
|
||||
{
|
||||
_failures.Add("player: the scenario places a second player");
|
||||
return null;
|
||||
}
|
||||
PlayerPlaced = true;
|
||||
var entity = Build(fit, "player", at, facing);
|
||||
if (entity == null) return null;
|
||||
if (entity is Ship ship) return PlayerShip = ship;
|
||||
_failures.Add($"player: {entity.Name} is not a ship hull");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Anything else in the arena. A stance holds both ways between it and the player. A piloted ship gets the zone's
|
||||
// pilot; an unpiloted entity has none and goes nowhere on its own. A turret hull stays where it is put.
|
||||
public Entity Place(ScenarioFit fit, float2 at, float2 facing = default, ScenarioStance stance = ScenarioStance.Neutral, bool piloted = false)
|
||||
{
|
||||
var where = $"entity {_placements++}";
|
||||
var entity = Build(fit, where, at, facing);
|
||||
if (entity == null) return null;
|
||||
if (piloted && !(entity is Ship)) _failures.Add($"{where}: {entity.Name} is not a ship hull, so it cannot be piloted");
|
||||
_placed.Add(new Placement { Entity = entity, Stance = stance, Piloted = piloted });
|
||||
return entity;
|
||||
}
|
||||
|
||||
// Items in the entity's first cargo bay, one per design named.
|
||||
public void Cargo(Entity entity, params string[] designs)
|
||||
{
|
||||
if (entity == null) return;
|
||||
var where = $"cargo of {entity.Name}";
|
||||
var bay = entity.CargoBays.FirstOrDefault();
|
||||
foreach (var design in designs)
|
||||
{
|
||||
var data = Named<EquippableItemData>(design, where);
|
||||
if (data == null) continue;
|
||||
var build = Loadouts.Resolve(_items, _items.ItemData.RefOf(data), _available, where, _failures);
|
||||
if (build != null && (bay == null || !bay.TryStore(build())))
|
||||
_failures.Add($"{where}: no room for {design}");
|
||||
}
|
||||
}
|
||||
|
||||
private Entity Build(ScenarioFit fit, string where, float2 at, float2 facing)
|
||||
{
|
||||
if (fit == null) return null; // the verb that made it named why
|
||||
|
||||
Entity entity;
|
||||
if (fit.Pack != null) entity = EntitySerializer.Unpack(_items, Arena, fit.Pack);
|
||||
else
|
||||
{
|
||||
var failures = new List<string>();
|
||||
entity = Loadouts.Materialize(_items, Arena, fit.Loadout, _available, failures);
|
||||
_failures.AddRange(failures.Select(failure => $"{where} ({fit.Loadout.Name}): {failure}"));
|
||||
if (entity == null) return null;
|
||||
}
|
||||
|
||||
entity.Position = float3(at.x, 0, at.y);
|
||||
if (lengthsq(facing) > 0) entity.Direction = normalize(facing);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private T Named<T>(string name, string where) where T : EquippableItemData
|
||||
{
|
||||
var named = _items.ItemData.GetAll<T>().Where(data => data.Name == name).Take(2).ToArray();
|
||||
if (named.Length == 1) return named[0];
|
||||
_failures.Add(named.Length == 0 ? $"{where}: no design named {name}" : $"{where}: more than one design is named {name}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// What a scenario places: a fit built from named designs or a preset, or a ship generated as the game generates them.
|
||||
// Only ScenarioStage makes one.
|
||||
public sealed class ScenarioFit
|
||||
{
|
||||
internal readonly Loadout Loadout;
|
||||
internal readonly EntityPack Pack;
|
||||
|
||||
internal ScenarioFit(Loadout loadout) => Loadout = loadout;
|
||||
internal ScenarioFit(EntityPack pack) => Pack = pack;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ee065f025d2f5374db2ceaa973b822d4
|
||||
guid: d8cd00dc23604dfb91e128e1a9a4a3d5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4a109a0b0bb6e8f40935ca2ec55e5bd7
|
||||
guid: 35866d82219241ae9fdb0030039afbed
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
46
Assets/Scripts/ServerShared/Scenarios/Arcs.cs
Normal file
46
Assets/Scripts/ServerShared/Scenarios/Arcs.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using static CultMath.math;
|
||||
using static ItemRotation;
|
||||
|
||||
public sealed class Arcs : Scenario
|
||||
{
|
||||
public override string Name => "Arcs";
|
||||
public override string Brief => "Forward mounts, a bare hull off the bow, one off the stern, a hostile turret. Verify: the bow " +
|
||||
"hull can be hit, nothing fires at the stern one through the ship, the turret tracks you all the way round.";
|
||||
public override uint Seed => 103;
|
||||
public override bool Ambient => false;
|
||||
|
||||
// Where the targets sit: the player is at the origin facing +z, and every mount fires forward.
|
||||
public static readonly CultMath.float2 Bow = float2(0, 400), Stern = float2(0, -400), Turret = float2(600, 0);
|
||||
|
||||
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude();
|
||||
|
||||
public override void Stage(ScenarioStage stage)
|
||||
{
|
||||
var fighter = stage.Fit("LonginusX",
|
||||
("Cockpit 2x2", int2(2, 6), None),
|
||||
("Traction", int2(2, 3), None),
|
||||
("Core Power", int2(2, 1), None),
|
||||
("GT 3K", int2(0, 5), None),
|
||||
("GT 3K", int2(5, 5), None),
|
||||
("FastBlast+-", int2(1, 8), None),
|
||||
("FastBlast+-", int2(4, 8), None),
|
||||
("Iapyx", int2(1, 2), CounterClockwise),
|
||||
("Iapyx", int2(4, 2), Clockwise),
|
||||
("not if i see you first", int2(3, 10), None),
|
||||
("Fire Control Array", int2(2, 5), None));
|
||||
var turret = stage.Fit("Turret",
|
||||
("ClearPath", int2(1, 2), None),
|
||||
("ClearPath", int2(5, 2), None),
|
||||
("Skiron", int2(0, 2), CounterClockwise),
|
||||
("Skiron", int2(7, 2), Clockwise),
|
||||
("Core Power", int2(3, 3), None),
|
||||
("Turret Control Module", int2(3, 5), None),
|
||||
("not if i see you first", int2(4, 5), None),
|
||||
("Fire Control Array", int2(3, 6), None));
|
||||
|
||||
stage.Player(fighter, float2(0, 0), facing: float2(0, 1));
|
||||
stage.Place(stage.Bare("LonginusX"), Bow);
|
||||
stage.Place(stage.Bare("LonginusX"), Stern);
|
||||
stage.Place(turret, Turret, stance: ScenarioStance.Hostile);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: adf902e96e0c51049b65bc002d6ef4ca
|
||||
guid: 724a07b881f34e8c9b32b3f2f338ac78
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
15
Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs
Normal file
15
Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using float2 = CultMath.float2;
|
||||
|
||||
public sealed class DjinniShakedown : Scenario
|
||||
{
|
||||
public override string Name => "Djinni Shakedown";
|
||||
public override string Brief => "A fitted Djinni in empty space. Verify: its thrusters move and turn it; with the hull " +
|
||||
"offline and thrusters firing, its speed does not run away.";
|
||||
public override uint Seed => 101;
|
||||
public override bool Ambient => false;
|
||||
|
||||
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude();
|
||||
|
||||
public override void Stage(ScenarioStage stage) =>
|
||||
stage.Player(stage.Generated("Djinni"), float2.zero);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 447289e858e19c14abbe91dfbd0e3089
|
||||
guid: 1afb559630c241ed813ddd3c444c618b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
35
Assets/Scripts/ServerShared/Scenarios/Duel.cs
Normal file
35
Assets/Scripts/ServerShared/Scenarios/Duel.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using static CultMath.math;
|
||||
using static ItemRotation;
|
||||
|
||||
public sealed class Duel : Scenario
|
||||
{
|
||||
public override string Name => "Duel";
|
||||
public override string Brief => "One piloted, hostile LonginusX with slow GT 3K missiles and a Spectra in its hold. Verify: rolled " +
|
||||
"hits and near-misses, subsystem aim, beam reads above bow, armour takes the missile, loot keeps its brand.";
|
||||
public override uint Seed => 104;
|
||||
public override bool Ambient => false;
|
||||
|
||||
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude();
|
||||
|
||||
public override void Stage(ScenarioStage stage)
|
||||
{
|
||||
var fighter = stage.Fit("LonginusX",
|
||||
("Cockpit 2x2", int2(2, 6), None),
|
||||
("Traction", int2(2, 3), None),
|
||||
("Core Power", int2(2, 1), None),
|
||||
("GT 3K", int2(0, 5), None),
|
||||
("GT 3K", int2(5, 5), None),
|
||||
("FastBlast+-", int2(1, 8), None),
|
||||
("FastBlast+-", int2(4, 8), None),
|
||||
("Iapyx", int2(1, 2), CounterClockwise),
|
||||
("Iapyx", int2(4, 2), Clockwise),
|
||||
("not if i see you first", int2(3, 10), None),
|
||||
("PotaT+-", int2(1, 5), None),
|
||||
("Fire Control Array", int2(2, 5), None),
|
||||
("Store-All Plus", int2(2, 8), None));
|
||||
|
||||
stage.Player(fighter, float2(0, 0));
|
||||
var rival = stage.Place(fighter, float2(0, 900), facing: float2(0, -1), stance: ScenarioStance.Hostile, piloted: true);
|
||||
stage.Cargo(rival, "Spectra");
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Scenarios/Duel.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Scenarios/Duel.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f6ba813d4a9447c89e1bbb6997b7ac35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs
Normal file
33
Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using static CultMath.math;
|
||||
using static ItemRotation;
|
||||
|
||||
public sealed class LauncherAngles : Scenario
|
||||
{
|
||||
public override string Name => "Launcher Angles";
|
||||
public override string Brief => "A GT 3K, a pswarm and two FastBlasts; a hostile LonginusX broadside on, a bare Longinus beside it. " +
|
||||
"Verify: hits pulse only the facing edge; a pswarm round whose target dies fades unexploded.";
|
||||
public override uint Seed => 105;
|
||||
public override bool Ambient => false;
|
||||
|
||||
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude();
|
||||
|
||||
public override void Stage(ScenarioStage stage)
|
||||
{
|
||||
var launcher = stage.Fit("LonginusX",
|
||||
("Cockpit 2x2", int2(2, 6), None),
|
||||
("Traction", int2(2, 3), None),
|
||||
("Core Power", int2(2, 1), None),
|
||||
("GT 3K", int2(0, 5), None),
|
||||
("pswarm", int2(5, 5), None),
|
||||
("FastBlast+-", int2(1, 8), None),
|
||||
("FastBlast+-", int2(4, 8), None),
|
||||
("Iapyx", int2(1, 2), CounterClockwise),
|
||||
("Iapyx", int2(4, 2), Clockwise),
|
||||
("not if i see you first", int2(3, 10), None),
|
||||
("Fire Control Array", int2(2, 5), None));
|
||||
|
||||
stage.Player(launcher, float2(0, 0));
|
||||
stage.Place(stage.Bare("LonginusX"), float2(0, 900), facing: float2(1, 0), stance: ScenarioStance.Hostile);
|
||||
stage.Place(stage.Bare("Longinus"), float2(300, 700));
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 58cfaf062ddf458c99c12f3652243b45
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
14
Assets/Scripts/ServerShared/Scenarios/LongHaul.cs
Normal file
14
Assets/Scripts/ServerShared/Scenarios/LongHaul.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using float2 = CultMath.float2;
|
||||
|
||||
public sealed class LongHaul : Scenario
|
||||
{
|
||||
public override string Name => "Long Haul";
|
||||
public override string Brief => "A standard ship in a populated galaxy. Verify: memory stays flat across zones with kills; " +
|
||||
"warp, quit and Continue keep items, tier and brand; death disables Continue.";
|
||||
public override uint Seed => 106;
|
||||
|
||||
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude();
|
||||
|
||||
public override void Stage(ScenarioStage stage) =>
|
||||
stage.Player(stage.Generated(stage.StartingHull), float2.zero);
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Scenarios/LongHaul.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Scenarios/LongHaul.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3a7911611f9845709e9ae5ba34d91dfb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
13
Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs
Normal file
13
Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using float2 = CultMath.float2;
|
||||
|
||||
// The full galaxy: factions drawn from the catalog, bosses on the road to the exit, entered in the starting hull.
|
||||
public sealed class MainGalaxy : Scenario
|
||||
{
|
||||
public override string Name => "Main Galaxy";
|
||||
public override string Brief => "The full galaxy: factions drawn at random, bosses guarding the road to the exit.";
|
||||
|
||||
public override Galaxy Generate(GalaxyStage stage) => stage.Main();
|
||||
|
||||
public override void Stage(ScenarioStage stage) =>
|
||||
stage.Player(stage.Generated(stage.StartingHull), float2.zero);
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c7f6bdb3ed7b4b1aa137569c7e79bccd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
11
Assets/Scripts/ServerShared/Scenarios/Scenarios.cs
Normal file
11
Assets/Scripts/ServerShared/Scenarios/Scenarios.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Every way to set up a new run, in the order New Game lists them. Modes are the game; Tests are arenas built to check
|
||||
// one system each, listed only in editor and development builds.
|
||||
public static class Scenarios
|
||||
{
|
||||
public static readonly Scenario[] Modes = { new TutorialGalaxy(), new MainGalaxy() };
|
||||
|
||||
public static readonly Scenario[] Tests =
|
||||
{
|
||||
new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Duel(), new LauncherAngles(), new LongHaul()
|
||||
};
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Scenarios/Scenarios.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Scenarios/Scenarios.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 12f60a9ad25c4aa0a9b8b098d385ea0e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
29
Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs
Normal file
29
Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using static CultMath.math;
|
||||
using static ItemRotation;
|
||||
|
||||
public sealed class StarvedReactor : Scenario
|
||||
{
|
||||
public override string Name => "Starved Reactor";
|
||||
public override string Brief => "Two Spectras on a MoveOnPro, which can fall to a quarter of Core Power's charge, and a bare hull " +
|
||||
"to shoot. Verify: starvation reads as starvation, brownout degrades, refills stutter.";
|
||||
public override uint Seed => 102;
|
||||
public override bool Ambient => false;
|
||||
|
||||
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude();
|
||||
|
||||
public override void Stage(ScenarioStage stage)
|
||||
{
|
||||
var starved = stage.Fit("LonginusX",
|
||||
("Cockpit 2x2", int2(2, 6), None),
|
||||
("Traction", int2(2, 3), None),
|
||||
("MoveOnPro", int2(2, 1), None),
|
||||
("Spectra", int2(1, 8), None),
|
||||
("Spectra", int2(4, 8), None),
|
||||
("Iapyx", int2(1, 2), CounterClockwise),
|
||||
("Iapyx", int2(4, 2), Clockwise),
|
||||
("not if i see you first", int2(3, 10), None),
|
||||
("Fire Control Array", int2(2, 5), None));
|
||||
stage.Player(starved, float2(0, 0));
|
||||
stage.Place(stage.Bare("LonginusX"), float2(0, 400));
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 91a81ac4784c4066969cac11ac1937b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs
Normal file
16
Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using float2 = CultMath.float2;
|
||||
|
||||
// New Game as it has always been: a prelude galaxy, entered in the starting hull of the protagonist faction.
|
||||
public sealed class TutorialGalaxy : Scenario
|
||||
{
|
||||
public override string Name => "Tutorial Galaxy";
|
||||
public override string Brief => "A small galaxy of fixed factions, entered in a ship of the protagonist faction.";
|
||||
|
||||
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude();
|
||||
|
||||
public override void Stage(ScenarioStage stage)
|
||||
{
|
||||
var protagonist = stage.Galaxy.ResolveFaction(stage.TutorialGenerationSettings.ProtagonistFaction);
|
||||
stage.Player(stage.Generated(stage.StartingHull, protagonist), float2.zero);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 94d688b37c5044dd9b67865c706d8918
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -206,7 +206,7 @@ public static class ZoneGenerator
|
|||
|
||||
var nearestFaction = galaxy.Factions.MinBy(f => galaxy.HomeZones[f].Distance[galaxyZone]);
|
||||
var nearestFactionHomeZone = galaxy.HomeZones[nearestFaction];
|
||||
var factionPresence = nearestFaction.InfluenceDistance - nearestFactionHomeZone.Distance[galaxyZone] + 1;
|
||||
var factionPresence = galaxy.InfluenceOf(nearestFaction) - nearestFactionHomeZone.Distance[galaxyZone] + 1;
|
||||
|
||||
var storyStations = galaxyZone.Locations.Where(story => story.Type == LocationType.Station).ToArray();
|
||||
var stationCount = (int)(random.NextFloat() * (factionPresence + 1)) + storyStations.Length;
|
||||
|
|
|
|||
|
|
@ -5,22 +5,18 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MessagePack;
|
||||
using TMPro;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
public class MainMenu : MonoBehaviour
|
||||
{
|
||||
public VolumeCloudRenderer CloudRenderer;
|
||||
public GameSettings Settings;
|
||||
public ConfirmationDialog Dialog;
|
||||
public bool InGame;
|
||||
public Prototype PanelPrototype;
|
||||
public float FadeTime = .5f;
|
||||
public float FadeDistance = 512;
|
||||
|
|
@ -95,9 +91,10 @@ public class MainMenu : MonoBehaviour
|
|||
{
|
||||
_nextMenu.panel.Clear();
|
||||
_nextMenu.panel.Title.text = TitleSubtitle("aetheria", "terminus");
|
||||
if (!InGame)
|
||||
if (ActionGameManager.CurrentGalaxy == null)
|
||||
{
|
||||
// A run exists only as the run store's SavedGame global.
|
||||
// No run is live: at the title, after death, or after a new run failed to start. A saved run exists only as
|
||||
// the run store's SavedGame global.
|
||||
var cache = ActionGameManager.CultCache;
|
||||
if (ActionGameManager.ModExclusions.Length > 0 && !_exclusionsShown)
|
||||
{
|
||||
|
|
@ -111,7 +108,6 @@ public class MainMenu : MonoBehaviour
|
|||
{
|
||||
try { RunSave.RequireDesigns(cache, saved); }
|
||||
catch (InvalidOperationException error) { Refuse("Cannot continue this run", error.Message); return; }
|
||||
ActionGameManager.IsTutorial = saved.IsTutorial;
|
||||
ActionGameManager.CurrentGalaxy = new Galaxy(cache, saved, Debug.Log);
|
||||
EnterGame();
|
||||
});
|
||||
|
|
@ -119,69 +115,8 @@ public class MainMenu : MonoBehaviour
|
|||
_nextMenu.panel.AddButton("New Game",
|
||||
() =>
|
||||
{
|
||||
RunSave.Clear(ActionGameManager.CultCache);
|
||||
var generatorState = "Loading Database Contents";
|
||||
Action<string> setState = s => generatorState = s;
|
||||
|
||||
ShowScenarios();
|
||||
Fade(true);
|
||||
_nextMenu.panel.gameObject.SetActive(false);
|
||||
Dialog.Clear();
|
||||
Dialog.Title.text = "Generating Galaxy";
|
||||
Dialog.AddProperty(() => generatorState);
|
||||
Dialog.Show();
|
||||
|
||||
if (ActionGameManager.PlayerSettings.TutorialPassed)
|
||||
{
|
||||
var backgroundSettings = MessagePackSerializer.Deserialize<SectorBackgroundSettings>(
|
||||
MessagePackSerializer.Serialize(Settings.SectorBackgroundSettings));
|
||||
backgroundSettings.NoisePosition = Random.value * 1000;
|
||||
ActionGameManager.IsTutorial = false;
|
||||
Task.Run(() =>
|
||||
{
|
||||
var sector = new Galaxy(
|
||||
Settings.SectorGenerationSettings,
|
||||
backgroundSettings,
|
||||
Settings.NameGeneratorSettings,
|
||||
ActionGameManager.CultCache,
|
||||
Debug.Log,
|
||||
setState);
|
||||
Observable.NextFrame().Subscribe(_ =>
|
||||
{
|
||||
ActionGameManager.CurrentGalaxy = sector;
|
||||
EnterGame();
|
||||
});
|
||||
}).ContinueWith(t => Debug.LogException(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
else
|
||||
{
|
||||
var backgroundSettings = MessagePackSerializer.Deserialize<SectorBackgroundSettings>(
|
||||
MessagePackSerializer.Serialize(Settings.TutorialBackgroundSettings));
|
||||
int iteration = 1;
|
||||
do
|
||||
{
|
||||
backgroundSettings.NoisePosition = Random.value * 1000;
|
||||
setState($"Finding Galaxy Position: iteration {iteration++}");
|
||||
} while (backgroundSettings.CloudDensity(float2(0.5f)) < .5f);
|
||||
|
||||
ActionGameManager.IsTutorial = true;
|
||||
Task.Run(() =>
|
||||
{
|
||||
var sector = new Galaxy(
|
||||
Settings.TutorialGenerationSettings,
|
||||
backgroundSettings,
|
||||
Settings.NameGeneratorSettings,
|
||||
ActionGameManager.CultCache,
|
||||
ActionGameManager.PlayerSettings,
|
||||
ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"),
|
||||
Debug.Log,
|
||||
setState);
|
||||
Observable.NextFrame().Subscribe(_ =>
|
||||
{
|
||||
ActionGameManager.CurrentGalaxy = sector;
|
||||
EnterGame();
|
||||
});
|
||||
}).ContinueWith(t => Debug.LogException(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
});
|
||||
_nextMenu.panel.AddButton("Settings",
|
||||
() =>
|
||||
|
|
@ -192,6 +127,77 @@ public class MainMenu : MonoBehaviour
|
|||
_nextMenu.panel.AddButton("Quit", Application.Quit);
|
||||
}
|
||||
|
||||
// New Game: every way to set up a run, each with its brief. The test arenas are listed only in editor and
|
||||
// development builds.
|
||||
private void ShowScenarios()
|
||||
{
|
||||
_nextMenu.panel.Clear();
|
||||
_nextMenu.panel.Title.text = TitleSubtitle("new game", "scenarios");
|
||||
AddScenarios(Scenarios.Modes);
|
||||
if (Debug.isDebugBuild)
|
||||
{
|
||||
_nextMenu.panel.AddSection("test arenas");
|
||||
AddScenarios(Scenarios.Tests);
|
||||
}
|
||||
_nextMenu.panel.AddButton("Back",
|
||||
() =>
|
||||
{
|
||||
ShowMain();
|
||||
Fade(false);
|
||||
});
|
||||
}
|
||||
|
||||
private void AddScenarios(IEnumerable<Scenario> scenarios)
|
||||
{
|
||||
foreach (var scenario in scenarios)
|
||||
{
|
||||
_nextMenu.panel.AddButton(scenario.Name, () => Launch(scenario));
|
||||
_nextMenu.panel.AddProperty(() => scenario.Brief);
|
||||
}
|
||||
}
|
||||
|
||||
// The only way into a new run: generates the scenario's galaxy off the main thread and enters the game, where
|
||||
// StartGame has RunStart stage it and only then replaces the saved run. A galaxy that cannot generate is refused
|
||||
// here, with the saved run untouched.
|
||||
private void Launch(Scenario scenario)
|
||||
{
|
||||
var generatorState = "Loading Database Contents";
|
||||
|
||||
Fade(true);
|
||||
_nextMenu.panel.gameObject.SetActive(false);
|
||||
Dialog.Clear();
|
||||
Dialog.Title.text = "Generating Galaxy";
|
||||
Dialog.AddProperty(() => generatorState);
|
||||
Dialog.Show();
|
||||
|
||||
var stage = new GalaxyStage(
|
||||
Settings.SectorGenerationSettings,
|
||||
Settings.SectorBackgroundSettings,
|
||||
Settings.TutorialGenerationSettings,
|
||||
Settings.TutorialBackgroundSettings,
|
||||
Settings.NameGeneratorSettings,
|
||||
ActionGameManager.CultCache,
|
||||
ActionGameManager.PlayerSettings,
|
||||
ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"),
|
||||
Debug.Log,
|
||||
state => generatorState = state);
|
||||
Task.Run(() => RunStart.Generate(scenario, stage)).ContinueWith(generation =>
|
||||
Observable.NextFrame().Subscribe(_ =>
|
||||
{
|
||||
if (generation.IsFaulted)
|
||||
{
|
||||
Debug.LogException(generation.Exception);
|
||||
ShowMain();
|
||||
Fade(false);
|
||||
Refuse($"{scenario.Name} could not generate its galaxy", generation.Exception.GetBaseException().Message);
|
||||
return;
|
||||
}
|
||||
ActionGameManager.CurrentGalaxy = generation.Result;
|
||||
ActionGameManager.PendingScenario = scenario;
|
||||
EnterGame();
|
||||
}));
|
||||
}
|
||||
|
||||
// The one way into the game scene. Mod ship prototypes import asynchronously at boot, so the scene waits for them,
|
||||
// and a preload that could not run at all stops here with its reason rather than in the first zone.
|
||||
private void EnterGame() => StartCoroutine(EnterGameWhenModShipsAreReady());
|
||||
|
|
@ -208,7 +214,7 @@ public class MainMenu : MonoBehaviour
|
|||
SceneManager.LoadScene("ARPG");
|
||||
}
|
||||
|
||||
private void Refuse(string title, string reason)
|
||||
public void Refuse(string title, string reason)
|
||||
{
|
||||
Debug.LogError($"{title}: {reason}");
|
||||
Dialog.Clear();
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ public sealed partial class RunStartTests
|
|||
var low = Heater("P30 Low Heater", 260, 10);
|
||||
var high = Heater("P30 High Heater", 320, 7);
|
||||
var hull = Hull("Djinni");
|
||||
var zone = Arena(new Scenario { Ambient = false });
|
||||
var zone = Arena(false);
|
||||
|
||||
List<float> Bills(params GearData[] order)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -282,25 +282,14 @@ public sealed class RestoredHullsTests
|
|||
var background = TutorialBackgroundSettings();
|
||||
var names = TutorialNameSettings();
|
||||
var gameData = Path.Combine(FindRepoRoot(), "GameData", "Aetheria.cc");
|
||||
|
||||
// Galaxy zone-graph generation has a pre-existing, seed-dependent flakiness of its own (a disconnected
|
||||
// zone graph throws inside the Galaxy constructor on roughly 1 seed in 15, confirmed unrelated to this
|
||||
// cut: it reproduces identically against the pre-fix-batch catalog). That failure belongs to Galaxy.cs,
|
||||
// not to ZoneGenerator's entrance-station override this test is pinning, so it is skipped rather than
|
||||
// asserted on; every seed whose Galaxy actually builds must still get a docked entrance station.
|
||||
var succeeded = 0;
|
||||
var sawGenuineLagrangeOrbit = false;
|
||||
for (uint seed = 1; seed <= 25; seed++)
|
||||
{
|
||||
var scratchRun = Path.Combine(Path.GetTempPath(), $"aetheria-entrance-station-{Guid.NewGuid():N}.cc");
|
||||
try
|
||||
{
|
||||
// Galaxy mutates Faction.InfluenceDistance in place; a fresh catalog open per seed keeps runs independent.
|
||||
using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun);
|
||||
Galaxy galaxy;
|
||||
try { galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed); }
|
||||
catch (Exception) { continue; }
|
||||
succeeded++;
|
||||
var galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
|
||||
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
|
||||
var pack = ZoneGenerator.GenerateZone(items, zoneSettings, galaxy, galaxy.Entrance, isTutorial: true);
|
||||
|
|
@ -317,8 +306,6 @@ public sealed class RestoredHullsTests
|
|||
if (File.Exists(scratchRun)) File.Delete(scratchRun);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(succeeded >= 15, $"only {succeeded} of 25 seeds produced a Galaxy at all; too few runs to trust this result.");
|
||||
Assert.True(sawGenuineLagrangeOrbit, "none of the 25 seeds seated the entrance station on a genuine (non-rosette) Lagrange orbit.");
|
||||
}
|
||||
|
||||
|
|
@ -336,8 +323,6 @@ public sealed class RestoredHullsTests
|
|||
var background = TutorialBackgroundSettings();
|
||||
var names = TutorialNameSettings();
|
||||
var gameData = Path.Combine(FindRepoRoot(), "GameData", "Aetheria.cc");
|
||||
|
||||
var succeeded = 0;
|
||||
var sawGenuineCandidate = false;
|
||||
for (uint seed = 1; seed <= 25; seed++)
|
||||
{
|
||||
|
|
@ -345,10 +330,7 @@ public sealed class RestoredHullsTests
|
|||
try
|
||||
{
|
||||
using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun);
|
||||
Galaxy galaxy;
|
||||
try { galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed); }
|
||||
catch (Exception) { continue; }
|
||||
succeeded++;
|
||||
var galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
|
||||
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
|
||||
var pack = ZoneGenerator.GenerateZone(items, zoneSettings, galaxy, galaxy.Entrance, isTutorial: true);
|
||||
|
|
@ -369,8 +351,6 @@ public sealed class RestoredHullsTests
|
|||
if (File.Exists(scratchRun)) File.Delete(scratchRun);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(succeeded >= 15, $"only {succeeded} of 25 seeds produced a Galaxy at all; too few runs to trust this result.");
|
||||
Assert.True(sawGenuineCandidate, "none of the 25 seeds had a genuine Lagrange candidate at all; too few runs to trust this result.");
|
||||
}
|
||||
|
||||
|
|
@ -477,9 +457,7 @@ public sealed class RestoredHullsTests
|
|||
try
|
||||
{
|
||||
using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun);
|
||||
Galaxy galaxy;
|
||||
try { galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed); }
|
||||
catch (Exception) { continue; }
|
||||
var galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
|
||||
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
|
||||
var pack = ZoneGenerator.GenerateZone(items, zoneSettings, galaxy, galaxy.Entrance, isTutorial: true);
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ public sealed class RunSaveTests : IDisposable
|
|||
var galaxy = new Galaxy(cache, cache.GetGlobal<SavedGame>(), _ => { });
|
||||
var itemManager = new ItemManager(cache, new ProvenanceLedger(), TestSettings(), _ => { });
|
||||
galaxy.Zones[0].Contents = new Zone(itemManager, new PlanetSettings(), new ZonePack(), galaxy.Zones[0], galaxy);
|
||||
var (game, zones) = RunSave.Capture(cache, galaxy, galaxy.Zones[0].Contents, null, false, new SavedActionBarBinding[0]);
|
||||
var (game, zones) = RunSave.Capture(cache, galaxy, galaxy.Zones[0].Contents, null, new SavedActionBarBinding[0]);
|
||||
RunSave.Commit(cache, game, zones, RunSave.Lots(cache));
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +126,62 @@ public sealed class RunSaveTests : IDisposable
|
|||
Assert.Equal(before, new[] { Catalog, Player }.Select(Hash).ToArray());
|
||||
}
|
||||
|
||||
// A new run's start writes its arena's orbits and bodies while the saved run is still stored. A start that fails,
|
||||
// by returning nothing or by throwing, takes away only what it wrote; one that starts takes away the saved run.
|
||||
[Fact]
|
||||
public void ANewRunReplacesTheSavedRunOnlyOnceItHasStarted()
|
||||
{
|
||||
using (var cache = Open())
|
||||
RunSave.Commit(cache, Game(cache), Zones(3, StageZoneContents(cache), 0), new ProvenanceLedger());
|
||||
string[] saved;
|
||||
using (var cache = Open())
|
||||
saved = RunKeys(cache);
|
||||
Assert.Equal(8, saved.Length);
|
||||
|
||||
using (var cache = Open())
|
||||
{
|
||||
Assert.Null(RunSave.Replace<ZonePack>(cache, () =>
|
||||
{
|
||||
WriteAnOrbitTheSavedRunLacks(cache);
|
||||
return null;
|
||||
}));
|
||||
Assert.Equal(saved, RunKeys(cache));
|
||||
}
|
||||
using (var cache = Open())
|
||||
{
|
||||
Assert.Equal(saved, RunKeys(cache));
|
||||
Assert.NotNull(cache.GetGlobal<SavedGame>());
|
||||
}
|
||||
|
||||
using (var cache = Open())
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => RunSave.Replace<ZonePack>(cache, () =>
|
||||
{
|
||||
WriteAnOrbitTheSavedRunLacks(cache);
|
||||
throw new InvalidOperationException("the arena would not generate");
|
||||
}));
|
||||
Assert.Equal(saved, RunKeys(cache));
|
||||
}
|
||||
using (var cache = Open())
|
||||
Assert.Equal(saved, RunKeys(cache));
|
||||
|
||||
ZonePack started;
|
||||
using (var cache = Open())
|
||||
started = RunSave.Replace(cache, () => StageZoneContents(cache));
|
||||
using (var cache = Open())
|
||||
{
|
||||
Assert.Null(cache.GetGlobal<SavedGame>());
|
||||
Assert.Equal(started.Orbits.Select(orbit => orbit.Key.Value).Concat(started.Planets.Select(body => body.Key.Value))
|
||||
.OrderBy(key => key, StringComparer.Ordinal), RunKeys(cache));
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] RunKeys(CultCache cache) => cache.AllStoredDocuments
|
||||
.Where(stored => RunSave.IsRunRecord(stored.Descriptor.DocumentType))
|
||||
.Select(stored => stored.Key.Value)
|
||||
.OrderBy(key => key, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
// Lot 1 sits on the root ship's hull; lot 3 (Produced from facility 4, input 5) sits in a docked child's
|
||||
// docking-bay contents; lots 2 and 6 are minted but referenced by nothing packed. Only the reachable closure
|
||||
// {1, 3, 4, 5} survives into the written copy; the live ledger keeps every lot.
|
||||
|
|
@ -532,6 +588,9 @@ public sealed class RunSaveTests : IDisposable
|
|||
QualityPriceModifier = new ExponentialLerp()
|
||||
};
|
||||
|
||||
// What a start that fails leaves behind: a record of its own, which the saved run does not hold.
|
||||
private static void WriteAnOrbitTheSavedRunLacks(CultCache cache) => cache.Upsert(new OrbitData { Distance = 99 });
|
||||
|
||||
// Two orbits and a body upserted into the run store and left staged, as zone generation leaves them.
|
||||
private static ZonePack StageZoneContents(CultCache cache)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,10 +16,9 @@ using static CultMath.math;
|
|||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
|
||||
// Scenarios Cut 1 (docs/scenarios-cut.md, 2.2): RunStart owns what a new run starts with, and Zone.Admit is the one
|
||||
// admission into a zone. Every test stages into a real arena: the entrance zone of a prelude galaxy at a fixed seed,
|
||||
// built from the shipped catalog and the authored settings (Assets/Resources/Settings.asset). The presets and
|
||||
// scenarios live in a scratch copy of the catalog, because the shipped one holds none.
|
||||
// RunStart owns what a new run starts with, and Zone.Admit is the one admission into a zone (docs/scenarios-cut.md,
|
||||
// 2.2 and R). Every test stages into a real arena: the entrance zone of a prelude galaxy at a fixed seed, built from a
|
||||
// scratch copy of the shipped catalog and the authored settings (Assets/Resources/Settings.asset).
|
||||
public sealed partial class RunStartTests : IDisposable
|
||||
{
|
||||
private const uint GalaxySeed = 1;
|
||||
|
|
@ -32,6 +31,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
private readonly PlanetSettings _planetSettings;
|
||||
private readonly string _startingHull;
|
||||
private readonly Faction _protagonist;
|
||||
private readonly TutorialGenerationSettings _tutorialSettings;
|
||||
|
||||
public RunStartTests()
|
||||
{
|
||||
|
|
@ -49,7 +49,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
var authored = AuthoredSettings.Load(repo);
|
||||
_zoneSettings = authored.Read<ZoneGenerationSettings>("ZoneSettings");
|
||||
_planetSettings = authored.Read<PlanetSettings>("PlanetSettings");
|
||||
var tutorial = authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings");
|
||||
var tutorial = _tutorialSettings = authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings");
|
||||
_galaxy = new Galaxy(tutorial, authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"),
|
||||
authored.Read<NameGeneratorSettings>("NameGeneratorSettings"), _cache, new PlayerSettings(),
|
||||
Directory.CreateDirectory(Path.Combine(_root, "Narrative")), _ => { }, null, GalaxySeed);
|
||||
|
|
@ -80,8 +80,9 @@ public sealed partial class RunStartTests : IDisposable
|
|||
throw new DirectoryNotFoundException("Run from inside the Aetheria repository.");
|
||||
}
|
||||
|
||||
private Zone Arena(Scenario scenario) =>
|
||||
new Zone(_items, _planetSettings, RunStart.GenerateArena(_items, _zoneSettings, _galaxy, scenario), _galaxy.Entrance, _galaxy);
|
||||
// The fixture galaxy's arena, with or without its generated ships and turrets.
|
||||
private Zone Arena(bool ambient) =>
|
||||
new Zone(_items, _planetSettings, RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scripted(ambient, _ => { })), _galaxy.Entrance, _galaxy);
|
||||
|
||||
private HullData Hull(string name) => _cache.GetAll<HullData>().Single(hull => hull.Name == name);
|
||||
|
||||
|
|
@ -92,314 +93,12 @@ public sealed partial class RunStartTests : IDisposable
|
|||
.OrderBy(hull => hull.Name, StringComparer.Ordinal)
|
||||
.First();
|
||||
|
||||
private CultRecordRef<Loadout> Preset(Loadout loadout)
|
||||
{
|
||||
var existing = _cache.GetAll<Loadout>().FirstOrDefault(other => other.Name == loadout.Name);
|
||||
if (existing != null) return _cache.RefOf(existing);
|
||||
_cache.Upsert(loadout);
|
||||
return _cache.RefOf(loadout);
|
||||
}
|
||||
|
||||
private CultRecordRef<Loadout> Bare(HullData hull) =>
|
||||
Preset(new Loadout { Name = "bare " + hull.Name, Hull = _cache.RefOf(hull), WeaponGroups = new int[0][] });
|
||||
|
||||
// A fully fitted preset: a generated ship of this hull, captured.
|
||||
private Loadout Fitted(string hullName)
|
||||
{
|
||||
var generator = new LoadoutGenerator(ref _items.Random, _items, null, null, null, 2);
|
||||
var ship = (Ship) EntitySerializer.Unpack(_items, null, generator.GenerateShipLoadout(hull => hull.Name == hullName));
|
||||
Assert.NotEmpty(ship.CargoBays);
|
||||
return Loadouts.Capture(_items, ship, "fitted " + hullName);
|
||||
}
|
||||
|
||||
private static ScenarioShip At(CultRecordRef<Loadout> preset, float x, float z) =>
|
||||
new ScenarioShip { Loadout = preset, Position = float2(x, z) };
|
||||
|
||||
private static ScenarioEntity Placed(CultRecordRef<Loadout> preset, float x, float z, ScenarioStance stance, bool piloted) =>
|
||||
new ScenarioEntity { Loadout = preset, Position = float2(x, z), Stance = stance, Piloted = piloted };
|
||||
|
||||
private Scenario Quiet(ScenarioShip player, params ScenarioEntity[] entities) =>
|
||||
new Scenario { Name = "test", Seed = GalaxySeed, Ambient = false, Player = player, Entities = entities.ToList() };
|
||||
|
||||
private RunStart.Staged Stage(Zone zone, Scenario scenario)
|
||||
{
|
||||
var failures = new List<string>();
|
||||
var staged = RunStart.Stage(_items, zone, scenario, _startingHull, _protagonist, failures);
|
||||
Assert.Empty(failures);
|
||||
Assert.NotNull(staged);
|
||||
return staged;
|
||||
}
|
||||
// A loadout with only its hull, in memory.
|
||||
private Loadout BareLoadout(HullData hull) =>
|
||||
new Loadout { Name = "bare " + hull.Name, Hull = _cache.RefOf(hull), WeaponGroups = new int[0][] };
|
||||
|
||||
private static int AgentsOf(Zone zone, Entity entity) => zone.Agents.Count(agent => agent.Ship == entity);
|
||||
|
||||
// New Game is the absent-scenario case: the authored starting hull, flagged as the player's, admitted at the
|
||||
// origin, for the protagonist faction, with no agent. The hull name is an input: staging honours another one too.
|
||||
[Fact]
|
||||
public void WithNoScenarioThePlayerIsTheStartingHullAtTheOrigin()
|
||||
{
|
||||
Assert.Equal("LonginusX", _startingHull);
|
||||
foreach (var hullName in new[] { _startingHull, "Djinni" })
|
||||
{
|
||||
var zone = Arena(null);
|
||||
var failures = new List<string>();
|
||||
var staged = RunStart.Stage(_items, zone, null, hullName, _protagonist, failures);
|
||||
Assert.Empty(failures);
|
||||
var player = staged.Player;
|
||||
Assert.Equal(hullName, player.HullData.Name);
|
||||
Assert.True(player.IsPlayerShip);
|
||||
SafeAssert.In(zone, player, "the player is admitted");
|
||||
Assert.True(zone == player.Zone, "the player's zone is the arena");
|
||||
Assert.Equal(float3.zero, player.Position);
|
||||
Assert.Same(_protagonist, player.Faction);
|
||||
Assert.Equal(0, AgentsOf(zone, player));
|
||||
Assert.Empty(staged.Entities);
|
||||
}
|
||||
}
|
||||
|
||||
// A scenario's player is its preset, fitted slot for slot, at the authored position and heading, carrying its cargo.
|
||||
[Fact]
|
||||
public void AScenarioPlayerIsItsPresetAtItsPlaceWithItsCargo()
|
||||
{
|
||||
var fitted = Fitted("Djinni");
|
||||
var preset = Preset(fitted);
|
||||
// Two one-cell designs a manufacturer makes, by name, so the cargo is ordinary branded stock
|
||||
var cargo = _cache.GetAll<GearData>()
|
||||
.Where(design => design.Shape.Coordinates.Length == 1)
|
||||
.Where(design => _cache.GetAll<FactionProductData>().Any(p => p.Design.Key.Equals(_cache.RefOf(design).Key)))
|
||||
.OrderBy(design => design.Name, StringComparer.Ordinal)
|
||||
.Take(2)
|
||||
.Select(design => new CultRecordRef<EquippableItemData>(_cache.RefOf(design).Key))
|
||||
.ToList();
|
||||
Assert.Equal(2, cargo.Count);
|
||||
var zone = Arena(null);
|
||||
var scenario = Quiet(new ScenarioShip { Loadout = preset, Position = float2(120, -40), Direction = float2(3, 4), Cargo = cargo });
|
||||
|
||||
var player = Stage(zone, scenario).Player;
|
||||
|
||||
Assert.Equal("Djinni", player.HullData.Name);
|
||||
Assert.True(player.IsPlayerShip);
|
||||
SafeAssert.In(zone, player, "the player is admitted");
|
||||
Assert.Equal(float3(120, 0, -40), player.Position);
|
||||
Assert.True(length(player.Direction - float2(.6f, .8f)) < 1e-5f, $"heading {player.Direction}");
|
||||
Assert.Equal(
|
||||
fitted.Slots.Select(slot => (slot.Position, slot.Design.Key)).OrderBy(s => s.ToString()),
|
||||
player.Equipment.Concat<EquippedItem>(player.CargoBays).Concat(player.DockingBays)
|
||||
.Where(item => item != player.EquippedHull)
|
||||
.Select(item => (item.Position, _cache.RefOf(_items.GetData(item.EquippableItem)).Key))
|
||||
.OrderBy(s => s.ToString()));
|
||||
Assert.Equal(cargo.Select(design => design.Key.Value).OrderBy(k => k),
|
||||
player.CargoBays.First().Cargo.Keys.Select(item => item.Data.Key.Value).OrderBy(k => k));
|
||||
}
|
||||
|
||||
// A piloted entity gets exactly one agent; an unpiloted one gets none, and nothing steers it. (It is not frozen:
|
||||
// the zone's gravity still moves it, as it moves any ship, so the rule is observed on its steering inputs.)
|
||||
[Fact]
|
||||
public void APilotedEntityHasOneAgentAndAnUnpilotedOneNoneAndStaysPut()
|
||||
{
|
||||
var zone = Arena(null);
|
||||
var staged = Stage(zone, Quiet(At(Bare(Hull("Djinni")), 0, 0),
|
||||
Placed(Preset(Fitted("LonginusX")), 300, 0, ScenarioStance.Neutral, piloted: true),
|
||||
Placed(Bare(Hull("LonginusX")), -300, 0, ScenarioStance.Neutral, piloted: false)));
|
||||
var piloted = staged.Entities[0];
|
||||
var unpiloted = staged.Entities[1];
|
||||
|
||||
Assert.Equal(1, AgentsOf(zone, piloted));
|
||||
Assert.Equal(0, AgentsOf(zone, unpiloted));
|
||||
Assert.Equal(0, AgentsOf(zone, staged.Player));
|
||||
SafeAssert.In(zone, piloted, "the piloted entity is admitted");
|
||||
SafeAssert.In(zone, unpiloted, "the unpiloted entity is admitted");
|
||||
|
||||
var look = unpiloted.LookDirection;
|
||||
for (var tick = 0; tick < 50; tick++)
|
||||
{
|
||||
zone.Update(.1f);
|
||||
Assert.Equal(float2.zero, ((Ship) unpiloted).MovementDirection);
|
||||
Assert.Equal(look, unpiloted.LookDirection);
|
||||
}
|
||||
Assert.Equal(0, AgentsOf(zone, unpiloted));
|
||||
}
|
||||
|
||||
// A stance holds both ways between the entity and the player.
|
||||
[Fact]
|
||||
public void AStanceIsSetBothWays()
|
||||
{
|
||||
var longinus = Bare(Hull("LonginusX"));
|
||||
var staged = Stage(Arena(null), Quiet(At(Bare(Hull("Djinni")), 0, 0),
|
||||
Placed(longinus, 300, 0, ScenarioStance.Hostile, piloted: false),
|
||||
Placed(longinus, -300, 0, ScenarioStance.Neutral, piloted: false)));
|
||||
var player = staged.Player;
|
||||
var hostile = staged.Entities[0];
|
||||
var neutral = staged.Entities[1];
|
||||
|
||||
Assert.True(player.IsHostileTo(hostile));
|
||||
Assert.True(hostile.IsHostileTo(player));
|
||||
Assert.False(player.IsHostileTo(neutral));
|
||||
Assert.False(neutral.IsHostileTo(player));
|
||||
|
||||
// Admission activates: the live hostility each side tracks (what targeting and grudges read) agrees.
|
||||
Assert.True(player.EntityHostility[hostile]);
|
||||
Assert.True(hostile.EntityHostility[player]);
|
||||
Assert.False(player.EntityHostility[neutral]);
|
||||
Assert.False(neutral.EntityHostility[player]);
|
||||
}
|
||||
|
||||
// A stance lasts the sitting: the player docking and undocking leaves it standing both ways. It goes with an entity
|
||||
// gone for good: once an entity is destroyed, the player holds no stance toward it.
|
||||
[Fact]
|
||||
public void AStanceSurvivesDockingButNotDestruction()
|
||||
{
|
||||
var zone = Arena(null);
|
||||
var longinus = Bare(Hull("LonginusX"));
|
||||
var staged = Stage(zone, Quiet(At(Bare(Hull("Djinni")), 0, 0),
|
||||
Placed(longinus, 300, 0, ScenarioStance.Hostile, piloted: false),
|
||||
Placed(longinus, -300, 0, ScenarioStance.Hostile, piloted: false)));
|
||||
var player = staged.Player;
|
||||
var hostile = staged.Entities[0];
|
||||
var doomed = staged.Entities[1];
|
||||
var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0);
|
||||
|
||||
Assert.NotNull(station.TryDock(player));
|
||||
SafeAssert.NotIn(zone, player, "a docked ship is out of the zone");
|
||||
Assert.True(station.TryUndock(player));
|
||||
Assert.True(player.IsHostileTo(hostile), "the player's stance was lost over docking");
|
||||
Assert.True(hostile.IsHostileTo(player), "the entity's stance was lost over docking");
|
||||
Assert.True(player.EntityHostility[hostile], "the player's live hostility was lost over docking");
|
||||
Assert.True(hostile.EntityHostility[player], "the entity's live hostility was lost over docking");
|
||||
|
||||
// Presets carry no faction, so without its stance the derived rule is not hostile
|
||||
Assert.True(player.Faction == null && doomed.Faction == null, "the fixture's ships are factionless");
|
||||
Assert.True(player.IsHostileTo(doomed));
|
||||
doomed.Hull.Durability = 0;
|
||||
doomed.HullDamage.OnNext(1);
|
||||
SafeAssert.NotIn(zone, doomed, "a destroyed entity leaves the zone");
|
||||
Assert.False(player.IsHostileTo(doomed), "the stance toward a destroyed entity outlived it");
|
||||
}
|
||||
|
||||
// Undocking admits the ship unpiloted: back in the zone, active (its live hostility is tracked), with no agent.
|
||||
[Fact]
|
||||
public void UndockingAdmitsWithNoAgent()
|
||||
{
|
||||
var zone = Arena(null);
|
||||
var player = Stage(zone, null).Player;
|
||||
var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0);
|
||||
var agents = zone.Agents.Count;
|
||||
|
||||
Assert.NotNull(station.TryDock(player));
|
||||
SafeAssert.NotIn(zone, player, "a docked ship is out of the zone");
|
||||
Assert.True(station.TryUndock(player));
|
||||
SafeAssert.In(zone, player, "an undocked ship is back in the zone");
|
||||
Assert.Equal(agents, zone.Agents.Count);
|
||||
Assert.Equal(0, AgentsOf(zone, player));
|
||||
Assert.True(player.EntityHostility.ContainsKey(station), "an undocked ship is active");
|
||||
}
|
||||
|
||||
// Ambient: false keeps the stations and drops every generated ship and turret; ambient, and a plain New Game, keep
|
||||
// them. The arena is the galaxy's entrance pack. The entrance holds a story station, placed as the narrative would
|
||||
// place one, so a story station's own turrets are dropped too.
|
||||
[Fact]
|
||||
public void AQuietArenaHasNoGeneratedShipOrTurretButKeepsItsStations()
|
||||
{
|
||||
(int ships, int turrets, int stations) Census(ZonePack pack) => (
|
||||
pack.Entities.OfType<ShipPack>().Count(),
|
||||
pack.Entities.OfType<OrbitalEntityPack>().Count(e => ((HullData) _cache.Get(e.Hull.Data)).HullType == HullType.Turret),
|
||||
pack.Entities.OfType<OrbitalEntityPack>().Count(e => ((HullData) _cache.Get(e.Hull.Data)).HullType == HullType.Station));
|
||||
|
||||
_galaxy.Entrance.Locations.Add(new LocationStory
|
||||
{
|
||||
Zone = _galaxy.Entrance, Name = "story", Type = LocationType.Station, Faction = _protagonist, Turrets = 2
|
||||
});
|
||||
var plain = Census(RunStart.GenerateArena(_items, _zoneSettings, _galaxy, null));
|
||||
var ambient = Census(RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scenario { Ambient = true }));
|
||||
var quietPack = RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scenario { Ambient = false });
|
||||
var quiet = Census(quietPack);
|
||||
|
||||
Assert.Same(quietPack, _galaxy.Entrance.PackedContents);
|
||||
Assert.True(plain.ships > 0 && plain.turrets > 0, $"a plain arena generated {plain}");
|
||||
Assert.True(ambient.ships > 0 && ambient.turrets > 0, $"an ambient arena generated {ambient}");
|
||||
Assert.Equal(0, quiet.ships);
|
||||
Assert.Equal(0, quiet.turrets);
|
||||
Assert.True(quiet.stations > 0);
|
||||
Assert.Equal(ambient.stations, quiet.stations);
|
||||
Assert.Contains(quietPack.Entities.OfType<OrbitalEntityPack>(), station => station.Story == 0);
|
||||
}
|
||||
|
||||
// A turret preset stages as an orbital entity with no orbit, which stays where it is put.
|
||||
[Fact]
|
||||
public void ATurretPresetStagesAsAStationaryOrbitalEntity()
|
||||
{
|
||||
var zone = Arena(null);
|
||||
var staged = Stage(zone, Quiet(At(Bare(Hull("Djinni")), 0, 0),
|
||||
Placed(Bare(TurretHull()), 200, 50, ScenarioStance.Hostile, piloted: false)));
|
||||
var turret = Assert.IsType<OrbitalEntity>(staged.Entities[0]);
|
||||
Assert.False(turret.OrbitData.IsSet());
|
||||
SafeAssert.In(zone, turret, "the turret is admitted");
|
||||
for (var tick = 0; tick < 20; tick++) zone.Update(.1f);
|
||||
Assert.Equal(float2(200, 50), turret.Position.xz);
|
||||
}
|
||||
|
||||
// A station preset is refused, and so is a turret as the player or a piloted turret: each is a staging failure
|
||||
// naming the hull, with nothing admitted.
|
||||
[Fact]
|
||||
public void AStationPresetOrAMisplacedTurretIsAStagingFailure()
|
||||
{
|
||||
var station = _cache.GetAll<HullData>().First(hull => hull.HullType == HullType.Station);
|
||||
var turret = TurretHull();
|
||||
var djinni = Bare(Hull("Djinni"));
|
||||
foreach (var (scenario, expected) in new[]
|
||||
{
|
||||
(Quiet(At(djinni, 0, 0), Placed(Bare(station), 200, 0, ScenarioStance.Neutral, false)), $"{station.Name} is a station hull"),
|
||||
(Quiet(At(Bare(turret), 0, 0)), $"player: {turret.Name} is not a ship hull"),
|
||||
(Quiet(At(djinni, 0, 0), Placed(Bare(turret), 200, 0, ScenarioStance.Neutral, true)), "cannot be piloted"),
|
||||
})
|
||||
{
|
||||
var zone = Arena(null);
|
||||
var entities = zone.Entities.ToList();
|
||||
var agents = zone.Agents.ToList();
|
||||
var failures = new List<string>();
|
||||
Assert.Null(RunStart.Stage(_items, zone, scenario, _startingHull, _protagonist, failures));
|
||||
Assert.Contains(failures, failure => failure.Contains(expected));
|
||||
SafeAssert.Unchanged(zone, entities, agents);
|
||||
Assert.Contains(RunStart.Check(_items, scenario), failure => failure.Contains(expected));
|
||||
}
|
||||
}
|
||||
|
||||
// A scenario naming a missing design or preset fails with each named; the check entry finds the same failures with
|
||||
// no zone and writes no document, and staging admits nothing, not even the parts that built: a player and a piloted
|
||||
// entity that build cleanly stay out when a later entity fails.
|
||||
[Fact]
|
||||
public void AMissingDesignFailsNamedAndChangesNothing()
|
||||
{
|
||||
var djinni = Bare(Hull("Djinni"));
|
||||
var scenario = Quiet(
|
||||
new ScenarioShip { Loadout = djinni, Cargo = { new CultRecordRef<EquippableItemData>(new CultRecordKey("absent-design")) } },
|
||||
Placed(Bare(Hull("LonginusX")), 300, 0, ScenarioStance.Hostile, piloted: true),
|
||||
Placed(new CultRecordRef<Loadout>(new CultRecordKey("absent-preset")), -300, 0, ScenarioStance.Neutral, false));
|
||||
var zone = Arena(null);
|
||||
var entities = zone.Entities.ToList();
|
||||
var agents = zone.Agents.ToList();
|
||||
var documents = _cache.AllStoredDocuments.Count();
|
||||
|
||||
var checkFailures = RunStart.Check(_items, scenario);
|
||||
Assert.Equal(2, checkFailures.Count);
|
||||
Assert.Contains("absent-design", checkFailures[0]);
|
||||
Assert.Contains("absent-preset", checkFailures[1]);
|
||||
Assert.Equal(documents, _cache.AllStoredDocuments.Count());
|
||||
|
||||
var failures = new List<string>();
|
||||
Assert.Null(RunStart.Stage(_items, zone, scenario, _startingHull, _protagonist, failures));
|
||||
Assert.Equal(checkFailures, failures);
|
||||
SafeAssert.Unchanged(zone, entities, agents);
|
||||
|
||||
var lateFailure = Quiet(At(djinni, 0, 0),
|
||||
Placed(Bare(Hull("LonginusX")), 300, 0, ScenarioStance.Hostile, piloted: true),
|
||||
Placed(new CultRecordRef<Loadout>(new CultRecordKey("absent-preset")), -300, 0, ScenarioStance.Neutral, false));
|
||||
failures.Clear();
|
||||
Assert.Null(RunStart.Stage(_items, zone, lateFailure, _startingHull, _protagonist, failures));
|
||||
Assert.Contains("absent-preset", Assert.Single(failures));
|
||||
SafeAssert.Unchanged(zone, entities, agents);
|
||||
Assert.Empty(RunStart.Check(_items, null));
|
||||
}
|
||||
|
||||
// A heater is gear whose thermostat runs its heat only below a target: a low-pass thermotoggle, then heat.
|
||||
private static bool IsHeater(EquippedItem item) =>
|
||||
item.Behaviors.OfType<Thermotoggle>().Any(t => !t.ThermotoggleData.HighPass) && item.Behaviors.OfType<Heat>().Any();
|
||||
|
|
@ -408,7 +107,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void EveryGeneratedStationCarriesOneHeaterAndAReactor()
|
||||
{
|
||||
var zone = Arena(null);
|
||||
var zone = Arena(true);
|
||||
var generator = new LoadoutGenerator(ref _items.Random, _items, _galaxy, _galaxy.Entrance, _protagonist, .5f);
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
|
|
@ -422,7 +121,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
|
||||
// A main-sector galaxy (not the prelude, which offers every product): each station's gear must come from
|
||||
// manufacturers its faction can reach, the galaxy's factions its allegiance names.
|
||||
private Galaxy MainGalaxy()
|
||||
private Galaxy MainSectorGalaxy()
|
||||
{
|
||||
var authored = AuthoredSettings.Load(FindRepoRoot());
|
||||
return new Galaxy(authored.Read<SectorGenerationSettings>("SectorGenerationSettings"),
|
||||
|
|
@ -443,9 +142,9 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void EveryFactionsStationsCarryOnlyGearItCanReachIncludingAHeaterAndAReactor()
|
||||
{
|
||||
var galaxy = MainGalaxy();
|
||||
var galaxy = MainSectorGalaxy();
|
||||
Assert.False(galaxy.IsPrelude);
|
||||
var zone = Arena(null);
|
||||
var zone = Arena(true);
|
||||
foreach (var faction in galaxy.Factions)
|
||||
{
|
||||
var generator = new LoadoutGenerator(ref _items.Random, _items, galaxy, galaxy.Entrance, faction, .5f);
|
||||
|
|
@ -466,7 +165,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void AFactionReachesItsOwnGearAndItsAllegianceNamesOnlyOthers()
|
||||
{
|
||||
var galaxy = MainGalaxy();
|
||||
var galaxy = MainSectorGalaxy();
|
||||
var products = _cache.GetAll<FactionProductData>().Where(p => p.Manufacturer.IsSet()).ToArray();
|
||||
foreach (var faction in galaxy.Factions)
|
||||
{
|
||||
|
|
@ -483,7 +182,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void AStationWithoutZhestokostAccessIsPoweredAndStaysAboveFreezing()
|
||||
{
|
||||
var galaxy = MainGalaxy();
|
||||
var galaxy = MainSectorGalaxy();
|
||||
Faction Named(string shortName) => _cache.GetAll<Faction>().Single(f => f.ShortName == shortName);
|
||||
var coop = new Faction { Name = "Test Cooperative", ShortName = "Coop" };
|
||||
_cache.Upsert(coop);
|
||||
|
|
@ -497,7 +196,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
var own = new FactionProductData { Name = "Cooperative Capacitor", Design = products.First().Design, Manufacturer = _cache.RefOf(coop) };
|
||||
_cache.Upsert(own);
|
||||
Assert.True(generator.IsAvailable(own), "a faction reaches its own manufacturer's gear");
|
||||
var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs
|
||||
var zone = Arena(false); // no ships: nothing but the zone runs
|
||||
Entity station = null;
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
|
|
@ -525,7 +224,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void AnIdleStationStaysAboveFreezing()
|
||||
{
|
||||
var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs
|
||||
var zone = Arena(false); // no ships: nothing but the zone runs
|
||||
var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0);
|
||||
var heater = station.Equipment.Single(IsHeater);
|
||||
var coldest = float.MaxValue;
|
||||
|
|
@ -542,7 +241,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void AStationHeaterHeatsBelowItsTargetAndNotAbove()
|
||||
{
|
||||
var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs
|
||||
var zone = Arena(false); // no ships: nothing but the zone runs
|
||||
var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0);
|
||||
var heater = station.Equipment.Single(IsHeater);
|
||||
var target = heater.Behaviors.OfType<Thermotoggle>().Single().TargetTemperature;
|
||||
|
|
@ -565,7 +264,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void AHeaterAboveItsTargetDrawsNoPower()
|
||||
{
|
||||
var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs
|
||||
var zone = Arena(false); // no ships: nothing but the zone runs
|
||||
var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0);
|
||||
var heater = station.Equipment.Single(IsHeater);
|
||||
var target = heater.Behaviors.OfType<Thermotoggle>().Single().TargetTemperature;
|
||||
|
|
@ -616,7 +315,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void NoOrbitStandsStillAgainstItsParent()
|
||||
{
|
||||
var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs
|
||||
var zone = Arena(false); // no ships: nothing but the zone runs
|
||||
Assert.True(zone.AsteroidBelts.Count > 0, "the arena has an asteroid belt to race against");
|
||||
float2 Relative(Orbit orbit, bool previous) =>
|
||||
(previous ? orbit.PreviousPosition : orbit.Position) -
|
||||
|
|
@ -638,7 +337,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void EveryAsteroidInABeltIsPlacedForTheSameTime()
|
||||
{
|
||||
var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs
|
||||
var zone = Arena(false); // no ships: nothing but the zone runs
|
||||
var (key, belt) = zone.AsteroidBelts.First();
|
||||
var asteroids = ((AsteroidBeltData) zone.Planets[key]).Asteroids;
|
||||
const float dt = .02f;
|
||||
|
|
@ -668,7 +367,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void MiningWhileTheBeltUpdatesNeverRacesIt()
|
||||
{
|
||||
var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs
|
||||
var zone = Arena(false); // no ships: nothing but the zone runs
|
||||
var (key, _) = zone.AsteroidBelts.First();
|
||||
var data = (AsteroidBeltData) zone.Planets[key];
|
||||
var count = data.Asteroids.Length;
|
||||
|
|
@ -963,7 +662,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void TheBusBillsAHeaterExactlyWhenItsThermostatIsOpen()
|
||||
{
|
||||
var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs
|
||||
var zone = Arena(false); // no ships: nothing but the zone runs
|
||||
var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0);
|
||||
var heater = station.Equipment.Single(IsHeater);
|
||||
var thermostat = heater.Behaviors.OfType<Thermotoggle>().Single();
|
||||
|
|
@ -1007,7 +706,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
{
|
||||
Name = "Two Group Gear", Design = new CultRecordRef<CraftedItemData>(_cache.RefOf(gear).Key), Manufacturer = _cache.RefOf(_protagonist)
|
||||
});
|
||||
var zone = Arena(new Scenario { Ambient = false });
|
||||
var zone = Arena(false);
|
||||
var ship = BareHull(Hull("Djinni"));
|
||||
ship.Zone = zone;
|
||||
Assert.True(ship.TryEquip(Instance(gear)));
|
||||
|
|
@ -1018,59 +717,6 @@ public sealed partial class RunStartTests : IDisposable
|
|||
Assert.True(abs(ship.PowerBus.TotalDemand - 7) < .01f, $"billed {ship.PowerBus.TotalDemand} for a closed 10 and an ungated 7");
|
||||
}
|
||||
|
||||
// Q3, with the operator's 2026-09-30 ruling on the ten product-less designs: a design no product sells is a
|
||||
// scenario test design and nothing else. A test design is one a scenario places: its preset's hull or a slot of
|
||||
// it, or cargo it carries. These are the unsold designs no scenario places, by name.
|
||||
private static List<string> UnsoldAndUnplaced(CultCache cache)
|
||||
{
|
||||
var sold = cache.GetAll<FactionProductData>().Select(product => product.Design.Key).ToHashSet();
|
||||
var placed = new HashSet<CultRecordKey>();
|
||||
foreach (var scenario in cache.GetAll<Scenario>())
|
||||
foreach (var ship in scenario.Entities.Prepend(scenario.Player).Where(ship => ship != null))
|
||||
{
|
||||
foreach (var cargo in ship.Cargo) placed.Add(cargo.Key);
|
||||
var preset = cache.Get(ship.Loadout);
|
||||
if (preset == null) continue;
|
||||
placed.Add(preset.Hull.Key);
|
||||
foreach (var slot in preset.Slots) placed.Add(slot.Design.Key);
|
||||
}
|
||||
return cache.GetAll<EquippableItemData>()
|
||||
.Where(design => !sold.Contains(cache.RefOf(design).Key) && !placed.Contains(cache.RefOf(design).Key))
|
||||
.Select(design => design.Name).OrderBy(name => name, StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
// The shipped catalog holds no unsold design outside a scenario (today, none unsold at all). A new unsold design is
|
||||
// flagged until a scenario places it, in a preset's slot or as an entity's cargo.
|
||||
[Fact]
|
||||
public void EveryUnsoldDesignIsAScenarioTestDesign()
|
||||
{
|
||||
Assert.Empty(UnsoldAndUnplaced(_cache));
|
||||
|
||||
GearData Probe(string name)
|
||||
{
|
||||
var design = new GearData { Name = name, Hardpoint = HardpointType.Tool, Shape = new Shape(), Durability = 1 };
|
||||
_cache.Upsert(design);
|
||||
return design;
|
||||
}
|
||||
var fitted = Probe("Census Fitted");
|
||||
var carried = Probe("Census Carried");
|
||||
Assert.Equal(new[] { "Census Carried", "Census Fitted" }, UnsoldAndUnplaced(_cache));
|
||||
|
||||
var preset = Preset(new Loadout
|
||||
{
|
||||
Name = "census preset", Hull = _cache.RefOf(Hull("Djinni")), WeaponGroups = new int[0][],
|
||||
Slots = { new LoadoutSlot { Design = new CultRecordRef<EquippableItemData>(_cache.RefOf(fitted).Key) } }
|
||||
});
|
||||
var scenario = Quiet(At(Bare(Hull("Djinni")), 0, 0), Placed(preset, 100, 0, ScenarioStance.Neutral, false));
|
||||
scenario.Name = "census";
|
||||
_cache.Upsert(scenario);
|
||||
Assert.Equal(new[] { "Census Carried" }, UnsoldAndUnplaced(_cache));
|
||||
|
||||
scenario.Entities[0].Cargo.Add(new CultRecordRef<EquippableItemData>(_cache.RefOf(carried).Key));
|
||||
_cache.Upsert(scenario);
|
||||
Assert.Empty(UnsoldAndUnplaced(_cache));
|
||||
}
|
||||
|
||||
// The Q3 rule must not reach a real hull: the hulls scenarios rely on each have a product, and materialize bare,
|
||||
// branded, under the prelude's availability.
|
||||
[Fact]
|
||||
|
|
@ -1080,7 +726,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
foreach (var hull in new[] { Hull("Djinni"), Hull("LonginusX"), TurretHull() })
|
||||
{
|
||||
var failures = new List<string>();
|
||||
var entity = Loadouts.Materialize(_items, null, _cache.Get(Bare(hull)), available.IsAvailable, failures);
|
||||
var entity = Loadouts.Materialize(_items, null, BareLoadout(hull), available.IsAvailable, failures);
|
||||
Assert.Empty(failures);
|
||||
Assert.NotNull(_items.Brand(entity.Hull).Maker);
|
||||
}
|
||||
|
|
@ -1091,7 +737,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void AdmitIsTheOnlyAgentCreator()
|
||||
{
|
||||
var pack = RunStart.GenerateArena(_items, _zoneSettings, _galaxy, null);
|
||||
var pack = RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scripted(true, _ => { }));
|
||||
var packedPlayer = pack.Entities.OfType<ShipPack>().First();
|
||||
packedPlayer.IsPlayerShip = true;
|
||||
var zone = new Zone(_items, _planetSettings, pack, _galaxy.Entrance, _galaxy);
|
||||
|
|
@ -1099,44 +745,10 @@ public sealed partial class RunStartTests : IDisposable
|
|||
Assert.True(ships.Count > 1);
|
||||
foreach (var ship in ships) Assert.Equal(ship.IsPlayerShip ? 0 : 1, AgentsOf(zone, ship));
|
||||
|
||||
var arriving = (Ship) Loadouts.Materialize(_items, zone, _cache.Get(Bare(Hull("Djinni"))), _ => true, new List<string>());
|
||||
var arriving = (Ship) Loadouts.Materialize(_items, zone, BareLoadout(Hull("Djinni")), _ => true, new List<string>());
|
||||
var agents = zone.Agents.Count;
|
||||
zone.Admit(arriving, piloted: false);
|
||||
SafeAssert.In(zone, arriving, "the arrival is admitted");
|
||||
Assert.Equal(agents, zone.Agents.Count);
|
||||
}
|
||||
|
||||
// The scenario type is catalog data: it round-trips through the catalog file.
|
||||
[Fact]
|
||||
public void AScenarioRoundTripsThroughTheCatalog()
|
||||
{
|
||||
var djinni = Bare(Hull("Djinni"));
|
||||
var cargo = new CultRecordRef<EquippableItemData>(_cache.RefOf(Hull("LonginusX")).Key);
|
||||
var scenario = new Scenario
|
||||
{
|
||||
Name = "Round trip", Brief = "one of everything", Seed = 42, Ambient = true,
|
||||
Player = new ScenarioShip { Loadout = djinni, Position = float2(1, 2), Direction = float2(0, -1), Cargo = { cargo } },
|
||||
Entities = { Placed(djinni, 3, 4, ScenarioStance.Hostile, piloted: true) }
|
||||
};
|
||||
// Written through its own cache over a second copy of the catalog, disposed, then read back fresh
|
||||
var copy = Path.Combine(_root, "roundtrip.cc");
|
||||
File.Copy(Path.Combine(FindRepoRoot(), "GameData", "Aetheria.cc"), copy);
|
||||
using (var writer = new CultCache(Registry()))
|
||||
{
|
||||
writer.AddBackingStore(new SingleFileMessagePackBackingStore(copy), AetheriaStores.CatalogTypes);
|
||||
writer.Commit(batch => batch.Upsert(typeof(Loadout), _cache.Get(djinni), djinni.Key));
|
||||
writer.Upsert(scenario);
|
||||
writer.FlushAsync().Wait();
|
||||
}
|
||||
|
||||
using var reopened = new CultCache(Registry());
|
||||
reopened.AddBackingStore(new SingleFileMessagePackBackingStore(copy, true), AetheriaStores.CatalogTypes);
|
||||
var loaded = Assert.Single(reopened.GetAll<Scenario>());
|
||||
Assert.Equal(("Round trip", "one of everything", 42u, true), (loaded.Name, loaded.Brief, loaded.Seed, loaded.Ambient));
|
||||
Assert.Equal(djinni.Key, loaded.Player.Loadout.Key);
|
||||
Assert.Equal((float2(1, 2), float2(0, -1)), (loaded.Player.Position, loaded.Player.Direction));
|
||||
Assert.Equal(cargo.Key, Assert.Single(loaded.Player.Cargo).Key);
|
||||
var entity = Assert.Single(loaded.Entities);
|
||||
Assert.Equal((djinni.Key, float2(3, 4), ScenarioStance.Hostile, true), (entity.Loadout.Key, entity.Position, entity.Stance, entity.Piloted));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
568
tests/Aetheria.Shared.Tests/ScenarioTests.cs
Normal file
568
tests/Aetheria.Shared.Tests/ScenarioTests.cs
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using MessagePack;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
|
||||
// Scenarios S2 (docs/scenarios-cut.md, R): every new run is a scenario. The fixture is RunStartTests': a scratch copy
|
||||
// of the shipped catalog, the authored settings and a prelude galaxy at a fixed seed.
|
||||
public sealed partial class RunStartTests
|
||||
{
|
||||
// A test's own scenario: a script given inline, staged into the fixture's arena or launched whole.
|
||||
private sealed class Scripted : Scenario
|
||||
{
|
||||
private readonly bool _ambient;
|
||||
private readonly Action<ScenarioStage> _stage;
|
||||
private readonly uint _seed;
|
||||
private readonly Func<GalaxyStage, Galaxy> _generate;
|
||||
|
||||
public Scripted(bool ambient, Action<ScenarioStage> stage, uint seed = GalaxySeed, Func<GalaxyStage, Galaxy> generate = null)
|
||||
{
|
||||
_ambient = ambient;
|
||||
_stage = stage;
|
||||
_seed = seed;
|
||||
_generate = generate ?? (galaxy => galaxy.Prelude());
|
||||
}
|
||||
|
||||
public override string Name => "Scripted";
|
||||
public override string Brief => "A test's own scenario.";
|
||||
public override uint Seed => _seed;
|
||||
public override bool Ambient => _ambient;
|
||||
public override Galaxy Generate(GalaxyStage stage) => _generate(stage);
|
||||
public override void Stage(ScenarioStage stage) => _stage(stage);
|
||||
}
|
||||
|
||||
private static IEnumerable<Scenario> EveryScenario => Scenarios.Modes.Concat(Scenarios.Tests);
|
||||
|
||||
// The galaxy inputs the menu hands to RunStart, read from the authored settings.
|
||||
private GalaxyStage Inputs(Func<uint> clock = null)
|
||||
{
|
||||
var authored = AuthoredSettings.Load(FindRepoRoot());
|
||||
return new GalaxyStage(
|
||||
authored.Read<SectorGenerationSettings>("SectorGenerationSettings"),
|
||||
authored.Read<SectorBackgroundSettings>("SectorBackgroundSettings"),
|
||||
authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings"),
|
||||
authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"),
|
||||
authored.Read<NameGeneratorSettings>("NameGeneratorSettings"),
|
||||
_cache, new PlayerSettings(), Directory.CreateDirectory(Path.Combine(_root, "Narrative")), _ => { }, null, clock);
|
||||
}
|
||||
|
||||
// What MainMenu.Launch and StartGame do with a scenario: its galaxy, its arena, then staging. beforeStaging stands in
|
||||
// for whatever the game runs between the arena and staging.
|
||||
private (Galaxy galaxy, Zone arena, RunStart.Staged staged, List<string> failures) Launch(Scenario scenario, GalaxyStage inputs = null, Action beforeStaging = null)
|
||||
{
|
||||
var galaxy = RunStart.Generate(scenario, inputs ?? Inputs());
|
||||
var arena = new Zone(_items, _planetSettings, RunStart.GenerateArena(_items, _zoneSettings, galaxy, scenario), galaxy.Entrance, galaxy);
|
||||
beforeStaging?.Invoke();
|
||||
var failures = new List<string>();
|
||||
var staged = RunStart.Stage(_items, arena, scenario, _startingHull, _tutorialSettings, failures);
|
||||
return (galaxy, arena, staged, failures);
|
||||
}
|
||||
|
||||
private RunStart.Staged Stage(Zone zone, Scenario scenario)
|
||||
{
|
||||
var failures = new List<string>();
|
||||
var staged = RunStart.Stage(_items, zone, scenario, _startingHull, _tutorialSettings, failures);
|
||||
Assert.True(failures.Count == 0, string.Join("; ", failures));
|
||||
Assert.NotNull(staged);
|
||||
return staged;
|
||||
}
|
||||
|
||||
// Staging that must fail: it returns null and admits nothing. Returns the failures.
|
||||
private List<string> Refused(Zone zone, Scenario scenario)
|
||||
{
|
||||
var entities = zone.Entities.ToList();
|
||||
var agents = zone.Agents.ToList();
|
||||
var failures = new List<string>();
|
||||
Assert.Null(RunStart.Stage(_items, zone, scenario, _startingHull, _tutorialSettings, failures));
|
||||
SafeAssert.Unchanged(zone, entities, agents);
|
||||
return failures;
|
||||
}
|
||||
|
||||
// The rot test: every scenario New Game lists generates its galaxy and stages against the live catalog and the
|
||||
// authored settings with no failure. A script naming a design the catalog lost fails here, by name.
|
||||
[Fact]
|
||||
public void EveryScenarioStages()
|
||||
{
|
||||
Assert.Equal(EveryScenario.Count(), EveryScenario.Select(scenario => scenario.Name).Distinct().Count());
|
||||
foreach (var scenario in EveryScenario)
|
||||
{
|
||||
var (_, arena, staged, failures) = Launch(scenario, Inputs(() => GalaxySeed));
|
||||
Assert.True(failures.Count == 0, $"{scenario.Name}: {string.Join("; ", failures)}");
|
||||
Assert.True(staged.Player.IsPlayerShip, $"{scenario.Name}: the player is not flagged");
|
||||
SafeAssert.In(arena, staged.Player, $"{scenario.Name}: the player is admitted");
|
||||
foreach (var entity in staged.Entities) SafeAssert.In(arena, entity, $"{scenario.Name}: {entity.Name} is admitted");
|
||||
Assert.False(string.IsNullOrWhiteSpace(scenario.Brief), $"{scenario.Name} has no brief");
|
||||
}
|
||||
}
|
||||
|
||||
// The two game modes are what New Game did: the tutorial a prelude galaxy and a starting-hull ship of the
|
||||
// protagonist faction at the entrance's origin; the main galaxy a main galaxy and a starting-hull ship there. Both
|
||||
// arenas keep their generated ships.
|
||||
[Fact]
|
||||
public void ModesMatchOldNewGame()
|
||||
{
|
||||
Assert.Equal("LonginusX", _startingHull);
|
||||
|
||||
var tutorial = Launch(new TutorialGalaxy(), Inputs(() => GalaxySeed));
|
||||
Assert.Empty(tutorial.failures);
|
||||
Assert.True(tutorial.galaxy.IsPrelude, "the tutorial is a prelude galaxy");
|
||||
Assert.Equal(_startingHull, tutorial.staged.Player.HullData.Name);
|
||||
Assert.Same(tutorial.galaxy.ResolveFaction(_tutorialSettings.ProtagonistFaction), tutorial.staged.Player.Faction);
|
||||
Assert.Equal(float3.zero, tutorial.staged.Player.Position);
|
||||
Assert.Empty(tutorial.staged.Entities);
|
||||
Assert.Contains(tutorial.arena.Pack.Entities, entity => entity is ShipPack { IsPlayerShip: false });
|
||||
|
||||
var main = Launch(new MainGalaxy(), Inputs(() => GalaxySeed));
|
||||
Assert.Empty(main.failures);
|
||||
Assert.False(main.galaxy.IsPrelude, "the main galaxy is not a prelude");
|
||||
Assert.Equal(_startingHull, main.staged.Player.HullData.Name);
|
||||
Assert.Equal(float3.zero, main.staged.Player.Position);
|
||||
Assert.Empty(main.staged.Entities);
|
||||
Assert.Contains(main.arena.Pack.Entities, entity => entity is ShipPack { IsPlayerShip: false });
|
||||
}
|
||||
|
||||
// Everything a galaxy's layout fixes: each zone's name, position, owner, factions and links, the homes, the
|
||||
// entrance, the exit and the background.
|
||||
private static string Layout(Galaxy galaxy) =>
|
||||
string.Join(";", galaxy.Zones.Select(zone =>
|
||||
$"{zone.Name}@{zone.Position}:{zone.Owner?.Name}:[{string.Join(",", zone.Factions.Select(faction => faction.Name))}]" +
|
||||
$":[{string.Join(",", zone.AdjacentZones.Select(adjacent => Array.IndexOf(galaxy.Zones, adjacent)).OrderBy(i => i))}]")) +
|
||||
$" homes {string.Join(",", galaxy.HomeZones.Select(home => $"{home.Key.Name}={Array.IndexOf(galaxy.Zones, home.Value)}").OrderBy(home => home))}" +
|
||||
$" entrance {Array.IndexOf(galaxy.Zones, galaxy.Entrance)} exit {Array.IndexOf(galaxy.Zones, galaxy.Exit)} noise {galaxy.Background.NoisePosition}";
|
||||
|
||||
// An arena's bodies and every entity in it, down to each item's design, maker, workmanship and role fills.
|
||||
private string Layout(ZonePack arena) =>
|
||||
$"{arena.Radius} {arena.Mass} planets [{string.Join(",", arena.Planets.Select(planet => _cache.Get(planet)).Select(body => $"{body.GetType().Name}:{body.Mass}"))}]" +
|
||||
$" orbits [{string.Join(",", arena.Orbits.Select(orbit => _cache.Get(orbit).Distance))}]" +
|
||||
$" entities [{string.Join(",", arena.Entities.Select(Gear))}]";
|
||||
|
||||
// What a launch staged: the player and everything placed, each where it is and with exactly what it carries.
|
||||
private string Fits(RunStart.Staged staged) =>
|
||||
string.Join(" | ", staged.Entities.Prepend(staged.Player).Select(entity => $"{entity.Name}@{entity.Position}:{Gear(EntitySerializer.Pack(entity))}"));
|
||||
|
||||
private string Gear(EntityPack pack) =>
|
||||
$"{pack.GetType().Name}{(pack is ShipPack ship ? "@" + ship.Position : "")}:{string.Join("/", EntitySerializer.Items(pack).Select(Item))}";
|
||||
|
||||
private string Item(ItemInstance item)
|
||||
{
|
||||
var name = _items.GetData(item)?.Name;
|
||||
if (!(item is CraftedItemInstance crafted)) return name;
|
||||
var lot = _items.GetLot(crafted);
|
||||
return $"{name}<{(lot.Origin as Attributed)?.Faction.Key.Value}>~{lot.Quality}" +
|
||||
string.Concat((lot.Roles ?? new List<RoleFill>()).Select(fill => $"/{fill.Role}={fill.Quality}"));
|
||||
}
|
||||
|
||||
// A fixed seed fixes the galaxy, its background, its arena and every fit, each item's maker and workmanship
|
||||
// included, launch after launch in one session, whatever ran in between. Seed zero takes the clock, so two launches
|
||||
// at two clock readings are two galaxies.
|
||||
[Fact]
|
||||
public void SeedRules()
|
||||
{
|
||||
foreach (var scenario in Scenarios.Tests)
|
||||
{
|
||||
Assert.NotEqual(0u, scenario.Seed);
|
||||
var first = Launch(scenario);
|
||||
Launch(new MainGalaxy(), Inputs(() => 4242));
|
||||
_items.Random = new CultMath.Random(0xC0FFEE); // play draws from the run's item random
|
||||
var second = Launch(scenario, beforeStaging: () => _items.Random = new CultMath.Random(0xBADC0DE));
|
||||
Assert.True(first.failures.Count == 0, $"{scenario.Name}: {string.Join("; ", first.failures)}");
|
||||
Assert.Equal(Layout(first.galaxy), Layout(second.galaxy));
|
||||
Assert.Equal(Layout(first.arena.Pack), Layout(second.arena.Pack));
|
||||
Assert.Equal(Fits(first.staged), Fits(second.staged));
|
||||
}
|
||||
|
||||
var clock = new Queue<uint>(new[] { 1000u, 2000u });
|
||||
var inputs = Inputs(() => clock.Dequeue());
|
||||
var everyLaunchNew = new Scripted(true, _ => { }, seed: 0, generate: galaxy => galaxy.Main());
|
||||
var a = RunStart.Generate(everyLaunchNew, inputs);
|
||||
Assert.Equal(1000u, inputs.Seed);
|
||||
var b = RunStart.Generate(everyLaunchNew, inputs);
|
||||
Assert.Equal(2000u, inputs.Seed);
|
||||
Assert.NotEqual(Layout(a), Layout(b));
|
||||
}
|
||||
|
||||
// Generating a galaxy writes nothing a later one reads: a prelude leaves the catalog's faction influence as authored.
|
||||
[Fact]
|
||||
public void AGalaxyLeavesTheCatalogsFactionsAsAuthored()
|
||||
{
|
||||
var authored = _cache.GetAll<Faction>().ToDictionary(faction => faction, faction => faction.InfluenceDistance);
|
||||
RunStart.Generate(new TutorialGalaxy(), Inputs());
|
||||
RunStart.Generate(new MainGalaxy(), Inputs());
|
||||
Assert.All(authored, pair => Assert.Equal(pair.Value, pair.Key.InfluenceDistance));
|
||||
}
|
||||
|
||||
// Every zone is linked into one graph. These seeds left zones with no link at all (the vendored float-precision
|
||||
// convex-hull triangulation dropped them), so the constructor threw KeyNotFoundException placing homes (prelude)
|
||||
// or the entrance (main). Each galaxy's links are a subset of its zone positions' Delaunay edges, and the
|
||||
// triangulation agrees with a brute-force empty-circumcircle oracle.
|
||||
[Theory]
|
||||
[InlineData(103u, true)]
|
||||
[InlineData(134u, true)]
|
||||
[InlineData(137u, true)]
|
||||
[InlineData(173u, true)]
|
||||
[InlineData(227u, true)]
|
||||
[InlineData(285u, true)]
|
||||
[InlineData(34u, false)]
|
||||
public void EveryZoneIsLinked(uint seed, bool prelude)
|
||||
{
|
||||
var galaxy = RunStart.Generate(new Scripted(true, _ => { }, seed, stage => prelude ? stage.Prelude() : stage.Main()), Inputs());
|
||||
Assert.All(galaxy.Zones, zone => Assert.Equal(galaxy.Zones.Length, zone.Distance.Count));
|
||||
|
||||
var positions = galaxy.Zones.Select(zone => zone.Position).ToArray();
|
||||
var edges = Delaunay.Edges(positions);
|
||||
Assert.Equal(BruteForceDelaunay(positions), edges);
|
||||
var delaunay = new HashSet<(int, int)>(edges);
|
||||
for (var i = 0; i < galaxy.Zones.Length; i++)
|
||||
foreach (var adjacent in galaxy.Zones[i].AdjacentZones)
|
||||
{
|
||||
var j = Array.IndexOf(galaxy.Zones, adjacent);
|
||||
Assert.Contains((Math.Min(i, j), Math.Max(i, j)), delaunay);
|
||||
}
|
||||
}
|
||||
|
||||
// Every triangle whose circumcircle holds no other point, as sorted edges.
|
||||
private static List<(int a, int b)> BruteForceDelaunay(float2[] points)
|
||||
{
|
||||
var edges = new SortedSet<(int a, int b)>();
|
||||
for (var i = 0; i < points.Length; i++)
|
||||
for (var j = i + 1; j < points.Length; j++)
|
||||
for (var k = j + 1; k < points.Length; k++)
|
||||
{
|
||||
double ax = points[i].x, ay = points[i].y, bx = points[j].x, by = points[j].y, cx = points[k].x, cy = points[k].y;
|
||||
var d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
||||
if (d == 0) continue;
|
||||
double a2 = ax * ax + ay * ay, b2 = bx * bx + by * by, c2 = cx * cx + cy * cy;
|
||||
var ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d;
|
||||
var uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
|
||||
var r2 = (ax - ux) * (ax - ux) + (ay - uy) * (ay - uy);
|
||||
var empty = true;
|
||||
for (var m = 0; m < points.Length && empty; m++)
|
||||
if (m != i && m != j && m != k)
|
||||
{
|
||||
double dx = points[m].x - ux, dy = points[m].y - uy;
|
||||
empty = dx * dx + dy * dy >= r2;
|
||||
}
|
||||
if (!empty) continue;
|
||||
edges.Add((i, j));
|
||||
edges.Add((j, k));
|
||||
edges.Add((i, k));
|
||||
}
|
||||
return edges.ToList();
|
||||
}
|
||||
|
||||
// A scenario naming one unknown design admits nothing, not even the parts that built, and names the design.
|
||||
[Fact]
|
||||
public void StagingIsAllOrNothing()
|
||||
{
|
||||
var failures = Refused(Arena(true), new Scripted(true, stage =>
|
||||
{
|
||||
stage.Player(stage.Bare("Djinni"), float2(0, 0));
|
||||
stage.Place(stage.Generated("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true);
|
||||
stage.Place(stage.Fit("LonginusX", ("No Such Gun", int2(1, 8), ItemRotation.None)), float2(-300, 0));
|
||||
}));
|
||||
Assert.Contains("No Such Gun", Assert.Single(failures));
|
||||
}
|
||||
|
||||
// A stance holds both ways between an entity and the player. A piloted entity has exactly one agent; an unpiloted
|
||||
// one has none, and nothing steers it. (It is not frozen: the zone's gravity still moves it, as it moves any ship,
|
||||
// so the rule is observed on its steering inputs.)
|
||||
[Fact]
|
||||
public void StanceAndPilot()
|
||||
{
|
||||
var zone = Arena(true);
|
||||
var staged = Stage(zone, new Scripted(true, stage =>
|
||||
{
|
||||
stage.Player(stage.Bare("Djinni"), float2(0, 0));
|
||||
stage.Place(stage.Generated("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true);
|
||||
stage.Place(stage.Bare("LonginusX"), float2(-300, 0), stance: ScenarioStance.Neutral, piloted: false);
|
||||
}));
|
||||
var player = staged.Player;
|
||||
var hostile = staged.Entities[0];
|
||||
var neutral = staged.Entities[1];
|
||||
|
||||
Assert.True(player.IsHostileTo(hostile) && hostile.IsHostileTo(player), "hostile both ways");
|
||||
Assert.False(player.IsHostileTo(neutral) || neutral.IsHostileTo(player), "neutral both ways");
|
||||
// Admission activates: the live hostility each side tracks (what targeting and grudges read) agrees.
|
||||
Assert.True(player.EntityHostility[hostile] && hostile.EntityHostility[player]);
|
||||
Assert.False(player.EntityHostility[neutral] || neutral.EntityHostility[player]);
|
||||
|
||||
Assert.Equal(1, AgentsOf(zone, hostile));
|
||||
Assert.Equal(0, AgentsOf(zone, neutral));
|
||||
Assert.Equal(0, AgentsOf(zone, player));
|
||||
var look = neutral.LookDirection;
|
||||
for (var tick = 0; tick < 50; tick++)
|
||||
{
|
||||
zone.Update(.1f);
|
||||
Assert.Equal(float2.zero, ((Ship) neutral).MovementDirection);
|
||||
Assert.Equal(look, neutral.LookDirection);
|
||||
}
|
||||
Assert.Equal(0, AgentsOf(zone, neutral));
|
||||
}
|
||||
|
||||
// A stance lasts the sitting: the player docking and undocking leaves it standing both ways. It goes with an entity
|
||||
// gone for good: once an entity is destroyed, the player holds no stance toward it.
|
||||
[Fact]
|
||||
public void AStanceSurvivesDockingButNotDestruction()
|
||||
{
|
||||
var zone = Arena(true);
|
||||
var staged = Stage(zone, new Scripted(true, stage =>
|
||||
{
|
||||
stage.Player(stage.Bare("Djinni"), float2(0, 0));
|
||||
stage.Place(stage.Bare("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile);
|
||||
stage.Place(stage.Bare("LonginusX"), float2(-300, 0), stance: ScenarioStance.Hostile);
|
||||
}));
|
||||
var player = staged.Player;
|
||||
var hostile = staged.Entities[0];
|
||||
var doomed = staged.Entities[1];
|
||||
var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0);
|
||||
|
||||
Assert.NotNull(station.TryDock(player));
|
||||
SafeAssert.NotIn(zone, player, "a docked ship is out of the zone");
|
||||
Assert.True(station.TryUndock(player));
|
||||
Assert.True(player.IsHostileTo(hostile) && hostile.IsHostileTo(player), "the stance was lost over docking");
|
||||
Assert.True(player.EntityHostility[hostile] && hostile.EntityHostility[player], "the live hostility was lost over docking");
|
||||
|
||||
// Bare fits carry no faction, so without its stance the derived rule is not hostile
|
||||
Assert.True(player.Faction == null && doomed.Faction == null, "the fixture's ships are factionless");
|
||||
Assert.True(player.IsHostileTo(doomed));
|
||||
doomed.Hull.Durability = 0;
|
||||
doomed.HullDamage.OnNext(1);
|
||||
SafeAssert.NotIn(zone, doomed, "a destroyed entity leaves the zone");
|
||||
Assert.False(player.IsHostileTo(doomed), "the stance toward a destroyed entity outlived it");
|
||||
}
|
||||
|
||||
// Undocking admits the ship unpiloted: back in the zone, active (its live hostility is tracked), with no agent.
|
||||
[Fact]
|
||||
public void UndockingAdmitsWithNoAgent()
|
||||
{
|
||||
var zone = Arena(true);
|
||||
var player = Stage(zone, new TutorialGalaxy()).Player;
|
||||
var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0);
|
||||
var agents = zone.Agents.Count;
|
||||
|
||||
Assert.NotNull(station.TryDock(player));
|
||||
SafeAssert.NotIn(zone, player, "a docked ship is out of the zone");
|
||||
Assert.True(station.TryUndock(player));
|
||||
SafeAssert.In(zone, player, "an undocked ship is back in the zone");
|
||||
Assert.Equal(agents, zone.Agents.Count);
|
||||
Assert.Equal(0, AgentsOf(zone, player));
|
||||
Assert.True(player.EntityHostility.ContainsKey(station), "an undocked ship is active");
|
||||
}
|
||||
|
||||
// Ambient false keeps the stations and drops every generated ship and turret; ambient keeps them. The arena is the
|
||||
// galaxy's entrance pack. The entrance holds a story station, placed as the narrative would place one, so a story
|
||||
// station's own turrets are dropped too.
|
||||
[Fact]
|
||||
public void AQuietArenaHasNoGeneratedShipOrTurretButKeepsItsStations()
|
||||
{
|
||||
(int ships, int turrets, int stations) Census(ZonePack pack) => (
|
||||
pack.Entities.OfType<ShipPack>().Count(),
|
||||
pack.Entities.OfType<OrbitalEntityPack>().Count(e => ((HullData) _cache.Get(e.Hull.Data)).HullType == HullType.Turret),
|
||||
pack.Entities.OfType<OrbitalEntityPack>().Count(e => ((HullData) _cache.Get(e.Hull.Data)).HullType == HullType.Station));
|
||||
|
||||
_galaxy.Entrance.Locations.Add(new LocationStory
|
||||
{
|
||||
Zone = _galaxy.Entrance, Name = "story", Type = LocationType.Station, Faction = _protagonist, Turrets = 2
|
||||
});
|
||||
var ambient = Census(RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scripted(true, _ => { })));
|
||||
var quietPack = RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scripted(false, _ => { }));
|
||||
var quiet = Census(quietPack);
|
||||
|
||||
Assert.Same(quietPack, _galaxy.Entrance.PackedContents);
|
||||
Assert.True(ambient.ships > 0 && ambient.turrets > 0, $"an ambient arena generated {ambient}");
|
||||
Assert.Equal(0, quiet.ships);
|
||||
Assert.Equal(0, quiet.turrets);
|
||||
Assert.True(quiet.stations > 0);
|
||||
Assert.Equal(ambient.stations, quiet.stations);
|
||||
Assert.Contains(quietPack.Entities.OfType<OrbitalEntityPack>(), station => station.Story == 0);
|
||||
}
|
||||
|
||||
// A scenario's player is its fit (here a catalog preset), at the scripted place and heading, carrying its cargo.
|
||||
[Fact]
|
||||
public void AScenarioPlayerIsItsFitAtItsPlaceWithItsCargo()
|
||||
{
|
||||
var generator = new LoadoutGenerator(ref _items.Random, _items, null, null, null, 2);
|
||||
var preset = Loadouts.Capture(_items, (Ship) EntitySerializer.Unpack(_items, null, generator.GenerateShipLoadout(hull => hull.Name == "Djinni")), "test djinni");
|
||||
_cache.Upsert(preset);
|
||||
// Two one-cell designs a manufacturer makes, by name, so the cargo is ordinary branded stock
|
||||
var cargo = _cache.GetAll<GearData>()
|
||||
.Where(design => design.Shape.Coordinates.Length == 1)
|
||||
.Where(design => _cache.GetAll<FactionProductData>().Any(p => p.Design.Key.Equals(_cache.RefOf(design).Key)))
|
||||
.OrderBy(design => design.Name, StringComparer.Ordinal)
|
||||
.Take(2)
|
||||
.ToList();
|
||||
Assert.Equal(2, cargo.Count);
|
||||
|
||||
var zone = Arena(true);
|
||||
var player = Stage(zone, new Scripted(true, stage =>
|
||||
stage.Cargo(stage.Player(stage.Preset("test djinni"), float2(120, -40), float2(3, 4)), cargo.Select(design => design.Name).ToArray()))).Player;
|
||||
|
||||
Assert.Equal("Djinni", player.HullData.Name);
|
||||
SafeAssert.In(zone, player, "the player is admitted");
|
||||
Assert.Equal(float3(120, 0, -40), player.Position);
|
||||
Assert.True(length(player.Direction - float2(.6f, .8f)) < 1e-5f, $"heading {player.Direction}");
|
||||
Assert.Equal(
|
||||
preset.Slots.Select(slot => (slot.Position, slot.Design.Key)).OrderBy(s => s.ToString()),
|
||||
player.Equipment.Concat<EquippedItem>(player.CargoBays).Concat(player.DockingBays)
|
||||
.Where(item => item != player.EquippedHull)
|
||||
.Select(item => (item.Position, _cache.RefOf(_items.GetData(item.EquippableItem)).Key))
|
||||
.OrderBy(s => s.ToString()));
|
||||
Assert.Equal(cargo.Select(design => _cache.RefOf(design).Key.Value).OrderBy(k => k),
|
||||
player.CargoBays.First().Cargo.Keys.Select(item => item.Data.Key.Value).OrderBy(k => k));
|
||||
}
|
||||
|
||||
// A turret hull stages as an orbital entity with no orbit, which stays where it is put.
|
||||
[Fact]
|
||||
public void ATurretStagesAsAStationaryOrbitalEntity()
|
||||
{
|
||||
var zone = Arena(true);
|
||||
var staged = Stage(zone, new Scripted(true, stage =>
|
||||
{
|
||||
stage.Player(stage.Bare("Djinni"), float2(0, 0));
|
||||
stage.Place(stage.Bare(TurretHull().Name), float2(200, 50), stance: ScenarioStance.Hostile);
|
||||
}));
|
||||
var turret = Assert.IsType<OrbitalEntity>(staged.Entities[0]);
|
||||
Assert.False(turret.OrbitData.IsSet());
|
||||
SafeAssert.In(zone, turret, "the turret is admitted");
|
||||
for (var tick = 0; tick < 20; tick++) zone.Update(.1f);
|
||||
Assert.Equal(float2(200, 50), turret.Position.xz);
|
||||
}
|
||||
|
||||
// A run has exactly one player, and it is a ship; a station is never staged, and a turret is never piloted. Each
|
||||
// is a staging failure, named, with nothing admitted.
|
||||
[Fact]
|
||||
public void AMisplacedHullOrPlayerIsAStagingFailure()
|
||||
{
|
||||
var station = _cache.GetAll<HullData>().First(hull => hull.HullType == HullType.Station).Name;
|
||||
var turret = TurretHull().Name;
|
||||
foreach (var (script, expected) in new (Action<ScenarioStage>, string)[]
|
||||
{
|
||||
(stage => { stage.Player(stage.Bare("Djinni"), float2(0, 0)); stage.Place(stage.Bare(station), float2(200, 0)); }, $"{station} is a station hull"),
|
||||
(stage => stage.Player(stage.Bare(turret), float2(0, 0)), $"player: {turret} is not a ship hull"),
|
||||
(stage => { stage.Player(stage.Bare("Djinni"), float2(0, 0)); stage.Place(stage.Bare(turret), float2(200, 0), piloted: true); }, "cannot be piloted"),
|
||||
(stage => stage.Place(stage.Bare("Djinni"), float2(200, 0)), "places no player"),
|
||||
(stage => { stage.Player(stage.Bare("Djinni"), float2(0, 0)); stage.Player(stage.Bare("Djinni"), float2(9, 0)); }, "a second player"),
|
||||
})
|
||||
{
|
||||
var failures = Refused(Arena(true), new Scripted(true, script));
|
||||
Assert.Contains(failures, failure => failure.Contains(expected));
|
||||
}
|
||||
}
|
||||
|
||||
// Arcs' geometry: the bow hull sits inside every one of the player's mount arcs, the stern hull outside all of them,
|
||||
// and the turret is hostile.
|
||||
[Fact]
|
||||
public void ArcsGeometry()
|
||||
{
|
||||
var staged = Stage(Arena(false), new Arcs());
|
||||
var player = staged.Player;
|
||||
var bow = staged.Entities[0];
|
||||
var stern = staged.Entities[1];
|
||||
var turret = Assert.IsType<OrbitalEntity>(staged.Entities[2]);
|
||||
Assert.Equal((Arcs.Bow, Arcs.Stern, Arcs.Turret), (bow.Position.xz, stern.Position.xz, turret.Position.xz));
|
||||
|
||||
var mounts = player.Equipment.Where(item => item.Behaviors.OfType<Weapon>().Any()).ToList();
|
||||
Assert.NotEmpty(mounts);
|
||||
foreach (var mount in mounts)
|
||||
{
|
||||
Assert.True(FireControl.InArc(mount, bow.Position - player.Position), $"{mount.Data.Name} cannot bear on the bow hull");
|
||||
Assert.False(FireControl.InArc(mount, stern.Position - player.Position), $"{mount.Data.Name} bears on the stern hull");
|
||||
}
|
||||
Assert.True(player.IsHostileTo(turret) && turret.IsHostileTo(player), "the turret is hostile");
|
||||
Assert.False(player.IsHostileTo(bow) || player.IsHostileTo(stern), "the bare hulls are neutral");
|
||||
}
|
||||
|
||||
// Q3 (operator, 2026-09-30): a design no product sells is a scenario test design and nothing else. A test design
|
||||
// is one some scenario's staged entities carry, fitted or in cargo. These are the unsold designs none carries.
|
||||
private List<string> UnsoldAndUnplaced(IEnumerable<RunStart.Staged> runs)
|
||||
{
|
||||
var sold = _cache.GetAll<FactionProductData>().Select(product => product.Design.Key).ToHashSet();
|
||||
var placed = new HashSet<CultRecordKey>();
|
||||
foreach (var entity in runs.SelectMany(run => run.Entities.Prepend(run.Player)))
|
||||
{
|
||||
foreach (var item in entity.Equipment.Concat<EquippedItem>(entity.CargoBays).Concat(entity.DockingBays))
|
||||
placed.Add(_cache.RefOf(_items.GetData(item.EquippableItem)).Key);
|
||||
foreach (var item in entity.CargoBays.SelectMany(bay => bay.Cargo.Keys)) placed.Add(item.Data.Key);
|
||||
}
|
||||
return _cache.GetAll<EquippableItemData>()
|
||||
.Where(design => !sold.Contains(_cache.RefOf(design).Key) && !placed.Contains(_cache.RefOf(design).Key))
|
||||
.Select(design => design.Name).OrderBy(name => name, StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
// The shipped catalog holds no unsold design that no scenario carries (today, none unsold at all). A new unsold
|
||||
// design is flagged until a scenario fits it or carries it as cargo.
|
||||
[Fact]
|
||||
public void EveryUnsoldDesignIsAScenarioTestDesign()
|
||||
{
|
||||
var runs = EveryScenario.Select(scenario => Launch(scenario, Inputs(() => GalaxySeed)).staged).ToList();
|
||||
Assert.Empty(UnsoldAndUnplaced(runs));
|
||||
|
||||
void Probe(string name)
|
||||
{
|
||||
var shape = new Shape(1, 1);
|
||||
shape[int2(0, 0)] = true;
|
||||
_cache.Upsert(new GearData { Name = name, Hardpoint = HardpointType.Tool, Shape = shape, Durability = 1 });
|
||||
}
|
||||
Probe("Census Fitted");
|
||||
Probe("Census Carried");
|
||||
Assert.Equal(new[] { "Census Carried", "Census Fitted" }, UnsoldAndUnplaced(runs));
|
||||
|
||||
runs.Add(Stage(Arena(false), new Scripted(false, stage =>
|
||||
{
|
||||
var ship = stage.Player(stage.Fit("Djinni", ("Census Fitted", int2(1, 6), ItemRotation.None), ("Store-All Plus", int2(3, 6), ItemRotation.None)), float2(0, 0));
|
||||
stage.Cargo(ship, "Census Carried");
|
||||
})));
|
||||
Assert.Empty(UnsoldAndUnplaced(runs));
|
||||
}
|
||||
|
||||
// Whether a run is the prelude is the galaxy's fact (Galaxy.IsPrelude); a save records it with no second owner.
|
||||
[Fact]
|
||||
public void SaveReadsPrelude()
|
||||
{
|
||||
var arena = Arena(true);
|
||||
var (prelude, _) = RunSave.Capture(_cache, _galaxy, arena, null, new SavedActionBarBinding[0]);
|
||||
Assert.True(prelude.IsTutorial);
|
||||
|
||||
var (main, _) = RunSave.Capture(_cache, MainSectorGalaxy(), arena, null, new SavedActionBarBinding[0]);
|
||||
Assert.False(main.IsTutorial);
|
||||
}
|
||||
|
||||
// PlayerSettings Key 2, the never-written tutorial flag, is retired: a player.cc written with it still loads, every other field intact.
|
||||
[Fact]
|
||||
public void OldPlayerSettingsLoad()
|
||||
{
|
||||
var options = CultDocumentMessagePackSerialization.OptionsFor(typeof(PlayerSettings).Assembly);
|
||||
var buffer = new ArrayBufferWriter<byte>();
|
||||
var writer = new MessagePackWriter(buffer);
|
||||
writer.WriteArrayHeader(7);
|
||||
writer.Write("Pilot");
|
||||
writer.WriteNil(); // Key 1, retired before this cut
|
||||
writer.Write(true); // Key 2, the retired tutorial flag
|
||||
MessagePackSerializer.Serialize(ref writer, new Dictionary<string, string> { ["story"] = "hash" }, options);
|
||||
MessagePackSerializer.Serialize(ref writer, new PlayerGameplaySettings { TemperatureUnit = TemperatureUnit.Kelvin, SignificantDigits = 5 }, options);
|
||||
MessagePackSerializer.Serialize(ref writer, new PlayerInputSettings { ActionBarInputs = { "<Keyboard>/1" } }, options);
|
||||
MessagePackSerializer.Serialize(ref writer, new PlayerGraphicsSettings { NebulaQuality = Quality.High, ShowAsteroidsInMinimap = true }, options);
|
||||
writer.Flush();
|
||||
|
||||
var settings = MessagePackSerializer.Deserialize<PlayerSettings>(buffer.WrittenMemory, options);
|
||||
|
||||
Assert.Equal("Pilot", settings.Name);
|
||||
Assert.Equal("hash", settings.HashedStoryFiles["story"]);
|
||||
Assert.Equal((TemperatureUnit.Kelvin, 5), (settings.GameplaySettings.TemperatureUnit, settings.GameplaySettings.SignificantDigits));
|
||||
Assert.Equal("<Keyboard>/1", Assert.Single(settings.InputSettings.ActionBarInputs));
|
||||
Assert.Equal((Quality.High, true), (settings.GraphicsSettings.NebulaQuality, settings.GraphicsSettings.ShowAsteroidsInMinimap));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue