Merge gravity-drawn-pose-at-drawn-time
Operator flew 39ba73d5 on 2026-10-16: no jitter coasting beside bodies. Soul s1-s3; residuals in follow-up drawn-pose-residuals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
5a788833f0
21 changed files with 864 additions and 151 deletions
212
Assets/Scripts/Editor/DrawnPoseWiringVerify.cs
Normal file
212
Assets/Scripts/Editor/DrawnPoseWiringVerify.cs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
|
||||
// Batch proof that the presenters read the drawn pose through the clock: DrawnPoseTests pins the pose maths headlessly,
|
||||
// and this is what pins that each presenter passes ActionGameManager.Clock.Lead into it. A zone is loaded into a fresh
|
||||
// ZoneRenderer with the pickup and mine prefabs ARPG.unity's ZoneRenderer names; a loose Projectile is launched on the
|
||||
// zone. The zone is stepped once, then the clock is set to leads 0, 0.008 and 0.015 s with no step, and each presenter's
|
||||
// own Update places its transform. A body is drawn at its last step plus its step velocity times the lead, spelled here
|
||||
// from the body's fields, so a presenter that reads lead 0 (or half the lead) sits on the step and fails.
|
||||
//
|
||||
// Reached: ItemPickup, MineInstance, Projectile.Draw. Not reached: ZoneRenderer.Update (Lower needs a ComputeBuffer, a
|
||||
// GPU device) and the ActionGameManager markers (they need the ARPG scene running); a Lower split so its record fill is
|
||||
// pure, or a run with a GPU and ARPG.unity in play mode, would reach them.
|
||||
//
|
||||
// Unity -batchmode -nographics -projectPath <project> -executeMethod Aetheria.EditorTools.DrawnPoseWiringVerify.Run -logFile <log>
|
||||
//
|
||||
// Do not pass -quit: Run enters play mode (a domain reload), the check runs there, and the Editor exits itself, 0 on
|
||||
// success and 1 on any failure.
|
||||
namespace Aetheria.EditorTools
|
||||
{
|
||||
[InitializeOnLoad]
|
||||
public static class DrawnPoseWiringVerify
|
||||
{
|
||||
private const string Tag = "[DrawnPoseWiringVerify]";
|
||||
private const string Flag = "DrawnPoseWiringVerify.running";
|
||||
private const string Wiring = "DrawnPoseWiringVerify.wiring";
|
||||
private const double Budget = 300; // real seconds
|
||||
private const float RoundSpeed = 100f, Tolerance = 1e-3f;
|
||||
private static IEnumerator _check;
|
||||
private static double _started;
|
||||
private static string _work;
|
||||
|
||||
static DrawnPoseWiringVerify()
|
||||
{
|
||||
if (SessionState.GetBool(Flag, false)) EditorApplication.playModeStateChanged += OnPlayMode;
|
||||
}
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
SessionState.SetString(Wiring, ReadWiring());
|
||||
SessionState.SetBool(Flag, true);
|
||||
EditorApplication.EnterPlaymode();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
EditorApplication.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnPlayMode(PlayModeStateChange change)
|
||||
{
|
||||
if (change != PlayModeStateChange.EnteredPlayMode) return;
|
||||
_work = Path.Combine(Path.GetTempPath(), $"drawn-pose-wiring-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_work);
|
||||
_started = Time.realtimeSinceStartupAsDouble;
|
||||
_check = Check();
|
||||
EditorApplication.update += Step;
|
||||
}
|
||||
|
||||
private static void Step()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Time.realtimeSinceStartupAsDouble - _started > Budget) throw new InvalidOperationException($"The check did not finish in {Budget} seconds.");
|
||||
if (_check.MoveNext()) return;
|
||||
Finish(0);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
Finish(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Finish(int code)
|
||||
{
|
||||
EditorApplication.update -= Step;
|
||||
SessionState.SetBool(Flag, false);
|
||||
try { if (Directory.Exists(_work)) Directory.Delete(_work, true); } catch (Exception) { /* a temp directory */ }
|
||||
EditorApplication.Exit(code);
|
||||
}
|
||||
|
||||
private static void Require(bool condition, string what)
|
||||
{
|
||||
if (!condition) throw new InvalidOperationException(what);
|
||||
}
|
||||
|
||||
private static IEnumerator Check()
|
||||
{
|
||||
var settings = AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Resources/Settings.asset")?.GameplaySettings
|
||||
?? throw new InvalidOperationException("No Settings.asset.");
|
||||
|
||||
var wired = SessionState.GetString(Wiring, "").Split('|').Select(entry => entry.Split('=')).ToDictionary(pair => pair[0], pair => pair[1]);
|
||||
var pickupPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(wired["SimpleCommodityPickup"])?.GetComponent<ItemPickup>();
|
||||
var minePrefab = AssetDatabase.LoadAssetAtPath<GameObject>(wired["MinePrefab"])?.GetComponent<MineInstance>();
|
||||
Require(pickupPrefab != null, "ZoneRenderer.SimpleCommodityPickup is not an ItemPickup in ARPG.unity.");
|
||||
Require(minePrefab != null, "ZoneRenderer.MinePrefab is not a MineInstance in ARPG.unity.");
|
||||
|
||||
var oneCell = new Shape(1, 1);
|
||||
oneCell[new CultMath.int2(0, 0)] = true;
|
||||
var cache = AetheriaStores.Open(Path.Combine(_work, "wiring.cc"), catalogWritable: true);
|
||||
cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
cache.Upsert(new SimpleCommodityData { Name = "Wiring Ore", Shape = oneCell, MaxStack = 100 });
|
||||
cache.FlushAsync().Wait();
|
||||
var ledger = new ProvenanceLedger();
|
||||
var items = new ItemManager(cache, ledger, settings, _ => { });
|
||||
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), null, null);
|
||||
|
||||
var rootObject = new GameObject("Wiring Zone Root");
|
||||
var rendererObject = new GameObject("Wiring Zone Renderer");
|
||||
rendererObject.SetActive(false); // inactive: ZoneRenderer.Update needs a GPU and is not part of this check
|
||||
var renderer = rendererObject.AddComponent<ZoneRenderer>();
|
||||
renderer.ZoneRoot = rootObject.transform;
|
||||
renderer.SimpleCommodityPickup = pickupPrefab;
|
||||
renderer.MinePrefab = minePrefab;
|
||||
renderer.ItemManager = items;
|
||||
renderer.SectorBrushes = new GameObject("Wiring Brushes").transform;
|
||||
renderer.SlimeGravityCamera = new GameObject("Wiring Gravity Camera").AddComponent<Camera>();
|
||||
var slimeObject = new GameObject("Wiring Slime");
|
||||
slimeObject.SetActive(false);
|
||||
renderer.SlimeRenderer = slimeObject.AddComponent<Slime>();
|
||||
renderer.LoadZone(zone);
|
||||
|
||||
var ore = new SimpleCommodity { Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<SimpleCommodityData>("Wiring Ore")), Quantity = 7 };
|
||||
var floating = zone.Release(ore, new float2(3, 4), new float2(10, 0));
|
||||
var mine = new Mine
|
||||
{
|
||||
Body = new KinematicBody { Position = new float2(-20, 8), Velocity = new float2(0, 14) },
|
||||
ArmingDelay = 1e6f, FuseDelay = 1f, Lifetime = 1e6f, BlastRadius = 1f
|
||||
};
|
||||
zone.Lay(mine);
|
||||
|
||||
// A round on its frozen line, no target and no outcome known: drawn at FireOrigin + direction * speed * (T - FireTime).
|
||||
var shot = new PendingShot
|
||||
{
|
||||
ShotId = 1, FireOrigin = new float3(0, 0, 0), FireTime = 0f, TravelDirection = new float2(1, 0), Speed = RoundSpeed,
|
||||
MaxRange = 1e6f, FireRange = 1e6f, CommitTime = 1e5f, ArrivalTime = 2e5f
|
||||
};
|
||||
var projectile = new GameObject("Wiring Round").AddComponent<Projectile>();
|
||||
projectile.Launch(zone, shot, new Vector3(0, 0, 0));
|
||||
yield return null;
|
||||
|
||||
zone.Update(.5f);
|
||||
|
||||
var pickup = rootObject.GetComponentsInChildren<ItemPickup>(true).Single(p => p.Item == ore);
|
||||
var mineInstance = UnityEngine.Object.FindObjectsByType<MineInstance>(FindObjectsInactive.Include, FindObjectsSortMode.None).Single();
|
||||
|
||||
var pickupVelocity = floating.Body.TotalVelocity;
|
||||
var mineVelocity = mine.Body.TotalVelocity;
|
||||
Require(Mathf.Abs(pickupVelocity.x) > 1f && Mathf.Abs(mineVelocity.y) > 1f, "The fixture's bodies stopped: a lead would not move them.");
|
||||
|
||||
Vector2 baseline = default;
|
||||
foreach (var leadSeconds in new[] { 0f, .008f, .015f })
|
||||
{
|
||||
ActionGameManager.Clock.Reset();
|
||||
if (leadSeconds > 0) ActionGameManager.Clock.Advance(leadSeconds, _ => throw new InvalidOperationException("a step ran"));
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
Require(Mathf.Abs(lead - leadSeconds) < 1e-6f, $"The clock's lead is {lead}, not {leadSeconds}.");
|
||||
yield return null;
|
||||
|
||||
var seen = pickup.transform.position;
|
||||
var want = floating.Body.Position + pickupVelocity * lead;
|
||||
Require(Mathf.Abs(seen.x - want.x) < Tolerance && Mathf.Abs(seen.z - want.y) < Tolerance,
|
||||
$"ItemPickup at lead {lead}: drawn x/z {seen.x},{seen.z}, the step plus velocity times lead {want.x},{want.y}.");
|
||||
|
||||
seen = mineInstance.transform.position;
|
||||
want = mine.Body.Position + mineVelocity * lead;
|
||||
Require(Mathf.Abs(seen.x - want.x) < Tolerance && Mathf.Abs(seen.z - want.y) < Tolerance,
|
||||
$"MineInstance at lead {lead}: drawn x/z {seen.x},{seen.z}, the step plus velocity times lead {want.x},{want.y}.");
|
||||
|
||||
// The round's line is a function of the drawn time: it moves RoundSpeed * lead along its direction between leads.
|
||||
seen = projectile.transform.position;
|
||||
if (leadSeconds == 0f) baseline = new Vector2(seen.x, seen.z);
|
||||
var moved = new Vector2(seen.x, seen.z) - baseline;
|
||||
Require(Mathf.Abs(moved.x - RoundSpeed * lead) < Tolerance && Mathf.Abs(moved.y) < Tolerance,
|
||||
$"Projectile at lead {lead}: drawn {moved.x},{moved.y} past its lead-0 pose, expected {RoundSpeed * lead},0.");
|
||||
}
|
||||
ActionGameManager.Clock.Reset();
|
||||
Debug.Log($"{Tag} ok: ItemPickup, MineInstance and Projectile draw at the clock's lead (0, 0.008, 0.015 s)");
|
||||
}
|
||||
|
||||
// What ARPG.unity's ZoneRenderer names in the pickup and mine fields, read from the saved scene before the play session
|
||||
// (a scene cannot be opened inside one) and carried across the domain reload as asset paths.
|
||||
private static string ReadWiring()
|
||||
{
|
||||
var scene = EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Additive);
|
||||
try
|
||||
{
|
||||
var found = UnityEngine.Object.FindObjectsByType<ZoneRenderer>(FindObjectsInactive.Include, FindObjectsSortMode.None)
|
||||
.Where(r => r.gameObject.scene == scene).ToArray();
|
||||
Require(found.Length == 1, $"ARPG.unity has {found.Length} ZoneRenderers.");
|
||||
var wired = found[0];
|
||||
return string.Join("|", new (string, UnityEngine.Object)[]
|
||||
{
|
||||
("SimpleCommodityPickup", wired.SimpleCommodityPickup), ("MinePrefab", wired.MinePrefab)
|
||||
}.Select(f => f.Item1 + "=" + (f.Item2 == null ? "" : AssetDatabase.GetAssetPath(f.Item2))));
|
||||
}
|
||||
finally { EditorSceneManager.CloseScene(scene, true); }
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Editor/DrawnPoseWiringVerify.cs.meta
Normal file
2
Assets/Scripts/Editor/DrawnPoseWiringVerify.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c2270cf68a4af384eb715c8b5a55f5e8
|
||||
|
|
@ -157,8 +157,9 @@ public static class FloatingItemsDrawnSmoke
|
|||
ItemPickup[] Pickups() => rootObject.GetComponentsInChildren<ItemPickup>(true).Where(p => p != null).ToArray();
|
||||
void At(ItemPickup pickup, FloatingItem floating, string what)
|
||||
{
|
||||
var at = floating.Body.Position;
|
||||
var want = new Vector3(at.x, zone.GetHeight(at), at.y);
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
var at = DrawAhead.Position(floating.Body, lead);
|
||||
var want = new Vector3(at.x, DrawAhead.Height(zone, at, lead), at.y);
|
||||
Require(Vector3.Distance(pickup.transform.position, want) < 1e-3f,
|
||||
$"{what}: the pickup is at {pickup.transform.position}, the body at {want}.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ namespace Aetheria.EditorTools
|
|||
?? throw new InvalidOperationException("No Gravity Field.mat.");
|
||||
|
||||
var zone = BuildZone(settings, work, out var orbits);
|
||||
GravityLowering.Lower(zone, ref wells);
|
||||
GravityLowering.Lower(zone, 0f, ref wells);
|
||||
field = GravityLowering.CreateField(material);
|
||||
|
||||
// Three frames through the one buffer, as ZoneRenderer.Update runs it: the bodies as built, then two moves
|
||||
|
|
@ -80,11 +80,11 @@ namespace Aetheria.EditorTools
|
|||
CheckFrame("built", zone, orbits);
|
||||
orbits[1].Position = new float2(-420f, 560f);
|
||||
orbits[2].Position = new float2(900f, -700f);
|
||||
GravityLowering.Lower(zone, ref wells);
|
||||
GravityLowering.Lower(zone, 0f, ref wells);
|
||||
CheckFrame("moved", zone, orbits);
|
||||
orbits[0].Position = new float2(210f, -95f);
|
||||
orbits[2].Position = new float2(-1800f, 1300f);
|
||||
GravityLowering.Lower(zone, ref wells);
|
||||
GravityLowering.Lower(zone, 0f, ref wells);
|
||||
CheckFrame("moved-again", zone, orbits);
|
||||
Debug.Log($"{Tag} max error {_maxError:G4} u (tolerance {Tolerance:G4}); max gradient angle {_maxAngle:F2} degrees (limit {GradientDegrees:F0})");
|
||||
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
Input.Global.ZoneMap.performed += context =>
|
||||
{
|
||||
ToggleMenuTab(MenuTab.Map);
|
||||
MenuMap.Position = CurrentEntity.Position.xz;
|
||||
MenuMap.Position = DrawAhead.Position(CurrentEntity, Clock.Lead).xz;
|
||||
};
|
||||
|
||||
Input.Global.Inventory.performed += context => ToggleMenuTab(MenuTab.Inventory);
|
||||
|
|
@ -1178,10 +1178,12 @@ public class ActionGameManager : MonoBehaviour
|
|||
}
|
||||
}
|
||||
|
||||
// A chunk's simulated position is planar (Zone.ChunkPose); the renderer draws it at the asteroid layer's height.
|
||||
// A chunk's pose is planar (Zone.ChunkPose), read at the drawn time so the reticle sits on the drawn rock; the renderer
|
||||
// draws it at the asteroid layer's height.
|
||||
private float3 ChunkWorldPosition(ChunkId chunk)
|
||||
{
|
||||
var pose = CurrentEntity.Zone.ChunkPose(chunk.Field, chunk.Index);
|
||||
var zone = CurrentEntity.Zone;
|
||||
var pose = zone.ChunkPose(chunk.Field, chunk.Index, DrawAhead.Time(zone, Clock.Lead));
|
||||
return float3(pose.x, Settings.PlanetSettings.AsteroidVerticalOffset, pose.y);
|
||||
}
|
||||
|
||||
|
|
@ -1314,13 +1316,13 @@ public class ActionGameManager : MonoBehaviour
|
|||
foreach (var indicator in _visibleHostileIndicators)
|
||||
{
|
||||
indicator.Value.gameObject.SetActive(indicator.Key!=CurrentEntity.Target.Value.Entity);
|
||||
indicator.Value.Place.Target = indicator.Key.Position.ToUnity();
|
||||
indicator.Value.Place.Target = DrawAhead.Position(indicator.Key, Clock.Lead).ToUnity();
|
||||
PlaceFill(indicator.Value, indicator.Key);
|
||||
}
|
||||
foreach (var indicator in _visibleFriendlyIndicators)
|
||||
{
|
||||
indicator.Value.gameObject.SetActive(indicator.Key!=CurrentEntity.Target.Value.Entity);
|
||||
indicator.Value.Place.Target = indicator.Key.Position.ToUnity();
|
||||
indicator.Value.Place.Target = DrawAhead.Position(indicator.Key, Clock.Lead).ToUnity();
|
||||
PlaceFill(indicator.Value, indicator.Key);
|
||||
}
|
||||
var look = Input.Player.Look.ReadValue<Vector2>();
|
||||
|
|
@ -1458,13 +1460,15 @@ public class ActionGameManager : MonoBehaviour
|
|||
{
|
||||
if (CurrentEntity == null || CurrentEntity.Parent != null) return;
|
||||
|
||||
var lead = Clock.Lead;
|
||||
var ownDrawn = DrawAhead.Position(CurrentEntity, lead);
|
||||
ViewDot.Target = ZoneRenderer.EntityInstances[CurrentEntity].LookAtPoint.position;
|
||||
var target = CurrentEntity.Target.Value;
|
||||
if (target.Chunk is ChunkId chunk)
|
||||
TargetIndicator.Target = ChunkWorldPosition(chunk).ToUnity();
|
||||
else if (target.Entity != null)
|
||||
TargetIndicator.Target = target.Entity.Position.ToUnity();
|
||||
var distance = length(ViewDot.Target.ToCultMath() - CurrentEntity.Position);
|
||||
TargetIndicator.Target = DrawAhead.Position(target.Entity, lead).ToUnity();
|
||||
var distance = length(ViewDot.Target.ToCultMath() - ownDrawn);
|
||||
foreach (var (_, barrels, crosshair) in _articulationGroups)
|
||||
{
|
||||
var averagePosition = Vector3.zero;
|
||||
|
|
@ -1482,7 +1486,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
indicator.gameObject.SetActive(showIndicator);
|
||||
if(showIndicator)
|
||||
{
|
||||
indicator.Target = targetEntity.Position.ToUnity();
|
||||
indicator.Target = DrawAhead.Position(targetEntity, lead).ToUnity();
|
||||
ApplyLockProgress(indicator, spin, value);
|
||||
}
|
||||
}
|
||||
|
|
@ -1499,8 +1503,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
if (strongest >= 0)
|
||||
{
|
||||
var warning = warnings[strongest];
|
||||
incomingIndicator.Target = CurrentEntity.Position.ToUnity() +
|
||||
new Vector3(warning.Bearing.x, 0, warning.Bearing.y) * IncomingLockIndicatorDistance;
|
||||
incomingIndicator.Target = DrawAhead.AtBearing(CurrentEntity, warning.Bearing, IncomingLockIndicatorDistance, lead).ToUnity();
|
||||
ApplyLockProgress(incomingIndicator, incomingSpin, warning.Strength);
|
||||
}
|
||||
|
||||
|
|
@ -1531,7 +1534,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
}
|
||||
marker.gameObject.SetActive(true);
|
||||
var bearing = CurrentEntity.ContactOf(subject).Bearing;
|
||||
marker.Place.Target = CurrentEntity.Position.ToUnity() + new Vector3(bearing.x, 0, bearing.y) * BearingMarkerDistance;
|
||||
marker.Place.Target = DrawAhead.AtBearing(CurrentEntity, bearing, BearingMarkerDistance, Clock.Lead).ToUnity();
|
||||
marker.ShowLabel(string.Join(", ", CurrentEntity.ClaimedClasses(subject).Select(c => c.Name)));
|
||||
}
|
||||
}
|
||||
|
|
@ -1570,7 +1573,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
_sweepFlashes.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
indicator.Target = CurrentEntity.Position.ToUnity() + new Vector3(bearing.x, 0, bearing.y) * IncomingLockIndicatorDistance;
|
||||
indicator.Target = DrawAhead.AtBearing(CurrentEntity, bearing, IncomingLockIndicatorDistance, Clock.Lead).ToUnity();
|
||||
if (image != null) image.color = new Color(image.color.r, image.color.g, image.color.b, 1 - age / SearchFlashSeconds);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ public class EntityInstance : MonoBehaviour
|
|||
{
|
||||
var pingInstance = Instantiate(PingPrefab);
|
||||
var pingMesh = pingInstance.GetComponent<MeshRenderer>();
|
||||
pingInstance.position = entity.Position.ToUnity();
|
||||
pingInstance.position = DrawAhead.Position(entity, ActionGameManager.Clock.Lead).ToUnity();
|
||||
_pingBrightness = pingMesh.material.GetFloat("_Depth");
|
||||
_currentPing = (pingInstance, pingMesh);
|
||||
};
|
||||
|
|
@ -390,6 +390,9 @@ public class EntityInstance : MonoBehaviour
|
|||
x.Value.material.SetFloat("_Emission", Entity.ItemManager.GameplaySettings.TemperatureEmissionCurve.Evaluate(x.Key.RadiatorTemperature));
|
||||
}
|
||||
|
||||
// The drawn pose first: the camera's LookAt and the barrels read this frame's pose, not the last frame's.
|
||||
var drawn = DrawAhead.Position(Entity, ActionGameManager.Clock.Lead);
|
||||
LocalSpace.localPosition = transform.position = drawn.ToUnity();
|
||||
var aimDistance = !Entity.Target.Value.IsNone
|
||||
? max(Entity.TargetRange, Entity.ItemManager.GameplaySettings.ConvergenceMinimumDistance)
|
||||
: 10000;
|
||||
|
|
@ -402,8 +405,6 @@ public class EntityInstance : MonoBehaviour
|
|||
var direction = FireControl.Solution(weapon, Entity, Entity.Target.Value).Direction;
|
||||
group.Value.position = barrel + new Vector3(direction.x, 0, direction.y) * aimDistance;
|
||||
}
|
||||
var drawn = DrawAhead.Position(Entity, ActionGameManager.Clock.Lead);
|
||||
LocalSpace.localPosition = transform.position = drawn.ToUnity();
|
||||
if (_influenceInstance)
|
||||
_influenceInstance.position = new Vector3(drawn.x, 0, drawn.z);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,9 @@ public class ItemPickup : MonoBehaviour
|
|||
|
||||
private void Place()
|
||||
{
|
||||
var p = _floating.Body.Position;
|
||||
transform.position = new Vector3(p.x, _zone.GetHeight(p) + GridOffset, p.y);
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
var p = DrawAhead.Position(_floating.Body, lead);
|
||||
transform.position = new Vector3(p.x, DrawAhead.Height(_zone, p, lead) + GridOffset, p.y);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
|
|
|
|||
|
|
@ -55,8 +55,9 @@ public class MineInstance : MonoBehaviour
|
|||
|
||||
private void Place()
|
||||
{
|
||||
var p = _mine.Body.Position;
|
||||
transform.position = new Vector3(p.x, _zone.GetHeight(p) + GridOffset, p.y);
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
var p = DrawAhead.Position(_mine.Body, lead);
|
||||
transform.position = new Vector3(p.x, DrawAhead.Height(_zone, p, lead) + GridOffset, p.y);
|
||||
}
|
||||
|
||||
// The mine left Zone.Mines, which only its blast does: show it where it was drawn last.
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class ShieldManager : MonoBehaviour, IAbsorbPresenter
|
|||
{
|
||||
if (contact.A != Entity && contact.B != Entity) return;
|
||||
if (Entity.Shield != null && Entity.Shield.Item.Active.Value)
|
||||
ShowHit(new Vector3(contact.Point.x, Entity.Position.y, contact.Point.y), CollisionHitDuration);
|
||||
ShowHit(DrawAhead.Carried(Entity, float3(contact.Point.x, Entity.Position.y, contact.Point.y), ActionGameManager.Clock.Lead).ToUnity(), CollisionHitDuration);
|
||||
}
|
||||
|
||||
private ShieldEnvelope _envelope;
|
||||
|
|
|
|||
|
|
@ -77,7 +77,11 @@ public class GuidedProjectileManager : InstantWeaponEffectManager
|
|||
p.Velocity = barrel.forward * weapon.Velocity;
|
||||
p.Thrust = item.Evaluate(MunitionParts.Of<MunitionThrusterData>(item).Acceleration);
|
||||
p.TopSpeed = item.Evaluate(MunitionParts.Of<MunitionThrusterData>(item).MaxSpeed);
|
||||
p.TargetPosition = () => (source.Entity.Position + length(source.LookAtPoint.position.ToCultMath() - source.Entity.Position) * source.Entity.Aim).ToUnity();
|
||||
p.TargetPosition = () =>
|
||||
{
|
||||
var drawn = DrawAhead.Position(source.Entity, ActionGameManager.Clock.Lead);
|
||||
return (drawn + length(source.LookAtPoint.position.ToCultMath() - drawn) * source.Entity.Aim).ToUnity();
|
||||
};
|
||||
Bind(p, source.Entity.Zone, shotId);
|
||||
}
|
||||
else Debug.LogError($"Weapon {item.Data.Name} linked to {name} effect, but is not a Launcher!");
|
||||
|
|
|
|||
|
|
@ -50,8 +50,9 @@ public class Projectile : MonoBehaviour
|
|||
private void Draw()
|
||||
{
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
transform.position = V(DrawAhead.Round(_shot, _known, _barrel, BlendTime, _zone.Time, lead));
|
||||
if (DrawAhead.RoundOver(_shot, _known, _zone.Time, lead)) Finish();
|
||||
var drawn = DrawAhead.Time(_zone, lead);
|
||||
transform.position = V(DrawAhead.Round(_shot, _known, _barrel, BlendTime, drawn));
|
||||
if (DrawAhead.RoundOver(_shot, _known, drawn)) Finish();
|
||||
}
|
||||
|
||||
// The sim's commit: the record it holds now (a contact burst's arrival is shortened here) and what it decided.
|
||||
|
|
|
|||
|
|
@ -3,16 +3,29 @@
|
|||
* 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. 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
|
||||
// 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));
|
||||
|
||||
// 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.
|
||||
|
|
@ -20,10 +33,39 @@ public static class DrawAhead
|
|||
{
|
||||
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 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);
|
||||
}
|
||||
|
||||
// One well as the shader takes it: its body's rail at the drawn time, with the strength, core and reference the sim's
|
||||
// GetHeight sums with. GravityLowering copies it into the shader's record and decides nothing.
|
||||
public readonly struct DrawnWell
|
||||
{
|
||||
public readonly float2 Position;
|
||||
public readonly float Strength, CoreRadius, Reference;
|
||||
public DrawnWell(float2 position, float strength, float coreRadius, float reference)
|
||||
{
|
||||
Position = position;
|
||||
Strength = strength;
|
||||
CoreRadius = coreRadius;
|
||||
Reference = reference;
|
||||
}
|
||||
}
|
||||
|
||||
public static DrawnWell Well(Zone zone, Planet body, float lead) =>
|
||||
new DrawnWell(Orbit(zone, body.BodyData.Orbit.Key, lead), body.WellStrength, body.RocheLimit, zone.WellReference(body));
|
||||
|
||||
// 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;
|
||||
|
|
@ -49,18 +91,18 @@ public static class DrawAhead
|
|||
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
|
||||
// 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, float simTime, float lead)
|
||||
public static float3 Round(in PendingShot shot, ShotOutcome known, float3 barrel, float blendTime, double drawnTime)
|
||||
{
|
||||
var time = simTime + lead;
|
||||
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, lead);
|
||||
: 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;
|
||||
|
|
@ -85,8 +127,8 @@ public static class DrawAhead
|
|||
}
|
||||
|
||||
// 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);
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1975,8 +1975,10 @@ public abstract class Entity
|
|||
}
|
||||
|
||||
// Where an unparented entity sits on the field at a planar point: the zone's height there plus the hull's grid offset.
|
||||
// The step puts the entity here at its own xz, and the presenter at its drawn xz (DrawAhead).
|
||||
public float RideHeight(float2 planar) => Zone.GetHeight(planar) + (ItemManager.GetData(Hull) as HullData).GridOffset;
|
||||
// The step rides at its own time and xz, the presenter at the drawn time and its drawn xz (DrawAhead).
|
||||
public float RideHeight(float2 planar, double time) => Zone.GetHeight(planar, time) + (ItemManager.GetData(Hull) as HullData).GridOffset;
|
||||
|
||||
public float RideHeight(float2 planar) => RideHeight(planar, Zone.ExactTime);
|
||||
|
||||
private void UpdateTemperature(float delta)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -506,32 +506,59 @@ public class Zone
|
|||
|
||||
if (!_updatedOrbits.Contains(orbitID))
|
||||
{
|
||||
var orbit = Orbits[orbitID];
|
||||
float2 pos = float2.zero;
|
||||
// An infinite period (a parentless orbit, or one with no field to circle in) leaves the orbit at its phase:
|
||||
// frac(t / inf) is 0.
|
||||
if (orbit.Period > .01f)
|
||||
{
|
||||
var phase = (float) frac(_time / orbit.Period);
|
||||
pos = OrbitData.Evaluate(frac(phase + orbit.Data.Phase)) * orbit.Data.Distance;
|
||||
|
||||
if (float.IsNaN(pos.x))
|
||||
{
|
||||
//_context.Log("Orbit position is NaN, something went very wrong!");
|
||||
pos = float2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
var parentPosition = !Orbits[orbitID].Data.Parent.IsSet()
|
||||
? Orbits[orbitID].Data.FixedPosition :
|
||||
GetOrbitPosition(orbit.Data.Parent.Key);
|
||||
Orbits[orbitID].Position = parentPosition + pos;
|
||||
Orbits[orbitID].Position = RailAt(orbitID, _time);
|
||||
_updatedOrbits.Add(orbitID);
|
||||
}
|
||||
|
||||
return Orbits[orbitID].Position;
|
||||
}
|
||||
|
||||
// The rail law, once: where an orbit sits from its parent's node at a time. An infinite period (a parentless orbit,
|
||||
// or one with no field to circle in) leaves the orbit at its phase: frac(t / inf) is 0.
|
||||
private static float2 Offset(Orbit orbit, double time)
|
||||
{
|
||||
var pos = float2.zero;
|
||||
if (orbit.Period > .01f)
|
||||
{
|
||||
var phase = (float) frac(time / orbit.Period);
|
||||
pos = OrbitData.Evaluate(frac(phase + orbit.Data.Phase)) * orbit.Data.Distance;
|
||||
|
||||
if (float.IsNaN(pos.x))
|
||||
pos = float2.zero;
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
// The same rail law at any time, with no memo read or written: a parent is its own rail at that time.
|
||||
private float2 RailAt(CultRecordKey orbitID, double time)
|
||||
{
|
||||
if (!orbitID.IsSet())
|
||||
return float2.zero;
|
||||
if (!Orbits.TryGetValue(orbitID, out var orbit))
|
||||
{
|
||||
Log?.Invoke("Requested orbit is not part of this zone!");
|
||||
return float2.zero;
|
||||
}
|
||||
var parentPosition = !orbit.Data.Parent.IsSet() ? orbit.Data.FixedPosition : RailAt(orbit.Data.Parent.Key, time);
|
||||
return parentPosition + Offset(orbit, time);
|
||||
}
|
||||
|
||||
// Where an orbit is at a time: pure, writes nothing. At the step's own time it is the step's memo (GetOrbitPosition);
|
||||
// at any other time (a presenter's drawn time) it is the rail law evaluated there.
|
||||
public float2 OrbitPositionAt(CultRecordKey orbitID, double time)
|
||||
{
|
||||
if (!orbitID.IsSet())
|
||||
return float2.zero;
|
||||
if (!Orbits.ContainsKey(orbitID))
|
||||
{
|
||||
Log?.Invoke("Requested orbit is not part of this zone!");
|
||||
return float2.zero;
|
||||
}
|
||||
|
||||
return time == _time ? GetOrbitPosition(orbitID) : RailAt(orbitID, time);
|
||||
}
|
||||
|
||||
// orbit -> the well-bearing body on it, and orbit -> the orbits whose parent it is. Derived from the pack in the
|
||||
// constructor, read only by PeriodAround and Strength.
|
||||
private readonly Dictionary<CultRecordKey, Planet> _wellOn = new Dictionary<CultRecordKey, Planet>();
|
||||
|
|
@ -656,12 +683,15 @@ public class Zone
|
|||
|
||||
// Cut 1 (docs/mining-cut.md): a chunk's pose is a pure function of zone time, computed when asked. No stored
|
||||
// pose survives a tick; the sim and the renderer both read it fresh through here.
|
||||
public float4 ChunkPose(CultRecordKey belt, int index)
|
||||
public float4 ChunkPose(CultRecordKey belt, int index) => ChunkPose(belt, index, _time);
|
||||
|
||||
// The same pose at any time: the sim reads it at _time, a presenter at the drawn time (DrawAhead.Time).
|
||||
public float4 ChunkPose(CultRecordKey belt, int index, double time)
|
||||
{
|
||||
var beltData = Planets[belt] as AsteroidBeltData;
|
||||
var parentPosition = GetOrbitPosition(Orbits[beltData.Orbit.Key].Data.Parent.Key);
|
||||
var parentPosition = OrbitPositionAt(Orbits[beltData.Orbit.Key].Data.Parent.Key, time);
|
||||
var size = ChunkRadius(new ChunkId(belt, index));
|
||||
return AsteroidBelts[belt].Pose(index, _time, parentPosition, size);
|
||||
return AsteroidBelts[belt].Pose(index, time, parentPosition, size);
|
||||
}
|
||||
|
||||
// Mining Cut 3 (docs/mining-cut-refresh.md): how bright a chunk is -- Reflector's rule (cross-section × the light
|
||||
|
|
@ -676,13 +706,13 @@ public class Zone
|
|||
return kind.CrossSection * PI * cells * cells * GetLight(ChunkPose(chunk.Field, chunk.Index).xy);
|
||||
}
|
||||
|
||||
public void EvaluateBelt(CultRecordKey belt, Span<float4> into)
|
||||
public void EvaluateBelt(CultRecordKey belt, Span<float4> into, double time)
|
||||
{
|
||||
var beltData = Planets[belt] as AsteroidBeltData;
|
||||
var parentPosition = GetOrbitPosition(Orbits[beltData.Orbit.Key].Data.Parent.Key);
|
||||
var parentPosition = OrbitPositionAt(Orbits[beltData.Orbit.Key].Data.Parent.Key, time);
|
||||
var beltInstance = AsteroidBelts[belt];
|
||||
for (var i = 0; i < beltData.Asteroids.Length; i++)
|
||||
into[i] = beltInstance.Pose(i, _time, parentPosition, ChunkRadius(new ChunkId(belt, i)));
|
||||
into[i] = beltInstance.Pose(i, time, parentPosition, ChunkRadius(new ChunkId(belt, i)));
|
||||
}
|
||||
|
||||
public SecurityLevel GetSecurityLevel(float2 pos)
|
||||
|
|
@ -706,12 +736,16 @@ public class Zone
|
|||
// reference. GetHeight sums with it and the renderer's lowering (GravityLowering) hands it to the shader unchanged.
|
||||
public float WellReference(Planet body) => GravityField.Potential(body.WellStrength, body.RocheLimit, Pack.Radius);
|
||||
|
||||
public float GetHeight(float2 position)
|
||||
public float GetHeight(float2 position) => GetHeight(position, _time);
|
||||
|
||||
// The field at a time: each well sits where its body's rail puts it then. The sim reads it at _time, a presenter at
|
||||
// the drawn time, so a ship rides the same slope the drawn planet's well makes.
|
||||
public float GetHeight(float2 position, double time)
|
||||
{
|
||||
var result = 0f;
|
||||
foreach (var body in PlanetInstances.Values)
|
||||
{
|
||||
var offset = position - body.Orbit.Position;
|
||||
var offset = position - OrbitPositionAt(body.BodyData.Orbit.Key, time);
|
||||
result += GravityField.Potential(body.WellStrength, body.RocheLimit, length(offset)) - WellReference(body);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,9 +32,10 @@ public static class GravityLowering
|
|||
|
||||
/// <summary>
|
||||
/// Fills one record per body of the zone (a null zone has none) and sets the shader globals. The buffer is the
|
||||
/// caller's, (re)allocated to the body count (at least one, so the global is always bound).
|
||||
/// caller's, (re)allocated to the body count (at least one, so the global is always bound). Wells are lowered at the
|
||||
/// drawn time (DrawAhead.Time), the same T the planets and a ship's ride height use.
|
||||
/// </summary>
|
||||
public static void Lower(Zone zone, ref ComputeBuffer wells)
|
||||
public static void Lower(Zone zone, float lead, ref ComputeBuffer wells)
|
||||
{
|
||||
var count = zone?.PlanetInstances.Count ?? 0;
|
||||
var capacity = Mathf.Max(count, 1);
|
||||
|
|
@ -48,13 +49,16 @@ public static class GravityLowering
|
|||
var i = 0;
|
||||
if (zone != null)
|
||||
foreach (var body in zone.PlanetInstances.Values)
|
||||
{
|
||||
var well = DrawAhead.Well(zone, body, lead);
|
||||
_records[i++] = new WellRecord
|
||||
{
|
||||
Position = new Vector2(body.Orbit.Position.x, body.Orbit.Position.y),
|
||||
Strength = body.WellStrength,
|
||||
CoreRadius = body.RocheLimit,
|
||||
Reference = zone.WellReference(body)
|
||||
Position = new Vector2(well.Position.x, well.Position.y),
|
||||
Strength = well.Strength,
|
||||
CoreRadius = well.CoreRadius,
|
||||
Reference = well.Reference
|
||||
};
|
||||
}
|
||||
wells.SetData(_records);
|
||||
|
||||
Shader.SetGlobalBuffer(WellsId, wells);
|
||||
|
|
|
|||
|
|
@ -106,10 +106,12 @@ public class VolumeSampling : MonoBehaviour
|
|||
static GlobalKeyword KeywordFlowSlope => _keywordFlowSlope ??= GlobalKeyword.Create("FLOW_SLOPE");
|
||||
|
||||
/// <summary>
|
||||
/// The time the flow runs on: the zone's own (so drift stops with the sim and follows sim speed), or on menus,
|
||||
/// where no zone exists, the presentation clock.
|
||||
/// The time the flow runs on: the zone's drawn time (DrawAhead.Time, so drift stops with the sim and follows sim
|
||||
/// speed), or on menus, where no zone exists, the presentation clock.
|
||||
/// </summary>
|
||||
public static double FlowClock => ActionGameManager.Instance?.Zone?.ExactTime ?? Time.timeSinceLevelLoad;
|
||||
public static double FlowClock => ActionGameManager.Instance?.Zone is { } zone
|
||||
? DrawAhead.Time(zone, ActionGameManager.Clock.Lead)
|
||||
: Time.timeSinceLevelLoad;
|
||||
|
||||
/// <summary>
|
||||
/// The one place that sets the flow uniforms and keywords: every shader flow term derives from FlowSettings and
|
||||
|
|
|
|||
|
|
@ -520,10 +520,11 @@ public class ZoneRenderer : MonoBehaviour
|
|||
void Update()
|
||||
{
|
||||
var maxDepth = 0f;
|
||||
GravityLowering.Lower(Zone, ref _gravityWells);
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
GravityLowering.Lower(Zone, lead, ref _gravityWells);
|
||||
foreach (var loot in _floatingItems.Values)
|
||||
{
|
||||
loot.ViewOrigin = PerspectiveEntity.Position;
|
||||
loot.ViewOrigin = DrawAhead.Position(PerspectiveEntity, lead);
|
||||
loot.ViewDirection = PerspectiveEntity.Aim;
|
||||
}
|
||||
|
||||
|
|
@ -542,11 +543,11 @@ public class ZoneRenderer : MonoBehaviour
|
|||
foreach (var (key, belt) in Zone.AsteroidBelts)
|
||||
{
|
||||
// Cut 1 (docs/mining-cut.md): belts have no stored pose any more. The renderer fills its own buffer
|
||||
// from Zone.EvaluateBelt, at the current zone time, every frame.
|
||||
var orbitPosition = Zone.GetOrbitPosition(Zone.Orbits[belt.Data.Orbit.Key].Data.Parent.Key);
|
||||
var height = Zone.GetHeight(orbitPosition);
|
||||
// from Zone.EvaluateBelt, at the drawn time (DrawAhead.Time), every frame.
|
||||
var orbitPosition = DrawAhead.Orbit(Zone, Zone.Orbits[belt.Data.Orbit.Key].Data.Parent.Key, lead);
|
||||
var height = DrawAhead.Height(Zone, orbitPosition, lead);
|
||||
var transforms = _beltTransforms[key];
|
||||
Zone.EvaluateBelt(key, transforms);
|
||||
Zone.EvaluateBelt(key, transforms, DrawAhead.Time(Zone, lead));
|
||||
if(isVisible(new Bounds(
|
||||
new Vector3(orbitPosition.x,height,orbitPosition.y),
|
||||
new Vector3(belt.Radius * 2,100,belt.Radius * 2))))
|
||||
|
|
@ -579,8 +580,8 @@ public class ZoneRenderer : MonoBehaviour
|
|||
foreach (var planet in Planets)
|
||||
{
|
||||
var planetInstance = Zone.PlanetInstances[planet.Key];
|
||||
var p = Zone.GetOrbitPosition(planetInstance.BodyData.Orbit.Key);
|
||||
var height = Zone.GetHeight(p);
|
||||
var p = DrawAhead.Orbit(Zone, planetInstance.BodyData.Orbit.Key, lead);
|
||||
var height = DrawAhead.Height(Zone, p, lead);
|
||||
if (-height > maxDepth) maxDepth = -height;
|
||||
planet.Value.transform.position = new Vector3(p.x, 0, p.y);
|
||||
planet.Value.Body.transform.localPosition = new Vector3(0, height + planetInstance.BodyRadius * 2, 0);
|
||||
|
|
@ -589,7 +590,7 @@ public class ZoneRenderer : MonoBehaviour
|
|||
{
|
||||
if (!(planet.Value is SunObject))
|
||||
{
|
||||
var toParent = normalize(Zone.GetOrbitPosition(Zone.Orbits[planetInstance.BodyData.Orbit.Key].Data.Parent.Key) - p);
|
||||
var toParent = normalize(DrawAhead.Orbit(Zone, Zone.Orbits[planetInstance.BodyData.Orbit.Key].Data.Parent.Key, lead) - p);
|
||||
gasGiantObject.SunMaterial.LightingDirection = new Vector3(toParent.x, 0, toParent.y);
|
||||
}
|
||||
}
|
||||
|
|
@ -600,7 +601,7 @@ public class ZoneRenderer : MonoBehaviour
|
|||
{
|
||||
if(entityInstance.CompassIcon)
|
||||
{
|
||||
var difference = entityInstance.Entity.Position.xz - PerspectiveEntity.Position.xz;
|
||||
var difference = DrawAhead.Position(entityInstance.Entity, lead).xz - DrawAhead.Position(PerspectiveEntity, lead).xz;
|
||||
var distance = length(difference);
|
||||
|
||||
entityInstance.CompassIcon.gameObject.SetActive(
|
||||
|
|
@ -612,7 +613,7 @@ public class ZoneRenderer : MonoBehaviour
|
|||
|
||||
foreach (var wormhole in WormholeInstances.Values)
|
||||
{
|
||||
var difference = wormhole.gravity.transform.position.Flatland() - PerspectiveEntity.Position.xz.ToUnity();
|
||||
var difference = wormhole.gravity.transform.position.Flatland() - DrawAhead.Position(PerspectiveEntity, lead).xz.ToUnity();
|
||||
var distance = difference.magnitude;
|
||||
wormhole.icon.gameObject.SetActive(distance > _minimapDistance);
|
||||
wormhole.icon.transform.rotation = Quaternion.Euler(0, 0, Mathf.Atan2(difference.y, difference.x) * Mathf.Rad2Deg - 90);
|
||||
|
|
@ -716,7 +717,7 @@ public class AsteroidBeltUI
|
|||
|
||||
public void Update(float4[] transforms, float height)
|
||||
{
|
||||
var parentPosition = _zone.GetOrbitPosition(_orbitParent);
|
||||
var parentPosition = DrawAhead.Orbit(_zone, _orbitParent, ActionGameManager.Clock.Lead);
|
||||
for (var i = 0; i < transforms.Length; i++)
|
||||
{
|
||||
var transform = transforms[i];
|
||||
|
|
|
|||
|
|
@ -67,28 +67,28 @@ public sealed partial class FireControlCut124Tests
|
|||
// The shooter moves on after the round left: the line is the one frozen at Fire, not one drawn from where it is now.
|
||||
e.Shooter.Position += float3(7, 0, 3);
|
||||
|
||||
var start = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime, 0f);
|
||||
var start = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime);
|
||||
Assert.Equal(origin.x, start.x, 3);
|
||||
Assert.Equal(origin.z, start.z, 3);
|
||||
|
||||
var second = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime + 1f, 0f);
|
||||
var second = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime + 1f);
|
||||
Assert.Equal(origin.x + aim.x * BallisticSpeed, second.x, 3);
|
||||
Assert.Equal(origin.z + aim.y * BallisticSpeed, second.z, 3);
|
||||
|
||||
foreach (var known in new[] { null, outcome })
|
||||
foreach (var later in new[] { 0f, 1f, 50f })
|
||||
{
|
||||
var arrival = DrawAhead.Round(burst, known, burst.FireOrigin, 0f, burst.ArrivalTime + later, 0f);
|
||||
var arrival = DrawAhead.Round(burst, known, burst.FireOrigin, 0f, burst.ArrivalTime + later);
|
||||
Assert.Equal(burst.BurstPosition.x, arrival.x, 3);
|
||||
Assert.Equal(burst.BurstPosition.z, arrival.z, 3);
|
||||
}
|
||||
|
||||
// Past the weapon's range it is held at the range point, however late it is asked.
|
||||
var held = DrawAhead.Round(burst, Known(burst, ShotResult.Miss), burst.FireOrigin, 0f, burst.FireTime + NoLockRange / BallisticSpeed + 50f, 0f);
|
||||
var held = DrawAhead.Round(burst, Known(burst, ShotResult.Miss), burst.FireOrigin, 0f, burst.FireTime + NoLockRange / BallisticSpeed + 50f);
|
||||
Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3);
|
||||
Assert.Equal(origin.z + aim.y * NoLockRange, held.z, 3);
|
||||
// Before it was fired it is still at the origin, not behind it.
|
||||
var before = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime - 5f, 0f);
|
||||
var before = DrawAhead.Round(burst, null, burst.FireOrigin, 0f, burst.FireTime - 5f);
|
||||
Assert.Equal(origin.x, before.x, 3);
|
||||
Assert.Equal(origin.z, before.z, 3);
|
||||
|
||||
|
|
@ -100,12 +100,12 @@ public sealed partial class FireControlCut124Tests
|
|||
// A round not known to miss waits at its arrival point however late it is asked.
|
||||
foreach (var at in new[] { direct.ArrivalTime, direct.FireTime + 99f })
|
||||
{
|
||||
var waiting = DrawAhead.Round(direct, null, direct.FireOrigin, 0f, at, 0f);
|
||||
var waiting = DrawAhead.Round(direct, null, direct.FireOrigin, 0f, at);
|
||||
Assert.Equal(d.Target.Position.x, waiting.x, 2);
|
||||
Assert.Equal(d.Target.Position.z, waiting.z, 2);
|
||||
}
|
||||
Assert.Equal(dOrigin.z + 100f / 2f, DrawAhead.Round(direct, null, direct.FireOrigin, 0f, direct.FireTime + 2.5f, 0f).z, 2);
|
||||
var heldDirect = DrawAhead.Round(direct, Known(direct, ShotResult.Miss), direct.FireOrigin, 0f, direct.FireTime + 1000f / BallisticSpeed + 99f, 0f);
|
||||
Assert.Equal(dOrigin.z + 100f / 2f, DrawAhead.Round(direct, null, direct.FireOrigin, 0f, direct.FireTime + 2.5f).z, 2);
|
||||
var heldDirect = DrawAhead.Round(direct, Known(direct, ShotResult.Miss), direct.FireOrigin, 0f, direct.FireTime + 1000f / BallisticSpeed + 99f);
|
||||
Assert.Equal(dOrigin.z + 1000f, heldDirect.z, 2);
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ public sealed partial class FireControlCut124Tests
|
|||
FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
|
||||
var shot = SafeAssert.OnlyShot(e.Zone);
|
||||
|
||||
var late = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.FireTime + 5000f, 0f);
|
||||
var late = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.FireTime + 5000f);
|
||||
Assert.Equal(origin.x, late.x, 3);
|
||||
Assert.Equal(origin.z, late.z, 3);
|
||||
}
|
||||
|
|
@ -139,7 +139,7 @@ public sealed partial class FireControlCut124Tests
|
|||
var after = SafeAssert.OnlyShot(e.Zone);
|
||||
Assert.Equal(BallisticSpeed, after.Speed, 4);
|
||||
var aim = normalize(float2(2, 1));
|
||||
var at = DrawAhead.Round(after, null, after.FireOrigin, 0f, after.FireTime + 1f, 0f);
|
||||
var at = DrawAhead.Round(after, null, after.FireOrigin, 0f, after.FireTime + 1f);
|
||||
Assert.Equal(origin.x + aim.x * BallisticSpeed, at.x, 3);
|
||||
Assert.Equal(origin.z + aim.y * BallisticSpeed, at.z, 3);
|
||||
}
|
||||
|
|
@ -164,8 +164,8 @@ public sealed partial class FireControlCut124Tests
|
|||
for (var i = 0; i < 25; i++)
|
||||
{
|
||||
var at = shot.FireTime + i * .37f;
|
||||
DrawAhead.Round(shot, known, barrel, .1f, at, .01f);
|
||||
DrawAhead.RoundOver(shot, known, at, .01f);
|
||||
DrawAhead.Round(shot, known, barrel, .1f, at);
|
||||
DrawAhead.RoundOver(shot, known, at);
|
||||
DrawAhead.RoundEnd(shot, known?.Result);
|
||||
DrawAhead.Impact(e.Target, cell, .01f * i);
|
||||
}
|
||||
|
|
@ -198,7 +198,7 @@ public sealed partial class FireControlCut124Tests
|
|||
Assert.Equal(NoLockRange, after.MaxRange, 4);
|
||||
Assert.Equal(endBefore, DrawAhead.RoundEnd(after, ShotResult.Miss), 4);
|
||||
var aim = normalize(float2(2, 1));
|
||||
var held = DrawAhead.Round(after, Known(after, ShotResult.Miss), after.FireOrigin, 0f, after.FireTime + 1000f, 0f);
|
||||
var held = DrawAhead.Round(after, Known(after, ShotResult.Miss), after.FireOrigin, 0f, after.FireTime + 1000f);
|
||||
Assert.Equal(origin.x + aim.x * NoLockRange, held.x, 3);
|
||||
Assert.Equal(origin.z + aim.y * NoLockRange, held.z, 3);
|
||||
}
|
||||
|
|
@ -232,12 +232,12 @@ public sealed partial class FireControlCut124Tests
|
|||
Assert.Equal(direct.FireTime + 100f / speed, DrawAhead.RoundEnd(direct, ShotResult.Burst), 4);
|
||||
Assert.Equal(direct.FireTime + 1000f / speed, DrawAhead.RoundEnd(direct, ShotResult.Miss), 4);
|
||||
|
||||
Assert.False(DrawAhead.RoundOver(direct, null, direct.FireTime + 10000f, 0f));
|
||||
Assert.False(DrawAhead.RoundOver(direct, null, direct.FireTime + 10000f));
|
||||
foreach (var result in new[] { ShotResult.Hit, ShotResult.Burst })
|
||||
Assert.False(DrawAhead.RoundOver(direct, Known(direct, result), direct.FireTime + 10000f, 0f));
|
||||
Assert.False(DrawAhead.RoundOver(direct, Known(direct, result), direct.FireTime + 10000f));
|
||||
var missEnd = direct.FireTime + 1000f / speed;
|
||||
Assert.False(DrawAhead.RoundOver(direct, Known(direct, ShotResult.Miss), missEnd - .01f, 0f));
|
||||
Assert.True(DrawAhead.RoundOver(direct, Known(direct, ShotResult.Miss), missEnd, 0f));
|
||||
Assert.False(DrawAhead.RoundOver(direct, Known(direct, ShotResult.Miss), missEnd - .01f));
|
||||
Assert.True(DrawAhead.RoundOver(direct, Known(direct, ShotResult.Miss), missEnd));
|
||||
|
||||
// A target beyond the weapon's range: a miss never ends short of its own arrival.
|
||||
var far = FireDirectRound(speed, 100f, targetRange: 150f);
|
||||
|
|
@ -269,17 +269,17 @@ public sealed partial class FireControlCut124Tests
|
|||
|
||||
foreach (var lead in new[] { 0f, step / 2f, .999f * step })
|
||||
{
|
||||
var waiting = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime, lead);
|
||||
var waiting = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime + lead);
|
||||
Assert.Equal(origin.x, waiting.x, 3);
|
||||
Assert.Equal(origin.z + 100f, waiting.z, 3);
|
||||
var missed = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.ArrivalTime, lead);
|
||||
var missed = DrawAhead.Round(shot, Known(shot, ShotResult.Miss), shot.FireOrigin, 0f, shot.ArrivalTime + lead);
|
||||
Assert.Equal(origin.x, missed.x, 3);
|
||||
Assert.Equal(origin.z + 100f + lead * speed, missed.z, 3);
|
||||
}
|
||||
|
||||
for (var time = shot.FireTime; time <= shot.ArrivalTime + 1f; time += 1f / 144f)
|
||||
{
|
||||
var drawn = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, time, 0f);
|
||||
var drawn = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, time);
|
||||
Assert.True(drawn.z - origin.z <= 100f + 1e-3f, "the round was drawn past its arrival point");
|
||||
}
|
||||
}
|
||||
|
|
@ -315,7 +315,7 @@ public sealed partial class FireControlCut124Tests
|
|||
Assert.Equal(fired.FireTime + 1.05f, record.ArrivalTime, 2);
|
||||
Assert.True(record.ArrivalTime < fired.ArrivalTime);
|
||||
|
||||
var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime, 0f);
|
||||
var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime);
|
||||
var expected = outcome.HasBurstPoint
|
||||
? outcome.BurstPoint
|
||||
: record.FireOrigin.xz + record.TravelDirection * (20f * (record.ArrivalTime - record.FireTime));
|
||||
|
|
@ -323,7 +323,7 @@ public sealed partial class FireControlCut124Tests
|
|||
Assert.Equal(expected.y, stop.z, 3);
|
||||
foreach (var later in new[] { 0f, .5f, 5f })
|
||||
{
|
||||
var drawn = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime + later, 0f);
|
||||
var drawn = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime + later);
|
||||
Assert.Equal(expected.x, drawn.x, 3);
|
||||
Assert.Equal(expected.y, drawn.z, 3);
|
||||
}
|
||||
|
|
@ -350,11 +350,11 @@ public sealed partial class FireControlCut124Tests
|
|||
Assert.Equal(want.z, got.z, 3);
|
||||
}
|
||||
|
||||
Close(barrel, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime, 0f));
|
||||
Close(Line(blend / 2f) + offset * .5f, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend / 2f, 0f));
|
||||
Close(Line(blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend, 0f));
|
||||
Close(Line(2f * blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + 2f * blend, 0f));
|
||||
Close(Line(0f), DrawAhead.Round(shot, null, barrel, 0f, shot.FireTime, 0f));
|
||||
Close(barrel, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime));
|
||||
Close(Line(blend / 2f) + offset * .5f, DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend / 2f));
|
||||
Close(Line(blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + blend));
|
||||
Close(Line(2f * blend), DrawAhead.Round(shot, null, barrel, blend, shot.FireTime + 2f * blend));
|
||||
Close(Line(0f), DrawAhead.Round(shot, null, barrel, 0f, shot.FireTime));
|
||||
}
|
||||
|
||||
// Placement depends on sim time alone: any history of frames walked to a checkpoint draws what a fresh call draws, which
|
||||
|
|
@ -379,18 +379,10 @@ public sealed partial class FireControlCut124Tests
|
|||
while (elapsed + 1e-4f < checkpoint)
|
||||
{
|
||||
elapsed = min(checkpoint, elapsed + frames[frame++ % frames.Length]);
|
||||
DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + elapsed, 0f);
|
||||
}
|
||||
var drawn = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint, 0f);
|
||||
var fresh = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint, 0f);
|
||||
// A frame that falls `lead` after the step draws what the sim time lead later would.
|
||||
foreach (var lead in new[] { 1f / 60f, 1f / 90f })
|
||||
{
|
||||
var ahead = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint - lead, lead);
|
||||
Assert.Equal(fresh.x, ahead.x, 3);
|
||||
Assert.Equal(fresh.y, ahead.y, 3);
|
||||
Assert.Equal(fresh.z, ahead.z, 3);
|
||||
DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + elapsed);
|
||||
}
|
||||
var drawn = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint);
|
||||
var fresh = DrawAhead.Round(shot, miss, shot.FireOrigin, .1f, shot.FireTime + checkpoint);
|
||||
Assert.Equal(fresh.z, drawn.z, 3);
|
||||
Assert.Equal(shot.FireOrigin.z + speed * min(checkpoint, 1000f / speed), drawn.z, 3);
|
||||
Assert.Equal(shot.FireOrigin.x, drawn.x, 3);
|
||||
|
|
@ -420,11 +412,13 @@ public sealed partial class FireControlCut124Tests
|
|||
e.Target.TurnRate = 3f;
|
||||
var cell = FarCell(e);
|
||||
var hit = Hit(shot, e.Target, cell, shot.CommitTime);
|
||||
var arrivalOnTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime, 0f);
|
||||
var arrivalOnTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, shot.ArrivalTime);
|
||||
var cellSize = e.Items.GameplaySettings.SchematicCellSize;
|
||||
|
||||
foreach (var lead in new[] { 0f, step / 2f, .999f * step })
|
||||
// The drawn time is the only input: the target's lead is that time less its zone's step time.
|
||||
foreach (var drawnTime in new[] { shot.ArrivalTime, shot.ArrivalTime + step / 2f, shot.ArrivalTime + .5f })
|
||||
{
|
||||
var lead = (float) (drawnTime - e.Zone.ExactTime);
|
||||
var position = e.Target.Position;
|
||||
var direction = e.Target.Direction;
|
||||
e.Target.Position += float3(e.Target.Velocity.x, 0, e.Target.Velocity.y) * lead;
|
||||
|
|
@ -439,13 +433,10 @@ public sealed partial class FireControlCut124Tests
|
|||
Assert.Equal(expected.x, impact.x, 3);
|
||||
Assert.Equal(expected.y, impact.y, 3);
|
||||
Assert.Equal(expected.z, impact.z, 3);
|
||||
foreach (var simTime in new[] { shot.ArrivalTime, shot.ArrivalTime + .5f })
|
||||
{
|
||||
var drawn = DrawAhead.Round(shot, hit, shot.FireOrigin, .1f, simTime, lead);
|
||||
Assert.Equal(expected.x, drawn.x, 3);
|
||||
Assert.Equal(expected.y, drawn.y, 3);
|
||||
Assert.Equal(expected.z, drawn.z, 3);
|
||||
}
|
||||
var drawn = DrawAhead.Round(shot, hit, shot.FireOrigin, .1f, drawnTime);
|
||||
Assert.Equal(expected.x, drawn.x, 3);
|
||||
Assert.Equal(expected.y, drawn.y, 3);
|
||||
Assert.Equal(expected.z, drawn.z, 3);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -464,26 +455,21 @@ public sealed partial class FireControlCut124Tests
|
|||
|
||||
foreach (var t in new[] { commit - 1f, commit })
|
||||
{
|
||||
var steered = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, t, 0f);
|
||||
var line = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, t, 0f);
|
||||
var steered = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, t);
|
||||
var line = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, t);
|
||||
Assert.Equal(line.x, steered.x, 3);
|
||||
Assert.Equal(line.y, steered.y, 3);
|
||||
Assert.Equal(line.z, steered.z, 3);
|
||||
}
|
||||
|
||||
var m = commit + 1f;
|
||||
var onTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, m, 0f);
|
||||
var end = DrawAhead.Impact(e.Target, cell, 0f);
|
||||
var onTheLine = DrawAhead.Round(shot, null, shot.FireOrigin, 0f, m);
|
||||
var end = DrawAhead.Impact(e.Target, cell, (float) (m - e.Zone.ExactTime));
|
||||
Assert.True(length((end - onTheLine).xz) > 1f, "the fixture put the impact cell on the line");
|
||||
var halfway = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, m, 0f);
|
||||
var halfway = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, m);
|
||||
Assert.Equal(onTheLine.x + (end.x - onTheLine.x) * .5f, halfway.x, 3);
|
||||
Assert.Equal(onTheLine.y + (end.y - onTheLine.y) * .5f, halfway.y, 3);
|
||||
Assert.Equal(onTheLine.z + (end.z - onTheLine.z) * .5f, halfway.z, 3);
|
||||
|
||||
var ahead = DrawAhead.Round(shot, hit, shot.FireOrigin, 0f, m - .02f, .02f);
|
||||
Assert.Equal(halfway.x, ahead.x, 3);
|
||||
Assert.Equal(halfway.y, ahead.y, 3);
|
||||
Assert.Equal(halfway.z, ahead.z, 3);
|
||||
}
|
||||
|
||||
// The sim states facts and draws nothing: no drawing function, and no lead, blend or barrel, lives in FireControl;
|
||||
|
|
@ -526,8 +512,8 @@ public sealed partial class FireControlCut124Tests
|
|||
|
||||
Assert.Equal(origin.x, outcome.BurstPoint.x, 3);
|
||||
Assert.Equal(origin.z + NoLockRange, outcome.BurstPoint.y, 3);
|
||||
Assert.Equal(origin.z + 50f, DrawAhead.Round(record, null, record.FireOrigin, 0f, record.ArrivalTime, 0f).z, 3);
|
||||
var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime, 0f);
|
||||
Assert.Equal(origin.z + 50f, DrawAhead.Round(record, null, record.FireOrigin, 0f, record.ArrivalTime).z, 3);
|
||||
var stop = DrawAhead.Round(record, outcome, record.FireOrigin, 0f, record.ArrivalTime);
|
||||
Assert.Equal(outcome.BurstPoint.x, stop.x, 3);
|
||||
Assert.Equal(outcome.BurstPoint.y, stop.z, 3);
|
||||
}
|
||||
|
|
|
|||
416
tests/Aetheria.Shared.Tests/DrawnPoseTests.cs
Normal file
416
tests/Aetheria.Shared.Tests/DrawnPoseTests.cs
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
/* 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;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
|
||||
// Every drawn pose is evaluated at one drawn time, T = Zone.ExactTime + Clock.Lead (DrawAhead.Time). The operator's report:
|
||||
// coasting beside a planet and looking at it, the planet jumped back and forth every frame, because the planet was drawn at
|
||||
// the last step's orbit while the ship was drawn a lead ahead of its last step. Each rail below is spelled here from the
|
||||
// field law outside the core (a = c / d, so T = 2 pi d / sqrt(c)), never by calling the production rail; the zone is built
|
||||
// by the production constructor and stepped by Zone.Update, and the clock is the production SimClock.
|
||||
public sealed class DrawnPoseTests : IDisposable
|
||||
{
|
||||
// Well strength is the body's mass (c = mass), with a small constant core, so every period is spelled from masses
|
||||
// chosen here. The sun's c = 400 puts its planet's rail speed at sqrt(c) = 20 u/s at any distance.
|
||||
private const float SunMass = 400f, PlanetMass = 25f, MoonMass = 4f;
|
||||
private const float PlanetDistance = 1000f, PlanetPhase = .3f;
|
||||
private const float MoonDistance = 60f, MoonPhase = .1f;
|
||||
private const float StationDistance = 3f, StationPhase = .4f;
|
||||
private const float GridOffset = 2.5f;
|
||||
// The sun's rail is parentless and off-centre, the shape ZoneGenerator gives a cluster root (FixedPosition = cluster centre).
|
||||
private static readonly float2 SunCentre = new float2(700f, -300f);
|
||||
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-drawnpose-" + Guid.NewGuid().ToString("N"));
|
||||
private readonly List<CultCache> _caches = new List<CultCache>();
|
||||
|
||||
public DrawnPoseTests() => Directory.CreateDirectory(_root);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var cache in _caches) cache.Dispose();
|
||||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private sealed class World
|
||||
{
|
||||
public Zone Zone;
|
||||
public ItemManager Items;
|
||||
public EquippableItem Hull;
|
||||
public CultRecordKey PlanetOrbit, MoonOrbit, StationOrbit, Belt;
|
||||
public Asteroid[] Asteroids;
|
||||
public float HeightExaggeration => Zone.Settings.HeightExaggeration;
|
||||
}
|
||||
|
||||
private static PlanetSettings Curves() => new PlanetSettings
|
||||
{
|
||||
BodyRadius = new ExponentialCurve(),
|
||||
WellStrength = new ExponentialCurve { Multiplier = 1f, Exponent = 1f },
|
||||
RocheLimit = new ExponentialCurve { Constant = 2f },
|
||||
AsteroidSize = new ExponentialLerp { Minimum = 2f, Maximum = 9f, Exponent = 1.7f },
|
||||
AsteroidHitpoints = new ExponentialLerp { Minimum = 10f, Maximum = 300f, Exponent = 2f },
|
||||
AsteroidRespawnTime = new ExponentialLerp { Minimum = 5f, Maximum = 60f, Exponent = 1f }
|
||||
};
|
||||
|
||||
// A sun on a parentless orbit, a planet round it, a moon round the planet, a bodyless station rail close round the planet,
|
||||
// and a belt round the planet; a hull to ride the field with.
|
||||
private World Build()
|
||||
{
|
||||
var dir = Path.Combine(_root, $"world{_caches.Count}");
|
||||
Directory.CreateDirectory(dir);
|
||||
var catalog = Path.Combine(dir, "Aetheria.cc");
|
||||
var shape = new Shape(3, 3);
|
||||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
using (var build = AetheriaStores.Open(catalog, catalogWritable: true))
|
||||
{
|
||||
build.Upsert(new VerseGrammar { Revision = 1 });
|
||||
build.Upsert(new HullData { Name = "TestHull", HullType = HullType.Ship, Shape = shape, Durability = 1, Mass = 1000, GridOffset = GridOffset });
|
||||
build.FlushAsync().Wait();
|
||||
}
|
||||
|
||||
var cache = AetheriaStores.Open(catalog, Path.Combine(dir, "run.cc"), catalogWritable: true);
|
||||
_caches.Add(cache);
|
||||
var ledger = new ProvenanceLedger();
|
||||
ledger.Lots[1] = new Lot { Origin = new Attributed(), Quality = 1 };
|
||||
var items = new ItemManager(cache, ledger, new GameplaySettings
|
||||
{
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp()
|
||||
}, _ => { });
|
||||
|
||||
CultRecordKey Orbit(CultRecordKey parent, float distance, float phase, float2 fixedPosition = default)
|
||||
{
|
||||
var data = new OrbitData { Distance = distance, Phase = phase, FixedPosition = fixedPosition };
|
||||
if (parent.IsSet()) data.Parent = new CultRecordRef<OrbitData>(parent);
|
||||
return cache.Upsert(data).Key;
|
||||
}
|
||||
|
||||
CultRecordKey Body(CultRecordKey orbit, string name, float mass) =>
|
||||
cache.Upsert(new PlanetData { Name = name, Orbit = new CultRecordRef<OrbitData>(orbit), Mass = mass }).Key;
|
||||
|
||||
var sunOrbit = Orbit(default, 0f, 0f, SunCentre);
|
||||
var planetOrbit = Orbit(sunOrbit, PlanetDistance, PlanetPhase);
|
||||
var moonOrbit = Orbit(planetOrbit, MoonDistance, MoonPhase);
|
||||
var stationOrbit = Orbit(planetOrbit, StationDistance, StationPhase);
|
||||
var beltOrbit = Orbit(planetOrbit, 400f, .42f);
|
||||
var asteroids = Enumerable.Range(0, 6).Select(i => new Asteroid
|
||||
{
|
||||
Distance = 120f + i * 11.3f, Phase = frac(i * .6180339887f), Size = frac(i * .37f + .1f), RotationSpeed = .15f + i * .037f
|
||||
}).ToArray();
|
||||
var belt = cache.Upsert(new AsteroidBeltData { Orbit = new CultRecordRef<OrbitData>(beltOrbit), Asteroids = asteroids }).Key;
|
||||
|
||||
var pack = new ZonePack { Radius = 5000f, Mass = 10000f, Time = 0 };
|
||||
foreach (var orbit in new[] { sunOrbit, planetOrbit, moonOrbit, stationOrbit, beltOrbit })
|
||||
pack.Orbits.Add(new CultRecordRef<OrbitData>(orbit));
|
||||
foreach (var body in new[] { Body(sunOrbit, "Sun", SunMass), Body(planetOrbit, "Planet", PlanetMass), Body(moonOrbit, "Moon", MoonMass), belt })
|
||||
pack.Planets.Add(new CultRecordRef<BodyData>(body));
|
||||
|
||||
var zone = new Zone(items, Curves(), pack, new GalaxyZone { Name = "DrawnPose", Owner = null }, null);
|
||||
var hull = new EquippableItem { Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("TestHull")), Durability = 1, Lot = 1 };
|
||||
return new World { Zone = zone, Items = items, Hull = hull, PlanetOrbit = planetOrbit, MoonOrbit = moonOrbit, StationOrbit = stationOrbit, Belt = belt, Asteroids = asteroids };
|
||||
}
|
||||
|
||||
// ---- The rails, spelled from the field law ----
|
||||
|
||||
private static float2 Around(float2 center, double time, float distance, float phase, float strength)
|
||||
{
|
||||
var period = 2 * PI * distance / sqrt(strength);
|
||||
var wrapped = (float) frac(time / period);
|
||||
return center + OrbitData.Evaluate(frac(wrapped + phase)) * distance;
|
||||
}
|
||||
|
||||
private static float2 PlanetAt(double time) => Around(SunCentre, time, PlanetDistance, PlanetPhase, SunMass);
|
||||
private static float2 MoonAt(double time) => Around(PlanetAt(time), time, MoonDistance, MoonPhase, PlanetMass);
|
||||
private static float2 StationAt(double time) => Around(PlanetAt(time), time, StationDistance, StationPhase, PlanetMass);
|
||||
|
||||
// The field at a point at a time, with each well where its rail puts it then.
|
||||
private static float FieldAt(World w, float2 point, double time)
|
||||
{
|
||||
var wells = new (float2 At, float Strength)[] { (SunCentre, SunMass), (PlanetAt(time), PlanetMass), (MoonAt(time), MoonMass) };
|
||||
var sum = 0f;
|
||||
foreach (var (at, strength) in wells)
|
||||
sum += GravityField.Potential(strength, 2f, length(point - at)) - GravityField.Potential(strength, 2f, w.Zone.Pack.Radius);
|
||||
return sum / w.HeightExaggeration;
|
||||
}
|
||||
|
||||
private static void Step(World w, float seconds, int times)
|
||||
{
|
||||
for (var i = 0; i < times; i++) w.Zone.Update(seconds);
|
||||
}
|
||||
|
||||
// ---- The tests ----
|
||||
|
||||
// The operator's report, from the production clock: a ship coasting at its planet's step velocity, 60 u beside it, seen
|
||||
// across display frames. Frames are 1/144 s (the report), and 75, 165 and 240 Hz, where a clamp or a fraction of the
|
||||
// lead would also show; at 60 Hz Lead is always 0 and could not tell. The planet minus the ship must not swing in either
|
||||
// world axis (0.306 u along the motion when the planet is drawn at the step's orbit).
|
||||
[Theory]
|
||||
[InlineData(144)]
|
||||
[InlineData(75)]
|
||||
[InlineData(165)]
|
||||
[InlineData(240)]
|
||||
public void ACoMovingShipSeesAStillPlanetAcrossFrames(int displayHz)
|
||||
{
|
||||
var w = Build();
|
||||
var ship = new Ship(w.Items, w.Zone, w.Hull, new EntitySettings());
|
||||
var clock = new SimClock(1f / 60f);
|
||||
var xs = new List<float>();
|
||||
var ys = new List<float>();
|
||||
var leads = new List<float>();
|
||||
|
||||
for (var frame = 0; frame < displayHz * 2; frame++)
|
||||
{
|
||||
clock.Advance(1f / displayHz, dt =>
|
||||
{
|
||||
w.Zone.Update(dt);
|
||||
var rail = w.Zone.Orbits[w.PlanetOrbit];
|
||||
ship.Position = float3(rail.Position.x + 60f, 0, rail.Position.y);
|
||||
ship.Velocity = rail.Velocity;
|
||||
});
|
||||
if (clock.Steps == 0) continue;
|
||||
var lead = clock.Lead;
|
||||
var relative = DrawAhead.Orbit(w.Zone, w.PlanetOrbit, lead) - DrawAhead.Position(ship, lead).xz;
|
||||
xs.Add(relative.x);
|
||||
ys.Add(relative.y);
|
||||
leads.Add(lead);
|
||||
}
|
||||
|
||||
Assert.True(xs.Count > displayHz, "the run covered its frames");
|
||||
Assert.True(leads.Max() > .005f, $"{displayHz} Hz: the frames fall far enough past a step to tell a lead from none");
|
||||
Assert.True(xs.Max() - xs.Min() < .005f, $"{displayHz} Hz: the planet swings {xs.Max() - xs.Min():F4} u in x against the ship");
|
||||
Assert.True(ys.Max() - ys.Min() < .005f, $"{displayHz} Hz: the planet swings {ys.Max() - ys.Min():F4} u in z against the ship");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void APlanetIsDrawnAtItsOrbitAtTheDrawnTime()
|
||||
{
|
||||
var w = Build();
|
||||
Step(w, 1f / 60f, 803);
|
||||
foreach (var lead in new[] { 0f, .004f, .0123f, .0166f })
|
||||
{
|
||||
var time = w.Zone.ExactTime + lead;
|
||||
AssertNear(PlanetAt(time), DrawAhead.Orbit(w.Zone, w.PlanetOrbit, lead), 1e-3f, $"planet at lead {lead}");
|
||||
AssertNear(MoonAt(time), DrawAhead.Orbit(w.Zone, w.MoonOrbit, lead), 1e-3f, $"moon (chained through the planet) at lead {lead}");
|
||||
}
|
||||
|
||||
Assert.Equal(w.Zone.Orbits[w.PlanetOrbit].Position, DrawAhead.Orbit(w.Zone, w.PlanetOrbit, 0f));
|
||||
Assert.Equal(w.Zone.Orbits[w.MoonOrbit].Position, DrawAhead.Orbit(w.Zone, w.MoonOrbit, 0f));
|
||||
Assert.True(length(DrawAhead.Orbit(w.Zone, w.PlanetOrbit, .0166f) - w.Zone.Orbits[w.PlanetOrbit].Position) > .1f, "the planet really moves over a lead");
|
||||
}
|
||||
|
||||
// The chain rule (a root at its FixedPosition, a child at its parent's node plus its offset) holds at the step's time
|
||||
// (the memo) and at a drawn time (the rail) alike: an off-centre root is the same place either way, and the planet
|
||||
// round it follows it.
|
||||
[Fact]
|
||||
public void ARootSitsAtItsFixedPositionAtEveryTime()
|
||||
{
|
||||
var w = Build();
|
||||
var sunOrbit = w.Zone.Orbits.Single(o => !o.Value.Data.Parent.IsSet()).Key;
|
||||
Step(w, 1f / 60f, 200);
|
||||
foreach (var lead in new[] { 0f, .004f, .0166f })
|
||||
{
|
||||
AssertNear(SunCentre, DrawAhead.Orbit(w.Zone, sunOrbit, lead), 1e-3f, $"root at lead {lead}");
|
||||
AssertNear(PlanetAt(w.Zone.ExactTime + lead), DrawAhead.Orbit(w.Zone, w.PlanetOrbit, lead), 1e-3f, $"planet round the root at lead {lead}");
|
||||
}
|
||||
|
||||
Assert.True(length(w.Zone.Orbits[w.PlanetOrbit].Position - SunCentre) > PlanetDistance - 1f, "the planet circles the off-centre root, not the origin");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrbitPositionAtWritesNothing()
|
||||
{
|
||||
var w = Build();
|
||||
Step(w, 1f / 60f, 100);
|
||||
var keys = w.Zone.Orbits.Keys.ToArray();
|
||||
var positions = keys.Select(k => w.Zone.Orbits[k].Position).ToArray();
|
||||
var velocities = keys.Select(k => w.Zone.Orbits[k].Velocity).ToArray();
|
||||
var probes = new[] { float2(1100f, 20f), float2(980f, -40f), SunCentre };
|
||||
var heights = probes.Select(p => w.Zone.GetHeight(p)).ToArray();
|
||||
|
||||
var later = w.Zone.ExactTime + .5;
|
||||
foreach (var key in keys) w.Zone.OrbitPositionAt(key, later);
|
||||
foreach (var p in probes) w.Zone.GetHeight(p, later);
|
||||
w.Zone.ChunkPose(w.Belt, 0, later);
|
||||
|
||||
for (var i = 0; i < keys.Length; i++)
|
||||
{
|
||||
Assert.Equal(positions[i], w.Zone.Orbits[keys[i]].Position);
|
||||
Assert.Equal(velocities[i], w.Zone.Orbits[keys[i]].Velocity);
|
||||
Assert.Equal(positions[i], w.Zone.OrbitPositionAt(keys[i], w.Zone.ExactTime));
|
||||
}
|
||||
|
||||
for (var i = 0; i < probes.Length; i++) Assert.Equal(heights[i], w.Zone.GetHeight(probes[i]));
|
||||
Assert.True(length(w.Zone.OrbitPositionAt(w.PlanetOrbit, later) - positions[Array.IndexOf(keys, w.PlanetOrbit)]) > 1f, "the later time is a different place");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheDrawnFieldMovesWithTheDrawnWell()
|
||||
{
|
||||
var w = Build();
|
||||
Step(w, 1f / 60f, 500);
|
||||
var probe = PlanetAt(w.Zone.ExactTime) + float2(30f, 0);
|
||||
Assert.Equal(w.Zone.GetHeight(probe), w.Zone.GetHeight(probe, w.Zone.ExactTime));
|
||||
|
||||
const float lead = .0166f;
|
||||
var time = w.Zone.ExactTime + lead;
|
||||
var drawn = DrawAhead.Orbit(w.Zone, w.PlanetOrbit, lead) + float2(30f, 0);
|
||||
Assert.Equal(FieldAt(w, drawn, time), DrawAhead.Height(w.Zone, drawn, lead), 3);
|
||||
Assert.True(abs(DrawAhead.Height(w.Zone, drawn, lead) - w.Zone.GetHeight(drawn)) > .01f, "the field has moved with its well over a lead");
|
||||
|
||||
// A ship that stands still 40 u from the planet rides the field of the drawn well, plus its hull's offset.
|
||||
var ship = new Ship(w.Items, w.Zone, w.Hull, new EntitySettings()) { Position = float3(PlanetAt(w.Zone.ExactTime).x + 40f, 0, PlanetAt(w.Zone.ExactTime).y) };
|
||||
var drawnShip = DrawAhead.Position(ship, lead);
|
||||
Assert.Equal(FieldAt(w, drawnShip.xz, time) + GridOffset, drawnShip.y, 3);
|
||||
Assert.Equal(ship.RideHeight(drawnShip.xz, time), drawnShip.y, 4);
|
||||
Assert.Equal(w.Zone.GetHeight(ship.Position.xz) + GridOffset, ship.RideHeight(ship.Position.xz), 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AStationIsDrawnOnItsOrbit()
|
||||
{
|
||||
var w = Build();
|
||||
const float step = .1f;
|
||||
Step(w, step, 40);
|
||||
var station = new OrbitalEntity(w.Items, w.Zone, w.Hull, w.StationOrbit, new EntitySettings());
|
||||
var rail = w.Zone.Orbits[w.StationOrbit];
|
||||
station.Position = float3(rail.Position.x, 0, rail.Position.y);
|
||||
station.Velocity = rail.Velocity;
|
||||
const float lead = .08f;
|
||||
|
||||
var drawn = DrawAhead.Position(station, lead);
|
||||
|
||||
AssertNear(StationAt(w.Zone.ExactTime + lead), drawn.xz, 2e-3f, "the station on its rail at the drawn time");
|
||||
AssertNear(DrawAhead.Orbit(w.Zone, w.StationOrbit, lead), drawn.xz, 1e-6f, "the same point DrawAhead.Orbit gives");
|
||||
Assert.True(length(station.Position.xz + station.Velocity * lead - drawn.xz) > .01f, "a tight rail curves away from its straight extrapolation");
|
||||
Assert.Equal(w.Zone.GetHeight(drawn.xz, w.Zone.ExactTime + lead) + GridOffset, drawn.y, 4);
|
||||
}
|
||||
|
||||
// The generators make orbital entities with no orbit set (Loadout, LoadoutGenerator): a rail-less one is an ordinary
|
||||
// integrated body, drawn at its step plus velocity times lead, never at the origin an unset rail would give.
|
||||
[Fact]
|
||||
public void AnOrbitalEntityWithoutAnOrbitIsDrawnWhereItIs()
|
||||
{
|
||||
var w = Build();
|
||||
Step(w, 1f / 60f, 30);
|
||||
var station = new OrbitalEntity(w.Items, w.Zone, w.Hull, default, new EntitySettings())
|
||||
{
|
||||
Position = float3(520f, 0, -140f),
|
||||
Velocity = float2(3f, 4f)
|
||||
};
|
||||
const float lead = .01f;
|
||||
|
||||
var drawn = DrawAhead.Position(station, lead);
|
||||
|
||||
AssertNear(float2(520f, -140f) + float2(3f, 4f) * lead, drawn.xz, 1e-4f, "an unrailed station");
|
||||
Assert.Equal(station.RideHeight(drawn.xz, w.Zone.ExactTime + lead), drawn.y, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ABeltIsDrawnAtTheDrawnTime()
|
||||
{
|
||||
var w = Build();
|
||||
Step(w, 1f / 60f, 301);
|
||||
var time = w.Zone.ExactTime + .01;
|
||||
var atStep = new float4[w.Asteroids.Length];
|
||||
var drawn = new float4[w.Asteroids.Length];
|
||||
w.Zone.EvaluateBelt(w.Belt, atStep, w.Zone.ExactTime);
|
||||
w.Zone.EvaluateBelt(w.Belt, drawn, time);
|
||||
|
||||
var parent = PlanetAt(time);
|
||||
for (var i = 0; i < w.Asteroids.Length; i++)
|
||||
{
|
||||
var asteroid = w.Asteroids[i];
|
||||
var period = 2 * PI * asteroid.Distance / sqrt(PlanetMass);
|
||||
var expected = OrbitData.Evaluate((float) frac(time / period + asteroid.Phase)) * asteroid.Distance + parent;
|
||||
AssertNear(expected, drawn[i].xy, 1e-3f, $"chunk {i} drawn");
|
||||
Assert.Equal(w.Zone.ChunkPose(w.Belt, i, time), drawn[i]);
|
||||
Assert.Equal(w.Zone.ChunkPose(w.Belt, i, w.Zone.ExactTime), atStep[i]);
|
||||
Assert.Equal(w.Zone.ChunkPose(w.Belt, i), atStep[i]);
|
||||
Assert.True(length(drawn[i].xy - atStep[i].xy) > .01f, $"chunk {i} has moved over the lead");
|
||||
}
|
||||
}
|
||||
|
||||
// The well the shader takes is its body's rail at the drawn time: the lowering copies DrawAhead.Well and decides
|
||||
// nothing, so the drawn well and the drawn planet share one pose at a non-zero lead, and the strength, core and
|
||||
// reference are the field law's (spelled here from the masses, the core 2 and the zone's edge), not the step's.
|
||||
[Fact]
|
||||
public void AWellIsLoweredAtTheDrawnTime()
|
||||
{
|
||||
var w = Build();
|
||||
Step(w, 1f / 60f, 803);
|
||||
foreach (var lead in new[] { 0f, .004f, .0166f })
|
||||
{
|
||||
var time = w.Zone.ExactTime + lead;
|
||||
foreach (var body in w.Zone.PlanetInstances.Values)
|
||||
{
|
||||
var key = body.BodyData.Orbit.Key;
|
||||
var (at, strength) = key.Equals(w.PlanetOrbit) ? (PlanetAt(time), PlanetMass)
|
||||
: key.Equals(w.MoonOrbit) ? (MoonAt(time), MoonMass)
|
||||
: (SunCentre, SunMass);
|
||||
var well = DrawAhead.Well(w.Zone, body, lead);
|
||||
AssertNear(at, well.Position, 1e-3f, $"well of the {strength} body at lead {lead}");
|
||||
Assert.Equal(strength, well.Strength, 3);
|
||||
Assert.Equal(2f, well.CoreRadius, 3);
|
||||
Assert.Equal(GravityField.Potential(strength, 2f, w.Zone.Pack.Radius), well.Reference, 3);
|
||||
}
|
||||
}
|
||||
|
||||
var planet = w.Zone.PlanetInstances.Values.Single(b => b.BodyData.Orbit.Key.Equals(w.PlanetOrbit));
|
||||
Assert.True(length(DrawAhead.Well(w.Zone, planet, .0166f).Position - DrawAhead.Well(w.Zone, planet, 0f).Position) > .1f, "the well moves over a lead");
|
||||
AssertNear(w.Zone.Orbits[w.PlanetOrbit].Position, DrawAhead.Well(w.Zone, planet, 0f).Position, 0f, "at lead 0 the well is the step's memo");
|
||||
}
|
||||
|
||||
// A HUD marker round the ship the camera follows (an incoming lock, a contact's bearing, a ping's flash) is anchored on
|
||||
// that ship's drawn position, so it holds still against the drawn hull at every lead. A marker anchored on the step's
|
||||
// position slides v * lead off it and snaps back each step.
|
||||
[Fact]
|
||||
public void AMarkerAtABearingIsAnchoredOnTheDrawnShip()
|
||||
{
|
||||
var w = Build();
|
||||
Step(w, 1f / 60f, 30);
|
||||
var ship = new OrbitalEntity(w.Items, w.Zone, w.Hull, default, new EntitySettings())
|
||||
{
|
||||
Position = float3(520f, 0, -140f),
|
||||
Velocity = float2(12f, -5f)
|
||||
};
|
||||
var bearing = normalize(float2(.6f, .8f));
|
||||
foreach (var lead in new[] { 0f, .004f, .0166f })
|
||||
{
|
||||
var drawn = DrawAhead.Position(ship, lead);
|
||||
var marker = DrawAhead.AtBearing(ship, bearing, 7f, lead);
|
||||
AssertNear(float2(520f, -140f) + float2(12f, -5f) * lead + bearing * 7f, marker.xz, 1e-4f, $"marker at lead {lead}");
|
||||
Assert.Equal(drawn.y, marker.y, 4);
|
||||
AssertNear(drawn.xz, marker.xz - bearing * 7f, 1e-4f, $"the marker's anchor is the drawn ship at lead {lead}");
|
||||
}
|
||||
}
|
||||
|
||||
// An orbit whose parent the zone does not hold is reported when a rail is evaluated at a drawn time.
|
||||
[Fact]
|
||||
public void AMissingParentOrbitIsLoggedAtADrawnTime()
|
||||
{
|
||||
var w = Build();
|
||||
Step(w, 1f / 60f, 30);
|
||||
var log = new List<string>();
|
||||
w.Zone.Log = log.Add;
|
||||
w.Zone.Orbits.Remove(w.PlanetOrbit);
|
||||
foreach (var lead in new[] { .004f, .5f })
|
||||
{
|
||||
log.Clear();
|
||||
w.Zone.OrbitPositionAt(w.MoonOrbit, w.Zone.ExactTime + lead);
|
||||
Assert.Contains("Requested orbit is not part of this zone!", log);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertNear(float2 expected, float2 actual, float eps, string what) =>
|
||||
Assert.True(length(expected - actual) <= eps, $"{what}: expected {expected}, got {actual} (eps {eps})");
|
||||
}
|
||||
|
|
@ -242,7 +242,7 @@ public sealed class MiningCut1Tests : IDisposable
|
|||
zone.Update(0.37f);
|
||||
|
||||
var buffer = new float4[asteroids.Length];
|
||||
zone.EvaluateBelt(beltKey, buffer);
|
||||
zone.EvaluateBelt(beltKey, buffer, zone.ExactTime);
|
||||
|
||||
Assert.Equal(asteroids.Length, buffer.Length);
|
||||
for (var i = 0; i < asteroids.Length; i++)
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ public sealed partial class RunStartTests
|
|||
const float lead = .007f;
|
||||
var ahead = DrawAhead.Position(player, lead);
|
||||
Assert.Equal(player.Position.x + player.Velocity.x * lead, ahead.x, 6);
|
||||
Assert.Equal(player.RideHeight(float2(ahead.x, ahead.z)), ahead.y, 4);
|
||||
Assert.Equal(player.RideHeight(float2(ahead.x, ahead.z), player.Zone.ExactTime + lead), ahead.y, 4);
|
||||
Assert.Equal(player.Position.z + player.Velocity.y * lead, ahead.z, 6);
|
||||
Assert.Equal(before, Snapshot(arena, new List<string>()));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue