using UnityEngine; using System.Collections; [RequireComponent(typeof(CharacterController))] public class HumanoidEnemy : MonoBehaviour { public enum EnemyType { Grunt, Brute, Runner } [Header("Enemy Type")] public EnemyType enemyType = EnemyType.Grunt; [Header("Movement")] public float moveSpeed = 5f; public float chaseRange = 25f; public float attackRange = 2f; public float gravity = -20f; [Header("Melee Combat")] public float attackDamage = 10f; public float attackCooldown = 1f; [Header("Ranged Combat")] public bool canShoot = true; public float shootRange = 18f; // within this range (but outside attackRange) the enemy shoots public float shootDamage = 8f; public float shootCooldown = 1.5f; public float bulletSpeed = 40f; // visual tracer speed public float shootAccuracyAngle = 5f; // random spread in degrees [Header("Patrol (optional)")] public float patrolRadius = 8f; public float patrolWaitTime = 2f; // Internal private Transform player; private CharacterController controller; private EnemyHealth health; private Vector3 spawnPoint; private Vector3 patrolTarget; private float nextAttackTime; private float nextShootTime; private float verticalVelocity; private float patrolWaitTimer; private bool isAggro = false; // Animation state private Transform bodyRoot; private Transform headTransform; private Transform torsoTransform; private Transform leftArm; private Transform rightArm; private Transform leftLeg; private Transform rightLeg; private float animTimer = 0f; void Start() { controller = GetComponent(); health = GetComponent(); GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); if (playerObj == null) playerObj = GameObject.Find("Player"); if (playerObj != null) player = playerObj.transform; spawnPoint = transform.position; patrolTarget = GetRandomPatrolPoint(); ApplyTypeStats(); BuildHumanoidModel(); } void ApplyTypeStats() { switch (enemyType) { case EnemyType.Grunt: moveSpeed = 4f; attackDamage = 10f; attackCooldown = 1.2f; chaseRange = 20f; canShoot = true; shootRange = 16f; shootDamage = 8f; shootCooldown = 1.4f; shootAccuracyAngle = 6f; if (health != null) health.maxHealth = 80f; break; case EnemyType.Brute: moveSpeed = 2.5f; attackDamage = 25f; attackCooldown = 2f; chaseRange = 18f; canShoot = false; // Brute is pure melee if (health != null) health.maxHealth = 200f; break; case EnemyType.Runner: moveSpeed = 8f; attackDamage = 8f; attackCooldown = 0.6f; chaseRange = 30f; canShoot = true; shootRange = 22f; shootDamage = 6f; shootCooldown = 0.8f; shootAccuracyAngle = 10f; // less accurate but fast if (health != null) health.maxHealth = 50f; break; } if (health != null) health.currentHealth = health.maxHealth; } void Update() { if (health != null && health.currentHealth <= 0f) return; if (player == null) return; float distToPlayer = Vector3.Distance(transform.position, player.position); if (distToPlayer <= chaseRange) isAggro = true; if (isAggro) { if (distToPlayer <= attackRange) { // Close enough to punch MeleeAttack(); } else if (canShoot && distToPlayer <= shootRange) { // In shoot range — strafe / stand and fire FacePlayer(); ShootAtPlayer(); } else { ChasePlayer(); } } else { Patrol(); } // Gravity if (controller.isGrounded && verticalVelocity < 0f) verticalVelocity = -2f; verticalVelocity += gravity * Time.deltaTime; controller.Move(Vector3.up * verticalVelocity * Time.deltaTime); bool isWalking = isAggro && distToPlayer > attackRange && !(canShoot && distToPlayer <= shootRange); AnimateModel(isWalking); } void FacePlayer() { Vector3 direction = (player.position - transform.position); direction.y = 0f; if (direction != Vector3.zero) transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 8f); } void ChasePlayer() { Vector3 direction = (player.position - transform.position); direction.y = 0f; direction.Normalize(); controller.Move(direction * moveSpeed * Time.deltaTime); if (direction != Vector3.zero) transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 8f); } void Patrol() { float distToTarget = Vector3.Distance(transform.position, patrolTarget); if (distToTarget < 1.5f) { patrolWaitTimer += Time.deltaTime; if (patrolWaitTimer >= patrolWaitTime) { patrolTarget = GetRandomPatrolPoint(); patrolWaitTimer = 0f; } return; } Vector3 direction = (patrolTarget - transform.position); direction.y = 0f; direction.Normalize(); controller.Move(direction * (moveSpeed * 0.4f) * Time.deltaTime); if (direction != Vector3.zero) transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 4f); } Vector3 GetRandomPatrolPoint() { Vector2 randomCircle = Random.insideUnitCircle * patrolRadius; return spawnPoint + new Vector3(randomCircle.x, 0f, randomCircle.y); } // ─── Melee ─── void MeleeAttack() { FacePlayer(); if (Time.time >= nextAttackTime) { nextAttackTime = Time.time + attackCooldown; StartCoroutine(PunchAnimation()); Player playerHealth = player.GetComponent(); if (playerHealth != null) { playerHealth.health -= attackDamage; Debug.Log($"[Enemy] Melee hit! Player health: {playerHealth.health}"); } } } // ─── Shooting ─── void ShootAtPlayer() { if (Time.time < nextShootTime) return; nextShootTime = Time.time + shootCooldown; StartCoroutine(ShootAnimation()); // Aim at player's centre-ish (chest height) with some spread Vector3 aimOrigin = headTransform != null ? headTransform.position : transform.position + Vector3.up * 1.4f; Vector3 aimTarget = player.position + Vector3.up * 0.9f; // aim for chest Vector3 aimDir = (aimTarget - aimOrigin).normalized; // Apply accuracy spread aimDir = Quaternion.Euler( Random.Range(-shootAccuracyAngle, shootAccuracyAngle), Random.Range(-shootAccuracyAngle, shootAccuracyAngle), 0f) * aimDir; RaycastHit hit; if (Physics.Raycast(aimOrigin, aimDir, out hit, shootRange * 1.5f)) { // Spawn a visual tracer StartCoroutine(SpawnBulletTracer(aimOrigin, hit.point)); Player playerHealth = hit.transform.GetComponentInParent(); if (playerHealth != null) { playerHealth.health -= shootDamage; Debug.Log($"[Enemy] Shot hit player! Player health: {playerHealth.health}"); } } else { // Missed — still show tracer going into the distance StartCoroutine(SpawnBulletTracer(aimOrigin, aimOrigin + aimDir * shootRange)); } } IEnumerator SpawnBulletTracer(Vector3 from, Vector3 to) { GameObject tracer = GameObject.CreatePrimitive(PrimitiveType.Cube); tracer.name = "EnemyBulletTracer"; Destroy(tracer.GetComponent()); float length = Vector3.Distance(from, to); tracer.transform.localScale = new Vector3(0.02f, 0.02f, Mathf.Min(length, 0.3f)); Renderer r = tracer.GetComponent(); Material mat = new Material(Shader.Find("Standard")); mat.color = new Color(1f, 0.9f, 0.3f); // Make it emissive so it's visible mat.EnableKeyword("_EMISSION"); mat.SetColor("_EmissionColor", new Color(1f, 0.8f, 0.1f) * 2f); r.material = mat; float travelTime = length / bulletSpeed; float elapsed = 0f; while (elapsed < travelTime) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / travelTime); Vector3 pos = Vector3.Lerp(from, to, t); tracer.transform.position = pos; tracer.transform.rotation = Quaternion.LookRotation((to - from).normalized); yield return null; } Destroy(tracer); } // ─── Procedural Humanoid Model ─── void BuildHumanoidModel() { float scale = 1f; Color skinColor = new Color(0.6f, 0.45f, 0.35f); Color shirtColor; Color pantsColor = new Color(0.2f, 0.2f, 0.25f); switch (enemyType) { case EnemyType.Grunt: scale = 1f; shirtColor = new Color(0.4f, 0.25f, 0.2f); break; case EnemyType.Brute: scale = 1.3f; shirtColor = new Color(0.5f, 0.1f, 0.1f); skinColor = new Color(0.5f, 0.35f, 0.3f); break; case EnemyType.Runner: scale = 0.9f; shirtColor = new Color(0.2f, 0.35f, 0.2f); break; default: shirtColor = Color.gray; break; } bodyRoot = new GameObject("BodyRoot").transform; bodyRoot.SetParent(transform); bodyRoot.localPosition = Vector3.zero; bodyRoot.localRotation = Quaternion.identity; // Head — kept with a collider and EnemyHeadHitbox for headshots headTransform = CreatePart("Head", bodyRoot, new Vector3(0f, 1.65f, 0f) * scale, new Vector3(0.3f, 0.3f, 0.3f) * scale, skinColor, PrimitiveType.Sphere, keepCollider: true); // Wire up head hitbox EnemyHeadHitbox headHitbox = headTransform.gameObject.AddComponent(); headHitbox.enemyHealth = health; // Eyes CreatePart("LeftEye", headTransform, new Vector3(-0.08f, 0.03f, 0.12f), new Vector3(0.08f, 0.08f, 0.05f), new Color(0.9f, 0.1f, 0.1f), PrimitiveType.Sphere); CreatePart("RightEye", headTransform, new Vector3(0.08f, 0.03f, 0.12f), new Vector3(0.08f, 0.08f, 0.05f), new Color(0.9f, 0.1f, 0.1f), PrimitiveType.Sphere); // Torso torsoTransform = CreatePart("Torso", bodyRoot, new Vector3(0f, 1.15f, 0f) * scale, new Vector3(0.45f, 0.6f, 0.25f) * scale, shirtColor, PrimitiveType.Cube, keepCollider: true); // Arms leftArm = CreatePart("LeftArm", bodyRoot, new Vector3(-0.35f, 1.15f, 0f) * scale, new Vector3(0.15f, 0.55f, 0.15f) * scale, skinColor, PrimitiveType.Cube, keepCollider: true); rightArm = CreatePart("RightArm", bodyRoot, new Vector3(0.35f, 1.15f, 0f) * scale, new Vector3(0.15f, 0.55f, 0.15f) * scale, skinColor, PrimitiveType.Cube, keepCollider: true); // Legs leftLeg = CreatePart("LeftLeg", bodyRoot, new Vector3(-0.12f, 0.4f, 0f) * scale, new Vector3(0.18f, 0.6f, 0.18f) * scale, pantsColor, PrimitiveType.Cube, keepCollider: true); rightLeg = CreatePart("RightLeg", bodyRoot, new Vector3(0.12f, 0.4f, 0f) * scale, new Vector3(0.18f, 0.6f, 0.18f) * scale, pantsColor, PrimitiveType.Cube, keepCollider: true); controller.height = 1.9f * scale; controller.radius = 0.3f * scale; controller.center = new Vector3(0f, 0.95f * scale, 0f); } /// True to leave the collider on so raycasts can hit this part. Transform CreatePart(string name, Transform parent, Vector3 localPos, Vector3 localScale, Color color, PrimitiveType shape = PrimitiveType.Cube, bool keepCollider = false) { GameObject part = GameObject.CreatePrimitive(shape); part.name = name; part.transform.SetParent(parent); part.transform.localPosition = localPos; part.transform.localScale = localScale; part.transform.localRotation = Quaternion.identity; if (!keepCollider) { Collider col = part.GetComponent(); if (col != null) Destroy(col); } Renderer rend = part.GetComponent(); rend.material = new Material(Shader.Find("Standard")); rend.material.color = color; return part.transform; } // ─── Simple Limb Animation ─── void AnimateModel(bool isWalking) { if (bodyRoot == null) return; if (isWalking) { animTimer += Time.deltaTime * moveSpeed * 1.5f; float limbSwing = Mathf.Sin(animTimer) * 30f; if (leftArm != null) leftArm.localRotation = Quaternion.Euler(limbSwing, 0f, 0f); if (rightArm != null) rightArm.localRotation = Quaternion.Euler(-limbSwing, 0f, 0f); if (leftLeg != null) leftLeg.localRotation = Quaternion.Euler(-limbSwing, 0f, 0f); if (rightLeg != null) rightLeg.localRotation = Quaternion.Euler(limbSwing, 0f, 0f); if (torsoTransform != null) { Vector3 torsoPos = torsoTransform.localPosition; float scale = enemyType == EnemyType.Brute ? 1.3f : (enemyType == EnemyType.Runner ? 0.9f : 1f); torsoPos.y = 1.15f * scale + Mathf.Abs(Mathf.Sin(animTimer * 2f)) * 0.03f; torsoTransform.localPosition = torsoPos; } } else { animTimer = 0f; if (leftArm != null) leftArm.localRotation = Quaternion.Slerp(leftArm.localRotation, Quaternion.identity, Time.deltaTime * 5f); if (rightArm != null) rightArm.localRotation = Quaternion.Slerp(rightArm.localRotation, Quaternion.identity, Time.deltaTime * 5f); if (leftLeg != null) leftLeg.localRotation = Quaternion.Slerp(leftLeg.localRotation, Quaternion.identity, Time.deltaTime * 5f); if (rightLeg != null) rightLeg.localRotation = Quaternion.Slerp(rightLeg.localRotation, Quaternion.identity, Time.deltaTime * 5f); } } IEnumerator PunchAnimation() { if (rightArm == null) yield break; float timer = 0f; float punchDuration = 0.15f; while (timer < punchDuration) { timer += Time.deltaTime; float t = timer / punchDuration; rightArm.localRotation = Quaternion.Euler(Mathf.Lerp(0f, -90f, t), 0f, 0f); yield return null; } timer = 0f; while (timer < punchDuration) { timer += Time.deltaTime; float t = timer / punchDuration; rightArm.localRotation = Quaternion.Euler(Mathf.Lerp(-90f, 0f, t), 0f, 0f); yield return null; } rightArm.localRotation = Quaternion.identity; } IEnumerator ShootAnimation() { if (rightArm == null) yield break; float timer = 0f; float duration = 0.1f; // Raise arm to aim while (timer < duration) { timer += Time.deltaTime; float t = timer / duration; rightArm.localRotation = Quaternion.Euler(Mathf.Lerp(0f, -70f, t), 0f, 0f); yield return null; } yield return new WaitForSeconds(0.05f); // Recoil kick back rightArm.localRotation = Quaternion.Euler(-60f, 0f, 0f); yield return new WaitForSeconds(0.08f); // Return timer = 0f; while (timer < duration * 2f) { timer += Time.deltaTime; float t = timer / (duration * 2f); rightArm.localRotation = Quaternion.Euler(Mathf.Lerp(-70f, 0f, t), 0f, 0f); yield return null; } rightArm.localRotation = Quaternion.identity; } // ─── Gizmos ─── void OnDrawGizmosSelected() { Gizmos.color = new Color(1f, 0f, 0f, 0.15f); Gizmos.DrawWireSphere(transform.position, chaseRange); Gizmos.color = new Color(1f, 0.5f, 0f, 0.3f); Gizmos.DrawWireSphere(transform.position, attackRange); if (canShoot) { Gizmos.color = new Color(1f, 1f, 0f, 0.2f); Gizmos.DrawWireSphere(Application.isPlaying ? transform.position : transform.position, shootRange); } Gizmos.color = new Color(0f, 0.5f, 1f, 0.15f); Gizmos.DrawWireSphere(Application.isPlaying ? spawnPoint : transform.position, patrolRadius); } }