Merge cut orbital-layering (closing verdicts cut-orbital-layering.s1, cut-orbital-layering.s2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
949fa005d6
27 changed files with 291 additions and 64 deletions
|
|
@ -146,6 +146,5 @@ MonoBehaviour:
|
|||
ActiveEmission: 100
|
||||
ArmedEmission: 1000
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
HitEffect: {fileID: 1121192042590650, guid: e1b08032bec16ba4eb1ae23cbd572664, type: 3}
|
||||
HitEffectScale: 10
|
||||
|
|
|
|||
|
|
@ -189,7 +189,6 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
LabelFadeDuration: 0.5
|
||||
LabelPersistDuration: 3
|
||||
LabelDisplayAngle: 15
|
||||
|
|
|
|||
|
|
@ -162,7 +162,6 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
LabelFadeDuration: 0.5
|
||||
LabelPersistDuration: 3
|
||||
LabelDisplayAngle: 15
|
||||
|
|
|
|||
|
|
@ -162,7 +162,6 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
LabelFadeDuration: 0.5
|
||||
LabelPersistDuration: 3
|
||||
LabelDisplayAngle: 15
|
||||
|
|
|
|||
|
|
@ -162,7 +162,6 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
LabelFadeDuration: 0.5
|
||||
LabelPersistDuration: 3
|
||||
LabelDisplayAngle: 15
|
||||
|
|
|
|||
|
|
@ -162,7 +162,6 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
LabelFadeDuration: 0.5
|
||||
LabelPersistDuration: 3
|
||||
LabelDisplayAngle: 15
|
||||
|
|
|
|||
BIN
Assets/Resources/Settings.asset
(Stored with Git LFS)
BIN
Assets/Resources/Settings.asset
(Stored with Git LFS)
Binary file not shown.
|
|
@ -100,6 +100,7 @@ namespace Aetheria.EditorTools
|
|||
{
|
||||
var settings = AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Resources/Settings.asset")?.GameplaySettings
|
||||
?? throw new InvalidOperationException("No Settings.asset.");
|
||||
settings.FloaterLayerHeight = -1.5f; // nonzero, so a presenter that drops the floater layer fails
|
||||
|
||||
var wired = SessionState.GetString(Wiring, "").Split('|').Select(entry => entry.Split('=')).ToDictionary(pair => pair[0], pair => pair[1]);
|
||||
var pickupPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(wired["SimpleCommodityPickup"])?.GetComponent<ItemPickup>();
|
||||
|
|
@ -178,6 +179,8 @@ namespace Aetheria.EditorTools
|
|||
want = mine.Body.Position + mineVelocity * lead;
|
||||
Require(Mathf.Abs(seen.x - want.x) < Tolerance && Mathf.Abs(seen.z - want.y) < Tolerance,
|
||||
$"MineInstance at lead {lead}: drawn x/z {seen.x},{seen.z}, the step plus velocity times lead {want.x},{want.y}.");
|
||||
Require(Mathf.Abs(seen.y - DrawAhead.FloaterHeight(zone, settings, new float2(want.x, want.y), lead)) < Tolerance,
|
||||
$"MineInstance y {seen.y} is not on the floater layer.");
|
||||
|
||||
// The round's line is a function of the drawn time: it moves RoundSpeed * lead along its direction between leads.
|
||||
seen = projectile.transform.position;
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ public static class FloatingItemsDrawnSmoke
|
|||
{
|
||||
var settings = AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Resources/Settings.asset")?.GameplaySettings
|
||||
?? throw new InvalidOperationException("No Settings.asset.");
|
||||
// A floater rides its own layer: a nonzero one (never saved back to the asset), so a presenter that drops it fails.
|
||||
settings.FloaterLayerHeight = -1.5f;
|
||||
|
||||
// The wiring: the scene's ZoneRenderer names an ItemPickup prefab in each of the four fields, and none of those
|
||||
// prefabs keeps a script that no longer exists.
|
||||
|
|
@ -159,7 +161,7 @@ public static class FloatingItemsDrawnSmoke
|
|||
{
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
var at = DrawAhead.Position(floating.Body, lead);
|
||||
var want = new Vector3(at.x, DrawAhead.Height(zone, at, lead), at.y);
|
||||
var want = new Vector3(at.x, DrawAhead.FloaterHeight(zone, settings, at, lead), at.y);
|
||||
Require(Vector3.Distance(pickup.transform.position, want) < 1e-3f,
|
||||
$"{what}: the pickup is at {pickup.transform.position}, the body at {want}.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ using Random = UnityEngine.Random;
|
|||
public class ItemPickup : MonoBehaviour
|
||||
{
|
||||
public float RotationSpeed = 1f;
|
||||
public float GridOffset;
|
||||
public float LabelFadeDuration = .5f;
|
||||
public float LabelPersistDuration = 3;
|
||||
public float LabelDisplayAngle = 15;
|
||||
|
|
@ -27,11 +26,13 @@ public class ItemPickup : MonoBehaviour
|
|||
private float _displayTime;
|
||||
private float _timeOffset;
|
||||
private Zone _zone;
|
||||
private GameplaySettings _settings;
|
||||
private FloatingItem _floating;
|
||||
|
||||
public void Bind(Zone zone, FloatingItem floating)
|
||||
public void Bind(Zone zone, FloatingItem floating, GameplaySettings settings)
|
||||
{
|
||||
_zone = zone;
|
||||
_settings = settings;
|
||||
_floating = floating;
|
||||
Item = floating.Item;
|
||||
_timeOffset = Random.value * 100;
|
||||
|
|
@ -42,7 +43,7 @@ public class ItemPickup : MonoBehaviour
|
|||
{
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
var p = DrawAhead.Position(_floating.Body, lead);
|
||||
transform.position = new Vector3(p.x, DrawAhead.Height(_zone, p, lead) + GridOffset, p.y);
|
||||
transform.position = new Vector3(p.x, DrawAhead.FloaterHeight(_zone, _settings, p, lead), p.y);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
|
|
|
|||
|
|
@ -17,21 +17,22 @@ public class MineInstance : MonoBehaviour
|
|||
public float ActiveEmission = 100f;
|
||||
public float ArmedEmission = 1000f;
|
||||
public float RotationSpeed = 1f;
|
||||
public float GridOffset;
|
||||
// The blast's particles; the effect prefab destroys itself when its particle system stops.
|
||||
public GameObject HitEffect;
|
||||
public float HitEffectScale = 10f;
|
||||
|
||||
private Zone _zone;
|
||||
private GameplaySettings _settings;
|
||||
private Mine _mine;
|
||||
private Material _material;
|
||||
private float _timeOffset;
|
||||
private float _pulse;
|
||||
private float _emission;
|
||||
|
||||
public void Bind(Zone zone, Mine mine)
|
||||
public void Bind(Zone zone, Mine mine, GameplaySettings settings)
|
||||
{
|
||||
_zone = zone;
|
||||
_settings = settings;
|
||||
_mine = mine;
|
||||
_material = MeshRenderer.materials[EmissionSubmesh];
|
||||
_timeOffset = Random.value * 100;
|
||||
|
|
@ -57,7 +58,7 @@ public class MineInstance : MonoBehaviour
|
|||
{
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
var p = DrawAhead.Position(_mine.Body, lead);
|
||||
transform.position = new Vector3(p.x, DrawAhead.Height(_zone, p, lead) + GridOffset, p.y);
|
||||
transform.position = new Vector3(p.x, DrawAhead.FloaterHeight(_zone, _settings, p, lead), p.y);
|
||||
}
|
||||
|
||||
// The mine left Zone.Mines, which only its blast does: show it where it was drawn last.
|
||||
|
|
|
|||
|
|
@ -167,5 +167,5 @@ public class Pickup : Behavior, IAlwaysUpdatedBehavior
|
|||
}
|
||||
|
||||
private void Publish(GrabPhase phase, FloatingItem item, float duration) =>
|
||||
Entity.Capabilities.PublishGrab(new GrabEvent(item.Id, phase, float3(item.Body.Position.x, Entity.Position.y, item.Body.Position.y), duration));
|
||||
Entity.Capabilities.PublishGrab(new GrabEvent(item.Id, phase, float3(item.Body.Position.x, DrawAhead.FloaterHeight(Entity.Zone, Entity.ItemManager.GameplaySettings, item.Body.Position, 0), item.Body.Position.y), duration));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ using CultMath;
|
|||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
|
||||
// A hull touching a hull (aetheria-release cut sim-contact; ruling unity-presents-only-r3): the simulation owns
|
||||
// contact between entities. Each hull is a disc about its centre of mass that bounds every occupied cell, and
|
||||
// A ship touching a ship (aetheria-release cut sim-contact; ruling unity-presents-only-r3): the simulation owns
|
||||
// contact between ships. Orbital entities (stations, turrets) sit on the orbital layer above the ship plane and floaters
|
||||
// below it, so neither touches a ship (ruling aetheria-gravity:orbitals-above-floaters-below). Each hull is a disc about its centre of mass that bounds every occupied cell, and
|
||||
// two discs that touch while closing along the line between their centres take the normal impulse
|
||||
// j = -(1 + e)(v_rel . n) / (1/m1 + 1/m2) (ruling ship-bounce-restitution-setting): momentum is conserved, the
|
||||
// result is frame-independent, and a pair that has separated is not bounced again. e is
|
||||
|
|
@ -35,7 +36,7 @@ public static class Contacts
|
|||
{
|
||||
public static float Radius(Entity e) => e.HullData.Reach(e.ItemManager.GameplaySettings.SchematicCellSize);
|
||||
|
||||
// For each unordered pair of the zone's entities, in list order: if their discs touch and are closing along
|
||||
// For each unordered pair of the zone's ships, in list order: if their discs touch and are closing along
|
||||
// the normal, the impulse moves each one's velocity along the normal only (the tangent components are
|
||||
// untouched), and the contact is published. Pairs resolve one after another in list order, each from the
|
||||
// velocities the earlier pairs left, so a body touching two others in one tick depends on that order; the
|
||||
|
|
@ -43,7 +44,7 @@ public static class Contacts
|
|||
// are not hulls in the zone. No positional push-out.
|
||||
public static void Step(Zone zone)
|
||||
{
|
||||
var entities = zone.Entities.Where(e => e.Active && e.Parent == null).ToArray();
|
||||
var entities = zone.Entities.OfType<Ship>().Where(e => e.Active && e.Parent == null).ToArray();
|
||||
var radii = entities.Select(Radius).ToArray();
|
||||
for (var i = 0; i < entities.Length; i++)
|
||||
for (var j = i + 1; j < entities.Length; j++)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ public static class DrawAhead
|
|||
// The field's height at a point at the drawn time, its wells where their rails put them at T.
|
||||
public static float Height(Zone zone, float2 planar, float lead) => zone.GetHeight(planar, Time(zone, lead));
|
||||
|
||||
// The drawn height of a floater (loot, mines): the field at the drawn time plus the floater layer.
|
||||
public static float FloaterHeight(Zone zone, GameplaySettings settings, float2 planar, float lead) => Height(zone, planar, lead) + settings.FloaterLayerHeight;
|
||||
|
||||
// A ship in a wormhole animation has its pose scripted by the step, not integrated from Velocity and TurnRate,
|
||||
// so it is drawn where the step put it: extrapolating a scripted pose draws a streak that resets every step.
|
||||
// A child is drawn where its parent is drawn, as the step copies the parent's pose onto it.
|
||||
|
|
|
|||
|
|
@ -1974,9 +1974,13 @@ public abstract class Entity
|
|||
Manoeuvre.Observe(Acceleration, delta, ItemManager.GameplaySettings.SolutionWindow);
|
||||
}
|
||||
|
||||
// Where an unparented entity sits on the field at a planar point: the zone's height there plus the hull's grid offset.
|
||||
// The step rides at its own time and xz, the presenter at the drawn time and its drawn xz (DrawAhead).
|
||||
public float RideHeight(float2 planar, double time) => Zone.GetHeight(planar, time) + (ItemManager.GetData(Hull) as HullData).GridOffset;
|
||||
// The height above the field of the layer this kind of entity rides on (GameplaySettings): ships the ship plane,
|
||||
// orbital entities the orbital layer above it.
|
||||
public abstract float LayerHeight { get; }
|
||||
|
||||
// Where an unparented entity sits on the field at a planar point: the zone's height there plus the layer this kind of
|
||||
// entity rides on. The step rides at its own time and xz, the presenter at the drawn time and its drawn xz (DrawAhead).
|
||||
public float RideHeight(float2 planar, double time) => Zone.GetHeight(planar, time) + LayerHeight;
|
||||
|
||||
public float RideHeight(float2 planar) => RideHeight(planar, Zone.ExactTime);
|
||||
|
||||
|
|
|
|||
|
|
@ -519,8 +519,7 @@ public class HullData : EquippableItemData
|
|||
[Inspectable, JsonProperty("hullType"), Key(25)]
|
||||
public HullType HullType;
|
||||
|
||||
[Inspectable, JsonProperty("gridOffset"), Key(26)]
|
||||
public float GridOffset;
|
||||
// Key(26) retired 2026-10-16 (per-hull GridOffset; the layer is the kind's, GameplaySettings). Do not reuse.
|
||||
|
||||
[Inspectable, JsonProperty("armor"), Key(27)]
|
||||
public float Armor;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ public class OrbitalEntity : Entity
|
|||
OrbitData = orbit;
|
||||
}
|
||||
|
||||
public override float LayerHeight => ItemManager.GameplaySettings.OrbitalLayerHeight;
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
if (OrbitData.IsSet())
|
||||
|
|
|
|||
|
|
@ -278,6 +278,13 @@ public class GameplaySettings
|
|||
// normal is this times the speed at which it closed. 0 is perfectly inelastic, 1 perfectly elastic.
|
||||
public float ContactRestitution = .5f;
|
||||
|
||||
// Heights above the field of the ship plane, the orbital layer (stations, turrets: every OrbitalEntity) and the floater
|
||||
// layer (loot, mines): orbitals above ships and floaters below them, so no ship ever touches either (ruling
|
||||
// aetheria-gravity:orbitals-above-floaters-below).
|
||||
public float ShipLayerHeight = 3f;
|
||||
public float OrbitalLayerHeight = 15f;
|
||||
public float FloaterLayerHeight = 0f;
|
||||
|
||||
// The floor an AI's own predicted hit probability (FireControl.HitProbability) must clear before it counts
|
||||
// a firing solution worth taking (Combat.cs, TurretController.cs). Player fire is gated on arc alone
|
||||
// (Weapon.ArcAllowsFire, Q2) -- this threshold is an AI fire-discipline heuristic, not part of the roll.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ using quaternion = CultMath.quaternion;
|
|||
[MessagePackObject]
|
||||
public class Ship : Entity
|
||||
{
|
||||
public override float LayerHeight => ItemManager.GameplaySettings.ShipLayerHeight;
|
||||
|
||||
|
||||
// [Key("bindings")] public Dictionary<KeyCode,Guid> Bindings = new Dictionary<KeyCode,Guid>();
|
||||
//[IgnoreMember] public int HullHardpointCount;
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ public class ZoneRenderer : MonoBehaviour
|
|||
private void LoadMine(Zone zone, Mine mine)
|
||||
{
|
||||
var instance = Instantiate(MinePrefab);
|
||||
instance.Bind(zone, mine);
|
||||
instance.Bind(zone, mine, ItemManager.GameplaySettings);
|
||||
_mines.Add(mine, instance);
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +293,7 @@ public class ZoneRenderer : MonoBehaviour
|
|||
_ => throw new NotImplementedException()
|
||||
};
|
||||
var pickup = Instantiate(prefab, ZoneRoot);
|
||||
pickup.Bind(zone, floating);
|
||||
pickup.Bind(zone, floating, ItemManager.GameplaySettings);
|
||||
pickup.ScanLabel.text = ItemManager.Title(floating.Item);
|
||||
if (floating.Item is CraftedItemInstance craftedItemInstance)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ using int2 = CultMath.int2;
|
|||
|
||||
// Hull contact in the sim (aetheria-release cut sim-contact; rulings unity-presents-only-r3 and
|
||||
// ship-bounce-restitution-setting): Zone.Update steps Contacts, which applies the restitution impulse along the
|
||||
// normal of two touching, closing hull discs and publishes the contact. Every test
|
||||
// normal of two touching, closing ship discs and publishes the contact. Contact is ship on ship: orbital entities
|
||||
// sit above the ship plane and take no part in it (ruling aetheria-gravity:orbitals-above-floaters-below). Every test
|
||||
// drives Zone.Update, the path the game takes. SchematicCellSize is 2, so a solid 5x5 hull has a radius of
|
||||
// 2.5 * sqrt(2) * 2 = 7.07 and two such hulls ten apart touch.
|
||||
public sealed class ContactTests : IDisposable
|
||||
|
|
@ -70,6 +71,17 @@ public sealed class ContactTests : IDisposable
|
|||
return ship;
|
||||
}
|
||||
|
||||
// A station or turret: an orbital entity with no orbit, so it stays where it is put.
|
||||
public OrbitalEntity AddOrbital(string hull, float3 at, float2 velocity)
|
||||
{
|
||||
var hullRef = Items.ItemData.RefOf<ItemData>(Items.ItemData.GetByName<HullData>(hull));
|
||||
var orbital = new OrbitalEntity(Items, Zone, new EquippableItem { Data = hullRef, Durability = 1000000, Lot = Lot++ }, default, new EntitySettings());
|
||||
Zone.Admit(orbital, piloted: false);
|
||||
orbital.Position = at;
|
||||
orbital.Velocity = velocity;
|
||||
return orbital;
|
||||
}
|
||||
|
||||
public void Step() => Zone.Update(.01f);
|
||||
}
|
||||
|
||||
|
|
@ -86,9 +98,9 @@ public sealed class ContactTests : IDisposable
|
|||
var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true);
|
||||
_caches.Add(cache);
|
||||
cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
HullData Hull(string name, Shape shape, float mass) => new HullData
|
||||
HullData Hull(string name, Shape shape, float mass, HullType type = HullType.Ship) => new HullData
|
||||
{
|
||||
Name = name, HullType = HullType.Ship, Shape = shape, Durability = 1000000, Mass = mass, Armor = 0,
|
||||
Name = name, HullType = type, Shape = shape, Durability = 1000000, Mass = mass, Armor = 0,
|
||||
Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() } }
|
||||
};
|
||||
cache.Upsert(Hull("Solid", Solid(5, 5), 1000));
|
||||
|
|
@ -97,6 +109,9 @@ public sealed class ContactTests : IDisposable
|
|||
cache.Upsert(Hull("Mid", Solid(5, 5), 3000));
|
||||
cache.Upsert(Hull("Ell", Ell(), 1000));
|
||||
cache.Upsert(Hull("Longinus", ShippedLonginus(), 1000));
|
||||
// The orbital hulls carry their real types, so a filter by runtime kind and one by hull type are told apart.
|
||||
cache.Upsert(Hull("Station", Solid(5, 5), 1000000, HullType.Station));
|
||||
cache.Upsert(Hull("Turret", Solid(5, 5), 1000, HullType.Turret));
|
||||
cache.FlushAsync().Wait();
|
||||
|
||||
var ledger = new ProvenanceLedger();
|
||||
|
|
@ -339,21 +354,21 @@ public sealed class ContactTests : IDisposable
|
|||
Assert.Empty(lab.Events);
|
||||
}
|
||||
|
||||
// A ship against a 1000x heavier body: the heavy body takes (1 + e) m / (m + M) of the ship's closing speed
|
||||
// A ship against a 1000x heavier ship: the heavy ship takes (1 + e) m / (m + M) of the ship's closing speed
|
||||
// and the ship rebounds with the rest of its momentum.
|
||||
[Fact]
|
||||
public void AShipAgainstAStationRebounds()
|
||||
public void AShipAgainstAHeavyHullRebounds()
|
||||
{
|
||||
var lab = Build(.5f);
|
||||
var ship = lab.Add("Solid", float3(0, 0, 0), float2(10, 0));
|
||||
var station = lab.Add("Heavy", float3(10, 0, 0), float2(0, 0));
|
||||
var heavy = lab.Add("Heavy", float3(10, 0, 0), float2(0, 0));
|
||||
Assert.Equal(1000f, ship.Mass);
|
||||
Assert.Equal(1000000f, station.Mass);
|
||||
Assert.Equal(1000000f, heavy.Mass);
|
||||
lab.Step();
|
||||
|
||||
var n = NormalOf(ship, station);
|
||||
var n = NormalOf(ship, heavy);
|
||||
var closing = 10f * n.x;
|
||||
Assert.Equal(1.5f * closing * 1000f / 1001000f, dot(n, station.Velocity), 4);
|
||||
Assert.Equal(1.5f * closing * 1000f / 1001000f, dot(n, heavy.Velocity), 4);
|
||||
Assert.Equal(closing - 1.5f * closing * 1000000f / 1001000f, dot(n, ship.Velocity), 3);
|
||||
}
|
||||
|
||||
|
|
@ -371,6 +386,41 @@ public sealed class ContactTests : IDisposable
|
|||
Assert.Equal(-10f, b.Velocity.x, 4);
|
||||
}
|
||||
|
||||
// Orbital entities live above the ship plane (ruling orbitals-above-floaters-below): a ship closing on a station
|
||||
// and on a turret flies through both, and nothing is published.
|
||||
[Fact]
|
||||
public void AShipFliesThroughAStationAndATurret()
|
||||
{
|
||||
var lab = Build();
|
||||
var ship = lab.Add("Solid", float3(0, 0, 0), float2(10, 0));
|
||||
var station = lab.AddOrbital("Station", float3(10, 0, 0), float2(0, 0));
|
||||
// Behind the ship and faster than it, so it closes on the ship as the station does.
|
||||
var turret = lab.AddOrbital("Turret", float3(-10, 0, 0), float2(30, 0));
|
||||
lab.Step();
|
||||
Assert.Empty(lab.Events);
|
||||
Assert.Equal(float2(10, 0), ship.Velocity);
|
||||
Assert.Equal(float2(0, 0), station.Velocity);
|
||||
Assert.Equal(float2(30, 0), turret.Velocity);
|
||||
}
|
||||
|
||||
// An orbital overlapping a colliding pair changes nothing about the pair (ruling ship-bounce-restitution-setting):
|
||||
// one contact, between the two ships, with the restitution split of the pair alone.
|
||||
[Fact]
|
||||
public void TwoShipsStillBounceBesideAnOrbital()
|
||||
{
|
||||
var lab = Build(.5f);
|
||||
var a = lab.Add("Solid", float3(0, 0, 0), float2(10, 0));
|
||||
var b = lab.Add("Solid", float3(10, 0, 0), float2(-10, 0));
|
||||
lab.AddOrbital("Station", float3(5, 0, 0), float2(0, 0));
|
||||
lab.Step();
|
||||
|
||||
var contact = Assert.Single(lab.Events);
|
||||
Assert.Contains(a, new[] { contact.A, contact.B });
|
||||
Assert.Contains(b, new[] { contact.A, contact.B });
|
||||
var n = NormalOf(a, b);
|
||||
Assert.Equal(.5f * 20f * n.x, dot(n, b.Velocity - a.Velocity), 3);
|
||||
}
|
||||
|
||||
// Pairs resolve one after another in list order, so a body touching two others in one tick can leave the pile
|
||||
// differently when the list is reversed (order independence is not claimed); what the order does not change is
|
||||
// the momentum of the pile and the two contacts it publishes.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ public sealed class DrawnPoseTests : IDisposable
|
|||
private const float PlanetDistance = 1000f, PlanetPhase = .3f;
|
||||
private const float MoonDistance = 60f, MoonPhase = .1f;
|
||||
private const float StationDistance = 3f, StationPhase = .4f;
|
||||
private const float GridOffset = 2.5f;
|
||||
private const float ShipLayerHeight = 2.5f;
|
||||
private const float OrbitalLayerHeight = 7.5f;
|
||||
// The sun's rail is parentless and off-centre, the shape ZoneGenerator gives a cluster root (FixedPosition = cluster centre).
|
||||
private static readonly float2 SunCentre = new float2(700f, -300f);
|
||||
|
||||
|
|
@ -73,7 +74,7 @@ public sealed class DrawnPoseTests : IDisposable
|
|||
using (var build = AetheriaStores.Open(catalog, catalogWritable: true))
|
||||
{
|
||||
build.Upsert(new VerseGrammar { Revision = 1 });
|
||||
build.Upsert(new HullData { Name = "TestHull", HullType = HullType.Ship, Shape = shape, Durability = 1, Mass = 1000, GridOffset = GridOffset });
|
||||
build.Upsert(new HullData { Name = "TestHull", HullType = HullType.Ship, Shape = shape, Durability = 1, Mass = 1000 });
|
||||
build.FlushAsync().Wait();
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +86,8 @@ public sealed class DrawnPoseTests : IDisposable
|
|||
{
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp()
|
||||
QualityPriceModifier = new ExponentialLerp(),
|
||||
ShipLayerHeight = ShipLayerHeight, OrbitalLayerHeight = OrbitalLayerHeight
|
||||
}, _ => { });
|
||||
|
||||
CultRecordKey Orbit(CultRecordKey parent, float distance, float phase, float2 fixedPosition = default)
|
||||
|
|
@ -270,9 +272,9 @@ public sealed class DrawnPoseTests : IDisposable
|
|||
// A ship that stands still 40 u from the planet rides the field of the drawn well, plus its hull's offset.
|
||||
var ship = new Ship(w.Items, w.Zone, w.Hull, new EntitySettings()) { Position = float3(PlanetAt(w.Zone.ExactTime).x + 40f, 0, PlanetAt(w.Zone.ExactTime).y) };
|
||||
var drawnShip = DrawAhead.Position(ship, lead);
|
||||
Assert.Equal(FieldAt(w, drawnShip.xz, time) + GridOffset, drawnShip.y, 3);
|
||||
Assert.Equal(FieldAt(w, drawnShip.xz, time) + ShipLayerHeight, drawnShip.y, 3);
|
||||
Assert.Equal(ship.RideHeight(drawnShip.xz, time), drawnShip.y, 4);
|
||||
Assert.Equal(w.Zone.GetHeight(ship.Position.xz) + GridOffset, ship.RideHeight(ship.Position.xz), 4);
|
||||
Assert.Equal(w.Zone.GetHeight(ship.Position.xz) + ShipLayerHeight, ship.RideHeight(ship.Position.xz), 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -292,7 +294,7 @@ public sealed class DrawnPoseTests : IDisposable
|
|||
AssertNear(StationAt(w.Zone.ExactTime + lead), drawn.xz, 2e-3f, "the station on its rail at the drawn time");
|
||||
AssertNear(DrawAhead.Orbit(w.Zone, w.StationOrbit, lead), drawn.xz, 1e-6f, "the same point DrawAhead.Orbit gives");
|
||||
Assert.True(length(station.Position.xz + station.Velocity * lead - drawn.xz) > .01f, "a tight rail curves away from its straight extrapolation");
|
||||
Assert.Equal(w.Zone.GetHeight(drawn.xz, w.Zone.ExactTime + lead) + GridOffset, drawn.y, 4);
|
||||
Assert.Equal(w.Zone.GetHeight(drawn.xz, w.Zone.ExactTime + lead) + OrbitalLayerHeight, drawn.y, 4);
|
||||
}
|
||||
|
||||
// The generators make orbital entities with no orbit set (Loadout, LoadoutGenerator): a rail-less one is an ordinary
|
||||
|
|
|
|||
|
|
@ -35,8 +35,9 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private static GameplaySettings TestSettings(float minHit) => new GameplaySettings
|
||||
private static GameplaySettings TestSettings(float minHit, float orbitalLayerHeight) => new GameplaySettings
|
||||
{
|
||||
OrbitalLayerHeight = orbitalLayerHeight,
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp(),
|
||||
|
|
@ -71,7 +72,7 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
{
|
||||
public Zone Zone;
|
||||
public Ship Shooter;
|
||||
public Ship Target;
|
||||
public Entity Target;
|
||||
public List<Gun> Guns = new List<Gun>();
|
||||
public Gun Gun => Guns[0];
|
||||
public Gun Decoy;
|
||||
|
|
@ -84,12 +85,12 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
(ItemRotation Rotation, float Arc)[] mounts = null, bool beam = false, WeaponFuse? fuse = null, float blast = 0f,
|
||||
float range = 100f, float targetRange = 60f, float accuracy = 1f, float minHit = .2f, bool lockWeapon = false,
|
||||
bool turret = false, float energy = 0f, float heat = 0f, float visibility = 0f, int magazine = 0, bool singleAmmoBurst = false, bool decoy = false, bool charged = false, bool auto = false,
|
||||
float effective = float.PositiveInfinity, float damageAtMax = 0f, float velocity = 0f, float targetGridOffset = 0f)
|
||||
float effective = float.PositiveInfinity, float damageAtMax = 0f, float velocity = 0f, float targetLayerHeight = 0f)
|
||||
{
|
||||
mounts ??= new[] { (ItemRotation.None, 0f) };
|
||||
var hullData = new HullData
|
||||
{
|
||||
Name = "Hull", HullType = HullType.Ship, Shape = SolidShape(5, 4), Durability = 1000000, Mass = 1000, Armor = 0, GridOffset = targetGridOffset,
|
||||
Name = "Hull", HullType = HullType.Ship, Shape = SolidShape(5, 4), Durability = 1000000, Mass = 1000, Armor = 0,
|
||||
Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() } }
|
||||
};
|
||||
var shooterHullData = new HullData
|
||||
|
|
@ -192,7 +193,7 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
|
||||
var ledger = new ProvenanceLedger();
|
||||
for (var i = 1; i <= 100; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
|
||||
var items = new ItemManager(cache, ledger, TestSettings(minHit), _ => { });
|
||||
var items = new ItemManager(cache, ledger, TestSettings(minHit, targetLayerHeight), _ => { });
|
||||
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "PerWeapon", Owner = null }, null);
|
||||
|
||||
EquippableItem Make(string name, int lot, float durability) => new EquippableItem
|
||||
|
|
@ -235,7 +236,12 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
Assert.True(shooter.TryEquip(t, upos));
|
||||
}
|
||||
|
||||
var target = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000000, Lot = lot++ }, new EntitySettings());
|
||||
// A target layer height rides the target on the orbital layer at that height (the shooter is a ship, on the ship layer),
|
||||
// so the two sit at different heights.
|
||||
var targetItem = new EquippableItem { Data = hullRef, Durability = 1000000, Lot = lot++ };
|
||||
Entity target = targetLayerHeight != 0f
|
||||
? new OrbitalEntity(items, zone, targetItem, default, new EntitySettings())
|
||||
: new Ship(items, zone, targetItem, new EntitySettings());
|
||||
Assert.True(target.TryEquip(Make("Gun", lot++, 1), new int2(0, 0)));
|
||||
|
||||
zone.Admit(shooter, piloted: false);
|
||||
|
|
@ -308,13 +314,14 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
Assert.True(r.Gun.Weapon.Firing, "the turret must fire a fused weapon at a designated target out of arc");
|
||||
}
|
||||
|
||||
// The AI aims at the height a target rides at (Entity.RideHeight: field plus the hull's GridOffset), not at the
|
||||
// plane. A target hull raised 40 u, a shot fast enough to be predicted: the turret's and Combat's aim point at the
|
||||
// target's stepped pose. Kills: either aim site spelling its own offset (dropped, added to, or scaled).
|
||||
// The AI aims at the height a target rides at (Entity.RideHeight: field plus the layer the target's kind rides on),
|
||||
// not at the plane and not at the shooter's own layer. A target on a layer 40 u up against a shooter on the ship
|
||||
// layer, a shot fast enough to be predicted: the turret's and Combat's aim point at the target's stepped pose.
|
||||
// Kills: either aim site spelling its own offset (dropped, added to, scaled, or the shooter's layer).
|
||||
[Fact]
|
||||
public void ATurretAimsAtTheHeightATargetRidesAt()
|
||||
{
|
||||
var r = Build(turret: true, velocity: 500f, targetGridOffset: 40f);
|
||||
var r = Build(turret: true, velocity: 500f, targetLayerHeight: 40f);
|
||||
r.Zone.Update(1f);
|
||||
|
||||
AssertAimsAtTarget(r);
|
||||
|
|
@ -323,7 +330,7 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
[Fact]
|
||||
public void CombatAimsAtTheHeightATargetRidesAt()
|
||||
{
|
||||
var r = Build(velocity: 500f, targetGridOffset: 40f);
|
||||
var r = Build(velocity: 500f, targetLayerHeight: 40f);
|
||||
r.Zone.Agents.Add(new Minion(r.Shooter));
|
||||
r.Zone.Update(1f);
|
||||
r.Zone.Update(1f);
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ public sealed class FireControlTests : IDisposable
|
|||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private static GameplaySettings TestSettings() => new GameplaySettings
|
||||
private static GameplaySettings TestSettings(float shipLayerHeight = 3f) => new GameplaySettings
|
||||
{
|
||||
ShipLayerHeight = shipLayerHeight,
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp(),
|
||||
|
|
@ -45,13 +46,13 @@ public sealed class FireControlTests : IDisposable
|
|||
|
||||
// `well` digs the zone one well at the origin (WellFixture), so heights differ across the zone.
|
||||
private (ItemManager items, Ship ship, EquippedItem[] weapons) BuildShipWithHardpoints(
|
||||
bool well, (ItemRotation rotation, float firingArc)[] hardpoints, float gridOffset = 0f)
|
||||
bool well, (ItemRotation rotation, float firingArc)[] hardpoints, float shipLayerHeight = 0f)
|
||||
{
|
||||
var hullShape = new Shape(Math.Max(hardpoints.Length, 3), 3);
|
||||
foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true;
|
||||
// Mass defaults to 0 (ItemData.cs:285), which zeroes ThermalMass (Entity.cs MapEntity) and NaNs
|
||||
// UpdateTemperature -- author a real mass so the entity's own temperature stays a real number.
|
||||
var hullData = new HullData { Name = "TestHull", HullType = HullType.Ship, Shape = hullShape, Durability = 1, Mass = 1000, GridOffset = gridOffset };
|
||||
var hullData = new HullData { Name = "TestHull", HullType = HullType.Ship, Shape = hullShape, Durability = 1, Mass = 1000 };
|
||||
for (var i = 0; i < hardpoints.Length; i++)
|
||||
hullData.Hardpoints.Add(new HardpointData
|
||||
{
|
||||
|
|
@ -87,7 +88,7 @@ public sealed class FireControlTests : IDisposable
|
|||
ledger.Lots[1] = new Lot { Origin = new Attributed(), Quality = 1 }; // hull
|
||||
for (var i = 0; i < hardpoints.Length; i++)
|
||||
ledger.Lots[2 + i] = new Lot { Origin = new Attributed(), Quality = 1 }; // guns
|
||||
var items = new ItemManager(_openCache, ledger, TestSettings(), _ => { });
|
||||
var items = new ItemManager(_openCache, ledger, TestSettings(shipLayerHeight), _ => { });
|
||||
var planets = new PlanetSettings();
|
||||
var pack = new ZonePack();
|
||||
if (well) WellFixture.Dig(_openCache, pack, planets, 300f, 100f);
|
||||
|
|
@ -386,7 +387,7 @@ public sealed class FireControlTests : IDisposable
|
|||
[Fact]
|
||||
public void ADrawnShipRidesTheFieldAtItsDrawnPosition()
|
||||
{
|
||||
var (items, ship, _) = BuildShipWithHardpoints(true, new[] { (ItemRotation.None, 0f) }, gridOffset: 2.5f);
|
||||
var (items, ship, _) = BuildShipWithHardpoints(true, new[] { (ItemRotation.None, 0f) }, shipLayerHeight: 2.5f);
|
||||
var zone = ship.Zone;
|
||||
ship.Position = float3(200, 0, 0);
|
||||
zone.Update(1f);
|
||||
|
|
|
|||
|
|
@ -354,8 +354,9 @@ public sealed partial class FireControlCut124Tests
|
|||
{
|
||||
foreach (var shield in new[] { false, true })
|
||||
{
|
||||
var e = Build(TestSettings(), SolidShape(5, 4), equipShield: shield, shieldCapacity: 100000f);
|
||||
e.HullData.GridOffset = 5f;
|
||||
var raised = TestSettings();
|
||||
raised.ShipLayerHeight = 5f;
|
||||
var e = Build(raised, SolidShape(5, 4), equipShield: shield, shieldCapacity: 100000f);
|
||||
for (var i = 0; i < 30; i++) e.Zone.Update(.1f);
|
||||
Assert.Equal(5f, e.Target.Position.y, 1e-4);
|
||||
var subs = new List<IDisposable>();
|
||||
|
|
@ -434,9 +435,10 @@ public sealed partial class FireControlCut124Tests
|
|||
[Fact]
|
||||
public void TheImpactHeightIsTheTargetsHeightAndCarriedKeepsRelativeHeight()
|
||||
{
|
||||
// Production y is the zone height plus the hull's GridOffset, so a raised hull is reachable headless.
|
||||
var e = Build(TestSettings(), SolidShape(21, 3), velocity: 0, damage: 100f, custom: new[] { ("Marker", new Shape(), new int2(2, 1), 1f) });
|
||||
e.HullData.GridOffset = 5f;
|
||||
// Production y is the zone height plus the ship layer, so a raised ship plane is reachable headless.
|
||||
var raised = TestSettings();
|
||||
raised.ShipLayerHeight = 5f;
|
||||
var e = Build(raised, SolidShape(21, 3), velocity: 0, damage: 100f, custom: new[] { ("Marker", new Shape(), new int2(2, 1), 1f) });
|
||||
e.Zone.Update(.01f);
|
||||
Assert.Equal(5f, e.Target.Position.y, 1e-4);
|
||||
var subs = new List<IDisposable>();
|
||||
|
|
|
|||
129
tests/Aetheria.Shared.Tests/LayerTests.cs
Normal file
129
tests/Aetheria.Shared.Tests/LayerTests.cs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
using int2 = CultMath.int2;
|
||||
|
||||
// Layers (aetheria-gravity cut orbital-layering; ruling orbitals-above-floaters-below): GameplaySettings' three
|
||||
// heights decide where each kind of thing sits above the field. A ship rides the ship plane, every orbital entity
|
||||
// (station, turret) the orbital layer above it, a floater (loot, mine) the floater layer below. No hull carries a
|
||||
// height of its own.
|
||||
public sealed class LayerTests : IDisposable
|
||||
{
|
||||
private const float ShipHeight = 2.5f, OrbitalHeight = 9f, FloaterHeight = -1.25f;
|
||||
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-layers-" + Guid.NewGuid().ToString("N"));
|
||||
private readonly CultCache _cache;
|
||||
|
||||
public LayerTests()
|
||||
{
|
||||
Directory.CreateDirectory(_root);
|
||||
_cache = AetheriaStores.Open(Path.Combine(_root, "layers.cc"), catalogWritable: true);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private static GameplaySettings 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(),
|
||||
ShipLayerHeight = ShipHeight, OrbitalLayerHeight = OrbitalHeight, FloaterLayerHeight = FloaterHeight
|
||||
};
|
||||
|
||||
private (ItemManager Items, Zone Zone) Build()
|
||||
{
|
||||
// Hull sizes and masses are spread so no kind coincides with a size or a mass: a layer chosen by either fails.
|
||||
Shape Solid(int side)
|
||||
{
|
||||
var shape = new Shape(side, side);
|
||||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
return shape;
|
||||
}
|
||||
_cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
_cache.Upsert(new HullData { Name = "Ship", HullType = HullType.Ship, Shape = Solid(5), Durability = 1, Mass = 1000 });
|
||||
_cache.Upsert(new HullData { Name = "Station", HullType = HullType.Station, Shape = Solid(3), Durability = 1, Mass = 1000000 });
|
||||
_cache.Upsert(new HullData { Name = "Turret", HullType = HullType.Turret, Shape = Solid(7), Durability = 1, Mass = 50 });
|
||||
_cache.FlushAsync().Wait();
|
||||
var ledger = new ProvenanceLedger();
|
||||
for (var i = 1; i <= 3; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
|
||||
var items = new ItemManager(_cache, ledger, Settings(), _ => { });
|
||||
var zone = new Zone(items, new PlanetSettings(), new ZonePack { Radius = 0 }, new GalaxyZone { Name = "Layers", Owner = null }, null);
|
||||
return (items, zone);
|
||||
}
|
||||
|
||||
private static EquippableItem Hull(ItemManager items, string name, int lot) => new EquippableItem
|
||||
{
|
||||
Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>(name)), Durability = 1, Lot = lot
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void EachKindRidesItsLayer()
|
||||
{
|
||||
var (items, zone) = Build();
|
||||
var ship = new Ship(items, zone, Hull(items, "Ship", 1), new EntitySettings());
|
||||
var station = new OrbitalEntity(items, zone, Hull(items, "Station", 2), default, new EntitySettings());
|
||||
var turret = new OrbitalEntity(items, zone, Hull(items, "Turret", 3), default, new EntitySettings());
|
||||
foreach (var e in new Entity[] { ship, station, turret }) zone.Admit(e, piloted: false);
|
||||
ship.Position = float3(40, 0, 10);
|
||||
station.Position = float3(-30, 0, 20);
|
||||
turret.Position = float3(5, 0, -60);
|
||||
|
||||
zone.Update(.1f);
|
||||
|
||||
Assert.Equal(zone.GetHeight(ship.Position.xz) + ShipHeight, ship.Position.y);
|
||||
Assert.Equal(zone.GetHeight(station.Position.xz) + OrbitalHeight, station.Position.y);
|
||||
Assert.Equal(zone.GetHeight(turret.Position.xz) + OrbitalHeight, turret.Position.y);
|
||||
const float lead = .05f;
|
||||
foreach (var (e, layer) in new (Entity, float)[] { (ship, ShipHeight), (station, OrbitalHeight), (turret, OrbitalHeight) })
|
||||
{
|
||||
var drawn = DrawAhead.Position(e, lead);
|
||||
Assert.Equal(zone.GetHeight(drawn.xz, DrawAhead.Time(zone, lead)) + layer, drawn.y);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrbitalsAboveShipsAboveFloaters()
|
||||
{
|
||||
var authored = AuthoredSettings.Load(RunStartTests.FindRepoRoot()).Read<GameplaySettings>("GameplaySettings");
|
||||
Assert.True(authored.OrbitalLayerHeight > authored.ShipLayerHeight);
|
||||
Assert.True(authored.ShipLayerHeight > authored.FloaterLayerHeight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AFloaterSitsOnTheFloaterLayer()
|
||||
{
|
||||
var (items, zone) = Build();
|
||||
var settings = items.GameplaySettings;
|
||||
foreach (var at in new[] { float2(0, 0), float2(37, -12) })
|
||||
foreach (var lead in new[] { 0f, .05f })
|
||||
Assert.Equal(DrawAhead.Height(zone, at, lead) + FloaterHeight, DrawAhead.FloaterHeight(zone, settings, at, lead));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HullKey26IsRetired()
|
||||
{
|
||||
var keys = new System.Collections.Generic.HashSet<int>();
|
||||
for (var t = typeof(HullData); t != null; t = t.BaseType)
|
||||
foreach (var m in t.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||
foreach (var k in m.GetCustomAttributes(typeof(MessagePack.KeyAttribute), false).Cast<MessagePack.KeyAttribute>())
|
||||
if (k.IntKey.HasValue) keys.Add(k.IntKey.Value);
|
||||
Assert.NotEmpty(keys);
|
||||
Assert.DoesNotContain(26, keys);
|
||||
|
||||
using var shipped = new CultCache(TestCatalog.Registry());
|
||||
shipped.AddBackingStore(new SingleFileMessagePackBackingStore(TestCatalog.Repo, true), AetheriaStores.CatalogTypes);
|
||||
Assert.NotEmpty(shipped.GetAll<HullData>());
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +74,8 @@ public sealed class PickupTests : IDisposable
|
|||
TargetDetectionInfoThreshold = .1f, TargetGearInfoThreshold = .8f,
|
||||
FiringArc = 170, CommitHorizon = .5f, SchematicCellSize = 1f,
|
||||
UnaidedAccuracy = .05f, UnaidedTracking = 10f, UnaidedPrecision = 2f, AgentMinHitProbability = 0f,
|
||||
LootDropProbability = 0, LootDropVelocity = 25, FloatingItemLifetime = lifetime
|
||||
LootDropProbability = 0, LootDropVelocity = 25, FloatingItemLifetime = lifetime,
|
||||
ShipLayerHeight = 3f, FloaterLayerHeight = -1.5f
|
||||
};
|
||||
var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true);
|
||||
_caches.Add(cache);
|
||||
|
|
@ -244,6 +245,22 @@ public sealed class PickupTests : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// The beam ends on the loot where the loot is drawn: the field plus the floater layer, not the grabbing ship's plane
|
||||
// (ruling orbitals-above-floaters-below).
|
||||
[Fact]
|
||||
public void AGrabPointRidesTheFloaterLayerNotTheShips()
|
||||
{
|
||||
using var bag = new CompositeDisposable();
|
||||
var lab = Build(Beam());
|
||||
var ship = Spawn(lab, float3(0, 0, 0));
|
||||
var item = Drift(lab, 40, 0);
|
||||
var events = Watch(ship, bag);
|
||||
ship.TractorCommand = 1;
|
||||
for (var tick = 1; tick <= 150; tick++) lab.Zone.Update(1 / 60f);
|
||||
Assert.NotEmpty(events);
|
||||
Assert.All(events, e => Assert.Equal(lab.Zone.GetHeight(e.TargetPosition.xz, lab.Zone.ExactTime) - 1.5f, e.TargetPosition.y, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrabEventsNameTheFloatingItem()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue