187 lines
5.3 KiB
C#
187 lines
5.3 KiB
C#
|
|
using System;
|
||
|
|
using System.Collections;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.Events;
|
||
|
|
|
||
|
|
public enum NPCType
|
||
|
|
{
|
||
|
|
Villager,
|
||
|
|
QuestGiver,
|
||
|
|
Vendor,
|
||
|
|
Guard,
|
||
|
|
Trainer
|
||
|
|
}
|
||
|
|
|
||
|
|
public enum NPCState
|
||
|
|
{
|
||
|
|
Idle,
|
||
|
|
Wandering,
|
||
|
|
Talking
|
||
|
|
}
|
||
|
|
|
||
|
|
public enum DialogueBehavior
|
||
|
|
{
|
||
|
|
SingleLine,
|
||
|
|
Sequential,
|
||
|
|
Random
|
||
|
|
}
|
||
|
|
|
||
|
|
[RequireComponent(typeof(Collider))]
|
||
|
|
public class NPC : MonoBehaviour, IInteractable
|
||
|
|
{
|
||
|
|
[Header("Identity")]
|
||
|
|
[SerializeField] private string npcName = "Villager";
|
||
|
|
[SerializeField] private string npcTitle = "Townsperson";
|
||
|
|
[SerializeField] private Sprite portrait;
|
||
|
|
|
||
|
|
[Header("NPC Configuration")]
|
||
|
|
[SerializeField] private NPCType npcType = NPCType.Villager;
|
||
|
|
[SerializeField] private NPCState currentState = NPCState.Idle;
|
||
|
|
[SerializeField] private float interactionRadius = 2.5f;
|
||
|
|
|
||
|
|
[Header("Dialogue Settings")]
|
||
|
|
[SerializeField] private DialogueBehavior dialogueType = DialogueBehavior.Sequential;
|
||
|
|
[TextArea(2, 5)]
|
||
|
|
[SerializeField] private string[] dialogueLines = new string[] { "Hello traveler!" };
|
||
|
|
[SerializeField] private AudioClip talkSound;
|
||
|
|
|
||
|
|
[Header("Wander Settings")]
|
||
|
|
[SerializeField] private float wanderRadius = 5.0f;
|
||
|
|
[SerializeField] private float wanderInterval = 4.0f;
|
||
|
|
[SerializeField] private float moveSpeed = 2.0f;
|
||
|
|
|
||
|
|
[Header("Vendor / Quest Data")]
|
||
|
|
[SerializeField] private int questId = 0;
|
||
|
|
[SerializeField] private List<string> vendorInventory = new List<string>();
|
||
|
|
|
||
|
|
[Header("Events")]
|
||
|
|
[SerializeField] private UnityEvent<GameObject> onInteractionStart;
|
||
|
|
[SerializeField] private UnityEvent<string> onSpeakLine;
|
||
|
|
|
||
|
|
private int currentDialogueIndex = 0;
|
||
|
|
private Vector3 initialPosition;
|
||
|
|
private Vector3 wanderTarget;
|
||
|
|
private float wanderTimer;
|
||
|
|
|
||
|
|
private void Start()
|
||
|
|
{
|
||
|
|
initialPosition = transform.position;
|
||
|
|
wanderTimer = wanderInterval;
|
||
|
|
|
||
|
|
// Ensure collider is present for raycasting
|
||
|
|
Collider col = GetComponent<Collider>();
|
||
|
|
if (col != null && !col.enabled) col.enabled = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void Update()
|
||
|
|
{
|
||
|
|
if (currentState == NPCState.Wandering)
|
||
|
|
{
|
||
|
|
ProcessWander();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- IInteractable Implementation ---
|
||
|
|
public void Interact(GameObject interactor)
|
||
|
|
{
|
||
|
|
// Face player on interact
|
||
|
|
Vector3 dir = (interactor.transform.position - transform.position);
|
||
|
|
dir.y = 0;
|
||
|
|
if (dir != Vector3.zero)
|
||
|
|
{
|
||
|
|
transform.rotation = Quaternion.LookRotation(dir);
|
||
|
|
}
|
||
|
|
|
||
|
|
currentState = NPCState.Talking;
|
||
|
|
|
||
|
|
// Trigger dialogue / action based on NPC type
|
||
|
|
SpeakNextLine();
|
||
|
|
onInteractionStart?.Invoke(interactor);
|
||
|
|
|
||
|
|
if (talkSound != null)
|
||
|
|
{
|
||
|
|
AudioSource.PlayClipAtPoint(talkSound, transform.position);
|
||
|
|
}
|
||
|
|
|
||
|
|
Debug.Log($"[{npcName} ({npcType})] Interacted with {interactor.name}");
|
||
|
|
}
|
||
|
|
|
||
|
|
public string GetInteractionPrompt()
|
||
|
|
{
|
||
|
|
return npcType switch
|
||
|
|
{
|
||
|
|
NPCType.QuestGiver => $"Talk to {npcName} [Quest]",
|
||
|
|
NPCType.Vendor => $"Trade with {npcName}",
|
||
|
|
NPCType.Guard => $"Ask {npcName}",
|
||
|
|
_ => $"Talk to {npcName}"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
public float GetInteractionRadius() => interactionRadius;
|
||
|
|
public Transform GetTransform() => transform;
|
||
|
|
|
||
|
|
// --- Dialogue Handling ---
|
||
|
|
private void SpeakNextLine()
|
||
|
|
{
|
||
|
|
if (dialogueLines == null || dialogueLines.Length == 0) return;
|
||
|
|
|
||
|
|
string lineToSpeak = "";
|
||
|
|
|
||
|
|
switch (dialogueType)
|
||
|
|
{
|
||
|
|
case DialogueBehavior.SingleLine:
|
||
|
|
lineToSpeak = dialogueLines[0];
|
||
|
|
break;
|
||
|
|
|
||
|
|
case DialogueBehavior.Sequential:
|
||
|
|
lineToSpeak = dialogueLines[currentDialogueIndex];
|
||
|
|
currentDialogueIndex = (currentDialogueIndex + 1) % dialogueLines.Length;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case DialogueBehavior.Random:
|
||
|
|
int randIndex = UnityEngine.Random.Range(0, dialogueLines.Length);
|
||
|
|
lineToSpeak = dialogueLines[randIndex];
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
onSpeakLine?.Invoke(lineToSpeak);
|
||
|
|
Debug.Log($"<color=cyan>{npcName}:</color> \"{lineToSpeak}\"");
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Simple Wandering Logic ---
|
||
|
|
private void ProcessWander()
|
||
|
|
{
|
||
|
|
wanderTimer += Time.deltaTime;
|
||
|
|
if (wanderTimer >= wanderInterval)
|
||
|
|
{
|
||
|
|
wanderTimer = 0f;
|
||
|
|
Vector2 randomCircle = UnityEngine.Random.insideUnitCircle * wanderRadius;
|
||
|
|
wanderTarget = initialPosition + new Vector3(randomCircle.x, 0, randomCircle.y);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (wanderTarget != Vector3.zero)
|
||
|
|
{
|
||
|
|
Vector3 moveDir = (wanderTarget - transform.position);
|
||
|
|
moveDir.y = 0;
|
||
|
|
if (moveDir.magnitude > 0.3f)
|
||
|
|
{
|
||
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(moveDir), 8.0f * Time.deltaTime);
|
||
|
|
transform.position += moveDir.normalized * moveSpeed * Time.deltaTime;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnDrawGizmosSelected()
|
||
|
|
{
|
||
|
|
Gizmos.color = Color.yellow;
|
||
|
|
Gizmos.DrawWireSphere(transform.position, interactionRadius);
|
||
|
|
|
||
|
|
if (currentState == NPCState.Wandering)
|
||
|
|
{
|
||
|
|
Gizmos.color = Color.blue;
|
||
|
|
Gizmos.DrawWireSphere(initialPosition, wanderRadius);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|