using MrGameEng.Core; using Xunit; namespace MrGameEng.Core.Tests; public class SceneTransitionTests { private sealed class TrackingScene : Scene { public int LoadCount; public int UnloadCount; protected override void OnLoad() => LoadCount++; protected override void OnUnload() => UnloadCount++; } // Тайминг-машина живёт в ядре и не зависит от визуала перехода — // тестируем на голой заглушке с длительностями, как у Fade. private sealed class TimedTransition(float outDuration, float inDuration) : Transition(outDuration, inDuration); private static Transition Fade(float duration) => new TimedTransition(duration / 2f, duration / 2f); private static void Tick(EngineContext context, float seconds) { context.Clock.Advance(seconds); context.Scenes.Update(context.Clock); } [Fact] public void TransitionSwitch_KeepsOldScene_UntilFullyCovered() { var context = new EngineContext(); var first = new TrackingScene(); var second = new TrackingScene(); context.Scenes.Switch(first); Tick(context, 0.016f); // Fade(1.0) → фаза закрытия 0.5 c, фаза открытия 0.5 c. context.Scenes.Switch(second, Fade(1f)); Tick(context, 0.2f); Assert.True(context.Scenes.IsTransitioning); Assert.Same(first, context.Scenes.Current); Assert.Equal(0, second.LoadCount); Tick(context, 0.4f); // суммарно 0.6 > 0.5 — экран закрыт, своп произошёл Assert.Same(second, context.Scenes.Current); Assert.Equal(1, first.UnloadCount); Assert.Equal(1, second.LoadCount); Assert.True(context.Scenes.IsTransitioning); // идёт фаза открытия Tick(context, 0.6f); // открытие завершено Assert.False(context.Scenes.IsTransitioning); Assert.Same(second, context.Scenes.Current); } [Fact] public void Transition_UsesUnscaledTime_WorksWhilePaused() { var context = new EngineContext(); var first = new TrackingScene(); var second = new TrackingScene(); context.Scenes.Switch(first); Tick(context, 0.016f); context.Clock.TimeScale = 0f; // игра на паузе context.Scenes.Switch(second, Fade(0.2f)); Tick(context, 0.15f); Tick(context, 0.15f); Assert.Same(second, context.Scenes.Current); Assert.False(context.Scenes.IsTransitioning); } [Fact] public void SwitchDuringTransition_ReplacesPendingTarget() { var context = new EngineContext(); var first = new TrackingScene(); var second = new TrackingScene(); var third = new TrackingScene(); context.Scenes.Switch(first); Tick(context, 0.016f); context.Scenes.Switch(second, Fade(1f)); Tick(context, 0.1f); context.Scenes.Switch(third); // передумали, пока экран закрывается Tick(context, 0.5f); Assert.Same(third, context.Scenes.Current); Assert.Equal(0, second.LoadCount); } [Fact] public void SwitchDuringReveal_CoversAgain_InsteadOfHardSwap() { var context = new EngineContext(); var first = new TrackingScene(); var second = new TrackingScene(); var third = new TrackingScene(); context.Scenes.Switch(first); Tick(context, 0.016f); context.Scenes.Switch(second, Fade(1f)); // 0.5 c закрытие + 0.5 c открытие Tick(context, 0.6f); // закрыто, своп на second, началось открытие Assert.Same(second, context.Scenes.Current); Tick(context, 0.25f); // открытие наполовину (coverage ~0.5) context.Scenes.Switch(third, Fade(1f)); // передумали во время открытия Tick(context, 0.05f); // экран снова закрывается — свопа ещё нет Assert.Same(second, context.Scenes.Current); Assert.True(context.Scenes.IsTransitioning); Tick(context, 0.6f); // полностью закрыт — теперь своп на third Assert.Same(third, context.Scenes.Current); Assert.Equal(1, third.LoadCount); } [Fact] public void ZeroDurationTransition_SwapsOnNextUpdates() { var context = new EngineContext(); var first = new TrackingScene(); var second = new TrackingScene(); context.Scenes.Switch(first); Tick(context, 0.016f); context.Scenes.Switch(second, Fade(0f)); Tick(context, 0.016f); Tick(context, 0.016f); Assert.Same(second, context.Scenes.Current); Assert.False(context.Scenes.IsTransitioning); } }