using NightclubArcadia.Core;
using NightclubArcadia.Dialogue;
using NightclubArcadia.UI;
using UnityEngine;
using UnityEngine.InputSystem;
using Yarn.Unity;
namespace NightclubArcadia.Interaction
{
///
/// Anything the player can walk up to and talk to: a character, or an object with
/// something to say.
///
/// Replaces NPCStandIn, which was written to be dropped on a sphere primitive and
/// carried its identity in per-instance public fields. Two objects could claim the
/// same character and disagree, and in practice did — the scene's second "Bartender"
/// was the chair, renamed in the data but not in the hierarchy.
///
/// Identity now comes from an InteractableDefinition asset, so a prefab is the thing
/// and the scene only places it. Also replaces DialogueInteractable, a trigger-zone
/// variant of the same idea that was in no scene at all.
///
[RequireComponent(typeof(SphereCollider))]
public sealed class DialogueInteractor : MonoBehaviour, IClickInteractable
{
[Header("Identity")]
[SerializeField] InteractableDefinition definition;
[Header("Presentation (optional)")]
[SerializeField] WorldSpaceLabel label;
[SerializeField] InteractionPrompt prompt;
[Header("Overrides")]
[Tooltip("Leave empty to use the Canvas under the DialogueRunner.")]
[SerializeField] GameObject dialogueUI;
DialogueRunner runner;
Transform player;
InputAction interact;
bool playerInRange;
public Transform ApproachPoint => transform;
public float ApproachRadius =>
definition != null ? Mathf.Max(0.15f, definition.InteractionRange - 0.5f) : 0.15f;
///
/// The dialogue runner, resolved on demand and cached only on success.
///
/// Never resolved once in Awake: the runner lives in the Systems scene and this
/// object lives in a level, so it can wake up before the runner exists. Caching a
/// failure there left NPCs permanently mute for the session. See SceneOrderTests.
///
public DialogueRunner ResolvedRunner =>
runner != null ? runner : runner = SceneServices.TryResolve(null);
Transform Player =>
player != null ? player : player = SceneServices.Player;
void Awake()
{
if (definition == null)
{
Debug.LogError($"[Interactor] {name} has no InteractableDefinition; it will do nothing.", this);
enabled = false;
return;
}
GetComponent().isTrigger = false;
if (label != null)
{
label.gameObject.SetActive(definition.ShowLabel);
label.SetText(definition.DisplayName);
}
prompt?.SetShown(false);
interact = new InputAction("Interact");
interact.AddBinding("/f");
interact.AddBinding("/buttonSouth");
interact.performed += _ =>
{
if (playerInRange)
{
Interact();
}
};
}
void OnEnable() => interact?.Enable();
void OnDisable() => interact?.Disable();
void Update()
{
var target = Player;
if (target == null)
{
return;
}
var inRange = Vector3.Distance(transform.position, target.position) <= definition.InteractionRange;
if (inRange == playerInRange)
{
return;
}
playerInRange = inRange;
prompt?.SetShown(inRange);
}
/// Entry point for both the proximity keypress and click-to-move arrival.
public void Interact()
{
var dialogue = ResolvedRunner;
if (dialogue == null)
{
Debug.LogError(
$"[Interactor] {definition.DisplayName}: no DialogueRunner in any loaded scene. " +
"It lives in Systems.unity — open Bootstrap, or Tools → Nightclub Arcadia → " +
"Open Game Scenes.", this);
return;
}
if (UIStateController.Instance != null && UIStateController.Instance.IsMenuOpen)
{
return;
}
if (!dialogue.IsDialogueRunning)
{
StartDialogueAsync(dialogue);
}
}
public void OnApproached() => Interact();
async void StartDialogueAsync(DialogueRunner dialogue)
{
// The dialogue UI is hidden when the player leaves a conversation via
// <>. Make it visible again before the first line renders.
if (DialogueUIVisibility.Show(ResolveDialogueUI(dialogue)))
{
// Only reached if the UI was found deactivated; give TMP a frame to
// re-cache its Canvas in OnEnable before any text is generated.
await Awaitable.NextFrameAsync();
}
await dialogue.StartDialogue(definition.YarnNode);
}
GameObject ResolveDialogueUI(DialogueRunner dialogue)
{
if (dialogueUI != null)
{
return dialogueUI;
}
var canvas = dialogue.GetComponentInChildren