Merge remote-tracking branch 'origin/nibu/ammo-stock-code' into nibu/ammo-data
This commit is contained in:
commit
afa048641f
8 changed files with 179 additions and 31 deletions
|
|
@ -133,13 +133,13 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
public InstantWeapon(InstantWeaponData data, EquippedItem item) : base(data, item)
|
||||
{
|
||||
_data = data;
|
||||
_ammo = data.MagazineSize;
|
||||
_ammo = Magazine;
|
||||
}
|
||||
|
||||
public InstantWeapon(InstantWeaponData data, ConsumableItemEffect item) : base(data, item)
|
||||
{
|
||||
_data = data;
|
||||
_ammo = data.MagazineSize;
|
||||
_ammo = Magazine;
|
||||
}
|
||||
|
||||
protected void Trigger()
|
||||
|
|
@ -229,9 +229,9 @@ 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 && TakeReloadRound())
|
||||
if (Magazine > 0 && _ammo == 0 && TakeReloadRound())
|
||||
{
|
||||
_ammo = _data.MagazineSize;
|
||||
_ammo = Magazine;
|
||||
OnReloadComplete?.Invoke();
|
||||
}
|
||||
else
|
||||
|
|
@ -250,7 +250,7 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
return true;
|
||||
}
|
||||
|
||||
private float CountdownSpan => max(ShortestCycle, _data.MagazineSize > 0 && _ammo == 0 ? _data.ReloadTime : Cooldown);
|
||||
private float CountdownSpan => max(ShortestCycle, Magazine > 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.
|
||||
protected void StartCountdown()
|
||||
|
|
@ -266,9 +266,13 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
return overshoot;
|
||||
}
|
||||
|
||||
// A weapon that names an AmmoType is magazine-fed whatever MagazineSize says: its magazine is at least one round, every round
|
||||
// leaves it and the reload takes a crate. A weapon with no AmmoType and a magazine of one or none fires free.
|
||||
private int Magazine => _data.AmmoType.IsSet() ? max(1, _data.MagazineSize) : _data.MagazineSize;
|
||||
|
||||
private bool UseAmmo(float dt)
|
||||
{
|
||||
if (_data.MagazineSize <= 1) return true;
|
||||
if (!_data.AmmoType.IsSet() && _data.MagazineSize <= 1) return true;
|
||||
|
||||
if (_ammo > 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -157,11 +157,12 @@ public static class Loadouts
|
|||
// spawner is to pass LoadoutGenerator.ChooseProduct, with no fallback to any manufacturer; RunStart does, and so does the
|
||||
// boss in ZoneGenerator, which is built here too from its faction's Loadout (Loadouts.BossOf). The hull's type
|
||||
// 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
|
||||
// put; a station hull is refused. The entity is stocked with the crates its weapons draw (StockAmmunition) unless the caller
|
||||
// authors the cargo itself and passes stockAmmunition false. The entity always gets exactly WeaponGroupCount groups; a loadout with fewer is
|
||||
// padded with empty groups. 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, float? quality = null)
|
||||
Func<CraftedItemData, FactionProductData> choose, List<string> failures, float? quality = null, bool stockAmmunition = true)
|
||||
{
|
||||
var cache = itemManager.ItemData;
|
||||
var reported = failures.Count;
|
||||
|
|
@ -202,9 +203,24 @@ public static class Loadouts
|
|||
built[g] = (items.Select(item => item.GetBehavior<Weapon>()).ToList(), items);
|
||||
}
|
||||
entity.WeaponGroups = built;
|
||||
if (stockAmmunition) StockAmmunition(itemManager, entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
// The crates an entity's weapons reload from: LoadoutGenerator.AmmunitionCrates of each distinct AmmoType, stopping when no hold takes
|
||||
// another. The one owner of ammunition stock: every path that fits weapons onto a new entity (the generator, a Loadout's Materialize,
|
||||
// so a scenario's Fit, a boss, a console-built ship) ends here. A saved or packed entity is never stocked: its cargo is its own.
|
||||
// Draws nothing from the random stream, so adding it moves no other roll.
|
||||
public static void StockAmmunition(ItemManager itemManager, 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 = LoadoutGenerator.AmmunitionCrates };
|
||||
if (!entity.CargoBays.Any(bay => bay.TryStore(crates)))
|
||||
itemManager.Log($"{entity.Name}: no room for {LoadoutGenerator.AmmunitionCrates} crates of {key.Value}.");
|
||||
}
|
||||
}
|
||||
|
||||
// The hull, then each slot's gear, made by the given builders and equipped to the loadout's cells: the one placement of
|
||||
// a loadout, run once on the throwaway manager to decide the fit and once on the live one to build. Null with every
|
||||
// misfit listed when any cell does not take its gear.
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ public class LoadoutGenerator
|
|||
OutfitEntity(entity);
|
||||
EquipPickup(entity);
|
||||
StockConsumables(entity, ShipConsumableChance, ShipConsumableUnits);
|
||||
StockAmmunition(entity);
|
||||
Loadouts.StockAmmunition(ItemManager, entity);
|
||||
return EntitySerializer.Pack(entity);
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ public class LoadoutGenerator
|
|||
var entity = new OrbitalEntity(ItemManager, null, hull, default, ItemManager.GameplaySettings.DefaultEntitySettings);
|
||||
entity.Faction = Faction;
|
||||
OutfitEntity(entity);
|
||||
StockAmmunition(entity);
|
||||
Loadouts.StockAmmunition(ItemManager, entity);
|
||||
return EntitySerializer.Pack(entity) as OrbitalEntityPack;
|
||||
}
|
||||
|
||||
|
|
@ -168,18 +168,6 @@ 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 }) &&
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ public static class RunStart
|
|||
if (!stage.PlayerPlaced)
|
||||
failures.Add("player: the scenario places no player");
|
||||
if (failures.Count > reported) return null;
|
||||
stage.StockUnauthoredAmmunition();
|
||||
|
||||
var player = stage.PlayerShip;
|
||||
player.IsPlayerShip = true;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ public sealed class ScenarioStage
|
|||
private readonly List<string> _failures = new List<string>();
|
||||
private readonly List<Placement> _placed = new List<Placement>();
|
||||
private int _placements;
|
||||
// The entities this stage built from a fit (a packed fit is a saved ship, whose cargo is its own) and those whose cargo the scenario
|
||||
// authored with Cargo: the rest are stocked with the crates their weapons draw when staging ends (StockUnauthoredAmmunition).
|
||||
private readonly HashSet<Entity> _fitted = new HashSet<Entity>();
|
||||
private readonly HashSet<Entity> _cargoAuthored = new HashSet<Entity>();
|
||||
|
||||
internal ScenarioStage(ItemManager items, Zone arena, string startingHull, TutorialGenerationSettings tutorialGenerationSettings)
|
||||
{
|
||||
|
|
@ -40,6 +44,14 @@ public sealed class ScenarioStage
|
|||
public int Credits { get; set; }
|
||||
|
||||
internal IReadOnlyList<string> Failures => _failures;
|
||||
|
||||
// Staging's last step: a fitted entity whose cargo the scenario did not author gets the crates its weapons draw, like every other
|
||||
// newly built armed entity (Loadouts.StockAmmunition). Draws nothing, so it moves no item roll.
|
||||
internal void StockUnauthoredAmmunition()
|
||||
{
|
||||
foreach (var entity in _fitted.Where(entity => !_cargoAuthored.Contains(entity)))
|
||||
Loadouts.StockAmmunition(_items, entity);
|
||||
}
|
||||
internal bool PlayerPlaced { get; private set; }
|
||||
internal Ship PlayerShip { get; private set; }
|
||||
internal IReadOnlyList<Placement> Placed => _placed;
|
||||
|
|
@ -160,6 +172,7 @@ public sealed class ScenarioStage
|
|||
public void Cargo(Entity entity, params string[] names)
|
||||
{
|
||||
if (entity == null) return;
|
||||
_cargoAuthored.Add(entity);
|
||||
var where = $"cargo of {entity.Name}";
|
||||
var bay = entity.CargoBays.FirstOrDefault();
|
||||
foreach (var name in names)
|
||||
|
|
@ -183,7 +196,8 @@ public sealed class ScenarioStage
|
|||
else
|
||||
{
|
||||
var failures = new List<string>();
|
||||
entity = Loadouts.Materialize(_items, Arena, fit.Loadout, _choose, failures, fit.Quality);
|
||||
entity = Loadouts.Materialize(_items, Arena, fit.Loadout, _choose, failures, fit.Quality, stockAmmunition: false);
|
||||
if (entity != null) _fitted.Add(entity);
|
||||
_failures.AddRange(failures.Select(failure => $"{where} ({fit.Loadout.Name}): {failure}"));
|
||||
if (entity == null) return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,12 +234,7 @@ public class SchematicDisplay : MonoBehaviour
|
|||
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.WeaponData.MagazineSize > 1)
|
||||
x.ListElement.AmmoLabel.text = $"{x.Weapon.Ammo}/{x.Weapon.Reserve}";
|
||||
else
|
||||
x.ListElement.AmmoLabel.text = _entity.CountItemsInCargo(x.Weapon.WeaponData.AmmoType.Key).ToString();
|
||||
}
|
||||
x.ListElement.AmmoLabel.text = $"{x.Weapon.Ammo}/{x.Weapon.Reserve}";
|
||||
}
|
||||
|
||||
if (x.ListElement.RangeLabel)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static CultMath.math;
|
||||
using Xunit;
|
||||
|
||||
// Ruling ballistics-real-ammo: generation stocks combatants with rounds, because a magazine weapon reloads from cargo and a ship with
|
||||
|
|
@ -54,4 +56,88 @@ public sealed partial class RunStartTests
|
|||
}
|
||||
Assert.True(fed > 0, "fixture: some generated turret carries an ammunition-fed weapon");
|
||||
}
|
||||
|
||||
private static int[] FedTypesStocked(Entity ship, out int fed)
|
||||
{
|
||||
var keys = ship.Weapons.Select(w => w.WeaponData.AmmoType).Where(t => t.IsSet()).Select(t => t.Key).Distinct().ToArray();
|
||||
fed = keys.Length;
|
||||
return keys.Select(key => ship.CountItemsInCargo(key)).ToArray();
|
||||
}
|
||||
|
||||
// A test hull, a one-cell ammunition-fed gun (a magazine of none, so the single-round rule is in play) and a bay,
|
||||
// in the shared catalog: the smallest authored ship that can carry crates.
|
||||
private (string Hull, (string, CultMath.int2, ItemRotation)[] Slots) FedFit(bool bay)
|
||||
{
|
||||
var shape = new Shape(7, 7);
|
||||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
var hullData = new HullData { Name = "Fed Hull", HullType = HullType.Ship, Shape = shape, Durability = 10, Mass = 100 };
|
||||
_cache.Upsert(hullData);
|
||||
var rounds = _cache.Upsert(new SimpleCommodityData { Name = "Fed Rounds", Shape = new Shape(), Price = 1, MaxStack = 100 });
|
||||
_cache.Upsert(new WeaponItemData
|
||||
{
|
||||
Name = "Fed Gun", Hardpoint = HardpointType.Tool, Shape = new Shape(), Durability = 1,
|
||||
Behaviors = { new InstantWeaponData { AmmoType = rounds, MagazineSize = 0 } }
|
||||
});
|
||||
var interior = new Shape(4, 4);
|
||||
foreach (var cell in interior.AllCoordinates) interior[cell] = true;
|
||||
_cache.Upsert(new CargoBayData { Name = "Fed Bay", Shape = new Shape(), InteriorShape = interior, Price = 20, Durability = 4 });
|
||||
var slots = new List<(string, CultMath.int2, ItemRotation)> { ("Fed Gun", int2(3, 3), ItemRotation.None) };
|
||||
if (bay) slots.Add(("Fed Bay", int2(4, 3), ItemRotation.None));
|
||||
return ("Fed Hull", slots.ToArray());
|
||||
}
|
||||
|
||||
// A ship a scenario fits slot by slot (stage.Fit, no generator) that carries a bay and an ammunition-fed weapon ends with the crates that
|
||||
// weapon draws, and its own Reserve reads them. Kills: Materialize returning the entity unstocked (the authored ship fires one magazine).
|
||||
[Fact]
|
||||
public void AScenarioFitWithAFedWeaponAndABayCarriesItsCrates()
|
||||
{
|
||||
var (hull, slots) = FedFit(bay: true);
|
||||
var (_, _, staged, failures) = Launch(new Scripted(false, stage => stage.Player(stage.Fit(hull, slots), float2(0, 0))), Inputs(() => GalaxySeed));
|
||||
Assert.True(failures.Count == 0, string.Join("; ", failures));
|
||||
var weapon = Assert.Single(staged.Player.Weapons);
|
||||
Assert.True(weapon.WeaponData.AmmoType.IsSet(), "fixture: the gun draws ammunition");
|
||||
Assert.Equal(LoadoutGenerator.AmmunitionCrates, weapon.Reserve);
|
||||
}
|
||||
|
||||
// A scenario that authors a fit's cargo (stage.Cargo) gets exactly that cargo: its author named what the hold holds, so no crates are added.
|
||||
// Kills: stocking that ignores authored cargo and puts missiles into a scenario whose check counts what the hold carries.
|
||||
[Fact]
|
||||
public void AScenarioFitWhoseCargoIsAuthoredIsNotStocked()
|
||||
{
|
||||
var (hull, slots) = FedFit(bay: true);
|
||||
var (_, _, staged, failures) = Launch(new Scripted(false, stage =>
|
||||
stage.Cargo(stage.Player(stage.Fit(hull, slots), float2(0, 0)), "Fed Gun")), Inputs(() => GalaxySeed));
|
||||
Assert.True(failures.Count == 0, string.Join("; ", failures));
|
||||
Assert.Equal(0, Assert.Single(staged.Player.Weapons).Reserve);
|
||||
}
|
||||
|
||||
// With no hold to put crates in there are none, and the fit still builds: stocking is bounded by what the ship can carry, never a failure.
|
||||
[Fact]
|
||||
public void AScenarioFitWithNoBayCarriesNoCratesAndStillBuilds()
|
||||
{
|
||||
var (hull, slots) = FedFit(bay: false);
|
||||
var (_, _, staged, failures) = Launch(new Scripted(false, stage => stage.Player(stage.Fit(hull, slots), float2(0, 0))), Inputs(() => GalaxySeed));
|
||||
Assert.True(failures.Count == 0, string.Join("; ", failures));
|
||||
Assert.Equal(0, Assert.Single(staged.Player.Weapons).Reserve);
|
||||
}
|
||||
|
||||
// The new run's player ship (stage.Generated at the starting hull) and every ship the shipped main-galaxy scenario generates carry the crates
|
||||
// their weapons draw: nothing that reaches play with ammunition-fed weapons is left to one magazine.
|
||||
[Fact]
|
||||
public void ANewRunPlayerShipCarriesTheCratesItsWeaponsDraw()
|
||||
{
|
||||
var fed = 0;
|
||||
for (uint seed = GalaxySeed; seed < GalaxySeed + 8; seed++)
|
||||
{
|
||||
var (_, _, staged, failures) = Launch(new Scripted(false, stage => stage.Player(stage.Generated(stage.StartingHull), float2(0, 0)), seed),
|
||||
Inputs(() => seed));
|
||||
Assert.True(failures.Count == 0, string.Join("; ", failures));
|
||||
foreach (var count in FedTypesStocked(staged.Player, out var types))
|
||||
{
|
||||
Assert.Equal(LoadoutGenerator.AmmunitionCrates, count);
|
||||
fed += types;
|
||||
}
|
||||
}
|
||||
Assert.True(fed > 0, "fixture: some new-run player ship carries an ammunition-fed weapon");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1328,9 +1328,9 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
|
||||
// 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)
|
||||
private (Rig Rig, Func<int> Shots) AmmoRig(int crates, int magazine = 2)
|
||||
{
|
||||
var r = Build(fuse: WeaponFuse.Proximity, blast: 30f, range: 40f, magazine: 2, auto: true, crates: crates, reload: 3f);
|
||||
var r = Build(fuse: WeaponFuse.Proximity, blast: 30f, range: 40f, magazine: magazine, auto: true, crates: crates, reload: 3f);
|
||||
r.Shooter.SetTarget(TargetRef.None);
|
||||
Aim(r, float2(0, 1));
|
||||
var shots = 0;
|
||||
|
|
@ -1406,4 +1406,48 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
Run(r, 30f);
|
||||
Assert.True(shots > 4, $"an unfed magazine weapon fires past its first magazine, fired {shots}");
|
||||
}
|
||||
|
||||
// A weapon that names an AmmoType is magazine-fed whatever MagazineSize says (0 or 1 is a magazine of one): it fires one round, every
|
||||
// reload takes a crate, and with none it fires no more. Kills: UseAmmo/reload ignoring AmmoType at MagazineSize <= 1 (free fire, or
|
||||
// fire that never touches the cargo).
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void ASingleRoundAmmunitionFedWeaponFiresOnePerCrateAndNoMore(int magazine)
|
||||
{
|
||||
var (r, shots) = AmmoRig(crates: 2, magazine: magazine);
|
||||
Assert.Equal(1, r.Gun.Weapon.Ammo);
|
||||
Run(r, 2f);
|
||||
Assert.Equal(1, shots());
|
||||
Assert.Equal(2, r.Gun.Weapon.Reserve);
|
||||
Run(r, 30f);
|
||||
Assert.Equal(3, shots());
|
||||
Assert.Equal(0, r.Gun.Weapon.Reserve);
|
||||
Assert.Equal(0, r.Gun.Weapon.Ammo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void ASingleRoundAmmunitionFedWeaponWithNoCrateFiresOnceAndNoMore(int magazine)
|
||||
{
|
||||
var (r, shots) = AmmoRig(crates: 0, magazine: magazine);
|
||||
Run(r, 30f);
|
||||
Assert.Equal(1, shots());
|
||||
}
|
||||
|
||||
// With no AmmoType a magazine of 0 or 1 is no magazine: the weapon fires free, as before. Kills: the new rule leaking onto unfed weapons.
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void AWeaponWithNoAmmunitionTypeAndNoRealMagazineFiresFree(int magazine)
|
||||
{
|
||||
var r = Build(fuse: WeaponFuse.Proximity, blast: 30f, range: 40f, magazine: magazine, 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 weapon fires on, fired {shots}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue