2026-10-12 05:46:32 +02:00
/ * 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.Linq ;
using CultMath ;
using static CultMath . math ;
using float2 = CultMath . float2 ;
2026-10-16 02:27:58 +02:00
// 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
2026-10-12 18:28:29 +02:00
// 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
// GameplaySettings.ContactRestitution.
2026-10-12 05:46:32 +02:00
// Presentations read Zone.Contacted; no Unity collision callback writes an Entity.
public readonly struct ContactEvent
{
public readonly Entity A ;
public readonly Entity B ;
public readonly float2 Point ;
// Unit vector from A to B.
public readonly float2 Normal ;
public ContactEvent ( Entity a , Entity b , float2 point , float2 normal )
{
A = a ;
B = b ;
Point = point ;
Normal = normal ;
}
}
public static class Contacts
{
2026-10-16 02:29:57 +02:00
public static float Radius ( Entity e ) = > e . HullData . Reach ( e . ItemManager . GameplaySettings . SchematicCellSize ) ;
2026-10-12 05:46:32 +02:00
2026-10-16 02:27:58 +02:00
// For each unordered pair of the zone's ships, in list order: if their discs touch and are closing along
2026-10-12 18:28:29 +02:00
// 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
// total momentum does not. A docked entity (it has a Parent and takes its parent's pose) and an inactive one
// are not hulls in the zone. No positional push-out.
2026-10-12 05:46:32 +02:00
public static void Step ( Zone zone )
{
2026-10-16 02:27:58 +02:00
var entities = zone . Entities . OfType < Ship > ( ) . Where ( e = > e . Active & & e . Parent = = null ) . ToArray ( ) ;
2026-10-12 05:46:32 +02:00
var radii = entities . Select ( Radius ) . ToArray ( ) ;
for ( var i = 0 ; i < entities . Length ; i + + )
for ( var j = i + 1 ; j < entities . Length ; j + + )
{
var a = entities [ i ] ;
var b = entities [ j ] ;
var offset = b . Position . xz - a . Position . xz ;
var distance = length ( offset ) ;
// Coincident centres have no normal.
if ( distance < = 0f | | distance > = radii [ i ] + radii [ j ] ) continue ;
var n = offset / distance ;
2026-10-12 18:28:29 +02:00
var closing = dot ( n , b . Velocity - a . Velocity ) ;
if ( closing > = 0f ) continue ;
2026-10-12 05:46:32 +02:00
2026-10-12 18:28:29 +02:00
var impulse = - ( 1f + a . ItemManager . GameplaySettings . ContactRestitution ) * closing / ( 1f / a . Mass + 1f / b . Mass ) ;
a . Velocity - = n * ( impulse / a . Mass ) ;
b . Velocity + = n * ( impulse / b . Mass ) ;
2026-10-12 05:46:32 +02:00
zone . Contacted . OnNext ( new ContactEvent ( a , b , a . Position . xz + n * radii [ i ] , n ) ) ;
}
}
}