Merge origin/master (arena cut 1: ItemManager takes a seed) into eureka/aetheria-gravity-laned-belts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
95903ab00e
118 changed files with 1157 additions and 297 deletions
|
|
@ -38,6 +38,12 @@ public class FactionProductData
|
|||
// no Assembly; the mint derives both from the line.
|
||||
[Inspectable, JsonProperty("bin"), Key(7)]
|
||||
public ProductBin Bin;
|
||||
|
||||
// Off the market (ruling off-market-is-unoffered-product): the product keeps its maker, brand, quality and price, but no
|
||||
// station stocks it, no generation or chooser draws it, and its hull gives no gear a home. It reaches a ship only through a
|
||||
// Loadout that pins it. LoadoutGenerator.IsOffered is its one reader.
|
||||
[Inspectable, JsonProperty("notOffered"), Key(8)]
|
||||
public bool NotOffered;
|
||||
}
|
||||
|
||||
// How good this product's part is in one of its design's roles: a pseudo-gaussian distribution whose mean is
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ using float4 = CultMath.float4;
|
|||
|
||||
public class ItemManager
|
||||
{
|
||||
public Random Random = new Random((uint) (DateTime.Now.Ticks%uint.MaxValue));
|
||||
// The item stream: each lot's quality and each generated fit. Seeded by whoever constructs the manager and reseeded
|
||||
// by RunStart at each step of a new run, never by the clock.
|
||||
public Random Random;
|
||||
|
||||
private Action<string> _logger;
|
||||
|
||||
|
|
@ -25,8 +27,10 @@ public class ItemManager
|
|||
public ProvenanceLedger Lots { get; }
|
||||
public GameplaySettings GameplaySettings { get; }
|
||||
|
||||
public ItemManager(CultCache itemData, ProvenanceLedger lots, GameplaySettings settings, Action<string> logger)
|
||||
public ItemManager(uint seed, CultCache itemData, ProvenanceLedger lots, GameplaySettings settings, Action<string> logger)
|
||||
{
|
||||
if (seed == 0) throw new ArgumentOutOfRangeException(nameof(seed), "an item stream needs a nonzero seed");
|
||||
Random = new Random(seed);
|
||||
ItemData = itemData;
|
||||
Lots = lots;
|
||||
GameplaySettings = settings;
|
||||
|
|
|
|||
|
|
@ -127,6 +127,9 @@ public static class Loadouts
|
|||
// The quality a design no manufacturer makes is built at: the console give's.
|
||||
public const float UnbrandedQuality = .95f;
|
||||
|
||||
// The seed of the scratch item manager a fit is tried on. It draws nothing, so any nonzero constant does.
|
||||
private const uint ScratchSeed = 1u;
|
||||
|
||||
// All-or-nothing: returns an entity only when every design resolved and fitted, the loadout has at most
|
||||
// WeaponGroupCount weapon groups, and every group index names a weapon slot. Every failure of a stage is listed; on any
|
||||
// failure nothing is returned and the zone and cache are unchanged. The stages run in the order that puts refusal
|
||||
|
|
@ -166,7 +169,7 @@ public static class Loadouts
|
|||
|
||||
// The cells fit or they do not whatever brand a design-only slot takes: the shape is the design's. Tried on its own
|
||||
// item manager, so nothing is minted into the live ledger and nothing is drawn.
|
||||
var scratch = new ItemManager(cache, new ProvenanceLedger(), itemManager.GameplaySettings, _ => { });
|
||||
var scratch = new ItemManager(ScratchSeed, cache, new ProvenanceLedger(), itemManager.GameplaySettings, _ => { });
|
||||
Assemble(scratch, null, hullData, loadout, Unbranded(scratch, hullSettled), slotsSettled.Select(settled => Unbranded(scratch, settled)).ToArray(), failures, out _);
|
||||
if (failures.Count > reported) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -197,6 +197,8 @@ public class LoadoutGenerator
|
|||
entry.design != null &&
|
||||
entry.design.Price > 0 &&
|
||||
entry.product.Manufacturer.IsSet() &&
|
||||
// A not-offered product is never a candidate, so the any-manufacturer fallback below cannot reach it either.
|
||||
IsOffered(ItemManager.ItemData, entry.product) &&
|
||||
(!(entry.design is EquippableItemData equippable) || HasHome(equippable, hulls)) &&
|
||||
(filter?.Invoke(entry.design) ?? true))
|
||||
.ToArray();
|
||||
|
|
@ -216,9 +218,10 @@ public class LoadoutGenerator
|
|||
// bunch more hulls before all the gear variety in the game has a home"): generation never offers it, so no station
|
||||
// stocks it. It gains one the moment a hull with a hardpoint that takes it exists. Tool gear goes in any interior.
|
||||
// Only a hull some product sells gives gear a home: a hull no faction builds (the Smoke Test Hull) is a test fixture, so its
|
||||
// hardpoints never put gear on a shelf. The one definition of the hulls HasHome considers.
|
||||
// hardpoints never put gear on a shelf. An off-market hull gives no gear a home either: only its pinned Loadout flies it.
|
||||
// The one definition of the hulls HasHome considers.
|
||||
public static HullData[] SoldHulls(CultCache items) =>
|
||||
items.GetAll<FactionProductData>().Select(product => items.Get(product.Design)).OfType<HullData>().Distinct().ToArray();
|
||||
items.GetAll<FactionProductData>().Where(product => IsOffered(items, product)).Select(product => items.Get(product.Design)).OfType<HullData>().Distinct().ToArray();
|
||||
|
||||
public static bool HasHome(EquippableItemData design, HullData[] hulls) =>
|
||||
design.HardpointType == HardpointType.Tool || design.HardpointType == HardpointType.Hull ||
|
||||
|
|
@ -231,9 +234,16 @@ public class LoadoutGenerator
|
|||
// always reaches its own manufacturer's gear, and otherwise gear made by a manufacturer its allegiance names
|
||||
// (operator, 2026-09-30: allegiance lists only other factions). Whether that manufacturer is present in the galaxy
|
||||
// does not matter (operator, 2026-10-06: a faction need not be present for its gear to be available).
|
||||
// A not-offered product is available to nobody, a stage with no faction included.
|
||||
public bool IsAvailable(FactionProductData product) =>
|
||||
Galaxy == null || Faction == null ||
|
||||
ItemManager.ItemData.Get(product.Manufacturer) == Faction || Faction.Allegiance.ContainsKey(product.Manufacturer);
|
||||
IsOffered(ItemManager.ItemData, product) && (
|
||||
Galaxy == null || Faction == null ||
|
||||
ItemManager.ItemData.Get(product.Manufacturer) == Faction || Faction.Allegiance.ContainsKey(product.Manufacturer));
|
||||
|
||||
// Whether a product is on the market at all (ruling off-market-is-unoffered-product). The one reader of NotOffered. A bin is
|
||||
// offered only if both it and its line are (ruling bin-inherits-line-offered); the catalog read guarantees a bin's line exists.
|
||||
public static bool IsOffered(CultCache items, FactionProductData product) =>
|
||||
!product.NotOffered && (product.Bin == null || !items.Get(product.Bin.Line).NotOffered);
|
||||
|
||||
// How much this outfitter favors a product: its maker's preference times the product's share of it. The one weight of
|
||||
// generation and of ChooseProduct. A bin takes the share of its maker's stock that would pass its floor and its line takes
|
||||
|
|
@ -248,9 +258,10 @@ public class LoadoutGenerator
|
|||
// 1 - Phi(floor): the fraction of a normal draw that lies at least floor deviations above its mean.
|
||||
private static float PassFraction(float floor) => .5f * (1 - erf(floor / sqrt(2f)));
|
||||
|
||||
// Each line's key to the sum of its bins' pass fractions: read from the catalog once per weighting pass, never kept.
|
||||
// Each line's key to the sum of its offered bins' pass fractions: read from the catalog once per weighting pass, never kept. An
|
||||
// off-market bin takes no share, so its line keeps the whole weight.
|
||||
private Dictionary<string, float> BinShares() => ItemManager.ItemData.GetAll<FactionProductData>()
|
||||
.Where(product => product.Bin != null)
|
||||
.Where(product => product.Bin != null && IsOffered(ItemManager.ItemData, product))
|
||||
.GroupBy(product => product.Bin.Line.Key.Value)
|
||||
.ToDictionary(group => group.Key, group => group.Sum(bin => PassFraction(bin.Bin.Floor)));
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public static class RunStart
|
|||
|
||||
public static ZonePack GenerateArena(ItemManager itemManager, ZoneGenerationSettings zoneSettings, PlanetSettings planetSettings, Galaxy galaxy, Scenario scenario)
|
||||
{
|
||||
SeedItems(itemManager, galaxy, 1);
|
||||
SeedItems(itemManager, galaxy, ArenaStep);
|
||||
return galaxy.Entrance.PackedContents = ZoneGenerator.GenerateZone(
|
||||
itemManager, zoneSettings, planetSettings, galaxy, galaxy.Entrance, galaxy.IsPrelude, scenario.Ambient);
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ public static class RunStart
|
|||
public static Staged Stage(ItemManager itemManager, Zone arena, Scenario scenario, string startingHull,
|
||||
TutorialGenerationSettings tutorialGenerationSettings, List<string> failures)
|
||||
{
|
||||
SeedItems(itemManager, arena.Galaxy, 2);
|
||||
SeedItems(itemManager, arena.Galaxy, StageStep);
|
||||
var stage = new ScenarioStage(itemManager, arena, startingHull, tutorialGenerationSettings);
|
||||
scenario.Stage(stage);
|
||||
var reported = failures.Count;
|
||||
|
|
@ -62,5 +62,22 @@ public static class RunStart
|
|||
// 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);
|
||||
itemManager.Random = new Random(ItemSeed(galaxy, step));
|
||||
|
||||
// The steps of a run that draw items: generating its arena, staging its scenario, and continuing a saved run.
|
||||
public const uint ArenaStep = 1;
|
||||
public const uint StageStep = 2;
|
||||
public const uint ContinueStep = 3;
|
||||
|
||||
// The item seed of an ActionGameManager scene opened with no galaxy. No run is loaded, so nothing a run decides
|
||||
// draws from it.
|
||||
public const uint NoRunSeed = 0x5EED0001u;
|
||||
|
||||
// The item stream's seed for one step of a run: the run's seed and the step. Never zero, which no stream accepts.
|
||||
public static uint ItemSeed(Galaxy galaxy, uint step)
|
||||
{
|
||||
var seed = galaxy.Seed * 0x9E3779B1u + step;
|
||||
if (seed == 0) throw new InvalidOperationException($"Item seed is zero for galaxy seed {galaxy.Seed} at step {step}.");
|
||||
return seed;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ public abstract class Scenario
|
|||
{
|
||||
public abstract string Name { get; }
|
||||
|
||||
// The scenario's stable identity: kebab-case, unique across Scenarios, never shown to the player, recorded by
|
||||
// matches and replays. Name is display only.
|
||||
public abstract string Id { get; }
|
||||
|
||||
// The conditions and what to verify in them, shown under the scenario's button.
|
||||
public abstract string Brief { get; }
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using static ItemRotation;
|
|||
public sealed class AiFusedDiscipline : Scenario
|
||||
{
|
||||
public override string Name => "AI Fused Discipline";
|
||||
public override string Id => "ai-fused-discipline";
|
||||
public override string Brief => "A Djinni; a piloted neutral Longinus carrying a Smoke Proximity and a piloted hostile Longinus whose only weapon is " +
|
||||
"Smoke Refused. Verify: the hostile closes on you and never fires its refused weapon; the neutral never fires its " +
|
||||
"fused one.";
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using static ItemRotation;
|
|||
public sealed class Arcs : Scenario
|
||||
{
|
||||
public override string Name => "Arcs";
|
||||
public override string Id => "arcs";
|
||||
public override string Brief => "The Smoke Test Hull with forward mounts and a side mount on each flank; a bare hull off the bow, one off " +
|
||||
"the stern, one abeam to port, one abeam to starboard, and a hostile turret. Verify: the hull keeps its " +
|
||||
"heading while you aim at the stern hull, and the forward guns hold. Designate the bow hull and fire with the " +
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using static ItemRotation;
|
|||
public sealed class Belt : Scenario
|
||||
{
|
||||
public override string Name => "Belt";
|
||||
public override string Id => "belt";
|
||||
public override string Brief => "The biggest asteroid belt the generator made, with the Longinus parked 100 m outside its ring, facing along it, " +
|
||||
"and nothing else in the arena. Verify: next and previous target step through the rocks and the reticle sweeps " +
|
||||
"across the belt without a hitch; Previous with nothing targeted takes the farthest rock; the target indicator " +
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using static ItemRotation;
|
|||
public sealed class Crossing : Scenario
|
||||
{
|
||||
public override string Name => "Crossing";
|
||||
public override string Id => "crossing";
|
||||
public override string Brief => "A Lightning Projector at the starboard gun and a Cell Blaster at the port gun, an unpiloted Djinni coasting across the bow " +
|
||||
"close ahead and an unpiloted one waiting at range. The crosser is launched to port at 50 m/s and only coasts: nothing steers it, " +
|
||||
"its hull drag slows it to about 25 m/s at the bow line, and it does not shoot back. Make the pass coasting: give no throttle until the Djinni has crossed the bow, then fly freely " +
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ public sealed class DemoTerminus : Scenario
|
|||
};
|
||||
|
||||
public override string Name => "Terminus (Demo)";
|
||||
public override string Id => "demo-terminus";
|
||||
public override string Brief => "One gate, one region, one boss. Allied: the Pirates. Opposed: Zhestokost. " +
|
||||
"Neutral: Lucent Media and Aeronautics Unlimited.";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using float2 = CultMath.float2;
|
|||
public sealed class DjinniShakedown : Scenario
|
||||
{
|
||||
public override string Name => "Djinni Shakedown";
|
||||
public override string Id => "djinni-shakedown";
|
||||
public override string Brief => "A fitted Djinni in empty space, facing +z, with a bare hull dead ahead and one abeam. Verify: its thrusters " +
|
||||
"move and turn it; with the hull offline and thrusters firing, its speed does not run away. Its " +
|
||||
"articulated ballistic barrel points at the hull ahead when it bears, and rests at the arc edge while you " +
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using static ItemRotation;
|
|||
public sealed class Duel : Scenario
|
||||
{
|
||||
public override string Name => "Duel";
|
||||
public override string Id => "duel";
|
||||
public override string Brief => "One piloted, hostile Longinus with slow guided missiles and a Light Laser 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;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using static ItemRotation;
|
|||
public sealed class FusedRounds : Scenario
|
||||
{
|
||||
public override string Name => "Fused Rounds";
|
||||
public override string Id => "fused-rounds";
|
||||
public override string Brief => "The Smoke Test Hull with Smoke Proximity, Smoke Delayed and a Cluster Cannon; a hostile bare Longinus at mid range, a bare " +
|
||||
"hull dead ahead, a fragile one to starboard, a close one inside the proximity blast radius, and one astern. " +
|
||||
"Verify with nothing selected: aim past every hull and the round bursts at max range along the aim; aim at the hull " +
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using static ItemRotation;
|
|||
public sealed class LauncherAngles : Scenario
|
||||
{
|
||||
public override string Name => "Launcher Angles";
|
||||
public override string Id => "launcher-angles";
|
||||
public override string Brief => "A Guided Missile Launcher, a Swarm Rocket Pod and two Cell Blasters; a hostile bare Longinus broadside on, another beside it. " +
|
||||
"Verify: hits pulse only the facing edge; a Swarm Rocket Pod round whose target dies fades unexploded.";
|
||||
public override uint Seed => 105;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using static ItemRotation;
|
|||
public sealed class LockOn : Scenario
|
||||
{
|
||||
public override string Name => "Lock On";
|
||||
public override string Id => "lock-on";
|
||||
public override string Brief => "A hostile closes from out of sight, finds you and locks. Verify: no warning at first; once it locks, one " +
|
||||
"indicator appears toward it and tightens as the lock grows; break line of sight or range and it vanishes; " +
|
||||
"your own ping shows no incoming paint on its side, only the lock does.";
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using float2 = CultMath.float2;
|
|||
public sealed class LongHaul : Scenario
|
||||
{
|
||||
public override string Name => "Long Haul";
|
||||
public override string Id => "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;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using float2 = CultMath.float2;
|
|||
public sealed class MainGalaxy : Scenario
|
||||
{
|
||||
public override string Name => "Main Galaxy";
|
||||
public override string Id => "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();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ public sealed class Pursuit : Scenario
|
|||
private const float Astern = 300;
|
||||
|
||||
public override string Name => "Pursuit";
|
||||
public override string Id => "pursuit";
|
||||
public override string Brief => "A hostile Longinus on your tail; a Mine Launcher fitted, three Coolant Vents and two Thruster " +
|
||||
"Overdrives aboard. Verify: lay mines in its path, the mine pulses slowly after 2 s, fast when " +
|
||||
"the hostile is near, explodes 2 s later; vent and coast to lose it; overdrive away.";
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using static ItemRotation;
|
|||
public sealed class RefusedRounds : Scenario
|
||||
{
|
||||
public override string Name => "Refused Rounds";
|
||||
public override string Id => "refused-rounds";
|
||||
public override string Brief => "The Smoke Test Hull with Smoke Refused, Smoke Refused Beam, Smoke Refused Charge and an Autocannon; a bare Longinus " +
|
||||
"dead ahead. Each smoke weapon's blast radius is wider than its range, so it can never be fired. Verify: pull each " +
|
||||
"trigger and a refused shot costs no ammo, energy, heat, sound, visibility or cooldown, the beam never starts and " +
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using static ItemRotation;
|
|||
public sealed class StarvedReactor : Scenario
|
||||
{
|
||||
public override string Name => "Starved Reactor";
|
||||
public override string Id => "starved-reactor";
|
||||
public override string Brief => "Two Light Lasers on a Muon Fusion Reactor, which can fall to a quarter of the Compact Reactor's charge, and a bare hull " +
|
||||
"to shoot. Verify: starvation reads as starvation, brownout degrades, refills stutter.";
|
||||
public override uint Seed => 102;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using float2 = CultMath.float2;
|
|||
public sealed class TutorialGalaxy : Scenario
|
||||
{
|
||||
public override string Name => "Tutorial Galaxy";
|
||||
public override string Id => "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();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using static ItemRotation;
|
|||
public sealed class WeaponFeel : Scenario
|
||||
{
|
||||
public override string Name => "Weapon Feel";
|
||||
public override string Id => "weapon-feel";
|
||||
public override string Brief => "The Smoke Test Hull with a Charged Scatter Blaster, an Autocannon and a Cell Blaster burst weapon; a bare Longinus dead ahead. " +
|
||||
"Verify: Charged Scatter Blaster and Autocannon fire with effects, particles and action-bar icons; the burst weapon's " +
|
||||
"cadence is as before; a fast release-and-press on the Charged Scatter Blaster starts no second charge.";
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ public static class SimulationDice
|
|||
// never rides ItemManager.Random, so minting items cannot change how a pilot fights.
|
||||
public const uint CombatSampleStream = 3;
|
||||
public const uint PatrolStream = 4;
|
||||
// Generation placement of an unpacked ship packed at the origin, ordinal its 1-based index in the pack, on the zone's
|
||||
// seed (CombatSeed): the run seed feeds drops and mines only (ruling run-seed-drops-only), and placement never rides
|
||||
// ItemManager.Random.
|
||||
public const uint ScatterStream = 5;
|
||||
|
||||
// 9.1 (docs/fire-control-cut.md): fmix32, MurmurHash3's 32-bit finalizer -- xor-shift, multiply,
|
||||
// xor-shift, multiply, xor-shift. Diffuses a structured seed (a zone seed XORed with a small,
|
||||
|
|
|
|||
|
|
@ -266,14 +266,18 @@ public class Zone
|
|||
foreach (var wear in pack.ChunkWear)
|
||||
_wear[new ChunkId(wear.Field, wear.Index)] = new ChunkWear { Damage = wear.Damage, BrokenUntil = wear.BrokenUntil };
|
||||
|
||||
var ordinal = 0u;
|
||||
foreach (var entityPack in pack.Entities)
|
||||
{
|
||||
ordinal++;
|
||||
var entity = EntitySerializer.Unpack(_itemManager, this, entityPack);
|
||||
var ship = entity as Ship;
|
||||
Admit(entity, piloted: ship is {IsPlayerShip: false});
|
||||
// Generation placement, not admission: a ship packed at the origin is scattered across the zone.
|
||||
// Generation placement, not admission: a ship packed at the origin is scattered across the zone, on the
|
||||
// zone's own dice, so nothing drawn from the item stream can move it.
|
||||
if (ship is {IsPlayerShip: false} && lengthsq(ship.Position) < 1)
|
||||
ship.Position = _itemManager.Random.NextFloat3(float3(-pack.Radius * .5f), float3(pack.Radius * .5f));
|
||||
ship.Position = SimulationDice.For(CombatSeed, SimulationDice.ScatterStream, ordinal)
|
||||
.NextFloat3(float3(-pack.Radius * .5f), float3(pack.Radius * .5f));
|
||||
}
|
||||
|
||||
// TODO: Associate planets with stored entities for planetary colonies
|
||||
|
|
|
|||
Loading…
Reference in a new issue