Mining cut 6: ore taken is visible in flight; one-click Sell Ore #21
12 changed files with 419 additions and 26 deletions
|
|
@ -21236,7 +21236,7 @@ GameObject:
|
|||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1396909587
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -24703,26 +24703,7 @@ MonoBehaviour:
|
|||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: "What the fuck did you just fucking type about me, you little bitch? I\u2019ll
|
||||
have you know I graduated top of my class at MIT, and I\u2019ve been involved
|
||||
in numerous secret raids with Anonymous, and I have over 300 confirmed DDoSes.\n\nI
|
||||
am trained in online trolling and I\u2019m the top hacker in the entire world.
|
||||
You are nothing to me but just another virus host. I will wipe you the fuck out
|
||||
with precision the likes of which has never been seen before on the Internet,
|
||||
mark my fucking words. You think you can get away with typing that shit to me
|
||||
over the Internet? Think again, fucker.\n\nAs we chat over IRC I am tracing your
|
||||
IP with my damn bare hands so you better prepare for the storm, maggot. The storm
|
||||
that wipes out the pathetic little thing you call your computer. You\u2019re
|
||||
fucking dead, kid. I can be anywhere, anytime, and I can hack into your files
|
||||
in over seven hundred ways, and that\u2019s just with my bare hands.\n\nNot only
|
||||
am I extensively trained in hacking, but I have access to the entire arsenal
|
||||
of every piece of malware ever created and I will use it to its full extent to
|
||||
wipe your miserable ass off the face of the world wide web, you little shit.
|
||||
If only you could have known what unholy retribution your little \u201Cclever\u201D
|
||||
comment was about to bring down upon you, maybe you would have held your fucking
|
||||
fingers. But you couldn\u2019t, you didn\u2019t, and now you\u2019re paying the
|
||||
price, you goddamn idiot. I will shit code all over you and you will drown in
|
||||
it.\n\nYou\u2019re fucking dead, kiddo."
|
||||
m_text: ""
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: 53555f7a2d5e745428813f1b1118734f, type: 2}
|
||||
m_sharedMaterial: {fileID: -2804126062856497447, guid: 53555f7a2d5e745428813f1b1118734f,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ public class EventLog : MonoBehaviour
|
|||
private SizedQueue<string> LogMessages;
|
||||
private StringBuilder LogText;
|
||||
|
||||
private void Start()
|
||||
// Awake, not Start: a message can arrive in the frame the parent activates, before Start would run.
|
||||
private void Awake()
|
||||
{
|
||||
LogMessages = new SizedQueue<string>(LineCount);
|
||||
LogText = new StringBuilder();
|
||||
|
|
|
|||
|
|
@ -1092,6 +1092,12 @@ public class ActionGameManager : MonoBehaviour
|
|||
_shipSubscriptions.Add(CurrentEntity.Zone.ShotResolved
|
||||
.Where(outcome => outcome.Source == CurrentEntity)
|
||||
.Subscribe(outcome => _debugLastShots[outcome.Weapon] = outcome));
|
||||
_shipSubscriptions.Add(CurrentEntity.Zone.CargoGained
|
||||
.Where(gain => gain.Taker == CurrentEntity)
|
||||
.Subscribe(gain =>
|
||||
{
|
||||
if (EventLog != null) EventLog.LogMessage(HaulFeed.Line(gain, ItemManager), gain.Adrift > 0 ? Color.yellow : Color.white);
|
||||
}));
|
||||
|
||||
FollowCamera.LookAt = ZoneRenderer.EntityInstances[CurrentEntity].LookAtPoint;
|
||||
FollowCamera.Follow = ZoneRenderer.EntityInstances[CurrentEntity].transform;
|
||||
|
|
|
|||
|
|
@ -1872,14 +1872,24 @@ public static class FireControl
|
|||
var commodity = (SimpleCommodityData) catalog.Get(ores[i].Commodity);
|
||||
var maxStack = Math.Max(1, commodity.MaxStack);
|
||||
var lot = counts[i] > 0 ? shooter.ItemManager.ExtractedLot(zoneIndex, chunk.Field, commodity) : 0;
|
||||
var adrift = 0;
|
||||
for (var left = counts[i]; left > 0; left -= maxStack)
|
||||
{
|
||||
var stack = new SimpleCommodity { Data = new CultRecordRef<ItemData>(key), Lot = lot, Quantity = Math.Min(left, maxStack) };
|
||||
// A partial store leaves the remainder in Quantity and returns false, so the next bay is offered the rest.
|
||||
var stored = false;
|
||||
if (present) foreach (var bay in shooter.CargoBays) if (stored = bay.TryStore(stack)) break;
|
||||
if (!stored) zone.Release(stack, at, velocity);
|
||||
if (!stored)
|
||||
{
|
||||
adrift += stack.Quantity;
|
||||
zone.Release(stack, at, velocity);
|
||||
}
|
||||
}
|
||||
if (counts[i] > 0)
|
||||
zone.CargoGained.OnNext(new CargoGain
|
||||
{
|
||||
Taker = shooter, Design = new CultRecordRef<ItemData>(key), Lot = lot, Stored = counts[i] - adrift, Adrift = adrift, Kind = GainKind.Mined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
47
Assets/Scripts/ServerShared/HaulFeed.cs
Normal file
47
Assets/Scripts/ServerShared/HaulFeed.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
|
||||
public enum GainKind { Mined, Collected }
|
||||
|
||||
// One entry into a hold: what Extract stored (and let float) of one ore, or what a pickup collected. Stored plus Adrift is
|
||||
// the units; a pickup has Adrift 0. Published on Zone.CargoGained by its two writers and read by presentations only.
|
||||
public struct CargoGain
|
||||
{
|
||||
public Entity Taker;
|
||||
public CultRecordRef<ItemData> Design;
|
||||
public int Lot;
|
||||
public int Stored;
|
||||
public int Adrift;
|
||||
public GainKind Kind;
|
||||
}
|
||||
|
||||
// The words for a cargo gain, and the taker's hold fill. Nothing here is stored: the line and the fill are derived on
|
||||
// every call.
|
||||
public static class HaulFeed
|
||||
{
|
||||
public static string Line(CargoGain gain, ItemManager items)
|
||||
{
|
||||
var name = items.ItemData.Get(gain.Design).Name;
|
||||
var (used, total) = HoldFill(gain.Taker?.CargoBays ?? Enumerable.Empty<EquippedCargoBay>());
|
||||
var hold = $"(hold {used}/{total})";
|
||||
if (gain.Kind == GainKind.Collected) return $"Collected {gain.Stored} {name} {hold}";
|
||||
return gain.Adrift > 0
|
||||
? $"+{gain.Stored} {name}, {gain.Adrift} adrift {hold}"
|
||||
: $"+{gain.Stored} {name} {hold}";
|
||||
}
|
||||
|
||||
// Occupied cells over the bays' interiors, a multi-cell item counted per cell.
|
||||
public static (int used, int total) HoldFill(IEnumerable<EquippedCargoBay> bays)
|
||||
{
|
||||
int used = 0, total = 0;
|
||||
foreach (var bay in bays)
|
||||
foreach (var v in bay.Data.InteriorShape.AllCoordinates)
|
||||
{
|
||||
if (!bay.Data.InteriorShape[v]) continue;
|
||||
total++;
|
||||
if (bay.Occupancy[v.x, v.y] != null) used++;
|
||||
}
|
||||
return (used, total);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/HaulFeed.cs.meta
Normal file
11
Assets/Scripts/ServerShared/HaulFeed.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4d3ef606ec5240119cbb3d7dd51123d9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -114,6 +114,30 @@ public static class StationServices
|
|||
return true;
|
||||
}
|
||||
|
||||
// The mined stacks of the bays: simple commodities with a lot whose origin is Extracted. Ammunition and consumables are
|
||||
// simple commodities too, and a bought or staged commodity has Lot 0, which GetLot cannot resolve.
|
||||
private static ItemInstance[] OreStacks(ItemManager items, IEnumerable<EquippedCargoBay> bays) =>
|
||||
bays.SelectMany(bay => bay.Cargo.Keys.OfType<SimpleCommodity>().Where(stack => stack.Lot != 0 && items.GetLot(stack).Origin is Extracted).Select(stack => (ItemInstance) stack)).ToArray();
|
||||
|
||||
// What the station pays for every mined stack of the bays: the sum of SellPrice over the stacks TrySellOre sells.
|
||||
public static int OreValue(ItemManager items, IEnumerable<EquippedCargoBay> bays) =>
|
||||
(int) Math.Min(OreStacks(items, bays).Sum(stack => (long) SellPrice(items, stack)), int.MaxValue);
|
||||
|
||||
// Sells every mined stack of the bays, each through TrySell, so the market's absorb rule and the price are the
|
||||
// single-stack path's. Returns the units sold.
|
||||
public static int TrySellOre(IEnumerable<EquippedCargoBay> bays, Entity station, ref int credits)
|
||||
{
|
||||
var bayList = bays.ToArray();
|
||||
var units = 0;
|
||||
foreach (var stack in OreStacks(station.ItemManager, bayList))
|
||||
{
|
||||
var quantity = ((SimpleCommodity) stack).Quantity;
|
||||
var from = bayList.First(bay => bay.Cargo.ContainsKey(stack));
|
||||
if (TrySell(from, station, stack, ref credits)) units += quantity;
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
// The repair: armour and every durability go back to design and the station charges RepairCost. False, with
|
||||
// nothing restored, when the credits fall short.
|
||||
public static bool TryRepair(Entity entity, ref int credits)
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ public class Zone
|
|||
public Subject<ShotOutcome> ShotResolved = new Subject<ShotOutcome>();
|
||||
// Contacts.Step publishes each hull-on-hull contact it resolves; presentations are the only readers.
|
||||
public Subject<ContactEvent> Contacted = new Subject<ContactEvent>();
|
||||
// Published by FireControl.Extract and TryPickUp once ore or an item enters (or is offered to) a hold; read by presentations only.
|
||||
public Subject<CargoGain> CargoGained = new Subject<CargoGain>();
|
||||
private int _nextShotId;
|
||||
public int NextShotId() => ++_nextShotId;
|
||||
|
||||
|
|
@ -441,8 +443,16 @@ public class Zone
|
|||
{
|
||||
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
|
||||
if (floating == null || (floating.Holder != null && !ReferenceEquals(floating.Holder, taker))) return false;
|
||||
// The quantity is read before the store: a store that merges into a part-filled stack and spills the rest into a
|
||||
// new cell rewrites the item's Quantity.
|
||||
var stored = floating.Item is SimpleCommodity stack ? stack.Quantity : 1;
|
||||
if (!taker.CargoBays.Any(bay => bay.TryStore(floating.Item))) return false;
|
||||
FloatingItems.Remove(floating);
|
||||
CargoGained.OnNext(new CargoGain
|
||||
{
|
||||
Taker = taker, Design = floating.Item.Data, Lot = floating.Item.Lot, Kind = GainKind.Collected,
|
||||
Stored = stored
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -312,6 +312,17 @@ public class TradeMenu : MonoBehaviour
|
|||
Dialog.MoveToCursor();
|
||||
}
|
||||
|
||||
// Sells every mined stack of the player's ships' bays in one step; the station pays through TrySell, so nothing refuses.
|
||||
private void SellOre()
|
||||
{
|
||||
StationServices.TrySellOre(PlayerBays(), GameManager.DockedEntity, ref GameManager.Credits);
|
||||
UpdateCreditsLabel();
|
||||
Populate();
|
||||
}
|
||||
|
||||
private IEnumerable<EquippedCargoBay> PlayerBays() =>
|
||||
GameManager.CurrentEntity.Parent.Children.Where(e => e is Ship {IsPlayerShip: true}).SelectMany(s => s.CargoBays);
|
||||
|
||||
private void Repair()
|
||||
{
|
||||
if (StationServices.TryRepair(GameManager.CurrentEntity, ref GameManager.Credits))
|
||||
|
|
@ -398,6 +409,8 @@ public class TradeMenu : MonoBehaviour
|
|||
});
|
||||
var repairCost = StationServices.RepairCost(GameManager.ItemManager, GameManager.CurrentEntity);
|
||||
if (repairCost > 0) ContextMenu.AddOption($"Repair ({repairCost:N0})", Repair);
|
||||
var oreValue = StationServices.OreValue(GameManager.ItemManager, PlayerBays());
|
||||
if (oreValue > 0) ContextMenu.AddOption($"Sell Ore ({oreValue:N0})", SellOre);
|
||||
ContextMenu.Show();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using CultMath;
|
||||
using UniRx;
|
||||
using Xunit;
|
||||
|
||||
// The demo loop on the shipped catalog (aetheria-release, mining 5a and the ore sheet): the player's own gun breaks a rock
|
||||
|
|
@ -36,6 +38,8 @@ public sealed partial class RunStartTests
|
|||
arena.Update(.02f);
|
||||
Hold();
|
||||
Assert.True(player.SetTarget(rock), "the rock is a target");
|
||||
var gains = new List<CargoGain>();
|
||||
arena.CargoGained.Subscribe(gains.Add);
|
||||
laser.Activate();
|
||||
for (var elapsed = 0f; elapsed < 20f && arena.ChunkExists(rock); elapsed += .02f) { Hold(); arena.Update(.02f); }
|
||||
var info = player.ChunkInfo(rock);
|
||||
|
|
@ -46,13 +50,19 @@ public sealed partial class RunStartTests
|
|||
.Where(h => ores.Contains(_items.GetData(h.stack).Name)).ToArray();
|
||||
Assert.NotEmpty(held);
|
||||
Assert.Empty(arena.FloatingItems);
|
||||
// The feed saw every unit that went into the hold and none float.
|
||||
Assert.NotEmpty(gains);
|
||||
Assert.All(gains, gain => Assert.Equal((GainKind.Mined, 0), (gain.Kind, gain.Adrift)));
|
||||
Assert.Equal(held.Sum(h => h.stack.Quantity), gains.Sum(gain => gain.Stored));
|
||||
|
||||
var zenith = arena.Entities.Single(entity => entity.HullData.Name == "Zenith");
|
||||
var credits = 0;
|
||||
// Every ore stack goes: the market takes it all, whatever the full hold of Zenith has room for.
|
||||
foreach (var (from, sold) in held) Assert.True(StationServices.TrySell(from, zenith, sold, ref credits), "the market takes the ore");
|
||||
// Every mined stack goes in one sale: the market takes it all, whatever the full hold of Zenith has room for.
|
||||
var value = StationServices.OreValue(_items, player.CargoBays);
|
||||
Assert.True(value > 0);
|
||||
Assert.Equal(gains.Sum(gain => gain.Stored), StationServices.TrySellOre(player.CargoBays, zenith, ref credits));
|
||||
Assert.Equal(value, credits);
|
||||
Assert.DoesNotContain(player.CargoBays.SelectMany(bay => bay.Cargo.Keys.OfType<SimpleCommodity>()), c => ores.Contains(_items.GetData(c).Name));
|
||||
Assert.True(credits > 0, "the station pays for the ore");
|
||||
|
||||
var cell = Enumerable.Range(0, player.MaxArmor.GetLength(0)).SelectMany(x => Enumerable.Range(0, player.MaxArmor.GetLength(1)).Select(y => (x, y)))
|
||||
.First(c => player.MaxArmor[c.x, c.y] > 0);
|
||||
|
|
|
|||
198
tests/Aetheria.Shared.Tests/HaulFeedTests.cs
Normal file
198
tests/Aetheria.Shared.Tests/HaulFeedTests.cs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using UniRx;
|
||||
using Xunit;
|
||||
|
||||
// Zone.CargoGained and HaulFeed (aetheria-release mining 6): Extract and TryPickUp publish what entered a hold, once; the feed
|
||||
// words it and counts the hold. Fixtures are Mining Cut 5a's rig.
|
||||
public sealed class HaulFeedTests : IDisposable
|
||||
{
|
||||
private readonly MiningCut5aTests _five = new MiningCut5aTests();
|
||||
private readonly MiningCut4bTests _rigs = new MiningCut4bTests();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_five.Dispose();
|
||||
_rigs.Dispose();
|
||||
}
|
||||
|
||||
// The 5a rig: a rock of 4 units, the shooter holding one bay of the given size; the event log records every gain.
|
||||
private (MiningCut4bTests.Rig Rig, EquippedCargoBay Bay, List<CargoGain> Gains) Mining(int w, int h, params (SimpleCommodityData, float)[] ores)
|
||||
{
|
||||
var kind = MiningCut3Tests.Kind("Asteroid", 20f);
|
||||
var rig = _rigs.Build(damage: 1000f, fuse: null, blastRadius: 0f, precision: 1000f, rockSize: .5f, velocity: 0f, kind: kind, galaxySeed: 0);
|
||||
MiningCut5aTests.Stock(rig.Scene, kind, ores);
|
||||
var bay = MiningCut5aTests.Bay(rig.Scene, rig.Shooter, w, h, "Hold");
|
||||
var gains = new List<CargoGain>();
|
||||
rig.Zone.CargoGained.Subscribe(gains.Add);
|
||||
return (rig, bay, gains);
|
||||
}
|
||||
|
||||
private static CultMath.Random Dice(MiningCut4bTests.Rig rig, uint n) => SimulationDice.For(rig.Zone.DropSeed, SimulationDice.OreStream, n);
|
||||
|
||||
private static void Break(MiningCut4bTests.Rig rig)
|
||||
{
|
||||
for (var shot = 0; shot < 40 && rig.Zone.ChunkExists(rig.Rock); shot++)
|
||||
{
|
||||
FireControl.Fire(rig.Gun, rig.GunItem, rig.Shooter);
|
||||
MiningCut4bTests.Run(rig, .05f);
|
||||
}
|
||||
Assert.False(rig.Zone.ChunkExists(rig.Rock));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnExtractionPublishesWhatWasStoredAndWhatFloated()
|
||||
{
|
||||
// Room for everything.
|
||||
var (rig, bay, gains) = Mining(4, 4, (MiningCut5aTests.Commodity("Rock"), 1f));
|
||||
Break(rig);
|
||||
var gain = Assert.Single(gains);
|
||||
Assert.Equal((GainKind.Mined, 4, 0), (gain.Kind, gain.Stored, gain.Adrift));
|
||||
Assert.Same(rig.Shooter, gain.Taker);
|
||||
Assert.Equal(4, MiningCut5aTests.Units(bay, "Rock", rig.Scene));
|
||||
Assert.NotEqual(0, gain.Lot);
|
||||
|
||||
// A full hold: everything floats.
|
||||
var (full, fullBay, fullGains) = Mining(1, 1, (MiningCut5aTests.Commodity("Grit", maxStack: 1), 1f));
|
||||
Assert.True(fullBay.TryStore(new SimpleCommodity { Data = full.Scene.Cache.RefOf<ItemData>(full.Scene.Cache.GetByName<SimpleCommodityData>("Grit")), Quantity = 1 }));
|
||||
Break(full);
|
||||
var overflow = Assert.Single(fullGains);
|
||||
Assert.Equal((0, 4), (overflow.Stored, overflow.Adrift));
|
||||
Assert.Equal(4, MiningCut5aTests.Units(full.Zone.FloatingItems.Select(f => f.Item), "Grit", full.Scene));
|
||||
|
||||
// A partial store: the hold takes two of four.
|
||||
var (part, partBay, partGains) = Mining(1, 1, (MiningCut5aTests.Commodity("Grit", maxStack: 2), 1f));
|
||||
Break(part);
|
||||
var split = Assert.Single(partGains);
|
||||
Assert.Equal((2, 2), (split.Stored, split.Adrift));
|
||||
Assert.Equal(2, MiningCut5aTests.Units(partBay, "Grit", part.Scene));
|
||||
Assert.Equal(2, MiningCut5aTests.Units(part.Zone.FloatingItems.Select(f => f.Item), "Grit", part.Scene));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AGoneShooterPublishesEverythingAdrift()
|
||||
{
|
||||
var (rig, _, gains) = Mining(4, 4, (MiningCut5aTests.Commodity("Rock"), 1f));
|
||||
rig.Shooter.HeatstrokeDeath.OnNext(UniRx.Unit.Default);
|
||||
var at = rig.Zone.ChunkPose(rig.Rock.Field, rig.Rock.Index).xy;
|
||||
FireControl.Extract(rig.Zone, rig.Rock, rig.Zone.Wear(rig.Rock, 1e6f), rig.Shooter, Dice(rig, 1));
|
||||
var gain = Assert.Single(gains);
|
||||
Assert.Equal((0, 4), (gain.Stored, gain.Adrift));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AMineAndANullShooterPublishNothing()
|
||||
{
|
||||
var (rig, _, gains) = Mining(4, 4, (MiningCut5aTests.Commodity("Rock"), 1f));
|
||||
FireControl.Extract(rig.Zone, rig.Rock, rig.Zone.Wear(rig.Rock, 1e6f), null, Dice(rig, 1));
|
||||
Assert.Empty(gains);
|
||||
Assert.Empty(rig.Zone.FloatingItems);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AMergeIntoAnExistingStackStillPublishes()
|
||||
{
|
||||
var (rig, bay, gains) = Mining(4, 4, (MiningCut5aTests.Commodity("Rock"), 1f));
|
||||
var chunk = rig.Rock;
|
||||
FireControl.Extract(rig.Zone, chunk, rig.Zone.Wear(chunk, 1e6f), rig.Shooter, Dice(rig, 1));
|
||||
var keys = bay.Cargo.Keys.ToArray();
|
||||
// Same rock field and index, same ore, so the same lot: the second extraction merges and writes no Cargo key.
|
||||
FireControl.Extract(rig.Zone, chunk, 40f, rig.Shooter, Dice(rig, 2));
|
||||
Assert.Equal(keys, bay.Cargo.Keys.ToArray());
|
||||
Assert.Equal(2, gains.Count);
|
||||
Assert.Equal(gains[0].Lot, gains[1].Lot);
|
||||
Assert.True(gains[1].Stored > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EveryOreOfOneExtractionPublishesOnce()
|
||||
{
|
||||
var (rig, _, gains) = Mining(8, 8, (MiningCut5aTests.Commodity("Rock"), 1f), (MiningCut5aTests.Commodity("Gold"), 1f));
|
||||
rig.Zone.Settings.AsteroidYield = new ExponentialLerp { Minimum = 400f, Maximum = 400f, Exponent = 1f };
|
||||
FireControl.Extract(rig.Zone, rig.Rock, rig.Zone.Wear(rig.Rock, 1e6f), rig.Shooter, Dice(rig, 3));
|
||||
Assert.Equal(2, gains.Count);
|
||||
Assert.Equal(2, gains.Select(g => g.Lot).Distinct().Count());
|
||||
Assert.All(gains, g => Assert.True(g.Stored + g.Adrift > 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void APickupPublishesCollected()
|
||||
{
|
||||
var grit = MiningCut5aTests.Commodity("Grit", maxStack: 2);
|
||||
var (rig, _, gains) = Mining(1, 1, (grit, 1f));
|
||||
Break(rig);
|
||||
gains.Clear();
|
||||
var floating = rig.Zone.FloatingItems.First();
|
||||
var quantity = ((SimpleCommodity) floating.Item).Quantity;
|
||||
|
||||
// A taker with no room is refused and publishes nothing.
|
||||
var packed = new Ship(rig.Scene.Items, rig.Zone, MiningCut3Tests.Mint(rig.Scene, rig.Scene.Hull), new EntitySettings());
|
||||
rig.Zone.Admit(packed, piloted: false);
|
||||
Assert.False(rig.Zone.TryPickUp(packed, floating.Id));
|
||||
Assert.Empty(gains);
|
||||
|
||||
var taker = new Ship(rig.Scene.Items, rig.Zone, MiningCut3Tests.Mint(rig.Scene, rig.Scene.Hull), new EntitySettings());
|
||||
rig.Zone.Admit(taker, piloted: false);
|
||||
MiningCut5aTests.Bay(rig.Scene, taker, 4, 4, "Taker");
|
||||
Assert.True(rig.Zone.TryPickUp(taker, floating.Id));
|
||||
var gain = Assert.Single(gains);
|
||||
Assert.Equal((GainKind.Collected, quantity, 0), (gain.Kind, gain.Stored, gain.Adrift));
|
||||
Assert.Same(taker, gain.Taker);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void APickupThatMergesAndSpillsReportsTheWholeStack()
|
||||
{
|
||||
var grit = MiningCut5aTests.Commodity("Grit", maxStack: 4);
|
||||
var (rig, _, gains) = Mining(1, 1, (grit, 1f));
|
||||
var taker = new Ship(rig.Scene.Items, rig.Zone, MiningCut3Tests.Mint(rig.Scene, rig.Scene.Hull), new EntitySettings());
|
||||
rig.Zone.Admit(taker, piloted: false);
|
||||
var bay = MiningCut5aTests.Bay(rig.Scene, taker, 2, 1, "Taker");
|
||||
var design = rig.Scene.Cache.RefOf<ItemData>(grit);
|
||||
Assert.True(bay.TryStore(new SimpleCommodity { Data = design, Lot = 7, Quantity = 3 }));
|
||||
gains.Clear();
|
||||
var floating = rig.Zone.Release(new SimpleCommodity { Data = design, Lot = 7, Quantity = 4 }, new float2(0, 0), new float2(0, 0));
|
||||
Assert.True(rig.Zone.TryPickUp(taker, floating.Id));
|
||||
Assert.Equal(7, MiningCut5aTests.Units(bay, "Grit", rig.Scene));
|
||||
Assert.Equal((GainKind.Collected, 4, 0), (Assert.Single(gains).Kind, gains[0].Stored, gains[0].Adrift));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheLineNamesWhatWasTakenAndWhatFloated()
|
||||
{
|
||||
var (rig, bay, _) = Mining(4, 4, (MiningCut5aTests.Commodity("Rock"), 1f));
|
||||
var items = rig.Scene.Items;
|
||||
var design = rig.Scene.Cache.RefOf<ItemData>(rig.Scene.Cache.GetByName<SimpleCommodityData>("Rock"));
|
||||
var fill = HaulFeed.HoldFill(rig.Shooter.CargoBays);
|
||||
var hold = $"hold {fill.used}/{fill.total}";
|
||||
|
||||
var clean = HaulFeed.Line(new CargoGain { Taker = rig.Shooter, Design = design, Stored = 3, Kind = GainKind.Mined }, items);
|
||||
Assert.Contains("3", clean);
|
||||
Assert.Contains("Rock", clean);
|
||||
Assert.Contains(hold, clean);
|
||||
Assert.DoesNotContain("adrift", clean);
|
||||
|
||||
var spill = HaulFeed.Line(new CargoGain { Taker = rig.Shooter, Design = design, Stored = 1, Adrift = 2, Kind = GainKind.Mined }, items);
|
||||
Assert.Contains("2 adrift", spill);
|
||||
Assert.Contains("Rock", spill);
|
||||
var collected = HaulFeed.Line(new CargoGain { Taker = rig.Shooter, Design = design, Stored = 2, Kind = GainKind.Collected }, items);
|
||||
Assert.DoesNotContain("adrift", collected);
|
||||
Assert.Contains("Collected 2", collected);
|
||||
Assert.Contains(hold, collected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HoldFillCountsOccupiedCellsAcrossBays()
|
||||
{
|
||||
var (rig, bay, _) = Mining(2, 2, (MiningCut5aTests.Commodity("Rock"), 1f));
|
||||
var second = MiningCut5aTests.Bay(rig.Scene, rig.Shooter, 3, 1, "Aft");
|
||||
Assert.Equal((0, 7), HaulFeed.HoldFill(new[] { bay, second }));
|
||||
var rock = rig.Scene.Cache.GetByName<SimpleCommodityData>("Rock");
|
||||
Assert.True(bay.TryStore(new SimpleCommodity { Data = rig.Scene.Cache.RefOf<ItemData>(rock), Quantity = 1 }));
|
||||
Assert.True(second.TryStore(new SimpleCommodity { Data = rig.Scene.Cache.RefOf<ItemData>(rock), Quantity = 1 }));
|
||||
Assert.Equal((2, 7), HaulFeed.HoldFill(new[] { bay, second }));
|
||||
}
|
||||
}
|
||||
|
|
@ -505,4 +505,86 @@ public sealed class StationServicesTests : IDisposable
|
|||
Assert.Equal(BuyResult.Bought, StationServices.TryBuy(_items, ore, 10, ref credits, () => true));
|
||||
Assert.Equal(0, credits);
|
||||
}
|
||||
|
||||
private SimpleCommodity Mined(int quantity)
|
||||
{
|
||||
var data = _cache.GetByName<SimpleCommodityData>("Ore");
|
||||
return new SimpleCommodity { Data = _cache.RefOf<ItemData>(data), Quantity = quantity, Lot = _items.ExtractedLot(0, default, data) };
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SellOreSellsOnlyMinedStacks()
|
||||
{
|
||||
var ship = Ship();
|
||||
var bay = ship.CargoBays.Single();
|
||||
var mined = Mined(10);
|
||||
var staged = Ore(5);
|
||||
var gun = Mint("Gun");
|
||||
Assert.True(bay.TryStore(mined));
|
||||
Assert.True(bay.TryStore(staged));
|
||||
Assert.True(bay.TryStore(gun));
|
||||
var station = Station();
|
||||
var value = StationServices.OreValue(_items, ship.CargoBays);
|
||||
Assert.Equal(StationServices.SellPrice(_items, mined), value);
|
||||
Assert.True(value > 0);
|
||||
|
||||
var credits = 100;
|
||||
Assert.Equal(10, StationServices.TrySellOre(ship.CargoBays, station, ref credits));
|
||||
Assert.Equal(100 + value, credits);
|
||||
Assert.DoesNotContain(mined, bay.Cargo.Keys);
|
||||
Assert.Contains(staged, bay.Cargo.Keys);
|
||||
Assert.Contains(gun, bay.Cargo.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SellOreLeavesALottedCommodityThatWasNotExtracted()
|
||||
{
|
||||
var ship = Ship();
|
||||
var bay = ship.CargoBays.Single();
|
||||
var data = _cache.GetByName<SimpleCommodityData>("Ore");
|
||||
var lot = _items.Lots.Add(new Lot { Design = _cache.RefOf<ItemData>(data), Origin = new Attributed(), Quality = 1 });
|
||||
Assert.NotEqual(0, lot);
|
||||
var traded = new SimpleCommodity { Data = _cache.RefOf<ItemData>(data), Quantity = 6, Lot = lot };
|
||||
Assert.True(bay.TryStore(traded));
|
||||
var credits = 100;
|
||||
Assert.Equal(0, StationServices.OreValue(_items, ship.CargoBays));
|
||||
Assert.Equal(0, StationServices.TrySellOre(ship.CargoBays, Station(), ref credits));
|
||||
Assert.Equal(100, credits);
|
||||
Assert.Contains(traded, bay.Cargo.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SellOreAcrossBaysAndShips()
|
||||
{
|
||||
var one = Ship();
|
||||
var two = Ship();
|
||||
var bays = one.CargoBays.Concat(two.CargoBays).ToArray();
|
||||
Assert.True(one.CargoBays.Single().TryStore(Mined(30)));
|
||||
Assert.True(two.CargoBays.Single().TryStore(Mined(40)));
|
||||
var station = Station();
|
||||
// Fill the station's bay so nothing more fits: the market absorbs the sale at full price all the same.
|
||||
for (var i = 0; i < 20; i++) station.CargoBays.Single().TryStore(Ore(100));
|
||||
var value = StationServices.OreValue(_items, bays);
|
||||
Assert.Equal(StationServices.SellPrice(_items, Ore(70)), value);
|
||||
|
||||
var credits = 0;
|
||||
Assert.Equal(70, StationServices.TrySellOre(bays, station, ref credits));
|
||||
Assert.Equal(value, credits);
|
||||
Assert.All(bays, bay => Assert.Empty(bay.Cargo.Keys.OfType<SimpleCommodity>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SellOreWithNoOreChangesNothing()
|
||||
{
|
||||
var ship = Ship();
|
||||
var bay = ship.CargoBays.Single();
|
||||
Assert.True(bay.TryStore(Ore(5)));
|
||||
Assert.True(bay.TryStore(Mint("Gun")));
|
||||
var held = bay.Cargo.Count;
|
||||
var credits = 100;
|
||||
Assert.Equal(0, StationServices.OreValue(_items, ship.CargoBays));
|
||||
Assert.Equal(0, StationServices.TrySellOre(ship.CargoBays, Station(), ref credits));
|
||||
Assert.Equal(100, credits);
|
||||
Assert.Equal(held, bay.Cargo.Count);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue