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
@@ -0,0 +1,700 @@
using System.Collections.Generic;
using BumpCombat.Core;
using BumpCombat.Stage;
using NUnit.Framework;
using UnityEngine;
namespace BumpCombat.Tests
{
public sealed class LevelPresetTests
{
[Test]
public void LevelPresetCatalog_ContainsAll12Presets()
{
LevelPresetCatalog.EnsureInitialized();
var all = LevelPresetCatalog.AllPresets;
string[] expectedPresets = new string[]
{
"GRAVE_S1_COMBAT_A",
"GRAVE_S1_COMBAT_B",
"GRAVE_S1_FORK",
"GRAVE_S1_ELITE_ARENA",
"GRAVE_S2_COMBAT_A",
"GRAVE_S2_COMBAT_B",
"GRAVE_S2_FORK_HAZARD",
"GRAVE_S2_MIDBOSS",
"GRAVE_S3_COMBAT_A",
"GRAVE_S3_COMBAT_B",
"GRAVE_S3_PREBOSS_FORK",
"GRAVE_S3_FINAL_NECROMANCER",
};
foreach (string presetId in expectedPresets)
{
RoomPreset preset = LevelPresetCatalog.GetPreset(presetId);
Assert.IsNotNull(preset, $"Preset {presetId} should be registered in LevelPresetCatalog.");
Assert.AreEqual(presetId, preset.PresetId);
}
}
[Test]
public void LevelPresets_MatchSpecificationDimensions()
{
// Stage 1: 18x12 (Elite 20x14)
AssertDimensions("GRAVE_S1_COMBAT_A", 18, 12);
AssertDimensions("GRAVE_S1_COMBAT_B", 18, 12);
AssertDimensions("GRAVE_S1_FORK", 18, 12);
AssertDimensions("GRAVE_S1_ELITE_ARENA", 20, 14);
// Stage 2: 20x14 (MidBoss 22x16)
AssertDimensions("GRAVE_S2_COMBAT_A", 20, 14);
AssertDimensions("GRAVE_S2_COMBAT_B", 20, 14);
AssertDimensions("GRAVE_S2_FORK_HAZARD", 20, 14);
AssertDimensions("GRAVE_S2_MIDBOSS", 22, 16);
// Stage 3: 22x16 (PreBoss 20x14, FinalBoss 24x18)
AssertDimensions("GRAVE_S3_COMBAT_A", 22, 16);
AssertDimensions("GRAVE_S3_COMBAT_B", 22, 16);
AssertDimensions("GRAVE_S3_PREBOSS_FORK", 20, 14);
AssertDimensions("GRAVE_S3_FINAL_NECROMANCER", 24, 18);
}
[Test]
public void S1AExpandedComparison_UsesActualGridAndPreservesCombatContract()
{
RoomPreset original = LevelPresetCatalog.GetPreset("GRAVE_S1_COMBAT_A");
RoomPreset expanded = LevelPresetCatalog.GetPreset(
LevelPresetCatalog.S1AComparisonExpandedPresetId);
Assert.That(original, Is.Not.Null);
Assert.That(expanded, Is.Not.Null);
Assert.That(original.GridWidth, Is.EqualTo(18));
Assert.That(original.GridHeight, Is.EqualTo(12));
Assert.That(expanded.GridWidth, Is.EqualTo(36));
Assert.That(expanded.GridHeight, Is.EqualTo(24));
List<Vector2Int> expectedObstacles = new()
{
new(10, 7), new(10, 8), new(11, 8),
new(24, 7),
new(20, 14), new(21, 14),
new(13, 16), new(14, 16),
};
List<Vector2Int> actualObstacles = new();
for (int y = 0; y < expanded.GridHeight; y++)
{
for (int x = 0; x < expanded.GridWidth; x++)
{
if (expanded.GridMap[x, y] == PresetCellType.Obstacle)
{
actualObstacles.Add(new Vector2Int(x, y));
}
}
}
Assert.That(actualObstacles, Is.EquivalentTo(expectedObstacles));
Assert.That(actualObstacles.Count, Is.EqualTo(8));
Assert.That(expanded.SpawnPoints.Count, Is.EqualTo(original.SpawnPoints.Count));
Assert.That(expanded.PlayerAnchor, Is.EqualTo(new Vector2Int(6, 11)));
foreach (RoomPresetSocket socket in expanded.Sockets)
{
Assert.That(socket.Width, Is.EqualTo(1), $"{socket.Id} must remain a narrow one-cell door.");
}
Assert.That(HasCellPath(expanded, new Vector2Int(2, 11), new Vector2Int(33, 11)), Is.True,
"Expanded S1-A must preserve a clear central approach.");
Assert.That(HasCellPath(expanded, new Vector2Int(2, 11), new Vector2Int(8, 6)), Is.True,
"Expanded S1-A must preserve a reachable left-flank approach.");
Assert.That(HasCellPath(expanded, new Vector2Int(33, 11), new Vector2Int(27, 17)), Is.True,
"Expanded S1-A must preserve a reachable right-flank approach.");
AssertExpandedEncounterPoolsMatch(original, expanded);
}
[Test]
public void StageFog_ExplorationPersistsOnRevisit_AndResetsForNewZone()
{
RoomPreset preset = LevelPresetCatalog.GetPreset(
LevelPresetCatalog.S1AComparisonExpandedPresetId);
StageNode node = new()
{
Id = 701,
NodeType = StageNodeType.Combat,
PresetId = preset.PresetId,
};
node.SetConnection(StageDirection.North, 1);
node.SetConnection(StageDirection.South, 2);
node.SetConnection(StageDirection.West, 3);
node.SetConnection(StageDirection.East, 4);
GameObject firstObject = new("FogZone_First");
GameObject secondObject = new("FogZone_Second");
GameObject partialObject = new("FogZone_PartialSockets");
try
{
ZoneInstance firstZone = firstObject.AddComponent<ZoneInstance>();
firstZone.Initialize(node, preset.PlayableHalfExtents);
Vector2 start = preset.CellToWorldPosition(Vector2.zero, new Vector2Int(6, 11));
Vector2 far = preset.CellToWorldPosition(Vector2.zero, new Vector2Int(30, 17));
Vector2 occlusionOrigin = preset.CellToWorldPosition(
Vector2.zero,
new Vector2Int(8, 11));
firstZone.UpdateVisibility(occlusionOrigin);
Assert.That(firstZone.IsCellVisible(12, 7), Is.False,
"A cover cell must block diagonal terrain LOS behind it.");
firstZone.UpdateVisibility(start);
Assert.That(firstZone.IsCellVisible(6, 11), Is.True);
Assert.That(firstZone.IsCellExplored(30, 17), Is.False);
firstZone.UpdateVisibility(far);
Assert.That(firstZone.IsCellVisible(30, 17), Is.True);
Assert.That(firstZone.IsCellExplored(6, 11), Is.True);
firstZone.UpdateVisibility(start);
Assert.That(firstZone.IsCellExplored(30, 17), Is.True,
"A revisited room must retain explored terrain.");
Assert.That(firstZone.IsCellVisible(30, 17), Is.False,
"Explored terrain must become hidden again outside current LOS.");
ZoneInstance secondZone = secondObject.AddComponent<ZoneInstance>();
secondZone.Initialize(node, preset.PlayableHalfExtents);
secondZone.UpdateVisibility(start);
Assert.That(secondZone.IsCellExplored(30, 17), Is.False,
"A newly created run zone must not inherit exploration memory.");
StageNode partialNode = new()
{
Id = 702,
NodeType = StageNodeType.Combat,
PresetId = preset.PresetId,
};
partialNode.SetConnection(StageDirection.North, 1);
ZoneInstance partialZone = partialObject.AddComponent<ZoneInstance>();
partialZone.Initialize(partialNode, preset.PlayableHalfExtents);
Assert.That(partialZone.GetRuntimeCell(17, 0), Is.EqualTo(PresetCellType.Socket),
"The active socket corridor must remain part of the runtime grid.");
Assert.That(partialZone.GetRuntimeCell(0, 11), Is.EqualTo(PresetCellType.Empty),
"An inactive socket must be opaque in the runtime grid.");
StageFogOfWar partialFog =
partialObject.AddComponent<StageFogOfWar>();
partialFog.Initialize(partialZone);
Assert.That(partialFog.GetCellFogAlpha(0, 11), Is.EqualTo(0f));
MeshFilter fogMeshFilter = partialObject.GetComponent<MeshFilter>();
Assert.That(fogMeshFilter, Is.Not.Null);
Color[] fogColors = fogMeshFilter.sharedMesh.colors;
int emptyVertex = 11 * preset.GridWidth * 4;
for (int vertexOffset = 0; vertexOffset < 4; vertexOffset++)
{
Color emptyColor = fogColors[emptyVertex + vertexOffset];
Assert.That(emptyColor.r, Is.EqualTo(0f).Within(0.0001f));
Assert.That(emptyColor.g, Is.EqualTo(0f).Within(0.0001f));
Assert.That(emptyColor.b, Is.EqualTo(0f).Within(0.0001f));
Assert.That(emptyColor.a, Is.EqualTo(0f).Within(0.0001f));
}
}
finally
{
Object.DestroyImmediate(firstObject);
Object.DestroyImmediate(secondObject);
Object.DestroyImmediate(partialObject);
}
}
private static void AssertExpandedEncounterPoolsMatch(RoomPreset expected, RoomPreset actual)
{
Assert.That(actual.EncounterPool.Count, Is.EqualTo(expected.EncounterPool.Count));
for (int encounterIndex = 0; encounterIndex < expected.EncounterPool.Count; encounterIndex++)
{
RoomEncounter expectedEncounter = expected.EncounterPool[encounterIndex];
RoomEncounter actualEncounter = actual.EncounterPool[encounterIndex];
Assert.That(actualEncounter.Name, Is.EqualTo(expectedEncounter.Name));
Assert.That(actualEncounter.Items.Count, Is.EqualTo(expectedEncounter.Items.Count));
for (int itemIndex = 0; itemIndex < expectedEncounter.Items.Count; itemIndex++)
{
Assert.That(actualEncounter.Items[itemIndex].EnemyKind,
Is.EqualTo(expectedEncounter.Items[itemIndex].EnemyKind));
Assert.That(actualEncounter.Items[itemIndex].Count,
Is.EqualTo(expectedEncounter.Items[itemIndex].Count * 2));
}
}
}
private static bool HasCellPath(RoomPreset preset, Vector2Int start, Vector2Int target)
{
if (!RoomPathfinder.IsWalkable(preset, start.x, start.y)
|| !RoomPathfinder.IsWalkable(preset, target.x, target.y))
{
return false;
}
Queue<Vector2Int> pending = new();
HashSet<Vector2Int> visited = new() { start };
pending.Enqueue(start);
Vector2Int[] directions =
{
Vector2Int.up,
Vector2Int.right,
Vector2Int.down,
Vector2Int.left,
};
while (pending.Count > 0)
{
Vector2Int current = pending.Dequeue();
if (current == target)
{
return true;
}
for (int i = 0; i < directions.Length; i++)
{
Vector2Int next = current + directions[i];
if (visited.Contains(next)
|| !RoomPathfinder.IsWalkable(preset, next.x, next.y))
{
continue;
}
visited.Add(next);
pending.Enqueue(next);
}
}
return false;
}
private void AssertDimensions(string presetId, int expectedW, int expectedH)
{
RoomPreset preset = LevelPresetCatalog.GetPreset(presetId);
Assert.IsNotNull(preset, $"Preset {presetId} must exist.");
Assert.AreEqual(expectedW, preset.GridWidth, $"{presetId} GridWidth mismatch");
Assert.AreEqual(expectedH, preset.GridHeight, $"{presetId} GridHeight mismatch");
Assert.AreEqual(new Vector2(expectedW * 0.5f, expectedH * 0.5f), preset.PlayableHalfExtents);
}
[Test]
public void LevelPresets_HaveObstaclesAndSpawnPoints()
{
string[] combatAndBossPresets = new string[]
{
"GRAVE_S1_COMBAT_A",
"GRAVE_S1_COMBAT_B",
"GRAVE_S1_FORK",
"GRAVE_S1_ELITE_ARENA",
"GRAVE_S2_COMBAT_A",
"GRAVE_S2_COMBAT_B",
"GRAVE_S2_FORK_HAZARD",
"GRAVE_S2_MIDBOSS",
"GRAVE_S3_COMBAT_A",
"GRAVE_S3_COMBAT_B",
"GRAVE_S3_PREBOSS_FORK",
"GRAVE_S3_FINAL_NECROMANCER",
};
foreach (string presetId in combatAndBossPresets)
{
RoomPreset preset = LevelPresetCatalog.GetPreset(presetId);
Assert.IsNotNull(preset);
// Check obstacles exist
int obstacleCount = 0;
for (int y = 0; y < preset.GridHeight; y++)
{
for (int x = 0; x < preset.GridWidth; x++)
{
if (preset.GridMap[x, y] == PresetCellType.Obstacle) obstacleCount++;
}
}
Assert.Greater(obstacleCount, 0, $"{presetId} must have at least one obstacle.");
// Check sockets exist
Assert.Greater(preset.Sockets.Count, 0, $"{presetId} must have connection sockets.");
// Check spawn points exist
Assert.Greater(preset.SpawnPoints.Count, 0, $"{presetId} must have spawn points.");
}
}
[Test]
public void StageGenerator_AssignsValidLevelPresetsToAllNodes()
{
for (int stage = 1; stage <= 3; stage++)
{
StagePlan plan = StageGenerator.GenerateStage(stage, 424242);
Assert.IsNotNull(plan);
Assert.Greater(plan.Nodes.Count, 0);
foreach (StageNode node in plan.Nodes)
{
Assert.IsFalse(string.IsNullOrEmpty(node.PresetId), $"Node {node.Id} ({node.NodeType}) should have a PresetId.");
RoomPreset preset = LevelPresetCatalog.GetPreset(node.PresetId);
Assert.IsNotNull(preset, $"Preset {node.PresetId} assigned to Node {node.Id} ({node.NodeType}) must exist in catalog.");
}
}
}
[Test]
public void RoomPreset_CoordinateConversionIsSymmetricAndAccurate()
{
RoomPreset preset = LevelPresetCatalog.GetPreset("GRAVE_S1_COMBAT_A"); // 18x12
Assert.IsNotNull(preset);
Vector2 leftTop = preset.CellToLocalPosition(0, 0);
Vector2 rightBottom = preset.CellToLocalPosition(17, 11);
Assert.AreEqual(-8.5f, leftTop.x, 0.001f);
Assert.AreEqual(5.5f, leftTop.y, 0.001f);
Assert.AreEqual(8.5f, rightBottom.x, 0.001f);
Assert.AreEqual(-5.5f, rightBottom.y, 0.001f);
// Origin symmetry
Assert.AreEqual(-leftTop.x, rightBottom.x, 0.001f);
Assert.AreEqual(-leftTop.y, rightBottom.y, 0.001f);
}
[Test]
public void RoomPreset_EntryPositionGuaranteesSafetyOffset()
{
RoomPreset preset = LevelPresetCatalog.GetPreset("GRAVE_S1_COMBAT_A"); // 18x12
Assert.IsNotNull(preset);
Vector2 entryWest = preset.GetEntryLocalPosition(StageDirection.West);
Vector2 socketWest = preset.GetSocketLocalPosition(StageDirection.West);
// Entry should be at least two cells inward so a transition cannot immediately retrigger.
Assert.Greater(entryWest.x, socketWest.x);
Assert.GreaterOrEqual(entryWest.x - socketWest.x, 2f);
}
[Test]
public void AllPresets_AllSockets_HaveClearPathAndNoObstacleOverlapAtEntry()
{
float playerRadius = 0.275f;
List<string> errors = new();
foreach (var kvp in LevelPresetCatalog.AllPresets)
{
RoomPreset preset = kvp.Value;
foreach (var socket in preset.Sockets)
{
Vector2 entryPos = preset.GetEntryLocalPosition(socket);
for (int y = 0; y < preset.GridHeight; y++)
{
for (int x = 0; x < preset.GridWidth; x++)
{
if (!RoomPathfinder.IsWalkable(preset, x, y))
{
Vector2 obsCenter = preset.CellToLocalPosition(x, y);
float dx = Mathf.Max(0f, Mathf.Abs(entryPos.x - obsCenter.x) - 0.5f);
float dy = Mathf.Max(0f, Mathf.Abs(entryPos.y - obsCenter.y) - 0.5f);
float dist = Mathf.Sqrt(dx * dx + dy * dy);
if (dist <= playerRadius)
{
errors.Add($"Preset '{preset.PresetId}' socket '{socket.Id}' ({socket.Direction}) entry {entryPos} overlaps blocked cell at ({x}, {y}) (dist={dist:F3}, radius={playerRadius})");
}
}
}
}
}
}
Assert.IsEmpty(errors, string.Join("\n", errors));
}
[Test]
public void AllPresets_SocketCellsMatchConfiguredWidths_AndCatalogUsesMixedWidths()
{
bool hasOneTileSocket = false;
bool hasWiderSocket = false;
foreach (var kvp in LevelPresetCatalog.AllPresets)
{
RoomPreset preset = kvp.Value;
HashSet<Vector2Int> expectedSocketCells = new();
foreach (RoomPresetSocket socket in preset.Sockets)
{
Assert.GreaterOrEqual(socket.Width, 1, $"{preset.PresetId}/{socket.Id} width must be at least 1.");
hasOneTileSocket |= socket.Width == 1;
hasWiderSocket |= socket.Width > 1;
bool horizontal = socket.Direction == StageDirection.North || socket.Direction == StageDirection.South;
for (int i = 0; i < socket.Width; i++)
{
expectedSocketCells.Add(new Vector2Int(
socket.CellPosition.x + (horizontal ? i : 0),
socket.CellPosition.y + (horizontal ? 0 : i)));
}
}
HashSet<Vector2Int> actualSocketCells = new();
for (int y = 0; y < preset.GridHeight; y++)
{
for (int x = 0; x < preset.GridWidth; x++)
{
if (preset.GridMap[x, y] == PresetCellType.Socket)
{
actualSocketCells.Add(new Vector2Int(x, y));
}
}
}
CollectionAssert.AreEquivalent(
expectedSocketCells,
actualSocketCells,
$"{preset.PresetId} socket cells must exactly match declared socket widths.");
}
Assert.IsTrue(hasOneTileSocket, "Catalog must contain at least one 1-tile doorway.");
Assert.IsTrue(hasWiderSocket, "Catalog must retain wider doorways for rooms that need them.");
}
[TestCase("GRAVE_S1_FORK")]
[TestCase("GRAVE_S2_FORK_HAZARD")]
[TestCase("GRAVE_S3_PREBOSS_FORK")]
public void ForkPresets_DoNotUseSolidTwoByTwoObstacleBlocks(string presetId)
{
RoomPreset preset = LevelPresetCatalog.GetPreset(presetId);
Assert.IsNotNull(preset);
for (int y = 0; y < preset.GridHeight - 1; y++)
{
for (int x = 0; x < preset.GridWidth - 1; x++)
{
bool solidBlock =
preset.GridMap[x, y] == PresetCellType.Obstacle &&
preset.GridMap[x + 1, y] == PresetCellType.Obstacle &&
preset.GridMap[x, y + 1] == PresetCellType.Obstacle &&
preset.GridMap[x + 1, y + 1] == PresetCellType.Obstacle;
Assert.IsFalse(solidBlock, $"{presetId} must use broken LOS cover instead of a solid 2x2 obstacle block at ({x}, {y}).");
}
}
}
[Test]
public void Player_ColliderBottom_AlignsWithGroundAnchor_NoInvisibleAirGapAboveObstacles()
{
#if UNITY_EDITOR
GameObject playerPrefab = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>("Assets/_Project/Prefabs/Player/Player.prefab");
Assert.IsNotNull(playerPrefab);
CircleCollider2D col = playerPrefab.GetComponent<CircleCollider2D>();
Assert.IsNotNull(col);
// Feet anchor in Swordsman sprite is around -0.28 (local units)
float expectedFeetLocalY = -0.28f;
float colliderBottomLocalY = col.offset.y - col.radius;
// Collider bottom must align with the feet anchor within 0.02 units,
// NOT hang 0.22 units below the feet creating an invisible 20px barrier
Assert.AreEqual(expectedFeetLocalY, colliderBottomLocalY, 0.02f,
$"Player collider bottom ({colliderBottomLocalY:F3}) must align with feet ground anchor ({expectedFeetLocalY:F3}) so approaching obstacles from above does not leave an invisible air gap.");
#endif
}
[Test]
public void L01_InvalidPresetData_ThrowsValidationException()
{
// Case 1: Row length mismatch (17 chars instead of 18)
string[] shortRowGrid = new string[]
{
"□□□□□□□○○□□□□□□□□□",
"□□□■■■■■■■■■■□□□□", // 17 chars!
"□□■■◇■■■■■■◇■■□□□□",
"□□■■■■▲▲■■■■■■□□□□",
"○■■■■■▲▲■■■■■■■■■○",
"○■■☆■■■■■■■■◇■■■■○",
"□■■■■■■■■■■■■■■■■□",
"□■■■■▲▲■■■■■■■■■■□",
"□■■◇■▲▲■■■■◇■■■■■□",
"□□■■■■■■■■■■■■□□□□",
"□□□□□■■■■■■□□□□□□□",
"□□□□□□□○○□□□□□□□□□",
};
RoomPreset p1 = new RoomPreset { PresetId = "TEST_SHORT_ROW" };
var ex1 = Assert.Throws<System.ArgumentException>(() =>
LevelPresetCatalog.ParseGridAndAnchors(p1, shortRowGrid, 18, 12));
StringAssert.Contains("length mismatch", ex1.Message);
// Case 2: Row length mismatch (19 chars instead of 18)
string[] longRowGrid = new string[]
{
"□□□□□□□○○□□□□□□□□□",
"□□□■■■■■■■■■■□□□□□",
"□□■■◇■■■■■■◇■■□□□□",
"□□■■■■▲▲■■■■■■□□□□",
"○■■■■■▲▲■■■■■■■■■○",
"○■■☆■■■■■■■■◇■■■■○",
"□■■■■■■■■■■■■■■■■□",
"□■■■■▲▲■■■■■■■■■■□",
"□■■◇■▲▲■■■■◇■■■■■□",
"□□■■■■■■■■■■■■□□□□",
"□□□□□■■■■■■□□□□□□□□", // 19 chars!
"□□□□□□□○○□□□□□□□□□",
};
RoomPreset p2 = new RoomPreset { PresetId = "TEST_LONG_ROW" };
var ex2 = Assert.Throws<System.ArgumentException>(() =>
LevelPresetCatalog.ParseGridAndAnchors(p2, longRowGrid, 18, 12));
StringAssert.Contains("length mismatch", ex2.Message);
// Case 3: Unknown character
string[] unknownCharGrid = new string[]
{
"□□□□□□□○○□□□□□□□□□",
"□□□■■■■■■■■■■□□□□□",
"□□■■◇■■■■■■◇■■□□□□",
"□□■■■■▲▲■■■■■■□□□□",
"○■■■■■▲▲■■■■■■■■■○",
"○■■☆■■■■■■■■◇■■■■○",
"□■■■■■■■■■■■■■■■■□",
"□■■■■▲▲■■■■■■■■■■□",
"□■■◇■▲▲■■■■◇■■■■■□",
"□□■■■■■■■■■■■■□□□□",
"□□□□□■■■■■■□□□□□□□",
"□□□□□□□○○□□□□□□□□X", // 'X' is unknown!
};
RoomPreset p3 = new RoomPreset { PresetId = "TEST_UNKNOWN_CHAR" };
var ex3 = Assert.Throws<System.ArgumentException>(() =>
LevelPresetCatalog.ParseGridAndAnchors(p3, unknownCharGrid, 18, 12));
StringAssert.Contains("Unknown cell character", ex3.Message);
// Case 4: Spawn anchor inside obstacle
string[] obstacleSpawnGrid = new string[]
{
"□□□□□□□○○□□□□□□□□□",
"□□□■■■■■■■■■■□□□□□",
"□□■■◇■■■■■■◇■■□□□□",
"□□■■■■▲▲■■■■■■□□□□",
"○■■■■■▲▲■■■■■■■■■○",
"○■■☆■■■■■■■■◇■■■■○",
"□■■■■■■■■■■■■■■■■□",
"□■■■■▲▲■■■■■■■■■■□",
"□■■◇■▲▲■■■■◇■■■■■□",
"□□■■■■■■■■■■■■□□□□",
"□□□□□■■■■■■□□□□□□□",
"□□□□□□□○○□□□□□□□□□",
};
RoomPreset p4 = new RoomPreset { PresetId = "TEST_OBSTACLE_SPAWN" };
p4.Sockets.Add(new RoomPresetSocket("N1", StageDirection.North, new Vector2Int(7, 0), 2));
p4.Sockets.Add(new RoomPresetSocket("W1", StageDirection.West, new Vector2Int(0, 4), 2));
p4.Sockets.Add(new RoomPresetSocket("E1", StageDirection.East, new Vector2Int(17, 4), 2));
p4.Sockets.Add(new RoomPresetSocket("S1", StageDirection.South, new Vector2Int(7, 11), 2));
// Spawn point at (6, 3) which is Obstacle!
p4.SpawnPoints.Add(new RoomPresetSpawnPoint("M_BAD", SpawnRole.NormalMelee, new Vector2Int(6, 3)));
var ex4 = Assert.Throws<System.ArgumentException>(() =>
LevelPresetCatalog.ParseGridAndAnchors(p4, obstacleSpawnGrid, 18, 12));
StringAssert.Contains("cannot be on Obstacle", ex4.Message);
}
[Test]
public void S1A_NormalizedPreset_HasAccurateGeometrySocketsAndEncounterPool()
{
RoomPreset preset = LevelPresetCatalog.GetPreset("GRAVE_S1_COMBAT_A");
Assert.IsNotNull(preset);
Assert.AreEqual(18, preset.GridWidth);
Assert.AreEqual(12, preset.GridHeight);
// Sockets check
Assert.AreEqual(4, preset.Sockets.Count);
RoomPresetSocket? n1 = preset.GetSocket("N1");
RoomPresetSocket? w1 = preset.GetSocket("W1");
RoomPresetSocket? e1 = preset.GetSocket("E1");
RoomPresetSocket? s1 = preset.GetSocket("S1");
Assert.IsTrue(n1.HasValue && n1.Value.Direction == StageDirection.North && n1.Value.CellPosition == new Vector2Int(7, 0) && n1.Value.Width == 1);
Assert.IsTrue(w1.HasValue && w1.Value.Direction == StageDirection.West && w1.Value.CellPosition == new Vector2Int(0, 4) && w1.Value.Width == 1);
Assert.IsTrue(e1.HasValue && e1.Value.Direction == StageDirection.East && e1.Value.CellPosition == new Vector2Int(17, 4) && e1.Value.Width == 1);
Assert.IsTrue(s1.HasValue && s1.Value.Direction == StageDirection.South && s1.Value.CellPosition == new Vector2Int(7, 11) && s1.Value.Width == 1);
// Obstacle check: five approved cover cells.
int obstacleCount = 0;
for (int y = 0; y < 12; y++)
{
for (int x = 0; x < 18; x++)
{
if (preset.GridMap[x, y] == PresetCellType.Obstacle) obstacleCount++;
}
}
Assert.AreEqual(5, obstacleCount, "S1-A must keep five obstacle cells at approved room coordinates");
// Approved cover positions.
Assert.AreEqual(PresetCellType.Obstacle, preset.GridMap[6, 3]);
Assert.AreEqual(PresetCellType.Obstacle, preset.GridMap[6, 4]);
Assert.AreEqual(PresetCellType.Obstacle, preset.GridMap[12, 4]);
Assert.AreEqual(PresetCellType.Obstacle, preset.GridMap[10, 6]);
Assert.AreEqual(PresetCellType.Obstacle, preset.GridMap[10, 7]);
// Spawn points check: M01~M05
Assert.AreEqual(5, preset.SpawnPoints.Count);
Assert.IsTrue(preset.SpawnPoints.Exists(p => p.Id == "M01" && p.Role == SpawnRole.NormalMelee && p.CellPosition == new Vector2Int(4, 3)));
Assert.IsTrue(preset.SpawnPoints.Exists(p => p.Id == "M02" && p.Role == SpawnRole.NormalMelee && p.CellPosition == new Vector2Int(10, 2)));
Assert.IsTrue(preset.SpawnPoints.Exists(p => p.Id == "M03" && p.Role == SpawnRole.Crowd && p.CellPosition == new Vector2Int(11, 5)));
Assert.IsTrue(preset.SpawnPoints.Exists(p => p.Id == "M04" && p.Role == SpawnRole.NormalMelee && p.CellPosition == new Vector2Int(4, 8)));
Assert.IsTrue(preset.SpawnPoints.Exists(p => p.Id == "M05" && p.Role == SpawnRole.NormalMelee && p.CellPosition == new Vector2Int(12, 8)));
// EncounterPool check
Assert.AreEqual(3, preset.EncounterPool.Count);
RoomEncounter encA = preset.EncounterPool.Find(e => e.Name == "Encounter A");
Assert.IsNotNull(encA);
Assert.AreEqual(2, encA.Items.Count);
Assert.AreEqual("Bat", encA.Items[0].EnemyKind);
Assert.AreEqual(3, encA.Items[0].Count);
Assert.AreEqual("Skeleton", encA.Items[1].EnemyKind);
Assert.AreEqual(2, encA.Items[1].Count);
}
[Test]
public void RoomPreset_SocketPhysicalCenter_MatchesConfiguredWidth()
{
RoomPreset preset = LevelPresetCatalog.GetPreset("GRAVE_S1_COMBAT_A");
Assert.IsNotNull(preset);
// Width 1 sockets are centered on their declared cell.
// W1 cell (0,4) => local (-8.5, 1.5).
RoomPresetSocket w1 = preset.GetSocket("W1").Value;
Vector2 w1Center = preset.GetSocketCenterLocalPosition(w1);
Assert.AreEqual(-8.5f, w1Center.x, 0.001f);
Assert.AreEqual(1.5f, w1Center.y, 0.001f);
// E1 cell (17,4) => local (8.5, 1.5).
RoomPresetSocket e1 = preset.GetSocket("E1").Value;
Vector2 e1Center = preset.GetSocketCenterLocalPosition(e1);
Assert.AreEqual(8.5f, e1Center.x, 0.001f);
Assert.AreEqual(1.5f, e1Center.y, 0.001f);
// N1 cell (7,0) => local (-1.5, 5.5).
RoomPresetSocket n1 = preset.GetSocket("N1").Value;
Vector2 n1Center = preset.GetSocketCenterLocalPosition(n1);
Assert.AreEqual(-1.5f, n1Center.x, 0.001f);
Assert.AreEqual(5.5f, n1Center.y, 0.001f);
// S1 cell (7,11) => local (-1.5, -5.5).
RoomPresetSocket s1 = preset.GetSocket("S1").Value;
Vector2 s1Center = preset.GetSocketCenterLocalPosition(s1);
Assert.AreEqual(-1.5f, s1Center.x, 0.001f);
Assert.AreEqual(-5.5f, s1Center.y, 0.001f);
}
}
}