Files
DepthsOfGreed/Assets/Scripts/HandbookUI/HandbookController.cs
T

184 lines
9.0 KiB
C#

using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace DepthsOfGreed.HandbookUI
{
public sealed class HandbookController : MonoBehaviour
{
[SerializeField] private GameObject closedBook;
[SerializeField] private GameObject openBook;
[SerializeField] private Button bookButton;
[SerializeField] private Button closeButton;
[SerializeField] private RectTransform closedTabsRoot;
[SerializeField] private RectTransform openTabsRoot;
[SerializeField] private Button[] closedTabs;
[SerializeField] private Button[] openTabs;
[SerializeField] private GameObject[] pages;
[SerializeField] private GameObject testTowerPrefab;
[SerializeField] private HandbookTowerCatalog towerCatalog;
[Tooltip("Optional: component implementing IHandbookGameplay. Empty uses the prototype backend.")]
[SerializeField] private MonoBehaviour gameplayBackend;
[Tooltip("Wartezeit nach erfolgreichem Platzieren, bevor das Buch hochfährt.")]
[SerializeField, Min(0)] private float successfulPlacementDelay = 0.5f;
[Header("Tab movement (Build, Statistics, Upgrades, Index)")]
[Tooltip("Distance left per closed tab. -1 automatically uses its width. Rest position is the authored RectTransform position.")]
[SerializeField] private float[] closedTabRevealDistances = { -1f, -1f, -1f, -1f };
[Tooltip("Distance up per open tab. -1 automatically uses its height.")]
[SerializeField] private float[] openTabRevealDistances = { -1f, -1f, -1f, -1f };
[Tooltip("Overlap retained when an automatically sized tab is fully revealed.")]
[SerializeField, Min(0)] private float closedTabOverlap = 10f;
[SerializeField, Min(0)] private float openTabOverlap = 25f;
[SerializeField] private float animationSpeed = 14f;
private readonly string[] titles = { "Bauen", "Statistik", "Upgrades", "Index" };
private Vector2[] closedPositions, openPositions;
private TMP_Text[] closedLabels, openLabels;
[SerializeField] private RectTransform activeLayer;
private int selected, hoveredClosed = -1, hoveredOpen = -1;
private bool isOpen, bookHovered;
private Vector3 bookScale;
private BuildMenuController buildMenu;
private TestPlacementController placement;
private HandbookIndexController indexMenu;
private void Awake()
{
if (!closedBook || !openBook || !bookButton || !closeButton ||
!openTabsRoot || !closedTabsRoot || closedTabs.Length != 4 ||
openTabs.Length != 4 || pages.Length != 4)
{
Debug.LogError("Handbuch: Inspector-Verbindungen fehlen.", this);
enabled = false;
return;
}
closedPositions = PrepareTabs(closedTabs, out closedLabels, false);
openPositions = PrepareTabs(openTabs, out openLabels, true);
bookScale = bookButton.transform.localScale;
bookButton.onClick.AddListener(() => Open(0));
closeButton.onClick.AddListener(Close);
AddHover(bookButton.gameObject, () => bookHovered = true, () => bookHovered = false);
buildMenu = pages[0].GetComponent<BuildMenuController>();
buildMenu.Initialize(towerCatalog);
placement = gameObject.GetComponent<TestPlacementController>();
placement.Initialize(this, buildMenu, (RectTransform)openBook.transform, testTowerPrefab, gameplayBackend);
placement.SuccessfulPlacementDelay = successfulPlacementDelay;
buildMenu.Placement = placement;
indexMenu = pages[3].GetComponent<HandbookIndexController>();
indexMenu.Initialize(towerCatalog);
var upgrades = pages[2].GetComponent<HandbookUpgradesController>();
upgrades.Initialize(placement);
Close();
}
private Vector2[] PrepareTabs(Button[] tabs, out TMP_Text[] labels, bool opened)
{
var positions = new Vector2[tabs.Length];
labels = new TMP_Text[tabs.Length];
for (int i = 0; i < tabs.Length; i++)
{
int index = i;
positions[i] = ((RectTransform)tabs[i].transform).anchoredPosition;
labels[i] = tabs[i].GetComponentInChildren<TMP_Text>(true);
if (labels[i]) labels[i].gameObject.SetActive(false);
tabs[i].onClick.AddListener(() => Open(index));
AddHover(tabs[i].gameObject,
() => { if (opened) hoveredOpen = index; else hoveredClosed = index; },
() => { if (opened) { if (hoveredOpen == index) hoveredOpen = -1; }
else if (hoveredClosed == index) hoveredClosed = -1; });
}
return positions;
}
private static void AddHover(GameObject target, UnityEngine.Events.UnityAction enter,
UnityEngine.Events.UnityAction exit)
{
var trigger = target.GetComponent<EventTrigger>();
if (!trigger) trigger = target.AddComponent<EventTrigger>();
var entry = new EventTrigger.Entry { eventID = EventTriggerType.PointerEnter };
entry.callback.AddListener(_ => enter());
trigger.triggers.Add(entry);
var leave = new EventTrigger.Entry { eventID = EventTriggerType.PointerExit };
leave.callback.AddListener(_ => exit());
trigger.triggers.Add(leave);
}
public void Open(int index)
{
if (index < 0 || index >= pages.Length) return;
bool samePage = isOpen && selected == index;
selected = index;
isOpen = true;
hoveredClosed = -1;
bookHovered = false;
closedBook.SetActive(false);
openBook.SetActive(true);
for (int i = 0; i < pages.Length; i++)
{
pages[i].SetActive(i == index);
var rect = (RectTransform)openTabs[i].transform;
rect.SetParent(i == selected ? activeLayer : openTabsRoot, false);
}
if (index == 3 && !samePage && indexMenu) indexMenu.ResetForEntry();
if (samePage && index == 3 && indexMenu) indexMenu.ResetForEntry();
}
public void Close()
{
if (placement && placement.IsDragging) placement.Cancel();
if (placement) placement.ResetBookPresentation();
if (buildMenu) buildMenu.ResetForClose();
if (indexMenu) indexMenu.ClearSelection();
isOpen = false;
hoveredOpen = hoveredClosed = -1;
bookHovered = false;
for (int i = 0; i < openTabs.Length; i++)
{
((RectTransform)openTabs[i].transform).anchoredPosition = openPositions[i];
if (openLabels[i]) openLabels[i].gameObject.SetActive(false);
}
openBook.SetActive(false);
closedBook.SetActive(true);
}
private void Update()
{
if (isOpen && (!placement || !placement.ConsumedCancelThisFrame) &&
(!placement || !placement.IsDragging) && Keyboard.current != null && Keyboard.current.escapeKey.wasPressedThisFrame)
Close();
float blend = 1f - Mathf.Exp(-animationSpeed * Time.unscaledDeltaTime);
if (!isOpen)
{
bookButton.transform.localScale = Vector3.Lerp(bookButton.transform.localScale,
bookScale * (bookHovered ? 1.04f : 1f), blend);
AnimateTabs(closedTabs, closedPositions, closedLabels, hoveredClosed, -1, blend);
}
else AnimateTabs(openTabs, openPositions, openLabels, hoveredOpen, selected, blend);
}
private void AnimateTabs(Button[] tabs, Vector2[] positions, TMP_Text[] labels,
int hovered, int active, float blend)
{
for (int i = 0; i < tabs.Length; i++)
{
bool show = i == hovered || i == active;
var rect = (RectTransform)tabs[i].transform;
// Reveal the full marker while retaining a small overlap with the page.
bool opened = tabs == openTabs;
var distances = opened ? openTabRevealDistances : closedTabRevealDistances;
float configured = distances != null && i < distances.Length ? distances[i] : -1f;
float automatic = opened
? Mathf.Max(0f, -positions[i].y - openTabOverlap)
: Mathf.Max(0f, positions[i].x - closedTabOverlap);
float distance = configured >= 0f ? configured : automatic;
var target = positions[i] + (show ? (opened ? Vector2.up : Vector2.left) * distance : Vector2.zero);
rect.anchoredPosition = Vector2.Lerp(rect.anchoredPosition, target, blend);
if (labels[i]) labels[i].gameObject.SetActive(show);
}
}
}
}