[1] feat: add interactions system #1

Merged
Filip merged 4 commits from feature/1-add-interactions into master 2026-08-26 17:46:49 +00:00
28 changed files with 255 additions and 0 deletions
Showing only changes of commit 25e32f20d9 - Show all commits
@@ -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);
}
@@ -0,0 +1 @@
uid://b1m7q4y9c5v2n
@@ -0,0 +1,6 @@
namespace Game.Features.Interactions.Contracts;
public interface IInteractionService
{
InteractionResult TryInteract(InteractionRequest request);
}
@@ -0,0 +1 @@
uid://a3n8v5r1q7m9c
@@ -0,0 +1,8 @@
namespace Game.Features.Interactions.Contracts;
public interface IInteractionTarget
{
InteractionTargetId Id { get; }
bool CanInteract { get; }
InteractionPrompt Prompt { get; }
}
@@ -0,0 +1 @@
uid://c7p2y5n8f1v6m
@@ -0,0 +1,6 @@
namespace Game.Features.Interactions.Contracts;
public interface IInteractionWorld
{
InteractionResult TryInteract(InteractionRequest request);
}
@@ -0,0 +1 @@
uid://d7k1x6m4v9q2b
@@ -0,0 +1,3 @@
namespace Game.Features.Interactions.Contracts;
public readonly record struct InteractionPrompt(string Text);
@@ -0,0 +1 @@
uid://b3m8r1x6d9w4k
@@ -0,0 +1,5 @@
using Game.Features.Players.Contracts;
namespace Game.Features.Interactions.Contracts;
public readonly record struct InteractionRequest(PlayerId PlayerId, InteractionTargetId TargetId);
@@ -0,0 +1 @@
uid://e2q7k5n1w8m4d
@@ -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);
}
@@ -0,0 +1 @@
uid://f9w2d7k4m6q1x
@@ -0,0 +1,9 @@
namespace Game.Features.Interactions.Contracts;
public enum InteractionStatus
{
Success,
TargetNotFound,
Unavailable,
Rejected
}
@@ -0,0 +1 @@
uid://b4y9m3r6v1c8p
@@ -0,0 +1,8 @@
using System;
namespace Game.Features.Interactions.Contracts;
public readonly record struct InteractionTargetId(Guid Value)
{
public override string ToString() => Value.ToString();
}
@@ -0,0 +1 @@
uid://c6m1v8q4r9x2k
@@ -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;
}
}
@@ -0,0 +1 @@
uid://b9r2w6n4f8k1v
@@ -0,0 +1,26 @@
using Godot;
namespace Game.Features.Interactions.Godot;
public partial class InteractionRaycaster : RayCast3D
{
public T GetTarget<T>() 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;
}
}
@@ -0,0 +1 @@
uid://e4m7k1x8c3q6h
@@ -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<InteractionTargetId, WorldInteractionController> _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);
}
}
@@ -0,0 +1 @@
uid://f6q1c9m4w7v2n
@@ -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;
}
}
}
}
@@ -0,0 +1 @@
uid://d2r8m5v1q9k4x
@@ -0,0 +1,8 @@
using System;
namespace Game.Features.Players.Contracts;
public readonly record struct PlayerId(Guid Value)
{
public override string ToString() => Value.ToString();
}
@@ -0,0 +1 @@
uid://d4m8x1q6v3k9r