DrawAhead moved xz ahead but carried the last step's y, so on a slope the ship's height stepped once per sim tick. Entity.RideHeight is the one owner of 'an unparented entity sits on the field'; the step and DrawAhead both call it. A child is drawn where its parent is drawn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
7.1 KiB
C#
117 lines
7.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 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. Ships are drawn where the frame falls Lead
|
|
// sim seconds after the last step, by their last step's velocity and turn, and ride the field at that drawn xz (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
|
|
{
|
|
// 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);
|
|
var planar = entity.Position.xz + entity.Velocity * lead;
|
|
return float3(planar.x, entity.RideHeight(planar), planar.y);
|
|
}
|
|
|
|
// 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 a sim time (the clock's Zone.Time + Lead): 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, float simTime, float lead)
|
|
{
|
|
var time = simTime + lead;
|
|
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, lead);
|
|
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, float simTime, float lead) =>
|
|
known != null && known.Result == ShotResult.Miss && simTime + lead >= 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;
|
|
}
|
|
}
|