using UnityEngine; using UnityEngine.Events; using UnityEngine.AI; using EasyTalk.Controller; using EasyTalk.Nodes; public abstract class Character : MonoBehaviour { public enum WeaponType { None = 0, Pistol = 1, AssultRifle01 = 2, AssultRifle02 = 3, Shotgun = 4, SniperRifle = 5, Rifle = 6, SubMachineGun = 7, RPG = 8, MiniGun = 9, Grenades = 10, Bow = 11, Melee = 12 } public enum MeleeType { Stab = 0, OneHanded = 1, TwoHanded = 2 } [Header("Stats")] [SerializeField] protected int CurHp = 10; [SerializeField] protected int MaxHp = 10; [Header("Experience & Leveling")] [SerializeField] protected int currentLevel = 1; [SerializeField] protected int currentExperience = 0; [SerializeField] [Tooltip("Experience points required to reach the next level. Increases per level.")] protected int experiencePerLevel = 100; [SerializeField] [Tooltip("Experience points awarded to the player when this character is defeated.")] protected int experienceReward = 50; [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; [SerializeField] private Animator animator; [SerializeField] private NavMeshAgent navMeshAgent; [SerializeField] [Tooltip("Max distance used to snap this character to the nearest NavMesh point at startup.")] private float navMeshSnapDistance = 2f; [Header("Animation Settings")] [SerializeField] private WeaponType weaponType = WeaponType.None; [SerializeField] private MeleeType meleeType = MeleeType.Stab; [SerializeField] private WeaponType attackWeaponType = WeaponType.Melee; [SerializeField] private MeleeType attackMeleeType = MeleeType.OneHanded; [SerializeField] private float attackHoldSeconds = 0.15f; [SerializeField] private bool restoreWeaponAfterAttack = true; [SerializeField] [Tooltip("Idle animation index (Animation_int). 0 = normal idle.")] private int idleAnimationIndex = 0; [SerializeField] private bool isGrounded = true; [SerializeField] private bool useRootMotion = true; [SerializeField] private float walkSpeedThreshold = 0.1f; [SerializeField] private float runSpeedThreshold = 2.5f; [Header("Animator Parameter Names")] [SerializeField] private string paramWeaponType = "WeaponType_int"; [SerializeField] private string paramMeleeType = "MeleeType_Int"; [SerializeField] private string paramIdleAnimation = "Animation_int"; [SerializeField] private string paramGrounded = "Grounded"; [SerializeField] private string paramRootMotion = "Static_b"; [SerializeField] private string paramShoot = "Shoot_b"; [SerializeField] private string paramHeadHorizontal = "Head_Horizontal_f"; [SerializeField] private string paramHeadVertical = "Head_Vertical_f"; [SerializeField] private string paramBodyHorizontal = "Body_Horizontal_f"; [SerializeField] private string paramBodyVertical = "Body_Vertical_f"; [SerializeField] private string paramDeath = "Death_b"; [SerializeField] private string paramSpeed = "Speed_f"; protected Character target; public event UnityAction onTakeDamage; private WeaponType lastWeaponType; private MeleeType lastMeleeType; private Coroutine attackRoutine; public int GetCurrentHP() => CurHp; public int GetMaxHP() => MaxHp; public int GetCurrentLevel() => currentLevel; public int GetCurrentExperience() => currentExperience; public int GetExperienceForNextLevel() => experiencePerLevel; public int GetExperienceReward() => experienceReward; protected virtual void Awake() { if (animator == null) { animator = FindAnimatorWithParams(); } if (animator == null) { Debug.LogWarning("Character: No Animator found in children. Assign one in the inspector.", gameObject); } if (navMeshAgent == null) { navMeshAgent = GetComponent(); } ApplyStaticAnimatorParams(); } protected virtual void OnValidate() { if (animator == null) { animator = FindAnimatorWithParams(); } if (navMeshAgent == null) { navMeshAgent = GetComponent(); } } protected virtual void Start() { EnsureNavMeshAgentReady(); } protected virtual void Update() { UpdateMovementAnimatorParams(); } protected bool EnsureNavMeshAgentReady() { if (navMeshAgent == null || !navMeshAgent.isActiveAndEnabled) { return false; } if (navMeshAgent.isOnNavMesh) { return true; } if (NavMesh.SamplePosition(transform.position, out NavMeshHit hit, navMeshSnapDistance, NavMesh.AllAreas)) { return navMeshAgent.Warp(hit.position) && navMeshAgent.isOnNavMesh; } return false; } public void TakeDamage(int damageToTake) { if (damageToTake < 0) damageToTake = 0; CurHp -= damageToTake; onTakeDamage?.Invoke(); if (CurHp <= 0) Die(); } public virtual void Die() { SetDeathAnimation(); // Reward experience to player if this character has an experience reward if (experienceReward > 0 && Player.current != null) { Player.current.AddExperience(experienceReward); Log($"Player gained {experienceReward} experience from defeating {gameObject.name}"); } Destroy(gameObject); } public void AddExperience(int amount) { if (amount <= 0) return; currentExperience += amount; Log($"Gained {amount} experience. Total: {currentExperience}/{experiencePerLevel}"); // Check if level up while (currentExperience >= experiencePerLevel) { LevelUp(); } } protected virtual void LevelUp() { currentExperience -= experiencePerLevel; currentLevel++; // Increase experience requirement for next level (e.g., 10% increase or fixed increment) experiencePerLevel += 50; // Increase by 50 exp per level Log($"LEVEL UP! Now level {currentLevel}. Next level requires {experiencePerLevel} exp."); } /// /// Grant experience to the player from a quest or other source. /// public static void AwardQuestExperience(int amount) { if (Player.current != null) { Player.current.AddExperience(amount); Debug.Log($"Player gained {amount} experience from quest completion!"); } else { Debug.LogWarning("Cannot award experience: Player.current is null"); } } public void SetTarget(Character t) { target = t; } public Character GetTarget() => target; public void SetWeaponType(WeaponType type) { weaponType = type; ApplyStaticAnimatorParams(); } public void SetMeleeType(MeleeType type) { meleeType = type; ApplyStaticAnimatorParams(); } public void SetIdleAnimation(int idleIndex) { idleAnimationIndex = Mathf.Max(0, idleIndex); ApplyStaticAnimatorParams(); } public void SetGrounded(bool grounded) { isGrounded = grounded; if (animator != null) { SetBoolIfExists(paramGrounded, isGrounded); } } public void SetUseRootMotion(bool enableRootMotion) { useRootMotion = enableRootMotion; if (animator != null) { SetBoolIfExists(paramRootMotion, useRootMotion); } } public void TriggerAttack() { if (animator == null) return; if (attackRoutine != null) { StopCoroutine(attackRoutine); } attackRoutine = StartCoroutine(AttackRoutine()); } public void SetHeadLook(float horizontal, float vertical) { if (animator == null) return; SetFloatIfExists(paramHeadHorizontal, horizontal); SetFloatIfExists(paramHeadVertical, vertical); } /// /// Make character look at a target with head rotation, body rotation when clamped /// 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); } } /// /// Called when player interacts with this character. Override to add custom behavior. /// 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(); if (playerChar != null) { playerChar.LookAtTarget(transform); } } if (hasDialogue) { ShowDialogue(); } } /// /// Display dialogue for this character using EasyTalk with centralized DialogueController /// /// Optional entry point override. If null/empty, uses dialogueEntryPoint field. 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); } } } /// /// Public wrapper for UnityEvents or other triggers /// /// Optional entry point name. If empty, uses default dialogueEntryPoint. 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); } /// /// Finds the central DialogueController in the scene /// 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(); if (controller != null) { Log($"Found central DialogueController on '{dialogueManagerName}'"); return controller; } } } // Fallback: search for any DialogueController in the scene DialogueController foundController = FindAnyObjectByType(); if (foundController != null) { Log($"Found DialogueController via FindAnyObjectByType on '{foundController.gameObject.name}'"); return foundController; } return null; } /// /// Debug logging utility /// protected void Log(string message) { if (enableDebugLogs) { Debug.Log($"[{GetType().Name}] {message}", gameObject); } } private void ApplyStaticAnimatorParams() { if (animator == null) return; SetIntIfExists(paramWeaponType, (int)weaponType); SetIntIfExists(paramMeleeType, (int)meleeType); SetIntIfExists(paramIdleAnimation, idleAnimationIndex); SetBoolIfExists(paramGrounded, isGrounded); SetBoolIfExists(paramRootMotion, useRootMotion); } private void UpdateMovementAnimatorParams() { if (animator == null) return; float speed = 0f; if (navMeshAgent != null && navMeshAgent.isActiveAndEnabled && EnsureNavMeshAgentReady()) { speed = navMeshAgent.velocity.magnitude; if (speed < 0.01f && navMeshAgent.hasPath && !navMeshAgent.pathPending) { // Use desired velocity when actual velocity is near zero speed = navMeshAgent.desiredVelocity.magnitude; } } else if (MovementController != null) { speed = MovementController.GetSpeed(); } MovementState state = MovementState.Idle; if (speed >= runSpeedThreshold) { state = MovementState.Run; } else if (speed >= walkSpeedThreshold) { state = MovementState.Walk; } Vector2 bodyParams = GetBodyParamsForState(weaponType, state); SetFloatIfExists(paramBodyHorizontal, bodyParams.x); SetFloatIfExists(paramBodyVertical, bodyParams.y); SetFloatIfExists(paramSpeed, speed); } private Vector2 GetBodyParamsForState(WeaponType type, MovementState state) { // Defaults per Simple Fantasy ReadMe if (type == WeaponType.None) { return Vector2.zero; } if (type == WeaponType.Pistol) { if (state == MovementState.Run) return new Vector2(0f, 0.2f); return Vector2.zero; } if (type == WeaponType.Grenades) { return Vector2.zero; } // All other weapons if (state == MovementState.Run) return new Vector2(0.3f, 0.6f); return new Vector2(0f, 0.6f); } private void SetDeathAnimation() { if (animator == null) return; SetBoolIfExists(paramDeath, true); } private System.Collections.IEnumerator ResetBoolNextFrame(string boolName) { SetBoolIfExists(boolName, true); yield return null; SetBoolIfExists(boolName, false); } private System.Collections.IEnumerator AttackRoutine() { lastWeaponType = weaponType; lastMeleeType = meleeType; weaponType = attackWeaponType; meleeType = attackMeleeType; ApplyStaticAnimatorParams(); SetBoolIfExists(paramShoot, true); if (attackHoldSeconds > 0f) { 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) { weaponType = lastWeaponType; meleeType = lastMeleeType; ApplyStaticAnimatorParams(); } attackRoutine = null; } private bool HasParam(string paramName, AnimatorControllerParameterType type) { if (animator == null || string.IsNullOrEmpty(paramName)) return false; foreach (var param in animator.parameters) { if (param.name == paramName && param.type == type) return true; } return false; } private bool HasParam(Animator targetAnimator, string paramName, AnimatorControllerParameterType type) { if (targetAnimator == null || string.IsNullOrEmpty(paramName)) return false; foreach (var param in targetAnimator.parameters) { if (param.name == paramName && param.type == type) return true; } return false; } private Animator FindAnimatorWithParams() { var animators = GetComponentsInChildren(true); if (animators == null || animators.Length == 0) return null; foreach (var anim in animators) { if (HasParam(anim, paramSpeed, AnimatorControllerParameterType.Float) || HasParam(anim, paramWeaponType, AnimatorControllerParameterType.Int)) { return anim; } } return animators[0]; } private void SetIntIfExists(string paramName, int value) { if (HasParam(paramName, AnimatorControllerParameterType.Int)) animator.SetInteger(paramName, value); } private void SetFloatIfExists(string paramName, float value) { if (HasParam(paramName, AnimatorControllerParameterType.Float)) animator.SetFloat(paramName, value); } private void SetBoolIfExists(string paramName, bool value) { if (HasParam(paramName, AnimatorControllerParameterType.Bool)) animator.SetBool(paramName, value); } private enum MovementState { Idle, Walk, Run } }