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