using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; using DG.Tweening; using UnityEngine.EventSystems; public class notebookController : MonoBehaviour { private const string NotebookCategoryPrefsKey = "notebook.selected_category"; private const string NotebookFatherPrefsKey = "notebook.selected_father_id"; private const string NotebookSonPrefsKey = "notebook.selected_son_id"; [Header("select father types")] public Toggle all_content_beforeAnything_toggle; public Toggle idols_toggle; public Toggle enemies_toggle; public Toggle organizations_toggle; public Toggle areas_toggle; public Toggle terminologies_toggle; public Toggle Logs_toggle; public ToggleGroup categoryToggleGroup; [Header("quit")] public Button quit_button; [Header("prefabs")] public GameObject fatherItem_prefab; public GameObject svContent; [Header("paths")] [SerializeField] private string editor_SO_path; [SerializeField] private string runtime_SO_path; [Header("Details Displaying")] public Image detailsImage; public Text passage_title; public Text passage_text; [SerializeField] float panel_Enter_Time = 0.35f; [SerializeField] float panel_Exit_Time = 0.25f; [SerializeField] float panel_Enter_Scale_From = 0.92f; [SerializeField] float panel_Exit_Scale_To = 0.9f; [SerializeField] Ease panel_Enter_Ease = Ease.OutCubic; [SerializeField] Ease panel_Exit_Ease = Ease.InCubic; private readonly List cachedItems = new List(); private Toggle[] categoryToggles; private readonly Dictionary fatherBaseAnchors = new Dictionary(); private readonly Dictionary toggleInvokeFrame = new Dictionary(); private readonly Dictionary fatherSpacers = new Dictionary(); private readonly Dictionary fatherAnimTokens = new Dictionary(); private bool initialized = false; private const float SonOffsetBase = 5f; private const float SonOffsetPerItem = 110f; private const float SonTweenDuration = 0.5f; private const float SonFadeDuration = 0.25f; private void Start() { Initialize(); RestoreNotebookSelectionOrDefault(); } private void OnEnable() { Initialize(); RestoreNotebookSelectionOrDefault(); PlayEnterAnim(); } private void Initialize() { StoreOwnershipLedger.EnsureInstance().InitializeIfNeeded(); LoadAllData(); if (initialized) return; initialized = true; categoryToggles = new[] { all_content_beforeAnything_toggle, idols_toggle, enemies_toggle, organizations_toggle, areas_toggle, terminologies_toggle, Logs_toggle }; categoryToggleGroup = ResolveCategoryToggleGroup(); if (categoryToggleGroup != null) categoryToggleGroup.allowSwitchOff = true; if (categoryToggles != null) { for (int i = 0; i < categoryToggles.Length; i++) { var t = categoryToggles[i]; if (t == null) continue; t.group = categoryToggleGroup; } } BindToggle(all_content_beforeAnything_toggle, () => Refresh(notebook_faterType.notebook_fatherType.content)); BindToggle(idols_toggle, () => Refresh(notebook_faterType.notebook_fatherType.idols)); BindToggle(enemies_toggle, () => Refresh(notebook_faterType.notebook_fatherType.enemies)); BindToggle(organizations_toggle, () => Refresh(notebook_faterType.notebook_fatherType.organizations)); BindToggle(areas_toggle, () => Refresh(notebook_faterType.notebook_fatherType.areas)); BindToggle(terminologies_toggle, () => Refresh(notebook_faterType.notebook_fatherType.terminologies)); BindToggle(Logs_toggle, () => Refresh(notebook_faterType.notebook_fatherType.Logs)); if (quit_button != null) { quit_button.onClick.RemoveAllListeners(); quit_button.onClick.AddListener(CloseSelf); } } private void BindToggle(Toggle toggle, System.Action onSelected) { if (toggle == null) return; toggle.onValueChanged.RemoveAllListeners(); toggle.onValueChanged.AddListener(isOn => { if (isOn) InvokeToggleAction(toggle, onSelected); }); var relay = toggle.GetComponent(); if (relay == null) relay = toggle.gameObject.AddComponent(); relay.onClick = () => { if (toggle != null && toggle.isOn) InvokeToggleAction(toggle, onSelected); }; } private ToggleGroup ResolveCategoryToggleGroup() { if (categoryToggleGroup != null) return categoryToggleGroup; Toggle first = null; if (categoryToggles != null) { for (int i = 0; i < categoryToggles.Length; i++) { if (categoryToggles[i] != null) { first = categoryToggles[i]; break; } } } if (first == null) return null; var parentGroup = first.GetComponentInParent(); var contentGroup = svContent != null ? svContent.GetComponent() : null; if (parentGroup != null && parentGroup != contentGroup) return parentGroup; var parent = first.transform.parent; if (parent == null) return null; var created = parent.GetComponent(); if (created == null) created = parent.gameObject.AddComponent(); return created; } private void LoadAllData() { cachedItems.Clear(); var set = new HashSet(); #if UNITY_EDITOR if (!string.IsNullOrEmpty(editor_SO_path)) { var guids = UnityEditor.AssetDatabase.FindAssets("t:notebook_faterType", new[] { editor_SO_path }); for (int i = 0; i < guids.Length; i++) { var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]); var so = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (so == null) continue; if (set.Add(so)) cachedItems.Add(so); } } #endif if (!string.IsNullOrEmpty(runtime_SO_path)) { var resources = Resources.LoadAll(runtime_SO_path); for (int i = 0; i < resources.Length; i++) { var so = resources[i]; if (so == null) continue; if (set.Add(so)) cachedItems.Add(so); } } } private void Refresh(notebook_faterType.notebook_fatherType? type) { if (fatherItem_prefab == null || svContent == null) return; notebook_faterType.notebook_fatherType effectiveType = type ?? LoadSavedCategory(); SaveSelectedCategory(effectiveType); ClearContent(); var fatherGroup = svContent.GetComponent(); if (fatherGroup == categoryToggleGroup) fatherGroup = null; Toggle firstFatherToggle = null; fatherItemPrefab firstFatherComp = null; notebook_faterType firstFatherData = null; Toggle restoredFatherToggle = null; fatherItemPrefab restoredFatherComp = null; notebook_faterType restoredFatherData = null; int savedFatherId = LoadSavedFatherId(); for (int i = 0; i < cachedItems.Count; i++) { var so = cachedItems[i]; if (so == null) continue; if (!so.isUnlocked) continue; if (so.fatherType != effectiveType) continue; var obj = Instantiate(fatherItem_prefab, svContent.transform); var comp = obj.GetComponent(); if (comp == null) continue; if (comp.father_name != null) comp.father_name.text = so.class_name; comp.SetFatherImage(so.class_image); if (comp.father_description != null) comp.father_description.text = so.class_description; SetupFatherItem(comp, so, fatherGroup); UI_OrderedEntryAnimator.PlaySingle(obj, i, 0.18f, 0.012f, -14f, 0.97f, true); if (firstFatherToggle == null) { firstFatherToggle = ResolveFatherToggle(comp); firstFatherComp = comp; firstFatherData = so; } if (restoredFatherToggle == null && so.class_id == savedFatherId) { restoredFatherToggle = ResolveFatherToggle(comp); restoredFatherComp = comp; restoredFatherData = so; } } CacheFatherBasePositions(); AutoSelectFather( restoredFatherToggle != null ? restoredFatherToggle : firstFatherToggle, restoredFatherComp != null ? restoredFatherComp : firstFatherComp, restoredFatherData != null ? restoredFatherData : firstFatherData); } private void SetupFatherItem(fatherItemPrefab comp, notebook_faterType so, ToggleGroup fatherGroup) { if (comp == null) return; ClearSonContainer(comp); if (comp.son_obj != null) comp.son_obj.SetActive(false); if (comp.son_prefab_toPut != null) comp.son_prefab_toPut.SetActive(false); Toggle toggle = ResolveFatherToggle(comp); if (toggle == null) return; if (fatherGroup != null) toggle.group = fatherGroup; toggle.onValueChanged.RemoveAllListeners(); toggle.onValueChanged.AddListener(isOn => { if (isOn) { SaveSelectedFather(so.class_id); } ApplySonState(comp, so, isOn); }); ApplySonState(comp, so, toggle.isOn); } private void ApplySonState(fatherItemPrefab comp, notebook_faterType so, bool isOn) { if (comp == null) return; int token = NextAnimToken(comp); if (isOn) { ClearSonDetails(); EnsureSonsLoaded(comp, so); UpdateFatherSpacer(comp, so, true, token); FadeSonContainer(comp, true, token, null); } else { FadeSonContainer(comp, false, token, () => ClearSonContainer(comp)); UpdateFatherSpacer(comp, so, false, token); } } private void ClearSonDetails() { if (detailsImage != null) { detailsImage.sprite = null; detailsImage.enabled = false; } if (passage_title != null) { passage_title.text = string.Empty; } if (passage_text != null) { passage_text.text = string.Empty; } } private void EnsureSonsLoaded(fatherItemPrefab comp, notebook_faterType so) { if (comp == null || so == null) return; if (comp.son_prefab == null || comp.son_prefab_toPut == null) return; if (so.sonList == null || so.sonList.Count == 0) return; ClearSonContainer(comp); var parent = comp.son_prefab_toPut.transform; var prefab = comp.son_prefab; ToggleGroup sonGroup = null; if (comp.son_prefab_toPut != null) sonGroup = comp.son_prefab_toPut.GetComponent(); if (sonGroup == null && comp.son_obj != null) sonGroup = comp.son_obj.GetComponent(); Dictionary sonIdByToggle = new Dictionary(); Dictionary sonById = new Dictionary(); Toggle firstSonToggle = null; notebook_faterType.notebook_sonClass firstUnlockedSon = null; bool appliedSelectedSon = false; for (int i = 0; i < so.sonList.Count; i++) { var son = so.sonList[i]; if (son == null) continue; if (!son.son_isUnlocked) continue; var obj = Instantiate(prefab, parent); UI_OrderedEntryAnimator.PlaySingle(obj, i, 0.16f, 0.018f, -10f, 0.97f, true); var sonComp = obj.GetComponent(); Toggle sonToggle = null; if (sonComp != null && sonComp.sonToggle != null) sonToggle = sonComp.sonToggle; if (sonToggle == null) sonToggle = obj.GetComponent(); if (sonToggle == null) sonToggle = obj.GetComponentInChildren(true); if (sonGroup != null && sonToggle != null) sonToggle.group = sonGroup; if (firstSonToggle == null && sonToggle != null) { firstSonToggle = sonToggle; firstUnlockedSon = son; } if (sonToggle != null) { sonIdByToggle[sonToggle] = son.son_id; } if (!sonById.ContainsKey(son.son_id)) { sonById.Add(son.son_id, son); } if (sonComp == null) continue; if (sonComp.sonName != null) sonComp.sonName.text = son.son_name; if (sonComp.sonDescription != null) sonComp.sonDescription.text = son.son_description; sonComp.SetSonImage(son.son_image); if (sonToggle != null) { sonToggle.onValueChanged.RemoveAllListeners(); sonToggle.onValueChanged.AddListener(isOn => { if (isOn) { ApplySonDetails(so, son); } }); if (sonToggle.isOn) { ApplySonDetails(so, son); appliedSelectedSon = true; } } } if (!appliedSelectedSon) { int savedSonId = LoadSavedSonId(); Toggle restoredSonToggle = null; notebook_faterType.notebook_sonClass restoredSon = null; if (savedSonId > 0) { foreach (KeyValuePair pair in sonIdByToggle) { if (pair.Key == null) { continue; } if (pair.Value == savedSonId) { restoredSonToggle = pair.Key; sonById.TryGetValue(savedSonId, out restoredSon); break; } } } if (restoredSonToggle != null && restoredSon != null) { SelectSon(restoredSonToggle, so, restoredSon); } else if (firstSonToggle != null && firstUnlockedSon != null) { SelectSon(firstSonToggle, so, firstUnlockedSon); } else if (firstUnlockedSon != null) { ApplySonDetails(so, firstUnlockedSon); } } } private void ApplySonDetails(notebook_faterType father, notebook_faterType.notebook_sonClass son) { if (son == null) return; if (father != null) { SaveSelectedFather(father.class_id); } SaveSelectedSon(son.son_id); if (detailsImage != null) { detailsImage.sprite = son.son_detailsImage; detailsImage.enabled = son.son_detailsImage != null; } if (passage_title != null) passage_title.text = son.son_title; if (passage_text != null) passage_text.text = son.son_passage; PlayDetailsRefresh(); } private void InvokeToggleAction(Toggle toggle, System.Action onSelected) { if (toggle == null || onSelected == null) return; int frame = Time.frameCount; if (toggleInvokeFrame.TryGetValue(toggle, out var lastFrame) && lastFrame == frame) return; toggleInvokeFrame[toggle] = frame; onSelected.Invoke(); } private class ToggleClickRelay : MonoBehaviour, IPointerClickHandler { public System.Action onClick; public void OnPointerClick(PointerEventData eventData) { onClick?.Invoke(); } } private void CacheFatherBasePositions() { if (svContent == null) return; var parent = svContent.transform as RectTransform; if (parent == null) return; LayoutRebuilder.ForceRebuildLayoutImmediate(parent); fatherBaseAnchors.Clear(); for (int i = 0; i < parent.childCount; i++) { var child = parent.GetChild(i) as RectTransform; if (child == null) continue; fatherBaseAnchors[child] = child.anchoredPosition; } } private void UpdateFatherSpacer(fatherItemPrefab comp, notebook_faterType so, bool expand, int token) { if (svContent == null || comp == null) return; var parent = svContent.transform as RectTransform; if (parent == null) return; int count = expand ? GetUnlockedSonCount(so) : 0; float targetHeight = count > 0 ? (SonOffsetBase + (SonOffsetPerItem * count)) : 0f; if (!fatherSpacers.TryGetValue(comp, out var spacer) || spacer == null) { if (!expand) return; var spacerObj = new GameObject("NotebookSpacer", typeof(RectTransform), typeof(LayoutElement)); spacerObj.transform.SetParent(parent, false); spacer = spacerObj.GetComponent(); spacer.minHeight = 0f; spacer.flexibleHeight = 0f; spacer.preferredHeight = 0f; var spacerRectInit = spacerObj.transform as RectTransform; if (spacerRectInit != null) { spacerRectInit.pivot = new Vector2(0.5f, 1f); spacerRectInit.sizeDelta = new Vector2(spacerRectInit.sizeDelta.x, 0f); } fatherSpacers[comp] = spacer; } var spacerRect = spacer.transform as RectTransform; int targetIndex = comp.transform.GetSiblingIndex() + 1; if (spacerRect != null && spacerRect.GetSiblingIndex() != targetIndex) spacerRect.SetSiblingIndex(targetIndex); DOTween.Kill(spacer, false); if (expand) { DOTween.To(() => spacer.preferredHeight, h => { spacer.preferredHeight = h; spacer.minHeight = h; if (spacerRect != null) spacerRect.sizeDelta = new Vector2(spacerRect.sizeDelta.x, h); }, targetHeight, SonTweenDuration) .SetTarget(spacer) .SetEase(Ease.OutCubic) .OnUpdate(() => LayoutRebuilder.ForceRebuildLayoutImmediate(parent)); } else { DOTween.To(() => spacer.preferredHeight, h => { spacer.preferredHeight = h; spacer.minHeight = h; if (spacerRect != null) spacerRect.sizeDelta = new Vector2(spacerRect.sizeDelta.x, h); }, 0f, SonTweenDuration) .SetTarget(spacer) .SetEase(Ease.OutCubic) .OnUpdate(() => LayoutRebuilder.ForceRebuildLayoutImmediate(parent)) .OnComplete(() => { if (!IsTokenCurrent(comp, token)) return; if (spacer != null) { fatherSpacers.Remove(comp); if (Application.isPlaying) Destroy(spacer.gameObject); else DestroyImmediate(spacer.gameObject); } }); } LayoutRebuilder.ForceRebuildLayoutImmediate(parent); } private void FadeSonContainer(fatherItemPrefab comp, bool show, int token, System.Action onComplete) { var rect = GetSonRect(comp); if (rect == null) { onComplete?.Invoke(); return; } rect.DOKill(); if (show) { rect.gameObject.SetActive(true); var cg = rect.GetComponent(); if (cg == null) cg = rect.gameObject.AddComponent(); cg.alpha = 0f; cg.DOKill(); cg.DOFade(1f, SonFadeDuration).SetTarget(cg).SetEase(Ease.OutCubic); } else { var cg = rect.GetComponent(); if (cg == null) { rect.gameObject.SetActive(false); onComplete?.Invoke(); return; } cg.DOKill(); cg.DOFade(0f, SonFadeDuration).SetTarget(cg).SetEase(Ease.OutCubic).OnComplete(() => { if (!IsTokenCurrent(comp, token)) return; rect.gameObject.SetActive(false); onComplete?.Invoke(); }); } } private int NextAnimToken(fatherItemPrefab comp) { if (comp == null) return 0; if (!fatherAnimTokens.TryGetValue(comp, out var token)) token = 0; token++; fatherAnimTokens[comp] = token; return token; } private bool IsTokenCurrent(fatherItemPrefab comp, int token) { if (comp == null) return false; return fatherAnimTokens.TryGetValue(comp, out var current) && current == token; } private RectTransform GetSonRect(fatherItemPrefab comp) { if (comp == null) return null; if (comp.son_prefab_toPut != null) return comp.son_prefab_toPut.transform as RectTransform; if (comp.son_obj != null) return comp.son_obj.transform as RectTransform; return null; } private int GetUnlockedSonCount(notebook_faterType so) { if (so == null || so.sonList == null) return 0; int count = 0; for (int i = 0; i < so.sonList.Count; i++) { var son = so.sonList[i]; if (son == null) continue; if (!son.son_isUnlocked) continue; count++; } return count; } private void PlayEnterAnim() { var rect = transform as RectTransform; if (rect == null) rect = GetComponent(); if (rect == null) return; rect.DOKill(); var cg = GetComponent(); if (cg == null) cg = gameObject.AddComponent(); cg.DOKill(); rect.localScale = Vector3.one * panel_Enter_Scale_From; cg.alpha = 0f; rect.DOScale(Vector3.one, panel_Enter_Time).SetEase(panel_Enter_Ease); cg.DOFade(1f, panel_Enter_Time).SetEase(panel_Enter_Ease); } private void CloseSelf() { var rect = transform as RectTransform; if (rect == null) rect = GetComponent(); if (rect == null) { Destroy(gameObject); return; } rect.DOKill(); var cg = GetComponent(); if (cg == null) cg = gameObject.AddComponent(); cg.DOKill(); Sequence seq = DOTween.Sequence(); seq.Join(rect.DOScale(Vector3.one * panel_Exit_Scale_To, panel_Exit_Time).SetEase(panel_Exit_Ease)); seq.Join(cg.DOFade(0f, panel_Exit_Time).SetEase(panel_Exit_Ease)); seq.OnComplete(() => { if (this != null) Destroy(gameObject); }); } private void ClearSonContainer(fatherItemPrefab comp) { if (comp == null || comp.son_prefab_toPut == null) return; var parent = comp.son_prefab_toPut.transform; var rect = parent as RectTransform; if (rect != null) rect.DOKill(); for (int i = parent.childCount - 1; i >= 0; i--) { var child = parent.GetChild(i); if (child == null) continue; if (Application.isPlaying) Destroy(child.gameObject); else DestroyImmediate(child.gameObject); } } private void ClearContent() { var parent = svContent.transform; for (int i = parent.childCount - 1; i >= 0; i--) { var child = parent.GetChild(i); if (child == null) continue; if (Application.isPlaying) Destroy(child.gameObject); else DestroyImmediate(child.gameObject); } fatherBaseAnchors.Clear(); fatherSpacers.Clear(); } private Toggle ResolveFatherToggle(fatherItemPrefab comp) { if (comp == null) return null; Toggle toggle = null; if (comp.father_obj != null) toggle = comp.father_obj.GetComponent(); if (toggle == null) toggle = comp.GetComponent(); if (toggle == null) toggle = comp.GetComponentInChildren(true); if (toggle == null) toggle = comp.GetComponentInParent(); return toggle; } private void AutoSelectFather(Toggle targetFatherToggle, fatherItemPrefab targetFatherComp, notebook_faterType targetFatherData) { if (targetFatherToggle == null || targetFatherComp == null || targetFatherData == null) { ClearSonDetails(); return; } targetFatherToggle.SetIsOnWithoutNotify(true); SaveSelectedFather(targetFatherData.class_id); ApplySonState(targetFatherComp, targetFatherData, true); } private void RestoreNotebookSelectionOrDefault() { notebook_faterType.notebook_fatherType savedCategory = ResolveInitialCategory(); SaveSelectedCategory(savedCategory); Toggle targetToggle = GetCategoryToggle(savedCategory); if (targetToggle != null) { SetCategorySelectionWithoutNotify(savedCategory); Refresh(savedCategory); return; } if (all_content_beforeAnything_toggle != null) { SetCategorySelectionWithoutNotify(notebook_faterType.notebook_fatherType.content); Refresh(notebook_faterType.notebook_fatherType.content); return; } Refresh(notebook_faterType.notebook_fatherType.content); } private notebook_faterType.notebook_fatherType ResolveInitialCategory() { notebook_faterType.notebook_fatherType savedCategory = LoadSavedCategory(); if (HasVisibleFatherForCategory(savedCategory)) { return savedCategory; } return notebook_faterType.notebook_fatherType.content; } private bool HasVisibleFatherForCategory(notebook_faterType.notebook_fatherType category) { for (int i = 0; i < cachedItems.Count; i++) { notebook_faterType item = cachedItems[i]; if (item == null || !item.isUnlocked) { continue; } if (item.fatherType == category) { return true; } } return false; } private Toggle GetCategoryToggle(notebook_faterType.notebook_fatherType category) { switch (category) { case notebook_faterType.notebook_fatherType.content: return all_content_beforeAnything_toggle; case notebook_faterType.notebook_fatherType.idols: return idols_toggle; case notebook_faterType.notebook_fatherType.enemies: return enemies_toggle; case notebook_faterType.notebook_fatherType.organizations: return organizations_toggle; case notebook_faterType.notebook_fatherType.areas: return areas_toggle; case notebook_faterType.notebook_fatherType.terminologies: return terminologies_toggle; case notebook_faterType.notebook_fatherType.Logs: return Logs_toggle; default: return all_content_beforeAnything_toggle; } } private void SetCategorySelectionWithoutNotify(notebook_faterType.notebook_fatherType category) { if (categoryToggles == null) { return; } for (int i = 0; i < categoryToggles.Length; i++) { Toggle toggle = categoryToggles[i]; if (toggle == null) { continue; } bool shouldBeOn = toggle == GetCategoryToggle(category); toggle.SetIsOnWithoutNotify(shouldBeOn); } } private void SaveSelectedCategory(notebook_faterType.notebook_fatherType category) { PlayerPrefs.SetInt(NotebookCategoryPrefsKey, (int)category); PlayerPrefs.Save(); } private notebook_faterType.notebook_fatherType LoadSavedCategory() { int defaultValue = (int)notebook_faterType.notebook_fatherType.content; int raw = PlayerPrefs.GetInt(NotebookCategoryPrefsKey, defaultValue); if (!System.Enum.IsDefined(typeof(notebook_faterType.notebook_fatherType), raw)) { raw = defaultValue; } notebook_faterType.notebook_fatherType category = (notebook_faterType.notebook_fatherType)raw; if (category == notebook_faterType.notebook_fatherType.none) { category = notebook_faterType.notebook_fatherType.content; } return category; } private void SaveSelectedFather(int fatherId) { if (fatherId <= 0) { return; } PlayerPrefs.SetInt(NotebookFatherPrefsKey, fatherId); PlayerPrefs.Save(); } private int LoadSavedFatherId() { return PlayerPrefs.GetInt(NotebookFatherPrefsKey, -1); } private void SaveSelectedSon(int sonId) { if (sonId <= 0) { return; } PlayerPrefs.SetInt(NotebookSonPrefsKey, sonId); PlayerPrefs.Save(); } private int LoadSavedSonId() { return PlayerPrefs.GetInt(NotebookSonPrefsKey, -1); } private void SelectSon(Toggle toggle, notebook_faterType father, notebook_faterType.notebook_sonClass son) { if (toggle != null) { toggle.SetIsOnWithoutNotify(true); } ApplySonDetails(father, son); } private void PlayDetailsRefresh() { GameObject target = null; if (detailsImage != null) target = detailsImage.gameObject; if (target == null && passage_title != null) target = passage_title.gameObject; if (target == null && passage_text != null) target = passage_text.gameObject; UI_OrderedEntryAnimator.PlayRefresh(target, 0.16f, -8f, 0.99f, true); } }