LANTERN feel pass (DR-052 + gap list): SoD facing, underwater feel, Bathynaut kit, walk/run gait, suit lamp + new Synty anim packs
- SoD facing: PlayerFacing = body-yaw only (move-facing / cast-turn / idle-hold); every fire direction re-sourced to FacingMath.ResolveAim (pre-code review blocking catch); TickWindowMath shared windows with Movement-skip; reticle/FX coupled to the damage direction; cursor-dash kept. - Underwater feel: sharpness 15->6, turn 720->360, MoveSpeed 6->4.2; TuningKnob 26-28 (0 = no-override sentinels, dev-protocol bump on DebugTuningReport); stride footsteps + silt + cadence floor; bubbles; underwater ambience bed + distant groans; camera drag + dev scroll zoom. - Bathynaut kit in-engine: dome/tank/shoulder-lamp + bare head grafted (GraftSmr rigid rebase, RecalculateTangents); EmissiveGloamSkinned shader (Rukhanka deformation); shoulder lamp CASTS (warm steady spot on body yaw). - Gait: two-ring walk/run FreeformDirectional tree (walk @0.35, run @1.0) + blended-natural StrideScale; additive Posture(Bank) + Lead(chest-lead) layers; idle = AnimationIdles Base; banking driven from facing turn rate; flat terrain (Env_SeabedKit seabed squashed - CC is planar). - New packs: Synty AnimationIdles + AnimationSwordCombat (combat pass queued) + SyntyPropBoneTool; four authored clips (sway/trudge/banks/lean) + Anim_Player_Underwater.blend + suit-kit FBX. - Validation: 411/411 EditMode green; Play smokes (server==client facing, Aim-true projectile, bank/stride live-sampled, lamp beam verified); pre-code + post-impl adversarial reviews applied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,7 @@ namespace ProjectM.Tests
|
||||
em.AddComponentData(e, new DashState());
|
||||
em.AddComponentData(e, new DashCooldown { NextTick = 0 });
|
||||
em.AddComponentData(e, new CharacterControl { MoveVelocity = float3.zero });
|
||||
em.AddComponentData(e, CharacterComponent.GetDefault()); // GroundedMovementSharpness = 15
|
||||
em.AddComponentData(e, CharacterComponent.GetDefault()); // GroundedMovementSharpness = DefaultGroundedSharpness
|
||||
em.AddComponentData(e, new PlayerInput());
|
||||
em.AddComponentData(e, new PlayerFacing { Direction = facing });
|
||||
em.AddComponent<Simulate>(e); // enabled by default
|
||||
@@ -91,7 +91,7 @@ namespace ProjectM.Tests
|
||||
|
||||
Assert.AreEqual(0f, math.length(em.GetComponentData<CharacterControl>(e).MoveVelocity), 1e-3f,
|
||||
"Recovery tail locks movement to zero (the punishable window).");
|
||||
Assert.AreEqual(15f, em.GetComponentData<CharacterComponent>(e).GroundedMovementSharpness, 1e-3f,
|
||||
Assert.AreEqual(CharacterComponent.DefaultGroundedSharpness, em.GetComponentData<CharacterComponent>(e).GroundedMovementSharpness, 1e-3f,
|
||||
"Recovery tail restores sharpness to the default (crisp stop).");
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ namespace ProjectM.Tests
|
||||
|
||||
group.Update(); // tick 200: window fully elapsed
|
||||
|
||||
Assert.AreEqual(15f, em.GetComponentData<CharacterComponent>(e).GroundedMovementSharpness, 1e-3f,
|
||||
Assert.AreEqual(CharacterComponent.DefaultGroundedSharpness, em.GetComponentData<CharacterComponent>(e).GroundedMovementSharpness, 1e-3f,
|
||||
"Sharpness restored to default after the dash window elapses.");
|
||||
Assert.AreEqual(7f, em.GetComponentData<CharacterControl>(e).MoveVelocity.x, 1e-3f,
|
||||
"Outside the window DashSystem does NOT touch MoveVelocity (PlayerControlSystem's input stands).");
|
||||
@@ -204,7 +204,7 @@ namespace ProjectM.Tests
|
||||
|
||||
Assert.IsTrue(em.IsComponentEnabled<Dead>(e), "Health<=0 derives Dead enabled.");
|
||||
Assert.AreEqual(0u, em.GetComponentData<DashState>(e).IFrameUntilTick, "Death clears the dash window (no stale i-frames on respawn).");
|
||||
Assert.AreEqual(15f, em.GetComponentData<CharacterComponent>(e).GroundedMovementSharpness, 1e-3f, "Death restores base sharpness.");
|
||||
Assert.AreEqual(CharacterComponent.DefaultGroundedSharpness, em.GetComponentData<CharacterComponent>(e).GroundedMovementSharpness, 1e-3f, "Death restores base sharpness.");
|
||||
Assert.AreEqual(0f, math.length(em.GetComponentData<CharacterControl>(e).MoveVelocity), 1e-3f, "Death zeroes movement.");
|
||||
}
|
||||
}
|
||||
@@ -227,7 +227,7 @@ namespace ProjectM.Tests
|
||||
|
||||
Assert.AreEqual(5f, em.GetComponentData<CharacterControl>(e).MoveVelocity.x, 1e-3f,
|
||||
"A re-simulated PRE-dash tick keeps PlayerControl's input velocity (no dash override, no recovery lock).");
|
||||
Assert.AreEqual(15f, em.GetComponentData<CharacterComponent>(e).GroundedMovementSharpness, 1e-3f,
|
||||
Assert.AreEqual(CharacterComponent.DefaultGroundedSharpness, em.GetComponentData<CharacterComponent>(e).GroundedMovementSharpness, 1e-3f,
|
||||
"A re-simulated PRE-dash tick keeps base sharpness.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
using NUnit.Framework;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure-math coverage for the Shape-of-Dreams facing model (07-15): ResolveAim (the shared gameplay
|
||||
/// fire-direction resolver), SelectTarget (the Aim→Move→hold cascade — castActive grants Aim PRIORITY,
|
||||
/// never bypasses the cascade), and RotateToward (the rate-limited turn extracted verbatim from
|
||||
/// PlayerAimSystem). Plain NUnit, no World.
|
||||
/// </summary>
|
||||
public class FacingMathTests
|
||||
{
|
||||
const float Eps = 1e-4f;
|
||||
|
||||
// ---- ResolveAim ----
|
||||
|
||||
[Test]
|
||||
public void ResolveAim_Aim_Wins_And_Normalizes()
|
||||
{
|
||||
var d = FacingMath.ResolveAim(new float2(3f, 0f), new float2(0f, 1f));
|
||||
Assert.AreEqual(1f, d.x, Eps);
|
||||
Assert.AreEqual(0f, d.y, Eps);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResolveAim_Zero_Aim_Falls_Back_To_Facing()
|
||||
{
|
||||
var d = FacingMath.ResolveAim(float2.zero, new float2(0f, -2f));
|
||||
Assert.AreEqual(0f, d.x, Eps);
|
||||
Assert.AreEqual(-1f, d.y, Eps);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResolveAim_Both_Zero_Falls_Back_To_PlusZ()
|
||||
{
|
||||
var d = FacingMath.ResolveAim(float2.zero, float2.zero);
|
||||
Assert.AreEqual(0f, d.x, Eps);
|
||||
Assert.AreEqual(1f, d.y, Eps);
|
||||
}
|
||||
|
||||
// ---- SelectTarget (the cascade) ----
|
||||
|
||||
[Test]
|
||||
public void SelectTarget_CastActive_Prefers_Aim_Over_Move()
|
||||
{
|
||||
Assert.IsTrue(FacingMath.SelectTarget(true, new float2(1f, 0f), new float2(0f, 1f), out var t));
|
||||
Assert.AreEqual(1f, t.x, Eps);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SelectTarget_CastActive_Zero_Aim_Falls_Through_To_Move()
|
||||
{
|
||||
// Gamepad resting right stick mid-cast: Aim is exact zero -> the cascade must fall to Move,
|
||||
// never freeze facing (the review's should-fix on the cast branch).
|
||||
Assert.IsTrue(FacingMath.SelectTarget(true, float2.zero, new float2(0f, 1f), out var t));
|
||||
Assert.AreEqual(1f, t.y, Eps);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SelectTarget_Not_Casting_Ignores_Aim()
|
||||
{
|
||||
// The cursor is never passively tracked: outside a cast window Aim must NOT win.
|
||||
Assert.IsTrue(FacingMath.SelectTarget(false, new float2(1f, 0f), new float2(0f, 1f), out var t));
|
||||
Assert.AreEqual(1f, t.y, Eps);
|
||||
Assert.AreEqual(0f, t.x, Eps);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SelectTarget_No_Input_Holds()
|
||||
{
|
||||
Assert.IsFalse(FacingMath.SelectTarget(false, float2.zero, float2.zero, out _));
|
||||
Assert.IsFalse(FacingMath.SelectTarget(true, float2.zero, float2.zero, out _));
|
||||
}
|
||||
|
||||
// ---- RotateToward ----
|
||||
|
||||
[Test]
|
||||
public void RotateToward_Uninitialized_Snaps()
|
||||
{
|
||||
var d = FacingMath.RotateToward(float2.zero, new float2(1f, 0f), 0.01f);
|
||||
Assert.AreEqual(1f, d.x, Eps);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RotateToward_Within_Reach_Snaps_To_Target()
|
||||
{
|
||||
var d = FacingMath.RotateToward(new float2(0f, 1f), new float2(0f, 1f), 0.1f);
|
||||
Assert.AreEqual(0f, d.x, Eps);
|
||||
Assert.AreEqual(1f, d.y, Eps);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RotateToward_Steps_By_MaxStep_Toward_Target()
|
||||
{
|
||||
// 90° to cover, 30° step -> lands at 60° remaining (rotated 30° toward +X, the clockwise side).
|
||||
float step = math.radians(30f);
|
||||
var d = FacingMath.RotateToward(new float2(0f, 1f), new float2(1f, 0f), step);
|
||||
float remaining = math.degrees(math.acos(math.clamp(math.dot(d, new float2(1f, 0f)), -1f, 1f)));
|
||||
Assert.AreEqual(60f, remaining, 0.05f);
|
||||
Assert.Greater(d.x, 0f, "rotates toward the target side");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RotateToward_Turns_The_Short_Way_Both_Sides()
|
||||
{
|
||||
float step = math.radians(10f);
|
||||
var right = FacingMath.RotateToward(new float2(0f, 1f), new float2(1f, 0.001f), step);
|
||||
var left = FacingMath.RotateToward(new float2(0f, 1f), new float2(-1f, 0.001f), step);
|
||||
Assert.Greater(right.x, 0f);
|
||||
Assert.Less(left.x, 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df3dffe03fe2912418e03469505c774b
|
||||
@@ -55,6 +55,10 @@ namespace ProjectM.Tests
|
||||
Add<StatRecomputeSystem>(); Add<MeleeComboSystem>(); Add<DashSystem>(); Add<DashTrailDamageSystem>();
|
||||
Add<AbilityFireSystem>(); Add<ProjectileMoveSystem>(); Add<ProjectileDamageSystem>();
|
||||
Add<HealthApplyDamageSystem>(); Add<KillRewardSystem>();
|
||||
// 07-15 facing rework: PlayerAimSystem gained [UpdateAfter(MeleeComboSystem)] (plus the existing
|
||||
// StatRecompute/PlayerDeathState UpdateBefore edges) - co-register the full facing neighborhood so a
|
||||
// cycle in these edges throws here instead of only at Play world-creation.
|
||||
Add<PlayerAimSystem>(); Add<PlayerControlSystem>(); Add<BlinkSystem>(); Add<PlayerDeathStateSystem>();
|
||||
|
||||
Assert.DoesNotThrow(() => group.SortSystems(),
|
||||
"A cycle in the Phase 1.7 predicted combat chain throws here instead of only at Play world-creation.");
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
using NUnit.Framework;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Coverage for the shared tick-window predicates (extracted 07-15 from PlayerAnimationDriveSystem so the
|
||||
/// predicted facing path and the anim path share one implementation): swing/fire window boundaries
|
||||
/// (wrap-safe NetworkTick math, 0-sentinel guards) and the Movement-archetype skip (a blink is a dodge,
|
||||
/// never a cast — and never a phantom IsFiring at its cooldown tail).
|
||||
/// </summary>
|
||||
public class TickWindowMathTests
|
||||
{
|
||||
const uint N = 13; // window ticks (the anim pulse length; callers pass their own)
|
||||
|
||||
// ---- SwingActive ----
|
||||
|
||||
[Test]
|
||||
public void SwingActive_Window_Is_HalfOpen_From_Start()
|
||||
{
|
||||
var mc = new MeleeCombo { SwingStartTick = 100u };
|
||||
Assert.IsFalse(TickWindowMath.SwingActive(mc, new NetworkTick(99u), N), "before start");
|
||||
Assert.IsTrue(TickWindowMath.SwingActive(mc, new NetworkTick(100u), N), "start tick included");
|
||||
Assert.IsTrue(TickWindowMath.SwingActive(mc, new NetworkTick(112u), N), "last tick included");
|
||||
Assert.IsFalse(TickWindowMath.SwingActive(mc, new NetworkTick(113u), N), "end excluded");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SwingActive_Zero_Stamp_Or_Invalid_Tick_Is_False()
|
||||
{
|
||||
Assert.IsFalse(TickWindowMath.SwingActive(default, new NetworkTick(100u), N));
|
||||
Assert.IsFalse(TickWindowMath.SwingActive(new MeleeCombo { SwingStartTick = 100u }, default, N));
|
||||
}
|
||||
|
||||
// ---- FireActive ----
|
||||
|
||||
[Test]
|
||||
public void FireActive_Reconstructs_The_Window_From_The_Cooldown_Stamp()
|
||||
{
|
||||
// NextFireTick 120, CooldownTicks 20 -> fired at 100 -> window [100, 113).
|
||||
Assert.IsFalse(TickWindowMath.FireActive(120u, 20, new NetworkTick(99u), N));
|
||||
Assert.IsTrue(TickWindowMath.FireActive(120u, 20, new NetworkTick(100u), N));
|
||||
Assert.IsTrue(TickWindowMath.FireActive(120u, 20, new NetworkTick(112u), N));
|
||||
Assert.IsFalse(TickWindowMath.FireActive(120u, 20, new NetworkTick(113u), N));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FireActive_Sentinels_Are_False()
|
||||
{
|
||||
Assert.IsFalse(TickWindowMath.FireActive(0u, 20, new NetworkTick(100u), N), "no stamp");
|
||||
Assert.IsFalse(TickWindowMath.FireActive(120u, 0, new NetworkTick(100u), N), "no cooldown");
|
||||
Assert.IsFalse(TickWindowMath.FireActive(120u, 20, default, N), "invalid now");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FireActive_Window_Start_Wrapping_To_Zero_Coerces_Through_NonZero()
|
||||
{
|
||||
// The project's canonical tick-wrap hazard: a computed start of exactly 0 (the "invalid" sentinel)
|
||||
// must coerce through TickUtil.NonZero to 1, not read as no-window. NextFireTick 20, CooldownTicks 20
|
||||
// -> raw start 0 -> window [1, 14).
|
||||
Assert.IsTrue(TickWindowMath.FireActive(20u, 20, new NetworkTick(5u), N));
|
||||
Assert.IsTrue(TickWindowMath.FireActive(20u, 20, new NetworkTick(13u), N));
|
||||
Assert.IsFalse(TickWindowMath.FireActive(20u, 20, new NetworkTick(14u), N), "end excluded");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Missing_AbilityDb_Falls_Back_To_Include_All()
|
||||
{
|
||||
// Without a blob the archetype is unknown: the socket is INCLUDED (matches the old include-all
|
||||
// behavior) — even a Movement spark counts, because it cannot be identified as one.
|
||||
using var world = new World("TickWindowTestNoDb");
|
||||
var em = world.EntityManager;
|
||||
var e = em.CreateEntity();
|
||||
em.AddBuffer<AbilitySocket>(e);
|
||||
em.AddBuffer<EffectiveSocketStats>(e);
|
||||
var sockets = em.GetBuffer<AbilitySocket>(e);
|
||||
sockets.Add(new AbilitySocket { SparkId = 2 });
|
||||
for (int i = 1; i < SocketId.Count; i++) sockets.Add(default);
|
||||
var effs = em.GetBuffer<EffectiveSocketStats>(e);
|
||||
effs.Add(new EffectiveSocketStats { CooldownTicks = 20 });
|
||||
for (int i = 1; i < SocketId.Count; i++) effs.Add(default);
|
||||
var cd = default(SocketCooldown);
|
||||
cd.Set(0, 120u);
|
||||
TickWindowMath.SocketFireAndCone(cd, em.GetBuffer<AbilitySocket>(e), em.GetBuffer<EffectiveSocketStats>(e),
|
||||
default, new NetworkTick(105u), N, out bool firing, out bool cone);
|
||||
Assert.IsTrue(firing);
|
||||
Assert.IsFalse(cone);
|
||||
}
|
||||
|
||||
|
||||
// ---- SocketFireAndCone (Movement skip) ----
|
||||
|
||||
static BlobAssetReference<AbilityDatabaseBlob> BuildDb()
|
||||
{
|
||||
using var builder = new BlobBuilder(Allocator.Temp);
|
||||
ref var root = ref builder.ConstructRoot<AbilityDatabaseBlob>();
|
||||
var abilities = builder.Allocate(ref root.Abilities, 3);
|
||||
abilities[0] = new AbilityDefBlob { Id = 1, Archetype = (byte)AbilityArchetype.Projectile, CooldownTicks = 20, Name = "Proj" };
|
||||
abilities[1] = new AbilityDefBlob { Id = 2, Archetype = (byte)AbilityArchetype.Movement, CooldownTicks = 20, Name = "Blink" };
|
||||
abilities[2] = new AbilityDefBlob { Id = 3, Archetype = (byte)AbilityArchetype.Cone, CooldownTicks = 20, Name = "Cone" };
|
||||
builder.Allocate(ref root.Characters, 0);
|
||||
return builder.CreateBlobAssetReference<AbilityDatabaseBlob>(Allocator.Persistent);
|
||||
}
|
||||
|
||||
static void RunSocketCase(byte sparkId, out bool firing, out bool cone)
|
||||
{
|
||||
using var world = new World("TickWindowTest");
|
||||
var em = world.EntityManager;
|
||||
var e = em.CreateEntity();
|
||||
em.AddBuffer<AbilitySocket>(e);
|
||||
em.AddBuffer<EffectiveSocketStats>(e);
|
||||
var sockets = em.GetBuffer<AbilitySocket>(e);
|
||||
sockets.Add(new AbilitySocket { SparkId = sparkId });
|
||||
for (int i = 1; i < SocketId.Count; i++) sockets.Add(default);
|
||||
var effs = em.GetBuffer<EffectiveSocketStats>(e);
|
||||
effs.Add(new EffectiveSocketStats { CooldownTicks = 20 });
|
||||
for (int i = 1; i < SocketId.Count; i++) effs.Add(default);
|
||||
var db = BuildDb();
|
||||
try
|
||||
{
|
||||
var cd = default(SocketCooldown);
|
||||
cd.Set(0, 120u); // fired at 100 (CooldownTicks 20) -> window [100, 113)
|
||||
TickWindowMath.SocketFireAndCone(cd, em.GetBuffer<AbilitySocket>(e), em.GetBuffer<EffectiveSocketStats>(e),
|
||||
db, new NetworkTick(105u), N, out firing, out cone);
|
||||
}
|
||||
finally { db.Dispose(); }
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Movement_Socket_Never_Counts_As_Firing()
|
||||
{
|
||||
RunSocketCase(2, out bool firing, out bool cone);
|
||||
Assert.IsFalse(firing, "a blink (Movement archetype) is a dodge, never a cast/IsFiring pulse");
|
||||
Assert.IsFalse(cone);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Projectile_Socket_Mid_Window_Fires_Without_Cone()
|
||||
{
|
||||
RunSocketCase(1, out bool firing, out bool cone);
|
||||
Assert.IsTrue(firing);
|
||||
Assert.IsFalse(cone);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Cone_Socket_Mid_Window_Fires_With_Cone()
|
||||
{
|
||||
RunSocketCase(3, out bool firing, out bool cone);
|
||||
Assert.IsTrue(firing);
|
||||
Assert.IsTrue(cone);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09e0ffd7ff3700143ada83fd776310d3
|
||||
@@ -84,6 +84,36 @@ namespace ProjectM.Tests
|
||||
Assert.AreEqual(0f, c.ChargerLungeSpeed, 1e-6f, "ChargerLungeSpeed floors at 0");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FeelOverride_Knobs_Default_To_Zero_And_Allow_The_NoOverride_Sentinel()
|
||||
{
|
||||
// 07-15 facing/underwater feel: TurnRateDeg/CastTurnRateDeg/MoveSharpness use 0 = NO-OVERRIDE
|
||||
// (consumers substitute the authored stat / cast const / sharpness const). They must default to 0,
|
||||
// clamp negatives to 0, and round-trip 0 through Apply (the default >=1 tick-branch would silently
|
||||
// floor them to 1 and force a permanent 1-unit override).
|
||||
var c = TuningConfig.Defaults();
|
||||
Assert.AreEqual(0f, c.TurnRateDeg, 1e-6f);
|
||||
Assert.AreEqual(0f, c.CastTurnRateDeg, 1e-6f);
|
||||
Assert.AreEqual(0f, c.MoveSharpness, 1e-6f);
|
||||
|
||||
TuningConfig.Apply(ref c, TuningKnob.TurnRateDeg, -5f);
|
||||
Assert.AreEqual(0f, c.TurnRateDeg, 1e-6f, "negatives clamp to the sentinel, not 1");
|
||||
TuningConfig.Apply(ref c, TuningKnob.CastTurnRateDeg, 720f);
|
||||
Assert.AreEqual(720f, c.CastTurnRateDeg, 1e-6f);
|
||||
TuningConfig.Apply(ref c, TuningKnob.CastTurnRateDeg, 0f);
|
||||
Assert.AreEqual(0f, c.CastTurnRateDeg, 1e-6f, "0 must survive Apply (back to no-override)");
|
||||
TuningConfig.Apply(ref c, TuningKnob.MoveSharpness, 9f);
|
||||
Assert.AreEqual(9f, TuningConfig.Get(c, TuningKnob.MoveSharpness), 1e-6f);
|
||||
|
||||
// The new fields ride the report like every other knob (a missed FromReport field would zero them).
|
||||
c.TurnRateDeg = 360f; c.CastTurnRateDeg = 1080f; c.MoveSharpness = 6f;
|
||||
var r = TuningConfig.FromReport(TuningConfig.ToReport(c));
|
||||
Assert.AreEqual(360f, r.TurnRateDeg, 1e-6f);
|
||||
Assert.AreEqual(1080f, r.CastTurnRateDeg, 1e-6f);
|
||||
Assert.AreEqual(6f, r.MoveSharpness, 1e-6f);
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void Apply_Ignores_An_Out_Of_Range_Knob_Index()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user