refactor: restructure interaction system into clean layers

This commit is contained in:
2026-08-28 22:35:09 +02:00
parent 3127c1b4af
commit 9fdd565637
33 changed files with 474 additions and 288 deletions
@@ -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");
}
}