Opens of suite-honest: honest station descriptions, Arcs bay, measured crates, drag test at play ThrustScale, HUD readout decision in Shared, gated Console capture #11
15 changed files with 265 additions and 61 deletions
|
|
@ -109,7 +109,9 @@ public abstract class Weapon : ActivatedBehavior
|
|||
public virtual bool HasMagazine => true;
|
||||
// What the HUD's ammunition label shows: "magazine/reserve" for a magazine weapon, the reserve alone for one without a magazine, empty when
|
||||
// the weapon draws no ammunition. The one rule the HUD reads, so the label cannot show a magazine a weapon does not have.
|
||||
public string AmmoReadout => !_data.AmmoType.IsSet() ? "" : HasMagazine ? $"{Ammo}/{Reserve}" : Reserve.ToString();
|
||||
public string AmmoReadout => !ShowsAmmoReadout ? "" : HasMagazine ? $"{Ammo}/{Reserve}" : Reserve.ToString();
|
||||
// Whether the HUD shows the ammunition label at all (and not the infinite-ammunition icon): the weapon names an AmmoType.
|
||||
public bool ShowsAmmoReadout => _data.AmmoType.IsSet();
|
||||
public WeaponData WeaponData => _data;
|
||||
public float RangeFactor(float at) => WeaponData.RangeFactor(MinRange, EffectiveRange, Range, _data.DamageAtMaxRange, at);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ public sealed class Arcs : Scenario
|
|||
public static readonly CultMath.float2 Bow = float2(0, 400), Stern = float2(0, -400), Turret = float2(600, 300),
|
||||
Port = float2(-400, 0), Starboard = float2(400, 0);
|
||||
|
||||
public const int TurretAmmoCrates = 30;
|
||||
// Crates of ammunition each shooter's hold is stocked with: the measured draw over a run (ScenarioAmmunitionRunTests: at most ten minutes of play, and in
|
||||
// practice the ~260 s a shooter holding every gun down lasts before it dies of heat; a pilot who vents heat is unmeasured) plus half again and one. The turret drew 14 Ammo with the player in its range; the player drew 27 Ammo
|
||||
// holding every gun down. The guided launchers fire only on a lock, which that run cannot give them, so they carry the default three crates.
|
||||
public const int TurretAmmoCrates = 22, PlayerAmmoCrates = 42, PlayerMissileCrates = 3;
|
||||
|
||||
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude();
|
||||
|
||||
|
|
@ -43,7 +46,8 @@ public sealed class Arcs : Scenario
|
|||
("not if i see you first", int2(5, 10), None), // Compact Sensor
|
||||
("Panopticon Prime", int2(3, 6), Clockwise), // Fire Control Array
|
||||
("🫳🔫", int2(8, 4), Clockwise), // Autocannon
|
||||
("ChargeBlast SG", int2(0, 4), CounterClockwise)); // Charged Scatter Blaster
|
||||
("ChargeBlast SG", int2(0, 4), CounterClockwise), // Charged Scatter Blaster
|
||||
("Store-All Plus", int2(4, 8), None)); // Cargo Bay: the only 2x2 the hull has free
|
||||
var turret = stage.Fit("Turret",
|
||||
("ClearPath", int2(1, 2), None), // Light Machine Gun
|
||||
("ClearPath", int2(5, 2), None), // Light Machine Gun
|
||||
|
|
@ -55,10 +59,11 @@ public sealed class Arcs : Scenario
|
|||
("Panopticon Prime", int2(3, 6), None), // Fire Control Array
|
||||
("Store-All Plus", int2(3, 1), None)); // Cargo Bay: the only 2x2 the turret hull has free
|
||||
|
||||
stage.Player(fighter, float2(0, 0), facing: float2(0, 1));
|
||||
var player = stage.Player(fighter, float2(0, 0), facing: float2(0, 1));
|
||||
stage.Cargo(player, Enumerable.Repeat("Ammo", PlayerAmmoCrates).Concat(Enumerable.Repeat("Missiles", PlayerMissileCrates)).ToArray());
|
||||
stage.Place(stage.Bare("Longinus"), Bow);
|
||||
stage.Place(stage.Bare("Longinus"), Stern);
|
||||
// The turret's guns reload from its hold, a crate of Ammo per 100-round magazine: both guns firing without pause spend about 3.5 crates a minute, so 30 outlast any session.
|
||||
// The turret's guns reload from its hold, a crate of Ammo per magazine.
|
||||
stage.Cargo(stage.Place(turret, Turret, stance: ScenarioStance.Hostile), Enumerable.Repeat("Ammo", TurretAmmoCrates).ToArray());
|
||||
stage.Place(stage.Bare("Longinus"), Port);
|
||||
stage.Place(stage.Bare("Longinus"), Starboard);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ public sealed class Pursuit : Scenario
|
|||
// cloud breaking a sensor's lock, and well outside the 40 at which AStrongCloseSensorBurnsThrough shows a close
|
||||
// sensor burning through.
|
||||
private const float Astern = 300;
|
||||
public const int MineCrates = 10;
|
||||
// Crates each shooter's hold is stocked with: the measured draw over a run (ScenarioAmmunitionRunTests: at most ten minutes of play, and in
|
||||
// practice the ~250-270 s a shooter holding every gun down lasts before it dies of heat; a pilot who vents heat is unmeasured) plus half again and one where the hold has room. The player laid four crates of Mines and drew
|
||||
// 33 Ammo; its hold takes 54 crates after five consumables and Mines, so its Ammo stops at 39. The pursuer drew 31 Ammo and one Missiles. A launcher
|
||||
// that drew under a reload carries the default three crates.
|
||||
public const int MineCrates = 7, PlayerAmmoCrates = 39, PlayerMissileCrates = 3, PursuerAmmoCrates = 47, PursuerMissileCrates = 3;
|
||||
|
||||
public override string Name => "Pursuit";
|
||||
public override string Id => "pursuit";
|
||||
|
|
@ -57,8 +61,9 @@ public sealed class Pursuit : Scenario
|
|||
("Store-All Plus", int2(2, 8), None)); // Cargo Bay
|
||||
|
||||
var player = stage.Player(layer, float2(0, 0), facing: float2(0, 1));
|
||||
// The Mine Launcher reloads a magazine of six from a crate of Mines: ten crates are sixty-six mines, minutes of laying at its cooldown, more than a chase lasts.
|
||||
stage.Cargo(player, new[] { "Morning Fog", "Morning Fog", "Morning Fog", "send it", "send it" }.Concat(Enumerable.Repeat("Mines", MineCrates)).ToArray());
|
||||
stage.Place(pursuer, float2(0, -Astern), facing: float2(0, 1), stance: ScenarioStance.Hostile, piloted: true);
|
||||
stage.Cargo(player, new[] { "Morning Fog", "Morning Fog", "Morning Fog", "send it", "send it" }
|
||||
.Concat(Enumerable.Repeat("Mines", MineCrates)).Concat(Enumerable.Repeat("Ammo", PlayerAmmoCrates)).Concat(Enumerable.Repeat("Missiles", PlayerMissileCrates)).ToArray());
|
||||
var hostile = stage.Place(pursuer, float2(0, -Astern), facing: float2(0, 1), stance: ScenarioStance.Hostile, piloted: true);
|
||||
stage.Cargo(hostile, Enumerable.Repeat("Ammo", PursuerAmmoCrates).Concat(Enumerable.Repeat("Missiles", PursuerMissileCrates)).ToArray());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,8 +132,8 @@ public class SchematicDisplay : MonoBehaviour
|
|||
//x.ListElement.Label.text = x.Item.EquippableItem.Name;
|
||||
if (!_enemy)
|
||||
{
|
||||
x.ListElement.InfiniteAmmoIcon.gameObject.SetActive(!x.Weapon.WeaponData.AmmoType.IsSet());
|
||||
x.ListElement.AmmoLabel.gameObject.SetActive(x.Weapon.WeaponData.AmmoType.IsSet());
|
||||
x.ListElement.InfiniteAmmoIcon.gameObject.SetActive(!x.Weapon.ShowsAmmoReadout);
|
||||
x.ListElement.AmmoLabel.gameObject.SetActive(x.Weapon.ShowsAmmoReadout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -233,7 +233,7 @@ public class SchematicDisplay : MonoBehaviour
|
|||
if (x.Cooldown != null)
|
||||
x.ListElement.CooldownFill.anchorMax = new Vector2(x.Cooldown.Progress, 1);
|
||||
x.ListElement.DurabilityLabel.text = $"{(int)(x.Row.Item.EquippableItem.Durability / itemData.Durability * 100)}%";
|
||||
if (x.Weapon.WeaponData.AmmoType.IsSet())
|
||||
if (x.Weapon.ShowsAmmoReadout)
|
||||
x.ListElement.AmmoLabel.text = x.Weapon.AmmoReadout;
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
GameData/Aetheria.cc
(Stored with Git LFS)
BIN
GameData/Aetheria.cc
(Stored with Git LFS)
Binary file not shown.
|
|
@ -193,5 +193,10 @@ public sealed partial class RunStartTests
|
|||
Assert.Equal(reserve.ToString(), Label("Fed Beam"));
|
||||
Assert.Equal($"{ship.Weapons.Single(w => _items.GetData(w.Item.EquippableItem).Name == "Fed Gun").Ammo}/{reserve}", Label("Fed Gun"));
|
||||
Assert.Equal("", Label("Plain Gun"));
|
||||
// The HUD shows the label (not the infinite-ammunition icon) exactly for the weapons that read something.
|
||||
bool Shown(string name) => ship.Weapons.Single(weapon => _items.GetData(weapon.Item.EquippableItem).Name == name).ShowsAmmoReadout;
|
||||
Assert.True(Shown("Fed Gun"));
|
||||
Assert.True(Shown("Fed Beam"));
|
||||
Assert.False(Shown("Plain Gun"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
tests/Aetheria.Shared.Tests/CoastingTests.cs
Normal file
48
tests/Aetheria.Shared.Tests/CoastingTests.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
using System;
|
||||
using CultMath;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float3 = CultMath.float3;
|
||||
using Random = CultMath.Random;
|
||||
|
||||
// The consumer's side of aetheria-gravity:ruling:no-drag-below-top-speed: a ship launched below its hull's top speed and
|
||||
// left alone keeps that speed, at the global thrust tuning the game ships with (GameplaySettings.ThrustScale 0.1) as well as
|
||||
// at the fixtures' 1. Reads only the ship's velocity after the zone steps: not Drag, DragScale or any field of the fix.
|
||||
// Kills: drag reintroduced below top speed, or any deceleration whose size depends on ThrustScale.
|
||||
public sealed class CoastingTests
|
||||
{
|
||||
private const float PlayThrustScale = .1f; // GameplaySettings.ThrustScale's default, the value play runs at
|
||||
|
||||
private static float CoastedSpeed(float thrustScale, float launchSpeed, float seconds)
|
||||
{
|
||||
using var cache = RestoredHullsTests.OpenCatalog();
|
||||
var hull = cache.GetByName<HullData>("Longinus");
|
||||
var settings = RestoredHullsTests.Settings();
|
||||
settings.ThrustScale = thrustScale;
|
||||
var items = new ItemManager(1u, cache, new ProvenanceLedger(), settings, _ => { });
|
||||
items.Random = new Random(1u);
|
||||
var random = new Random(1);
|
||||
var pack = new LoadoutGenerator(ref random, items, null, null, null, .5f).GenerateShipLoadout(candidate => candidate == hull);
|
||||
Assert.NotNull(pack);
|
||||
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test", Owner = null }, null);
|
||||
var ship = (Ship) EntitySerializer.Unpack(items, zone, pack);
|
||||
zone.Admit(ship, piloted: false);
|
||||
ship.Position = float3.zero;
|
||||
zone.Update(0f);
|
||||
|
||||
ship.MovementDirection = float2(0, 0);
|
||||
ship.Velocity = float2(0, launchSpeed);
|
||||
for (var i = 0; i < (int) (seconds * 60); i++) zone.Update(1f / 60f);
|
||||
return length(ship.Velocity);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PlayThrustScale)]
|
||||
[InlineData(1f)]
|
||||
public void AShipLaunchedBelowTopSpeedKeepsItsSpeed(float thrustScale)
|
||||
{
|
||||
const float launch = 50f; // Longinus's top speed is 100
|
||||
var speed = CoastedSpeed(thrustScale, launch, 10f);
|
||||
Assert.True(speed >= launch - .01f, $"ThrustScale {thrustScale}: launched at {launch}, coasting 10 s left it at {speed}");
|
||||
}
|
||||
}
|
||||
|
|
@ -13,15 +13,28 @@ internal static class ConsoleProbe
|
|||
private static readonly object Gate = new object();
|
||||
|
||||
public static string WrittenBy(Action act)
|
||||
{
|
||||
Capture<int>(() => { act(); return 0; }, out var output, out _);
|
||||
return output;
|
||||
}
|
||||
|
||||
// The action's result with what it wrote to Console.Out and Console.Error on this thread.
|
||||
public static T Capture<T>(Func<T> act, out string output, out string error)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
var previous = Console.Out;
|
||||
var probe = new ThreadWriter(Environment.CurrentManagedThreadId);
|
||||
Console.SetOut(probe);
|
||||
try { act(); }
|
||||
finally { Console.SetOut(previous); }
|
||||
return probe.Mine.ToString();
|
||||
var (previousOut, previousError) = (Console.Out, Console.Error);
|
||||
var (probeOut, probeError) = (new ThreadWriter(Environment.CurrentManagedThreadId), new ThreadWriter(Environment.CurrentManagedThreadId));
|
||||
Console.SetOut(probeOut);
|
||||
Console.SetError(probeError);
|
||||
try { return act(); }
|
||||
finally
|
||||
{
|
||||
Console.SetOut(previousOut);
|
||||
Console.SetError(previousError);
|
||||
output = probeOut.Mine.ToString();
|
||||
error = probeError.Mine.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
102
tests/Aetheria.Shared.Tests/ScenarioAmmunitionRunTests.cs
Normal file
102
tests/Aetheria.Shared.Tests/ScenarioAmmunitionRunTests.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
using Xunit;
|
||||
|
||||
// How much ammunition a staged scenario really spends over a run (ruling ballistics-real-ammo): the crates a scenario authors are pinned to
|
||||
// this measurement, not to an estimate. A run is RunSeconds of continuous play: the player holds every ammunition-drawing weapon down with its
|
||||
// hostile-or-flank hull standing in weapon range, an AI shooter fires on its own, and every hull keeps its armour so no shooter or target leaves
|
||||
// the run early. The draw is counted in crates (one reload per crate) and each drawn crate is put back, so the count is not capped by the stock.
|
||||
public sealed partial class RunStartTests
|
||||
{
|
||||
// Ceiling of the window: a shooter holding every gun down dies of heat at ~250-270 s, so the draws are bounded by heat death, not by this.
|
||||
public const float RunSeconds = 600f;
|
||||
|
||||
// Crates each armed entity of the scenario draws over the run, keyed by (index in the stage's entities, -1 for the player; ammunition name),
|
||||
// and the crates each held when the run began.
|
||||
private (Dictionary<(int, string), int> drawn, Dictionary<(int, string), int> stocked) RunAmmunition(Scenario scenario, bool playerFires, bool standNearHostile)
|
||||
{
|
||||
var (_, arena, staged, failures) = Launch(scenario, Inputs(() => GalaxySeed));
|
||||
Assert.Empty(failures);
|
||||
arena.Update(0);
|
||||
var shooters = staged.Entities.Select((entity, index) => (entity, index)).Append((staged.Player, -1))
|
||||
.Where(s => s.Item1.Weapons.Any(weapon => weapon.WeaponData.AmmoType.IsSet())).ToList();
|
||||
var drawn = new Dictionary<(int, string), int>();
|
||||
var stocked = new Dictionary<(int, string), int>();
|
||||
var held = new Dictionary<(int, string), int>();
|
||||
string NameOf(CultRecordKey key) => _items.ItemData.Get<SimpleCommodityData>(key).Name;
|
||||
foreach (var (entity, index) in shooters)
|
||||
foreach (var key in entity.Weapons.Where(weapon => weapon.WeaponData.AmmoType.IsSet()).Select(weapon => weapon.WeaponData.AmmoType.Key).Distinct())
|
||||
{
|
||||
stocked[(index, NameOf(key))] = entity.CountItemsInCargo(key);
|
||||
// A shooter with no crate cannot reload at all, so a run could not show what it would draw: it is given one to start.
|
||||
if (stocked[(index, NameOf(key))] == 0)
|
||||
entity.CargoBays.First().TryStore(new SimpleCommodity { Data = new CultRecordRef<ItemData>(key), Quantity = 1 });
|
||||
drawn[(index, NameOf(key))] = 0;
|
||||
held[(index, NameOf(key))] = entity.CountItemsInCargo(key);
|
||||
}
|
||||
var everyone = staged.Entities.Append(staged.Player).ToList();
|
||||
var player = staged.Player;
|
||||
var sound = everyone.SelectMany(entity => entity.Equipment).ToDictionary(item => item, item => item.EquippableItem.Durability);
|
||||
var hostile = standNearHostile ? staged.Entities.First(entity => player.IsHostileTo(entity)) : null;
|
||||
for (var tick = 0; tick < System.MathF.Round(RunSeconds / SimClock.GameStep); tick++)
|
||||
{
|
||||
foreach (var entity in everyone)
|
||||
for (var x = 0; x < entity.Armor.GetLength(0); x++)
|
||||
for (var y = 0; y < entity.Armor.GetLength(1); y++)
|
||||
entity.Armor[x, y] = entity.MaxArmor[x, y];
|
||||
foreach (var (item, durability) in sound) item.EquippableItem.Durability = durability;
|
||||
if (hostile != null) player.Position = hostile.Position + CultMath.math.float3(-200, 0, 0);
|
||||
if (playerFires)
|
||||
foreach (var weapon in player.Weapons.Where(weapon => weapon.WeaponData.AmmoType.IsSet())) weapon.Activate();
|
||||
arena.Update(SimClock.GameStep);
|
||||
foreach (var (entity, index) in shooters)
|
||||
foreach (var key in entity.Weapons.Where(weapon => weapon.WeaponData.AmmoType.IsSet()).Select(weapon => weapon.WeaponData.AmmoType.Key).Distinct())
|
||||
{
|
||||
var slot = (index, NameOf(key));
|
||||
var spent = held[slot] - entity.CountItemsInCargo(key);
|
||||
if (spent <= 0) continue;
|
||||
drawn[slot] += spent;
|
||||
for (var crate = 0; crate < spent; crate++)
|
||||
entity.CargoBays.First().TryStore(new SimpleCommodity { Data = new CultRecordRef<ItemData>(key), Quantity = 1 });
|
||||
}
|
||||
}
|
||||
return (drawn, stocked);
|
||||
}
|
||||
|
||||
// The most crates each shooter draws in any of the modes, and what it was stocked with.
|
||||
private List<(int shooter, string ammo, int drawn, int stocked)> WorstRun(Scenario scenario, params (bool playerFires, bool standNearHostile)[] modes)
|
||||
{
|
||||
var worst = new Dictionary<(int, string), (int drawn, int stocked)>();
|
||||
foreach (var (fires, near) in modes)
|
||||
{
|
||||
var (drawn, stocked) = RunAmmunition(scenario, fires, near);
|
||||
foreach (var slot in drawn.Keys)
|
||||
worst[slot] = (System.Math.Max(drawn[slot], worst.TryGetValue(slot, out var seen) ? seen.drawn : 0), stocked[slot]);
|
||||
}
|
||||
return worst.Select(kv => (kv.Key.Item1, kv.Key.Item2, kv.Value.drawn, kv.Value.stocked)).ToList();
|
||||
}
|
||||
|
||||
// Arcs, run to its end twice: the player holding every gun down, and the player standing in the hostile turret's range while the turret
|
||||
// shoots. Kills: a crate count nobody measured (the turret's 30 and the player's none were estimates), a scenario shooter stocked below
|
||||
// what a run draws so its guns go silent, and a harness that measures nothing.
|
||||
[Fact]
|
||||
public void ArcsStocksEveryShooterForARun()
|
||||
{
|
||||
var run = WorstRun(new Arcs(), (true, false), (false, true));
|
||||
Assert.Contains(run, r => r.shooter == 2 && r.ammo == "Ammo" && r.drawn > 0);
|
||||
Assert.Contains(run, r => r.shooter == -1 && r.ammo == "Ammo" && r.drawn > 0);
|
||||
Assert.All(run, r => Assert.True(r.stocked >= r.drawn, $"Arcs shooter {r.shooter} drew {r.drawn} {r.ammo} over a run and was stocked {r.stocked}"));
|
||||
}
|
||||
|
||||
// Pursuit, the player holding every gun down and the pursuer shooting back, then the player idle. Kills: as above, for the Mine Launcher's
|
||||
// crates and the pursuer's.
|
||||
[Fact]
|
||||
public void PursuitStocksEveryShooterForARun()
|
||||
{
|
||||
var run = WorstRun(new Pursuit(), (true, false), (false, false));
|
||||
Assert.Contains(run, r => r.shooter == -1 && r.ammo == "Mines" && r.drawn > 0);
|
||||
Assert.Contains(run, r => r.shooter == 0 && r.ammo == "Ammo" && r.drawn > 0);
|
||||
Assert.All(run, r => Assert.True(r.stocked >= r.drawn, $"Pursuit shooter {r.shooter} drew {r.drawn} {r.ammo} over a run and was stocked {r.stocked}"));
|
||||
}
|
||||
}
|
||||
|
|
@ -546,12 +546,8 @@ public sealed class ShipModCatalogTests : IDisposable
|
|||
|
||||
private static (int Code, string Output, string Error) Run(params string[] args)
|
||||
{
|
||||
var (output, error) = (new StringWriter(), new StringWriter());
|
||||
var (previousOut, previousError) = (Console.Out, Console.Error);
|
||||
Console.SetOut(output);
|
||||
Console.SetError(error);
|
||||
try { return (ShipAuthoringCommands.Run(args), output.ToString(), error.ToString()); }
|
||||
finally { Console.SetOut(previousOut); Console.SetError(previousError); }
|
||||
var code = ConsoleProbe.Capture(() => ShipAuthoringCommands.Run(args), out var output, out var error);
|
||||
return (code, output, error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -247,12 +247,8 @@ public sealed class ShipProductTests : IDisposable
|
|||
|
||||
private static (int Code, string Output, string Error) Run(params string[] args)
|
||||
{
|
||||
var (output, error) = (new StringWriter(), new StringWriter());
|
||||
var (previousOut, previousError) = (Console.Out, Console.Error);
|
||||
Console.SetOut(output);
|
||||
Console.SetError(error);
|
||||
try { return (ShipAuthoringCommands.Run(args), output.ToString(), error.ToString()); }
|
||||
finally { Console.SetOut(previousOut); Console.SetError(previousError); }
|
||||
var code = ConsoleProbe.Capture(() => ShipAuthoringCommands.Run(args), out var output, out var error);
|
||||
return (code, output, error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using GameCult.Caching;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -34,4 +35,32 @@ public sealed class StationKindDescriptionTests
|
|||
Assert.Contains("Diamond", cache.Get<StationKindData>(StationKindData.KeyOf("mining")).Description);
|
||||
Assert.Contains("Ammonia", cache.Get<StationKindData>(StationKindData.KeyOf("volatiles")).Description);
|
||||
}
|
||||
|
||||
// Kills: a station-kind description naming a commodity no recipe yields or consumes (Uranium, Hydrogen, Bacteria, Fungi and
|
||||
// Animal Products were named by the shipped descriptions with no recipe behind them).
|
||||
[Fact]
|
||||
public void EveryCommodityAStationKindNamesIsMadeOrTakenByARecipe()
|
||||
{
|
||||
using var db = AetherDb.Open();
|
||||
var c = db.Cache;
|
||||
var used = c.GetAll<RecipeData>().SelectMany(r => r.Inputs.Select(i => i.Commodity.Key.Value).Append(r.Output.Key.Value)).ToHashSet();
|
||||
var commodities = c.GetAll<SimpleCommodityData>().ToArray();
|
||||
Assert.True(commodities.Length >= 20 && used.Count >= 20, $"fixture: the catalog holds its commodities and recipes ({commodities.Length}, {used.Count})");
|
||||
var kinds = c.GetAll<StationKindData>().ToArray();
|
||||
Assert.True(kinds.Length >= 17, $"fixture: the catalog holds its station kinds ({kinds.Length})");
|
||||
var orphans = from kind in kinds
|
||||
from good in commodities
|
||||
where Regex.IsMatch(kind.Description ?? "", @"\b" + Regex.Escape(good.Name) + @"\b") && !used.Contains(c.RefOf(good).Key.Value)
|
||||
select $"{kind.Name} names {good.Name}";
|
||||
Assert.True(!orphans.Any(), string.Join("\n", orphans));
|
||||
}
|
||||
|
||||
// Kills: the fresh-catalog text and the shipped catalog saying different things about the same kind.
|
||||
[Fact]
|
||||
public void TheShippedDescriptionsAreTheAuthoredTable()
|
||||
{
|
||||
using var db = AetherDb.Open();
|
||||
foreach (var id in new[] { "mining", "volatiles", "agriculture", "habitat-support", "power" })
|
||||
Assert.Equal(StationKindsCatalog.DescriptionOf(id), db.Cache.Get<StationKindData>(StationKindData.KeyOf(id)).Description);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
using System;
|
||||
using GameCult.Caching;
|
||||
|
||||
// Economy content, batch 1c: the mining and volatiles station-kind descriptions name every good their kind now extracts.
|
||||
// MunitionsFeed gave mining a Diamond recipe and volatiles an Ammonia recipe; the descriptions the station-round bench
|
||||
// wrote predate them. The same text is StationKindsCatalog.Table's, so a catalog authored fresh says the same thing.
|
||||
// Sets the description whatever it was, so a rerun converges.
|
||||
public sealed class ExtractionKindDescriptions : EconomyBatch
|
||||
{
|
||||
public override void Author(EconomyAuthor author)
|
||||
{
|
||||
foreach (var (id, description) in new[]
|
||||
{
|
||||
("mining", StationKindsCatalog.DescriptionOf("mining")),
|
||||
("volatiles", StationKindsCatalog.DescriptionOf("volatiles")),
|
||||
})
|
||||
{
|
||||
var key = StationKindData.KeyOf(id);
|
||||
var kind = author.Catalog.Get<StationKindData>(key) ?? throw new InvalidOperationException($"The catalog holds no station kind '{id}'.");
|
||||
kind.Description = description;
|
||||
author.Put(kind, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
tools/AetherDb/Economy/StationKindDescriptions.cs
Normal file
27
tools/AetherDb/Economy/StationKindDescriptions.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using GameCult.Caching;
|
||||
|
||||
// Economy content: station-kind descriptions that name only goods the recipe graph makes or takes. The station-round bench wrote
|
||||
// descriptions naming Uranium, Hydrogen, Bacteria, Fungi and Animal Products, none of which any recipe yields or consumes; this
|
||||
// restates the five kinds that named them (mining, volatiles, agriculture, habitat-support, power) as StationKindsCatalog.Table
|
||||
// has them, so a catalog authored fresh says the same thing. Sets the description whatever it was, so a rerun converges.
|
||||
public sealed class StationKindDescriptions : EconomyBatch
|
||||
{
|
||||
public override void Author(EconomyAuthor author)
|
||||
{
|
||||
foreach (var (id, description) in new[]
|
||||
{
|
||||
("mining", StationKindsCatalog.DescriptionOf("mining")),
|
||||
("volatiles", StationKindsCatalog.DescriptionOf("volatiles")),
|
||||
("agriculture", StationKindsCatalog.DescriptionOf("agriculture")),
|
||||
("habitat-support", StationKindsCatalog.DescriptionOf("habitat-support")),
|
||||
("power", StationKindsCatalog.DescriptionOf("power")),
|
||||
})
|
||||
{
|
||||
var key = StationKindData.KeyOf(id);
|
||||
var kind = author.Catalog.Get<StationKindData>(key) ?? throw new InvalidOperationException($"The catalog holds no station kind '{id}'.");
|
||||
kind.Description = description;
|
||||
author.Put(kind, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,19 +14,19 @@ public static class StationKindsCatalog
|
|||
|
||||
private static readonly (string id, string name, string description)[] Table =
|
||||
{
|
||||
("mining", "Mining Station", "Extracts ore (Rock, New Metals, Gold, Uranium, Potassium) and Diamond from bodies and belts; consumes tools and food."),
|
||||
("volatiles", "Volatiles Station", "Skims Hydrogen, Methane, Ammonia and Water from gas giants and ice; consumes tools."),
|
||||
("agriculture", "Agriculture Station", "Grows Organics (Algae, Fungi, Bacteria, Animal Products) from Water and Potassium; feeds residential."),
|
||||
("mining", "Mining Station", "Extracts ore (Rock, New Metals, Gold, Potassium) and Diamond from bodies and belts; consumes tools and food."),
|
||||
("volatiles", "Volatiles Station", "Skims Methane, Ammonia and Water from gas giants and ice; consumes tools."),
|
||||
("agriculture", "Agriculture Station", "Grows Algae from Water and Potassium; feeds residential."),
|
||||
("processing", "Processing Station", "Refines ore and volatiles into Metals, Alloys and Compounds."),
|
||||
("fabrication", "Fabrication Station", "Turns Alloys and Compounds into Manufacturing compounds, Assemblies, gear, Ammo and consumer goods."),
|
||||
("shipyard", "Shipyard", "Assembles hulls from Assemblies and Alloys."),
|
||||
("depot", "Depot", "Stores and dispatches stock; loads haulers and tenders."),
|
||||
("trade-hub", "Trade Hub", "Exchanges every bloc's goods between haulers and takes a cut; produces nothing."),
|
||||
("residential", "Residential Station", "A bloom: consumes food, water, Wearables, Consumables and Luxuries; supplies crews."),
|
||||
("habitat-support", "Habitat Support Station", "Air, water recycling, life support and medicine for nearby habitats; consumes Bacteria, Water, Compounds."),
|
||||
("habitat-support", "Habitat Support Station", "Air, water recycling, life support and medicine for nearby habitats; consumes Water and Compounds."),
|
||||
("urban", "Urban Station", "Dense city and services: market, media, repair; consumes luxuries and food."),
|
||||
("administrative", "Administrative Station", "Issues quotas, contracts and security; consumes luxuries."),
|
||||
("power", "Power Station", "Makes reactor fuel, reservoirs and capacitors from Uranium and Hydrogen."),
|
||||
("power", "Power Station", "Generates and distributes power for nearby stations."),
|
||||
("research", "Research Station", "Certification, compute, prototypes; consumes Assemblies."),
|
||||
("salvage", "Salvage Station", "Breaks wrecks into Metals and second-hand gear."),
|
||||
("black-market", "Black Market", "Buys anything, fences stolen cargo."),
|
||||
|
|
|
|||
Loading…
Reference in a new issue