Files
Click-PointRPG/Assets/Scripts/Character.cs

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