diff --git a/GameCore/Events/DoorLockedEvent.cs b/GameCore/Events/DoorLockedEvent.cs new file mode 100644 index 0000000..e0fba15 --- /dev/null +++ b/GameCore/Events/DoorLockedEvent.cs @@ -0,0 +1,10 @@ +using GameCore.ECS; +using GameCore.Events.Interfaces; + +namespace GameCore.Events; + +public readonly struct DoorLockedEvent(Entity doorEntity, Entity interactor) : IEvent +{ + public readonly Entity DoorEntity = doorEntity; + public readonly Entity Interactor = interactor; +} \ No newline at end of file diff --git a/GameCore/Events/DoorStateChangedEvent.cs b/GameCore/Events/DoorStateChangedEvent.cs new file mode 100644 index 0000000..37a2543 --- /dev/null +++ b/GameCore/Events/DoorStateChangedEvent.cs @@ -0,0 +1,11 @@ +using GameCore.ECS; +using GameCore.Events.Interfaces; +using GameCore.Interaction; + +namespace GameCore.Events; + +public readonly struct DoorStateChangedEvent(Entity doorEntity, DoorComponent.DoorState newState) : IEvent +{ + public readonly Entity DoorEntity = doorEntity; + public readonly DoorComponent.DoorState NewState = newState; +} \ No newline at end of file diff --git a/GameCore/Interaction/DoorComponent.cs b/GameCore/Interaction/DoorComponent.cs new file mode 100644 index 0000000..9cef280 --- /dev/null +++ b/GameCore/Interaction/DoorComponent.cs @@ -0,0 +1,25 @@ +using GameCore.ECS.Interfaces; +using GameCore.Interaction.Interfaces; + +namespace GameCore.Interaction; + +public class DoorComponent : IComponent +{ + public enum DoorState + { + Locked, + Closed, + Opening, + Open, + Closing + } + + public DoorState CurrentState { get; set; } = DoorState.Locked; + public List Requirements { get; set; } = []; + + public bool IsOneTimeUnlock { get; set; } = false; + + public float OpenSpeed { get; set; } = 2.0f; + public float OpenProgress { get; set; } = 0.0f; + public float Timer { get; set; } = 0.0f; +} \ No newline at end of file diff --git a/GameCore/Interaction/DoorSystem.cs b/GameCore/Interaction/DoorSystem.cs new file mode 100644 index 0000000..8ebb8d9 --- /dev/null +++ b/GameCore/Interaction/DoorSystem.cs @@ -0,0 +1,59 @@ +using GameCore.ECS; +using GameCore.ECS.Interfaces; +using GameCore.Events; + +namespace GameCore.Interaction; + +public class DoorSystem : ISystem +{ + public void Update(World world, float deltaTime) + { + var doors = world.GetEntitiesWith(); + foreach (var entity in doors) + { + var door = world.GetComponent(entity); + if (door == null) continue; + + switch (door.CurrentState) + { + case DoorComponent.DoorState.Opening: + ApplyMovement(entity, door, world, deltaTime, DoorComponent.DoorState.Open, + System.Math.Max(door.OpenSpeed, 0f)); + break; + + case DoorComponent.DoorState.Closing: + ApplyMovement(entity, door, world, -deltaTime, DoorComponent.DoorState.Closed, 0f); + break; + + case DoorComponent.DoorState.Open: + door.Timer = System.Math.Max(door.OpenSpeed, 0f); + door.OpenProgress = 1f; + break; + + case DoorComponent.DoorState.Closed: + case DoorComponent.DoorState.Locked: + door.Timer = 0f; + door.OpenProgress = 0f; + break; + } + } + } + + private void ApplyMovement(Entity entity, DoorComponent door, World world, float delta, + DoorComponent.DoorState targetState, float targetBoundary) + { + var effectiveSpeed = System.Math.Max(door.OpenSpeed, 0f); + door.Timer = System.Math.Clamp(door.Timer + delta, 0f, effectiveSpeed); + + if (effectiveSpeed > 0f) + door.OpenProgress = System.Math.Clamp(door.Timer / effectiveSpeed, 0f, 1f); + else + door.OpenProgress = door.Timer > 0f ? 1f : 0f; + + if ((!(delta > 0f) || !(door.Timer >= targetBoundary)) && + (!(delta < 0f) || !(door.Timer <= targetBoundary))) return; + + door.CurrentState = targetState; + world.PublishEvent(new DoorStateChangedEvent(entity, door.CurrentState)); + } +} \ No newline at end of file diff --git a/GameCore/Interaction/InteractionSystem.cs b/GameCore/Interaction/InteractionSystem.cs new file mode 100644 index 0000000..6199883 --- /dev/null +++ b/GameCore/Interaction/InteractionSystem.cs @@ -0,0 +1,80 @@ +using GameCore.ECS; +using GameCore.ECS.Interfaces; +using GameCore.Events; +using GameCore.Input; +using GameCore.Player; + +namespace GameCore.Interaction; + +public class InteractionSystem(float interactionRange) : ISystem +{ + public void Update(World world, float deltaTime) + { + var playerEntities = world.GetEntitiesWith(); + var players = playerEntities.ToList(); + if (!players.Any()) return; + + var player = players.First(); + var input = world.GetComponent(player); + if (input == null) return; + + world.RemoveComponent(player); + + var from = input.MuzzlePosition; + var to = from + input.MuzzleDirection * interactionRange; + var hit = world.WorldQuery.Raycast(from, to, player); + + if (hit.DidHit && hit.HitEntity.HasValue) + { + var door = world.GetComponent(hit.HitEntity.Value); + if (door != null) + { + world.AddComponent(player, new IsLookingAtInteractableComponent(hit.HitEntity.Value)); + + if (input.IsInteracting) TryInteractWithDoor(world, player, hit.HitEntity.Value, door); + } + } + } + + private void TryInteractWithDoor(World world, Entity interactor, Entity doorEntity, DoorComponent door) + { + switch (door.CurrentState) + { + case DoorComponent.DoorState.Locked: + if (CheckRequirements(world, interactor, door)) + { + world.Logger.Info($"Door {doorEntity.Id} requirements met. Unlocking door."); + door.CurrentState = DoorComponent.DoorState.Opening; + if (door.IsOneTimeUnlock) door.Requirements.Clear(); + + world.PublishEvent(new DoorStateChangedEvent(doorEntity, door.CurrentState)); + } + else + { + world.Logger.Info($"Door {doorEntity.Id} requirements not met. Cannot unlock door."); + world.PublishEvent(new DoorLockedEvent(doorEntity, interactor)); + } + + break; + case DoorComponent.DoorState.Closed: + door.CurrentState = DoorComponent.DoorState.Opening; + world.PublishEvent(new DoorStateChangedEvent(doorEntity, door.CurrentState)); + break; + case DoorComponent.DoorState.Open: + door.CurrentState = DoorComponent.DoorState.Closing; + world.PublishEvent(new DoorStateChangedEvent(doorEntity, door.CurrentState)); + break; + } + } + + private bool CheckRequirements(World world, Entity interactor, DoorComponent door) + { + foreach (var req in door.Requirements) + if (!req.IsMet(interactor, world)) + return false; + + foreach (var req in door.Requirements) req.ApplySideEffects(interactor, world); + + return true; + } +} \ No newline at end of file diff --git a/GameCore/Interaction/Interfaces/IInteractionRequirement.cs b/GameCore/Interaction/Interfaces/IInteractionRequirement.cs new file mode 100644 index 0000000..977dba8 --- /dev/null +++ b/GameCore/Interaction/Interfaces/IInteractionRequirement.cs @@ -0,0 +1,9 @@ +using GameCore.ECS; + +namespace GameCore.Interaction.Interfaces; + +public interface IInteractionRequirement +{ + bool IsMet(Entity interactor, World world); + void ApplySideEffects(Entity interactor, World world); +} \ No newline at end of file diff --git a/GameCore/Interaction/IsLookingAtInteractableComponent.cs b/GameCore/Interaction/IsLookingAtInteractableComponent.cs new file mode 100644 index 0000000..df33ff7 --- /dev/null +++ b/GameCore/Interaction/IsLookingAtInteractableComponent.cs @@ -0,0 +1,9 @@ +using GameCore.ECS; +using GameCore.ECS.Interfaces; + +namespace GameCore.Interaction; + +public class IsLookingAtInteractableComponent(Entity target) : IComponent +{ + public readonly Entity Target = target; +} \ No newline at end of file diff --git a/GameCore/Interaction/RequiresAttributeRequirement.cs b/GameCore/Interaction/RequiresAttributeRequirement.cs new file mode 100644 index 0000000..3daa8e1 --- /dev/null +++ b/GameCore/Interaction/RequiresAttributeRequirement.cs @@ -0,0 +1,37 @@ +using GameCore.Attributes; +using GameCore.ECS; +using GameCore.Interaction.Interfaces; +using Attribute = GameCore.Attributes.Attribute; + +namespace GameCore.Interaction; + +public enum ComparisonType +{ + GreaterOrEqual = 0, + LessThan = 1, + Equal = 2, +} + +public class RequiresAttributeRequirement(Attribute attribute, float value, ComparisonType comparison) + : IInteractionRequirement +{ + public bool IsMet(Entity interactor, World world) + { + var attributes = world.GetComponent(interactor); + if (attributes == null) return false; + + var actualValue = attributes.GetValue(attribute); + + return comparison switch + { + ComparisonType.GreaterOrEqual => actualValue >= value, + ComparisonType.LessThan => actualValue < value, + ComparisonType.Equal => System.Math.Abs(actualValue - value) < 0.0001f, + _ => false + }; + } + + public void ApplySideEffects(Entity interactor, World world) + { + } +} \ No newline at end of file diff --git a/GameCore/Interaction/RequiresItemRequirement.cs b/GameCore/Interaction/RequiresItemRequirement.cs new file mode 100644 index 0000000..37d1d2b --- /dev/null +++ b/GameCore/Interaction/RequiresItemRequirement.cs @@ -0,0 +1,26 @@ +using GameCore.ECS; +using GameCore.Events; +using GameCore.Interaction.Interfaces; +using GameCore.Inventory; + +namespace GameCore.Interaction; + +public class RequiresItemRequirement(string itemId, int quantity, bool consumeItem) : IInteractionRequirement +{ + public bool IsMet(Entity interactor, World world) + { + var inventory = world.GetComponent(interactor); + return inventory != null && inventory.HasItem(itemId, quantity); + } + + public void ApplySideEffects(Entity interactor, World world) + { + if (!consumeItem) return; + + var inventory = world.GetComponent(interactor); + if (inventory == null || !inventory.RemoveItem(itemId, quantity)) return; + + var newQuantity = inventory.GetItemCount(itemId); + world.PublishEvent(new InventoryItemChangedEvent(interactor, itemId, newQuantity)); + } +} \ No newline at end of file