Files
tiny_tackle_heroes/Assets/_Project/Tests/PlayMode/VisualAlignmentTests.cs
T

1855 lines
85 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using BumpCombat.Combat;
using BumpCombat.Core;
using BumpCombat.Enemies;
using BumpCombat.Player;
using BumpCombat.Progression;
using BumpCombat.Spawning;
using BumpCombat.UI;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using UnityEngine.UI;
namespace BumpCombat.PlayModeTests
{
public sealed class VisualAlignmentTests
{
private static readonly Vector2[] Directions =
{
Vector2.right,
Vector2.left,
Vector2.up,
Vector2.down,
new Vector2(1f, 1f).normalized,
new Vector2(-1f, 1f).normalized,
new Vector2(1f, -1f).normalized,
new Vector2(-1f, -1f).normalized,
};
[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 ScorchingRay_NormalVisualAndHitsAlignAcrossCardinalAndDiagonalDirections()
{
yield return AssertScorchingRayAlignment(false);
}
[UnityTest]
public IEnumerator ScorchingRay_ChargedVisualAndHitsAlignAcrossCardinalAndDiagonalDirections()
{
yield return AssertScorchingRayAlignment(true);
}
[UnityTest]
public IEnumerator PulseRing_RendererEllipseAndGroundTargetFilterStayAligned()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
director.DebugSpawnImmediate(2);
yield return null;
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
Rigidbody2D playerBody = player.GetComponent<Rigidbody2D>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
playerBody.position = new Vector2(1.25f, -0.75f);
EnemyController[] enemies = Object.FindObjectsByType<EnemyController>(
FindObjectsSortMode.InstanceID);
Assert.That(enemies.Length, Is.GreaterThanOrEqualTo(2));
for (int enemyIndex = 2; enemyIndex < enemies.Length; enemyIndex++)
{
enemies[enemyIndex].gameObject.SetActive(false);
}
ActiveArtifactDefinition definition = SelectArtifact(
artifacts,
ActiveArtifactEffect.Pulse);
float radius = definition.ChargedRange;
float verticalRadius = 0.8125f;
Vector2 diagonal = new Vector2(1f, 1f).normalized;
Vector2 targetStartPosition = playerBody.position
+ new Vector2(radius * diagonal.x, verticalRadius * diagonal.y);
float targetHealthBefore = enemies[0].CurrentHealth;
PlaceEnemy(
enemies[0],
targetStartPosition);
Vector2 outsideStartPosition = playerBody.position
+ Vector2.up * (verticalRadius + 0.1f);
float outsideHealthBefore = enemies[1].CurrentHealth;
PlaceEnemy(
enemies[1],
outsideStartPosition);
AddOverlappingCollider(enemies[1]);
Physics2D.SyncTransforms();
EnsureGauge(artifacts, definition.ChargedGaugeCost);
Assert.That(artifacts.TryUseCurrent(true), Is.True);
GameObject visual = GameObject.Find("Charged Artifact Pulse");
Assert.That(visual, Is.Not.Null);
Assert.That(
Vector2.Distance((Vector2)visual.transform.position, playerBody.position),
Is.LessThan(0.0001f));
Assert.That(
visual.GetComponentsInChildren<LineRenderer>(true),
Is.Empty);
Assert.That(
visual.GetComponentInChildren<SpriteRenderer>(),
Is.Not.Null);
Assert.That(enemies[0].IsStunned, Is.True);
Assert.That(enemies[1].IsStunned, Is.False);
Assert.That(enemies[0].transform.Find("Stun Runes"), Is.Not.Null);
Assert.That(enemies[1].transform.Find("Stun Runes"), Is.Null);
Vector2 repelledOffset =
enemies[0].GetComponent<Rigidbody2D>().position
- playerBody.position;
Vector2 initialOffset = targetStartPosition - playerBody.position;
float boundaryDistance = 1f / Mathf.Sqrt(
diagonal.x * diagonal.x / (radius * radius)
+ diagonal.y * diagonal.y
/ (verticalRadius * verticalRadius));
Assert.That(
Vector2.Distance(initialOffset, repelledOffset),
Is.GreaterThanOrEqualTo(definition.ChargedKnockback - 0.01f));
Assert.That(
repelledOffset.magnitude,
Is.GreaterThanOrEqualTo(boundaryDistance + 0.099f));
Assert.That(
repelledOffset.x * repelledOffset.x / (radius * radius)
+ repelledOffset.y * repelledOffset.y
/ (verticalRadius * verticalRadius),
Is.GreaterThan(1.0001f));
Assert.That(
Vector2.Dot(initialOffset.normalized, repelledOffset.normalized),
Is.GreaterThan(0.9999f));
Assert.That(enemies[0].CurrentHealth, Is.EqualTo(targetHealthBefore));
Assert.That(
enemies[1].GetComponent<Rigidbody2D>().position,
Is.EqualTo(outsideStartPosition));
Assert.That(enemies[1].CurrentHealth, Is.EqualTo(outsideHealthBefore));
enemies[0].enabled = true;
yield return new WaitForSeconds(0.7f);
Assert.That(enemies[0].IsStunned, Is.False);
}
[UnityTest]
public IEnumerator LevelUpSelectionPause_DoesNotLeaveArtifactRangeLineRenderer()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
director.DebugSpawnImmediate(1);
yield return null;
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
ActiveArtifactDefinition definition = SelectArtifact(
artifacts,
ActiveArtifactEffect.Pulse);
EnsureGauge(artifacts, definition.NormalGaugeCost);
Assert.That(artifacts.TryUseCurrent(false), Is.True);
GameObject visual = GameObject.Find("Artifact Pulse");
Assert.That(visual, Is.Not.Null);
ExperienceSystem experience =
player.GetComponent<ExperienceSystem>();
Assert.That(experience, Is.Not.Null);
experience.AddExperience(experience.RequiredExperience);
Assert.That(RunManager.Instance.IsSelectionOpen, Is.True);
Assert.That(Time.timeScale, Is.Zero);
Assert.That(
visual.GetComponentsInChildren<LineRenderer>(true),
Is.Empty);
RunManager.Instance.SetSelectionOpen(false);
Time.timeScale = 1f;
yield return new WaitForSecondsRealtime(0.25f);
}
[UnityTest]
public IEnumerator Cyclone_RendererFollowsPlayerBodyWhileItsHitCenterMoves()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
director.DebugSpawnImmediate(2);
yield return null;
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
Rigidbody2D playerBody = player.GetComponent<Rigidbody2D>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
SetPlayerDirection(player, Vector2.right);
playerBody.position = new Vector2(1.25f, -0.75f);
EnemyController[] enemies = Object.FindObjectsByType<EnemyController>(
FindObjectsSortMode.InstanceID);
Assert.That(enemies.Length, Is.GreaterThanOrEqualTo(2));
ActiveArtifactDefinition definition = SelectArtifact(
artifacts,
ActiveArtifactEffect.Cyclone);
PlaceEnemy(enemies[0], playerBody.position + Vector2.right * 0.3f);
PlaceEnemy(enemies[1], playerBody.position + Vector2.up * 0.3f);
AddOverlappingCollider(enemies[1]);
Physics2D.SyncTransforms();
EnsureGauge(artifacts, definition.NormalGaugeCost);
List<CombatHitResult> hits = new();
System.Action<CombatHitResult> handler = result =>
{
if (result.SourceId == definition.ArtifactId)
{
hits.Add(result);
}
};
CombatEvents.OnValidHit += handler;
try
{
Assert.That(artifacts.TryUseCurrent(false), Is.True);
GameObject visual = GameObject.Find("Cyclone");
Assert.That(visual, Is.Not.Null);
Vector3 expectedShadowOffset = player.transform.TransformVector(
new Vector3(0f, -7.5f / 32f, 0f));
Assert.That(
Vector2.Distance(
(Vector2)visual.transform.position,
playerBody.position + (Vector2)expectedShadowOffset),
Is.LessThan(0.0001f));
Assert.That(visual.transform.parent, Is.Null);
Assert.That(
visual.GetComponentsInChildren<LineRenderer>(true),
Is.Empty);
SpriteRenderer[] cycloneRenderers =
visual.GetComponentsInChildren<SpriteRenderer>();
Assert.That(cycloneRenderers, Has.Length.EqualTo(2));
foreach (SpriteRenderer cycloneRenderer in cycloneRenderers)
{
AssertUniformScale(cycloneRenderer.transform.lossyScale, 1f);
}
SpriteRenderer normalBack = visual.transform.Find(
"Cyclone Orbit Back").GetComponent<SpriteRenderer>();
SpriteRenderer normalFront = visual.transform.Find(
"Cyclone Orbit Front").GetComponent<SpriteRenderer>();
YSortRenderer ySort = player.GetComponent<YSortRenderer>();
Assert.That(normalBack.sortingOrder,
Is.EqualTo(ySort.CalculateSortingOrder() - 1));
Assert.That(normalFront.sortingOrder,
Is.EqualTo(ySort.CalculateSortingOrder() + 1));
Assert.That(hits.Count, Is.EqualTo(2));
Assert.That(
hits.TrueForAll(
hit => hit.Target == enemies[0].gameObject
|| hit.Target == enemies[1].gameObject),
Is.True);
Assert.That(
hits.Exists(hit => hit.Target == enemies[0].gameObject),
Is.True);
Assert.That(
hits.Exists(hit => hit.Target == enemies[1].gameObject),
Is.True);
Time.timeScale = 1f;
playerBody.position += new Vector2(0f, 0.4f);
Physics2D.SyncTransforms();
yield return new WaitForFixedUpdate();
Assert.That(visual, Is.Not.Null);
Assert.That(
Vector2.Distance(
(Vector2)visual.transform.position,
playerBody.position + (Vector2)expectedShadowOffset),
Is.LessThan(0.0001f));
Assert.That(normalBack.sortingOrder,
Is.EqualTo(ySort.CalculateSortingOrder() - 1));
Assert.That(normalFront.sortingOrder,
Is.EqualTo(ySort.CalculateSortingOrder() + 1));
float timeout = 1.5f;
while (artifacts.IsExecutingArtifact && timeout > 0f)
{
timeout -= Time.unscaledDeltaTime;
yield return null;
}
Assert.That(timeout, Is.GreaterThan(0f));
}
finally
{
CombatEvents.OnValidHit -= handler;
}
yield return null;
Assert.That(GameObject.Find("Cyclone"), Is.Null);
Assert.That(GameObject.Find("Cyclone Orbit Back"), Is.Null);
Assert.That(GameObject.Find("Cyclone Orbit Front"), Is.Null);
EnsureGauge(artifacts, definition.ChargedGaugeCost);
Assert.That(artifacts.TryUseCurrent(true), Is.True);
GameObject chargedVisual = GameObject.Find("Charged Cyclone");
Assert.That(chargedVisual, Is.Not.Null);
Assert.That(
chargedVisual.GetComponentsInChildren<LineRenderer>(true),
Is.Empty);
SpriteRenderer[] chargedRenderers =
chargedVisual.GetComponentsInChildren<SpriteRenderer>();
Assert.That(chargedRenderers, Has.Length.EqualTo(2));
foreach (SpriteRenderer chargedRenderer in chargedRenderers)
{
AssertUniformScale(chargedRenderer.transform.lossyScale, 1f);
}
yield return new WaitForSecondsRealtime(0.7f);
Assert.That(GameObject.Find("Charged Cyclone"), Is.Null);
Assert.That(GameObject.Find("Cyclone Orbit Back"), Is.Null);
Assert.That(GameObject.Find("Cyclone Orbit Front"), Is.Null);
}
[UnityTest]
public IEnumerator ThunderCrash_RendererAndFixedFrontHitCenterRejectOutsideBody()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
director.DebugSpawnImmediate(2);
yield return null;
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
Rigidbody2D playerBody = player.GetComponent<Rigidbody2D>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
SetPlayerDirection(player, Vector2.right);
playerBody.position = new Vector2(1.25f, -0.75f);
Vector2 playerGroundCenter = playerBody.position
+ (Vector2)player.transform.TransformVector(
new Vector2(0f, -0.1f));
EnemyController[] enemies = Object.FindObjectsByType<EnemyController>(
FindObjectsSortMode.InstanceID);
Assert.That(enemies.Length, Is.GreaterThanOrEqualTo(2));
ActiveArtifactDefinition definition = SelectArtifact(
artifacts,
ActiveArtifactEffect.ThunderCrash);
float chargedVisualScale = 1.05f * definition.ChargedRange;
Vector2 chargedHitCenter = playerGroundCenter
+ Vector2.right * 0.45f;
Vector2 effectCenter = chargedHitCenter
- new Vector2(-0.5f, 0.5f) / 32f * chargedVisualScale;
PlaceEnemy(enemies[0], chargedHitCenter + Vector2.right * 0.5f);
float chargedHitRadiusY = 7.25f / 32f * chargedVisualScale;
PlaceEnemy(
enemies[1],
chargedHitCenter
+ Vector2.up * (chargedHitRadiusY + 2f));
SetEnemyHealth(enemies[0], 1000f);
AddOverlappingCollider(enemies[1]);
Physics2D.SyncTransforms();
EnsureGauge(artifacts, definition.ChargedGaugeCost);
List<CombatHitResult> hits = new();
System.Action<CombatHitResult> handler = result =>
{
if (result.SourceId == definition.ArtifactId)
{
hits.Add(result);
}
};
CombatEvents.OnValidHit += handler;
Assert.That(artifacts.TryUseCurrent(true), Is.True);
GameObject visual = GameObject.Find("Charged Thunder Crash");
Assert.That(visual, Is.Not.Null);
Assert.That(
Vector2.Distance((Vector2)visual.transform.position, effectCenter),
Is.LessThan(0.0001f));
Assert.That(
visual.GetComponentsInChildren<LineRenderer>(true),
Is.Empty);
SpriteRenderer[] thunderRenderers =
visual.GetComponentsInChildren<SpriteRenderer>(true);
Assert.That(thunderRenderers, Has.Length.EqualTo(1));
SpriteRenderer reachRenderer = System.Array.Find(
thunderRenderers,
renderer => renderer.transform.name == "Thunder Crash Sprite");
Assert.That(reachRenderer, Is.Not.Null);
Assert.That(reachRenderer.sprite, Is.Not.Null);
Assert.That(reachRenderer.transform.localScale.x * 17f / 32f,
Is.EqualTo(17f / 32f * chargedVisualScale).Within(0.0001f));
Assert.That(reachRenderer.transform.localScale.y * 7.25f / 32f,
Is.EqualTo(chargedHitRadiusY).Within(0.0001f));
Vector2 reachCenter = (Vector2)reachRenderer.transform.position
+ new Vector2(-0.5f, 0.5f) / 32f * reachRenderer.transform.localScale.y;
Assert.That(Vector2.Distance(reachCenter, chargedHitCenter),
Is.LessThan(0.0001f));
SpriteRenderer warlockRenderer = System.Array.Find(
thunderRenderers,
renderer => renderer.transform.name == "Thunder Crash Sprite");
Assert.That(warlockRenderer, Is.Not.Null);
foreach (var layer in thunderRenderers)
Assert.That(layer.transform.localPosition, Is.EqualTo(Vector3.zero));
AssertUniformScale(warlockRenderer.transform.localScale, chargedVisualScale);
Assert.That(warlockRenderer.sprite.pivot,
Is.EqualTo(new Vector2(50.5f, 42.5f)));
Assert.That(warlockRenderer.sprite.rect,
Is.EqualTo(new Rect(0f, 0f, 100f, 100f)));
Vector2 fixedVisualPosition = visual.transform.position;
playerBody.position += Vector2.down * 0.2f;
Assert.That((Vector2)visual.transform.position,
Is.EqualTo(fixedVisualPosition));
Time.timeScale = 1f;
float timeout = 2f;
while (artifacts.IsExecutingArtifact && timeout > 0f)
{
timeout -= Time.unscaledDeltaTime;
yield return null;
}
CombatEvents.OnValidHit -= handler;
Assert.That(timeout, Is.GreaterThan(0f));
Assert.That(hits.Count, Is.EqualTo(5));
Assert.That(hits.TrueForAll(hit => hit.Target == enemies[0].gameObject), Is.True);
EnsureGauge(artifacts, definition.NormalGaugeCost);
Assert.That(artifacts.TryUseCurrent(false), Is.True);
GameObject normalVisual = GameObject.Find("Thunder Crash");
Assert.That(normalVisual, Is.Not.Null);
Vector2 normalGroundCenter = playerBody.position
+ (Vector2)player.transform.TransformVector(
new Vector2(0f, -0.1f))
+ Vector2.right * 0.45f;
Vector2 normalCenter = normalGroundCenter
- new Vector2(-0.5f, 0.5f) / 32f
* (1.05f * definition.NormalRange);
Assert.That(
Vector2.Distance((Vector2)normalVisual.transform.position, normalCenter),
Is.LessThan(0.0001f));
Assert.That(
normalVisual.GetComponentsInChildren<LineRenderer>(true),
Is.Empty);
SpriteRenderer normalGroundRenderer = System.Array.Find(
normalVisual.GetComponentsInChildren<SpriteRenderer>(true),
renderer => renderer.transform.name == "Thunder Crash Sprite");
Assert.That(normalGroundRenderer, Is.Not.Null);
AssertUniformScale(
normalGroundRenderer.transform.localScale,
1.05f * definition.NormalRange);
Assert.That(normalGroundRenderer.sprite.pivot,
Is.EqualTo(new Vector2(50.5f, 42.5f)));
yield return new WaitForSecondsRealtime(0.4f);
}
[UnityTest]
public IEnumerator RunHUD_UsesScorchingRayForHitDebugLabelAndPopup()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
director.DebugSpawnImmediate(1);
yield return null;
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
Rigidbody2D playerBody = player.GetComponent<Rigidbody2D>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
EnemyController enemy = Object.FindAnyObjectByType<EnemyController>();
enemy.enabled = false;
playerBody.position = new Vector2(1.25f, -0.75f);
enemy.GetComponent<Rigidbody2D>().position = playerBody.position
+ Vector2.right * 1.5f;
Physics2D.SyncTransforms();
ActiveArtifactDefinition definition = SelectArtifact(
artifacts,
ActiveArtifactEffect.Phoenix);
EnsureGauge(artifacts, definition.NormalGaugeCost);
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
Assert.That(hud, Is.Not.Null);
Assert.That(artifacts.TryUseCurrent(false), Is.True);
Text hitDebugText = (Text)typeof(RunHUD)
.GetField("hitDebugText", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(hud);
Assert.That(hitDebugText.text, Is.EqualTo("ARTIFACT 잿불의 마검 35"));
Text[] texts = Object.FindObjectsByType<Text>(
FindObjectsInactive.Include,
FindObjectsSortMode.None);
Assert.That(
System.Array.Exists(
texts,
text => text != hitDebugText
&& text.gameObject.activeInHierarchy
&& text.text == "잿불의 마검 35"),
Is.True);
yield return new WaitForSecondsRealtime(0.2f);
}
[UnityTest]
public IEnumerator PresentationUi_UsesSharedSlicesIconsFontAndGameOverStyle()
{
yield return LoadCombatPrototype();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
Assert.That(hud, Is.Not.Null);
SpriteRenderer arenaRenderer = GameObject.Find("Arena")
.GetComponent<SpriteRenderer>();
Assert.That(arenaRenderer.sharedMaterial, Is.Not.Null);
Assert.That(
arenaRenderer.sharedMaterial.shader.name,
Is.EqualTo("Universal Render Pipeline/2D/Sprite-Unlit-Default"));
Image healthFill = (Image)typeof(RunHUD)
.GetField("healthFill", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(hud);
Text healthText = (Text)typeof(RunHUD)
.GetField("healthText", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(hud);
Font galmuri = Resources.Load<Font>("Presentation/Fonts/Galmuri9");
Assert.That(galmuri, Is.Not.Null);
Assert.That(
Resources.Load<Font>("Presentation/Fonts/neodgm"),
Is.Not.Null);
Assert.That(
Resources.Load<Font>("Presentation/Fonts/NanumGothic-Regular"),
Is.Not.Null);
Assert.That(healthText.font, Is.Not.Null);
Assert.That(healthText.font, Is.SameAs(galmuri));
Assert.That(healthFill.sprite, Is.Not.Null);
Assert.That(healthFill.type, Is.EqualTo(Image.Type.Filled));
Assert.That(
healthFill.transform.parent.GetComponent<Image>().type,
Is.EqualTo(Image.Type.Sliced));
GameObject artifactHud = GameObject.Find("Active Artifact HUD");
Assert.That(artifactHud, Is.Not.Null);
Assert.That(
artifactHud.GetComponent<Image>().type,
Is.EqualTo(Image.Type.Sliced));
Image slotOne = GameObject.Find("Artifact Slot 1")
.GetComponent<Image>();
Image slotTwo = GameObject.Find("Artifact Slot 2")
.GetComponent<Image>();
Image slotThree = GameObject.Find("Artifact Slot 3")
.GetComponent<Image>();
Assert.That(slotOne.sprite, Is.Not.Null);
Assert.That(slotTwo.sprite, Is.Not.Null);
Assert.That(slotThree.sprite, Is.Not.Null);
Assert.That(slotOne.type, Is.EqualTo(Image.Type.Sliced));
Assert.That(slotTwo.type, Is.EqualTo(Image.Type.Sliced));
Assert.That(slotThree.type, Is.EqualTo(Image.Type.Sliced));
Image[] slotImages = slotOne.GetComponentsInChildren<Image>(true);
Assert.That(slotImages.Length, Is.GreaterThan(1));
Assert.That(slotImages[1].sprite, Is.Not.Null);
ExperienceSystem experience =
Object.FindAnyObjectByType<ExperienceSystem>();
experience.AddExperience(experience.RequiredExperience);
yield return null;
GameObject levelPanel = GameObject.Find("Level Up Selection");
Assert.That(levelPanel.activeSelf, Is.True);
Assert.That(levelPanel.GetComponent<Image>().type, Is.EqualTo(Image.Type.Sliced));
for (int i = 1; i <= 3; i++)
{
Image card = GameObject.Find($"Option Card {i}")
.GetComponent<Image>();
Text optionText = GameObject.Find($"Option {i}")
.GetComponent<Text>();
RectTransform cardRect = card.rectTransform;
RectTransform textRect = optionText.rectTransform;
Assert.That(card.sprite, Is.Not.Null);
Assert.That(card.type, Is.EqualTo(Image.Type.Sliced));
Assert.That(card.raycastTarget, Is.False);
Assert.That(card.preserveAspect, Is.False);
Assert.That(cardRect.anchorMin, Is.EqualTo(textRect.anchorMin));
Assert.That(cardRect.anchorMax, Is.EqualTo(textRect.anchorMax));
Assert.That(cardRect.pivot, Is.EqualTo(textRect.pivot));
Assert.That(cardRect.anchoredPosition,
Is.EqualTo(textRect.anchoredPosition));
Assert.That(cardRect.sizeDelta, Is.EqualTo(textRect.sizeDelta));
Assert.That(card.transform.GetSiblingIndex(),
Is.EqualTo(optionText.transform.GetSiblingIndex() - 1));
Assert.That(optionText.raycastTarget, Is.False);
}
Text levelTitle = levelPanel.transform.Find("Title").GetComponent<Text>();
Assert.That(levelTitle.resizeTextForBestFit, Is.True);
Assert.That(levelTitle.resizeTextMinSize, Is.EqualTo(24));
Assert.That(levelTitle.resizeTextMaxSize, Is.EqualTo(38));
Assert.That(levelTitle.horizontalOverflow, Is.EqualTo(HorizontalWrapMode.Overflow));
Assert.That(levelTitle.verticalOverflow, Is.EqualTo(VerticalWrapMode.Truncate));
levelPanel.SetActive(false);
RunManager.Instance.SetSelectionOpen(false);
RunManager.Instance.EndRun();
yield return null;
GameObject gameOver = GameObject.Find("Game Over");
Assert.That(gameOver.activeSelf, Is.True);
Assert.That(gameOver.GetComponent<Image>().type, Is.EqualTo(Image.Type.Sliced));
Assert.That(gameOver.GetComponent<Image>().sprite, Is.Not.Null);
}
[UnityTest]
public IEnumerator RunHUD_ReferenceCanvasLayoutBoundsAreSafeFor1280And1920()
{
yield return LoadCombatPrototype();
Canvas canvas = Object.FindAnyObjectByType<Canvas>();
CanvasScaler scaler = canvas.GetComponent<CanvasScaler>();
Assert.That(scaler.referenceResolution, Is.EqualTo(new Vector2(1920f, 1080f)));
RectTransform status = GameObject.Find("Run Status HUD")
.GetComponent<RectTransform>();
RectTransform artifact = GameObject.Find("Active Artifact HUD")
.GetComponent<RectTransform>();
RectTransform timer = GameObject.Find("Timer Panel")
.GetComponent<RectTransform>();
RectTransform debug = GameObject.Find("Debug Event Spawn Buttons")
.GetComponent<RectTransform>();
RectTransform minimap = GameObject.Find("Minimap")
.GetComponent<RectTransform>();
RectTransform minimapMap = GameObject.Find("Map")
.GetComponent<RectTransform>();
RectTransform minimapViewport = GameObject.Find("Minimap Camera Viewport")
.GetComponent<RectTransform>();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
Text eventText = GetPrivateField<Text>(hud, "eventText");
Image artifactGaugeFill = GetPrivateField<Image>(hud, "artifactGaugeFill");
Text artifactName = GetPrivateField<Text>(hud, "artifactNameText");
Text healthText = GetPrivateField<Text>(hud, "healthText");
Text levelText = GetPrivateField<Text>(hud, "levelText");
Text artifactGaugeText = GetPrivateField<Text>(hud, "artifactGaugeText");
Text artifactState = GetPrivateField<Text>(hud, "artifactStateText");
Image guardTrack = GetPrivateField<Image>(hud, "guardGaugeTrack");
Image guardFill = GetPrivateField<Image>(hud, "guardGaugeFill");
Text guardLabel = GetPrivateField<Text>(hud, "guardLabelText");
RectTransform gauge = artifactGaugeFill.transform.parent.GetComponent<RectTransform>();
Image healthPanelImage = GetPrivateField<Image>(hud, "healthFill")
.transform.parent.GetComponent<Image>();
Image experiencePanelImage = GetPrivateField<Image>(hud, "experienceFill")
.transform.parent.GetComponent<Image>();
Image healthFill = GetPrivateField<Image>(hud, "healthFill");
Image experienceFill = GetPrivateField<Image>(hud, "experienceFill");
RectTransform healthPanel = healthFill.transform.parent
.GetComponent<RectTransform>();
RectTransform experiencePanel = experienceFill.transform.parent
.GetComponent<RectTransform>();
AssertRect(status, Vector2.zero, Vector2.zero, new Vector2(24f, 24f),
new Vector2(392f, 136f));
AssertRect(artifact, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
new Vector2(-16f, 0f), new Vector2(360f, 136f));
Assert.That(artifact.parent, Is.SameAs(status.transform));
AssertRect(timer, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
new Vector2(0f, -24f), new Vector2(180f, 66f));
AssertRect(debug, new Vector2(0f, 1f), new Vector2(0f, 1f),
new Vector2(24f, -24f),
new Vector2(240f, 144f));
AssertRect(minimap, Vector2.one, Vector2.one, new Vector2(-24f, -24f),
new Vector2(300f, 162f));
AssertRect(minimapMap, new Vector2(0.5f, 0.5f),
new Vector2(0.5f, 0.5f), Vector2.zero,
new Vector2(288f, 150f));
Assert.That(GameObject.Find("Minimap/Title"), Is.Null);
Assert.That(minimap.GetComponent<Image>().fillCenter, Is.False);
Image minimapMapImage = minimapMap.GetComponent<Image>();
Assert.That(minimapMapImage.sprite, Is.Null);
Assert.That(minimapMapImage.color.a, Is.EqualTo(0.28f).Within(0.001f));
Assert.That(minimapMap.GetComponent<RectMask2D>(), Is.Not.Null);
Rect mapWorldRect = GetWorldRect(minimapMap);
Rect viewportWorldRect = GetWorldRect(minimapViewport);
Assert.That(viewportWorldRect.xMin, Is.GreaterThanOrEqualTo(mapWorldRect.xMin - 0.01f));
Assert.That(viewportWorldRect.xMax, Is.LessThanOrEqualTo(mapWorldRect.xMax + 0.01f));
Assert.That(viewportWorldRect.yMin, Is.GreaterThanOrEqualTo(mapWorldRect.yMin - 0.01f));
Assert.That(viewportWorldRect.yMax, Is.LessThanOrEqualTo(mapWorldRect.yMax + 0.01f));
AssertRect(eventText.rectTransform, new Vector2(0.5f, 1f),
new Vector2(0.5f, 1f), new Vector2(0f, -100f),
new Vector2(420f, 28f));
Assert.That(eventText.gameObject.activeSelf, Is.True);
Assert.That(eventText.text, Does.Contain("다음"));
Assert.That(debug.gameObject.activeSelf, Is.True);
AssertRect(gauge, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
new Vector2(0f, -24f), new Vector2(352f, 18f));
AssertRect(healthText.rectTransform,
new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
new Vector2(78f, 48f), new Vector2(172f, 32f));
Assert.That(healthText.transform.parent, Is.SameAs(status.transform));
AssertRect(healthPanel,
new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
new Vector2(-16f, -7f), new Vector2(352f, 12f));
AssertRect(experiencePanel,
new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
new Vector2(-16f, -39f), new Vector2(352f, 8f));
AssertRect(levelText.rectTransform,
new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
Vector2.zero, new Vector2(352f, 24f));
Assert.That(levelText.transform.parent, Is.SameAs(experiencePanel));
AssertRect(artifactGaugeText.rectTransform,
new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
Vector2.zero, new Vector2(352f, 26f));
Assert.That(artifactName.transform.parent, Is.SameAs(gauge.transform));
Assert.That(artifactName.gameObject.activeSelf, Is.False);
Assert.That(artifactState.rectTransform.anchoredPosition,
Is.EqualTo(new Vector2(116f, 21f)));
Assert.That(artifactState.resizeTextForBestFit, Is.True);
Assert.That(artifactState.resizeTextMinSize, Is.EqualTo(11));
Assert.That(artifactState.resizeTextMaxSize, Is.EqualTo(18));
Assert.That(healthPanelImage.enabled, Is.False);
Assert.That(experiencePanelImage.enabled, Is.False);
Assert.That(healthFill.rectTransform.rect.width, Is.EqualTo(352f));
Assert.That(experienceFill.rectTransform.rect.width, Is.EqualTo(352f));
Assert.That(experienceFill.rectTransform.rect.height, Is.EqualTo(8f));
AssertRect(guardTrack.rectTransform,
new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
new Vector2(182f, -3f), new Vector2(18f, 94f));
AssertRect(guardLabel.rectTransform,
new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
new Vector2(182f, 56f), new Vector2(32f, 20f));
Assert.That(guardTrack.pixelsPerUnitMultiplier, Is.EqualTo(5f));
Assert.That(guardFill.fillMethod, Is.EqualTo(Image.FillMethod.Vertical));
Assert.That(guardFill.fillOrigin, Is.EqualTo(0));
Assert.That(guardFill.rectTransform.offsetMin,
Is.EqualTo(new Vector2(2f, 2f)));
Assert.That(guardFill.rectTransform.offsetMax,
Is.EqualTo(new Vector2(-2f, -2f)));
Assert.That(guardFill.fillAmount, Is.EqualTo(1f).Within(0.001f));
Assert.That((Color32)guardFill.color, Is.EqualTo(new Color32(255, 205, 92, 255)));
Assert.That(guardLabel.text, Is.EqualTo("D"));
Assert.That(guardTrack.transform.parent, Is.SameAs(status.transform));
Assert.That(guardFill.transform.parent, Is.SameAs(guardTrack.transform));
Assert.That(RectanglesOverlap(status, artifact), Is.True);
Assert.That(RectanglesOverlap(timer, eventText.rectTransform), Is.False);
int previousWidth = Screen.width;
int previousHeight = Screen.height;
try
{
int[,] resolutions = { { 1280, 720 }, { 1920, 1080 } };
for (int i = 0; i < resolutions.GetLength(0); i++)
{
Screen.SetResolution(
resolutions[i, 0],
resolutions[i, 1],
FullScreenMode.Windowed);
yield return null;
Canvas.ForceUpdateCanvases();
AssertRectInsideCanvas(status, canvas.GetComponent<RectTransform>());
AssertRectInsideCanvas(artifact, canvas.GetComponent<RectTransform>());
AssertRectInsideCanvas(timer, canvas.GetComponent<RectTransform>());
AssertRectInsideCanvas(debug, canvas.GetComponent<RectTransform>());
AssertRectInsideCanvas(minimap, canvas.GetComponent<RectTransform>());
AssertRectInsideCanvas(eventText.rectTransform,
canvas.GetComponent<RectTransform>());
}
}
finally
{
if (previousWidth > 0 && previousHeight > 0)
{
Screen.SetResolution(
previousWidth,
previousHeight,
FullScreenMode.Windowed);
}
}
}
[UnityTest]
public IEnumerator PixelPerfectCamera_PixelSnappingUsesEvenNativeViewportAtOddTarget()
{
yield return LoadCombatPrototype();
Camera camera = Camera.main;
Component pixelPerfect = camera.GetComponent(
"UnityEngine.Rendering.Universal.PixelPerfectCamera");
Assert.That(pixelPerfect, Is.Not.Null);
PropertyInfo gridSnapping = pixelPerfect.GetType()
.GetProperty("gridSnapping");
Assert.That(
gridSnapping.GetValue(pixelPerfect).ToString(),
Is.EqualTo("PixelSnapping"));
RenderTexture target = new(1399, 799, 24, RenderTextureFormat.ARGB32);
target.Create();
RenderTexture previousTarget = camera.targetTexture;
camera.targetTexture = target;
try
{
camera.Render();
yield return null;
Rect pixelRect = camera.pixelRect;
Assert.That(pixelRect.width, Is.EqualTo(1398f).Within(0.01f));
Assert.That(pixelRect.height, Is.EqualTo(798f).Within(0.01f));
Assert.That(pixelRect.x, Is.EqualTo(0f).Within(0.01f));
Assert.That(pixelRect.y, Is.EqualTo(0f).Within(0.01f));
Assert.That(pixelRect.width % 2f, Is.EqualTo(0f).Within(0.01f));
Assert.That(pixelRect.height % 2f, Is.EqualTo(0f).Within(0.01f));
Assert.That(camera.aspect, Is.EqualTo(1398f / 798f).Within(0.001f));
Assert.That(camera.orthographicSize, Is.EqualTo(6.234375f).Within(0.01f));
FieldInfo internalField = pixelPerfect.GetType().GetField(
"m_Internal",
BindingFlags.Instance | BindingFlags.NonPublic);
object internalCamera = internalField.GetValue(pixelPerfect);
FieldInfo widthField = internalCamera.GetType().GetField(
"offscreenRTWidth",
BindingFlags.Instance | BindingFlags.NonPublic);
FieldInfo heightField = internalCamera.GetType().GetField(
"offscreenRTHeight",
BindingFlags.Instance | BindingFlags.NonPublic);
FieldInfo useOffscreenField = internalCamera.GetType().GetField(
"useOffscreenRT",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(widthField.GetValue(internalCamera), Is.EqualTo(0));
Assert.That(heightField.GetValue(internalCamera), Is.EqualTo(0));
Assert.That(useOffscreenField.GetValue(internalCamera), Is.False);
float correctedOrthoSize = camera.orthographicSize;
camera.Render();
Assert.That(camera.pixelRect.width, Is.EqualTo(1398f).Within(0.01f));
Assert.That(camera.pixelRect.height, Is.EqualTo(798f).Within(0.01f));
Assert.That(camera.orthographicSize,
Is.EqualTo(correctedOrthoSize).Within(0.01f));
ArenaCameraFollow follow = camera.GetComponent<ArenaCameraFollow>();
bool previousFollowEnabled = follow.enabled;
follow.enabled = false;
camera.Render();
follow.enabled = previousFollowEnabled;
camera.Render();
Assert.That(camera.pixelRect.width, Is.EqualTo(1398f).Within(0.01f));
Assert.That(camera.pixelRect.height, Is.EqualTo(798f).Within(0.01f));
}
finally
{
camera.targetTexture = previousTarget;
target.Release();
Object.Destroy(target);
}
}
[UnityTest]
public IEnumerator RunHUD_MinimapTracksPlayerAndEventEnemies()
{
yield return LoadCombatPrototype();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
ArenaBounds arena = ArenaBounds.Resolve();
Assert.That(hud, Is.Not.Null);
Assert.That(arena, Is.Not.Null);
director.enabled = false;
EnemyController[] existingEnemies = Object.FindObjectsByType<EnemyController>(
FindObjectsSortMode.None);
for (int i = 0; i < existingEnemies.Length; i++)
{
existingEnemies[i].gameObject.SetActive(false);
}
RunManager.Instance.SetSelectionOpen(false);
yield return null;
director.DebugSpawnImmediate(1);
Assert.That(director.TryDebugSpawnEventEnemy(RunTimedEvent.Elite), Is.True);
yield return null;
Time.timeScale = 0f;
yield return new WaitForSecondsRealtime(0.2f);
GameObject minimap = GameObject.Find("Minimap");
Image playerMarker = GameObject.Find("Minimap Player")
.GetComponent<Image>();
Transform markerRoot = GameObject.Find("Markers").transform;
Image eventMarker = null;
Image[] markerImages = markerRoot.GetComponentsInChildren<Image>(true);
for (int i = 0; i < markerImages.Length; i++)
{
if (((Color32)markerImages[i].color).Equals(
new Color32(255, 220, 107, 255)))
{
eventMarker = markerImages[i];
break;
}
}
RectTransform viewport = GameObject.Find("Minimap Camera Viewport")
.GetComponent<RectTransform>();
Assert.That(minimap.activeSelf, Is.True);
Assert.That(
((Color32)playerMarker.color).Equals(
new Color32(106, 236, 255, 255)),
Is.True);
Assert.That(eventMarker, Is.Not.Null);
Assert.That(
((Color32)eventMarker.color).Equals(
new Color32(255, 220, 107, 255)),
Is.True);
Assert.That(eventMarker.rectTransform.rect.width, Is.EqualTo(10f));
Assert.That(viewport.rect.width, Is.GreaterThan(0f));
Assert.That(viewport.rect.height, Is.GreaterThan(0f));
Vector2 oldMarkerPosition = playerMarker.rectTransform.anchoredPosition;
Vector2 movedPosition = arena.ClampPlayerPosition(new Vector2(6f, 2f));
player.GetComponent<Rigidbody2D>().position = movedPosition;
player.transform.position = movedPosition;
Physics2D.SyncTransforms();
yield return new WaitForSecondsRealtime(0.1f);
Assert.That(
playerMarker.rectTransform.anchoredPosition,
Is.Not.EqualTo(oldMarkerPosition));
Assert.That(
playerMarker.rectTransform.anchoredPosition.x,
Is.InRange(-144f, 144f));
Assert.That(
playerMarker.rectTransform.anchoredPosition.y,
Is.InRange(-75f, 75f));
}
[UnityTest]
public IEnumerator RunHUD_ArtifactColorSwitchPreservesGaugeAndGuardReadiness()
{
RunManager.ForceProductionModeForTests = true;
ArtifactRewardController.GrantCatalogForTests = false;
yield return LoadCombatPrototype();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
ActiveArtifactController artifacts =
Object.FindAnyObjectByType<ActiveArtifactController>();
Image artifactFill = GetPrivateField<Image>(hud, "artifactGaugeFill");
Image guardFill = GetPrivateField<Image>(hud, "guardGaugeFill");
Assert.That(artifacts.TryAddArtifact(artifacts.GetCatalogArtifactAt(0)), Is.True);
Assert.That(artifacts.TryAddArtifact(artifacts.GetCatalogArtifactAt(1)), Is.True);
Assert.That(artifacts.TryAddArtifact(artifacts.GetCatalogArtifactAt(4)), Is.True);
EnsureGauge(artifacts, 12f);
float gaugeBefore = artifacts.CurrentGauge;
for (int i = 0; i < 3; i++)
{
yield return null;
Assert.That(artifactFill.color, Is.EqualTo(artifacts.CurrentArtifact.EffectColor));
Assert.That(artifacts.CurrentGauge, Is.EqualTo(gaugeBefore));
Assert.That(guardFill.fillAmount, Is.EqualTo(1f).Within(0.001f));
Assert.That(guardFill.fillMethod, Is.EqualTo(Image.FillMethod.Vertical));
Assert.That(guardFill.fillOrigin, Is.EqualTo(0));
if (i < 2)
{
Assert.That(artifacts.SelectNext(), Is.True);
}
}
}
[UnityTest]
public IEnumerator RunHUD_GuardGaugeTracksActivationAndCooldownIndependently()
{
yield return LoadCombatPrototype();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
PlayerHealth health = Object.FindAnyObjectByType<PlayerHealth>();
Image guardFill = GetPrivateField<Image>(hud, "guardGaugeFill");
Text guardLabel = GetPrivateField<Text>(hud, "guardLabelText");
Assert.That(health, Is.Not.Null);
Assert.That(guardFill.fillAmount, Is.EqualTo(1f).Within(0.001f));
Assert.That(guardLabel.text, Is.EqualTo("D"));
Assert.That((Color32)guardFill.color, Is.EqualTo(new Color32(255, 205, 92, 255)));
SetPrivateField(health, "guardCooldownDuration", 0.8f);
SetPrivateField(health, "guardDuration", 0.1f);
Assert.That(health.TryActivateGuard(), Is.True);
yield return null;
Assert.That(health.IsGuarding, Is.True);
Assert.That(
guardFill.fillAmount,
Is.EqualTo(health.GuardReadinessNormalized).Within(0.02f));
Assert.That((Color32)guardFill.color, Is.EqualTo(new Color32(255, 232, 128, 255)));
Assert.That((Color32)guardLabel.color, Is.EqualTo(new Color32(255, 232, 128, 255)));
yield return new WaitForSecondsRealtime(0.4f);
Assert.That(health.IsGuarding, Is.False);
float cooldownFill = guardFill.fillAmount;
Assert.That(
cooldownFill,
Is.EqualTo(health.GuardReadinessNormalized).Within(0.02f));
Assert.That(cooldownFill, Is.EqualTo(0.5f).Within(0.15f));
Assert.That((Color32)guardFill.color, Is.EqualTo(new Color32(137, 129, 123, 255)));
Assert.That((Color32)guardLabel.color, Is.EqualTo(new Color32(137, 129, 123, 255)));
}
[UnityTest]
public IEnumerator RunHUD_CompactClusterFadesForVisibleActorOverlap()
{
yield return LoadCombatPrototype();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
RectTransform cluster = GameObject.Find("Run Status HUD")
.GetComponent<RectTransform>();
CanvasGroup group = cluster.GetComponent<CanvasGroup>();
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
Rigidbody2D body = player.GetComponent<Rigidbody2D>();
MethodInfo refresh = typeof(RunHUD).GetMethod(
"UpdateCompactHudVisibility",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(refresh, Is.Not.Null);
Assert.That(group.alpha, Is.EqualTo(1f));
Vector3[] corners = new Vector3[4];
cluster.GetWorldCorners(corners);
Vector3 screenCenter = (corners[0] + corners[2]) * 0.5f;
Vector3 worldCenter = Camera.main.ScreenToWorldPoint(
new Vector3(screenCenter.x, screenCenter.y, -Camera.main.transform.position.z));
player.enabled = false;
body.position = worldCenter;
Physics2D.SyncTransforms();
refresh.Invoke(hud, null);
Assert.That(group.alpha, Is.EqualTo(0.4f));
body.position = Vector2.zero;
Physics2D.SyncTransforms();
refresh.Invoke(hud, null);
Assert.That(group.alpha, Is.EqualTo(1f));
}
[UnityTest]
public IEnumerator RunHUD_AllTextUsesGalmuriPixelFont()
{
yield return LoadCombatPrototype();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
Font galmuri = Resources.Load<Font>("Presentation/Fonts/Galmuri9");
Assert.That(galmuri, Is.Not.Null);
Text health = GetPrivateField<Text>(hud, "healthText");
Text level = GetPrivateField<Text>(hud, "levelText");
Text timer = GetPrivateField<Text>(hud, "timerText");
Text enemyCount = GetPrivateField<Text>(hud, "enemyCountText");
Text gauge = GetPrivateField<Text>(hud, "artifactGaugeText");
Text state = GetPrivateField<Text>(hud, "artifactStateText");
Text name = GetPrivateField<Text>(hud, "artifactNameText");
Assert.That(health.font, Is.SameAs(galmuri));
Assert.That(level.font, Is.SameAs(galmuri));
Assert.That(timer.font, Is.SameAs(galmuri));
Assert.That(name.font, Is.SameAs(galmuri));
Assert.That(health.fontSize, Is.EqualTo(24));
Assert.That(level.fontSize, Is.EqualTo(18));
Assert.That(timer.fontSize, Is.EqualTo(30));
Assert.That(gauge.fontSize, Is.EqualTo(18));
Assert.That(gauge.rectTransform.rect.size, Is.EqualTo(new Vector2(352f, 26f)));
Assert.That(name.fontSize, Is.EqualTo(30));
Assert.That(enemyCount.font, Is.SameAs(galmuri));
Assert.That(state.font, Is.SameAs(galmuri));
Text damagePopupTemplate = GetPrivateField<Text>(hud, "damagePopupTemplate");
Assert.That(damagePopupTemplate.font, Is.SameAs(galmuri));
Assert.That(damagePopupTemplate.fontSize, Is.EqualTo(30));
Outline outline = damagePopupTemplate.GetComponent<Outline>();
Assert.That(outline, Is.Not.Null);
Assert.That(outline.effectDistance, Is.EqualTo(Vector2.one));
Assert.That(name.gameObject.activeSelf, Is.False);
Assert.That(name.horizontalOverflow, Is.EqualTo(HorizontalWrapMode.Overflow));
Assert.That(name.verticalOverflow, Is.EqualTo(VerticalWrapMode.Overflow));
Canvas.ForceUpdateCanvases();
Text[] compactTexts = { health, level, gauge, state };
foreach (Text compactText in compactTexts)
{
Assert.That(compactText.isActiveAndEnabled, Is.True);
Assert.That(compactText.text, Is.Not.EqualTo("준비"), compactText.name);
if (compactText == state && string.IsNullOrEmpty(state.text))
{
continue;
}
Assert.That(compactText.cachedTextGenerator.characterCountVisible,
Is.GreaterThan(0), compactText.name);
Assert.That(compactText.preferredHeight,
Is.LessThanOrEqualTo(compactText.rectTransform.rect.height + 1f),
compactText.name);
}
ActiveArtifactController artifacts =
Object.FindAnyObjectByType<ActiveArtifactController>();
ActiveArtifactDefinition longNameDefinition = SelectArtifact(
artifacts,
ActiveArtifactEffect.ChainLightning);
yield return null;
Canvas.ForceUpdateCanvases();
Assert.That(name.text, Is.EqualTo(longNameDefinition.DisplayName));
Assert.That(name.gameObject.activeSelf, Is.False);
}
[UnityTest]
public IEnumerator RunHUD_UpdatesZeroOneTwoAndThreeArtifactSlotsWithoutOverflow()
{
RunManager.ForceProductionModeForTests = true;
ArtifactRewardController.GrantCatalogForTests = false;
yield return LoadCombatPrototype();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
ActiveArtifactController artifacts =
Object.FindAnyObjectByType<ActiveArtifactController>();
Image[] slotIcons = GetPrivateField<Image[]>(hud, "artifactSlotIcons");
Text name = GetPrivateField<Text>(hud, "artifactNameText");
Assert.That(slotIcons, Has.Length.EqualTo(3));
Assert.That(slotIcons[0].enabled, Is.False);
Assert.That(slotIcons[1].enabled, Is.False);
Assert.That(slotIcons[2].enabled, Is.False);
Assert.That(name.text, Is.EqualTo("EMPTY"));
ActiveArtifactDefinition first = artifacts.GetCatalogArtifactAt(0);
ActiveArtifactDefinition second = artifacts.GetCatalogArtifactAt(2);
ActiveArtifactDefinition third = artifacts.GetCatalogArtifactAt(4);
Assert.That(artifacts.TryAddArtifact(first), Is.True);
yield return null;
Assert.That(slotIcons[0].enabled, Is.True);
Assert.That(slotIcons[1].enabled, Is.False);
Assert.That(slotIcons[2].enabled, Is.False);
Assert.That(name.text, Is.EqualTo(first.DisplayName));
Assert.That(artifacts.TryAddArtifact(second), Is.True);
yield return null;
Assert.That(slotIcons[0].enabled, Is.True);
Assert.That(slotIcons[1].enabled, Is.True);
Assert.That(slotIcons[2].enabled, Is.False);
Assert.That(artifacts.TryAddArtifact(third), Is.True);
yield return null;
Assert.That(slotIcons[0].enabled, Is.True);
Assert.That(slotIcons[1].enabled, Is.True);
Assert.That(slotIcons[2].enabled, Is.True);
Assert.That(artifacts.SelectNext(), Is.True);
yield return null;
Assert.That(name.text, Is.EqualTo(second.DisplayName));
}
[UnityTest]
public IEnumerator PlayerDamagePopup_ReportsActualLethalHealthLoss()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
PlayerHealth health = Object.FindAnyObjectByType<PlayerHealth>();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
Font galmuri = Resources.Load<Font>("Presentation/Fonts/Galmuri9");
float healthBefore = health.CurrentHealth;
float reportedDamage = -1f;
health.OnDamageTaken += damage => reportedDamage = damage;
Assert.That(
health.TryTakeDamage(
healthBefore + 25f,
Vector2.right,
0f),
Is.True);
Assert.That(reportedDamage, Is.EqualTo(healthBefore).Within(0.0001f));
Assert.That(health.CurrentHealth, Is.Zero);
yield return null;
Text popup = FindActiveDamagePopup();
Assert.That(popup, Is.Not.Null);
Assert.That(popup.text, Is.EqualTo($"-{Mathf.FloorToInt(healthBefore)}"));
Assert.That(popup.font, Is.SameAs(galmuri));
Assert.That(popup.fontSize, Is.EqualTo(30));
Assert.That(popup.color.r, Is.GreaterThan(popup.color.g));
Assert.That(
popup.rectTransform.position.y,
Is.GreaterThan(
Camera.main.WorldToScreenPoint(health.transform.position).y));
yield return new WaitForSecondsRealtime(0.7f);
Assert.That(GetPrivateField<int>(hud, "activeDamagePopupCount"), Is.Zero);
Assert.That(FindActiveDamagePopup(), Is.Null);
}
[UnityTest]
public IEnumerator PlayerDamagePopup_SkipsInvulnerabilityAndHealing()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
PlayerHealth health = Object.FindAnyObjectByType<PlayerHealth>();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
int eventCount = 0;
health.OnDamageTaken += _ => eventCount++;
health.GrantInvulnerability(10f);
Assert.That(
health.TryTakeDamage(10f, Vector2.right, 0f),
Is.False);
health.Heal(20f);
yield return new WaitForSecondsRealtime(0.1f);
Assert.That(eventCount, Is.Zero);
Assert.That(GetPrivateField<int>(hud, "activeDamagePopupCount"), Is.Zero);
Assert.That(FindActiveDamagePopup(), Is.Null);
}
[UnityTest]
public IEnumerator DamagePopupPool_UsesSharedCapAndReturnsAllEntries()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
Text popupTemplate = GetPrivateField<Text>(hud, "damagePopupTemplate");
Transform popupParent = popupTemplate.transform.parent;
CombatHitResult result = new(
player.gameObject,
player.gameObject,
false,
HitSide.Front,
4f,
Vector2.right,
0f,
player.transform.position,
showsHitFeedback: false);
for (int i = 0; i < 16; i++)
{
CombatEvents.RaiseValidHit(result);
}
Assert.That(
GetPrivateField<int>(hud, "activeDamagePopupCount"),
Is.EqualTo(12));
PlayerHealth health = Object.FindAnyObjectByType<PlayerHealth>();
Assert.That(health.TryTakeDamage(1f, Vector2.right, 0f), Is.True);
Assert.That(
GetPrivateField<int>(hud, "activeDamagePopupCount"),
Is.EqualTo(12));
yield return new WaitForSecondsRealtime(0.7f);
Assert.That(GetPrivateField<int>(hud, "activeDamagePopupCount"), Is.Zero);
Assert.That(FindActiveDamagePopup(), Is.Null);
int popupChildCountBeforeToggle = popupParent.childCount;
hud.gameObject.SetActive(false);
yield return null;
Assert.That(GetPrivateField<int>(hud, "activeDamagePopupCount"), Is.Zero);
Assert.That(FindActiveDamagePopup(), Is.Null);
Assert.That(
popupParent.childCount,
Is.LessThanOrEqualTo(popupChildCountBeforeToggle));
hud.gameObject.SetActive(true);
yield return null;
yield return new WaitForSeconds(health.InvulnerabilityDuration + 0.05f);
Assert.That(health.TryTakeDamage(1f, Vector2.right, 0f), Is.True);
yield return null;
Assert.That(FindActiveDamagePopup(), Is.Not.Null);
hud.gameObject.SetActive(false);
yield return null;
Assert.That(
popupParent.childCount,
Is.LessThanOrEqualTo(popupChildCountBeforeToggle));
}
[UnityTest]
public IEnumerator DamagePopup_UsesArtifactIdentityPaletteForNormalChargedAndLegacyHits()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
GameObject target = new("Artifact Popup Palette Target");
target.transform.position = player.transform.position + Vector3.right;
MethodInfo handleValidHit = typeof(RunHUD).GetMethod(
"HandleValidHit",
BindingFlags.Instance | BindingFlags.NonPublic);
ActiveArtifactEffect[] effects =
{
ActiveArtifactEffect.Dash,
ActiveArtifactEffect.Pulse,
ActiveArtifactEffect.Phoenix,
ActiveArtifactEffect.Cyclone,
ActiveArtifactEffect.ThunderCrash,
ActiveArtifactEffect.ChainLightning,
};
string[] sourceIds =
{
"dash", "pulse", "phoenix", "cyclone", "thunder_crash",
"chain_lightning",
};
Color32[] expectedColors =
{
new(103, 231, 178, 255),
new(255, 100, 100, 255),
new(255, 100, 100, 255),
new(103, 231, 178, 255),
new(104, 179, 255, 255),
new(104, 179, 255, 255),
};
string[] expectedNames =
{
"척후의 장화",
"군주의 방패",
"잿불의 마검",
"바람깃 브로치",
"뇌격의 수갑",
"창뢰의 보주",
};
for (int group = 0; group < 3; group++)
{
bool isArtifactHit = group < 2;
bool charged = group == 1;
for (int i = 0; i < effects.Length; i++)
{
// The charged Arc case intentionally carries a stale
// legacy source id; the actual effect in the hit result
// must take priority whenever it is available.
string sourceId = charged && i == 5
? "pulse"
: sourceIds[i];
CombatHitResult result = new(
player.gameObject,
target,
false,
HitSide.Back,
100f + group * 10f + i,
Vector2.right,
0f,
target.transform.position,
sourceId: sourceId,
usesPositionBonus: true,
showsHitFeedback: false,
launchSucceeded: charged,
isArtifactHit: isArtifactHit,
artifactEffect: effects[i],
isChargedArtifact: charged);
handleValidHit.Invoke(hud, new object[] { result });
}
for (int i = 0; i < effects.Length; i++)
{
int damage = 100 + group * 10 + i;
string sourceId = charged && i == 5
? "PULSE"
: sourceIds[i].ToUpperInvariant();
string displayName = charged && i == 5
? "군주의 방패"
: expectedNames[i];
string expectedText = charged
? $"LAUNCH {displayName} {damage}"
: $"{displayName} {damage}";
Text popup = FindActiveDamagePopupByText(expectedText);
Assert.That(popup, Is.Not.Null, expectedText);
AssertPopupColor(popup, expectedColors[i]);
}
yield return new WaitForSecondsRealtime(0.7f);
Assert.That(
GetPrivateField<int>(hud, "activeDamagePopupCount"),
Is.Zero);
}
HitSide[] ordinarySides =
{
HitSide.Front,
HitSide.Side,
HitSide.Back,
};
string[] ordinaryTexts =
{
"42",
"SIDE ×1.2 43",
"BACK ×1.5 44",
};
Color32[] ordinaryColors =
{
new(255, 255, 255, 255),
new(166, 255, 76, 255),
new(255, 191, 51, 255),
};
for (int i = 0; i < ordinarySides.Length; i++)
{
CombatHitResult ordinary = new(
player.gameObject,
target,
false,
ordinarySides[i],
42f + i,
Vector2.right,
0f,
target.transform.position,
showsHitFeedback: false);
handleValidHit.Invoke(hud, new object[] { ordinary });
Text ordinaryPopup = FindActiveDamagePopupByText(ordinaryTexts[i]);
Assert.That(ordinaryPopup, Is.Not.Null, ordinaryTexts[i]);
AssertPopupColor(ordinaryPopup, ordinaryColors[i]);
yield return new WaitForSecondsRealtime(0.7f);
}
CombatHitResult fractionalDamage = new(
player.gameObject,
target,
false,
HitSide.Front,
14.5f,
Vector2.right,
0f,
target.transform.position,
showsHitFeedback: false);
handleValidHit.Invoke(hud, new object[] { fractionalDamage });
Text hitDebugText = GetPrivateField<Text>(hud, "hitDebugText");
Assert.That(hitDebugText.text, Is.EqualTo("FRONT 14"));
Assert.That(FindActiveDamagePopupByText("14"), Is.Not.Null);
Assert.That(FindActiveDamagePopupByText("14.5"), Is.Null);
yield return new WaitForSecondsRealtime(0.7f);
CombatHitResult unknownSource = new(
player.gameObject,
target,
false,
HitSide.Front,
45f,
Vector2.right,
0f,
target.transform.position,
sourceId: "unknown_source",
showsHitFeedback: false);
handleValidHit.Invoke(hud, new object[] { unknownSource });
Text unknownPopup = FindActiveDamagePopupByText("UNKNOWN_SOURCE 45");
Assert.That(unknownPopup, Is.Not.Null);
AssertPopupColor(unknownPopup, new Color32(255, 255, 255, 255));
yield return new WaitForSecondsRealtime(0.7f);
Object.Destroy(target);
}
[UnityTest]
public IEnumerator DamagePopup_LongArtifactLabelsStayOnOneVisibleLine()
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
ActiveArtifactController artifacts =
Object.FindAnyObjectByType<ActiveArtifactController>();
ActiveArtifactDefinition chainLightning = null;
for (int i = 0; i < artifacts.CatalogCount; i++)
{
ActiveArtifactDefinition definition =
artifacts.GetCatalogArtifactAt(i);
if (definition != null
&& string.Equals(
definition.ArtifactId,
"chain_lightning",
System.StringComparison.OrdinalIgnoreCase))
{
chainLightning = definition;
break;
}
}
Assert.That(chainLightning, Is.Not.Null);
string previousChainLightningName = chainLightning.DisplayName;
SetPrivateField(chainLightning, "displayName", "CHAIN_LIGHTNING");
GameObject target = new("Long Artifact Popup Target");
target.transform.position = player.transform.position + Vector3.right;
MethodInfo handleValidHit = typeof(RunHUD).GetMethod(
"HandleValidHit",
BindingFlags.Instance | BindingFlags.NonPublic);
try
{
CombatHitResult normal = new(
player.gameObject,
target,
false,
HitSide.Front,
192f,
Vector2.right,
0f,
target.transform.position,
sourceId: "chain_lightning",
showsHitFeedback: false,
isArtifactHit: true,
artifactEffect: ActiveArtifactEffect.ChainLightning);
handleValidHit.Invoke(hud, new object[] { normal });
Text normalPopup = FindActiveDamagePopupByText("CHAIN_LIGHTNING 192");
AssertPopupIsSingleVisibleLine(normalPopup, "CHAIN_LIGHTNING 192");
yield return new WaitForSecondsRealtime(0.7f);
CombatHitResult charged = new(
player.gameObject,
target,
false,
HitSide.Front,
192f,
Vector2.right,
0f,
target.transform.position,
sourceId: "chain_lightning",
showsHitFeedback: false,
launchSucceeded: true,
isArtifactHit: true,
artifactEffect: ActiveArtifactEffect.ChainLightning,
isChargedArtifact: true);
handleValidHit.Invoke(hud, new object[] { charged });
Text chargedPopup = FindActiveDamagePopupByText(
"LAUNCH CHAIN_LIGHTNING 192");
AssertPopupIsSingleVisibleLine(
chargedPopup,
"LAUNCH CHAIN_LIGHTNING 192");
yield return new WaitForSecondsRealtime(0.7f);
}
finally
{
SetPrivateField(
chainLightning,
"displayName",
previousChainLightningName);
Object.Destroy(target);
}
}
private static T GetPrivateField<T>(object target, string fieldName)
{
return (T)target.GetType()
.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(target);
}
private static void SetPrivateField<T>(
object target,
string fieldName,
T value)
{
target.GetType()
.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(target, value);
}
private static Text FindActiveDamagePopup()
{
Text[] texts = Object.FindObjectsByType<Text>(
FindObjectsInactive.Include,
FindObjectsSortMode.None);
for (int i = 0; i < texts.Length; i++)
{
if (texts[i].gameObject.activeInHierarchy
&& !string.IsNullOrEmpty(texts[i].text)
&& texts[i].text[0] == '-')
{
return texts[i];
}
}
return null;
}
private static Text FindActiveDamagePopupByText(string expectedText)
{
Text[] texts = Object.FindObjectsByType<Text>(
FindObjectsInactive.Include,
FindObjectsSortMode.None);
for (int i = 0; i < texts.Length; i++)
{
if (texts[i].gameObject.activeInHierarchy
&& texts[i].GetComponent<Outline>() != null
&& texts[i].text == expectedText)
{
return texts[i];
}
}
return null;
}
private static void AssertPopupColor(Text popup, Color32 expected)
{
Assert.That(popup.color.r, Is.EqualTo(expected.r / 255f).Within(0.002f));
Assert.That(popup.color.g, Is.EqualTo(expected.g / 255f).Within(0.002f));
Assert.That(popup.color.b, Is.EqualTo(expected.b / 255f).Within(0.002f));
}
private static void AssertPopupIsSingleVisibleLine(
Text popup,
string expectedText)
{
Assert.That(popup, Is.Not.Null, expectedText);
Canvas.ForceUpdateCanvases();
Assert.That(popup.text, Is.EqualTo(expectedText));
Assert.That(popup.resizeTextForBestFit, Is.False);
Assert.That(
popup.horizontalOverflow,
Is.EqualTo(HorizontalWrapMode.Overflow));
Assert.That(
popup.verticalOverflow,
Is.EqualTo(VerticalWrapMode.Overflow));
TextGenerator generator = popup.cachedTextGenerator;
Assert.That(generator.lineCount, Is.EqualTo(1), expectedText);
Assert.That(
generator.characterCountVisible,
Is.EqualTo(expectedText.Length),
expectedText);
Assert.That(
popup.rectTransform.rect.width,
Is.GreaterThanOrEqualTo(popup.preferredWidth - 0.5f),
expectedText);
}
private static void AssertRect(
RectTransform rect,
Vector2 anchor,
Vector2 pivot,
Vector2 anchoredPosition,
Vector2 size)
{
Assert.That(rect.anchorMin, Is.EqualTo(anchor));
Assert.That(rect.anchorMax, Is.EqualTo(anchor));
Assert.That(rect.pivot, Is.EqualTo(pivot));
Assert.That(rect.anchoredPosition, Is.EqualTo(anchoredPosition));
Assert.That(rect.rect.size, Is.EqualTo(size));
}
private static bool RectanglesOverlap(RectTransform first, RectTransform second)
{
return GetWorldRect(first).Overlaps(GetWorldRect(second));
}
private static void AssertRectInsideCanvas(
RectTransform target,
RectTransform canvas)
{
Rect targetRect = GetWorldRect(target);
Rect canvasRect = GetWorldRect(canvas);
const float tolerance = 1f;
Assert.That(targetRect.xMin, Is.GreaterThanOrEqualTo(canvasRect.xMin - tolerance));
Assert.That(targetRect.xMax, Is.LessThanOrEqualTo(canvasRect.xMax + tolerance));
Assert.That(targetRect.yMin, Is.GreaterThanOrEqualTo(canvasRect.yMin - tolerance));
Assert.That(targetRect.yMax, Is.LessThanOrEqualTo(canvasRect.yMax + tolerance));
}
private static Rect GetWorldRect(RectTransform rect)
{
Vector3[] corners = new Vector3[4];
rect.GetWorldCorners(corners);
float minX = corners[0].x;
float maxX = corners[0].x;
float minY = corners[0].y;
float maxY = corners[0].y;
for (int i = 1; i < corners.Length; i++)
{
minX = Mathf.Min(minX, corners[i].x);
maxX = Mathf.Max(maxX, corners[i].x);
minY = Mathf.Min(minY, corners[i].y);
maxY = Mathf.Max(maxY, corners[i].y);
}
return Rect.MinMaxRect(minX, minY, maxX, maxY);
}
private static IEnumerator AssertScorchingRayAlignment(bool charged)
{
for (int i = 0; i < Directions.Length; i++)
{
yield return LoadCombatPrototype();
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
director.enabled = false;
director.DebugSpawnImmediate(2);
yield return null;
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
Rigidbody2D playerBody = player.GetComponent<Rigidbody2D>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
Vector2 direction = Directions[i];
SetPlayerDirection(player, direction);
playerBody.position = new Vector2(1.25f, -0.75f);
EnemyController[] enemies = Object.FindObjectsByType<EnemyController>(
FindObjectsSortMode.InstanceID);
Assert.That(enemies.Length, Is.GreaterThanOrEqualTo(2));
for (int enemyIndex = 2; enemyIndex < enemies.Length; enemyIndex++)
{
enemies[enemyIndex].gameObject.SetActive(false);
}
ActiveArtifactDefinition definition = SelectArtifact(
artifacts,
ActiveArtifactEffect.Phoenix);
float range = charged
? definition.ChargedRange
: definition.NormalRange;
float width = charged
? definition.ChargedWidth
: definition.NormalWidth;
Vector2 perpendicular = new Vector2(-direction.y, direction.x);
PlaceEnemy(
enemies[0],
playerBody.position + direction * (range * 0.5f));
PlaceEnemy(
enemies[1],
playerBody.position
+ direction * (range * 0.5f)
+ perpendicular * (width * 0.5f + 0.06f));
AddOverlappingCollider(enemies[0]);
AddOverlappingCollider(enemies[1]);
Physics2D.SyncTransforms();
EnsureGauge(
artifacts,
charged
? definition.ChargedGaugeCost
: definition.NormalGaugeCost);
List<CombatHitResult> hits = new();
System.Action<CombatHitResult> handler = result =>
{
if (result.SourceId == definition.ArtifactId)
{
hits.Add(result);
}
};
CombatEvents.OnValidHit += handler;
Assert.That(artifacts.TryUseCurrent(charged), Is.True);
GameObject visual = GameObject.Find(
charged ? "Charged Scorching Ray" : "Scorching Ray");
Assert.That(visual, Is.Not.Null);
Assert.That(
Vector2.Distance((Vector2)visual.transform.position, playerBody.position),
Is.LessThan(0.0001f));
Assert.That(
Mathf.Abs(Mathf.DeltaAngle(
visual.transform.eulerAngles.z,
Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg)),
Is.LessThan(0.001f));
SpriteRenderer renderer = visual.GetComponent<SpriteRenderer>();
Assert.That(renderer, Is.Not.Null);
Assert.That(renderer.transform.localScale, Is.EqualTo(Vector3.one));
Assert.That(renderer.sprite.rect.width, Is.EqualTo(charged ? 256f : 128f));
Assert.That(renderer.sprite.rect.height, Is.EqualTo(charged ? 128f : 64f));
Assert.That(
renderer.sprite.pivot,
Is.EqualTo(charged ? new Vector2(16f, 64f) : new Vector2(8f, 32f)));
Vector2 bodyStart = renderer.transform.TransformPoint(Vector3.zero);
Vector2 bodyEnd = renderer.transform.TransformPoint(
new Vector3(range, 0f, 0f));
Assert.That(
Vector2.Distance(bodyStart, playerBody.position),
Is.LessThan(0.0001f));
Assert.That(
Vector2.Distance(
bodyEnd,
playerBody.position + direction * range),
Is.LessThan(0.0001f));
Vector2 bodyTop = renderer.transform.TransformPoint(
new Vector3(0f, width * 0.5f, 0f));
Vector2 bodyBottom = renderer.transform.TransformPoint(
new Vector3(0f, -width * 0.5f, 0f));
Assert.That(
Vector2.Distance(bodyTop, playerBody.position + new Vector2(
-direction.y * width * 0.5f,
direction.x * width * 0.5f)),
Is.LessThan(0.0001f));
Assert.That(
Vector2.Distance(bodyBottom, playerBody.position - new Vector2(
-direction.y * width * 0.5f,
direction.x * width * 0.5f)),
Is.LessThan(0.0001f));
Assert.That(hits.Count, Is.EqualTo(1));
Assert.That(hits[0].Target, Is.EqualTo(enemies[0].gameObject));
CombatEvents.OnValidHit -= handler;
Time.timeScale = 1f;
yield return new WaitForSecondsRealtime(0.2f);
}
}
private static IEnumerator LoadCombatPrototype()
{
AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype");
while (!load.isDone)
{
yield return null;
}
yield return null;
yield return null;
}
private static ActiveArtifactDefinition SelectArtifact(
ActiveArtifactController artifacts,
ActiveArtifactEffect effect)
{
for (int i = 0; i < artifacts.OwnedArtifactCount; i++)
{
if (artifacts.CurrentArtifact != null
&& artifacts.CurrentArtifact.Effect == effect)
{
return artifacts.CurrentArtifact;
}
Assert.That(artifacts.SelectNext(), Is.True);
}
Assert.Fail($"Artifact {effect} was not found in the debug catalog.");
return null;
}
private static void EnsureGauge(
ActiveArtifactController artifacts,
float required)
{
while (artifacts.CurrentGauge < required)
{
artifacts.AddMovementCharge(1f);
}
}
private static void SetPlayerDirection(
PlayerController player,
Vector2 direction)
{
typeof(PlayerController)
.GetField(
"<FacingDirection>k__BackingField",
BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(player, direction);
typeof(PlayerController)
.GetField(
"<MoveDirection>k__BackingField",
BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(player, direction);
}
private static void PlaceEnemy(
EnemyController enemy,
Vector2 position)
{
enemy.enabled = false;
Rigidbody2D body = enemy.GetComponent<Rigidbody2D>();
body.linearVelocity = Vector2.zero;
body.position = position;
}
private static void SetEnemyHealth(
EnemyController enemy,
float health)
{
typeof(EnemyController)
.GetProperty("CurrentHealth")
.SetValue(enemy, health);
Assert.That(enemy.CurrentHealth, Is.EqualTo(health));
}
private static void AddOverlappingCollider(EnemyController enemy)
{
GameObject child = new("Alignment Overlap Collider");
child.transform.SetParent(enemy.transform, false);
BoxCollider2D collider = child.AddComponent<BoxCollider2D>();
collider.isTrigger = true;
collider.size = new Vector2(0.6f, 0.6f);
}
private static void AssertUniformScale(Vector3 actual, float expected)
{
Assert.That(actual.x, Is.EqualTo(expected).Within(0.0001f));
Assert.That(actual.y, Is.EqualTo(expected).Within(0.0001f));
Assert.That(actual.z, Is.EqualTo(expected).Within(0.0001f));
}
}
}