From 3fcb31d92ffccb0bb49de8b5bff589227938aef6 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 30 Oct 2025 02:21:33 +0100 Subject: [PATCH] Add button interaction system with event publishing and requirements handling --- GameCore/ECS/World.cs | 15 ++++++ GameCore/Events/ButtonPressedEvent.cs | 9 ++++ GameCore/Interaction/ButtonComponent.cs | 14 ++++++ GameCore/Interaction/InteractionSystem.cs | 47 +++++++++++++++++-- GameCore/Interaction/WorldIdComponent.cs | 8 ++++ GameCore/Logic/DebugMessageAction.cs | 12 +++++ GameCore/Logic/Interfaces/ITriggerAction.cs | 8 ++++ GameCore/Logic/LogicSequenceComponent.cs | 13 ++++++ GameCore/Logic/LogicSequenceSystem.cs | 52 +++++++++++++++++++++ GameCore/Logic/SpawnEntityAction.cs | 35 ++++++++++++++ GameCore/Logic/UnlockDoorAction.cs | 32 +++++++++++++ 11 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 GameCore/Events/ButtonPressedEvent.cs create mode 100644 GameCore/Interaction/ButtonComponent.cs create mode 100644 GameCore/Interaction/WorldIdComponent.cs create mode 100644 GameCore/Logic/DebugMessageAction.cs create mode 100644 GameCore/Logic/Interfaces/ITriggerAction.cs create mode 100644 GameCore/Logic/LogicSequenceComponent.cs create mode 100644 GameCore/Logic/LogicSequenceSystem.cs create mode 100644 GameCore/Logic/SpawnEntityAction.cs create mode 100644 GameCore/Logic/UnlockDoorAction.cs diff --git a/GameCore/ECS/World.cs b/GameCore/ECS/World.cs index 3281994..876d609 100644 --- a/GameCore/ECS/World.cs +++ b/GameCore/ECS/World.cs @@ -3,6 +3,7 @@ using GameCore.ECS.Interfaces; using GameCore.Events; using GameCore.Events.Interfaces; using GameCore.Input.Interfaces; +using GameCore.Interaction; using GameCore.Logging.Interfaces; namespace GameCore.ECS; @@ -112,4 +113,18 @@ public class World(IInputService inputService, IWorldQuery worldQuery, Simulatio { _eventBus.Unsubscribe(handler); } + + public Entity? FindEntityByWorldId(string worldId) + { + if (string.IsNullOrEmpty(worldId)) return null; + + foreach (var entity in _componentsByEntityId.Keys.Select(id => new Entity(id))) + { + var idComponent = GetComponent(entity); + if (idComponent != null && idComponent.WorldId == worldId) + return entity; + } + + return null; + } } \ No newline at end of file diff --git a/GameCore/Events/ButtonPressedEvent.cs b/GameCore/Events/ButtonPressedEvent.cs new file mode 100644 index 0000000..55d055d --- /dev/null +++ b/GameCore/Events/ButtonPressedEvent.cs @@ -0,0 +1,9 @@ +using GameCore.Events.Interfaces; + +namespace GameCore.Events; + +public readonly struct ButtonPressedEvent(string channelId, bool newState) : IEvent +{ + public readonly string ChannelId = channelId; + public readonly bool NewState = newState; +} \ No newline at end of file diff --git a/GameCore/Interaction/ButtonComponent.cs b/GameCore/Interaction/ButtonComponent.cs new file mode 100644 index 0000000..a6ad533 --- /dev/null +++ b/GameCore/Interaction/ButtonComponent.cs @@ -0,0 +1,14 @@ +using GameCore.ECS.Interfaces; +using GameCore.Interaction.Interfaces; + +namespace GameCore.Interaction; + +public class ButtonComponent : IComponent +{ + public string ChannelId { get; set; } = "default_channel"; + public bool IsToggle { get; set; } = false; + public bool IsOneTimeUse { get; set; } = false; + public bool IsPressed { get; set; } = false; + public bool HasBeenUsed { get; set; } = false; + public List Requirements { get; set; } = []; +} \ No newline at end of file diff --git a/GameCore/Interaction/InteractionSystem.cs b/GameCore/Interaction/InteractionSystem.cs index 6199883..d6e86f8 100644 --- a/GameCore/Interaction/InteractionSystem.cs +++ b/GameCore/Interaction/InteractionSystem.cs @@ -2,6 +2,7 @@ using GameCore.ECS; using GameCore.ECS.Interfaces; using GameCore.Events; using GameCore.Input; +using GameCore.Interaction.Interfaces; using GameCore.Player; namespace GameCore.Interaction; @@ -26,12 +27,22 @@ public class InteractionSystem(float interactionRange) : ISystem if (hit.DidHit && hit.HitEntity.HasValue) { - var door = world.GetComponent(hit.HitEntity.Value); + var targetEntity = hit.HitEntity.Value; + + var door = world.GetComponent(targetEntity); if (door != null) { world.AddComponent(player, new IsLookingAtInteractableComponent(hit.HitEntity.Value)); if (input.IsInteracting) TryInteractWithDoor(world, player, hit.HitEntity.Value, door); + return; + } + + var button = world.GetComponent(targetEntity); + if (button != null) + { + world.AddComponent(player, new IsLookingAtInteractableComponent(targetEntity)); + if (input.IsInteracting) TryInteractWithButton(world, player, targetEntity, button); } } } @@ -41,7 +52,7 @@ public class InteractionSystem(float interactionRange) : ISystem switch (door.CurrentState) { case DoorComponent.DoorState.Locked: - if (CheckRequirements(world, interactor, door)) + if (CheckRequirements(world, interactor, door.Requirements)) { world.Logger.Info($"Door {doorEntity.Id} requirements met. Unlocking door."); door.CurrentState = DoorComponent.DoorState.Opening; @@ -67,13 +78,39 @@ public class InteractionSystem(float interactionRange) : ISystem } } - private bool CheckRequirements(World world, Entity interactor, DoorComponent door) + private void TryInteractWithButton(World world, Entity interactor, Entity buttonEntity, ButtonComponent button) { - foreach (var req in door.Requirements) + if (button.IsOneTimeUse && button.HasBeenUsed) + { + world.Logger.Info($"Button {buttonEntity.Id} is one-time-use and has already been used."); + return; + } + + if (CheckRequirements(world, interactor, button.Requirements)) + { + if (button.IsToggle) + button.IsPressed = !button.IsPressed; + else + button.IsPressed = true; + + button.HasBeenUsed = true; + world.Logger.Info( + $"Button {buttonEntity.Id} pressed. Channel: {button.ChannelId}, NewState: {button.IsPressed}"); + world.PublishEvent(new ButtonPressedEvent(button.ChannelId, button.IsPressed)); + } + else + { + world.Logger.Info($"Button {buttonEntity.Id} requirements not met."); + } + } + + private bool CheckRequirements(World world, Entity interactor, List requirements) + { + foreach (var req in requirements) if (!req.IsMet(interactor, world)) return false; - foreach (var req in door.Requirements) req.ApplySideEffects(interactor, world); + foreach (var req in requirements) req.ApplySideEffects(interactor, world); return true; } diff --git a/GameCore/Interaction/WorldIdComponent.cs b/GameCore/Interaction/WorldIdComponent.cs new file mode 100644 index 0000000..66646ff --- /dev/null +++ b/GameCore/Interaction/WorldIdComponent.cs @@ -0,0 +1,8 @@ +using GameCore.ECS.Interfaces; + +namespace GameCore.Interaction; + +public class WorldIdComponent(string worldId) : IComponent +{ + public readonly string WorldId = worldId; +} \ No newline at end of file diff --git a/GameCore/Logic/DebugMessageAction.cs b/GameCore/Logic/DebugMessageAction.cs new file mode 100644 index 0000000..543ab78 --- /dev/null +++ b/GameCore/Logic/DebugMessageAction.cs @@ -0,0 +1,12 @@ +using GameCore.ECS; +using GameCore.Logic.Interfaces; + +namespace GameCore.Logic; + +public class DebugMessageAction(string message) : ITriggerAction +{ + public void Execute(World world) + { + world.Logger.Debug($"[DebugMessageAction] {message}"); + } +} \ No newline at end of file diff --git a/GameCore/Logic/Interfaces/ITriggerAction.cs b/GameCore/Logic/Interfaces/ITriggerAction.cs new file mode 100644 index 0000000..fa2aaa2 --- /dev/null +++ b/GameCore/Logic/Interfaces/ITriggerAction.cs @@ -0,0 +1,8 @@ +using GameCore.ECS; + +namespace GameCore.Logic.Interfaces; + +public interface ITriggerAction +{ + void Execute(World world); +} \ No newline at end of file diff --git a/GameCore/Logic/LogicSequenceComponent.cs b/GameCore/Logic/LogicSequenceComponent.cs new file mode 100644 index 0000000..5a4f748 --- /dev/null +++ b/GameCore/Logic/LogicSequenceComponent.cs @@ -0,0 +1,13 @@ +using GameCore.ECS.Interfaces; +using GameCore.Logic.Interfaces; + +namespace GameCore.Logic; + +public class LogicSequenceComponent : IComponent +{ + public List RequiredChannels { get; set; } = []; + public HashSet ActivatedChannels { get; set; } = []; + public List OnCompleteActions { get; set; } = []; + public bool IsOneTimeTrigger { get; set; } = false; + public bool HasTriggered { get; set; } = false; +} \ No newline at end of file diff --git a/GameCore/Logic/LogicSequenceSystem.cs b/GameCore/Logic/LogicSequenceSystem.cs new file mode 100644 index 0000000..1a8e7ae --- /dev/null +++ b/GameCore/Logic/LogicSequenceSystem.cs @@ -0,0 +1,52 @@ +using GameCore.ECS; +using GameCore.ECS.Interfaces; +using GameCore.Events; + +namespace GameCore.Logic; + +public class LogicSequenceSystem : ISystem +{ + private readonly World _world; + + public LogicSequenceSystem(World world) + { + _world = world; + world.Subscribe(OnButtonPressed); + } + + public void Update(World world, float deltaTime) + { + } + + private void OnButtonPressed(ButtonPressedEvent e) + { + var logicEntities = _world.GetEntitiesWith(); + + foreach (var entity in logicEntities) + { + var logic = _world.GetComponent(entity); + if (logic == null || (logic.IsOneTimeTrigger && logic.HasTriggered)) continue; + + if (logic.RequiredChannels.Contains(e.ChannelId)) + { + if (e.NewState) + logic.ActivatedChannels.Add(e.ChannelId); + else + logic.ActivatedChannels.Remove(e.ChannelId); + + _world.Logger.Info( + $"[LogicSequenceSystem] Channel '{e.ChannelId}' activated. Total {logic.ActivatedChannels.Count}/{logic.RequiredChannels.Count}"); + + if (logic.ActivatedChannels.Count == logic.RequiredChannels.Count) + { + _world.Logger.Info("[LogicSequenceSystem] All channels activated. Executing actions."); + foreach (var action in logic.OnCompleteActions) action.Execute(_world); + + if (logic.IsOneTimeTrigger) logic.HasTriggered = true; + + logic.ActivatedChannels.Clear(); + } + } + } + } +} \ No newline at end of file diff --git a/GameCore/Logic/SpawnEntityAction.cs b/GameCore/Logic/SpawnEntityAction.cs new file mode 100644 index 0000000..a19aa3a --- /dev/null +++ b/GameCore/Logic/SpawnEntityAction.cs @@ -0,0 +1,35 @@ +using GameCore.ECS; +using GameCore.Events; +using GameCore.Logic.Interfaces; +using GameCore.Math; +using GameCore.Physics; + +namespace GameCore.Logic; + +public class SpawnEntityAction(string archetypeId, string spawnerWorldId) : ITriggerAction +{ + public void Execute(World world) + { + var spawnerEntity = world.FindEntityByWorldId(spawnerWorldId); + if (spawnerEntity == null) + { + world.Logger.Warn($"[SpawnEntityAction] Could not find spawner with WorldId: {spawnerWorldId}"); + return; + } + + var position = world.GetComponent(spawnerEntity.Value); + if (position == null) + { + world.Logger.Warn($"[SpawnEntityAction] Spawner '{spawnerWorldId}' does not have a PositionComponent."); + return; + } + + world.PublishEvent(new SpawnEntityEvent( + archetypeId, + position.Position, + Vector3.Zero, + default)); + + world.Logger.Info($"[SpawnEntityAction] Spawning '{archetypeId}' at '{spawnerWorldId}'."); + } +} \ No newline at end of file diff --git a/GameCore/Logic/UnlockDoorAction.cs b/GameCore/Logic/UnlockDoorAction.cs new file mode 100644 index 0000000..2a1ab64 --- /dev/null +++ b/GameCore/Logic/UnlockDoorAction.cs @@ -0,0 +1,32 @@ +using GameCore.ECS; +using GameCore.Interaction; +using GameCore.Logic.Interfaces; + +namespace GameCore.Logic; + +public class UnlockDoorAction(string targetWorldId) : ITriggerAction +{ + public void Execute(World world) + { + var doorEntity = world.FindEntityByWorldId(targetWorldId); + if (doorEntity == null) + { + world.Logger.Warn($"[UnlockDoorAction] Could not find entity with WorldId: {targetWorldId}"); + return; + } + + var door = world.GetComponent(doorEntity.Value); + if (door == null) + { + world.Logger.Warn($"[UnlockDoorAction] Entity '{targetWorldId}' does not have a DoorComponent."); + return; + } + + if (door.CurrentState == DoorComponent.DoorState.Locked) + { + door.CurrentState = DoorComponent.DoorState.Closed; + door.Requirements.Clear(); + world.Logger.Info($"[UnlockDoorAction] Unlocked door: {targetWorldId}"); + } + } +} \ No newline at end of file