Merge gravity-fog-precision-and-jitter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-15 19:52:55 +02:00
commit 49e8ea58e1
13 changed files with 129 additions and 22 deletions

View file

@ -255,6 +255,19 @@ namespace Aetheria.EditorTools
// menu's own brush objects (Boundary on layer 9, the Waves on layer 10) and are presentation by spec (follow-up
// menu-grid-brushes), not a reader of the potential. This pins that: a ZoneRenderer in the menu would make its
// cameras readers of the field, and they would have to cull layer 9 alone like ARPG's.
static void CheckTestSceneWiring()
{
EditorSceneManager.OpenScene("Assets/Scenes/TestScene.unity", OpenSceneMode.Single);
try
{
if (CheckPatchCameras("TestScene.unity") < 1) Fail("wiring", "no Fog Patch Displacement camera in TestScene.unity");
}
finally
{
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
}
}
static void CheckMenuWiring()
{
var scene = EditorSceneManager.OpenScene("Assets/Scenes/Main Menu.unity", OpenSceneMode.Single);
@ -272,6 +285,7 @@ namespace Aetheria.EditorTools
Fail("wiring", $"Main Menu camera {camera.name} culls mask {camera.cullingMask}, not the menu brush layers 9 and 10");
}
if (seen != 1) Fail("wiring", $"{seen} Fog Surface Displacement cameras in Main Menu.unity, expected 1");
if (CheckPatchCameras("Main Menu.unity") < 1) Fail("wiring", "no Fog Patch Displacement camera in Main Menu.unity");
}
finally
{
@ -279,6 +293,21 @@ namespace Aetheria.EditorTools
}
}
// Every scene's Fog Patch Displacement camera: cloudDensity adds the R32 gravity height to the patch texture, so
// a patch camera that culls the gravity layer counts the field twice. Returns how many it found in the open scene.
static int CheckPatchCameras(string sceneName)
{
var found = 0;
foreach (var camera in UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Include, FindObjectsSortMode.None))
{
if (camera.targetTexture == null || camera.targetTexture.name != "Fog Patch Displacement") continue;
found++;
if ((camera.cullingMask & 1 << GravityLowering.FieldLayer) != 0)
Fail("wiring", $"camera {camera.name} (target Fog Patch Displacement) in {sceneName} culls layer {GravityLowering.FieldLayer}, the gravity layer");
}
return found;
}
static void CheckWiring(GameSettings settings)
{
var scene = EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Single);
@ -295,6 +324,9 @@ namespace Aetheria.EditorTools
}
if (seen < gravityTargets.Length) Fail("wiring", $"only {seen} gravity cameras found in ARPG.unity");
// The patch texture is half-float: it holds the patch layers only, the nebula reads gravity from the R32 surface.
if (CheckPatchCameras("ARPG.unity") != 1) Fail("wiring", "ARPG.unity does not have exactly 1 Fog Patch Displacement camera");
// The map screen's gravity camera has no target until MapRenderer gives it one at run time.
var map = UnityEngine.Object.FindFirstObjectByType<MapRenderer>(FindObjectsInactive.Include);
if (map == null || map.GravityCamera == null) Fail("wiring", "no MapRenderer.GravityCamera in ARPG.unity");
@ -316,6 +348,7 @@ namespace Aetheria.EditorTools
}
CheckMenuWiring();
CheckTestSceneWiring();
foreach (var path in new[]
{

View file

@ -109,9 +109,8 @@ public class CombatState : BaseState
// Cut 3 (docs/fire-control-cut.md): the one prediction function, shared with FireControl.Fire's
// own snapshot and the roll's deviation measurement -- two separate predictions here and in the
// roll would let an AI aim at one point and get judged for missing a different one.
var targetHullData = _agent.ItemManager.GetData(target.Hull) as HullData;
var predictedPosition = FireControl.PredictedIntercept(testWeapon, _agent.Ship, target);
predictedPosition.y = _agent.Ship.Zone.GetHeight(predictedPosition.xz) + targetHullData.GridOffset;
predictedPosition.y = target.RideHeight(predictedPosition.xz);
toTarget = normalize(predictedPosition - _agent.Ship.Position);
}

View file

@ -64,13 +64,12 @@ public class TurretController : Behavior, IInitializableBehavior
var diff = target.Position - Entity.Position;
if (_predictShots)
{
var targetHullData = Entity.ItemManager.GetData(target.Hull) as HullData;
var targetVelocity = float3(target.Velocity.x, 0, target.Velocity.y);
var predictedPosition = first_order_intercept(
Entity.Position, float3.zero, _shotSpeed,
target.Position, targetVelocity
);
predictedPosition.y = Entity.Zone.GetHeight(predictedPosition.xz) + targetHullData.GridOffset;
predictedPosition.y = target.RideHeight(predictedPosition.xz);
Entity.Aim = normalize(predictedPosition - Entity.Position);
}
else

View file

@ -7,17 +7,22 @@ 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. A ballistic round's flight is presentation
// 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.
public static float3 Position(Entity entity, float lead) =>
entity is Ship { WormholeAnimationInProgress: true }
? entity.Position
: entity.Position + float3(entity.Velocity.x, 0, entity.Velocity.y) * lead;
// 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.

View file

@ -1849,7 +1849,6 @@ public abstract class Entity
var velocityBefore = Velocity;
LimitClamp = float2(0, 0);
var hullData = ItemManager.GetData(Hull) as HullData;
// Mining Cut 3: a chunk target is lost the moment this entity can no longer see it -- the lost-track rule
// through the chunk path. One check of the one slot per tick, not a detection loop over chunks.
@ -1969,12 +1968,16 @@ public abstract class Entity
Position = Parent.Position;
Velocity = Parent.Velocity;
}
else Position.y = Zone.GetHeight(Position.xz) + hullData.GridOffset;
else Position.y = RideHeight(Position.xz);
Acceleration = delta > 0f ? (Velocity + LimitClamp - velocityBefore) / delta : float2(0, 0);
Manoeuvre.Observe(Acceleration, delta, ItemManager.GameplaySettings.SolutionWindow);
}
// 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;
private void UpdateTemperature(float delta)
{
var hullData = ItemManager.GetData(Hull) as HullData;