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]>
102 lines
3.0 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|