Add button interaction system with event publishing and requirements handling

This commit is contained in:
2025-10-30 02:21:33 +01:00
parent 86afb57809
commit 3fcb31d92f
11 changed files with 240 additions and 5 deletions

View File

@@ -3,6 +3,7 @@ using GameCore.ECS.Interfaces;
using GameCore.Events; using GameCore.Events;
using GameCore.Events.Interfaces; using GameCore.Events.Interfaces;
using GameCore.Input.Interfaces; using GameCore.Input.Interfaces;
using GameCore.Interaction;
using GameCore.Logging.Interfaces; using GameCore.Logging.Interfaces;
namespace GameCore.ECS; namespace GameCore.ECS;
@@ -112,4 +113,18 @@ public class World(IInputService inputService, IWorldQuery worldQuery, Simulatio
{ {
_eventBus.Unsubscribe(handler); _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<WorldIdComponent>(entity);
if (idComponent != null && idComponent.WorldId == worldId)
return entity;
}
return null;
}
} }

View File

@@ -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;
}

View File

@@ -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<IInteractionRequirement> Requirements { get; set; } = [];
}

View File

@@ -2,6 +2,7 @@ using GameCore.ECS;
using GameCore.ECS.Interfaces; using GameCore.ECS.Interfaces;
using GameCore.Events; using GameCore.Events;
using GameCore.Input; using GameCore.Input;
using GameCore.Interaction.Interfaces;
using GameCore.Player; using GameCore.Player;
namespace GameCore.Interaction; namespace GameCore.Interaction;
@@ -26,12 +27,22 @@ public class InteractionSystem(float interactionRange) : ISystem
if (hit.DidHit && hit.HitEntity.HasValue) if (hit.DidHit && hit.HitEntity.HasValue)
{ {
var door = world.GetComponent<DoorComponent>(hit.HitEntity.Value); var targetEntity = hit.HitEntity.Value;
var door = world.GetComponent<DoorComponent>(targetEntity);
if (door != null) if (door != null)
{ {
world.AddComponent(player, new IsLookingAtInteractableComponent(hit.HitEntity.Value)); world.AddComponent(player, new IsLookingAtInteractableComponent(hit.HitEntity.Value));
if (input.IsInteracting) TryInteractWithDoor(world, player, hit.HitEntity.Value, door); if (input.IsInteracting) TryInteractWithDoor(world, player, hit.HitEntity.Value, door);
return;
}
var button = world.GetComponent<ButtonComponent>(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) switch (door.CurrentState)
{ {
case DoorComponent.DoorState.Locked: 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."); world.Logger.Info($"Door {doorEntity.Id} requirements met. Unlocking door.");
door.CurrentState = DoorComponent.DoorState.Opening; 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<IInteractionRequirement> requirements)
{
foreach (var req in requirements)
if (!req.IsMet(interactor, world)) if (!req.IsMet(interactor, world))
return false; return false;
foreach (var req in door.Requirements) req.ApplySideEffects(interactor, world); foreach (var req in requirements) req.ApplySideEffects(interactor, world);
return true; return true;
} }

View File

@@ -0,0 +1,8 @@
using GameCore.ECS.Interfaces;
namespace GameCore.Interaction;
public class WorldIdComponent(string worldId) : IComponent
{
public readonly string WorldId = worldId;
}

View File

@@ -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}");
}
}

View File

@@ -0,0 +1,8 @@
using GameCore.ECS;
namespace GameCore.Logic.Interfaces;
public interface ITriggerAction
{
void Execute(World world);
}

View File

@@ -0,0 +1,13 @@
using GameCore.ECS.Interfaces;
using GameCore.Logic.Interfaces;
namespace GameCore.Logic;
public class LogicSequenceComponent : IComponent
{
public List<string> RequiredChannels { get; set; } = [];
public HashSet<string> ActivatedChannels { get; set; } = [];
public List<ITriggerAction> OnCompleteActions { get; set; } = [];
public bool IsOneTimeTrigger { get; set; } = false;
public bool HasTriggered { get; set; } = false;
}

View File

@@ -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<ButtonPressedEvent>(OnButtonPressed);
}
public void Update(World world, float deltaTime)
{
}
private void OnButtonPressed(ButtonPressedEvent e)
{
var logicEntities = _world.GetEntitiesWith<LogicSequenceComponent>();
foreach (var entity in logicEntities)
{
var logic = _world.GetComponent<LogicSequenceComponent>(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();
}
}
}
}
}

View File

@@ -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<PositionComponent>(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}'.");
}
}

View File

@@ -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<DoorComponent>(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}");
}
}
}