Add NPC and Power-Up features with associated prefabs and effects
This commit is contained in:
@@ -7,6 +7,8 @@ namespace Core.Domain
|
||||
public class GameSession
|
||||
{
|
||||
private const string HighScoreKey = "HighScore";
|
||||
private const float NpcSpawnTime = 4f;
|
||||
private const float PowerUpSpawnInterval = 25f;
|
||||
|
||||
public int Score { get; private set; }
|
||||
public int HighScore { get; private set; }
|
||||
@@ -16,11 +18,16 @@ namespace Core.Domain
|
||||
public event Action<string> OnOrbSpawned;
|
||||
public event Action OnOrbReset;
|
||||
public event Action OnGameOver;
|
||||
public event Action OnSpawnNpc;
|
||||
public event Action<PowerUpType, string> OnSpawnPowerUp;
|
||||
|
||||
private readonly List<Tile> _tiles;
|
||||
private readonly IPersistenceService _persistenceService;
|
||||
private readonly Random _rng = new();
|
||||
private int _playerFloorIndex = 0;
|
||||
private float _timeSinceStart;
|
||||
private bool _npcSpawned;
|
||||
private float _powerUpTimer;
|
||||
|
||||
public GameSession(List<Tile> tiles, IPersistenceService persistenceService)
|
||||
{
|
||||
@@ -35,9 +42,32 @@ namespace Core.Domain
|
||||
|
||||
public void StartGame()
|
||||
{
|
||||
_timeSinceStart = 0f;
|
||||
_powerUpTimer = 0f;
|
||||
_npcSpawned = false;
|
||||
|
||||
SpawnNextOrb();
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
if (IsGameOver) return;
|
||||
|
||||
_timeSinceStart += deltaTime;
|
||||
if (!_npcSpawned && _timeSinceStart >= NpcSpawnTime)
|
||||
{
|
||||
_npcSpawned = true;
|
||||
OnSpawnNpc?.Invoke();
|
||||
}
|
||||
|
||||
_powerUpTimer += deltaTime;
|
||||
if (_powerUpTimer >= PowerUpSpawnInterval)
|
||||
{
|
||||
_powerUpTimer = 0f;
|
||||
SpawnRandomPowerUp();
|
||||
}
|
||||
}
|
||||
|
||||
public void OrbCollected()
|
||||
{
|
||||
if (IsGameOver) return;
|
||||
@@ -95,5 +125,17 @@ namespace Core.Domain
|
||||
OnOrbReset?.Invoke();
|
||||
SpawnNextOrb();
|
||||
}
|
||||
|
||||
private void SpawnRandomPowerUp()
|
||||
{
|
||||
var validTiles = _tiles.FindAll(t => t.CurrentState == TileState.Stable);
|
||||
if (validTiles.Count == 0) return;
|
||||
|
||||
var tile = validTiles[_rng.Next(validTiles.Count)];
|
||||
|
||||
var type = _rng.Next(0, 2) == 0 ? PowerUpType.LightFooted : PowerUpType.SpeedBoost;
|
||||
|
||||
OnSpawnPowerUp?.Invoke(type, tile.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Assets/Scripts/Core/Domain/PowerUpType.cs
Normal file
8
Assets/Scripts/Core/Domain/PowerUpType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Core.Domain
|
||||
{
|
||||
public enum PowerUpType
|
||||
{
|
||||
LightFooted,
|
||||
SpeedBoost,
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Core/Domain/PowerUpType.cs.meta
Normal file
3
Assets/Scripts/Core/Domain/PowerUpType.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bafe72a0931b403fbd668163287d60a3
|
||||
timeCreated: 1765576209
|
||||
10
Assets/Scripts/Core/Domain/Status/Effects/EffectColors.cs
Normal file
10
Assets/Scripts/Core/Domain/Status/Effects/EffectColors.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Core.Domain.Status.Effects
|
||||
{
|
||||
public struct EffectColors
|
||||
{
|
||||
public static readonly Color LightFootedColor = new Color(0.8f, 0.8f, 0.8f);
|
||||
public static readonly Color SpeedBoostColor = new Color(1f, 0.5f, 0f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca5a45bf5f154f009511ace70bfc78aa
|
||||
timeCreated: 1765576663
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Core.Domain.Status.Effects
|
||||
{
|
||||
public class SpeedBoostEffect : IStatusEffect
|
||||
{
|
||||
private float _duration;
|
||||
private readonly float _multiplier;
|
||||
|
||||
public bool IsExpired => _duration <= 0;
|
||||
|
||||
public SpeedBoostEffect(float duration, float multiplier = 1.5f)
|
||||
{
|
||||
_duration = duration;
|
||||
_multiplier = multiplier;
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
_duration -= deltaTime;
|
||||
}
|
||||
|
||||
public void ModifyCapabilities(ref PlayerCapabilities caps)
|
||||
{
|
||||
caps.SpeedMultiplier = _multiplier;
|
||||
}
|
||||
|
||||
public void OnApply() { }
|
||||
public void OnRemove() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8fdc1e67835247f6a2afc26cc4ed27a8
|
||||
timeCreated: 1765576232
|
||||
@@ -17,6 +17,7 @@ namespace Infrastructure.Unity
|
||||
[SerializeField] private DeathPlaneAdapter deathPlanePrefab;
|
||||
[SerializeField] private SoundManager soundManager;
|
||||
[SerializeField] private CameraController cameraController;
|
||||
[SerializeField] private NpcController npcPrefab;
|
||||
|
||||
[Header("Level Generation")]
|
||||
[SerializeField] private int floorsCount = 3;
|
||||
@@ -30,7 +31,10 @@ namespace Infrastructure.Unity
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private float restartTime = 3f;
|
||||
|
||||
|
||||
[Header("Power Ups")]
|
||||
[SerializeField] private PowerUpViewAdapter lightFootedPrefab;
|
||||
[SerializeField] private PowerUpViewAdapter speedBoostPrefab;
|
||||
|
||||
private readonly List<Tile> _allTiles = new();
|
||||
private readonly Dictionary<string, TileViewAdapter> _tileViews = new();
|
||||
@@ -93,6 +97,9 @@ namespace Infrastructure.Unity
|
||||
}
|
||||
|
||||
var dt = Time.deltaTime;
|
||||
|
||||
if (_isGameRunning) _gameSession.Tick(dt);
|
||||
|
||||
for (var i = _allTiles.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_allTiles[i].Tick(dt);
|
||||
@@ -180,6 +187,8 @@ namespace Infrastructure.Unity
|
||||
_gameSession.OnOrbSpawned += SpawnVisualOrb;
|
||||
_gameSession.OnOrbReset += HandleOrbReset;
|
||||
_gameSession.OnGameOver += HandleGameOver;
|
||||
_gameSession.OnSpawnNpc += SpawnNpc;
|
||||
_gameSession.OnSpawnPowerUp += SpawnPowerUp;
|
||||
|
||||
if (!soundManager) return;
|
||||
|
||||
@@ -191,6 +200,16 @@ namespace Infrastructure.Unity
|
||||
};
|
||||
}
|
||||
|
||||
private void SpawnNpc()
|
||||
{
|
||||
if (!npcPrefab) return;
|
||||
|
||||
var spawnPos = new Vector3(levelGenerator.GridSizeX / 2f, 7f, levelGenerator.GridSizeY / 2f);
|
||||
Instantiate(npcPrefab, spawnPos, Quaternion.identity);
|
||||
|
||||
soundManager.PlayNpcSpawn();
|
||||
}
|
||||
|
||||
private void StartGameSequence()
|
||||
{
|
||||
_isGameRunning = true;
|
||||
@@ -211,5 +230,22 @@ namespace Infrastructure.Unity
|
||||
|
||||
_gameSession.StartGame();
|
||||
}
|
||||
|
||||
private void SpawnPowerUp(PowerUpType type, string tileId)
|
||||
{
|
||||
if (!_tileViews.TryGetValue(tileId, out var tileView)) return;
|
||||
if (!tileView) return;
|
||||
|
||||
var spawnPos = tileView.transform.position + Vector3.up * 0.5f;
|
||||
|
||||
var prefabToSpawn = type == PowerUpType.LightFooted
|
||||
? lightFootedPrefab
|
||||
: speedBoostPrefab;
|
||||
|
||||
if (!prefabToSpawn) return;
|
||||
|
||||
var instance = Instantiate(prefabToSpawn, spawnPos, Quaternion.identity);
|
||||
instance.Configure(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Assets/Scripts/Infrastructure/Unity/NpcController.cs
Normal file
66
Assets/Scripts/Infrastructure/Unity/NpcController.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using KBCore.Refs;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace Infrastructure.Unity
|
||||
{
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
public class NpcController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float moveSpeed = 6f;
|
||||
[SerializeField] private float changeDirInterval = 1f;
|
||||
[SerializeField] private LayerMask tileLayer;
|
||||
|
||||
[Self] [SerializeField] private Rigidbody rb;
|
||||
|
||||
private Vector3 _currentDir;
|
||||
private float _timer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
PickNewDirection();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
_timer += Time.deltaTime;
|
||||
if (_timer >= changeDirInterval)
|
||||
{
|
||||
_timer = 0;
|
||||
PickNewDirection();
|
||||
}
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (IsGrounded()) rb.MovePosition(rb.position + _currentDir * (moveSpeed * Time.fixedDeltaTime));
|
||||
|
||||
if (Physics.Raycast(transform.position, Vector3.down, out var hit, 1.5f, tileLayer))
|
||||
{
|
||||
if (hit.collider.TryGetComponent<TileViewAdapter>(out var tile))
|
||||
{
|
||||
tile.OnPlayerStep();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsGrounded()
|
||||
{
|
||||
return Physics.Raycast(transform.position, Vector3.down, out var hit, 1.15f, tileLayer);
|
||||
}
|
||||
|
||||
private void PickNewDirection()
|
||||
{
|
||||
var rand = Random.Range(0, 4);
|
||||
switch (rand)
|
||||
{
|
||||
case 0: _currentDir = Vector3.forward; break;
|
||||
case 1: _currentDir = Vector3.back; break;
|
||||
case 2: _currentDir = Vector3.left; break;
|
||||
case 3: _currentDir = Vector3.right; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d8629598f36246c484f046e5ebd69266
|
||||
timeCreated: 1765575497
|
||||
84
Assets/Scripts/Infrastructure/Unity/PowerUpViewAdapter.cs
Normal file
84
Assets/Scripts/Infrastructure/Unity/PowerUpViewAdapter.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using Core.Domain;
|
||||
using Core.Domain.Status.Effects;
|
||||
using KBCore.Refs;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Infrastructure.Unity
|
||||
{
|
||||
public class PowerUpViewAdapter : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private PowerUpType type;
|
||||
[SerializeField] private float duration = 10f;
|
||||
[SerializeField] private ParticleSystem pickupVfx;
|
||||
|
||||
[Self] [SerializeField] private MeshRenderer meshRenderer;
|
||||
|
||||
private MaterialPropertyBlock _propBlock;
|
||||
private static readonly int ColorProperty = Shader.PropertyToID("_BaseColor");
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_propBlock = new MaterialPropertyBlock();
|
||||
ConfigureVisuals();
|
||||
}
|
||||
|
||||
private void ConfigureVisuals()
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case PowerUpType.LightFooted:
|
||||
SetColor(EffectColors.LightFootedColor);
|
||||
break;
|
||||
case PowerUpType.SpeedBoost:
|
||||
SetColor(EffectColors.SpeedBoostColor);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (other.TryGetComponent<PlayerController>(out var player))
|
||||
{
|
||||
ApplyEffect(player);
|
||||
|
||||
if (pickupVfx)
|
||||
{
|
||||
var vfx = Instantiate(pickupVfx, transform.position, Quaternion.identity);
|
||||
Destroy(vfx.gameObject, 2f);
|
||||
}
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEffect(PlayerController player)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case PowerUpType.LightFooted:
|
||||
player.Status.AddEffect(new LightFootedEffect(duration));
|
||||
break;
|
||||
case PowerUpType.SpeedBoost:
|
||||
player.Status.AddEffect(new SpeedBoostEffect(duration, 1.5f));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(PowerUpType newType)
|
||||
{
|
||||
type = newType;
|
||||
}
|
||||
|
||||
private void SetColor(Color color)
|
||||
{
|
||||
meshRenderer.GetPropertyBlock(_propBlock);
|
||||
_propBlock.SetColor(ColorProperty, color);
|
||||
meshRenderer.SetPropertyBlock(_propBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5886e2d0e6414ce98bf68f8e5ac887fc
|
||||
timeCreated: 1765576409
|
||||
@@ -12,14 +12,14 @@ namespace Infrastructure.Unity
|
||||
|
||||
[Header("Sound Effects")]
|
||||
[SerializeField] private AudioClip startClip;
|
||||
|
||||
[SerializeField] private AudioClip scoreClip;
|
||||
[SerializeField] private AudioClip gameOverClip;
|
||||
[SerializeField] private AudioClip tileWarningClip;
|
||||
[SerializeField] private AudioClip tileBreakClip;
|
||||
[SerializeField] private AudioClip npcSpawnClip;
|
||||
|
||||
[Self] [SerializeField] private AudioSource musicSource;
|
||||
[Self] [SerializeField] private AudioSource sfxSource;
|
||||
[SerializeField] private AudioSource musicSource;
|
||||
[SerializeField] private AudioSource sfxSource;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -43,6 +43,8 @@ namespace Infrastructure.Unity
|
||||
public void PlayGameStart() => PlaySfx(startClip);
|
||||
public void PlayScore() => PlaySfx(scoreClip);
|
||||
public void PlayGameOver() => PlaySfx(gameOverClip);
|
||||
|
||||
public void PlayNpcSpawn() => PlaySfx(npcSpawnClip);
|
||||
|
||||
public void PlayTileBreak(Vector3 position)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user