using System.Collections; using System.Collections.Generic; using System.Reflection; using System.IO; using BumpCombat.Combat; using BumpCombat.Core; using BumpCombat.Enemies; using BumpCombat.Player; using BumpCombat.Progression; using BumpCombat.Spawning; using NUnit.Framework; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TestTools; namespace BumpCombat.PlayModeTests { public sealed class StatusVisualIntegrationTests { private const string IgniteVisualName = "Ignite Status Visual"; private const string ShockVisualName = "Shock Status Visual"; private const string CycloneGhostName = "Cyclone Move Afterimage"; [UnityTest] public IEnumerator CrowdControlSpirals_FollowHeadsRotatePauseAndReplaceGroggyPips() { yield return LoadCombatPrototype(); var director = Object.FindAnyObjectByType(); director.enabled = false; foreach (var old in Object.FindObjectsByType(FindObjectsSortMode.None)) old.gameObject.SetActive(false); var player = Object.FindAnyObjectByType(); player.GetComponent().position = new Vector2(8f, -5f); var prefabs = new List(); foreach (string field in new[] { "enemyPrefabs", "elitePrefabs", "midBossPrefabs", "finalBossPrefabs" }) prefabs.AddRange((EnemyController[])typeof(SpawnDirector).GetField(field, BindingFlags.NonPublic | BindingFlags.Instance).GetValue(director)); var actors = new List(); var roles = new[] { RunTimedEvent.Elite, RunTimedEvent.MidBoss, RunTimedEvent.FinalBoss }; var kinds = new[] { EnemyKind.Slime, EnemyKind.Bat, director.GetEventEnemyTuning(roles[0]).PrefabKind, director.GetEventEnemyTuning(roles[1]).PrefabKind, director.GetEventEnemyTuning(roles[2]).PrefabKind }; for (int i = 0; i < kinds.Length; i++) { var prefab = prefabs.Find(p => p != null && p.Definition.Kind == kinds[i]); Assert.That(prefab, Is.Not.Null); actors.Add(Object.Instantiate(prefab, new Vector3((i - 2) * 2.6f, 0f, 0f), Quaternion.identity)); if (i >= 2) actors[i].ConfigureRunEventEnemy(roles[i - 2], director.GetEventEnemyTuning(roles[i - 2])); } yield return new WaitForSeconds(1.5f); for (int i = 0; i < actors.Count; i++) actors[i].TryReposition(new Vector2((i - 2) * 2.6f, 0f)); Assert.That(actors[0].ApplyStun(3f), Is.True); Assert.That(actors[1].ApplyStun(3f), Is.True); for (int i = 2; i < actors.Count; i++) { for (int contact = 0; contact < 10 && !actors[i].IsGroggy; contact++) actors[i].RegisterArtifactContact(actors[i].ShieldColor, 987654 + contact); Assert.That(actors[i].IsGroggy, Is.True, kinds[i].ToString()); } Assert.That(actors[2].IsGroggy, Is.True); yield return null; var marker = actors[0].transform.Find("Stun Runes").GetComponent(); Sprite first = marker.sprite; yield return new WaitForSeconds(.075f); Assert.That(marker.sprite, Is.Not.SameAs(first)); actors[0].TryReposition(new Vector2(-5.4f, .1f)); yield return null; for (int i = 0; i < actors.Count; i++) { var actor = actors[i]; var body = actor.GetComponent(); var cc = actor.transform.Find(i >= 2 ? "Groggy Spiral" : "Stun Runes").GetComponent(); Assert.That(cc.enabled, Is.True); Assert.That(cc.transform.lossyScale.x, Is.EqualTo(.5f * body.transform.lossyScale.x).Within(.0001f)); Assert.That(cc.transform.lossyScale.y, Is.EqualTo(.5f * body.transform.lossyScale.y).Within(.0001f)); Assert.That(cc.sprite.texture.name, Does.Contain(i >= 2 ? "Groggy" : "Stun")); Assert.That(Vector3.Distance(cc.transform.position, CrowdControlMarkerArt.MarkerWorldPosition(body)), Is.LessThan(.02f)); Assert.That(cc.sortingOrder, Is.EqualTo(body.sortingOrder + 100)); Assert.That(actor.transform.Find("Stagger Pips"), Is.Null); Assert.That(actor.transform.Find("Groggy Star 0"), Is.Null); } Time.timeScale = 0f; yield return null; Sprite paused = marker.sprite; yield return new WaitForSecondsRealtime(.12f); Assert.That(marker.sprite, Is.SameAs(paused)); CaptureCrowdControlReview(); Time.timeScale = 1f; actors[0].gameObject.SetActive(false); yield return null; Assert.That(marker == null || !marker.enabled || !marker.gameObject.activeInHierarchy, Is.True); LogAssert.NoUnexpectedReceived(); } private static void CaptureCrowdControlReview() { var host = new GameObject("CC Review Camera"); var camera = host.AddComponent(); camera.enabled = false; camera.orthographic = true; camera.orthographicSize = 3.8f; camera.aspect = 1440f / 720f; camera.transform.position = new Vector3(0f, .3f, -10f); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color32(31, 34, 40, 255); camera.cullingMask = ~(1 << 5); var renderTarget = new RenderTexture(1440, 720, 24); renderTarget.Create(); camera.targetTexture = renderTarget; camera.Render(); var previous = RenderTexture.active; RenderTexture.active = renderTarget; var snapshot = new Texture2D(1440, 720, TextureFormat.RGB24, false); snapshot.ReadPixels(new Rect(0, 0, 1440, 720), 0, 0); snapshot.Apply(); string folder = Path.GetFullPath(Path.Combine(Application.dataPath, "../../../../output/cc-proportional-guard-v3")); // The optional image is local QA output; CI without this workspace only runs assertions. if (Directory.Exists(folder)) File.WriteAllBytes(Path.Combine(folder, "runtime-review.png"), snapshot.EncodeToPNG()); RenderTexture.active = previous; camera.targetTexture = null; renderTarget.Release(); Object.Destroy(snapshot); Object.Destroy(renderTarget); Object.Destroy(host); } [SetUp] public void SetUp() { RunManager.ForceProductionModeForTests = false; ArtifactRewardController.GrantCatalogForTests = true; Time.timeScale = 1f; } [TearDown] public void TearDown() { RunManager.ForceProductionModeForTests = false; ArtifactRewardController.GrantCatalogForTests = false; Time.timeScale = 1f; } [UnityTest] public IEnumerator EnemyStatusVisual_LoadsBothSheetsAndSharesGroundAnchor() { yield return LoadCombatPrototype(); EnemyController enemy = FindOrSpawnEnemy(); Assert.That(enemy == null, Is.False); SpriteRenderer source = enemy.GetComponent(); Assert.That(source == null, Is.False); enemy.transform.localScale = Vector3.one * 0.8f; source.flipX = true; source.flipY = false; Assert.That(enemy.ApplyIgnite(3f, 0.5f, 0.01f), Is.True); Assert.That(enemy.ApplyShock(3f, 0.2f), Is.True); Assert.That(enemy.ApplyStun(2f), Is.True); yield return null; yield return null; SpriteRenderer ignite = FindChildRenderer(enemy, IgniteVisualName); SpriteRenderer shock = FindChildRenderer(enemy, ShockVisualName); Assert.That(ignite == null, Is.False); Assert.That(shock == null, Is.False); Assert.That(ignite.sprite == null, Is.False); Assert.That(shock.sprite == null, Is.False); Assert.That(ignite.sprite.rect.width, Is.EqualTo(64f).Within(0.01f)); Assert.That(ignite.sprite.rect.height, Is.EqualTo(64f).Within(0.01f)); Assert.That(shock.sprite.rect.width, Is.EqualTo(64f).Within(0.01f)); Assert.That(shock.sprite.rect.height, Is.EqualTo(64f).Within(0.01f)); Assert.That(ignite.sprite.pixelsPerUnit, Is.EqualTo(32f).Within(0.01f)); Assert.That(shock.sprite.pixelsPerUnit, Is.EqualTo(32f).Within(0.01f)); Assert.That( Vector3.Distance( ignite.transform.position, (Vector3)enemy.GroundAnchorPosition), Is.LessThan(0.0001f)); Assert.That( Vector3.Distance( shock.transform.position, (Vector3)enemy.GroundAnchorPosition), Is.LessThan(0.0001f)); Assert.That(ignite.flipX, Is.EqualTo(source.flipX)); Assert.That(shock.flipX, Is.EqualTo(source.flipX)); Assert.That(ignite.sortingLayerID, Is.EqualTo(source.sortingLayerID)); Assert.That(shock.sortingLayerID, Is.EqualTo(source.sortingLayerID)); Assert.That( ignite.sortingOrder, Is.EqualTo(source.sortingOrder + ArtifactStatusVisual.StatusSortingOffset)); Assert.That( shock.sortingOrder, Is.EqualTo(source.sortingOrder + ArtifactStatusVisual.StatusSortingOffset)); Assert.That( ignite.transform.lossyScale.x, Is.EqualTo(source.transform.lossyScale.x).Within(0.001f)); Assert.That( shock.transform.lossyScale.x, Is.EqualTo(source.transform.lossyScale.x).Within(0.001f)); Assert.That(FindChildRendererCount(enemy, IgniteVisualName), Is.EqualTo(1)); Assert.That(FindChildRendererCount(enemy, ShockVisualName), Is.EqualTo(1)); Assert.That(enemy.transform.Find("Stun Runes") == null, Is.False); } [UnityTest] public IEnumerator EnemyStatusVisual_FreezesDuringPauseAndAdvancesAfterResume() { yield return LoadCombatPrototype(); EnemyController enemy = FindOrSpawnEnemy(); Assert.That(enemy == null, Is.False); Assert.That(enemy.ApplyIgnite(3f, 0.5f, 0.01f), Is.True); yield return null; SpriteRenderer ignite = FindChildRenderer(enemy, IgniteVisualName); Assert.That(ignite == null, Is.False); Sprite frameBeforePause = ignite.sprite; Time.timeScale = 0f; for (int i = 0; i < 8; i++) { yield return null; } Assert.That(ignite == null, Is.False); Assert.That(ignite.sprite == frameBeforePause, Is.True); Time.timeScale = 1f; yield return new WaitForSeconds(0.19f); Assert.That(ignite.sprite == frameBeforePause, Is.False); } [UnityTest] public IEnumerator EnemyStatusVisual_ClearsOnExpiryDeathAndDisable() { yield return LoadCombatPrototype(); EnemyController expiringEnemy = FindOrSpawnEnemy(); Assert.That(expiringEnemy == null, Is.False); Assert.That(expiringEnemy.ApplyIgnite(0.12f, 0.5f, 0.01f), Is.True); Assert.That(expiringEnemy.ApplyShock(0.12f, 0.2f), Is.True); yield return new WaitForSeconds(0.3f); Assert.That(FindChildRenderer(expiringEnemy, IgniteVisualName) == null, Is.True); Assert.That(FindChildRenderer(expiringEnemy, ShockVisualName) == null, Is.True); EnemyController dyingEnemy = FindOrSpawnEnemy(); Assert.That(dyingEnemy == null, Is.False); Assert.That(dyingEnemy.ApplyIgnite(3f, 0.5f, 0.01f), Is.True); yield return null; Assert.That(FindChildRenderer(dyingEnemy, IgniteVisualName) == null, Is.False); Assert.That( dyingEnemy.TryTakeDamage( dyingEnemy.CurrentHealth + 1f, Vector2.zero, 0f), Is.True); Assert.That(dyingEnemy.IsDead, Is.True); yield return null; Assert.That(FindChildRenderer(dyingEnemy, IgniteVisualName) == null, Is.True); EnemyController disabledEnemy = FindOrSpawnEnemy(); Assert.That(disabledEnemy == null, Is.False); Assert.That(disabledEnemy.ApplyShock(3f, 0.2f), Is.True); yield return null; disabledEnemy.gameObject.SetActive(false); yield return null; Assert.That(FindChildRenderer(disabledEnemy, ShockVisualName) == null, Is.True); } [UnityTest] public IEnumerator CycloneGhosts_CopyPlayerSpriteAndRespectMovementPauseAndExpiry() { yield return LoadCombatPrototype(); PlayerController player = Object.FindAnyObjectByType(); Assert.That(player == null, Is.False); PlayerStats stats = player.GetComponent(); ActiveArtifactController artifacts = player.GetComponent(); CombatFeedback feedback = player.GetComponent(); SpriteRenderer playerRenderer = player.GetComponent(); Animator playerAnimator = player.GetComponent(); Assert.That(stats == null, Is.False); Assert.That(artifacts == null, Is.False); Assert.That(feedback == null, Is.False); Assert.That(playerRenderer == null, Is.False); Assert.That(playerAnimator == null, Is.False); playerAnimator.enabled = false; Sprite expectedGhostSprite = playerRenderer.sprite; Assert.That(expectedGhostSprite == null, Is.False); SelectArtifact(artifacts, ActiveArtifactEffect.Cyclone); ActiveArtifactDefinition cyclone = artifacts.CurrentArtifact; FieldInfo artifactIdField = typeof(ActiveArtifactDefinition).GetField( "artifactId", BindingFlags.Instance | BindingFlags.NonPublic); Assert.That(artifactIdField, Is.Not.Null); string originalArtifactId = cyclone.ArtifactId; const string testArtifactId = "cyclone-afterimage-test"; bool used; try { artifactIdField.SetValue(cyclone, testArtifactId); used = artifacts.TryUseCurrent(false); } finally { artifactIdField.SetValue(cyclone, originalArtifactId); } Assert.That(used, Is.True); Assert.That(artifacts.HasCycloneMoveSpeedBuff, Is.True); Assert.That( stats.GetStackCount( testArtifactId + ".move-speed", CharacterStat.MoveSpeed), Is.GreaterThan(0)); for (int i = 0; i < 12; i++) { player.transform.position += Vector3.right * 0.02f; yield return new WaitForSeconds(0.02f); } SpriteRenderer ghost = FindFirstRenderer(CycloneGhostName); Assert.That(ghost == null, Is.False); Assert.That(CountRenderers(CycloneGhostName), Is.LessThanOrEqualTo(4)); Assert.That(ghost.sprite == expectedGhostSprite, Is.True); Assert.That(ghost.flipX, Is.EqualTo(playerRenderer.flipX)); Assert.That(ghost.flipY, Is.EqualTo(playerRenderer.flipY)); Assert.That(ghost.sortingLayerID, Is.EqualTo(playerRenderer.sortingLayerID)); Assert.That(ghost.sortingOrder, Is.EqualTo(playerRenderer.sortingOrder - 1)); Assert.That( ghost.transform.lossyScale.x, Is.EqualTo(player.transform.lossyScale.x).Within(0.001f)); int countBeforePause = CountRenderers(CycloneGhostName); Time.timeScale = 0f; player.transform.position += Vector3.right * 0.5f; for (int i = 0; i < 8; i++) { yield return null; } Assert.That(CountRenderers(CycloneGhostName), Is.EqualTo(countBeforePause)); Time.timeScale = 1f; int countBeforeStop = CountRenderers(CycloneGhostName); yield return new WaitForSeconds(0.12f); Assert.That(CountRenderers(CycloneGhostName), Is.LessThanOrEqualTo(countBeforeStop)); yield return new WaitForSeconds(3.2f); Assert.That(CountRenderers(CycloneGhostName), Is.EqualTo(0)); Assert.That(artifacts.HasCycloneMoveSpeedBuff, Is.False); Assert.That( stats.GetStackCount( testArtifactId + ".move-speed", CharacterStat.MoveSpeed), Is.Zero); } [UnityTest] public IEnumerator CycloneGhosts_ClearWhenPlayerDiesOrDisables() { yield return LoadCombatPrototype(); PlayerController player = Object.FindAnyObjectByType(); Assert.That(player == null, Is.False); PlayerStats stats = player.GetComponent(); ActiveArtifactController artifacts = player.GetComponent(); SelectArtifact(artifacts, ActiveArtifactEffect.Cyclone); Assert.That(artifacts.TryUseCurrent(false), Is.True); Assert.That(artifacts.HasCycloneMoveSpeedBuff, Is.True); player.transform.position += Vector3.right * 0.2f; yield return null; Assert.That(CountRenderers(CycloneGhostName), Is.GreaterThan(0)); player.GetComponent().TryTakeDamage( player.GetComponent().CurrentHealth + 1f, Vector2.zero, 0f); yield return null; Assert.That(CountRenderers(CycloneGhostName), Is.EqualTo(0)); Assert.That(artifacts.HasCycloneMoveSpeedBuff, Is.False); Assert.That(stats == null, Is.False); Assert.That(artifacts == null, Is.False); player.gameObject.SetActive(false); yield return null; Assert.That(CountRenderers(CycloneGhostName), Is.EqualTo(0)); Assert.That(artifacts.HasCycloneMoveSpeedBuff, Is.False); } private static IEnumerator LoadCombatPrototype() { AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype"); while (!load.isDone) { yield return null; } yield return null; yield return null; yield return null; } private static EnemyController FindOrSpawnEnemy() { EnemyController[] enemies = Object.FindObjectsByType( FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID); foreach (EnemyController enemy in enemies) { if (enemy != null && !enemy.IsDead && enemy.gameObject.activeInHierarchy) { return enemy; } } SpawnDirector director = Object.FindAnyObjectByType(); if (director == null) { return null; } director.DebugSpawnImmediate(1); enemies = Object.FindObjectsByType( FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID); foreach (EnemyController enemy in enemies) { if (enemy != null && !enemy.IsDead && enemy.gameObject.activeInHierarchy) { return enemy; } } return null; } private static SpriteRenderer FindChildRenderer( EnemyController enemy, string objectName) { if (enemy == null) { return null; } SpriteRenderer[] renderers = enemy.GetComponentsInChildren(true); foreach (SpriteRenderer renderer in renderers) { if (renderer != null && renderer.gameObject.name == objectName) { return renderer; } } return null; } private static int FindChildRendererCount( EnemyController enemy, string objectName) { if (enemy == null) { return 0; } int count = 0; SpriteRenderer[] renderers = enemy.GetComponentsInChildren(true); foreach (SpriteRenderer renderer in renderers) { if (renderer != null && renderer.gameObject.name == objectName) { count++; } } return count; } private static SpriteRenderer FindFirstRenderer(string objectName) { SpriteRenderer[] renderers = Object.FindObjectsByType( FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (SpriteRenderer renderer in renderers) { if (renderer != null && renderer.gameObject.name == objectName) { return renderer; } } return null; } private static int CountRenderers(string objectName) { int count = 0; SpriteRenderer[] renderers = Object.FindObjectsByType( FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (SpriteRenderer renderer in renderers) { if (renderer != null && renderer.gameObject.name == objectName) { count++; } } return count; } private static void SelectArtifact( ActiveArtifactController artifacts, ActiveArtifactEffect effect) { Assert.That(artifacts.OwnedArtifactCount, Is.GreaterThanOrEqualTo(2)); for (int i = 0; i < artifacts.OwnedArtifactCount; i++) { if (artifacts.CurrentArtifact != null && artifacts.CurrentArtifact.Effect == effect) { return; } Assert.That(artifacts.SelectNext(), Is.True); } Assert.Fail($"Artifact {effect} was not found in the debug catalog."); } } }