493 lines
15 KiB
C#
493 lines
15 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using CustomComponent;
|
|
using Event;
|
|
using GameFramework.Event;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.Serialization;
|
|
using UnityGameFramework.Runtime;
|
|
|
|
namespace UI
|
|
{
|
|
/// <summary>
|
|
/// MVC UI form for GameplayA. It builds slots and draggable parts from external data.
|
|
/// </summary>
|
|
public class CombineForm : UGuiForm
|
|
{
|
|
#region Property
|
|
|
|
[SerializeField] private RectTransform _slotRoot;
|
|
|
|
[SerializeField] private RectTransform _partRoot;
|
|
|
|
[SerializeField] private CombineDraggablePart _partPrefab;
|
|
|
|
[FormerlySerializedAs("_progressText")] [SerializeField]
|
|
private TMP_Text _hintText;
|
|
|
|
[SerializeField] private Vector2 _partStartAnchoredPosition = new(0f, -40f);
|
|
|
|
[SerializeField] private float _partVerticalSpacing = 120f;
|
|
|
|
[SerializeField] [Range(0f, 1f)] private float _partSpawnMinXNormalized = 0.55f;
|
|
|
|
[SerializeField] [Range(0f, 1f)] private float _partSpawnMaxXNormalized = 0.95f;
|
|
|
|
[SerializeField] [Range(0f, 1f)] private float _partSpawnMinYNormalized = 0.1f;
|
|
|
|
[SerializeField] [Range(0f, 1f)] private float _partSpawnMaxYNormalized = 0.9f;
|
|
|
|
[SerializeField] private float _completeToFinalStageDelay = 0.35f;
|
|
|
|
[SerializeField] private float _finalStageToCloseDelay = 0.35f;
|
|
|
|
private CombineComponent _controller;
|
|
|
|
private CombineStageOverlay _stageOverlay;
|
|
|
|
private Coroutine _completeFlowCoroutine;
|
|
|
|
private bool _completionFlowStarted;
|
|
|
|
private bool _fireCompletedEventOnClose;
|
|
|
|
private readonly List<GameObject> _runtimeNodes = new();
|
|
|
|
#endregion
|
|
|
|
#region FSM
|
|
|
|
protected override void OnInit(object userData)
|
|
{
|
|
base.OnInit(userData);
|
|
|
|
if (_controller == null)
|
|
{
|
|
_controller = GameEntry.Combine;
|
|
}
|
|
|
|
ResetTexts();
|
|
}
|
|
|
|
protected override void OnOpen(object userData)
|
|
{
|
|
base.OnOpen(userData);
|
|
|
|
if (_controller == null)
|
|
{
|
|
_controller = GameEntry.Combine;
|
|
}
|
|
|
|
if (userData is CombineFormContext context)
|
|
{
|
|
if (!CanBuildContext(context))
|
|
{
|
|
Log.Warning("CombineMvcForm open failed. StructurePrefab is invalid.");
|
|
return;
|
|
}
|
|
|
|
Build(context);
|
|
_controller.BindRuntimeContext(_slotRoot, _partRoot);
|
|
|
|
GameEntry.Event.Subscribe(CombineFeedbackEventArgs.EventId, OnFeedbackChanged);
|
|
GameEntry.Event.Subscribe(CombineProgressEventArgs.EventId, OnCombineProgressChanged);
|
|
|
|
_completionFlowStarted = false;
|
|
_fireCompletedEventOnClose = false;
|
|
|
|
if (context.AutoStart)
|
|
{
|
|
_controller.StartPuzzle();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log.Error("CombineMvcForm open failed. userData is invalid.");
|
|
}
|
|
}
|
|
|
|
private static bool CanBuildContext(CombineFormContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool hasStructurePrefab = context.StructurePrefab != null;
|
|
return hasStructurePrefab;
|
|
}
|
|
|
|
protected override void OnClose(bool isShutdown, object userData)
|
|
{
|
|
GameEntry.Event.Unsubscribe(CombineFeedbackEventArgs.EventId, OnFeedbackChanged);
|
|
GameEntry.Event.Unsubscribe(CombineProgressEventArgs.EventId, OnCombineProgressChanged);
|
|
|
|
StopCompleteFlow();
|
|
|
|
if (_fireCompletedEventOnClose)
|
|
{
|
|
_fireCompletedEventOnClose = false;
|
|
GameEntry.Event.Fire(this, CombineCompletedEventArgs.Create());
|
|
}
|
|
|
|
if (_controller != null)
|
|
{
|
|
_controller.ClearRuntimeContext();
|
|
}
|
|
|
|
ClearRuntimeNodes();
|
|
|
|
ResetTexts();
|
|
|
|
base.OnClose(isShutdown, userData);
|
|
}
|
|
|
|
protected override void OnPause()
|
|
{
|
|
base.OnPause();
|
|
|
|
if (_controller != null)
|
|
{
|
|
_controller.PausePuzzle();
|
|
}
|
|
}
|
|
|
|
protected override void OnResume()
|
|
{
|
|
base.OnResume();
|
|
|
|
if (_controller != null)
|
|
{
|
|
_controller.ResumePuzzle();
|
|
}
|
|
}
|
|
|
|
protected override void OnCover()
|
|
{
|
|
base.OnCover();
|
|
|
|
if (_controller != null)
|
|
{
|
|
_controller.PausePuzzle();
|
|
}
|
|
}
|
|
|
|
protected override void OnReveal()
|
|
{
|
|
base.OnReveal();
|
|
|
|
if (_controller != null)
|
|
{
|
|
_controller.ResumePuzzle();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Other Methods
|
|
|
|
private void ClearRuntimeNodes()
|
|
{
|
|
_stageOverlay = null;
|
|
|
|
for (int i = _runtimeNodes.Count - 1; i >= 0; i--)
|
|
{
|
|
GameObject node = _runtimeNodes[i];
|
|
if (node != null)
|
|
{
|
|
GameObject.Destroy(node);
|
|
}
|
|
}
|
|
|
|
_runtimeNodes.Clear();
|
|
}
|
|
|
|
private void ResetTexts()
|
|
{
|
|
if (_hintText != null)
|
|
{
|
|
_hintText.text = string.Empty;
|
|
}
|
|
}
|
|
|
|
private void Build(CombineFormContext context)
|
|
{
|
|
ClearRuntimeNodes();
|
|
|
|
List<CombineSlot> runtimeSlots = BuildSlots(context);
|
|
if (runtimeSlots.Count == 0)
|
|
{
|
|
Log.Warning("CombineMvcForm build failed. No CombineSlot found in structure prefab.");
|
|
return;
|
|
}
|
|
|
|
List<Sprite> partSprites = ClonePartSprites(context.PartSprites);
|
|
BuildParts(partSprites, runtimeSlots);
|
|
}
|
|
|
|
private List<CombineSlot> BuildSlots(CombineFormContext context)
|
|
{
|
|
var runtimeSlots = new List<CombineSlot>();
|
|
if (context.StructurePrefab == null)
|
|
{
|
|
return runtimeSlots;
|
|
}
|
|
|
|
GameObject structureNode = Instantiate(context.StructurePrefab, _slotRoot, false);
|
|
CenterStructureNode(structureNode);
|
|
_runtimeNodes.Add(structureNode);
|
|
|
|
CombineStageOverlay stageOverlay = structureNode.GetComponentInChildren<CombineStageOverlay>(true);
|
|
if (stageOverlay != null)
|
|
{
|
|
_stageOverlay = stageOverlay;
|
|
List<CombineSlot> managedSlots = stageOverlay.GetManagedSlots();
|
|
if (managedSlots.Count > 0)
|
|
{
|
|
return managedSlots;
|
|
}
|
|
}
|
|
|
|
CombineSlot[] structureSlots = structureNode.GetComponentsInChildren<CombineSlot>(true);
|
|
for (int i = 0; i < structureSlots.Length; i++)
|
|
{
|
|
CombineSlot slot = structureSlots[i];
|
|
if (slot != null)
|
|
{
|
|
runtimeSlots.Add(slot);
|
|
}
|
|
}
|
|
|
|
return runtimeSlots;
|
|
}
|
|
|
|
private void BuildParts(List<Sprite> partSprites, List<CombineSlot> runtimeSlots)
|
|
{
|
|
if (_partPrefab == null)
|
|
{
|
|
Log.Warning("CombineMvcForm build failed. Part prefab is missing.");
|
|
return;
|
|
}
|
|
|
|
List<CombineSlot> orderedSlots = new List<CombineSlot>(runtimeSlots);
|
|
orderedSlots.Sort((a, b) => a.BuildOrder.CompareTo(b.BuildOrder));
|
|
|
|
int spawnCount = Mathf.Min(partSprites.Count, orderedSlots.Count);
|
|
if (partSprites.Count != orderedSlots.Count)
|
|
{
|
|
Log.Warning("CombineMvcForm build mismatch. partSprites={0}, slots={1}.", partSprites.Count.ToString(),
|
|
orderedSlots.Count.ToString());
|
|
}
|
|
|
|
for (int i = 0; i < spawnCount; i++)
|
|
{
|
|
CombineSlot slot = orderedSlots[i];
|
|
if (slot == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
CombineDraggablePart part = Instantiate(_partPrefab, _partRoot, false);
|
|
part.ConfigureRuntime(slot.RequiredPartType, partSprites[i]);
|
|
SetPartSpawnPosition(part, i);
|
|
_runtimeNodes.Add(part.gameObject);
|
|
}
|
|
}
|
|
|
|
private static List<Sprite> ClonePartSprites(List<Sprite> source)
|
|
{
|
|
var result = new List<Sprite>();
|
|
if (source == null)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
result.Add(source[i]);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static void CenterStructureNode(GameObject structureNode)
|
|
{
|
|
if (structureNode == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RectTransform rectTransform = structureNode.transform as RectTransform;
|
|
if (rectTransform != null)
|
|
{
|
|
rectTransform.anchoredPosition = Vector2.zero;
|
|
rectTransform.localScale = Vector3.one;
|
|
return;
|
|
}
|
|
|
|
structureNode.transform.localPosition = Vector3.zero;
|
|
structureNode.transform.localScale = Vector3.one;
|
|
}
|
|
|
|
private void SetPartSpawnPosition(CombineDraggablePart part, int index)
|
|
{
|
|
RectTransform partRect = part.transform as RectTransform;
|
|
if (partRect == null || _partRoot == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Rect rootRect = _partRoot.rect;
|
|
if (rootRect.width <= 0f || rootRect.height <= 0f)
|
|
{
|
|
partRect.anchoredPosition = _partStartAnchoredPosition + Vector2.down * (_partVerticalSpacing * index);
|
|
ClampAnchoredPositionToPartRoot(partRect);
|
|
return;
|
|
}
|
|
|
|
float minX = Mathf.Lerp(rootRect.xMin, rootRect.xMax,
|
|
Mathf.Min(_partSpawnMinXNormalized, _partSpawnMaxXNormalized));
|
|
float maxX = Mathf.Lerp(rootRect.xMin, rootRect.xMax,
|
|
Mathf.Max(_partSpawnMinXNormalized, _partSpawnMaxXNormalized));
|
|
float minY = Mathf.Lerp(rootRect.yMin, rootRect.yMax,
|
|
Mathf.Min(_partSpawnMinYNormalized, _partSpawnMaxYNormalized));
|
|
float maxY = Mathf.Lerp(rootRect.yMin, rootRect.yMax,
|
|
Mathf.Max(_partSpawnMinYNormalized, _partSpawnMaxYNormalized));
|
|
|
|
Vector2 size = GetPartSize(partRect);
|
|
Vector2 xRange = BuildSafeAxisRange(minX, maxX, rootRect.xMin, rootRect.xMax, size.x, partRect.pivot.x);
|
|
Vector2 yRange = BuildSafeAxisRange(minY, maxY, rootRect.yMin, rootRect.yMax, size.y, partRect.pivot.y);
|
|
|
|
partRect.anchoredPosition = new Vector2(
|
|
Random.Range(xRange.x, xRange.y),
|
|
Random.Range(yRange.x, yRange.y));
|
|
}
|
|
|
|
private static Vector2 BuildSafeAxisRange(float desiredMin, float desiredMax, float containerMin, float containerMax,
|
|
float partSize, float pivot)
|
|
{
|
|
float min = Mathf.Min(desiredMin, desiredMax);
|
|
float max = Mathf.Max(desiredMin, desiredMax);
|
|
|
|
float size = Mathf.Max(0f, partSize);
|
|
float safeMin = containerMin + size * Mathf.Clamp01(pivot);
|
|
float safeMax = containerMax - size * (1f - Mathf.Clamp01(pivot));
|
|
|
|
if (safeMin > safeMax)
|
|
{
|
|
float center = (containerMin + containerMax) * 0.5f;
|
|
safeMin = center;
|
|
safeMax = center;
|
|
}
|
|
|
|
min = Mathf.Clamp(min, safeMin, safeMax);
|
|
max = Mathf.Clamp(max, safeMin, safeMax);
|
|
|
|
if (min > max)
|
|
{
|
|
float temp = min;
|
|
min = max;
|
|
max = temp;
|
|
}
|
|
|
|
return new Vector2(min, max);
|
|
}
|
|
|
|
private static Vector2 GetPartSize(RectTransform partRect)
|
|
{
|
|
Rect rect = partRect.rect;
|
|
Vector3 scale = partRect.localScale;
|
|
return new Vector2(
|
|
Mathf.Abs(rect.width * scale.x),
|
|
Mathf.Abs(rect.height * scale.y));
|
|
}
|
|
|
|
private void ClampAnchoredPositionToPartRoot(RectTransform partRect)
|
|
{
|
|
if (partRect == null || _partRoot == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Rect rootRect = _partRoot.rect;
|
|
Vector2 anchored = partRect.anchoredPosition;
|
|
Vector2 size = GetPartSize(partRect);
|
|
|
|
Vector2 xRange = BuildSafeAxisRange(anchored.x, anchored.x, rootRect.xMin, rootRect.xMax, size.x, partRect.pivot.x);
|
|
Vector2 yRange = BuildSafeAxisRange(anchored.y, anchored.y, rootRect.yMin, rootRect.yMax, size.y, partRect.pivot.y);
|
|
|
|
partRect.anchoredPosition = new Vector2(xRange.x, yRange.x);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Handlers
|
|
|
|
private void OnFeedbackChanged(object sender, GameEventArgs e)
|
|
{
|
|
if (!(e is CombineFeedbackEventArgs args)) return;
|
|
if (_hintText != null)
|
|
{
|
|
_hintText.text = args.Message;
|
|
}
|
|
}
|
|
|
|
private void OnCombineProgressChanged(object sender, GameEventArgs e)
|
|
{
|
|
if (!(e is CombineProgressEventArgs args))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (args.TotalSteps <= 0 || args.CurrentStep < args.TotalSteps || _completionFlowStarted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_completionFlowStarted = true;
|
|
StopCompleteFlow();
|
|
_completeFlowCoroutine = StartCoroutine(PlayCompleteFlow());
|
|
}
|
|
|
|
private IEnumerator PlayCompleteFlow()
|
|
{
|
|
float delayToFinalStage = Mathf.Max(0f, _completeToFinalStageDelay);
|
|
if (delayToFinalStage > 0f)
|
|
{
|
|
yield return new WaitForSecondsRealtime(delayToFinalStage);
|
|
}
|
|
|
|
if (_stageOverlay != null && _controller != null)
|
|
{
|
|
// For N parts, progress displays sprite[N-1]; completion tail uses sprite[N].
|
|
int finalStageIndex = _controller.TotalStep;
|
|
_stageOverlay.ApplyOverlayByIndex(finalStageIndex);
|
|
}
|
|
|
|
float delayToClose = Mathf.Max(0f, _finalStageToCloseDelay);
|
|
if (delayToClose > 0f)
|
|
{
|
|
yield return new WaitForSecondsRealtime(delayToClose);
|
|
}
|
|
|
|
_fireCompletedEventOnClose = true;
|
|
_completeFlowCoroutine = null;
|
|
Close();
|
|
}
|
|
|
|
private void StopCompleteFlow()
|
|
{
|
|
if (_completeFlowCoroutine == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StopCoroutine(_completeFlowCoroutine);
|
|
_completeFlowCoroutine = null;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|