feat: Player

This commit is contained in:
2026-10-07 15:46:51 +02:00
parent 074eb51084
commit d21f0c9b6c
45 changed files with 1801 additions and 74 deletions

View File

@@ -0,0 +1,34 @@
using UnityEngine;
using Arena.Characters;
namespace Arena.Interactions
{
/// <summary>
/// Gives ammo to the player. Apply is meant to be linked to a PlayerEvent.
/// </summary>
public class AmmoEffect : MonoBehaviour
{
[System.Serializable]
public class Settings
{
[Tooltip("Ammo given to the player.")]
[Min(1)] public int Amount = 10;
}
#region Fields
[SerializeField] private Settings _settings;
#endregion
#region Public Methods
public void Apply()
{
if (Player.Instance != null)
Player.Instance.Launcher.AddAmmo(_settings.Amount);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3831749972a75dc4dbe47f8a551785fc

View File

@@ -0,0 +1,34 @@
using UnityEngine;
using Arena.Characters;
namespace Arena.Interactions
{
/// <summary>
/// Heals the player. Apply is meant to be linked to a PlayerEvent.
/// </summary>
public class HealEffect : MonoBehaviour
{
[System.Serializable]
public class Settings
{
[Tooltip("Hit points given to the player.")]
[Min(1)] public int Amount = 25;
}
#region Fields
[SerializeField] private Settings _settings;
#endregion
#region Public Methods
public void Apply()
{
if (Player.Instance != null)
Player.Instance.Health.Heal(_settings.Amount);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3b9353836241da04e9bb64cc87f89745

View File

@@ -0,0 +1,83 @@
using System;
using UnityEngine;
namespace Arena.Combat
{
/// <summary>
/// Hit points of an entity. Serialized inside the component that owns it (Player, Enemy),
/// which calls Initialize in its Awake.
/// </summary>
[System.Serializable]
public class Health
{
#region Fields
[Tooltip("Hit points when full.")]
[Min(1)] public int Max = 100;
[Tooltip("Current hit points, visible for debugging.")]
[SerializeField] private int _current;
/// <summary>
/// Raised with the current and max hit points each time they change.
/// </summary>
public event Action<int, int> OnChange;
/// <summary>
/// Raised once, when the hit points reach zero.
/// </summary>
public event Action OnDied;
public int Current => _current;
public bool IsDead => _current <= 0;
#endregion
#region Public Methods
/// <summary>
/// Fills the hit points. Called by the owner in its Awake.
/// </summary>
public void Initialize()
{
SetCurrent(Max);
}
/// <summary>
/// Removes hit points and raises OnDied the first time they reach zero.
/// </summary>
public void TakeDamage(int amount)
{
if (IsDead || amount <= 0)
return;
SetCurrent(_current - amount);
if (IsDead)
OnDied?.Invoke();
}
/// <summary>
/// Gives hit points back without exceeding Max. Has no effect once dead.
/// </summary>
public void Heal(int amount)
{
if (IsDead || amount <= 0)
return;
SetCurrent(_current + amount);
}
#endregion
#region Private Methods
private void SetCurrent(int value)
{
_current = Mathf.Clamp(value, 0, Max);
OnChange?.Invoke(_current, Max);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 220d6096ae28c12449f72c38860cbc2e

View File

@@ -0,0 +1,10 @@
namespace Arena.Combat
{
/// <summary>
/// Anything that can receive damage: player, enemy, destructible object.
/// </summary>
public interface IDamageable
{
void TakeDamage(int amount);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 14df4410feff3d044afb4ec552a18997

View File

@@ -0,0 +1,68 @@
using UnityEngine;
namespace Arena.Combat
{
/// <summary>
/// Moves straight ahead and damages the first IDamageable it touches.
/// Needs a trigger collider and a kinematic Rigidbody.
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class Projectile : MonoBehaviour
{
[System.Serializable]
public class Settings
{
[Tooltip("Speed, in meters per second.")]
[Min(0f)] public float Speed = 20f;
[Tooltip("Time before an unused projectile is destroyed, in seconds.")]
[Min(0.1f)] public float Lifetime = 2f;
}
#region Fields
[SerializeField] private Settings _settings;
private int _damage;
private GameObject _owner;
#endregion
#region Unity Lifecycle
private void Update()
{
transform.position += transform.forward * _settings.Speed * Time.deltaTime;
}
private void OnTriggerEnter(Collider other)
{
// Ignore the shooter and the other trigger zones
if (other.isTrigger || other.gameObject == _owner)
return;
if (other.TryGetComponent(out IDamageable target))
target.TakeDamage(_damage);
Destroy(gameObject);
}
#endregion
#region Public Methods
/// <summary>
/// Starts the projectile. Called right after Instantiate by whoever fires it.
/// </summary>
/// <param name="damage">Damage dealt to the IDamageable it touches.</param>
/// <param name="owner">Shooter, ignored by the projectile.</param>
public void Launch(int damage, GameObject owner)
{
_damage = damage;
_owner = owner;
Destroy(gameObject, _settings.Lifetime);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7671f358930703f4683397845e17e460

View File

@@ -0,0 +1,107 @@
using System;
using UnityEngine;
namespace Arena.Combat
{
/// <summary>
/// Fires projectiles from a muzzle, limited by the fire interval and the ammo count.
/// Placed on the root of the shooter, which the projectiles ignore.
/// </summary>
public class ProjectileLauncher : MonoBehaviour
{
[System.Serializable]
public class Settings
{
[Tooltip("Weapon data: damage, fire interval, ammo, projectile.")]
public WeaponData Weapon;
}
[System.Serializable]
public struct References
{
[Tooltip("Spawn point of the projectiles. Its forward axis gives the direction.")]
public Transform Muzzle;
}
[System.Serializable]
public class StateContainer
{
public int Ammo;
}
#region Fields
[SerializeField] private Settings _settings;
[SerializeField] private References _references;
[SerializeField] private StateContainer _state;
/// <summary>
/// Raised with the current and max ammo each time the ammo changes.
/// </summary>
public event Action<int, int> OnAmmoChange;
public StateContainer State => _state;
public int MaxAmmo => _settings.Weapon.MaxAmmo;
private float _nextFireTime;
#endregion
#region Unity Lifecycle
private void Awake()
{
_state.Ammo = MaxAmmo;
}
#endregion
#region Public Methods
/// <summary>
/// Fires one projectile if the weapon is ready and has ammo. Returns true if a shot was fired.
/// </summary>
public bool TryFire()
{
if (Time.time < _nextFireTime || _state.Ammo <= 0)
return false;
_nextFireTime = Time.time + _settings.Weapon.FireInterval;
SetAmmo(_state.Ammo - 1);
Transform muzzle = _references.Muzzle;
Projectile projectile = Instantiate(_settings.Weapon.ProjectilePrefab, muzzle.position, muzzle.rotation);
projectile.Launch(_settings.Weapon.Damage, gameObject);
return true;
}
/// <summary>
/// Adds ammo without exceeding the weapon maximum.
/// </summary>
public void AddAmmo(int amount)
{
if (amount <= 0)
return;
SetAmmo(Mathf.Min(_state.Ammo + amount, MaxAmmo));
}
#endregion
#region Private Methods
private void SetAmmo(int value)
{
_state.Ammo = value;
OnAmmoChange?.Invoke(_state.Ammo, MaxAmmo);
}
[ContextMenu("Debug/Try Fire")]
private void DebugTryFire()
{
TryFire();
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,68 @@
using UnityEngine;
using Arena.Characters;
namespace Arena.UI
{
/// <summary>
/// Single entry point of the UI: connects the views to the player and switches screens.
/// </summary>
public class UiManager : MonoBehaviour
{
[System.Serializable]
public struct References
{
[Tooltip("In-game screen: health, ammo.")]
public GameObject Hud;
public HealthView HealthView;
public AmmoView AmmoView;
public GameOverView GameOverView;
}
#region Fields
[SerializeField] private References _references;
private Player _player;
#endregion
#region Unity Lifecycle
private void Start()
{
// Start: every Awake has run, so Player.Instance is set
_player = Player.Instance;
if (_player == null)
{
Debug.LogError("UiManager: no Player in the scene.");
return;
}
_references.HealthView.Bind(_player.Health);
_references.AmmoView.Bind(_player.Launcher);
_player.Health.OnDied += HandlePlayerDied;
_references.Hud.SetActive(true);
_references.GameOverView.Hide();
}
private void OnDestroy()
{
if (_player != null)
_player.Health.OnDied -= HandlePlayerDied;
}
#endregion
#region Private Methods
private void HandlePlayerDied()
{
_references.Hud.SetActive(false);
_references.GameOverView.Show();
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4c6cb5ac8e4f8d0479afda241378ff0b

View File

@@ -0,0 +1,32 @@
using UnityEngine;
namespace Arena.Combat
{
/// <summary>
/// Settings of a weapon, shared by every launcher that uses it.
/// </summary>
[CreateAssetMenu(fileName = "WeaponData_new", menuName = "Arena/Combat/Weapon Data")]
public class WeaponData : ScriptableObject
{
#region Fields
[Tooltip("Damage dealt by one projectile.")]
[SerializeField, Min(0)] private int _damage = 10;
[Tooltip("Time between two shots, in seconds.")]
[SerializeField, Min(0.05f)] private float _fireInterval = 0.2f;
[Tooltip("Ammo when the weapon is full.")]
[SerializeField, Min(1)] private int _maxAmmo = 30;
[Tooltip("Prefab fired by the weapon.")]
[SerializeField] private Projectile _projectilePrefab;
public int Damage => _damage;
public float FireInterval => _fireInterval;
public int MaxAmmo => _maxAmmo;
public Projectile ProjectilePrefab => _projectilePrefab;
#endregion
}
}

View File

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