273 lines
12 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|