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

102 lines
3.0 KiB
C#

using System.Collections.Generic;
using NightclubArcadia.Player;
using StarterAssets;
using UnityEngine;
namespace NightclubArcadia.UI
{
/// <summary>
/// Ref-counted freeze of the player's ThirdPersonController. Multiple
/// independent holders (dialogue, menu) can Acquire/Release; the controller
/// stays disabled until the last holder releases.
/// </summary>
public sealed class PlayerControlLock : MonoBehaviour
{
readonly HashSet<object> holders = new HashSet<object>();
ThirdPersonController playerController;
StarterAssetsInputs playerInputs;
Animator playerAnimator;
PlayerMotionArbiter motionArbiter;
static readonly int AnimIDSpeed = Animator.StringToHash("Speed");
static readonly int AnimIDMotionSpeed = Animator.StringToHash("MotionSpeed");
public bool IsLocked => holders.Count > 0;
void Awake()
{
playerController = GetComponent<ThirdPersonController>();
playerInputs = GetComponent<StarterAssetsInputs>();
playerAnimator = GetComponent<Animator>();
motionArbiter = GetComponent<PlayerMotionArbiter>();
}
public void Acquire(object reason)
{
if (reason == null)
{
throw new System.ArgumentNullException(nameof(reason));
}
var wasLocked = IsLocked;
holders.Add(reason);
if (!wasLocked && IsLocked)
{
ApplyLock(true);
}
}
public void Release(object reason)
{
if (reason == null)
{
return;
}
var wasLocked = IsLocked;
holders.Remove(reason);
if (wasLocked && !IsLocked)
{
ApplyLock(false);
}
}
void ApplyLock(bool locked)
{
if (playerController == null)
{
return;
}
if (locked)
{
// Clear pending input and the locomotion blend before freezing
// the controller. Disabling it mid-stride otherwise leaves
// Speed at its last value, and the character keeps running on
// the spot for the whole conversation.
if (playerInputs != null)
{
playerInputs.MoveInput(Vector2.zero);
playerInputs.LookInput(Vector2.zero);
playerInputs.SprintInput(false);
playerInputs.JumpInput(false);
}
if (playerAnimator != null)
{
playerAnimator.SetFloat(AnimIDSpeed, 0f);
playerAnimator.SetFloat(AnimIDMotionSpeed, 0f);
}
motionArbiter?.ResetState();
}
// cursor state hook — see plan §3.4
playerController.enabled = !locked;
}
}
}