TH1/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideWikiView.cs
2026-05-03 01:40:55 +08:00

748 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<WikiType> SmallTypes = new List<WikiType>();
}
public class UIOutsideWikiView : Base.View
{
[Header("Close")]
public Button CloseButton;
public Button BlockerButton;
[Header("BigSelect")]
// 二级嵌套:每个 BigSelectGroup 包含一个 Big 类型和它对应的 Small 类型列表
public List<BigSelectGroup> SelectGroups = new List<BigSelectGroup>();
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.LeaderAvatarHero 使用 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<WikiType> OnBigSelectClick;
public ViDelegateAssisstant.Dele<WikiType> OnSmallSelectClick;
public ViDelegateAssisstant.Dele<WikiItem> OnWikiListItemClick;
public ViDelegateAssisstant.Dele OnBtnCloseClick;
private readonly List<BigSelectButton> _bigSelectButtons = new List<BigSelectButton>();
private readonly List<SmallSelectButton> _smallSelectButtons = new List<SmallSelectButton>();
private readonly List<WikiListItem> _wikiListItems = new List<WikiListItem>();
private readonly List<WikiDescGroup> _wikiDescGroups = new List<WikiDescGroup>();
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<LayoutGroup>(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<RectTransform> { rt };
var fitters = rt.GetComponentsInChildren<ContentSizeFitter>(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<WikiItem> 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<MultilingualTextMono>();
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<MultilingualTextMono>();
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 → PlayerIconContainerHero=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<MultilingualTextMono>();
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 局部空间下的中心 Ycontent 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<ScrollRect>();
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;
}
}
}