Working on quests, moved to CineMachine for camera and started to build out areas, started combat system
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.AI;
|
||||
using EasyTalk.Controller;
|
||||
using EasyTalk.Nodes;
|
||||
|
||||
public abstract class Character : MonoBehaviour
|
||||
{
|
||||
@@ -34,6 +36,22 @@ public abstract class Character : MonoBehaviour
|
||||
[SerializeField]
|
||||
protected int MaxHp = 10;
|
||||
|
||||
[Header("Dialogue")]
|
||||
public bool hasDialogue = false;
|
||||
[Tooltip("The Dialogue asset for this character. Will be loaded into the central DialogueController.")]
|
||||
public Dialogue dialogue;
|
||||
[Tooltip("Optional: Entry point name for starting dialogue. Leave empty to use default entry point.")]
|
||||
public string dialogueEntryPoint = "";
|
||||
[Tooltip("Name of the GameObject with the central DialogueController. Leave empty to auto-find.")]
|
||||
public string dialogueManagerName = "Manager";
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField]
|
||||
protected bool enableDebugLogs = false;
|
||||
|
||||
// Cache the central DialogueController
|
||||
protected static DialogueController centralDialogueController;
|
||||
|
||||
[Header("Components")]
|
||||
// Optional movement controller reference, assign in inspector if used
|
||||
public NavMeshMovementController MovementController;
|
||||
@@ -219,6 +237,174 @@ public abstract class Character : MonoBehaviour
|
||||
SetFloatIfExists(paramHeadVertical, vertical);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make character look at a target with head rotation, body rotation when clamped
|
||||
/// </summary>
|
||||
public void LookAtTarget(Transform target)
|
||||
{
|
||||
if (target == null) return;
|
||||
|
||||
Vector3 directionToTarget = target.position - transform.position;
|
||||
directionToTarget.y = 0; // Keep on horizontal plane
|
||||
|
||||
if (directionToTarget.sqrMagnitude < 0.01f) return;
|
||||
|
||||
// Calculate angle to target
|
||||
float angleToTarget = Vector3.SignedAngle(transform.forward, directionToTarget, Vector3.up);
|
||||
|
||||
// Head rotation limits (before body needs to rotate)
|
||||
const float headHorizontalLimit = 60f;
|
||||
|
||||
if (Mathf.Abs(angleToTarget) > headHorizontalLimit)
|
||||
{
|
||||
// Rotate body to face target
|
||||
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 5f);
|
||||
|
||||
// Reset head to neutral when body rotates
|
||||
SetHeadLook(0f, 0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Just rotate head
|
||||
float normalizedAngle = angleToTarget / headHorizontalLimit; // -1 to 1
|
||||
SetHeadLook(normalizedAngle, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when player interacts with this character. Override to add custom behavior.
|
||||
/// </summary>
|
||||
public virtual void OnInteract(GameObject player)
|
||||
{
|
||||
Log($"Interacting with {gameObject.name}");
|
||||
|
||||
// Look at player
|
||||
if (player != null)
|
||||
{
|
||||
LookAtTarget(player.transform);
|
||||
|
||||
// Make player look at this character
|
||||
Character playerChar = player.GetComponent<Character>();
|
||||
if (playerChar != null)
|
||||
{
|
||||
playerChar.LookAtTarget(transform);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDialogue)
|
||||
{
|
||||
ShowDialogue();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display dialogue for this character using EasyTalk with centralized DialogueController
|
||||
/// </summary>
|
||||
/// <param name="entryPoint">Optional entry point override. If null/empty, uses dialogueEntryPoint field.</param>
|
||||
protected virtual void ShowDialogue(string entryPoint = null)
|
||||
{
|
||||
// Find the central DialogueController if not cached
|
||||
if (centralDialogueController == null)
|
||||
{
|
||||
centralDialogueController = FindCentralDialogueController();
|
||||
}
|
||||
|
||||
// Check if we have both a controller and dialogue asset
|
||||
if (centralDialogueController != null && dialogue != null)
|
||||
{
|
||||
// Load this character's dialogue into the central controller
|
||||
centralDialogueController.ChangeDialogue(dialogue);
|
||||
|
||||
// Use provided entry point, fallback to field, then default
|
||||
string targetEntryPoint = !string.IsNullOrEmpty(entryPoint) ? entryPoint : dialogueEntryPoint;
|
||||
|
||||
// Play dialogue with optional entry point
|
||||
if (!string.IsNullOrEmpty(targetEntryPoint))
|
||||
{
|
||||
centralDialogueController.PlayDialogue(targetEntryPoint);
|
||||
Log($"{gameObject.name}: Starting EasyTalk dialogue at entry point '{targetEntryPoint}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
centralDialogueController.PlayDialogue();
|
||||
Log($"{gameObject.name}: Starting EasyTalk dialogue");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Warnings for missing components
|
||||
if (centralDialogueController == null)
|
||||
{
|
||||
Debug.LogWarning($"No central DialogueController found. Make sure there's a DialogueController on a GameObject named '{dialogueManagerName}'.", gameObject);
|
||||
}
|
||||
if (dialogue == null)
|
||||
{
|
||||
Debug.LogWarning($"No Dialogue asset assigned to {gameObject.name}. Assign a Dialogue asset in the inspector.", gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public wrapper for UnityEvents or other triggers
|
||||
/// </summary>
|
||||
/// <param name="entryPoint">Optional entry point name. If empty, uses default dialogueEntryPoint.</param>
|
||||
public virtual void StartDialogue(string entryPoint = "")
|
||||
{
|
||||
// Look at player if available
|
||||
if (Player.current != null)
|
||||
{
|
||||
LookAtTarget(Player.current.transform);
|
||||
|
||||
// Make player look at this character
|
||||
Player.current.LookAtTarget(transform);
|
||||
}
|
||||
|
||||
ShowDialogue(entryPoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the central DialogueController in the scene
|
||||
/// </summary>
|
||||
protected DialogueController FindCentralDialogueController()
|
||||
{
|
||||
// First, try to find by the specified manager name
|
||||
if (!string.IsNullOrEmpty(dialogueManagerName))
|
||||
{
|
||||
GameObject manager = GameObject.Find(dialogueManagerName);
|
||||
if (manager != null)
|
||||
{
|
||||
DialogueController controller = manager.GetComponent<DialogueController>();
|
||||
if (controller != null)
|
||||
{
|
||||
Log($"Found central DialogueController on '{dialogueManagerName}'");
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: search for any DialogueController in the scene
|
||||
DialogueController foundController = FindAnyObjectByType<DialogueController>();
|
||||
if (foundController != null)
|
||||
{
|
||||
Log($"Found DialogueController via FindAnyObjectByType on '{foundController.gameObject.name}'");
|
||||
return foundController;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debug logging utility
|
||||
/// </summary>
|
||||
protected void Log(string message)
|
||||
{
|
||||
if (enableDebugLogs)
|
||||
{
|
||||
Debug.Log($"[{GetType().Name}] {message}", gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyStaticAnimatorParams()
|
||||
{
|
||||
if (animator == null) return;
|
||||
@@ -322,6 +508,9 @@ public abstract class Character : MonoBehaviour
|
||||
yield return new WaitForSeconds(attackHoldSeconds);
|
||||
}
|
||||
SetBoolIfExists(paramShoot, false);
|
||||
|
||||
// Wait one frame for animator to process the Shoot_b = false transition
|
||||
yield return null;
|
||||
|
||||
if (restoreWeaponAfterAttack)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ public class ClickToMoveInputSystem : MonoBehaviour
|
||||
Vector2 pointerScreenPos;
|
||||
PlayerInteractionController cachedInteraction;
|
||||
NavMeshMovementController cachedMovement;
|
||||
PlayerCombatController cachedCombat;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
@@ -46,11 +47,13 @@ public class ClickToMoveInputSystem : MonoBehaviour
|
||||
{
|
||||
cachedInteraction = null;
|
||||
cachedMovement = null;
|
||||
cachedCombat = null;
|
||||
return;
|
||||
}
|
||||
|
||||
cachedInteraction = Player.current.GetComponent<PlayerInteractionController>();
|
||||
cachedMovement = Player.current.MovementController;
|
||||
cachedCombat = Player.current.GetComponent<PlayerCombatController>();
|
||||
if (cachedMovement == null)
|
||||
{
|
||||
cachedMovement = Player.current.GetComponent<NavMeshMovementController>();
|
||||
@@ -77,6 +80,16 @@ public class ClickToMoveInputSystem : MonoBehaviour
|
||||
return cachedMovement;
|
||||
}
|
||||
|
||||
PlayerCombatController GetCombat()
|
||||
{
|
||||
if (Player.current == null) return null;
|
||||
if (cachedCombat == null || cachedCombat.gameObject != Player.current.gameObject)
|
||||
{
|
||||
CachePlayerReferences();
|
||||
}
|
||||
return cachedCombat;
|
||||
}
|
||||
|
||||
void Log(string message)
|
||||
{
|
||||
if (enableDebugLogs)
|
||||
@@ -189,10 +202,35 @@ public class ClickToMoveInputSystem : MonoBehaviour
|
||||
// First check if right-clicking on a building to interact with it
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, 500f, selectionLayers))
|
||||
{
|
||||
// Check for enemy combat target first
|
||||
var enemy = hit.collider.GetComponentInParent<Enemy>();
|
||||
if (enemy != null)
|
||||
{
|
||||
var combat = GetCombat();
|
||||
if (combat != null)
|
||||
{
|
||||
combat.SetTargetEnemy(enemy);
|
||||
Log($"Set combat target: {enemy.enemyName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: move to enemy and look at them
|
||||
var movement = GetMovement();
|
||||
if (movement != null)
|
||||
{
|
||||
movement.MoveToAndLookAt(enemy.transform.position, enemy.transform.position);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for NPC interaction first
|
||||
var npc = hit.collider.GetComponentInParent<NPC>();
|
||||
if (npc != null)
|
||||
{
|
||||
var combat = GetCombat();
|
||||
if (combat != null) combat.ClearCombatTarget();
|
||||
|
||||
var interaction = GetInteraction();
|
||||
if (interaction != null)
|
||||
{
|
||||
@@ -215,6 +253,9 @@ public class ClickToMoveInputSystem : MonoBehaviour
|
||||
var building = hit.collider.GetComponentInParent<Building>();
|
||||
if (building != null)
|
||||
{
|
||||
var combat = GetCombat();
|
||||
if (combat != null) combat.ClearCombatTarget();
|
||||
|
||||
// If building can be entered and has an entry point, move to the door
|
||||
if (building.canEnter && building.entryPoint != null)
|
||||
{
|
||||
@@ -252,6 +293,9 @@ public class ClickToMoveInputSystem : MonoBehaviour
|
||||
var interactiveObject = hit.collider.GetComponentInParent<IInteractiveObject>();
|
||||
if (interactiveObject != null)
|
||||
{
|
||||
var combat = GetCombat();
|
||||
if (combat != null) combat.ClearCombatTarget();
|
||||
|
||||
var interaction = GetInteraction();
|
||||
if (interaction != null)
|
||||
{
|
||||
@@ -277,6 +321,9 @@ public class ClickToMoveInputSystem : MonoBehaviour
|
||||
// If not an interactive object, check for movement destination
|
||||
if (Physics.Raycast(ray, out RaycastHit groundHit, 500f, movementLayers))
|
||||
{
|
||||
var combat = GetCombat();
|
||||
if (combat != null) combat.ClearCombatTarget();
|
||||
|
||||
var movement = GetMovement();
|
||||
if (movement != null)
|
||||
{
|
||||
|
||||
102
Assets/Scripts/Enemy.cs
Normal file
102
Assets/Scripts/Enemy.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class Enemy : Character
|
||||
{
|
||||
[Header("Enemy Settings")]
|
||||
public string enemyName = "Enemy";
|
||||
public int contactDamage = 1;
|
||||
public float aggroRange = 6f;
|
||||
public float attackRange = 1.5f;
|
||||
public float attackCooldown = 1f;
|
||||
public bool autoAcquirePlayer = true;
|
||||
|
||||
|
||||
|
||||
private float nextAttackTime = 0f;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
UpdateTargetAndCombat();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when player interacts with this enemy
|
||||
/// </summary>
|
||||
public override void OnInteract(GameObject player)
|
||||
{
|
||||
// Call base implementation (handles look-at and dialogue)
|
||||
base.OnInteract(player);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void UpdateTargetAndCombat()
|
||||
{
|
||||
if (autoAcquirePlayer && target == null)
|
||||
{
|
||||
TryAcquirePlayer();
|
||||
}
|
||||
|
||||
if (target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float distance = Vector3.Distance(transform.position, target.transform.position);
|
||||
if (distance > aggroRange)
|
||||
{
|
||||
target = null;
|
||||
if (MovementController != null)
|
||||
{
|
||||
MovementController.Stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (MovementController != null)
|
||||
{
|
||||
if (distance > attackRange)
|
||||
{
|
||||
MovementController.MoveTo(target.transform.position);
|
||||
}
|
||||
else
|
||||
{
|
||||
MovementController.Stop();
|
||||
MovementController.SetLookTarget(target.transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
if (distance <= attackRange && Time.time >= nextAttackTime)
|
||||
{
|
||||
nextAttackTime = Time.time + attackCooldown;
|
||||
TriggerAttack();
|
||||
target.TakeDamage(contactDamage);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryAcquirePlayer()
|
||||
{
|
||||
if (Player.current == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float distance = Vector3.Distance(transform.position, Player.current.transform.position);
|
||||
if (distance <= aggroRange)
|
||||
{
|
||||
SetTarget(Player.current);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
// Draw aggro range
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(transform.position, aggroRange);
|
||||
|
||||
// Draw attack range
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, attackRange);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Enemy.cs.meta
Normal file
2
Assets/Scripts/Enemy.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7dbaebd502c3bf048a421cc649730492
|
||||
8
Assets/Scripts/Managers.meta
Normal file
8
Assets/Scripts/Managers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af098d5ef0d53294e888e6362c26dbae
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
180
Assets/Scripts/Managers/CameraManager.cs
Normal file
180
Assets/Scripts/Managers/CameraManager.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
|
||||
public class CameraManager : MonoBehaviour
|
||||
{
|
||||
public static CameraManager Instance { get; private set; }
|
||||
|
||||
[System.Serializable]
|
||||
public class CameraEntry
|
||||
{
|
||||
public string id;
|
||||
public CinemachineVirtualCameraBase camera;
|
||||
public int activePriority = 20;
|
||||
public int inactivePriority = 10;
|
||||
}
|
||||
|
||||
[Header("Setup")]
|
||||
public bool dontDestroyOnLoad = true;
|
||||
public bool disableInactiveCameras = false;
|
||||
public CinemachineBrain brain;
|
||||
|
||||
[Header("Cameras")]
|
||||
public List<CameraEntry> cameras = new List<CameraEntry>();
|
||||
|
||||
private readonly Dictionary<string, CameraEntry> cameraLookup = new Dictionary<string, CameraEntry>();
|
||||
private CameraEntry current;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
if (dontDestroyOnLoad)
|
||||
{
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
RebuildLookup();
|
||||
if (brain == null)
|
||||
{
|
||||
brain = FindAnyObjectByType<CinemachineBrain>();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
for (int i = 0; i < cameras.Count; i++)
|
||||
{
|
||||
CameraEntry entry = cameras[i];
|
||||
if (entry != null && string.IsNullOrEmpty(entry.id) && entry.camera != null)
|
||||
{
|
||||
entry.id = entry.camera.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RebuildLookup()
|
||||
{
|
||||
cameraLookup.Clear();
|
||||
foreach (CameraEntry entry in cameras)
|
||||
{
|
||||
if (entry == null || entry.camera == null || string.IsNullOrEmpty(entry.id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!cameraLookup.ContainsKey(entry.id))
|
||||
{
|
||||
cameraLookup.Add(entry.id, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SwitchTo(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id)) return;
|
||||
if (!cameraLookup.TryGetValue(id, out CameraEntry entry) || entry.camera == null)
|
||||
{
|
||||
Debug.LogWarning($"CameraManager: No camera registered with id '{id}'.", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
SwitchTo(entry.camera);
|
||||
}
|
||||
|
||||
public void SwitchTo(CinemachineVirtualCameraBase targetCamera)
|
||||
{
|
||||
if (targetCamera == null) return;
|
||||
|
||||
foreach (CameraEntry entry in cameras)
|
||||
{
|
||||
if (entry == null || entry.camera == null) continue;
|
||||
|
||||
bool isActive = entry.camera == targetCamera;
|
||||
entry.camera.Priority = isActive ? entry.activePriority : entry.inactivePriority;
|
||||
|
||||
if (disableInactiveCameras)
|
||||
{
|
||||
entry.camera.gameObject.SetActive(isActive);
|
||||
}
|
||||
|
||||
if (isActive)
|
||||
{
|
||||
current = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CinemachineVirtualCameraBase GetActiveCamera()
|
||||
{
|
||||
return current != null ? current.camera : null;
|
||||
}
|
||||
|
||||
public void SetFollow(Transform followTarget)
|
||||
{
|
||||
CinemachineVirtualCameraBase cam = GetActiveCamera();
|
||||
if (cam != null)
|
||||
{
|
||||
cam.Follow = followTarget;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLookAt(Transform lookTarget)
|
||||
{
|
||||
CinemachineVirtualCameraBase cam = GetActiveCamera();
|
||||
if (cam != null)
|
||||
{
|
||||
cam.LookAt = lookTarget;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDefaultBlend(string style, float time)
|
||||
{
|
||||
if (brain == null)
|
||||
{
|
||||
brain = FindAnyObjectByType<CinemachineBrain>();
|
||||
}
|
||||
|
||||
if (brain != null && System.Enum.TryParse<CinemachineBlendDefinition.Styles>(style, out var blendStyle))
|
||||
{
|
||||
brain.DefaultBlend = new CinemachineBlendDefinition(blendStyle, time);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterCamera(string id, CinemachineVirtualCameraBase cam, int activePriority = 20, int inactivePriority = 10)
|
||||
{
|
||||
if (cam == null || string.IsNullOrEmpty(id)) return;
|
||||
|
||||
CameraEntry entry = new CameraEntry
|
||||
{
|
||||
id = id,
|
||||
camera = cam,
|
||||
activePriority = activePriority,
|
||||
inactivePriority = inactivePriority
|
||||
};
|
||||
|
||||
cameras.Add(entry);
|
||||
RebuildLookup();
|
||||
}
|
||||
|
||||
public void UnregisterCamera(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id)) return;
|
||||
|
||||
for (int i = cameras.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (cameras[i] != null && cameras[i].id == id)
|
||||
{
|
||||
cameras.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
RebuildLookup();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/CameraManager.cs.meta
Normal file
2
Assets/Scripts/Managers/CameraManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 301957f69b431174f9fb965be9b869db
|
||||
@@ -1,6 +1,4 @@
|
||||
using UnityEngine;
|
||||
using EasyTalk.Controller;
|
||||
using EasyTalk.Nodes;
|
||||
|
||||
public enum NPCType
|
||||
{
|
||||
@@ -18,27 +16,11 @@ public class NPC : Character
|
||||
public string npcName = "NPC";
|
||||
public NPCType npcType = NPCType.Villager;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField]
|
||||
private bool enableDebugLogs = false;
|
||||
|
||||
[Header("Interaction Capabilities")]
|
||||
public bool hasDialogue = true;
|
||||
public bool isQuestGiver = false;
|
||||
public bool isMerchant = false;
|
||||
public bool isTrainer = false;
|
||||
|
||||
[Header("EasyTalk Integration")]
|
||||
[Tooltip("The Dialogue asset for this NPC. Will be loaded into the central DialogueController.")]
|
||||
public Dialogue dialogue;
|
||||
[Tooltip("Optional: Entry point name for starting dialogue. Leave empty to use default entry point.")]
|
||||
public string dialogueEntryPoint = "";
|
||||
[Tooltip("Name of the GameObject with the central DialogueController. Leave empty to auto-find.")]
|
||||
public string dialogueManagerName = "Manager";
|
||||
|
||||
// Cache the central DialogueController
|
||||
private static DialogueController centralDialogueController;
|
||||
|
||||
[Header("Fallback Dialogue (if no EasyTalk)")]
|
||||
[TextArea(3, 10)]
|
||||
public string greetingText = "Hello, traveler!";
|
||||
@@ -58,15 +40,12 @@ public class NPC : Character
|
||||
/// <summary>
|
||||
/// Called when player interacts with this NPC
|
||||
/// </summary>
|
||||
public virtual void OnInteract(GameObject player)
|
||||
public override void OnInteract(GameObject player)
|
||||
{
|
||||
Log($"Interacting with {npcName} ({npcType})");
|
||||
|
||||
if (hasDialogue)
|
||||
{
|
||||
ShowDialogue();
|
||||
}
|
||||
// Call base implementation (handles look-at and dialogue)
|
||||
base.OnInteract(player);
|
||||
|
||||
// NPC-specific interactions
|
||||
if (isQuestGiver && !string.IsNullOrEmpty(questId))
|
||||
{
|
||||
//OfferQuest(player);
|
||||
@@ -84,80 +63,18 @@ public class NPC : Character
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display dialogue for this NPC using EasyTalk with centralized DialogueController
|
||||
/// Override to add NPC-specific dialogue fallback
|
||||
/// </summary>
|
||||
protected virtual void ShowDialogue()
|
||||
protected override void ShowDialogue(string entryPoint = null)
|
||||
{
|
||||
// Find the central DialogueController if not cached
|
||||
if (centralDialogueController == null)
|
||||
base.ShowDialogue(entryPoint);
|
||||
|
||||
// Fallback to simple greeting if no dialogue was shown
|
||||
if (centralDialogueController == null || dialogue == null)
|
||||
{
|
||||
centralDialogueController = FindCentralDialogueController();
|
||||
}
|
||||
|
||||
// Check if we have both a controller and dialogue asset
|
||||
if (centralDialogueController != null && dialogue != null)
|
||||
{
|
||||
// Load this NPC's dialogue into the central controller
|
||||
centralDialogueController.ChangeDialogue(dialogue);
|
||||
|
||||
// Play dialogue with optional entry point
|
||||
if (!string.IsNullOrEmpty(dialogueEntryPoint))
|
||||
{
|
||||
centralDialogueController.PlayDialogue(dialogueEntryPoint);
|
||||
Log($"{npcName}: Starting EasyTalk dialogue at entry point '{dialogueEntryPoint}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
centralDialogueController.PlayDialogue();
|
||||
Log($"{npcName}: Starting EasyTalk dialogue");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to simple greeting if no EasyTalk setup
|
||||
if (centralDialogueController == null)
|
||||
{
|
||||
Debug.LogWarning($"No central DialogueController found. Make sure there's a DialogueController on a GameObject named '{dialogueManagerName}'.", gameObject);
|
||||
}
|
||||
if (dialogue == null)
|
||||
{
|
||||
Debug.LogWarning($"No Dialogue asset assigned to {npcName}. Assign a Dialogue asset in the inspector.", gameObject);
|
||||
}
|
||||
|
||||
Log($"{npcName}: {greetingText}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the central DialogueController in the scene
|
||||
/// </summary>
|
||||
private DialogueController FindCentralDialogueController()
|
||||
{
|
||||
// First, try to find by the specified manager name
|
||||
if (!string.IsNullOrEmpty(dialogueManagerName))
|
||||
{
|
||||
GameObject manager = GameObject.Find(dialogueManagerName);
|
||||
if (manager != null)
|
||||
{
|
||||
DialogueController controller = manager.GetComponent<DialogueController>();
|
||||
if (controller != null)
|
||||
{
|
||||
Log($"Found central DialogueController on '{dialogueManagerName}'");
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: search for any DialogueController in the scene
|
||||
DialogueController foundController = FindAnyObjectByType<DialogueController>();
|
||||
if (foundController != null)
|
||||
{
|
||||
Log($"Found DialogueController via FindAnyObjectByType on '{foundController.gameObject.name}'");
|
||||
return foundController;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offer quest to the player
|
||||
@@ -277,12 +194,4 @@ public class NPC : Character
|
||||
// NPCs might drop loot, give quest credit, etc.
|
||||
base.Die();
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
if (enableDebugLogs)
|
||||
{
|
||||
Debug.Log(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using UnityEngine.AI;
|
||||
public class NavMeshMovementController : MonoBehaviour
|
||||
{
|
||||
[Header("Movement")]
|
||||
public float stopDistanceTolerance = 0.1f;
|
||||
public float stopDistanceTolerance = 0.5f;
|
||||
|
||||
[Header("Rotation")]
|
||||
public bool enableSmartRotation = true;
|
||||
@@ -53,9 +53,13 @@ public class NavMeshMovementController : MonoBehaviour
|
||||
}
|
||||
|
||||
// Auto-stop when reaching destination
|
||||
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance + stopDistanceTolerance)
|
||||
if (!agent.pathPending && agent.hasPath)
|
||||
{
|
||||
agent.isStopped = true;
|
||||
if (agent.remainingDistance <= agent.stoppingDistance + stopDistanceTolerance)
|
||||
{
|
||||
agent.isStopped = true;
|
||||
agent.velocity = Vector3.zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ public class Player : Character
|
||||
{
|
||||
public static Player current { get; private set; }
|
||||
|
||||
void Awake()
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake(); // IMPORTANT: Initialize animator and parameters
|
||||
|
||||
if (current != null && current != this)
|
||||
{
|
||||
Debug.LogWarning("Multiple Player instances detected. Destroying duplicate.", gameObject);
|
||||
|
||||
131
Assets/Scripts/PlayerCombatController.cs
Normal file
131
Assets/Scripts/PlayerCombatController.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Basic player combat controller: right-click an enemy to chase and attack in range.
|
||||
/// </summary>
|
||||
public class PlayerCombatController : MonoBehaviour
|
||||
{
|
||||
[Header("Combat")]
|
||||
public int attackDamage = 1;
|
||||
public float attackRange = 1.5f;
|
||||
public float attackCooldown = 0.6f;
|
||||
[Tooltip("Maximum chase distance. Clear target if enemy gets farther than this.")]
|
||||
public float maxChaseDistance = 20f;
|
||||
|
||||
[Header("Movement")]
|
||||
public bool useMovementController = true;
|
||||
|
||||
[Header("Debug")]
|
||||
public bool enableDebugLogs = false;
|
||||
|
||||
private Enemy targetEnemy;
|
||||
private float nextAttackTime = 0f;
|
||||
private NavMeshMovementController movementController;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
movementController = GetComponent<NavMeshMovementController>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (targetEnemy == null) return;
|
||||
|
||||
if (targetEnemy.GetCurrentHP() <= 0)
|
||||
{
|
||||
Log("Target enemy is dead, clearing combat target");
|
||||
ClearCombatTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
float distance = Vector3.Distance(transform.position, targetEnemy.transform.position);
|
||||
|
||||
// Auto-clear if enemy goes out of chase range
|
||||
if (distance > maxChaseDistance)
|
||||
{
|
||||
Log($"Enemy out of chase range (distance: {distance:F2}, max: {maxChaseDistance:F2}), clearing target");
|
||||
ClearCombatTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
if (useMovementController && movementController != null)
|
||||
{
|
||||
if (distance > attackRange)
|
||||
{
|
||||
Log($"Moving to enemy (distance: {distance:F2} > attackRange: {attackRange})");
|
||||
movementController.MoveTo(targetEnemy.transform.position);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log($"In attack range, stopping (distance: {distance:F2})");
|
||||
movementController.Stop();
|
||||
movementController.SetLookTarget(targetEnemy.transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
if (distance <= attackRange && Time.time >= nextAttackTime)
|
||||
{
|
||||
Log($"ATTACKING! Distance: {distance:F2}, Cooldown ready: {Time.time >= nextAttackTime}");
|
||||
nextAttackTime = Time.time + attackCooldown;
|
||||
|
||||
Character character = GetComponent<Character>();
|
||||
if (character != null)
|
||||
{
|
||||
character.TriggerAttack();
|
||||
}
|
||||
|
||||
targetEnemy.TakeDamage(attackDamage);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTargetEnemy(Enemy enemy)
|
||||
{
|
||||
if (enemy == null) return;
|
||||
|
||||
Log($"Setting target: {enemy.enemyName}");
|
||||
targetEnemy = enemy;
|
||||
nextAttackTime = Time.time;
|
||||
|
||||
// Set weapon type to Melee for combat
|
||||
Character character = GetComponent<Character>();
|
||||
if (character != null)
|
||||
{
|
||||
character.SetWeaponType(Character.WeaponType.Melee);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearCombatTarget()
|
||||
{
|
||||
if (targetEnemy != null)
|
||||
Log($"Clearing target: {targetEnemy.enemyName}");
|
||||
targetEnemy = null;
|
||||
|
||||
// Return to idle weapon state
|
||||
Character character = GetComponent<Character>();
|
||||
if (character != null)
|
||||
{
|
||||
character.SetWeaponType(Character.WeaponType.None);
|
||||
}
|
||||
}
|
||||
|
||||
public Enemy GetTargetEnemy() => targetEnemy;
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
if (enableDebugLogs)
|
||||
{
|
||||
Debug.Log($"[PlayerCombat] {message}", gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
// Draw attack range
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, attackRange);
|
||||
|
||||
// Draw max chase distance
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(transform.position, maxChaseDistance);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/PlayerCombatController.cs.meta
Normal file
2
Assets/Scripts/PlayerCombatController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82ad2efd495c74c448c1a7ccf06710dd
|
||||
@@ -31,6 +31,8 @@ public class QuestTrigger : MonoBehaviour
|
||||
{
|
||||
OnTriggerEnter,
|
||||
OnCollisionEnter,
|
||||
OnItemPickup,
|
||||
OnDeath,
|
||||
Manual
|
||||
}
|
||||
|
||||
@@ -50,12 +52,34 @@ public class QuestTrigger : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call when an item is picked up (from item pickup scripts)
|
||||
/// </summary>
|
||||
public void OnItemPickup(GameObject picker)
|
||||
{
|
||||
if (triggerType == TriggerType.OnItemPickup)
|
||||
{
|
||||
TryTrigger(picker);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call when a target dies (from enemy death scripts)
|
||||
/// </summary>
|
||||
public void OnDeath(GameObject deadObject)
|
||||
{
|
||||
if (triggerType == TriggerType.OnDeath)
|
||||
{
|
||||
TryTrigger(deadObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually trigger quest progress (call from other scripts)
|
||||
/// </summary>
|
||||
public void ManualTrigger(GameObject triggerer = null)
|
||||
{
|
||||
if (triggerType == TriggerType.Manual || triggerType == TriggerType.OnTriggerEnter)
|
||||
if (triggerType == TriggerType.Manual)
|
||||
{
|
||||
TryTrigger(triggerer);
|
||||
}
|
||||
|
||||
107
Assets/Scripts/TriggerEvents.cs
Normal file
107
Assets/Scripts/TriggerEvents.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class TriggerEvents : MonoBehaviour
|
||||
{
|
||||
[Header("Trigger Conditions")]
|
||||
[Tooltip("Only trigger once for all events")]
|
||||
public bool triggerOnce = true;
|
||||
[Tooltip("Destroy this GameObject after triggering")]
|
||||
public bool destroyAfterTrigger = false;
|
||||
[Tooltip("Stop player movement when triggered")]
|
||||
public bool stopPlayerMovement = true;
|
||||
[Tooltip("Tag required to trigger (leave empty to ignore)")]
|
||||
public string requiredTag = "Player";
|
||||
[Tooltip("Layer mask required to trigger")]
|
||||
public LayerMask requiredLayers = ~0;
|
||||
|
||||
[Header("Events")]
|
||||
public UnityEvent onEnter;
|
||||
public UnityEvent onExit;
|
||||
public UnityEvent onStay;
|
||||
|
||||
private bool hasTriggered = false;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
Collider collider = GetComponent<Collider>();
|
||||
if (collider != null)
|
||||
{
|
||||
collider.isTrigger = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (!CanTrigger(other)) return;
|
||||
|
||||
hasTriggered = true;
|
||||
|
||||
// Stop player movement if enabled
|
||||
if (stopPlayerMovement)
|
||||
{
|
||||
StopMovement(other.gameObject);
|
||||
}
|
||||
|
||||
onEnter?.Invoke();
|
||||
|
||||
if (destroyAfterTrigger)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerExit(Collider other)
|
||||
{
|
||||
if (!CanTrigger(other)) return;
|
||||
onExit?.Invoke();
|
||||
}
|
||||
|
||||
private void OnTriggerStay(Collider other)
|
||||
{
|
||||
if (!CanTrigger(other)) return;
|
||||
onStay?.Invoke();
|
||||
}
|
||||
|
||||
private bool CanTrigger(Collider other)
|
||||
{
|
||||
if (other == null) return false;
|
||||
if (triggerOnce && hasTriggered) return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(requiredTag) && !other.CompareTag(requiredTag))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int otherLayerMask = 1 << other.gameObject.layer;
|
||||
if ((requiredLayers.value & otherLayerMask) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void StopMovement(GameObject target)
|
||||
{
|
||||
if (target == null) return;
|
||||
|
||||
// Try NavMeshMovementController first
|
||||
NavMeshMovementController movementController = target.GetComponent<NavMeshMovementController>();
|
||||
if (movementController != null)
|
||||
{
|
||||
movementController.Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to NavMeshAgent
|
||||
UnityEngine.AI.NavMeshAgent agent = target.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||||
if (agent != null)
|
||||
{
|
||||
agent.isStopped = true;
|
||||
agent.ResetPath();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/TriggerEvents.cs.meta
Normal file
2
Assets/Scripts/TriggerEvents.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de1e15f9e006b534fb3a5261c25bab34
|
||||
Reference in New Issue
Block a user