Working on quests, moved to CineMachine for camera and started to build out areas, started combat system

This commit is contained in:
2026-02-12 17:30:04 +00:00
parent c08d7d8830
commit d564c5f44e
50 changed files with 21700 additions and 10726 deletions

View File

@@ -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)
{

View File

@@ -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
View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7dbaebd502c3bf048a421cc649730492

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: af098d5ef0d53294e888e6362c26dbae
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 301957f69b431174f9fb965be9b869db

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);

View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 82ad2efd495c74c448c1a7ccf06710dd

View File

@@ -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);
}

View 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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: de1e15f9e006b534fb3a5261c25bab34