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:
2026-08-25 19:03:42 +02:00
co-authored by Claude Opus 5
parent 932cd0582f
commit cba87dcb31
57 changed files with 7707 additions and 1 deletions
@@ -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);
}
}
}
}