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,139 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NightclubArcadia.UI.Menu
|
||||
{
|
||||
/// <summary>
|
||||
/// Opens and closes the player menu from Tab (PlayerMenuInput) or an
|
||||
/// external Open() call (portrait click). Holds a PlayerControlLock so
|
||||
/// opening the menu during dialogue does not re-enable movement when the
|
||||
/// menu is closed — the dialogue holder remains in the lock set.
|
||||
/// </summary>
|
||||
public sealed class PlayerMenuController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] GameObject menuRoot;
|
||||
[SerializeField] PlayerMenuInput menuInput;
|
||||
[SerializeField] PlayerControlLock controlLock;
|
||||
[SerializeField] MenuTabBar tabBar;
|
||||
|
||||
bool isOpen;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
SubscribeToMenuInput();
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Re-run unconditionally: when this component is created by
|
||||
// UILayerBootstrap, AddComponent fires OnEnable *before* menuInput
|
||||
// has been assigned, so the OnEnable subscription is a no-op.
|
||||
// Subscribing again here (idempotent via -= then +=) is what makes
|
||||
// Tab work on both the bootstrap and the authored-prefab paths.
|
||||
SubscribeToMenuInput();
|
||||
|
||||
if (controlLock == null)
|
||||
{
|
||||
controlLock = FindFirstObjectByType<PlayerControlLock>();
|
||||
}
|
||||
|
||||
if (menuRoot != null)
|
||||
{
|
||||
PanelVisibility.Hide(menuRoot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves <see cref="menuInput"/> if unassigned and (re)subscribes.
|
||||
/// Safe to call repeatedly — the -= before += keeps it single-shot.
|
||||
/// </summary>
|
||||
void SubscribeToMenuInput()
|
||||
{
|
||||
if (menuInput == null)
|
||||
{
|
||||
menuInput = FindFirstObjectByType<PlayerMenuInput>();
|
||||
}
|
||||
|
||||
if (menuInput != null)
|
||||
{
|
||||
menuInput.MenuToggleRequested -= Toggle;
|
||||
menuInput.MenuToggleRequested += Toggle;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (menuInput != null)
|
||||
{
|
||||
menuInput.MenuToggleRequested -= Toggle;
|
||||
}
|
||||
|
||||
if (isOpen)
|
||||
{
|
||||
// Ensure we release the lock if destroyed while open.
|
||||
CloseInternal(notifyState: false);
|
||||
}
|
||||
}
|
||||
|
||||
public void Toggle()
|
||||
{
|
||||
if (isOpen)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
Open();
|
||||
}
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
if (isOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isOpen = true;
|
||||
|
||||
if (menuRoot != null)
|
||||
{
|
||||
PanelVisibility.Show(menuRoot);
|
||||
}
|
||||
|
||||
UIStateController.Instance?.SetMenuOpen(true);
|
||||
controlLock?.Acquire(this);
|
||||
tabBar?.SelectTab(0);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (!isOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseInternal(notifyState: true);
|
||||
}
|
||||
|
||||
void CloseInternal(bool notifyState)
|
||||
{
|
||||
isOpen = false;
|
||||
|
||||
if (menuRoot != null)
|
||||
{
|
||||
PanelVisibility.Hide(menuRoot);
|
||||
}
|
||||
|
||||
if (notifyState)
|
||||
{
|
||||
UIStateController.Instance?.SetMenuOpen(false);
|
||||
}
|
||||
|
||||
// Tab during dialogue works and is the whole point of the
|
||||
// ref-counted lock: the dialogue holder is still in the set, so
|
||||
// closing the menu leaves the player locked and the conversation
|
||||
// intact.
|
||||
controlLock?.Release(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user