diff --git a/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs b/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs index b5421ec..f756155 100644 --- a/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs +++ b/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs @@ -403,7 +403,16 @@ namespace DanroJump.Gameplay /// public void Bounce(float velocity) { - body.linearVelocity = new Vector2(body.linearVelocity.x, Mathf.Max(body.linearVelocity.y, velocity)); + if (body == null) + { + body = GetComponent(); + } + + if (body != null) + { + body.linearVelocity = new Vector2(body.linearVelocity.x, Mathf.Max(body.linearVelocity.y, velocity)); + } + jumpAnimationPulseUntil = Time.time + jumpAnimationPulseDuration; audioService?.Play(GameAudioEvent.PlayerJump, transform.position); vfxService?.SpawnJumpEffect(transform.position); @@ -1366,7 +1375,17 @@ namespace DanroJump.Gameplay IsDead = true; respawnBlocked = false; respawnAtTime = -1f; - body.linearVelocity = Vector2.zero; + + if (body == null) + { + body = GetComponent(); + } + + if (body != null) + { + body.linearVelocity = Vector2.zero; + } + ResetMoveSpeedMultiplier(); ResetHeightLift(); EndEnemyPassThrough(); @@ -1555,7 +1574,14 @@ namespace DanroJump.Gameplay var clone = wrapRendererClones[index]; if (clone != null) { - Destroy(clone.gameObject); + if (Application.isPlaying) + { + Destroy(clone.gameObject); + } + else + { + DestroyImmediate(clone.gameObject); + } } } @@ -1716,7 +1742,14 @@ namespace DanroJump.Gameplay var clone = wrapRendererClones[index]; if (clone != null) { - Destroy(clone.gameObject); + if (Application.isPlaying) + { + Destroy(clone.gameObject); + } + else + { + DestroyImmediate(clone.gameObject); + } } wrapRendererClones.RemoveAt(index); @@ -1750,7 +1783,14 @@ namespace DanroJump.Gameplay var clone = wrapRendererClones[index]; if (clone != null) { - Destroy(clone.gameObject); + if (Application.isPlaying) + { + Destroy(clone.gameObject); + } + else + { + DestroyImmediate(clone.gameObject); + } } wrapRendererClones.RemoveAt(index); diff --git a/Assets/Scripts/Runtime/UI/MainMenu/ServiceTextInputModalWindow.cs b/Assets/Scripts/Runtime/UI/MainMenu/ServiceTextInputModalWindow.cs index 1d5fc3a..30328f7 100644 --- a/Assets/Scripts/Runtime/UI/MainMenu/ServiceTextInputModalWindow.cs +++ b/Assets/Scripts/Runtime/UI/MainMenu/ServiceTextInputModalWindow.cs @@ -256,9 +256,8 @@ namespace DanroJump.UI.MainMenu var confirmedCallback = confirmed; confirmed = null; - confirmedCallback.Invoke(state.Value); - SetStatus("Значение сохранено."); RequestClose(); + confirmedCallback.Invoke(state.Value); return true; } diff --git a/Assets/Scripts/Runtime/Vfx/GameVfxService.cs b/Assets/Scripts/Runtime/Vfx/GameVfxService.cs index fc3ed7b..841358b 100644 --- a/Assets/Scripts/Runtime/Vfx/GameVfxService.cs +++ b/Assets/Scripts/Runtime/Vfx/GameVfxService.cs @@ -43,7 +43,10 @@ namespace DanroJump.Vfx colorOverLifetime.color = gradient; ps.Play(); - Object.Destroy(go, main.duration + main.startLifetime.constantMax); + if (Application.isPlaying) + { + Object.Destroy(go, main.duration + main.startLifetime.constantMax); + } } public void SpawnCoinCollectedEffect(Vector3 position) @@ -79,7 +82,10 @@ namespace DanroJump.Vfx colorOverLifetime.color = gradient; ps.Play(); - Object.Destroy(go, main.duration + main.startLifetime.constantMax); + if (Application.isPlaying) + { + Object.Destroy(go, main.duration + main.startLifetime.constantMax); + } } public void SpawnPlayerDeathEffect(Vector3 position) @@ -115,7 +121,10 @@ namespace DanroJump.Vfx colorOverLifetime.color = gradient; ps.Play(); - Object.Destroy(go, main.duration + main.startLifetime.constantMax); + if (Application.isPlaying) + { + Object.Destroy(go, main.duration + main.startLifetime.constantMax); + } } public void SpawnPrizeEffect(Vector3 position) @@ -154,7 +163,10 @@ namespace DanroJump.Vfx ); ps.Play(); - Object.Destroy(go, main.duration + main.startLifetime.constantMax); + if (Application.isPlaying) + { + Object.Destroy(go, main.duration + main.startLifetime.constantMax); + } } public void SpawnComErrorEffect(Vector3 position) @@ -191,7 +203,10 @@ namespace DanroJump.Vfx colorOverLifetime.color = gradient; ps.Play(); - Object.Destroy(go, main.duration + main.startLifetime.constantMax); + if (Application.isPlaying) + { + Object.Destroy(go, main.duration + main.startLifetime.constantMax); + } } } } diff --git a/Assets/Tests/EditMode/Editor/ComSystemShutdownTests.cs b/Assets/Tests/EditMode/Editor/ComSystemShutdownTests.cs new file mode 100644 index 0000000..d54b348 --- /dev/null +++ b/Assets/Tests/EditMode/Editor/ComSystemShutdownTests.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; +using System.Threading; +using DanroJump.Hardware.Com; +using NUnit.Framework; +using UnityEngine; + +namespace DanroJump.Tests.EditMode +{ + /// + /// Тесты для проверки быстрого и безопасного закрытия (graceful shutdown) COM-системы. + /// + public sealed class ComSystemShutdownTests + { + private GameObject systemGo; + private ComSystem comSystem; + private MockComTransport mockTransport; + + [SetUp] + public void SetUp() + { + systemGo = new GameObject("Test_ComSystem"); + comSystem = systemGo.AddComponent(); + + // Отключаем автоматический запуск и сохранение между сценами + SetPrivateField(comSystem, "startOnEnable", false); + SetPrivateField(comSystem, "dontDestroyOnLoad", false); + SetPrivateField(comSystem, "useEncryptedHandshake", false); + SetPrivateField(comSystem, "keepAlive", false); + } + + [TearDown] + public void TearDown() + { + if (comSystem != null) + { + comSystem.StopSystem(false); + } + + if (systemGo != null) + { + UnityEngine.Object.DestroyImmediate(systemGo); + } + + mockTransport?.Dispose(); + } + + [Test] + public void ComSystem_StopSystem_CompletesImmediately() + { + // Настраиваем mock-окружение + var mockProvider = new MockComPortProvider(); + mockTransport = new MockComTransport(); + var mockFactory = new MockComTransportFactory(mockTransport); + + comSystem.ConfigureInfrastructure(mockProvider, mockFactory); + + // Запускаем систему + comSystem.StartSystem(); + Assert.That(comSystem.IsRunning, Is.True); + + // Проверяем время остановки + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + comSystem.StopSystem(sendShutdownCommands: false); + stopwatch.Stop(); + + Assert.That(comSystem.IsRunning, Is.False); + Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(500), "StopSystem took too long to complete!"); + } + + [Test] + public void ComSystem_StopSystem_WithShutdownCommands_CompletesImmediately() + { + var mockProvider = new MockComPortProvider(); + mockTransport = new MockComTransport(); + var mockFactory = new MockComTransportFactory(mockTransport); + + comSystem.ConfigureInfrastructure(mockProvider, mockFactory); + + comSystem.StartSystem(); + Assert.That(comSystem.IsRunning, Is.True); + + // Проверяем время остановки с отправкой команд выключения + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + comSystem.StopSystem(sendShutdownCommands: true); + stopwatch.Stop(); + + Assert.That(comSystem.IsRunning, Is.False); + Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(500), "StopSystem with shutdown commands took too long to complete!"); + } + + private static void SetPrivateField(object target, string fieldName, object value) + { + var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + if (field == null) + { + throw new ArgumentException($"Field '{fieldName}' not found in type '{target.GetType().FullName}'"); + } + field.SetValue(target, value); + } + + private sealed class MockComPortProvider : IComPortProvider + { + public string[] GetPortNames() => new[] { "COM_TEST" }; + } + + private sealed class MockComTransportFactory : IComTransportFactory + { + private readonly IComTransport transport; + + public MockComTransportFactory(IComTransport transport) + { + this.transport = transport; + } + + public IComTransport Create(SerialSettings settings) => transport; + } + + private sealed class MockComTransport : IComTransport + { + private readonly AutoResetEvent readEvent = new(false); + private readonly ConcurrentQueue readQueue = new(); + private bool isOpen; + + public bool IsOpen => isOpen; + + public void Open() + { + isOpen = true; + } + + public void Close() + { + isOpen = false; + readEvent.Set(); + } + + public void DiscardInBuffer() { } + public void DiscardOutBuffer() { } + + public string ReadLine() + { + while (isOpen) + { + if (readQueue.TryDequeue(out var line)) + { + return line; + } + readEvent.WaitOne(10); + } + throw new System.IO.IOException("Port is closed"); + } + + public void WriteLine(string value) + { + // Для простоты теста просто игнорируем команды записи + } + + public void Dispose() + { + Close(); + } + } + } +} diff --git a/Assets/Tests/EditMode/Editor/ComSystemShutdownTests.cs.meta b/Assets/Tests/EditMode/Editor/ComSystemShutdownTests.cs.meta new file mode 100644 index 0000000..104d6ec --- /dev/null +++ b/Assets/Tests/EditMode/Editor/ComSystemShutdownTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a64abf3bf7d4d8849b8bca91eddc37bc \ No newline at end of file diff --git a/Assets/Tests/EditMode/Editor/MainMenuControllerServiceActionTests.cs b/Assets/Tests/EditMode/Editor/MainMenuControllerServiceActionTests.cs index ca9c4af..a108166 100644 --- a/Assets/Tests/EditMode/Editor/MainMenuControllerServiceActionTests.cs +++ b/Assets/Tests/EditMode/Editor/MainMenuControllerServiceActionTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Reflection; using DanroJump.Prizes.Qr; using DanroJump.Settings; +using DanroJump.UI.ArcadeInput; using DanroJump.UI.MainMenu; using DanroJump.UI.Modals; using NUnit.Framework; @@ -81,6 +82,90 @@ namespace DanroJump.Tests.EditMode textInputModal.Dispose(); } + [Test] + public void OpenSettings_RequiresPinCodeInput() + { + var controller = CreateController(); + var settings = new StubSettingsService(); + var settingsModal = new SettingsModalWindow(new VisualElement(), settings, null); + var textInputRoot = CreateTextInputRoot(); + var textInputModal = new ServiceTextInputModalWindow(textInputRoot); + + TestObjectFactory.SetPrivateField(controller, "serviceSettingsService", settings); + TestObjectFactory.SetPrivateField(controller, "serviceTextInputModal", textInputModal); + TestObjectFactory.SetPrivateField(controller, "settingsModal", settingsModal); + TestObjectFactory.SetPrivateField(controller, "modalStack", new ModalStackController(new VisualElement(), null)); + + InvokeOpenSettings(controller); + + Assert.That(textInputModal.IsOpen, Is.True); + Assert.That(settingsModal.IsOpen, Is.False); + Assert.That(textInputRoot.Q