Browse Source

Files required for more Unity parity.

CartBlanche 1 week ago
parent
commit
2cded2f55e
43 changed files with 1482 additions and 175 deletions
  1. BIN
      Shooter/Core/Content/Audio/Music/Wind Ambience.mp3
  2. BIN
      Shooter/Core/Content/Audio/SFX/Enemies/Hoverbot_Alert.wav
  3. BIN
      Shooter/Core/Content/Audio/SFX/Enemies/Hoverbot_Attack.wav
  4. BIN
      Shooter/Core/Content/Audio/SFX/Enemies/Hoverbot_Death.wav
  5. BIN
      Shooter/Core/Content/Audio/SFX/Enemies/Hoverbot_Movement.wav
  6. BIN
      Shooter/Core/Content/Audio/SFX/Enemies/Turret_Alert.mp3
  7. BIN
      Shooter/Core/Content/Audio/SFX/Enemies/Turret_Attack.wav
  8. BIN
      Shooter/Core/Content/Audio/SFX/Enemies/Turret_Death.wav
  9. BIN
      Shooter/Core/Content/Audio/SFX/Pickups/Pickup_Health.wav
  10. BIN
      Shooter/Core/Content/Audio/SFX/Pickups/Pickup_Jetpack.wav
  11. BIN
      Shooter/Core/Content/Audio/SFX/Pickups/Pickup_Weapon_Medium.wav
  12. BIN
      Shooter/Core/Content/Audio/SFX/Pickups/Pickup_Weapon_Small.wav
  13. BIN
      Shooter/Core/Content/Audio/SFX/Player/Damage_Tick.mp3
  14. BIN
      Shooter/Core/Content/Audio/SFX/Player/Damage_Tick2.mp3
  15. BIN
      Shooter/Core/Content/Audio/SFX/Player/Footstep.wav
  16. BIN
      Shooter/Core/Content/Audio/SFX/Player/Jetpack_Use.wav
  17. BIN
      Shooter/Core/Content/Audio/SFX/Player/Jump.wav
  18. BIN
      Shooter/Core/Content/Audio/SFX/Player/Land.wav
  19. BIN
      Shooter/Core/Content/Audio/SFX/Player/Land_Damage.wav
  20. BIN
      Shooter/Core/Content/Audio/SFX/UI/Notification_Chime.wav
  21. BIN
      Shooter/Core/Content/Audio/SFX/UI/Notification_End.wav
  22. BIN
      Shooter/Core/Content/Audio/SFX/UI/Notification_Snap.wav
  23. BIN
      Shooter/Core/Content/Audio/SFX/Weapons/Blaster_Shot.wav
  24. BIN
      Shooter/Core/Content/Audio/SFX/Weapons/Launcher_Charge_BuildUp.wav
  25. BIN
      Shooter/Core/Content/Audio/SFX/Weapons/Launcher_Charge_Loop.wav
  26. BIN
      Shooter/Core/Content/Audio/SFX/Weapons/Launcher_Explosion.wav
  27. BIN
      Shooter/Core/Content/Audio/SFX/Weapons/Launcher_Release.wav
  28. BIN
      Shooter/Core/Content/Audio/SFX/Weapons/Shotgun_Shot.wav
  29. BIN
      Shooter/Core/Content/Audio/SFX/Weapons/Weapon_Cooling.wav
  30. 0 0
      Shooter/Core/Content/Scenes/ExampleScene.json
  31. 58 42
      Shooter/Core/Services/AudioService.cs
  32. 51 80
      Shooter/Gameplay/Components/EnemyAI.cs
  33. 142 0
      Shooter/Gameplay/Components/EnemyController.cs
  34. 8 0
      Shooter/Gameplay/Components/Health.cs
  35. 148 0
      Shooter/Gameplay/Components/MuzzleFlash.cs
  36. 174 0
      Shooter/Gameplay/Components/ParticleEmitter.cs
  37. 170 0
      Shooter/Gameplay/Components/Pickup.cs
  38. 119 0
      Shooter/Gameplay/Systems/PickupSystem.cs
  39. 13 1
      Shooter/Gameplay/Systems/ProjectileSystem.cs
  40. 48 2
      Shooter/Gameplay/Weapons/Gun.cs
  41. 9 0
      Shooter/Gameplay/Weapons/Weapon.cs
  42. 149 4
      Shooter/Graphics/ForwardGraphicsProvider.cs
  43. 393 46
      Shooter/Platforms/Desktop/Game.cs

BIN
Shooter/Core/Content/Audio/Music/Wind Ambience.mp3


BIN
Shooter/Core/Content/Audio/SFX/Enemies/Hoverbot_Alert.wav


BIN
Shooter/Core/Content/Audio/SFX/Enemies/Hoverbot_Attack.wav


BIN
Shooter/Core/Content/Audio/SFX/Enemies/Hoverbot_Death.wav


BIN
Shooter/Core/Content/Audio/SFX/Enemies/Hoverbot_Movement.wav


BIN
Shooter/Core/Content/Audio/SFX/Enemies/Turret_Alert.mp3


BIN
Shooter/Core/Content/Audio/SFX/Enemies/Turret_Attack.wav


BIN
Shooter/Core/Content/Audio/SFX/Enemies/Turret_Death.wav


BIN
Shooter/Core/Content/Audio/SFX/Pickups/Pickup_Health.wav


BIN
Shooter/Core/Content/Audio/SFX/Pickups/Pickup_Jetpack.wav


BIN
Shooter/Core/Content/Audio/SFX/Pickups/Pickup_Weapon_Medium.wav


BIN
Shooter/Core/Content/Audio/SFX/Pickups/Pickup_Weapon_Small.wav


BIN
Shooter/Core/Content/Audio/SFX/Player/Damage_Tick.mp3


BIN
Shooter/Core/Content/Audio/SFX/Player/Damage_Tick2.mp3


BIN
Shooter/Core/Content/Audio/SFX/Player/Footstep.wav


BIN
Shooter/Core/Content/Audio/SFX/Player/Jetpack_Use.wav


BIN
Shooter/Core/Content/Audio/SFX/Player/Jump.wav


BIN
Shooter/Core/Content/Audio/SFX/Player/Land.wav


BIN
Shooter/Core/Content/Audio/SFX/Player/Land_Damage.wav


BIN
Shooter/Core/Content/Audio/SFX/UI/Notification_Chime.wav


BIN
Shooter/Core/Content/Audio/SFX/UI/Notification_End.wav


BIN
Shooter/Core/Content/Audio/SFX/UI/Notification_Snap.wav


BIN
Shooter/Core/Content/Audio/SFX/Weapons/Blaster_Shot.wav


BIN
Shooter/Core/Content/Audio/SFX/Weapons/Launcher_Charge_BuildUp.wav


BIN
Shooter/Core/Content/Audio/SFX/Weapons/Launcher_Charge_Loop.wav


BIN
Shooter/Core/Content/Audio/SFX/Weapons/Launcher_Explosion.wav


BIN
Shooter/Core/Content/Audio/SFX/Weapons/Launcher_Release.wav


BIN
Shooter/Core/Content/Audio/SFX/Weapons/Shotgun_Shot.wav


BIN
Shooter/Core/Content/Audio/SFX/Weapons/Weapon_Cooling.wav


+ 0 - 0
Shooter/Platforms/Desktop/Content/Scenes/ExampleScene.json → Shooter/Core/Content/Scenes/ExampleScene.json


+ 58 - 42
Shooter/Core/Services/AudioService.cs

@@ -1,4 +1,6 @@
 using System.Numerics;
 using System.Numerics;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Content;
 
 
 namespace Shooter.Core.Services;
 namespace Shooter.Core.Services;
 
 
@@ -35,9 +37,18 @@ namespace Shooter.Core.Services;
 public class AudioService : IAudioService
 public class AudioService : IAudioService
 {
 {
     private float _masterVolume = 1.0f;
     private float _masterVolume = 1.0f;
-    private Dictionary<string, object> _loadedSounds = new(); // Will be SoundEffect objects
-    private Dictionary<string, object> _loadedMusic = new(); // Will be Song objects
+    private Dictionary<string, SoundEffect> _loadedSounds = new();
+    private ContentManager? _contentManager;
     
     
+    /// <summary>
+    /// Initialize the audio service with a ContentManager
+    /// </summary>
+    public void Initialize(ContentManager contentManager)
+    {
+        _contentManager = contentManager;
+        Console.WriteLine("[AudioService] Initialized");
+    }
+
     /// <summary>
     /// <summary>
     /// Master volume control (0.0 to 1.0).
     /// Master volume control (0.0 to 1.0).
     /// Similar to AudioListener.volume in Unity.
     /// Similar to AudioListener.volume in Unity.
@@ -60,30 +71,32 @@ public class AudioService : IAudioService
     /// <param name="volume">Volume multiplier (0.0 to 1.0)</param>
     /// <param name="volume">Volume multiplier (0.0 to 1.0)</param>
     public void PlaySound(string soundName, Vector3? position = null, float volume = 1.0f)
     public void PlaySound(string soundName, Vector3? position = null, float volume = 1.0f)
     {
     {
-        // TODO Phase 4: Implement with MonoGame's SoundEffect
-        // For now, just log for debugging
-        Console.WriteLine($"[Audio] PlaySound: {soundName} at {position} (volume: {volume * MasterVolume})");
-        
-        // Future implementation:
-        // if (_loadedSounds.TryGetValue(soundName, out var soundEffect))
-        // {
-        //     var instance = soundEffect.CreateInstance();
-        //     instance.Volume = volume * MasterVolume;
-        //     
-        //     if (position.HasValue)
-        //     {
-        //         // Calculate 3D audio positioning
-        //         var listener = ServiceLocator.Get<ICamera>().Position;
-        //         var distance = Vector3.Distance(listener, position.Value);
-        //         var attenuation = 1.0f / (1.0f + distance * 0.1f);
-        //         instance.Volume *= attenuation;
-        //         
-        //         // Calculate panning based on left/right position
-        //         // Pan: -1 (left) to +1 (right)
-        //     }
-        //     
-        //     instance.Play();
-        // }
+        try
+        {
+            // Try to get already loaded sound
+            if (!_loadedSounds.TryGetValue(soundName, out var soundEffect))
+            {
+                // Try to load it on-demand
+                if (_contentManager != null)
+                {
+                    soundEffect = _contentManager.Load<SoundEffect>(soundName);
+                    _loadedSounds[soundName] = soundEffect;
+                }
+                else
+                {
+                    Console.WriteLine($"[Audio] WARNING: Cannot load sound '{soundName}' - ContentManager not initialized");
+                    return;
+                }
+            }
+
+            // Play the sound (simple 2D playback for now, 3D audio in Phase 4)
+            float finalVolume = volume * MasterVolume;
+            soundEffect.Play(finalVolume, 0f, 0f); // volume, pitch, pan
+        }
+        catch (Exception ex)
+        {
+            Console.WriteLine($"[Audio] ERROR playing sound '{soundName}': {ex.Message}");
+        }
     }
     }
     
     
     /// <summary>
     /// <summary>
@@ -128,24 +141,27 @@ public class AudioService : IAudioService
     }
     }
     
     
     /// <summary>
     /// <summary>
-    /// Load a sound effect from the content pipeline.
-    /// Call this during initialization for sounds you'll use.
+    /// Pre-load a sound effect from the content pipeline.
+    /// Optional - sounds are loaded on-demand if not pre-loaded.
     /// </summary>
     /// </summary>
-    /// <param name="soundName">Sound name (matches .xnb file)</param>
+    /// <param name="soundName">Sound name (path in Content folder without extension)</param>
     public void LoadSound(string soundName)
     public void LoadSound(string soundName)
     {
     {
-        // TODO Phase 4:
-        // var sound = ServiceLocator.Get<ContentManager>().Load<SoundEffect>(soundName);
-        // _loadedSounds[soundName] = sound;
-    }
-    
-    /// <summary>
-    /// Load music from the content pipeline.
-    /// </summary>
-    public void LoadMusic(string musicName)
-    {
-        // TODO Phase 4:
-        // var song = ServiceLocator.Get<ContentManager>().Load<Song>(musicName);
-        // _loadedMusic[musicName] = song;
+        if (_contentManager == null)
+        {
+            Console.WriteLine($"[Audio] WARNING: Cannot load sound '{soundName}' - ContentManager not initialized");
+            return;
+        }
+
+        try
+        {
+            var soundEffect = _contentManager.Load<SoundEffect>(soundName);
+            _loadedSounds[soundName] = soundEffect;
+            Console.WriteLine($"[Audio] Loaded sound: {soundName}");
+        }
+        catch (Exception ex)
+        {
+            Console.WriteLine($"[Audio] ERROR loading sound '{soundName}': {ex.Message}");
+        }
     }
     }
 }
 }

+ 51 - 80
Shooter/Gameplay/Components/EnemyAI.cs

@@ -63,15 +63,14 @@ public class EnemyAI : EntityComponent
     private Transform3D? _targetTransform;
     private Transform3D? _targetTransform;
     private Transform3D? _transform;
     private Transform3D? _transform;
     private Health? _health;
     private Health? _health;
-    
+    private EnemyController? _enemyController;
+
     // AI parameters
     // AI parameters
     private float _detectionRange = 20f;
     private float _detectionRange = 20f;
-    private float _attackRange = 2f;
+    private float _attackRange = 10f; // Ranged attack range (Unity default)
+    private float _attackStopDistanceRatio = 0.5f; // Stop at 50% of attack range (Unity default)
     private float _moveSpeed = 3f;
     private float _moveSpeed = 3f;
     private float _turnSpeed = 5f;
     private float _turnSpeed = 5f;
-    private float _attackDamage = 10f;
-    private float _attackCooldown = 1.5f;
-    private float _lastAttackTime = 0f;
     
     
     // Line of sight
     // Line of sight
     private float _losCheckInterval = 0.5f;
     private float _losCheckInterval = 0.5f;
@@ -137,21 +136,13 @@ public class EnemyAI : EntityComponent
     }
     }
 
 
     /// <summary>
     /// <summary>
-    /// Damage dealt per attack
+    /// Stop distance ratio (0.0-1.0) relative to attack range.
+    /// Unity default: 0.5 (stop at 50% of attack range)
     /// </summary>
     /// </summary>
-    public float AttackDamage
+    public float AttackStopDistanceRatio
     {
     {
-        get => _attackDamage;
-        set => _attackDamage = value;
-    }
-
-    /// <summary>
-    /// Time between attacks in seconds
-    /// </summary>
-    public float AttackCooldown
-    {
-        get => _attackCooldown;
-        set => _attackCooldown = value;
+        get => _attackStopDistanceRatio;
+        set => _attackStopDistanceRatio = Math.Clamp(value, 0f, 1f);
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -160,21 +151,23 @@ public class EnemyAI : EntityComponent
     public override void Initialize()
     public override void Initialize()
     {
     {
         base.Initialize();
         base.Initialize();
-        
+
         _transform = Owner?.GetComponent<Transform3D>();
         _transform = Owner?.GetComponent<Transform3D>();
         _health = Owner?.GetComponent<Health>();
         _health = Owner?.GetComponent<Health>();
-        
+        _enemyController = Owner?.GetComponent<EnemyController>();
+
         // Subscribe to death event
         // Subscribe to death event
         if (_health != null)
         if (_health != null)
         {
         {
             _health.OnDeath += OnDeath;
             _health.OnDeath += OnDeath;
         }
         }
-        
-        // Auto-find player as target if not set
-        // For now, we'll need to manually set the target
-        // TODO: Add scene reference to Entity class or use a global entity manager
-        
-        Console.WriteLine($"[EnemyAI] {Owner?.Name} initialized in {_currentState} state");
+
+        if (_enemyController == null)
+        {
+            Console.WriteLine($"[EnemyAI] WARNING: {Owner?.Name} has no EnemyController - ranged attacks disabled");
+        }
+
+        Console.WriteLine($"[EnemyAI] {Owner?.Name} initialized in {_currentState} state (Range: {_detectionRange}, Attack: {_attackRange})");
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -266,68 +259,55 @@ public class EnemyAI : EntityComponent
     }
     }
 
 
     /// <summary>
     /// <summary>
-    /// Update attack state - deal damage to player
+    /// Update attack state - shoot at player
     /// </summary>
     /// </summary>
     private void UpdateAttack(float deltaTime)
     private void UpdateAttack(float deltaTime)
     {
     {
         if (_targetTransform == null || _transform == null || _target == null)
         if (_targetTransform == null || _transform == null || _target == null)
             return;
             return;
-        
+
         float distanceToTarget = Vector3.Distance(_transform.Position, _targetTransform.Position);
         float distanceToTarget = Vector3.Distance(_transform.Position, _targetTransform.Position);
-        
-        // If player moved out of range, chase again
-        if (distanceToTarget > _attackRange * 1.2f) // Small buffer to prevent state flickering
+
+        // If player moved out of range or lost line of sight, chase again
+        if (distanceToTarget > _attackRange * 1.2f || !_hasLineOfSight) // Small buffer to prevent state flickering
         {
         {
             CurrentState = AIState.Chase;
             CurrentState = AIState.Chase;
             return;
             return;
         }
         }
-        
-        // Rotate toward target while attacking
-        LookAt(_targetTransform.Position, deltaTime);
-        
-        // Attack on cooldown
-        var timeService = ServiceLocator.Get<Core.Services.ITimeService>();
-        float currentTime = timeService.TotalTime;
-        if (currentTime - _lastAttackTime >= _attackCooldown)
+
+        // Calculate stop distance based on attack range ratio
+        float stopDistance = _attackRange * _attackStopDistanceRatio;
+
+        // Move toward or away to maintain optimal attack distance
+        if (distanceToTarget > stopDistance)
         {
         {
-            _lastAttackTime = currentTime;
-            PerformAttack();
+            // Move closer
+            Vector3 direction = Vector3.Normalize(_targetTransform.Position - _transform.Position);
+            _transform.Position += direction * _moveSpeed * deltaTime;
+        }
+        else if (distanceToTarget < stopDistance * 0.8f)
+        {
+            // Back away slightly (too close)
+            Vector3 direction = Vector3.Normalize(_transform.Position - _targetTransform.Position);
+            _transform.Position += direction * _moveSpeed * 0.5f * deltaTime;
         }
         }
-    }
 
 
-    /// <summary>
-    /// Perform an attack on the target
-    /// </summary>
-    private void PerformAttack()
-    {
-        if (_target == null || _transform == null)
-            return;
-        
-        var targetHealth = _target.GetComponent<Health>();
-        if (targetHealth != null && !targetHealth.IsDead)
+        // Aim and fire at target
+        if (_enemyController != null)
         {
         {
-            // Calculate hit direction and point
-            var targetTransform = _target.GetComponent<Transform3D>();
-            Vector3 hitDirection = Vector3.Normalize(targetTransform?.Position - _transform.Position ?? Vector3.UnitX);
-            Vector3 hitPoint = targetTransform?.Position ?? Vector3.Zero;
-            Vector3 hitNormal = -hitDirection; // Normal points back toward attacker
-            
-            // Create damage info
-            var damageInfo = DamageInfo.CreateFromHit(
-                _attackDamage, 
-                DamageType.Melee, 
-                Owner, 
-                hitPoint,
-                hitDirection,
-                hitNormal);
-            
-            // Deal damage
-            targetHealth.TakeDamage(damageInfo);
-            
-            Console.WriteLine($"[EnemyAI] {Owner?.Name} attacked {_target.Name} for {_attackDamage} damage!");
+            // Calculate aim point (target's center mass)
+            Vector3 aimPoint = _targetTransform.Position + new Vector3(0, 0.5f, 0);
+
+            // Orient toward target
+            _enemyController.OrientTowards(aimPoint);
+            _enemyController.OrientWeaponsTowards(aimPoint);
+
+            // Try to shoot (weapon handles its own fire rate)
+            _enemyController.TryAttack(aimPoint);
         }
         }
     }
     }
 
 
+
     /// <summary>
     /// <summary>
     /// Check if enemy has line of sight to target
     /// Check if enemy has line of sight to target
     /// Uses raycasting to detect obstacles
     /// Uses raycasting to detect obstacles
@@ -428,15 +408,6 @@ public class EnemyAI : EntityComponent
     private void OnStateEnter(AIState state)
     private void OnStateEnter(AIState state)
     {
     {
         Console.WriteLine($"[EnemyAI] {Owner?.Name} entered {state} state");
         Console.WriteLine($"[EnemyAI] {Owner?.Name} entered {state} state");
-        
-        switch (state)
-        {
-            case AIState.Attack:
-                // Reset attack timer when entering attack state
-                var timeService = ServiceLocator.Get<Core.Services.ITimeService>();
-                _lastAttackTime = timeService.TotalTime - _attackCooldown;
-                break;
-        }
     }
     }
 
 
     /// <summary>
     /// <summary>

+ 142 - 0
Shooter/Gameplay/Components/EnemyController.cs

@@ -0,0 +1,142 @@
+using Shooter.Core.Components;
+using Shooter.Core.Entities;
+using Shooter.Gameplay.Weapons;
+using System.Numerics;
+
+namespace Shooter.Gameplay.Components;
+
+/// <summary>
+/// Enemy controller component that manages enemy weapons and combat.
+/// Similar to Unity's EnemyController that handles weapon management.
+///
+/// UNITY COMPARISON:
+/// - Unity: EnemyController manages weapons array, handles shooting/aiming
+/// - MonoGame: Similar approach with weapon reference and combat methods
+/// </summary>
+public class EnemyController : EntityComponent
+{
+    private Weapon? _weapon;
+    private Transform3D? _transform;
+
+    // Orientation speed for aiming
+    private float _orientationSpeed = 10f;
+
+    /// <summary>
+    /// Speed at which enemy orients toward targets (degrees per second)
+    /// </summary>
+    public float OrientationSpeed
+    {
+        get => _orientationSpeed;
+        set => _orientationSpeed = value;
+    }
+
+    /// <summary>
+    /// Event fired when enemy attacks
+    /// </summary>
+    public event Action? OnAttack;
+
+    /// <summary>
+    /// Set the weapon for this enemy
+    /// </summary>
+    public void SetWeapon(Weapon weapon)
+    {
+        _weapon = weapon;
+        Console.WriteLine($"[EnemyController] {Owner?.Name} equipped with {weapon?.Name}");
+    }
+
+    /// <summary>
+    /// Initialize component
+    /// </summary>
+    public override void Initialize()
+    {
+        base.Initialize();
+        _transform = Owner?.GetComponent<Transform3D>();
+    }
+
+    /// <summary>
+    /// Get the current weapon
+    /// </summary>
+    public Weapon? GetCurrentWeapon()
+    {
+        return _weapon;
+    }
+
+    /// <summary>
+    /// Attempt to attack the target position.
+    /// Orients weapon toward target and fires if possible.
+    /// Returns true if weapon was fired.
+    /// </summary>
+    public bool TryAttack(Vector3 targetPosition)
+    {
+        if (_weapon == null || _transform == null)
+            return false;
+
+        // Orient toward target
+        OrientTowards(targetPosition);
+
+        // Calculate firing direction
+        Vector3 origin = _transform.Position + new Vector3(0, 0.5f, 0); // Offset for weapon height
+        Vector3 direction = Vector3.Normalize(targetPosition - origin);
+
+        // Attempt to fire
+        bool didFire = _weapon.TryFire(origin, direction);
+
+        if (didFire)
+        {
+            OnAttack?.Invoke();
+        }
+
+        return didFire;
+    }
+
+    /// <summary>
+    /// Orient the enemy's body toward a target position.
+    /// Uses smooth rotation based on OrientationSpeed.
+    /// </summary>
+    public void OrientTowards(Vector3 lookPosition)
+    {
+        if (_transform == null)
+            return;
+
+        // Calculate direction (horizontal plane only)
+        Vector3 lookDirection = lookPosition - _transform.Position;
+        lookDirection.Y = 0;
+
+        if (lookDirection.LengthSquared() < 0.001f)
+            return;
+
+        lookDirection = Vector3.Normalize(lookDirection);
+
+        // Calculate target rotation
+        float targetYaw = MathF.Atan2(lookDirection.X, lookDirection.Z);
+        Quaternion targetRotation = Quaternion.CreateFromYawPitchRoll(targetYaw, 0, 0);
+
+        // Smooth rotation (using Update's delta time would be better, but for now instant)
+        // TODO: Add deltaTime parameter for smooth interpolation
+        _transform.Rotation = targetRotation;
+    }
+
+    /// <summary>
+    /// Orient weapons toward a target position.
+    /// In MonoGame, we don't have separate weapon transforms like Unity,
+    /// so this currently just ensures proper aiming direction.
+    /// </summary>
+    public void OrientWeaponsTowards(Vector3 lookPosition)
+    {
+        // In Unity, this would rotate weapon transforms independently
+        // For our simplified system, the weapon fires from enemy position
+        // in the direction they're facing, so we just orient the body
+        OrientTowards(lookPosition);
+    }
+
+    /// <summary>
+    /// Update weapon state
+    /// </summary>
+    public override void Update(GameTime gameTime)
+    {
+        base.Update(gameTime);
+
+        // Update weapon timers
+        _weapon?.Update(gameTime);
+    }
+}

+ 8 - 0
Shooter/Gameplay/Components/Health.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using Shooter.Core.Components;
 using Shooter.Core.Components;
+using Shooter.Core.Services;
 using Shooter.Gameplay.Systems;
 using Shooter.Gameplay.Systems;
 
 
 namespace Shooter.Gameplay.Components
 namespace Shooter.Gameplay.Components
@@ -151,6 +152,13 @@ namespace Shooter.Gameplay.Components
 
 
             Console.WriteLine($"[Health] {Owner?.Name} took {actualDamage:F1} damage ({_currentHealth:F1}/{_maxHealth:F1} HP remaining)");
             Console.WriteLine($"[Health] {Owner?.Name} took {actualDamage:F1} damage ({_currentHealth:F1}/{_maxHealth:F1} HP remaining)");
 
 
+            // Play damage sound (only for player)
+            if (Owner?.Tag == "Player")
+            {
+                var audioService = ServiceLocator.Get<IAudioService>();
+                audioService?.PlaySound("Audio/SFX/Player/Damage_Tick", null, 0.5f);
+            }
+
             // Trigger damage event
             // Trigger damage event
             OnDamaged?.Invoke(damageInfo, _currentHealth);
             OnDamaged?.Invoke(damageInfo, _currentHealth);
 
 

+ 148 - 0
Shooter/Gameplay/Components/MuzzleFlash.cs

@@ -0,0 +1,148 @@
+using System;
+using System.Numerics;
+using Shooter.Core.Components;
+using Shooter.Core.Entities;
+
+namespace Shooter.Gameplay.Components;
+
+/// <summary>
+/// Muzzle flash effect component for weapons.
+///
+/// UNITY COMPARISON - PARTICLE SYSTEM FOR MUZZLE FLASH:
+///
+/// Unity Approach:
+/// 1. Use ParticleSystem component with emission burst
+/// 2. Attach to weapon's "MuzzlePoint" transform
+/// 3. Play() on fire, automatic lifetime management
+/// 4. Often includes light component for flash lighting
+///
+/// MonoGame Approach (This Component):
+/// 1. Simple billboard sprite (quad facing camera)
+/// 2. Manual flash timing and fade-out
+/// 3. Position at weapon barrel offset
+/// 4. Optional random rotation for variety
+///
+/// EDUCATIONAL NOTE - WHY SIMPLE BILLBOARD?
+///
+/// For an educational project, we start with the simplest approach:
+/// - Billboard = 2D quad that always faces the camera
+/// - Flash duration = 1-2 frames (50-100ms)
+/// - Random rotation = visual variety without complexity
+///
+/// This is actually how many older FPS games did muzzle flashes!
+/// Modern games use particle systems, but the principle is the same:
+/// "Show bright flash briefly at weapon barrel when firing"
+///
+/// Phase 4 can upgrade this to a proper particle system if desired.
+/// </summary>
+public class MuzzleFlash : EntityComponent
+{
+    private float _flashTimer = 0f;
+    private float _flashDuration = 0.05f; // 50ms flash (about 3 frames at 60fps)
+    private bool _isFlashing = false;
+    private float _randomRotation = 0f;
+
+    /// <summary>
+    /// Position offset from weapon origin to barrel (where flash appears).
+    /// In camera space: (right, up, forward)
+    /// </summary>
+    public Vector3 BarrelOffset { get; set; } = new Vector3(0f, 0f, -0.8f);
+
+    /// <summary>
+    /// Scale of the flash sprite
+    /// </summary>
+    public float FlashScale { get; set; } = 0.3f;
+
+    /// <summary>
+    /// Whether the flash is currently visible
+    /// </summary>
+    public bool IsVisible => _isFlashing;
+
+    /// <summary>
+    /// Color/tint of the flash (typically yellow-orange)
+    /// </summary>
+    public Vector4 FlashColor { get; set; } = new Vector4(1f, 0.8f, 0.4f, 1f);
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        _isFlashing = false;
+    }
+
+    public override void Update(Core.Components.GameTime gameTime)
+    {
+        base.Update(gameTime);
+
+        // Update flash timer
+        if (_isFlashing)
+        {
+            _flashTimer -= (float)gameTime.ElapsedGameTime.TotalSeconds;
+            if (_flashTimer <= 0f)
+            {
+                _isFlashing = false;
+            }
+        }
+    }
+
+    /// <summary>
+    /// Trigger the muzzle flash effect
+    /// </summary>
+    public void Flash()
+    {
+        _isFlashing = true;
+        _flashTimer = _flashDuration;
+
+        // Random rotation for visual variety (0-360 degrees)
+        Random random = new Random();
+        _randomRotation = (float)(random.NextDouble() * Math.PI * 2);
+
+        // Debug output
+        Console.WriteLine($"[MuzzleFlash] Flash triggered! Duration: {_flashDuration}s");
+
+        // EDUCATIONAL NOTE:
+        // In Unity, you'd call particleSystem.Play()
+        // Here, we just set a flag and timer - the renderer will check IsVisible
+    }
+
+    /// <summary>
+    /// Get the world position where the flash should be rendered
+    /// </summary>
+    public Vector3 GetWorldPosition(Vector3 cameraPosition, Vector3 cameraTarget)
+    {
+        // Get camera direction vectors
+        var forward = Vector3.Normalize(cameraTarget - cameraPosition);
+        var worldUp = new Vector3(0, 1, 0);
+        var right = Vector3.Normalize(Vector3.Cross(worldUp, forward));
+        var up = Vector3.Normalize(Vector3.Cross(forward, right));
+
+        // Transform barrel offset from camera space to world space
+        var worldOffset =
+            right * BarrelOffset.X +
+            up * BarrelOffset.Y +
+            forward * -BarrelOffset.Z;
+
+        return cameraPosition + worldOffset;
+    }
+
+    /// <summary>
+    /// Get the rotation to apply to the flash sprite
+    /// </summary>
+    public float GetRotation()
+    {
+        return _randomRotation;
+    }
+
+    // EDUCATIONAL NOTE - RENDERING APPROACH:
+    //
+    // For Phase 2, we'll use a simple approach:
+    // 1. Check if MuzzleFlash.IsVisible
+    // 2. If yes, render a camera-facing quad at GetWorldPosition()
+    // 3. Apply FlashColor as tint
+    // 4. Apply GetRotation() around the camera's forward axis
+    //
+    // In Phase 4, we can upgrade to:
+    // - Additive blending (flash adds light to scene)
+    // - Particle system with multiple flash sprites
+    // - Point light that flashes with the sprite
+    // - Smoke particles after flash
+}

+ 174 - 0
Shooter/Gameplay/Components/ParticleEmitter.cs

@@ -0,0 +1,174 @@
+using System;
+using System.Numerics;
+using Shooter.Core.Components;
+
+namespace Shooter.Gameplay.Components;
+
+/// <summary>
+/// Simple particle emitter for visual effects (impacts, sparks, smoke).
+/// Similar to Unity's ParticleSystem but much simpler for Phase 2C.
+/// </summary>
+public class ParticleEmitter : EntityComponent
+{
+    public struct Particle
+    {
+        public Vector3 Position;
+        public Vector3 Velocity;
+        public Vector4 Color;
+        public float Size;
+        public float Lifetime;
+        public float Age;
+        public bool Active;
+    }
+
+    private Particle[] _particles = new Particle[50];
+    private int _maxParticles = 50;
+    private Random _random = new Random();
+
+    // Emitter properties
+    public int MaxParticles
+    {
+        get => _maxParticles;
+        set
+        {
+            if (value != _maxParticles)
+            {
+                _maxParticles = value;
+                _particles = new Particle[value];
+            }
+        }
+    }
+
+    public Vector3 EmissionPosition { get; set; }
+    public Vector4 StartColor { get; set; } = new Vector4(1, 1, 1, 1);
+    public Vector4 EndColor { get; set; } = new Vector4(1, 1, 1, 0);
+    public float StartSize { get; set; } = 0.1f;
+    public float EndSize { get; set; } = 0.05f;
+    public float Lifetime { get; set; } = 0.5f;
+    public float EmissionSpeed { get; set; } = 5f;
+    public float Gravity { get; set; } = -9.8f;
+    public bool OneShot { get; set; } = true; // Burst or continuous
+
+    private bool _hasEmitted = false;
+
+    /// <summary>
+    /// Get active particles for rendering
+    /// </summary>
+    public Particle[] GetActiveParticles()
+    {
+        return _particles;
+    }
+
+    /// <summary>
+    /// Emit a burst of particles
+    /// </summary>
+    public void Emit(int count, Vector3 position, Vector3 direction)
+    {
+        EmissionPosition = position;
+        _hasEmitted = true;
+
+        int emitted = 0;
+        for (int i = 0; i < _particles.Length && emitted < count; i++)
+        {
+            if (!_particles[i].Active)
+            {
+                // Random cone spread around direction
+                Vector3 randomDir = GetRandomDirection(direction, 30f); // 30 degree cone
+
+                _particles[i].Position = position;
+                _particles[i].Velocity = randomDir * EmissionSpeed;
+                _particles[i].Color = StartColor;
+                _particles[i].Size = StartSize;
+                _particles[i].Lifetime = Lifetime;
+                _particles[i].Age = 0f;
+                _particles[i].Active = true;
+                emitted++;
+            }
+        }
+    }
+
+    public override void Update(Core.Components.GameTime gameTime)
+    {
+        base.Update(gameTime);
+
+        float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
+
+        // Update all active particles
+        for (int i = 0; i < _particles.Length; i++)
+        {
+            if (!_particles[i].Active)
+                continue;
+
+            _particles[i].Age += deltaTime;
+
+            // Deactivate if lifetime exceeded
+            if (_particles[i].Age >= _particles[i].Lifetime)
+            {
+                _particles[i].Active = false;
+                continue;
+            }
+
+            // Update position with velocity and gravity
+            _particles[i].Velocity += new Vector3(0, Gravity * deltaTime, 0);
+            _particles[i].Position += _particles[i].Velocity * deltaTime;
+
+            // Interpolate color and size based on age
+            float t = _particles[i].Age / _particles[i].Lifetime;
+            _particles[i].Color = Vector4.Lerp(StartColor, EndColor, t);
+            _particles[i].Size = MathHelper.Lerp(StartSize, EndSize, t);
+        }
+
+        // Auto-destroy one-shot emitters after all particles die
+        if (OneShot && _hasEmitted && AllParticlesDead())
+        {
+            if (Owner != null)
+                Owner.Active = false; // Deactivate entity
+        }
+    }
+
+    private bool AllParticlesDead()
+    {
+        foreach (var particle in _particles)
+        {
+            if (particle.Active)
+                return false;
+        }
+        return true;
+    }
+
+    private Vector3 GetRandomDirection(Vector3 baseDirection, float coneAngleDegrees)
+    {
+        // Convert to radians
+        float coneAngle = coneAngleDegrees * (float)(Math.PI / 180.0);
+
+        // Random angles within cone
+        float randomAngle = (float)(_random.NextDouble() * Math.PI * 2);
+        float randomDistance = (float)(_random.NextDouble() * coneAngle);
+
+        // Create perpendicular vectors
+        Vector3 up = Vector3.UnitY;
+        if (Math.Abs(Vector3.Dot(baseDirection, up)) > 0.99f)
+            up = Vector3.UnitX;
+
+        Vector3 right = Vector3.Normalize(Vector3.Cross(up, baseDirection));
+        up = Vector3.Normalize(Vector3.Cross(baseDirection, right));
+
+        // Apply spread
+        float spreadX = MathF.Cos(randomAngle) * randomDistance;
+        float spreadY = MathF.Sin(randomAngle) * randomDistance;
+
+        Vector3 result = baseDirection + (right * spreadX) + (up * spreadY);
+        return Vector3.Normalize(result);
+    }
+}
+
+/// <summary>
+/// Helper class for math utilities
+/// </summary>
+public static class MathHelper
+{
+    public static float Lerp(float a, float b, float t)
+    {
+        return a + (b - a) * Math.Clamp(t, 0f, 1f);
+    }
+}

+ 170 - 0
Shooter/Gameplay/Components/Pickup.cs

@@ -0,0 +1,170 @@
+using System;
+using System.Numerics;
+using Shooter.Core.Components;
+
+namespace Shooter.Gameplay.Components;
+
+/// <summary>
+/// Pickup component for collectible items (health, ammo, power-ups).
+/// Similar to pickup/collectible systems in Unity FPS games.
+///
+/// USAGE:
+/// - Attach to entities that can be collected
+/// - Configure PickupType and value
+/// - Subscribe to OnPickedUp event for feedback (sound, particles)
+/// - Physics trigger detection handled automatically
+/// </summary>
+public class Pickup : EntityComponent
+{
+    /// <summary>
+    /// Type of pickup
+    /// </summary>
+    public PickupType Type { get; set; } = PickupType.Health;
+
+    /// <summary>
+    /// Value/amount of the pickup (health restored, ammo added, etc.)
+    /// </summary>
+    public float Value { get; set; } = 25f;
+
+    /// <summary>
+    /// Whether this pickup has been collected
+    /// </summary>
+    public bool IsCollected { get; private set; } = false;
+
+    /// <summary>
+    /// Should this pickup rotate for visual effect?
+    /// </summary>
+    public bool RotatePickup { get; set; } = true;
+
+    /// <summary>
+    /// Rotation speed in radians per second
+    /// </summary>
+    public float RotationSpeed { get; set; } = 2.0f;
+
+    /// <summary>
+    /// Should this pickup bob up and down?
+    /// </summary>
+    public bool BobPickup { get; set; } = true;
+
+    /// <summary>
+    /// Bob amplitude (how far up/down)
+    /// </summary>
+    public float BobAmplitude { get; set; } = 0.2f;
+
+    /// <summary>
+    /// Bob frequency (how fast)
+    /// </summary>
+    public float BobFrequency { get; set; } = 2.0f;
+
+    /// <summary>
+    /// Auto-pickup radius (distance at which player auto-collects)
+    /// </summary>
+    public float PickupRadius { get; set; } = 1.5f;
+
+    // State
+    private float _rotationAngle = 0f;
+    private float _bobTimer = 0f;
+    private Vector3 _originalPosition;
+
+    /// <summary>
+    /// Event fired when this pickup is collected. Parameters: (pickupType, value, collector)
+    /// </summary>
+    public event Action<PickupType, float, Core.Entities.Entity>? OnPickedUp;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        // Store original position for bobbing effect
+        var transform = Owner?.GetComponent<Transform3D>();
+        if (transform != null)
+        {
+            _originalPosition = transform.Position;
+        }
+    }
+
+    public override void Update(Core.Components.GameTime gameTime)
+    {
+        base.Update(gameTime);
+
+        if (IsCollected)
+            return;
+
+        float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
+        var transform = Owner?.GetComponent<Transform3D>();
+        if (transform == null)
+            return;
+
+        // Rotate pickup
+        if (RotatePickup)
+        {
+            _rotationAngle += RotationSpeed * deltaTime;
+            transform.LocalRotation = Quaternion.CreateFromAxisAngle(
+                Vector3.UnitY,
+                _rotationAngle
+            );
+        }
+
+        // Bob up and down
+        if (BobPickup)
+        {
+            _bobTimer += deltaTime * BobFrequency;
+            float bobOffset = MathF.Sin(_bobTimer) * BobAmplitude;
+            transform.Position = new Vector3(
+                _originalPosition.X,
+                _originalPosition.Y + bobOffset,
+                _originalPosition.Z
+            );
+        }
+    }
+
+    /// <summary>
+    /// Attempt to collect this pickup
+    /// </summary>
+    /// <param name="collector">The entity collecting this pickup</param>
+    /// <returns>True if successfully collected</returns>
+    public bool TryCollect(Core.Entities.Entity collector)
+    {
+        if (IsCollected)
+            return false;
+
+        IsCollected = true;
+
+        Console.WriteLine($"[Pickup] {collector.Name} collected {Type} (+{Value})");
+
+        // Trigger event
+        OnPickedUp?.Invoke(Type, Value, collector);
+
+        // Hide the pickup
+        if (Owner != null)
+        {
+            Owner.Active = false;
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    /// Check if an entity is close enough to auto-collect
+    /// </summary>
+    public bool IsInRange(Vector3 position)
+    {
+        var transform = Owner?.GetComponent<Transform3D>();
+        if (transform == null)
+            return false;
+
+        float distance = Vector3.Distance(transform.Position, position);
+        return distance <= PickupRadius;
+    }
+}
+
+/// <summary>
+/// Types of pickups available
+/// </summary>
+public enum PickupType
+{
+    Health,
+    Ammo,
+    Armor,
+    PowerUp
+}

+ 119 - 0
Shooter/Gameplay/Systems/PickupSystem.cs

@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using Shooter.Core.Entities;
+using Shooter.Core.Services;
+using Shooter.Gameplay.Components;
+
+namespace Shooter.Gameplay.Systems;
+
+/// <summary>
+/// System for managing pickups and automatic collection.
+/// Checks proximity between player and pickups each frame.
+/// </summary>
+public class PickupSystem
+{
+    private Entity? _player;
+
+    public PickupSystem(Entity? player)
+    {
+        _player = player;
+    }
+
+    /// <summary>
+    /// Update the pickup system - check for auto-collection
+    /// </summary>
+    public void Update(Core.Components.GameTime gameTime, IEnumerable<Pickup> pickups)
+    {
+        if (_player == null)
+            return;
+
+        var playerTransform = _player.GetComponent<Core.Components.Transform3D>();
+        if (playerTransform == null)
+            return;
+
+        Vector3 playerPosition = playerTransform.Position;
+
+        // Check each pickup for proximity
+        foreach (var pickup in pickups)
+        {
+            if (pickup.IsCollected)
+                continue;
+
+            if (pickup.IsInRange(playerPosition))
+            {
+                CollectPickup(pickup, _player);
+            }
+        }
+    }
+
+    /// <summary>
+    /// Collect a pickup and apply its effects to the player
+    /// </summary>
+    private void CollectPickup(Pickup pickup, Entity player)
+    {
+        switch (pickup.Type)
+        {
+            case PickupType.Health:
+                ApplyHealthPickup(pickup, player);
+                break;
+
+            case PickupType.Ammo:
+                ApplyAmmoPickup(pickup, player);
+                break;
+
+            case PickupType.Armor:
+                // TODO: Implement armor system
+                Console.WriteLine($"[PickupSystem] Armor pickup not yet implemented");
+                break;
+
+            case PickupType.PowerUp:
+                // TODO: Implement power-up system
+                Console.WriteLine($"[PickupSystem] Power-up pickup not yet implemented");
+                break;
+        }
+
+        // Trigger the pickup's collection
+        pickup.TryCollect(player);
+    }
+
+    /// <summary>
+    /// Apply health pickup to player
+    /// </summary>
+    private void ApplyHealthPickup(Pickup pickup, Entity player)
+    {
+        var health = player.GetComponent<Health>();
+        if (health != null && health.CurrentHealth < health.MaxHealth)
+        {
+            health.Heal(pickup.Value);
+            Console.WriteLine($"[PickupSystem] Player healed for {pickup.Value} HP");
+
+            // Play pickup sound
+            var audioService = ServiceLocator.Get<IAudioService>();
+            audioService?.PlaySound("Audio/SFX/Pickups/Pickup_Health", null, 0.6f);
+        }
+    }
+
+    /// <summary>
+    /// Apply ammo pickup to player
+    /// </summary>
+    private void ApplyAmmoPickup(Pickup pickup, Entity player)
+    {
+        var weaponController = player.GetComponent<WeaponController>();
+        if (weaponController != null)
+        {
+            var currentWeapon = weaponController.CurrentWeapon;
+            if (currentWeapon != null)
+            {
+                int ammoToAdd = (int)pickup.Value;
+                currentWeapon.AddAmmo(ammoToAdd);
+                Console.WriteLine($"[PickupSystem] Player gained {ammoToAdd} reserve ammo");
+
+                // Play pickup sound
+                var audioService = ServiceLocator.Get<IAudioService>();
+                audioService?.PlaySound("Audio/SFX/Pickups/Pickup_Weapon_Small", null, 0.6f);
+            }
+        }
+    }
+}

+ 13 - 1
Shooter/Gameplay/Systems/ProjectileSystem.cs

@@ -26,11 +26,17 @@ namespace Shooter.Gameplay.Systems
     public class ProjectileSystem
     public class ProjectileSystem
     {
     {
         private readonly IPhysicsProvider _physicsProvider;
         private readonly IPhysicsProvider _physicsProvider;
-        
+
         // Visual feedback for debugging (optional)
         // Visual feedback for debugging (optional)
         private readonly List<DebugRay> _debugRays = new();
         private readonly List<DebugRay> _debugRays = new();
         private const float DEBUG_RAY_DURATION = 0.1f; // seconds
         private const float DEBUG_RAY_DURATION = 0.1f; // seconds
 
 
+        // Event fired when a hitscan successfully hits an entity with Health
+        public event Action<Entity>? OnHitEntity;
+
+        // Event fired when any hitscan impact occurs (for particles, decals, etc.)
+        public event Action<Vector3, Vector3>? OnImpact; // position, normal
+
         public ProjectileSystem(IPhysicsProvider physicsProvider)
         public ProjectileSystem(IPhysicsProvider physicsProvider)
         {
         {
             _physicsProvider = physicsProvider ?? throw new ArgumentNullException(nameof(physicsProvider));
             _physicsProvider = physicsProvider ?? throw new ArgumentNullException(nameof(physicsProvider));
@@ -92,6 +98,9 @@ namespace Shooter.Gameplay.Systems
             {
             {
                 Console.WriteLine($"[Projectile] Hitscan hit at distance {hit.Distance:F2}");
                 Console.WriteLine($"[Projectile] Hitscan hit at distance {hit.Distance:F2}");
 
 
+                // Trigger impact event for visual effects (particles, decals)
+                OnImpact?.Invoke(hit.Point, hit.Normal);
+
                 // Try to get the entity from the physics body's user data
                 // Try to get the entity from the physics body's user data
                 Entity? hitEntity = hit.UserData as Entity;
                 Entity? hitEntity = hit.UserData as Entity;
                 
                 
@@ -113,6 +122,9 @@ namespace Shooter.Gameplay.Systems
 
 
                         // Apply damage
                         // Apply damage
                         healthComponent.TakeDamage(damageInfo);
                         healthComponent.TakeDamage(damageInfo);
+
+                        // Trigger hit event for feedback (hit markers, sounds, etc.)
+                        OnHitEntity?.Invoke(hitEntity);
                     }
                     }
                     else
                     else
                     {
                     {

+ 48 - 2
Shooter/Gameplay/Weapons/Gun.cs

@@ -104,7 +104,7 @@ namespace Shooter.Gameplay.Weapons
         /// </summary>
         /// </summary>
         public static Gun CreatePistol()
         public static Gun CreatePistol()
         {
         {
-            return new Gun(
+            var pistol = new Gun(
                 name: "Pistol",
                 name: "Pistol",
                 damage: 25f,
                 damage: 25f,
                 fireRate: 3f,        // 3 rounds per second
                 fireRate: 3f,        // 3 rounds per second
@@ -116,6 +116,8 @@ namespace Shooter.Gameplay.Weapons
                 recoilAmount: 0.3f,
                 recoilAmount: 0.3f,
                 spread: 0.0f         // No spread for testing
                 spread: 0.0f         // No spread for testing
             );
             );
+            pistol.FireSound = "Audio/SFX/Weapons/Blaster_Shot";
+            return pistol;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -123,7 +125,7 @@ namespace Shooter.Gameplay.Weapons
         /// </summary>
         /// </summary>
         public static Gun CreateAssaultRifle()
         public static Gun CreateAssaultRifle()
         {
         {
-            return new Gun(
+            var rifle = new Gun(
                 name: "Assault Rifle",
                 name: "Assault Rifle",
                 damage: 20f,
                 damage: 20f,
                 fireRate: 10f,       // 10 rounds per second
                 fireRate: 10f,       // 10 rounds per second
@@ -135,6 +137,8 @@ namespace Shooter.Gameplay.Weapons
                 recoilAmount: 0.5f,
                 recoilAmount: 0.5f,
                 spread: 2.0f
                 spread: 2.0f
             );
             );
+            rifle.FireSound = "Audio/SFX/Weapons/Shotgun_Shot";
+            return rifle;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -155,5 +159,47 @@ namespace Shooter.Gameplay.Weapons
                 spread: 10.0f        // Wide spread
                 spread: 10.0f        // Wide spread
             );
             );
         }
         }
+
+        /// <summary>
+        /// Create enemy HoverBot "Eye Lazers" weapon (Unity specs)
+        /// </summary>
+        public static Gun CreateEnemyEyeLazers()
+        {
+            var weapon = new Gun(
+                name: "Eye Lazers",
+                damage: 15f,         // Moderate damage
+                fireRate: 1.25f,     // 0.8s between shots = 1.25 shots/sec
+                magazineSize: 999,   // Infinite ammo for enemies
+                maxAmmo: 999,
+                reloadTime: 0f,      // No reload for enemies
+                range: 100f,
+                firingMode: FiringMode.SemiAutomatic,
+                recoilAmount: 0f,
+                spread: 3.0f         // Some inaccuracy for gameplay balance
+            );
+            weapon.FireSound = "Audio/SFX/Weapons/Blaster_Shot";
+            return weapon;
+        }
+
+        /// <summary>
+        /// Create enemy Turret "Machine Gun" weapon (Unity specs)
+        /// </summary>
+        public static Gun CreateEnemyMachineGun()
+        {
+            var weapon = new Gun(
+                name: "Machine Gun",
+                damage: 10f,         // Lower damage but rapid fire
+                fireRate: 16.67f,    // 0.06s between shots = ~16.67 shots/sec
+                magazineSize: 999,   // Infinite ammo for enemies
+                maxAmmo: 999,
+                reloadTime: 0f,      // No reload for enemies
+                range: 100f,
+                firingMode: FiringMode.FullyAutomatic,
+                recoilAmount: 0f,
+                spread: 5.0f         // Higher spread for balance (very fast fire rate)
+            );
+            weapon.FireSound = "Audio/SFX/Weapons/Shotgun_Shot";
+            return weapon;
+        }
     }
     }
 }
 }

+ 9 - 0
Shooter/Gameplay/Weapons/Weapon.cs

@@ -1,5 +1,6 @@
 using System.Numerics;
 using System.Numerics;
 using Shooter.Core.Components;
 using Shooter.Core.Components;
+using Shooter.Core.Services;
 
 
 namespace Shooter.Gameplay.Weapons
 namespace Shooter.Gameplay.Weapons
 {
 {
@@ -17,6 +18,7 @@ namespace Shooter.Gameplay.Weapons
         public int MaxAmmo { get; protected set; }
         public int MaxAmmo { get; protected set; }
         public float ReloadTime { get; protected set; }
         public float ReloadTime { get; protected set; }
         public float Range { get; protected set; }
         public float Range { get; protected set; }
+        public string? FireSound { get; set; } // Audio path for weapon fire sound
 
 
         // Current State
         // Current State
         public int CurrentAmmoInMag { get; protected set; }
         public int CurrentAmmoInMag { get; protected set; }
@@ -72,6 +74,13 @@ namespace Shooter.Gameplay.Weapons
             // Set next fire time based on fire rate
             // Set next fire time based on fire rate
             _nextFireTime = _currentTime + (1f / FireRate);
             _nextFireTime = _currentTime + (1f / FireRate);
 
 
+            // Play fire sound
+            if (!string.IsNullOrEmpty(FireSound))
+            {
+                var audioService = ServiceLocator.Get<IAudioService>();
+                audioService?.PlaySound(FireSound, origin, 0.7f);
+            }
+
             // Perform the actual firing logic (implemented by derived classes)
             // Perform the actual firing logic (implemented by derived classes)
             OnFire(origin, direction);
             OnFire(origin, direction);
 
 

+ 149 - 4
Shooter/Graphics/ForwardGraphicsProvider.cs

@@ -293,11 +293,71 @@ public class ForwardGraphicsProvider : IGraphicsProvider
             );
             );
         }
         }
     }
     }
-    
+
+    /// <summary>
+    /// Render all active muzzle flashes in the scene.
+    /// Called after models to render with additive blending on top.
+    /// </summary>
+    public void RenderMuzzleFlashes(ICamera camera, IEnumerable<MuzzleFlash> muzzleFlashes)
+    {
+        if (_graphicsDevice == null || _basicEffect == null)
+            return;
+
+        _currentCamera = camera;
+
+        // Set up camera matrices
+        _basicEffect.View = ToXnaMatrix(camera.ViewMatrix);
+        _basicEffect.Projection = ToXnaMatrix(camera.ProjectionMatrix);
+
+        foreach (var flash in muzzleFlashes)
+        {
+            if (!flash.IsVisible)
+                continue;
+
+            // Get flash position and properties
+            var cameraTarget = camera.Position + camera.Forward;
+            var position = flash.GetWorldPosition(camera.Position, cameraTarget);
+            var rotation = flash.GetRotation();
+            var size = flash.FlashScale;
+            var color = flash.FlashColor;
+
+            // Render billboard
+            DrawBillboard(position, size, color, rotation);
+        }
+    }
+
+    /// <summary>
+    /// Render particle emitters in the scene
+    /// </summary>
+    public void RenderParticles(ICamera camera, IEnumerable<Gameplay.Components.ParticleEmitter> emitters)
+    {
+        if (_graphicsDevice == null || _basicEffect == null)
+            return;
+
+        _currentCamera = camera;
+
+        // Set up camera matrices
+        _basicEffect.View = ToXnaMatrix(camera.ViewMatrix);
+        _basicEffect.Projection = ToXnaMatrix(camera.ProjectionMatrix);
+
+        foreach (var emitter in emitters)
+        {
+            var particles = emitter.GetActiveParticles();
+            foreach (var particle in particles)
+            {
+                if (!particle.Active)
+                    continue;
+
+                // Render each particle as a billboard
+                DrawBillboard(particle.Position, particle.Size, particle.Color, 0f);
+            }
+        }
+    }
+
     /// <summary>
     /// <summary>
     /// Draw a debug primitive for visualization.
     /// Draw a debug primitive for visualization.
     /// Useful during development to see physics shapes, AI paths, etc.
     /// Useful during development to see physics shapes, AI paths, etc.
-    /// 
+    ///
     /// UNITY COMPARISON:
     /// UNITY COMPARISON:
     /// Similar to Unity's Debug.DrawLine(), Debug.DrawRay(), Gizmos.DrawSphere()
     /// Similar to Unity's Debug.DrawLine(), Debug.DrawRay(), Gizmos.DrawSphere()
     /// Unity also has OnDrawGizmos() for editor visualization.
     /// Unity also has OnDrawGizmos() for editor visualization.
@@ -582,13 +642,98 @@ public class ForwardGraphicsProvider : IGraphicsProvider
     {
     {
         return new Microsoft.Xna.Framework.Vector3(v.X, v.Y, v.Z);
         return new Microsoft.Xna.Framework.Vector3(v.X, v.Y, v.Z);
     }
     }
-    
+
     private Microsoft.Xna.Framework.Vector3 ToXnaVector3(Vector3 v)
     private Microsoft.Xna.Framework.Vector3 ToXnaVector3(Vector3 v)
     {
     {
         return new Microsoft.Xna.Framework.Vector3(v.X, v.Y, v.Z);
         return new Microsoft.Xna.Framework.Vector3(v.X, v.Y, v.Z);
     }
     }
-    
+
     #endregion
     #endregion
+
+    /// <summary>
+    /// Render a camera-facing billboard (muzzle flash, particles, etc.)
+    ///
+    /// EDUCATIONAL NOTE - BILLBOARDS:
+    ///
+    /// A billboard is a quad (rectangle) that always faces the camera.
+    /// Used for: muzzle flashes, particles, sprites in 3D, health bars, etc.
+    ///
+    /// Unity's ParticleSystem uses billboards internally.
+    /// Here we implement a simple version manually.
+    ///
+    /// How it works:
+    /// 1. Get camera's right and up vectors
+    /// 2. Create quad corners using: position + (right * x + up * y)
+    /// 3. Apply rotation around the camera's forward axis (optional)
+    /// 4. Render with additive blending for "glow" effect
+    /// </summary>
+    public void DrawBillboard(Vector3 worldPosition, float size, Vector4 color, float rotation = 0f)
+    {
+        if (_graphicsDevice == null || _basicEffect == null || _currentCamera == null)
+            return;
+
+        // Get camera basis vectors
+        var cameraForward = Vector3.Normalize(_currentCamera.Forward);
+        var worldUp = new Vector3(0, 1, 0);
+        var cameraRight = Vector3.Normalize(Vector3.Cross(worldUp, cameraForward));
+        var cameraUp = Vector3.Normalize(Vector3.Cross(cameraForward, cameraRight));
+
+        // Apply rotation around camera forward axis
+        if (rotation != 0f)
+        {
+            float cos = MathF.Cos(rotation);
+            float sin = MathF.Sin(rotation);
+            var rotatedRight = cameraRight * cos - cameraUp * sin;
+            var rotatedUp = cameraRight * sin + cameraUp * cos;
+            cameraRight = rotatedRight;
+            cameraUp = rotatedUp;
+        }
+
+        // Create simple billboard quad vertices (solid color for now)
+        var halfSize = size * 0.5f;
+        var vertices = new VertexPositionColor[4];
+
+        vertices[0].Position = ToXnaVector3(worldPosition - cameraRight * halfSize - cameraUp * halfSize);
+        vertices[1].Position = ToXnaVector3(worldPosition + cameraRight * halfSize - cameraUp * halfSize);
+        vertices[2].Position = ToXnaVector3(worldPosition - cameraRight * halfSize + cameraUp * halfSize);
+        vertices[3].Position = ToXnaVector3(worldPosition + cameraRight * halfSize + cameraUp * halfSize);
+
+        var xnaColor = new Color(color.X, color.Y, color.Z, color.W);
+        vertices[0].Color = xnaColor;
+        vertices[1].Color = xnaColor;
+        vertices[2].Color = xnaColor;
+        vertices[3].Color = xnaColor;
+
+        // Set up rendering state for additive blending (glow effect)
+        var oldBlendState = _graphicsDevice.BlendState;
+        var oldDepthStencilState = _graphicsDevice.DepthStencilState;
+
+        _graphicsDevice.BlendState = BlendState.Additive; // Colors add together (bright!)
+        _graphicsDevice.DepthStencilState = DepthStencilState.DepthRead; // Read depth but don't write
+
+        // Configure effect for unlit, vertex-colored rendering
+        _basicEffect.LightingEnabled = false;
+        _basicEffect.VertexColorEnabled = true;
+        _basicEffect.World = Microsoft.Xna.Framework.Matrix.Identity;
+
+        // Draw the billboard quad
+        foreach (var pass in _basicEffect.CurrentTechnique.Passes)
+        {
+            pass.Apply();
+            _graphicsDevice.DrawUserPrimitives(
+                PrimitiveType.TriangleStrip,
+                vertices,
+                0,
+                2 // 2 triangles = 1 quad
+            );
+        }
+
+        // Restore rendering state
+        _graphicsDevice.BlendState = oldBlendState;
+        _graphicsDevice.DepthStencilState = oldDepthStencilState;
+        _basicEffect.LightingEnabled = true;
+        _basicEffect.VertexColorEnabled = false;
+    }
 }
 }
 
 
 /// <summary>
 /// <summary>

+ 393 - 46
Shooter/Platforms/Desktop/Game.cs

@@ -28,6 +28,16 @@ public class ShooterGame : Game
     private GraphicsDeviceManager _graphics;
     private GraphicsDeviceManager _graphics;
     private SpriteBatch? _spriteBatch;
     private SpriteBatch? _spriteBatch;
     private SceneManager? _sceneManager;
     private SceneManager? _sceneManager;
+    private Gameplay.Systems.PickupSystem? _pickupSystem;
+
+    // Hit marker state
+    private bool _showHitMarker = false;
+    private float _hitMarkerTimer = 0f;
+    private const float HIT_MARKER_DURATION = 0.15f; // 150ms
+
+    // Pause state
+    private bool _isPaused = false;
+    private bool _escapeWasPressed = false;
 
 
     public ShooterGame()
     public ShooterGame()
     {
     {
@@ -62,7 +72,19 @@ public class ShooterGame : Game
         
         
         // Phase 2: Register gameplay systems
         // Phase 2: Register gameplay systems
         var projectileSystem = new Gameplay.Systems.ProjectileSystem(physicsProvider);
         var projectileSystem = new Gameplay.Systems.ProjectileSystem(physicsProvider);
-        
+
+        // Subscribe to hit events for hit markers
+        projectileSystem.OnHitEntity += (hitEntity) =>
+        {
+            TriggerHitMarker();
+        };
+
+        // Subscribe to impact events for particle effects
+        projectileSystem.OnImpact += (position, normal) =>
+        {
+            SpawnImpactParticles(position, normal);
+        };
+
         // Initialize input service with screen center for mouse locking
         // Initialize input service with screen center for mouse locking
         var screenCenter = new Microsoft.Xna.Framework.Point(
         var screenCenter = new Microsoft.Xna.Framework.Point(
             _graphics.PreferredBackBufferWidth / 2,
             _graphics.PreferredBackBufferWidth / 2,
@@ -158,7 +180,10 @@ public class ShooterGame : Game
         var rifle = Gameplay.Weapons.Gun.CreateAssaultRifle();
         var rifle = Gameplay.Weapons.Gun.CreateAssaultRifle();
         weaponController.EquipWeapon(pistol, 0);   // Slot 1 (press '1' to select)
         weaponController.EquipWeapon(pistol, 0);   // Slot 1 (press '1' to select)
         weaponController.EquipWeapon(rifle, 1);    // Slot 2 (press '2' to select)
         weaponController.EquipWeapon(rifle, 1);    // Slot 2 (press '2' to select)
-        
+
+        // Store player entity reference for later (for weapon firing events)
+        _playerEntity = playerEntity;
+
         Console.WriteLine("[Phase 2 Test Scene] Player created with FirstPersonController");
         Console.WriteLine("[Phase 2 Test Scene] Player created with FirstPersonController");
         Console.WriteLine("  Controls: WASD = Move, Mouse = Look, Space = Jump, Shift = Sprint, Escape = Release Mouse");
         Console.WriteLine("  Controls: WASD = Move, Mouse = Look, Space = Jump, Shift = Sprint, Escape = Release Mouse");
         Console.WriteLine("  Weapons: 1 = Pistol, 2 = Assault Rifle, LMB = Fire, R = Reload");
         Console.WriteLine("  Weapons: 1 = Pistol, 2 = Assault Rifle, LMB = Fire, R = Reload");
@@ -175,17 +200,20 @@ public class ShooterGame : Game
         groundRigid.BodyType = Core.Plugins.Physics.BodyType.Static;
         groundRigid.BodyType = Core.Plugins.Physics.BodyType.Static;
         groundRigid.Shape = new Core.Plugins.Physics.BoxShape(20, 0.5f, 20);
         groundRigid.Shape = new Core.Plugins.Physics.BoxShape(20, 0.5f, 20);
         scene.AddEntity(groundEntity);
         scene.AddEntity(groundEntity);
-        
-        // Create a few colored cubes (larger for easier targeting)
-    CreateCube(scene, "RedCube", new System.Numerics.Vector3(-3, 2, 0), 2.0f, Color.Red);
-    CreateCube(scene, "BlueCube", new System.Numerics.Vector3(0, 4, 0), 2.0f, Color.Blue);
-    CreateCube(scene, "YellowCube", new System.Numerics.Vector3(3, 3, 0), 2.0f, Color.Yellow);
-        
-        // Create enemy entities for AI testing (bright red, taller/skinnier to distinguish from cubes)
+
+        // Create enemy entities for AI testing (bright red, taller/skinnier)
         // Position them right next to the weapon for visibility testing
         // Position them right next to the weapon for visibility testing
         CreateEnemy(scene, playerEntity, "Enemy1", new System.Numerics.Vector3(-1, 1.5f, 2), 1.0f, 0.0f, 0.0f);
         CreateEnemy(scene, playerEntity, "Enemy1", new System.Numerics.Vector3(-1, 1.5f, 2), 1.0f, 0.0f, 0.0f);
         CreateEnemy(scene, playerEntity, "Enemy2", new System.Numerics.Vector3(1, 1.5f, 2), 1.0f, 0.0f, 0.0f);
         CreateEnemy(scene, playerEntity, "Enemy2", new System.Numerics.Vector3(1, 1.5f, 2), 1.0f, 0.0f, 0.0f);
-        
+
+        // Create pickups for testing
+        CreateHealthPickup(scene, "HealthPickup1", new System.Numerics.Vector3(-5, 1.5f, 0), 25f);
+        CreateHealthPickup(scene, "HealthPickup2", new System.Numerics.Vector3(5, 1.5f, 0), 50f);
+        CreateAmmoPickup(scene, "AmmoPickup1", new System.Numerics.Vector3(0, 1.5f, -5), 30);
+
+        // Create pickup system
+        _pickupSystem = new Gameplay.Systems.PickupSystem(playerEntity);
+
         // Initialize the scene
         // Initialize the scene
         scene.Initialize();
         scene.Initialize();
         
         
@@ -203,9 +231,9 @@ public class ShooterGame : Game
         Console.WriteLine("[Phase 2 Test Scene] Created:");
         Console.WriteLine("[Phase 2 Test Scene] Created:");
         Console.WriteLine("  - Player with FPS controller, weapons, health (100 HP), and HUD at (0, 2, 10)");
         Console.WriteLine("  - Player with FPS controller, weapons, health (100 HP), and HUD at (0, 2, 10)");
         Console.WriteLine("  - Ground plane (20x0.5x20 static box)");
         Console.WriteLine("  - Ground plane (20x0.5x20 static box)");
-        Console.WriteLine("  - 3 colored cubes with Health (50 HP each) for target practice");
         Console.WriteLine("  - 2 enemy entities with AI (20 HP, 10 damage melee attacks)");
         Console.WriteLine("  - 2 enemy entities with AI (20 HP, 10 damage melee attacks)");
-        Console.WriteLine("  - Controls: WASD=Move, Mouse=Look, Space=Jump, Shift=Sprint, Esc=Menu");
+        Console.WriteLine("  - 3 pickups (2 health, 1 ammo) with auto-collect");
+        Console.WriteLine("  - Controls: WASD=Move, Mouse=Look, Space=Jump, Shift=Sprint, Esc=Pause");
         Console.WriteLine("  - Weapons: 1=Pistol, 2=Assault Rifle, LMB=Fire, R=Reload");
         Console.WriteLine("  - Weapons: 1=Pistol, 2=Assault Rifle, LMB=Fire, R=Reload");
     }
     }
     
     
@@ -265,29 +293,111 @@ public class ShooterGame : Game
         
         
         // Add Health component
         // Add Health component
         var health = entity.AddComponent<Gameplay.Components.Health>();
         var health = entity.AddComponent<Gameplay.Components.Health>();
-        health.MaxHealth = 20f;
+        health.MaxHealth = 100f; // Unity HoverBot health
         health.DestroyOnDeath = false; // Keep visible after death for testing
         health.DestroyOnDeath = false; // Keep visible after death for testing
-        
-        // Subscribe to death event
-        health.OnDeath += (damageInfo) =>
+
+        // Create weapon for enemy (Eye Lazers)
+        var eyeLazers = Gameplay.Weapons.Gun.CreateEnemyEyeLazers();
+
+        // Hook up weapon firing to projectile system
+        if (eyeLazers is Gameplay.Weapons.Gun gun)
         {
         {
-            Console.WriteLine($"[Test Scene] {name} was killed by {damageInfo.Attacker?.Name ?? "unknown"}!");
-        };
-        
+            gun.OnHitscanFired += (origin, direction, damage) =>
+            {
+                var projectileSystem = ServiceLocator.Get<Gameplay.Systems.ProjectileSystem>();
+                projectileSystem?.FireHitscan(origin, direction, damage, 100f, entity);
+            };
+        }
+
+        // Add EnemyController for weapon management
+        var enemyController = entity.AddComponent<Gameplay.Components.EnemyController>();
+        enemyController.SetWeapon(eyeLazers);
+
         // Add EnemyAI component
         // Add EnemyAI component
         var enemyAI = entity.AddComponent<Gameplay.Components.EnemyAI>();
         var enemyAI = entity.AddComponent<Gameplay.Components.EnemyAI>();
         enemyAI.Target = player; // Set player as target
         enemyAI.Target = player; // Set player as target
-        enemyAI.DetectionRange = 15f;
-        enemyAI.AttackRange = 2.5f;
+        enemyAI.DetectionRange = 20f; // Unity default
+        enemyAI.AttackRange = 10f; // Unity default
+        enemyAI.AttackStopDistanceRatio = 0.5f; // Stop at 50% of attack range
         enemyAI.MoveSpeed = 2.5f;
         enemyAI.MoveSpeed = 2.5f;
-        enemyAI.AttackDamage = 10f;
-        enemyAI.AttackCooldown = 2.0f;
+
+        // Subscribe to death event (after enemyAI is created so we can reference it)
+        health.OnDeath += (damageInfo) =>
+        {
+            Console.WriteLine($"[Test Scene] {name} was killed by {damageInfo.Attacker?.Name ?? "unknown"}!");
+
+            // Death effect: change color to dark gray (corpse color)
+            renderer.Material.Color = new System.Numerics.Vector4(0.3f, 0.3f, 0.3f, 1.0f);
+
+            // Death effect: rotate/fall down (tilt forward)
+            transform.LocalRotation = System.Numerics.Quaternion.CreateFromAxisAngle(
+                new System.Numerics.Vector3(1, 0, 0), // Rotate around X-axis
+                (float)(System.Math.PI / 2) // 90 degrees forward
+            );
+
+            // Death effect: lower to ground (half height)
+            var currentPos = transform.Position;
+            transform.Position = new System.Numerics.Vector3(currentPos.X, currentPos.Y - 0.9f, currentPos.Z);
+
+            // Death effect: disable rigidbody collisions (corpse shouldn't block movement)
+            rigidbody.BodyType = Core.Plugins.Physics.BodyType.Static;
+        };
         
         
         scene.AddEntity(entity);
         scene.AddEntity(entity);
         
         
         Console.WriteLine($"[Test Scene] Created enemy '{name}' at {position} targeting player");
         Console.WriteLine($"[Test Scene] Created enemy '{name}' at {position} targeting player");
     }
     }
 
 
+    /// <summary>
+    /// Helper to create a health pickup
+    /// </summary>
+    private void CreateHealthPickup(Core.Scenes.Scene scene, string name, System.Numerics.Vector3 position, float healAmount)
+    {
+        var entity = new Core.Entities.Entity(name);
+        entity.Tag = "Pickup";
+
+        var transform = entity.AddComponent<Core.Components.Transform3D>();
+        transform.Position = position;
+        transform.LocalScale = new System.Numerics.Vector3(0.5f, 0.5f, 0.5f);
+
+        var renderer = entity.AddComponent<Core.Components.MeshRenderer>();
+        renderer.SetCube(1.0f, new System.Numerics.Vector4(0.0f, 1.0f, 0.0f, 1.0f)); // Green for health
+
+        // No physics collider needed - proximity detection handled by PickupSystem
+
+        var pickup = entity.AddComponent<Gameplay.Components.Pickup>();
+        pickup.Type = Gameplay.Components.PickupType.Health;
+        pickup.Value = healAmount;
+        pickup.PickupRadius = 2.0f;
+
+        scene.AddEntity(entity);
+        Console.WriteLine($"[Test Scene] Created health pickup '{name}' (+{healAmount} HP) at {position}");
+    }
+
+    /// <summary>
+    /// Helper to create an ammo pickup
+    /// </summary>
+    private void CreateAmmoPickup(Core.Scenes.Scene scene, string name, System.Numerics.Vector3 position, int ammoAmount)
+    {
+        var entity = new Core.Entities.Entity(name);
+        entity.Tag = "Pickup";
+
+        var transform = entity.AddComponent<Core.Components.Transform3D>();
+        transform.Position = position;
+        transform.LocalScale = new System.Numerics.Vector3(0.5f, 0.5f, 0.5f);
+
+        var renderer = entity.AddComponent<Core.Components.MeshRenderer>();
+        renderer.SetCube(1.0f, new System.Numerics.Vector4(1.0f, 0.8f, 0.0f, 1.0f)); // Yellow/gold for ammo
+
+        var pickup = entity.AddComponent<Gameplay.Components.Pickup>();
+        pickup.Type = Gameplay.Components.PickupType.Ammo;
+        pickup.Value = ammoAmount;
+        pickup.PickupRadius = 2.0f;
+
+        scene.AddEntity(entity);
+        Console.WriteLine($"[Test Scene] Created ammo pickup '{name}' (+{ammoAmount} rounds) at {position}");
+    }
+
     /// <summary>
     /// <summary>
     /// Load content (textures, models, sounds).
     /// Load content (textures, models, sounds).
     /// Similar to Unity's resource loading but more manual.
     /// Similar to Unity's resource loading but more manual.
@@ -314,6 +424,22 @@ public class ShooterGame : Game
         // Load SpriteFont for HUD
         // Load SpriteFont for HUD
         SpriteFont hudFont = Content.Load<SpriteFont>("font");
         SpriteFont hudFont = Content.Load<SpriteFont>("font");
 
 
+        // Initialize AudioService with ContentManager
+        var audioService = ServiceLocator.Get<IAudioService>() as AudioService;
+        if (audioService != null)
+        {
+            audioService.Initialize(Content);
+
+            // Pre-load commonly used sounds to avoid first-shot delay
+            audioService.LoadSound("Audio/SFX/Weapons/Blaster_Shot");
+            audioService.LoadSound("Audio/SFX/Weapons/Shotgun_Shot");
+            audioService.LoadSound("Audio/SFX/Pickups/Pickup_Health");
+            audioService.LoadSound("Audio/SFX/Pickups/Pickup_Weapon_Small");
+            audioService.LoadSound("Audio/SFX/Player/Damage_Tick");
+
+            Console.WriteLine("[Game] AudioService initialized with ContentManager");
+        }
+
         // Create Phase 2 test scene AFTER all services are initialized
         // Create Phase 2 test scene AFTER all services are initialized
         CreateTestScene();
         CreateTestScene();
 
 
@@ -369,6 +495,7 @@ public class ShooterGame : Game
     private Core.Entities.Entity? _primaryWeaponViewModel;
     private Core.Entities.Entity? _primaryWeaponViewModel;
     private Core.Entities.Entity? _secondaryWeaponViewModel;
     private Core.Entities.Entity? _secondaryWeaponViewModel;
     private int _lastWeaponIndex = -1;
     private int _lastWeaponIndex = -1;
+    private Core.Entities.Entity? _playerEntity;
 
 
     private void LoadWeaponModel(Core.Entities.Entity playerEntity)
     private void LoadWeaponModel(Core.Entities.Entity playerEntity)
     {
     {
@@ -407,7 +534,10 @@ public class ShooterGame : Game
                 // Start with primary weapon visible
                 // Start with primary weapon visible
                 SetActiveWeaponViewModel(0);
                 SetActiveWeaponViewModel(0);
 
 
-                Console.WriteLine("[LoadContent] Loaded both weapon viewmodels with camera tracking");
+                // Hook up muzzle flash to weapon firing
+                HookupMuzzleFlashEvents(playerEntity);
+
+                Console.WriteLine("[LoadContent] Loaded both weapon viewmodels with camera tracking and muzzle flash");
             }
             }
         }
         }
         catch (Exception ex)
         catch (Exception ex)
@@ -417,6 +547,45 @@ public class ShooterGame : Game
         }
         }
     }
     }
 
 
+    /// <summary>
+    /// Hook up weapon firing events to trigger muzzle flash on viewmodels
+    /// </summary>
+    private void HookupMuzzleFlashEvents(Core.Entities.Entity playerEntity)
+    {
+        var weaponController = playerEntity.GetComponent<Gameplay.Components.WeaponController>();
+        if (weaponController == null) return;
+
+        // Get the weapons and hook up their firing events
+        var pistol = weaponController.GetWeapon(0);
+        var rifle = weaponController.GetWeapon(1);
+
+        if (pistol is Gameplay.Weapons.Gun pistolGun)
+        {
+            pistolGun.OnHitscanFired += (origin, direction, damage) =>
+            {
+                // Trigger muzzle flash on primary viewmodel
+                var muzzleFlash = _primaryWeaponViewModel?.GetComponent<Gameplay.Components.MuzzleFlash>();
+                muzzleFlash?.Flash();
+
+                // Add muzzle flash particles
+                SpawnMuzzleFlashParticles(origin, direction);
+            };
+        }
+
+        if (rifle is Gameplay.Weapons.Gun rifleGun)
+        {
+            rifleGun.OnHitscanFired += (origin, direction, damage) =>
+            {
+                // Trigger muzzle flash on secondary viewmodel
+                var muzzleFlash = _secondaryWeaponViewModel?.GetComponent<Gameplay.Components.MuzzleFlash>();
+                muzzleFlash?.Flash();
+
+                // Add muzzle flash particles
+                SpawnMuzzleFlashParticles(origin, direction);
+            };
+        }
+    }
+
     /// <summary>
     /// <summary>
     /// Helper to create a weapon viewmodel entity
     /// Helper to create a weapon viewmodel entity
     /// </summary>
     /// </summary>
@@ -442,6 +611,14 @@ public class ShooterGame : Game
         weaponRenderer.Scale = scale;
         weaponRenderer.Scale = scale;
         weaponRenderer.TintColor = new System.Numerics.Vector4(1, 1, 1, 1);
         weaponRenderer.TintColor = new System.Numerics.Vector4(1, 1, 1, 1);
 
 
+        // Add muzzle flash effect
+        var muzzleFlash = weaponEntity.AddComponent<Gameplay.Components.MuzzleFlash>();
+        if (muzzleFlash != null)
+        {
+            muzzleFlash.BarrelOffset = new System.Numerics.Vector3(0f, -0.2f, -1.2f);
+            muzzleFlash.FlashScale = 0.2f;
+        }
+
         return weaponEntity;
         return weaponEntity;
     }
     }
 
 
@@ -560,26 +737,57 @@ public class ShooterGame : Game
     /// <param name="gameTime">Provides timing information</param>
     /// <param name="gameTime">Provides timing information</param>
     protected override void Update(GameTime gameTime)
     protected override void Update(GameTime gameTime)
     {
     {
-        // Exit on Escape (temporary - will be replaced with proper menu)
-        if (Keyboard.GetState().IsKeyDown(Keys.Escape))
-            Exit();
+        // Handle pause toggle with Escape key
+        bool escapePressed = Keyboard.GetState().IsKeyDown(Keys.Escape);
+        if (escapePressed && !_escapeWasPressed)
+        {
+            _isPaused = !_isPaused;
+            IsMouseVisible = _isPaused; // Show cursor when paused
+        }
+        _escapeWasPressed = escapePressed;
 
 
-        // Update core services
+        // Update core services (always update input/time even when paused)
         var timeService = ServiceLocator.Get<ITimeService>();
         var timeService = ServiceLocator.Get<ITimeService>();
         var inputService = ServiceLocator.Get<IInputService>();
         var inputService = ServiceLocator.Get<IInputService>();
         var physicsProvider = ServiceLocator.Get<IPhysicsProvider>();
         var physicsProvider = ServiceLocator.Get<IPhysicsProvider>();
-        
+
         timeService.Update(gameTime);
         timeService.Update(gameTime);
         inputService.Update();
         inputService.Update();
-        
+
+        // Skip game updates when paused
+        if (_isPaused)
+        {
+            base.Update(gameTime);
+            return;
+        }
+
         // Update active scene
         // Update active scene
         var coreGameTime = new Core.Components.GameTime(
         var coreGameTime = new Core.Components.GameTime(
             gameTime.TotalGameTime,
             gameTime.TotalGameTime,
             gameTime.ElapsedGameTime
             gameTime.ElapsedGameTime
         );
         );
-        
+
         _sceneManager?.Update(coreGameTime);
         _sceneManager?.Update(coreGameTime);
 
 
+        // Update pickup system
+        if (_pickupSystem != null && _sceneManager?.ActiveScene != null)
+        {
+            var pickups = _sceneManager.ActiveScene.Entities
+                .SelectMany(e => e.GetComponents<Gameplay.Components.Pickup>())
+                .Where(p => !p.IsCollected);
+            _pickupSystem.Update(coreGameTime, pickups);
+        }
+
+        // Update hit marker timer
+        if (_showHitMarker)
+        {
+            _hitMarkerTimer -= (float)gameTime.ElapsedGameTime.TotalSeconds;
+            if (_hitMarkerTimer <= 0f)
+            {
+                _showHitMarker = false;
+            }
+        }
+
         // Check for weapon switching (update viewmodel visibility)
         // Check for weapon switching (update viewmodel visibility)
         var playerEntity = _sceneManager?.ActiveScene?.FindEntitiesByTag("Player").FirstOrDefault();
         var playerEntity = _sceneManager?.ActiveScene?.FindEntitiesByTag("Player").FirstOrDefault();
         if (playerEntity != null)
         if (playerEntity != null)
@@ -678,6 +886,20 @@ public class ShooterGame : Game
             if (graphics is ForwardGraphicsProvider forwardGraphics)
             if (graphics is ForwardGraphicsProvider forwardGraphics)
             {
             {
                 forwardGraphics.RenderModels(camera, modelRenderers);
                 forwardGraphics.RenderModels(camera, modelRenderers);
+
+                // Render muzzle flashes (after models, with additive blending)
+                if (_sceneManager?.ActiveScene != null)
+                {
+                    var muzzleFlashes = _sceneManager.ActiveScene.Entities
+                        .SelectMany(e => e.GetComponents<Gameplay.Components.MuzzleFlash>())
+                        .Where(m => m.IsVisible);
+                    forwardGraphics.RenderMuzzleFlashes(camera, muzzleFlashes);
+
+                    // Render particles (after muzzle flashes, also additive)
+                    var particleEmitters = _sceneManager.ActiveScene.Entities
+                        .SelectMany(e => e.GetComponents<Gameplay.Components.ParticleEmitter>());
+                    forwardGraphics.RenderParticles(camera, particleEmitters);
+                }
             }
             }
         }
         }
         else
         else
@@ -689,8 +911,17 @@ public class ShooterGame : Game
         graphics.EndFrame();
         graphics.EndFrame();
         
         
         // Draw crosshair (simple 2D overlay)
         // Draw crosshair (simple 2D overlay)
-        DrawCrosshair();
-        
+        if (!_isPaused)
+        {
+            DrawCrosshair();
+        }
+
+        // Draw pause menu if paused
+        if (_isPaused)
+        {
+            DrawPauseMenu();
+        }
+
         // Draw scene entities (UI/debug overlay - Phase 5)
         // Draw scene entities (UI/debug overlay - Phase 5)
         var coreGameTime = new Core.Components.GameTime(
         var coreGameTime = new Core.Components.GameTime(
             gameTime.TotalGameTime,
             gameTime.TotalGameTime,
@@ -706,38 +937,154 @@ public class ShooterGame : Game
     /// Draw a simple crosshair in the center of the screen.
     /// Draw a simple crosshair in the center of the screen.
     /// This helps with aiming when the mouse cursor is hidden.
     /// This helps with aiming when the mouse cursor is hidden.
     /// </summary>
     /// </summary>
+    /// <summary>
+    /// Trigger hit marker feedback (call when player hits an enemy)
+    /// </summary>
+    public void TriggerHitMarker()
+    {
+        _showHitMarker = true;
+        _hitMarkerTimer = HIT_MARKER_DURATION;
+    }
+
+    /// <summary>
+    /// Spawn impact particles when bullets hit surfaces
+    /// </summary>
+    private void SpawnImpactParticles(System.Numerics.Vector3 position, System.Numerics.Vector3 normal)
+    {
+        if (_sceneManager?.ActiveScene == null)
+            return;
+
+        // Create a new entity for the particle effect
+        var particleEntity = new Core.Entities.Entity("ImpactParticles");
+
+        var transform = particleEntity.AddComponent<Core.Components.Transform3D>();
+        transform.Position = position;
+
+        // Add particle emitter
+        var emitter = particleEntity.AddComponent<Gameplay.Components.ParticleEmitter>();
+        emitter.EmissionPosition = position;
+        emitter.StartColor = new System.Numerics.Vector4(1.0f, 0.8f, 0.3f, 1.0f); // Orange/yellow
+        emitter.EndColor = new System.Numerics.Vector4(0.3f, 0.3f, 0.3f, 0.0f); // Fade to gray
+        emitter.StartSize = 0.15f;
+        emitter.EndSize = 0.05f;
+        emitter.Lifetime = 0.3f;
+        emitter.EmissionSpeed = 3f;
+        emitter.Gravity = -5f;
+        emitter.OneShot = true;
+
+        // Emit particles in direction of surface normal
+        emitter.Emit(15, position, normal);
+
+        // Add to scene
+        _sceneManager.ActiveScene.AddEntity(particleEntity);
+    }
+
+    /// <summary>
+    /// Spawn muzzle flash particles when weapons fire
+    /// </summary>
+    private void SpawnMuzzleFlashParticles(System.Numerics.Vector3 position, System.Numerics.Vector3 direction)
+    {
+        if (_sceneManager?.ActiveScene == null)
+            return;
+
+        // Create a new entity for the particle effect
+        var particleEntity = new Core.Entities.Entity("MuzzleFlashParticles");
+
+        var transform = particleEntity.AddComponent<Core.Components.Transform3D>();
+        transform.Position = position;
+
+        // Add particle emitter
+        var emitter = particleEntity.AddComponent<Gameplay.Components.ParticleEmitter>();
+        emitter.EmissionPosition = position;
+        emitter.StartColor = new System.Numerics.Vector4(1.0f, 0.9f, 0.5f, 1.0f); // Bright yellow/white
+        emitter.EndColor = new System.Numerics.Vector4(1.0f, 0.3f, 0.0f, 0.0f); // Fade to orange
+        emitter.StartSize = 0.2f;
+        emitter.EndSize = 0.02f;
+        emitter.Lifetime = 0.15f; // Short-lived flash
+        emitter.EmissionSpeed = 8f; // Fast particles
+        emitter.Gravity = 0f; // No gravity for muzzle flash
+        emitter.OneShot = true;
+
+        // Emit particles forward in firing direction (cone spread)
+        emitter.Emit(20, position, direction);
+
+        // Add to scene
+        _sceneManager.ActiveScene.AddEntity(particleEntity);
+    }
+
     private void DrawCrosshair()
     private void DrawCrosshair()
     {
     {
         if (_spriteBatch == null) return;
         if (_spriteBatch == null) return;
 
 
         _spriteBatch.Begin();
         _spriteBatch.Begin();
-        
+
         // Get screen center
         // Get screen center
         int centerX = _graphics.PreferredBackBufferWidth / 2;
         int centerX = _graphics.PreferredBackBufferWidth / 2;
         int centerY = _graphics.PreferredBackBufferHeight / 2;
         int centerY = _graphics.PreferredBackBufferHeight / 2;
-        
-        // Crosshair size
+
+        // Crosshair size and color (changes when hitting)
         int crosshairSize = 10;
         int crosshairSize = 10;
         int thickness = 2;
         int thickness = 2;
         int gap = 3;
         int gap = 3;
-        
+        Color crosshairColor = Color.White;
+
+        // Hit marker effect: red color and larger size
+        if (_showHitMarker)
+        {
+            float progress = _hitMarkerTimer / HIT_MARKER_DURATION;
+            crosshairColor = Color.Lerp(Color.White, Color.OrangeRed, progress);
+            crosshairSize = (int)(10 + 4 * progress); // Grows from 10 to 14
+            gap = (int)(3 + 2 * progress); // Gap grows too
+        }
+
         // Create a 1x1 white pixel texture for drawing lines
         // Create a 1x1 white pixel texture for drawing lines
         Texture2D pixel = new Texture2D(GraphicsDevice, 1, 1);
         Texture2D pixel = new Texture2D(GraphicsDevice, 1, 1);
         pixel.SetData(new[] { Color.White });
         pixel.SetData(new[] { Color.White });
-        
+
         // Draw horizontal line (left and right from center)
         // Draw horizontal line (left and right from center)
-        _spriteBatch.Draw(pixel, new Rectangle(centerX - crosshairSize - gap, centerY - thickness / 2, crosshairSize, thickness), Color.White);
-        _spriteBatch.Draw(pixel, new Rectangle(centerX + gap, centerY - thickness / 2, crosshairSize, thickness), Color.White);
-        
+        _spriteBatch.Draw(pixel, new Rectangle(centerX - crosshairSize - gap, centerY - thickness / 2, crosshairSize, thickness), crosshairColor);
+        _spriteBatch.Draw(pixel, new Rectangle(centerX + gap, centerY - thickness / 2, crosshairSize, thickness), crosshairColor);
+
         // Draw vertical line (top and bottom from center)
         // Draw vertical line (top and bottom from center)
-        _spriteBatch.Draw(pixel, new Rectangle(centerX - thickness / 2, centerY - crosshairSize - gap, thickness, crosshairSize), Color.White);
-        _spriteBatch.Draw(pixel, new Rectangle(centerX - thickness / 2, centerY + gap, thickness, crosshairSize), Color.White);
-        
+        _spriteBatch.Draw(pixel, new Rectangle(centerX - thickness / 2, centerY - crosshairSize - gap, thickness, crosshairSize), crosshairColor);
+        _spriteBatch.Draw(pixel, new Rectangle(centerX - thickness / 2, centerY + gap, thickness, crosshairSize), crosshairColor);
+
         _spriteBatch.End();
         _spriteBatch.End();
-        
+
         pixel.Dispose();
         pixel.Dispose();
     }
     }
-    
+
+    /// <summary>
+    /// Draw the pause menu overlay
+    /// </summary>
+    private void DrawPauseMenu()
+    {
+        if (_spriteBatch == null)
+            return;
+
+        _spriteBatch.Begin();
+
+        int screenWidth = GraphicsDevice.Viewport.Width;
+        int screenHeight = GraphicsDevice.Viewport.Height;
+
+        // Draw semi-transparent black overlay
+        var overlayTexture = new Texture2D(GraphicsDevice, 1, 1);
+        overlayTexture.SetData(new[] { Color.Black });
+        _spriteBatch.Draw(
+            overlayTexture,
+            new Rectangle(0, 0, screenWidth, screenHeight),
+            Color.Black * 0.7f // 70% opacity
+        );
+
+        // Note: For a full pause menu with text, we'd need to load a SpriteFont
+        // For now, we just show the dark overlay as visual feedback that the game is paused
+        // The user can see the mouse cursor appears when paused
+
+        _spriteBatch.End();
+
+        overlayTexture.Dispose();
+    }
+
     /// <summary>
     /// <summary>
     /// Cleanup when game exits.
     /// Cleanup when game exits.
     /// Similar to Unity's OnApplicationQuit() or OnDestroy().
     /// Similar to Unity's OnApplicationQuit() or OnDestroy().