initial commit

This commit is contained in:
2026-10-12 21:28:47 +01:00
commit 6dd11d33ae
11551 changed files with 2353518 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
using UnityEngine;
public class AmmoPickup : PickupItem
{
[Header("Ammo Settings")]
[SerializeField] private int ammoAmount = 30;
[SerializeField] private string targetWeaponName = ""; // Leave blank for currently equipped weapon
protected override bool CanCollect(GameObject player)
{
var weaponsController = player.GetComponent<WeaponsController>();
if (weaponsController == null)
{
weaponsController = player.GetComponentInChildren<WeaponsController>();
}
if (weaponsController == null) return false;
Weapon activeWeapon = weaponsController.ActiveWeapon;
if (activeWeapon == null) return false;
// If target is specified, check if that weapon can receive ammo
if (!string.IsNullOrEmpty(targetWeaponName))
{
return true;
}
// Active weapon check: if infinite reserve and current mag full, skip pickup if desired
if (activeWeapon.InfiniteReserve)
{
return activeWeapon.currentAmmo < activeWeapon.maxAmmo;
}
return activeWeapon.CurrentReserveAmmo < activeWeapon.MaxReserveAmmo || activeWeapon.currentAmmo < activeWeapon.maxAmmo;
}
protected override void ApplyPickupEffect(GameObject player)
{
var weaponsController = player.GetComponent<WeaponsController>();
if (weaponsController == null)
{
weaponsController = player.GetComponentInChildren<WeaponsController>();
}
if (weaponsController != null)
{
if (string.IsNullOrEmpty(targetWeaponName))
{
weaponsController.AddAmmoToActiveWeapon(ammoAmount);
}
else
{
weaponsController.AddAmmoToMatchingWeapon(targetWeaponName, ammoAmount);
}
}
}
}

View File

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

View File

@@ -0,0 +1,84 @@
using System.Collections.Generic;
using UnityEngine;
public class AreaClearanceManager : MonoBehaviour
{
public static AreaClearanceManager Instance { get; private set; }
[SerializeField] private LevelExit levelExit;
private readonly HashSet<EnemyController> activeEnemies = new HashSet<EnemyController>();
private bool areaCleared = false;
public int RemainingEnemies => activeEnemies.Count;
public bool IsAreaCleared => areaCleared;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
}
else
{
Instance = this;
}
}
private void Start()
{
if (levelExit != null)
{
levelExit.gameObject.SetActive(false);
}
}
public void RegisterEnemy(EnemyController enemy)
{
if (enemy == null) return;
if (!activeEnemies.Contains(enemy))
{
activeEnemies.Add(enemy);
UpdateUI();
}
}
public void UnregisterEnemy(EnemyController enemy)
{
if (enemy == null) return;
if (activeEnemies.Contains(enemy))
{
activeEnemies.Remove(enemy);
UpdateUI();
if (activeEnemies.Count == 0 && !areaCleared)
{
UnlockArea();
}
}
}
private void UnlockArea()
{
areaCleared = true;
if (levelExit != null)
{
levelExit.gameObject.SetActive(true);
}
if (UIController.Instance != null)
{
UIController.Instance.ShowNotification("Area Cleared! Proceed to Exit.");
}
}
private void UpdateUI()
{
if (UIController.Instance != null)
{
UIController.Instance.UpdateZombiesRemaining(activeEnemies.Count);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 70a30e5a795ddc948a0786fc868b6da0

View File

@@ -0,0 +1,222 @@
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public class EnemyController : MonoBehaviour
{
public enum State { Idle, Chase, Attack, Dead }
private State currentState = State.Idle;
[Header("Stats")]
[SerializeField] private int maxHealth = 60;
[SerializeField] private int attackDamage = 15;
[SerializeField] private float attackRange = 1.8f;
[SerializeField] private float attackCooldown = 1.2f;
[SerializeField] private float detectionRadius = 18f;
[Header("Ranged Attack Settings (Optional)")]
[SerializeField] private bool isRangedEnemy = false;
[SerializeField] private GameObject rangedProjectilePrefab;
[SerializeField] private Transform projectileSpawnPoint;
[SerializeField] private float projectileSpeed = 25f;
[Header("References")]
[SerializeField] private Animator animator;
[SerializeField] private AudioSource audioSource;
[SerializeField] private AudioClip attackSound;
[SerializeField] private AudioClip hitSound;
[SerializeField] private AudioClip deathSound;
private NavMeshAgent agent;
private Transform playerTransform;
private int currentHealth;
private float nextAttackTime;
public State CurrentState => currentState;
public int CurrentHealth => currentHealth;
private void Awake()
{
agent = GetComponent<NavMeshAgent>();
if (animator == null)
{
animator = GetComponentInChildren<Animator>();
}
if (audioSource == null)
{
audioSource = GetComponent<AudioSource>();
}
currentHealth = maxHealth;
}
private void Start()
{
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null)
{
playerTransform = playerObj.transform;
}
AreaClearanceManager.Instance?.RegisterEnemy(this);
}
private void Update()
{
if (currentState == State.Dead) return;
// Re-acquire player reference if it was spawned dynamically
if (playerTransform == null)
{
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null)
{
playerTransform = playerObj.transform;
}
return;
}
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
switch (currentState)
{
case State.Idle:
if (distanceToPlayer <= detectionRadius)
{
currentState = State.Chase;
}
break;
case State.Chase:
if (agent.isOnNavMesh)
{
agent.SetDestination(playerTransform.position);
}
if (animator != null)
{
animator.SetFloat("Speed", agent.velocity.magnitude);
}
if (distanceToPlayer <= attackRange)
{
currentState = State.Attack;
if (agent.isOnNavMesh)
{
agent.ResetPath();
}
}
break;
case State.Attack:
Vector3 lookTarget = new Vector3(playerTransform.position.x, transform.position.y, playerTransform.position.z);
transform.LookAt(lookTarget);
if (Time.time >= nextAttackTime)
{
PerformAttack();
nextAttackTime = Time.time + attackCooldown;
}
if (distanceToPlayer > attackRange)
{
currentState = State.Chase;
}
break;
}
}
private void PerformAttack()
{
if (animator != null)
{
animator.SetTrigger("Attack");
}
if (attackSound != null && audioSource != null)
{
audioSource.PlayOneShot(attackSound);
}
if (isRangedEnemy && rangedProjectilePrefab != null)
{
// Fire projectile towards player
Vector3 spawnPos = projectileSpawnPoint != null ? projectileSpawnPoint.position : transform.position + Vector3.up * 1.2f + transform.forward * 0.5f;
Vector3 aimDir = (playerTransform.position + Vector3.up * 1.0f - spawnPos).normalized;
GameObject projObj = Instantiate(rangedProjectilePrefab, spawnPos, Quaternion.LookRotation(aimDir));
if (projObj.TryGetComponent<Projectile>(out var proj))
{
proj.Initialize(attackDamage, projectileSpeed, "Enemy");
}
}
else
{
// Direct melee strike
if (PlayerHealthController.Instance != null)
{
PlayerHealthController.Instance.TakeDamage(attackDamage);
}
}
}
public void TakeDamage(int damage)
{
if (currentState == State.Dead) return;
currentHealth -= damage;
if (hitSound != null && audioSource != null)
{
audioSource.PlayOneShot(hitSound);
}
if (animator != null)
{
animator.SetTrigger("Hit");
}
// Auto-aggro when damaged if idle
if (currentState == State.Idle)
{
currentState = State.Chase;
}
if (currentHealth <= 0)
{
Die();
}
}
private void Die()
{
currentState = State.Dead;
if (agent != null && agent.isOnNavMesh)
{
agent.isStopped = true;
}
if (agent != null)
{
agent.enabled = false;
}
if (animator != null)
{
animator.SetBool("IsDead", true);
}
if (deathSound != null && audioSource != null)
{
audioSource.PlayOneShot(deathSound);
}
Collider col = GetComponent<Collider>();
if (col != null)
{
col.enabled = false;
}
AreaClearanceManager.Instance?.UnregisterEnemy(this);
Destroy(gameObject, 5f);
}
}

View File

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

View File

@@ -0,0 +1,98 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[Header("Spawn Configuration")]
[SerializeField] private GameObject[] enemyPrefabs;
[SerializeField] private Transform[] spawnPoints;
[SerializeField] private int totalEnemiesToSpawn = 10;
[SerializeField] private float timeBetweenSpawns = 2.0f;
[SerializeField] private int maxSimultaneousEnemies = 5;
[Header("Trigger Mode")]
[SerializeField] private bool spawnOnStart = true;
[SerializeField] private bool triggerOnPlayerEnter = false;
private int spawnedCount = 0;
private bool isSpawning = false;
private readonly List<EnemyController> livingEnemies = new List<EnemyController>();
private void Start()
{
if (spawnOnStart)
{
StartSpawning();
}
}
private void OnTriggerEnter(Collider other)
{
if (triggerOnPlayerEnter && !isSpawning && other.CompareTag("Player"))
{
StartSpawning();
}
}
public void StartSpawning()
{
if (!isSpawning)
{
isSpawning = true;
StartCoroutine(SpawnRoutine());
}
}
private IEnumerator SpawnRoutine()
{
while (spawnedCount < totalEnemiesToSpawn)
{
// Clean up any destroyed references
livingEnemies.RemoveAll(e => e == null || e.CurrentState == EnemyController.State.Dead);
if (livingEnemies.Count < maxSimultaneousEnemies)
{
SpawnEnemy();
}
yield return new WaitForSeconds(timeBetweenSpawns);
}
isSpawning = false;
}
private void SpawnEnemy()
{
if (enemyPrefabs == null || enemyPrefabs.Length == 0) return;
if (spawnPoints == null || spawnPoints.Length == 0) return;
// Choose random prefab and random spawn point
GameObject chosenPrefab = enemyPrefabs[Random.Range(0, enemyPrefabs.Length)];
Transform chosenPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
GameObject enemyObj = Instantiate(chosenPrefab, chosenPoint.position, chosenPoint.rotation);
if (enemyObj.TryGetComponent<EnemyController>(out var enemy))
{
livingEnemies.Add(enemy);
AreaClearanceManager.Instance?.RegisterEnemy(enemy);
}
spawnedCount++;
}
private void OnDrawGizmos()
{
if (spawnPoints == null) return;
Gizmos.color = new Color(1f, 0.2f, 0.2f, 0.7f);
foreach (var point in spawnPoints)
{
if (point != null)
{
Gizmos.DrawWireSphere(point.position, 0.5f);
Gizmos.DrawLine(point.position, point.position + point.forward * 1.0f);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4830dd96f9166e84abaa3e220046962d

View File

@@ -0,0 +1,67 @@
using UnityEngine;
[RequireComponent(typeof(Collider))]
public class HazardZone : MonoBehaviour
{
[Header("Hazard Properties")]
[SerializeField] private bool instantKill = false;
[SerializeField] private int damagePerSecond = 20;
[SerializeField] private float damageTickRate = 0.5f;
[SerializeField] private bool damageEnemies = true;
[Header("Effects")]
[SerializeField] private AudioClip burnOrPainSound;
private float nextDamageTime;
private void Awake()
{
GetComponent<Collider>().isTrigger = true;
}
private void OnTriggerStay(Collider other)
{
if (Time.time < nextDamageTime) return;
if (other.CompareTag("Player"))
{
if (PlayerHealthController.Instance != null)
{
if (instantKill)
{
PlayerHealthController.Instance.TakeDamage(PlayerHealthController.Instance.MaxHealth);
}
else
{
int damageToDeal = Mathf.RoundToInt(damagePerSecond * damageTickRate);
PlayerHealthController.Instance.TakeDamage(Mathf.Max(1, damageToDeal));
}
PlayPainSound();
}
nextDamageTime = Time.time + damageTickRate;
}
else if (damageEnemies && other.TryGetComponent<EnemyController>(out var enemy))
{
if (instantKill)
{
enemy.TakeDamage(9999);
}
else
{
int damageToDeal = Mathf.RoundToInt(damagePerSecond * damageTickRate);
enemy.TakeDamage(Mathf.Max(1, damageToDeal));
}
nextDamageTime = Time.time + damageTickRate;
}
}
private void PlayPainSound()
{
if (burnOrPainSound != null)
{
AudioSource.PlayClipAtPoint(burnOrPainSound, transform.position);
}
}
}

View File

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

View File

@@ -0,0 +1,30 @@
using UnityEngine;
public class HealthPickup : PickupItem
{
[Header("Health Settings")]
[SerializeField] private int healAmount = 25;
[SerializeField] private bool onlyCollectWhenDamaged = true;
protected override bool CanCollect(GameObject player)
{
if (PlayerHealthController.Instance == null) return false;
if (PlayerHealthController.Instance.IsDead) return false;
if (onlyCollectWhenDamaged)
{
return PlayerHealthController.Instance.CurrentHealth < PlayerHealthController.Instance.MaxHealth;
}
return true;
}
protected override void ApplyPickupEffect(GameObject player)
{
if (PlayerHealthController.Instance != null)
{
PlayerHealthController.Instance.Heal(healAmount);
}
}
}

View File

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

View File

@@ -0,0 +1,75 @@
using System.Collections;
using UnityEngine;
public class InteractiveDoor : MonoBehaviour
{
[Header("Door Movement")]
[SerializeField] private Vector3 openOffsetPosition = new Vector3(0, 3.5f, 0); // e.g. sliding up
[SerializeField] private Vector3 openRotationEuler = Vector3.zero; // or swinging open
[SerializeField] private float transitionSpeed = 3f;
[Header("Behavior")]
[SerializeField] private bool openOnAreaCleared = true;
[SerializeField] private bool openOnTriggerEnter = false;
[Header("Audio")]
[SerializeField] private AudioClip doorOpenSound;
private Vector3 closedPosition;
private Quaternion closedRotation;
private Vector3 targetPosition;
private Quaternion targetRotation;
private bool isOpen = false;
private void Awake()
{
closedPosition = transform.position;
closedRotation = transform.rotation;
targetPosition = closedPosition;
targetRotation = closedRotation;
}
private void Update()
{
if (openOnAreaCleared && !isOpen && AreaClearanceManager.Instance != null && AreaClearanceManager.Instance.IsAreaCleared)
{
OpenDoor();
}
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * transitionSpeed);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * transitionSpeed);
}
private void OnTriggerEnter(Collider other)
{
if (openOnTriggerEnter && !isOpen && other.CompareTag("Player"))
{
OpenDoor();
}
}
public void OpenDoor()
{
if (isOpen) return;
isOpen = true;
targetPosition = closedPosition + transform.TransformDirection(openOffsetPosition);
if (openRotationEuler != Vector3.zero)
{
targetRotation = closedRotation * Quaternion.Euler(openRotationEuler);
}
if (doorOpenSound != null)
{
AudioSource.PlayClipAtPoint(doorOpenSound, transform.position);
}
}
public void CloseDoor()
{
isOpen = false;
targetPosition = closedPosition;
targetRotation = closedRotation;
}
}

View File

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

View File

@@ -0,0 +1,38 @@
using UnityEngine;
using UnityEngine.SceneManagement;
public class LevelExit : MonoBehaviour
{
[SerializeField] private string nextSceneName = "Level 2";
[SerializeField] private bool loadNextBuildIndex = false;
private bool isTransitioning = false;
private void OnTriggerEnter(Collider other)
{
if (isTransitioning) return;
if (other.CompareTag("Player"))
{
isTransitioning = true;
if (loadNextBuildIndex)
{
int nextIndex = SceneManager.GetActiveScene().buildIndex + 1;
if (nextIndex < SceneManager.sceneCountInBuildSettings)
{
SceneManager.LoadScene(nextIndex);
}
else
{
Debug.Log("No more scenes in build settings. Reloading current scene.");
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
}
else if (!string.IsNullOrEmpty(nextSceneName))
{
SceneManager.LoadScene(nextSceneName);
}
}
}
}

View File

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

View File

@@ -0,0 +1,118 @@
using UnityEngine;
[RequireComponent(typeof(Collider))]
public abstract class PickupItem : MonoBehaviour
{
[Header("Hover & Rotation Animation")]
[SerializeField] private float rotationSpeed = 90f;
[SerializeField] private float bobFrequency = 2f;
[SerializeField] private float bobAmplitude = 0.2f;
[Header("Pickup Feedback")]
[SerializeField] private GameObject pickupVFXPrefab;
[SerializeField] private AudioClip pickupSFX;
[SerializeField] private string pickupMessage = "Picked up item!";
[Header("Respawn Settings")]
[SerializeField] private bool respawns = false;
[SerializeField] private float respawnTime = 15f;
private Vector3 startPosition;
private Collider itemCollider;
private Renderer[] itemRenderers;
private bool isCollected = false;
protected virtual void Awake()
{
itemCollider = GetComponent<Collider>();
itemCollider.isTrigger = true;
itemRenderers = GetComponentsInChildren<Renderer>();
startPosition = transform.position;
}
protected virtual void Update()
{
if (isCollected) return;
// Rotate item
transform.Rotate(Vector3.up, rotationSpeed * Time.deltaTime, Space.World);
// Bob up and down
float newY = startPosition.y + Mathf.Sin(Time.time * bobFrequency) * bobAmplitude;
transform.position = new Vector3(transform.position.x, newY, transform.position.z);
}
private void OnTriggerEnter(Collider other)
{
if (isCollected) return;
if (other.CompareTag("Player"))
{
if (CanCollect(other.gameObject))
{
Collect(other.gameObject);
}
}
}
protected virtual bool CanCollect(GameObject player)
{
return true;
}
protected abstract void ApplyPickupEffect(GameObject player);
private void Collect(GameObject player)
{
isCollected = true;
ApplyPickupEffect(player);
// Play VFX
if (pickupVFXPrefab != null)
{
GameObject vfx = Instantiate(pickupVFXPrefab, transform.position, Quaternion.identity);
Destroy(vfx, 2.0f);
}
// Play SFX
if (pickupSFX != null)
{
AudioSource.PlayClipAtPoint(pickupSFX, transform.position);
}
// Display toast notification on UI
if (!string.IsNullOrEmpty(pickupMessage) && UIController.Instance != null)
{
UIController.Instance.ShowNotification(pickupMessage);
}
if (respawns)
{
SetVisibility(false);
Invoke(nameof(Respawn), respawnTime);
}
else
{
Destroy(gameObject);
}
}
private void Respawn()
{
isCollected = false;
SetVisibility(true);
}
private void SetVisibility(bool visible)
{
if (itemCollider != null) itemCollider.enabled = visible;
if (itemRenderers != null)
{
foreach (var rend in itemRenderers)
{
rend.enabled = visible;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 04769e61ce15306468800708c4df0ad7

View File

@@ -0,0 +1,84 @@
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
[Header("Movement Settings")]
[SerializeField] private float walkSpeed = 5.0f;
[SerializeField] private float sprintSpeed = 8.5f;
[SerializeField] private float gravity = -19.62f;
[SerializeField] private float jumpHeight = 1.2f;
[Header("Look Settings")]
[SerializeField] private Transform cameraHolder;
[SerializeField] private float mouseSensitivity = 2.0f;
[SerializeField] private float upDownLookLimit = 80.0f;
private CharacterController characterController;
private Vector3 velocity;
private Vector2 moveInput;
private Vector2 lookInput;
private float verticalRotation = 0f;
private bool isSprinting;
private bool jumpRequested;
private void Awake()
{
characterController = GetComponent<CharacterController>();
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
private void Update()
{
HandleRotation();
HandleMovement();
}
public void OnMove(InputValue value) => moveInput = value.Get<Vector2>();
public void OnLook(InputValue value) => lookInput = value.Get<Vector2>();
public void OnSprint(InputValue value) => isSprinting = value.isPressed;
public void OnJump(InputValue value)
{
if (value.isPressed && characterController.isGrounded)
jumpRequested = true;
}
private void HandleRotation()
{
if (cameraHolder == null) return;
float mouseX = lookInput.x * mouseSensitivity * 0.1f;
float mouseY = lookInput.y * mouseSensitivity * 0.1f;
transform.Rotate(Vector3.up * mouseX);
verticalRotation -= mouseY;
verticalRotation = Mathf.Clamp(verticalRotation, -upDownLookLimit, upDownLookLimit);
cameraHolder.localRotation = Quaternion.Euler(verticalRotation, 0f, 0f);
}
private void HandleMovement()
{
if (characterController.isGrounded && velocity.y < 0)
{
velocity.y = -2f;
}
float currentSpeed = isSprinting ? sprintSpeed : walkSpeed;
Vector3 moveDirection = transform.right * moveInput.x + transform.forward * moveInput.y;
characterController.Move(moveDirection * currentSpeed * Time.deltaTime);
if (jumpRequested)
{
velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
jumpRequested = false;
}
velocity.y += gravity * Time.deltaTime;
characterController.Move(velocity * Time.deltaTime);
}
public void SetCameraHolder(Transform camHolder) => cameraHolder = camHolder;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9ffbc22d96e5fa743b1dc6e68796edce

View File

@@ -0,0 +1,66 @@
using System;
using UnityEngine;
public class PlayerHealthController : MonoBehaviour
{
public static PlayerHealthController Instance { get; private set; }
[SerializeField] private int maxHealth = 100;
private int currentHealth;
public event Action<int, int> OnHealthChanged;
public event Action OnPlayerDied;
public int CurrentHealth => currentHealth;
public int MaxHealth => maxHealth;
public bool IsDead => currentHealth <= 0;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
}
else
{
Instance = this;
}
}
private void Start()
{
currentHealth = maxHealth;
OnHealthChanged?.Invoke(currentHealth, maxHealth);
}
public void TakeDamage(int damageAmount)
{
if (currentHealth <= 0) return;
currentHealth = Mathf.Max(0, currentHealth - damageAmount);
OnHealthChanged?.Invoke(currentHealth, maxHealth);
if (currentHealth <= 0)
{
Die();
}
}
public void Heal(int healAmount)
{
if (currentHealth <= 0) return;
currentHealth = Mathf.Min(maxHealth, currentHealth + healAmount);
OnHealthChanged?.Invoke(currentHealth, maxHealth);
}
private void Die()
{
OnPlayerDied?.Invoke();
if (UIController.Instance != null)
{
UIController.Instance.ShowGameOverScreen();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1986d9aa7b4031343af66565767eccc7

View File

@@ -0,0 +1,166 @@
using UnityEngine;
public class Projectile : MonoBehaviour
{
[Header("Flight Settings")]
[SerializeField] private float speed = 50f;
[SerializeField] private float lifeTime = 5f;
[SerializeField] private bool usePhysics = false;
[Header("Damage Settings")]
[SerializeField] private int damage = 50;
[SerializeField] private bool isExplosive = false;
[SerializeField] private float explosionRadius = 4f;
[SerializeField] private LayerMask damageableLayers = ~0;
[Header("Visual & Audio Effects")]
[SerializeField] private GameObject impactEffectPrefab;
[SerializeField] private AudioClip impactSound;
private Rigidbody rb;
private string ownerTag = "Player";
private bool hasImpacted = false;
private void Awake()
{
rb = GetComponent<Rigidbody>();
if (rb != null)
{
rb.useGravity = usePhysics;
if (usePhysics)
{
rb.linearVelocity = transform.forward * speed;
}
}
}
private void Start()
{
Destroy(gameObject, lifeTime);
}
private void Update()
{
if (!usePhysics || rb == null)
{
// Manual kinematic translation with raycast sweep to prevent tunneling
float step = speed * Time.deltaTime;
Ray ray = new Ray(transform.position, transform.forward);
if (Physics.Raycast(ray, out RaycastHit hit, step, damageableLayers, QueryTriggerInteraction.Ignore))
{
if (hit.collider.CompareTag(ownerTag)) return;
transform.position = hit.point;
OnHit(hit.collider, hit.point, hit.normal);
return;
}
transform.position += transform.forward * step;
}
}
public void Initialize(int customDamage, float customSpeed, string shooterTag)
{
damage = customDamage;
speed = customSpeed;
ownerTag = shooterTag;
if (rb != null && usePhysics)
{
rb.linearVelocity = transform.forward * speed;
}
}
private void OnTriggerEnter(Collider other)
{
if (hasImpacted || other.CompareTag(ownerTag)) return;
HandleCollision(other, transform.position, -transform.forward);
}
private void OnCollisionEnter(Collision collision)
{
if (hasImpacted || collision.collider.CompareTag(ownerTag)) return;
ContactPoint contact = collision.GetContact(0);
HandleCollision(collision.collider, contact.point, contact.normal);
}
private void OnHit(Collider hitCollider, Vector3 hitPoint, Vector3 hitNormal)
{
if (hasImpacted) return;
HandleCollision(hitCollider, hitPoint, hitNormal);
}
private void HandleCollision(Collider hitCollider, Vector3 hitPoint, Vector3 hitNormal)
{
hasImpacted = true;
if (isExplosive)
{
Explode(hitPoint);
}
else
{
// Direct hit damage
if (hitCollider.TryGetComponent<EnemyController>(out var enemy))
{
enemy.TakeDamage(damage);
}
else if (hitCollider.TryGetComponent<PlayerHealthController>(out var playerHealth))
{
playerHealth.TakeDamage(damage);
}
}
// Spawn visual impact effect
if (impactEffectPrefab != null)
{
Quaternion rot = hitNormal != Vector3.zero ? Quaternion.LookRotation(hitNormal) : Quaternion.identity;
GameObject impact = Instantiate(impactEffectPrefab, hitPoint, rot);
Destroy(impact, 2.5f);
}
// Play impact sound
if (impactSound != null)
{
AudioSource.PlayClipAtPoint(impactSound, hitPoint);
}
Destroy(gameObject);
}
private void Explode(Vector3 center)
{
Collider[] hits = Physics.OverlapSphere(center, explosionRadius, damageableLayers);
foreach (Collider col in hits)
{
if (col.CompareTag(ownerTag)) continue;
if (col.TryGetComponent<EnemyController>(out var enemy))
{
float distance = Vector3.Distance(center, col.transform.position);
float damageFalloff = Mathf.Clamp01(1f - (distance / explosionRadius));
int finalDamage = Mathf.RoundToInt(damage * damageFalloff);
enemy.TakeDamage(Mathf.Max(1, finalDamage));
}
else if (col.TryGetComponent<PlayerHealthController>(out var playerHealth))
{
float distance = Vector3.Distance(center, col.transform.position);
float damageFalloff = Mathf.Clamp01(1f - (distance / explosionRadius));
int finalDamage = Mathf.RoundToInt(damage * damageFalloff);
playerHealth.TakeDamage(Mathf.Max(1, finalDamage));
}
}
}
private void OnDrawGizmosSelected()
{
if (isExplosive)
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, explosionRadius);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6f332dafbb7f75749b484d86aae04b60

View File

@@ -0,0 +1,108 @@
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class UIController : MonoBehaviour
{
public static UIController Instance { get; private set; }
[Header("HUD Elements")]
[SerializeField] private Slider healthBar;
[SerializeField] private TextMeshProUGUI ammoText;
[SerializeField] private TextMeshProUGUI zombieCountText;
[SerializeField] private TextMeshProUGUI notificationText;
[SerializeField] private GameObject gameOverPanel;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
}
else
{
Instance = this;
}
}
private void Start()
{
if (PlayerHealthController.Instance != null)
{
PlayerHealthController.Instance.OnHealthChanged += UpdateHealthBar;
UpdateHealthBar(PlayerHealthController.Instance.CurrentHealth, PlayerHealthController.Instance.MaxHealth);
}
if (gameOverPanel != null)
{
gameOverPanel.SetActive(false);
}
}
private void OnDestroy()
{
if (PlayerHealthController.Instance != null)
{
PlayerHealthController.Instance.OnHealthChanged -= UpdateHealthBar;
}
}
public void UpdateHealthBar(int current, int max)
{
if (healthBar != null && max > 0)
{
healthBar.value = (float)current / max;
}
}
public void UpdateAmmoDisplay(int current, int max)
{
if (ammoText != null)
{
ammoText.text = $"{current} / {max}";
}
}
public void UpdateZombiesRemaining(int count)
{
if (zombieCountText != null)
{
zombieCountText.text = $"Zombies Left: {count}";
}
}
public void ShowNotification(string message)
{
if (notificationText != null)
{
notificationText.text = message;
CancelInvoke(nameof(ClearNotification));
Invoke(nameof(ClearNotification), 4f);
}
}
private void ClearNotification()
{
if (notificationText != null)
{
notificationText.text = "";
}
}
public void ShowGameOverScreen()
{
if (gameOverPanel != null)
{
gameOverPanel.SetActive(true);
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
}
public void RestartCurrentScene()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 519f2304e430a5644a8ebb33249b6c38

160
Assets/Scripts/Weapon.cs Normal file
View File

@@ -0,0 +1,160 @@
using System.Collections;
using UnityEngine;
public class Weapon : MonoBehaviour
{
[Header("Weapon Stats")]
public string weaponName = "Assault Rifle";
public int damage = 25;
public float fireRate = 0.12f;
public float range = 100f;
public int maxAmmo = 30;
public int currentAmmo;
public float reloadTime = 1.5f;
[Header("Effects")]
[SerializeField] private ParticleSystem muzzleFlash;
[SerializeField] private GameObject impactEffectPrefab;
[SerializeField] private Transform muzzlePoint;
[Header("Ballistics & Projectile Support")]
[SerializeField] private bool useProjectile = false;
[SerializeField] private GameObject projectilePrefab;
[SerializeField] private float projectileSpeed = 50f;
[Header("Ammo Reserves")]
[SerializeField] private bool infiniteReserve = true;
[SerializeField] private int currentReserveAmmo = 90;
[SerializeField] private int maxReserveAmmo = 180;
[Header("Audio Effects (Optional)")]
[SerializeField] private AudioSource audioSource;
[SerializeField] private AudioClip fireSound;
[SerializeField] private AudioClip reloadSound;
[SerializeField] private AudioClip emptySound;
private float nextFireTime;
private bool isReloading;
public bool IsReloading => isReloading;
public bool InfiniteReserve => infiniteReserve;
public int CurrentReserveAmmo => currentReserveAmmo;
public int MaxReserveAmmo => maxReserveAmmo;
private void Awake()
{
currentAmmo = maxAmmo;
if (audioSource == null)
{
audioSource = GetComponent<AudioSource>();
}
}
public bool CanShoot() => Time.time >= nextFireTime && !isReloading && currentAmmo > 0;
public void Shoot(Camera playerCamera, LayerMask hitMask)
{
nextFireTime = Time.time + fireRate;
currentAmmo--;
if (muzzleFlash) muzzleFlash.Play();
PlaySound(fireSound);
if (useProjectile && projectilePrefab != null)
{
FireProjectile(playerCamera);
}
else
{
FireHitscan(playerCamera, hitMask);
}
}
private void FireHitscan(Camera playerCamera, LayerMask hitMask)
{
Ray ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
if (Physics.Raycast(ray, out RaycastHit hit, range, hitMask))
{
if (hit.collider.TryGetComponent<EnemyController>(out var enemy))
{
enemy.TakeDamage(damage);
}
if (impactEffectPrefab)
{
GameObject impact = Instantiate(impactEffectPrefab, hit.point, Quaternion.LookRotation(hit.normal));
Destroy(impact, 2.0f);
}
}
}
private void FireProjectile(Camera playerCamera)
{
Vector3 spawnPosition = muzzlePoint != null ? muzzlePoint.position : playerCamera.transform.position + playerCamera.transform.forward * 0.5f;
// Calculate aim point by casting from crosshair
Ray ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
Vector3 targetPoint;
if (Physics.Raycast(ray, out RaycastHit hit, range))
{
targetPoint = hit.point;
}
else
{
targetPoint = ray.GetPoint(range);
}
Vector3 direction = (targetPoint - spawnPosition).normalized;
Quaternion rotation = Quaternion.LookRotation(direction);
GameObject projectileObj = Instantiate(projectilePrefab, spawnPosition, rotation);
if (projectileObj.TryGetComponent<Projectile>(out var projectile))
{
projectile.Initialize(damage, projectileSpeed, "Player");
}
}
public IEnumerator ReloadRoutine()
{
if (isReloading || currentAmmo == maxAmmo) yield break;
if (!infiniteReserve && currentReserveAmmo <= 0)
{
PlaySound(emptySound);
yield break;
}
isReloading = true;
PlaySound(reloadSound);
yield return new WaitForSeconds(reloadTime);
int neededAmmo = maxAmmo - currentAmmo;
if (infiniteReserve)
{
currentAmmo = maxAmmo;
}
else
{
int ammoToAdd = Mathf.Min(neededAmmo, currentReserveAmmo);
currentAmmo += ammoToAdd;
currentReserveAmmo -= ammoToAdd;
}
isReloading = false;
}
public void AddReserveAmmo(int amount)
{
currentReserveAmmo = Mathf.Clamp(currentReserveAmmo + amount, 0, maxReserveAmmo);
}
private void PlaySound(AudioClip clip)
{
if (clip != null && audioSource != null)
{
audioSource.PlayOneShot(clip);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 834e0aba591c01943a67d3ebcb8ac841

View File

@@ -0,0 +1,23 @@
using UnityEngine;
public class WeaponPickup : PickupItem
{
[Header("Weapon Unlock Settings")]
[SerializeField] private int weaponSlotIndex = 1;
protected override bool CanCollect(GameObject player)
{
var weaponsController = player.GetComponent<WeaponsController>() ?? player.GetComponentInChildren<WeaponsController>();
return weaponsController != null;
}
protected override void ApplyPickupEffect(GameObject player)
{
var weaponsController = player.GetComponent<WeaponsController>() ?? player.GetComponentInChildren<WeaponsController>();
if (weaponsController != null)
{
weaponsController.EquipWeapon(weaponSlotIndex);
}
}
}

View File

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

View File

@@ -0,0 +1,126 @@
using UnityEngine;
using UnityEngine.InputSystem;
public class WeaponsController : MonoBehaviour
{
[SerializeField] private Weapon[] weaponSlots;
[SerializeField] private Camera playerCamera;
[SerializeField] private LayerMask hitMask;
private int activeWeaponIndex = 0;
private bool isFiring;
public Weapon ActiveWeapon => (weaponSlots != null && activeWeaponIndex >= 0 && activeWeaponIndex < weaponSlots.Length) ? weaponSlots[activeWeaponIndex] : null;
public int ActiveWeaponIndex => activeWeaponIndex;
private void Start()
{
if (playerCamera == null)
{
playerCamera = Camera.main;
}
EquipWeapon(0);
}
private void Update()
{
if (isFiring && ActiveWeapon != null && ActiveWeapon.CanShoot())
{
ActiveWeapon.Shoot(playerCamera, hitMask);
UpdateAmmoUI();
}
}
// Input System Handlers (supporting both "Fire" and default "Attack" action names)
public void OnFire(InputValue value) => isFiring = value.isPressed;
public void OnAttack(InputValue value) => isFiring = value.isPressed;
public void OnReload(InputValue value)
{
if (value.isPressed && ActiveWeapon != null)
{
StartCoroutine(ActiveWeapon.ReloadRoutine());
}
}
public void OnNextWeapon(InputValue value)
{
if (value.isPressed) NextWeapon();
}
public void OnNext(InputValue value)
{
if (value.isPressed) NextWeapon();
}
public void OnPrevious(InputValue value)
{
if (value.isPressed) PreviousWeapon();
}
public void NextWeapon()
{
if (weaponSlots == null || weaponSlots.Length <= 1) return;
int next = (activeWeaponIndex + 1) % weaponSlots.Length;
EquipWeapon(next);
}
public void PreviousWeapon()
{
if (weaponSlots == null || weaponSlots.Length <= 1) return;
int prev = (activeWeaponIndex - 1 + weaponSlots.Length) % weaponSlots.Length;
EquipWeapon(prev);
}
public void EquipWeapon(int index)
{
if (weaponSlots == null || weaponSlots.Length == 0) return;
if (index < 0 || index >= weaponSlots.Length) return;
for (int i = 0; i < weaponSlots.Length; i++)
{
if (weaponSlots[i] != null)
{
weaponSlots[i].gameObject.SetActive(i == index);
}
}
activeWeaponIndex = index;
UpdateAmmoUI();
}
public bool AddAmmoToActiveWeapon(int amount)
{
if (ActiveWeapon != null)
{
ActiveWeapon.AddReserveAmmo(amount);
UpdateAmmoUI();
return true;
}
return false;
}
public bool AddAmmoToMatchingWeapon(string weaponIdentifier, int amount)
{
if (weaponSlots == null) return false;
foreach (var weapon in weaponSlots)
{
if (weapon != null && (string.IsNullOrEmpty(weaponIdentifier) || weapon.weaponName.ToLower().Contains(weaponIdentifier.ToLower())))
{
weapon.AddReserveAmmo(amount);
UpdateAmmoUI();
return true;
}
}
return false;
}
public void UpdateAmmoUI()
{
if (ActiveWeapon != null && UIController.Instance != null)
{
UIController.Instance.UpdateAmmoDisplay(ActiveWeapon.currentAmmo, ActiveWeapon.maxAmmo);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 58999ab6c071a3e40abcb6c42fb9537b