593 lines
17 KiB
C#
593 lines
17 KiB
C#
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("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;
|
|
|
|
[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;
|
|
|
|
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<NavMeshAgent>();
|
|
}
|
|
|
|
ApplyStaticAnimatorParams();
|
|
}
|
|
|
|
protected virtual void OnValidate()
|
|
{
|
|
if (animator == null)
|
|
{
|
|
animator = FindAnimatorWithParams();
|
|
}
|
|
|
|
if (navMeshAgent == null)
|
|
{
|
|
navMeshAgent = GetComponent<NavMeshAgent>();
|
|
}
|
|
}
|
|
|
|
protected virtual void Update()
|
|
{
|
|
UpdateMovementAnimatorParams();
|
|
}
|
|
|
|
public void TakeDamage(int damageToTake)
|
|
{
|
|
if (damageToTake < 0) damageToTake = 0;
|
|
|
|
CurHp -= damageToTake;
|
|
onTakeDamage?.Invoke();
|
|
|
|
if (CurHp <= 0) Die();
|
|
}
|
|
|
|
public virtual void Die()
|
|
{
|
|
SetDeathAnimation();
|
|
Destroy(gameObject);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// <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;
|
|
|
|
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)
|
|
{
|
|
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<Animator>(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
|
|
}
|
|
}
|