Files
nightclub-arcadia/NightclubArcadia/Assets/Scripts/UI/Menu/PlayerMenuController.cs
T
lennartandClaude Opus 5 cba87dcb31 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]>
2026-08-25 19:03:42 +02:00

140 lines
3.7 KiB
C#

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);
}
}
}