add UI layer: dialogue stream, HUD, player menu, and skills panel
Runtime UI under Scripts/UI: a UIStateController owning menu/dialogue state, a PlayerControlLock so opening a panel suppresses world input, panel visibility helpers, and the menu input binding. Views: the dialogue stream presenter and its options presenter and message row, the portrait HUD, and the player menu with character, journal, and tab-bar views. Skills gains SkillsPanelView and SkillRowView, which is why the Skills asmdef now references UnityEngine.UI. Prefabs/UI holds the authored canvases (dialogue stream, HUD, player menu) and their rows. UILayerBootstrap remains a hand-added runtime fallback for scenes where those prefabs are not instantiated; it deliberately does not auto-spawn. Editor/UILayerSetup.cs is the one-shot menu that builds and wires those prefabs into a scene, with UILayerSetupAutoRun reminding after a script reload. Art/UI/Placeholder carries the placeholder portrait the HUD falls back to. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace NightclubArcadia.UI.Menu
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects among menu tabs. Panels are toggled via CanvasGroup (never
|
||||
/// SetActive) so TMP's cached Canvas stays valid. Each tab can fire an
|
||||
/// Inspector-wired UnityEvent on show — used by SkillsPanelView.Refresh
|
||||
/// without a cross-assembly interface.
|
||||
/// </summary>
|
||||
public sealed class MenuTabBar : MonoBehaviour
|
||||
{
|
||||
[Serializable]
|
||||
public struct TabEntry
|
||||
{
|
||||
public Button button;
|
||||
public CanvasGroup panel;
|
||||
public UnityEvent onShown;
|
||||
}
|
||||
|
||||
[SerializeField] TabEntry[] tabs = Array.Empty<TabEntry>();
|
||||
|
||||
int currentIndex = -1;
|
||||
bool bound;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
BindButtons();
|
||||
}
|
||||
|
||||
void BindButtons()
|
||||
{
|
||||
if (bound || tabs == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < tabs.Length; i++)
|
||||
{
|
||||
var index = i;
|
||||
if (tabs[i].button != null)
|
||||
{
|
||||
tabs[i].button.onClick.RemoveAllListeners();
|
||||
tabs[i].button.onClick.AddListener(() => SelectTab(index));
|
||||
}
|
||||
}
|
||||
|
||||
bound = tabs.Length > 0;
|
||||
}
|
||||
|
||||
/// <summary>Runtime / bootstrap entry point for assigning tabs after AddComponent.</summary>
|
||||
public void SetTabs(TabEntry[] entries)
|
||||
{
|
||||
tabs = entries ?? System.Array.Empty<TabEntry>();
|
||||
bound = false;
|
||||
BindButtons();
|
||||
}
|
||||
|
||||
public void SelectTab(int index)
|
||||
{
|
||||
if (tabs == null || tabs.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
index = Mathf.Clamp(index, 0, tabs.Length - 1);
|
||||
currentIndex = index;
|
||||
|
||||
for (var i = 0; i < tabs.Length; i++)
|
||||
{
|
||||
var active = i == index;
|
||||
var panel = tabs[i].panel;
|
||||
if (panel != null)
|
||||
{
|
||||
panel.alpha = active ? 1f : 0f;
|
||||
panel.interactable = active;
|
||||
panel.blocksRaycasts = active;
|
||||
}
|
||||
}
|
||||
|
||||
tabs[index].onShown?.Invoke();
|
||||
|
||||
if (tabs[index].button != null && EventSystem.current != null)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(tabs[index].button.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user