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; private CombineComponent _controller; 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); 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); 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() { 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) { 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); 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)); partRect.anchoredPosition = new Vector2( Random.Range(minX, maxX), Random.Range(minY, maxY)); } #endregion #region Event Handlers private void OnFeedbackChanged(object sender, GameEventArgs e) { if (!(e is CombineFeedbackEventArgs args)) return; if (_hintText != null) { _hintText.text = args.Message; } } #endregion } }