diff --git a/Movement tests.sln.DotSettings.user b/Movement tests.sln.DotSettings.user index 8a1bb742..f3230b19 100644 --- a/Movement tests.sln.DotSettings.user +++ b/Movement tests.sln.DotSettings.user @@ -5,12 +5,8 @@ ForceIncluded ForceIncluded ForceIncluded - <SessionState ContinuousTestingMode="0" IsActive="True" Name="Junie Session" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <TestAncestor> - <TestId>VsTest::22FEBE6E-769C-4716-A687-A0AC8F3EF84A::net9.0::executor://gdunit4.testadapter/#Movementtests.tests.PlayerControllerUnitTest</TestId> - </TestAncestor> -</SessionState> - <SessionState ContinuousTestingMode="0" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Solution /> </SessionState> D:\Godot\Projects\movement-tests\.runsettings diff --git a/maps/levels/tuto_enemies.tscn b/maps/levels/tuto_enemies.tscn index 43f94cbf..8405211b 100644 --- a/maps/levels/tuto_enemies.tscn +++ b/maps/levels/tuto_enemies.tscn @@ -32,7 +32,6 @@ HealthInputs = ExtResource("7_ucbss") DamageInputs = ExtResource("8_2brdd") Target = NodePath("../Player") SpawnInterval = 5.0 -IsActiveOnStart = false [node name="GroundedSpawner2" parent="." index="9" unique_id=1026317919 node_paths=PackedStringArray("Target") instance=ExtResource("4_jaqjx")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 46.5, 11.5, -34.5) @@ -42,7 +41,6 @@ HealthInputs = ExtResource("7_ucbss") DamageInputs = ExtResource("8_2brdd") Target = NodePath("../Player") SpawnInterval = 5.0 -IsActiveOnStart = false [node name="GroundedSpawner3" parent="." index="10" unique_id=241829575 node_paths=PackedStringArray("Target") instance=ExtResource("4_jaqjx")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 44.5, 0, -3) @@ -52,7 +50,6 @@ HealthInputs = ExtResource("7_ucbss") DamageInputs = ExtResource("8_2brdd") Target = NodePath("../Player") SpawnInterval = 5.0 -IsActiveOnStart = false [node name="FlyingSpawner" parent="." index="11" unique_id=962840208 node_paths=PackedStringArray("Target") instance=ExtResource("4_jaqjx")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16.5, 19, -19.5) @@ -62,7 +59,6 @@ HealthInputs = ExtResource("11_5jlg7") DamageInputs = ExtResource("12_pjgox") Target = NodePath("../Player") SpawnInterval = 5.0 -IsActiveOnStart = false [node name="FlyingSpawner2" parent="." index="12" unique_id=365997644 node_paths=PackedStringArray("Target") instance=ExtResource("4_jaqjx")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45.5, 25.5, -42.5) @@ -72,7 +68,6 @@ HealthInputs = ExtResource("11_5jlg7") DamageInputs = ExtResource("12_pjgox") Target = NodePath("../Player") SpawnInterval = 5.0 -IsActiveOnStart = false [node name="Targets" type="Node3D" parent="." index="13" unique_id=1620747784] diff --git a/scenes/components/damage/RDamageModifier.cs b/scenes/components/damage/RDamageModifier.cs index b421b409..29bebc7b 100644 --- a/scenes/components/damage/RDamageModifier.cs +++ b/scenes/components/damage/RDamageModifier.cs @@ -7,7 +7,7 @@ using Movementtests.systems.damage; [GlobalClass] public partial class RDamageModifier : Resource, IDamageable { - public event Action DamageTaken; + public event Action DamageTaken = null!; [Export] public EDamageTypes DamageType { get; set;} diff --git a/scenes/components/health/CHealth.cs b/scenes/components/health/CHealth.cs index f07ec474..ea6b6f25 100644 --- a/scenes/components/health/CHealth.cs +++ b/scenes/components/health/CHealth.cs @@ -5,11 +5,11 @@ using Movementtests.interfaces; [GlobalClass, Icon("res://assets/ui/IconGodotNode/white/icon_heart.png")] public partial class CHealth : Node, IHealthable { - public event Action HealthChanged; - public event Action HealthDepleted; + public event Action HealthChanged = null!; + public event Action HealthDepleted = null!; [Export] - public RHealth RHealth { get; set; } + public RHealth RHealth { get; set; } = null!; public float CurrentHealth { get; set; } diff --git a/scenes/components/knockback/CKnockback.cs b/scenes/components/knockback/CKnockback.cs index a2ca9c9b..b631b29b 100644 --- a/scenes/components/knockback/CKnockback.cs +++ b/scenes/components/knockback/CKnockback.cs @@ -5,9 +5,9 @@ using Movementtests.interfaces; [GlobalClass, Icon("res://assets/ui/IconGodotNode/node_3D/icon_wind.png")] public partial class CKnockback : Node3D, IKnockbackable { - [Export] public RKnockback RKnockback { get; set;} - - private KnockbackRecord _knockbackRecord = null; + [Export] public RKnockback RKnockback { get; set;} = null!; + + private KnockbackRecord _knockbackRecord = null!; public void RegisterKnockback(KnockbackRecord knockbackRecord) { @@ -20,7 +20,7 @@ public partial class CKnockback : Node3D, IKnockbackable var knockbackDirection = GlobalPosition - _knockbackRecord.DamageRecord.SourceLocation; var finalKnockback = knockbackDirection.Normalized() * RKnockback.Modifier * _knockbackRecord.ForceMultiplier; - _knockbackRecord = null; + _knockbackRecord = null!; return finalKnockback; } } diff --git a/scenes/components/movement/CGroundedMovement.cs b/scenes/components/movement/CGroundedMovement.cs index 3168e4aa..bd804fca 100644 --- a/scenes/components/movement/CGroundedMovement.cs +++ b/scenes/components/movement/CGroundedMovement.cs @@ -6,10 +6,9 @@ namespace Movementtests.scenes.movement; [GlobalClass, Icon("res://assets/ui/IconGodotNode/node_3D/icon_path_follow.png")] public partial class CGroundedMovement : Node3D, IMoveable { - [Export] public RMovement RMovement { get; set; } + [Export] public RMovement RMovement { get; set; } = null!; - [Export] - public RayCast3D WallInFrontRayCast { get; set; } + [Export] public RayCast3D WallInFrontRayCast { get; set; } = null!; public Vector3 ComputeVelocity(MovementInputs inputs) diff --git a/scenes/enemies/Enemy.cs b/scenes/enemies/Enemy.cs index 831daf27..9b86f798 100644 --- a/scenes/enemies/Enemy.cs +++ b/scenes/enemies/Enemy.cs @@ -16,36 +16,37 @@ public partial class Enemy : CharacterBody3D, IStunnable { // Signals and events - public event Action DamageTaken; - public event Action HealthChanged; - public event Action HealthDepleted; + public event Action DamageTaken = null!; + public event Action HealthChanged = null!; + public event Action HealthDepleted = null!; // Public export components [Export] - public Node3D Target { get; set; } + public Node3D Target { get; set; } = null!; + [Export] public float EnemyHeight { get; set; } = 1f; [ExportGroup("Health")] [Export] - public RHealth RHealth { get; set; } + public RHealth RHealth { get; set; } = null!; [Export] - public RDeathEffect[] DeathEffects { get; set; } - public IHealthable CHealth { get; set; } + public RDeathEffect[] DeathEffects { get; set; } = null!; + public IHealthable CHealth { get; set; } = null!; [ExportGroup("Damage")] [Export] - public RDamage RDamage { get; set; } - public IDamageable CDamageable { get; set; } + public RDamage RDamage { get; set; } = null!; + public IDamageable CDamageable { get; set; } = null!; [Export] - public RKnockback RKnockback { get; set; } - public IKnockbackable CKnockback { get; set; } + public RKnockback RKnockback { get; set; } = null!; + public IKnockbackable CKnockback { get; set; } = null!; [ExportGroup("Movement")] [Export] - public RMovement RMovement { get; set; } - public IMoveable CMovement { get; set; } + public RMovement RMovement { get; set; } = null!; + public IMoveable CMovement { get; set; } = null!; // Public stuff public float CurrentHealth @@ -55,9 +56,9 @@ public partial class Enemy : CharacterBody3D, } // Private stuff - private Area3D _damageBox; - private Node3D _target; - private Healthbar _healthbar; + private Area3D _damageBox = null!; + internal Node3D _target = null!; + private Healthbar _healthbar = null!; public override void _Ready() { @@ -70,34 +71,28 @@ public partial class Enemy : CharacterBody3D, _damageBox = GetNode("DamageBox"); _target = GetNode("CTarget"); - CDamageable = GetNode("CDamageable") as IDamageable; - CMovement = GetNode("CMovement") as IMoveable; - CHealth = GetNode("CHealth") as IHealthable; - CKnockback = GetNode("CKnockback") as IKnockbackable; - if (CDamageable is null) GD.PrintErr("This node needs a 'CDamage' child of type IDamageable!"); - if (CMovement is null) GD.PrintErr("This node needs a 'CMovement' child of type IMoveable!"); - if (CHealth is null) GD.PrintErr("This node needs a 'CHealth' child of type IHealthable!"); - if (CKnockback is null) GD.PrintErr("This node needs a 'CKnockback' child of type IKnockbackable!"); + CDamageable = (GetNode("CDamageable") as IDamageable)!; + CMovement = (GetNode("CMovement") as IMoveable)!; + CHealth = (GetNode("CHealth") as IHealthable)!; + CKnockback = (GetNode("CKnockback") as IKnockbackable)!; _healthbar = GetNode("CHealthBar").Healthbar; - if (RMovement != null) CMovement!.RMovement = RMovement; - if (RHealth != null) - { - CHealth!.RHealth = RHealth; - CHealth.CurrentHealth = RHealth.StartingHealth; - } - if (RKnockback != null) CKnockback!.RKnockback = RKnockback; - _healthbar.Initialize(CHealth!.CurrentHealth); + CMovement.RMovement = RMovement; + CHealth.RHealth = RHealth; + CHealth.CurrentHealth = RHealth.StartingHealth; + CKnockback.RKnockback = RKnockback; + + _healthbar.Initialize(CHealth.CurrentHealth); } public void SetupSignals() { // Anonymous function call to erase return values of ReduceHealth CDamageable.DamageTaken += (source, record) => ReduceHealth(source, record); - CDamageable.DamageTaken += (source, record) => RegisterKnockback(new KnockbackRecord(record)); + CDamageable.DamageTaken += (_, record) => RegisterKnockback(new KnockbackRecord(record)); CHealth.HealthDepleted += Kill; - HealthChanged += (source, record) => _healthbar.SetHealth(record.CurrentHealth); + HealthChanged += (_, record) => _healthbar.SetHealth(record.CurrentHealth); } public override void _PhysicsProcess(double delta) @@ -187,24 +182,21 @@ public partial class Enemy : CharacterBody3D, public void RegisterKnockback(KnockbackRecord knockbackRecord) { - if (CKnockback is null) return; CKnockback.RegisterKnockback(knockbackRecord); } public Vector3 ComputeKnockback() { - if (CKnockback is null) return Vector3.Zero; return CKnockback.ComputeKnockback(); } public Vector3 GetTargetGlobalPosition() { - if (_target is null) return GlobalPosition; - return _target.GlobalPosition; + return _target == null ? GlobalPosition : _target.GlobalPosition; } // Stun management - public bool IsStunned { get; set; } = false; + public bool IsStunned { get; set; } [Export(PropertyHint.Range, "0.1, 2, 0.1, or_greater")] public float StunDuration { get; set; } = 1f; diff --git a/scenes/player_controller/PlayerUi.cs b/scenes/player_controller/PlayerUi.cs index 746ef740..bb9bf0f0 100644 --- a/scenes/player_controller/PlayerUi.cs +++ b/scenes/player_controller/PlayerUi.cs @@ -5,7 +5,7 @@ using Movementtests.interfaces; [GlobalClass, Icon("res://assets/ui/IconGodotNode/control/icon_text_panel.png")] public partial class PlayerUi : Control { - private TextureRect[] _dashIcons = new TextureRect[3]; + internal TextureRect[] _dashIcons = new TextureRect[3]; private TextureRect _enemyTarget; private Healthbar _healthbar; diff --git a/scenes/player_controller/components/dash/DashSystem.cs b/scenes/player_controller/components/dash/DashSystem.cs index 0ebecc6a..686c0125 100644 --- a/scenes/player_controller/components/dash/DashSystem.cs +++ b/scenes/player_controller/components/dash/DashSystem.cs @@ -26,18 +26,18 @@ public partial class DashSystem: Node3D public Vector3 PlannedMantleLocation { get; set; } public MantleSystem MantleSystem { get; set; } - private HeadSystem _head; + internal HeadSystem _head; public ShapeCast3D DashCast3D; - private Camera3D _camera; - private Vector3 _dashDirection = Vector3.Zero; + internal Camera3D _camera; + internal Vector3 _dashDirection = Vector3.Zero; - private ShapeCast3D _dashCastDrop; - private MeshInstance3D _dashDropIndicator; - private MeshInstance3D _dashDropLocationIndicator; + internal ShapeCast3D _dashCastDrop; + internal MeshInstance3D _dashDropIndicator; + internal MeshInstance3D _dashDropLocationIndicator; - private MeshInstance3D _dashTarget; - private CpuParticles3D _dashIndicator; - private AnimationPlayer _dashIndicatorAnim; + internal MeshInstance3D _dashTarget; + internal CpuParticles3D _dashIndicator; + internal AnimationPlayer _dashIndicatorAnim; [Export] public PackedScene DashIndicatorScene { get; set; } @@ -77,7 +77,7 @@ public partial class DashSystem: Node3D _dashIndicatorAnim = GetNode("DashIndicator/AnimationPlayer"); } - private DashLocation ComputeDashLocation() + internal DashLocation ComputeDashLocation() { var targetLocation = DashCast3D.ToGlobal(DashCast3D.TargetPosition); var hasHit = DashCast3D.IsColliding(); diff --git a/scenes/player_controller/components/head/HeadSystem.cs b/scenes/player_controller/components/head/HeadSystem.cs index 9e883ad9..50653503 100644 --- a/scenes/player_controller/components/head/HeadSystem.cs +++ b/scenes/player_controller/components/head/HeadSystem.cs @@ -39,10 +39,10 @@ public partial class HeadSystem : Node3D float BobbingMultiplier, float FovMultiplier); - private Camera3D _camera; - private Marker3D _cameraAnchor; - private AnimationPlayer _animationPlayer; - private AnimationTree _animationTree; + internal Camera3D _camera; + internal Marker3D _cameraAnchor; + internal AnimationPlayer _animationPlayer; + internal AnimationTree _animationTree; [Export(PropertyHint.Range, "0,10,0.1,or_greater")] public float LookSensitivity { get; set; } = 1f; @@ -63,11 +63,11 @@ public partial class HeadSystem : Node3D [Export(PropertyHint.Range, "0,1,0.01,or_greater")] public float SlidingJitterAmplitude { get; set; } = 0.1f; - private FastNoiseLite _slidingNoise = new FastNoiseLite(); + internal FastNoiseLite _slidingNoise = new FastNoiseLite(); [ExportGroup("Bobbing")] - private float _bobbingAccumulator; // Constantly increases when player moves in X or/and Z axis + internal float _bobbingAccumulator; // Constantly increases when player moves in X or/and Z axis [Export(PropertyHint.Range, "0,10,0.01,or_greater")] public float BobbingFrequency { set; get; } = 2.4f; [Export(PropertyHint.Range, "0,0.4,0.01,or_greater")] @@ -84,11 +84,11 @@ public partial class HeadSystem : Node3D public float FovMaxedOutSpeed { get; set; } = 20f; [ExportGroup("First Person rig")] - private Node3D _fpRig; - private Node3D _rightHandedWeapon; - private Node3D _leftHandedWeapon; - private Node3D _fpDisplacedRig; - private Vector3 _fpDisplacedRigInitialRotation; + internal Node3D _fpRig; + internal Node3D _rightHandedWeapon; + internal Node3D _leftHandedWeapon; + internal Node3D _fpDisplacedRig; + internal Vector3 _fpDisplacedRigInitialRotation; [Export(PropertyHint.Range, "0,10,0.1,or_greater")] public float WeaponSway { get; set; } = 5f; [Export(PropertyHint.Range, "0,10,0.1,or_greater")] @@ -190,8 +190,8 @@ public partial class HeadSystem : Node3D EmitSignalHitboxDeactivated(); } - private bool _footstepEmitted; - private bool _isPlayingForcingAnim; + internal bool _footstepEmitted; + internal bool _isPlayingForcingAnim; public void ResetHeadBobbing() { diff --git a/scenes/player_controller/scripts/PlayerController.cs b/scenes/player_controller/scripts/PlayerController.cs index 99393e03..abfe61b6 100644 --- a/scenes/player_controller/scripts/PlayerController.cs +++ b/scenes/player_controller/scripts/PlayerController.cs @@ -265,8 +265,8 @@ public partial class PlayerController : CharacterBody3D, private float _inputRotateFloorplane; // Basic falling - private float _targetSpeed; - private float _gravity; + internal float _targetSpeed; + internal float _gravity; // Jump stuff private int _currentInputBufferFrames; @@ -290,7 +290,7 @@ public partial class PlayerController : CharacterBody3D, private Vector3 _currentWallContactPoint = Vector3.Zero; // Dash stuff - private bool _canDash = true; + internal bool _canDash = true; private bool _canDashAirborne = true; private float _playerHeight; private float _playerRadius; diff --git a/tests/components/DamageComponentUnitTest.cs b/tests/components/DamageComponentUnitTest.cs new file mode 100644 index 00000000..7baeaabd --- /dev/null +++ b/tests/components/DamageComponentUnitTest.cs @@ -0,0 +1,59 @@ +using Godot; +using GdUnit4; +using static GdUnit4.Assertions; +using Movementtests.interfaces; +using Movementtests.systems.damage; + +namespace Movementtests.tests; + +[TestSuite, RequireGodotRuntime] +public class DamageComponentUnitTest +{ + [TestCase] + public void DamageModifier_Applies_WhenTypeMatches() + { + var modifier = new RDamageModifier(EDamageTypes.Normal, 2.0f); + var input = new DamageRecord(Vector3.Zero, new RDamage(10.0f, EDamageTypes.Normal)); + + var result = modifier.TakeDamage(input); + AssertFloat(result.Damage.DamageDealt).IsEqual(20.0f); + } + + [TestCase] + public void DamageModifier_Ignores_WhenTypeDifferent() + { + var modifier = new RDamageModifier(EDamageTypes.Fire, 3.0f); + var input = new DamageRecord(Vector3.Zero, new RDamage(10.0f, EDamageTypes.Normal)); + + var result = modifier.TakeDamage(input); + AssertFloat(result.Damage.DamageDealt).IsEqual(0.0f); + } + + [TestCase] + public void CDamageable_Sums_All_Modifiers() + { + var mod1 = new RDamageModifier(EDamageTypes.Normal, 1.0f); + var mod2 = new RDamageModifier(EDamageTypes.Normal, 0.5f); + + var cDamageable = new CDamageable(); + cDamageable.DamageModifiers = new[] { mod1, mod2 }; + + var input = new DamageRecord(Vector3.Zero, new RDamage(10.0f, EDamageTypes.Normal)); + var result = cDamageable.TakeDamage(input); + + // 10*1.0 + 10*0.5 = 15 + AssertFloat(result.Damage.DamageDealt).IsEqual(15.0f); + } + + [TestCase] + public void CDamageable_ComputeDamage_DoesNotEmit() + { + var mod = new RDamageModifier(EDamageTypes.Normal, 2.0f); + var cDamageable = new CDamageable(); + cDamageable.DamageModifiers = new[] { mod }; + + var input = new DamageRecord(Vector3.Zero, new RDamage(5.0f, EDamageTypes.Normal)); + var result = cDamageable.ComputeDamage(input); + AssertFloat(result.Damage.DamageDealt).IsEqual(10.0f); + } +} diff --git a/tests/components/DamageComponentUnitTest.cs.uid b/tests/components/DamageComponentUnitTest.cs.uid new file mode 100644 index 00000000..1351834c --- /dev/null +++ b/tests/components/DamageComponentUnitTest.cs.uid @@ -0,0 +1 @@ +uid://db6rva7uccppc diff --git a/tests/components/HealthComponentUnitTest.cs b/tests/components/HealthComponentUnitTest.cs new file mode 100644 index 00000000..c054ccd9 --- /dev/null +++ b/tests/components/HealthComponentUnitTest.cs @@ -0,0 +1,56 @@ +using Godot; +using GdUnit4; +using static GdUnit4.Assertions; +using Movementtests.interfaces; +using Movementtests.systems.damage; + +namespace Movementtests.tests; + +[TestSuite, RequireGodotRuntime] +public class HealthComponentUnitTest +{ + [TestCase] + public void ReadyInitializesCurrentHealth() + { + var cHealth = new CHealth(); + cHealth.RHealth = new RHealth(150.0f); + // Simulate Godot ready + cHealth._Ready(); + AssertFloat(cHealth.CurrentHealth).IsEqual(150.0f); + } + + [TestCase] + public void ReduceHealthDecreasesAndDoesNotDeplete() + { + var cHealth = new CHealth(); + cHealth.RHealth = new RHealth(100.0f); + cHealth.CurrentHealth = 100.0f; + + var damage = new DamageRecord(Vector3.Zero, new RDamage(25.0f, EDamageTypes.Normal)); + var record = cHealth.ReduceHealth(source: null!, damageRecord: damage); + + AssertFloat(cHealth.CurrentHealth).IsEqual(75.0f); + AssertFloat(record.CurrentHealth).IsEqual(75.0f); + AssertFloat(record.PreviousHealth).IsEqual(100.0f); + AssertFloat(record.MaxHealth).IsEqual(100.0f); + } + + [TestCase] + public void ReduceHealthTriggersDepletionToZero() + { + var cHealth = new CHealth(); + cHealth.RHealth = new RHealth(50.0f); + cHealth.CurrentHealth = 50.0f; + + bool depleted = false; + cHealth.HealthDepleted += _ => depleted = true; + + var damage = new DamageRecord(Vector3.Zero, new RDamage(100.0f, EDamageTypes.Normal)); + var record = cHealth.ReduceHealth(source: null!, damageRecord: damage); + + AssertBool(depleted).IsTrue(); + AssertFloat(cHealth.CurrentHealth).IsEqual(0.0f); + AssertFloat(record.CurrentHealth).IsEqual(-50.0f); + AssertFloat(record.MaxHealth).IsEqual(50.0f); + } +} diff --git a/tests/components/HealthComponentUnitTest.cs.uid b/tests/components/HealthComponentUnitTest.cs.uid new file mode 100644 index 00000000..00cee7a1 --- /dev/null +++ b/tests/components/HealthComponentUnitTest.cs.uid @@ -0,0 +1 @@ +uid://bd52i51hncgmf diff --git a/tests/components/KnockbackComponentUnitTest.cs b/tests/components/KnockbackComponentUnitTest.cs new file mode 100644 index 00000000..71ed316d --- /dev/null +++ b/tests/components/KnockbackComponentUnitTest.cs @@ -0,0 +1,31 @@ +using Godot; +using GdUnit4; +using static GdUnit4.Assertions; +using Movementtests.interfaces; + +namespace Movementtests.tests; + +[TestSuite, RequireGodotRuntime] +public class KnockbackComponentUnitTest +{ + [TestCase] + public void RegisterAndComputeKnockback_Works_And_Resets() + { + var cKnock = new CKnockback(); + cKnock.RKnockback = new RKnockback(2.0f); + cKnock.GlobalPosition = Vector3.Zero; + + var damage = new DamageRecord(new Vector3(10, 0, 0), new RDamage(0, Movementtests.systems.damage.EDamageTypes.Normal)); + var record = new KnockbackRecord(damage, 1.5f); + + cKnock.RegisterKnockback(record); + var force = cKnock.ComputeKnockback(); + + // Direction from source(10,0,0) to target(0,0,0) is (-1,0,0), scaled by modifier(2) and multiplier(1.5) => (-3,0,0) + AssertVector(force).IsEqual(new Vector3(-3, 0, 0)); + + // Second call returns zero since internal state resets + var second = cKnock.ComputeKnockback(); + AssertVector(second).IsEqual(Vector3.Zero); + } +} diff --git a/tests/components/KnockbackComponentUnitTest.cs.uid b/tests/components/KnockbackComponentUnitTest.cs.uid new file mode 100644 index 00000000..c5795539 --- /dev/null +++ b/tests/components/KnockbackComponentUnitTest.cs.uid @@ -0,0 +1 @@ +uid://bv0eionbgbig5 diff --git a/tests/components/MovementSystemUnitTest.cs b/tests/components/MovementSystemUnitTest.cs new file mode 100644 index 00000000..fc172e23 --- /dev/null +++ b/tests/components/MovementSystemUnitTest.cs @@ -0,0 +1,32 @@ +using Godot; +using GdUnit4; +using static GdUnit4.Assertions; +using Movementtests.interfaces; +using Movementtests.scenes.movement; + +namespace Movementtests.tests; + +[TestSuite, RequireGodotRuntime] +public class MovementSystemUnitTest +{ + [TestCase] + public void GroundedMovement_Accelerates_And_Applies_Gravity() + { + var move = new CGroundedMovement(); + move.RMovement = new RMovement(speed: 10.0f, acceleration: 1.0f, gravityModifier: 0.5f, targetHeight: 0.0f); + move.WallInFrontRayCast = new RayCast3D(); + move.GlobalPosition = Vector3.Zero; + + var inputs = new MovementInputs( + Velocity: Vector3.Zero, + TargetLocation: new Vector3(10, 0, 0), + isOnFloor: false, + gravity: Vector3.Down * 9.8f, + delta: 1.0 + ); + + var v = move.ComputeVelocity(inputs); + + AssertVector(v).IsEqualApprox(new Vector3(10, -4.9f, 0), new Vector3(0.0001f, 0.0001f, 0.0001f)); + } +} diff --git a/tests/components/MovementSystemUnitTest.cs.uid b/tests/components/MovementSystemUnitTest.cs.uid new file mode 100644 index 00000000..83493b96 --- /dev/null +++ b/tests/components/MovementSystemUnitTest.cs.uid @@ -0,0 +1 @@ +uid://cofj5s4x74ay diff --git a/tests/enemies/EnemyUnitTest.cs b/tests/enemies/EnemyUnitTest.cs new file mode 100644 index 00000000..dcdcc598 --- /dev/null +++ b/tests/enemies/EnemyUnitTest.cs @@ -0,0 +1,78 @@ +using Godot; +using GdUnit4; +using static GdUnit4.Assertions; +using Movementtests.interfaces; +using Movementtests.systems.damage; + +namespace Movementtests.tests; + +[TestSuite, RequireGodotRuntime] +public class EnemyUnitTest +{ + [TestCase] + public void ComputeDamage_NoComponent_ReturnsZero() + { + var enemy = new Enemy(); + var input = new DamageRecord(Vector3.Zero, new RDamage(10.0f, EDamageTypes.Normal)); + + var result = enemy.ComputeDamage(input); + AssertFloat(result.Damage.DamageDealt).IsEqual(0.0f); + } + + [TestCase] + public void TakeDamage_WithCDamageable_AggregatesDamage() + { + var enemy = new Enemy(); + var cDamage = new CDamageable(); + cDamage.DamageModifiers = new[] + { + new RDamageModifier(EDamageTypes.Normal, 1.0f), + new RDamageModifier(EDamageTypes.Normal, 2.0f) + }; + enemy.CDamageable = cDamage; + + var input = new DamageRecord(Vector3.Zero, new RDamage(10.0f, EDamageTypes.Normal)); + var result = enemy.TakeDamage(input); + AssertFloat(result.Damage.DamageDealt).IsEqual(30.0f); + } + + [TestCase] + public void ReduceHealth_WithCHealth_Decreases() + { + var enemy = new Enemy(); + var health = new CHealth { RHealth = new RHealth(100.0f), CurrentHealth = 100.0f }; + enemy.CHealth = health; + + var input = new DamageRecord(Vector3.Zero, new RDamage(25.0f, EDamageTypes.Normal)); + var record = enemy.ReduceHealth(enemy, input); + + AssertFloat(health.CurrentHealth).IsEqual(75.0f); + AssertFloat(record.CurrentHealth).IsEqual(75.0f); + } + + [TestCase] + public void Knockback_Register_And_Compute() + { + var enemy = new Enemy(); + var cKnock = new CKnockback { RKnockback = new RKnockback(1.0f) }; + enemy.CKnockback = cKnock; + enemy.GlobalPosition = Vector3.Zero; + cKnock.GlobalPosition = Vector3.Zero; + + var dmg = new DamageRecord(new Vector3(5, 0, 0), new RDamage(0, EDamageTypes.Normal)); + var krec = new KnockbackRecord(dmg, 2.0f); + enemy.RegisterKnockback(krec); + + var k = enemy.ComputeKnockback(); + AssertVector(k).IsEqual(new Vector3(-2, 0, 0)); + } + + [TestCase] + public void Unstun_ResetsFlag() + { + var enemy = new Enemy(); + enemy.IsStunned = true; + enemy.Unstun(); + AssertBool(enemy.IsStunned).IsFalse(); + } +} diff --git a/tests/enemies/EnemyUnitTest.cs.uid b/tests/enemies/EnemyUnitTest.cs.uid new file mode 100644 index 00000000..1c82d5ec --- /dev/null +++ b/tests/enemies/EnemyUnitTest.cs.uid @@ -0,0 +1 @@ +uid://cojxgcs6xqqoq diff --git a/tests/player/DashSystemUnitTest.cs b/tests/player/DashSystemUnitTest.cs new file mode 100644 index 00000000..30ba91d2 --- /dev/null +++ b/tests/player/DashSystemUnitTest.cs @@ -0,0 +1,55 @@ +using Godot; +using GdUnit4; +using static GdUnit4.Assertions; +using Movementtests.systems; + +namespace Movementtests.tests; + +[TestSuite, RequireGodotRuntime] +public class DashSystemUnitTest +{ + private DashSystem _dashSystem; + + [BeforeTest] + public void SetupTest() + { + _dashSystem = new DashSystem(); + + _dashSystem.DashCast3D = new ShapeCast3D(); + _dashSystem.AddChild(_dashSystem.DashCast3D); + + _dashSystem._dashCastDrop = new ShapeCast3D(); + _dashSystem.AddChild(_dashSystem._dashCastDrop); + + _dashSystem._dashTarget = new MeshInstance3D(); + _dashSystem.AddChild(_dashSystem._dashTarget); + + _dashSystem._dashDropIndicator = new MeshInstance3D(); + _dashSystem.AddChild(_dashSystem._dashDropIndicator); + + _dashSystem._dashDropLocationIndicator = new MeshInstance3D(); + _dashSystem.AddChild(_dashSystem._dashDropLocationIndicator); + } + + [AfterTest] + public void CleanupTest() + { + _dashSystem?.Free(); + } + + [TestCase] + public void TestStopPreparingDash() + { + _dashSystem.CanDashThroughTarget = true; + _dashSystem._dashTarget.Visible = true; + _dashSystem._dashDropIndicator.Visible = true; + _dashSystem._dashDropLocationIndicator.Visible = true; + + _dashSystem.StopPreparingDash(); + + AssertBool(_dashSystem.CanDashThroughTarget).IsFalse(); + AssertBool(_dashSystem._dashTarget.Visible).IsFalse(); + AssertBool(_dashSystem._dashDropIndicator.Visible).IsFalse(); + AssertBool(_dashSystem._dashDropLocationIndicator.Visible).IsFalse(); + } +} diff --git a/tests/player/DashSystemUnitTest.cs.uid b/tests/player/DashSystemUnitTest.cs.uid new file mode 100644 index 00000000..545620a4 --- /dev/null +++ b/tests/player/DashSystemUnitTest.cs.uid @@ -0,0 +1 @@ +uid://pv570go4cxws diff --git a/tests/player/HeadSystemUnitTest.cs b/tests/player/HeadSystemUnitTest.cs new file mode 100644 index 00000000..07d06790 --- /dev/null +++ b/tests/player/HeadSystemUnitTest.cs @@ -0,0 +1,91 @@ +using Godot; +using GdUnit4; +using static GdUnit4.Assertions; +using Movementtests.systems; + +namespace Movementtests.tests; + +[TestSuite, RequireGodotRuntime] +public class HeadSystemUnitTest +{ + private HeadSystem _head; + + [BeforeTest] + public void SetupTest() + { + _head = new HeadSystem(); + _head._camera = new Camera3D(); + _head.AddChild(_head._camera); + + _head._cameraAnchor = new Marker3D(); + _head.AddChild(_head._cameraAnchor); + + _head._fpRig = new Node3D(); + _head.AddChild(_head._fpRig); + + _head._fpDisplacedRig = new Node3D(); + _head.AddChild(_head._fpDisplacedRig); + } + + [AfterTest] + public void CleanupTest() + { + _head?.Free(); + } + + [TestCase] + public void TestResetHeadBobbing() + { + _head._bobbingAccumulator = 10.0f; + _head.ResetHeadBobbing(); + AssertFloat(_head._bobbingAccumulator).IsEqual(0.0f); + } + + [TestCase] + public void TestComputeHowMuchInputForward() + { + Vector3 forwardInput = new Vector3(0, 0, -1); + AssertFloat(_head.ComputeHowMuchInputForward(forwardInput)).IsEqual(1.0f); + + Vector3 backwardInput = new Vector3(0, 0, 1); + AssertFloat(_head.ComputeHowMuchInputForward(backwardInput)).IsEqual(-1.0f); + } + + [TestCase] + public void TestComputeHowMuchInputSideways() + { + Vector3 rightInput = new Vector3(1, 0, 0); + AssertFloat(_head.ComputeHowMuchInputSideways(rightInput)).IsEqual(1.0f); + + Vector3 leftInput = new Vector3(-1, 0, 0); + AssertFloat(_head.ComputeHowMuchInputSideways(leftInput)).IsEqual(-1.0f); + } + + [TestCase] + public void TestGetForwardHorizontalVector() + { + Vector3 forward = _head.GetForwardHorizontalVector(); + AssertVector(forward).IsEqualApprox(Vector3.Back, new Vector3(0.001f, 0.001f, 0.001f)); + } + + [TestCase] + public void TestLookAroundRotation() + { + var inputs = new HeadSystem.CameraParameters( + Delta: 0.016, + LookDir: new Vector2(1, 0), + PlayerInput: Vector3.Zero, + PlayerVelocity: Vector3.Zero, + WallContactPoint: Vector3.Zero, + SensitivitMultiplier: 1.0f, + WithCameraJitter: false, + WithCameraBobbing: false, + BobbingMultiplier: 1.0f, + FovMultiplier: 1.0f + ); + + float initialY = _head.Rotation.Y; + _head.LookAround(inputs); + AssertFloat(_head.Rotation.Y).IsEqual(initialY + 1.0f); + } +} diff --git a/tests/player/HeadSystemUnitTest.cs.uid b/tests/player/HeadSystemUnitTest.cs.uid new file mode 100644 index 00000000..110c87c4 --- /dev/null +++ b/tests/player/HeadSystemUnitTest.cs.uid @@ -0,0 +1 @@ +uid://bp0xn8k3dmfkg diff --git a/tests/player/PlayerControllerUnitTest.cs b/tests/player/PlayerControllerUnitTest.cs index 947ecf14..3a82f4a8 100644 --- a/tests/player/PlayerControllerUnitTest.cs +++ b/tests/player/PlayerControllerUnitTest.cs @@ -1,4 +1,3 @@ -using System.Reflection; using Godot; using GdUnit4; using static GdUnit4.Assertions; @@ -16,12 +15,9 @@ public class PlayerControllerUnitTest public void SetupTest() { _player = new PlayerController(); - // We don't call _Ready() to avoid node dependency issues, - // but we need to initialize some private fields for unit testing. - SetPrivateField(_player, "_targetSpeed", 7.0f); - SetPrivateField(_player, "_gravity", 9.8f); + _player._targetSpeed = 7.0f; + _player._gravity = 9.8f; - // Setup Combat/Health dependencies var rHealth = new RHealth(100.0f); _player.RHealth = rHealth; _player.CHealth = new CHealth { RHealth = rHealth, CurrentHealth = 100.0f }; @@ -33,17 +29,11 @@ public class PlayerControllerUnitTest _player?.Free(); } - private void SetPrivateField(object obj, string fieldName, object value) - { - var field = typeof(PlayerController).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); - field?.SetValue(obj, value); - } - [TestCase] public void TestCalculateGravityForce() { _player.Weight = 3.0f; - // _gravity is 9.8f + // gravity is 9.8f AssertFloat(_player.CalculateGravityForce()).IsEqualApprox(29.4f, 0.001f); } @@ -51,15 +41,15 @@ public class PlayerControllerUnitTest public void TestIsPlayerInputtingForward() { // Test Keyboard Input - _player.InputDeviceChanged(false); // isUsingGamepad = false - _player.OnInputMoveKeyboard(new Vector3(0, 0, -1)); // Forward is -Z in Godot + _player.InputDeviceChanged(false); + _player.OnInputMoveKeyboard(Vector3.Forward); AssertBool(_player.IsPlayerInputtingForward()).IsTrue(); - _player.OnInputMoveKeyboard(new Vector3(0, 0, 1)); // Backward + _player.OnInputMoveKeyboard(Vector3.Back); AssertBool(_player.IsPlayerInputtingForward()).IsFalse(); // Test Gamepad Input - _player.InputDeviceChanged(true); // isUsingGamepad = true + _player.InputDeviceChanged(true); _player.OnInputMove(new Vector3(0, 0, -1)); AssertBool(_player.IsPlayerInputtingForward()).IsTrue(); } @@ -78,14 +68,8 @@ public class PlayerControllerUnitTest _player.Velocity = Vector3.Zero; _player.AccelerationFloor = 10.0f; - // Moving forward - Vector3 direction = Vector3.Forward; // (0, 0, -1) float delta = 0.1f; - - // _targetSpeed is 7.0f - // Expected velocity change: Lerp(0, -7.0, 0.1 * 10.0) -> Lerp(0, -7.0, 1.0) -> -7.0 - Vector3 newVelocity = _player.ComputeHVelocity(delta, _player.AccelerationFloor, _player.DecelerationFloor, direction); - + Vector3 newVelocity = _player.ComputeHVelocity(delta, _player.AccelerationFloor, _player.DecelerationFloor, Vector3.Forward); AssertVector(newVelocity).IsEqual(new Vector3(0, 0, -7.0f)); } @@ -96,12 +80,8 @@ public class PlayerControllerUnitTest _player.AccelerationAir = 2.0f; _player.DecelerationAir = 2.0f; - // No input direction (deceleration) - Vector3 direction = Vector3.Zero; float delta = 0.5f; - - // Expected velocity change: Lerp(5, 0, 0.5 * 2.0) -> Lerp(5, 0, 1.0) -> 0 - Vector3 newVelocity = _player.ComputeHVelocity(delta, _player.AccelerationAir, _player.DecelerationAir, direction); + Vector3 newVelocity = _player.ComputeHVelocity(delta, _player.AccelerationAir, _player.DecelerationAir, Vector3.Zero); AssertVector(newVelocity).IsEqual(Vector3.Zero); } @@ -109,23 +89,17 @@ public class PlayerControllerUnitTest [TestCase] public void TestReduceHealth() { - // Initial health is 100 var damageRecord = new DamageRecord(Vector3.Zero, new RDamage(25.0f, EDamageTypes.Normal)); _player.ReduceHealth(_player, damageRecord); - AssertFloat(_player.CHealth.CurrentHealth).IsEqual(75.0f); } [TestCase] public void TestEmpoweredActionsLeft() { - // EmpoweredActionsLeft setter calls PlayerUi.SetNumberOfDashesLeft - // PlayerUi.SetNumberOfDashesLeft accesses _dashIcons array, which is null if _Ready() isn't called. - // We can initialize _dashIcons via reflection to allow the setter to work. var mockUi = new PlayerUi(); var dashIcons = new TextureRect[3] { new TextureRect(), new TextureRect(), new TextureRect() }; - var field = typeof(PlayerUi).GetField("_dashIcons", BindingFlags.NonPublic | BindingFlags.Instance); - field?.SetValue(mockUi, dashIcons); + mockUi._dashIcons = dashIcons; _player.PlayerUi = mockUi; @@ -139,23 +113,16 @@ public class PlayerControllerUnitTest [TestCase] public void TestDashCooldownTimeout() { - SetPrivateField(_player, "_canDash", false); + _player._canDash = false; _player.DashCooldownTimeout(); - bool canDash = (bool)GetPrivateField(_player, "_canDash"); - AssertBool(canDash).IsTrue(); - } - - private object GetPrivateField(object obj, string fieldName) - { - var field = typeof(PlayerController).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); - return field?.GetValue(obj); + AssertBool(_player._canDash).IsTrue(); } [TestCase] public void TestGetInputLocalHDirection() { - _player.InputDeviceChanged(false); // Keyboard - _player.OnInputMoveKeyboard(new Vector3(1, 0, 1)); // Diagonal + _player.InputDeviceChanged(false); + _player.OnInputMoveKeyboard(new Vector3(1, 0, 1)); Vector3 expected = new Vector3(1, 0, 1).Normalized(); AssertVector(_player.GetInputLocalHDirection()).IsEqualApprox(expected, new Vector3(0.001f, 0.001f, 0.001f)); @@ -168,7 +135,6 @@ public class PlayerControllerUnitTest cKnockback.RKnockback = new RKnockback(10.0f); _player.CKnockback = cKnockback; - // Setup knockback record var damageRecord = new DamageRecord(new Vector3(10, 0, 0), new RDamage(0, EDamageTypes.Normal)); var knockbackRecord = new KnockbackRecord(damageRecord, 1.0f); @@ -177,10 +143,6 @@ public class PlayerControllerUnitTest _player.RegisterKnockback(knockbackRecord); - // Expected direction: GlobalPosition (0,0,0) - SourceLocation (10,0,0) = (-10,0,0) - // Normalized: (-1, 0, 0) - // finalKnockback: (-1, 0, 0) * 10.0 (Modifier) * 1.0 (ForceMultiplier) = (-10, 0, 0) - Vector3 knockback = cKnockback.ComputeKnockback(); AssertVector(knockback).IsEqual(new Vector3(-10, 0, 0)); } diff --git a/tests/player/WeaponSystemUnitTest.cs b/tests/player/WeaponSystemUnitTest.cs new file mode 100644 index 00000000..cd575190 --- /dev/null +++ b/tests/player/WeaponSystemUnitTest.cs @@ -0,0 +1,51 @@ +using Godot; +using GdUnit4; +using static GdUnit4.Assertions; +using Movementtests.systems; +using Movementtests.systems.damage; + +namespace Movementtests.tests; + +[TestSuite, RequireGodotRuntime] +public class WeaponSystemUnitTest +{ + private WeaponSystem _weapon; + + [BeforeTest] + public void SetupTest() + { + _weapon = new WeaponSystem(); + _weapon.RDamage = new RDamage(5.0f, EDamageTypes.Normal); + + _weapon.WeaponMesh = new MeshInstance3D(); + _weapon.AddChild(_weapon.WeaponMesh); + _weapon.WeaponLocationIndicator = new MeshInstance3D(); + _weapon.AddChild(_weapon.WeaponLocationIndicator); + } + + [AfterTest] + public void CleanupTest() + { + _weapon?.Free(); + } + + [TestCase] + public void TestWeaponLeftAndBackVisibility() + { + _weapon.Visible = false; + + _weapon.WeaponLeft(); + AssertBool(_weapon.Visible).IsTrue(); + + _weapon.WeaponBack(); + AssertBool(_weapon.Visible).IsFalse(); + } + + [TestCase] + public void TestThrowWeaponOnCurveSetsUnfrozen() + { + _weapon.Freeze = true; + _weapon.ThrowWeaponOnCurve(); + AssertBool(_weapon.Freeze).IsFalse(); + } +} diff --git a/tests/player/WeaponSystemUnitTest.cs.uid b/tests/player/WeaponSystemUnitTest.cs.uid new file mode 100644 index 00000000..487b0dea --- /dev/null +++ b/tests/player/WeaponSystemUnitTest.cs.uid @@ -0,0 +1 @@ +uid://vkv8aderakcb