Staged fits: a scenario states the quality of a fit its checks rest on
ScenarioStage.Fit(hull, quality, slots) mints every lot of the fit at the stated quality through Loadouts.Materialize/Resolve, so Crossing's fighter and EvasionTermTests' hand-fitted Longinus and Djinni do not ride the item stream. No seed changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
30597b0e45
commit
7b2a6d064d
5 changed files with 65 additions and 16 deletions
|
|
@ -108,18 +108,19 @@ public static class Loadouts
|
|||
// picks the entity: a ship hull builds a Ship, a turret hull an OrbitalEntity with no orbit, which stays where it is
|
||||
// put; a station hull is refused. The entity always gets exactly WeaponGroupCount groups; a loadout with fewer is
|
||||
// padded with empty groups. A failed materialization leaves any minted lots in the live ledger; they are not roots,
|
||||
// so the next commit drops them.
|
||||
// so the next commit drops them. A stated quality mints every lot of the fit at it, drawing nothing (a scenario states
|
||||
// one for a fit its checks rest on).
|
||||
public static Entity Materialize(ItemManager itemManager, Zone liveZone, Loadout loadout,
|
||||
Func<CraftedItemData, FactionProductData> choose, List<string> failures)
|
||||
Func<CraftedItemData, FactionProductData> choose, List<string> failures, float? quality = null)
|
||||
{
|
||||
var cache = itemManager.ItemData;
|
||||
var reported = failures.Count;
|
||||
|
||||
var hullBuild = Resolve(itemManager, loadout.Hull, loadout.HullProduct, choose, "hull", failures);
|
||||
var hullBuild = Resolve(itemManager, loadout.Hull, loadout.HullProduct, choose, "hull", failures, quality);
|
||||
var hullData = cache.Get(loadout.Hull);
|
||||
if (hullData is { HullType: HullType.Station })
|
||||
failures.Add($"hull: {hullData.Name} is a station hull; a preset builds a ship or a turret");
|
||||
var slotBuilds = loadout.Slots.Select(slot => Resolve(itemManager, slot.Design, slot.Product, choose, Cell(slot), failures)).ToArray();
|
||||
var slotBuilds = loadout.Slots.Select(slot => Resolve(itemManager, slot.Design, slot.Product, choose, Cell(slot), failures, quality)).ToArray();
|
||||
var groups = loadout.WeaponGroups ?? new int[0][];
|
||||
var groupCount = itemManager.GameplaySettings.WeaponGroupCount;
|
||||
if (groups.Length > groupCount) failures.Add($"weapon groups: {groups.Length} groups, at most {groupCount}");
|
||||
|
|
@ -168,11 +169,11 @@ public static class Loadouts
|
|||
// How one design is built, or null with the failure listed; nothing is minted until the returned builder runs. A
|
||||
// pinned product is built as named, whatever choose says; it must be a product of the design. A design with no
|
||||
// product at all is outside the economy (docs/scenarios-cut.md, Q3: generation, stock and sale all go through
|
||||
// products) and is built unbranded, with no maker, at UnbrandedQuality. Otherwise choose names the product, and
|
||||
// products) and is built unbranded, with no maker, at UnbrandedQuality (a stated quality replaces it). Otherwise choose names the product, and
|
||||
// null (nothing available) or a product of another design is a failure.
|
||||
public static Func<CraftedItemInstance> Resolve<T>(ItemManager itemManager, CultRecordRef<T> design,
|
||||
CultRecordRef<FactionProductData> product, Func<CraftedItemData, FactionProductData> choose, string where,
|
||||
List<string> failures) where T : CraftedItemData
|
||||
List<string> failures, float? quality = null) where T : CraftedItemData
|
||||
{
|
||||
var cache = itemManager.ItemData;
|
||||
var data = cache.Get(design);
|
||||
|
|
@ -195,7 +196,7 @@ public static class Loadouts
|
|||
else
|
||||
{
|
||||
if (!cache.GetAll<FactionProductData>().Any(p => p.Design.Key.Equals(design.Key)))
|
||||
return () => itemManager.CreateInstance(itemManager.CreateLot(data, default, UnbrandedQuality));
|
||||
return () => itemManager.CreateInstance(itemManager.CreateLot(data, default, quality ?? UnbrandedQuality));
|
||||
made = choose(data);
|
||||
if (made == null)
|
||||
{
|
||||
|
|
@ -209,7 +210,7 @@ public static class Loadouts
|
|||
failures.Add($"{where}: product {made.Name} is not a product of {data.Name}");
|
||||
return null;
|
||||
}
|
||||
return () => itemManager.CreateInstance(made);
|
||||
return () => itemManager.CreateInstance(quality is { } q ? itemManager.CreateLot(made, q) : itemManager.CreateLot(made));
|
||||
}
|
||||
|
||||
private static string Cell(LoadoutSlot slot) => $"slot {slot.Position.x},{slot.Position.y}";
|
||||
|
|
|
|||
|
|
@ -52,7 +52,15 @@ public sealed class ScenarioStage
|
|||
|
||||
// A hull fitted slot by slot: each slot a design or a product, the hull cell it goes in, and its rotation. A name is
|
||||
// a design (open: the stage chooses its product) or a product (pinned: that brand).
|
||||
public ScenarioFit Fit(string hull, params (string design, int2 cell, ItemRotation rotation)[] slots)
|
||||
public ScenarioFit Fit(string hull, params (string design, int2 cell, ItemRotation rotation)[] slots) =>
|
||||
FitAt(hull, null, slots);
|
||||
|
||||
// The same fit with every lot it mints (hull, slots, unbranded) at the stated quality, drawing nothing. A fit a scenario's
|
||||
// checks rest on states its quality, so the check does not ride the item stream.
|
||||
public ScenarioFit Fit(string hull, float quality, params (string design, int2 cell, ItemRotation rotation)[] slots) =>
|
||||
FitAt(hull, quality, slots);
|
||||
|
||||
private ScenarioFit FitAt(string hull, float? quality, (string design, int2 cell, ItemRotation rotation)[] slots)
|
||||
{
|
||||
var where = $"fit {hull}";
|
||||
var (hullData, hullProduct) = NamedOrProduct<HullData>(hull, where);
|
||||
|
|
@ -69,7 +77,7 @@ public sealed class ScenarioStage
|
|||
Product = product == null ? default : _items.ItemData.RefOf(product)
|
||||
});
|
||||
}
|
||||
return hullData != null && loadout.Slots.Count == slots.Length ? new ScenarioFit(loadout) : null;
|
||||
return hullData != null && loadout.Slots.Count == slots.Length ? new ScenarioFit(loadout, quality) : null;
|
||||
}
|
||||
|
||||
// A hull with nothing fitted.
|
||||
|
|
@ -154,7 +162,7 @@ public sealed class ScenarioStage
|
|||
else
|
||||
{
|
||||
var failures = new List<string>();
|
||||
entity = Loadouts.Materialize(_items, Arena, fit.Loadout, _choose, failures);
|
||||
entity = Loadouts.Materialize(_items, Arena, fit.Loadout, _choose, failures, fit.Quality);
|
||||
_failures.AddRange(failures.Select(failure => $"{where} ({fit.Loadout.Name}): {failure}"));
|
||||
if (entity == null) return null;
|
||||
}
|
||||
|
|
@ -190,7 +198,13 @@ public sealed class ScenarioFit
|
|||
{
|
||||
internal readonly Loadout Loadout;
|
||||
internal readonly EntityPack Pack;
|
||||
internal readonly float? Quality;
|
||||
|
||||
internal ScenarioFit(Loadout loadout, float? quality = null)
|
||||
{
|
||||
Loadout = loadout;
|
||||
Quality = quality;
|
||||
}
|
||||
|
||||
internal ScenarioFit(Loadout loadout) => Loadout = loadout;
|
||||
internal ScenarioFit(EntityPack pack) => Pack = pack;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ public sealed class Crossing : Scenario
|
|||
// The waiting Djinni sits inside the Lightning Projector's arc at 80% of its Range.
|
||||
public static readonly CultMath.float2 Origin = float2(724, 124);
|
||||
public static readonly CultMath.float2 CrosserAt = Origin + float2(-128, 45), CrosserFacing = float2(1, 0), CrosserVelocity = float2(50, 0);
|
||||
// The fighter is staged at this quality, so its gear and the range below do not ride the item stream.
|
||||
public const float StagedQuality = .6f;
|
||||
public const float ProjectorRange = 1216f; // the Lightning Projector's Range in metres, 1216.4 as the fit stands settled at rest
|
||||
public static readonly CultMath.float2 Waiting = Origin + float2(0, ProjectorRange * .8f);
|
||||
|
||||
|
|
@ -29,7 +31,7 @@ public sealed class Crossing : Scenario
|
|||
|
||||
public override void Stage(ScenarioStage stage)
|
||||
{
|
||||
var fighter = stage.Fit("Longinus",
|
||||
var fighter = stage.Fit("Longinus", StagedQuality,
|
||||
("Cockpit 2x2", int2(2, 6), None),
|
||||
("Large Drive", int2(1, 0), Reversed),
|
||||
("Large Drive", int2(3, 0), Reversed),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ public sealed partial class RunStartTests
|
|||
{
|
||||
private const float EvDt = 1f / 60f;
|
||||
|
||||
// The quality the hand-fitted ships are staged at, so their measured constants do not ride the item stream.
|
||||
private const float EvStagedQuality = .6f;
|
||||
|
||||
// The Longinus's drive train, fitted by name: its cockpit and power and the generated Longinus's thrusters (two Large Drive
|
||||
// forward, two Rugged Drive on the flanks).
|
||||
private static readonly (string design, int2 cell, ItemRotation rotation)[] EvLonginusDrive =
|
||||
|
|
@ -32,7 +35,7 @@ public sealed partial class RunStartTests
|
|||
};
|
||||
|
||||
// The Duel's guns on a Longinus: the drive train above, its Targeting Computer, and the Duel's guns, launchers and sensor.
|
||||
private static ScenarioFit EvLonginus(ScenarioStage stage, string gun = "Cell Blaster", string second = null) => stage.Fit("Longinus",
|
||||
private static ScenarioFit EvLonginus(ScenarioStage stage, string gun = "Cell Blaster", string second = null) => stage.Fit("Longinus", EvStagedQuality,
|
||||
EvLonginusDrive.Concat(new (string design, int2 cell, ItemRotation rotation)[]
|
||||
{
|
||||
("Guided Missile Launcher", int2(0, 5), ItemRotation.None), ("Guided Missile Launcher", int2(5, 5), ItemRotation.None), (gun, int2(1, 8), ItemRotation.None),
|
||||
|
|
@ -70,7 +73,7 @@ public sealed partial class RunStartTests
|
|||
{
|
||||
stage.Player(stage.Bare("Djinni"), float2(-50000, -50000));
|
||||
djinni = stage.Place(EvDjinni(stage), float2(-10000, -30000), facing: float2(0, 1)) as Ship;
|
||||
longinus = stage.Place(stage.Fit("Longinus", EvLonginusDrive), float2(-30000, -30000), facing: float2(0, 1)) as Ship;
|
||||
longinus = stage.Place(stage.Fit("Longinus", EvStagedQuality, EvLonginusDrive), float2(-30000, -30000), facing: float2(0, 1)) as Ship;
|
||||
});
|
||||
var (_, arena, _, failures) = Launch(scenario, Inputs(() => GalaxySeed));
|
||||
Assert.True(failures.Count == 0, string.Join("; ", failures));
|
||||
|
|
@ -621,7 +624,7 @@ public sealed partial class RunStartTests
|
|||
// The target's fit, written out: the Djinni's own hardpoints filled with the Rugged Drive it is generated with, its
|
||||
// reactor, cockpit and radiators. What the evasion tests price is this ship's strafing, not whatever the generator
|
||||
// rolls for it from the galaxy.
|
||||
private static ScenarioFit EvDjinni(ScenarioStage stage) => stage.Fit("Djinni",
|
||||
private static ScenarioFit EvDjinni(ScenarioStage stage) => stage.Fit("Djinni", EvStagedQuality,
|
||||
("Cockpit 2x2", int2(6, 3), ItemRotation.None), ("Compact Reactor", int2(6, 11), ItemRotation.None),
|
||||
("Rugged Drive", int2(6, 0), ItemRotation.Reversed), ("Rugged Drive", int2(5, 1), ItemRotation.Reversed), ("Rugged Drive", int2(7, 1), ItemRotation.Reversed),
|
||||
("Rugged Drive", int2(4, 4), ItemRotation.CounterClockwise), ("Rugged Drive", int2(4, 11), ItemRotation.CounterClockwise),
|
||||
|
|
|
|||
|
|
@ -291,6 +291,35 @@ public sealed partial class RunStartTests
|
|||
Assert.Contains("No Such Gun", Assert.Single(failures));
|
||||
}
|
||||
|
||||
// A fit that states its quality mints every lot it builds at it, assembly and role fills alike; the same fit without one
|
||||
// draws them from the item stream, so some lot comes out otherwise. A scenario's checks rest on the first.
|
||||
[Fact]
|
||||
public void StagedFitMintsAtItsQuality()
|
||||
{
|
||||
(string design, int2 cell, ItemRotation rotation)[] slots =
|
||||
{
|
||||
("Cockpit 2x2", int2(2, 6), ItemRotation.None), ("Compact Reactor", int2(2, 4), ItemRotation.None),
|
||||
("Large Drive", int2(1, 0), ItemRotation.Reversed), ("Radiator", int2(2, 2), ItemRotation.CounterClockwise)
|
||||
};
|
||||
var staged = Stage(Arena(true), new Scripted(true, stage =>
|
||||
{
|
||||
stage.Player(stage.Bare("Djinni"), float2(0, 0));
|
||||
stage.Place(stage.Fit("Longinus", .6f, slots), float2(300, 0));
|
||||
stage.Place(stage.Fit("Longinus", slots), float2(-300, 0));
|
||||
}));
|
||||
IEnumerable<Lot> Lots(Entity entity) =>
|
||||
EntitySerializer.Items(EntitySerializer.Pack(entity)).OfType<CraftedItemInstance>().Select(_items.GetLot);
|
||||
var stated = Lots(staged.Entities[0]).ToArray();
|
||||
var drawn = Lots(staged.Entities[1]).ToArray();
|
||||
Assert.True(stated.Length > slots.Length);
|
||||
Assert.All(stated, lot =>
|
||||
{
|
||||
Assert.Equal(.6f, lot.Quality, 3);
|
||||
Assert.All(lot.Roles ?? new List<RoleFill>(), fill => Assert.Equal(.6f, fill.Quality, 3));
|
||||
});
|
||||
Assert.Contains(drawn, lot => Math.Abs(lot.Quality - .6f) > .001f);
|
||||
}
|
||||
|
||||
// A stance holds both ways between an entity and the player. A piloted entity has exactly one agent; an unpiloted
|
||||
// one has none, and nothing steers it. (It is not frozen: the zone's gravity still moves it, as it moves any ship,
|
||||
// so the rule is observed on its steering inputs.)
|
||||
|
|
|
|||
Loading…
Reference in a new issue