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(); mechanicView.Configure(definition.Cells, level, origin); RefreshMechanics(definition.InitialIceCount, definition.HasLayer, definition.LayerColorId); visualRenderers = visualRoot.GetComponentsInChildren(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.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); } } }