Files
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

68 lines
2.5 KiB
C#

using UnityEngine;
namespace NightclubArcadia.UI
{
/// <summary>
/// Shows and hides a UI panel *without* deactivating its GameObject.
///
/// SetActive(false) is not a safe way to hide this UI. TextMeshProUGUI
/// resolves its Canvas through Graphic.CacheCanvas, which calls
/// GetComponentsInParent(includeInactive: false) and then requires
/// canvas.isActiveAndEnabled. Once the Canvas GameObject has been
/// deactivated, that lookup yields null, and TMP.GetTextInfo does:
///
/// if (m_canvas == null) m_canvas = this.canvas; // still null
/// GenerateTextMesh(); // m_canvas.worldCamera -> NRE
///
/// GenerateTextMesh dereferences m_canvas unguarded, so the very next line
/// Yarn tries to type throws NullReferenceException. Keeping the hierarchy
/// active and driving a CanvasGroup instead means the cached Canvas
/// reference stays valid across any number of enter/exit dialogue cycles.
/// </summary>
public static class PanelVisibility
{
/// <summary>
/// Makes the panel visible and interactive. Returns true if the
/// GameObject had to be re-activated (legacy scene state), in which
/// case the caller must wait one frame before rendering text so that
/// TMP's OnEnable can re-cache its Canvas.
/// </summary>
public static bool Show(GameObject panel) => SetVisible(panel, true);
/// <summary>
/// Hides the panel, leaving the hierarchy active. See class docs.
/// </summary>
public static void Hide(GameObject panel) => SetVisible(panel, false);
static bool SetVisible(GameObject panel, bool visible)
{
if (panel == null)
{
return false;
}
// The UI may still be deactivated by an older saved scene state or
// by code that predates this helper. Recover from that, and tell
// the caller a frame of grace is needed.
var wasReactivated = false;
if (!panel.activeSelf)
{
panel.SetActive(true);
wasReactivated = true;
}
var group = panel.GetComponent<CanvasGroup>();
if (group == null)
{
group = panel.AddComponent<CanvasGroup>();
}
group.alpha = visible ? 1f : 0f;
group.interactable = visible;
group.blocksRaycasts = visible;
return wasReactivated;
}
}
}