Compare commits
4
Commits
1445fc014f
...
00627a7e91
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00627a7e91 | ||
|
|
bed12ad335 | ||
|
|
f4331f426c | ||
|
|
25e32f20d9 |
@@ -66,6 +66,26 @@ move_down={
|
|||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
interact={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
use_left_hand={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
use_right_hand={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
drop_modifier={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Game.Features.Console.Contracts;
|
||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Console.Application.Commands;
|
||||||
|
|
||||||
|
public sealed class SpawnItemCommand : ICommand
|
||||||
|
{
|
||||||
|
private readonly IItemSpawner _spawner;
|
||||||
|
private readonly IItemSpawnPoint _spawnPoint;
|
||||||
|
|
||||||
|
public string Name => "spawn_item";
|
||||||
|
public string Description => "Spawns an item in front of the player.";
|
||||||
|
public IReadOnlyList<CommandArgSpec> Args { get; }
|
||||||
|
|
||||||
|
public SpawnItemCommand(IItemSpawner spawner, IItemSpawnPoint spawnPoint)
|
||||||
|
{
|
||||||
|
_spawner = spawner;
|
||||||
|
_spawnPoint = spawnPoint;
|
||||||
|
|
||||||
|
string[] definitions = spawner.DefinitionIds
|
||||||
|
.Select(id => id.Value)
|
||||||
|
.OrderBy(id => id, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Args = new CommandArgsBuilder().AddChoiceArg("item", definitions).Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandResult Execute(CommandArgValues args)
|
||||||
|
{
|
||||||
|
ItemDefinitionId definitionId = new(args.GetString(0));
|
||||||
|
ItemActionResult result = _spawner.TrySpawn(
|
||||||
|
definitionId,
|
||||||
|
_spawnPoint.GetItemSpawnPosition(),
|
||||||
|
out ItemInstanceId itemId
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.Success
|
||||||
|
? CommandResult.Ok($"Spawned {definitionId} ({itemId}).")
|
||||||
|
: CommandResult.Fail(result.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://e8m3q6v1r9k4w
|
||||||
@@ -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,223 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Game.Features.Interactions.Contracts;
|
||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Game.Features.Players.Contracts;
|
||||||
|
using Game.Godot;
|
||||||
|
using Game.Godot.Extensions;
|
||||||
|
using Game.Godot.Mappings;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
private IItemInteractionService _items;
|
||||||
|
private IInteractionService _interactions;
|
||||||
|
private PlayerId _playerId;
|
||||||
|
private readonly HashSet<Hand> _dropActions = [];
|
||||||
|
private readonly Dictionary<Hand, InteractionTargetId?> _activeUseTargets = [];
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
PromptController.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePrompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _UnhandledInput(InputEvent @event)
|
||||||
|
{
|
||||||
|
if (_items is null || _interactions is null || GamePause.IsPaused || !MouseCapture.IsCaptured)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@event.IsActionPressed(InputAction.UseLeftHand.ActionName()))
|
||||||
|
{
|
||||||
|
ProcessHandActionStarted(Hand.Left);
|
||||||
|
GetViewport().SetInputAsHandled();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@event.IsActionReleased(InputAction.UseLeftHand.ActionName()))
|
||||||
|
{
|
||||||
|
ProcessHandActionCompleted(Hand.Left);
|
||||||
|
GetViewport().SetInputAsHandled();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@event.IsActionPressed(InputAction.UseRightHand.ActionName()))
|
||||||
|
{
|
||||||
|
ProcessHandActionStarted(Hand.Right);
|
||||||
|
GetViewport().SetInputAsHandled();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@event.IsActionReleased(InputAction.UseRightHand.ActionName()))
|
||||||
|
{
|
||||||
|
ProcessHandActionCompleted(Hand.Right);
|
||||||
|
GetViewport().SetInputAsHandled();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@event.IsActionPressed(InputAction.Interact.ActionName()))
|
||||||
|
{
|
||||||
|
IInteractionTarget target = InteractionRaycaster.GetTarget<IInteractionTarget>();
|
||||||
|
if (target?.CanInteract == true)
|
||||||
|
{
|
||||||
|
_interactions.TryInteract(new InteractionRequest(_playerId, target.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c2v8m5r1y7q4d
|
||||||
@@ -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,83 @@
|
|||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Game.Features.Items.Domain;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Application;
|
||||||
|
|
||||||
|
public sealed class ItemInteractionService(
|
||||||
|
HandsInventory hands,
|
||||||
|
IItemRepository itemRepository,
|
||||||
|
IHeldItemPresenter presenter
|
||||||
|
) : IItemInteractionService
|
||||||
|
{
|
||||||
|
public bool IsHandFree(Hand hand) => hands.IsFree(hand);
|
||||||
|
|
||||||
|
public ItemInstanceId? GetHeldItem(Hand hand) => hands.Get(hand);
|
||||||
|
|
||||||
|
public ItemActionResult TryPickup(ItemInstanceId itemId, Hand hand)
|
||||||
|
{
|
||||||
|
if (!itemRepository.TryGet(itemId, out IItemInstance item))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hands.TryHold(itemId, item.Info.HandRequirement, hand))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.HandOccupied, "The requested hand is not available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presenter.TryShowHeld(item, hand))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
hands.Release(hand);
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.PresentationFailed, "Could not present the held item.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemActionResult TryUse(ItemUseRequest request)
|
||||||
|
{
|
||||||
|
ItemInstanceId? itemId = hands.Get(request.Hand);
|
||||||
|
if (itemId is null)
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.HandEmpty, "The requested hand is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemRepository.TryGet(itemId.Value, out IItemInstance item))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemActionResult result = item.Use(request);
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
presenter.Refresh(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemActionResult TryDrop(Hand hand, Vector3 worldPosition)
|
||||||
|
{
|
||||||
|
ItemInstanceId? itemId = hands.Get(hand);
|
||||||
|
if (itemId is null)
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.HandEmpty, "The requested hand is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemRepository.TryGet(itemId.Value, out IItemInstance item))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
hands.Release(hand);
|
||||||
|
|
||||||
|
if (presenter.TryShowDropped(item, worldPosition))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
hands.TryHold(itemId.Value, item.Info.HandRequirement, hand);
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.PresentationFailed, "Could not present the dropped item.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://e6q2h9v5b8k3p
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public enum Hand
|
||||||
|
{
|
||||||
|
Left,
|
||||||
|
Right
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c4n7h2w8m5q1p
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public enum HandRequirement
|
||||||
|
{
|
||||||
|
One,
|
||||||
|
Both
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d8p3k6v1r9y4b
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IHeldItemPresenter
|
||||||
|
{
|
||||||
|
bool TryShowHeld(IItemInstance item, Hand hand);
|
||||||
|
bool TryShowDropped(IItemInstance item, Vector3 worldPosition);
|
||||||
|
void Refresh(IItemInstance item);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c1q7y4d9m5x2v
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IItemInstance
|
||||||
|
{
|
||||||
|
ItemInfo Info { get; }
|
||||||
|
ItemActionResult Use(ItemUseRequest request);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b5x9q3h7m2w8f
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IItemInteractionService
|
||||||
|
{
|
||||||
|
bool IsHandFree(Hand hand);
|
||||||
|
ItemInstanceId? GetHeldItem(Hand hand);
|
||||||
|
ItemActionResult TryPickup(ItemInstanceId itemId, Hand hand);
|
||||||
|
ItemActionResult TryUse(ItemUseRequest request);
|
||||||
|
ItemActionResult TryDrop(Hand hand, Vector3 worldPosition);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://f2k8q5b1w7m4n
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IItemRepository
|
||||||
|
{
|
||||||
|
bool TryGet(ItemInstanceId itemId, out IItemInstance item);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://e7n1m6v4k9r3q
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IItemSpawnPoint
|
||||||
|
{
|
||||||
|
Vector3 GetItemSpawnPosition();
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://a2v6r8m3q7k5d
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IItemSpawner
|
||||||
|
{
|
||||||
|
IReadOnlyCollection<ItemDefinitionId> DefinitionIds { get; }
|
||||||
|
ItemActionResult TrySpawn(ItemDefinitionId definitionId, Vector3 worldPosition, out ItemInstanceId itemId);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://f4m9k2w6b8n1r
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IPickupTarget
|
||||||
|
{
|
||||||
|
ItemInfo ItemInfo { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c9v4g7p2m5x8d
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public enum ItemActionPhase
|
||||||
|
{
|
||||||
|
Started,
|
||||||
|
Completed,
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c9w5d2n8q4m1h
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemActionResult(ItemActionStatus Status, string Message = "")
|
||||||
|
{
|
||||||
|
public bool Success => Status == ItemActionStatus.Success;
|
||||||
|
|
||||||
|
public static ItemActionResult Ok() => new(ItemActionStatus.Success);
|
||||||
|
|
||||||
|
public static ItemActionResult Fail(ItemActionStatus status, string message) => new(status, message);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://a8m4p1x7d3q6k
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public enum ItemActionStatus
|
||||||
|
{
|
||||||
|
Success,
|
||||||
|
ItemNotFound,
|
||||||
|
HandOccupied,
|
||||||
|
HandEmpty,
|
||||||
|
UnsupportedAction,
|
||||||
|
PresentationFailed,
|
||||||
|
UnknownDefinition
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://f1r6y3b9k5v2m
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemDefinition(
|
||||||
|
ItemDefinitionId Id,
|
||||||
|
string DisplayName,
|
||||||
|
HandRequirement HandRequirement
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://e3v8k4r1m7x2p
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemDefinitionId(string Value)
|
||||||
|
{
|
||||||
|
public override string ToString() => Value;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b7q2n9c5w1m6y
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemInfo(
|
||||||
|
ItemInstanceId Id,
|
||||||
|
ItemDefinitionId DefinitionId,
|
||||||
|
string DisplayName,
|
||||||
|
HandRequirement HandRequirement
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://e1r5j8w3n6c9h
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemInstanceId(Guid Value)
|
||||||
|
{
|
||||||
|
public override string ToString() => Value.ToString();
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b6x2f9m4t7k3q
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Game.Features.Interactions.Contracts;
|
||||||
|
using Game.Features.Players.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemUseRequest(
|
||||||
|
PlayerId PlayerId,
|
||||||
|
Hand Hand,
|
||||||
|
ItemActionPhase Phase,
|
||||||
|
InteractionTargetId? TargetId = null
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d6k2v8m5r1c9n
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Domain.Flashlight;
|
||||||
|
|
||||||
|
public sealed class FlashlightItemInstance(ItemInfo info) : IItemInstance
|
||||||
|
{
|
||||||
|
public ItemInfo Info { get; } = info;
|
||||||
|
public bool IsOn { get; private set; }
|
||||||
|
|
||||||
|
public ItemActionResult Use(ItemUseRequest request)
|
||||||
|
{
|
||||||
|
if (request.Phase == ItemActionPhase.Started)
|
||||||
|
{
|
||||||
|
IsOn = !IsOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ItemActionResult.Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b8r2n5q9w4m1y
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Domain;
|
||||||
|
|
||||||
|
public sealed class HandsInventory
|
||||||
|
{
|
||||||
|
private ItemInstanceId? _leftHand;
|
||||||
|
private ItemInstanceId? _rightHand;
|
||||||
|
|
||||||
|
public bool IsFree(Hand hand) => Get(hand) is null;
|
||||||
|
|
||||||
|
public ItemInstanceId? Get(Hand hand) => hand switch
|
||||||
|
{
|
||||||
|
Hand.Left => _leftHand,
|
||||||
|
Hand.Right => _rightHand,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(hand), hand, null)
|
||||||
|
};
|
||||||
|
|
||||||
|
public bool TryHold(ItemInstanceId itemId, HandRequirement requirement, Hand hand)
|
||||||
|
{
|
||||||
|
if (_leftHand == itemId || _rightHand == itemId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requirement == HandRequirement.Both)
|
||||||
|
{
|
||||||
|
if (_leftHand is not null || _rightHand is not null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_leftHand = itemId;
|
||||||
|
_rightHand = itemId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsFree(hand))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set(hand, itemId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemInstanceId? Release(Hand hand)
|
||||||
|
{
|
||||||
|
ItemInstanceId? itemId = Get(hand);
|
||||||
|
if (itemId is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_leftHand == itemId)
|
||||||
|
{
|
||||||
|
_leftHand = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_rightHand == itemId)
|
||||||
|
{
|
||||||
|
_rightHand = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Set(Hand hand, ItemInstanceId itemId)
|
||||||
|
{
|
||||||
|
switch (hand)
|
||||||
|
{
|
||||||
|
case Hand.Left:
|
||||||
|
_leftHand = itemId;
|
||||||
|
break;
|
||||||
|
case Hand.Right:
|
||||||
|
_rightHand = itemId;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(hand), hand, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d5w1n8c4q7m2y
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Domain;
|
||||||
|
|
||||||
|
public sealed class PassiveItemInstance(ItemInfo info) : IItemInstance
|
||||||
|
{
|
||||||
|
public ItemInfo Info { get; } = info;
|
||||||
|
|
||||||
|
public ItemActionResult Use(ItemUseRequest request) => ItemActionResult.Fail(
|
||||||
|
ItemActionStatus.UnsupportedAction,
|
||||||
|
$"Item {Info.DefinitionId} cannot be used directly."
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d9p4x1m7v3k6c
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://dy6p2w9m4c8kr"]
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_flashlight"]
|
||||||
|
albedo_color = Color(0.08, 0.09, 0.1, 1)
|
||||||
|
metallic = 0.65
|
||||||
|
roughness = 0.3
|
||||||
|
|
||||||
|
[sub_resource type="CylinderMesh" id="Mesh_flashlight"]
|
||||||
|
material = SubResource("Material_flashlight")
|
||||||
|
top_radius = 0.055
|
||||||
|
bottom_radius = 0.07
|
||||||
|
height = 0.32
|
||||||
|
radial_segments = 12
|
||||||
|
|
||||||
|
[node name="FlashlightHeldView" type="Node3D"]
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 0, -0.08)
|
||||||
|
mesh = SubResource("Mesh_flashlight")
|
||||||
|
|
||||||
|
[node name="SpotLight3D" type="SpotLight3D" parent="."]
|
||||||
|
visible = false
|
||||||
|
light_energy = 3.0
|
||||||
|
shadow_enabled = true
|
||||||
|
spot_range = 14.0
|
||||||
|
spot_angle = 24.0
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
[gd_scene load_steps=6 format=3 uid="uid://cv8m3q1y6r4nf"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://a5q9y2d7m4v8p" path="res://src/Features/Items/Godot/Flashlight/FlashlightItemController.cs" id="1_script"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dy6p2w9m4c8kr" path="res://src/Features/Items/Godot/Flashlight/FlashlightHeldView.tscn" id="2_held"]
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_flashlight"]
|
||||||
|
albedo_color = Color(0.08, 0.09, 0.1, 1)
|
||||||
|
metallic = 0.65
|
||||||
|
roughness = 0.3
|
||||||
|
|
||||||
|
[sub_resource type="CylinderMesh" id="Mesh_flashlight"]
|
||||||
|
material = SubResource("Material_flashlight")
|
||||||
|
top_radius = 0.055
|
||||||
|
bottom_radius = 0.07
|
||||||
|
height = 0.32
|
||||||
|
radial_segments = 12
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="Shape_flashlight"]
|
||||||
|
radius = 0.07
|
||||||
|
height = 0.32
|
||||||
|
|
||||||
|
[node name="FlashlightItem" type="RigidBody3D" node_paths=PackedStringArray("Collision", "WorldVisual", "WorldLight")]
|
||||||
|
mass = 0.4
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
DefinitionId = "flashlight"
|
||||||
|
DisplayName = "latarkę"
|
||||||
|
Collision = NodePath("CollisionShape3D")
|
||||||
|
WorldVisual = NodePath("WorldVisual")
|
||||||
|
HeldViewScene = ExtResource("2_held")
|
||||||
|
WorldLight = NodePath("WorldVisual/SpotLight3D")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 0, 0)
|
||||||
|
shape = SubResource("Shape_flashlight")
|
||||||
|
|
||||||
|
[node name="WorldVisual" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="WorldVisual"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 0, 0)
|
||||||
|
mesh = SubResource("Mesh_flashlight")
|
||||||
|
|
||||||
|
[node name="SpotLight3D" type="SpotLight3D" parent="WorldVisual"]
|
||||||
|
visible = false
|
||||||
|
light_energy = 3.0
|
||||||
|
shadow_enabled = true
|
||||||
|
spot_range = 14.0
|
||||||
|
spot_angle = 24.0
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Game.Features.Items.Domain.Flashlight;
|
||||||
|
using Game.Features.Items.Godot;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Godot.Flashlight;
|
||||||
|
|
||||||
|
public partial class FlashlightItemController : WorldItemController
|
||||||
|
{
|
||||||
|
[Export] public SpotLight3D WorldLight;
|
||||||
|
|
||||||
|
private SpotLight3D _heldLight;
|
||||||
|
protected override IItemInstance CreateItemInstance(ItemInfo itemInfo) => new FlashlightItemInstance(itemInfo);
|
||||||
|
|
||||||
|
protected override void OnHeldViewCreated(Node3D heldView)
|
||||||
|
{
|
||||||
|
_heldLight = heldView.GetNodeOrNull<SpotLight3D>("SpotLight3D");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHeldViewRemoving(Node3D heldView)
|
||||||
|
{
|
||||||
|
_heldLight = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPresentationChanged(IItemInstance item)
|
||||||
|
{
|
||||||
|
bool isOn = item is FlashlightItemInstance flashlight && flashlight.IsOn;
|
||||||
|
|
||||||
|
if (WorldLight is not null)
|
||||||
|
{
|
||||||
|
WorldLight.Visible = isOn && !IsHeld;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_heldLight is not null)
|
||||||
|
{
|
||||||
|
_heldLight.Visible = isOn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://a5q9y2d7m4v8p
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Game.Godot.Extensions;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Godot;
|
||||||
|
|
||||||
|
public partial class ItemSpawnerController : Node, IItemSpawner
|
||||||
|
{
|
||||||
|
[Export] public Node3D ItemsRoot;
|
||||||
|
[Export] public ItemWorldController ItemWorld;
|
||||||
|
[Export] public global::Godot.Collections.Array<PackedScene> ItemScenes = [];
|
||||||
|
|
||||||
|
private readonly Dictionary<ItemDefinitionId, PackedScene> _scenes = [];
|
||||||
|
|
||||||
|
public IReadOnlyCollection<ItemDefinitionId> DefinitionIds => _scenes.Keys.ToArray();
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
this.AssertRequiredExports();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_scenes.Clear();
|
||||||
|
|
||||||
|
foreach (PackedScene scene in ItemScenes)
|
||||||
|
{
|
||||||
|
RegisterScene(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (WorldItemController item in ItemWorld.Items)
|
||||||
|
{
|
||||||
|
string scenePath = item.SceneFilePath;
|
||||||
|
if (string.IsNullOrWhiteSpace(scenePath))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PackedScene scene = ResourceLoader.Load<PackedScene>(scenePath);
|
||||||
|
if (scene is not null)
|
||||||
|
{
|
||||||
|
_scenes.TryAdd(item.ItemInfo.DefinitionId, scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_scenes.Count == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("The item spawn catalog is empty.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemActionResult TrySpawn(
|
||||||
|
ItemDefinitionId definitionId,
|
||||||
|
Vector3 worldPosition,
|
||||||
|
out ItemInstanceId itemId
|
||||||
|
) {
|
||||||
|
itemId = default;
|
||||||
|
|
||||||
|
if (!_scenes.TryGetValue(definitionId, out PackedScene scene))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(
|
||||||
|
ItemActionStatus.UnknownDefinition,
|
||||||
|
$"Unknown item definition: {definitionId}."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldItemController item = scene.Instantiate<WorldItemController>();
|
||||||
|
item.InstanceId = Guid.NewGuid().ToString();
|
||||||
|
ItemsRoot.AddChild(item);
|
||||||
|
item.GlobalPosition = worldPosition;
|
||||||
|
ItemWorld.Register(item);
|
||||||
|
itemId = item.ItemInfo.Id;
|
||||||
|
return ItemActionResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterScene(PackedScene scene)
|
||||||
|
{
|
||||||
|
if (scene is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldItemController prototype = scene.Instantiate<WorldItemController>();
|
||||||
|
ItemDefinitionId definitionId = new(prototype.DefinitionId);
|
||||||
|
prototype.Free();
|
||||||
|
|
||||||
|
if (!_scenes.TryAdd(definitionId, scene))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Duplicate item spawn definition: {definitionId}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c3x9m6w2q8r1b
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Godot;
|
||||||
|
|
||||||
|
public partial class ItemWorldController : Node, IItemRepository
|
||||||
|
{
|
||||||
|
public const string RegistryGroup = "item_world_registry";
|
||||||
|
|
||||||
|
private readonly Dictionary<ItemInstanceId, WorldItemController> _items = [];
|
||||||
|
|
||||||
|
public IReadOnlyCollection<WorldItemController> Items => _items.Values;
|
||||||
|
|
||||||
|
public override void _EnterTree()
|
||||||
|
{
|
||||||
|
AddToGroup(RegistryGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterSceneItems()
|
||||||
|
{
|
||||||
|
foreach (Node node in GetTree().GetNodesInGroup(WorldItemController.ItemGroup))
|
||||||
|
{
|
||||||
|
if (node is WorldItemController item)
|
||||||
|
{
|
||||||
|
Register(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(WorldItemController item)
|
||||||
|
{
|
||||||
|
if (_items.TryGetValue(item.ItemInfo.Id, out WorldItemController existing))
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(existing, item))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Duplicate item instance id: {item.ItemInfo.Id}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_items.Add(item.ItemInfo.Id, item);
|
||||||
|
item.RegisterWith(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unregister(ItemInstanceId itemId)
|
||||||
|
{
|
||||||
|
_items.Remove(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet(ItemInstanceId itemId, out IItemInstance item)
|
||||||
|
{
|
||||||
|
if (_items.TryGetValue(itemId, out WorldItemController controller))
|
||||||
|
{
|
||||||
|
item = controller.Item;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
item = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetController(ItemInstanceId itemId, out WorldItemController controller)
|
||||||
|
{
|
||||||
|
return _items.TryGetValue(itemId, out controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://f8n3b6w1k5r9m
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Godot;
|
||||||
|
|
||||||
|
public sealed class PlayerHeldItemPresenter(
|
||||||
|
ItemWorldController itemWorld,
|
||||||
|
Node3D leftHandAnchor,
|
||||||
|
Node3D rightHandAnchor
|
||||||
|
) : IHeldItemPresenter
|
||||||
|
{
|
||||||
|
public bool TryShowHeld(IItemInstance item, Hand hand)
|
||||||
|
{
|
||||||
|
if (!itemWorld.TryGetController(item.Info.Id, out WorldItemController controller))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node3D anchor = hand == Hand.Left ? leftHandAnchor : rightHandAnchor;
|
||||||
|
controller.SetHeld(anchor, item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryShowDropped(IItemInstance item, Vector3 worldPosition)
|
||||||
|
{
|
||||||
|
if (!itemWorld.TryGetController(item.Info.Id, out WorldItemController controller))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.SetDropped(worldPosition, item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh(IItemInstance item)
|
||||||
|
{
|
||||||
|
if (itemWorld.TryGetController(item.Info.Id, out WorldItemController controller))
|
||||||
|
{
|
||||||
|
controller.ApplyState(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://e5m2r8q1v7k4n
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
using System;
|
||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Game.Features.Items.Domain;
|
||||||
|
using Game.Godot.Extensions;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Godot;
|
||||||
|
|
||||||
|
public partial class WorldItemController : RigidBody3D, IPickupTarget
|
||||||
|
{
|
||||||
|
public const string ItemGroup = "world_items";
|
||||||
|
|
||||||
|
[Export] public string InstanceId;
|
||||||
|
[Export] public string DefinitionId = "item";
|
||||||
|
[Export] public string DisplayName = "Item";
|
||||||
|
[Export] public HandRequirement HandRequirement = HandRequirement.One;
|
||||||
|
[Export] public CollisionShape3D Collision;
|
||||||
|
[Export] public Node3D WorldVisual;
|
||||||
|
[Export] public PackedScene HeldViewScene;
|
||||||
|
|
||||||
|
protected Node3D HeldView { get; private set; }
|
||||||
|
protected bool IsHeld => HeldView is not null;
|
||||||
|
|
||||||
|
public ItemInfo ItemInfo { get; private set; }
|
||||||
|
public ItemDefinition Definition { get; private set; }
|
||||||
|
public IItemInstance Item { get; private set; }
|
||||||
|
|
||||||
|
private ItemWorldController _registry;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
this.AssertRequiredExports();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(DefinitionId))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{Name}: item definition id cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HeldViewScene is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{Name}: held view scene is not assigned.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Guid id = string.IsNullOrWhiteSpace(InstanceId)
|
||||||
|
? Guid.NewGuid()
|
||||||
|
: Guid.Parse(InstanceId);
|
||||||
|
|
||||||
|
Definition = new ItemDefinition(new ItemDefinitionId(DefinitionId), DisplayName, HandRequirement);
|
||||||
|
ItemInfo = new ItemInfo(
|
||||||
|
new ItemInstanceId(id),
|
||||||
|
Definition.Id,
|
||||||
|
Definition.DisplayName,
|
||||||
|
Definition.HandRequirement
|
||||||
|
);
|
||||||
|
Item = CreateItemInstance(ItemInfo);
|
||||||
|
AddToGroup(ItemGroup);
|
||||||
|
RegisterWithAvailableWorld();
|
||||||
|
ApplyState(Item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
_registry?.Unregister(ItemInfo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterWith(ItemWorldController registry)
|
||||||
|
{
|
||||||
|
_registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHeld(Node3D anchor, IItemInstance item)
|
||||||
|
{
|
||||||
|
if (IsHeld)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Freeze = true;
|
||||||
|
Sleeping = true;
|
||||||
|
LinearVelocity = Vector3.Zero;
|
||||||
|
AngularVelocity = Vector3.Zero;
|
||||||
|
Collision.SetDeferred(CollisionShape3D.PropertyName.Disabled, true);
|
||||||
|
WorldVisual.Visible = false;
|
||||||
|
|
||||||
|
HeldView = HeldViewScene.Instantiate<Node3D>();
|
||||||
|
anchor.AddChild(HeldView);
|
||||||
|
HeldView.Transform = Transform3D.Identity;
|
||||||
|
OnHeldViewCreated(HeldView);
|
||||||
|
ApplyState(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDropped(Vector3 worldPosition, IItemInstance item)
|
||||||
|
{
|
||||||
|
if (HeldView is not null)
|
||||||
|
{
|
||||||
|
OnHeldViewRemoving(HeldView);
|
||||||
|
HeldView.QueueFree();
|
||||||
|
HeldView = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalPosition = worldPosition;
|
||||||
|
LinearVelocity = Vector3.Zero;
|
||||||
|
AngularVelocity = Vector3.Zero;
|
||||||
|
WorldVisual.Visible = true;
|
||||||
|
Collision.SetDeferred(CollisionShape3D.PropertyName.Disabled, false);
|
||||||
|
Freeze = false;
|
||||||
|
Sleeping = false;
|
||||||
|
ApplyState(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyState(IItemInstance item)
|
||||||
|
{
|
||||||
|
OnPresentationChanged(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IItemInstance CreateItemInstance(ItemInfo itemInfo) => new PassiveItemInstance(itemInfo);
|
||||||
|
|
||||||
|
protected virtual void OnHeldViewCreated(Node3D heldView)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnHeldViewRemoving(Node3D heldView)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnPresentationChanged(IItemInstance item)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterWithAvailableWorld()
|
||||||
|
{
|
||||||
|
foreach (Node node in GetTree().GetNodesInGroup(ItemWorldController.RegistryGroup))
|
||||||
|
{
|
||||||
|
if (node is ItemWorldController registry)
|
||||||
|
{
|
||||||
|
registry.Register(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d1x7m4q9v2c6k
|
||||||
@@ -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
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
[ext_resource type="Script" uid="uid://mjm7ghkibnxa" path="res://src/Features/Players/Godot/PlayerController.cs" id="1_gkqfc"]
|
[ext_resource type="Script" uid="uid://mjm7ghkibnxa" path="res://src/Features/Players/Godot/PlayerController.cs" id="1_gkqfc"]
|
||||||
[ext_resource type="Texture2D" uid="uid://bymgknsy145ap" path="res://assets/models/simple-character-psx_character_256.png" id="2_tduai"]
|
[ext_resource type="Texture2D" uid="uid://bymgknsy145ap" path="res://assets/models/simple-character-psx_character_256.png" id="2_tduai"]
|
||||||
|
[ext_resource type="Script" uid="uid://c2v8m5r1y7q4d" path="res://src/Features/Interactions/Godot/PlayerInteractionController.cs" id="3_interaction"]
|
||||||
|
[ext_resource type="Script" uid="uid://e4m7k1x8c3q6h" path="res://src/Features/Interactions/Godot/InteractionRaycaster.cs" id="4_raycaster"]
|
||||||
|
[ext_resource type="Script" uid="uid://b9r2w6n4f8k1v" path="res://src/Features/Interactions/Godot/InteractionPromptController.cs" id="5_prompt"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_130ks"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_130ks"]
|
||||||
radius = 0.25
|
radius = 0.25
|
||||||
@@ -775,3 +778,82 @@ skeleton = NodePath("..")
|
|||||||
[node name="Camera3D" type="Camera3D" parent="CharacterBody3D" unique_id=718409984]
|
[node name="Camera3D" type="Camera3D" parent="CharacterBody3D" unique_id=718409984]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.817, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.817, 0)
|
||||||
cull_mask = 1048573
|
cull_mask = 1048573
|
||||||
|
|
||||||
|
[node name="LeftHandAnchor" type="Node3D" parent="CharacterBody3D/Camera3D"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.22, -0.2, -0.48)
|
||||||
|
|
||||||
|
[node name="RightHandAnchor" type="Node3D" parent="CharacterBody3D/Camera3D"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.22, -0.2, -0.48)
|
||||||
|
|
||||||
|
[node name="InteractionRaycaster" type="RayCast3D" parent="CharacterBody3D/Camera3D"]
|
||||||
|
enabled = true
|
||||||
|
target_position = Vector3(0, 0, -2.5)
|
||||||
|
collide_with_areas = true
|
||||||
|
script = ExtResource("4_raycaster")
|
||||||
|
|
||||||
|
[node name="DropRaycaster" type="RayCast3D" parent="CharacterBody3D/Camera3D"]
|
||||||
|
enabled = true
|
||||||
|
target_position = Vector3(0, 0, -1)
|
||||||
|
collide_with_areas = true
|
||||||
|
|
||||||
|
[node name="PlayerInteraction" type="Node" parent="." node_paths=PackedStringArray("Camera", "Body", "InteractionRaycaster", "DropRaycaster", "PromptController", "LeftHandAnchor", "RightHandAnchor")]
|
||||||
|
script = ExtResource("3_interaction")
|
||||||
|
Camera = NodePath("../CharacterBody3D/Camera3D")
|
||||||
|
Body = NodePath("../CharacterBody3D")
|
||||||
|
InteractionRaycaster = NodePath("../CharacterBody3D/Camera3D/InteractionRaycaster")
|
||||||
|
DropRaycaster = NodePath("../CharacterBody3D/Camera3D/DropRaycaster")
|
||||||
|
PromptController = NodePath("InteractionUi")
|
||||||
|
LeftHandAnchor = NodePath("../CharacterBody3D/Camera3D/LeftHandAnchor")
|
||||||
|
RightHandAnchor = NodePath("../CharacterBody3D/Camera3D/RightHandAnchor")
|
||||||
|
|
||||||
|
[node name="InteractionUi" type="CanvasLayer" parent="PlayerInteraction" node_paths=PackedStringArray("PromptLabel")]
|
||||||
|
layer = 50
|
||||||
|
script = ExtResource("5_prompt")
|
||||||
|
PromptLabel = NodePath("Root/Prompt")
|
||||||
|
|
||||||
|
[node name="Root" type="Control" parent="PlayerInteraction/InteractionUi"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="Crosshair" type="Label" parent="PlayerInteraction/InteractionUi/Root"]
|
||||||
|
layout_mode = 0
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -10.0
|
||||||
|
offset_top = -15.0
|
||||||
|
offset_right = 10.0
|
||||||
|
offset_bottom = 15.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
text = "+"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="Prompt" type="Label" parent="PlayerInteraction/InteractionUi/Root"]
|
||||||
|
layout_mode = 0
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -320.0
|
||||||
|
offset_top = 34.0
|
||||||
|
offset_right = 320.0
|
||||||
|
offset_bottom = 94.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_colors/font_shadow_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/shadow_offset_x = 2
|
||||||
|
theme_override_constants/shadow_offset_y = 2
|
||||||
|
theme_override_font_sizes/font_size = 16
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
using System;
|
||||||
using Game.Features.Console.Application.Commands;
|
using Game.Features.Console.Application.Commands;
|
||||||
using Game.Features.Console.Godot;
|
using Game.Features.Console.Godot;
|
||||||
using Game.Features.Console.Infrastructure;
|
using Game.Features.Console.Infrastructure;
|
||||||
using Game.Features.Logging.Godot;
|
using Game.Features.Logging.Godot;
|
||||||
using Game.Features.Logging.Infrastructure;
|
using Game.Features.Logging.Infrastructure;
|
||||||
|
using Game.Features.Interactions.Application;
|
||||||
|
using Game.Features.Interactions.Godot;
|
||||||
|
using Game.Features.Items.Application;
|
||||||
|
using Game.Features.Items.Domain;
|
||||||
|
using Game.Features.Items.Godot;
|
||||||
using Game.Features.Multiplayer.Godot;
|
using Game.Features.Multiplayer.Godot;
|
||||||
|
using Game.Features.Players.Contracts;
|
||||||
using Game.Features.Players.Godot;
|
using Game.Features.Players.Godot;
|
||||||
using Game.Godot.Extensions;
|
using Game.Godot.Extensions;
|
||||||
using Game.Godot.Mappings;
|
using Game.Godot.Mappings;
|
||||||
@@ -15,6 +22,10 @@ public partial class GameBootstrap : Node
|
|||||||
{
|
{
|
||||||
[Export] public ConsoleController Console;
|
[Export] public ConsoleController Console;
|
||||||
[Export] public PlayerController Player;
|
[Export] public PlayerController Player;
|
||||||
|
[Export] public PlayerInteractionController PlayerInteraction;
|
||||||
|
[Export] public ItemWorldController ItemWorld;
|
||||||
|
[Export] public ItemSpawnerController ItemSpawner;
|
||||||
|
[Export] public InteractionWorldController InteractionWorld;
|
||||||
[Export] public MultiplayerController MultiplayerController;
|
[Export] public MultiplayerController MultiplayerController;
|
||||||
|
|
||||||
private FileLogger _fileLogger;
|
private FileLogger _fileLogger;
|
||||||
@@ -37,6 +48,7 @@ public partial class GameBootstrap : Node
|
|||||||
registry.Register(new EchoCommand());
|
registry.Register(new EchoCommand());
|
||||||
registry.Register(new ClearCommand());
|
registry.Register(new ClearCommand());
|
||||||
registry.Register(new NoclipCommand(Player));
|
registry.Register(new NoclipCommand(Player));
|
||||||
|
registry.Register(new SpawnItemCommand(ItemSpawner, PlayerInteraction));
|
||||||
registry.Register(new HostCommand(MultiplayerController));
|
registry.Register(new HostCommand(MultiplayerController));
|
||||||
registry.Register(new JoinCommand(MultiplayerController));
|
registry.Register(new JoinCommand(MultiplayerController));
|
||||||
|
|
||||||
@@ -53,6 +65,22 @@ public partial class GameBootstrap : Node
|
|||||||
MultiplayerController.PeerLeft += id => _logger.Info("Multiplayer", $"Peer {id} left.");
|
MultiplayerController.PeerLeft += id => _logger.Info("Multiplayer", $"Peer {id} left.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InitializeItems()
|
||||||
|
{
|
||||||
|
ItemWorld.RegisterSceneItems();
|
||||||
|
ItemSpawner.Initialize();
|
||||||
|
InteractionWorld.RegisterSceneTargets();
|
||||||
|
|
||||||
|
PlayerHeldItemPresenter presenter = new(
|
||||||
|
ItemWorld,
|
||||||
|
PlayerInteraction.LeftHandAnchor,
|
||||||
|
PlayerInteraction.RightHandAnchor
|
||||||
|
);
|
||||||
|
ItemInteractionService itemInteractions = new(new HandsInventory(), ItemWorld, presenter);
|
||||||
|
InteractionService interactions = new(InteractionWorld);
|
||||||
|
PlayerInteraction.Initialize(new PlayerId(Guid.NewGuid()), itemInteractions, interactions);
|
||||||
|
}
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
this.AssertRequiredExports();
|
this.AssertRequiredExports();
|
||||||
@@ -60,6 +88,7 @@ public partial class GameBootstrap : Node
|
|||||||
MouseCapture.Capture();
|
MouseCapture.Capture();
|
||||||
|
|
||||||
InitializeLogging();
|
InitializeLogging();
|
||||||
|
InitializeItems();
|
||||||
InitializeMultiplayer();
|
InitializeMultiplayer();
|
||||||
RegisterConsoleCommands();
|
RegisterConsoleCommands();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ public enum InputAction
|
|||||||
MoveBack,
|
MoveBack,
|
||||||
MoveDown,
|
MoveDown,
|
||||||
Jump,
|
Jump,
|
||||||
|
Interact,
|
||||||
|
UseLeftHand,
|
||||||
|
UseRightHand,
|
||||||
|
DropModifier,
|
||||||
Cancel,
|
Cancel,
|
||||||
ToggleConsole
|
ToggleConsole
|
||||||
}
|
}
|
||||||
@@ -27,6 +31,10 @@ public static class InputActionExtensions
|
|||||||
InputAction.MoveBack => "move_back",
|
InputAction.MoveBack => "move_back",
|
||||||
InputAction.MoveDown => "move_down",
|
InputAction.MoveDown => "move_down",
|
||||||
InputAction.Jump => "jump",
|
InputAction.Jump => "jump",
|
||||||
|
InputAction.Interact => "interact",
|
||||||
|
InputAction.UseLeftHand => "use_left_hand",
|
||||||
|
InputAction.UseRightHand => "use_right_hand",
|
||||||
|
InputAction.DropModifier => "drop_modifier",
|
||||||
InputAction.Cancel => "ui_cancel",
|
InputAction.Cancel => "ui_cancel",
|
||||||
InputAction.ToggleConsole => "toggle_console",
|
InputAction.ToggleConsole => "toggle_console",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(action), action, null)
|
_ => throw new ArgumentOutOfRangeException(nameof(action), action, null)
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
[ext_resource type="Script" uid="uid://drctu5y21ey2y" path="res://src/Godot/GameBootstrap.cs" id="3_bootstrap"]
|
[ext_resource type="Script" uid="uid://drctu5y21ey2y" path="res://src/Godot/GameBootstrap.cs" id="3_bootstrap"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bk0771dvonxh8" path="res://src/Features/Console/Godot/ConsoleController.tscn" id="4_console"]
|
[ext_resource type="PackedScene" uid="uid://bk0771dvonxh8" path="res://src/Features/Console/Godot/ConsoleController.tscn" id="4_console"]
|
||||||
[ext_resource type="Script" uid="uid://drqx0wiw0sbj1" path="res://src/Features/Multiplayer/Godot/MultiplayerController.cs" id="5_multiplayer"]
|
[ext_resource type="Script" uid="uid://drqx0wiw0sbj1" path="res://src/Features/Multiplayer/Godot/MultiplayerController.cs" id="5_multiplayer"]
|
||||||
|
[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"]
|
||||||
|
|
||||||
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_epykj"]
|
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_epykj"]
|
||||||
panorama = ExtResource("1_g4n7o")
|
panorama = ExtResource("1_g4n7o")
|
||||||
@@ -18,10 +22,14 @@ background_mode = 2
|
|||||||
sky = SubResource("Sky_1kpcd")
|
sky = SubResource("Sky_1kpcd")
|
||||||
glow_enabled = true
|
glow_enabled = true
|
||||||
|
|
||||||
[node name="GameWorld" type="Node" unique_id=222644234 node_paths=PackedStringArray("Console", "Player", "MultiplayerController")]
|
[node name="GameWorld" type="Node" unique_id=222644234 node_paths=PackedStringArray("Console", "Player", "PlayerInteraction", "ItemWorld", "ItemSpawner", "InteractionWorld", "MultiplayerController")]
|
||||||
script = ExtResource("3_bootstrap")
|
script = ExtResource("3_bootstrap")
|
||||||
Console = NodePath("Console")
|
Console = NodePath("Console")
|
||||||
Player = NodePath("Player")
|
Player = NodePath("Player")
|
||||||
|
PlayerInteraction = NodePath("Player/PlayerInteraction")
|
||||||
|
ItemWorld = NodePath("ItemWorld")
|
||||||
|
ItemSpawner = NodePath("ItemSpawner")
|
||||||
|
InteractionWorld = NodePath("InteractionWorld")
|
||||||
MultiplayerController = NodePath("Multiplayer")
|
MultiplayerController = NodePath("Multiplayer")
|
||||||
|
|
||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1669211403]
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1669211403]
|
||||||
@@ -39,6 +47,18 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
|
|||||||
[node name="Multiplayer" type="Node" parent="." unique_id=1209131013]
|
[node name="Multiplayer" type="Node" parent="." unique_id=1209131013]
|
||||||
script = ExtResource("5_multiplayer")
|
script = ExtResource("5_multiplayer")
|
||||||
|
|
||||||
|
[node name="ItemWorld" type="Node" parent="."]
|
||||||
|
script = ExtResource("6_items")
|
||||||
|
|
||||||
|
[node name="ItemSpawner" type="Node" parent="." node_paths=PackedStringArray("ItemsRoot", "ItemWorld")]
|
||||||
|
script = ExtResource("8_spawner")
|
||||||
|
ItemsRoot = NodePath("../Entities")
|
||||||
|
ItemWorld = NodePath("../ItemWorld")
|
||||||
|
ItemScenes = Array[PackedScene]([ExtResource("7_flashlight")])
|
||||||
|
|
||||||
|
[node name="InteractionWorld" type="Node" parent="."]
|
||||||
|
script = ExtResource("9_interactions")
|
||||||
|
|
||||||
[node name="Entities" type="Node3D" parent="." unique_id=851492709]
|
[node name="Entities" type="Node3D" parent="." unique_id=851492709]
|
||||||
|
|
||||||
[node name="floor_ceiling_hr_46" parent="Entities" unique_id=33949496 instance=ExtResource("2_1kpcd")]
|
[node name="floor_ceiling_hr_46" parent="Entities" unique_id=33949496 instance=ExtResource("2_1kpcd")]
|
||||||
@@ -66,3 +86,11 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0, -4)
|
|||||||
|
|
||||||
[node name="floor_ceiling_hr_50" parent="Entities" unique_id=1804933431 instance=ExtResource("2_1kpcd")]
|
[node name="floor_ceiling_hr_50" parent="Entities" unique_id=1804933431 instance=ExtResource("2_1kpcd")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0, 0)
|
||||||
|
|
||||||
|
[node name="FlashlightLeft" parent="Entities" instance=ExtResource("7_flashlight")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.25, 0.65, -0.7)
|
||||||
|
InstanceId = "c08790de-b2de-44e6-a73c-a7502ca27e34"
|
||||||
|
|
||||||
|
[node name="FlashlightRight" parent="Entities" instance=ExtResource("7_flashlight")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.25, 0.65, -0.7)
|
||||||
|
InstanceId = "f71e67cb-786b-49eb-bd56-cb42ad19a754"
|
||||||
|
|||||||
Reference in New Issue
Block a user