Loot 1 (simulation): Zone owns floating items, rolls death drops on SimulationDice, expires them and commits pickups
Zone.Release is the one adder, StepFloatingItems and TryPickUp the only removers; DropLoot runs from the one death subscription before the entity leaves the zone. Three C# default settings. Unity's drop path is untouched (loot-1-present deletes it). Not yet built or run at this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d50f54bf53
commit
fa4467e9a3
4 changed files with 468 additions and 1 deletions
|
|
@ -40,3 +40,14 @@ public struct KinematicBody
|
|||
Position += (Velocity + Drift) * dt;
|
||||
}
|
||||
}
|
||||
|
||||
// An item adrift in a zone: a death drop or jettisoned cargo. Zone.Release is its only adder; Zone's floating-item
|
||||
// step (expiry) and TryPickUp are its only removers. Runtime state, never saved.
|
||||
public sealed class FloatingItem
|
||||
{
|
||||
public FloatingBodyId Id;
|
||||
public ItemInstance Item;
|
||||
public KinematicBody Body;
|
||||
public double ReleasedAt;
|
||||
public float Lifetime;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,6 +178,11 @@ public class GameplaySettings
|
|||
public float FloatingBodyDrag = .05f;
|
||||
public float FloatingBodyLaunchDrag = .2f;
|
||||
public float FloatingBodyGravity = 1f;
|
||||
// Loot: the chance each equipped item (the hull excepted) drops when its carrier dies, the launch speed of a
|
||||
// dropped item, and how many seconds a floating item lasts.
|
||||
public float LootDropProbability = .25f;
|
||||
public float LootDropVelocity = 25f;
|
||||
public float FloatingItemLifetime = 30f;
|
||||
public float SevereHeatstrokeRiskThreshold = .25f;
|
||||
public float WormholeDepth = 1000;
|
||||
public float WormholeExitVelocity = 20;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ public class Zone
|
|||
// Vented vapour clouds, in vent order. Zone owns their existence: Vent is the one adder, StepClouds the one
|
||||
// remover, and Obscuration the one reader that detection uses. Runtime state, never saved.
|
||||
public ReactiveCollection<VapourCloud> Clouds = new ReactiveCollection<VapourCloud>();
|
||||
|
||||
// Floating items (death drops, jettisoned cargo), in release order. Zone owns their existence: Release is the one
|
||||
// adder; StepFloatingItems (expiry) and TryPickUp are the only removers. Runtime state, never saved.
|
||||
public ReactiveCollection<FloatingItem> FloatingItems = new ReactiveCollection<FloatingItem>();
|
||||
public Dictionary<CultRecordKey, BodyData> Planets = new Dictionary<CultRecordKey, BodyData>();
|
||||
public Dictionary<CultRecordKey, Planet> PlanetInstances = new Dictionary<CultRecordKey, Planet>();
|
||||
|
||||
|
|
@ -93,6 +97,9 @@ public class Zone
|
|||
private uint _lastBodyId;
|
||||
public FloatingBodyId NextBodyId() => new FloatingBodyId(++_lastBodyId);
|
||||
|
||||
// The death ordinal of the loot dice: the nth death in this zone rolls on SimulationDice.For(CombatSeed, LootStream, n).
|
||||
private uint _deaths;
|
||||
|
||||
public Zone(ItemManager itemManager, PlanetSettings settings, ZonePack pack, GalaxyZone galaxyZone, Galaxy galaxy)
|
||||
{
|
||||
_time = pack.Time;
|
||||
|
|
@ -108,7 +115,7 @@ public class Zone
|
|||
// in the simulation, not only in Unity's own loot-drop subscription. One subscription point covers
|
||||
// every join, whichever caller admits the entity (deserialization below, a jump, a spawned turret) --
|
||||
// ObserveAdd fires for all of them. Forbidden writer: no presentation may remove an entity from Zone.
|
||||
Entities.ObserveAdd().Subscribe(add => add.Value.Death.Subscribe(_ => { Entities.Remove(add.Value); add.Value.Deactivate(); }));
|
||||
Entities.ObserveAdd().Subscribe(add => add.Value.Death.Subscribe(_ => { DropLoot(add.Value); Entities.Remove(add.Value); add.Value.Deactivate(); }));
|
||||
|
||||
Targets.Add(new EntityTargets());
|
||||
|
||||
|
|
@ -232,6 +239,7 @@ public class Zone
|
|||
|
||||
StepMines(deltaTime);
|
||||
StepClouds(deltaTime);
|
||||
StepFloatingItems(deltaTime);
|
||||
|
||||
// Cut 3: after every entity has had its chance to fire this tick, age and resolve the shots that
|
||||
// firing queued. A shot fired this tick with a flight time shorter than CommitHorizon commits and
|
||||
|
|
@ -252,6 +260,61 @@ public class Zone
|
|||
Clouds.Add(cloud);
|
||||
}
|
||||
|
||||
// The one adder of floating items: death drops and jettison both come here. The item drifts as a KinematicBody
|
||||
// launched at `velocity` and expires after FloatingItemLifetime.
|
||||
public FloatingItem Release(ItemInstance item, float2 position, float2 velocity)
|
||||
{
|
||||
var floating = new FloatingItem
|
||||
{
|
||||
Id = NextBodyId(), Item = item, Body = new KinematicBody { Position = position, Velocity = velocity },
|
||||
ReleasedAt = _time, Lifetime = _itemManager.GameplaySettings.FloatingItemLifetime
|
||||
};
|
||||
FloatingItems.Add(floating);
|
||||
return floating;
|
||||
}
|
||||
|
||||
// The one commit of a pickup: the first of the taker's cargo bays that accepts the item stores it and the item
|
||||
// stops floating. Otherwise nothing changes.
|
||||
public bool TryPickUp(Entity taker, FloatingBodyId id)
|
||||
{
|
||||
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
|
||||
if (floating == null || !taker.CargoBays.Any(bay => bay.TryStore(floating.Item))) return false;
|
||||
FloatingItems.Remove(floating);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Called from the death subscription, before the entity leaves the zone: one generator per death. Each equipped
|
||||
// item but the hull drops on a LootDropProbability draw; every cargo stack drops whole. Each drop leaves from the
|
||||
// dead entity's position at a random heading. Draw order: Equipment order (the probability draw, then the heading
|
||||
// of a dropped item), then each cargo bay's stacks in CargoBays order (a heading each).
|
||||
private void DropLoot(Entity dead)
|
||||
{
|
||||
var settings = _itemManager.GameplaySettings;
|
||||
var random = SimulationDice.For(CombatSeed, SimulationDice.LootStream, ++_deaths);
|
||||
var at = new float2(dead.Position.x, dead.Position.z);
|
||||
float2 Heading() { var angle = random.NextFloat() * 2f * PI; return float2(cos(angle), sin(angle)); }
|
||||
foreach (var equipped in dead.Equipment.ToArray())
|
||||
{
|
||||
if (equipped == dead.EquippedHull || !(random.NextFloat() < settings.LootDropProbability)) continue;
|
||||
Release(equipped.EquippableItem, at, Heading() * settings.LootDropVelocity);
|
||||
}
|
||||
foreach (var bay in dead.CargoBays.ToArray())
|
||||
foreach (var stack in bay.Cargo.Keys.ToArray())
|
||||
Release(stack, at, Heading() * settings.LootDropVelocity);
|
||||
}
|
||||
|
||||
// After StepClouds and before FireControl.Step: each floating item drifts as a KinematicBody on the zone's force
|
||||
// and is removed once its lifetime has run out.
|
||||
private void StepFloatingItems(float deltaTime)
|
||||
{
|
||||
var settings = _itemManager.GameplaySettings;
|
||||
foreach (var floating in FloatingItems.ToArray())
|
||||
{
|
||||
floating.Body.Step(deltaTime, GetForce(floating.Body.Position), settings);
|
||||
if (floating.ReleasedAt + floating.Lifetime <= _time) FloatingItems.Remove(floating);
|
||||
}
|
||||
}
|
||||
|
||||
// The fraction of a sight line's signal that survives the clouds between `a` and `b`: the product of
|
||||
// (1 - opacity now) over every cloud whose disc the segment touches, so 1f when none does. Derived from the
|
||||
// live clouds at each call and stored nowhere; Sensor.Gain is where it enters detection.
|
||||
|
|
|
|||
388
tests/Aetheria.Shared.Tests/LootTests.cs
Normal file
388
tests/Aetheria.Shared.Tests/LootTests.cs
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
/* 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;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using UniRx;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
using int2 = CultMath.int2;
|
||||
|
||||
// Loot in the sim (aetheria-release cut loot-1): a death drops gear by probability and cargo whole, rolled on the
|
||||
// zone's own dice; the drops float as free bodies, expire, and are collected through Zone.TryPickUp. Rulings:
|
||||
// jettison-shared-floating-items, mines-float-like-loot, one-gravity-law. Every death here goes through the
|
||||
// production path: an entity's own death stream, observed by the one death subscription in Zone.
|
||||
public sealed class LootTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-loot-" + Guid.NewGuid().ToString("N"));
|
||||
private readonly List<CultCache> _caches = new List<CultCache>();
|
||||
|
||||
public LootTests() => Directory.CreateDirectory(_root);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var c in _caches) c.Dispose();
|
||||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private static PerformanceStat Constant(float v) => new PerformanceStat { Min = v, Max = v };
|
||||
|
||||
private static Shape Solid(int w, int h)
|
||||
{
|
||||
var shape = new Shape(w, h);
|
||||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
return shape;
|
||||
}
|
||||
|
||||
private sealed class Lab
|
||||
{
|
||||
public ItemManager Items;
|
||||
public Zone Zone;
|
||||
public int Lot = 4001;
|
||||
|
||||
public EquippableItem Make(string name, float durability = 1) => new EquippableItem
|
||||
{
|
||||
Data = Items.ItemData.RefOf<ItemData>(Items.ItemData.GetByName<EquippableItemData>(name)), Durability = durability, Lot = Lot++
|
||||
};
|
||||
|
||||
public SimpleCommodity Ore(int quantity) => new SimpleCommodity
|
||||
{
|
||||
Data = Items.ItemData.RefOf<ItemData>(Items.ItemData.GetByName<SimpleCommodityData>("Ore")), Quantity = quantity
|
||||
};
|
||||
|
||||
public string Describe(ItemInstance item) => Items.GetData(item).Name + (item is SimpleCommodity c ? "x" + c.Quantity : "");
|
||||
}
|
||||
|
||||
private Lab Build(string zoneName = "Loot", float probability = .5f, float lifetime = 30f, float velocity = 25f,
|
||||
PlanetSettings planets = null, float spread = 0f)
|
||||
{
|
||||
var settings = new GameplaySettings
|
||||
{
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp(),
|
||||
TargetDetectionInfoThreshold = .1f, TargetArmorInfoThreshold = .2f, TargetGearInfoThreshold = .8f,
|
||||
FiringArc = 170, CommitHorizon = .5f, SchematicCellSize = 1f,
|
||||
UnaidedAccuracy = .05f, UnaidedTracking = 10f, UnaidedPrecision = 2f, AgentMinHitProbability = 0f,
|
||||
LootDropProbability = probability, LootDropVelocity = velocity, FloatingItemLifetime = lifetime
|
||||
};
|
||||
var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true);
|
||||
_caches.Add(cache);
|
||||
cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
var hardpoint = new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() };
|
||||
cache.Upsert(new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = Solid(5, 5), Durability = 1000000, Mass = 1000, Hardpoints = { hardpoint } });
|
||||
cache.Upsert(new HullData { Name = "Hub", HullType = HullType.Station, Shape = Solid(5, 5), Durability = 1000000, Mass = 1000 });
|
||||
cache.Upsert(new GearData
|
||||
{
|
||||
Name = "Gun", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 1,
|
||||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
|
||||
Behaviors = { new InstantWeaponData
|
||||
{
|
||||
Damage = Constant(1), Range = Constant(1000), MinRange = Constant(0), Velocity = Constant(0), Spread = Constant(spread),
|
||||
DamageSpread = Constant(0), Penetration = Constant(0), Count = Constant(1), BurstTime = Constant(0), Cooldown = Constant(1000),
|
||||
DamageCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }
|
||||
} }
|
||||
});
|
||||
cache.Upsert(new GearData
|
||||
{
|
||||
Name = "Targeting", Hardpoint = HardpointType.Tool, Shape = new Shape(), Durability = 1,
|
||||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
|
||||
Behaviors = { new TargetingSystemData { Accuracy = Constant(1), Resolution = Constant(1000), Precision = Constant(1), Tracking = Constant(100000) } }
|
||||
});
|
||||
// One occupied cell, so a bay can be filled with it.
|
||||
cache.Upsert(new GearData
|
||||
{
|
||||
Name = "Spare", Hardpoint = HardpointType.Tool, Shape = Solid(1, 1), Durability = 1,
|
||||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000
|
||||
});
|
||||
cache.Upsert(new CargoBayData { Name = "Crate", Shape = new Shape(), InteriorShape = Solid(4, 4), Durability = 4 });
|
||||
cache.Upsert(new CargoBayData { Name = "Tiny", Shape = new Shape(), InteriorShape = Solid(1, 1), Durability = 4 });
|
||||
cache.Upsert(new SimpleCommodityData { Name = "Ore", Shape = Solid(1, 1), MaxStack = 100 });
|
||||
cache.FlushAsync().Wait();
|
||||
|
||||
var ledger = new ProvenanceLedger();
|
||||
for (var i = 1; i <= 4000; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
|
||||
var items = new ItemManager(cache, ledger, settings, _ => { });
|
||||
var zone = new Zone(items, planets ?? new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = zoneName, Owner = null }, null);
|
||||
return new Lab { Items = items, Zone = zone };
|
||||
}
|
||||
|
||||
// A ship carrying a gun, a targeting system and a spare, with ore and a spare gun in its crate.
|
||||
private static Ship Spawn(Lab lab, float3 at, string bay = "Crate", bool cargo = true)
|
||||
{
|
||||
var hull = lab.Items.ItemData.RefOf<ItemData>(lab.Items.ItemData.GetByName<HullData>("Skiff"));
|
||||
var ship = new Ship(lab.Items, lab.Zone, new EquippableItem { Data = hull, Durability = 1000000, Lot = lab.Lot++ }, new EntitySettings());
|
||||
Assert.True(ship.TryEquip(lab.Make("Gun"), new int2(0, 0)));
|
||||
foreach (var name in new[] { "Targeting", "Spare" })
|
||||
{
|
||||
var gear = lab.Make(name);
|
||||
Assert.True(ship.TryFindSpace(gear, out var pos));
|
||||
Assert.True(ship.TryEquip(gear, pos));
|
||||
}
|
||||
var crate = lab.Make(bay);
|
||||
Assert.True(ship.TryFindSpace(crate, out var cratePos));
|
||||
Assert.True(ship.TryEquip(crate, cratePos));
|
||||
if (cargo)
|
||||
{
|
||||
Assert.True(ship.CargoBays.Single().TryStore(lab.Ore(7)));
|
||||
Assert.True(ship.CargoBays.Single().TryStore(lab.Make("Spare")));
|
||||
}
|
||||
lab.Zone.Entities.Add(ship);
|
||||
ship.Activate();
|
||||
ship.Position = at;
|
||||
return ship;
|
||||
}
|
||||
|
||||
private static Entity Station(Lab lab, float3 at)
|
||||
{
|
||||
var hull = lab.Items.ItemData.RefOf<ItemData>(lab.Items.ItemData.GetByName<HullData>("Hub"));
|
||||
var station = new OrbitalEntity(lab.Items, lab.Zone, new EquippableItem { Data = hull, Durability = 1000000, Lot = lab.Lot++ }, default, new EntitySettings());
|
||||
var spare = lab.Make("Spare");
|
||||
Assert.True(station.TryFindSpace(spare, out var pos));
|
||||
Assert.True(station.TryEquip(spare, pos));
|
||||
var crate = lab.Make("Crate");
|
||||
Assert.True(station.TryFindSpace(crate, out var cratePos));
|
||||
Assert.True(station.TryEquip(crate, cratePos));
|
||||
Assert.True(station.CargoBays.Single().TryStore(lab.Ore(3)));
|
||||
lab.Zone.Entities.Add(station);
|
||||
station.Activate();
|
||||
station.Position = at;
|
||||
return station;
|
||||
}
|
||||
|
||||
private static void Kill(Entity e) => e.HeatstrokeDeath.OnNext(Unit.Default);
|
||||
|
||||
private static List<(string Item, float2 Position, float2 Velocity)> Released(Lab lab, int from) =>
|
||||
lab.Zone.FloatingItems.Skip(from).Select(f => (lab.Describe(f.Item), f.Body.Position, f.Body.Velocity)).ToList();
|
||||
|
||||
private static void AssertSameDrops(List<(string Item, float2 Position, float2 Velocity)> a, List<(string Item, float2 Position, float2 Velocity)> b)
|
||||
{
|
||||
Assert.Equal(a.Count, b.Count);
|
||||
for (var i = 0; i < a.Count; i++)
|
||||
{
|
||||
Assert.Equal(a[i].Item, b[i].Item);
|
||||
Assert.Equal(a[i].Position, b[i].Position);
|
||||
Assert.Equal(a[i].Velocity, b[i].Velocity);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Differ(List<(string Item, float2 Position, float2 Velocity)> a, List<(string Item, float2 Position, float2 Velocity)> b) =>
|
||||
a.Count != b.Count || a.Zip(b, (x, y) => x.Item != y.Item || !x.Position.Equals(y.Position) || !x.Velocity.Equals(y.Velocity)).Any(d => d);
|
||||
|
||||
[Fact]
|
||||
public void DeathDropsAreRolledOnTheZonesDice()
|
||||
{
|
||||
var at = float3(31, 0, -17);
|
||||
var first = Build("Dice");
|
||||
Kill(Spawn(first, at));
|
||||
var dropsA = Released(first, 0);
|
||||
Assert.True(dropsA.Count > 2, "the fixture must drop enough for a draw to show");
|
||||
Assert.All(dropsA, d => Assert.Equal(new float2(31, -17), d.Position));
|
||||
Assert.All(dropsA, d => Assert.Equal(25f, length(d.Velocity), 3));
|
||||
|
||||
// Another zone with the same name (the same galaxy seed) and the same ship drops the same things the same way,
|
||||
// whatever the item manager's own generator has been asked in between.
|
||||
var second = Build("Dice");
|
||||
for (var i = 0; i < 5; i++) second.Items.Random.NextFloat();
|
||||
var shipB = Spawn(second, at);
|
||||
for (var i = 0; i < 5; i++) second.Items.Random.NextFloat();
|
||||
Kill(shipB);
|
||||
AssertSameDrops(dropsA, Released(second, 0));
|
||||
|
||||
// Another combat seed rolls differently.
|
||||
var other = Build("Other dice");
|
||||
Kill(Spawn(other, at));
|
||||
Assert.True(Differ(dropsA, Released(other, 0)));
|
||||
|
||||
// A second death in the same zone uses the next ordinal.
|
||||
var count = first.Zone.FloatingItems.Count;
|
||||
Kill(Spawn(first, at));
|
||||
Assert.True(Differ(dropsA, Released(first, count)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GearDropsByProbabilityCargoAlways()
|
||||
{
|
||||
foreach (var probability in new[] { 0f, 1f })
|
||||
{
|
||||
var lab = Build(probability: probability);
|
||||
var ship = Spawn(lab, float3(5, 0, 5));
|
||||
var gear = ship.Equipment.Where(e => e != ship.EquippedHull).Select(e => (ItemInstance) e.EquippableItem).ToList();
|
||||
var cargo = ship.CargoBays.Single().Cargo.Keys.ToList();
|
||||
Assert.Equal(3, gear.Count);
|
||||
Assert.Equal(2, cargo.Count);
|
||||
Kill(ship);
|
||||
var dropped = lab.Zone.FloatingItems.Select(f => f.Item).ToList();
|
||||
Assert.All(cargo, c => Assert.Contains(c, dropped));
|
||||
Assert.DoesNotContain(ship.EquippedHull.EquippableItem, dropped);
|
||||
if (probability == 0f) Assert.Equal(cargo.Count, dropped.Count);
|
||||
else
|
||||
{
|
||||
Assert.Equal(cargo.Count + gear.Count, dropped.Count);
|
||||
Assert.All(gear, g => Assert.Contains(g, dropped));
|
||||
}
|
||||
}
|
||||
|
||||
// 400 deaths at .5: the drop rate of the equipped gear is .5 within three sigma (sigma = .5 / sqrt(1200)).
|
||||
var rate = Build(probability: .5f);
|
||||
var equipped = 0;
|
||||
for (var i = 0; i < 400; i++)
|
||||
{
|
||||
var ship = Spawn(rate, float3(i, 0, 0), cargo: false);
|
||||
var gear = ship.Equipment.Where(e => e != ship.EquippedHull).Select(e => (ItemInstance) e.EquippableItem).ToList();
|
||||
var before = rate.Zone.FloatingItems.Count;
|
||||
Kill(ship);
|
||||
equipped += rate.Zone.FloatingItems.Skip(before).Count(f => gear.Contains(f.Item));
|
||||
}
|
||||
Assert.InRange(equipped / (400f * 3f), .5f - 3 * .0144f, .5f + 3 * .0144f);
|
||||
|
||||
// A station's death drops through the same path.
|
||||
var hub = Build(probability: 1f);
|
||||
Kill(Station(hub, float3(9, 0, 9)));
|
||||
Assert.Equal(new[] { "Orex3", "Spare" }, hub.Zone.FloatingItems.Select(f => hub.Describe(f.Item)).OrderBy(x => x).ToArray());
|
||||
}
|
||||
|
||||
// One unaimed shot with spread, and what the commit decided.
|
||||
private static (ShotResult Result, int2 Cell, float Lateral, float2 Bearing) OneShot(Lab lab, Ship shooter)
|
||||
{
|
||||
ShotOutcome outcome = null;
|
||||
using var c = lab.Zone.ShotCommitted.Subscribe(o => outcome = o);
|
||||
var gun = shooter.Equipment.Single(e => lab.Items.GetData(e.EquippableItem).Name == "Gun");
|
||||
((InstantWeapon) gun.Behaviors.Single(b => b is Weapon)).Activate();
|
||||
lab.Zone.Update(.01f);
|
||||
Assert.NotNull(outcome);
|
||||
return (outcome.Result, outcome.Cell, outcome.Lateral, outcome.Bearing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LootDiceDoNotTouchShotDice()
|
||||
{
|
||||
(ShotResult Result, int2 Cell, float Lateral, float2 Bearing) Run(bool death)
|
||||
{
|
||||
var lab = Build("Shots", spread: 5f);
|
||||
var shooter = Spawn(lab, float3(37, 0, -11), cargo: false);
|
||||
var target = Spawn(lab, float3(37, 0, 89), cargo: false);
|
||||
var bystander = Spawn(lab, float3(900, 0, 900));
|
||||
shooter.SetTarget(target);
|
||||
shooter.EntityInfoGathered[target] = 1f;
|
||||
shooter.SetIff(target, true);
|
||||
lab.Zone.Update(0f);
|
||||
if (death) Kill(bystander);
|
||||
return OneShot(lab, shooter);
|
||||
}
|
||||
var quiet = Run(false);
|
||||
var after = Run(true);
|
||||
Assert.NotEqual(0f, quiet.Lateral);
|
||||
Assert.Equal(quiet, after);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AFloatingItemDriftsAsABodyAndExpires()
|
||||
{
|
||||
var planets = new PlanetSettings { ZoneDepth = 1000, ZoneDepthExponent = 1, GravityStrength = 1 };
|
||||
var lab = Build(lifetime: 3f, planets: planets);
|
||||
var settings = lab.Items.GameplaySettings;
|
||||
var start = new float2(40, -25);
|
||||
var launch = new float2(6, 2);
|
||||
var item = lab.Ore(1);
|
||||
var floating = lab.Zone.Release(item, start, launch);
|
||||
Assert.Same(item, floating.Item);
|
||||
Assert.Equal(lab.Zone.Time, floating.ReleasedAt);
|
||||
Assert.Equal(3f, floating.Lifetime);
|
||||
Assert.True(length(lab.Zone.GetForce(start)) > 0, "the fixture's zone must pull, or drift proves nothing");
|
||||
|
||||
var removals = 0;
|
||||
using var sub = lab.Zone.FloatingItems.ObserveRemove().Subscribe(_ => removals++);
|
||||
var alone = new KinematicBody { Position = start, Velocity = launch };
|
||||
var steps = 0;
|
||||
while (lab.Zone.FloatingItems.Count > 0)
|
||||
{
|
||||
Assert.True(steps < 20, "the item must expire");
|
||||
alone.Step(.5f, lab.Zone.GetForce(alone.Position), settings);
|
||||
lab.Zone.Update(.5f);
|
||||
steps++;
|
||||
if (lab.Zone.FloatingItems.Count > 0)
|
||||
{
|
||||
Assert.Equal(alone.Position, floating.Body.Position);
|
||||
Assert.Equal(alone.Velocity, floating.Body.Velocity);
|
||||
Assert.Equal(alone.Drift, floating.Body.Drift);
|
||||
}
|
||||
}
|
||||
// Three seconds at half-second steps: it survives five steps and goes on the sixth, not before and not after.
|
||||
Assert.Equal(6, steps);
|
||||
Assert.Equal(1, removals);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPickUpStoresAndRemoves()
|
||||
{
|
||||
var lab = Build();
|
||||
var taker = Spawn(lab, float3(0, 0, 0), bay: "Tiny", cargo: false);
|
||||
var big = lab.Make("Crate");
|
||||
Assert.True(taker.TryFindSpace(big, out var pos));
|
||||
Assert.True(taker.TryEquip(big, pos));
|
||||
Assert.Equal(2, taker.CargoBays.Count);
|
||||
|
||||
var removals = 0;
|
||||
using var sub = lab.Zone.FloatingItems.ObserveRemove().Subscribe(_ => removals++);
|
||||
var ore = lab.Ore(5);
|
||||
var floating = lab.Zone.Release(ore, new float2(1, 2), new float2(3, 4));
|
||||
|
||||
// The one-cell bay is full, so the first bay that accepts the ore is the second.
|
||||
Assert.True(taker.CargoBays[0].TryStore(lab.Make("Spare")));
|
||||
Assert.True(lab.Zone.TryPickUp(taker, floating.Id));
|
||||
Assert.Equal(1, removals);
|
||||
Assert.Empty(lab.Zone.FloatingItems);
|
||||
Assert.Contains(ore, taker.CargoBays[1].Cargo.Keys);
|
||||
Assert.DoesNotContain(ore, taker.CargoBays[0].Cargo.Keys);
|
||||
|
||||
// The same id again is unknown now.
|
||||
Assert.False(lab.Zone.TryPickUp(taker, floating.Id));
|
||||
|
||||
// Every bay full: false, and the item floats on with its body unchanged.
|
||||
while (taker.CargoBays[1].TryStore(lab.Make("Spare"))) { }
|
||||
var stuck = lab.Zone.Release(lab.Make("Spare"), new float2(7, 8), new float2(1, 1));
|
||||
var body = stuck.Body;
|
||||
Assert.False(lab.Zone.TryPickUp(taker, stuck.Id));
|
||||
Assert.Same(stuck, lab.Zone.FloatingItems.Single());
|
||||
Assert.Equal(body.Position, stuck.Body.Position);
|
||||
Assert.Equal(body.Velocity, stuck.Body.Velocity);
|
||||
Assert.Equal(1, removals);
|
||||
|
||||
Assert.False(lab.Zone.TryPickUp(taker, new FloatingBodyId(9999)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropsTakeFreshBodyIds()
|
||||
{
|
||||
var lab = Build(probability: 1f);
|
||||
Kill(Spawn(lab, float3(0, 0, 0)));
|
||||
var before = lab.Zone.FloatingItems.Select(f => f.Id.Value).ToList();
|
||||
var mine = new Mine { Id = lab.Zone.NextBodyId(), Body = new KinematicBody { Position = new float2(50, 50) }, Lifetime = 1000, FuseDelay = 1000, ArmingDelay = 1000 };
|
||||
lab.Zone.Lay(mine);
|
||||
Kill(Spawn(lab, float3(0, 0, 0)));
|
||||
var all = lab.Zone.FloatingItems.Select(f => f.Id.Value).ToList();
|
||||
var after = all.Skip(before.Count).ToList();
|
||||
Assert.NotEmpty(before);
|
||||
Assert.NotEmpty(after);
|
||||
Assert.Equal(Enumerable.Range(1, all.Count + 1).Select(i => (uint) i), before.Concat(new[] { mine.Id.Value }).Concat(after));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FloatingItemsAreNotSaved()
|
||||
{
|
||||
var lab = Build(probability: 1f);
|
||||
Kill(Spawn(lab, float3(0, 0, 0)));
|
||||
Assert.NotEmpty(lab.Zone.FloatingItems);
|
||||
var pack = lab.Zone.PackZone();
|
||||
var restored = new Zone(lab.Items, new PlanetSettings(), pack, new GalaxyZone { Name = "Loot", Owner = null }, null);
|
||||
Assert.Empty(restored.FloatingItems);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue