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

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