using System.Collections.Generic; using BlockOutReplica.Domain; using UnityEngine; namespace BlockOutReplica.Gameplay { public readonly struct DoorExitPlan { public readonly int DoorIndex; public readonly DoorSide Side; public readonly Vector3 Direction; public readonly Vector3 MouthPosition; public readonly Vector3 SnapPosition; public readonly Vector3 ClearPosition; public DoorExitPlan(int doorIndex, DoorSide side, Vector3 direction, Vector3 mouthPosition, Vector3 snapPosition, Vector3 clearPosition) { DoorIndex = doorIndex; Side = side; Direction = direction; MouthPosition = mouthPosition; SnapPosition = snapPosition; ClearPosition = clearPosition; } } public sealed class BlockOutMovementRules { // Reconstruction tuning, not recovered numeric values from the IL2CPP methods. public const float DoorCaptureDistance = .28f; public const float DoorAlignmentTolerance = .14f; public const float AdditionalExitOffset = .16f; private readonly LevelDefinition level; private readonly System.Func canUseDoor; private readonly List interiorHoles = new List(); public BlockOutMovementRules(LevelDefinition level, System.Func canUseDoor = null) { this.level = level; this.canUseDoor = canUseDoor; var playable = new HashSet(level.Cells); for(int x=level.MinX;x<=level.MaxX;x++) for(int y=level.MinY;y<=level.MaxY;y++) { var cell=new GridPosition(x,y); if(playable.Contains(cell))continue; Vector3 world=level.GridToWorld(cell); interiorHoles.Add(new Vector2(world.x,world.z)); } } public bool IsAllowed(BlockPieceView moving, Vector3 candidate, IReadOnlyList activePieces) { if (!AllCellsInsideBoardOrMatchingCorridor(moving, candidate)) return false; // CMS is the playable footprint, not necessarily its bounding box. // Levels 8/9 and later contain cutouts that must remain solid. for(int i=0;i activePieces, Vector3 pointerDelta, bool released, out DoorExitPlan plan) { plan = default; float bestDistance = float.PositiveInfinity; bool found = false; // There are shipped levels with several doors of the same color. for (int index = 0; index < level.Doors.Count; index++) { DoorDefinition door = level.Doors[index]; if(canUseDoor!=null&&!canUseDoor(index))continue; if (door.ColorId != piece.ColorId) continue; Vector3 direction = DirectionFor(door.Side); if (!released && Vector3.Dot(pointerDelta, direction) <= .0001f) continue; Vector3 tangent = door.Side == DoorSide.Top || door.Side == DoorSide.Bottom ? Vector3.right : Vector3.forward; Vector3 mouth = level.AverageWorld(door.Cells, current.y) - direction * .5f; GetProjectionBounds(piece, direction, out float minDepth, out float maxDepth); GetProjectionBounds(piece, tangent, out float minWidth, out float maxWidth); GetDoorSpan(door, tangent, out float doorMin, out float doorMax); if (maxWidth - minWidth > doorMax - doorMin + .001f) continue; float minRoot = doorMin - minWidth, maxRoot = doorMax - maxWidth; float currentTangent = Vector3.Dot(current, tangent); float snapTangent = Mathf.Clamp(currentTangent, minRoot, maxRoot); float lateralError = Mathf.Abs(currentTangent - snapTangent); float mouthDepth = Vector3.Dot(mouth, direction); float currentDepth = Vector3.Dot(current, direction); float gap = mouthDepth - (currentDepth + maxDepth); if (gap > DoorCaptureDistance || lateralError > DoorAlignmentTolerance) continue; Vector3 snap = current + tangent * (snapTangent - currentTangent) + direction * Mathf.Max(0f, mouthDepth - maxDepth - currentDepth); Vector3 clear = snap + direction * Mathf.Max(0f, mouthDepth - minDepth + AdditionalExitOffset - Vector3.Dot(snap, direction)); // A free endpoint is insufficient: both stages must be unobstructed. if (!IsPathClear(piece, current, snap, activePieces) || !IsPathClear(piece, snap, clear, activePieces)) continue; float distance = (snap - current).sqrMagnitude; if (distance >= bestDistance) continue; plan = new DoorExitPlan(index, door.Side, direction, mouth, snap, clear); bestDistance = distance; found = true; } return found; } public static Vector3 DirectionFor(DoorSide side) { switch (side) { case DoorSide.Top: return Vector3.forward; case DoorSide.Bottom: return Vector3.back; case DoorSide.Left: return Vector3.left; default: return Vector3.right; } } public bool IsPathClear(BlockPieceView piece, Vector3 start, Vector3 target, IReadOnlyList activePieces) { int steps = Mathf.Max(1, Mathf.CeilToInt(Vector3.Distance(start, target) / .08f)); for (int i = 0; i <= steps; i++) { if (!IsAllowed(piece, Vector3.Lerp(start, target, i / (float)steps), activePieces)) return false; } return true; } public Vector3 FindNearestLegalSit(BlockPieceView piece, Vector3 current, IReadOnlyList activePieces) { Vector3 origin = piece.InitialBoardPosition; Vector3 basePosition = new Vector3( origin.x + Mathf.Round(current.x - origin.x), current.y, origin.z + Mathf.Round(current.z - origin.z)); if (IsPathClear(piece, current, basePosition, activePieces)) return basePosition; Vector3 best = piece.transform.position; float bestDistance = float.PositiveInfinity; for (int radius = 1; radius <= 3; radius++) for (int x = -radius; x <= radius; x++) for (int z = -radius; z <= radius; z++) { if (Mathf.Max(Mathf.Abs(x), Mathf.Abs(z)) != radius) continue; Vector3 candidate = basePosition + new Vector3(x, 0f, z); if (!IsPathClear(piece, current, candidate, activePieces)) continue; float distance = (candidate - current).sqrMagnitude; if (distance < bestDistance) { bestDistance = distance; best = candidate; } } return best; } private static void GetProjectionBounds(BlockPieceView piece, Vector3 axis, out float minimum, out float maximum) { minimum = float.PositiveInfinity; maximum = float.NegativeInfinity; foreach (Vector2 cell in piece.LocalCells) { float value = cell.x * axis.x + cell.y * axis.z; minimum = Mathf.Min(minimum, value - .5f); maximum = Mathf.Max(maximum, value + .5f); } } private void GetDoorSpan(DoorDefinition door, Vector3 tangent, out float minimum, out float maximum) { minimum = float.PositiveInfinity; maximum = float.NegativeInfinity; foreach (GridPosition cell in door.Cells) { float value = Vector3.Dot(level.GridToWorld(cell), tangent); minimum = Mathf.Min(minimum, value - .5f); maximum = Mathf.Max(maximum, value + .5f); } } private bool AllCellsInsideBoardOrMatchingCorridor(BlockPieceView piece, Vector3 candidate) { for (int i = 0; i < piece.LocalCells.Length; i++) { Vector2 cell = piece.GetCellWorld(i, candidate); if (!CellInsideBoardOrMatchingCorridor(piece.ColorId, cell)) return false; } return true; } private bool CellInsideBoardOrMatchingCorridor(int colorId, Vector2 cell) { float halfWidth = (level.MaxX - level.MinX + 1) * .5f; float halfHeight = (level.MaxY - level.MinY + 1) * .5f; if (cell.x >= -halfWidth + .49f && cell.x <= halfWidth - .49f && cell.y >= -halfHeight + .49f && cell.y <= halfHeight - .49f) return true; for (int i = 0; i < level.Doors.Count; i++) { DoorDefinition door = level.Doors[i]; if(canUseDoor!=null&&!canUseDoor(i))continue; if (door.ColorId != colorId) continue; Vector3 tangent = door.Side == DoorSide.Top || door.Side == DoorSide.Bottom ? Vector3.right : Vector3.forward; GetDoorSpan(door, tangent, out float minimum, out float maximum); float transverse = cell.x * tangent.x + cell.y * tangent.z; if (transverse < minimum + .49f || transverse > maximum - .49f) continue; switch (door.Side) { case DoorSide.Top: if (cell.y >= -halfHeight + .49f) return true; break; case DoorSide.Bottom: if (cell.y <= halfHeight - .49f) return true; break; case DoorSide.Left: if (cell.x <= halfWidth - .49f) return true; break; case DoorSide.Right: if (cell.x >= -halfWidth + .49f) return true; break; } } return false; } } }