Aetheria/Assets/Scripts/ServerShared/DrawAhead.cs
Meta Vens eeeb6db290 Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.

Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00

144 lines
9.1 KiB
C#

/* 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 CultMath;
using GameCult.Caching;
using static CultMath.math;
// Pure presentation statics over sim state, never called by ServerShared, with no state of their own: the sim owns every
// pose and no presenter stores one, a previous state or a velocity of its own.
// The rule: a presenter draws every moving thing at the drawn time T = Zone.ExactTime + Clock.Lead. A pose the sim defines
// in closed form of time (an orbit, a belt chunk, a well, the field's height, the flow scroll) is that function evaluated
// at T; a pose the sim integrates (a ship, a kinematic body) is its last step plus its step velocity times Lead; a child
// is drawn where its parent is. DrawAhead.Time is the one definition of T. Ships ride the field at that drawn xz and T (a
// ship's height is the field's there, not the last step's). A ballistic round's flight is presentation
// (ruling projectile-flight-is-presentation), drawn from the sim's facts: its frozen line, the published outcome, the
// commit tick, the impact cell on the target's drawn pose, the burst point.
public static class DrawAhead
{
// The drawn time: the one definition of T.
public static double Time(Zone zone, float lead) => zone.ExactTime + lead;
// An orbit's node at the drawn time: the rail law at T, not the step's memo.
public static float2 Orbit(Zone zone, CultRecordKey orbit, float lead) => zone.OrbitPositionAt(orbit, Time(zone, lead));
// 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.
public static float3 Position(Entity entity, float lead)
{
if (entity is Ship { WormholeAnimationInProgress: true }) return entity.Position;
if (entity.Parent != null) return Position(entity.Parent, lead);
// A body on a rail (a station) is on its orbit at T: the rail is closed form, not integrated.
var planar = entity is OrbitalEntity { OrbitData: var orbit } && orbit.IsSet()
? Orbit(entity.Zone, orbit, lead)
: entity.Position.xz + entity.Velocity * lead;
return float3(planar.x, entity.RideHeight(planar, Time(entity.Zone, lead)), planar.y);
}
// The point `distance` out along a planar bearing from the ship's drawn position: where the HUD anchors a marker to the
// ship the camera follows (an incoming lock, a remembered contact, a ping's flash).
public static float3 AtBearing(Entity origin, float2 bearing, float distance, float lead)
{
var at = Position(origin, lead);
return float3(at.x + bearing.x * distance, at.y, at.z + bearing.y * distance);
}
// A free body (a vapour cloud) drawn where the frame falls lead sim seconds after the last step, by the velocity
// that step moved it with: the launch velocity and the drift together.
public static float2 Position(KinematicBody body, float lead) => body.Position + body.TotalVelocity * lead;
// The ship's rotation turned about its own up axis by the rate its last step turned, for lead seconds.
public static quaternion Rotation(Ship ship, float lead) =>
ship.WormholeAnimationInProgress ? ship.Rotation : Turned(ship.Rotation, ship.TurnRate * lead);
// q turned about its own up axis by the angle, in the sense Direction turns for a positive TurnRate.
public static quaternion Turned(quaternion q, float angle)
{
var u = float3(q.x, q.y, q.z);
var up = float3(0, 1, 0);
up += 2 * (q.w * cross(u, up) + cross(u, cross(u, up)));
var half = angle * .5f;
var s = sin(half);
var t = new quaternion(up.x * s, up.y * s, up.z * s, cos(half));
// Hamilton product t * q: CultMath carries no quaternion product (a gap in CultMath, not filled here).
return normalize(new quaternion(
t.w * q.x + t.x * q.w + t.y * q.z - t.z * q.y,
t.w * q.y - t.x * q.z + t.y * q.w + t.z * q.x,
t.w * q.z + t.x * q.y - t.y * q.x + t.z * q.w,
t.w * q.w - t.x * q.x - t.y * q.y - t.z * q.z));
}
// A ballistic round at the drawn time (DrawAhead.Time, passed in by the caller; the target's lead is that time less its zone's step time, derived here, never a second input): on its frozen line while the outcome is unknown or a
// miss, with the barrel's offset from that line decaying to nothing over blendTime sim seconds from the moment of firing.
// A round the sim published as a hit or burst steers from its line, from the commit tick, onto where it ends by the
// time damage lands: a burst's frozen point, else the impact cell on the target's drawn pose; and is held there after.
public static float3 Round(in PendingShot shot, ShotOutcome known, float3 barrel, float blendTime, double drawnTime)
{
var time = (float) drawnTime;
var drawn = Lifted(shot, Line(shot, known?.Result, time), barrel, blendTime, time);
if (known == null || known.Result == ShotResult.Miss) return drawn;
var end = known.HasBurstPoint
? float3(known.BurstPoint.x, shot.FireOrigin.y, known.BurstPoint.y)
: Impact(known.Target, known.Cell, (float) (drawnTime - known.Target.Zone.ExactTime));
var commit = shot.ArrivalTime - known.ArrivalIn;
var steer = shot.ArrivalTime > commit ? saturate((time - commit) / (shot.ArrivalTime - commit)) : 1f;
return drawn + (end - drawn) * steer;
}
// The impact cell's centre on the target's drawn pose.
public static float3 Impact(Entity target, int2 cell, float lead)
{
var world = target.ToWorldPoint((float2) cell);
return Carried(target, float3(world.x, target.Position.y, world.y), lead);
}
// A world point on the target's sim pose, moved and turned with the target for lead seconds exactly as Position and
// Rotation draw the entity: the one owner of where a point on a hull is drawn, for the drawn round's end and for
// impact presenters.
public static float3 Carried(Entity target, float3 simPoint, float lead)
{
var at = Position(target, lead);
var offset = float2(simPoint.x - target.Position.x, simPoint.z - target.Position.z);
var turned = mul(offset, CultMath.float2x2.Rotate(Turn(target, lead)));
return float3(at.x + turned.x, at.y + (simPoint.y - target.Position.y), at.z + turned.y);
}
// Whether the drawn flight is over by time: only a published miss ends by the clock. A hit or burst ends on ShotResolved.
public static bool RoundOver(in PendingShot shot, ShotOutcome known, double drawnTime) =>
known != null && known.Result == ShotResult.Miss && (float) drawnTime >= RoundEnd(shot, ShotResult.Miss);
// The sim time a round's drawn line ends, for what is known of its outcome. A round that hit or burst (or whose outcome
// is not yet published) ends at its ArrivalTime; a round the sim has published as a Miss flies on to the weapon's frozen
// range and never ends short of its own arrival (a direct round's FireRange is measured to any target, in range or not).
public static float RoundEnd(in PendingShot shot, ShotResult? known) =>
known == ShotResult.Miss
? max(shot.ArrivalTime, shot.FireTime + (shot.Speed > .01f ? shot.MaxRange / shot.Speed : 0f))
: shot.ArrivalTime;
// The frozen line a round follows until it steers: from its frozen origin along its frozen direction at its frozen
// speed, held at RoundEnd once it has flown that far. With no outcome known the round is drawn no further than its
// arrival point and waits there for the sim to publish one.
private static float2 Line(in PendingShot shot, ShotResult? known, float time)
{
if (shot.Speed <= .01f) return shot.FireOrigin.xz;
var flown = clamp(time - shot.FireTime, 0f, RoundEnd(shot, known) - shot.FireTime);
return shot.FireOrigin.xz + shot.TravelDirection * (shot.Speed * flown);
}
private static float Turn(Entity entity, float lead) =>
entity is Ship { WormholeAnimationInProgress: true } ? 0f : entity.TurnRate * lead;
private static float3 Lifted(in PendingShot shot, float2 planar, float3 barrel, float blendTime, float time)
{
var blend = blendTime > 0 ? saturate(1 - (time - shot.FireTime) / blendTime) : 0f;
return float3(planar.x, shot.FireOrigin.y, planar.y) + (barrel - shot.FireOrigin) * blend;
}
}