diff --git a/Assets/Scripts/ServerShared/Galaxy.cs b/Assets/Scripts/ServerShared/Galaxy.cs index a5660f52e..5fc919d7b 100644 --- a/Assets/Scripts/ServerShared/Galaxy.cs +++ b/Assets/Scripts/ServerShared/Galaxy.cs @@ -24,7 +24,7 @@ public class Galaxy public Faction[] Factions { get; } public GalaxyZone[] Zones { get; } public GalaxyZone Entrance { get; } - public GalaxyZone Exit { get; } + public GalaxyZone Exit { get; private set; } public Dictionary FactionRelationships { get; } = new Dictionary(); private Action Log { get; } public bool IsPrelude { get; } @@ -143,6 +143,18 @@ public class Galaxy return _cache.GetAll().FirstOrDefault(f => f.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase)); } + // A cast member of a prelude galaxy, by name. A name the catalog does not hold refuses the galaxy, naming the field. + private Faction ResolveCast(string field, string name) => + ResolveFaction(name) ?? throw new InvalidOperationException($"cast faction {name} ({field}) is not in the catalog"); + + // Gives a prelude galaxy its gate: the antagonist's home is the boss zone and the exit. The only writer of Exit + // besides the constructors. + public void PlaceGate(Faction antagonist) + { + BossZones[antagonist] = HomeZones[antagonist]; + Exit = HomeZones[antagonist]; + } + // 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) => @@ -169,20 +181,23 @@ public class Galaxy var factions = new List(); - var protagonistFaction = ResolveFaction(settings.ProtagonistFaction); + var protagonistFaction = ResolveCast(nameof(settings.ProtagonistFaction), settings.ProtagonistFaction); factions.Add(protagonistFaction); - var antagonistFaction = ResolveFaction(settings.AntagonistFaction); + var antagonistFaction = ResolveCast(nameof(settings.AntagonistFaction), settings.AntagonistFaction); factions.Add(antagonistFaction); - var bufferFaction = ResolveFaction(settings.BufferFaction); + var bufferFaction = ResolveCast(nameof(settings.BufferFaction), settings.BufferFaction); factions.Add(bufferFaction); - var questFaction = ResolveFaction(settings.QuestFaction); - factions.Add(questFaction); + // A cast without a quest faction places no quest zone. + var questFaction = string.IsNullOrEmpty(settings.QuestFaction) + ? null + : ResolveCast(nameof(settings.QuestFaction), settings.QuestFaction); + if (questFaction != null) factions.Add(questFaction); var neutralFactions = settings.NeutralFactions - .Select(ResolveFaction) + .Select(name => ResolveCast(nameof(settings.NeutralFactions), name)) .ToArray(); factions.AddRange(neutralFactions); @@ -223,17 +238,20 @@ public class Galaxy CalculateFactionInfluence(progressCallback); - var potentialQuestZones = Zones - .Where(z => z.Factions.Contains(antagonistFaction) && z.Factions.Contains(bufferFaction)); - if (potentialQuestZones.Any()) - HomeZones[questFaction] = potentialQuestZones - .MaxBy(z => z.Distance[HomeZones[antagonistFaction]] * ConnectedRegion(z, InfluenceOf(questFaction)).Count); - else - HomeZones[questFaction] = Zones - .Where(z => z.Factions.Contains(antagonistFaction)) - .MinBy(z=>z.Distance[HomeZones[bufferFaction]]); - - CalculateFactionInfluence(progressCallback); + if (questFaction != null) + { + var potentialQuestZones = Zones + .Where(z => z.Factions.Contains(antagonistFaction) && z.Factions.Contains(bufferFaction)); + if (potentialQuestZones.Any()) + HomeZones[questFaction] = potentialQuestZones + .MaxBy(z => z.Distance[HomeZones[antagonistFaction]] * ConnectedRegion(z, InfluenceOf(questFaction)).Count); + else + HomeZones[questFaction] = Zones + .Where(z => z.Factions.Contains(antagonistFaction)) + .MinBy(z=>z.Distance[HomeZones[bufferFaction]]); + + CalculateFactionInfluence(progressCallback); + } Entrance = Zones.Where(z => z.Owner == null).MinBy(z => z.Distance[HomeZones[protagonistFaction]]); diff --git a/Assets/Scripts/ServerShared/GalaxyStage.cs b/Assets/Scripts/ServerShared/GalaxyStage.cs index f4ac7d5f8..8b6e5a1a6 100644 --- a/Assets/Scripts/ServerShared/GalaxyStage.cs +++ b/Assets/Scripts/ServerShared/GalaxyStage.cs @@ -68,7 +68,10 @@ public sealed class GalaxyStage } // A prelude galaxy: the tutorial settings and their fixed factions, centred in dense cloud. - public Galaxy Prelude() + public Galaxy Prelude() => Prelude(_prelude); + + // A prelude galaxy of the given fixed cast, centred in dense cloud. + public Galaxy Prelude(TutorialGenerationSettings cast) { var background = Copy(_preludeBackground); var random = new Random(Seed); @@ -78,7 +81,7 @@ public sealed class GalaxyStage 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); + return new Galaxy(cast, background, _names, _cache, _playerSettings, _narrativeDirectory, _log, _progress, Seed); } private static SectorBackgroundSettings Copy(SectorBackgroundSettings settings) => diff --git a/Assets/Scripts/ServerShared/Scenarios/DemoTerminus.cs b/Assets/Scripts/ServerShared/Scenarios/DemoTerminus.cs new file mode 100644 index 000000000..0555a3b29 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/DemoTerminus.cs @@ -0,0 +1,41 @@ +using float2 = CultMath.float2; + +// The demo: one gate, one region and one boss, with a fixed cast. The Pirates are the protagonist's ostensible +// employer, Zhestokost the antagonist whose home is the boss zone and the exit, Lucent Media and Aeronautics Unlimited +// the neutrals. A prelude galaxy of that cast, entered in a Pirates-made hull. +public sealed class DemoTerminus : Scenario +{ + public static readonly TutorialGenerationSettings Cast = new TutorialGenerationSettings + { + ProtagonistFaction = "Pirates", + AntagonistFaction = "Zhe", + BufferFaction = "Luc", + NeutralFactions = new[] { "Aero" }, + QuestFaction = null, + ZoneCount = 64, + LinkDensity = .5f + }; + + public override string Name => "Terminus (Demo)"; + public override string Brief => "One gate, one region, one boss. Allied: the Pirates. Opposed: Zhestokost. " + + "Neutral: Lucent Media and Aeronautics Unlimited."; + + public override Galaxy Generate(GalaxyStage stage) + { + var galaxy = stage.Prelude(Cast); + galaxy.PlaceGate(galaxy.ResolveFaction(Cast.AntagonistFaction)); + return galaxy; + } + + public override void Stage(ScenarioStage stage) + { + var galaxy = stage.Galaxy; + var pirates = galaxy.ResolveFaction(Cast.ProtagonistFaction); + galaxy.FactionRelationships[pirates] = FactionRelationship.Friendly; + galaxy.FactionRelationships[galaxy.ResolveFaction(Cast.AntagonistFaction)] = FactionRelationship.Hated; + galaxy.FactionRelationships[galaxy.ResolveFaction(Cast.BufferFaction)] = FactionRelationship.Neutral; + foreach (var neutral in Cast.NeutralFactions) + galaxy.FactionRelationships[galaxy.ResolveFaction(neutral)] = FactionRelationship.Neutral; + stage.Player(stage.Generated(stage.StartingHull, pirates), float2.zero); + } +} diff --git a/Assets/Scripts/ServerShared/Scenarios/DemoTerminus.cs.meta b/Assets/Scripts/ServerShared/Scenarios/DemoTerminus.cs.meta new file mode 100644 index 000000000..706f1bf0e --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/DemoTerminus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5ba7152d3c8b46a98924a4148be550db +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs b/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs index 31890d789..4a43c8960 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs @@ -1,8 +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. +// Every way to set up a new run, in the order New Game lists them. Modes are what a release build lists; Development +// are the other ways the game sets up a run and Tests are arenas built to check one system each, both listed only in +// editor and development builds. public static class Scenarios { - public static readonly Scenario[] Modes = { new TutorialGalaxy(), new MainGalaxy() }; + public static readonly Scenario[] Modes = { new DemoTerminus() }; + + public static readonly Scenario[] Development = { new TutorialGalaxy(), new MainGalaxy() }; public static readonly Scenario[] Tests = { diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs index f20208ff7..7c2666420 100644 --- a/Assets/Scripts/UI/MainMenu.cs +++ b/Assets/Scripts/UI/MainMenu.cs @@ -127,8 +127,8 @@ 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. + // New Game: every way to set up a run, each with its brief. A release build lists only the modes; the other ways + // to set up a run and the test arenas are listed only in editor and development builds. private void ShowScenarios() { _nextMenu.panel.Clear(); @@ -136,6 +136,8 @@ public class MainMenu : MonoBehaviour AddScenarios(Scenarios.Modes); if (Debug.isDebugBuild) { + _nextMenu.panel.AddSection("development"); + AddScenarios(Scenarios.Development); _nextMenu.panel.AddSection("test arenas"); AddScenarios(Scenarios.Tests); }