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; 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 public class ConsumableComponent : Engine.Scene.Component.Component
{ {
/// <summary>
/// The consumable logic tied to this component.
/// </summary>
private readonly IConsumable _consumable; private readonly IConsumable _consumable;
/// <summary>
/// The collider used for detecting interactions with other objects.
/// </summary>
private readonly AABBColliderComponent _collider; private readonly AABBColliderComponent _collider;
/// <summary>
/// The 2D box renderer used to display the consumable's icon.
/// </summary>
private readonly Box2DRenderer _box2DRenderer; private readonly Box2DRenderer _box2DRenderer;
/// <summary>
/// Provides random values for movement adjustments.
/// </summary>
private readonly Random _random = new(); private readonly Random _random = new();
/// <summary>
/// Controls the movement of the consumable object.
/// </summary>
private MovementController _movementController = null!; 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, public ConsumableComponent(IConsumable parConsumable, AABBColliderComponent parCollider,
Box2DRenderer parBox2DRenderer) Box2DRenderer parBox2DRenderer)
{ {
@@ -36,6 +62,10 @@ public class ConsumableComponent : Engine.Scene.Component.Component
ArgumentNullException.ThrowIfNull(_movementController); 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) private void OnCollision(AABBColliderComponent parCollider)
{ {
if (parCollider.ColliderGroups.Contains("player")) 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) private void TryConsume(AABBColliderComponent parCollider)
{ {
var playerController = parCollider.GameObject.GetComponent<PlayerController>(); var playerController = parCollider.GameObject.GetComponent<PlayerController>();
@@ -61,6 +95,10 @@ public class ConsumableComponent : Engine.Scene.Component.Component
EngineUtil.SceneManager.CurrentScene!.Remove(GameObject); 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) private void MoveAway(AABBColliderComponent parCollider)
{ {
var direction = _collider.GameObject.Transform.GetFullTranslation() - var direction = _collider.GameObject.Transform.GetFullTranslation() -
@@ -78,6 +116,10 @@ public class ConsumableComponent : Engine.Scene.Component.Component
_movementController.ApplyMovement(direction); _movementController.ApplyMovement(direction);
} }
/// <summary>
/// Generates a random direction vector for movement.
/// </summary>
/// <returns>A normalized random direction vector.</returns>
private Vector3 GetRandomDirection() private Vector3 GetRandomDirection()
{ {
var x = (_random.NextSingle() * 2) - 1; var x = (_random.NextSingle() * 2) - 1;

View File

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

View File

@@ -1,19 +1,31 @@
using DoomDeathmatch.Script.Model.Enemy; using DoomDeathmatch.Script.Model.Enemy;
using DoomDeathmatch.Script.MVC;
using Engine.Graphics.Texture; using Engine.Graphics.Texture;
using Engine.Scene.Component.BuiltIn.Renderer; using Engine.Scene.Component.BuiltIn.Renderer;
using Engine.Util; using Engine.Util;
namespace DoomDeathmatch.Component.MVC.Enemy; 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; 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) public EnemyView(Box2DRenderer parBox2DRenderer)
{ {
_box2DRenderer = parBox2DRenderer; _box2DRenderer = parBox2DRenderer;
} }
/// <inheritdoc/>
public void UpdateView(EnemyData parEnemyData) public void UpdateView(EnemyData parEnemyData)
{ {
_box2DRenderer.Texture = EngineUtil.AssetResourceManager.Load<Texture>(parEnemyData.Texture); _box2DRenderer.Texture = EngineUtil.AssetResourceManager.Load<Texture>(parEnemyData.Texture);

View File

@@ -7,19 +7,50 @@ using Engine.Util;
namespace DoomDeathmatch.Component.MVC; 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 public class GameController : Engine.Scene.Component.Component
{ {
/// <summary>
/// Indicates whether the game is currently paused.
/// </summary>
public bool IsPaused { get; private set; } public bool IsPaused { get; private set; }
/// <summary>
/// Indicates whether the game is over.
/// </summary>
public bool IsGameOver { get; private set; } 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!; public PlayerController PlayerController { get; private set; } = null!;
/// <summary>
/// The score controller tracking the player's score.
/// </summary>
public ScoreController ScoreController { get; private set; } = null!; public ScoreController ScoreController { get; private set; } = null!;
/// <summary>
/// The input handler for handling keyboard and mouse input.
/// </summary>
private readonly IInputHandler _inputHandler = EngineUtil.InputHandler; private readonly IInputHandler _inputHandler = EngineUtil.InputHandler;
/// <summary>
/// The menu controller for handling the main menu.
/// </summary>
private readonly MenuControllerComponent _menuController; private readonly MenuControllerComponent _menuController;
/// <summary>
/// The timer controller for managing the game timer.
/// </summary>
private TimerController _timerController = null!; 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) public GameController(MenuControllerComponent parMenuController)
{ {
_menuController = parMenuController; _menuController = parMenuController;
@@ -39,20 +70,6 @@ public class GameController : Engine.Scene.Component.Component
_timerController.OnFinished += GameOver; _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) public override void Update(double parDeltaTime)
{ {
if (_inputHandler.IsKeyJustPressed(KeyboardButtonCode.Escape)) 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() private void GameOver()
{ {
IsGameOver = true; IsGameOver = true;

View File

@@ -1,15 +1,30 @@
using DoomDeathmatch.Script.Model; using DoomDeathmatch.Script.Model;
using DoomDeathmatch.Script.MVC;
using Engine.Scene.Component.BuiltIn.Renderer; using Engine.Scene.Component.BuiltIn.Renderer;
namespace DoomDeathmatch.Component.MVC.Health; 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}"; _healthTextRenderer.Text = $"Здоровье: {parHealthModel.Health:000}";
} }

View File

@@ -1,15 +1,37 @@
using DoomDeathmatch.Script.Model; using DoomDeathmatch.Script.Model;
using DoomDeathmatch.Script.MVC;
namespace DoomDeathmatch.Component.MVC.Health; namespace DoomDeathmatch.Component.MVC.Health;
/// <summary>
/// Manages health-related logic, including damage, healing, and health changes.
/// </summary>
public class HealthController : Engine.Scene.Component.Component public class HealthController : Engine.Scene.Component.Component
{ {
/// <summary>
/// Triggered when the entity dies.
/// </summary>
public event Action? OnDeath; public event Action? OnDeath;
/// <summary>
/// Indicates whether the entity is alive.
/// </summary>
public bool IsAlive => _healthModel.Health > 0; public bool IsAlive => _healthModel.Health > 0;
/// <summary>
/// The health model containing the current and maximum health.
/// </summary>
private readonly HealthModel _healthModel; 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) public HealthController(float parHealth = 100)
{ {
_healthModel = new HealthModel(parHealth); _healthModel = new HealthModel(parHealth);
@@ -18,7 +40,7 @@ public class HealthController : Engine.Scene.Component.Component
public override void Awake() public override void Awake()
{ {
_healthModel.HealthChanged += OnHealthChanged; _healthModel.HealthChanged += OnHealthChanged;
_healthView = GameObject.GetComponent<HealthView>(); _healthView = GameObject.GetComponent<IView<HealthModel>>();
if (_healthView != null) 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) public void SetMaxHealth(float parMaxHealth)
{ {
_healthModel.MaxHealth = parMaxHealth; _healthModel.MaxHealth = parMaxHealth;
_healthModel.Health = 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) public void TakeDamage(float parDamage)
{ {
_healthModel.Health -= 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) public void Heal(float parHeal)
{ {
_healthModel.Health += 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) private void OnHealthChanged(HealthModel parHealthModel)
{ {
if (!IsAlive) if (!IsAlive)

View File

@@ -1,17 +1,29 @@
using DoomDeathmatch.Script.Model; using DoomDeathmatch.Script.Model;
using DoomDeathmatch.Script.MVC;
using Engine.Scene.Component.BuiltIn.Renderer; using Engine.Scene.Component.BuiltIn.Renderer;
namespace DoomDeathmatch.Component.MVC.Health; 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) public HealthView(TextRenderer parHealthTextRenderer)
{ {
_healthTextRenderer = parHealthTextRenderer; _healthTextRenderer = parHealthTextRenderer;
} }
/// <inheritdoc/>
public virtual void UpdateView(HealthModel parHealthModel) public virtual void UpdateView(HealthModel parHealthModel)
{ {
var percentage = parHealthModel.Health / parHealthModel.MaxHealth * 100; var percentage = parHealthModel.Health / parHealthModel.MaxHealth * 100;

View File

@@ -3,11 +3,24 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.MVC; 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 public class MovementController : Engine.Scene.Component.Component
{ {
/// <summary>
/// The movement speed.
/// </summary>
public float Speed { get; set; } = 10.0f; public float Speed { get; set; } = 10.0f;
/// <summary>
/// The rigidbody component for the game object.
/// </summary>
private RigidbodyComponent _rigidbody = null!; private RigidbodyComponent _rigidbody = null!;
/// <summary>
/// The drag component for the game object.
/// </summary>
private DragComponent _dragComponent = null!; private DragComponent _dragComponent = null!;
public override void Awake() public override void Awake()
@@ -19,6 +32,10 @@ public class MovementController : Engine.Scene.Component.Component
ArgumentNullException.ThrowIfNull(_dragComponent); 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) public void ApplyMovement(Vector3 parDirection)
{ {
_rigidbody.Force += _dragComponent.Drag * Speed * parDirection.Normalized(); _rigidbody.Force += _dragComponent.Drag * Speed * parDirection.Normalized();

View File

@@ -9,17 +9,45 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.MVC; 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 public class PlayerController : Engine.Scene.Component.Component
{ {
/// <summary>
/// Event triggered when the player dies.
/// </summary>
public event Action? OnDeath; public event Action? OnDeath;
/// <summary>
/// Indicates whether the player is alive.
/// </summary>
public bool IsAlive => HealthController.IsAlive; public bool IsAlive => HealthController.IsAlive;
/// <summary>
/// The health controller managing the player's health.
/// </summary>
public HealthController HealthController { get; private set; } = null!; public HealthController HealthController { get; private set; } = null!;
/// <summary>
/// The weapon controller managing the player's weapons.
/// </summary>
public WeaponController WeaponController { get; private set; } = null!; public WeaponController WeaponController { get; private set; } = null!;
/// <summary>
/// The player's camera providing a perspective view.
/// </summary>
public PerspectiveCamera Camera { get; } public PerspectiveCamera Camera { get; }
/// <summary>
/// The input handler for handling keyboard and mouse input.
/// </summary>
private readonly IInputHandler _inputHandler = EngineUtil.InputHandler; 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) public PlayerController(PerspectiveCamera parCamera)
{ {
Camera = parCamera; Camera = parCamera;
@@ -66,24 +94,32 @@ public class PlayerController : Engine.Scene.Component.Component
return; return;
} }
var position = Camera.GameObject.Transform.GetFullTranslation(); HandleShootingRaycast();
var forward = (Camera.Forward - position).Normalized(); }
var right = Vector3.Cross(forward, Vector3.UnitZ).Normalized(); }
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); var collisionManager = EngineUtil.SceneManager.CurrentScene!.FindFirstComponent<CollisionManagerComponent>();
foreach (var offset in offsets)
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; continue;
if (!collisionManager!.Raycast(position, direction, ["enemy", "wall"], out var result))
{
continue;
}
var enemyController = result.HitObject.GetComponent<EnemyController>();
enemyController?.HealthController.TakeDamage(WeaponController.WeaponData.Damage);
} }
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.Model;
using DoomDeathmatch.Script.MVC;
namespace DoomDeathmatch.Component.MVC.Score; namespace DoomDeathmatch.Component.MVC.Score;
/// <summary>
/// Manages the player's score, including updating and displaying it.
/// </summary>
public class ScoreController : Engine.Scene.Component.Component public class ScoreController : Engine.Scene.Component.Component
{ {
/// <summary>
/// Current score value.
/// </summary>
public int Score => _scoreModel.Score; public int Score => _scoreModel.Score;
/// <summary>
/// The score model containing the current score.
/// </summary>
private readonly ScoreModel _scoreModel = new(); 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() public override void Awake()
{ {
_scoreView = GameObject.GetComponent<ScoreView>()!; _scoreView = GameObject.GetComponent<IView<ScoreModel>>()!;
_scoreView.UpdateView(_scoreModel); _scoreView.UpdateView(_scoreModel);
_scoreModel.ScoreChanged += _scoreView.UpdateView; _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) public void AddScore(int parScore)
{ {
_scoreModel.Score += parScore; _scoreModel.Score += parScore;

View File

@@ -1,17 +1,29 @@
using DoomDeathmatch.Script.Model; using DoomDeathmatch.Script.Model;
using DoomDeathmatch.Script.MVC;
using Engine.Scene.Component.BuiltIn.Renderer; using Engine.Scene.Component.BuiltIn.Renderer;
namespace DoomDeathmatch.Component.MVC.Score; 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; 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) public ScoreView(TextRenderer parScoreTextRenderer)
{ {
_scoreTextRenderer = parScoreTextRenderer; _scoreTextRenderer = parScoreTextRenderer;
} }
/// <inheritdoc/>
public void UpdateView(ScoreModel parScoreModel) public void UpdateView(ScoreModel parScoreModel)
{ {
_scoreTextRenderer.Text = $"Счет: {parScoreModel.Score:00000}"; _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; namespace DoomDeathmatch.Component.MVC.Timer;
/// <summary>
/// Controls the timer functionality, including updates and view synchronization.
/// </summary>
public class TimerController : Engine.Scene.Component.Component public class TimerController : Engine.Scene.Component.Component
{ {
/// <summary>
/// Triggered when the timer finishes.
/// </summary>
public event Action? OnFinished; 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() public override void Awake()
{ {
_timerView = GameObject.GetComponent<TimerView>()!; _timerView = GameObject.GetComponent<IView<TickableTimer>>()!;
_timerView.UpdateView(_tickableTimer.CurrentTime); _timerView.UpdateView(_tickableTimer);
_tickableTimer.OnUpdate += OnTimeChanged; _tickableTimer.OnUpdate += OnTimeChanged;
_tickableTimer.OnFinished += OnFinished; _tickableTimer.OnFinished += OnFinished;
@@ -24,8 +46,12 @@ public class TimerController : Engine.Scene.Component.Component
_tickableTimer.Update(parDeltaTime); _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) 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; 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; 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) public TimerView(TextRenderer parTimerTextRenderer)
{ {
_timerTextRenderer = parTimerTextRenderer; _timerTextRenderer = parTimerTextRenderer;
} }
public void UpdateView(double parTime) /// <inheritdoc/>
public void UpdateView(TickableTimer parTime)
{ {
var seconds = Math.Floor(parTime) % 60; var time = parTime.CurrentTime;
var minutes = Math.Floor(parTime / 60) % 60; var seconds = Math.Floor(time) % 60;
var minutes = Math.Floor(time / 60) % 60;
_timerTextRenderer.Text = $"Время: {minutes:00}:{seconds:00}"; _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; namespace DoomDeathmatch.Component.MVC.Weapon;
/// <summary>
/// Manages weapon functionality, including shooting, reloading, and weapon selection.
/// </summary>
public class WeaponController : Engine.Scene.Component.Component public class WeaponController : Engine.Scene.Component.Component
{ {
/// <summary>
/// Triggered when a weapon is fired.
/// </summary>
public event Action<WeaponData>? OnWeaponShot; public event Action<WeaponData>? OnWeaponShot;
/// <summary>
/// The currently selected weapon's data.
/// </summary>
public WeaponData WeaponData => _weaponModel.SelectedWeapon; public WeaponData WeaponData => _weaponModel.SelectedWeapon;
/// <summary>
/// The internal weapon model containing weapon-related data.
/// </summary>
private readonly WeaponModel _weaponModel = new(); 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() public override void Awake()
{ {
_weaponView = GameObject.GetComponent<WeaponView>()!; _weaponView = GameObject.GetComponent<IWeaponView>()!;
_weaponView.UpdateView(_weaponModel.SelectedWeapon); _weaponView.UpdateView(_weaponModel.SelectedWeapon);
_weaponView.UpdateView(new AmmoData
{
Ammo = _weaponModel.SelectedWeapon.Ammo, MaxAmmo = _weaponModel.SelectedWeapon.MaxAmmo
});
_weaponModel.OnWeaponSelected += WeaponSelected; _weaponModel.OnWeaponSelected += WeaponSelected;
WeaponSelected(null, _weaponModel.SelectedWeapon); 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() public bool TryShoot()
{ {
if (_weaponModel.SelectedWeapon.Ammo <= 0) if (_weaponModel.SelectedWeapon.Ammo <= 0)
@@ -30,16 +55,23 @@ public class WeaponController : Engine.Scene.Component.Component
_weaponModel.SelectedWeapon.Ammo--; _weaponModel.SelectedWeapon.Ammo--;
OnWeaponShot?.Invoke(_weaponModel.SelectedWeapon); OnWeaponShot?.Invoke(_weaponModel.SelectedWeapon);
_weaponView.Fire(); _weaponView.PlayFireAnimation();
return true; return true;
} }
/// <summary>
/// Reloads the currently selected weapon, restoring its ammo to the maximum value.
/// </summary>
public void Reload() public void Reload()
{ {
_weaponModel.SelectedWeapon.Ammo = _weaponModel.SelectedWeapon.MaxAmmo; _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) public void AddWeapon(WeaponData parWeaponData)
{ {
if (_weaponModel.Weapons.Contains(parWeaponData)) if (_weaponModel.Weapons.Contains(parWeaponData))
@@ -50,6 +82,10 @@ public class WeaponController : Engine.Scene.Component.Component
_weaponModel.Weapons.Add(parWeaponData); _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) public void AddOrMergeWeapon(WeaponData parWeaponData)
{ {
if (!_weaponModel.Weapons.Contains(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) public void RemoveWeapon(int parIndex)
{ {
if (parIndex <= 0 || parIndex >= _weaponModel.Weapons.Count) if (parIndex <= 0 || parIndex >= _weaponModel.Weapons.Count)
@@ -75,6 +115,10 @@ public class WeaponController : Engine.Scene.Component.Component
_weaponModel.Weapons.RemoveAt(parIndex); _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) public void SelectWeapon(int parIndex)
{ {
if (parIndex >= _weaponModel.Weapons.Count) if (parIndex >= _weaponModel.Weapons.Count)
@@ -85,14 +129,26 @@ public class WeaponController : Engine.Scene.Component.Component
_weaponModel.SelectedWeaponIndex = parIndex; _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) private void WeaponSelected(WeaponData? parOldWeapon, WeaponData parNewWeapon)
{ {
if (parOldWeapon != null) if (parOldWeapon != null)
{ {
parOldWeapon.OnAmmoChanged -= _weaponView.UpdateAmmoView; parOldWeapon.OnAmmoChanged -= AmmoChanged;
} }
parNewWeapon.OnAmmoChanged += _weaponView.UpdateAmmoView; parNewWeapon.OnAmmoChanged += AmmoChanged;
_weaponView.UpdateView(parNewWeapon); _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; 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; private readonly TextRenderer _weaponName;
/// <summary>
/// The text renderer used to display the weapon ammo.
/// </summary>
private readonly TextRenderer _weaponAmmo; private readonly TextRenderer _weaponAmmo;
/// <summary>
/// The box2d renderer used to display the weapon sprite.
/// </summary>
private readonly Box2DRenderer _weaponSprite; private readonly Box2DRenderer _weaponSprite;
/// <summary>
/// The texture used for the weapon sprite when it is idle.
/// </summary>
private Texture? _idleTexture; private Texture? _idleTexture;
/// <summary>
/// The animation player used to display the weapon sprite when it is firing.
/// </summary>
private AnimationPlayer<Texture>? _weaponFireAnimation; 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) public WeaponView(TextRenderer parWeaponName, TextRenderer parWeaponAmmo, Box2DRenderer parWeaponSprite)
{ {
_weaponName = parWeaponName; _weaponName = parWeaponName;
@@ -28,9 +54,9 @@ public class WeaponView : Engine.Scene.Component.Component
_weaponFireAnimation?.Update(parDeltaTime); _weaponFireAnimation?.Update(parDeltaTime);
} }
/// <inheritdoc/>
public void UpdateView(WeaponData parWeaponData) public void UpdateView(WeaponData parWeaponData)
{ {
UpdateAmmoView(parWeaponData);
_weaponName.Text = $"Оружие: {parWeaponData.Name}"; _weaponName.Text = $"Оружие: {parWeaponData.Name}";
_idleTexture = EngineUtil.AssetResourceManager.Load<Texture>(parWeaponData.IdleTexture); _idleTexture = EngineUtil.AssetResourceManager.Load<Texture>(parWeaponData.IdleTexture);
@@ -50,21 +76,30 @@ public class WeaponView : Engine.Scene.Component.Component
_weaponFireAnimation.OnFinish += OnAnimationFinish; _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(); _weaponFireAnimation?.Start();
} }
/// <summary>
/// Handles the animation frame change event.
/// </summary>
/// <param name="parFrameTexture">The current frame texture.</param>
private void OnAnimationFrame(Texture parFrameTexture) private void OnAnimationFrame(Texture parFrameTexture)
{ {
_weaponSprite.Texture = parFrameTexture; _weaponSprite.Texture = parFrameTexture;
} }
/// <summary>
/// Handles the animation finish event.
/// </summary>
private void OnAnimationFinish() private void OnAnimationFinish()
{ {
_weaponSprite.Texture = _idleTexture; _weaponSprite.Texture = _idleTexture;

View File

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

View File

@@ -3,16 +3,44 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.Physics.Collision; 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 public class AABBColliderComponent : Engine.Scene.Component.Component
{ {
/// <summary>
/// Triggered when a collision occurs with another <see cref="AABBColliderComponent"/>.
/// </summary>
public event Action<AABBColliderComponent>? OnCollision; public event Action<AABBColliderComponent>? OnCollision;
/// <summary>
/// The collider defining the bounds of this component.
/// </summary>
public AABBCollider Collider { get; set; } = new(); 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; public Vector3 Offset { get; set; } = Vector3.Zero;
/// <summary>
/// A set of groups that this collider belongs to.
/// </summary>
public ISet<string> ColliderGroups => _colliderGroups; public ISet<string> ColliderGroups => _colliderGroups;
/// <summary>
/// A set of groups that this collider will not collide with.
/// </summary>
public ISet<string> ExcludeColliderCollideGroups => _excludeColliderCollideGroups; public ISet<string> ExcludeColliderCollideGroups => _excludeColliderCollideGroups;
/// <summary>
/// A set of groups that this collider belongs to.
/// </summary>
private readonly HashSet<string> _colliderGroups = ["default"]; private readonly HashSet<string> _colliderGroups = ["default"];
/// <summary>
/// A set of groups that this collider will not collide with.
/// </summary>
private readonly HashSet<string> _excludeColliderCollideGroups = []; private readonly HashSet<string> _excludeColliderCollideGroups = [];
public override void Update(double parDeltaTime) public override void Update(double parDeltaTime)
@@ -20,13 +48,12 @@ public class AABBColliderComponent : Engine.Scene.Component.Component
Collider.Position = GameObject.Transform.GetFullTranslation() + Offset; 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) public void CollideWith(AABBColliderComponent parCollider)
{ {
OnCollision?.Invoke(parCollider); OnCollision?.Invoke(parCollider);
} }
public bool InColliderGroup(string parGroup)
{
return ColliderGroups.Contains(parGroup);
}
} }

View File

@@ -2,6 +2,9 @@
namespace DoomDeathmatch.Component.Physics.Collision; 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 public class ColliderForceFieldComponent : Engine.Scene.Component.Component
{ {
private AABBColliderComponent _collider = null!; private AABBColliderComponent _collider = null!;
@@ -12,6 +15,10 @@ public class ColliderForceFieldComponent : Engine.Scene.Component.Component
_collider.OnCollision += OnCollision; _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) private void OnCollision(AABBColliderComponent parCollider)
{ {
var rigidbody = parCollider.GameObject.GetComponent<RigidbodyComponent>(); var rigidbody = parCollider.GameObject.GetComponent<RigidbodyComponent>();
@@ -27,6 +34,6 @@ public class ColliderForceFieldComponent : Engine.Scene.Component.Component
return; 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; namespace DoomDeathmatch.Component.Physics.Collision;
/// <summary>
/// Manages collisions between AABB colliders in the current scene.
/// </summary>
public class CollisionManagerComponent : Engine.Scene.Component.Component public class CollisionManagerComponent : Engine.Scene.Component.Component
{ {
/// <summary>
/// A list of colliders in the current scene.
/// </summary>
private List<AABBColliderComponent> _colliders = []; private List<AABBColliderComponent> _colliders = [];
public override void PreUpdate(double parDeltaTime) public override void PreUpdate(double parDeltaTime)
@@ -24,34 +30,19 @@ public class CollisionManagerComponent : Engine.Scene.Component.Component
{ {
var colliderB = _colliders[j]; var colliderB = _colliders[j];
var canCollideAB = colliderA.ExcludeColliderCollideGroups.Count == 0 || PerformCollisionCheck(colliderA, colliderB);
!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);
}
} }
} }
} }
/// <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, public bool Raycast(Vector3 parStart, Vector3 parDirection, HashSet<string> parColliderGroups,
[MaybeNullWhen(false)] out RaycastResult parResult) [MaybeNullWhen(false)] out RaycastResult parResult)
{ {
@@ -90,6 +81,48 @@ public class CollisionManagerComponent : Engine.Scene.Component.Component
return parResult != null; 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, private static bool RaycastAABB(Vector3 parOrigin, Vector3 parDirection, AABBCollider parCollider,
out Vector3 parHitPoint, out Vector3 parHitPoint,
out Vector3 parHitNormal) out Vector3 parHitNormal)

View File

@@ -2,11 +2,24 @@
namespace DoomDeathmatch.Component.Physics; namespace DoomDeathmatch.Component.Physics;
/// <summary>
/// Applies drag to a rigidbody to simulate resistance to motion.
/// </summary>
public class DragComponent : Engine.Scene.Component.Component 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; 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; public Vector3 Multiplier { get; set; } = Vector3.One;
/// <summary>
/// The rigidbody to apply drag to.
/// </summary>
private RigidbodyComponent _rigidbody = null!; private RigidbodyComponent _rigidbody = null!;
public override void Awake() public override void Awake()

View File

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

View File

@@ -2,11 +2,24 @@
namespace DoomDeathmatch.Component.Physics; 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 public class RigidbodyComponent : Engine.Scene.Component.Component
{ {
/// <summary>
/// The mass of the rigidbody.
/// </summary>
public float Mass { get; set; } = 1.0f; 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; public bool IsStatic { get; set; } = false;
/// <summary>
/// The force currently applied to the rigidbody.
/// </summary>
public Vector3 Force public Vector3 Force
{ {
get => _force; get => _force;
@@ -21,6 +34,9 @@ public class RigidbodyComponent : Engine.Scene.Component.Component
} }
} }
/// <summary>
/// The velocity of the rigidbody.
/// </summary>
public Vector3 Velocity public Vector3 Velocity
{ {
get => _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 _force = Vector3.Zero;
private Vector3 _velocity = Vector3.Zero;
/// <summary>
/// The acceleration of the rigidbody.
/// </summary>
private Vector3 _acceleration = Vector3.Zero; 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) if (IsStatic)
{ {

View File

@@ -2,20 +2,39 @@
namespace DoomDeathmatch.Component.UI; 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 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(); 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) public void AddMenuItem(string parName, GameObject parGameObject)
{ {
_menuItems.Add(parName, 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) public void RemoveMenuItem(string parName)
{ {
_menuItems.Remove(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) public void SelectMenuItem(string parName)
{ {
foreach (var (name, menuItem) in _menuItems) foreach (var (name, menuItem) in _menuItems)

View File

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

View File

@@ -3,9 +3,19 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.UI; 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 public class StackComponent : UiContainerComponent
{ {
/// <summary>
/// The child components contained in this stack.
/// </summary>
public List<UiComponent> Children { get; } = []; 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 Orientation Orientation { get; set; } = Orientation.Vertical;
public override void Update(double parDeltaTime) public override void Update(double parDeltaTime)

View File

@@ -4,11 +4,24 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.UI; 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 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; public Align Alignment { get; set; } = Align.Left;
/// <summary>
/// The text renderer used to measure the text size.
/// </summary>
private TextRenderer _textRenderer = null!; private TextRenderer _textRenderer = null!;
/// <summary>
/// The cached text to avoid unnecessary measurements.
/// </summary>
private string? _cachedText; private string? _cachedText;
public override void Awake() public override void Awake()
@@ -36,6 +49,11 @@ public class TextAlignComponent : Engine.Scene.Component.Component
GameObject.Transform.Translation.Xy = offset; 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) public Vector2 GetOffset(Vector2 parSize)
{ {
return Alignment switch return Alignment switch

View File

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

View File

@@ -6,16 +6,44 @@ using Math = System.Math;
namespace DoomDeathmatch.Component.UI; namespace DoomDeathmatch.Component.UI;
/// <summary>
/// Represents a UI element that can interact with user input.
/// </summary>
public class UiComponent : Engine.Scene.Component.Component public class UiComponent : Engine.Scene.Component.Component
{ {
/// <summary>
/// Invoked when this UI component is clicked.
/// </summary>
public event Action<UiComponent>? OnClick; public event Action<UiComponent>? OnClick;
/// <summary>
/// Invoked when the mouse is hovering over this UI component.
/// </summary>
public event Action<UiComponent>? OnMouseOver; public event Action<UiComponent>? OnMouseOver;
/// <summary>
/// The parent container of this UI component.
/// </summary>
public UiContainerComponent? Container { get; set; } public UiContainerComponent? Container { get; set; }
/// <summary>
/// The alignment of this component's center within its container.
/// </summary>
public Anchor Center { get; set; } = Anchor.Center; 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; 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; public Vector2 Offset { get; set; } = Vector2.Zero;
/// <summary>
/// The input handler used to check for mouse input.
/// </summary>
private readonly IInputHandler _inputHandler = EngineUtil.InputHandler; private readonly IInputHandler _inputHandler = EngineUtil.InputHandler;
public override void Update(double parDeltaTime) 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; var size = GameObject.Transform.Size * GameObject.Transform.Scale;
GameObject.Transform.Translation.Xy = GetAnchorPosition(Container.GameObject.Transform.Size.Xy, Anchor) + Offset - GameObject.Transform.Translation.Xy = Anchor.GetPosition(Container.GameObject.Transform.Size.Xy) + Offset -
GetAnchorPosition(size.Xy, Center); Center.GetPosition(size.Xy);
var transformMatrix = GameObject.Transform.FullTransformMatrix; var transformMatrix = GameObject.Transform.FullTransformMatrix;
var actualSize = transformMatrix.ExtractScale(); 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() public void InvokeClick()
{ {
OnClick?.Invoke(this); 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; namespace DoomDeathmatch.Component.UI;
/// <summary>
/// Represents a container for UI components, responsible for handling mouse position and camera interactions.
/// </summary>
public class UiContainerComponent : UiComponent public class UiContainerComponent : UiComponent
{ {
/// <summary>
/// The camera used to calculate the mouse position in world space.
/// </summary>
public Camera? Camera { get; set; } public Camera? Camera { get; set; }
/// <summary>
/// The current mouse position in world coordinates.
/// </summary>
public Vector3 MousePosition { get; private set; } public Vector3 MousePosition { get; private set; }
/// <summary>
/// The input handler used to check for mouse input.
/// </summary>
private readonly IInputHandler _inputHandler = EngineUtil.InputHandler; private readonly IInputHandler _inputHandler = EngineUtil.InputHandler;
public override void Update(double parDeltaTime) public override void Update(double parDeltaTime)

View File

@@ -3,9 +3,19 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.Util; 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 public class BillboardComponent : Engine.Scene.Component.Component
{ {
/// <summary>
/// The target transform that the object will face.
/// </summary>
public Transform? Target { get; set; } 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 Vector3 Up { get; set; } = Vector3.UnitZ;
public override void Update(double parDeltaTime) public override void Update(double parDeltaTime)
@@ -17,15 +27,28 @@ public class BillboardComponent : Engine.Scene.Component.Component
var targetPosition = Target.GetFullTranslation(); var targetPosition = Target.GetFullTranslation();
var currentPosition = GameObject.Transform.GetFullTranslation(); var currentPosition = GameObject.Transform.GetFullTranslation();
var rotationMatrix = CalculateRotationMatrix(currentPosition, targetPosition, Up);
var forward = targetPosition - currentPosition; GameObject.Transform.Rotation = Quaternion.FromMatrix(rotationMatrix);
forward -= Vector3.Dot(forward, Up) * Up; }
/// <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) if (forward.LengthSquared > 0)
{ {
forward.Normalize(); forward.Normalize();
} }
var right = Vector3.Cross(Up, forward); var right = Vector3.Cross(parUp, forward);
if (right.LengthSquared > 0) if (right.LengthSquared > 0)
{ {
right.Normalize(); right.Normalize();
@@ -39,6 +62,6 @@ public class BillboardComponent : Engine.Scene.Component.Component
right.Z, recalculatedUp.Z, forward.Z 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; 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 public class CopySizeComponent : Engine.Scene.Component.Component
{ {
/// <summary>
/// The target transform whose size is copied.
/// </summary>
public Transform? Target { get; set; } 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 Vector3 Multiplier { get; set; } = Vector3.One;
public override void Update(double parDeltaTime) public override void Update(double parDeltaTime)

View File

@@ -4,8 +4,14 @@ using Engine.Util;
namespace DoomDeathmatch.Component.Util; namespace DoomDeathmatch.Component.Util;
/// <summary>
/// Represents a fireball that deals damage on collision.
/// </summary>
public class FireballComponent : Engine.Scene.Component.Component public class FireballComponent : Engine.Scene.Component.Component
{ {
/// <summary>
/// The amount of damage the fireball inflicts on collision.
/// </summary>
public float Damage { get; set; } public float Damage { get; set; }
public override void Awake() public override void Awake()
@@ -17,6 +23,10 @@ public class FireballComponent : Engine.Scene.Component.Component
collider.OnCollision += OnCollision; 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) private void OnCollision(AABBColliderComponent parCollider)
{ {
var healthController = parCollider.GameObject.GetComponent<HealthController>(); var healthController = parCollider.GameObject.GetComponent<HealthController>();

View File

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

View File

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

View File

@@ -3,17 +3,55 @@ using Engine.Util;
namespace DoomDeathmatch.Script; 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; public event Action? OnFinish;
/// <summary>
/// Occurs when the animation advances to a new frame.
/// </summary>
public event Action<T>? OnFrameChanged; public event Action<T>? OnFrameChanged;
/// <summary>
/// Indicates whether the animation is currently playing.
/// </summary>
public bool IsPlaying { get; private set; } public bool IsPlaying { get; private set; }
/// <summary>
/// The list of frames in the animation.
/// </summary>
public List<T> Frames { get; init; } = []; public List<T> Frames { get; init; } = [];
/// <summary>
/// The index of the next frame to be displayed.
/// </summary>
public int NextFrame { get; private set; } 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) public void Update(double parDeltaTime)
{ {
if (!IsPlaying) 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() public void Start()
{ {
Reset(); Reset();
IsPlaying = true; IsPlaying = true;
} }
/// <summary>
/// Resets the animation to its initial state.
/// </summary>
public void Reset() public void Reset()
{ {
_timer.Reset(); _timer.Reset();

View File

@@ -2,14 +2,36 @@
namespace DoomDeathmatch.Script.Collision; namespace DoomDeathmatch.Script.Collision;
/// <summary>
/// Represents an Axis-Aligned Bounding Box (AABB) collider for 3D collision detection.
/// </summary>
public class AABBCollider public class AABBCollider
{ {
/// <summary>
/// The position of the collider's center in 3D space.
/// </summary>
public Vector3 Position { get; set; } public Vector3 Position { get; set; }
/// <summary>
/// The size (width, height, depth) of the collider.
/// </summary>
public Vector3 Size { get; set; } public Vector3 Size { get; set; }
/// <summary>
/// The minimum point (corner) of the collider in 3D space.
/// </summary>
public Vector3 Min => Position - (Size / 2); public Vector3 Min => Position - (Size / 2);
/// <summary>
/// The maximum point (corner) of the collider in 3D space.
/// </summary>
public Vector3 Max => Position + (Size / 2); 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) public bool Intersects(AABBCollider parCollider)
{ {
var max = Max; var max = Max;
@@ -21,6 +43,14 @@ public class AABBCollider
max.Z >= otherMin.Z && min.Z <= otherMax.Z; 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) public Vector3 GetCollisionNormal(AABBCollider parOther)
{ {
var normal = Vector3.Zero; var normal = Vector3.Zero;

View File

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

View File

@@ -2,11 +2,23 @@
namespace DoomDeathmatch.Script.Condition; 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 public interface ICondition : IUpdate
{ {
/// <summary>
/// Occurs when the condition evaluates to true.
/// </summary>
public event Action? OnTrue; public event Action? OnTrue;
/// <summary>
/// Indicates whether the condition is currently true.
/// </summary>
public bool IsTrue { get; } public bool IsTrue { get; }
/// <summary>
/// Resets the condition to its initial state.
/// </summary>
public void Reset(); public void Reset();
} }

View File

@@ -2,25 +2,49 @@
namespace DoomDeathmatch.Script.Condition; 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 public class TickableTimerCondition : ICondition
{ {
/// <summary>
/// Occurs when the timer finishes, causing the condition to become true.
/// </summary>
public event Action? OnTrue; public event Action? OnTrue;
/// <summary>
/// Indicates whether the timer has finished, making the condition true.
/// </summary>
public bool IsTrue => _timer.IsFinished; public bool IsTrue => _timer.IsFinished;
/// <summary>
/// The timer used to evaluate the condition's state.
/// </summary>
private readonly TickableTimer _timer; 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) public TickableTimerCondition(float parInterval)
{ {
_timer = new TickableTimer(parInterval); _timer = new TickableTimer(parInterval);
_timer.OnFinished += () => OnTrue?.Invoke(); _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) public void Update(double parDeltaTime)
{ {
_timer.Update(parDeltaTime); _timer.Update(parDeltaTime);
} }
/// <summary>
/// Resets the timer and the condition to their initial state.
/// </summary>
public void Reset() public void Reset()
{ {
_timer.Reset(); _timer.Reset();

View File

@@ -2,12 +2,32 @@
namespace DoomDeathmatch.Script.Consumable; 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"; 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) public void Consume(PlayerController parPlayerController)
{ {
parPlayerController.HealthController.Heal(parHealth); parPlayerController.HealthController.Heal(_health);
} }
} }

View File

@@ -2,9 +2,21 @@
namespace DoomDeathmatch.Script.Consumable; 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 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; } 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); public void Consume(PlayerController parPlayerController);
} }

View File

@@ -3,12 +3,31 @@ using DoomDeathmatch.Script.Model.Weapon;
namespace DoomDeathmatch.Script.Consumable; 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) 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; 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; /// <summary>
protected readonly HealthController _healthController = parHealthController; /// 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); public abstract bool Attack(double parDeltaTime);
} }

View File

@@ -3,22 +3,47 @@ using DoomDeathmatch.Component.MVC.Health;
namespace DoomDeathmatch.Script.Model.Enemy.Attack; namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public class CloseContinuousAttackBehavior( /// <summary>
EnemyController parEnemyController, /// Represents a behavior where the enemy continuously attacks when in close proximity.
HealthController parHealthController, /// </summary>
float parRadius, public class CloseContinuousAttackBehavior : AttackBehavior
float parDamage)
: AttackBehavior(parEnemyController, parHealthController)
{ {
/// <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) public override bool Attack(double parDeltaTime)
{ {
var distanceSquared = var distanceSquared =
(_enemyController.GameObject.Transform.Translation - _healthController.GameObject.Transform.Translation) (_enemyController.GameObject.Transform.Translation - _healthController.GameObject.Transform.Translation)
.LengthSquared; .LengthSquared;
if (distanceSquared <= parRadius * parRadius) if (distanceSquared <= _radius * _radius)
{ {
_healthController.TakeDamage(parDamage * (float)parDeltaTime); _healthController.TakeDamage(_damage * (float)parDeltaTime);
return true; return true;
} }

View File

@@ -3,26 +3,53 @@ using DoomDeathmatch.Component.MVC.Health;
namespace DoomDeathmatch.Script.Model.Enemy.Attack; namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public class CloseCooldownAttackBehavior( /// <summary>
EnemyController parEnemyController, /// Represents a behavior where the enemy performs a cooldown-based close-range attack.
HealthController parHealthController, /// </summary>
float parRadius, public class CloseCooldownAttackBehavior : CooldownAttackBehavior
float parCooldown,
float parDamage)
: CooldownAttackBehavior(parEnemyController, parHealthController, parCooldown)
{ {
/// <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() protected override bool CanAttack()
{ {
var distanceSquared = var distanceSquared =
(_enemyController.GameObject.Transform.Translation - _healthController.GameObject.Transform.Translation) (_enemyController.GameObject.Transform.Translation - _healthController.GameObject.Transform.Translation)
.LengthSquared; .LengthSquared;
return distanceSquared <= parRadius * parRadius; return distanceSquared <= _radius * _radius;
} }
/// <inheritdoc />
protected override bool ActivateAttack() protected override bool ActivateAttack()
{ {
_healthController.TakeDamage(parDamage); _healthController.TakeDamage(_damage);
return true; return true;
} }

View File

@@ -3,17 +3,35 @@ using DoomDeathmatch.Component.MVC.Health;
namespace DoomDeathmatch.Script.Model.Enemy.Attack; namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public class CompositeAttackBehavior( /// <summary>
EnemyController parEnemyController, /// Represents a composite attack behavior combining multiple attack behaviors.
HealthController parHealthController, /// </summary>
List<AttackBehavior> parBehaviors) public class CompositeAttackBehavior : AttackBehavior
: AttackBehavior(parEnemyController, parHealthController)
{ {
/// <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) public override bool Attack(double parDeltaTime)
{ {
var result = false; var result = false;
foreach (var behavior in parBehaviors) foreach (var behavior in _behaviors)
{ {
result |= behavior.Attack(parDeltaTime); result |= behavior.Attack(parDeltaTime);
} }

View File

@@ -4,14 +4,30 @@ using Engine.Util;
namespace DoomDeathmatch.Script.Model.Enemy.Attack; namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public abstract class CooldownAttackBehavior( /// <summary>
EnemyController parEnemyController, /// Represents a base class for attacks with cooldown functionality.
HealthController parHealthController, /// </summary>
float parCooldown) public abstract class CooldownAttackBehavior : AttackBehavior
: AttackBehavior(parEnemyController, parHealthController)
{ {
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) public sealed override bool Attack(double parDeltaTime)
{ {
_tickableTimer.Update(parDeltaTime); _tickableTimer.Update(parDeltaTime);
@@ -33,6 +49,15 @@ public abstract class CooldownAttackBehavior(
return false; 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(); protected abstract bool CanAttack();
/// <summary>
/// Executes the attack logic.
/// </summary>
/// <returns>True if the attack was successful; otherwise, false.</returns>
protected abstract bool ActivateAttack(); protected abstract bool ActivateAttack();
} }

View File

@@ -3,11 +3,31 @@ using DoomDeathmatch.Component.MVC.Health;
namespace DoomDeathmatch.Script.Model.Enemy.Attack; namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public class FuncAttackBehaviorCreator(Func<EnemyController, HealthController, AttackBehavior> parFunc) /// <summary>
: IAttackBehaviorCreator /// 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) 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; namespace DoomDeathmatch.Script.Model.Enemy.Attack;
/// <summary>
/// Interface for creating instances of <see cref="AttackBehavior"/>.
/// </summary>
public interface IAttackBehaviorCreator 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); public AttackBehavior Create(EnemyController parEnemyController, HealthController parHealthController);
} }

View File

@@ -5,22 +5,42 @@ using Engine.Util;
namespace DoomDeathmatch.Script.Model.Enemy.Attack; namespace DoomDeathmatch.Script.Model.Enemy.Attack;
public class ObjectSpawnAttackBehavior( /// <summary>
EnemyController parEnemyController, /// Represents a behavior where an object is spawned as part of the attack.
HealthController parHealthController, /// </summary>
float parCooldown, public class ObjectSpawnAttackBehavior : CooldownAttackBehavior
Func<EnemyController, HealthController, GameObject> parObjectSpawnFunc
)
: CooldownAttackBehavior(parEnemyController, parHealthController, parCooldown)
{ {
/// <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() protected override bool CanAttack()
{ {
return true; return true;
} }
/// <inheritdoc />
protected override bool ActivateAttack() protected override bool ActivateAttack()
{ {
var enemyObject = parObjectSpawnFunc(_enemyController, _healthController); var enemyObject = _objectSpawnFunc(_enemyController, _healthController);
EngineUtil.SceneManager.CurrentScene!.Add(enemyObject); EngineUtil.SceneManager.CurrentScene!.Add(enemyObject);
return true; return true;

View File

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

View File

@@ -2,11 +2,34 @@
namespace DoomDeathmatch.Script.Model.Enemy.Movement; 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) public Vector3 GetNextPosition(Vector3 parPosition, Vector3 parPlayerPosition)
{ {
var direction = (parPosition - parPlayerPosition).Normalized(); 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; namespace DoomDeathmatch.Script.Model.Enemy.Movement;
/// <summary>
/// Interface for defining enemy movement behavior.
/// </summary>
public interface IMovementBehavior 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); public Vector3 GetNextPosition(Vector3 parPosition, Vector3 parPlayerPosition);
} }

View File

@@ -2,8 +2,17 @@
namespace DoomDeathmatch.Script.Model.Enemy.Movement; namespace DoomDeathmatch.Script.Model.Enemy.Movement;
/// <summary>
/// Movement behavior where the enemy remains stationary.
/// </summary>
public class StandingMovementBehavior : IMovementBehavior 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) public Vector3 GetNextPosition(Vector3 parPosition, Vector3 parPlayerPosition)
{ {
return parPosition; return parPosition;

View File

@@ -1,9 +1,20 @@
namespace DoomDeathmatch.Script.Model; 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 public class HealthModel
{ {
/// <summary>
/// Occurs when the health value changes.
/// </summary>
public event Action<HealthModel>? HealthChanged; 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 public float Health
{ {
get => _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 public float MaxHealth
{ {
get => _maxHealth; get => _maxHealth;
@@ -31,9 +46,21 @@ public class HealthModel
} }
} }
/// <summary>
/// The current health value of the entity.
/// </summary>
private float _health; private float _health;
/// <summary>
/// The maximum health value of the entity.
/// </summary>
private float _maxHealth; 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) public HealthModel(float parMaxHealth)
{ {
MaxHealth = parMaxHealth; MaxHealth = parMaxHealth;

View File

@@ -1,9 +1,20 @@
namespace DoomDeathmatch.Script.Model; 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 public class ScoreModel
{ {
/// <summary>
/// Occurs when the score value changes.
/// </summary>
public event Action<ScoreModel>? ScoreChanged; public event Action<ScoreModel>? ScoreChanged;
/// <summary>
/// The current score of the player or entity.
/// The score cannot be negative.
/// </summary>
public int Score public int Score
{ {
get => _score; get => _score;
@@ -14,5 +25,8 @@ public class ScoreModel
} }
} }
/// <summary>
/// The current score of the player or entity.
/// </summary>
private int _score; private int _score;
} }

View File

@@ -2,7 +2,17 @@
namespace DoomDeathmatch.Script.Model.Weapon; namespace DoomDeathmatch.Script.Model.Weapon;
/// <summary>
/// Defines a shooting pattern for a weapon.
/// </summary>
public interface IShootPattern 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); public IEnumerable<Vector3> GetShootPattern(Vector3 parForward, Vector3 parUp, Vector3 parRight);
} }

View File

@@ -2,8 +2,18 @@
namespace DoomDeathmatch.Script.Model.Weapon; namespace DoomDeathmatch.Script.Model.Weapon;
/// <summary>
/// Represents a simple linear shooting pattern.
/// </summary>
public class LineShootPattern : IShootPattern 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) public IEnumerable<Vector3> GetShootPattern(Vector3 parForward, Vector3 parUp, Vector3 parRight)
{ {
return [Vector3.Zero]; return [Vector3.Zero];

View File

@@ -2,15 +2,49 @@
namespace DoomDeathmatch.Script.Model.Weapon; 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(); 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) 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 delta = MathF.Tan(angle);
var offset = parRight * delta; var offset = parRight * delta;

View File

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

View File

@@ -2,13 +2,31 @@
namespace DoomDeathmatch.Script.Model; 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 public class WeaponModel
{ {
/// <summary>
/// Occurs when the selected weapon changes.
/// </summary>
public event Action<WeaponData, WeaponData>? OnWeaponSelected; public event Action<WeaponData, WeaponData>? OnWeaponSelected;
/// <summary>
/// The list of available weapons for the player or entity.
/// </summary>
public IList<WeaponData> Weapons => _weapons; public IList<WeaponData> Weapons => _weapons;
/// <summary>
/// The currently selected weapon.
/// </summary>
public WeaponData SelectedWeapon => _weapons[_selectedWeaponIndex]; 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 public int SelectedWeaponIndex
{ {
get => _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]; private readonly List<WeaponData> _weapons = [WeaponData.Pistol];
/// <summary>
/// The index of the currently selected weapon in the weapons list.
/// </summary>
private int _selectedWeaponIndex; private int _selectedWeaponIndex;
} }

View File

@@ -5,14 +5,37 @@ using OpenTK.Mathematics;
namespace DoomDeathmatch.Script; 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 public class ObjectSpawner : IUpdate
{ {
/// <summary>
/// Occurs when a new game object is spawned.
/// </summary>
public event Action<GameObject>? OnSpawned; public event Action<GameObject>? OnSpawned;
/// <summary>
/// Provides the game object to spawn.
/// </summary>
private readonly IValueProvider<GameObject> _gameObjectProvider; private readonly IValueProvider<GameObject> _gameObjectProvider;
/// <summary>
/// Provides the position where the game object will be spawned.
/// </summary>
private readonly IValueProvider<Vector3> _positionProvider; private readonly IValueProvider<Vector3> _positionProvider;
/// <summary>
/// Determines when the spawner should spawn a new object.
/// </summary>
private readonly ICondition _condition; 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, public ObjectSpawner(IValueProvider<GameObject> parGameObjectProvider, IValueProvider<Vector3> parPositionProvider,
ICondition parCondition) ICondition parCondition)
{ {
@@ -23,11 +46,18 @@ public class ObjectSpawner : IUpdate
_condition.OnTrue += Spawn; _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) public void Update(double parDeltaTime)
{ {
_condition.Update(parDeltaTime); _condition.Update(parDeltaTime);
} }
/// <summary>
/// Spawns a new game object at the position provided by the position provider.
/// </summary>
private void Spawn() private void Spawn()
{ {
var gameObject = _gameObjectProvider.GetValue(); var gameObject = _gameObjectProvider.GetValue();

View File

@@ -1,9 +1,38 @@
namespace DoomDeathmatch.Script.Provider; 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); 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); public static implicit operator ConstValueProvider<T>(T parValue) => new(parValue);
} }

View File

@@ -1,9 +1,38 @@
namespace DoomDeathmatch.Script.Provider; 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); 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); public static implicit operator GeneratorValueProvider<T>(Func<T> parGenerator) => new(parGenerator);
} }

View File

@@ -1,6 +1,14 @@
namespace DoomDeathmatch.Script.Provider; 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> 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(); public T GetValue();
} }

View File

@@ -1,10 +1,25 @@
namespace DoomDeathmatch.Script.Provider; 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> public class RandomListValueProvider<T> : IValueProvider<T>
{ {
/// <summary>
/// A collection of value providers to choose from.
/// </summary>
private readonly List<IValueProvider<T>> _providers = []; private readonly List<IValueProvider<T>> _providers = [];
/// <summary>
/// The random number generator for selecting providers.
/// </summary>
private readonly Random _random = new(); 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) public RandomListValueProvider(IEnumerable<IValueProvider<T>> parProviders)
{ {
foreach (var provider in parProviders) foreach (var provider in parProviders)
@@ -13,6 +28,7 @@ public class RandomListValueProvider<T> : IValueProvider<T>
} }
} }
/// <inheritdoc />
public T GetValue() public T GetValue()
{ {
return _providers[_random.Next(_providers.Count)].GetValue(); return _providers[_random.Next(_providers.Count)].GetValue();

View File

@@ -1,11 +1,31 @@
namespace DoomDeathmatch.Script.Provider; 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> public class WeightedRandomValueProvider<T> : IValueProvider<T>
{ {
/// <summary>
/// A collection of weighted value providers.
/// </summary>
private readonly List<(int, IValueProvider<T>)> _providers = []; private readonly List<(int, IValueProvider<T>)> _providers = [];
/// <summary>
/// The random number generator used to select a provider.
/// </summary>
private readonly Random _random = new(); private readonly Random _random = new();
/// <summary>
/// The total weight of all providers.
/// </summary>
private readonly int _totalWeight; 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) public WeightedRandomValueProvider(IEnumerable<(int, IValueProvider<T>)> parProviders)
{ {
foreach (var (weight, provider) in parProviders) foreach (var (weight, provider) in parProviders)
@@ -20,6 +40,7 @@ public class WeightedRandomValueProvider<T> : IValueProvider<T>
} }
} }
/// <inheritdoc />
public T GetValue() public T GetValue()
{ {
var random = _random.Next(_totalWeight); var random = _random.Next(_totalWeight);

View File

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

View File

@@ -1,8 +1,22 @@
namespace DoomDeathmatch.Script.UI; namespace DoomDeathmatch.Script.UI;
/// <summary>
/// Specifies horizontal alignment options.
/// </summary>
public enum Align public enum Align
{ {
/// <summary>
/// Align content to the left.
/// </summary>
Left, Left,
/// <summary>
/// Align content to the center.
/// </summary>
Center, Center,
/// <summary>
/// Align content to the right.
/// </summary>
Right 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 public enum Anchor
{ {
/// <summary>
/// Anchor to the top-left corner.
/// </summary>
TopLeft, TopLeft,
/// <summary>
/// Anchor to the top-center.
/// </summary>
TopCenter, TopCenter,
/// <summary>
/// Anchor to the top-right corner.
/// </summary>
TopRight, TopRight,
/// <summary>
/// Anchor to the center-left.
/// </summary>
CenterLeft, CenterLeft,
/// <summary>
/// Anchor to the center.
/// </summary>
Center, Center,
/// <summary>
/// Anchor to the center-right.
/// </summary>
CenterRight, CenterRight,
/// <summary>
/// Anchor to the bottom-left corner.
/// </summary>
BottomLeft, BottomLeft,
/// <summary>
/// Anchor to the bottom-center.
/// </summary>
BottomCenter, BottomCenter,
/// <summary>
/// Anchor to the bottom-right corner.
/// </summary>
BottomRight 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; namespace DoomDeathmatch.Script.UI;
/// <summary>
/// Specifies the orientation of UI elements.
/// </summary>
public enum Orientation public enum Orientation
{ {
/// <summary>
/// Arrange elements horizontally.
/// </summary>
Horizontal, Horizontal,
/// <summary>
/// Arrange elements vertically.
/// </summary>
Vertical Vertical
} }

View File

@@ -17,7 +17,7 @@ public partial class ProgramLoader : IResourceLoader
/// <returns>The loaded shader program.</returns> /// <returns>The loaded shader program.</returns>
public object Load(string parPath, IResourceStreamProvider parStreamProvider) 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 vertexSource = new StringBuilder();
var fragmentSource = 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> /// <summary>
/// Called during the main render loop. /// Called during the main render loop.
/// </summary> /// </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/> /// <inheritdoc/>
public void Render() public void Render()
{ {
@@ -181,9 +193,9 @@ public sealed class GameObject : IUpdate, IRender
/// </summary> /// </summary>
/// <typeparam name="T">The type of the component to retrieve.</typeparam> /// <typeparam name="T">The type of the component to retrieve.</typeparam>
/// <returns>The component if found, otherwise null.</returns> /// <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; return null;
} }
@@ -204,7 +216,7 @@ public sealed class GameObject : IUpdate, IRender
/// </summary> /// </summary>
/// <typeparam name="T">The type of the component to retrieve.</typeparam> /// <typeparam name="T">The type of the component to retrieve.</typeparam>
/// <returns>The component if found, otherwise null.</returns> /// <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>(); var component = GetComponent<T>();
if (component != null) if (component != null)
@@ -221,7 +233,7 @@ public sealed class GameObject : IUpdate, IRender
/// </summary> /// </summary>
/// <typeparam name="T">The type of the component to retrieve.</typeparam> /// <typeparam name="T">The type of the component to retrieve.</typeparam>
/// <returns>The component if found, otherwise null.</returns> /// <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); var children = Scene!.Hierarchy.GetChildren(this);
@@ -334,7 +346,7 @@ public sealed class GameObject : IUpdate, IRender
/// </summary> /// </summary>
/// <typeparam name="T">The type of the component to check for.</typeparam> /// <typeparam name="T">The type of the component to check for.</typeparam>
/// <returns>True if the component exists, otherwise false.</returns> /// <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(); var baseType = typeof(T).GetComponentBaseType();
return _addedComponentTypes.Contains(baseType); return _addedComponentTypes.Contains(baseType);

View File

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

View File

@@ -5,7 +5,7 @@ namespace Engine.Util;
/// <summary> /// <summary>
/// Contains mathematical utility methods. /// Contains mathematical utility methods.
/// </summary> /// </summary>
public static class Math public static class MathUtil
{ {
/// <summary> /// <summary>
/// Multiplies a <see cref="Vector4"/> by a <see cref="Matrix4"/> and performs projective division. /// 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 public class ConsolePresenter : IPresenter
{ {
/// <inheritdoc/>
public event Action<ResizeEventArgs>? OnResize; public event Action<ResizeEventArgs>? OnResize;
/// <inheritdoc/>
public int Width { get; private set; } = 2; public int Width { get; private set; } = 2;
/// <inheritdoc/>
public int Height { get; private set; } = 1; public int Height { get; private set; } = 1;
/// <inheritdoc/>
public bool IsExiting { get; private set; } public bool IsExiting { get; private set; }
private static readonly char[] LIGHTMAP = " .,:;=*#%@".Reverse().ToArray(); private static readonly char[] LIGHTMAP = " .,:;=*#%@".Reverse().ToArray();
@@ -27,8 +33,8 @@ public class ConsolePresenter : IPresenter
private readonly Engine.Graphics.Shader.Program _asciiProgram; private readonly Engine.Graphics.Shader.Program _asciiProgram;
private readonly Framebuffer _framebuffer; private readonly Framebuffer _framebuffer;
private readonly IndexBuffer _indexBuffer; private readonly IndexBuffer _indexBuffer;
private readonly VertexArray _vertexArray;
private readonly VertexBuffer<AsciiVertex> _vertexBuffer; private readonly VertexBuffer<AsciiVertex> _vertexBuffer;
private readonly VertexArray _vertexArray;
private Image<AsciiPixel>? _asciiImage; private Image<AsciiPixel>? _asciiImage;
@@ -62,8 +68,6 @@ public class ConsolePresenter : IPresenter
{ {
var openglTexture = (Texture)parTexture; var openglTexture = (Texture)parTexture;
// GL.Viewport(0, 0, Width / 2, Height);
_framebuffer.Bind(); _framebuffer.Bind();
openglTexture.BindUnit(); openglTexture.BindUnit();

View File

@@ -8,38 +8,101 @@ using Microsoft.Win32.SafeHandles;
namespace PresenterConsole; 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 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)] [StructLayout(LayoutKind.Sequential)]
public struct Coord(short parX, short parY) public struct Coord(short parX, short parY)
{ {
/// <summary>
/// The X coordinate.
/// </summary>
public short X = parX; public short X = parX;
/// <summary>
/// The Y coordinate.
/// </summary>
public short Y = parY; 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)] [StructLayout(LayoutKind.Explicit)]
public struct CharUnion public struct CharUnion
{ {
/// <summary>
/// The Unicode character value.
/// </summary>
[FieldOffset(0)] public char UnicodeChar; [FieldOffset(0)] public char UnicodeChar;
/// <summary>
/// The ASCII character value.
/// </summary>
[FieldOffset(0)] public byte AsciiChar; [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)] [StructLayout(LayoutKind.Explicit)]
public struct CharInfo public struct CharInfo
{ {
/// <summary>
/// The character.
/// </summary>
[FieldOffset(0)] public CharUnion Char; [FieldOffset(0)] public CharUnion Char;
/// <summary>
/// The attributes associated with the character (e.g., color).
/// </summary>
[FieldOffset(2)] public short Attributes; [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)] [StructLayout(LayoutKind.Sequential)]
public struct SmallRect public struct SmallRect
{ {
/// <summary>
/// The left coordinate of the rectangle.
/// </summary>
public short Left; public short Left;
/// <summary>
/// The top coordinate of the rectangle.
/// </summary>
public short Top; public short Top;
/// <summary>
/// The right coordinate of the rectangle.
/// </summary>
public short Right; public short Right;
/// <summary>
/// The bottom coordinate of the rectangle.
/// </summary>
public short Bottom; 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")] [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "CreateFileW")]
public static partial SafeFileHandle CreateFile( public static partial SafeFileHandle CreateFile(
[MarshalAs(UnmanagedType.LPWStr)] string parFileName, [MarshalAs(UnmanagedType.LPWStr)] string parFileName,
@@ -51,6 +114,15 @@ public static partial class WindowsFFI
IntPtr parTemplate 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")] [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "WriteConsoleOutputW")]
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
public static partial bool WriteConsoleOutput( public static partial bool WriteConsoleOutput(
@@ -62,32 +134,61 @@ public static partial class WindowsFFI
ref SmallRect parLpWriteRegion 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")] [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetKeyboardState")]
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
public static partial bool GetKeyboardState(byte[] parKeyboardState); 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")] [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetAsyncKeyState")]
public static partial short GetAsyncKeyState(int parKeyCode); 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")] [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetForegroundWindow")]
public static partial IntPtr 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")] [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetWindowThreadProcessId")]
public static partial uint GetWindowThreadProcessId(IntPtr parHwnd, IntPtr parLpdwProcessId); 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")] [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetKeyboardLayout")]
public static partial IntPtr GetKeyboardLayout(uint parThreadId); 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() public static CultureInfo GetCurrentKeyboardLayout()
{ {
try try
{ {
IntPtr foregroundWindow = GetForegroundWindow(); var foregroundWindow = GetForegroundWindow();
uint foregroundProcess = GetWindowThreadProcessId(foregroundWindow, IntPtr.Zero); var foregroundProcess = GetWindowThreadProcessId(foregroundWindow, IntPtr.Zero);
int keyboardLayout = GetKeyboardLayout(foregroundProcess).ToInt32() & 0xFFFF; var keyboardLayout = GetKeyboardLayout(foregroundProcess).ToInt32() & 0xFFFF;
return new CultureInfo(keyboardLayout); return new CultureInfo(keyboardLayout);
} }
catch (Exception _) catch (Exception)
{ {
return new CultureInfo(1033); // Assume English if something went wrong. return new CultureInfo(1033); // Assume English if something went wrong.
} }