Launchers draw real missile rounds; reloads cost a crate #8
11 changed files with 258 additions and 18 deletions
|
|
@ -229,7 +229,7 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
if (!_coolingDown || !(_cooldown < 0)) return false;
|
||||
_overshoot = min(-_cooldown * CountdownSpan, dt);
|
||||
_coolingDown = false;
|
||||
if (_data.MagazineSize > 0 && _ammo == 0)
|
||||
if (_data.MagazineSize > 0 && _ammo == 0 && TakeReloadRound())
|
||||
{
|
||||
_ammo = _data.MagazineSize;
|
||||
OnReloadComplete?.Invoke();
|
||||
|
|
@ -239,6 +239,17 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
return true;
|
||||
}
|
||||
|
||||
// A reload pays a round (ruling ballistics-real-ammo): one unit of the weapon's AmmoType leaves the cargo as the magazine refills,
|
||||
// and with none to take the magazine stays empty. A weapon with no AmmoType reloads free.
|
||||
private bool TakeReloadRound()
|
||||
{
|
||||
if (!_data.AmmoType.IsSet()) return true;
|
||||
var cargo = Entity.FindItemInCargo(_data.AmmoType.Key);
|
||||
if (cargo == null) return false;
|
||||
if (cargo.ItemsOfType[_data.AmmoType.Key][0] is SimpleCommodity simpleCommodity) cargo.Remove(simpleCommodity, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
private float CountdownSpan => max(ShortestCycle, _data.MagazineSize > 0 && _ammo == 0 ? _data.ReloadTime : Cooldown);
|
||||
|
||||
// A countdown that starts outside a round (a charge failing) carries the last one's overshoot like every restart.
|
||||
|
|
@ -265,18 +276,8 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
return true;
|
||||
}
|
||||
|
||||
var hasAmmo = true;
|
||||
if (_data.AmmoType.IsSet())
|
||||
{
|
||||
var cargo = Entity.FindItemInCargo(_data.AmmoType.Key);
|
||||
if (cargo != null)
|
||||
{
|
||||
var item = cargo.ItemsOfType[_data.AmmoType.Key][0];
|
||||
if (item is SimpleCommodity simpleCommodity)
|
||||
cargo.Remove(simpleCommodity, 1);
|
||||
}
|
||||
else hasAmmo = false;
|
||||
}
|
||||
// The reload starts only if a round is there to load; FinishCountdown takes it.
|
||||
var hasAmmo = !_data.AmmoType.IsSet() || Entity.CountItemsInCargo(_data.AmmoType.Key) > 0;
|
||||
if(hasAmmo)
|
||||
{
|
||||
OnReloadBegin?.Invoke();
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ public abstract class Weapon : ActivatedBehavior
|
|||
public abstract float DamagePerSecond { get; }
|
||||
public abstract float RangeDamagePerSecond(float range);
|
||||
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;
|
||||
public WeaponData WeaponData => _data;
|
||||
public float RangeFactor(float at) => WeaponData.RangeFactor(MinRange, EffectiveRange, Range, _data.DamageAtMaxRange, at);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ public class LoadoutGenerator
|
|||
public const float ShipConsumableChance = .1f;
|
||||
public const int ShipConsumableUnits = 1;
|
||||
|
||||
// Ammunition-fed weapons reload from cargo (ruling ballistics-real-ammo), so a generated ship carries this many crates of every
|
||||
// distinct AmmoType its weapons draw: enough to fight on past the first magazine, few enough to leave the rest of the hold.
|
||||
public const int AmmunitionCrates = 3;
|
||||
|
||||
public LoadoutGenerator(
|
||||
ref Random random,
|
||||
ItemManager itemManager,
|
||||
|
|
@ -56,6 +60,7 @@ public class LoadoutGenerator
|
|||
OutfitEntity(entity);
|
||||
EquipPickup(entity);
|
||||
StockConsumables(entity, ShipConsumableChance, ShipConsumableUnits);
|
||||
StockAmmunition(entity);
|
||||
return EntitySerializer.Pack(entity);
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +76,7 @@ public class LoadoutGenerator
|
|||
var entity = new OrbitalEntity(ItemManager, null, hull, default, ItemManager.GameplaySettings.DefaultEntitySettings);
|
||||
entity.Faction = Faction;
|
||||
OutfitEntity(entity);
|
||||
StockAmmunition(entity);
|
||||
return EntitySerializer.Pack(entity) as OrbitalEntityPack;
|
||||
}
|
||||
|
||||
|
|
@ -162,6 +168,18 @@ public class LoadoutGenerator
|
|||
}
|
||||
}
|
||||
|
||||
// The crates the ship's weapons reload from: AmmunitionCrates of each distinct AmmoType, stopping when no hold takes another.
|
||||
// Draws nothing from the random stream, so adding it moves no other roll.
|
||||
private void StockAmmunition(Entity entity)
|
||||
{
|
||||
foreach (var key in entity.Weapons.Select(weapon => weapon.WeaponData.AmmoType).Where(type => type.IsSet()).Select(type => type.Key).Distinct())
|
||||
{
|
||||
var crates = new SimpleCommodity { Data = new CultRecordRef<ItemData>(key), Quantity = AmmunitionCrates };
|
||||
if (!entity.CargoBays.Any(bay => bay.TryStore(crates)))
|
||||
ItemManager.Log($"{entity.Name}: no room for {AmmunitionCrates} crates of {key.Value}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsHeater(GearData item) =>
|
||||
item.HardpointType == HardpointType.Tool &&
|
||||
item.Behaviors.Any(b => b is ThermotoggleData { HighPass: false }) &&
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ public class SchematicDisplay : MonoBehaviour
|
|||
if (x.Weapon.WeaponData.AmmoType.IsSet())
|
||||
{
|
||||
if(x.Weapon.WeaponData.MagazineSize > 1)
|
||||
x.ListElement.AmmoLabel.text = x.Weapon.Ammo.ToString();
|
||||
x.ListElement.AmmoLabel.text = $"{x.Weapon.Ammo}/{x.Weapon.Reserve}";
|
||||
else
|
||||
x.ListElement.AmmoLabel.text = _entity.CountItemsInCargo(x.Weapon.WeaponData.AmmoType.Key).ToString();
|
||||
}
|
||||
|
|
|
|||
BIN
GameData/Aetheria.cc
(Stored with Git LFS)
BIN
GameData/Aetheria.cc
(Stored with Git LFS)
Binary file not shown.
57
tests/Aetheria.Shared.Tests/AmmunitionStockingTests.cs
Normal file
57
tests/Aetheria.Shared.Tests/AmmunitionStockingTests.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
// Ruling ballistics-real-ammo: generation stocks combatants with rounds, because a magazine weapon reloads from cargo and a ship with
|
||||
// no crates fires one magazine and goes quiet. Measured on ships the production generator returns, over the shipped catalog.
|
||||
public sealed partial class RunStartTests
|
||||
{
|
||||
// Every generated ship whose weapons draw an ammunition type carries crates of each such type; a ship whose weapons draw none
|
||||
// carries none of an ammunition commodity it does not draw. Kills: generation that stocks nothing (every NPC gun dies after one magazine),
|
||||
// that stocks only one of several types, or that stocks ships with no use for them.
|
||||
[Fact]
|
||||
public void GeneratedShipsCarryTheAmmunitionTheirWeaponsDraw()
|
||||
{
|
||||
var generator = new LoadoutGenerator(ref _items.Random, _items, _galaxy, _galaxy.Entrance, _protagonist, .5f);
|
||||
var shipHulls = LoadoutGenerator.SoldHulls(_cache).Where(h => h.HullType == HullType.Ship).Select(h => h.Name).ToList();
|
||||
var ammunition = _cache.GetAll<WeaponItemData>().SelectMany(i => i.Behaviors.OfType<WeaponData>()).Where(w => w.AmmoType.IsSet())
|
||||
.Select(w => w.AmmoType.Key).Distinct().ToArray();
|
||||
Assert.NotEmpty(ammunition);
|
||||
var fed = 0;
|
||||
foreach (var name in shipHulls)
|
||||
for (var i = 0; i < 12; i++)
|
||||
{
|
||||
var pack = generator.GenerateShipLoadout(h => h.Name == name);
|
||||
if (pack == null) continue;
|
||||
var ship = EntitySerializer.Unpack(_items, null, pack);
|
||||
var drawn = ship.Weapons.Select(w => w.WeaponData.AmmoType).Where(t => t.IsSet()).Select(t => t.Key).Distinct().ToArray();
|
||||
foreach (var key in drawn)
|
||||
{
|
||||
Assert.True(ship.CountItemsInCargo(key) > 0, $"{name}: a weapon draws {key.Value} and the hold has none");
|
||||
Assert.Equal(LoadoutGenerator.AmmunitionCrates, ship.CountItemsInCargo(key));
|
||||
}
|
||||
foreach (var key in ammunition.Except(drawn))
|
||||
Assert.True(ship.CountItemsInCargo(key) == 0, $"{name}: no weapon draws {key.Value} and the hold carries it");
|
||||
if (drawn.Length > 0) fed++;
|
||||
}
|
||||
Assert.True(fed > 0, "fixture: some generated ship carries an ammunition-fed weapon");
|
||||
}
|
||||
|
||||
// A generated turret reloads from its own hold like a ship: every ammunition type its weapons draw has crates in it. Kills: a turret
|
||||
// path that skips StockAmmunition, which leaves an ammo-fed turret quiet after one magazine.
|
||||
[Fact]
|
||||
public void GeneratedTurretsCarryTheAmmunitionTheirWeaponsDraw()
|
||||
{
|
||||
var generator = new LoadoutGenerator(ref _items.Random, _items, _galaxy, _galaxy.Entrance, _protagonist, .5f);
|
||||
var fed = 0;
|
||||
for (var i = 0; i < 60; i++)
|
||||
{
|
||||
var turret = EntitySerializer.Unpack(_items, null, generator.GenerateTurretLoadout());
|
||||
foreach (var key in turret.Weapons.Select(w => w.WeaponData.AmmoType).Where(t => t.IsSet()).Select(t => t.Key).Distinct())
|
||||
{
|
||||
fed++;
|
||||
Assert.Equal(LoadoutGenerator.AmmunitionCrates, turret.CountItemsInCargo(key));
|
||||
}
|
||||
}
|
||||
Assert.True(fed > 0, "fixture: some generated turret carries an ammunition-fed weapon");
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ public sealed class CommodityRecordsTests
|
|||
public void ShippedCatalogOpensWithTheRetiredKeysGone()
|
||||
{
|
||||
using var cache = RestoredHullsTests.OpenCatalog();
|
||||
Assert.Equal(22, cache.GetAll<SimpleCommodityData>().Count());
|
||||
Assert.Equal(23, cache.GetAll<SimpleCommodityData>().Count());
|
||||
Assert.Equal(63, cache.GetAll<CompoundCommodityData>().Count());
|
||||
Assert.Contains(cache.GetAll<SimpleCommodityData>(), c => c.Name == "Ammo");
|
||||
Assert.All(cache.GetAll<CompoundCommodityData>(), c => Assert.False(string.IsNullOrEmpty(c.Name)));
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
(ItemRotation Rotation, float Arc)[] mounts = null, bool beam = false, WeaponFuse? fuse = null, float blast = 0f,
|
||||
float range = 100f, float targetRange = 60f, float accuracy = 1f, float minHit = .2f, bool lockWeapon = false,
|
||||
bool turret = false, float energy = 0f, float heat = 0f, float visibility = 0f, int magazine = 0, bool singleAmmoBurst = false, bool decoy = false, bool charged = false, bool auto = false, float damage = 100f,
|
||||
float effective = float.PositiveInfinity, float damageAtMax = 0f, float velocity = 0f, float targetLayerHeight = 0f)
|
||||
float effective = float.PositiveInfinity, float damageAtMax = 0f, float velocity = 0f, float targetLayerHeight = 0f, int? crates = null, float reload = 1f)
|
||||
{
|
||||
mounts ??= new[] { (ItemRotation.None, 0f) };
|
||||
var hullData = new HullData
|
||||
|
|
@ -144,10 +144,20 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
behavior.Heat = Constant(heat);
|
||||
behavior.Visibility = Constant(visibility);
|
||||
behavior.MagazineSize = magazine;
|
||||
behavior.ReloadTime = reload;
|
||||
|
||||
var cache = AetheriaStores.Open(Catalog + Guid.NewGuid().ToString("N"), catalogWritable: true);
|
||||
_openCaches.Add(cache);
|
||||
cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
// An ammunition-fed weapon: it names the Missiles commodity, and the shooter carries `crates` of it in a bay.
|
||||
if (crates.HasValue)
|
||||
{
|
||||
var bayShape = new Shape(4, 4);
|
||||
foreach (var cell in bayShape.AllCoordinates) bayShape[cell] = true;
|
||||
cache.Upsert(new CargoBayData { Name = "Bay", Shape = new Shape(), InteriorShape = bayShape, Price = 20, Durability = 4 });
|
||||
cache.Upsert(new SimpleCommodityData { Name = "Missiles", Shape = new Shape(), Price = 1, MaxStack = 100 });
|
||||
behavior.AmmoType = new CultRecordRef<SimpleCommodityData>(cache.RefOf<ItemData>(cache.GetByName<SimpleCommodityData>("Missiles")).Key);
|
||||
}
|
||||
cache.Upsert(hullData);
|
||||
cache.Upsert(shooterHullData);
|
||||
cache.Upsert(new WeaponItemData
|
||||
|
|
@ -220,6 +230,13 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
var item = shooter.Equipment.Single(x => x.EquippableItem == d);
|
||||
rig.Decoy = new Gun { Item = item, Weapon = (Weapon) item.Behaviors.Single(b => b is Weapon) };
|
||||
}
|
||||
if (crates.HasValue)
|
||||
{
|
||||
var bay = new EquippedCargoBay(items, Make("Bay", lot++, 4), new int2(1, 1), shooter, "bay");
|
||||
shooter.CargoBays.Add(bay);
|
||||
if (crates.Value > 0)
|
||||
Assert.True(bay.TryStore(new SimpleCommodity { Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<SimpleCommodityData>("Missiles")), Quantity = crates.Value }));
|
||||
}
|
||||
var targeting = Make("Targeting", lot++, 1);
|
||||
Assert.True(shooter.TryFindSpace(targeting, out var tpos));
|
||||
Assert.True(shooter.TryEquip(targeting, tpos));
|
||||
|
|
@ -1306,4 +1323,87 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
|
||||
Assert.False(weapon.CanFire);
|
||||
}
|
||||
|
||||
// ==== Ruling ballistics-real-ammo: an ammunition-fed weapon pays a round per magazine reload, no free reload ====
|
||||
|
||||
// One launcher-shaped weapon (magazine 2, reload 3 s, Missiles) held on its trigger at no target, `crates` of Missiles in the
|
||||
// hold. Returns the rig and a counter of committed shots.
|
||||
private (Rig Rig, Func<int> Shots) AmmoRig(int crates)
|
||||
{
|
||||
var r = Build(fuse: WeaponFuse.Proximity, blast: 30f, range: 40f, magazine: 2, auto: true, crates: crates, reload: 3f);
|
||||
r.Shooter.SetTarget(TargetRef.None);
|
||||
Aim(r, float2(0, 1));
|
||||
var shots = 0;
|
||||
r.Zone.ShotCommitted.Subscribe(_ => shots++);
|
||||
r.Gun.Weapon.Activate();
|
||||
return (r, () => shots);
|
||||
}
|
||||
|
||||
private static void Run(Rig r, float seconds)
|
||||
{
|
||||
for (var t = 0f; t < seconds; t += .25f) { r.Gun.Weapon.Activate(); r.Zone.Update(.25f); }
|
||||
}
|
||||
|
||||
// With no crate in the hold the magazine is fired once and the gun then stays empty, however long the trigger is held. Kills:
|
||||
// a reload that refills the magazine without taking a crate (UseAmmo or FinishCountdown refilling on the timer alone).
|
||||
[Fact]
|
||||
public void AnAmmunitionFedWeaponWithNoCratesFiresItsMagazineAndNoMore()
|
||||
{
|
||||
var (r, shots) = AmmoRig(crates: 0);
|
||||
Run(r, 30f);
|
||||
Assert.Equal(2, shots());
|
||||
Assert.Equal(0, r.Gun.Weapon.Ammo);
|
||||
}
|
||||
|
||||
// One crate is one reload: after ReloadTime the full magazine fires again, the crate is gone, and then the gun is empty. Kills:
|
||||
// a reload that consumes no crate, or more than one, or refills a partial magazine.
|
||||
[Fact]
|
||||
public void OneCrateReloadsTheWholeMagazineOnceAfterTheReloadTime()
|
||||
{
|
||||
var (r, shots) = AmmoRig(crates: 1);
|
||||
Run(r, 2f);
|
||||
Assert.Equal(2, shots());
|
||||
Assert.Equal(1, r.Gun.Weapon.Reserve);
|
||||
Run(r, 30f);
|
||||
Assert.Equal(4, shots());
|
||||
Assert.Equal(0, r.Gun.Weapon.Reserve);
|
||||
Assert.Equal(0, r.Gun.Weapon.Ammo);
|
||||
}
|
||||
|
||||
// The reserve a HUD shows is the cargo count of the weapon's ammunition, and it falls by one when the magazine reloads. Kills: a
|
||||
// Reserve that reads a constant, the wrong commodity, or a count that ignores the stack quantity.
|
||||
[Fact]
|
||||
public void TheReserveIsTheCargoCountOfTheAmmunitionAndFallsByOnePerReload()
|
||||
{
|
||||
var (r, shots) = AmmoRig(crates: 3);
|
||||
Assert.Equal(3, r.Gun.Weapon.Reserve);
|
||||
Assert.Equal(3, r.Shooter.CountItemsInCargo(r.Gun.Weapon.WeaponData.AmmoType.Key));
|
||||
Run(r, 2f);
|
||||
Assert.Equal(2, shots());
|
||||
Assert.Equal(3, r.Gun.Weapon.Reserve);
|
||||
Run(r, 3.5f);
|
||||
Assert.Equal(2, r.Gun.Weapon.Reserve);
|
||||
}
|
||||
|
||||
// A weapon with no ammunition type has no reserve.
|
||||
[Fact]
|
||||
public void AWeaponThatDrawsNoAmmunitionHasNoReserve()
|
||||
{
|
||||
var r = Build(magazine: 2);
|
||||
Assert.Equal(0, r.Gun.Weapon.Reserve);
|
||||
}
|
||||
|
||||
// A magazine weapon that names no ammunition still reloads free and keeps firing: only an AmmoType makes a reload cost a crate.
|
||||
// Kills: TakeReloadRound refusing when AmmoType is unset, which would silence every unfed magazine gun after one magazine.
|
||||
[Fact]
|
||||
public void AMagazineWeaponWithNoAmmunitionTypeStillReloadsFree()
|
||||
{
|
||||
var r = Build(fuse: WeaponFuse.Proximity, blast: 30f, range: 40f, magazine: 2, auto: true, reload: 3f);
|
||||
r.Shooter.SetTarget(TargetRef.None);
|
||||
Aim(r, float2(0, 1));
|
||||
var shots = 0;
|
||||
r.Zone.ShotCommitted.Subscribe(_ => shots++);
|
||||
Run(r, 30f);
|
||||
Assert.True(shots > 4, $"an unfed magazine weapon fires past its first magazine, fired {shots}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
using Xunit;
|
||||
|
||||
// Ruling ballistics-real-ammo, follow-up launcher-ammunition: a launcher is not a free gun. Read from the shipped catalog through
|
||||
// AetherDb's own open, so what is checked is what ships.
|
||||
public sealed class LauncherAmmunitionCatalogTests
|
||||
{
|
||||
private static bool IsLauncher(WeaponData data) => data is LauncherData || data is GuidedWeaponData || data is LockWeaponData;
|
||||
|
||||
// Every launcher design reloads from the Missiles commodity, and Missiles can be made. Kills: a launcher authored (or a new one
|
||||
// added) with AmmoType unset, which reloads for free forever; Missiles without a recipe, which no station would ever supply.
|
||||
[Fact]
|
||||
public void EveryShippedLauncherReloadsFromMissilesAndMissilesCanBeMade()
|
||||
{
|
||||
using var db = AetherDb.Open();
|
||||
var missiles = db.Cache.GetAll<SimpleCommodityData>().Single(c => c.Name == "Missiles");
|
||||
var key = db.Cache.RefOf(missiles).Key;
|
||||
var launchers = db.Cache.GetAll<WeaponItemData>().SelectMany(i => i.Behaviors.OfType<WeaponData>().Where(IsLauncher).Select(d => (i.Name, d))).ToArray();
|
||||
Assert.True(launchers.Length >= 5, $"fixture: the catalog holds its launchers ({launchers.Length})");
|
||||
foreach (var (name, data) in launchers)
|
||||
{
|
||||
Assert.True(data.AmmoType.IsSet(), $"{name} draws no ammunition");
|
||||
Assert.Equal(key, data.AmmoType.Key);
|
||||
}
|
||||
Assert.Contains(db.Cache.GetAll<RecipeData>(), r => r.Output.Key.Equals(key) && r.Inputs.Count > 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -141,6 +141,18 @@ public sealed class Munitions : EconomyBatch
|
|||
"The same powder as a gun charge, shaped to burn for seconds instead of milliseconds: missile stocks and gun stocks compete for one powder train.",
|
||||
new[] { "thruster" }, (powder, 2), (steel, 1));
|
||||
|
||||
// Missiles: the crate a launcher design reloads from. One crate is one magazine reload (the reload takes one unit of the weapon's
|
||||
// AmmoType), the same convention as Ammo for guns, whatever the magazine holds (2 to 288 rounds across the five designs). A crate is
|
||||
// a warhead and a motor; the guidance package (seeker and fin actuators) waits on the electronics chain, so the recipe does not
|
||||
// yet draw it. Shape and stack match Ammo's, so a hold takes crates cell for cell.
|
||||
var missiles = new SimpleCommodityData { Name = "Missiles", Shape = new Shape(), MaxStack = 5, Mass = 8f, Price = 1800,
|
||||
Description = "Crated guided munitions: a high-explosive warhead mated to a rocket motor on a launch rail. The crate a launcher reloads from; the rounds are real, only the reactor energy of the launch is not." };
|
||||
_a.Put(missiles, CommodityKey("missiles"));
|
||||
var missilesRef = new CultRecordRef<ItemData>(CommodityKey("missiles"));
|
||||
Recipe(missilesRef, "munitions-works", 1200, 24, 2,
|
||||
"A missile crate is a warhead and a motor in one box, and the box is the arsenal's most counted object: Zhestokost ties each launcher crew's allowance to a serial range, so a cache of loose crates is either a deserter's or a quartermaster's crime.",
|
||||
new string[0], (warhead, 1), (motor, 1));
|
||||
|
||||
var dispenser = Compound("mine-dispenser", "Mine Dispenser", "A rack and ejector that lays mines in a pattern.", 20f, 1600);
|
||||
Recipe(dispenser, "fabrication", 900, 20, 1,
|
||||
"Little more than steel and actuators, which is why every convoy navy can build one and every blockade starts with one.",
|
||||
|
|
@ -151,6 +163,14 @@ public sealed class Munitions : EconomyBatch
|
|||
foreach (var gun in new[] { "Light Machine Gun", "Autocannon", "Long Slug Rifle", "Charged Scattergun", "Slug Rifle", "Flak Gun", "Cluster Cannon", "Point Defense Gun" })
|
||||
Design(gun, 1800, 30, "A ballistic gun is a barrel and a feed; the round brings the energy.");
|
||||
Design("Mine Launcher", 1800, 24, "A dispenser and the warheads it lays.");
|
||||
|
||||
// Launchers draw real rounds (ruling ballistics-real-ammo): the reactor energy per shot stays as authored, and each magazine
|
||||
// reload consumes one Missiles crate.
|
||||
foreach (var launcher in new[] { "Guided Missile Launcher", "Long-Range Missile Rack", "Short-Range Missile Rack", "Incendiary Rocket Pod", "Swarm Rocket Pod" })
|
||||
_a.Edit<WeaponItemData>(launcher, w =>
|
||||
{
|
||||
foreach (var data in w.Behaviors.OfType<WeaponData>()) data.AmmoType = new CultRecordRef<SimpleCommodityData>(CommodityKey("missiles"));
|
||||
});
|
||||
}
|
||||
|
||||
private CultRecordRef<Faction> FactionRef(string name)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ public static class Program
|
|||
case "brownout-migrate": return BrownoutMigrate(args.Contains("apply"));
|
||||
case "roles-migrate": return RolesMigrate(args.Contains("apply"));
|
||||
case "firing-arc-migrate": return FiringArcMigrate(args.Contains("apply"));
|
||||
case "weapon-ammo": return WeaponAmmo();
|
||||
case "range-falloff": return RangeFalloff(args.Skip(1).ToArray());
|
||||
case "targeting-catalog": return TargetingCatalog(args.Contains("apply"));
|
||||
case "targeting-catalog-6c": return TargetingCatalog6c(args.Contains("apply"));
|
||||
|
|
@ -1104,6 +1105,19 @@ public static class Program
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Read-only: what every weapon design draws per shot (reactor energy, magazine, rounds), so ammunition rulings are audited
|
||||
// against the catalog and not against names.
|
||||
private static int WeaponAmmo()
|
||||
{
|
||||
var db = AetherDb.Open();
|
||||
Console.WriteLine($"{"weapon",-28} {"behaviour",-20} {"energy",-11} {"mag",5} {"reload",6} ammo");
|
||||
foreach (var item in db.Cache.GetAll<WeaponItemData>().OrderBy(i => i.Name, StringComparer.Ordinal))
|
||||
foreach (var w in item.Behaviors.OfType<WeaponData>())
|
||||
Console.WriteLine($"{item.Name,-28} {w.GetType().Name,-20} {w.Energy.Min}-{w.Energy.Max,-6} {w.MagazineSize,5} {w.ReloadTime,6} " +
|
||||
(w.AmmoType.IsSet() ? db.Cache.Get<SimpleCommodityData>(w.AmmoType.Key)?.Name ?? w.AmmoType.Key.Value : "-"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Damage falloff (aetheria-release, damage-curve-fate-ruled): every gun and beam authors an effective range at a
|
||||
// fraction of its Range (Min and Max scaled, Terms copied so the effective range wears and heats as the range does)
|
||||
// and a damage multiplier at maximum range. Guided weapons, lock weapons and mine layers keep the unauthored
|
||||
|
|
|
|||
Loading…
Reference in a new issue