240 lines
11 KiB
C#
240 lines
11 KiB
C#
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<int,bool> canUseDoor;
|
|
private readonly List<Vector2> interiorHoles = new List<Vector2>();
|
|
public BlockOutMovementRules(LevelDefinition level, System.Func<int,bool> canUseDoor = null)
|
|
{
|
|
this.level = level;
|
|
this.canUseDoor = canUseDoor;
|
|
var playable = new HashSet<GridPosition>(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<BlockPieceView> 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<moving.LocalCells.Length;i++)
|
|
{
|
|
Vector2 cell=moving.GetCellWorld(i,candidate);
|
|
foreach(Vector2 hole in interiorHoles)
|
|
if(Mathf.Abs(cell.x-hole.x)<.98f&&Mathf.Abs(cell.y-hole.y)<.98f)return false;
|
|
}
|
|
for (int i = 0; i < activePieces.Count; i++)
|
|
{
|
|
BlockPieceView other = activePieces[i];
|
|
if (other == null || other == moving) continue;
|
|
for (int a = 0; a < moving.LocalCells.Length; a++)
|
|
for (int b = 0; b < other.LocalCells.Length; b++)
|
|
{
|
|
Vector2 p = moving.GetCellWorld(a, candidate);
|
|
Vector2 q = other.GetCellWorld(b, other.transform.position);
|
|
if (Mathf.Abs(p.x - q.x) < .98f && Mathf.Abs(p.y - q.y) < .98f)
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public bool TryPlanDoorExit(BlockPieceView piece, Vector3 current,
|
|
IReadOnlyList<BlockPieceView> 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<BlockPieceView> 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<BlockPieceView> 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;
|
|
}
|
|
|
|
}
|
|
}
|