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 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 Check(ItemManager itemManager, Scenario scenario) { var failures = new List(); 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 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() }; } // 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 entities)? Build(ItemManager itemManager, Zone zone, Scenario scenario, Predicate isAvailable, List 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(); 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 isAvailable, List 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(); 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; } }