Merge eureka/aetheria-release-transfer-verb: give-only ship-to-ship transfer; partial cargo moves conserve units
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
7e2fff1ff1
7 changed files with 688 additions and 40 deletions
124
Assets/Scripts/ServerShared/CargoTransfer.cs
Normal file
124
Assets/Scripts/ServerShared/CargoTransfer.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
|
||||
// Why a give ended. Notification only: nothing reads it to decide anything.
|
||||
public enum GiveEnd
|
||||
{
|
||||
Done,
|
||||
Stopped,
|
||||
OutOfRange,
|
||||
ReceiverGone,
|
||||
ReceiverFull,
|
||||
NothingLeft,
|
||||
GiverDocked
|
||||
}
|
||||
|
||||
// A pilot's standing offer of goods to a ship alongside. Runtime only: nothing about a give is saved.
|
||||
public sealed class GiveOrder
|
||||
{
|
||||
public readonly Entity Receiver;
|
||||
// A commodity order is for any stack of this item's design in the giver's bays; a crafted order is for this instance.
|
||||
public readonly ItemInstance Item;
|
||||
// Units still to move.
|
||||
public int Remaining { get; internal set; }
|
||||
// Units earned and not yet moved, fractional between steps.
|
||||
internal double Budget;
|
||||
|
||||
internal GiveOrder(Entity receiver, ItemInstance item, int quantity)
|
||||
{
|
||||
Receiver = receiver;
|
||||
Item = item;
|
||||
Remaining = quantity;
|
||||
}
|
||||
}
|
||||
|
||||
// The one owner of a give's progress and its end (ruling tender-transfer-verb). A ship's own pilot asks for a give through
|
||||
// PilotCommand.Give and PilotApplier starts it here; nothing else moves goods out of a hold in space (ruling
|
||||
// transfer-is-give-only). Every unit moves through EquippedCargoBay.TryTransferItem, the one conserving bay-to-bay commit.
|
||||
public static class CargoTransfer
|
||||
{
|
||||
internal static void Begin(Entity giver, GiveOrder order)
|
||||
{
|
||||
End(giver, GiveEnd.Stopped);
|
||||
giver.Giving = order;
|
||||
}
|
||||
|
||||
// Ends the giver's give, if it has one, for the reason.
|
||||
internal static void End(Entity giver, GiveEnd reason)
|
||||
{
|
||||
if (giver.Giving == null) return;
|
||||
giver.Giving = null;
|
||||
giver.LastGiveEnd = reason;
|
||||
}
|
||||
|
||||
// One step of every give in the zone: the receiver is still here and alongside, and the rate earns units, each moved
|
||||
// into the first receiver bay that takes it.
|
||||
public static void Step(Zone zone, float deltaTime)
|
||||
{
|
||||
foreach (var giver in zone.Entities.ToArray())
|
||||
{
|
||||
var order = giver.Giving;
|
||||
if (order == null) continue;
|
||||
var settings = giver.ItemManager.GameplaySettings;
|
||||
var range = settings.DockingDistance;
|
||||
if (!zone.Entities.Contains(order.Receiver))
|
||||
{
|
||||
End(giver, GiveEnd.ReceiverGone);
|
||||
continue;
|
||||
}
|
||||
if (lengthsq(order.Receiver.Position.xz - giver.Position.xz) > range * range)
|
||||
{
|
||||
End(giver, GiveEnd.OutOfRange);
|
||||
continue;
|
||||
}
|
||||
|
||||
order.Budget += (double) settings.TransferRate * deltaTime;
|
||||
while (order.Budget >= 1 && order.Remaining > 0)
|
||||
{
|
||||
var source = Source(giver, order);
|
||||
if (source.Bay == null)
|
||||
{
|
||||
End(giver, GiveEnd.NothingLeft);
|
||||
break;
|
||||
}
|
||||
if (!Move(source.Bay, source.Item, order.Receiver))
|
||||
{
|
||||
End(giver, GiveEnd.ReceiverFull);
|
||||
break;
|
||||
}
|
||||
order.Budget -= 1;
|
||||
order.Remaining--;
|
||||
}
|
||||
if (order.Remaining == 0) End(giver, GiveEnd.Done);
|
||||
}
|
||||
}
|
||||
|
||||
// Where the next unit comes from: the first stack of the order's design in the giver's bays, or the crafted instance.
|
||||
private static (EquippedCargoBay Bay, ItemInstance Item) Source(Entity giver, GiveOrder order)
|
||||
{
|
||||
foreach (var bay in giver.CargoBays)
|
||||
{
|
||||
if (order.Item is CraftedItemInstance crafted)
|
||||
{
|
||||
if (bay.Cargo.ContainsKey(crafted)) return (bay, crafted);
|
||||
}
|
||||
else if (bay.ItemsOfType.TryGetValue(order.Item.Data.Key, out var stacks))
|
||||
{
|
||||
var stack = stacks.OfType<SimpleCommodity>().FirstOrDefault(s => s.Quantity > 0);
|
||||
if (stack != null) return (bay, stack);
|
||||
}
|
||||
}
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
// One unit of a commodity, or the crafted item, into the first receiver bay that takes it.
|
||||
private static bool Move(EquippedCargoBay from, ItemInstance item, Entity receiver) =>
|
||||
item is SimpleCommodity commodity
|
||||
? receiver.CargoBays.Any(bay => from.TryTransferItem(bay, commodity, 1))
|
||||
: receiver.CargoBays.Any(bay => from.TryTransferItem(bay, (CraftedItemInstance) item));
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/CargoTransfer.cs.meta
Normal file
11
Assets/Scripts/ServerShared/CargoTransfer.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7be41c09d3a84f5e9a206d18c4f3b5a2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -191,6 +191,10 @@ public abstract class Entity
|
|||
// The pilot's tractor command (0..1); the Pickup behaviour spins its beam toward it.
|
||||
public float TractorCommand { get; internal set; }
|
||||
|
||||
// The give the pilot has under way, and why the last one ended. CargoTransfer is the only writer of both.
|
||||
public GiveOrder Giving { get; internal set; }
|
||||
public GiveEnd? LastGiveEnd { get; internal set; }
|
||||
|
||||
// The beam's spun-up state: Pickup's, or 0 without one.
|
||||
public float TractorPower => GetBehavior<Pickup>()?.Power ?? 0f;
|
||||
|
||||
|
|
@ -1460,42 +1464,6 @@ public abstract class Entity
|
|||
}
|
||||
}
|
||||
|
||||
// Attempts to move a given number of items of the given type to the target Entity
|
||||
// Returns the number of items successfully transferred
|
||||
public int TryTransferItems(Entity target, CultRecordKey itemDataID, int quantity)
|
||||
{
|
||||
int quantityTransferred = 0;
|
||||
while (quantityTransferred < quantity)
|
||||
{
|
||||
EquippedCargoBay originInventory = CargoBays.FirstOrDefault(c => c.ItemsOfType.ContainsKey(itemDataID));
|
||||
|
||||
if (originInventory == null) break;
|
||||
|
||||
var itemInstance = originInventory.ItemsOfType[itemDataID][0];
|
||||
|
||||
if (itemInstance is SimpleCommodity simpleCommodity)
|
||||
{
|
||||
var targetQuantity = min(simpleCommodity.Quantity, quantity - quantityTransferred);
|
||||
if (!target.CargoBays.Any(c => originInventory.TryTransferItem(c, simpleCommodity, targetQuantity)))
|
||||
{
|
||||
quantityTransferred += targetQuantity - simpleCommodity.Quantity;
|
||||
break;
|
||||
}
|
||||
|
||||
quantityTransferred += targetQuantity;
|
||||
}
|
||||
else if (itemInstance is CraftedItemInstance craftedItemInstance)
|
||||
{
|
||||
if (!target.CargoBays.Any(c => originInventory.TryTransferItem(c, craftedItemInstance)))
|
||||
break;
|
||||
|
||||
quantityTransferred++;
|
||||
}
|
||||
}
|
||||
|
||||
return quantityTransferred;
|
||||
}
|
||||
|
||||
public EquippableItem TryUnequip(EquippedItem item)
|
||||
{
|
||||
// Don't allow unequipping when the entity is active
|
||||
|
|
@ -1752,6 +1720,7 @@ public abstract class Entity
|
|||
if (bay != null)
|
||||
{
|
||||
bay.DockedShip = ship;
|
||||
CargoTransfer.End(ship, GiveEnd.GiverDocked);
|
||||
ship.SetParent(this);
|
||||
Zone.Remove(ship);
|
||||
ship.Deactivate();
|
||||
|
|
@ -2697,10 +2666,11 @@ public class EquippedCargoBay : EquippedItem
|
|||
|
||||
var oldPos = Cargo[item];
|
||||
var newItem = Remove(item, quantity);
|
||||
|
||||
if (target.TryStore(item)) return true;
|
||||
|
||||
// Failed to transfer full quantity, move the remaining items back to their old slot
|
||||
|
||||
// The part removed goes across, never the stack left behind.
|
||||
if (target.TryStore(newItem)) return true;
|
||||
|
||||
// Whole or nothing: TryStore takes all of the part or none of it, so a refusal puts the part back where it came from.
|
||||
TryStore(newItem, oldPos);
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ public sealed class PilotCommand
|
|||
internal readonly List<(ActivatedBehavior Behavior, bool Pull)> Triggers = new List<(ActivatedBehavior, bool)>();
|
||||
internal readonly List<(Entity Other, bool? Hostile, bool Toggle)> Iffs = new List<(Entity, bool?, bool)>();
|
||||
internal TargetVerb TargetVerb;
|
||||
// The step's give verb: a Give, or a stop when its Receiver is null. The last one in a step wins.
|
||||
internal (Entity Receiver, ItemInstance Item, int Quantity)? GiveVerb;
|
||||
internal int AimCycles;
|
||||
internal int ShutdownOverrideToggles, HeatsinkToggles, ShieldToggles, Pings;
|
||||
|
||||
|
|
@ -77,6 +79,17 @@ public sealed class PilotCommand
|
|||
public void ToggleHeatsinks() => HeatsinkToggles++;
|
||||
public void ToggleShield() => ShieldToggles++;
|
||||
public void Ping() => Pings++;
|
||||
// Offers goods from this entity's hold to a ship alongside (ruling transfer-is-give-only): the quantity of a commodity,
|
||||
// or the one crafted instance. It replaces any give under way. A give of nothing, or of less than one, is ignored, and
|
||||
// so is one to oneself when applied. The last give or stop in a step wins.
|
||||
public void Give(Entity receiver, ItemInstance item, int quantity)
|
||||
{
|
||||
if (receiver == null || item == null) return;
|
||||
var count = item is CraftedItemInstance ? 1 : quantity;
|
||||
if (count < 1) return;
|
||||
GiveVerb = (receiver, item, count);
|
||||
}
|
||||
public void StopGiving() => GiveVerb = (null, null, 0);
|
||||
|
||||
// The pilot's own pending decisions, which it reads back instead of the ship (that is last step's applied state).
|
||||
public TargetRef? Designated => Designations.Count > 0 ? Designations[Designations.Count - 1] : (TargetRef?) null;
|
||||
|
|
@ -98,6 +111,7 @@ public sealed class PilotCommand
|
|||
Triggers.Clear();
|
||||
Iffs.Clear();
|
||||
TargetVerb = TargetVerb.None;
|
||||
GiveVerb = null;
|
||||
AimCycles = 0;
|
||||
ShutdownOverrideToggles = HeatsinkToggles = ShieldToggles = Pings = 0;
|
||||
}
|
||||
|
|
@ -134,6 +148,11 @@ public static class PilotApplier
|
|||
for (var i = 0; i < command.Pings; i++) entity.Sensor?.Ping();
|
||||
foreach (var hail in command.Hails) entity.SetMessage(hail);
|
||||
foreach (var consumable in command.Uses) entity.TryActivateConsumable(consumable);
|
||||
if (command.GiveVerb is { } give)
|
||||
{
|
||||
if (give.Receiver == null) CargoTransfer.End(entity, GiveEnd.Stopped);
|
||||
else if (!ReferenceEquals(give.Receiver, entity)) CargoTransfer.Begin(entity, new GiveOrder(give.Receiver, give.Item, give.Quantity));
|
||||
}
|
||||
if (command.Aim is float3 aim) entity.Aim = aim;
|
||||
foreach (var (behavior, pull) in command.Triggers)
|
||||
if (pull) behavior.Activate();
|
||||
|
|
|
|||
|
|
@ -205,6 +205,8 @@ public class GameplaySettings
|
|||
public int WeaponGroupCount = 6;
|
||||
public float WarpDistance = 25;
|
||||
public float DockingDistance = 25;
|
||||
// Units per second one pilot gives to a ship within DockingDistance; a commodity unit or one crafted item.
|
||||
public float TransferRate = 2;
|
||||
public float ProductionPersonalityLerp = .05f;
|
||||
public float MessageDuration = 4f;
|
||||
public float TargetPersistenceDuration = 3;
|
||||
|
|
|
|||
|
|
@ -388,6 +388,7 @@ public class Zone
|
|||
foreach (var entity in entities) entity.Sense();
|
||||
_departures.Clear();
|
||||
|
||||
CargoTransfer.Step(this, deltaTime);
|
||||
StepMines(deltaTime);
|
||||
StepClouds(deltaTime);
|
||||
StepFloatingItems(deltaTime);
|
||||
|
|
|
|||
521
tests/Aetheria.Shared.Tests/CargoTransferTests.cs
Normal file
521
tests/Aetheria.Shared.Tests/CargoTransferTests.cs
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using Xunit;
|
||||
|
||||
// Goods moving bay to bay between ships and stations: the one conserving commit (EquippedCargoBay.TryTransferItem) that
|
||||
// the docked trade, the sale and the give all go through. Fixture as StationServicesTests: authored settings, Ore.
|
||||
public sealed class CargoTransferTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-cargotransfer-" + Guid.NewGuid().ToString("N"));
|
||||
private readonly CultCache _cache;
|
||||
private readonly ItemManager _items;
|
||||
private readonly Zone _zone;
|
||||
|
||||
public CargoTransferTests()
|
||||
{
|
||||
Directory.CreateDirectory(_root);
|
||||
_cache = AetheriaStores.Open(Path.Combine(_root, "Aetheria.cc"), catalogWritable: true);
|
||||
_cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
var shape = new Shape(5, 5);
|
||||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
_cache.Upsert(new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = shape, Price = 1000, Durability = 10, Armor = 10 });
|
||||
_cache.Upsert(new HullData { Name = "Hub", HullType = HullType.Station, Shape = shape, Price = 5000, Durability = 10 });
|
||||
var bay = new Shape(4, 4);
|
||||
foreach (var cell in bay.AllCoordinates) bay[cell] = true;
|
||||
_cache.Upsert(new CargoBayData { Name = "Crate", Shape = new Shape(), InteriorShape = bay, Price = 20, Durability = 4 });
|
||||
var pocket = new Shape(1, 1);
|
||||
foreach (var cell in pocket.AllCoordinates) pocket[cell] = true;
|
||||
_cache.Upsert(new CargoBayData { Name = "Pocket", Shape = new Shape(), InteriorShape = pocket, Price = 5, Durability = 4 });
|
||||
_cache.Upsert(new SimpleCommodityData { Name = "Ore", Shape = new Shape(), Price = 7, MaxStack = 100 });
|
||||
_cache.Upsert(new SimpleCommodityData { Name = "Grit", Shape = new Shape(), Price = 1, MaxStack = 2 });
|
||||
_cache.Upsert(new SimpleCommodityData { Name = "Slag", Shape = new Shape(), Price = 1, MaxStack = 100 });
|
||||
_cache.Upsert(new DockingBayData { Name = "Berth", Shape = new Shape(), InteriorShape = bay, MaxSize = new int2(2, 2), Price = 60, Durability = 6 });
|
||||
_cache.Upsert(new GearData { Name = "Gun", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 400, Durability = 8 });
|
||||
_cache.FlushAsync().Wait();
|
||||
|
||||
var settings = AuthoredSettings.Load(RestoredHullsTests.FindRepoRoot()).Read<GameplaySettings>("GameplaySettings");
|
||||
_items = new ItemManager(1u, _cache, new ProvenanceLedger(), settings, _ => { });
|
||||
_zone = new Zone(_items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private EquippableItem Mint(string design)
|
||||
{
|
||||
var data = _cache.GetByName<EquippableItemData>(design);
|
||||
return new EquippableItem
|
||||
{
|
||||
Data = _cache.RefOf<ItemData>(data),
|
||||
Durability = data.Durability,
|
||||
Lot = _items.Lots.Add(new Lot { Design = _cache.RefOf<ItemData>(data), Origin = new Attributed(), Quality = .5f, Roles = new List<RoleFill>() })
|
||||
};
|
||||
}
|
||||
|
||||
private SimpleCommodity Commodity(string design, int quantity) =>
|
||||
new SimpleCommodity { Data = _cache.RefOf<ItemData>(_cache.GetByName<SimpleCommodityData>(design)), Quantity = quantity };
|
||||
|
||||
private Ship ShipWith(string bay)
|
||||
{
|
||||
var ship = new Ship(_items, _zone, Mint("Skiff"), new EntitySettings());
|
||||
Assert.True(ship.TryEquip(Mint(bay), new int2(1, 1)));
|
||||
return ship;
|
||||
}
|
||||
|
||||
private OrbitalEntity Station()
|
||||
{
|
||||
var station = new OrbitalEntity(_items, _zone, Mint("Hub"), default, new EntitySettings());
|
||||
Assert.True(station.TryEquip(Mint("Crate"), new int2(1, 1)));
|
||||
return station;
|
||||
}
|
||||
|
||||
// The units of the same design as `like` that the bay holds, over every stack.
|
||||
private static int Units(EquippedCargoBay bay, SimpleCommodity like) =>
|
||||
bay.Cargo.Keys.OfType<SimpleCommodity>().Where(c => c.Data.Key.Equals(like.Data.Key)).Sum(c => c.Quantity);
|
||||
|
||||
[Fact]
|
||||
public void PartialCommodityTransferConserves()
|
||||
{
|
||||
var from = ShipWith("Crate").CargoBays[0];
|
||||
var to = ShipWith("Crate").CargoBays[0];
|
||||
var ore = Commodity("Ore", 10);
|
||||
Assert.True(from.TryStore(ore));
|
||||
var massBefore = from.Mass + to.Mass;
|
||||
|
||||
Assert.True(from.TryTransferItem(to, ore, 4));
|
||||
Assert.True(from.TryTransferItem(to, ore, 4));
|
||||
|
||||
Assert.Equal(2, Units(from, ore));
|
||||
Assert.Equal(8, Units(to, ore));
|
||||
Assert.Empty(from.Cargo.Keys.Intersect(to.Cargo.Keys));
|
||||
Assert.Equal(massBefore, from.Mass + to.Mass, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ABoughtPartOfAStationStackLeavesTheRestWithTheStation()
|
||||
{
|
||||
// TradeMenu.Buy delivers by this exact commit: the station's stack to the player's bay, a part at a time.
|
||||
var station = Station();
|
||||
var stationBay = station.CargoBays[0];
|
||||
var player = ShipWith("Crate").CargoBays[0];
|
||||
var stock = Commodity("Ore", 10);
|
||||
Assert.True(stationBay.TryStore(stock));
|
||||
var credits = 1000;
|
||||
|
||||
Assert.Equal(BuyResult.Bought, StationServices.TryBuy(_items, stock, 4, ref credits, () => stationBay.TryTransferItem(player, stock, 4)));
|
||||
Assert.Equal(BuyResult.Bought, StationServices.TryBuy(_items, stock, 4, ref credits, () => stationBay.TryTransferItem(player, stock, 4)));
|
||||
|
||||
Assert.Equal(1000 - 2 * 4 * 7, credits);
|
||||
Assert.Equal(2, Units(stationBay, stock));
|
||||
Assert.Equal(8, Units(player, stock));
|
||||
Assert.Empty(stationBay.Cargo.Keys.Intersect(player.Cargo.Keys));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RefusedCommodityTransferChangesNothing()
|
||||
{
|
||||
var from = ShipWith("Crate").CargoBays[0];
|
||||
var to = ShipWith("Pocket").CargoBays[0];
|
||||
var ore = Commodity("Ore", 10);
|
||||
Assert.True(from.TryStore(ore));
|
||||
Assert.True(to.TryStore(Commodity("Slag", 5)));
|
||||
var at = from.Cargo[ore];
|
||||
var massBefore = from.Mass + to.Mass;
|
||||
|
||||
Assert.False(from.TryTransferItem(to, ore, 4));
|
||||
|
||||
Assert.Equal(10, ore.Quantity);
|
||||
Assert.Equal(at, from.Cargo[ore]);
|
||||
Assert.Equal(10, Units(from, ore));
|
||||
Assert.Equal(0, Units(to, ore));
|
||||
Assert.Equal(massBefore, from.Mass + to.Mass, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ATransferThatNeedsMoreRoomThanTheStackHasLeftChangesNothing()
|
||||
{
|
||||
// The receiver's only stack of Ore has room for 2 of the 4: the move is whole or nothing.
|
||||
var from = ShipWith("Crate").CargoBays[0];
|
||||
var to = ShipWith("Pocket").CargoBays[0];
|
||||
var ore = Commodity("Ore", 10);
|
||||
Assert.True(from.TryStore(ore));
|
||||
Assert.True(to.TryStore(Commodity("Ore", 98)));
|
||||
|
||||
Assert.False(from.TryTransferItem(to, ore, 4));
|
||||
|
||||
Assert.Equal(10, Units(from, ore));
|
||||
Assert.Equal(98, Units(to, ore));
|
||||
}
|
||||
|
||||
// ---- The give verb ------------------------------------------------------------------------------------------
|
||||
|
||||
private const float Dt = .1f;
|
||||
|
||||
// Two ships in the zone, alongside each other, the giver holding `ore` units of Ore.
|
||||
private (Ship Giver, Ship Receiver, SimpleCommodity Stack) Alongside(int ore, string receiverBay = "Crate")
|
||||
{
|
||||
var giver = ShipWith("Crate");
|
||||
var receiver = ShipWith(receiverBay);
|
||||
var stack = Commodity("Ore", ore);
|
||||
if (ore > 0) Assert.True(giver.CargoBays[0].TryStore(stack));
|
||||
// Off the origin, so a distance taken from the wrong sum or difference of positions shows.
|
||||
giver.Position = new float3(-50, 0, 10);
|
||||
receiver.Position = giver.Position + new float3(5, 0, 0);
|
||||
_zone.Admit(giver, piloted: false);
|
||||
_zone.Admit(receiver, piloted: false);
|
||||
return (giver, receiver, stack);
|
||||
}
|
||||
|
||||
private static int Held(Entity entity, SimpleCommodity like) => entity.CargoBays.Sum(bay => Units(bay, like));
|
||||
|
||||
private void Steps(int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++) _zone.Update(Dt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GiveMovesAtTheRate()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
|
||||
for (var i = 1; i <= 10; i++)
|
||||
{
|
||||
Steps(1);
|
||||
Assert.Equal(10, Held(giver, ore) + Held(receiver, ore));
|
||||
}
|
||||
|
||||
// Rate 2 for one second: exactly two units, not one and not three.
|
||||
Assert.Equal(2, Held(receiver, ore));
|
||||
Assert.Equal(8, Held(giver, ore));
|
||||
Assert.NotNull(giver.Giving);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GiveEndsDoneAtItsQuantity()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Give(receiver, ore, 3);
|
||||
|
||||
Steps(40);
|
||||
|
||||
Assert.Equal(3, Held(receiver, ore));
|
||||
Assert.Equal(7, Held(giver, ore));
|
||||
Assert.Null(giver.Giving);
|
||||
Assert.Equal(GiveEnd.Done, giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GiveEndsOutOfRange()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
var range = _items.GameplaySettings.DockingDistance;
|
||||
receiver.Position = giver.Position + new float3(range - 1, 0, 0);
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
Steps(10);
|
||||
Assert.Equal(2, Held(receiver, ore));
|
||||
Assert.Null(giver.LastGiveEnd);
|
||||
|
||||
receiver.Position = giver.Position + new float3(range + 1, 0, 0);
|
||||
Steps(10);
|
||||
|
||||
Assert.Equal(2, Held(receiver, ore));
|
||||
Assert.Null(giver.Giving);
|
||||
Assert.Equal(GiveEnd.OutOfRange, giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AReceiverExactlyAtTheDockingDistanceIsStillAlongside()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
receiver.Position = giver.Position + new float3(0, 0, _items.GameplaySettings.DockingDistance);
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
Steps(10);
|
||||
Assert.Equal(2, Held(receiver, ore));
|
||||
Assert.Null(giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AStepOfExactlyOneUnitMovesOneUnit()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
_zone.Update(1f / _items.GameplaySettings.TransferRate);
|
||||
Assert.Equal(1, Held(receiver, ore));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ALongStepNeverMovesMoreThanTheQuantity()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Give(receiver, ore, 3);
|
||||
_zone.Update(5f);
|
||||
Assert.Equal(3, Held(receiver, ore));
|
||||
Assert.Equal(7, Held(giver, ore));
|
||||
Assert.Equal(GiveEnd.Done, giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AGiveOfOneMovesOne()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Give(receiver, ore, 1);
|
||||
Steps(20);
|
||||
Assert.Equal(1, Held(receiver, ore));
|
||||
Assert.Equal(GiveEnd.Done, giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AGiveOfACraftedItemTheGiverDoesNotHoldEndsNothingLeft()
|
||||
{
|
||||
var (giver, receiver, _) = Alongside(0);
|
||||
var gun = Mint("Gun");
|
||||
Assert.True(receiver.CargoBays[0].TryStore(gun));
|
||||
giver.Command.Give(receiver, gun, 1);
|
||||
Steps(20);
|
||||
Assert.Equal(GiveEnd.NothingLeft, giver.LastGiveEnd);
|
||||
Assert.True(receiver.CargoBays[0].Cargo.ContainsKey(gun));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ALongStepAfterTheReceiverLeavesMovesNothing()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
Steps(1);
|
||||
_zone.Remove(receiver);
|
||||
|
||||
_zone.Update(5f);
|
||||
|
||||
Assert.Equal(0, Held(receiver, ore));
|
||||
Assert.Equal(10, Held(giver, ore));
|
||||
Assert.Equal(GiveEnd.ReceiverGone, giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ALongStepOutOfRangeMovesNothing()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
Steps(1);
|
||||
receiver.Position = giver.Position + new float3(0, 0, _items.GameplaySettings.DockingDistance + 1);
|
||||
|
||||
_zone.Update(5f);
|
||||
|
||||
Assert.Equal(0, Held(receiver, ore));
|
||||
Assert.Equal(GiveEnd.OutOfRange, giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GiveRangeIsMeasuredOnTheFlatPlane()
|
||||
{
|
||||
// The plane is xz: a ship far apart in y only is still alongside.
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
receiver.Position = giver.Position + new float3(5, 1000, 0);
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
Steps(10);
|
||||
Assert.Equal(2, Held(receiver, ore));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GiveEndsWhenTheReceiverLeaves()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
Steps(10);
|
||||
Assert.Equal(2, Held(receiver, ore));
|
||||
|
||||
_zone.Remove(receiver);
|
||||
Steps(10);
|
||||
|
||||
Assert.Equal(8, Held(giver, ore));
|
||||
Assert.Equal(2, Held(receiver, ore));
|
||||
Assert.Null(giver.Giving);
|
||||
Assert.Equal(GiveEnd.ReceiverGone, giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DockingEndsTheGiversGive()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
var station = new OrbitalEntity(_items, _zone, Mint("Hub"), default, new EntitySettings());
|
||||
Assert.True(station.TryEquip(Mint("Berth"), new int2(1, 1)));
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
Steps(10);
|
||||
|
||||
Assert.NotNull(station.TryDock(giver));
|
||||
Steps(10);
|
||||
|
||||
Assert.Null(giver.Giving);
|
||||
Assert.Equal(GiveEnd.GiverDocked, giver.LastGiveEnd);
|
||||
Assert.Equal(2, Held(receiver, ore));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GiveEndsOnAFullHold()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10, "Pocket");
|
||||
Assert.True(receiver.CargoBays[0].TryStore(Commodity("Slag", 5)));
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
|
||||
Steps(20);
|
||||
|
||||
Assert.Equal(10, Held(giver, ore));
|
||||
Assert.Equal(0, Held(receiver, ore));
|
||||
Assert.Null(giver.Giving);
|
||||
Assert.Equal(GiveEnd.ReceiverFull, giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GiveEndsWhenNothingIsLeft()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Give(receiver, ore, 50);
|
||||
|
||||
Steps(100);
|
||||
|
||||
Assert.Equal(10, Held(receiver, ore));
|
||||
Assert.Equal(0, Held(giver, ore));
|
||||
Assert.Null(giver.Giving);
|
||||
Assert.Equal(GiveEnd.NothingLeft, giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AGiveDrawsOnEveryStackOfTheDesign()
|
||||
{
|
||||
// Grit stacks two to a stack, so three units are two stacks: the order is for the design, not the stack named.
|
||||
var (giver, receiver, _) = Alongside(0);
|
||||
var grit = Commodity("Grit", 2);
|
||||
Assert.True(giver.CargoBays[0].TryStore(grit));
|
||||
Assert.True(giver.CargoBays[0].TryStore(Commodity("Grit", 1)));
|
||||
Assert.Equal(2, giver.CargoBays[0].ItemsOfType[grit.Data.Key].Count);
|
||||
giver.Command.Give(receiver, grit, 3);
|
||||
|
||||
Steps(40);
|
||||
|
||||
Assert.Equal(3, Held(receiver, grit));
|
||||
Assert.Equal(0, Held(giver, grit));
|
||||
Assert.Equal(GiveEnd.Done, giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AGivenCraftedItemIsThatInstance()
|
||||
{
|
||||
var (giver, receiver, _) = Alongside(0);
|
||||
var first = Mint("Gun");
|
||||
var second = Mint("Gun");
|
||||
Assert.True(giver.CargoBays[0].TryStore(first));
|
||||
Assert.True(giver.CargoBays[0].TryStore(second));
|
||||
giver.Command.Give(receiver, second, 5);
|
||||
|
||||
Steps(20);
|
||||
|
||||
Assert.True(receiver.CargoBays[0].Cargo.ContainsKey(second));
|
||||
Assert.False(receiver.CargoBays[0].Cargo.ContainsKey(first));
|
||||
Assert.True(giver.CargoBays[0].Cargo.ContainsKey(first));
|
||||
Assert.False(giver.CargoBays[0].Cargo.ContainsKey(second));
|
||||
Assert.Equal(GiveEnd.Done, giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheLastGivePressInAStepWins()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
var third = ShipWith("Crate");
|
||||
third.Position = giver.Position + new float3(0, 0, 5);
|
||||
_zone.Admit(third, piloted: false);
|
||||
|
||||
// A give then a stop leaves nothing, and nothing ended.
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
giver.Command.StopGiving();
|
||||
Steps(1);
|
||||
Assert.Null(giver.Giving);
|
||||
Assert.Null(giver.LastGiveEnd);
|
||||
|
||||
// A stop then a give leaves the give.
|
||||
giver.Command.StopGiving();
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
Steps(1);
|
||||
Assert.Same(receiver, giver.Giving.Receiver);
|
||||
|
||||
// A second give in the same step replaces the first before it starts.
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
giver.Command.Give(third, ore, 10);
|
||||
Steps(1);
|
||||
Assert.Same(third, giver.Giving.Receiver);
|
||||
Assert.Equal(GiveEnd.Stopped, giver.LastGiveEnd);
|
||||
|
||||
// A give in a later step replaces the one under way, which ends Stopped.
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
Steps(1);
|
||||
Assert.Same(receiver, giver.Giving.Receiver);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoppingEndsTheGiveAndKeepsWhatMoved()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
Steps(10);
|
||||
giver.Command.StopGiving();
|
||||
Steps(10);
|
||||
|
||||
Assert.Null(giver.Giving);
|
||||
Assert.Equal(GiveEnd.Stopped, giver.LastGiveEnd);
|
||||
Assert.Equal(2, Held(receiver, ore));
|
||||
Assert.Equal(8, Held(giver, ore));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AGiveToOneselfOrOfNothingIsIgnored()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
Steps(1);
|
||||
var order = giver.Giving;
|
||||
|
||||
giver.Command.Give(giver, ore, 5);
|
||||
giver.Command.Give(receiver, ore, 0);
|
||||
giver.Command.Give(receiver, ore, -3);
|
||||
giver.Command.Give(null, ore, 5);
|
||||
giver.Command.Give(receiver, null, 5);
|
||||
Steps(1);
|
||||
|
||||
Assert.Same(order, giver.Giving);
|
||||
Assert.Null(giver.LastGiveEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AGiverStaysInTheZone()
|
||||
{
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
giver.Command.Designate(new TargetRef(receiver));
|
||||
receiver.Command.Designate(new TargetRef(giver));
|
||||
giver.Command.Give(receiver, ore, 10);
|
||||
|
||||
Steps(20);
|
||||
|
||||
Assert.True(_zone.Entities.Contains(giver));
|
||||
Assert.True(_zone.Entities.Contains(receiver));
|
||||
Assert.Same(receiver, giver.Target.Value.Entity);
|
||||
Assert.Same(giver, receiver.Target.Value.Entity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoOrderMovesNothing()
|
||||
{
|
||||
// No order, no work: a zone of ships that are not giving moves nothing.
|
||||
var (giver, receiver, ore) = Alongside(10);
|
||||
Steps(20);
|
||||
Assert.Equal(10, Held(giver, ore));
|
||||
Assert.Equal(0, Held(receiver, ore));
|
||||
Assert.Null(giver.LastGiveEnd);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue