convert game playing..

This commit is contained in:
2026-09-23 12:53:52 +09:00
parent 37c7942b71
commit 5d11288a6f
136 changed files with 15856 additions and 299 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f16c4a8dfe44f9e8c455a2e24ad7281
@@ -1,15 +1,18 @@
using System.Collections;
using System.Reflection;
using BumpCombat.Constants;
using BumpCombat.Core;
using BumpCombat.Enemies;
using BumpCombat.Player;
using BumpCombat.Progression;
using BumpCombat.Spawning;
using BumpCombat.Stage;
using BumpCombat.UI;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using UnityEngine.UI;
namespace BumpCombat.PlayModeTests
{
@@ -298,6 +301,142 @@ namespace BumpCombat.PlayModeTests
}
}
[UnityTest]
public IEnumerator RoomScaleComparison_UsesMatchedCameraAndMinimapForOriginalAndExpanded()
{
const int referenceSeed = 20260922;
RunConstants constants = GameplayConstants.Current.Run;
RoomScaleComparisonMode previousMode = constants.Stage1FirstCombatRoomScaleComparison;
UnityEngine.Random.State previousRandomState = UnityEngine.Random.state;
float previousTimeScale = Time.timeScale;
bool previousProductionMode = RunManager.ForceProductionModeForTests;
string captureDirectory = System.Environment.GetEnvironmentVariable(
"BUMPCOMBAT_COMPARISON_CAPTURE_DIRECTORY");
try
{
constants.Stage1FirstCombatRoomScaleComparison = RoomScaleComparisonMode.Original;
yield return LoadAndStartRun();
StageManager stageManager = Object.FindAnyObjectByType<StageManager>();
Camera camera = Camera.main;
Assert.That(stageManager, Is.Not.Null);
Assert.That(camera, Is.Not.Null);
RoomScaleComparisonMode[] modes =
{
RoomScaleComparisonMode.Original,
RoomScaleComparisonMode.Expanded,
};
for (int modeIndex = 0; modeIndex < modes.Length; modeIndex++)
{
RoomScaleComparisonMode mode = modes[modeIndex];
constants.Stage1FirstCombatRoomScaleComparison = mode;
stageManager.StartRun(referenceSeed);
yield return new WaitForSecondsRealtime(0.4f);
StageNode firstCombat = FindFirstCombat(stageManager.CurrentPlan);
Assert.That(firstCombat, Is.Not.Null);
Assert.That(firstCombat.PresetId,
Is.EqualTo(mode == RoomScaleComparisonMode.Expanded
? LevelPresetCatalog.S1AComparisonExpandedPresetId
: "GRAVE_S1_COMBAT_A"));
StageDirection entryDirection = GetEntryDirection(
stageManager.CurrentPlan.StartNode,
firstCombat);
// Reset immediately before the same room activation so the
// encounter's UnityEngine.Random group rolls match A/B.
UnityEngine.Random.InitState(referenceSeed);
stageManager.RequestZoneTransition(firstCombat.Id, entryDirection);
float deadline = Time.realtimeSinceStartup + 4f;
while (stageManager.CurrentZone == null
|| stageManager.CurrentZone.NodeId != firstCombat.Id
|| stageManager.IsTransitioning)
{
Assert.That(Time.realtimeSinceStartup, Is.LessThan(deadline),
"The comparison fixture did not reach the first combat room.");
yield return null;
}
// CoTransitionZone sets CurrentZone before its doorway
// hold. Wait for ActivateZone and encounter spawn to
// finish before inspecting or capturing the room.
yield return new WaitForSecondsRealtime(0.4f);
ZoneInstance zone = stageManager.CurrentZone;
Assert.That(stageManager.IsTransitioning, Is.False);
AssertReferenceEncounter(
zone,
mode == RoomScaleComparisonMode.Expanded ? 6 : 3,
mode == RoomScaleComparisonMode.Expanded ? 4 : 2);
float expectedOrtho = mode == RoomScaleComparisonMode.Expanded
? constants.StageComparisonCameraViewHalfExtents.y
: constants.StageComparisonOriginalCameraViewHalfExtents.y;
Assert.That(zone.Preset.GridWidth,
Is.EqualTo(mode == RoomScaleComparisonMode.Expanded ? 36 : 18));
Assert.That(zone.Preset.GridHeight,
Is.EqualTo(mode == RoomScaleComparisonMode.Expanded ? 24 : 12));
PlayerController comparisonPlayer =
Object.FindAnyObjectByType<PlayerController>();
Vector2 returnPosition = comparisonPlayer.transform.position;
Vector2 explorationPosition = returnPosition;
bool foundUnseenCell = false;
for (int y = 0; y < zone.Preset.GridHeight && !foundUnseenCell; y++)
{
for (int x = 0; x < zone.Preset.GridWidth; x++)
{
if (zone.GetRuntimeCell(x, y) == PresetCellType.Empty
|| zone.IsCellVisible(x, y))
{
continue;
}
explorationPosition = zone.Preset.CellToWorldPosition(
zone.ZoneCenter,
new Vector2Int(x, y));
foundUnseenCell = true;
break;
}
}
Assert.That(foundUnseenCell, Is.True,
"The comparison room must have an unseen terrain cell.");
Assert.That(comparisonPlayer.TryReposition(explorationPosition), Is.True);
zone.UpdateVisibility(explorationPosition);
yield return null;
Assert.That(comparisonPlayer.TryReposition(returnPosition), Is.True);
zone.UpdateVisibility(returnPosition);
yield return null;
camera.Render();
Assert.That(camera.orthographicSize, Is.EqualTo(expectedOrtho).Within(0.01f));
AssertPixelPerfectReference(
camera,
mode == RoomScaleComparisonMode.Expanded ? 480 : 320,
mode == RoomScaleComparisonMode.Expanded ? 270 : 180);
AssertMinimapTerrainMatches(zone);
if (!string.IsNullOrWhiteSpace(captureDirectory))
{
CaptureWorldOnlyCamera(
camera,
captureDirectory,
mode == RoomScaleComparisonMode.Expanded ? "expanded" : "original");
}
}
}
finally
{
constants.Stage1FirstCombatRoomScaleComparison = previousMode;
UnityEngine.Random.state = previousRandomState;
Time.timeScale = previousTimeScale;
RunManager.ForceProductionModeForTests = previousProductionMode;
}
}
private static IEnumerator DestroyActiveEnemies()
{
EnemyController[] enemies = Object.FindObjectsByType<EnemyController>(
@@ -313,6 +452,184 @@ namespace BumpCombat.PlayModeTests
yield return null;
}
private static StageNode FindFirstCombat(StagePlan plan)
{
StageNode entrance = plan.StartNode;
for (int i = 0; i < plan.Nodes.Count; i++)
{
StageNode node = plan.Nodes[i];
if (node.NodeType != StageNodeType.Combat)
{
continue;
}
for (int direction = 0; direction < 4; direction++)
{
if (entrance.GetConnection((StageDirection)direction) == node.Id)
{
return node;
}
}
}
return null;
}
private static StageDirection GetEntryDirection(StageNode entrance, StageNode target)
{
for (int direction = 0; direction < 4; direction++)
{
StageDirection sourceDirection = (StageDirection)direction;
if (entrance.GetConnection(sourceDirection) == target.Id)
{
return sourceDirection.Opposite();
}
}
return StageDirection.West;
}
private static void AssertPixelPerfectReference(
Camera camera,
int expectedWidth,
int expectedHeight)
{
Component pixelPerfect = camera.GetComponent(
"UnityEngine.Rendering.Universal.PixelPerfectCamera");
Assert.That(pixelPerfect, Is.Not.Null);
PropertyInfo referenceWidth = pixelPerfect.GetType().GetProperty("refResolutionX");
PropertyInfo referenceHeight = pixelPerfect.GetType().GetProperty("refResolutionY");
Assert.That(referenceWidth.GetValue(pixelPerfect), Is.EqualTo(expectedWidth));
Assert.That(referenceHeight.GetValue(pixelPerfect), Is.EqualTo(expectedHeight));
}
private static void AssertMinimapTerrainMatches(ZoneInstance zone)
{
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
Assert.That(hud, Is.Not.Null);
FieldInfo terrainField = typeof(RunHUD).GetField(
"minimapTerrainRoot",
BindingFlags.Instance | BindingFlags.NonPublic);
RectTransform terrainRoot = terrainField?.GetValue(hud) as RectTransform;
Assert.That(terrainRoot, Is.Not.Null);
int visibleCellCount = 0;
for (int y = 0; y < zone.Preset.GridHeight; y++)
{
for (int x = 0; x < zone.Preset.GridWidth; x++)
{
if (zone.GetRuntimeCell(x, y) != PresetCellType.Empty)
{
visibleCellCount++;
}
}
}
Assert.That(terrainRoot.childCount, Is.EqualTo(visibleCellCount));
Assert.That(zone.VisibilityRevision, Is.GreaterThan(0));
int darkCellCount = 0;
int dimCellCount = 0;
for (int y = 0; y < zone.Preset.GridHeight; y++)
{
for (int x = 0; x < zone.Preset.GridWidth; x++)
{
if (zone.GetRuntimeCell(x, y) == PresetCellType.Empty)
{
continue;
}
Image cell = terrainRoot.Find($"Terrain {x},{y}")?.GetComponent<Image>();
Assert.That(cell, Is.Not.Null);
Color32 color = cell.color;
if (zone.IsCellVisible(x, y))
{
Assert.That(color, Is.Not.EqualTo(new Color32(2, 4, 5, 255)));
}
else if (zone.IsCellExplored(x, y))
{
dimCellCount++;
Assert.That(color.a, Is.EqualTo(255));
}
else
{
darkCellCount++;
Assert.That(color, Is.EqualTo(new Color32(2, 4, 5, 255)));
}
}
}
Assert.That(dimCellCount, Is.GreaterThan(0));
Assert.That(darkCellCount, Is.GreaterThan(0));
}
private static void AssertReferenceEncounter(
ZoneInstance zone,
int expectedBatCount,
int expectedSkeletonCount)
{
int batCount = 0;
int skeletonCount = 0;
for (int i = 0; i < zone.SpawnedEnemies.Count; i++)
{
EnemyController enemy = zone.SpawnedEnemies[i];
if (enemy == null)
{
continue;
}
if (enemy.name.StartsWith("Bat", System.StringComparison.Ordinal))
{
batCount++;
}
else if (enemy.name.StartsWith("Skeleton", System.StringComparison.Ordinal))
{
skeletonCount++;
}
}
Assert.That(batCount, Is.EqualTo(expectedBatCount));
Assert.That(skeletonCount, Is.EqualTo(expectedSkeletonCount));
}
private static void CaptureWorldOnlyCamera(
Camera camera,
string directory,
string variant)
{
System.IO.Directory.CreateDirectory(directory);
RenderTexture previousTarget = camera.targetTexture;
RenderTexture previousActive = RenderTexture.active;
RenderTexture target = new(640, 360, 24, RenderTextureFormat.ARGB32);
Texture2D snapshot = null;
try
{
target.Create();
camera.targetTexture = target;
camera.Render();
RenderTexture.active = target;
snapshot = new Texture2D(640, 360, TextureFormat.RGB24, false);
snapshot.ReadPixels(new Rect(0f, 0f, 640f, 360f), 0, 0);
snapshot.Apply();
string path = System.IO.Path.Combine(
directory,
$"s1a-{variant}-world.png");
System.IO.File.WriteAllBytes(path, snapshot.EncodeToPNG());
}
finally
{
camera.targetTexture = previousTarget;
RenderTexture.active = previousActive;
if (snapshot != null)
{
Object.Destroy(snapshot);
}
target.Release();
Object.Destroy(target);
}
}
private static EnemyController FindRangedPrefab(SpawnDirector director)
{
FieldInfo field = typeof(SpawnDirector).GetField(
@@ -15,6 +15,21 @@ namespace BumpCombat.PlayModeTests
private static readonly BindingFlags NonPublicInstance =
BindingFlags.Instance | BindingFlags.NonPublic;
private float previousTimeScale;
[SetUp]
public void SetUp()
{
previousTimeScale = Time.timeScale;
Time.timeScale = 1f;
}
[TearDown]
public void TearDown()
{
Time.timeScale = previousTimeScale;
}
[UnityTest]
public IEnumerator ExperienceOrb_UsesChangedPickupRadius()
{
@@ -40,6 +40,10 @@ namespace BumpCombat.PlayModeTests
LancerThrustVisual visual = lancer.GetComponent<LancerThrustVisual>();
SpriteRenderer source = lancer.GetComponent<SpriteRenderer>();
lancer.enabled = false;
Rigidbody2D lancerBody = lancer.GetComponent<Rigidbody2D>();
lancerBody.linearVelocity = Vector2.zero;
lancerBody.angularVelocity = 0f;
lancerBody.constraints = RigidbodyConstraints2D.FreezeAll;
Vector2[] directions =
{
@@ -740,7 +740,7 @@ namespace BumpCombat.PlayModeTests
Assert.That(keyboard.escapeKey.isPressed, Is.False);
}
private static void CaptureAtResolution(
internal static void CaptureAtResolution(
Canvas canvas,
RectTransform panelRect,
RectTransform finalMissionRow,
@@ -174,6 +174,10 @@ namespace BumpCombat.PlayModeTests
submenuLabels.Add("개발용 시작");
}
submenuLabels.Add("기본 모드");
if (showDevelopment)
{
submenuLabels.Add("방어전 테스트");
}
submenuLabels.Add("챌린지 모드(준비 중)");
submenuLabels.Add("하드코어 모드(준비 중)");
submenuLabels.Add("뒤로");
@@ -196,7 +200,7 @@ namespace BumpCombat.PlayModeTests
label.text);
}
int placeholderIndex = showDevelopment ? 2 : 1;
int placeholderIndex = showDevelopment ? 3 : 1;
for (int i = placeholderIndex; i < placeholderIndex + 2; i++)
{
submenu.Find($"Button {i + 1}").GetComponent<Button>().onClick.Invoke();
@@ -212,12 +216,28 @@ namespace BumpCombat.PlayModeTests
EventSystem.current.currentSelectedGameObject
.GetComponentInChildren<Text>().text,
Is.EqualTo("기본 모드"));
if (showDevelopment)
{
yield return PressAndRelease(keyboard, Key.DownArrow);
Assert.That(
EventSystem.current.currentSelectedGameObject
.GetComponentInChildren<Text>().text,
Is.EqualTo("방어전 테스트"));
}
yield return PressAndRelease(keyboard, Key.DownArrow);
Assert.That(
EventSystem.current.currentSelectedGameObject
.GetComponentInChildren<Text>().text,
Is.EqualTo("뒤로"),
"Keyboard navigation skips disabled mode placeholders.");
if (showDevelopment)
{
yield return PressAndRelease(keyboard, Key.UpArrow);
Assert.That(
EventSystem.current.currentSelectedGameObject
.GetComponentInChildren<Text>().text,
Is.EqualTo("방어전 테스트"));
}
yield return PressAndRelease(keyboard, Key.UpArrow);
Assert.That(
EventSystem.current.currentSelectedGameObject
@@ -2,6 +2,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using BumpCombat.Combat;
using BumpCombat.Constants;
using BumpCombat.Core;
using BumpCombat.Enemies;
using BumpCombat.Player;
@@ -155,7 +156,186 @@ namespace BumpCombat.PlayModeTests
}
[UnityTest]
public IEnumerator MovingPlayer_BumpKillsPassThroughCrowdWithoutImpactRecoil()
public IEnumerator MovingPlayer_BumpFrontCollision_DefaultAndToggleControlPlayerDamage()
{
CombatConstants combatConstants = GameplayConstants.Current.Combat;
bool originalFrontalBumpSelfDamage =
combatConstants.EnableFrontalBumpSelfDamage;
EnemyController targetEnemy = null;
try
{
Assert.That(
originalFrontalBumpSelfDamage,
Is.False,
"CombatConstants default must preserve the removed front-bump self damage");
combatConstants.EnableFrontalBumpSelfDamage = false;
AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype");
while (!load.isDone)
{
yield return null;
}
yield return null;
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
Assert.That(director, Is.Not.Null);
director.enabled = false;
yield return null;
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
Assert.That(player, Is.Not.Null);
PlayerHealth playerHealth = player.GetComponent<PlayerHealth>();
Rigidbody2D playerBody = player.GetComponent<Rigidbody2D>();
BumpCombatResolver resolver = player.GetComponent<BumpCombatResolver>();
Assert.That(playerBody, Is.Not.Null);
Vector2 playerOrigin = playerBody.position;
FieldInfo enemyPrefabsField = typeof(SpawnDirector).GetField(
"enemyPrefabs",
BindingFlags.Instance | BindingFlags.NonPublic);
EnemyController[] enemyPrefabs =
enemyPrefabsField?.GetValue(director) as EnemyController[];
EnemyController skeletonPrefab = null;
if (enemyPrefabs != null)
{
foreach (EnemyController prefab in enemyPrefabs)
{
if (prefab != null
&& prefab.Definition != null
&& prefab.Definition.Kind == EnemyKind.Skeleton)
{
skeletonPrefab = prefab;
break;
}
}
}
Assert.That(skeletonPrefab, Is.Not.Null);
targetEnemy = Object.Instantiate(
skeletonPrefab.gameObject,
playerOrigin + Vector2.right * 0.5f,
Quaternion.identity).GetComponent<EnemyController>();
yield return null;
Assert.That(targetEnemy, Is.Not.Null);
Assert.That(targetEnemy.ContactDamage, Is.GreaterThan(0f));
Assert.That(targetEnemy.HasContactDamage, Is.False);
Assert.That(
EnemyController.AllowsContactDamage(
targetEnemy.State,
targetEnemy.AttackShape),
Is.False);
Collider2D enemyCollider = targetEnemy.GetComponent<Collider2D>();
Assert.That(
enemyCollider.isTrigger,
Is.False,
"Promoted enemy collider must be solid");
foreach (EnemyController enemy in
Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None))
{
if (enemy != targetEnemy)
{
enemy.enabled = false;
}
}
Assert.That(
targetEnemy.TryReposition(playerOrigin + Vector2.right * 0.5f),
Is.True);
SetAutoProperty(
targetEnemy,
"FacingDirection",
((Vector2)player.transform.position
- (Vector2)targetEnemy.transform.position).normalized);
targetEnemy.enabled = false;
Physics2D.SyncTransforms();
yield return new WaitForFixedUpdate();
Assert.That(RunManager.GameplayInputEnabled, Is.True);
float initialPlayerHealth = playerHealth.CurrentHealth;
float initialEnemyHealth = targetEnemy.CurrentHealth;
SetAutoProperty(player, "AttackIntentVelocity", Vector2.right * 3f);
SetAutoProperty(player, "MoveDirection", Vector2.right);
MethodInfo resolveNormalContact = typeof(BumpCombatResolver).GetMethod(
"TryResolveNormalContact",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(resolveNormalContact, Is.Not.Null);
resolveNormalContact.Invoke(
resolver,
new object[] { targetEnemy, Vector2.Lerp(playerBody.position, targetEnemy.transform.position, 0.5f) });
Assert.That(
targetEnemy.CurrentHealth,
Is.LessThan(initialEnemyHealth),
"Enemy must take damage from front bump");
Assert.That(
playerHealth.CurrentHealth,
Is.EqualTo(initialPlayerHealth),
"OFF must preserve no front-bump self damage");
Assert.That(
player.IsDamageKnockbackActive,
Is.False,
"OFF must preserve no front-bump damage knockback");
yield return new WaitForSeconds(0.3f);
Assert.That(
targetEnemy.TryReposition(playerBody.position + Vector2.right * 0.5f),
Is.True);
SetAutoProperty(
targetEnemy,
"FacingDirection",
((Vector2)player.transform.position
- (Vector2)targetEnemy.transform.position).normalized);
Physics2D.SyncTransforms();
yield return new WaitForFixedUpdate();
Assert.That(targetEnemy.ContactDamage, Is.GreaterThan(0f));
Assert.That(targetEnemy.HasContactDamage, Is.False);
Assert.That(
EnemyController.AllowsContactDamage(
targetEnemy.State,
targetEnemy.AttackShape),
Is.False);
combatConstants.EnableFrontalBumpSelfDamage = true;
SetAutoProperty(player, "AttackIntentVelocity", Vector2.right * 3f);
SetAutoProperty(player, "MoveDirection", Vector2.right);
float enabledPlayerHealth = playerHealth.CurrentHealth;
float enabledEnemyHealth = targetEnemy.CurrentHealth;
resolveNormalContact.Invoke(
resolver,
new object[] { targetEnemy, Vector2.Lerp(playerBody.position, targetEnemy.transform.position, 0.5f) });
Assert.That(
targetEnemy.CurrentHealth,
Is.LessThan(enabledEnemyHealth),
"ON must preserve enemy bump damage");
Assert.That(
playerHealth.CurrentHealth,
Is.LessThan(enabledPlayerHealth),
"ON must restore front-bump self damage");
Assert.That(
player.IsDamageKnockbackActive,
Is.True,
"ON must use normal PlayerHealth damage knockback");
LogAssert.NoUnexpectedReceived();
}
finally
{
combatConstants.EnableFrontalBumpSelfDamage = originalFrontalBumpSelfDamage;
if (targetEnemy != null)
{
Object.Destroy(targetEnemy.gameObject);
}
}
}
[UnityTest]
public IEnumerator MovingPlayer_BumpBackHit_DamagesEnemyWithoutPlayerDamage()
{
AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype");
while (!load.isDone)
@@ -170,39 +350,45 @@ namespace BumpCombat.PlayModeTests
yield return null;
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
Rigidbody2D playerBody = player.GetComponent<Rigidbody2D>();
PlayerHealth playerHealth = player.GetComponent<PlayerHealth>();
BumpCombatResolver resolver = player.GetComponent<BumpCombatResolver>();
EnemyController crowd = null;
EnemyController targetEnemy = null;
foreach (EnemyController enemy in
Object.FindObjectsByType<EnemyController>(
FindObjectsSortMode.None))
{
if (enemy.IsCrowd && !enemy.IsDead)
if (!enemy.IsDead)
{
crowd = enemy;
targetEnemy = enemy;
break;
}
}
Assert.That(crowd, Is.Not.Null);
Collider2D crowdCollider = crowd.GetComponent<Collider2D>();
Assert.That(crowdCollider.isTrigger, Is.True);
playerBody.position = Vector2.zero;
Assert.That(crowd.TryReposition(new Vector2(0.2f, 0f)), Is.True);
Assert.That(targetEnemy, Is.Not.Null);
targetEnemy.TryReposition(new Vector2(0.5f, 0f));
SetAutoProperty(targetEnemy, "FacingDirection", Vector2.right);
Physics2D.SyncTransforms();
yield return new WaitForFixedUpdate();
float initialPlayerHealth = playerHealth.CurrentHealth;
float initialEnemyHealth = targetEnemy.CurrentHealth;
SetAutoProperty(player, "AttackIntentVelocity", Vector2.right * 3f);
SetAutoProperty(player, "MoveDirection", Vector2.right);
typeof(BumpCombatResolver)
.GetMethod(
"OnTriggerEnter2D",
BindingFlags.Instance | BindingFlags.NonPublic)
?.Invoke(resolver, new object[] { crowdCollider });
Assert.That(crowd.IsDead, Is.True);
yield return new WaitForFixedUpdate();
Assert.That(playerBody.position.x, Is.EqualTo(0f).Within(0.01f));
MethodInfo resolveNormalContact = typeof(BumpCombatResolver).GetMethod(
"TryResolveNormalContact",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(resolveNormalContact, Is.Not.Null);
resolveNormalContact.Invoke(
resolver,
new object[] { targetEnemy, new Vector2(0.25f, 0f) });
Assert.That(targetEnemy.CurrentHealth, Is.LessThan(initialEnemyHealth), "Enemy must take damage from back bump");
Assert.That(playerHealth.CurrentHealth, Is.EqualTo(initialPlayerHealth), "Player must NOT take damage from back bump");
Assert.That(player.IsDamageKnockbackActive, Is.False, "Player must NOT receive damage knockback from back bump");
LogAssert.NoUnexpectedReceived();
}
@@ -1580,6 +1766,8 @@ namespace BumpCombat.PlayModeTests
PlayerStats stats = player.GetComponent<PlayerStats>();
ActiveArtifactController artifacts =
player.GetComponent<ActiveArtifactController>();
Animator animator = player.GetComponent<Animator>();
RunManager runManager = RunManager.Instance;
for (int i = 0; i < artifacts.OwnedArtifactCount * 2; i++)
{
@@ -1607,12 +1795,28 @@ namespace BumpCombat.PlayModeTests
0.75f),
Is.True);
Assert.That(health.CurrentHealth, Is.Zero);
Assert.That(runManager.IsGameOver, Is.False);
Assert.That(runManager.IsGameOverPresentationActive, Is.True);
Assert.That(RunManager.GameplayInputEnabled, Is.False);
Assert.That(
animator.GetCurrentAnimatorStateInfo(0).IsName(PlayerController.DeathStateName),
Is.True);
Assert.That(
stats.GetStackCount(
"pulse.damage-reduction",
CharacterStat.IncomingDamage),
Is.Zero);
Assert.That(artifacts.IsExecutingArtifact, Is.False);
float timeout = Time.realtimeSinceStartup + 3f;
while (!runManager.IsGameOver && Time.realtimeSinceStartup < timeout)
{
yield return null;
}
Assert.That(runManager.IsGameOver, Is.True);
Assert.That(runManager.IsGameOverPresentationActive, Is.False);
Assert.That(Time.timeScale, Is.Zero);
LogAssert.NoUnexpectedReceived();
}
@@ -1678,6 +1882,119 @@ namespace BumpCombat.PlayModeTests
LogAssert.NoUnexpectedReceived();
}
[UnityTest]
public IEnumerator ValidRearHit_ShakesInKnockbackDirectionRefreshesCooldownAndIgnoresDashBack()
{
yield return SceneManager.LoadSceneAsync("CombatPrototype");
yield return null;
var player = Object.FindFirstObjectByType<PlayerController>();
var cameraFollow = Object.FindFirstObjectByType<ArenaCameraFollow>();
Assert.IsNotNull(player);
Assert.IsNotNull(cameraFollow);
var target = new GameObject("RearHitTarget");
var shakeUnit = 1f / 32f;
var firstDirectionOffset = new Vector2(shakeUnit, 0f);
CombatEvents.RaiseValidHit(new CombatHitResult(
player.gameObject,
target,
false,
HitSide.Back,
10f,
Vector2.right,
1f,
new Vector2(0.25f, 0f)));
Assert.That(cameraFollow.ExternalShakeOffset, Is.EqualTo(firstDirectionOffset));
CombatEvents.RaiseValidHit(new CombatHitResult(
player.gameObject,
target,
false,
HitSide.Back,
10f,
Vector2.left,
1f,
new Vector2(0.25f, 0f)));
Assert.That(cameraFollow.ExternalShakeOffset, Is.EqualTo(firstDirectionOffset));
yield return new WaitForSecondsRealtime(0.10f);
Assert.That(cameraFollow.ExternalShakeOffset, Is.EqualTo(Vector2.zero));
yield return new WaitForSecondsRealtime(0.13f);
Assert.That(Time.timeScale, Is.EqualTo(1f));
yield return new WaitForSeconds(0.20f);
CombatEvents.RaiseValidHit(new CombatHitResult(
player.gameObject,
target,
false,
HitSide.Back,
10f,
Vector2.left,
1f,
new Vector2(0.25f, 0f)));
Assert.That(cameraFollow.ExternalShakeOffset, Is.EqualTo(Vector2.zero));
yield return new WaitForSecondsRealtime(0.2f);
yield return new WaitForSeconds(0.15f);
CombatEvents.RaiseValidHit(new CombatHitResult(
player.gameObject,
target,
false,
HitSide.Back,
10f,
Vector2.right,
1f,
new Vector2(0.25f, 0f)));
Assert.That(cameraFollow.ExternalShakeOffset, Is.EqualTo(Vector2.zero));
yield return new WaitForSecondsRealtime(0.2f);
yield return new WaitForSeconds(0.15f);
CombatEvents.RaiseValidHit(new CombatHitResult(
player.gameObject,
target,
false,
HitSide.Back,
10f,
Vector2.left,
1f,
new Vector2(0.25f, 0f)));
Assert.That(cameraFollow.ExternalShakeOffset, Is.EqualTo(Vector2.zero));
yield return new WaitForSecondsRealtime(0.2f);
yield return new WaitForSeconds(0.45f);
CombatEvents.RaiseValidHit(new CombatHitResult(
player.gameObject,
target,
false,
HitSide.Back,
10f,
Vector2.left,
1f,
new Vector2(0.25f, 0f)));
Assert.That(
cameraFollow.ExternalShakeOffset,
Is.EqualTo(new Vector2(-shakeUnit, 0f)));
yield return new WaitForSecondsRealtime(0.2f);
yield return new WaitForSeconds(0.45f);
CombatEvents.RaiseValidHit(new CombatHitResult(
player.gameObject,
target,
true,
HitSide.Back,
10f,
Vector2.right,
1f,
new Vector2(0.25f, 0f)));
Assert.That(cameraFollow.ExternalShakeOffset, Is.EqualTo(Vector2.zero));
yield return new WaitForSecondsRealtime(0.2f);
Object.Destroy(target);
LogAssert.NoUnexpectedReceived();
}
private static IEnumerator AssertAreaArtifactBatchesHitFeedback(
ActiveArtifactEffect expectedEffect,
int selectionCount,
@@ -2345,7 +2662,7 @@ namespace BumpCombat.PlayModeTests
Assert.That(panelRect.anchorMin, Is.EqualTo(new Vector2(0f, 1f)));
Assert.That(panelRect.anchorMax, Is.EqualTo(new Vector2(0f, 1f)));
Assert.That(panelRect.pivot, Is.EqualTo(new Vector2(0f, 1f)));
string[] labels = { "엘리트 출현", "중간보스 출현", "보스 출현" };
string[] labels = { "엘리트 출현", "중간보스 출현", "보스방 이동" };
for (int i = 0; i < buttons.Length; i++)
{
Assert.That(buttons[i].GetComponentInChildren<Text>().text, Is.EqualTo(labels[i]));
@@ -2353,18 +2670,10 @@ namespace BumpCombat.PlayModeTests
}
float elapsed = RunManager.Instance.ElapsedTime;
ClickButtonThroughEventSystem(buttons[2]);
Assert.That(RunManager.Instance.ElapsedTime, Is.EqualTo(elapsed));
Assert.That(director.CanDebugSpawnEventEnemy(RunTimedEvent.FinalBoss), Is.False);
Assert.That(FindEventEnemy(RunTimedEvent.FinalBoss), Is.Not.Null);
EnemyController firstFinalBoss = FindEventEnemy(RunTimedEvent.FinalBoss);
yield return KillEventEnemyThroughGroggy(firstFinalBoss);
Assert.That(director.CanDebugSpawnEventEnemy(RunTimedEvent.FinalBoss), Is.True);
yield return null;
Assert.That(buttons[2].interactable, Is.True);
ClickButtonThroughEventSystem(buttons[2]);
Assert.That(FindEventEnemy(RunTimedEvent.FinalBoss), Is.Not.Null);
Assert.That(RunManager.Instance.ElapsedTime, Is.EqualTo(elapsed));
yield return null;
ClickButtonThroughEventSystem(buttons[0]);
ClickButtonThroughEventSystem(buttons[1]);
@@ -2374,8 +2683,7 @@ namespace BumpCombat.PlayModeTests
Assert.That(director.CanDebugSpawnEventEnemy(RunTimedEvent.Elite), Is.False);
RunManager.ForceProductionModeForTests = true;
EnemyController secondFinalBoss = FindEventEnemy(RunTimedEvent.FinalBoss);
yield return KillEventEnemyThroughGroggy(secondFinalBoss);
yield return null;
Assert.That(panel.activeSelf, Is.False);
Assert.That(director.CanDebugSpawnEventEnemy(RunTimedEvent.FinalBoss), Is.False);
Assert.That(director.TryDebugSpawnEventEnemy(RunTimedEvent.FinalBoss), Is.False);
@@ -2423,12 +2731,12 @@ namespace BumpCombat.PlayModeTests
eventSystem.GetComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
Assert.That(inputModule, Is.Not.Null);
Assert.That(eventSystem.currentInputModule, Is.SameAs(inputModule));
yield return ClickButtonWithVirtualMouse(buttons[2], mouse, inputModule);
yield return ClickButtonWithVirtualMouse(buttons[0], mouse, inputModule);
Assert.That(
Object.FindAnyObjectByType<SpawnDirector>()
.CanDebugSpawnEventEnemy(RunTimedEvent.FinalBoss),
.CanDebugSpawnEventEnemy(RunTimedEvent.Elite),
Is.False);
Assert.That(FindEventEnemy(RunTimedEvent.FinalBoss), Is.Not.Null);
Assert.That(FindEventEnemy(RunTimedEvent.Elite), Is.Not.Null);
}
finally
{
@@ -141,7 +141,7 @@ namespace BumpCombat.PlayModeTests
}
[UnityTest]
public IEnumerator EnemyStatusVisual_LoadsBothSheetsAndSharesGroundAnchor()
public IEnumerator EnemyStatusVisual_LoadsBothSheetsAndFollowsRenderedAnchor()
{
yield return LoadCombatPrototype();
EnemyController enemy = FindOrSpawnEnemy();
@@ -151,7 +151,7 @@ namespace BumpCombat.PlayModeTests
Assert.That(source == null, Is.False);
enemy.transform.localScale = Vector3.one * 0.8f;
source.flipX = true;
source.flipY = false;
source.flipY = true;
Assert.That(enemy.ApplyIgnite(3f, 0.5f, 0.01f), Is.True);
Assert.That(enemy.ApplyShock(3f, 0.2f), Is.True);
@@ -172,15 +172,18 @@ namespace BumpCombat.PlayModeTests
Assert.That(ignite.sprite.pixelsPerUnit, Is.EqualTo(32f).Within(0.01f));
Assert.That(shock.sprite.pixelsPerUnit, Is.EqualTo(32f).Within(0.01f));
Vector3 expectedStatusPosition = ExpectedStatusPosition(enemy, source);
Assert.That(
Vector3.Distance(
ignite.transform.position,
(Vector3)enemy.GroundAnchorPosition),
ignite.transform.parent,
Is.EqualTo(GetCurrentVisualTransform(enemy)));
Assert.That(
shock.transform.parent,
Is.EqualTo(GetCurrentVisualTransform(enemy)));
Assert.That(
Vector3.Distance(ignite.transform.position, expectedStatusPosition),
Is.LessThan(0.0001f));
Assert.That(
Vector3.Distance(
shock.transform.position,
(Vector3)enemy.GroundAnchorPosition),
Vector3.Distance(shock.transform.position, expectedStatusPosition),
Is.LessThan(0.0001f));
Assert.That(ignite.flipX, Is.EqualTo(source.flipX));
Assert.That(shock.flipX, Is.EqualTo(source.flipX));
@@ -203,6 +206,122 @@ namespace BumpCombat.PlayModeTests
Assert.That(enemy.transform.Find("Stun Runes") == null, Is.False);
}
[UnityTest]
public IEnumerator EnemyStatusVisual_UsesRenderedTransformWhenPhysicsPositionDiffers()
{
yield return LoadCombatPrototype();
EnemyController enemy = FindOrSpawnEnemy();
Assert.That(enemy, Is.Not.Null);
Rigidbody2D body = enemy.GetComponent<Rigidbody2D>();
SpriteRenderer source = enemy.GetComponent<SpriteRenderer>();
Assert.That(body, Is.Not.Null);
Assert.That(source, Is.Not.Null);
Time.timeScale = 0f;
body.simulated = false;
Vector2 physicalPosition = (Vector2)enemy.transform.position
+ Vector2.down * 3f;
Vector3 renderedPosition = enemy.transform.position
+ Vector3.up * 1.5f;
body.position = physicalPosition;
enemy.transform.position = renderedPosition;
Assert.That(
Vector2.Distance(body.position, enemy.transform.position),
Is.GreaterThan(1f));
Assert.That(enemy.ApplyIgnite(3f, 0.5f, 0.01f), Is.True);
ArtifactStatusVisual statusVisual =
enemy.GetComponent<ArtifactStatusVisual>();
Assert.That(statusVisual, Is.Not.Null);
statusVisual.SendMessage(
"LateUpdate",
SendMessageOptions.RequireReceiver);
SpriteRenderer ignite = FindChildRenderer(enemy, IgniteVisualName);
Assert.That(ignite, Is.Not.Null);
Assert.That(
Vector3.Distance(
ignite.transform.position,
ExpectedStatusPosition(enemy, source)),
Is.LessThan(0.0001f));
Assert.That(
Vector2.Distance(ignite.transform.position, body.position),
Is.GreaterThan(0.5f));
body.simulated = true;
}
[UnityTest]
public IEnumerator EnemyStatusVisual_FollowsLaunchAndFogVisibility()
{
yield return LoadCombatPrototype();
EnemyController enemy = FindOrSpawnEnemy();
Assert.That(enemy, Is.Not.Null);
SpriteRenderer source = enemy.GetComponent<SpriteRenderer>();
Assert.That(source, Is.Not.Null);
Assert.That(enemy.ApplyIgnite(3f, 0.5f, 0.01f), Is.True);
yield return null;
SpriteRenderer ignite = FindChildRenderer(enemy, IgniteVisualName);
Assert.That(ignite, Is.Not.Null);
enemy.PlayLaunchVisual(Vector2.right, 0.8f, 0.4f);
yield return null;
Transform launchVisual = GetCurrentVisualTransform(enemy);
Assert.That(enemy.IsLaunchVisualActive, Is.True);
Assert.That(launchVisual, Is.Not.EqualTo(enemy.transform));
Assert.That(ignite.transform.parent, Is.EqualTo(launchVisual));
Assert.That(
Vector3.Distance(
ignite.transform.position,
ExpectedStatusPosition(enemy, source)),
Is.LessThan(0.0001f));
enemy.SetFogVisibility(false);
yield return null;
Assert.That(ignite.forceRenderingOff, Is.True);
SpriteRenderer airborneRenderer =
launchVisual.GetComponent<SpriteRenderer>();
Assert.That(airborneRenderer, Is.Not.Null);
Assert.That(airborneRenderer.forceRenderingOff, Is.True);
Assert.That(enemy.ApplyShock(3f, 0.2f), Is.True);
yield return null;
SpriteRenderer shock = FindChildRenderer(enemy, ShockVisualName);
Assert.That(shock, Is.Not.Null);
Assert.That(shock.forceRenderingOff, Is.True);
enemy.SetFogVisibility(true);
yield return null;
Assert.That(ignite.forceRenderingOff, Is.False);
Assert.That(shock.forceRenderingOff, Is.False);
Assert.That(airborneRenderer.forceRenderingOff, Is.False);
yield return new WaitForSeconds(0.45f);
yield return null;
Assert.That(enemy.IsLaunchVisualActive, Is.False);
Assert.That(enemy.IsIgnited, Is.True);
SpriteRenderer landedIgnite =
FindChildRenderer(enemy, IgniteVisualName);
Assert.That(landedIgnite, Is.Not.Null);
Assert.That(
landedIgnite.transform.parent,
Is.EqualTo(enemy.transform));
Assert.That(
Vector3.Distance(
landedIgnite.transform.position,
ExpectedStatusPosition(enemy, source)),
Is.LessThan(0.0001f));
}
[UnityTest]
public IEnumerator EnemyStatusVisual_FreezesDuringPauseAndAdvancesAfterResume()
{
@@ -438,6 +557,26 @@ namespace BumpCombat.PlayModeTests
return null;
}
private static Transform GetCurrentVisualTransform(EnemyController enemy)
{
Transform airborneVisual =
enemy.transform.Find("Airborne Enemy Visual");
return airborneVisual != null
? airborneVisual
: enemy.transform;
}
private static Vector3 ExpectedStatusPosition(
EnemyController enemy,
SpriteRenderer source)
{
float anchorOffsetY = source.flipY
? 13f / 32f
: -13f / 32f;
return GetCurrentVisualTransform(enemy).TransformPoint(
new Vector3(0f, anchorOffsetY, 0f));
}
private static SpriteRenderer FindChildRenderer(
EnemyController enemy,
string objectName)
@@ -2,11 +2,13 @@ using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using BumpCombat.Combat;
using BumpCombat.Constants;
using BumpCombat.Core;
using BumpCombat.Enemies;
using BumpCombat.Player;
using BumpCombat.Progression;
using BumpCombat.Spawning;
using BumpCombat.Stage;
using BumpCombat.UI;
using NUnit.Framework;
using UnityEngine;
@@ -676,8 +678,7 @@ namespace BumpCombat.PlayModeTests
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(eventText.gameObject.activeSelf, Is.False);
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));
@@ -780,25 +781,13 @@ namespace BumpCombat.PlayModeTests
gridSnapping.GetValue(pixelPerfect).ToString(),
Is.EqualTo("PixelSnapping"));
RenderTexture target = new(1399, 799, 24, RenderTextureFormat.ARGB32);
target.Create();
RenderTexture previousTarget = camera.targetTexture;
camera.targetTexture = target;
RunConstants stageConstants = GameplayConstants.Current.Run;
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));
PropertyInfo assetsPpu = pixelPerfect.GetType().GetProperty("assetsPPU");
PropertyInfo referenceWidth = pixelPerfect.GetType().GetProperty("refResolutionX");
PropertyInfo referenceHeight = pixelPerfect.GetType().GetProperty("refResolutionY");
FieldInfo internalField = pixelPerfect.GetType().GetField(
"m_Internal",
BindingFlags.Instance | BindingFlags.NonPublic);
@@ -812,31 +801,66 @@ namespace BumpCombat.PlayModeTests
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);
int[,] targetSizes = { { 1920, 1080 }, { 1280, 720 }, { 1399, 799 } };
for (int i = 0; i < targetSizes.GetLength(0); i++)
{
RenderTexture target = new(
targetSizes[i, 0],
targetSizes[i, 1],
24,
RenderTextureFormat.ARGB32);
target.Create();
camera.targetTexture = target;
try
{
camera.Render();
yield return null;
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));
int targetWidth = targetSizes[i, 0];
int targetHeight = targetSizes[i, 1];
int expectedWidth = targetWidth & ~1;
int expectedHeight = targetHeight & ~1;
Rect pixelRect = camera.pixelRect;
Assert.That(pixelRect.width, Is.EqualTo(expectedWidth).Within(0.01f));
Assert.That(pixelRect.height, Is.EqualTo(expectedHeight).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(camera.aspect,
Is.EqualTo(expectedWidth / (float)expectedHeight).Within(0.001f));
Assert.That(camera.orthographicSize,
Is.EqualTo(stageConstants.StageCameraViewHalfExtents.y
* expectedHeight / targetHeight).Within(0.01f));
Assert.That(assetsPpu.GetValue(pixelPerfect), Is.EqualTo(32));
Assert.That(
referenceWidth.GetValue(pixelPerfect),
Is.EqualTo(Mathf.RoundToInt(
stageConstants.StageCameraViewHalfExtents.x * 2f * 32f)));
Assert.That(
referenceHeight.GetValue(pixelPerfect),
Is.EqualTo(Mathf.RoundToInt(
stageConstants.StageCameraViewHalfExtents.y * 2f * 32f)));
Assert.That(widthField.GetValue(internalCamera), Is.EqualTo(0));
Assert.That(heightField.GetValue(internalCamera), Is.EqualTo(0));
Assert.That(useOffscreenField.GetValue(internalCamera), Is.False);
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));
camera.Render();
Assert.That(camera.pixelRect.width, Is.EqualTo(expectedWidth).Within(0.01f));
Assert.That(camera.pixelRect.height, Is.EqualTo(expectedHeight).Within(0.01f));
Assert.That(camera.orthographicSize,
Is.EqualTo(stageConstants.StageCameraViewHalfExtents.y
* expectedHeight / targetHeight).Within(0.01f));
}
finally
{
camera.targetTexture = previousTarget;
target.Release();
Object.Destroy(target);
}
}
}
finally
{
camera.targetTexture = previousTarget;
target.Release();
Object.Destroy(target);
}
}
@@ -846,10 +870,14 @@ namespace BumpCombat.PlayModeTests
yield return LoadCombatPrototype();
RunHUD hud = Object.FindAnyObjectByType<RunHUD>();
StageManager stageManager = StageManager.Instance;
SpawnDirector director = Object.FindAnyObjectByType<SpawnDirector>();
PlayerController player = Object.FindAnyObjectByType<PlayerController>();
ArenaBounds arena = ArenaBounds.Resolve();
Assert.That(hud, Is.Not.Null);
Assert.That(stageManager, Is.Not.Null);
Assert.That(stageManager.CurrentZone, Is.Not.Null);
Assert.That(stageManager.CurrentZone.Preset, Is.Not.Null);
Assert.That(arena, Is.Not.Null);
director.enabled = false;
@@ -867,6 +895,28 @@ namespace BumpCombat.PlayModeTests
Time.timeScale = 0f;
yield return new WaitForSecondsRealtime(0.2f);
Camera worldCamera = Camera.main;
EnemyController eventEnemy = null;
EnemyController[] activeEnemies = Object.FindObjectsByType<EnemyController>(
FindObjectsInactive.Exclude,
FindObjectsSortMode.None);
for (int i = 0; i < activeEnemies.Length; i++)
{
if (activeEnemies[i].IsEventEnemy)
{
eventEnemy = activeEnemies[i];
break;
}
}
Assert.That(eventEnemy, Is.Not.Null);
ZoneInstance visibilityZone = stageManager.CurrentZone;
Vector2 onScreenPosition = player.transform.position;
visibilityZone.UpdateVisibility(onScreenPosition);
eventEnemy.GetComponent<Rigidbody2D>().position = onScreenPosition;
eventEnemy.transform.position = onScreenPosition;
Physics2D.SyncTransforms();
yield return new WaitForSecondsRealtime(0.12f);
GameObject minimap = GameObject.Find("Minimap");
Image playerMarker = GameObject.Find("Minimap Player")
.GetComponent<Image>();
@@ -884,7 +934,73 @@ namespace BumpCombat.PlayModeTests
}
RectTransform viewport = GameObject.Find("Minimap Camera Viewport")
.GetComponent<RectTransform>();
Transform terrain = GameObject.Find("Minimap Terrain")?.transform;
Assert.That(minimap.activeSelf, Is.True);
Assert.That(terrain, Is.Not.Null);
int expectedTerrainCells = 0;
RoomPreset currentPreset = stageManager.CurrentZone.Preset;
for (int y = 0; y < currentPreset.GridHeight; y++)
{
for (int x = 0; x < currentPreset.GridWidth; x++)
{
PresetCellType cellType = stageManager.CurrentZone.GetRuntimeCell(x, y);
if (cellType != PresetCellType.Empty)
{
expectedTerrainCells++;
}
}
}
Assert.That(terrain.childCount, Is.EqualTo(expectedTerrainCells));
for (int i = 0; i < currentPreset.Sockets.Count; i++)
{
RoomPresetSocket socket = currentPreset.Sockets[i];
for (int offset = 0; offset < socket.Width; offset++)
{
Vector2Int socketCell = socket.CellPosition;
if (socket.Direction == StageDirection.North ||
socket.Direction == StageDirection.South)
{
socketCell.x += offset;
}
else
{
socketCell.y += offset;
}
bool isActive = stageManager.CurrentZone.GetRuntimeCell(socketCell.x, socketCell.y)
== PresetCellType.Socket;
Image terrainCell = terrain.Find($"Terrain {socketCell.x},{socketCell.y}")
?.GetComponent<Image>();
if (isActive)
{
Assert.That(terrainCell, Is.Not.Null, socket.Id);
Color32 terrainColor = terrainCell.color;
if (visibilityZone.IsCellVisible(socketCell.x, socketCell.y))
{
Assert.That(
terrainColor.Equals(new Color32(54, 103, 112, 230)),
Is.True,
socket.Id);
}
else if (visibilityZone.IsCellExplored(socketCell.x, socketCell.y))
{
Assert.That(terrainColor, Is.Not.EqualTo(
new Color32(54, 103, 112, 230)), socket.Id);
Assert.That(terrainColor.a, Is.EqualTo(255), socket.Id);
}
else
{
Assert.That(terrainColor, Is.EqualTo(new Color32(2, 4, 5, 255)),
socket.Id);
}
}
else
{
Assert.That(terrainCell, Is.Null, socket.Id);
}
}
}
Assert.That(
((Color32)playerMarker.color).Equals(
new Color32(106, 236, 255, 255)),
@@ -898,6 +1014,68 @@ namespace BumpCombat.PlayModeTests
Assert.That(viewport.rect.width, Is.GreaterThan(0f));
Assert.That(viewport.rect.height, Is.GreaterThan(0f));
Vector2 nearEdgePosition = onScreenPosition;
bool foundVisiblePosition = false;
for (int y = 0; y < currentPreset.GridHeight && !foundVisiblePosition; y++)
{
for (int x = 0; x < currentPreset.GridWidth; x++)
{
if (!visibilityZone.IsCellVisible(x, y))
{
continue;
}
Vector2 candidate = currentPreset.CellToWorldPosition(
visibilityZone.ZoneCenter,
new Vector2Int(x, y));
if (Vector2.Distance(candidate, onScreenPosition) > 0.5f
&& arena.IsInsideCameraVisible(worldCamera, candidate))
{
nearEdgePosition = candidate;
foundVisiblePosition = true;
break;
}
}
}
Assert.That(foundVisiblePosition, Is.True);
eventEnemy.GetComponent<Rigidbody2D>().position = nearEdgePosition;
eventEnemy.transform.position = nearEdgePosition;
Physics2D.SyncTransforms();
yield return new WaitForSecondsRealtime(0.12f);
Assert.That(visibilityZone.IsWorldPositionVisible(nearEdgePosition), Is.True);
Assert.That(eventMarker.gameObject.activeSelf, Is.True, "An enemy inside the current camera view should have a marker");
Vector2 outsidePosition = onScreenPosition + Vector2.right * 6.5f;
bool foundHiddenPosition = false;
for (int y = 0; y < currentPreset.GridHeight && !foundHiddenPosition; y++)
{
for (int x = 0; x < currentPreset.GridWidth; x++)
{
if (visibilityZone.IsCellVisible(x, y))
{
continue;
}
Vector2 candidate = currentPreset.CellToWorldPosition(
visibilityZone.ZoneCenter,
new Vector2Int(x, y));
if (arena.IsInsideCameraVisible(worldCamera, candidate))
{
outsidePosition = candidate;
foundHiddenPosition = true;
break;
}
}
}
eventEnemy.GetComponent<Rigidbody2D>().position = outsidePosition;
eventEnemy.transform.position = outsidePosition;
Physics2D.SyncTransforms();
yield return new WaitForSecondsRealtime(0.12f);
Assert.That(visibilityZone.IsWorldPositionVisible(outsidePosition), Is.False);
Assert.That(eventMarker.gameObject.activeSelf, Is.False, "An enemy outside the current camera view should not have a marker");
Vector2 oldMarkerPosition = playerMarker.rectTransform.anchoredPosition;
Vector2 movedPosition = arena.ClampPlayerPosition(new Vector2(6f, 2f));
player.GetComponent<Rigidbody2D>().position = movedPosition;
@@ -0,0 +1,164 @@
using System.Collections;
using BumpCombat.Core;
using BumpCombat.Player;
using BumpCombat.Stage;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace BumpCombat.PlayModeTests
{
public sealed class ZoneArrivalPlayModeTests
{
private GameObject arenaObject;
private GameObject playerObject;
private GameObject stageObject;
private RunManager existingRunManager;
private bool existingRunManagerWasEnabled;
private float previousTimeScale;
[SetUp]
public void SetUp()
{
previousTimeScale = Time.timeScale;
existingRunManager = Object.FindAnyObjectByType<RunManager>();
if (existingRunManager != null)
{
existingRunManagerWasEnabled = existingRunManager.enabled;
existingRunManager.enabled = false;
}
Time.timeScale = 1f;
}
[TearDown]
public void TearDown()
{
if (stageObject != null)
{
Object.Destroy(stageObject);
}
if (playerObject != null)
{
Object.Destroy(playerObject);
}
if (arenaObject != null)
{
Object.Destroy(arenaObject);
}
if (existingRunManager != null)
{
existingRunManager.enabled = existingRunManagerWasEnabled;
}
Time.timeScale = previousTimeScale;
}
[Timeout(60000)]
[UnityTest]
public IEnumerator ArrivalDoor_RejectsImmediateReverseAndRetriesOnStayAfterCooldown()
{
arenaObject = new GameObject("ArrivalPlayMode_ArenaBounds");
arenaObject.AddComponent<ArenaBounds>();
playerObject = new GameObject("ArrivalPlayMode_Player");
PlayerController player = playerObject.AddComponent<PlayerController>();
CircleCollider2D playerBody = playerObject.AddComponent<CircleCollider2D>();
playerBody.radius = 0.22f;
playerBody.offset = new Vector2(0.11f, -0.07f);
Rigidbody2D rigidbody = playerObject.GetComponent<Rigidbody2D>();
rigidbody.bodyType = RigidbodyType2D.Kinematic;
rigidbody.gravityScale = 0f;
rigidbody.constraints = RigidbodyConstraints2D.FreezeRotation;
playerObject.GetComponent<Animator>().enabled = false;
player.enabled = false;
stageObject = new GameObject("ArrivalPlayMode_StageManager");
StageManager stageManager = stageObject.AddComponent<StageManager>();
stageManager.StartRun(4242);
// StageManager.Start also builds its autonomous test stage. Let
// that lifecycle pass before taking connector references.
yield return null;
ZoneInstance startZone = stageManager.CurrentZone;
Assert.That(startZone, Is.Not.Null);
ZoneConnector sourceConnector = FindOutgoingConnector(stageManager);
Assert.That(sourceConnector, Is.Not.Null);
yield return new WaitForSecondsRealtime(0.4f);
MoveBodyCenter(player, playerBody, sourceConnector.GetComponent<BoxCollider2D>().bounds.center);
yield return new WaitForFixedUpdate();
int targetNodeId = sourceConnector.TargetNodeId;
float transitionDeadline = Time.realtimeSinceStartup + 2f;
while ((stageManager.IsTransitioning
|| stageManager.CurrentZone == null
|| stageManager.CurrentZone.NodeId != targetNodeId)
&& Time.realtimeSinceStartup < transitionDeadline)
{
yield return null;
}
Assert.That(stageManager.CurrentZone.NodeId, Is.EqualTo(targetNodeId));
Assert.That(stageManager.IsTransitioning, Is.False);
ZoneConnector arrivalConnector = stageManager.CurrentZone.GetConnector(sourceConnector.TargetSocketId);
Assert.That(arrivalConnector, Is.Not.Null);
arrivalConnector.SetLocked(false);
BoxCollider2D arrivalTrigger = arrivalConnector.GetComponent<BoxCollider2D>();
Assert.That(arrivalTrigger.bounds.Intersects(playerBody.bounds), Is.False);
// Enter the destination trigger immediately after transition. The
// trigger stay must be ignored until the manager cooldown expires.
MoveBodyCenter(player, playerBody, arrivalTrigger.bounds.center);
yield return new WaitForFixedUpdate();
yield return new WaitForSecondsRealtime(0.1f);
Assert.That(stageManager.CurrentZone.NodeId, Is.EqualTo(targetNodeId));
float returnDeadline = Time.realtimeSinceStartup + 2f;
while ((stageManager.IsTransitioning
|| stageManager.CurrentZone == null
|| stageManager.CurrentZone.NodeId != startZone.NodeId)
&& Time.realtimeSinceStartup < returnDeadline)
{
yield return null;
}
Assert.That(stageManager.CurrentZone.NodeId, Is.EqualTo(startZone.NodeId));
// The return arrival is also outside its doorway. Leaving the
// player there must not produce an automatic pingpong transition.
yield return new WaitForSecondsRealtime(0.45f);
Assert.That(stageManager.CurrentZone.NodeId, Is.EqualTo(startZone.NodeId));
}
private static ZoneConnector FindOutgoingConnector(StageManager stageManager)
{
for (int direction = 0; direction < 4; direction++)
{
ZoneConnector connector = stageManager.CurrentZone.GetConnector((StageDirection)direction);
if (connector != null && connector.TargetNodeId >= 0)
{
return connector;
}
}
return null;
}
private static void MoveBodyCenter(
PlayerController player,
Collider2D playerBody,
Vector2 bodyCenter)
{
Vector2 bodyOffset = (Vector2)playerBody.bounds.center
- (Vector2)player.transform.position;
player.transform.position = bodyCenter - bodyOffset;
Physics2D.SyncTransforms();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7a4d1e6f9b824c1aa5e0d3f7b6c29841