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 { /// /// MVC UI form for GameplayA. It builds slots and draggable parts from external data. /// 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 _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 runtimeSlots = BuildSlots(context); if (runtimeSlots.Count == 0) { Log.Warning("CombineMvcForm build failed. No CombineSlot found in structure prefab."); return; } List partSprites = ClonePartSprites(context.PartSprites); BuildParts(partSprites, runtimeSlots); } private List BuildSlots(CombineFormContext context) { var runtimeSlots = new List(); if (context.StructurePrefab == null) { return runtimeSlots; } GameObject structureNode = Instantiate(context.StructurePrefab, _slotRoot, false); CenterStructureNode(structureNode); _runtimeNodes.Add(structureNode); CombineStageOverlay stageOverlay = structureNode.GetComponentInChildren(true); if (stageOverlay != null) { _stageOverlay = stageOverlay; List managedSlots = stageOverlay.GetManagedSlots(); if (managedSlots.Count > 0) { return managedSlots; } } CombineSlot[] structureSlots = structureNode.GetComponentsInChildren(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 partSprites, List runtimeSlots) { if (_partPrefab == null) { Log.Warning("CombineMvcForm build failed. Part prefab is missing."); return; } List orderedSlots = new List(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 ClonePartSprites(List source) { var result = new List(); 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 } }