Every new run is a scenario: the script harness, the tutorial and main galaxies as scenarios, the New Game submenu and six test arenas

A Scenario is an abstract C# class compiled with the game: Generate picks the galaxy through GalaxyStage (Main or
Prelude, at the scenario's seed, the background noise drawn from the same seed), Stage places the run's first
entities through ScenarioStage (Fit, Bare, Generated, Preset, Player, Place, Cargo), which collects named failures
and admits nothing. RunStart generates the galaxy, the arena under Ambient and admits everything through Zone.Admit,
all or nothing. The aetheria.scenario record, ScenarioShip, ScenarioEntity, RunStart.Check and the null-scenario
branch are cut.

New Game opens a submenu: Tutorial Galaxy and Main Galaxy, then (editor and development builds) Djinni Shakedown,
Starved Reactor, Arcs, Duel, Launcher Angles and Long Haul, each with its brief. MainMenu.Launch is the one way into a
new run; ActionGameManager.PendingScenario carries the scenario to StartGame once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-03 20:33:55 +02:00
commit d0782a7d14
31 changed files with 1173 additions and 611 deletions

View file

@ -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) };

View 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));
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f5cf458f47074786bf11e72b5d93c545
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,131 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CultMath;
using static CultMath.math;
using float2 = CultMath.float2;
using float3 = CultMath.float3;
// 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 scenario's galaxy, at the scenario's seed or, when that is zero, the clock's.
public static Galaxy Generate(Scenario scenario, GalaxyStage stage)
{
stage.SeedFrom(scenario);
return scenario.Generate(stage) ?? throw new InvalidOperationException($"Scenario {scenario.Name} generated no galaxy.");
}
// 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);
itemManager, zoneSettings, galaxy, galaxy.Entrance, galaxy.IsPrelude, scenario.Ambient);
// 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)
{
var failures = new List<string>();
if (scenario != null) Build(itemManager, null, scenario, _ => true, failures);
return failures;
}
// 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.
// 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)
{
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() };
}
}

View file

@ -1,38 +1,27 @@
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 and its arena, 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

View 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. 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;
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d8cd00dc23604dfb91e128e1a9a4a3d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 35866d82219241ae9fdb0030039afbed
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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);
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 724a07b881f34e8c9b32b3f2f338ac78
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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);
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1afb559630c241ed813ddd3c444c618b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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");
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6ba813d4a9447c89e1bbb6997b7ac35
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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));
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 58cfaf062ddf458c99c12f3652243b45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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);
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a7911611f9845709e9ae5ba34d91dfb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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);
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7f6bdb3ed7b4b1aa137569c7e79bccd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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()
};
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 12f60a9ad25c4aa0a9b8b098d385ea0e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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));
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 91a81ac4784c4066969cac11ac1937b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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);
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 94d688b37c5044dd9b67865c706d8918
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: