162 lines
6.7 KiB
C#
162 lines
6.7 KiB
C#
using BlockOutReplica.Domain;
|
|
using BlockOutReplica.Presentation;
|
|
using UnityEngine;
|
|
|
|
namespace BlockOutReplica.Gameplay
|
|
{
|
|
public sealed class BlockPieceView : MonoBehaviour
|
|
{
|
|
private Transform visualRoot;
|
|
private Renderer[] visualRenderers;
|
|
private Vector3 filteredMotion;
|
|
private float dragBlend;
|
|
private float snapTime;
|
|
private float blockedTime;
|
|
private float exitProgress = -1f;
|
|
private static Material doorClipMaterial;
|
|
private static readonly int DoorClipPlaneId = Shader.PropertyToID("_DoorClipPlane");
|
|
private OriginalPieceMechanicView mechanicView;
|
|
|
|
public int ColorId { get; private set; }
|
|
public Vector2 Size { get; private set; }
|
|
public Vector2[] LocalCells { get; private set; }
|
|
public Vector3 InitialBoardPosition { get; private set; }
|
|
|
|
public void Configure(PieceDefinition definition, Vector2 size, LevelDefinition level,
|
|
Transform visual)
|
|
{
|
|
ColorId = definition.ColorId;
|
|
Size = size;
|
|
InitialBoardPosition = transform.position;
|
|
LocalCells = new Vector2[definition.Cells.Count];
|
|
Vector3 origin = transform.position;
|
|
for (int i = 0; i < definition.Cells.Count; i++)
|
|
{
|
|
Vector3 world = level.GridToWorld(definition.Cells[i]);
|
|
LocalCells[i] = new Vector2(world.x - origin.x, world.z - origin.z);
|
|
}
|
|
visualRoot = visual;
|
|
mechanicView = visualRoot.gameObject.AddComponent<OriginalPieceMechanicView>();
|
|
mechanicView.Configure(definition.Cells, level, origin);
|
|
RefreshMechanics(definition.InitialIceCount, definition.HasLayer, definition.LayerColorId);
|
|
visualRenderers = visualRoot.GetComponentsInChildren<Renderer>(true);
|
|
}
|
|
|
|
public void RefreshMechanics(int iceRemaining, bool hasLayer, int layerColorId, bool curtainLocked = false)
|
|
{
|
|
if (mechanicView != null) mechanicView.Refresh(iceRemaining, hasLayer, layerColorId, curtainLocked);
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
if (visualRoot == null) return;
|
|
// Feedback must never accumulate Euler rotations on flat atlas sprites.
|
|
// Multiplying a collision tilt each frame used to turn the whole block over.
|
|
visualRoot.localRotation = Quaternion.identity;
|
|
float dt = Mathf.Min(Time.unscaledDeltaTime, .04f);
|
|
float response = 1f - Mathf.Exp(-dt * 18f);
|
|
|
|
if (exitProgress >= 0f)
|
|
{
|
|
// The rigid block disappears progressively behind the door plane.
|
|
// Its whole silhouette must not stretch, rotate, or fade in midair.
|
|
visualRoot.localScale = Vector3.Lerp(visualRoot.localScale,
|
|
Vector3.one, response);
|
|
visualRoot.localPosition = Vector3.Lerp(visualRoot.localPosition, Vector3.zero, response);
|
|
return;
|
|
}
|
|
|
|
dragBlend = Mathf.MoveTowards(dragBlend, isDragging ? 1f : 0f, dt * 9f);
|
|
filteredMotion = Vector3.Lerp(filteredMotion, dragMotion, response);
|
|
|
|
float pulse = 0f;
|
|
if (snapTime > 0f)
|
|
{
|
|
snapTime = Mathf.Max(0f, snapTime - dt);
|
|
float phase = 1f - snapTime / .24f;
|
|
pulse = Mathf.Sin(phase * Mathf.PI * 2.25f) * (1f - phase) * .065f;
|
|
}
|
|
float blockedPulse = 0f;
|
|
if (blockedTime > 0f)
|
|
{
|
|
blockedTime = Mathf.Max(0f, blockedTime - dt);
|
|
float phase = 1f - blockedTime / .16f;
|
|
blockedPulse = -Mathf.Sin(phase * Mathf.PI) * .055f;
|
|
}
|
|
float scale = 1f + dragBlend * .035f + pulse + blockedPulse;
|
|
visualRoot.localScale = Vector3.Lerp(visualRoot.localScale,
|
|
new Vector3(scale, scale, scale), response);
|
|
visualRoot.localPosition = Vector3.Lerp(visualRoot.localPosition,
|
|
Vector3.up * (dragBlend * .025f), response);
|
|
}
|
|
|
|
private bool isDragging;
|
|
private Vector3 dragMotion;
|
|
|
|
public void SetDragging(bool value)
|
|
{
|
|
isDragging = value;
|
|
if (!value) dragMotion = Vector3.zero;
|
|
}
|
|
|
|
public void SetDragMotion(Vector3 delta)
|
|
{
|
|
dragMotion = Vector3.ClampMagnitude(delta, 1.6f);
|
|
}
|
|
|
|
public void PlaySnap()
|
|
{
|
|
snapTime = .24f;
|
|
}
|
|
|
|
public void PlayBlocked(Vector3 attemptedDirection)
|
|
{
|
|
if (blockedTime > .02f) return;
|
|
blockedTime = .16f;
|
|
}
|
|
|
|
public void SetExitProgress(float progress)
|
|
{
|
|
isDragging = false;
|
|
exitProgress = Mathf.Clamp01(progress);
|
|
}
|
|
|
|
public void BeginDoorExit(DoorExitPlan plan)
|
|
{
|
|
isDragging = false;
|
|
exitProgress = 0f;
|
|
// Counts are resolved before an exit. Do not replace SDF number materials
|
|
// with the sprite clipping shader (that would draw the entire font atlas).
|
|
if (mechanicView != null) mechanicView.HideForExit();
|
|
foreach (Collider collider in GetComponentsInChildren<Collider>()) collider.enabled = false;
|
|
if (doorClipMaterial == null)
|
|
{
|
|
Shader shader = Shader.Find("BlockOut/ClippedSprite");
|
|
if (shader == null) throw new System.InvalidOperationException("Door clip shader was not imported.");
|
|
doorClipMaterial = new Material(shader) { name = "Door Directional Sprite Clip" };
|
|
}
|
|
var properties = new MaterialPropertyBlock();
|
|
Vector4 plane = new Vector4(plan.Direction.x, plan.Direction.z,
|
|
Vector3.Dot(plan.MouthPosition, plan.Direction), 0f);
|
|
foreach (Renderer renderer in visualRenderers)
|
|
{
|
|
if (!renderer.gameObject.activeInHierarchy) continue;
|
|
Color tint = renderer.sharedMaterial != null && renderer.sharedMaterial.HasProperty("_Color")
|
|
? renderer.sharedMaterial.color : Color.white;
|
|
renderer.sharedMaterial = doorClipMaterial;
|
|
renderer.GetPropertyBlock(properties);
|
|
properties.SetVector(DoorClipPlaneId, plane);
|
|
if (!(renderer is SpriteRenderer)) properties.SetColor("_Color", tint);
|
|
renderer.SetPropertyBlock(properties);
|
|
renderer.sortingOrder -= 20;
|
|
}
|
|
}
|
|
|
|
public Vector2 GetCellWorld(int index, Vector3 candidate)
|
|
{
|
|
Vector2 offset = LocalCells[index];
|
|
return new Vector2(candidate.x + offset.x, candidate.z + offset.y);
|
|
}
|
|
}
|
|
}
|