diff --git a/src/Features/Interactions/Application/InteractionService.cs b/src/Features/Interactions/Application/InteractionService.cs new file mode 100644 index 0000000..4657e77 --- /dev/null +++ b/src/Features/Interactions/Application/InteractionService.cs @@ -0,0 +1,8 @@ +using Game.Features.Interactions.Contracts; + +namespace Game.Features.Interactions.Application; + +public sealed class InteractionService(IInteractionWorld world) : IInteractionService +{ + public InteractionResult TryInteract(InteractionRequest request) => world.TryInteract(request); +} diff --git a/src/Features/Interactions/Application/InteractionService.cs.uid b/src/Features/Interactions/Application/InteractionService.cs.uid new file mode 100644 index 0000000..156e7ae --- /dev/null +++ b/src/Features/Interactions/Application/InteractionService.cs.uid @@ -0,0 +1 @@ +uid://b1m7q4y9c5v2n diff --git a/src/Features/Interactions/Contracts/IInteractionService.cs b/src/Features/Interactions/Contracts/IInteractionService.cs new file mode 100644 index 0000000..dbf9365 --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionService.cs @@ -0,0 +1,6 @@ +namespace Game.Features.Interactions.Contracts; + +public interface IInteractionService +{ + InteractionResult TryInteract(InteractionRequest request); +} diff --git a/src/Features/Interactions/Contracts/IInteractionService.cs.uid b/src/Features/Interactions/Contracts/IInteractionService.cs.uid new file mode 100644 index 0000000..6407843 --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionService.cs.uid @@ -0,0 +1 @@ +uid://a3n8v5r1q7m9c diff --git a/src/Features/Interactions/Contracts/IInteractionTarget.cs b/src/Features/Interactions/Contracts/IInteractionTarget.cs new file mode 100644 index 0000000..e6c18c1 --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionTarget.cs @@ -0,0 +1,8 @@ +namespace Game.Features.Interactions.Contracts; + +public interface IInteractionTarget +{ + InteractionTargetId Id { get; } + bool CanInteract { get; } + InteractionPrompt Prompt { get; } +} diff --git a/src/Features/Interactions/Contracts/IInteractionTarget.cs.uid b/src/Features/Interactions/Contracts/IInteractionTarget.cs.uid new file mode 100644 index 0000000..e57a990 --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionTarget.cs.uid @@ -0,0 +1 @@ +uid://c7p2y5n8f1v6m diff --git a/src/Features/Interactions/Contracts/IInteractionWorld.cs b/src/Features/Interactions/Contracts/IInteractionWorld.cs new file mode 100644 index 0000000..79e4a1a --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionWorld.cs @@ -0,0 +1,6 @@ +namespace Game.Features.Interactions.Contracts; + +public interface IInteractionWorld +{ + InteractionResult TryInteract(InteractionRequest request); +} diff --git a/src/Features/Interactions/Contracts/IInteractionWorld.cs.uid b/src/Features/Interactions/Contracts/IInteractionWorld.cs.uid new file mode 100644 index 0000000..880f095 --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionWorld.cs.uid @@ -0,0 +1 @@ +uid://d7k1x6m4v9q2b diff --git a/src/Features/Interactions/Contracts/InteractionPrompt.cs b/src/Features/Interactions/Contracts/InteractionPrompt.cs new file mode 100644 index 0000000..c908810 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionPrompt.cs @@ -0,0 +1,3 @@ +namespace Game.Features.Interactions.Contracts; + +public readonly record struct InteractionPrompt(string Text); diff --git a/src/Features/Interactions/Contracts/InteractionPrompt.cs.uid b/src/Features/Interactions/Contracts/InteractionPrompt.cs.uid new file mode 100644 index 0000000..6bc8887 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionPrompt.cs.uid @@ -0,0 +1 @@ +uid://b3m8r1x6d9w4k diff --git a/src/Features/Interactions/Contracts/InteractionRequest.cs b/src/Features/Interactions/Contracts/InteractionRequest.cs new file mode 100644 index 0000000..3ee11be --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionRequest.cs @@ -0,0 +1,5 @@ +using Game.Features.Players.Contracts; + +namespace Game.Features.Interactions.Contracts; + +public readonly record struct InteractionRequest(PlayerId PlayerId, InteractionTargetId TargetId); diff --git a/src/Features/Interactions/Contracts/InteractionRequest.cs.uid b/src/Features/Interactions/Contracts/InteractionRequest.cs.uid new file mode 100644 index 0000000..84ff848 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionRequest.cs.uid @@ -0,0 +1 @@ +uid://e2q7k5n1w8m4d diff --git a/src/Features/Interactions/Contracts/InteractionResult.cs b/src/Features/Interactions/Contracts/InteractionResult.cs new file mode 100644 index 0000000..6ca4d92 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionResult.cs @@ -0,0 +1,10 @@ +namespace Game.Features.Interactions.Contracts; + +public readonly record struct InteractionResult(InteractionStatus Status, string Message = "") +{ + public bool Success => Status == InteractionStatus.Success; + + public static InteractionResult Ok() => new(InteractionStatus.Success); + + public static InteractionResult Fail(InteractionStatus status, string message) => new(status, message); +} diff --git a/src/Features/Interactions/Contracts/InteractionResult.cs.uid b/src/Features/Interactions/Contracts/InteractionResult.cs.uid new file mode 100644 index 0000000..cdc88f1 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionResult.cs.uid @@ -0,0 +1 @@ +uid://f9w2d7k4m6q1x diff --git a/src/Features/Interactions/Contracts/InteractionStatus.cs b/src/Features/Interactions/Contracts/InteractionStatus.cs new file mode 100644 index 0000000..d324c1c --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionStatus.cs @@ -0,0 +1,9 @@ +namespace Game.Features.Interactions.Contracts; + +public enum InteractionStatus +{ + Success, + TargetNotFound, + Unavailable, + Rejected +} diff --git a/src/Features/Interactions/Contracts/InteractionStatus.cs.uid b/src/Features/Interactions/Contracts/InteractionStatus.cs.uid new file mode 100644 index 0000000..ae4d81d --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionStatus.cs.uid @@ -0,0 +1 @@ +uid://b4y9m3r6v1c8p diff --git a/src/Features/Interactions/Contracts/InteractionTargetId.cs b/src/Features/Interactions/Contracts/InteractionTargetId.cs new file mode 100644 index 0000000..2b31238 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionTargetId.cs @@ -0,0 +1,8 @@ +using System; + +namespace Game.Features.Interactions.Contracts; + +public readonly record struct InteractionTargetId(Guid Value) +{ + public override string ToString() => Value.ToString(); +} diff --git a/src/Features/Interactions/Contracts/InteractionTargetId.cs.uid b/src/Features/Interactions/Contracts/InteractionTargetId.cs.uid new file mode 100644 index 0000000..8f0098d --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionTargetId.cs.uid @@ -0,0 +1 @@ +uid://c6m1v8q4r9x2k diff --git a/src/Features/Interactions/Godot/InteractionPromptController.cs b/src/Features/Interactions/Godot/InteractionPromptController.cs new file mode 100644 index 0000000..b7fcc30 --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionPromptController.cs @@ -0,0 +1,27 @@ +using Game.Godot.Extensions; +using Godot; + +namespace Game.Features.Interactions.Godot; + +public partial class InteractionPromptController : CanvasLayer +{ + [Export] public Label PromptLabel; + + public override void _Ready() + { + this.AssertRequiredExports(); + Clear(); + } + + public void ShowPrompt(string text) + { + PromptLabel.Text = text; + PromptLabel.Visible = text.Length > 0; + } + + public void Clear() + { + PromptLabel.Text = string.Empty; + PromptLabel.Visible = false; + } +} diff --git a/src/Features/Interactions/Godot/InteractionPromptController.cs.uid b/src/Features/Interactions/Godot/InteractionPromptController.cs.uid new file mode 100644 index 0000000..b6a2c4a --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionPromptController.cs.uid @@ -0,0 +1 @@ +uid://b9r2w6n4f8k1v diff --git a/src/Features/Interactions/Godot/InteractionRaycaster.cs b/src/Features/Interactions/Godot/InteractionRaycaster.cs new file mode 100644 index 0000000..faa417a --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionRaycaster.cs @@ -0,0 +1,26 @@ +using Godot; + +namespace Game.Features.Interactions.Godot; + +public partial class InteractionRaycaster : RayCast3D +{ + public T GetTarget() where T : class + { + if (!IsColliding() || GetCollider() is not Node node) + { + return null; + } + + while (node is not null) + { + if (node is T target) + { + return target; + } + + node = node.GetParent(); + } + + return null; + } +} diff --git a/src/Features/Interactions/Godot/InteractionRaycaster.cs.uid b/src/Features/Interactions/Godot/InteractionRaycaster.cs.uid new file mode 100644 index 0000000..4ee7ffe --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionRaycaster.cs.uid @@ -0,0 +1 @@ +uid://e4m7k1x8c3q6h diff --git a/src/Features/Interactions/Godot/InteractionWorldController.cs b/src/Features/Interactions/Godot/InteractionWorldController.cs new file mode 100644 index 0000000..22a47f3 --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionWorldController.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using Game.Features.Interactions.Contracts; +using Godot; + +namespace Game.Features.Interactions.Godot; + +public partial class InteractionWorldController : Node, IInteractionWorld +{ + public const string RegistryGroup = "interaction_world_registry"; + + private readonly Dictionary _targets = []; + + public override void _EnterTree() + { + AddToGroup(RegistryGroup); + } + + public void RegisterSceneTargets() + { + foreach (Node node in GetTree().GetNodesInGroup(WorldInteractionController.InteractionGroup)) + { + if (node is WorldInteractionController target) + { + Register(target); + } + } + } + + public void Register(WorldInteractionController target) + { + if (_targets.TryGetValue(target.Id, out WorldInteractionController existing)) + { + if (ReferenceEquals(existing, target)) + { + return; + } + + throw new InvalidOperationException($"Duplicate interaction target id: {target.Id}."); + } + + _targets.Add(target.Id, target); + target.RegisterWith(this); + } + + public void Unregister(InteractionTargetId targetId) + { + _targets.Remove(targetId); + } + + public InteractionResult TryInteract(InteractionRequest request) + { + if (!_targets.TryGetValue(request.TargetId, out WorldInteractionController target)) + { + return InteractionResult.Fail(InteractionStatus.TargetNotFound, $"Target {request.TargetId} was not found."); + } + + if (!target.CanInteract) + { + return InteractionResult.Fail(InteractionStatus.Unavailable, "The target is not currently available."); + } + + return target.Execute(request.PlayerId); + } +} diff --git a/src/Features/Interactions/Godot/InteractionWorldController.cs.uid b/src/Features/Interactions/Godot/InteractionWorldController.cs.uid new file mode 100644 index 0000000..5ee2706 --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionWorldController.cs.uid @@ -0,0 +1 @@ +uid://f6q1c9m4w7v2n diff --git a/src/Features/Interactions/Godot/WorldInteractionController.cs b/src/Features/Interactions/Godot/WorldInteractionController.cs new file mode 100644 index 0000000..d2a1d23 --- /dev/null +++ b/src/Features/Interactions/Godot/WorldInteractionController.cs @@ -0,0 +1,52 @@ +using System; +using Game.Features.Interactions.Contracts; +using Game.Features.Players.Contracts; +using Godot; + +namespace Game.Features.Interactions.Godot; + +public abstract partial class WorldInteractionController : Node3D, IInteractionTarget +{ + public const string InteractionGroup = "world_interactions"; + + [Export] public string InstanceId; + [Export] public string PromptText = "Wejdź w interakcję"; + + public InteractionTargetId Id { get; private set; } + public virtual bool CanInteract => true; + public InteractionPrompt Prompt => new(PromptText); + + private InteractionWorldController _registry; + + public override void _Ready() + { + Guid id = string.IsNullOrWhiteSpace(InstanceId) ? Guid.NewGuid() : Guid.Parse(InstanceId); + Id = new InteractionTargetId(id); + AddToGroup(InteractionGroup); + RegisterWithAvailableWorld(); + } + + public override void _ExitTree() + { + _registry?.Unregister(Id); + } + + public void RegisterWith(InteractionWorldController registry) + { + _registry = registry; + } + + public abstract InteractionResult Execute(PlayerId playerId); + + private void RegisterWithAvailableWorld() + { + foreach (Node node in GetTree().GetNodesInGroup(InteractionWorldController.RegistryGroup)) + { + if (node is InteractionWorldController registry) + { + registry.Register(this); + return; + } + } + } +} diff --git a/src/Features/Interactions/Godot/WorldInteractionController.cs.uid b/src/Features/Interactions/Godot/WorldInteractionController.cs.uid new file mode 100644 index 0000000..ac116fc --- /dev/null +++ b/src/Features/Interactions/Godot/WorldInteractionController.cs.uid @@ -0,0 +1 @@ +uid://d2r8m5v1q9k4x diff --git a/src/Features/Players/Contracts/PlayerId.cs b/src/Features/Players/Contracts/PlayerId.cs new file mode 100644 index 0000000..c5f15ff --- /dev/null +++ b/src/Features/Players/Contracts/PlayerId.cs @@ -0,0 +1,8 @@ +using System; + +namespace Game.Features.Players.Contracts; + +public readonly record struct PlayerId(Guid Value) +{ + public override string ToString() => Value.ToString(); +} diff --git a/src/Features/Players/Contracts/PlayerId.cs.uid b/src/Features/Players/Contracts/PlayerId.cs.uid new file mode 100644 index 0000000..888852e --- /dev/null +++ b/src/Features/Players/Contracts/PlayerId.cs.uid @@ -0,0 +1 @@ +uid://d4m8x1q6v3k9r