Contacts: hull contact is between ships only; orbitals take no part

Step pairs OfType<Ship>; stations and turrets sit on the orbital layer above the ship plane
(ruling aetheria-gravity:orbitals-above-floaters-below). Tests: a ship flies through a station and a turret;
two ships still bounce beside an orbital. Not yet compile-verified (verified once at the cut's head).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-16 02:27:58 +02:00
commit 624d5420e9
2 changed files with 58 additions and 11 deletions

View file

@ -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<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++)

View file

@ -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);
}
@ -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.