From c86423936efc3a74739ddc1d054603bc2b031673 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 7 Oct 2026 12:33:20 +0200 Subject: [PATCH] Helm split, simulation: Ship.Turn is the only facing command; LookDirection becomes Aim Ship.Update reads Turn and feeds the rotation thrusters and aether drives; it no longer reads the look direction or snaps Direction toward it. Steering.Toward is the one law from heading to demand (a heading astern now turns at full demand). HelmInput owns the face-the-aim state. Accelerate, Combat and MoveTo write Turn through Steering.Toward. Entity.LookDirection is renamed Aim in the simulation, the Unity readers and the tests. Unity player input follows in the next commit. Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scripts/Gameplay/EntityInstance.cs | 2 +- Assets/Scripts/Gameplay/ShipInstance.cs | 2 +- .../Weapons/GuidedProjectileManager.cs | 2 +- Assets/Scripts/ServerShared/Agents/Agent.cs | 4 +- .../ServerShared/Agents/States/Combat.cs | 3 +- .../ServerShared/Agents/States/MoveTo.cs | 3 +- .../ServerShared/Behaviors/LockWeapon.cs | 2 +- .../Behaviors/TurretController.cs | 4 +- Assets/Scripts/ServerShared/Entity.cs | 6 +- Assets/Scripts/ServerShared/FireControl.cs | 6 +- Assets/Scripts/ServerShared/HelmInput.cs | 18 ++ Assets/Scripts/ServerShared/Ship.cs | 19 +- Assets/Scripts/ServerShared/Steering.cs | 24 +++ Assets/Scripts/Zone Display/ZoneRenderer.cs | 2 +- tests/Aetheria.Shared.Tests/BrownoutTests.cs | 10 +- .../ConditionRatioTests.cs | 2 +- .../FireControlCut124Tests.cs | 10 +- .../FireControlCut8Tests.cs | 2 +- .../FireControlCut9Tests.cs | 2 +- .../FireControlPerWeaponTests.cs | 2 +- .../Aetheria.Shared.Tests/FireControlTests.cs | 43 ++++- .../IffAndCombatTests.cs | 2 +- .../Aetheria.Shared.Tests/MiningCut3Tests.cs | 6 +- .../Aetheria.Shared.Tests/MiningIndexTests.cs | 2 +- .../MiningTargetQueriesTests.cs | 14 +- .../RestoredHullsTests.cs | 36 ++-- tests/Aetheria.Shared.Tests/ScenarioTests.cs | 4 +- tests/Aetheria.Shared.Tests/SteeringTests.cs | 177 ++++++++++++++++++ .../TargetingSystemTests.cs | 2 +- 29 files changed, 331 insertions(+), 80 deletions(-) create mode 100644 Assets/Scripts/ServerShared/HelmInput.cs create mode 100644 Assets/Scripts/ServerShared/Steering.cs create mode 100644 tests/Aetheria.Shared.Tests/SteeringTests.cs diff --git a/Assets/Scripts/Gameplay/EntityInstance.cs b/Assets/Scripts/Gameplay/EntityInstance.cs index 31b6d093c..fb356dd75 100644 --- a/Assets/Scripts/Gameplay/EntityInstance.cs +++ b/Assets/Scripts/Gameplay/EntityInstance.cs @@ -398,7 +398,7 @@ public class EntityInstance : MonoBehaviour x.Value.material.SetFloat("_Emission", Entity.ItemManager.GameplaySettings.TemperatureEmissionCurve.Evaluate(x.Key.RadiatorTemperature)); } - LookAtPoint.position = transform.position + Entity.LookDirection.ToUnity() * + LookAtPoint.position = transform.position + Entity.Aim.ToUnity() * (!Entity.Target.Value.IsNone ? max(Entity.TargetRange,Entity.ItemManager.GameplaySettings.ConvergenceMinimumDistance) : 10000); LocalSpace.localPosition = transform.position = Entity.Position.ToUnity(); if (_influenceInstance) diff --git a/Assets/Scripts/Gameplay/ShipInstance.cs b/Assets/Scripts/Gameplay/ShipInstance.cs index 8f4fe40bf..cc110fc8f 100644 --- a/Assets/Scripts/Gameplay/ShipInstance.cs +++ b/Assets/Scripts/Gameplay/ShipInstance.cs @@ -113,7 +113,7 @@ public class ShipInstance : EntityInstance base.Update(); TractorBeam.Power = Entity.TractorPower; - TractorBeam.Direction = Entity.LookDirection.ToUnity(); + TractorBeam.Direction = Entity.Aim.ToUnity(); if (_aetherDrive != null) { diff --git a/Assets/Scripts/Gameplay/Weapons/GuidedProjectileManager.cs b/Assets/Scripts/Gameplay/Weapons/GuidedProjectileManager.cs index a3302a93e..876dda1f6 100644 --- a/Assets/Scripts/Gameplay/Weapons/GuidedProjectileManager.cs +++ b/Assets/Scripts/Gameplay/Weapons/GuidedProjectileManager.cs @@ -76,7 +76,7 @@ public class GuidedProjectileManager : InstantWeaponEffectManager p.Velocity = barrel.forward * weapon.Velocity; p.Thrust = item.Evaluate(guidance.Thrust); p.TopSpeed = item.Evaluate(guidance.MissileVelocity); - p.TargetPosition = () => (source.Entity.Position + length(source.LookAtPoint.position.ToCultMath() - source.Entity.Position) * source.Entity.LookDirection).ToUnity(); + p.TargetPosition = () => (source.Entity.Position + length(source.LookAtPoint.position.ToCultMath() - source.Entity.Position) * 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!"); diff --git a/Assets/Scripts/ServerShared/Agents/Agent.cs b/Assets/Scripts/ServerShared/Agents/Agent.cs index e9710b464..403aece7d 100644 --- a/Assets/Scripts/ServerShared/Agents/Agent.cs +++ b/Assets/Scripts/ServerShared/Agents/Agent.cs @@ -63,10 +63,10 @@ public class Agent // If Delta V is above the threshold, direct the ship towards the delta and use only main thrusters if (!noTurn && deltaVMag > FORWARD_DELTA_THRESHOLD) { - Ship.LookDirection = float3(deltaVDirection.x, 0, deltaVDirection.y); + Ship.Turn = Steering.Toward(Ship, deltaVDirection); Ship.MovementDirection = float2(0, pow(dot(Ship.Direction, deltaVDirection), 2)); } - // If Delta V is low, direct the ship towards the target and use all thrusters + // If Delta V is low, use all thrusters and leave Turn to the caller (MoveTo already pointed it at the target) else if(deltaVMag > THRUST_DELTA_THRESHOLD) { var right = Ship.Direction.Rotate(ItemRotation.Clockwise); diff --git a/Assets/Scripts/ServerShared/Agents/States/Combat.cs b/Assets/Scripts/ServerShared/Agents/States/Combat.cs index c93fb3c99..092c6af4c 100644 --- a/Assets/Scripts/ServerShared/Agents/States/Combat.cs +++ b/Assets/Scripts/ServerShared/Agents/States/Combat.cs @@ -124,7 +124,8 @@ public class CombatState : BaseState weapon.Deactivate(); } } - _agent.Ship.LookDirection = toTarget; + _agent.Ship.Aim = toTarget; + _agent.Ship.Turn = Steering.Toward(_agent.Ship, toTarget.xz); _agent.Accelerate(movementDirection * _agent.TopSpeed, true);//selectedGroup >= 0); // Fire charged guns! diff --git a/Assets/Scripts/ServerShared/Agents/States/MoveTo.cs b/Assets/Scripts/ServerShared/Agents/States/MoveTo.cs index 39c4e1135..b677cf0cd 100644 --- a/Assets/Scripts/ServerShared/Agents/States/MoveTo.cs +++ b/Assets/Scripts/ServerShared/Agents/States/MoveTo.cs @@ -23,7 +23,8 @@ public abstract class MoveToState : BaseState // We want to go top speed in the direction of our target var desiredVelocity = dir * _agent.TopSpeed; - _agent.Ship.LookDirection = float3(dir.x, 0, dir.y); + _agent.Ship.Aim = float3(dir.x, 0, dir.y); + _agent.Ship.Turn = Steering.Toward(_agent.Ship, dir); _agent.Accelerate(desiredVelocity); } } diff --git a/Assets/Scripts/ServerShared/Behaviors/LockWeapon.cs b/Assets/Scripts/ServerShared/Behaviors/LockWeapon.cs index 6b88f4ba4..16f56b72d 100644 --- a/Assets/Scripts/ServerShared/Behaviors/LockWeapon.cs +++ b/Assets/Scripts/ServerShared/Behaviors/LockWeapon.cs @@ -97,7 +97,7 @@ public class LockWeapon : InstantWeapon DirectionImpact = Evaluate(_data.DirectionImpact); Decay = Evaluate(_data.Decay); - var degrees = acos(dot(normalize(target.Position - Entity.Position), normalize(Entity.LookDirection))) * 57.2958f; + var degrees = acos(dot(normalize(target.Position - Entity.Position), normalize(Entity.Aim))) * 57.2958f; if (degrees < LockAngle) { var lerp = 1 - unlerp(0, 90, degrees); diff --git a/Assets/Scripts/ServerShared/Behaviors/TurretController.cs b/Assets/Scripts/ServerShared/Behaviors/TurretController.cs index a5a395373..654ee5ec1 100644 --- a/Assets/Scripts/ServerShared/Behaviors/TurretController.cs +++ b/Assets/Scripts/ServerShared/Behaviors/TurretController.cs @@ -71,10 +71,10 @@ public class TurretController : Behavior, IInitializableBehavior target.Position, targetVelocity ); predictedPosition.y = Entity.Zone.GetHeight(predictedPosition.xz) + targetHullData.GridOffset; - Entity.LookDirection = normalize(predictedPosition - Entity.Position); + Entity.Aim = normalize(predictedPosition - Entity.Position); } else - Entity.LookDirection = normalize(diff); + Entity.Aim = normalize(diff); foreach (var x in _weapons) { diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 853bea91e..bcd6ad7ab 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -55,7 +55,9 @@ public abstract class Entity // notice and clear it. public ReactiveProperty TargetItem = new ReactiveProperty((EquippedItem)null); - public float3 LookDirection; + // Where the pilot points: reticle, lock cone, tractor, guided rounds, fire direction. Steers nothing; the + // hull turns only by Ship.Turn. + public float3 Aim; public string Name; @@ -406,7 +408,7 @@ public abstract class Entity // The candidate nearest the look direction by planar angle; picking the held target again clears it. public bool TargetUnderReticle() { - var look = float2(LookDirection.x, LookDirection.z); + var look = float2(Aim.x, Aim.z); if (!PickTarget(new AngleKey(this, Position.xz, look), out var picked)) return false; return SetTarget(Target.Value.Equals(picked) ? TargetRef.None : picked); } diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index 04e960d33..ccab1b14e 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -107,16 +107,16 @@ public static class FireControl } // Operator ruling 2026-09-30: a shot with nothing to bear on flies along where its shooter is aiming - // (Entity.LookDirection -- the player's mouse or a turret's tracking), restricted by the mount's arc. The + // (Entity.Aim -- the player's mouse or a turret's tracking), restricted by the mount's arc. The // arc is InArc's own cone: an aim inside it is used as is, one outside it is turned to the nearest edge of // the same cone. There is no second bearing test here; the edge is where InArc stops passing. A shooter - // with no aim set (LookDirection zero) aims down the mount, the same fallback InArc takes for a zero bearing. + // with no aim set (Aim zero) aims down the mount, the same fallback InArc takes for a zero bearing. // The simulation had no aim clamp before this: the Unity barrel's ArticulationPoint clamps the picture, // and a target outside the arc merely priced the shot at zero. public static float2 AimDirection(EquippedItem item, Entity shooter) { var mount = MountDirection(item).xz; - var look = shooter.LookDirection.xz; + var look = shooter.Aim.xz; if (lengthsq(look) < 1e-6f) return mount; look = normalize(look); if (InArc(item, float3(look.x, 0, look.y))) return look; diff --git a/Assets/Scripts/ServerShared/HelmInput.cs b/Assets/Scripts/ServerShared/HelmInput.cs new file mode 100644 index 000000000..fe1316aff --- /dev/null +++ b/Assets/Scripts/ServerShared/HelmInput.cs @@ -0,0 +1,18 @@ +using CultMath; +using static CultMath.math; + +// The player's face-the-aim state, engine-free. Held follows the Face Aim hold input each frame; Latched is +// flipped by the Caps Lock press. Either one faces the aim; neither leaves the hull to the turn axis. +// Runtime only, never saved: Latched starts false each session. +public class HelmInput +{ + public bool Held; + public bool Latched; + + public bool FaceAim => Held || Latched; + + public void Toggle() => Latched = !Latched; + + public float Demand(Ship ship, float2 aim, float turnAxis) => + FaceAim ? Steering.Toward(ship, aim) : clamp(turnAxis, -1f, 1f); +} diff --git a/Assets/Scripts/ServerShared/Ship.cs b/Assets/Scripts/ServerShared/Ship.cs index d10f097ca..875dd81be 100644 --- a/Assets/Scripts/ServerShared/Ship.cs +++ b/Assets/Scripts/ServerShared/Ship.cs @@ -25,6 +25,9 @@ public class Ship : Entity // [IgnoreMember] public Targetable Target; public Entity HomeEntity; public float2 MovementDirection; + // The one facing command, -1..1, positive clockwise. Written each tick by the pilot (player input or the + // ship's agent); a heading becomes a demand only through Steering.Toward. Runtime only, never packed. + public float Turn; public bool IsPlayerShip; private HashSet _thrusterItems; @@ -275,20 +278,12 @@ public class Ship : Entity foreach (var thruster in _forwardThrusters) thruster.Axis += MovementDirection.y; foreach (var thruster in _reverseThrusters) thruster.Axis += -MovementDirection.y; - var look = normalize(LookDirection.xz); - var deltaRot = dot(look, normalize(Direction).Rotate(ItemRotation.Clockwise)); - if (abs(deltaRot) < .01f) - { - deltaRot = 0; - Direction = lerp(Direction, look, min(delta, 1)); - } - deltaRot = pow(abs(deltaRot), .5f) * sign(deltaRot); - - foreach (var thruster in _clockwiseThrusters) thruster.Axis += deltaRot; - foreach (var thruster in _counterClockwiseThrusters) thruster.Axis += -deltaRot; + var turn = clamp(Turn, -1f, 1f); + foreach (var thruster in _clockwiseThrusters) thruster.Axis += turn; + foreach (var thruster in _counterClockwiseThrusters) thruster.Axis += -turn; foreach (var drive in _aetherDrives) - drive.Axis = float3(MovementDirection.y, MovementDirection.x, deltaRot); + drive.Axis = float3(MovementDirection.y, MovementDirection.x, turn); } var velocityMagnitude = length(Velocity); diff --git a/Assets/Scripts/ServerShared/Steering.cs b/Assets/Scripts/ServerShared/Steering.cs new file mode 100644 index 000000000..de477332f --- /dev/null +++ b/Assets/Scripts/ServerShared/Steering.cs @@ -0,0 +1,24 @@ +using System; +using CultMath; +using static CultMath.math; + +// The one law that turns a wanted heading into a facing command. Ship.Update reads only Ship.Turn; whoever +// wants the hull to face somewhere (the player's face-the-aim state, an agent state) asks here and writes Turn. +public static class Steering +{ + const float Deadband = .01f; + + // Positive is clockwise. Lateral component of the heading with sqrt shaping and a deadband; a heading + // behind the hull turns at full demand (its lateral component alone would vanish at exactly astern). + // A zero or non-finite heading holds course. + public static float Toward(Ship ship, float2 heading) + { + if (!float.IsFinite(heading.x) || !float.IsFinite(heading.y) || lengthsq(heading) < 1e-12f) return 0f; + var dir = normalize(ship.Direction); + var wanted = normalize(heading); + var d = dot(wanted, dir.Rotate(ItemRotation.Clockwise)); + if (dot(wanted, dir) < 0f) return d < 0f ? -1f : 1f; + if (abs(d) < Deadband) return 0f; + return sqrt(abs(d)) * sign(d); + } +} diff --git a/Assets/Scripts/Zone Display/ZoneRenderer.cs b/Assets/Scripts/Zone Display/ZoneRenderer.cs index b6021027a..5ef289cde 100644 --- a/Assets/Scripts/Zone Display/ZoneRenderer.cs +++ b/Assets/Scripts/Zone Display/ZoneRenderer.cs @@ -437,7 +437,7 @@ public class ZoneRenderer : MonoBehaviour foreach (var loot in _loot) { loot.ViewOrigin = PerspectiveEntity.Position; - loot.ViewDirection = PerspectiveEntity.LookDirection; + loot.ViewDirection = PerspectiveEntity.Aim; } // if (SlimeRenderer.SpawnPositions.Length != _suns.Length) diff --git a/tests/Aetheria.Shared.Tests/BrownoutTests.cs b/tests/Aetheria.Shared.Tests/BrownoutTests.cs index e9a2dbf11..660b4106b 100644 --- a/tests/Aetheria.Shared.Tests/BrownoutTests.cs +++ b/tests/Aetheria.Shared.Tests/BrownoutTests.cs @@ -121,11 +121,9 @@ public sealed class BrownoutTests : IDisposable Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Consumer")))); zone.Entities.Add(ship); - // A ship already "looking" the way Direction already points keeps Ship.Update's own steering pass a - // no-op (deltaRot == 0): no NaN from an unset LookDirection propagating into AetherDrive's Axis.z, and - // no incidental torque thrust from the ship trying to turn toward a look direction none of these tests - // care about. - ship.LookDirection = float3(0, 0, 1); + // Ship.Turn stays 0, so Ship.Update commands no rotation: no incidental torque thrust from the + // ship trying to turn. Aim steers nothing, so this one is only a plausible aim. + ship.Aim = float3(0, 0, 1); ship.Activate(); return ship; } @@ -184,7 +182,7 @@ public sealed class BrownoutTests : IDisposable // --- AetherDrive: PassiveCoupling = 1 engages the rotor regardless of axis input, so the drive can be // --- exercised without threading Ship's own movement/torque classification -- Axis stays at Ship.Update's - // --- own default (0,0,deltaRot), and deltaRot is 0 by BuildShip's own LookDirection setup. Rpm starts at 0, + // --- own default (0,0,Turn), and Turn is 0 by default. Rpm starts at 0, // --- so decay(0, ...) contributes nothing and the whole tick's Rpm gain is exactly this behaviour's own // --- spin-up under test. --- private (float rpmGain, AetherDrive drive) RunAetherDrive(float reactorCharge, float exponent = 1) diff --git a/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs b/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs index 2ff03d18f..9781f0ec9 100644 --- a/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs +++ b/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs @@ -95,7 +95,7 @@ public sealed class ConditionRatioTests : IDisposable Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Consumer")))); zone.Entities.Add(ship); - ship.LookDirection = float3(0, 0, 1); // Ship.Update's own steering pass stays a no-op, as BrownoutTests + ship.Aim = float3(0, 0, 1); // Turn stays 0, as in BrownoutTests ship.Activate(); consumer = ship.Equipment.Single(e => e.Data.Name == "Consumer"); diff --git a/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs index 92b26bbce..c6b1bb00c 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs @@ -1155,7 +1155,7 @@ public sealed class FireControlCut124Tests : IDisposable } // Operator ruling 2026-09-30: "fused weapons without a lock explode at max range." A fused shot with no - // target bursts at the weapon's Range along its aim (Entity.LookDirection), whatever its fuse. The fixture + // target bursts at the weapon's Range along its aim (Entity.Aim), whatever its fuse. The fixture // aims the shooter along (2,1), a non-axis facing; witnesses sit at the burst point and beside it. Radius 4 // with the witness hull 5x4 cells of 2 world units: a witness centred on the burst point is covered, and // one 30 units off the aim line is not. @@ -1234,7 +1234,7 @@ public sealed class FireControlCut124Tests : IDisposable // ==== Operator rulings 2026-09-30 (Soul's pass on the no-lock fix): aim, invalid target, contact, shooter ==== - private static void Aim(Engagement e, float2 direction) => e.Shooter.LookDirection = float3(direction.x, 0, direction.y); + private static void Aim(Engagement e, float2 direction) => e.Shooter.Aim = float3(direction.x, 0, direction.y); private static float3 PointAlong(Engagement e, float2 direction, float distance) => e.Shooter.Position + float3(direction.x, 0, direction.y) * distance; @@ -1266,7 +1266,7 @@ public sealed class FireControlCut124Tests : IDisposable } // Ruling 1: an aim inside the arc is used as it is (unnormalised here), and a shooter aiming at nothing - // (LookDirection zero) fires down its mount. Kills: reading MountDirection instead of the aim; a missing + // (Aim zero) fires down its mount. Kills: reading MountDirection instead of the aim; a missing // zero-aim fallback, which would normalise to NaN and burst nowhere. [Fact] public void ANoLockShotFliesAlongTheAimAndDownTheMountWhenNothingIsAimed() @@ -2113,9 +2113,9 @@ public sealed class FireControlCut124Tests : IDisposable Near(float2(0, 1), Clamped(e, float2(0, 0))); Near(float2(0, 1), Clamped(e, float2(5e-4f, 0))); Near(Dir(85), Clamped(e, float2(2e-3f, 0))); - e.Shooter.LookDirection = float3(0, 1, 0); + e.Shooter.Aim = float3(0, 1, 0); Near(float2(0, 1), FireControl.AimDirection(e.WeaponItem, e.Shooter)); - e.Shooter.LookDirection = float3(float.NaN, 0, float.NaN); + e.Shooter.Aim = float3(float.NaN, 0, float.NaN); var nan = FireControl.AimDirection(e.WeaponItem, e.Shooter); Assert.False(float.IsNaN(nan.x) || float.IsNaN(nan.y), "a NaN aim must not produce a NaN flight direction"); } diff --git a/tests/Aetheria.Shared.Tests/FireControlCut8Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut8Tests.cs index a978aed7f..ffde777bc 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut8Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut8Tests.cs @@ -224,7 +224,7 @@ public sealed class FireControlCut8Tests : IDisposable locker.Position = float3.zero; bystander.Position = float3(0, 0, 100); - locker.LookDirection = float3(0, 0, 1); + locker.Aim = float3(0, 0, 1); locker.SetTarget(bystander); locker.EntityInfoGathered[bystander] = 1f; locker.SetIff(bystander, true); diff --git a/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs index 2ebeb72f1..8e715d7eb 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs @@ -560,7 +560,7 @@ public sealed class FireControlCut9Tests : IDisposable locker.Position = float3.zero; bystander.Position = float3(0, 0, 100); - locker.LookDirection = float3(0, 0, 1); + locker.Aim = float3(0, 0, 1); locker.SetTarget(bystander); locker.EntityInfoGathered[bystander] = 1f; locker.SetIff(bystander, true); diff --git a/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs b/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs index edca68dde..60bc55e00 100644 --- a/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs @@ -257,7 +257,7 @@ public sealed class FireControlPerWeaponTests : IDisposable return rig; } - private static void Aim(Rig r, float2 direction) => r.Shooter.LookDirection = float3(direction.x, 0, direction.y); + private static void Aim(Rig r, float2 direction) => r.Shooter.Aim = float3(direction.x, 0, direction.y); private static readonly (ItemRotation, float)[] SideThenForward = { (ItemRotation.Clockwise, 0f), (ItemRotation.None, 0f) }; private static readonly (ItemRotation, float)[] SideMount = { (ItemRotation.Clockwise, 0f) }; diff --git a/tests/Aetheria.Shared.Tests/FireControlTests.cs b/tests/Aetheria.Shared.Tests/FireControlTests.cs index 33c90eeba..51d03bd9f 100644 --- a/tests/Aetheria.Shared.Tests/FireControlTests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlTests.cs @@ -175,13 +175,8 @@ public sealed class FireControlTests : IDisposable Assert.True(FireControl.InArc(defaultArc, AtAngle(5))); } - // The cut's real deliverable: Entity.HardpointTransforms is gone, so a Minion can step through a full - // Zone.Update tick and reach the fire decision without Unity ever having run. Before this cut, - // Combat.cs:100-102 threw KeyNotFoundException on the first combat tick headless, because the AI's aim - // direction was a readback from a renderer that never ran. Mutation: restore the HardpointTransforms - // read in Behavior.Direction / Combat.cs -- this throws again outside Unity. - [Fact] - public void CombatStateStepsHeadless() + // A shooter with one in-range, in-arc gun, a detected target at `targetPosition`, and a Minion driving the shooter. + private (Ship shooter, Ship target, Zone zone) BuildCombatEncounter(float3 targetPosition) { var (items, shooter, weapons) = BuildShipWithHardpoints((ItemRotation.None, 0f)); var weaponData = (InstantWeaponData) weapons[0].Data.Behaviors.Single(b => b is InstantWeaponData); @@ -203,7 +198,7 @@ public sealed class FireControlTests : IDisposable target.Activate(); shooter.Position = float3.zero; - target.Position = float3(0, 0, 10); // dead ahead of the None-rotation hardpoint, well within range and arc + target.Position = targetPosition; shooter.SetTarget(target); // Cut 3: CombatState now gates on FireControl.HitProbability, which is zero for an undetected target @@ -216,6 +211,19 @@ public sealed class FireControlTests : IDisposable // unaided shot still counts as "worth it." items.GameplaySettings.AgentMinHitProbability = 0f; zone.Agents.Add(new Minion(shooter)); + return (shooter, target, zone); + } + + // The cut's real deliverable: Entity.HardpointTransforms is gone, so a Minion can step through a full + // Zone.Update tick and reach the fire decision without Unity ever having run. Before this cut, + // Combat.cs:100-102 threw KeyNotFoundException on the first combat tick headless, because the AI's aim + // direction was a readback from a renderer that never ran. Mutation: restore the HardpointTransforms + // read in Behavior.Direction / Combat.cs -- this throws again outside Unity. + [Fact] + public void CombatStateStepsHeadless() + { + // dead ahead of the None-rotation hardpoint, well within range and arc + var (shooter, _, zone) = BuildCombatEncounter(float3(0, 0, 10)); var ex = Record.Exception(() => { @@ -227,4 +235,23 @@ public sealed class FireControlTests : IDisposable var weapon = shooter.Weapons.Single(); Assert.True(weapon.Firing); // arc + range satisfied: the fight actually decided to fire, not just avoided a crash } + + // Controls cut (helm split): Combat aims the shooter at the intercept (Aim) and commands the hull toward it + // through the one steering law (Turn); it never turns the hull by writing Aim. + [Fact] + public void CombatAimsAtTheInterceptAndTurnsTowardIt() + { + var (shooter, target, zone) = BuildCombatEncounter(float3(10, 0, 10)); // 45 degrees to starboard, in arc + var direction = shooter.Direction; + + zone.Update(1f); + zone.Update(1f); + + var bearing = normalize(target.Position.xz - shooter.Position.xz); + var aim = normalize(shooter.Aim.xz); + Assert.True(dot(aim, bearing) > .999f, "Aim points at the stationary target's intercept"); + Assert.Equal(direction, shooter.Direction); // no thrusters on this hull: only Turn was commanded + Assert.Equal(Steering.Toward(shooter, shooter.Aim.xz), shooter.Turn); + Assert.True(abs(shooter.Turn) > .5f); + } } diff --git a/tests/Aetheria.Shared.Tests/IffAndCombatTests.cs b/tests/Aetheria.Shared.Tests/IffAndCombatTests.cs index 4c56b5ff6..e77bfad51 100644 --- a/tests/Aetheria.Shared.Tests/IffAndCombatTests.cs +++ b/tests/Aetheria.Shared.Tests/IffAndCombatTests.cs @@ -307,7 +307,7 @@ public sealed class IffAndCombatTests : IDisposable // detected (EntityInfoGathered=1 saturates the sensor-impact term). This makes the assertion // below actually depend on the gate, instead of a coincidental zero from unset geometry. shooter.Position = float3(0, 0, 0); - shooter.LookDirection = float3(0, 0, 1); + shooter.Aim = float3(0, 0, 1); target.Position = float3(0, 0, 10); shooter.EntityInfoGathered[target] = 1; diff --git a/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs b/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs index 1170ade7a..496955b98 100644 --- a/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs +++ b/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs @@ -224,7 +224,7 @@ public sealed partial class MiningCut3Tests : IDisposable } s.Zone.Entities.Add(ship); Hold(ship, at); - ship.LookDirection = float3(0, 0, 1); + ship.Aim = float3(0, 0, 1); ship.Activate(); return ship; } @@ -588,7 +588,7 @@ public sealed partial class MiningCut3Tests : IDisposable var enemy = SpawnShip(s, eyeAt + float2(0, 50)); shooter.SetIff(enemy, true); shooter.EntityInfoGathered[enemy] = 1; - shooter.LookDirection = float3(0, 0, 1); + shooter.Aim = float3(0, 0, 1); var launcher = new LockWeapon(new LockWeaponData { LockSpeed = Constant(.5f), @@ -817,7 +817,7 @@ public sealed partial class MiningCut3Tests : IDisposable foreach (var weapon in weapons) Assert.True(ship.TryEquip(Mint(s, weapon))); s.Zone.Entities.Add(ship); Hold(ship, eyeAt); - ship.LookDirection = float3(0, 0, 1); + ship.Aim = float3(0, 0, 1); ship.Activate(); Tick((ship, eyeAt)); // weapons read their ranges when they run return ship; diff --git a/tests/Aetheria.Shared.Tests/MiningIndexTests.cs b/tests/Aetheria.Shared.Tests/MiningIndexTests.cs index d6880d32d..785854911 100644 --- a/tests/Aetheria.Shared.Tests/MiningIndexTests.cs +++ b/tests/Aetheria.Shared.Tests/MiningIndexTests.cs @@ -56,7 +56,7 @@ public sealed partial class MiningCut3Tests Assert.True(ship.TryEquip(Mint(s, gun))); s.Zone.Entities.Add(ship); Hold(ship, at); - ship.LookDirection = float3(0, 0, 1); + ship.Aim = float3(0, 0, 1); ship.Activate(); Tick((ship, at)); // equipment comes online and weapons read their ranges on the first tick return ship; diff --git a/tests/Aetheria.Shared.Tests/MiningTargetQueriesTests.cs b/tests/Aetheria.Shared.Tests/MiningTargetQueriesTests.cs index 5cfd6d72b..64304e066 100644 --- a/tests/Aetheria.Shared.Tests/MiningTargetQueriesTests.cs +++ b/tests/Aetheria.Shared.Tests/MiningTargetQueriesTests.cs @@ -157,7 +157,7 @@ public sealed partial class MiningCut3Tests for (var i = 0; i < 60; i++) { var heading = rng.NextDouble() * 2 * Math.PI; - observer.LookDirection = float3((float) Math.Cos(heading), 0, (float) Math.Sin(heading)); + observer.Aim = float3((float) Math.Cos(heading), 0, (float) Math.Sin(heading)); double Angle(TargetRef t) { var to = Where(t) - at; @@ -188,7 +188,7 @@ public sealed partial class MiningCut3Tests var s = BuildSceneAt(float2.zero, 20000f, 0, null, new[] { kind }, Belt(kind, Annulus(new System.Random(21), count, 2000f, outer))); var at = 2100f * float2(cos(1f), sin(1f)); var observer = Observer(s, at, 150f, 3f, Falloff()); - observer.LookDirection = float3(1, 0, 0); + observer.Aim = float3(1, 0, 0); var order = ByDistance(s, observer, 150f); Assert.True(order.Count > 5); if (held) observer.SetTarget(order[order.Count / 2]); @@ -262,7 +262,7 @@ public sealed partial class MiningCut3Tests Check("Previous from the median rock", Cost(observer.TargetPrevious, median), previousHeld); foreach (var heading in new[] { 0f, 2f, 4f }) { - observer.LookDirection = float3(cos(heading), 0, sin(heading)); + observer.Aim = float3(cos(heading), 0, sin(heading)); Check($"the reticle at {heading}", Cost(observer.TargetUnderReticle, TargetRef.None), reticle); } s.Zone.Entities.Remove(observer); @@ -314,7 +314,7 @@ public sealed partial class MiningCut3Tests for (var i = 0; i < 200; i++) { var heading = rng.NextDouble() * 2 * Math.PI; - observer.LookDirection = float3((float) Math.Cos(heading), 0, (float) Math.Sin(heading)); + observer.Aim = float3((float) Math.Cos(heading), 0, (float) Math.Sin(heading)); observer.SetTarget(TargetRef.None); switch (i % 3) { @@ -338,7 +338,7 @@ public sealed partial class MiningCut3Tests Assert.True(ship.TryEquip(Mint(s, launcher))); s.Zone.Entities.Add(ship); Hold(ship, at); - ship.LookDirection = float3(0, 0, 1); + ship.Aim = float3(0, 0, 1); ship.Activate(); Tick((ship, at)); Assert.Contains(ship.Weapons, w => w.Range == 400f); @@ -410,7 +410,7 @@ public sealed partial class MiningCut3Tests // visible target, never a hidden one. for (var heading = 0f; heading < 2 * PI; heading += .7f) { - observer.LookDirection = float3(sin(heading), 0, cos(heading)); + observer.Aim = float3(sin(heading), 0, cos(heading)); observer.SetTarget(TargetRef.None); Assert.True(observer.TargetUnderReticle()); Assert.DoesNotContain(observer.Target.Value, hidden); @@ -462,7 +462,7 @@ public sealed partial class MiningCut3Tests } // The reticle, looking at the spot: the ship; then with the ship gone from sight, the first chunk. - observer.LookDirection = float3(0, 0, 1); + observer.Aim = float3(0, 0, 1); observer.SetTarget(TargetRef.None); Assert.True(observer.TargetUnderReticle()); Assert.Equal(expected[0], observer.Target.Value); diff --git a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs index 328009b24..229cb042f 100644 --- a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs +++ b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs @@ -18,7 +18,7 @@ using Random = CultMath.Random; // Cut 1 (docs/locomotion-cut.md, "Restore the legacy thruster ship hulls to the live catalog"). Verifies the // content restore-hulls landed, against the real shipped catalog, read-only: the hulls exist with the expected // thruster hardpoint counts, every thruster hardpoint accepts a catalog thruster design through TryEquip at its -// own position, and the ship then moves and turns for a few ticks under MovementDirection/LookDirection on the +// own position, and the ship then moves and turns for a few ticks under MovementDirection/Turn on the // pre-Cut-3 mixer -- proof of the restored content, not of a controller this cut does not touch. public sealed class RestoredHullsTests { @@ -31,7 +31,7 @@ public sealed class RestoredHullsTests // (tools/AetherDb/AuthoredSettings.cs) is the same headless YAML reader tools/AetherDb already uses to // inspect Settings.asset outside Unity; this loads the real GameplaySettings the same way instead of // hand-copying its fields a second time. - private static GameplaySettings Settings() => AuthoredSettings.Load(FindRepoRoot()).Read("GameplaySettings"); + internal static GameplaySettings Settings() => AuthoredSettings.Load(FindRepoRoot()).Read("GameplaySettings"); // Same composition FireControlCut7Tests.OpenReadOnlyRealCatalog uses: a registry scoped to the shipped // assembly's own [CultDocument] types, so this test's own process-wide type registration never pollutes the @@ -54,7 +54,7 @@ public sealed class RestoredHullsTests throw new DirectoryNotFoundException("Run from inside the Aetheria repository."); } - private static CultCache OpenCatalog() => OpenReadOnlyRealCatalog(Path.Combine(FindRepoRoot(), "GameData", "Aetheria.cc")); + internal static CultCache OpenCatalog() => OpenReadOnlyRealCatalog(Path.Combine(FindRepoRoot(), "GameData", "Aetheria.cc")); // Shared tutorial-galaxy construction settings (Assets/Resources/Settings.asset's own values), factored out // so every test that needs a real tutorial Galaxy -- not just EntranceZoneAlwaysGetsADockedStationAcrossSeeds @@ -201,16 +201,10 @@ public sealed class RestoredHullsTests } } - // "The ship then moves and turns... for a few ticks" (docs/locomotion-cut.md verification list). Equips - // every thruster hardpoint with a fitting design, then drives MovementDirection/LookDirection the same way - // ActionGameManager's player-input path does (docs' own consumer audit) and checks Entity.Velocity and - // Entity.Direction both move over a few ticks on the current (pre-Cut-3) mixer. - [Theory] - [InlineData("Longinus")] - [InlineData("Djinni")] - public void RestoredHullMovesAndTurnsUnderMovementAndLookDirection(string name) + // A restored hull with every thruster hardpoint (and its reactor) filled, activated and warmed up: the + // rotation thrusters the helm tests need. The caller owns the cache. + internal static Ship BuildThrustedShip(CultCache cache, string name) { - using var cache = OpenCatalog(); var hull = cache.GetByName(name); var thrusterDesigns = cache.GetAll().Where(g => g.Hardpoint == HardpointType.Thruster).ToArray(); @@ -247,12 +241,27 @@ public sealed class RestoredHullsTests ship.Position = float3.zero; ship.Activate(); zone.Update(0f); // warm-up: resolves equipment stats before the driven ticks read them + return ship; + } + + // "The ship then moves and turns... for a few ticks" (docs/locomotion-cut.md verification list). Equips + // every thruster hardpoint with a fitting design, then drives MovementDirection/Turn the same way + // ActionGameManager's player-input path does (docs' own consumer audit) and checks Entity.Velocity and + // Entity.Direction both move over a few ticks on the current (pre-Cut-3) mixer. + [Theory] + [InlineData("Longinus")] + [InlineData("Djinni")] + public void RestoredHullMovesAndTurnsUnderMovementAndTurn(string name) + { + using var cache = OpenCatalog(); + var ship = BuildThrustedShip(cache, name); + var zone = ship.Zone; var startVelocity = ship.Velocity; var startDirection = ship.Direction; ship.MovementDirection = float2(0, 1); // full forward, same axis ActionGameManager.Input.Player.Move drives - ship.LookDirection = float3(1, 0, 0); // off-axis target heading, so the mixer's yaw thrusters have work to do + ship.Turn = 1; // a turn command, so the mixer's yaw thrusters have work to do for (var i = 0; i < 10; i++) zone.Update(1f / 60f); Assert.NotEqual(startVelocity, ship.Velocity); @@ -538,7 +547,6 @@ public sealed class RestoredHullsTests zone.Update(0f); ship.MovementDirection = float2(0, 1); // full forward - ship.LookDirection = float3(ship.Direction.x, 0, ship.Direction.y); // straight line: hold the ship's own heading // Soul measured this loadout's forward thrusters crossing 386 K (thermal shutdown) around 7.3 s under // real settings; 5 s keeps every tick inside the window where thrust runs continuously and unforced, so diff --git a/tests/Aetheria.Shared.Tests/ScenarioTests.cs b/tests/Aetheria.Shared.Tests/ScenarioTests.cs index 44d64db50..cfd5cc174 100644 --- a/tests/Aetheria.Shared.Tests/ScenarioTests.cs +++ b/tests/Aetheria.Shared.Tests/ScenarioTests.cs @@ -317,12 +317,12 @@ public sealed partial class RunStartTests Assert.Equal(1, AgentsOf(zone, hostile)); Assert.Equal(0, AgentsOf(zone, neutral)); Assert.Equal(0, AgentsOf(zone, player)); - var look = neutral.LookDirection; + var look = neutral.Aim; for (var tick = 0; tick < 50; tick++) { zone.Update(.1f); Assert.Equal(float2.zero, ((Ship) neutral).MovementDirection); - Assert.Equal(look, neutral.LookDirection); + Assert.Equal(look, neutral.Aim); } Assert.Equal(0, AgentsOf(zone, neutral)); } diff --git a/tests/Aetheria.Shared.Tests/SteeringTests.cs b/tests/Aetheria.Shared.Tests/SteeringTests.cs new file mode 100644 index 000000000..6d0bd5922 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/SteeringTests.cs @@ -0,0 +1,177 @@ +using System; +using System.Linq; +using CultMath; +using GameCult.Caching; +using Xunit; +using static CultMath.math; +using float2 = CultMath.float2; +using float3 = CultMath.float3; + +// The helm split: Ship.Turn is the one facing command, Steering.Toward turns a heading into it, HelmInput owns +// the player's face-the-aim state, and the hull never reads Aim. Ships are the restored Longinus with its real +// rotation thrusters, so rotation pays the thruster path the way it does in play. +public sealed class SteeringTests +{ + private const string Hull = "Longinus"; + + private static float2 Right(Ship ship) => normalize(ship.Direction).Rotate(ItemRotation.Clockwise); + + // A unit heading `degrees` clockwise of the hull's nose. + private static float2 Heading(Ship ship, float degrees) + { + var r = radians(degrees); + return normalize(ship.Direction) * cos(r) + Right(ship) * sin(r); + } + + private static float Angle(float2 from, float2 to) => acos(clamp(dot(normalize(from), normalize(to)), -1f, 1f)); + + [Fact] + public void TowardIsZeroOnTheHeading() + { + using var cache = RestoredHullsTests.OpenCatalog(); + var ship = RestoredHullsTests.BuildThrustedShip(cache, Hull); + + Assert.Equal(0f, Steering.Toward(ship, ship.Direction)); + Assert.Equal(0f, Steering.Toward(ship, Heading(ship, .5f))); + Assert.Equal(0f, Steering.Toward(ship, Heading(ship, -.5f))); + } + + [Fact] + public void TowardTurnsClockwiseForAStarboardHeading() + { + using var cache = RestoredHullsTests.OpenCatalog(); + var ship = RestoredHullsTests.BuildThrustedShip(cache, Hull); + + var expected = sqrt(sin(radians(45f))); + Assert.Equal(expected, Steering.Toward(ship, Heading(ship, 45f)), 4); + Assert.Equal(-expected, Steering.Toward(ship, Heading(ship, -45f)), 4); + } + + [Fact] + public void AHeadingBehindStillTurns() + { + using var cache = RestoredHullsTests.OpenCatalog(); + var ship = RestoredHullsTests.BuildThrustedShip(cache, Hull); + + Assert.Equal(1f, abs(Steering.Toward(ship, -ship.Direction))); + Assert.Equal(-1f, Steering.Toward(ship, Heading(ship, -170f))); + Assert.Equal(1f, Steering.Toward(ship, Heading(ship, 170f))); + } + + [Fact] + public void ANonFiniteHeadingHoldsCourse() + { + using var cache = RestoredHullsTests.OpenCatalog(); + var ship = RestoredHullsTests.BuildThrustedShip(cache, Hull); + + foreach (var heading in new[] { float2.zero, float2(float.NaN, 1), float2(1, float.NaN), float2(float.PositiveInfinity, 0) }) + { + ship.Turn = Steering.Toward(ship, heading); + Assert.Equal(0f, ship.Turn); + for (var i = 0; i < 5; i++) ship.Update(1f / 60f); + Assert.False(float.IsNaN(ship.Direction.x) || float.IsNaN(ship.Direction.y)); + } + } + + [Fact] + public void HoldAndToggleAgree() + { + using var cache = RestoredHullsTests.OpenCatalog(); + var ship = RestoredHullsTests.BuildThrustedShip(cache, Hull); + var held = new HelmInput { Held = true }; + var latched = new HelmInput(); + latched.Toggle(); + + foreach (var direction in new[] { float2(0, 1), float2(1, 0), float2(-1, -1), float2(.3f, -.9f) }) + foreach (var aim in new[] { float2(0, 1), float2(1, 1), float2(-1, 0), float2(0, -1), float2(.2f, -.7f), float2.zero }) + foreach (var axis in new[] { -1f, 0f, .5f, 1f }) + { + ship.Direction = direction; + var expected = Steering.Toward(ship, aim); + Assert.Equal(expected, held.Demand(ship, aim, axis)); + Assert.Equal(expected, latched.Demand(ship, aim, axis)); + } + } + + [Fact] + public void FaceAimOffFollowsTheAxis() + { + using var cache = RestoredHullsTests.OpenCatalog(); + var ship = RestoredHullsTests.BuildThrustedShip(cache, Hull); + var helm = new HelmInput(); + + foreach (var aim in new[] { float2(0, 1), float2(1, 0), float2(-1, -1) }) + { + Assert.Equal(.25f, helm.Demand(ship, aim, .25f)); + Assert.Equal(1f, helm.Demand(ship, aim, 3f)); + Assert.Equal(-1f, helm.Demand(ship, aim, -3f)); + } + + helm.Toggle(); + Assert.True(helm.Latched); + helm.Toggle(); + Assert.False(helm.Latched); + Assert.False(helm.FaceAim); + } + + [Fact] + public void TheHullIgnoresTheAim() + { + using var cache = RestoredHullsTests.OpenCatalog(); + var ship = RestoredHullsTests.BuildThrustedShip(cache, Hull); + var start = ship.Direction; + + ship.Turn = 0; + ship.Aim = float3(Right(ship).x, 0, Right(ship).y); + for (var i = 0; i < 60; i++) ship.Update(1f / 60f); + + Assert.True(Angle(start, ship.Direction) < radians(.01f)); + } + + [Fact] + public void TurnRotatesThroughTheThrusters() + { + using var cache = RestoredHullsTests.OpenCatalog(); + var clockwise = RestoredHullsTests.BuildThrustedShip(cache, Hull); + var counter = RestoredHullsTests.BuildThrustedShip(cache, Hull); + var dead = RestoredHullsTests.BuildThrustedShip(cache, Hull); + var start = clockwise.Direction; + var right = Right(clockwise); + + foreach (var item in dead.Equipment) + if (item.Behaviors.Any(b => b is Thruster)) item.Enabled.Value = false; + + clockwise.Turn = 1; + counter.Turn = -1; + dead.Turn = 1; + for (var i = 0; i < 60; i++) + { + clockwise.Update(1f / 60f); + counter.Update(1f / 60f); + dead.Update(1f / 60f); + } + + Assert.True(dot(clockwise.Direction, right) > .01f); + Assert.True(dot(counter.Direction, right) < -.01f); + Assert.True(Angle(start, dead.Direction) < radians(.01f)); + } + + [Fact] + public void AccelerateWritesTurnNotAim() + { + using var cache = RestoredHullsTests.OpenCatalog(); + var ship = RestoredHullsTests.BuildThrustedShip(cache, Hull); + var agent = new Agent(ship); + var aim = float3(0, 0, 1); + ship.Aim = aim; + + agent.Accelerate(Right(ship) * 100f); + Assert.True(ship.Turn > 0f); + Assert.Equal(aim, ship.Aim); + + ship.Turn = .37f; + agent.Accelerate(-Right(ship) * 100f, true); + Assert.Equal(.37f, ship.Turn); + Assert.Equal(aim, ship.Aim); + } +} diff --git a/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs b/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs index 29bb3f954..bc6af3597 100644 --- a/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs +++ b/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs @@ -352,7 +352,7 @@ public sealed class TargetingSystemTests : IDisposable Assert.True(ship.TryEquip(targeting)); zone.Entities.Add(ship); - ship.LookDirection = float3(0, 0, 1); + ship.Aim = float3(0, 0, 1); ship.Activate(); return ship; }