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

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