using UnityEngine;
using UnityEngine.InputSystem;
using TMPro;
using Yarn.Unity;
using NightclubArcadia.Dialogue;
///
/// Drop this on a sphere primitive to use it as a placeholder NPC:
/// - Sets sphere color per-instance
/// - Spawns a floating TextMeshPro name label that billboards to face the camera
/// - Shows a proximity interaction prompt ("[F]", "(X)", "(✕)") that swaps based on
/// the last input device used, via InputDeviceTracker
/// - Press the interact button while in range to start a Yarn Spinner dialogue node
/// Requires: TextMeshPro Essentials imported, Input System package installed,
/// InputDeviceTracker.cs in the project, and a "Player" tag on your player object
/// (or assign the Player field manually in the Inspector).
///
[RequireComponent(typeof(SphereCollider))]
public class NPCStandIn : MonoBehaviour
{
[Header("Identity")]
public string npcName = "Unnamed NPC";
public Color npcColor = Color.white;
[Header("Dialogue")]
public DialogueRunner dialogueRunner;
public string yarnStartNode = "Start";
[Tooltip("Optional. Left empty, the Canvas under the DialogueRunner is used.")]
public GameObject dialogueUI;
[Header("Label")]
public float labelHeight = 1.2f;
public float labelFontSize = 4f;
public Color labelColor = Color.white;
[Header("Interaction Prompt")]
public Transform player; // auto-found by "Player" tag if left empty
public float interactionRange = 3f;
public float promptHeight = 0.7f;
public float promptFontSize = 3f;
private TextMeshPro labelText;
private TextMeshPro promptText;
private Renderer rend;
private Camera mainCam;
private InputAction interactAction;
private bool playerInRange;
void Awake()
{
rend = GetComponent();
rend.material.color = npcColor;
mainCam = Camera.main;
if (player == null)
{
GameObject found = GameObject.FindGameObjectWithTag("Player");
if (found != null) player = found.transform;
}
CreateLabel();
CreatePrompt();
interactAction = new InputAction("Interact");
interactAction.AddBinding("/f");
interactAction.AddBinding("/buttonSouth");
interactAction.performed += ctx =>
{
if (playerInRange) Interact();
};
}
void OnEnable() => interactAction?.Enable();
void OnDisable() => interactAction?.Disable();
void CreateLabel()
{
GameObject labelObj = new GameObject($"{npcName}_Label");
labelObj.transform.SetParent(transform);
labelObj.transform.localPosition = Vector3.up * labelHeight;
labelText = labelObj.AddComponent();
labelText.text = npcName;
labelText.fontSize = labelFontSize;
labelText.color = labelColor;
labelText.alignment = TextAlignmentOptions.Center;
labelText.rectTransform.sizeDelta = new Vector2(2f, 0.5f);
}
void CreatePrompt()
{
GameObject promptObj = new GameObject($"{npcName}_Prompt");
promptObj.transform.SetParent(transform);
promptObj.transform.localPosition = Vector3.up * promptHeight;
promptText = promptObj.AddComponent();
promptText.fontSize = promptFontSize;
promptText.color = Color.yellow;
promptText.alignment = TextAlignmentOptions.Center;
promptText.rectTransform.sizeDelta = new Vector2(1.5f, 0.4f);
promptObj.SetActive(false); // hidden until player is in range
}
void Update()
{
if (player == null) return;
float dist = Vector3.Distance(transform.position, player.position);
bool inRangeNow = dist <= interactionRange;
if (inRangeNow != playerInRange)
{
playerInRange = inRangeNow;
promptText.gameObject.SetActive(playerInRange);
}
if (playerInRange)
{
promptText.text = GlyphFor(InputDeviceTracker.Current);
}
}
void LateUpdate()
{
if (mainCam == null) return;
if (labelText != null)
labelText.transform.rotation = Quaternion.LookRotation(
labelText.transform.position - mainCam.transform.position);
if (promptText != null)
promptText.transform.rotation = Quaternion.LookRotation(
promptText.transform.position - mainCam.transform.position);
}
static string GlyphFor(InputDeviceTracker.DeviceType type)
{
switch (type)
{
case InputDeviceTracker.DeviceType.Xbox: return "(X)";
case InputDeviceTracker.DeviceType.PlayStation: return "(\u2715)"; // Cross button
default: return "[F]";
}
}
public void Interact()
{
if (dialogueRunner == null)
{
Debug.LogWarning($"[NPCStandIn] No DialogueRunner assigned on {npcName}");
return;
}
if (!dialogueRunner.IsDialogueRunning)
{
StartDialogueAsync();
}
}
async void StartDialogueAsync()
{
// The dialogue UI is hidden when the player leaves a conversation via
// <>. Make it visible again before the runner
// renders its first line.
if (DialogueUIVisibility.Show(ResolveDialogueUI()))
{
// 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 dialogueRunner.StartDialogue(yarnStartNode);
}
///
/// The Canvas under the DialogueRunner, unless one was assigned explicitly.
/// Resolved at call time (including inactive children) so this works
/// without any per-NPC Inspector wiring.
///
GameObject ResolveDialogueUI()
{
if (dialogueUI != null)
{
return dialogueUI;
}
var canvas = dialogueRunner.GetComponentInChildren