Merge gravity-fog-precision-and-jitter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
49e8ea58e1
13 changed files with 129 additions and 22 deletions
|
|
@ -8037,7 +8037,7 @@ Camera:
|
|||
m_Depth: 0
|
||||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 5632
|
||||
m_Bits: 5120
|
||||
m_RenderingPath: 1
|
||||
m_TargetTexture: {fileID: 8400000, guid: 06ec5ef8a5830fc44b1ac1a6e123add5, type: 2}
|
||||
m_TargetDisplay: 0
|
||||
|
|
|
|||
|
|
@ -982,7 +982,7 @@ Camera:
|
|||
m_Depth: 0
|
||||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 5632
|
||||
m_Bits: 5120
|
||||
m_RenderingPath: 1
|
||||
m_TargetTexture: {fileID: 8400000, guid: 06ec5ef8a5830fc44b1ac1a6e123add5, type: 2}
|
||||
m_TargetDisplay: 0
|
||||
|
|
|
|||
|
|
@ -3769,7 +3769,7 @@ Camera:
|
|||
m_Depth: 0
|
||||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 5632
|
||||
m_Bits: 5120
|
||||
m_RenderingPath: 1
|
||||
m_TargetTexture: {fileID: 8400000, guid: 06ec5ef8a5830fc44b1ac1a6e123add5, type: 2}
|
||||
m_TargetDisplay: 0
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Draws the zone's gravity: depth = -Zone.GetHeight at the world xz of every texel, into whichever gravity camera culls
|
||||
// layer 9 (Surface Displacement, Patch Displacement, Slime Gravity, both Map Gravity). One quad covers the zone; the
|
||||
// layer 9 (Surface Displacement, Slime Gravity, both Map Gravity). One quad covers the zone; the
|
||||
// law lives in Gravity.cginc, which mirrors ServerShared's GravityField.
|
||||
Shader "Brushes/Gravity Field" {
|
||||
Category {
|
||||
|
|
|
|||
|
|
@ -97,7 +97,9 @@ float cloudDensity(float3 pos, float surfaceDisp)
|
|||
float2 uv = getUV(pos.xz);
|
||||
const float dist = pos.y + surfaceDisp - _NebulaFloorOffset;
|
||||
const float patch = tex2Dlod(_NebulaPatch, half4(uv, 0, 0)).r;
|
||||
const float patchDisp = tex2Dlod(_NebulaPatchHeight, half4(uv, 0, 0)).r;
|
||||
// Gravity comes from the R32 surface height the caller sampled at this uv; the patch texture carries only the patch
|
||||
// layers (sector waves, patch brushes), small enough for half floats.
|
||||
const float patchDisp = surfaceDisp + tex2Dlod(_NebulaPatchHeight, half4(uv, 0, 0)).r;
|
||||
|
||||
const float patchDensity = saturate((-abs(pos.y+patchDisp - _NebulaFloorOffset)+patch)/_NebulaPatchBlend)*_NebulaPatchDensity;
|
||||
const float floorDist = -dist;
|
||||
|
|
|
|||
|
|
@ -84,12 +84,12 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
(ItemRotation Rotation, float Arc)[] mounts = null, bool beam = false, WeaponFuse? fuse = null, float blast = 0f,
|
||||
float range = 100f, float targetRange = 60f, float accuracy = 1f, float minHit = .2f, bool lockWeapon = false,
|
||||
bool turret = false, float energy = 0f, float heat = 0f, float visibility = 0f, int magazine = 0, bool singleAmmoBurst = false, bool decoy = false, bool charged = false, bool auto = false,
|
||||
float effective = float.PositiveInfinity, float damageAtMax = 0f)
|
||||
float effective = float.PositiveInfinity, float damageAtMax = 0f, float velocity = 0f, float targetGridOffset = 0f)
|
||||
{
|
||||
mounts ??= new[] { (ItemRotation.None, 0f) };
|
||||
var hullData = new HullData
|
||||
{
|
||||
Name = "Hull", HullType = HullType.Ship, Shape = SolidShape(5, 4), Durability = 1000000, Mass = 1000, Armor = 0,
|
||||
Name = "Hull", HullType = HullType.Ship, Shape = SolidShape(5, 4), Durability = 1000000, Mass = 1000, Armor = 0, GridOffset = targetGridOffset,
|
||||
Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() } }
|
||||
};
|
||||
var shooterHullData = new HullData
|
||||
|
|
@ -135,7 +135,7 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
behavior.MinRange = Constant(0);
|
||||
behavior.EffectiveRange = Constant(effective);
|
||||
behavior.DamageAtMaxRange = damageAtMax;
|
||||
behavior.Velocity = Constant(0);
|
||||
behavior.Velocity = Constant(velocity);
|
||||
behavior.Spread = Constant(0);
|
||||
behavior.DamageSpread = Constant(0);
|
||||
behavior.Penetration = Constant(0);
|
||||
|
|
@ -308,6 +308,39 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
Assert.True(r.Gun.Weapon.Firing, "the turret must fire a fused weapon at a designated target out of arc");
|
||||
}
|
||||
|
||||
// The AI aims at the height a target rides at (Entity.RideHeight: field plus the hull's GridOffset), not at the
|
||||
// plane. A target hull raised 40 u, a shot fast enough to be predicted: the turret's and Combat's aim point at the
|
||||
// target's stepped pose. Kills: either aim site spelling its own offset (dropped, added to, or scaled).
|
||||
[Fact]
|
||||
public void ATurretAimsAtTheHeightATargetRidesAt()
|
||||
{
|
||||
var r = Build(turret: true, velocity: 500f, targetGridOffset: 40f);
|
||||
r.Zone.Update(1f);
|
||||
|
||||
AssertAimsAtTarget(r);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombatAimsAtTheHeightATargetRidesAt()
|
||||
{
|
||||
var r = Build(velocity: 500f, targetGridOffset: 40f);
|
||||
r.Zone.Agents.Add(new Minion(r.Shooter));
|
||||
r.Zone.Update(1f);
|
||||
r.Zone.Update(1f);
|
||||
|
||||
AssertAimsAtTarget(r);
|
||||
}
|
||||
|
||||
private static void AssertAimsAtTarget(Rig r)
|
||||
{
|
||||
var toTarget = r.Target.Position - r.Shooter.Position;
|
||||
Assert.True(abs(toTarget.y) > 30f, "fixture: the raised hull is well above the shooter");
|
||||
var expected = normalize(toTarget);
|
||||
Assert.Equal(expected.x, r.Shooter.Aim.x, 3);
|
||||
Assert.Equal(expected.y, r.Shooter.Aim.y, 3);
|
||||
Assert.Equal(expected.z, r.Shooter.Aim.z, 3);
|
||||
}
|
||||
|
||||
public enum Withheld { NoTarget, OutOfRange, NotVisible, Unlocked }
|
||||
|
||||
// The AI never fires a fused weapon at nothing: a missing target, one out of range, one not visible and one
|
||||
|
|
|
|||
|
|
@ -45,13 +45,13 @@ public sealed class FireControlTests : IDisposable
|
|||
|
||||
// `well` digs the zone one well at the origin (WellFixture), so heights differ across the zone.
|
||||
private (ItemManager items, Ship ship, EquippedItem[] weapons) BuildShipWithHardpoints(
|
||||
bool well, (ItemRotation rotation, float firingArc)[] hardpoints)
|
||||
bool well, (ItemRotation rotation, float firingArc)[] hardpoints, float gridOffset = 0f)
|
||||
{
|
||||
var hullShape = new Shape(Math.Max(hardpoints.Length, 3), 3);
|
||||
foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true;
|
||||
// Mass defaults to 0 (ItemData.cs:285), which zeroes ThermalMass (Entity.cs MapEntity) and NaNs
|
||||
// UpdateTemperature -- author a real mass so the entity's own temperature stays a real number.
|
||||
var hullData = new HullData { Name = "TestHull", HullType = HullType.Ship, Shape = hullShape, Durability = 1, Mass = 1000 };
|
||||
var hullData = new HullData { Name = "TestHull", HullType = HullType.Ship, Shape = hullShape, Durability = 1, Mass = 1000, GridOffset = gridOffset };
|
||||
for (var i = 0; i < hardpoints.Length; i++)
|
||||
hullData.Hardpoints.Add(new HardpointData
|
||||
{
|
||||
|
|
@ -380,6 +380,39 @@ public sealed class FireControlTests : IDisposable
|
|||
// height, so its distance in space is longer than its planar distance. A claim whose Range lies between the two, full
|
||||
// damage out to the planar distance, is in range on the plane and out of range in space: it (against Slug's 5) is the
|
||||
// chosen claim. Mutation: Combat measures the three-dimensional distance (the claim then reads 0).
|
||||
// B32: a ship drawn ahead rides the field at its drawn xz, not at the height of its last step (the per-step height
|
||||
// staircase that read as jitter on a steep slope). Mutation: DrawAhead's y back to entity.Position.y fails the first
|
||||
// assertion; reading the height at the last step's xz fails it too.
|
||||
[Fact]
|
||||
public void ADrawnShipRidesTheFieldAtItsDrawnPosition()
|
||||
{
|
||||
var (items, ship, _) = BuildShipWithHardpoints(true, new[] { (ItemRotation.None, 0f) }, gridOffset: 2.5f);
|
||||
var zone = ship.Zone;
|
||||
ship.Position = float3(200, 0, 0);
|
||||
zone.Update(1f);
|
||||
ship.Velocity = float2(100, 0); // up the slope, away from the well
|
||||
const float lead = .1f;
|
||||
|
||||
var drawn = DrawAhead.Position(ship, lead);
|
||||
|
||||
Assert.Equal(ship.Position.x + 100 * lead, drawn.x, 4);
|
||||
// The hull rides 2.5 u above the field (authored above): dropping or doubling the offset moves the ship by 2.5.
|
||||
Assert.Equal(zone.GetHeight(float2(drawn.x, drawn.z)) + 2.5f, drawn.y, 4);
|
||||
Assert.Equal(zone.GetHeight(float2(ship.Position.x, ship.Position.z)) + 2.5f, ship.Position.y, 4);
|
||||
Assert.True(abs(drawn.y - ship.Position.y) > .1f, "the slope is real: the drawn height differs from the last step's");
|
||||
var still = DrawAhead.Position(ship, 0f);
|
||||
Assert.Equal(ship.Position.x, still.x);
|
||||
Assert.Equal(ship.Position.y, still.y);
|
||||
Assert.Equal(ship.Position.z, still.z);
|
||||
|
||||
// A child (a docked ship) is drawn where its parent is drawn, whatever its own pose and velocity.
|
||||
var child = new Ship(items, zone, ship.Hull, new EntitySettings()) { Position = float3(-500, 7, 300), Velocity = float2(-30, 40), Parent = ship };
|
||||
var carried = DrawAhead.Position(child, lead);
|
||||
Assert.Equal(drawn.x, carried.x, 4);
|
||||
Assert.Equal(drawn.y, carried.y, 4);
|
||||
Assert.Equal(drawn.z, carried.z, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheAIMeasuresTheTargetOnTheGroundPlane()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.Position.y, ahead.y);
|
||||
Assert.Equal(player.RideHeight(float2(ahead.x, ahead.z)), 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