Generation never offers gear no hull can mount
Operator ruling, 2026-09-30, on the Autocannon: "Leave the autocannon unsold, we'll need to author a bunch more hulls before all the gear variety in the game has a home." The fit rule gets one owner, HardpointData.Takes (the gear's type, laid in at the hardpoint's rotation, filling every cell); LoadoutGenerator's hardpoint picks, its reuse of a previous product and `AetherDb hardpoint-fit` all read it. RandomProducts, which both station stock and NPC loadouts draw through, offers only gear with a home: Tool gear, hulls, or hardpoint gear some hull in the catalog takes. The Autocannon keeps its product and price. Test: the Autocannon is never drawn in 2000 draws of its hardpoint type; after a scratch hull whose hardpoint takes it is authored, it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5c3cd599ac
commit
d91ab8daf9
4 changed files with 42 additions and 9 deletions
|
|
@ -567,6 +567,12 @@ public class HardpointData
|
|||
return $"{Enum.GetName(typeof(HardpointType), Type)} Hardpoint {Rotation.Arrow()}";
|
||||
}
|
||||
|
||||
// The one fit rule for hardpoint gear: its type, laid in at this hardpoint's rotation, filling every cell.
|
||||
public bool Takes(EquippableItemData design) =>
|
||||
design.HardpointType == Type &&
|
||||
design.Shape.FitsWithin(Shape, Rotation, out _) &&
|
||||
design.Shape.Coordinates.Length == Shape.Coordinates.Length;
|
||||
|
||||
[IgnoreMember]
|
||||
public float3 TintColor
|
||||
{
|
||||
|
|
|
|||
|
|
@ -141,12 +141,14 @@ public class LoadoutGenerator
|
|||
// lacks variety, so it is logged rather than hidden.
|
||||
public (FactionProductData product, T design)[] RandomProducts<T>(int count, float sizeExponent, Predicate<T> filter = null, bool required = false) where T : EquippableItemData
|
||||
{
|
||||
var hulls = ItemManager.ItemData.GetAll<HullData>().ToArray();
|
||||
var candidates = ItemManager.ItemData.GetAll<FactionProductData>()
|
||||
.Select(product => (product, design: ItemManager.ItemData.Get(product.Design) as T))
|
||||
.Where(entry =>
|
||||
entry.design != null &&
|
||||
entry.design.Price > 0 &&
|
||||
entry.product.Manufacturer.IsSet() &&
|
||||
HasHome(entry.design, hulls) &&
|
||||
(filter?.Invoke(entry.design) ?? true))
|
||||
.ToArray();
|
||||
var available = candidates.Where(entry => IsAvailable(entry.product)).ToArray();
|
||||
|
|
@ -165,6 +167,13 @@ public class LoadoutGenerator
|
|||
count);
|
||||
}
|
||||
|
||||
// Hardpoint gear that no hull in the catalog can mount has no home (operator, 2026-09-30: "we'll need to author a
|
||||
// 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.
|
||||
private static bool HasHome(EquippableItemData design, HullData[] hulls) =>
|
||||
design.HardpointType == HardpointType.Tool || design.HardpointType == HardpointType.Hull ||
|
||||
hulls.Any(hull => hull.Hardpoints.Any(hardpoint => hardpoint.Takes(design)));
|
||||
|
||||
// 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; RunStart, the game's one preset spawner, passes this, so
|
||||
|
|
@ -192,10 +201,7 @@ public class LoadoutGenerator
|
|||
|
||||
public (FactionProductData product, T design) RandomProduct<T>(HardpointData hardpoint, float sizeExponent, Predicate<T> filter = null, bool required = false) where T : EquippableItemData
|
||||
{
|
||||
return RandomProduct<T>(sizeExponent, item => item.HardpointType == hardpoint.Type &&
|
||||
(filter?.Invoke(item) ?? true) &&
|
||||
item.Shape.FitsWithin(hardpoint.Shape, hardpoint.Rotation, out _) &&
|
||||
item.Shape.Coordinates.Length==hardpoint.Shape.Coordinates.Length, required);
|
||||
return RandomProduct<T>(sizeExponent, item => hardpoint.Takes(item) && (filter?.Invoke(item) ?? true), required);
|
||||
}
|
||||
|
||||
private void OutfitEntity(Entity entity)
|
||||
|
|
@ -227,7 +233,7 @@ public class LoadoutGenerator
|
|||
{
|
||||
// If a previously selected product fits, use that one (this is why we must process larger hardpoints first)
|
||||
var entry = previousProducts
|
||||
.FirstOrDefault(e => e.design.HardpointType == hardpoint.Type && e.design.Shape.FitsWithin(hardpoint.Shape, hardpoint.Rotation, out _));
|
||||
.FirstOrDefault(e => hardpoint.Takes(e.design));
|
||||
var previousItem = entity.Equipment.FirstOrDefault(item => item.Data == entry.design);
|
||||
if (entry.design == null) entry = RandomProduct<GearData>(hardpoint, 2);
|
||||
if (entry.design == null) ItemManager.Log($"No compatible item found for entity {Enum.GetName(typeof(HardpointType), hardpoint.Type)} hardpoint!");
|
||||
|
|
|
|||
|
|
@ -486,6 +486,30 @@ public sealed class RunStartTests : IDisposable
|
|||
Assert.True(stalls.Count == 0, $"{stalls.Count} readings of an orbit standing still; first {stalls.FirstOrDefault()}");
|
||||
}
|
||||
|
||||
// Gear no hull can mount has no home (operator, 2026-09-30): generation never offers it, so no station stocks it
|
||||
// and no loadout carries it, though it keeps its product and price. The Autocannon fits no hardpoint of any
|
||||
// shipped hull. Authoring a hull whose hardpoint takes it makes it one generation offers.
|
||||
[Fact]
|
||||
public void GearNoHullCanMountIsOfferedOnlyOnceAHullTakesIt()
|
||||
{
|
||||
var autocannon = _cache.GetAll<EquippableItemData>().Single(design => design.Name == "Autocannon");
|
||||
Assert.True(autocannon.Price > 0 && _cache.GetAll<FactionProductData>().Any(p => p.Design.Key.Equals(_cache.RefOf(autocannon).Key)),
|
||||
"the Autocannon is priced and sold");
|
||||
var generator = new LoadoutGenerator(ref _items.Random, _items, _galaxy, _galaxy.Entrance, _protagonist, .5f);
|
||||
int Offered() => generator.RandomProducts<EquippableItemData>(2000, 0, design => design.HardpointType == autocannon.HardpointType)
|
||||
.Count(entry => entry.design == autocannon);
|
||||
Assert.Equal(0, Offered());
|
||||
|
||||
var shape = new Shape(autocannon.Shape.Width + 2, autocannon.Shape.Height + 2);
|
||||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
_cache.Upsert(new HullData
|
||||
{
|
||||
Name = "Autocannon Mount", HullType = HullType.Ship, Shape = shape, Durability = 100, Mass = 1000, Price = 1,
|
||||
Hardpoints = { new HardpointData { Type = autocannon.HardpointType, Position = int2(1, 1), Shape = autocannon.Shape } }
|
||||
});
|
||||
Assert.True(Offered() > 0, "a hull that takes the Autocannon gives it a home");
|
||||
}
|
||||
|
||||
// Q3, with the operator's 2026-09-30 ruling on the ten product-less designs: a design no product sells is a
|
||||
// scenario test design and nothing else. A test design is one a scenario places: its preset's hull or a slot of
|
||||
// it, or cargo it carries. These are the unsold designs no scenario places, by name.
|
||||
|
|
|
|||
|
|
@ -468,10 +468,7 @@ public static class Program
|
|||
foreach (var hardpoint in hull.Hardpoints.OrderByDescending(h => h.Shape.Coordinates.Length))
|
||||
{
|
||||
var cells = hardpoint.Shape.Coordinates.Length;
|
||||
var matches = gear.Where(g =>
|
||||
g.HardpointType == hardpoint.Type &&
|
||||
g.Shape.FitsWithin(hardpoint.Shape, hardpoint.Rotation, out _) &&
|
||||
g.Shape.Coordinates.Length == cells).ToArray();
|
||||
var matches = gear.Where(hardpoint.Takes).ToArray();
|
||||
var soldMatches = matches.Where(m => sold.Contains(db.Cache.RefOf(m).Key)).ToArray();
|
||||
if (soldMatches.Length == 0) unfillable++;
|
||||
Console.WriteLine($" {hardpoint.Type,-14} {cells,2} cells: {matches.Length} designs match, {soldMatches.Length} sold" +
|
||||
|
|
|
|||
Loading…
Reference in a new issue