Scenarios Cut 1: RunStart stages a new run; the Scenario catalog type
RunStart (ServerShared, no Unity type) owns what a new run starts with (docs/scenarios-cut.md, 2.2): - GenerateArena: the galaxy's entrance pack, generated under the scenario's Ambient rule. ZoneGenerator.GenerateZone gains `ambient`; false keeps planets, orbits and stations and generates no ship, turret or wanderer. - Check: every failure staging a scenario would hit, found with no zone, touching no store. - Stage: all-or-nothing. With no scenario, the generated starting-hull ship at the origin, as StartGame built it. With one, the player preset and the entities at their authored place and heading with cargo, stance set both ways through SetIff, a Minion for each piloted one; all through Zone.Admit. Scenario (aetheria.scenario v1) joins AetheriaStores.CatalogTypes. RunStartTests stage into a real prelude arena from the shipped catalog and authored settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5706b657e3
commit
85ebe036e1
8 changed files with 582 additions and 6 deletions
|
|
@ -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(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(NameFile), typeof(InputLayout), typeof(Loadout) };
|
||||
public static readonly Type[] CatalogTypes = { typeof(ItemData), typeof(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(NameFile), typeof(InputLayout), typeof(Loadout), typeof(Scenario) };
|
||||
public static readonly Type[] RunTypes = { typeof(OrbitData), typeof(BodyData), typeof(SavedZone), typeof(SavedGame), typeof(ProvenanceLedger) };
|
||||
public static readonly Type[] PlayerTypes = { typeof(PlayerSettings) };
|
||||
|
||||
|
|
|
|||
|
|
@ -149,8 +149,8 @@ public class LoadoutGenerator
|
|||
|
||||
// No galaxy means no availability to filter by: every product is on offer. A fixture generates loadouts that
|
||||
// way, so a test can exercise placement and products without standing up a whole galaxy; no game path does.
|
||||
// Loadouts.Materialize takes availability as a predicate; a game spawner materializing presets is to pass this, so
|
||||
// presets and generation share one availability rule. No game path materializes a preset yet.
|
||||
// Loadouts.Materialize takes availability as a predicate; RunStart, the game's one preset spawner, passes this, so
|
||||
// presets and generation share one availability rule.
|
||||
public bool IsAvailable(FactionProductData product) =>
|
||||
Galaxy == null || Galaxy.IsPrelude ||
|
||||
Galaxy.ContainsFaction(product.Manufacturer) && (Faction == null || Faction.Allegiance.ContainsKey(product.Manufacturer));
|
||||
|
|
|
|||
131
Assets/Scripts/ServerShared/RunStart.cs
Normal file
131
Assets/Scripts/ServerShared/RunStart.cs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
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.
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
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.
|
||||
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)
|
||||
{
|
||||
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();
|
||||
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 item = build();
|
||||
if (bay == null || !bay.TryStore(item))
|
||||
failures.Add($"{where} cargo: no room for {itemManager.GetData(item).Name}");
|
||||
}
|
||||
return failures.Count > reported ? null : entity;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ServerShared/RunStart.cs.meta
Normal file
2
Assets/Scripts/ServerShared/RunStart.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 28412ed9a0fc4f95b157846b0b132c27
|
||||
42
Assets/Scripts/ServerShared/Scenario.cs
Normal file
42
Assets/Scripts/ServerShared/Scenario.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
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
|
||||
{
|
||||
[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>();
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
[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
|
||||
}
|
||||
|
||||
public enum ScenarioStance
|
||||
{
|
||||
Neutral,
|
||||
Hostile
|
||||
}
|
||||
2
Assets/Scripts/ServerShared/Scenario.cs.meta
Normal file
2
Assets/Scripts/ServerShared/Scenario.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: da26f6b6c5f34deaa1057177e2dd3262
|
||||
|
|
@ -35,12 +35,15 @@ public static class ZoneGenerator
|
|||
|
||||
private const int MaximumPlacementSamples = 32;
|
||||
|
||||
// ambient: false keeps the planets, orbits and stations but generates no ship and no turret, so nothing wanders
|
||||
// into a scenario's authored conditions (docs/scenarios-cut.md, 2.1). The rule is applied here, not by pruning.
|
||||
public static ZonePack GenerateZone(
|
||||
ItemManager itemManager,
|
||||
ZoneGenerationSettings zoneSettings,
|
||||
Galaxy galaxy,
|
||||
GalaxyZone galaxyZone,
|
||||
bool isTutorial = false)
|
||||
bool isTutorial = false,
|
||||
bool ambient = true)
|
||||
{
|
||||
var pack = new ZonePack();
|
||||
var cache = itemManager.ItemData;
|
||||
|
|
@ -306,7 +309,7 @@ public static class ZoneGenerator
|
|||
station.Story = i;
|
||||
pack.Entities.Add(station);
|
||||
|
||||
PlaceTurrets(lagrangeOrbit, GetLoadoutGenerator(story.Faction), story.Turrets);
|
||||
if (ambient) PlaceTurrets(lagrangeOrbit, GetLoadoutGenerator(story.Faction), story.Turrets);
|
||||
}
|
||||
|
||||
for (var i = storyStations.Length; i < selectedStationOrbits.Length; i++)
|
||||
|
|
@ -328,9 +331,11 @@ public static class ZoneGenerator
|
|||
station.SecurityRadius = pack.Radius;
|
||||
pack.Entities.Add(station);
|
||||
|
||||
PlaceTurrets(lagrangeOrbit, GetLoadoutGenerator(nearestFaction), 2);
|
||||
if (ambient) PlaceTurrets(lagrangeOrbit, GetLoadoutGenerator(nearestFaction), 2);
|
||||
}
|
||||
|
||||
if (!ambient) return pack;
|
||||
|
||||
var enemyCount = (int)(random.NextFloat() * factionPresence * 2) + baseStationCount;
|
||||
for (int i = 0; i < enemyCount; i++)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue