continuing with combat, sword combos working, not finished, blockign working, need to build blockpush and improving bow combat
This commit is contained in:
61
Assets/Scripts/AnimationEventRelay.cs
Normal file
61
Assets/Scripts/AnimationEventRelay.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Attach this script directly to the child GameObject that has the Animator component (e.g., "Model").
|
||||
/// It will catch Animation Events triggered by the animations and forward them up to the parent GameObject
|
||||
/// where your actual combat scripts (like MeleeCombatLayerController) are sitting!
|
||||
/// </summary>
|
||||
public class AnimationEventRelay : MonoBehaviour
|
||||
{
|
||||
// ==========================================
|
||||
// MELEE EVENTS
|
||||
// ==========================================
|
||||
|
||||
public void DealDamage()
|
||||
{
|
||||
if (transform.parent != null)
|
||||
{
|
||||
transform.parent.SendMessageUpwards("DealDamage", SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
public void Hit()
|
||||
{
|
||||
if (transform.parent != null)
|
||||
{
|
||||
transform.parent.SendMessageUpwards("Hit", SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
public void loadArrow()
|
||||
{
|
||||
if (transform.parent != null)
|
||||
{
|
||||
transform.parent.SendMessageUpwards("loadArrow", SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
public void Fire()
|
||||
{
|
||||
if (transform.parent != null)
|
||||
{
|
||||
transform.parent.SendMessageUpwards("Fire", SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
public void FootR()
|
||||
{
|
||||
if (transform.parent != null)
|
||||
{
|
||||
transform.parent.SendMessageUpwards("FootR", SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
public void FootL()
|
||||
{
|
||||
if (transform.parent != null)
|
||||
{
|
||||
transform.parent.SendMessageUpwards("FootL", SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/AnimationEventRelay.cs.meta
Normal file
2
Assets/Scripts/AnimationEventRelay.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32d17db2c71c8af529d64ca75cc67182
|
||||
@@ -89,6 +89,7 @@ public class BowAimLayerController : MonoBehaviour
|
||||
|
||||
// Are we currently holding the aim button (right click)?
|
||||
private bool _isAiming;
|
||||
public bool IsAiming => _isAiming;
|
||||
|
||||
// Are we currently holding the draw button (left click)?
|
||||
private bool _isDrawing;
|
||||
@@ -277,6 +278,16 @@ public class BowAimLayerController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public float CurrentDrawPercentage
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_isDrawing) return 0f;
|
||||
float drawDuration = Time.time - _drawStartTime;
|
||||
return Mathf.Clamp01(drawDuration / _maxDrawTime);
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelDraw()
|
||||
{
|
||||
if (_isDrawing)
|
||||
|
||||
57
Assets/Scripts/LevelLoader.cs
Normal file
57
Assets/Scripts/LevelLoader.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using System.Collections;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// A global helper to seamlessly load levels using a dedicated Loading Scene.
|
||||
/// </summary>
|
||||
public class LevelLoader : MonoBehaviour
|
||||
{
|
||||
// The static variable that remembers where we want to go!
|
||||
public static string targetSceneName = "";
|
||||
|
||||
[Header("Loading UI")]
|
||||
[Tooltip("The UI Slider used as a visual loading progress bar.")]
|
||||
public Slider loadingProgressBar;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// If this script wakes up and there's a target scene defined, it means
|
||||
// we are currently sitting in the LoadingScene and should start the heavy lifting!
|
||||
if (!string.IsNullOrEmpty(targetSceneName))
|
||||
{
|
||||
StartCoroutine(LoadSceneAsynchronously(targetSceneName));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this from anywhere in your game to trigger the loading screen and go to a new level!
|
||||
/// Example: LevelLoader.LoadLevel("HouseInterior");
|
||||
/// </summary>
|
||||
public static void LoadLevel(string sceneName)
|
||||
{
|
||||
targetSceneName = sceneName;
|
||||
// Asynchronously load the Loading Scene to prevent the Main Menu from freezing!
|
||||
SceneManager.LoadSceneAsync("LoadingScene");
|
||||
}
|
||||
|
||||
private IEnumerator LoadSceneAsynchronously(string sceneName)
|
||||
{
|
||||
// Start loading the heavy scene in the background
|
||||
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName);
|
||||
|
||||
// Update the loading bar while the scene is loading
|
||||
while (!operation.isDone)
|
||||
{
|
||||
float progress = Mathf.Clamp01(operation.progress / 0.9f);
|
||||
|
||||
if (loadingProgressBar != null)
|
||||
{
|
||||
loadingProgressBar.value = progress;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/LevelLoader.cs.meta
Normal file
2
Assets/Scripts/LevelLoader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb3f5ab1352b63859b783088a5b19318
|
||||
39
Assets/Scripts/MainMenuManager.cs
Normal file
39
Assets/Scripts/MainMenuManager.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class MainMenuManager : MonoBehaviour
|
||||
{
|
||||
[Header("Scene Settings")]
|
||||
[Tooltip("The exact name of the scene you want to load when starting a new game (e.g. 'Level1' or 'WorldMap').")]
|
||||
public string newGameSceneName = "Level1";
|
||||
|
||||
/// <summary>
|
||||
/// Call this method from the 'On Click ()' event of your Start Game Button.
|
||||
/// </summary>
|
||||
public void StartNewGame()
|
||||
{
|
||||
Debug.Log($"[MainMenuManager] Handing off to LevelLoader for: {newGameSceneName}...");
|
||||
|
||||
// Pass the request to our global LevelLoader which will handle the loading screen!
|
||||
LevelLoader.LoadLevel(newGameSceneName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this method from the 'On Click ()' event of your Quit or Exit Button.
|
||||
/// </summary>
|
||||
public void ExitToDesktop()
|
||||
{
|
||||
Debug.Log("[MainMenuManager] Exiting to Desktop...");
|
||||
|
||||
// This closes the actual built game executable (.exe)
|
||||
Application.Quit();
|
||||
|
||||
// If you are testing this inside the Unity Editor, Application.Quit() does nothing.
|
||||
// This preprocessor directive forcefully stops Play Mode so you can still test the quit button!
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.isPlaying = false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainMenuManager.cs.meta
Normal file
2
Assets/Scripts/MainMenuManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4944dbaf0ec1f1d3793a5dbe9aafa239
|
||||
@@ -21,6 +21,16 @@ public class MeleeCombatLayerController : MonoBehaviour
|
||||
[Tooltip("The active weapon used for damage and stats.")]
|
||||
[SerializeField] private Weapon _currentWeapon;
|
||||
|
||||
[Header("Combo System")]
|
||||
[Tooltip("The UI Image (like a Chain Icon) that appears when the player is allowed to combo.")]
|
||||
[SerializeField] private UnityEngine.UI.Image _comboIndicatorUI;
|
||||
[SerializeField] private Sprite _comboStandbySprite;
|
||||
[SerializeField] private Sprite _comboMissedSprite;
|
||||
|
||||
[SerializeField] private Color _comboStandbyColor = Color.white;
|
||||
[SerializeField] private Color _comboSuccessColor = Color.green;
|
||||
[SerializeField] private Color _comboMissedColor = Color.red;
|
||||
|
||||
[Tooltip("The Transform socket where the weapon is held when equipped (e.g., Prop_R)")]
|
||||
[SerializeField] private Transform _equippedSocket;
|
||||
|
||||
@@ -103,6 +113,12 @@ public class MeleeCombatLayerController : MonoBehaviour
|
||||
}
|
||||
|
||||
private bool _isAttacking;
|
||||
public bool IsAttacking => _isAttacking;
|
||||
|
||||
// --- Debug Tracking ---
|
||||
private float _lastAttackInputTime = 0f;
|
||||
private float _lastAttackAnimationEndTime = 0f;
|
||||
private bool _wasAttackingLastFrame = false;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
@@ -127,6 +143,21 @@ public class MeleeCombatLayerController : MonoBehaviour
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Track exactly when the attack animation finishes
|
||||
if (!_isAttacking && _wasAttackingLastFrame)
|
||||
{
|
||||
_lastAttackAnimationEndTime = Time.time;
|
||||
_comboSuccessfullyBuffered = false;
|
||||
_isComboWindowOpen = false;
|
||||
|
||||
// Forcefully hide the UI just in case an animation was interrupted before its Close event fired!
|
||||
if (_comboIndicatorUI != null)
|
||||
{
|
||||
_comboIndicatorUI.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
_wasAttackingLastFrame = _isAttacking;
|
||||
|
||||
if (_meleeLayerIndex != -1)
|
||||
{
|
||||
@@ -245,10 +276,39 @@ public class MeleeCombatLayerController : MonoBehaviour
|
||||
|
||||
#region Input Handlers
|
||||
|
||||
private float _comboPenaltyEndTime = 0f;
|
||||
|
||||
private void OnAttackStarted(InputAction.CallbackContext context)
|
||||
{
|
||||
if (!_isEquipped || _isBlocking) return;
|
||||
|
||||
// 1. Penalty Check (Are we locked out for missing a combo?)
|
||||
if (Time.time < _comboPenaltyEndTime)
|
||||
{
|
||||
Debug.Log("[Combat Debug] Attack locked out! You must recover from your missed combo.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Stamina Check (Requires at least 15 stamina to swing the sword)
|
||||
if (_playerStats != null && _playerStats.currentStamina < 15f)
|
||||
{
|
||||
Debug.Log("[Combat Debug] Not enough stamina to attack!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Input Debounce (0.4s)
|
||||
// This is long enough to stop jitter, but short enough that if you click
|
||||
// in the middle of your current attack, it successfully buffers the next Combo hit!
|
||||
if (Time.time < _lastAttackInputTime + 0.4f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Debug Log ---
|
||||
Debug.Log($"[Combat Debug] Attack PRESSED at {Time.time:F2}s. (Last pressed: {_lastAttackInputTime:F2}s). If not playing, it last finished at: {_lastAttackAnimationEndTime:F2}s");
|
||||
_lastAttackInputTime = Time.time;
|
||||
// -----------------
|
||||
|
||||
_isPoweringUp = true;
|
||||
if (_animator != null)
|
||||
{
|
||||
@@ -260,6 +320,10 @@ public class MeleeCombatLayerController : MonoBehaviour
|
||||
{
|
||||
if (_isPoweringUp)
|
||||
{
|
||||
// --- Debug Log ---
|
||||
Debug.Log($"[Combat Debug] Attack RELEASED at {Time.time:F2}s. Triggering Attack()!");
|
||||
// -----------------
|
||||
|
||||
_isPoweringUp = false;
|
||||
|
||||
if (_animator != null)
|
||||
@@ -280,6 +344,8 @@ public class MeleeCombatLayerController : MonoBehaviour
|
||||
if (_playerStats != null)
|
||||
{
|
||||
_playerStats.staminaRegenMultiplier = 0.5f; // Half regen while blocking
|
||||
_playerStats.isBlocking = true;
|
||||
_playerStats.currentBlockReduction = _currentWeapon != null ? _currentWeapon.blockReductionPercentage : 0.5f;
|
||||
}
|
||||
|
||||
// Slow down to walk speed while blocking
|
||||
@@ -295,6 +361,7 @@ public class MeleeCombatLayerController : MonoBehaviour
|
||||
if (_playerStats != null)
|
||||
{
|
||||
_playerStats.staminaRegenMultiplier = 1f; // Restore regen rate
|
||||
_playerStats.isBlocking = false;
|
||||
}
|
||||
|
||||
// Return to normal speed
|
||||
@@ -306,19 +373,99 @@ public class MeleeCombatLayerController : MonoBehaviour
|
||||
|
||||
#region Combat Actions
|
||||
|
||||
private float _lastStaminaDrainTime = 0f;
|
||||
|
||||
private bool _isComboWindowOpen = false;
|
||||
private bool _comboSuccessfullyBuffered = false;
|
||||
|
||||
// --- Animation Events to add to your attack clips! ---
|
||||
public void OpenComboWindow()
|
||||
{
|
||||
_isComboWindowOpen = true;
|
||||
_comboSuccessfullyBuffered = false; // Reset the buffer for the new attack!
|
||||
|
||||
if (_comboIndicatorUI != null)
|
||||
{
|
||||
_comboIndicatorUI.sprite = _comboStandbySprite;
|
||||
_comboIndicatorUI.color = _comboStandbyColor;
|
||||
_comboIndicatorUI.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseComboWindow()
|
||||
{
|
||||
_isComboWindowOpen = false;
|
||||
if (_comboIndicatorUI != null)
|
||||
{
|
||||
// Only hide it if it hasn't been changed to the missed sprite (so missed icon has time to show)
|
||||
if (_comboIndicatorUI.sprite == _comboStandbySprite || _comboIndicatorUI.color == _comboSuccessColor)
|
||||
{
|
||||
_comboIndicatorUI.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// -----------------------------------------------------
|
||||
|
||||
public void Attack()
|
||||
{
|
||||
if (_animator != null)
|
||||
{
|
||||
// Optional: You can check if there's enough stamina to attack here
|
||||
// e.g. if (_playerStats != null && !_playerStats.HasStaminaPercentage(0.1f)) return;
|
||||
|
||||
if (_playerStats != null)
|
||||
if (_isAttacking)
|
||||
{
|
||||
_playerStats.UseStamina(15f); // Use 15 stamina per swing
|
||||
// If they already earned the combo, ignore their extra spam clicks!
|
||||
// Otherwise, the penalty block below will wipe their hard-earned trigger!
|
||||
if (_comboSuccessfullyBuffered) return;
|
||||
|
||||
// If they click during an attack, it MUST be inside the combo window!
|
||||
if (_isComboWindowOpen)
|
||||
{
|
||||
Debug.Log("[Combat Debug] PERFECT TIMING! Combo buffered.");
|
||||
_animator.SetTrigger(_attackHash);
|
||||
_comboSuccessfullyBuffered = true; // Lock in the success!
|
||||
|
||||
if (_comboIndicatorUI != null)
|
||||
{
|
||||
_comboIndicatorUI.color = _comboSuccessColor;
|
||||
}
|
||||
|
||||
// We set it to false so they can't trigger again, but we don't call CloseComboWindow
|
||||
// directly so the Success color stays visible for a split second!
|
||||
_isComboWindowOpen = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[Combat Debug] Combo Dropped! Clicked too early or too late.");
|
||||
|
||||
// PUNISHMENT: Lock out attacks for 1 full second and explicitly wipe the trigger
|
||||
// so the Animator is absolutely forbidden from storing up a delayed attack!
|
||||
_comboPenaltyEndTime = Time.time + 1.0f;
|
||||
_animator.ResetTrigger(_attackHash);
|
||||
|
||||
if (_comboIndicatorUI != null)
|
||||
{
|
||||
_comboIndicatorUI.sprite = _comboMissedSprite;
|
||||
_comboIndicatorUI.color = _comboMissedColor;
|
||||
_comboIndicatorUI.gameObject.SetActive(true);
|
||||
|
||||
StopCoroutine(HideMissedComboIcon());
|
||||
StartCoroutine(HideMissedComboIcon());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_animator.SetTrigger(_attackHash);
|
||||
else
|
||||
{
|
||||
// Not attacking, just start the first attack normally
|
||||
_animator.SetTrigger(_attackHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator HideMissedComboIcon()
|
||||
{
|
||||
yield return new WaitForSeconds(0.4f);
|
||||
if (_comboIndicatorUI != null && _comboIndicatorUI.sprite == _comboMissedSprite)
|
||||
{
|
||||
_comboIndicatorUI.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,6 +484,13 @@ public class MeleeCombatLayerController : MonoBehaviour
|
||||
public void DealDamage()
|
||||
{
|
||||
Debug.Log("DealDamage called!");
|
||||
|
||||
// Drain stamina EXACTLY when the sword swings, not when the button is clicked!
|
||||
if (_playerStats != null)
|
||||
{
|
||||
_playerStats.UseStamina(15f);
|
||||
}
|
||||
|
||||
if (_currentWeapon == null)
|
||||
{
|
||||
Debug.LogWarning("No weapon assigned to deal damage!");
|
||||
@@ -357,42 +511,52 @@ public class MeleeCombatLayerController : MonoBehaviour
|
||||
foreach (Collider hit in hitColliders)
|
||||
{
|
||||
Debug.Log($"Melee hit {hit.name} for {_currentWeapon.damage} damage!");
|
||||
hit.GetComponent<WorldObject>()?.TakeDamage(_currentWeapon.damage);
|
||||
hit.GetComponent<NPCController>()?.TakeDamage(_currentWeapon.damage);
|
||||
hit.GetComponent<PlayerStats>()?.TakeDamage(_currentWeapon.damage);
|
||||
hit.GetComponentInParent<WorldObject>()?.TakeDamage(_currentWeapon.damage);
|
||||
hit.GetComponentInParent<NPCController>()?.TakeDamage(_currentWeapon.damage);
|
||||
hit.GetComponentInParent<PlayerStats>()?.TakeDamage(_currentWeapon.damage);
|
||||
}
|
||||
}
|
||||
|
||||
// Add hashes for blocking animations
|
||||
private readonly int _blockHitHash = Animator.StringToHash("BlockHit");
|
||||
private readonly int _blockBreakHash = Animator.StringToHash("BlockBreak");
|
||||
private readonly int _blockPushHash = Animator.StringToHash("BlockPush");
|
||||
|
||||
public void TakeDamage(float amount)
|
||||
{
|
||||
if (_isBlocking)
|
||||
{
|
||||
float blockReduction = _currentWeapon != null ? _currentWeapon.blockReductionPercentage : 0.5f;
|
||||
float blockedAmount = amount * blockReduction;
|
||||
float damageTaken = amount - blockedAmount;
|
||||
|
||||
if (_playerStats != null)
|
||||
{
|
||||
if (_playerStats.currentStamina >= amount)
|
||||
// We consume stamina equal to the damage blocked
|
||||
if (_playerStats.currentStamina >= blockedAmount)
|
||||
{
|
||||
Debug.Log($"Blocked {amount} damage! Cost {amount} stamina.");
|
||||
_playerStats.UseStamina(amount);
|
||||
Debug.Log($"Blocked {blockedAmount} damage! Cost {blockedAmount} stamina. Took {damageTaken} damage.");
|
||||
_playerStats.UseStamina(blockedAmount);
|
||||
_playerStats.TakeDamage(damageTaken);
|
||||
|
||||
if (_animator != null) _animator.SetTrigger(_blockHitHash);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"Block broken! Took {amount} damage.");
|
||||
Debug.Log($"Block broken! Took full {amount} damage.");
|
||||
_isBlocking = false;
|
||||
_playerStats.staminaRegenMultiplier = 1f;
|
||||
_playerStats.UseStamina(_playerStats.currentStamina); // Drain rest of stamina
|
||||
_playerStats.PauseStaminaRegen(3f); // Pause regen for 3 seconds
|
||||
_inputReader.onWalkToggled?.Invoke(); // Stop walking slow
|
||||
|
||||
// Optionally, play a guard break animation here if you have one
|
||||
// _animator.SetTrigger("GuardBreak");
|
||||
_playerStats.TakeDamage(amount);
|
||||
|
||||
if (_animator != null) _animator.SetTrigger(_blockBreakHash);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"Blocked {amount} damage!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"Took {amount} damage!");
|
||||
|
||||
@@ -22,6 +22,12 @@ public class NPCController : MonoBehaviour
|
||||
[Tooltip("The socket on the character model where the weapon should spawn (e.g., Prop_R). Leave blank to auto-search.")]
|
||||
public Transform weaponSocket;
|
||||
|
||||
[Header("Ragdoll Settings")]
|
||||
[Tooltip("If true, the NPC will collapse into a ragdoll on death rather than playing an animation.")]
|
||||
public bool useRagdoll = true;
|
||||
|
||||
private Rigidbody[] _boneRigidbodies;
|
||||
|
||||
[Header("Patrol Testing")]
|
||||
[Tooltip("If true, the NPC will automatically walk between the patrol points below.")]
|
||||
public bool isPatrolling = false;
|
||||
@@ -42,6 +48,8 @@ public class NPCController : MonoBehaviour
|
||||
private float _lastAttackTime = 0f;
|
||||
private float _stunTimer = 0f;
|
||||
private bool _isDead = false;
|
||||
private bool _isBlocking = false;
|
||||
private float _lastBlockAttemptTime = 0f;
|
||||
private GameObject _equippedWeaponInstance;
|
||||
|
||||
// Animator Hashes specifically for Synty's AC_Sidekick and AC_Polygon controllers
|
||||
@@ -59,6 +67,7 @@ public class NPCController : MonoBehaviour
|
||||
private readonly int _attackHash = Animator.StringToHash("Attack");
|
||||
private readonly int _takeDamageHash = Animator.StringToHash("TakeDamage");
|
||||
private readonly int _deathHash = Animator.StringToHash("Death"); // Optional
|
||||
private readonly int _isBlockingHash = Animator.StringToHash("IsBlocking");
|
||||
|
||||
private void Start()
|
||||
{
|
||||
@@ -69,6 +78,10 @@ public class NPCController : MonoBehaviour
|
||||
_animator = GetComponentInChildren<Animator>();
|
||||
}
|
||||
|
||||
// Gather all rigidbodies that belong to the ragdoll
|
||||
_boneRigidbodies = GetComponentsInChildren<Rigidbody>();
|
||||
SetRagdollState(false); // Make them kinematic while alive
|
||||
|
||||
// Auto-assign player as combat target if empty (great for quick testing!)
|
||||
if (combatTarget == null)
|
||||
{
|
||||
@@ -135,6 +148,19 @@ public class NPCController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private void SetRagdollState(bool isActive)
|
||||
{
|
||||
if (_boneRigidbodies == null) return;
|
||||
|
||||
foreach (Rigidbody rb in _boneRigidbodies)
|
||||
{
|
||||
// We ignore the root GameObject's Rigidbody if it has one
|
||||
if (rb.gameObject == gameObject) continue;
|
||||
|
||||
rb.isKinematic = !isActive;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_isDead) return;
|
||||
@@ -177,11 +203,16 @@ public class NPCController : MonoBehaviour
|
||||
|
||||
float distToTarget = Vector3.Distance(transform.position, combatTarget.position);
|
||||
|
||||
PlayerStats playerStats = combatTarget.GetComponent<PlayerStats>();
|
||||
MeleeCombatLayerController playerCombat = combatTarget.GetComponent<MeleeCombatLayerController>();
|
||||
|
||||
bool isPlayerBlocking = playerStats != null && playerStats.isBlocking;
|
||||
bool isPlayerAttacking = playerCombat != null && playerCombat.IsAttacking;
|
||||
|
||||
if (distToTarget <= npcData.attackRadius)
|
||||
{
|
||||
// We are close enough to attack! Stop moving and face them.
|
||||
// Stop and face them
|
||||
_agent.isStopped = true;
|
||||
|
||||
Vector3 direction = (combatTarget.position - transform.position).normalized;
|
||||
direction.y = 0;
|
||||
if (direction != Vector3.zero)
|
||||
@@ -189,19 +220,51 @@ public class NPCController : MonoBehaviour
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 5f);
|
||||
}
|
||||
|
||||
// Attack if cooldown is ready
|
||||
if (Time.time >= _lastAttackTime + npcData.attackCooldown)
|
||||
// Blocking Logic: Try to block if the player is attacking
|
||||
if (isPlayerAttacking && !_isBlocking && Time.time >= _lastBlockAttemptTime + 1.5f)
|
||||
{
|
||||
PerformAttack();
|
||||
_lastBlockAttemptTime = Time.time;
|
||||
if (Random.value <= npcData.blockChance)
|
||||
{
|
||||
SetBlockState(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop blocking if the player stops attacking (or after a small delay)
|
||||
if (!isPlayerAttacking && _isBlocking && Time.time >= _lastBlockAttemptTime + 1.0f)
|
||||
{
|
||||
SetBlockState(false);
|
||||
}
|
||||
|
||||
// Attacking Logic: Only attack if we are NOT currently blocking
|
||||
if (!_isBlocking)
|
||||
{
|
||||
// If the player is turtling (blocking), attack twice as fast to try and break their guard!
|
||||
float currentCooldown = isPlayerBlocking ? npcData.attackCooldown * 0.5f : npcData.attackCooldown;
|
||||
|
||||
if (Time.time >= _lastAttackTime + currentCooldown)
|
||||
{
|
||||
PerformAttack();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Target is too far, chase them!
|
||||
// Target is too far, chase them! (And stop blocking if we were)
|
||||
if (_isBlocking) SetBlockState(false);
|
||||
RunTo(combatTarget.position);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBlockState(bool block)
|
||||
{
|
||||
_isBlocking = block;
|
||||
if (_animator != null)
|
||||
{
|
||||
_animator.SetBool(_isBlockingHash, block);
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformAttack()
|
||||
{
|
||||
_lastAttackTime = Time.time;
|
||||
@@ -211,6 +274,14 @@ public class NPCController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Some Synty animations use "DealDamage" instead of "Hit" as the event name.
|
||||
/// </summary>
|
||||
public void DealDamage()
|
||||
{
|
||||
Hit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this from the "Hit" Animation Event on the NPC's attack animation
|
||||
/// </summary>
|
||||
@@ -221,12 +292,19 @@ public class NPCController : MonoBehaviour
|
||||
// If the target is still within attack range when the animation hits, deal damage
|
||||
if (Vector3.Distance(transform.position, combatTarget.position) <= npcData.attackRadius + 0.5f)
|
||||
{
|
||||
PlayerStats playerStats = combatTarget.GetComponent<PlayerStats>();
|
||||
if (playerStats != null)
|
||||
float damageToDeal = npcData.weaponData != null ? npcData.weaponData.damage : npcData.baseDamage;
|
||||
Debug.Log($"NPC landed a hit on the Player for {damageToDeal} damage!");
|
||||
|
||||
// Try to hit the combat controller first so it can process blocks/stamina/animations
|
||||
MeleeCombatLayerController playerCombat = combatTarget.GetComponent<MeleeCombatLayerController>();
|
||||
if (playerCombat != null)
|
||||
{
|
||||
float damageToDeal = npcData.weaponData != null ? npcData.weaponData.damage : npcData.baseDamage;
|
||||
Debug.Log($"NPC landed a hit on the Player for {damageToDeal} damage!");
|
||||
playerStats.TakeDamage(damageToDeal);
|
||||
playerCombat.TakeDamage(damageToDeal);
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayerStats playerStats = combatTarget.GetComponent<PlayerStats>();
|
||||
if (playerStats != null) playerStats.TakeDamage(damageToDeal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,6 +313,16 @@ public class NPCController : MonoBehaviour
|
||||
{
|
||||
if (_isDead) return;
|
||||
|
||||
if (_isBlocking)
|
||||
{
|
||||
float blockedAmount = amount * npcData.blockReductionPercentage;
|
||||
amount -= blockedAmount;
|
||||
Debug.Log($"NPC Blocked {blockedAmount} damage! Taking only {amount} damage.");
|
||||
|
||||
// Optionally: Break guard if hit hard?
|
||||
// SetBlockState(false);
|
||||
}
|
||||
|
||||
currentHealth -= amount;
|
||||
Debug.Log($"NPC took {amount} damage! Health remaining: {currentHealth}");
|
||||
|
||||
@@ -267,25 +355,53 @@ public class NPCController : MonoBehaviour
|
||||
if (_agent != null && _agent.isOnNavMesh)
|
||||
{
|
||||
_agent.isStopped = true;
|
||||
_agent.enabled = false; // Turn off the agent completely so physics don't fight it
|
||||
}
|
||||
|
||||
if (_animator != null)
|
||||
if (useRagdoll)
|
||||
{
|
||||
_animator.SetTrigger(_deathHash);
|
||||
if (_animator != null) _animator.enabled = false; // Turn off animator so physics take over
|
||||
SetRagdollState(true); // Let the limbs fall!
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_animator != null)
|
||||
{
|
||||
_animator.SetTrigger(_deathHash);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable ALL colliders on this object so it doesn't block the player or stay lock-on-able
|
||||
// Disable ALL root colliders on this object (capsules) so it doesn't block the player
|
||||
Collider[] cols = GetComponents<Collider>();
|
||||
foreach(Collider col in cols)
|
||||
{
|
||||
col.enabled = false;
|
||||
}
|
||||
|
||||
// Specifically disable the lock-on script if it exists
|
||||
MonoBehaviour lockOnScript = GetComponent("SampleObjectLockOn") as MonoBehaviour;
|
||||
// Find and disable the Lock-On script even if it's on a child object
|
||||
MonoBehaviour lockOnScript = null;
|
||||
foreach (MonoBehaviour mb in GetComponentsInChildren<MonoBehaviour>())
|
||||
{
|
||||
if (mb != null && mb.GetType().Name == "SampleObjectLockOn")
|
||||
{
|
||||
lockOnScript = mb;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lockOnScript != null)
|
||||
{
|
||||
lockOnScript.enabled = false;
|
||||
// Also disable its collider so we can't lock on anymore!
|
||||
Collider lockOnCol = lockOnScript.GetComponent<Collider>();
|
||||
if (lockOnCol != null) lockOnCol.enabled = false;
|
||||
}
|
||||
|
||||
// Just to be absolutely safe, let's disable ALL trigger colliders in the children
|
||||
// (This catches the giant lock-on sphere without disabling the physical ragdoll bone colliders!)
|
||||
Collider[] allCols = GetComponentsInChildren<Collider>();
|
||||
foreach (Collider c in allCols)
|
||||
{
|
||||
if (c.isTrigger) c.enabled = false;
|
||||
}
|
||||
|
||||
Debug.Log("NPC has died!");
|
||||
@@ -381,4 +497,19 @@ public class NPCController : MonoBehaviour
|
||||
_agent.isStopped = false;
|
||||
_agent.SetDestination(destination);
|
||||
}
|
||||
|
||||
#region Animation Event Dummies
|
||||
// These methods are here purely to absorb the Animation Events you added for the Player's Combo System.
|
||||
// Since NPCs use the exact same animation clips, they will fire the events too!
|
||||
// This stops Unity from throwing "has no receiver" errors.
|
||||
public void OpenComboWindow()
|
||||
{
|
||||
// NPCs don't have a UI combo system, so they just ignore this!
|
||||
}
|
||||
|
||||
public void CloseComboWindow()
|
||||
{
|
||||
// Ignored!
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ public class NPCData : ScriptableObject
|
||||
public float attackRadius = 2.0f;
|
||||
public float attackCooldown = 2.5f;
|
||||
|
||||
[Tooltip("Chance (0 to 1) the NPC will block an incoming attack if they see the player attacking.")]
|
||||
[Range(0f, 1f)] public float blockChance = 0.5f;
|
||||
[Tooltip("How much damage the NPC blocks (0 to 1).")]
|
||||
[Range(0f, 1f)] public float blockReductionPercentage = 0.5f;
|
||||
|
||||
[Header("Movement")]
|
||||
public float walkSpeed = 2.0f;
|
||||
public float runSpeed = 5.0f;
|
||||
|
||||
@@ -97,6 +97,9 @@ public class PlayerStats : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
[HideInInspector] public bool isBlocking = false;
|
||||
[HideInInspector] public float currentBlockReduction = 0f;
|
||||
|
||||
public void TakeDamage(float amount)
|
||||
{
|
||||
if (_isDead) return;
|
||||
|
||||
@@ -19,29 +19,41 @@ public class PlayerUIManager : MonoBehaviour
|
||||
public Image manaFill;
|
||||
public Image staminaFill;
|
||||
|
||||
[Header("Combat UI")]
|
||||
public BowAimLayerController bowAimLayerController;
|
||||
[Tooltip("A Slider that fills up from 0 to 1 as the bow is drawn.")]
|
||||
public Slider bowDrawBar;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (playerStats == null)
|
||||
{
|
||||
Debug.LogWarning("PlayerUIManager: PlayerStats reference is missing! Please assign it in the Inspector.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize Slider max values if they are being used
|
||||
if (healthBar != null) healthBar.maxValue = playerStats.maxHealth;
|
||||
if (manaBar != null) manaBar.maxValue = playerStats.maxMana;
|
||||
if (staminaBar != null) staminaBar.maxValue = playerStats.maxStamina;
|
||||
|
||||
if (bowDrawBar != null)
|
||||
{
|
||||
bowDrawBar.minValue = 0f;
|
||||
bowDrawBar.maxValue = 1f;
|
||||
bowDrawBar.value = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (playerStats == null) return;
|
||||
|
||||
UpdateBars();
|
||||
UpdateCombatUI();
|
||||
}
|
||||
|
||||
private void UpdateBars()
|
||||
{
|
||||
if (playerStats == null) return;
|
||||
|
||||
// 1. Update Sliders (if assigned)
|
||||
if (healthBar != null) healthBar.value = playerStats.currentHealth;
|
||||
if (manaBar != null) manaBar.value = playerStats.currentMana;
|
||||
@@ -52,4 +64,16 @@ public class PlayerUIManager : MonoBehaviour
|
||||
if (manaFill != null) manaFill.fillAmount = playerStats.currentMana / playerStats.maxMana;
|
||||
if (staminaFill != null) staminaFill.fillAmount = playerStats.currentStamina / playerStats.maxStamina;
|
||||
}
|
||||
|
||||
private void UpdateCombatUI()
|
||||
{
|
||||
if (bowDrawBar != null && bowAimLayerController != null)
|
||||
{
|
||||
float drawPercentage = bowAimLayerController.CurrentDrawPercentage;
|
||||
bowDrawBar.value = drawPercentage;
|
||||
|
||||
// Optional: Hide the bar completely if we aren't drawing the bow
|
||||
bowDrawBar.gameObject.SetActive(drawPercentage > 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ public class Projectile : MonoBehaviour
|
||||
{
|
||||
[Header("Projectile Settings")]
|
||||
[Tooltip("How fast the projectile moves forward")]
|
||||
public float speed = 25f;
|
||||
public float speed = 50f;
|
||||
|
||||
[Tooltip("How long before the projectile destroys itself if it misses")]
|
||||
public float lifetime = 5f;
|
||||
@@ -95,9 +95,13 @@ public class Projectile : MonoBehaviour
|
||||
// Deal damage and log distance
|
||||
float distanceFlown = Vector3.Distance(_startPosition, transform.position);
|
||||
Debug.Log($"Arrow hit {hit.collider.name} for {_damage:F1} damage! It flew {distanceFlown:F1} meters.");
|
||||
hit.collider.GetComponent<WorldObject>()?.TakeDamage(_damage);
|
||||
hit.collider.GetComponent<NPCController>()?.TakeDamage(_damage);
|
||||
hit.collider.GetComponent<PlayerStats>()?.TakeDamage(_damage);
|
||||
hit.collider.GetComponentInParent<WorldObject>()?.TakeDamage(_damage);
|
||||
hit.collider.GetComponentInParent<NPCController>()?.TakeDamage(_damage);
|
||||
|
||||
// Try to hit the combat controller first so it can process blocks/stamina
|
||||
MeleeCombatLayerController playerCombat = hit.collider.GetComponentInParent<MeleeCombatLayerController>();
|
||||
if (playerCombat != null) playerCombat.TakeDamage(_damage);
|
||||
else hit.collider.GetComponentInParent<PlayerStats>()?.TakeDamage(_damage);
|
||||
|
||||
// Optional: Disable collider or script so it doesn't keep checking for hits
|
||||
Collider myCollider = GetComponent<Collider>();
|
||||
|
||||
@@ -6,6 +6,8 @@ public class Weapon : MonoBehaviour
|
||||
|
||||
[Header("Combat Stats")]
|
||||
public float damage = 10f;
|
||||
[Tooltip("How much damage is blocked when holding block with this weapon? (0.0 to 1.0)")]
|
||||
[Range(0f, 1f)] public float blockReductionPercentage = 0.5f;
|
||||
|
||||
[Header("Hit Detection")]
|
||||
public HitShape hitShape = HitShape.Sphere;
|
||||
|
||||
Reference in New Issue
Block a user