feat: add player-aware world interaction system
This commit is contained in:
@@ -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
|
||||||
Reference in New Issue
Block a user