Merge nibu/ammo-tail into nibu/suite-honest

This commit is contained in:
Nibu 2026-11-11 05:15:55 +00:00
commit 663d054b02
15 changed files with 252 additions and 39 deletions

View file

@ -46,6 +46,7 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerC
{
get => _ammo;
}
public override bool HasMagazine => false;
public float Progress
{

View file

@ -104,6 +104,12 @@ public abstract class Weapon : ActivatedBehavior
public abstract int Ammo { get; }
// Rounds in the ship's cargo that reload this weapon's magazine: crates of AmmoType, 0 when the weapon draws none.
public int Reserve => _data.AmmoType.IsSet() ? Entity.CountItemsInCargo(_data.AmmoType.Key) : 0;
// Whether Ammo counts a magazine. A weapon that names an AmmoType and fires from one (InstantWeapon) does; a beam (ConstantWeapon) draws one
// crate per AmmoInterval straight from the cargo and has none.
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 WeaponData WeaponData => _data;
public float RangeFactor(float at) => WeaponData.RangeFactor(MinRange, EffectiveRange, Range, _data.DamageAtMaxRange, at);

View file

@ -32,16 +32,13 @@ public static class Extensions
// Every loadable type assignable to this one. An assembly whose dependencies are missing (a Unity plugin
// referencing UnityEngine outside Unity) still yields the types that do load.
private static readonly Dictionary<Type, Type[]> ChildClasses = new Dictionary<Type, Type[]>();
public static Type[] GetAllChildClasses(this Type type)
// Read from parallel loaders (catalog validation, parallel tests), so the memo is concurrent.
private static readonly System.Collections.Concurrent.ConcurrentDictionary<Type, Type[]> ChildClasses = new System.Collections.Concurrent.ConcurrentDictionary<Type, Type[]>();
public static Type[] GetAllChildClasses(this Type type) => ChildClasses.GetOrAdd(type, t => AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly =>
{
if (ChildClasses.TryGetValue(type, out var children)) return children;
return ChildClasses[type] = AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly =>
{
try { return assembly.GetTypes(); }
catch (ReflectionTypeLoadException e) { return e.Types.Where(t => t != null); }
}).Where(type.IsAssignableFrom).ToArray();
}
try { return assembly.GetTypes(); }
catch (ReflectionTypeLoadException e) { return e.Types.Where(x => x != null); }
}).Where(t.IsAssignableFrom).ToArray());
public static string SplitCamelCase(this string str) =>
Regex.Replace(Regex.Replace(str, @"(\P{Ll})(\P{Ll}\p{Ll})", "$1 $2"), @"(\p{Ll})(\P{Ll})", "$1 $2");

View file

@ -168,7 +168,8 @@ public sealed class ScenarioStage
}
// Items in the entity's first cargo bay, one per name. A name is an equippable or consumable design (open: the stage
// chooses its product) or a product of one (pinned); a name that is neither is a failure.
// chooses its product) or a product of one (pinned), or a commodity (one unit: the crates an AmmoType weapon reloads
// from); a name that is none of these is a failure.
public void Cargo(Entity entity, params string[] names)
{
if (entity == null) return;
@ -177,6 +178,13 @@ public sealed class ScenarioStage
var bay = entity.CargoBays.FirstOrDefault();
foreach (var name in names)
{
var commodity = _items.ItemData.GetAll<SimpleCommodityData>().FirstOrDefault(data => data.Name == name);
if (commodity != null)
{
if (bay == null || !bay.TryStore(new SimpleCommodity { Data = new CultRecordRef<ItemData>(_items.ItemData.RefOf<ItemData>(commodity).Key), Quantity = 1 }))
_failures.Add($"{where}: no room for {name}");
continue;
}
var (data, product) = NamedOrProduct<CraftedItemData>(name, where, found => found is EquippableItemData || found is ConsumableItemData);
if (data == null) continue;
var build = Loadouts.Resolve(_items, _items.ItemData.RefOf(data),

View file

@ -1,3 +1,4 @@
using System.Linq;
using static CultMath.math;
using static ItemRotation;
@ -20,6 +21,8 @@ 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;
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude();
public override void Stage(ScenarioStage stage)
@ -49,12 +52,14 @@ public sealed class Arcs : Scenario
("Compact Reactor", int2(3, 3), None),
("Turret Control Module", int2(3, 5), None),
("not if i see you first", int2(4, 5), None), // Compact Sensor
("Panopticon Prime", int2(3, 6), None)); // Fire Control Array
("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));
stage.Place(stage.Bare("Longinus"), Bow);
stage.Place(stage.Bare("Longinus"), Stern);
stage.Place(turret, Turret, stance: ScenarioStance.Hostile);
// 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.
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);
}

View file

@ -1,3 +1,4 @@
using System.Linq;
using static CultMath.math;
using static ItemRotation;
@ -7,6 +8,7 @@ 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;
public override string Name => "Pursuit";
public override string Id => "pursuit";
@ -55,7 +57,8 @@ 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));
stage.Cargo(player, "Morning Fog", "Morning Fog", "Morning Fog", "send it", "send it");
// 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);
}
}