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

273 lines
12 KiB
C#

using BlockOutReplica.Domain;
using UnityEngine;
using BlockOutReplica.Gameplay;
namespace BlockOutReplica.Presentation
{
public sealed class DoorView : MonoBehaviour
{
private Transform visual;
private Vector3 baseScale;
private Vector3 basePosition;
private Transform shine;
private Renderer innerGlow;
private Renderer outerGlow;
private Vector3 shineBaseScale;
private Vector3 innerGlowBaseScale;
private Color innerGlowColor;
private Color outerGlowColor;
private float highlightBlend;
private bool isHighlighted;
private float acceptTime;
private DoorSide side;
private OriginalDoorExitEffect recoveredEffect;
public OriginalDoorExitEffect RecoveredEffect => recoveredEffect;
public bool IsExitEffectPlaying => recoveredEffect != null ? recoveredEffect.IsPlaying : acceptTime > 0f;
public bool IsEffectPaused { get; set; }
private bool pendingClose, isClosed;
private Sprite closureSprite;
private LevelDefinition closureLevel;
private DoorDefinition closureDefinition;
private OriginalDoorMechanicView mechanicView;
public void MarkCleared() => pendingClose = true;
public void ConfigureWallClosure(LevelDefinition level, DoorDefinition definition)
{ closureLevel = level; closureDefinition = definition; }
public void RefreshMechanics(int iceRemaining)
{ if (mechanicView != null) mechanicView.Refresh(iceRemaining); }
private void CloseClearedDoor()
{
if (isClosed) return;
if (closureLevel == null || closureDefinition == null || closureDefinition.Cells.Count == 0)
{
pendingClose = false;
Debug.LogWarning("Door closure requires ConfigureWallClosure(level, definition).", this);
return;
}
bool horizontal = side == DoorSide.Top || side == DoorSide.Bottom;
string cornerName = BoardRimLayout.ProfileNameFor(side);
var corner=Resources.Load<Sprite>("OriginalRecovered/"+cornerName);
if (corner == null)
{
pendingClose = false;
Debug.LogWarning("Missing original wall profile for cleared door: " + cornerName, this);
return;
}
Vector2 artSize = corner.rect.size / corner.pixelsPerUnit;
Rect bounds = BoardRimLayout.DoorBounds(closureLevel, closureDefinition, artSize);
closureSprite = BoardRimLayout.CreateStraightSprite(corner, horizontal);
var patch = new GameObject("Cleared Door Wall");
patch.transform.SetParent(transform, false);
patch.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
Vector3 center = new Vector3(bounds.center.x, BoardRimLayout.SurfaceHeight, bounds.center.y);
patch.transform.position = BoardRimLayout.CenteredSpriteOrigin(center, bounds.size,
closureSprite.bounds.size, closureSprite.bounds.center);
patch.transform.localScale = new Vector3(bounds.width / closureSprite.bounds.size.x,
bounds.height / closureSprite.bounds.size.y, 1f);
var renderer = patch.AddComponent<SpriteRenderer>();
renderer.sprite = closureSprite;
renderer.sortingOrder = 21;
isClosed = true; pendingClose = false;
if (visual != null) visual.gameObject.SetActive(false);
}
private void OnDestroy()
{
if (closureSprite == null) return;
if (Application.isPlaying) Destroy(closureSprite);
else DestroyImmediate(closureSprite);
}
public int ColorId { get; private set; }
public void Configure(int colorId, DoorSide doorSide, Transform visualRoot)
{
ColorId = colorId;
side = doorSide;
visual = visualRoot;
mechanicView = visual.gameObject.AddComponent<OriginalDoorMechanicView>();
mechanicView.Configure(doorSide, closureDefinition?.Cells.Count ?? 1);
RefreshMechanics(closureDefinition?.InitialIceCount ?? 0);
baseScale = visual.localScale;
basePosition = visual.localPosition;
shine = visual.Find("Door Highlight");
innerGlow = FindRenderer("Door Inner Glow");
outerGlow = FindRenderer("Door Outer Glow");
if (shine != null) shineBaseScale = shine.localScale;
if (innerGlow != null)
{
innerGlowBaseScale = innerGlow.transform.localScale;
innerGlowColor = innerGlow.material.color;
SetRendererAlpha(innerGlow, 0f);
}
if (outerGlow != null)
{
outerGlowColor = outerGlow.material.color;
SetRendererAlpha(outerGlow, 0f);
}
}
public void SetHighlighted(bool value)
{
isHighlighted = value;
}
public void PlayAccept()
{
if (recoveredEffect != null) { recoveredEffect.Play(); return; }
acceptTime = .67f;
}
public void PlayAccept(DoorExitPlan plan,BlockPieceView piece,bool playSound=true)
{
if(recoveredEffect==null){PlayAccept();return;}
Vector3 tangent=side==DoorSide.Top||side==DoorSide.Bottom?Vector3.right:Vector3.forward;
float minimum=float.PositiveInfinity,maximum=float.NegativeInfinity;
foreach(Vector2 cell in piece.LocalCells)
{
float projected=cell.x*tangent.x+cell.y*tangent.z;
minimum=Mathf.Min(minimum,projected-.5f);maximum=Mathf.Max(maximum,projected+.5f);
}
Vector3 crossing=plan.MouthPosition+tangent*(Vector3.Dot(plan.SnapPosition-plan.MouthPosition,tangent)+(minimum+maximum)*.5f);
// The exact source FxOffsetsForDirection values are not recovered.
// Keep the emitter on the actual clip/mouth plane, never prefab zero.
recoveredEffect.PlayAtCrossing(crossing,maximum-minimum,playSound);
}
public void ConfigureRecovered(OriginalDoorExitEffect effect) => recoveredEffect = effect;
private void LateUpdate()
{
if (isClosed) return;
if (pendingClose && !IsExitEffectPlaying) { CloseClearedDoor(); return; }
if (recoveredEffect != null)
{
recoveredEffect.Tick(Time.deltaTime, IsEffectPaused);
return;
}
if (visual == null) return;
float dt = Mathf.Min(Time.unscaledDeltaTime, .04f);
float target = isHighlighted || acceptTime > 0f ? 1f : 0f;
float blend = highlightBlend;
blend = Mathf.MoveTowards(blend, target, dt * 5.5f);
highlightBlend = blend;
float breathe = blend * (.025f + Mathf.Sin(Time.unscaledTime * 6.5f) * .018f);
float rebound = 0f;
if (acceptTime > 0f)
{
acceptTime = Mathf.Max(0f, acceptTime - dt);
float seconds = .67f - acceptTime;
rebound = EvaluateDoorDisplacement(seconds);
UpdateExitLayers(seconds);
}
else
{
ResetExitLayers();
}
visual.localScale = Vector3.Lerp(visual.localScale,
baseScale * (1f + breathe + Mathf.Abs(rebound) * .08f),
1f - Mathf.Exp(-dt * 20f));
Vector3 inward = side == DoorSide.Top ? Vector3.back
: side == DoorSide.Bottom ? Vector3.forward
: side == DoorSide.Left ? Vector3.right : Vector3.left;
visual.localPosition = Vector3.Lerp(visual.localPosition,
basePosition + inward * rebound, 1f - Mathf.Exp(-dt * 22f));
}
// Reconstructed from the shipped 2-cell directional DoorExit clips.
// The top clip has slightly later keyframes than the other directions.
private float EvaluateDoorDisplacement(float seconds)
{
if (side == DoorSide.Top)
return Piecewise(seconds,
new Vector2(0f, 0f), new Vector2(.25f, .20371f),
new Vector2(.3167f, .23f), new Vector2(.4667f, .00182f),
new Vector2(.5667f, .022f), new Vector2(.6667f, 0f));
float secondary = side == DoorSide.Right ? .033f : .011f;
return Piecewise(seconds,
new Vector2(0f, 0f), new Vector2(.25f, .213f),
new Vector2(.30f, .23f), new Vector2(.45f, 0f),
new Vector2(.55f, secondary), new Vector2(.6667f, 0f));
}
private void UpdateExitLayers(float seconds)
{
if (shine != null)
{
float stretch = Piecewise(seconds,
new Vector2(0f, 1f), new Vector2(.20f, 1f),
new Vector2(.30f, side == DoorSide.Left || side == DoorSide.Top ? 2.24f : 2.84f),
new Vector2(.50f, 1f), new Vector2(.6667f, 1f));
shine.localScale = new Vector3(shineBaseScale.x * stretch,
shineBaseScale.y, shineBaseScale.z);
}
float innerAlpha = Piecewise(seconds,
new Vector2(0f, 0f), new Vector2(.0667f, .7843f),
new Vector2(.3667f, .7843f), new Vector2(.50f, 0f),
new Vector2(.6667f, 0f));
if (innerGlow != null)
{
SetRendererAlpha(innerGlow, innerAlpha);
float expand = Piecewise(seconds,
new Vector2(0f, .7f), new Vector2(.20f, 1.12f),
new Vector2(.3667f, 1.28f), new Vector2(.50f, .9f),
new Vector2(.6667f, .7f));
innerGlow.transform.localScale = innerGlowBaseScale * expand;
}
if (outerGlow != null)
{
float outerAlpha = Piecewise(seconds,
new Vector2(0f, 0f), new Vector2(.40f, 0f),
new Vector2(.4167f, .392f), new Vector2(.60f, 0f),
new Vector2(.6667f, 0f));
SetRendererAlpha(outerGlow, outerAlpha);
}
}
private void ResetExitLayers()
{
if (shine != null) shine.localScale = shineBaseScale;
if (innerGlow != null)
{
innerGlow.transform.localScale = innerGlowBaseScale;
SetRendererAlpha(innerGlow, 0f);
}
if (outerGlow != null) SetRendererAlpha(outerGlow, 0f);
}
private Renderer FindRenderer(string childName)
{
Transform child = visual.Find(childName);
return child == null ? null : child.GetComponent<Renderer>();
}
private void SetRendererAlpha(Renderer target, float alpha)
{
if (target == null || target.material == null || !target.material.HasProperty("_Color")) return;
Color color = target == innerGlow ? innerGlowColor : outerGlowColor;
color.a = Mathf.Clamp01(alpha);
target.material.color = color;
}
private static float Piecewise(float time, params Vector2[] keys)
{
if (keys == null || keys.Length == 0) return 0f;
if (time <= keys[0].x) return keys[0].y;
for (int i = 1; i < keys.Length; i++)
{
if (time > keys[i].x) continue;
float t = Mathf.InverseLerp(keys[i - 1].x, keys[i].x, time);
t = t * t * (3f - 2f * t);
return Mathf.Lerp(keys[i - 1].y, keys[i].y, t);
}
return keys[keys.Length - 1].y;
}
}
}