continuing with combat, sword combos working, not finished, blockign working, need to build blockpush and improving bow combat

This commit is contained in:
2026-09-30 16:39:18 +01:00
parent 7486c8d310
commit 4980fb8128
2095 changed files with 1069655 additions and 652 deletions

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 32d17db2c71c8af529d64ca75cc67182

View File

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

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

View File

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

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4944dbaf0ec1f1d3793a5dbe9aafa239

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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