add docs for game

This commit is contained in:
2025-01-07 20:29:20 +03:00
parent 926ce601d9
commit 3f269b2324
81 changed files with 1954 additions and 207 deletions

View File

@@ -8,16 +8,42 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component;
/// <summary>
/// Represents a consumable item in the game, handling interaction with players and other objects.
/// </summary>
public class ConsumableComponent : Engine.Scene.Component.Component
{
/// <summary>
/// The consumable logic tied to this component.
/// </summary>
private readonly IConsumable _consumable;
/// <summary>
/// The collider used for detecting interactions with other objects.
/// </summary>
private readonly AABBColliderComponent _collider;
/// <summary>
/// The 2D box renderer used to display the consumable's icon.
/// </summary>
private readonly Box2DRenderer _box2DRenderer;
/// <summary>
/// Provides random values for movement adjustments.
/// </summary>
private readonly Random _random = new();
/// <summary>
/// Controls the movement of the consumable object.
/// </summary>
private MovementController _movementController = null!;
/// <summary>
/// Initializes a new instance of the <see cref="ConsumableComponent"/> class.
/// </summary>
/// <param name="parConsumable">The consumable logic associated with this component.</param>
/// <param name="parCollider">The collider for detecting interactions.</param>
/// <param name="parBox2DRenderer">The renderer for displaying the consumable's visual representation.</param>
public ConsumableComponent(IConsumable parConsumable, AABBColliderComponent parCollider,
Box2DRenderer parBox2DRenderer)
{
@@ -36,6 +62,10 @@ public class ConsumableComponent : Engine.Scene.Component.Component
ArgumentNullException.ThrowIfNull(_movementController);
}
/// <summary>
/// Handles collision events with other objects.
/// </summary>
/// <param name="parCollider">The collider of the object that this collided with.</param>
private void OnCollision(AABBColliderComponent parCollider)
{
if (parCollider.ColliderGroups.Contains("player"))
@@ -48,6 +78,10 @@ public class ConsumableComponent : Engine.Scene.Component.Component
}
}
/// <summary>
/// Attempts to consume the consumable if it collided with a player.
/// </summary>
/// <param name="parCollider">The player's collider component.</param>
private void TryConsume(AABBColliderComponent parCollider)
{
var playerController = parCollider.GameObject.GetComponent<PlayerController>();
@@ -61,6 +95,10 @@ public class ConsumableComponent : Engine.Scene.Component.Component
EngineUtil.SceneManager.CurrentScene!.Remove(GameObject);
}
/// <summary>
/// Moves the consumable away if it collides with another consumable.
/// </summary>
/// <param name="parCollider">The collider of the other consumable.</param>
private void MoveAway(AABBColliderComponent parCollider)
{
var direction = _collider.GameObject.Transform.GetFullTranslation() -
@@ -78,6 +116,10 @@ public class ConsumableComponent : Engine.Scene.Component.Component
_movementController.ApplyMovement(direction);
}
/// <summary>
/// Generates a random direction vector for movement.
/// </summary>
/// <returns>A normalized random direction vector.</returns>
private Vector3 GetRandomDirection()
{
var x = (_random.NextSingle() * 2) - 1;

View File

@@ -2,21 +2,50 @@
using DoomDeathmatch.Component.Util;
using DoomDeathmatch.Script.Model.Enemy;
using DoomDeathmatch.Script.Model.Enemy.Attack;
using DoomDeathmatch.Script.MVC;
using Engine.Util;
namespace DoomDeathmatch.Component.MVC.Enemy;
/// <summary>
/// Controls enemy behavior, including movement, health, and attacks.
/// </summary>
public class EnemyController : Engine.Scene.Component.Component
{
/// <summary>
/// Manages the health of the enemy.
/// </summary>
public HealthController HealthController { get; private set; } = null!;
/// <summary>
/// Data associated with the enemy, including stats and behaviors.
/// </summary>
private readonly EnemyData _enemyData;
/// <summary>
/// Reference to the game controller.
/// </summary>
private GameController _gameController = null!;
private MovementController _movementController = null!;
private AttackBehavior _attackBehavior = null!;
private EnemyView _enemyView = null!;
/// <summary>
/// Controls the movement of the enemy.
/// </summary>
private MovementController _movementController = null!;
/// <summary>
/// Handles the enemy's attack behavior.
/// </summary>
private AttackBehavior _attackBehavior = null!;
/// <summary>
/// Handles visual representation of the enemy.
/// </summary>
private IView<EnemyData> _enemyView = null!;
/// <summary>
/// Initializes a new instance of the <see cref="EnemyController"/> class.
/// </summary>
/// <param name="parEnemyData">Data for the enemy, including stats and behaviors.</param>
public EnemyController(EnemyData parEnemyData)
{
_enemyData = parEnemyData;
@@ -26,7 +55,7 @@ public class EnemyController : Engine.Scene.Component.Component
{
_gameController = EngineUtil.SceneManager.CurrentScene!.FindFirstComponent<GameController>()!;
HealthController = GameObject.GetComponent<HealthController>()!;
_enemyView = GameObject.GetComponent<EnemyView>()!;
_enemyView = GameObject.GetComponent<IView<EnemyData>>()!;
_movementController = GameObject.GetComponent<MovementController>()!;
ArgumentNullException.ThrowIfNull(_gameController);
@@ -69,6 +98,9 @@ public class EnemyController : Engine.Scene.Component.Component
_attackBehavior.Attack(parDeltaTime);
}
/// <summary>
/// Handles the death of the enemy by removing it from the scene and updating the score.
/// </summary>
private void OnDeath()
{
EngineUtil.SceneManager.CurrentScene!.Remove(GameObject);

View File

@@ -1,19 +1,31 @@
using DoomDeathmatch.Script.Model.Enemy;
using DoomDeathmatch.Script.MVC;
using Engine.Graphics.Texture;
using Engine.Scene.Component.BuiltIn.Renderer;
using Engine.Util;
namespace DoomDeathmatch.Component.MVC.Enemy;
public class EnemyView : Engine.Scene.Component.Component
/// <summary>
/// View component for displaying the enemy.
/// </summary>
public class EnemyView : Engine.Scene.Component.Component, IView<EnemyData>
{
/// <summary>
/// The box2d renderer used to display the enemy.
/// </summary>
private readonly Box2DRenderer _box2DRenderer;
/// <summary>
/// Initializes a new instance of the <see cref="EnemyView"/> class.
/// </summary>
/// <param name="parBox2DRenderer">Box2d renderer for displaying the enemy.</param>
public EnemyView(Box2DRenderer parBox2DRenderer)
{
_box2DRenderer = parBox2DRenderer;
}
/// <inheritdoc/>
public void UpdateView(EnemyData parEnemyData)
{
_box2DRenderer.Texture = EngineUtil.AssetResourceManager.Load<Texture>(parEnemyData.Texture);

View File

@@ -7,19 +7,50 @@ using Engine.Util;
namespace DoomDeathmatch.Component.MVC;
/// <summary>
/// Controls the main game logic, including pausing, unpausing, and handling game over states.
/// </summary>
public class GameController : Engine.Scene.Component.Component
{
/// <summary>
/// Indicates whether the game is currently paused.
/// </summary>
public bool IsPaused { get; private set; }
/// <summary>
/// Indicates whether the game is over.
/// </summary>
public bool IsGameOver { get; private set; }
/// <summary>
/// The player controller managing the player's state and actions.
/// </summary>
public PlayerController PlayerController { get; private set; } = null!;
/// <summary>
/// The score controller tracking the player's score.
/// </summary>
public ScoreController ScoreController { get; private set; } = null!;
/// <summary>
/// The input handler for handling keyboard and mouse input.
/// </summary>
private readonly IInputHandler _inputHandler = EngineUtil.InputHandler;
/// <summary>
/// The menu controller for handling the main menu.
/// </summary>
private readonly MenuControllerComponent _menuController;
/// <summary>
/// The timer controller for managing the game timer.
/// </summary>
private TimerController _timerController = null!;
/// <summary>
/// Initializes a new instance of the <see cref="GameController"/> class with a specified menu controller.
/// </summary>
/// <param name="parMenuController">The menu controller used for menu navigation during the game.</param>
public GameController(MenuControllerComponent parMenuController)
{
_menuController = parMenuController;
@@ -39,20 +70,6 @@ public class GameController : Engine.Scene.Component.Component
_timerController.OnFinished += GameOver;
}
public void Unpause()
{
EngineUtil.SceneManager.CurrentScene!.TimeScale = 1.0f;
IsPaused = false;
_menuController.SelectMenuItem("play");
}
public void Pause()
{
EngineUtil.SceneManager.CurrentScene!.TimeScale = 0.0f;
IsPaused = true;
_menuController.SelectMenuItem("escape");
}
public override void Update(double parDeltaTime)
{
if (_inputHandler.IsKeyJustPressed(KeyboardButtonCode.Escape))
@@ -68,6 +85,29 @@ public class GameController : Engine.Scene.Component.Component
}
}
/// <summary>
/// Pauses the game.
/// </summary>
public void Pause()
{
EngineUtil.SceneManager.CurrentScene!.TimeScale = 0.0f;
IsPaused = true;
_menuController.SelectMenuItem("escape");
}
/// <summary>
/// Resumes the game from a paused state.
/// </summary>
public void Unpause()
{
EngineUtil.SceneManager.CurrentScene!.TimeScale = 1.0f;
IsPaused = false;
_menuController.SelectMenuItem("play");
}
/// <summary>
/// Handles the game over state, transitioning to the Game Over scene.
/// </summary>
private void GameOver()
{
IsGameOver = true;

View File

@@ -1,15 +1,30 @@
using DoomDeathmatch.Script.Model;
using DoomDeathmatch.Script.MVC;
using Engine.Scene.Component.BuiltIn.Renderer;
namespace DoomDeathmatch.Component.MVC.Health;
public class EnemyHealthView : HealthView
/// <summary>
/// View component for displaying the health status of an enemy.
/// </summary>
public class EnemyHealthView : Engine.Scene.Component.Component, IView<HealthModel>
{
public EnemyHealthView(TextRenderer parHealthTextRenderer) : base(parHealthTextRenderer)
/// <summary>
/// The text renderer used to display the health.
/// </summary>
private readonly TextRenderer _healthTextRenderer;
/// <summary>
/// Initializes a new instance of the <see cref="EnemyHealthView"/> class.
/// </summary>
/// <param name="parHealthTextRenderer">Text renderer for displaying the health status text.</param>
public EnemyHealthView(TextRenderer parHealthTextRenderer)
{
_healthTextRenderer = parHealthTextRenderer;
}
public override void UpdateView(HealthModel parHealthModel)
/// <inheritdoc/>
public void UpdateView(HealthModel parHealthModel)
{
_healthTextRenderer.Text = $"Здоровье: {parHealthModel.Health:000}";
}

View File

@@ -1,15 +1,37 @@
using DoomDeathmatch.Script.Model;
using DoomDeathmatch.Script.MVC;
namespace DoomDeathmatch.Component.MVC.Health;
/// <summary>
/// Manages health-related logic, including damage, healing, and health changes.
/// </summary>
public class HealthController : Engine.Scene.Component.Component
{
/// <summary>
/// Triggered when the entity dies.
/// </summary>
public event Action? OnDeath;
/// <summary>
/// Indicates whether the entity is alive.
/// </summary>
public bool IsAlive => _healthModel.Health > 0;
/// <summary>
/// The health model containing the current and maximum health.
/// </summary>
private readonly HealthModel _healthModel;
private HealthView? _healthView;
/// <summary>
/// The view responsible for displaying health information.
/// </summary>
private IView<HealthModel>? _healthView;
/// <summary>
/// Initializes a new instance of the <see cref="HealthController"/> class with an optional starting health value.
/// </summary>
/// <param name="parHealth">Initial health value. Defaults to 100.</param>
public HealthController(float parHealth = 100)
{
_healthModel = new HealthModel(parHealth);
@@ -18,7 +40,7 @@ public class HealthController : Engine.Scene.Component.Component
public override void Awake()
{
_healthModel.HealthChanged += OnHealthChanged;
_healthView = GameObject.GetComponent<HealthView>();
_healthView = GameObject.GetComponent<IView<HealthModel>>();
if (_healthView != null)
{
@@ -27,22 +49,38 @@ public class HealthController : Engine.Scene.Component.Component
}
}
/// <summary>
/// Sets the maximum health and restores the entity's health to the maximum value.
/// </summary>
/// <param name="parMaxHealth">The maximum health value.</param>
public void SetMaxHealth(float parMaxHealth)
{
_healthModel.MaxHealth = parMaxHealth;
_healthModel.Health = parMaxHealth;
}
/// <summary>
/// Reduces the entity's health by the specified damage value.
/// </summary>
/// <param name="parDamage">The amount of damage to apply.</param>
public void TakeDamage(float parDamage)
{
_healthModel.Health -= parDamage;
}
/// <summary>
/// Increases the entity's health by the specified heal value.
/// </summary>
/// <param name="parHeal">The amount of healing to apply.</param>
public void Heal(float parHeal)
{
_healthModel.Health += parHeal;
}
/// <summary>
/// Handles health changes and triggers death logic if health reaches zero.
/// </summary>
/// <param name="parHealthModel">The updated health model.</param>
private void OnHealthChanged(HealthModel parHealthModel)
{
if (!IsAlive)

View File

@@ -1,17 +1,29 @@
using DoomDeathmatch.Script.Model;
using DoomDeathmatch.Script.MVC;
using Engine.Scene.Component.BuiltIn.Renderer;
namespace DoomDeathmatch.Component.MVC.Health;
public class HealthView : Engine.Scene.Component.Component
/// <summary>
/// View component for displaying the health status.
/// </summary>
public class HealthView : Engine.Scene.Component.Component, IView<HealthModel>
{
protected readonly TextRenderer _healthTextRenderer;
/// <summary>
/// The text renderer used to display the health percentage.
/// </summary>
private readonly TextRenderer _healthTextRenderer;
/// <summary>
/// Initializes a new instance of the <see cref="HealthView"/> class.
/// </summary>
/// <param name="parHealthTextRenderer">Text renderer for displaying the health status text.</param>
public HealthView(TextRenderer parHealthTextRenderer)
{
_healthTextRenderer = parHealthTextRenderer;
}
/// <inheritdoc/>
public virtual void UpdateView(HealthModel parHealthModel)
{
var percentage = parHealthModel.Health / parHealthModel.MaxHealth * 100;

View File

@@ -3,11 +3,24 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.MVC;
/// <summary>
/// Controls the movement logic for a game object, applying movement forces to the rigidbody.
/// </summary>
public class MovementController : Engine.Scene.Component.Component
{
/// <summary>
/// The movement speed.
/// </summary>
public float Speed { get; set; } = 10.0f;
/// <summary>
/// The rigidbody component for the game object.
/// </summary>
private RigidbodyComponent _rigidbody = null!;
/// <summary>
/// The drag component for the game object.
/// </summary>
private DragComponent _dragComponent = null!;
public override void Awake()
@@ -19,6 +32,10 @@ public class MovementController : Engine.Scene.Component.Component
ArgumentNullException.ThrowIfNull(_dragComponent);
}
/// <summary>
/// Applies a directional movement force to the object.
/// </summary>
/// <param name="parDirection">The direction of movement.</param>
public void ApplyMovement(Vector3 parDirection)
{
_rigidbody.Force += _dragComponent.Drag * Speed * parDirection.Normalized();

View File

@@ -9,17 +9,45 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.MVC;
/// <summary>
/// Controls the player's actions and state, including health, weapons, and input handling.
/// </summary>
public class PlayerController : Engine.Scene.Component.Component
{
/// <summary>
/// Event triggered when the player dies.
/// </summary>
public event Action? OnDeath;
/// <summary>
/// Indicates whether the player is alive.
/// </summary>
public bool IsAlive => HealthController.IsAlive;
/// <summary>
/// The health controller managing the player's health.
/// </summary>
public HealthController HealthController { get; private set; } = null!;
/// <summary>
/// The weapon controller managing the player's weapons.
/// </summary>
public WeaponController WeaponController { get; private set; } = null!;
/// <summary>
/// The player's camera providing a perspective view.
/// </summary>
public PerspectiveCamera Camera { get; }
/// <summary>
/// The input handler for handling keyboard and mouse input.
/// </summary>
private readonly IInputHandler _inputHandler = EngineUtil.InputHandler;
/// <summary>
/// Initializes a new instance of the <see cref="PlayerController"/> class with a specified camera.
/// </summary>
/// <param name="parCamera">The player's perspective camera.</param>
public PlayerController(PerspectiveCamera parCamera)
{
Camera = parCamera;
@@ -66,24 +94,32 @@ public class PlayerController : Engine.Scene.Component.Component
return;
}
var position = Camera.GameObject.Transform.GetFullTranslation();
var forward = (Camera.Forward - position).Normalized();
var right = Vector3.Cross(forward, Vector3.UnitZ).Normalized();
HandleShootingRaycast();
}
}
var collisionManager = EngineUtil.SceneManager.CurrentScene!.FindFirstComponent<CollisionManagerComponent>();
/// <summary>
/// Handles the shooting raycast for the player.
/// </summary>
private void HandleShootingRaycast()
{
var position = Camera.GameObject.Transform.GetFullTranslation();
var forward = (Camera.Forward - position).Normalized();
var right = Vector3.Cross(forward, Vector3.UnitZ).Normalized();
var offsets = WeaponController.WeaponData.ShootPattern.GetShootPattern(forward, Vector3.UnitZ, right);
foreach (var offset in offsets)
var collisionManager = EngineUtil.SceneManager.CurrentScene!.FindFirstComponent<CollisionManagerComponent>();
var offsets = WeaponController.WeaponData.ShootPattern.GetShootPattern(forward, Vector3.UnitZ, right);
foreach (var offset in offsets)
{
var direction = forward + offset;
if (!collisionManager!.Raycast(position, direction, ["enemy", "wall"], out var result))
{
var direction = forward + offset;
if (!collisionManager!.Raycast(position, direction, ["enemy", "wall"], out var result))
{
continue;
}
var enemyController = result.HitObject.GetComponent<EnemyController>();
enemyController?.HealthController.TakeDamage(WeaponController.WeaponData.Damage);
continue;
}
var enemyController = result.HitObject.GetComponent<EnemyController>();
enemyController?.HealthController.TakeDamage(WeaponController.WeaponData.Damage);
}
}
}

View File

@@ -1,21 +1,39 @@
using DoomDeathmatch.Script.Model;
using DoomDeathmatch.Script.MVC;
namespace DoomDeathmatch.Component.MVC.Score;
/// <summary>
/// Manages the player's score, including updating and displaying it.
/// </summary>
public class ScoreController : Engine.Scene.Component.Component
{
/// <summary>
/// Current score value.
/// </summary>
public int Score => _scoreModel.Score;
/// <summary>
/// The score model containing the current score.
/// </summary>
private readonly ScoreModel _scoreModel = new();
private ScoreView _scoreView = null!;
/// <summary>
/// The view responsible for displaying score information.
/// </summary>
private IView<ScoreModel> _scoreView = null!;
public override void Awake()
{
_scoreView = GameObject.GetComponent<ScoreView>()!;
_scoreView = GameObject.GetComponent<IView<ScoreModel>>()!;
_scoreView.UpdateView(_scoreModel);
_scoreModel.ScoreChanged += _scoreView.UpdateView;
}
/// <summary>
/// Adds the specified value to the current score.
/// </summary>
/// <param name="parScore">The score value to add.</param>
public void AddScore(int parScore)
{
_scoreModel.Score += parScore;

View File

@@ -1,17 +1,29 @@
using DoomDeathmatch.Script.Model;
using DoomDeathmatch.Script.MVC;
using Engine.Scene.Component.BuiltIn.Renderer;
namespace DoomDeathmatch.Component.MVC.Score;
public class ScoreView : Engine.Scene.Component.Component
/// <summary>
/// View component for displaying the score.
/// </summary>
public class ScoreView : Engine.Scene.Component.Component, IView<ScoreModel>
{
/// <summary>
/// The text renderer used to display the score.
/// </summary>
private readonly TextRenderer _scoreTextRenderer;
/// <summary>
/// Initializes a new instance of the <see cref="ScoreView"/> class.
/// </summary>
/// <param name="parScoreTextRenderer">Text renderer for displaying the score text.</param>
public ScoreView(TextRenderer parScoreTextRenderer)
{
_scoreTextRenderer = parScoreTextRenderer;
}
/// <inheritdoc/>
public void UpdateView(ScoreModel parScoreModel)
{
_scoreTextRenderer.Text = $"Счет: {parScoreModel.Score:00000}";

View File

@@ -1,19 +1,41 @@
using Engine.Util;
using DoomDeathmatch.Script.MVC;
using Engine.Util;
namespace DoomDeathmatch.Component.MVC.Timer;
/// <summary>
/// Controls the timer functionality, including updates and view synchronization.
/// </summary>
public class TimerController : Engine.Scene.Component.Component
{
/// <summary>
/// Triggered when the timer finishes.
/// </summary>
public event Action? OnFinished;
private readonly TickableTimer _tickableTimer = new(60 + 10);
/// <summary>
/// Internal timer that tracks the current countdown time.
/// </summary>
private readonly TickableTimer _tickableTimer;
private TimerView _timerView = null!;
/// <summary>
/// View responsible for displaying the timer's current state.
/// </summary>
private IView<TickableTimer> _timerView = null!;
/// <summary>
/// Initializes a new instance of the <see cref="TimerController"/> class with an optional starting time.
/// </summary>
/// <param name="parTime">The initial time value. Defaults to 60 seconds.</param>
public TimerController(float parTime = 60)
{
_tickableTimer = new TickableTimer(parTime);
}
public override void Awake()
{
_timerView = GameObject.GetComponent<TimerView>()!;
_timerView.UpdateView(_tickableTimer.CurrentTime);
_timerView = GameObject.GetComponent<IView<TickableTimer>>()!;
_timerView.UpdateView(_tickableTimer);
_tickableTimer.OnUpdate += OnTimeChanged;
_tickableTimer.OnFinished += OnFinished;
@@ -24,8 +46,12 @@ public class TimerController : Engine.Scene.Component.Component
_tickableTimer.Update(parDeltaTime);
}
/// <summary>
/// Updates the timer view with the new time value.
/// </summary>
/// <param name="parTime">The updated time value.</param>
private void OnTimeChanged(double parTime)
{
_timerView.UpdateView(parTime);
_timerView.UpdateView(_tickableTimer);
}
}

View File

@@ -1,20 +1,34 @@
using Engine.Scene.Component.BuiltIn.Renderer;
using DoomDeathmatch.Script.MVC;
using Engine.Scene.Component.BuiltIn.Renderer;
using Engine.Util;
namespace DoomDeathmatch.Component.MVC.Timer;
public class TimerView : Engine.Scene.Component.Component
/// <summary>
/// View component for displaying the timer.
/// </summary>
public class TimerView : Engine.Scene.Component.Component, IView<TickableTimer>
{
/// <summary>
/// The text renderer used to display the timer.
/// </summary>
private readonly TextRenderer _timerTextRenderer;
/// <summary>
/// Initializes a new instance of the <see cref="TimerView"/> class.
/// </summary>
/// <param name="parTimerTextRenderer">Text renderer for displaying the timer text.</param>
public TimerView(TextRenderer parTimerTextRenderer)
{
_timerTextRenderer = parTimerTextRenderer;
}
public void UpdateView(double parTime)
/// <inheritdoc/>
public void UpdateView(TickableTimer parTime)
{
var seconds = Math.Floor(parTime) % 60;
var minutes = Math.Floor(parTime / 60) % 60;
var time = parTime.CurrentTime;
var seconds = Math.Floor(time) % 60;
var minutes = Math.Floor(time / 60) % 60;
_timerTextRenderer.Text = $"Время: {minutes:00}:{seconds:00}";
}

View File

@@ -0,0 +1,17 @@
namespace DoomDeathmatch.Component.MVC.Weapon;
/// <summary>
/// Data for ammo information.
/// </summary>
public struct AmmoData
{
/// <summary>
/// The maximum amount of ammo.
/// </summary>
public int MaxAmmo { get; set; }
/// <summary>
/// The current amount of ammo.
/// </summary>
public int Ammo { get; set; }
}

View File

@@ -0,0 +1,15 @@
using DoomDeathmatch.Script.Model.Weapon;
using DoomDeathmatch.Script.MVC;
namespace DoomDeathmatch.Component.MVC.Weapon;
/// <summary>
/// Interface for weapon view components.
/// </summary>
public interface IWeaponView : IView<WeaponData>, IView<AmmoData>
{
/// <summary>
/// Starts the weapon fire animation.
/// </summary>
public void PlayFireAnimation();
}

View File

@@ -3,23 +3,48 @@ using DoomDeathmatch.Script.Model.Weapon;
namespace DoomDeathmatch.Component.MVC.Weapon;
/// <summary>
/// Manages weapon functionality, including shooting, reloading, and weapon selection.
/// </summary>
public class WeaponController : Engine.Scene.Component.Component
{
/// <summary>
/// Triggered when a weapon is fired.
/// </summary>
public event Action<WeaponData>? OnWeaponShot;
/// <summary>
/// The currently selected weapon's data.
/// </summary>
public WeaponData WeaponData => _weaponModel.SelectedWeapon;
/// <summary>
/// The internal weapon model containing weapon-related data.
/// </summary>
private readonly WeaponModel _weaponModel = new();
private WeaponView _weaponView = null!;
/// <summary>
/// View responsible for displaying weapon information and animations.
/// </summary>
private IWeaponView _weaponView = null!;
public override void Awake()
{
_weaponView = GameObject.GetComponent<WeaponView>()!;
_weaponView = GameObject.GetComponent<IWeaponView>()!;
_weaponView.UpdateView(_weaponModel.SelectedWeapon);
_weaponView.UpdateView(new AmmoData
{
Ammo = _weaponModel.SelectedWeapon.Ammo, MaxAmmo = _weaponModel.SelectedWeapon.MaxAmmo
});
_weaponModel.OnWeaponSelected += WeaponSelected;
WeaponSelected(null, _weaponModel.SelectedWeapon);
}
/// <summary>
/// Attempts to fire the currently selected weapon.
/// </summary>
/// <returns><c>true</c> if the weapon is fired successfully; otherwise, <c>false</c>.</returns>
public bool TryShoot()
{
if (_weaponModel.SelectedWeapon.Ammo <= 0)
@@ -30,16 +55,23 @@ public class WeaponController : Engine.Scene.Component.Component
_weaponModel.SelectedWeapon.Ammo--;
OnWeaponShot?.Invoke(_weaponModel.SelectedWeapon);
_weaponView.Fire();
_weaponView.PlayFireAnimation();
return true;
}
/// <summary>
/// Reloads the currently selected weapon, restoring its ammo to the maximum value.
/// </summary>
public void Reload()
{
_weaponModel.SelectedWeapon.Ammo = _weaponModel.SelectedWeapon.MaxAmmo;
}
/// <summary>
/// Adds a new weapon to the weapon model if it does not already exist.
/// </summary>
/// <param name="parWeaponData">The weapon data to add.</param>
public void AddWeapon(WeaponData parWeaponData)
{
if (_weaponModel.Weapons.Contains(parWeaponData))
@@ -50,6 +82,10 @@ public class WeaponController : Engine.Scene.Component.Component
_weaponModel.Weapons.Add(parWeaponData);
}
/// <summary>
/// Adds a weapon to the model or merges its ammo with an existing weapon of the same type.
/// </summary>
/// <param name="parWeaponData">The weapon data to add or merge.</param>
public void AddOrMergeWeapon(WeaponData parWeaponData)
{
if (!_weaponModel.Weapons.Contains(parWeaponData))
@@ -62,6 +98,10 @@ public class WeaponController : Engine.Scene.Component.Component
}
}
/// <summary>
/// Removes a weapon from the model by its index.
/// </summary>
/// <param name="parIndex">The index of the weapon to remove.</param>
public void RemoveWeapon(int parIndex)
{
if (parIndex <= 0 || parIndex >= _weaponModel.Weapons.Count)
@@ -75,6 +115,10 @@ public class WeaponController : Engine.Scene.Component.Component
_weaponModel.Weapons.RemoveAt(parIndex);
}
/// <summary>
/// Selects a weapon from the model by its index.
/// </summary>
/// <param name="parIndex">The index of the weapon to select.</param>
public void SelectWeapon(int parIndex)
{
if (parIndex >= _weaponModel.Weapons.Count)
@@ -85,14 +129,26 @@ public class WeaponController : Engine.Scene.Component.Component
_weaponModel.SelectedWeaponIndex = parIndex;
}
/// <summary>
/// Updates the view when the selected weapon changes.
/// </summary>
/// <param name="parOldWeapon">The previously selected weapon.</param>
/// <param name="parNewWeapon">The newly selected weapon.</param>
private void WeaponSelected(WeaponData? parOldWeapon, WeaponData parNewWeapon)
{
if (parOldWeapon != null)
{
parOldWeapon.OnAmmoChanged -= _weaponView.UpdateAmmoView;
parOldWeapon.OnAmmoChanged -= AmmoChanged;
}
parNewWeapon.OnAmmoChanged += _weaponView.UpdateAmmoView;
parNewWeapon.OnAmmoChanged += AmmoChanged;
_weaponView.UpdateView(parNewWeapon);
}
private void AmmoChanged(WeaponData parWeapon)
{
var ammoData = new AmmoData { Ammo = parWeapon.Ammo, MaxAmmo = parWeapon.MaxAmmo };
_weaponView.UpdateView(ammoData);
}
}

View File

@@ -6,16 +6,42 @@ using Engine.Util;
namespace DoomDeathmatch.Component.MVC.Weapon;
public class WeaponView : Engine.Scene.Component.Component
/// <summary>
/// View component for displaying weapon information such as name, ammo, and sprite.
/// </summary>
public class WeaponView : Engine.Scene.Component.Component, IWeaponView
{
/// <summary>
/// The text renderer used to display the weapon name.
/// </summary>
private readonly TextRenderer _weaponName;
/// <summary>
/// The text renderer used to display the weapon ammo.
/// </summary>
private readonly TextRenderer _weaponAmmo;
/// <summary>
/// The box2d renderer used to display the weapon sprite.
/// </summary>
private readonly Box2DRenderer _weaponSprite;
/// <summary>
/// The texture used for the weapon sprite when it is idle.
/// </summary>
private Texture? _idleTexture;
/// <summary>
/// The animation player used to display the weapon sprite when it is firing.
/// </summary>
private AnimationPlayer<Texture>? _weaponFireAnimation;
/// <summary>
/// Initializes a new instance of the <see cref="WeaponView"/> class.
/// </summary>
/// <param name="parWeaponName">Text renderer for displaying the weapon name.</param>
/// <param name="parWeaponAmmo">Text renderer for displaying the ammo count.</param>
/// <param name="parWeaponSprite">Renderer for displaying the weapon sprite.</param>
public WeaponView(TextRenderer parWeaponName, TextRenderer parWeaponAmmo, Box2DRenderer parWeaponSprite)
{
_weaponName = parWeaponName;
@@ -28,9 +54,9 @@ public class WeaponView : Engine.Scene.Component.Component
_weaponFireAnimation?.Update(parDeltaTime);
}
/// <inheritdoc/>
public void UpdateView(WeaponData parWeaponData)
{
UpdateAmmoView(parWeaponData);
_weaponName.Text = $"Оружие: {parWeaponData.Name}";
_idleTexture = EngineUtil.AssetResourceManager.Load<Texture>(parWeaponData.IdleTexture);
@@ -50,21 +76,30 @@ public class WeaponView : Engine.Scene.Component.Component
_weaponFireAnimation.OnFinish += OnAnimationFinish;
}
public void UpdateAmmoView(WeaponData parWeaponData)
/// <inheritdoc/>
public void UpdateView(AmmoData parAmmoData)
{
_weaponAmmo.Text = $"Патроны: {parWeaponData.Ammo}/{parWeaponData.MaxAmmo}";
_weaponAmmo.Text = $"Патроны: {parAmmoData.Ammo}/{parAmmoData.MaxAmmo}";
}
public void Fire()
/// <inheritdoc/>
public void PlayFireAnimation()
{
_weaponFireAnimation?.Start();
}
/// <summary>
/// Handles the animation frame change event.
/// </summary>
/// <param name="parFrameTexture">The current frame texture.</param>
private void OnAnimationFrame(Texture parFrameTexture)
{
_weaponSprite.Texture = parFrameTexture;
}
/// <summary>
/// Handles the animation finish event.
/// </summary>
private void OnAnimationFinish()
{
_weaponSprite.Texture = _idleTexture;

View File

@@ -2,6 +2,9 @@
namespace DoomDeathmatch.Component;
/// <summary>
/// Component for spawning objects using <see cref="ObjectSpawner"/>.
/// </summary>
public class ObjectSpawnerComponent(ObjectSpawner parObjectSpawner) : Engine.Scene.Component.Component
{
public override void Update(double parDeltaTime)

View File

@@ -3,16 +3,44 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.Physics.Collision;
/// <summary>
/// Represents an axis-aligned bounding box (AABB) collider component used for collision detection.
/// </summary>
public class AABBColliderComponent : Engine.Scene.Component.Component
{
/// <summary>
/// Triggered when a collision occurs with another <see cref="AABBColliderComponent"/>.
/// </summary>
public event Action<AABBColliderComponent>? OnCollision;
/// <summary>
/// The collider defining the bounds of this component.
/// </summary>
public AABBCollider Collider { get; set; } = new();
/// <summary>
/// Offset applied to the collider position relative to the parent object.
/// </summary>
public Vector3 Offset { get; set; } = Vector3.Zero;
/// <summary>
/// A set of groups that this collider belongs to.
/// </summary>
public ISet<string> ColliderGroups => _colliderGroups;
/// <summary>
/// A set of groups that this collider will not collide with.
/// </summary>
public ISet<string> ExcludeColliderCollideGroups => _excludeColliderCollideGroups;
/// <summary>
/// A set of groups that this collider belongs to.
/// </summary>
private readonly HashSet<string> _colliderGroups = ["default"];
/// <summary>
/// A set of groups that this collider will not collide with.
/// </summary>
private readonly HashSet<string> _excludeColliderCollideGroups = [];
public override void Update(double parDeltaTime)
@@ -20,13 +48,12 @@ public class AABBColliderComponent : Engine.Scene.Component.Component
Collider.Position = GameObject.Transform.GetFullTranslation() + Offset;
}
/// <summary>
/// Invokes the collision logic for this collider with the specified collider.
/// </summary>
/// <param name="parCollider">The collider with which this component collided.</param>
public void CollideWith(AABBColliderComponent parCollider)
{
OnCollision?.Invoke(parCollider);
}
public bool InColliderGroup(string parGroup)
{
return ColliderGroups.Contains(parGroup);
}
}

View File

@@ -2,6 +2,9 @@
namespace DoomDeathmatch.Component.Physics.Collision;
/// <summary>
/// A force field component that applies collision-based velocity changes to rigid bodies.
/// </summary>
public class ColliderForceFieldComponent : Engine.Scene.Component.Component
{
private AABBColliderComponent _collider = null!;
@@ -12,6 +15,10 @@ public class ColliderForceFieldComponent : Engine.Scene.Component.Component
_collider.OnCollision += OnCollision;
}
/// <summary>
/// Handles collision events and modifies the velocity of colliding rigid bodies.
/// </summary>
/// <param name="parCollider">The collider involved in the collision.</param>
private void OnCollision(AABBColliderComponent parCollider)
{
var rigidbody = parCollider.GameObject.GetComponent<RigidbodyComponent>();
@@ -27,6 +34,6 @@ public class ColliderForceFieldComponent : Engine.Scene.Component.Component
return;
}
rigidbody.Velocity -= normal * (speedAlongNormal * 1.75f);
rigidbody.Velocity -= normal * speedAlongNormal;
}
}

View File

@@ -5,8 +5,14 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.Physics.Collision;
/// <summary>
/// Manages collisions between AABB colliders in the current scene.
/// </summary>
public class CollisionManagerComponent : Engine.Scene.Component.Component
{
/// <summary>
/// A list of colliders in the current scene.
/// </summary>
private List<AABBColliderComponent> _colliders = [];
public override void PreUpdate(double parDeltaTime)
@@ -24,34 +30,19 @@ public class CollisionManagerComponent : Engine.Scene.Component.Component
{
var colliderB = _colliders[j];
var canCollideAB = colliderA.ExcludeColliderCollideGroups.Count == 0 ||
!colliderB.ColliderGroups.Overlaps(colliderA.ExcludeColliderCollideGroups);
var canCollideBA = colliderB.ExcludeColliderCollideGroups.Count == 0 ||
!colliderA.ColliderGroups.Overlaps(colliderB.ExcludeColliderCollideGroups);
if (!canCollideAB && !canCollideBA)
{
continue;
}
if (!colliderA.Collider.Intersects(colliderB.Collider))
{
continue;
}
if (canCollideAB)
{
colliderA.CollideWith(colliderB);
}
if (canCollideBA)
{
colliderB.CollideWith(colliderA);
}
PerformCollisionCheck(colliderA, colliderB);
}
}
}
/// <summary>
/// Performs a raycast and finds the closest collider intersected by the ray.
/// </summary>
/// <param name="parStart">The starting point of the ray.</param>
/// <param name="parDirection">The direction of the ray.</param>
/// <param name="parColliderGroups">The collider groups to consider for the raycast.</param>
/// <param name="parResult">The result of the raycast, if an intersection occurs.</param>
/// <returns><c>true</c> if an intersection occurs; otherwise, <c>false</c>.</returns>
public bool Raycast(Vector3 parStart, Vector3 parDirection, HashSet<string> parColliderGroups,
[MaybeNullWhen(false)] out RaycastResult parResult)
{
@@ -90,6 +81,48 @@ public class CollisionManagerComponent : Engine.Scene.Component.Component
return parResult != null;
}
/// <summary>
/// Performs a collision check between two colliders and invokes the appropriate collision events.
/// </summary>
/// <param name="parColliderA">The first collider.</param>
/// <param name="parColliderB">The second collider.</param>
private static void PerformCollisionCheck(AABBColliderComponent parColliderA, AABBColliderComponent parColliderB)
{
var canCollideAB = parColliderA.ExcludeColliderCollideGroups.Count == 0 ||
!parColliderB.ColliderGroups.Overlaps(parColliderA.ExcludeColliderCollideGroups);
var canCollideBA = parColliderB.ExcludeColliderCollideGroups.Count == 0 ||
!parColliderA.ColliderGroups.Overlaps(parColliderB.ExcludeColliderCollideGroups);
if (!canCollideAB && !canCollideBA)
{
return;
}
if (!parColliderA.Collider.Intersects(parColliderB.Collider))
{
return;
}
if (canCollideAB)
{
parColliderA.CollideWith(parColliderB);
}
if (canCollideBA)
{
parColliderB.CollideWith(parColliderA);
}
}
/// <summary>
/// Checks for intersection between a ray and an AABB collider.
/// </summary>
/// <param name="parOrigin">The origin of the ray.</param>
/// <param name="parDirection">The direction of the ray.</param>
/// <param name="parCollider">The collider to test against.</param>
/// <param name="parHitPoint">The intersection point, if the ray intersects.</param>
/// <param name="parHitNormal">The normal of the intersection surface.</param>
/// <returns><c>true</c> if the ray intersects the collider; otherwise, <c>false</c>.</returns>
private static bool RaycastAABB(Vector3 parOrigin, Vector3 parDirection, AABBCollider parCollider,
out Vector3 parHitPoint,
out Vector3 parHitNormal)

View File

@@ -2,11 +2,24 @@
namespace DoomDeathmatch.Component.Physics;
/// <summary>
/// Applies drag to a rigidbody to simulate resistance to motion.
/// </summary>
public class DragComponent : Engine.Scene.Component.Component
{
/// <summary>
/// The drag coefficient applied to the velocity of the rigidbody.
/// </summary>
public float Drag { get; set; } = 1f;
/// <summary>
/// A multiplier applied to each axis of the velocity when calculating drag.
/// </summary>
public Vector3 Multiplier { get; set; } = Vector3.One;
/// <summary>
/// The rigidbody to apply drag to.
/// </summary>
private RigidbodyComponent _rigidbody = null!;
public override void Awake()

View File

@@ -2,24 +2,45 @@
namespace DoomDeathmatch.Component.Physics;
/// <summary>
/// Simulates the effect of gravity on a rigidbody.
/// </summary>
public class GravityComponent : Engine.Scene.Component.Component
{
/// <summary>
/// Indicates whether the object is currently in the air.
/// </summary>
public bool IsInAir { get; private set; }
/// <summary>
/// The strength of the gravitational force.
/// </summary>
public float Strength { get; set; } = 10.0f;
/// <summary>
/// The direction of the gravitational force.
/// </summary>
public Vector3 Direction
{
get => _direction;
set => _direction = value.Normalized();
}
/// <summary>
/// The height at which gravity stops affecting the object.
/// </summary>
public float Floor { get; set; } = 5.0f;
private RigidbodyComponent _rigidbody = null!;
/// <summary>
/// The direction of the gravitational force.
/// </summary>
private Vector3 _direction = -Vector3.UnitZ;
/// <summary>
/// The rigidbody to apply gravity to.
/// </summary>
private RigidbodyComponent _rigidbody = null!;
public override void Awake()
{
_rigidbody = GameObject.GetComponent<RigidbodyComponent>()!;

View File

@@ -2,11 +2,24 @@
namespace DoomDeathmatch.Component.Physics;
/// <summary>
/// Represents a rigidbody component that simulates physics behavior such as force, velocity, and acceleration.
/// </summary>
public class RigidbodyComponent : Engine.Scene.Component.Component
{
/// <summary>
/// The mass of the rigidbody.
/// </summary>
public float Mass { get; set; } = 1.0f;
/// <summary>
/// Indicates whether the rigidbody is static and unaffected by forces.
/// </summary>
public bool IsStatic { get; set; } = false;
/// <summary>
/// The force currently applied to the rigidbody.
/// </summary>
public Vector3 Force
{
get => _force;
@@ -21,6 +34,9 @@ public class RigidbodyComponent : Engine.Scene.Component.Component
}
}
/// <summary>
/// The velocity of the rigidbody.
/// </summary>
public Vector3 Velocity
{
get => _velocity;
@@ -35,11 +51,22 @@ public class RigidbodyComponent : Engine.Scene.Component.Component
}
}
/// <summary>
/// The force currently applied to the rigidbody.
/// </summary>
private Vector3 _force = Vector3.Zero;
private Vector3 _velocity = Vector3.Zero;
/// <summary>
/// The acceleration of the rigidbody.
/// </summary>
private Vector3 _acceleration = Vector3.Zero;
public override void Update(double parDeltaTime)
/// <summary>
/// The velocity of the rigidbody.
/// </summary>
private Vector3 _velocity = Vector3.Zero;
public override void PostUpdate(double parDeltaTime)
{
if (IsStatic)
{

View File

@@ -2,20 +2,39 @@
namespace DoomDeathmatch.Component.UI;
/// <summary>
/// A component responsible for managing and selecting menu items in a UI.
/// </summary>
public class MenuControllerComponent : Engine.Scene.Component.Component
{
/// <summary>
/// A dictionary of menu items, mapping their names to their game objects.
/// </summary>
private readonly Dictionary<string, GameObject> _menuItems = new();
/// <summary>
/// Adds a new menu item to the menu.
/// </summary>
/// <param name="parName">The name of the menu item.</param>
/// <param name="parGameObject">The game object representing the menu item.</param>
public void AddMenuItem(string parName, GameObject parGameObject)
{
_menuItems.Add(parName, parGameObject);
}
/// <summary>
/// Removes a menu item from the menu by name.
/// </summary>
/// <param name="parName">The name of the menu item to remove.</param>
public void RemoveMenuItem(string parName)
{
_menuItems.Remove(parName);
}
/// <summary>
/// Selects a menu item by name, enabling only the selected item.
/// </summary>
/// <param name="parName">The name of the menu item to select.</param>
public void SelectMenuItem(string parName)
{
foreach (var (name, menuItem) in _menuItems)

View File

@@ -3,17 +3,44 @@ using Engine.Util;
namespace DoomDeathmatch.Component.UI;
/// <summary>
/// A component that manages selection of UI components using keyboard inputs.
/// </summary>
public class SelectorComponent : Engine.Scene.Component.Component
{
/// <summary>
/// Event triggered when a UI component is selected.
/// </summary>
public event Action<UiComponent>? OnSelect;
/// <summary>
/// The list of selectable UI components.
/// </summary>
public List<UiComponent> Children { get; } = [];
/// <summary>
/// The key used to navigate to the next item in the list.
/// </summary>
public KeyboardButtonCode NextKey { get; set; } = KeyboardButtonCode.Down;
/// <summary>
/// The key used to navigate to the previous item in the list.
/// </summary>
public KeyboardButtonCode PrevKey { get; set; } = KeyboardButtonCode.Up;
/// <summary>
/// The key used to select the currently selected item.
/// </summary>
public KeyboardButtonCode SelectKey { get; set; } = KeyboardButtonCode.Space;
/// <summary>
/// The input handler used to check for keyboard input.
/// </summary>
private readonly IInputHandler _inputHandler = EngineUtil.InputHandler;
/// <summary>
/// The index of the currently selected item.
/// </summary>
private int _selectedIndex;
public override void Start()
@@ -58,11 +85,17 @@ public class SelectorComponent : Engine.Scene.Component.Component
UpdatePosition();
}
/// <summary>
/// Triggers the <see cref="OnSelect"/> event for the currently selected child.
/// </summary>
private void SelectionChanged()
{
OnSelect?.Invoke(Children[_selectedIndex]);
}
/// <summary>
/// Updates the position of the selector based on the selected child component.
/// </summary>
private void UpdatePosition()
{
var child = Children[_selectedIndex];
@@ -77,6 +110,10 @@ public class SelectorComponent : Engine.Scene.Component.Component
GameObject.Transform.Size.Y = scale.Y;
}
/// <summary>
/// Selects a UI component when the mouse hovers over it.
/// </summary>
/// <param name="parComponent">The UI component to select.</param>
private void Select(UiComponent parComponent)
{
var index = Children.IndexOf(parComponent);

View File

@@ -3,9 +3,19 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.UI;
/// <summary>
/// A UI container that arranges its child components in a stack, either horizontally or vertically.
/// </summary>
public class StackComponent : UiContainerComponent
{
/// <summary>
/// The child components contained in this stack.
/// </summary>
public List<UiComponent> Children { get; } = [];
/// <summary>
/// The orientation of the stack, which determines whether the components are arranged horizontally or vertically.
/// </summary>
public Orientation Orientation { get; set; } = Orientation.Vertical;
public override void Update(double parDeltaTime)

View File

@@ -4,11 +4,24 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.UI;
/// <summary>
/// A component that handles text alignment for UI elements that use text rendering.
/// </summary>
public class TextAlignComponent : Engine.Scene.Component.Component
{
/// <summary>
/// The alignment of the text within its bounding box.
/// </summary>
public Align Alignment { get; set; } = Align.Left;
/// <summary>
/// The text renderer used to measure the text size.
/// </summary>
private TextRenderer _textRenderer = null!;
/// <summary>
/// The cached text to avoid unnecessary measurements.
/// </summary>
private string? _cachedText;
public override void Awake()
@@ -36,6 +49,11 @@ public class TextAlignComponent : Engine.Scene.Component.Component
GameObject.Transform.Translation.Xy = offset;
}
/// <summary>
/// Calculates the offset for the text alignment based on the given size.
/// </summary>
/// <param name="parSize">The size of the text.</param>
/// <returns>The calculated offset.</returns>
public Vector2 GetOffset(Vector2 parSize)
{
return Alignment switch

View File

@@ -3,26 +3,60 @@ using Engine.Util;
namespace DoomDeathmatch.Component.UI;
/// <summary>
/// A component that handles text input from the user, such as typing and backspacing.
/// </summary>
public class TextInputComponent : Engine.Scene.Component.Component
{
/// <summary>
/// Event triggered when there is new input.
/// </summary>
public event Action<string>? OnInput;
/// <summary>
/// Indicates whether the input component is active and accepting input.
/// </summary>
public bool IsActive { get; set; } = false;
/// <summary>
/// The current input string the user has typed.
/// </summary>
public string Input { get; private set; } = "";
/// <summary>
/// The time delay before input is registered after a key is pressed.
/// </summary>
public float InputDelay
{
get => (float)_inputTimer.TotalTime;
set => _inputTimer.TotalTime = value;
}
/// <summary>
/// The keys that are accepted as input.
/// </summary>
private readonly KeyboardButtonCode[] _acceptedKeys =
KeyboardButtonCodeHelper.GetAllPrintableKeys().Append(KeyboardButtonCode.Backspace).ToArray();
/// <summary>
/// The input handler used to check for keyboard input.
/// </summary>
private readonly IInputHandler _inputHandler = EngineUtil.InputHandler;
/// <summary>
/// The timer used to delay input.
/// </summary>
private readonly TickableTimer _inputTimer;
/// <summary>
/// The keys that were last pressed.
/// </summary>
private readonly HashSet<KeyboardButtonCode> _lastKeys = [];
/// <summary>
/// Initializes a new instance of the <see cref="TextInputComponent"/> class with an optional input delay.
/// </summary>
/// <param name="parInputDelay">The input delay before registering the next key press.</param>
public TextInputComponent(float parInputDelay = 0.2f)
{
_inputTimer = new TickableTimer(parInputDelay) { CurrentTime = 0 };

View File

@@ -6,16 +6,44 @@ using Math = System.Math;
namespace DoomDeathmatch.Component.UI;
/// <summary>
/// Represents a UI element that can interact with user input.
/// </summary>
public class UiComponent : Engine.Scene.Component.Component
{
/// <summary>
/// Invoked when this UI component is clicked.
/// </summary>
public event Action<UiComponent>? OnClick;
/// <summary>
/// Invoked when the mouse is hovering over this UI component.
/// </summary>
public event Action<UiComponent>? OnMouseOver;
/// <summary>
/// The parent container of this UI component.
/// </summary>
public UiContainerComponent? Container { get; set; }
/// <summary>
/// The alignment of this component's center within its container.
/// </summary>
public Anchor Center { get; set; } = Anchor.Center;
/// <summary>
/// The anchor point of this component relative to its container.
/// </summary>
public Anchor Anchor { get; set; } = Anchor.Center;
/// <summary>
/// The positional offset of this component relative to its anchor point.
/// </summary>
public Vector2 Offset { get; set; } = Vector2.Zero;
/// <summary>
/// The input handler used to check for mouse input.
/// </summary>
private readonly IInputHandler _inputHandler = EngineUtil.InputHandler;
public override void Update(double parDeltaTime)
@@ -26,8 +54,8 @@ public class UiComponent : Engine.Scene.Component.Component
}
var size = GameObject.Transform.Size * GameObject.Transform.Scale;
GameObject.Transform.Translation.Xy = GetAnchorPosition(Container.GameObject.Transform.Size.Xy, Anchor) + Offset -
GetAnchorPosition(size.Xy, Center);
GameObject.Transform.Translation.Xy = Anchor.GetPosition(Container.GameObject.Transform.Size.Xy) + Offset -
Center.GetPosition(size.Xy);
var transformMatrix = GameObject.Transform.FullTransformMatrix;
var actualSize = transformMatrix.ExtractScale();
@@ -46,30 +74,11 @@ public class UiComponent : Engine.Scene.Component.Component
}
}
/// <summary>
/// Manually triggers the click event for this component.
/// </summary>
public void InvokeClick()
{
OnClick?.Invoke(this);
}
private static Vector2 GetAnchorPosition(Vector2 parSize, Anchor parAnchor)
{
return parSize * GetAnchorRatio(parAnchor);
}
private static Vector2 GetAnchorRatio(Anchor parAnchor)
{
return parAnchor switch
{
Anchor.TopLeft => new Vector2(-0.5f, 0.5f),
Anchor.TopCenter => new Vector2(0, 0.5f),
Anchor.TopRight => new Vector2(0.5f, 0.5f),
Anchor.CenterLeft => new Vector2(-0.5f, 0),
Anchor.Center => new Vector2(0, 0),
Anchor.CenterRight => new Vector2(0.5f, 0),
Anchor.BottomLeft => new Vector2(-0.5f, -0.5f),
Anchor.BottomCenter => new Vector2(0, -0.5f),
Anchor.BottomRight => new Vector2(0.5f, -0.5f),
_ => throw new ArgumentOutOfRangeException(nameof(parAnchor), parAnchor, null)
};
}
}

View File

@@ -5,11 +5,24 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.UI;
/// <summary>
/// Represents a container for UI components, responsible for handling mouse position and camera interactions.
/// </summary>
public class UiContainerComponent : UiComponent
{
/// <summary>
/// The camera used to calculate the mouse position in world space.
/// </summary>
public Camera? Camera { get; set; }
/// <summary>
/// The current mouse position in world coordinates.
/// </summary>
public Vector3 MousePosition { get; private set; }
/// <summary>
/// The input handler used to check for mouse input.
/// </summary>
private readonly IInputHandler _inputHandler = EngineUtil.InputHandler;
public override void Update(double parDeltaTime)

View File

@@ -3,9 +3,19 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.Util;
/// <summary>
/// A component that aligns the orientation of an object to always face a target, similar to a billboard.
/// </summary>
public class BillboardComponent : Engine.Scene.Component.Component
{
/// <summary>
/// The target transform that the object will face.
/// </summary>
public Transform? Target { get; set; }
/// <summary>
/// The upward direction used as a reference for recalculating orientation.
/// </summary>
public Vector3 Up { get; set; } = Vector3.UnitZ;
public override void Update(double parDeltaTime)
@@ -17,15 +27,28 @@ public class BillboardComponent : Engine.Scene.Component.Component
var targetPosition = Target.GetFullTranslation();
var currentPosition = GameObject.Transform.GetFullTranslation();
var rotationMatrix = CalculateRotationMatrix(currentPosition, targetPosition, Up);
var forward = targetPosition - currentPosition;
forward -= Vector3.Dot(forward, Up) * Up;
GameObject.Transform.Rotation = Quaternion.FromMatrix(rotationMatrix);
}
/// <summary>
/// Calculates the rotation matrix required to align the object to face the target.
/// </summary>
/// <param name="parCurrentPosition">The current position of the object.</param>
/// <param name="parTargetPosition">The target position the object should face.</param>
/// <param name="parUp">The upward direction used for orientation adjustment.</param>
/// <returns>A <see cref="Matrix3"/> representing the required rotation.</returns>
private static Matrix3 CalculateRotationMatrix(Vector3 parCurrentPosition, Vector3 parTargetPosition, Vector3 parUp)
{
var forward = parTargetPosition - parCurrentPosition;
forward -= Vector3.Dot(forward, parUp) * parUp;
if (forward.LengthSquared > 0)
{
forward.Normalize();
}
var right = Vector3.Cross(Up, forward);
var right = Vector3.Cross(parUp, forward);
if (right.LengthSquared > 0)
{
right.Normalize();
@@ -39,6 +62,6 @@ public class BillboardComponent : Engine.Scene.Component.Component
right.Z, recalculatedUp.Z, forward.Z
);
GameObject.Transform.Rotation = Quaternion.FromMatrix(rotationMatrix);
return rotationMatrix;
}
}

View File

@@ -3,9 +3,19 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.Util;
/// <summary>
/// A component that synchronizes the size of an object with a target object's size, applying a multiplier.
/// </summary>
public class CopySizeComponent : Engine.Scene.Component.Component
{
/// <summary>
/// The target transform whose size is copied.
/// </summary>
public Transform? Target { get; set; }
/// <summary>
/// The multiplier applied to the target's size when copying.
/// </summary>
public Vector3 Multiplier { get; set; } = Vector3.One;
public override void Update(double parDeltaTime)

View File

@@ -4,8 +4,14 @@ using Engine.Util;
namespace DoomDeathmatch.Component.Util;
/// <summary>
/// Represents a fireball that deals damage on collision.
/// </summary>
public class FireballComponent : Engine.Scene.Component.Component
{
/// <summary>
/// The amount of damage the fireball inflicts on collision.
/// </summary>
public float Damage { get; set; }
public override void Awake()
@@ -17,6 +23,10 @@ public class FireballComponent : Engine.Scene.Component.Component
collider.OnCollision += OnCollision;
}
/// <summary>
/// Handles collision logic, dealing damage to colliding objects.
/// </summary>
/// <param name="parCollider">The collider of the object this fireball collided with.</param>
private void OnCollision(AABBColliderComponent parCollider)
{
var healthController = parCollider.GameObject.GetComponent<HealthController>();

View File

@@ -5,12 +5,24 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.Util;
/// <summary>
/// Handles player movement based on input and applies rotation to the player object.
/// </summary>
public class PlayerMovementComponent : Engine.Scene.Component.Component
{
/// <summary>
/// The speed at which the player rotates.
/// </summary>
public float RotationSpeed { get; set; } = 110.0f;
/// <summary>
/// Handles input from the player.
/// </summary>
private readonly IInputHandler _inputHandler = EngineUtil.InputHandler;
/// <summary>
/// Controls the movement logic of the player.
/// </summary>
private MovementController _movementController = null!;
public override void Awake()

View File

@@ -57,7 +57,7 @@ public static class PlayScene
var gameControllerObject = GameObjectUtil.CreateGameObject(scene, [
new GameController(menuController),
new TimerController(),
new TimerController(5 * 60),
plaTimerView,
new ScoreController(),
@@ -88,11 +88,10 @@ public static class PlayScene
var monsterSpawnPoints = MeshToSpawnPoints(
EngineUtil.AssetResourceManager.Load<Mesh>($"map/{parMapName}/monster_spawners.obj"));
var valuableSpawnPoints = MeshToSpawnPoints(
EngineUtil.AssetResourceManager.Load<Mesh>($"map/{parMapName}/valuable_spawners.obj"));
parScene.AddChild(mapObject, collidersObject);
#region Monster Objects
var monsterVector3ValueProvider =
new RandomListValueProvider<Vector3>(monsterSpawnPoints.Select(ConstValueProvider<Vector3>.Create));
@@ -114,6 +113,12 @@ public static class PlayScene
parScene.AddChild(parGameControllerObject, monsterSpawnerObject);
#endregion
#region Consumable Objects
var valuableSpawnPoints = MeshToSpawnPoints(
EngineUtil.AssetResourceManager.Load<Mesh>($"map/{parMapName}/valuable_spawners.obj"));
var consumableVector3ValueProvider =
new RandomListValueProvider<Vector3>(valuableSpawnPoints.Select(ConstValueProvider<Vector3>.Create));
@@ -157,6 +162,8 @@ public static class PlayScene
parScene.AddChild(parGameControllerObject, consumableSpawnerObject);
#endregion
return mapObject;
}

View File

@@ -3,17 +3,55 @@ using Engine.Util;
namespace DoomDeathmatch.Script;
public class AnimationPlayer<T>(float parInterval) : IUpdate
/// <summary>
/// Handles the playback of animations consisting of frames of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of the animation frames.</typeparam>
public class AnimationPlayer<T> : IUpdate
{
/// <summary>
/// Occurs when the animation finishes playing.
/// </summary>
public event Action? OnFinish;
/// <summary>
/// Occurs when the animation advances to a new frame.
/// </summary>
public event Action<T>? OnFrameChanged;
/// <summary>
/// Indicates whether the animation is currently playing.
/// </summary>
public bool IsPlaying { get; private set; }
/// <summary>
/// The list of frames in the animation.
/// </summary>
public List<T> Frames { get; init; } = [];
/// <summary>
/// The index of the next frame to be displayed.
/// </summary>
public int NextFrame { get; private set; }
private readonly TickableTimer _timer = new(parInterval);
/// <summary>
/// The timer used to control frame intervals.
/// </summary>
private readonly TickableTimer _timer;
/// <summary>
/// Initializes a new instance of the <see cref="AnimationPlayer{T}"/> class with a specified frame interval.
/// </summary>
/// <param name="parInterval">The total time in seconds for whole animation.</param>
public AnimationPlayer(float parInterval)
{
_timer = new TickableTimer(parInterval);
}
/// <summary>
/// Updates the animation's playback state based on the elapsed time.
/// </summary>
/// <param name="parDeltaTime">The time in seconds since the last update.</param>
public void Update(double parDeltaTime)
{
if (!IsPlaying)
@@ -37,12 +75,18 @@ public class AnimationPlayer<T>(float parInterval) : IUpdate
}
}
/// <summary>
/// Starts the animation playback from the beginning.
/// </summary>
public void Start()
{
Reset();
IsPlaying = true;
}
/// <summary>
/// Resets the animation to its initial state.
/// </summary>
public void Reset()
{
_timer.Reset();

View File

@@ -2,14 +2,36 @@
namespace DoomDeathmatch.Script.Collision;
/// <summary>
/// Represents an Axis-Aligned Bounding Box (AABB) collider for 3D collision detection.
/// </summary>
public class AABBCollider
{
/// <summary>
/// The position of the collider's center in 3D space.
/// </summary>
public Vector3 Position { get; set; }
/// <summary>
/// The size (width, height, depth) of the collider.
/// </summary>
public Vector3 Size { get; set; }
/// <summary>
/// The minimum point (corner) of the collider in 3D space.
/// </summary>
public Vector3 Min => Position - (Size / 2);
/// <summary>
/// The maximum point (corner) of the collider in 3D space.
/// </summary>
public Vector3 Max => Position + (Size / 2);
/// <summary>
/// Checks if this collider intersects with another AABB collider.
/// </summary>
/// <param name="parCollider">The other collider to check for intersection.</param>
/// <returns><see langword="true"/> if the colliders intersect; otherwise, <see langword="false"/>.</returns>
public bool Intersects(AABBCollider parCollider)
{
var max = Max;
@@ -21,6 +43,14 @@ public class AABBCollider
max.Z >= otherMin.Z && min.Z <= otherMax.Z;
}
/// <summary>
/// Calculates the collision normal between this collider and another collider.
/// </summary>
/// <param name="parOther">The other collider involved in the collision.</param>
/// <returns>
/// A <see cref="Vector3"/> representing the normal of the collision surface.
/// This indicates the direction of the collision resolution.
/// </returns>
public Vector3 GetCollisionNormal(AABBCollider parOther)
{
var normal = Vector3.Zero;

View File

@@ -3,11 +3,28 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Script.Collision;
/// <summary>
/// Represents the result of a raycast in 3D space.
/// </summary>
public class RaycastResult
{
/// <summary>
/// The distance from the ray's origin to the hit point.
/// </summary>
public float Distance { get; init; }
/// <summary>
/// The point in 3D space where the ray hit an object.
/// </summary>
public Vector3 HitPoint { get; init; }
/// <summary>
/// The normal vector at the surface of the hit object.
/// </summary>
public Vector3 Normal { get; init; }
/// <summary>
/// The game object that was hit by the ray.
/// </summary>
public GameObject HitObject { get; init; }
}

View File

@@ -2,11 +2,23 @@
namespace DoomDeathmatch.Script.Condition;
/// <summary>
/// Represents a condition that can be updated and triggers an event when it becomes true.
/// </summary>
public interface ICondition : IUpdate
{
/// <summary>
/// Occurs when the condition evaluates to true.
/// </summary>
public event Action? OnTrue;
/// <summary>
/// Indicates whether the condition is currently true.
/// </summary>
public bool IsTrue { get; }
/// <summary>
/// Resets the condition to its initial state.
/// </summary>
public void Reset();
}

View File

@@ -2,25 +2,49 @@
namespace DoomDeathmatch.Script.Condition;
/// <summary>
/// Represents a condition based on a timer that can be updated over time.
/// The condition becomes true when the timer finishes.
/// </summary>
public class TickableTimerCondition : ICondition
{
/// <summary>
/// Occurs when the timer finishes, causing the condition to become true.
/// </summary>
public event Action? OnTrue;
/// <summary>
/// Indicates whether the timer has finished, making the condition true.
/// </summary>
public bool IsTrue => _timer.IsFinished;
/// <summary>
/// The timer used to evaluate the condition's state.
/// </summary>
private readonly TickableTimer _timer;
/// <summary>
/// Initializes a new instance of the <see cref="TickableTimerCondition"/> class with a specified interval.
/// </summary>
/// <param name="parInterval">The interval in seconds for the timer.</param>
public TickableTimerCondition(float parInterval)
{
_timer = new TickableTimer(parInterval);
_timer.OnFinished += () => OnTrue?.Invoke();
}
/// <summary>
/// Updates the timer with the elapsed time, progressing the condition's state.
/// </summary>
/// <param name="parDeltaTime">The time in seconds since the last update.</param>
public void Update(double parDeltaTime)
{
_timer.Update(parDeltaTime);
}
/// <summary>
/// Resets the timer and the condition to their initial state.
/// </summary>
public void Reset()
{
_timer.Reset();

View File

@@ -2,12 +2,32 @@
namespace DoomDeathmatch.Script.Consumable;
public class HealthPackConsumable(float parHealth) : IConsumable
/// <summary>
/// Represents a health pack consumable that restores health to the player.
/// </summary>
/// <param name="parHealth">The amount of health restored by the consumable.</param>
public class HealthPackConsumable : IConsumable
{
/// <inheritdoc/>
public string Icon => "texture/health_pack.png";
/// <summary>
/// The amount of health restored by the consumable.
/// </summary>
private readonly float _health;
/// <summary>
/// Initializes a new instance of the <see cref="HealthPackConsumable"/> class with a specified health restoration value.
/// </summary>
/// <param name="parHealth">The amount of health this consumable restores.</param>
public HealthPackConsumable(float parHealth)
{
_health = parHealth;
}
/// <inheritdoc/>
public void Consume(PlayerController parPlayerController)
{
parPlayerController.HealthController.Heal(parHealth);
parPlayerController.HealthController.Heal(_health);
}
}

View File

@@ -2,9 +2,21 @@
namespace DoomDeathmatch.Script.Consumable;
/// <summary>
/// Represents a consumable item in the game.
/// Consumables can be used by a player to apply specific effects, such as healing or adding a weapon.
/// </summary>
public interface IConsumable
{
/// <summary>
/// The icon path associated with the consumable.
/// Used for displaying the consumable in the user interface.
/// </summary>
public string Icon { get; }
/// <summary>
/// Consumes the item and applies its effect to the specified player.
/// </summary>
/// <param name="parPlayerController">The player controller to apply the consumable effect to.</param>
public void Consume(PlayerController parPlayerController);
}

View File

@@ -3,12 +3,31 @@ using DoomDeathmatch.Script.Model.Weapon;
namespace DoomDeathmatch.Script.Consumable;
public class WeaponConsumable(WeaponData parWeaponData) : IConsumable
/// <summary>
/// Represents a weapon consumable that grants weapon or reloads ammo to the player.
/// </summary>
public class WeaponConsumable : IConsumable
{
public string Icon => parWeaponData.IdleTexture;
/// <inheritdoc/>
public string Icon => _weaponData.IdleTexture;
/// <summary>
/// The weapon data associated with this consumable.
/// </summary>
private readonly WeaponData _weaponData;
/// <summary>
/// Initializes a new instance of the <see cref="WeaponConsumable"/> class with the specified weapon data.
/// </summary>
/// <param name="parWeaponData">The weapon data associated with this consumable.</param>
public WeaponConsumable(WeaponData parWeaponData)
{
_weaponData = parWeaponData;
}
/// <inheritdoc/>
public void Consume(PlayerController parPlayerController)
{
parPlayerController.WeaponController.AddOrMergeWeapon(parWeaponData);
parPlayerController.WeaponController.AddOrMergeWeapon(_weaponData);
}
}

View File

@@ -0,0 +1,14 @@
namespace DoomDeathmatch.Script.MVC;
/// <summary>
/// Interface for view components.
/// </summary>
/// <typeparam name="T">The type of the model to display.</typeparam>
public interface IView<in T>
{
/// <summary>
/// Updates the view based on the specified model.
/// </summary>
/// <param name="parModel">The model to display.</param>
public void UpdateView(T parModel);
}

View File

@@ -3,10 +3,36 @@ using DoomDeathmatch.Component.MVC.Health;
namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public abstract class AttackBehavior(EnemyController parEnemyController, HealthController parHealthController)
/// <summary>
/// Represents the base class for defining enemy attack behaviors.
/// </summary>
public abstract class AttackBehavior
{
protected readonly EnemyController _enemyController = parEnemyController;
protected readonly HealthController _healthController = parHealthController;
/// <summary>
/// The controller for the enemy performing the attack.
/// </summary>
protected readonly EnemyController _enemyController;
/// <summary>
/// The controller for the health of the target being attacked.
/// </summary>
protected readonly HealthController _healthController;
/// <summary>
/// Initializes a new instance of the <see cref="AttackBehavior"/> class.
/// </summary>
/// <param name="parEnemyController">The enemy controller.</param>
/// <param name="parHealthController">The health controller of the target.</param>
protected AttackBehavior(EnemyController parEnemyController, HealthController parHealthController)
{
_enemyController = parEnemyController;
_healthController = parHealthController;
}
/// <summary>
/// Performs the attack behavior.
/// </summary>
/// <param name="parDeltaTime">The time elapsed since the last update.</param>
/// <returns>True if the attack was successful; otherwise, false.</returns>
public abstract bool Attack(double parDeltaTime);
}

View File

@@ -3,22 +3,47 @@ using DoomDeathmatch.Component.MVC.Health;
namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public class CloseContinuousAttackBehavior(
EnemyController parEnemyController,
HealthController parHealthController,
float parRadius,
float parDamage)
: AttackBehavior(parEnemyController, parHealthController)
/// <summary>
/// Represents a behavior where the enemy continuously attacks when in close proximity.
/// </summary>
public class CloseContinuousAttackBehavior : AttackBehavior
{
/// <summary>
/// The attack radius within which the attack is triggered.
/// </summary>
private readonly float _radius;
/// <summary>
/// The amount of damage dealt per second.
/// </summary>
private readonly float _damage;
/// <summary>
/// Initializes a new instance of the <see cref="CloseContinuousAttackBehavior"/> class.
/// </summary>
/// <param name="parEnemyController">The enemy controller.</param>
/// <param name="parHealthController">The health controller of the target.</param>
/// <param name="parRadius">The attack radius.</param>
/// <param name="parDamage">The damage dealt per second.</param>
public CloseContinuousAttackBehavior(EnemyController parEnemyController,
HealthController parHealthController,
float parRadius,
float parDamage) : base(parEnemyController, parHealthController)
{
_radius = parRadius;
_damage = parDamage;
}
/// <inheritdoc />
public override bool Attack(double parDeltaTime)
{
var distanceSquared =
(_enemyController.GameObject.Transform.Translation - _healthController.GameObject.Transform.Translation)
.LengthSquared;
if (distanceSquared <= parRadius * parRadius)
if (distanceSquared <= _radius * _radius)
{
_healthController.TakeDamage(parDamage * (float)parDeltaTime);
_healthController.TakeDamage(_damage * (float)parDeltaTime);
return true;
}

View File

@@ -3,26 +3,53 @@ using DoomDeathmatch.Component.MVC.Health;
namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public class CloseCooldownAttackBehavior(
EnemyController parEnemyController,
HealthController parHealthController,
float parRadius,
float parCooldown,
float parDamage)
: CooldownAttackBehavior(parEnemyController, parHealthController, parCooldown)
/// <summary>
/// Represents a behavior where the enemy performs a cooldown-based close-range attack.
/// </summary>
public class CloseCooldownAttackBehavior : CooldownAttackBehavior
{
/// <summary>
/// The attack radius within which the attack is triggered.
/// </summary>
private readonly float _radius;
/// <summary>
/// The damage dealt by the attack.
/// </summary>
private readonly float _damage;
/// <summary>
/// Initializes a new instance of the <see cref="CloseCooldownAttackBehavior"/> class.
/// </summary>
/// <param name="parEnemyController">The enemy controller.</param>
/// <param name="parHealthController">The health controller of the target.</param>
/// <param name="parRadius">The attack radius.</param>
/// <param name="parCooldown">The cooldown duration.</param>
/// <param name="parDamage">The damage dealt by the attack.</param>
public CloseCooldownAttackBehavior(EnemyController parEnemyController,
HealthController parHealthController,
float parRadius,
float parCooldown,
float parDamage) : base(parEnemyController, parHealthController, parCooldown)
{
_radius = parRadius;
_damage = parDamage;
}
/// <inheritdoc />
protected override bool CanAttack()
{
var distanceSquared =
(_enemyController.GameObject.Transform.Translation - _healthController.GameObject.Transform.Translation)
.LengthSquared;
return distanceSquared <= parRadius * parRadius;
return distanceSquared <= _radius * _radius;
}
/// <inheritdoc />
protected override bool ActivateAttack()
{
_healthController.TakeDamage(parDamage);
_healthController.TakeDamage(_damage);
return true;
}

View File

@@ -3,17 +3,35 @@ using DoomDeathmatch.Component.MVC.Health;
namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public class CompositeAttackBehavior(
EnemyController parEnemyController,
HealthController parHealthController,
List<AttackBehavior> parBehaviors)
: AttackBehavior(parEnemyController, parHealthController)
/// <summary>
/// Represents a composite attack behavior combining multiple attack behaviors.
/// </summary>
public class CompositeAttackBehavior : AttackBehavior
{
/// <summary>
/// A collection of individual attack behaviors.
/// </summary>
private readonly List<AttackBehavior> _behaviors;
/// <summary>
/// Initializes a new instance of the <see cref="CompositeAttackBehavior"/> class.
/// </summary>
/// <param name="parEnemyController">The enemy controller.</param>
/// <param name="parHealthController">The health controller of the target.</param>
/// <param name="parBehaviors">The collection of attack behaviors.</param>
public CompositeAttackBehavior(EnemyController parEnemyController,
HealthController parHealthController,
IEnumerable<AttackBehavior> parBehaviors) : base(parEnemyController, parHealthController)
{
_behaviors = parBehaviors.ToList();
}
/// <inheritdoc />
public override bool Attack(double parDeltaTime)
{
var result = false;
foreach (var behavior in parBehaviors)
foreach (var behavior in _behaviors)
{
result |= behavior.Attack(parDeltaTime);
}

View File

@@ -4,14 +4,30 @@ using Engine.Util;
namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public abstract class CooldownAttackBehavior(
EnemyController parEnemyController,
HealthController parHealthController,
float parCooldown)
: AttackBehavior(parEnemyController, parHealthController)
/// <summary>
/// Represents a base class for attacks with cooldown functionality.
/// </summary>
public abstract class CooldownAttackBehavior : AttackBehavior
{
private readonly TickableTimer _tickableTimer = new(parCooldown);
/// <summary>
/// Timer that tracks the cooldown duration between attacks.
/// </summary>
private readonly TickableTimer _tickableTimer;
/// <summary>
/// Initializes a new instance of the <see cref="CooldownAttackBehavior"/> class.
/// </summary>
/// <param name="parEnemyController">The enemy controller.</param>
/// <param name="parHealthController">The health controller of the target.</param>
/// <param name="parCooldown">The cooldown duration.</param>
protected CooldownAttackBehavior(EnemyController parEnemyController,
HealthController parHealthController,
float parCooldown) : base(parEnemyController, parHealthController)
{
_tickableTimer = new TickableTimer(parCooldown);
}
/// <inheritdoc />
public sealed override bool Attack(double parDeltaTime)
{
_tickableTimer.Update(parDeltaTime);
@@ -33,6 +49,15 @@ public abstract class CooldownAttackBehavior(
return false;
}
/// <summary>
/// Determines whether the attack can be performed.
/// </summary>
/// <returns>True if the attack can be performed; otherwise, false.</returns>
protected abstract bool CanAttack();
/// <summary>
/// Executes the attack logic.
/// </summary>
/// <returns>True if the attack was successful; otherwise, false.</returns>
protected abstract bool ActivateAttack();
}

View File

@@ -3,11 +3,31 @@ using DoomDeathmatch.Component.MVC.Health;
namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public class FuncAttackBehaviorCreator(Func<EnemyController, HealthController, AttackBehavior> parFunc)
: IAttackBehaviorCreator
/// <summary>
/// A creator that uses a function to generate instances of <see cref="AttackBehavior"/>.
/// </summary>
public class FuncAttackBehaviorCreator : IAttackBehaviorCreator
{
/// <summary>
/// The function used to create <see cref="AttackBehavior"/> instances.
/// </summary>
private readonly Func<EnemyController, HealthController, AttackBehavior> _func;
/// <summary>
/// Initializes a new instance of the <see cref="FuncAttackBehaviorCreator"/> class.
/// </summary>
/// <param name="parFunc">
/// A function that takes an <see cref="EnemyController"/> and <see cref="HealthController"/>
/// and returns a new instance of <see cref="AttackBehavior"/>.
/// </param>
public FuncAttackBehaviorCreator(Func<EnemyController, HealthController, AttackBehavior> parFunc)
{
_func = parFunc;
}
/// <inheritdoc/>
public AttackBehavior Create(EnemyController parEnemyController, HealthController parHealthController)
{
return parFunc(parEnemyController, parHealthController);
return _func(parEnemyController, parHealthController);
}
}

View File

@@ -3,7 +3,16 @@ using DoomDeathmatch.Component.MVC.Health;
namespace DoomDeathmatch.Script.Model.Enemy.Attack;
/// <summary>
/// Interface for creating instances of <see cref="AttackBehavior"/>.
/// </summary>
public interface IAttackBehaviorCreator
{
/// <summary>
/// Creates a new instance of an <see cref="AttackBehavior"/>.
/// </summary>
/// <param name="parEnemyController">The enemy controller for the behavior.</param>
/// <param name="parHealthController">The health controller for the target.</param>
/// <returns>An instance of <see cref="AttackBehavior"/>.</returns>
public AttackBehavior Create(EnemyController parEnemyController, HealthController parHealthController);
}

View File

@@ -5,22 +5,42 @@ using Engine.Util;
namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public class ObjectSpawnAttackBehavior(
EnemyController parEnemyController,
HealthController parHealthController,
float parCooldown,
Func<EnemyController, HealthController, GameObject> parObjectSpawnFunc
)
: CooldownAttackBehavior(parEnemyController, parHealthController, parCooldown)
/// <summary>
/// Represents a behavior where an object is spawned as part of the attack.
/// </summary>
public class ObjectSpawnAttackBehavior : CooldownAttackBehavior
{
/// <summary>
/// The function used to spawn an object based on the enemy and target controllers.
/// </summary>
private readonly Func<EnemyController, HealthController, GameObject> _objectSpawnFunc;
/// <summary>
/// Initializes a new instance of the <see cref="ObjectSpawnAttackBehavior"/> class.
/// </summary>
/// <param name="parEnemyController">The enemy controller.</param>
/// <param name="parHealthController">The health controller of the target.</param>
/// <param name="parCooldown">The cooldown duration between spawns.</param>
/// <param name="parObjectSpawnFunc">The function used to create the spawned object.</param>
public ObjectSpawnAttackBehavior(EnemyController parEnemyController,
HealthController parHealthController,
float parCooldown,
Func<EnemyController, HealthController, GameObject> parObjectSpawnFunc) : base(parEnemyController,
parHealthController, parCooldown)
{
_objectSpawnFunc = parObjectSpawnFunc;
}
/// <inheritdoc />
protected override bool CanAttack()
{
return true;
}
/// <inheritdoc />
protected override bool ActivateAttack()
{
var enemyObject = parObjectSpawnFunc(_enemyController, _healthController);
var enemyObject = _objectSpawnFunc(_enemyController, _healthController);
EngineUtil.SceneManager.CurrentScene!.Add(enemyObject);
return true;

View File

@@ -6,8 +6,14 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Script.Model.Enemy;
/// <summary>
/// Represents data associated with an enemy type, including its stats, behavior, and appearance.
/// </summary>
public class EnemyData
{
/// <summary>
/// Data for the "Demon" enemy type.
/// </summary>
public static EnemyData Demon =>
new()
{
@@ -24,6 +30,9 @@ public class EnemyData
)
};
/// <summary>
/// Data for the "Imp" enemy type.
/// </summary>
public static EnemyData Imp =>
new()
{
@@ -61,25 +70,58 @@ public class EnemyData
)
};
/// <summary>
/// Unique identifier for the enemy type.
/// </summary>
public string Id { get; private init; } = "";
/// <summary>
/// Display name for the enemy type.
/// </summary>
public string Name { get; private init; } = "";
/// <summary>
/// Path to the texture file used to render the enemy.
/// </summary>
public string Texture { get; private init; } = "";
/// <summary>
/// The base health of the enemy.
/// </summary>
public float BaseHealth { get; private init; }
/// <summary>
/// The base score awarded for defeating this enemy.
/// </summary>
public int BaseScore { get; private init; }
/// <summary>
/// The base speed of the enemy.
/// </summary>
public float BaseSpeed { get; private init; }
/// <summary>
/// The movement behavior defining how the enemy moves.
/// </summary>
public IMovementBehavior MovementBehavior { get; private init; }
/// <summary>
/// The creator for the enemy's attack behavior.
/// </summary>
public IAttackBehaviorCreator AttackBehaviorCreator { get; private init; }
/// <summary>
/// Private constructor to prevent direct instantiation. Use the predefined static properties instead.
/// </summary>
private EnemyData() { }
/// <inheritdoc/>
public override bool Equals(object? parObj)
{
return parObj is EnemyData enemyData && Id == enemyData.Id;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(Id);

View File

@@ -2,11 +2,34 @@
namespace DoomDeathmatch.Script.Model.Enemy.Movement;
public class FollowPlayerMovementBehavior(float parRadius) : IMovementBehavior
/// <summary>
/// Movement behavior where the enemy follows the player while maintaining a specific radius.
/// </summary>
public class FollowPlayerMovementBehavior : IMovementBehavior
{
/// <summary>
/// The radius to maintain between the enemy and the player.
/// </summary>
private readonly float _radius;
/// <summary>
/// Initializes a new instance of the <see cref="FollowPlayerMovementBehavior"/> class.
/// </summary>
/// <param name="parRadius">The radius to maintain between the enemy and the player.</param>
public FollowPlayerMovementBehavior(float parRadius)
{
_radius = parRadius;
}
/// <summary>
/// Calculates the next position for the enemy, keeping it at the specified radius from the player.
/// </summary>
/// <param name="parPosition">The current position of the enemy.</param>
/// <param name="parPlayerPosition">The current position of the player.</param>
/// <returns>The next position of the enemy, maintaining the specified radius from the player.</returns>
public Vector3 GetNextPosition(Vector3 parPosition, Vector3 parPlayerPosition)
{
var direction = (parPosition - parPlayerPosition).Normalized();
return parPlayerPosition + (parRadius * direction);
return parPlayerPosition + (_radius * direction);
}
}

View File

@@ -2,7 +2,16 @@
namespace DoomDeathmatch.Script.Model.Enemy.Movement;
/// <summary>
/// Interface for defining enemy movement behavior.
/// </summary>
public interface IMovementBehavior
{
/// <summary>
/// Calculates the next position for the enemy based on the current position and the player's position.
/// </summary>
/// <param name="parPosition">The current position of the enemy.</param>
/// <param name="parPlayerPosition">The current position of the player.</param>
/// <returns>The next position of the enemy.</returns>
public Vector3 GetNextPosition(Vector3 parPosition, Vector3 parPlayerPosition);
}

View File

@@ -2,8 +2,17 @@
namespace DoomDeathmatch.Script.Model.Enemy.Movement;
/// <summary>
/// Movement behavior where the enemy remains stationary.
/// </summary>
public class StandingMovementBehavior : IMovementBehavior
{
/// <summary>
/// Returns the current position of the enemy without any change, as the enemy does not move.
/// </summary>
/// <param name="parPosition">The current position of the enemy.</param>
/// <param name="parPlayerPosition">The current position of the player (ignored).</param>
/// <returns>The current position of the enemy.</returns>
public Vector3 GetNextPosition(Vector3 parPosition, Vector3 parPlayerPosition)
{
return parPosition;

View File

@@ -1,9 +1,20 @@
namespace DoomDeathmatch.Script.Model;
/// <summary>
/// Represents the health state of a player or entity in the game.
/// Provides functionality to track and modify health, and notify listeners when health changes.
/// </summary>
public class HealthModel
{
/// <summary>
/// Occurs when the health value changes.
/// </summary>
public event Action<HealthModel>? HealthChanged;
/// <summary>
/// The current health value of the entity.
/// Health is clamped between 0 and <see cref="MaxHealth"/>.
/// </summary>
public float Health
{
get => _health;
@@ -20,6 +31,10 @@ public class HealthModel
}
}
/// <summary>
/// The maximum health value of the entity.
/// When modified, the current health is clamped between 0 and the new <see cref="MaxHealth"/>.
/// </summary>
public float MaxHealth
{
get => _maxHealth;
@@ -31,9 +46,21 @@ public class HealthModel
}
}
/// <summary>
/// The current health value of the entity.
/// </summary>
private float _health;
/// <summary>
/// The maximum health value of the entity.
/// </summary>
private float _maxHealth;
/// <summary>
/// Initializes a new instance of the <see cref="HealthModel"/> class with a specified maximum health.
/// Sets the initial health to the maximum health.
/// </summary>
/// <param name="parMaxHealth">The maximum health value.</param>
public HealthModel(float parMaxHealth)
{
MaxHealth = parMaxHealth;

View File

@@ -1,9 +1,20 @@
namespace DoomDeathmatch.Script.Model;
/// <summary>
/// Represents the score state of a player or entity in the game.
/// Provides functionality to track and modify the score, and notify listeners when the score changes.
/// </summary>
public class ScoreModel
{
/// <summary>
/// Occurs when the score value changes.
/// </summary>
public event Action<ScoreModel>? ScoreChanged;
/// <summary>
/// The current score of the player or entity.
/// The score cannot be negative.
/// </summary>
public int Score
{
get => _score;
@@ -14,5 +25,8 @@ public class ScoreModel
}
}
/// <summary>
/// The current score of the player or entity.
/// </summary>
private int _score;
}

View File

@@ -2,7 +2,17 @@
namespace DoomDeathmatch.Script.Model.Weapon;
/// <summary>
/// Defines a shooting pattern for a weapon.
/// </summary>
public interface IShootPattern
{
/// <summary>
/// Computes the shooting pattern based on directional vectors.
/// </summary>
/// <param name="parForward">The forward direction of the weapon.</param>
/// <param name="parUp">The upward direction of the weapon.</param>
/// <param name="parRight">The rightward direction of the weapon.</param>
/// <returns>A collection of directional offsets representing the shoot pattern.</returns>
public IEnumerable<Vector3> GetShootPattern(Vector3 parForward, Vector3 parUp, Vector3 parRight);
}

View File

@@ -2,8 +2,18 @@
namespace DoomDeathmatch.Script.Model.Weapon;
/// <summary>
/// Represents a simple linear shooting pattern.
/// </summary>
public class LineShootPattern : IShootPattern
{
/// <summary>
/// Computes a linear shooting pattern, which always shoots straight ahead.
/// </summary>
/// <param name="parForward">The forward direction of the weapon.</param>
/// <param name="parUp">The upward direction of the weapon.</param>
/// <param name="parRight">The rightward direction of the weapon.</param>
/// <returns>A collection containing a single zero vector, indicating no offset.</returns>
public IEnumerable<Vector3> GetShootPattern(Vector3 parForward, Vector3 parUp, Vector3 parRight)
{
return [Vector3.Zero];

View File

@@ -2,15 +2,49 @@
namespace DoomDeathmatch.Script.Model.Weapon;
public class RandomFlatSpreadShootPattern(float parAngle, uint parCount) : IShootPattern
/// <summary>
/// Represents a shooting pattern with a random flat spread.
/// </summary>
public class RandomFlatSpreadShootPattern : IShootPattern
{
/// <summary>
/// The random number generator for calculating spread angles.
/// </summary>
private readonly Random _random = new();
/// <summary>
/// The maximum angle of spread in radians.
/// </summary>
private readonly float _angle;
/// <summary>
/// The number of projectiles in the spread.
/// </summary>
private readonly uint _count;
/// <summary>
/// Initializes a new instance of the <see cref="RandomFlatSpreadShootPattern"/> class.
/// </summary>
/// <param name="parAngle">The maximum angle of spread in radians.</param>
/// <param name="parCount">The number of projectiles in the spread.</param>
public RandomFlatSpreadShootPattern(float parAngle, uint parCount)
{
_angle = parAngle;
_count = parCount;
}
/// <summary>
/// Computes a random flat spread shooting pattern.
/// </summary>
/// <param name="parForward">The forward direction of the weapon.</param>
/// <param name="parUp">The upward direction of the weapon.</param>
/// <param name="parRight">The rightward direction of the weapon.</param>
/// <returns>A collection of directional offsets representing the spread pattern.</returns>
public IEnumerable<Vector3> GetShootPattern(Vector3 parForward, Vector3 parUp, Vector3 parRight)
{
for (var i = 0; i < parCount; i++)
for (var i = 0; i < _count; i++)
{
var angle = parAngle * (((float)_random.NextDouble() * 2) - 1);
var angle = _angle * (((float)_random.NextDouble() * 2) - 1);
var delta = MathF.Tan(angle);
var offset = parRight * delta;

View File

@@ -2,8 +2,14 @@
namespace DoomDeathmatch.Script.Model.Weapon;
/// <summary>
/// Represents data associated with a weapon, including its stats, animations, and shooting pattern.
/// </summary>
public class WeaponData
{
/// <summary>
/// Data for the "Pistol" weapon.
/// </summary>
public static WeaponData Pistol =>
new(30)
{
@@ -19,6 +25,9 @@ public class WeaponData
ShootPattern = new LineShootPattern()
};
/// <summary>
/// Data for the "Shotgun" weapon.
/// </summary>
public static WeaponData Shotgun =>
new(10)
{
@@ -31,12 +40,24 @@ public class WeaponData
ShootPattern = new RandomFlatSpreadShootPattern(MathHelper.DegreesToRadians(10), 40)
};
/// <summary>
/// Triggered when the ammo of the weapon changes.
/// </summary>
public event Action<WeaponData>? OnAmmoChanged;
/// <summary>
/// A unique identifier for the weapon.
/// </summary>
public string Id { get; private init; } = "";
/// <summary>
/// The display name of the weapon.
/// </summary>
public string Name { get; private init; } = "";
/// <summary>
/// The current ammo count of the weapon. Updates trigger the <see cref="OnAmmoChanged"/> event.
/// </summary>
public int Ammo
{
get => _ammo;
@@ -62,29 +83,58 @@ public class WeaponData
}
}
/// <summary>
/// The maximum ammo capacity of the weapon.
/// </summary>
public int MaxAmmo { get; }
/// <summary>
/// The base damage dealt by the weapon.
/// </summary>
public int Damage { get; private init; }
/// <summary>
/// The path to the texture used when the weapon is idle.
/// </summary>
public string IdleTexture { get; private init; } = "";
/// <summary>
/// The duration of the weapon's firing animation, in seconds.
/// </summary>
public float FireAnimationDuration { get; private init; }
/// <summary>
/// A list of textures used for the weapon's firing animation.
/// </summary>
public List<string> FireAnimation { get; } = [];
/// <summary>
/// The shooting pattern associated with the weapon.
/// </summary>
public IShootPattern ShootPattern { get; private init; }
/// <summary>
///The current ammo count of the weapon.
/// </summary>
private int _ammo;
/// <summary>
/// Initializes a new instance of the <see cref="WeaponData"/> class with the specified maximum ammo.
/// </summary>
/// <param name="parMaxAmmo">The maximum ammo capacity for the weapon.</param>
private WeaponData(int parMaxAmmo)
{
MaxAmmo = parMaxAmmo;
Ammo = MaxAmmo;
}
/// <inheritdoc/>
public override bool Equals(object? parObj)
{
return parObj is WeaponData weaponModel && Id == weaponModel.Id;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(Id);

View File

@@ -2,13 +2,31 @@
namespace DoomDeathmatch.Script.Model;
/// <summary>
/// Represents the weapon state of a player or entity in the game.
/// Provides functionality to track available weapons, the selected weapon, and notify listeners when the selected weapon changes.
/// </summary>
public class WeaponModel
{
/// <summary>
/// Occurs when the selected weapon changes.
/// </summary>
public event Action<WeaponData, WeaponData>? OnWeaponSelected;
/// <summary>
/// The list of available weapons for the player or entity.
/// </summary>
public IList<WeaponData> Weapons => _weapons;
/// <summary>
/// The currently selected weapon.
/// </summary>
public WeaponData SelectedWeapon => _weapons[_selectedWeaponIndex];
/// <summary>
/// The index of the currently selected weapon in the weapons list.
/// When modified, triggers the <see cref="OnWeaponSelected"/> event with the old and new selected weapons.
/// </summary>
public int SelectedWeaponIndex
{
get => _selectedWeaponIndex;
@@ -27,7 +45,13 @@ public class WeaponModel
}
}
/// <summary>
/// The list of available weapons for the player or entity.
/// </summary>
private readonly List<WeaponData> _weapons = [WeaponData.Pistol];
/// <summary>
/// The index of the currently selected weapon in the weapons list.
/// </summary>
private int _selectedWeaponIndex;
}

View File

@@ -5,14 +5,37 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Script;
/// <summary>
/// Handles the spawning of game objects based on a condition and provided position and object data.
/// </summary>
public class ObjectSpawner : IUpdate
{
/// <summary>
/// Occurs when a new game object is spawned.
/// </summary>
public event Action<GameObject>? OnSpawned;
/// <summary>
/// Provides the game object to spawn.
/// </summary>
private readonly IValueProvider<GameObject> _gameObjectProvider;
/// <summary>
/// Provides the position where the game object will be spawned.
/// </summary>
private readonly IValueProvider<Vector3> _positionProvider;
/// <summary>
/// Determines when the spawner should spawn a new object.
/// </summary>
private readonly ICondition _condition;
/// <summary>
/// Initializes a new instance of the <see cref="ObjectSpawner"/> class.
/// </summary>
/// <param name="parGameObjectProvider">The provider for the game object to spawn.</param>
/// <param name="parPositionProvider">The provider for the position of the spawned object.</param>
/// <param name="parCondition">The condition that triggers the spawning.</param>
public ObjectSpawner(IValueProvider<GameObject> parGameObjectProvider, IValueProvider<Vector3> parPositionProvider,
ICondition parCondition)
{
@@ -23,11 +46,18 @@ public class ObjectSpawner : IUpdate
_condition.OnTrue += Spawn;
}
/// <summary>
/// Updates the state of the spawner, evaluating the condition to determine if a new object should spawn.
/// </summary>
/// <param name="parDeltaTime">The time elapsed since the last update, in seconds.</param>
public void Update(double parDeltaTime)
{
_condition.Update(parDeltaTime);
}
/// <summary>
/// Spawns a new game object at the position provided by the position provider.
/// </summary>
private void Spawn()
{
var gameObject = _gameObjectProvider.GetValue();

View File

@@ -1,9 +1,38 @@
namespace DoomDeathmatch.Script.Provider;
public class ConstValueProvider<T>(T parValue) : IValueProvider<T>
/// <summary>
/// Provides a constant value of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of value provided.</typeparam>
public class ConstValueProvider<T> : IValueProvider<T>
{
public T GetValue() => parValue;
/// <summary>
/// The value to provide.
/// </summary>
private readonly T _value;
/// <summary>
/// Initializes a new instance of <see cref="ConstValueProvider{T}"/>.
/// </summary>
/// <param name="parValue">The value to store.</param>
public ConstValueProvider(T parValue)
{
_value = parValue;
}
/// <inheritdoc />
public T GetValue() => _value;
/// <summary>
/// Creates a new instance of <see cref="ConstValueProvider{T}"/> with the specified value.
/// </summary>
/// <param name="parValue">The constant value to provide.</param>
/// <returns>A new instance of <see cref="ConstValueProvider{T}"/>.</returns>
public static ConstValueProvider<T> Create(T parValue) => new(parValue);
/// <summary>
/// Implicitly converts a value of type <typeparamref name="T"/> to a <see cref="ConstValueProvider{T}"/>.
/// </summary>
/// <param name="parValue">The value to convert.</param>
public static implicit operator ConstValueProvider<T>(T parValue) => new(parValue);
}

View File

@@ -1,9 +1,38 @@
namespace DoomDeathmatch.Script.Provider;
public class GeneratorValueProvider<T>(Func<T> parGenerator) : IValueProvider<T>
/// <summary>
/// Provides a value generated by a function.
/// </summary>
/// <typeparam name="T">The type of value provided.</typeparam>
public class GeneratorValueProvider<T> : IValueProvider<T>
{
public T GetValue() => parGenerator();
/// <summary>
/// A function used to generate values.
/// </summary>
private readonly Func<T> _generator;
/// <summary>
/// Initializes a new instance of <see cref="GeneratorValueProvider{T}"/>.
/// </summary>
/// <param name="parGenerator">The function used to generate values.</param>
public GeneratorValueProvider(Func<T> parGenerator)
{
_generator = parGenerator;
}
/// <inheritdoc />
public T GetValue() => _generator();
/// <summary>
/// Creates a new instance of <see cref="GeneratorValueProvider{T}"/> with the specified generator function.
/// </summary>
/// <param name="parGenerator">The function that generates values.</param>
/// <returns>A new instance of <see cref="GeneratorValueProvider{T}"/>.</returns>
public static GeneratorValueProvider<T> Create(Func<T> parGenerator) => new(parGenerator);
/// <summary>
/// Implicitly converts a function of type <see cref="Func{T}"/> to a <see cref="GeneratorValueProvider{T}"/>.
/// </summary>
/// <param name="parGenerator">The function to convert.</param>
public static implicit operator GeneratorValueProvider<T>(Func<T> parGenerator) => new(parGenerator);
}

View File

@@ -1,6 +1,14 @@
namespace DoomDeathmatch.Script.Provider;
/// <summary>
/// Represents a provider that supplies a value of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of value provided.</typeparam>
public interface IValueProvider<out T>
{
/// <summary>
/// Retrieves the value provided by this instance.
/// </summary>
/// <returns>The value of type <typeparamref name="T"/>.</returns>
public T GetValue();
}

View File

@@ -1,10 +1,25 @@
namespace DoomDeathmatch.Script.Provider;
/// <summary>
/// Provides a value randomly selected from a list of value providers.
/// </summary>
/// <typeparam name="T">The type of value provided.</typeparam>
public class RandomListValueProvider<T> : IValueProvider<T>
{
/// <summary>
/// A collection of value providers to choose from.
/// </summary>
private readonly List<IValueProvider<T>> _providers = [];
/// <summary>
/// The random number generator for selecting providers.
/// </summary>
private readonly Random _random = new();
/// <summary>
/// Initializes a new instance of <see cref="RandomListValueProvider{T}"/> with the specified providers.
/// </summary>
/// <param name="parProviders">The collection of value providers to select from.</param>
public RandomListValueProvider(IEnumerable<IValueProvider<T>> parProviders)
{
foreach (var provider in parProviders)
@@ -13,6 +28,7 @@ public class RandomListValueProvider<T> : IValueProvider<T>
}
}
/// <inheritdoc />
public T GetValue()
{
return _providers[_random.Next(_providers.Count)].GetValue();

View File

@@ -1,11 +1,31 @@
namespace DoomDeathmatch.Script.Provider;
/// <summary>
/// Provides a value randomly selected from weighted value providers.
/// </summary>
/// <typeparam name="T">The type of value provided.</typeparam>
public class WeightedRandomValueProvider<T> : IValueProvider<T>
{
/// <summary>
/// A collection of weighted value providers.
/// </summary>
private readonly List<(int, IValueProvider<T>)> _providers = [];
/// <summary>
/// The random number generator used to select a provider.
/// </summary>
private readonly Random _random = new();
/// <summary>
/// The total weight of all providers.
/// </summary>
private readonly int _totalWeight;
/// <summary>
/// Initializes a new instance of <see cref="WeightedRandomValueProvider{T}"/> with the specified providers.
/// </summary>
/// <param name="parProviders">The collection of weighted value providers.</param>
/// <exception cref="InvalidOperationException">Thrown if the total weight is less than or equal to 0.</exception>
public WeightedRandomValueProvider(IEnumerable<(int, IValueProvider<T>)> parProviders)
{
foreach (var (weight, provider) in parProviders)
@@ -20,6 +40,7 @@ public class WeightedRandomValueProvider<T> : IValueProvider<T>
}
}
/// <inheritdoc />
public T GetValue()
{
var random = _random.Next(_totalWeight);

View File

@@ -7,7 +7,7 @@ namespace DoomDeathmatch.Script.Score;
[JsonSerializable(typeof(ScoreTable))]
public class ScoreTable
{
public List<ScoreRow> Rows { get; } = new();
public List<ScoreRow> Rows { get; } = [];
private static readonly JsonSerializerOptions OPTIONS = new() { Converters = { new ScoreTableJsonConverter() } };

View File

@@ -1,8 +1,22 @@
namespace DoomDeathmatch.Script.UI;
/// <summary>
/// Specifies horizontal alignment options.
/// </summary>
public enum Align
{
/// <summary>
/// Align content to the left.
/// </summary>
Left,
/// <summary>
/// Align content to the center.
/// </summary>
Center,
/// <summary>
/// Align content to the right.
/// </summary>
Right
}
}

View File

@@ -1,14 +1,90 @@
namespace DoomDeathmatch.Script.UI;
using OpenTK.Mathematics;
namespace DoomDeathmatch.Script.UI;
/// <summary>
/// Specifies anchoring options for positioning elements.
/// </summary>
public enum Anchor
{
/// <summary>
/// Anchor to the top-left corner.
/// </summary>
TopLeft,
/// <summary>
/// Anchor to the top-center.
/// </summary>
TopCenter,
/// <summary>
/// Anchor to the top-right corner.
/// </summary>
TopRight,
/// <summary>
/// Anchor to the center-left.
/// </summary>
CenterLeft,
/// <summary>
/// Anchor to the center.
/// </summary>
Center,
/// <summary>
/// Anchor to the center-right.
/// </summary>
CenterRight,
/// <summary>
/// Anchor to the bottom-left corner.
/// </summary>
BottomLeft,
/// <summary>
/// Anchor to the bottom-center.
/// </summary>
BottomCenter,
/// <summary>
/// Anchor to the bottom-right corner.
/// </summary>
BottomRight
}
public static class AnchorHelper
{
/// <summary>
/// Retrieves the ratio of an anchor.
/// </summary>
/// <param name="parAnchor">The anchor to retrieve the ratio of.</param>
/// <returns>The ratio of the anchor.</returns>
public static Vector2 GetRatio(this Anchor parAnchor)
{
return parAnchor switch
{
Anchor.TopLeft => new Vector2(-0.5f, 0.5f),
Anchor.TopCenter => new Vector2(0, 0.5f),
Anchor.TopRight => new Vector2(0.5f, 0.5f),
Anchor.CenterLeft => new Vector2(-0.5f, 0),
Anchor.Center => new Vector2(0, 0),
Anchor.CenterRight => new Vector2(0.5f, 0),
Anchor.BottomLeft => new Vector2(-0.5f, -0.5f),
Anchor.BottomCenter => new Vector2(0, -0.5f),
Anchor.BottomRight => new Vector2(0.5f, -0.5f),
_ => throw new ArgumentOutOfRangeException(nameof(parAnchor), parAnchor, null)
};
}
/// <summary>
/// Retrieves the position of an anchor relative to a size.
/// </summary>
/// <param name="parAnchor">The anchor to retrieve the position of.</param>
/// <param name="parSize">The size to use for calculations.</param>
/// <returns>The position of the anchor relative to the size.</returns>
public static Vector2 GetPosition(this Anchor parAnchor, Vector2 parSize)
{
return parSize * parAnchor.GetRatio();
}
}

View File

@@ -1,7 +1,17 @@
namespace DoomDeathmatch.Script.UI;
/// <summary>
/// Specifies the orientation of UI elements.
/// </summary>
public enum Orientation
{
/// <summary>
/// Arrange elements horizontally.
/// </summary>
Horizontal,
/// <summary>
/// Arrange elements vertically.
/// </summary>
Vertical
}

View File

@@ -17,7 +17,7 @@ public partial class ProgramLoader : IResourceLoader
/// <returns>The loaded shader program.</returns>
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
var textReader = new StreamReader(parStreamProvider.GetStream(parPath));
using var textReader = new StreamReader(parStreamProvider.GetStream(parPath));
var vertexSource = new StringBuilder();
var fragmentSource = new StringBuilder();

View File

@@ -45,6 +45,14 @@ public abstract class Component : IUpdate, IRender
{
}
/// <summary>
/// Called after the main update loop.
/// </summary>
/// <param name="parDeltaTime">Time elapsed since the last frame.</param>
public virtual void PostUpdate(double parDeltaTime)
{
}
/// <summary>
/// Called during the main render loop.
/// </summary>

View File

@@ -151,6 +151,18 @@ public sealed class GameObject : IUpdate, IRender
}
}
/// <summary>
/// Performs post-update operations for all components.
/// </summary>
/// <param name="parDeltaTime">The time delta since the last update.</param>
public void PostUpdate(double parDeltaTime)
{
foreach (var component in _components)
{
component.PostUpdate(parDeltaTime);
}
}
/// <inheritdoc/>
public void Render()
{
@@ -181,9 +193,9 @@ public sealed class GameObject : IUpdate, IRender
/// </summary>
/// <typeparam name="T">The type of the component to retrieve.</typeparam>
/// <returns>The component if found, otherwise null.</returns>
public T? GetComponent<T>() where T : Component.Component
public T? GetComponent<T>() where T : class
{
if (!HasComponent<T>())
if (!typeof(T).IsInterface && !HasComponent<T>())
{
return null;
}
@@ -204,7 +216,7 @@ public sealed class GameObject : IUpdate, IRender
/// </summary>
/// <typeparam name="T">The type of the component to retrieve.</typeparam>
/// <returns>The component if found, otherwise null.</returns>
public T? GetComponentAny<T>() where T : Component.Component
public T? GetComponentAny<T>() where T : class
{
var component = GetComponent<T>();
if (component != null)
@@ -221,7 +233,7 @@ public sealed class GameObject : IUpdate, IRender
/// </summary>
/// <typeparam name="T">The type of the component to retrieve.</typeparam>
/// <returns>The component if found, otherwise null.</returns>
public T? GetComponentInChildren<T>() where T : Component.Component
public T? GetComponentInChildren<T>() where T : class
{
var children = Scene!.Hierarchy.GetChildren(this);
@@ -334,7 +346,7 @@ public sealed class GameObject : IUpdate, IRender
/// </summary>
/// <typeparam name="T">The type of the component to check for.</typeparam>
/// <returns>True if the component exists, otherwise false.</returns>
public bool HasComponent<T>() where T : Component.Component
public bool HasComponent<T>()
{
var baseType = typeof(T).GetComponentBaseType();
return _addedComponentTypes.Contains(baseType);

View File

@@ -61,6 +61,11 @@ public class Scene : IUpdate, IRender
{
gameObject.Update(parDeltaTime * TimeScale);
}
foreach (var gameObject in hierarchyObjects)
{
gameObject.PostUpdate(parDeltaTime * TimeScale);
}
}
/// <inheritdoc/>

View File

@@ -5,7 +5,7 @@ namespace Engine.Util;
/// <summary>
/// Contains mathematical utility methods.
/// </summary>
public static class Math
public static class MathUtil
{
/// <summary>
/// Multiplies a <see cref="Vector4"/> by a <see cref="Matrix4"/> and performs projective division.

View File

@@ -14,10 +14,16 @@ namespace PresenterConsole;
public class ConsolePresenter : IPresenter
{
/// <inheritdoc/>
public event Action<ResizeEventArgs>? OnResize;
/// <inheritdoc/>
public int Width { get; private set; } = 2;
/// <inheritdoc/>
public int Height { get; private set; } = 1;
/// <inheritdoc/>
public bool IsExiting { get; private set; }
private static readonly char[] LIGHTMAP = " .,:;=*#%@".Reverse().ToArray();
@@ -27,8 +33,8 @@ public class ConsolePresenter : IPresenter
private readonly Engine.Graphics.Shader.Program _asciiProgram;
private readonly Framebuffer _framebuffer;
private readonly IndexBuffer _indexBuffer;
private readonly VertexArray _vertexArray;
private readonly VertexBuffer<AsciiVertex> _vertexBuffer;
private readonly VertexArray _vertexArray;
private Image<AsciiPixel>? _asciiImage;
@@ -62,8 +68,6 @@ public class ConsolePresenter : IPresenter
{
var openglTexture = (Texture)parTexture;
// GL.Viewport(0, 0, Width / 2, Height);
_framebuffer.Bind();
openglTexture.BindUnit();

View File

@@ -8,38 +8,101 @@ using Microsoft.Win32.SafeHandles;
namespace PresenterConsole;
/// <summary>
/// Provides Windows FFI (Foreign Function Interface) functionality to interact with the Windows API
/// for various operations such as file handling, console operations, keyboard state management, and window handling.
/// </summary>
public static partial class WindowsFFI
{
/// <summary>
/// Represents a coordinate with X and Y values, typically used for positioning or grid-like structures.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Coord(short parX, short parY)
{
/// <summary>
/// The X coordinate.
/// </summary>
public short X = parX;
/// <summary>
/// The Y coordinate.
/// </summary>
public short Y = parY;
}
/// <summary>
/// A union structure that can hold either a Unicode or an ASCII character at the same memory location.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct CharUnion
{
/// <summary>
/// The Unicode character value.
/// </summary>
[FieldOffset(0)] public char UnicodeChar;
/// <summary>
/// The ASCII character value.
/// </summary>
[FieldOffset(0)] public byte AsciiChar;
}
/// <summary>
/// Represents a character and its associated attributes in the console output, including the character's color.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct CharInfo
{
/// <summary>
/// The character.
/// </summary>
[FieldOffset(0)] public CharUnion Char;
/// <summary>
/// The attributes associated with the character (e.g., color).
/// </summary>
[FieldOffset(2)] public short Attributes;
}
/// <summary>
/// Represents a rectangle in terms of its left, top, right, and bottom coordinates, used for defining the bounds of a console screen area.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct SmallRect
{
/// <summary>
/// The left coordinate of the rectangle.
/// </summary>
public short Left;
/// <summary>
/// The top coordinate of the rectangle.
/// </summary>
public short Top;
/// <summary>
/// The right coordinate of the rectangle.
/// </summary>
public short Right;
/// <summary>
/// The bottom coordinate of the rectangle.
/// </summary>
public short Bottom;
}
/// <summary>
/// Uses the kernel32.dll's CreateFileW function to open or create a file with the specified parameters.
/// </summary>
/// <param name="parFileName">The name of the file to open or create.</param>
/// <param name="parFileAccess">The requested access to the file.</param>
/// <param name="parFileShare">The sharing mode for the file.</param>
/// <param name="parSecurityAttributes">Security attributes for the file.</param>
/// <param name="parCreationDisposition">Action to take when opening the file.</param>
/// <param name="parFlags">File-specific flags.</param>
/// <param name="parTemplate">An optional template file.</param>
/// <returns>A handle to the opened or created file.</returns>
[LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "CreateFileW")]
public static partial SafeFileHandle CreateFile(
[MarshalAs(UnmanagedType.LPWStr)] string parFileName,
@@ -51,6 +114,15 @@ public static partial class WindowsFFI
IntPtr parTemplate
);
/// <summary>
/// Uses the kernel32.dll's WriteConsoleOutputW function to write characters to the console output at the specified coordinates.
/// </summary>
/// <param name="parHConsoleOutput">A handle to the console output.</param>
/// <param name="parLpBuffer">The buffer containing the characters and their attributes to be written.</param>
/// <param name="parDwBufferSize">The size of the buffer.</param>
/// <param name="parDwBufferCoord">The coordinates of the starting position in the console buffer.</param>
/// <param name="parLpWriteRegion">The region of the console buffer to write to.</param>
/// <returns>True if the operation was successful, otherwise false.</returns>
[LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "WriteConsoleOutputW")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool WriteConsoleOutput(
@@ -62,32 +134,61 @@ public static partial class WindowsFFI
ref SmallRect parLpWriteRegion
);
/// <summary>
/// Uses the user32.dll's GetKeyboardState function to retrieve the state of the keyboard keys.
/// </summary>
/// <param name="parKeyboardState">An array to receive the current state of the keyboard keys.</param>
/// <returns>True if the operation was successful, otherwise false.</returns>
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetKeyboardState")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool GetKeyboardState(byte[] parKeyboardState);
/// <summary>
/// Uses the user32.dll's GetAsyncKeyState function to retrieve the state of a specific key.
/// </summary>
/// <param name="parKeyCode">The key code for the key whose state is to be retrieved.</param>
/// <returns>A short value representing the key's state.</returns>
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetAsyncKeyState")]
public static partial short GetAsyncKeyState(int parKeyCode);
/// <summary>
/// Uses the user32.dll's GetForegroundWindow function to retrieve the handle of the foreground window.
/// </summary>
/// <returns>The handle of the foreground window.</returns>
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetForegroundWindow")]
public static partial IntPtr GetForegroundWindow();
/// <summary>
/// Uses the user32.dll's GetWindowThreadProcessId function to retrieve the process ID of the thread that owns a specified window.
/// </summary>
/// <param name="parHwnd">The handle to the window.</param>
/// <param name="parLpdwProcessId">An optional pointer to receive the process ID of the window's thread.</param>
/// <returns>The thread ID of the window.</returns>
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetWindowThreadProcessId")]
public static partial uint GetWindowThreadProcessId(IntPtr parHwnd, IntPtr parLpdwProcessId);
/// <summary>
/// Uses the user32.dll's GetKeyboardLayout function to retrieve the input locale identifier for the current thread.
/// </summary>
/// <param name="parThreadId">The ID of the thread for which to retrieve the keyboard layout.</param>
/// <returns>The handle to the keyboard layout for the thread.</returns>
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetKeyboardLayout")]
public static partial IntPtr GetKeyboardLayout(uint parThreadId);
/// <summary>
/// Retrieves the current keyboard layout for the foreground window's process. Assumes English (1033) if an error occurs.
/// </summary>
/// <returns>A <see cref="CultureInfo"/> object representing the current keyboard layout.</returns>
public static CultureInfo GetCurrentKeyboardLayout()
{
try
{
IntPtr foregroundWindow = GetForegroundWindow();
uint foregroundProcess = GetWindowThreadProcessId(foregroundWindow, IntPtr.Zero);
int keyboardLayout = GetKeyboardLayout(foregroundProcess).ToInt32() & 0xFFFF;
var foregroundWindow = GetForegroundWindow();
var foregroundProcess = GetWindowThreadProcessId(foregroundWindow, IntPtr.Zero);
var keyboardLayout = GetKeyboardLayout(foregroundProcess).ToInt32() & 0xFFFF;
return new CultureInfo(keyboardLayout);
}
catch (Exception _)
catch (Exception)
{
return new CultureInfo(1033); // Assume English if something went wrong.
}