Files
blockout/BlockOutReplica/Assets/Scripts/Gameplay/BlockOutMovementRules.cs
T
2026-10-20 13:21:04 +08:00

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;
}
}
}