using System; using System.Collections.Generic; using Logic.Multilingual; using TH1_Core.Events; using TH1_Logic.MatchConfig; using TH1_UI.View.Outside.WikiSub; using TMPro; using UnityEngine; using UnityEngine.UI; namespace TH1_UI.View.Outside { // 一级 Big 分类 + 该分类下的二级 Small 分类 [Serializable] public class BigSelectGroup { public WikiType BigType; public List SmallTypes = new List(); } public class UIOutsideWikiView : Base.View { [Header("Close")] public Button CloseButton; public Button BlockerButton; [Header("BigSelect")] // 二级嵌套:每个 BigSelectGroup 包含一个 Big 类型和它对应的 Small 类型列表 public List SelectGroups = new List(); public BigSelectButton BigSelectButtonPrefab; public Transform BigSelectArea; [Header("SmallSelect")] public SmallSelectButton SmallSelectButtonPrefab; public Transform SmallSelectArea; [Header("WikiItem List")] public WikiListItem WikiListItemPrefab; public Transform WikiListContent; [Header("WikiContentArea")] public TextMeshProUGUI TitleText; // 按 WikiType 分组的 Icon 容器:父对象用于激活/隐藏,Image 用于替换 sprite。 // 资源大小由外部预先调好,这里只做激活和 sprite 替换,不调用 IconSizingUtility。 [Header("Typed Icon Groups")] public GameObject UnitIconGroup; public Image UnitIconImage; public GameObject SkillIconGroup; public Image SkillIconImage; // Skill 分组的底色:根据 SkillViewType 走 SkillDataAssets.GetBGColor 着色 public Image SkillIconBg; public GameObject ActionIconGroup; public Image ActionIconImage; public GameObject ResourceIconGroup; public Image ResourceIconImage; // Force / Hero 共用:Force 使用 PlayerInfo.LeaderAvatar,Hero 使用 HeroDataAssets.HeroAvatar public GameObject PlayerIconContainer; public Image PlayerIconImage; // Tag 文本:把 wikiItem.Types 转成多语言名后用 "," 拼接 [Header("Tag")] public TextMeshProUGUI TagText; public WikiDescGroup WikiDescGroupPrefab; public Transform DescArea; public ViDelegateAssisstant.Dele OnBigSelectClick; public ViDelegateAssisstant.Dele OnSmallSelectClick; public ViDelegateAssisstant.Dele OnWikiListItemClick; public ViDelegateAssisstant.Dele OnBtnCloseClick; private readonly List _bigSelectButtons = new List(); private readonly List _smallSelectButtons = new List(); private readonly List _wikiListItems = new List(); private readonly List _wikiDescGroups = new List(); protected override void OnInit() { base.OnInit(); InitBigSelectButtons(); ClearSmallSelectButtons(); // Small 区域初始为空,等 Big 被点击后再填充 ClearWikiList(); ClearTitle(); InitCloseButtons(); WikiSub.WikiDescGroup.OnRequestWikiJump += HandleWikiJumpRequest; } private void InitCloseButtons() { if (CloseButton != null) { CloseButton.onClick.RemoveAllListeners(); CloseButton.onClick.AddListener(OnClose); } if (BlockerButton != null) { BlockerButton.onClick.RemoveAllListeners(); BlockerButton.onClick.AddListener(OnClose); } } private void OnClose() { ViDelegateAssisstant.Invoke(OnBtnCloseClick); } public void SetContent(ShowUIOutsideWiki evt) { ClearTitle(); ClearWikiList(); } private void InitBigSelectButtons() { ClearBigSelectButtons(); if (BigSelectButtonPrefab == null || BigSelectArea == null || SelectGroups == null) return; for (int i = 0; i < SelectGroups.Count; i++) { var group = SelectGroups[i]; if (group == null) continue; var button = Instantiate(BigSelectButtonPrefab, BigSelectArea); button.SetContent(group.BigType); button.OnClick += HandleBigSelectButtonClick; _bigSelectButtons.Add(button); } // BigSelectArea 是 LayoutGroup,动态增删后需要强制重建 Layout RebuildLayout(BigSelectArea as RectTransform); } // 由 Controller 在 Big 被点击后调用:根据当前 Big 重新生成对应的 Small 按钮。 // 返回值表示该 Big 下是否存在 Small;不存在时 SmallSelectArea 会被隐藏。 public bool ShowSmallSelectButtonsForBig(WikiType bigType) { ClearSmallSelectButtons(); if (SelectGroups == null) { SetSmallAreaActive(false); return false; } for (int i = 0; i < SelectGroups.Count; i++) { var group = SelectGroups[i]; if (group == null) continue; if (!group.BigType.Equals(bigType)) continue; bool hasSmalls = group.SmallTypes != null && group.SmallTypes.Count > 0; SetSmallAreaActive(hasSmalls); if (!hasSmalls) return false; if (SmallSelectButtonPrefab == null || SmallSelectArea == null) return false; for (int j = 0; j < group.SmallTypes.Count; j++) { var button = Instantiate(SmallSelectButtonPrefab, SmallSelectArea); button.SetContent(group.SmallTypes[j]); button.OnClick += HandleSmallSelectButtonClick; _smallSelectButtons.Add(button); } // SmallSelectArea 是 LayoutGroup,动态增删后需要强制重建 Layout RebuildLayout(SmallSelectArea as RectTransform); return true; } // 没有匹配到 group SetSmallAreaActive(false); return false; } private void SetSmallAreaActive(bool active) { if (SmallSelectArea == null) return; if (SmallSelectArea.gameObject.activeSelf != active) SmallSelectArea.gameObject.SetActive(active); } // 强制立即重建 LayoutGroup 的布局,避免动态增删子对象后一帧残留的尺寸/位置(spacing)问题。 // 仅 ForceRebuildLayoutImmediate 在 LayoutGroup 刚 SetActive 或子节点刚 Instantiate 时 // 经常算错 spacing,需要 toggle LayoutGroup 把内部脏状态彻底重置。 // 多层嵌套的 LayoutGroup + ContentSizeFitter 必须自底向上 Rebuild, // 否则父 Fitter 在子还没算完尺寸时就先收尾,首帧布局会错位。 private static void RebuildLayout(RectTransform rt) { if (rt == null) return; // 先 toggle 所有相关 LayoutGroup(含自身和子节点),强制其重建内部缓存 var groups = rt.GetComponentsInChildren(true); for (int i = 0; i < groups.Length; i++) { var g = groups[i]; if (g == null) continue; g.enabled = false; g.enabled = true; } // 收集所有需要重建的 RectTransform:自身 + 任何含 LayoutGroup 或 ContentSizeFitter 的子节点 var rebuildTargets = new List { rt }; var fitters = rt.GetComponentsInChildren(true); for (int i = 0; i < fitters.Length; i++) { var t = fitters[i] != null ? fitters[i].transform as RectTransform : null; if (t != null && !rebuildTargets.Contains(t)) rebuildTargets.Add(t); } for (int i = 0; i < groups.Length; i++) { var t = groups[i] != null ? groups[i].transform as RectTransform : null; if (t != null && !rebuildTargets.Contains(t)) rebuildTargets.Add(t); } // 自底向上:按层级深度降序排序,先 rebuild 叶子,再 rebuild 父 rebuildTargets.Sort((a, b) => GetDepth(b).CompareTo(GetDepth(a))); Canvas.ForceUpdateCanvases(); for (int i = 0; i < rebuildTargets.Count; i++) { if (rebuildTargets[i] == null) continue; LayoutRebuilder.ForceRebuildLayoutImmediate(rebuildTargets[i]); } } private static int GetDepth(Transform t) { int d = 0; while (t != null && t.parent != null) { t = t.parent; d++; } return d; } public void RefreshWikiList(List wikiItems) { ClearWikiList(); if (WikiListItemPrefab == null || WikiListContent == null || wikiItems == null) return; for (int i = 0; i < wikiItems.Count; i++) { var item = Instantiate(WikiListItemPrefab, WikiListContent); item.SetContent(wikiItems[i]); item.OnClick += HandleWikiListItemClick; _wikiListItems.Add(item); } // WikiListContent 是 LayoutGroup,动态增删后需要强制重建 Layout RebuildLayout(WikiListContent as RectTransform); } // Stage 1 行为:没有同时选中 Big 和 Small 时,由 Controller 调用此方法清空列表。 public void ClearWikiList() { for (int i = 0; i < _wikiListItems.Count; i++) { if (_wikiListItems[i] == null) continue; _wikiListItems[i].ClearCallback(); DetachAndDestroy(_wikiListItems[i].gameObject); } _wikiListItems.Clear(); } public void SetTitle(WikiItem wikiItem) { if (wikiItem == null) { ClearTitle(); return; } if (TitleText != null) { var ml = TitleText.GetComponent(); if (ml != null) ml.ID = 0; if (!string.IsNullOrEmpty(wikiItem.Name) && uint.TryParse(wikiItem.Name, out _)) MultilingualManager.Instance.SetUIText(TitleText, wikiItem.Name); else TitleText.text = wikiItem.Name ?? string.Empty; } ApplyContentIcon(wikiItem); ApplyTagText(wikiItem); RefreshDescArea(wikiItem); } // 把 wikiItem.Types 里每一个枚举转成多语言名(通过 WikiData.GetTypeName 拿 key, // 再走 MultilingualManager 转成当前语种),用 ", " 拼成一长串塞进 TagText。 // 没有任何 tag 时清空文本。 private void ApplyTagText(WikiItem wikiItem) { if (TagText == null) return; // 与 TitleText 同样的处理:清掉可能存在的 MultilingualTextMono 绑定, // 否则下一帧会被它覆盖回去 var ml = TagText.GetComponent(); if (ml != null) ml.ID = 0; if (wikiItem == null || wikiItem.Types == null || wikiItem.Types.Count == 0) { TagText.text = string.Empty; return; } var wikiData = Table.Instance != null ? Table.Instance.WikiData : null; var sb = new System.Text.StringBuilder(); for (int i = 0; i < wikiItem.Types.Count; i++) { string key = wikiData != null ? wikiData.GetTypeName(wikiItem.Types[i]) : null; if (string.IsNullOrEmpty(key)) continue; string text = key; if (uint.TryParse(key, out _)) text = MultilingualManager.Instance.GetMultilingualText(key); if (string.IsNullOrEmpty(text)) continue; if (sb.Length > 0) sb.Append(", "); sb.Append(text); } TagText.text = sb.ToString(); } private void ApplyContentIcon(WikiItem wikiItem) { // 按类型挑出目标分组:5 个分组(Unit/Skill/Action/Resource/Player)择一激活 // wikiItem 无效、Icon 为 null 或都不命中类型时全部隐藏 ResolveTypedIconGroup(wikiItem, out var targetGroup, out var targetImage); // 保护:sprite 为 null 时(任何分组都一样),不显示该分组 bool hasSprite = wikiItem != null && wikiItem.Icon != null; if (!hasSprite) { targetGroup = null; targetImage = null; } SetGroupActive(UnitIconGroup, targetGroup); SetGroupActive(SkillIconGroup, targetGroup); SetGroupActive(ActionIconGroup, targetGroup); SetGroupActive(ResourceIconGroup, targetGroup); SetGroupActive(PlayerIconContainer, targetGroup); if (targetGroup != null && targetImage != null && wikiItem != null) { targetImage.sprite = wikiItem.Icon; if (!targetImage.gameObject.activeSelf) targetImage.gameObject.SetActive(true); } // Skill 分组命中时根据 SkillViewType 给底色上色(其它分组不处理) if (targetGroup == SkillIconGroup) ApplySkillBgColor(wikiItem); } // 从 wikiItem.Types 里反推 SkillViewType(与 WikiTagRules.ForSkill 的映射相反), // 然后查 SkillDataAssets.GetBGColor 设置到 SkillIconBg。 // 没有 SmallTypeSkill* 时退回 SkillViewType.Normal。 private void ApplySkillBgColor(WikiItem wikiItem) { if (SkillIconBg == null) return; var viewType = SkillViewType.Normal; if (wikiItem != null && wikiItem.Types != null) { for (int i = 0; i < wikiItem.Types.Count; i++) { switch (wikiItem.Types[i]) { case WikiType.SmallTypeSkillNormal: viewType = SkillViewType.Normal; break; case WikiType.SmallTypeSkillSpecial: viewType = SkillViewType.Special; break; case WikiType.SmallTypeSkillUnique: viewType = SkillViewType.Unique; break; case WikiType.SmallTypeSkillPositive: viewType = SkillViewType.Positive; break; case WikiType.SmallTypeSkillNegative: viewType = SkillViewType.Negative; break; } } } var skillAssets = Table.Instance != null ? Table.Instance.SkillDataAssets : null; if (skillAssets == null) return; // 图鉴里没有 HasTimeLimit 概念,传 false SkillIconBg.color = skillAssets.GetBGColor(viewType, false); } // 当前分组只激活与 target 同一引用的那个,其它统一关闭 private static void SetGroupActive(GameObject group, GameObject target) { if (group == null) return; bool active = group != null && group == target; if (group.activeSelf != active) group.SetActive(active); } // 按 WikiItem.Types 决定使用哪个分组: // BigTypeUnit → UnitIconGroup // BigTypeHero / BigTypeForce → PlayerIconContainer(Hero=HeroAvatar, Force=LeaderAvatar) // BigTypeSkill → SkillIconGroup // BigTypeAction → ActionIconGroup // BigTypeResource / BigTypeBuilding → ResourceIconGroup // 都不命中时 outGroup/outImage 为 null,所有分组全部隐藏 private void ResolveTypedIconGroup(WikiItem wikiItem, out GameObject outGroup, out Image outImage) { outGroup = null; outImage = null; if (wikiItem == null || wikiItem.Types == null) return; for (int i = 0; i < wikiItem.Types.Count; i++) { switch (wikiItem.Types[i]) { case WikiType.BigTypeUnit: outGroup = UnitIconGroup; outImage = UnitIconImage; return; case WikiType.BigTypeHero: case WikiType.BigTypeForce: outGroup = PlayerIconContainer; outImage = PlayerIconImage; return; case WikiType.BigTypeSkill: outGroup = SkillIconGroup; outImage = SkillIconImage; return; case WikiType.BigTypeAction: ResolveActionIconGroup(wikiItem, out outGroup, out outImage); return; case WikiType.BigTypeResource: case WikiType.BigTypeBuilding: outGroup = ResourceIconGroup; outImage = ResourceIconImage; return; } } } // Action 类下,根据 wikiItem.IconSizeType 决定具体使用哪个 typed group: // Building / Ground / MountainBuilding / Resource / Defense → ResourceIconGroup // _256x256 → SkillIconGroup // Unit → UnitIconGroup private void ResolveActionIconGroup(WikiItem wikiItem, out GameObject outGroup, out Image outImage) { outGroup = null; outImage = null; if (wikiItem == null) return; switch (wikiItem.IconSizeType) { case IconViewSizeType.Building: case IconViewSizeType.Ground: case IconViewSizeType.MountainBuilding: case IconViewSizeType.Resource: case IconViewSizeType.Defense: outGroup = ResourceIconGroup; outImage = ResourceIconImage; return; case IconViewSizeType._256x256: outGroup = SkillIconGroup; outImage = SkillIconImage; return; case IconViewSizeType.Unit: outGroup = UnitIconGroup; outImage = UnitIconImage; return; } } public void ClearTitle() { if (TitleText != null) { var ml = TitleText.GetComponent(); if (ml != null) ml.ID = 0; TitleText.text = string.Empty; } ApplyContentIcon(null); ApplyTagText(null); ClearDescArea(); } private void RefreshDescArea(WikiItem wikiItem) { ClearDescArea(); if (wikiItem == null) return; if (WikiDescGroupPrefab == null || DescArea == null) return; var descItems = wikiItem.DescItems; if (descItems == null) return; for (int i = 0; i < descItems.Count; i++) { var descItem = descItems[i]; if (descItem == null) continue; var group = Instantiate(WikiDescGroupPrefab, DescArea); // SetContent 返回 false 表示该 desc 无可显示内容(如 HeroUpgrade 当级无任务), // 直接销毁本 group,避免 LayoutGroup 中残留空段 if (!group.SetContent(wikiItem, descItem)) { DetachAndDestroy(group.gameObject); continue; } _wikiDescGroups.Add(group); } // DescArea 通常也是 LayoutGroup,动态增删后强制重建一次 RebuildLayout(DescArea as RectTransform); } private void ClearDescArea() { for (int i = 0; i < _wikiDescGroups.Count; i++) { if (_wikiDescGroups[i] == null) continue; DetachAndDestroy(_wikiDescGroups[i].gameObject); } _wikiDescGroups.Clear(); } public void OnCloseView() { ClearWikiList(); ClearTitle(); } // 触发第一个 BigSelectButton 的点击;用于 OnOpen 时设置默认选中 public bool SelectFirstBig() { for (int i = 0; i < _bigSelectButtons.Count; i++) { var btn = _bigSelectButtons[i]; if (btn == null || btn.Button == null) continue; btn.Button.onClick.Invoke(); return true; } return false; } // 触发第一个 SmallSelectButton 的点击(若当前 Big 下有 Small 则点);用于 Big 选中后默认选 Small public bool SelectFirstSmallIfAny() { for (int i = 0; i < _smallSelectButtons.Count; i++) { var btn = _smallSelectButtons[i]; if (btn == null || btn.Button == null) continue; btn.Button.onClick.Invoke(); return true; } return false; } // 触发第一个 WikiListItem 的点击;用于刷新列表后默认展示首项 Content public bool SelectFirstWikiListItemIfAny() { for (int i = 0; i < _wikiListItems.Count; i++) { var item = _wikiListItems[i]; if (item == null || item.Button == null) continue; item.Button.onClick.Invoke(); return true; } return false; } private void HandleBigSelectButtonClick(BigSelectButton button) { for (int i = 0; i < _bigSelectButtons.Count; i++) { if (_bigSelectButtons[i] == null) continue; _bigSelectButtons[i].SetSelected(_bigSelectButtons[i] == button); } ViDelegateAssisstant.Invoke(OnBigSelectClick, button.WikiType); } private void HandleSmallSelectButtonClick(SmallSelectButton button) { for (int i = 0; i < _smallSelectButtons.Count; i++) { if (_smallSelectButtons[i] == null) continue; _smallSelectButtons[i].SetSelected(_smallSelectButtons[i] == button); } ViDelegateAssisstant.Invoke(OnSmallSelectClick, button.WikiType); } private void HandleWikiListItemClick(WikiListItem item) { // 互斥切换选中态:被点中的高亮,其余取消 for (int i = 0; i < _wikiListItems.Count; i++) { if (_wikiListItems[i] == null) continue; _wikiListItems[i].SetSelected(_wikiListItems[i] == item); } ViDelegateAssisstant.Invoke(OnWikiListItemClick, item.WikiItem); } private void ClearBigSelectButtons() { for (int i = 0; i < _bigSelectButtons.Count; i++) { if (_bigSelectButtons[i] == null) continue; _bigSelectButtons[i].ClearCallback(); DetachAndDestroy(_bigSelectButtons[i].gameObject); } _bigSelectButtons.Clear(); } private void ClearSmallSelectButtons() { for (int i = 0; i < _smallSelectButtons.Count; i++) { if (_smallSelectButtons[i] == null) continue; _smallSelectButtons[i].ClearCallback(); DetachAndDestroy(_smallSelectButtons[i].gameObject); } _smallSelectButtons.Clear(); } // Destroy 是异步的,子物体会留在父节点里直到帧末才被回收。 // 紧接着重建 LayoutGroup 会同时看到旧(待销毁)和新子物体,导致布局错乱。 // 销毁前先把子物体从父节点摘出来,让 LayoutGroup 立刻看不到它。 private static void DetachAndDestroy(GameObject go) { if (go == null) return; if (go.transform != null) go.transform.SetParent(null, false); Destroy(go); } protected override void OnDispose() { WikiSub.WikiDescGroup.OnRequestWikiJump -= HandleWikiJumpRequest; ClearBigSelectButtons(); ClearSmallSelectButtons(); ClearWikiList(); ClearDescArea(); base.OnDispose(); } // GridItem 点击跳转:在已有的 BigSelectGroups 内找一个能匹配 wikiId 对应 WikiItem.Types 的 // (Big, Small?) 组合,依次触发 BigSelectButton.click → SmallSelectButton.click → WikiList 选中。 // 找不到任何能容纳此 wikiId 的 group 就不切(按需求)。 private void HandleWikiJumpRequest(uint wikiId) { var wikiData = Table.Instance != null ? Table.Instance.WikiData : null; if (wikiData == null) return; var target = wikiData.GetById(wikiId); if (target == null) return; if (target.Types == null || target.Types.Count == 0) return; if (!TryResolveJumpRoute(target, out var bigType, out var smallType, out var hasSmall)) return; // 1) 触发 Big 按钮(SetSelected 视觉 + 通知 Controller 重建 Small) var bigBtn = FindBigButton(bigType); if (bigBtn == null || bigBtn.Button == null) return; bigBtn.Button.onClick.Invoke(); // 2) 触发 Small 按钮(如果该 Big 下有 Small 子分类) if (hasSmall) { var smallBtn = FindSmallButton(smallType); if (smallBtn == null || smallBtn.Button == null) return; smallBtn.Button.onClick.Invoke(); } // 3) 在已经刷新好的 _wikiListItems 里点中目标 wikiId,并把它滚到可视区 for (int i = 0; i < _wikiListItems.Count; i++) { var it = _wikiListItems[i]; if (it == null || it.Button == null) continue; if (it.WikiItemId != wikiId) continue; it.Button.onClick.Invoke(); ScrollWikiListItemIntoView(it); return; } } // 把指定的 WikiListItem 滚到 ScrollRect 的可视区中央。 // ScrollRect 没在 prefab 上单独 expose,运行时从 WikiListContent 沿父链找第一个。 // 注意:列表是 RefreshWikiList 后立刻调用此方法,需要先 ForceRebuildLayout 让 Content // 的高度和子节点位置都已经确定,否则 anchoredPosition.y 可能还是 0。 private void ScrollWikiListItemIntoView(WikiListItem item) { if (item == null) return; var content = WikiListContent as RectTransform; if (content == null) return; var scroll = FindParentScrollRect(content); if (scroll == null) return; var viewport = scroll.viewport != null ? scroll.viewport : content.parent as RectTransform; if (viewport == null) return; Canvas.ForceUpdateCanvases(); LayoutRebuilder.ForceRebuildLayoutImmediate(content); var target = item.transform as RectTransform; if (target == null) return; float contentHeight = content.rect.height; float viewportHeight = viewport.rect.height; // Content 比 Viewport 矮的时候不需要滚 if (contentHeight <= viewportHeight) return; // target 在 content 局部空间下的中心 Y(content pivot 默认是 (0.5,1),y 向下为负) // 用 target.localPosition.y 即可(已是相对 content) float targetY = -target.localPosition.y; // 转成"距 content 顶部"的正向距离 float scrollableRange = contentHeight - viewportHeight; // verticalNormalizedPosition: 1 = 顶部,0 = 底部 // 让 target 中心对齐 viewport 中心:顶部需要让出 (targetY - viewportHeight/2) float topOffset = Mathf.Clamp(targetY - viewportHeight * 0.5f, 0f, scrollableRange); scroll.verticalNormalizedPosition = 1f - topOffset / scrollableRange; } private static ScrollRect FindParentScrollRect(Transform start) { var t = start; while (t != null) { var sr = t.GetComponent(); if (sr != null) return sr; t = t.parent; } return null; } private bool TryResolveJumpRoute(WikiItem target, out WikiType bigType, out WikiType smallType, out bool hasSmall) { bigType = default; smallType = default; hasSmall = false; if (SelectGroups == null) return false; // target.Types 的顺序里,BigType* 总是先于 SmallType*;遍历 SelectGroups 找可命中的组合 for (int g = 0; g < SelectGroups.Count; g++) { var grp = SelectGroups[g]; if (grp == null) continue; if (!target.Types.Contains(grp.BigType)) continue; if (grp.SmallTypes == null || grp.SmallTypes.Count == 0) { bigType = grp.BigType; hasSmall = false; return true; } for (int s = 0; s < grp.SmallTypes.Count; s++) { if (!target.Types.Contains(grp.SmallTypes[s])) continue; bigType = grp.BigType; smallType = grp.SmallTypes[s]; hasSmall = true; return true; } } return false; } private BigSelectButton FindBigButton(WikiType bigType) { for (int i = 0; i < _bigSelectButtons.Count; i++) { var b = _bigSelectButtons[i]; if (b == null) continue; if (b.WikiType.Equals(bigType)) return b; } return null; } private SmallSelectButton FindSmallButton(WikiType smallType) { for (int i = 0; i < _smallSelectButtons.Count; i++) { var b = _smallSelectButtons[i]; if (b == null) continue; if (b.WikiType.Equals(smallType)) return b; } return null; } } }