refactor: restructure interaction system into clean layers
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
using Game.Features.Interactions.Contracts;
|
||||
|
||||
namespace Game.Features.Interactions.Application;
|
||||
|
||||
public sealed class Interact(IInteractionRegistry registry) : IInteractionHandler
|
||||
{
|
||||
public InteractionResult TryInteract(InteractionRequest request)
|
||||
{
|
||||
if (!registry.TryGetTarget(request.TargetId, out IInteractionTarget 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);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
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,19 @@
|
||||
using Game.Features.Items.Contracts;
|
||||
|
||||
namespace Game.Features.Interactions.Application;
|
||||
|
||||
public readonly record struct PickupAvailability(bool LeftHand, bool RightHand, bool RequiresBoth);
|
||||
|
||||
public sealed class ResolvePickupAvailability
|
||||
{
|
||||
public PickupAvailability Resolve(HandRequirement requirement, bool leftHandFree, bool rightHandFree)
|
||||
{
|
||||
if (requirement == HandRequirement.Both)
|
||||
{
|
||||
bool canUseBoth = leftHandFree && rightHandFree;
|
||||
return new PickupAvailability(canUseBoth, canUseBoth, true);
|
||||
}
|
||||
|
||||
return new PickupAvailability(leftHandFree, rightHandFree, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c6mhpn06a47qu
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
namespace Game.Features.Interactions.Contracts;
|
||||
|
||||
public interface IInteractionService
|
||||
public interface IInteractionHandler
|
||||
{
|
||||
InteractionResult TryInteract(InteractionRequest request);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Game.Features.Interactions.Contracts;
|
||||
|
||||
public interface IInteractionRegistry
|
||||
{
|
||||
void Register(IInteractionTarget target);
|
||||
void Unregister(InteractionTargetId targetId);
|
||||
bool TryGetTarget(InteractionTargetId targetId, out IInteractionTarget target);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://baejppnflmpnn
|
||||
@@ -1,8 +1,12 @@
|
||||
using Game.Features.Players.Contracts;
|
||||
|
||||
namespace Game.Features.Interactions.Contracts;
|
||||
|
||||
public interface IInteractionTarget
|
||||
{
|
||||
InteractionTargetId Id { get; }
|
||||
InteractionTargetId InteractionTargetId { get; }
|
||||
bool CanInteract { get; }
|
||||
InteractionPrompt Prompt { get; }
|
||||
|
||||
InteractionResult Execute(PlayerId playerId);
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Game.Features.Interactions.Contracts;
|
||||
|
||||
public interface IInteractionWorld
|
||||
{
|
||||
InteractionResult TryInteract(InteractionRequest request);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
uid://d7k1x6m4v9q2b
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using Game.Shared.Domain;
|
||||
|
||||
namespace Game.Features.Interactions.Contracts;
|
||||
|
||||
public readonly record struct InteractionTargetId(Guid Value)
|
||||
public readonly record struct InteractionTargetId(InstanceId Value)
|
||||
{
|
||||
public static InteractionTargetId From(InstanceId value) => new(value);
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Godot;
|
||||
|
||||
namespace Game.Features.Interactions.Domain;
|
||||
|
||||
public readonly record struct DropRayHit(
|
||||
Vector3 Origin,
|
||||
Vector3 Direction,
|
||||
float RayLength,
|
||||
bool IsBlocked,
|
||||
Vector3 BlockPoint
|
||||
);
|
||||
|
||||
public static class DropPosition
|
||||
{
|
||||
public static Vector3 CalculateFrom(DropRayHit hit)
|
||||
{
|
||||
float distance = hit.RayLength;
|
||||
|
||||
if (hit.IsBlocked)
|
||||
{
|
||||
distance = Mathf.Max(0.25f, hit.Origin.DistanceTo(hit.BlockPoint) - 0.2f);
|
||||
}
|
||||
|
||||
return hit.Origin + hit.Direction * distance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://derjakse05m3d
|
||||
@@ -0,0 +1,58 @@
|
||||
using Game.Features.Interactions.Contracts;
|
||||
using Game.Features.Players.Contracts;
|
||||
using Game.Godot.Extensions;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Game.Features.Interactions.Godot;
|
||||
|
||||
public abstract partial class InteractableController : Node3D, IInteractionTarget
|
||||
{
|
||||
public const string InteractionGroup = "interactions";
|
||||
|
||||
[Export]
|
||||
public string Key;
|
||||
|
||||
[Export]
|
||||
public string PromptText = "Wejdź w interakcję";
|
||||
|
||||
public InteractionTargetId InteractionTargetId { get; private set; }
|
||||
public virtual bool CanInteract => true;
|
||||
public InteractionPrompt Prompt => new(PromptText);
|
||||
|
||||
private InteractionsRegistryController _registry;
|
||||
|
||||
private void RegisterWithAvailableRegistry()
|
||||
{
|
||||
Array<Node> availableRegistries = GetTree()
|
||||
.GetNodesInGroup(InteractionsRegistryController.RegistryGroup);
|
||||
|
||||
foreach (Node node in availableRegistries)
|
||||
{
|
||||
if (node is InteractionsRegistryController registry)
|
||||
{
|
||||
registry.Register(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UseRegistry(InteractionsRegistryController registry)
|
||||
{
|
||||
_registry = registry;
|
||||
}
|
||||
|
||||
public abstract InteractionResult Execute(PlayerId playerId);
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
_registry?.Unregister(InteractionTargetId);
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
InteractionTargetId = InteractionTargetId.From(this.GetStableInstanceIdFrom(Key));
|
||||
AddToGroup(InteractionGroup);
|
||||
RegisterWithAvailableRegistry();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bh0e1kwa1i4r
|
||||
@@ -4,7 +4,7 @@ namespace Game.Features.Interactions.Godot;
|
||||
|
||||
public partial class InteractionRaycaster : RayCast3D
|
||||
{
|
||||
public T GetTarget<T>() where T : class
|
||||
public T GetTargetOrNull<T>() where T : class
|
||||
{
|
||||
if (!IsColliding() || GetCollider() is not Node node)
|
||||
{
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
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,47 @@
|
||||
using Game.Features.Interactions.Contracts;
|
||||
using Godot;
|
||||
using GodotNodeArray = Godot.Collections.Array<Godot.Node>;
|
||||
|
||||
namespace Game.Features.Interactions.Godot;
|
||||
|
||||
public partial class InteractionsRegistryController : Node
|
||||
{
|
||||
public const string RegistryGroup = "interactions_registry";
|
||||
|
||||
private IInteractionRegistry _registry;
|
||||
|
||||
private GodotNodeArray GetInteractionTargetNodesInScene() =>
|
||||
GetTree().GetNodesInGroup(InteractableController.InteractionGroup);
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
AddToGroup(RegistryGroup);
|
||||
}
|
||||
|
||||
public void UseRegistry(IInteractionRegistry registry)
|
||||
{
|
||||
_registry = registry;
|
||||
}
|
||||
|
||||
public void RegisterSceneTargets()
|
||||
{
|
||||
foreach (Node node in GetInteractionTargetNodesInScene())
|
||||
{
|
||||
if (node is InteractableController target)
|
||||
{
|
||||
Register(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Register(InteractableController target)
|
||||
{
|
||||
_registry.Register(target);
|
||||
target.UseRegistry(this);
|
||||
}
|
||||
|
||||
public void Unregister(InteractionTargetId targetId)
|
||||
{
|
||||
_registry.Unregister(targetId);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Game.Features.Interactions.Application;
|
||||
using Game.Features.Interactions.Contracts;
|
||||
using Game.Features.Interactions.Domain;
|
||||
using Game.Features.Items.Contracts;
|
||||
using Game.Features.Logging.Contracts;
|
||||
using Game.Features.Players.Contracts;
|
||||
using Game.Godot;
|
||||
using Game.Godot.Extensions;
|
||||
@@ -11,39 +14,177 @@ namespace Game.Features.Interactions.Godot;
|
||||
|
||||
public partial class PlayerInteractionController : Node, IItemSpawnPoint
|
||||
{
|
||||
[Export] public Camera3D Camera;
|
||||
[Export] public CharacterBody3D Body;
|
||||
[Export] public InteractionRaycaster InteractionRaycaster;
|
||||
[Export] public RayCast3D DropRaycaster;
|
||||
[Export] public InteractionPromptController PromptController;
|
||||
[Export] public Node3D LeftHandAnchor;
|
||||
[Export] public Node3D RightHandAnchor;
|
||||
[Export]
|
||||
public Camera3D Camera;
|
||||
|
||||
[Export]
|
||||
public CharacterBody3D Body;
|
||||
|
||||
[Export]
|
||||
public InteractionRaycaster InteractionRaycaster;
|
||||
|
||||
[Export]
|
||||
public RayCast3D DropRaycaster;
|
||||
|
||||
[Export]
|
||||
public InteractionPromptController PromptController;
|
||||
|
||||
[Export]
|
||||
public Node3D LeftHandAnchor;
|
||||
|
||||
[Export]
|
||||
public Node3D RightHandAnchor;
|
||||
|
||||
private const string LogCategory = "PlayerInteraction";
|
||||
private IItemInteractionService _items;
|
||||
private IInteractionService _interactions;
|
||||
private IInteractionHandler _interactions;
|
||||
private ILogger _logger;
|
||||
private PlayerId _playerId;
|
||||
private readonly HashSet<Hand> _dropActions = [];
|
||||
private readonly Dictionary<Hand, InteractionTargetId?> _activeUseTargets = [];
|
||||
private readonly ResolvePickupAvailability _resolvePickupAvailability = new();
|
||||
|
||||
private ItemUseRequest CreateUseRequest(Hand hand, ItemActionPhase phase)
|
||||
{
|
||||
InteractionTargetId? targetId = InteractionRaycaster
|
||||
.GetTargetOrNull<IInteractionTarget>()
|
||||
?.InteractionTargetId;
|
||||
|
||||
return new ItemUseRequest(_playerId, hand, phase, targetId);
|
||||
}
|
||||
|
||||
private void ProcessHandActionStarted(Hand hand)
|
||||
{
|
||||
if (Input.IsActionPressed(InputAction.DropModifier.ActionName()))
|
||||
{
|
||||
_items.TryUse(new ItemUseRequest(_playerId, hand, ItemActionPhase.Cancelled));
|
||||
_items.TryDrop(hand, CalculateDropPosition());
|
||||
_dropActions.Add(hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_items.IsHandFree(hand))
|
||||
{
|
||||
_items.TryUse(CreateUseRequest(hand, ItemActionPhase.Started));
|
||||
return;
|
||||
}
|
||||
|
||||
IPickupTarget target = InteractionRaycaster.GetTargetOrNull<IPickupTarget>();
|
||||
if (target is not null)
|
||||
{
|
||||
_items.TryPickup(target.ItemInfo.Id, hand);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessHandActionCompleted(Hand hand)
|
||||
{
|
||||
if (_dropActions.Remove(hand))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_items.TryUse(new ItemUseRequest(_playerId, hand, ItemActionPhase.Completed));
|
||||
}
|
||||
|
||||
private Vector3 CalculateDropPosition()
|
||||
{
|
||||
DropRaycaster.ForceRaycastUpdate();
|
||||
|
||||
bool isBlocked = DropRaycaster.IsColliding();
|
||||
DropRayHit hit = new(
|
||||
Camera.GlobalPosition,
|
||||
-Camera.GlobalBasis.Z.Normalized(),
|
||||
DropRaycaster.TargetPosition.Length(),
|
||||
isBlocked,
|
||||
isBlocked ? DropRaycaster.GetCollisionPoint() : default
|
||||
);
|
||||
|
||||
return DropPosition.CalculateFrom(hit);
|
||||
}
|
||||
|
||||
private void UpdatePrompt()
|
||||
{
|
||||
IPickupTarget pickupTarget = InteractionRaycaster.GetTargetOrNull<IPickupTarget>();
|
||||
if (pickupTarget is not null)
|
||||
{
|
||||
ShowPickupPrompt(pickupTarget.ItemInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
IInteractionTarget interactionTarget = InteractionRaycaster.GetTargetOrNull<IInteractionTarget>();
|
||||
if (interactionTarget?.CanInteract == true)
|
||||
{
|
||||
PromptController.ShowPrompt($"[E] {interactionTarget.Prompt.Text}");
|
||||
return;
|
||||
}
|
||||
|
||||
PromptController.Clear();
|
||||
}
|
||||
|
||||
private void ShowPickupPrompt(ItemInfo itemInfo)
|
||||
{
|
||||
PickupAvailability availability = _resolvePickupAvailability.Resolve(
|
||||
itemInfo.HandRequirement,
|
||||
_items.IsHandFree(Hand.Left),
|
||||
_items.IsHandFree(Hand.Right)
|
||||
);
|
||||
|
||||
if (availability.RequiresBoth)
|
||||
{
|
||||
PromptController.ShowPrompt(
|
||||
availability.LeftHand
|
||||
? $"[LPM lub PPM] Podnieś {itemInfo.DisplayName} — obie ręce"
|
||||
: "Potrzebujesz obu wolnych rąk"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> actions = [];
|
||||
|
||||
if (availability.LeftHand)
|
||||
{
|
||||
actions.Add($"[LPM] Podnieś {itemInfo.DisplayName} — lewa ręka");
|
||||
}
|
||||
|
||||
if (availability.RightHand)
|
||||
{
|
||||
actions.Add($"[PPM] Podnieś {itemInfo.DisplayName} — prawa ręka");
|
||||
}
|
||||
|
||||
PromptController.ShowPrompt(
|
||||
actions.Count > 0 ? string.Join("\n", actions) : "Brak wolnej ręki"
|
||||
);
|
||||
}
|
||||
|
||||
public void Initialize(
|
||||
PlayerId playerId,
|
||||
IItemInteractionService items,
|
||||
IInteractionHandler interactions,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_playerId = playerId;
|
||||
_items = items;
|
||||
_interactions = interactions;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Vector3 GetItemSpawnPosition() => CalculateDropPosition();
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
this.AssertRequiredExports();
|
||||
}
|
||||
|
||||
public void Initialize(
|
||||
PlayerId playerId,
|
||||
IItemInteractionService items,
|
||||
IInteractionService interactions
|
||||
)
|
||||
{
|
||||
_playerId = playerId;
|
||||
_items = items;
|
||||
_interactions = interactions;
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (_items is null || _interactions is null || GamePause.IsPaused || !MouseCapture.IsCaptured)
|
||||
if (_items is null || _interactions is null)
|
||||
{
|
||||
_logger?.Warning(LogCategory, "Skipped frame: controller not yet initialized.");
|
||||
PromptController.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (GamePause.IsPaused || !MouseCapture.IsCaptured)
|
||||
{
|
||||
PromptController.Clear();
|
||||
return;
|
||||
@@ -54,7 +195,13 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint
|
||||
|
||||
public override void _UnhandledInput(InputEvent @event)
|
||||
{
|
||||
if (_items is null || _interactions is null || GamePause.IsPaused || !MouseCapture.IsCaptured)
|
||||
if (_items is null || _interactions is null)
|
||||
{
|
||||
_logger?.Warning(LogCategory, "Ignored input: controller not yet initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (GamePause.IsPaused || !MouseCapture.IsCaptured)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -89,135 +236,15 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint
|
||||
|
||||
if (@event.IsActionPressed(InputAction.Interact.ActionName()))
|
||||
{
|
||||
IInteractionTarget target = InteractionRaycaster.GetTarget<IInteractionTarget>();
|
||||
IInteractionTarget target = InteractionRaycaster.GetTargetOrNull<IInteractionTarget>();
|
||||
if (target?.CanInteract == true)
|
||||
{
|
||||
_interactions.TryInteract(new InteractionRequest(_playerId, target.Id));
|
||||
_interactions.TryInteract(
|
||||
new InteractionRequest(_playerId, target.InteractionTargetId)
|
||||
);
|
||||
}
|
||||
|
||||
GetViewport().SetInputAsHandled();
|
||||
}
|
||||
}
|
||||
|
||||
public Vector3 GetItemSpawnPosition() => CalculateDropPosition();
|
||||
|
||||
private void ProcessHandActionStarted(Hand hand)
|
||||
{
|
||||
if (Input.IsActionPressed(InputAction.DropModifier.ActionName()))
|
||||
{
|
||||
if (_activeUseTargets.Remove(hand, out InteractionTargetId? activeTargetId))
|
||||
{
|
||||
_items.TryUse(new ItemUseRequest(
|
||||
_playerId,
|
||||
hand,
|
||||
ItemActionPhase.Cancelled,
|
||||
activeTargetId
|
||||
));
|
||||
}
|
||||
|
||||
_items.TryDrop(hand, CalculateDropPosition());
|
||||
_dropActions.Add(hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_items.IsHandFree(hand))
|
||||
{
|
||||
ItemUseRequest request = CreateUseRequest(hand, ItemActionPhase.Started);
|
||||
ItemActionResult result = _items.TryUse(request);
|
||||
if (result.Success)
|
||||
{
|
||||
_activeUseTargets[hand] = request.TargetId;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
IPickupTarget target = InteractionRaycaster.GetTarget<IPickupTarget>();
|
||||
if (target is not null)
|
||||
{
|
||||
_items.TryPickup(target.ItemInfo.Id, hand);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessHandActionCompleted(Hand hand)
|
||||
{
|
||||
if (_dropActions.Remove(hand))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_activeUseTargets.Remove(hand, out InteractionTargetId? targetId))
|
||||
{
|
||||
_items.TryUse(new ItemUseRequest(_playerId, hand, ItemActionPhase.Completed, targetId));
|
||||
}
|
||||
}
|
||||
|
||||
private ItemUseRequest CreateUseRequest(Hand hand, ItemActionPhase phase)
|
||||
{
|
||||
InteractionTargetId? targetId = InteractionRaycaster.GetTarget<IInteractionTarget>()?.Id;
|
||||
return new ItemUseRequest(_playerId, hand, phase, targetId);
|
||||
}
|
||||
|
||||
private Vector3 CalculateDropPosition()
|
||||
{
|
||||
DropRaycaster.ForceRaycastUpdate();
|
||||
|
||||
Vector3 origin = Camera.GlobalPosition;
|
||||
Vector3 direction = -Camera.GlobalBasis.Z.Normalized();
|
||||
float distance = DropRaycaster.TargetPosition.Length();
|
||||
|
||||
if (DropRaycaster.IsColliding())
|
||||
{
|
||||
distance = Mathf.Max(0.25f, origin.DistanceTo(DropRaycaster.GetCollisionPoint()) - 0.2f);
|
||||
}
|
||||
|
||||
return origin + direction * distance;
|
||||
}
|
||||
|
||||
private void UpdatePrompt()
|
||||
{
|
||||
IPickupTarget pickupTarget = InteractionRaycaster.GetTarget<IPickupTarget>();
|
||||
if (pickupTarget is not null)
|
||||
{
|
||||
ShowPickupPrompt(pickupTarget.ItemInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
IInteractionTarget interactionTarget = InteractionRaycaster.GetTarget<IInteractionTarget>();
|
||||
if (interactionTarget?.CanInteract == true)
|
||||
{
|
||||
PromptController.ShowPrompt($"[E] {interactionTarget.Prompt.Text}");
|
||||
return;
|
||||
}
|
||||
|
||||
PromptController.Clear();
|
||||
}
|
||||
|
||||
private void ShowPickupPrompt(ItemInfo itemInfo)
|
||||
{
|
||||
if (itemInfo.HandRequirement == HandRequirement.Both)
|
||||
{
|
||||
bool bothHandsFree = _items.IsHandFree(Hand.Left) && _items.IsHandFree(Hand.Right);
|
||||
PromptController.ShowPrompt(bothHandsFree
|
||||
? $"[LPM lub PPM] Podnieś {itemInfo.DisplayName} — obie ręce"
|
||||
: "Potrzebujesz obu wolnych rąk");
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> actions = [];
|
||||
|
||||
if (_items.IsHandFree(Hand.Left))
|
||||
{
|
||||
actions.Add($"[LPM] Podnieś {itemInfo.DisplayName} — lewa ręka");
|
||||
}
|
||||
|
||||
if (_items.IsHandFree(Hand.Right))
|
||||
{
|
||||
actions.Add($"[PPM] Podnieś {itemInfo.DisplayName} — prawa ręka");
|
||||
}
|
||||
|
||||
PromptController.ShowPrompt(actions.Count > 0
|
||||
? string.Join("\n", actions)
|
||||
: "Brak wolnej ręki");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
uid://d2r8m5v1q9k4x
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Game.Features.Interactions.Contracts;
|
||||
|
||||
namespace Game.Features.Interactions.Infrastructure;
|
||||
|
||||
public sealed class InteractionRegistry : IInteractionRegistry
|
||||
{
|
||||
private readonly Dictionary<InteractionTargetId, IInteractionTarget> _interactionTargetsById =
|
||||
[];
|
||||
|
||||
public void Register(IInteractionTarget target)
|
||||
{
|
||||
if (
|
||||
_interactionTargetsById.TryGetValue(
|
||||
target.InteractionTargetId,
|
||||
out IInteractionTarget existing
|
||||
)
|
||||
)
|
||||
{
|
||||
if (ReferenceEquals(existing, target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Duplicate interaction target id: {target.InteractionTargetId}."
|
||||
);
|
||||
}
|
||||
|
||||
_interactionTargetsById.Add(target.InteractionTargetId, target);
|
||||
}
|
||||
|
||||
public void Unregister(InteractionTargetId targetId)
|
||||
{
|
||||
_interactionTargetsById.Remove(targetId);
|
||||
}
|
||||
|
||||
public bool TryGetTarget(InteractionTargetId targetId, out IInteractionTarget target) =>
|
||||
_interactionTargetsById.TryGetValue(targetId, out target);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtnwqalxdu6n2
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Game.Features.Interactions.Contracts;
|
||||
using Game.Features.Items.Contracts;
|
||||
using Game.Features.Items.Domain;
|
||||
using Godot;
|
||||
@@ -10,6 +12,8 @@ public sealed class ItemInteractionService(
|
||||
IHeldItemPresenter presenter
|
||||
) : IItemInteractionService
|
||||
{
|
||||
private readonly Dictionary<Hand, InteractionTargetId?> _activeUseTargets = [];
|
||||
|
||||
public bool IsHandFree(Hand hand) => hands.IsFree(hand);
|
||||
|
||||
public ItemInstanceId? GetHeldItem(Hand hand) => hands.Get(hand);
|
||||
@@ -36,6 +40,27 @@ public sealed class ItemInteractionService(
|
||||
}
|
||||
|
||||
public ItemActionResult TryUse(ItemUseRequest request)
|
||||
{
|
||||
if (request.Phase == ItemActionPhase.Started)
|
||||
{
|
||||
ItemActionResult startResult = ExecuteUse(request);
|
||||
if (startResult.Success)
|
||||
{
|
||||
_activeUseTargets[request.Hand] = request.TargetId;
|
||||
}
|
||||
|
||||
return startResult;
|
||||
}
|
||||
|
||||
if (!_activeUseTargets.Remove(request.Hand, out InteractionTargetId? activeTargetId))
|
||||
{
|
||||
return ItemActionResult.Ok();
|
||||
}
|
||||
|
||||
return ExecuteUse(request with { TargetId = activeTargetId });
|
||||
}
|
||||
|
||||
private ItemActionResult ExecuteUse(ItemUseRequest request)
|
||||
{
|
||||
ItemInstanceId? itemId = hands.Get(request.Hand);
|
||||
if (itemId is null)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using Game.Shared.Domain;
|
||||
using Godot;
|
||||
|
||||
namespace Game.Godot.Extensions;
|
||||
|
||||
public static class NodeInstanceIdExtensions
|
||||
{
|
||||
private static string GetKeyFrom(Node node) => node.GetPath().ToString();
|
||||
|
||||
public static InstanceId GetStableInstanceId(this Node node) =>
|
||||
InstanceId.FromKey(GetKeyFrom(node));
|
||||
|
||||
public static InstanceId GetStableInstanceIdFrom(this Node node, string key) =>
|
||||
string.IsNullOrWhiteSpace(key) ? node.GetStableInstanceId() : InstanceId.FromKey(key);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://csng1pjcgsh80
|
||||
@@ -6,6 +6,7 @@ using Game.Features.Logging.Godot;
|
||||
using Game.Features.Logging.Infrastructure;
|
||||
using Game.Features.Interactions.Application;
|
||||
using Game.Features.Interactions.Godot;
|
||||
using Game.Features.Interactions.Infrastructure;
|
||||
using Game.Features.Items.Application;
|
||||
using Game.Features.Items.Domain;
|
||||
using Game.Features.Items.Godot;
|
||||
@@ -25,7 +26,7 @@ public partial class GameBootstrap : Node
|
||||
[Export] public PlayerInteractionController PlayerInteraction;
|
||||
[Export] public ItemWorldController ItemWorld;
|
||||
[Export] public ItemSpawnerController ItemSpawner;
|
||||
[Export] public InteractionWorldController InteractionWorld;
|
||||
[Export] public InteractionsRegistryController InteractionWorld;
|
||||
[Export] public MultiplayerController MultiplayerController;
|
||||
|
||||
private FileLogger _fileLogger;
|
||||
@@ -69,6 +70,9 @@ public partial class GameBootstrap : Node
|
||||
{
|
||||
ItemWorld.RegisterSceneItems();
|
||||
ItemSpawner.Initialize();
|
||||
|
||||
InteractionRegistry interactionRegistry = new();
|
||||
InteractionWorld.UseRegistry(interactionRegistry);
|
||||
InteractionWorld.RegisterSceneTargets();
|
||||
|
||||
PlayerHeldItemPresenter presenter = new(
|
||||
@@ -77,8 +81,8 @@ public partial class GameBootstrap : Node
|
||||
PlayerInteraction.RightHandAnchor
|
||||
);
|
||||
ItemInteractionService itemInteractions = new(new HandsInventory(), ItemWorld, presenter);
|
||||
InteractionService interactions = new(InteractionWorld);
|
||||
PlayerInteraction.Initialize(new PlayerId(Guid.NewGuid()), itemInteractions, interactions);
|
||||
Interact interactions = new(interactionRegistry);
|
||||
PlayerInteraction.Initialize(new PlayerId(Guid.NewGuid()), itemInteractions, interactions, _logger);
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
[ext_resource type="Script" uid="uid://f8n3b6w1k5r9m" path="res://src/Features/Items/Godot/ItemWorldController.cs" id="6_items"]
|
||||
[ext_resource type="PackedScene" uid="uid://cv8m3q1y6r4nf" path="res://src/Features/Items/Godot/Flashlight/FlashlightItem.tscn" id="7_flashlight"]
|
||||
[ext_resource type="Script" uid="uid://c3x9m6w2q8r1b" path="res://src/Features/Items/Godot/ItemSpawnerController.cs" id="8_spawner"]
|
||||
[ext_resource type="Script" uid="uid://f6q1c9m4w7v2n" path="res://src/Features/Interactions/Godot/InteractionWorldController.cs" id="9_interactions"]
|
||||
[ext_resource type="Script" uid="uid://f6q1c9m4w7v2n" path="res://src/Features/Interactions/Godot/InteractionsRegistryController.cs" id="9_interactions"]
|
||||
|
||||
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_epykj"]
|
||||
panorama = ExtResource("1_g4n7o")
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Game.Shared.Domain;
|
||||
|
||||
public readonly record struct InstanceId(Guid Value)
|
||||
{
|
||||
private static Guid CreateDeterministicId(string source) =>
|
||||
new(MD5.HashData(Encoding.UTF8.GetBytes(source)));
|
||||
|
||||
public static InstanceId FromKey(string key) => new(CreateDeterministicId(key));
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpikg7xirrc1g
|
||||
Reference in New Issue
Block a user