diff --git a/Assets/Scripts/ServerShared/Contacts.cs b/Assets/Scripts/ServerShared/Contacts.cs index f680faa5a..9eea3691e 100644 --- a/Assets/Scripts/ServerShared/Contacts.cs +++ b/Assets/Scripts/ServerShared/Contacts.cs @@ -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 @@ -47,7 +48,7 @@ public static class Contacts return far * 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 @@ -55,7 +56,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().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++) diff --git a/tests/Aetheria.Shared.Tests/ContactTests.cs b/tests/Aetheria.Shared.Tests/ContactTests.cs index eafb44cc6..47cf05bb2 100644 --- a/tests/Aetheria.Shared.Tests/ContactTests.cs +++ b/tests/Aetheria.Shared.Tests/ContactTests.cs @@ -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(Items.ItemData.GetByName(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); } @@ -339,21 +351,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 +383,40 @@ 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("Heavy", float3(10, 0, 0), float2(0, 0)); + var turret = lab.AddOrbital("Solid", float3(-10, 0, 0), float2(10, 0)); + lab.Step(); + Assert.Empty(lab.Events); + Assert.Equal(float2(10, 0), ship.Velocity); + Assert.Equal(float2(0, 0), station.Velocity); + Assert.Equal(float2(10, 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("Heavy", 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.