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(true); if (canvas == null) { Debug.LogWarning( $"[Interactor] No Canvas under {dialogue.name} for {definition.DisplayName}", this); return null; } return canvas.gameObject; } } }