diff --git a/project.godot b/project.godot index 0586cf0..5f1c612 100644 --- a/project.godot +++ b/project.godot @@ -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) ] } +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] diff --git a/src/Features/Console/Application/Commands/SpawnItemCommand.cs b/src/Features/Console/Application/Commands/SpawnItemCommand.cs new file mode 100644 index 0000000..22988a6 --- /dev/null +++ b/src/Features/Console/Application/Commands/SpawnItemCommand.cs @@ -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 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); + } +} diff --git a/src/Features/Console/Application/Commands/SpawnItemCommand.cs.uid b/src/Features/Console/Application/Commands/SpawnItemCommand.cs.uid new file mode 100644 index 0000000..e3d37ec --- /dev/null +++ b/src/Features/Console/Application/Commands/SpawnItemCommand.cs.uid @@ -0,0 +1 @@ +uid://e8m3q6v1r9k4w diff --git a/src/Features/Interactions/Application/InteractionService.cs b/src/Features/Interactions/Application/InteractionService.cs new file mode 100644 index 0000000..4657e77 --- /dev/null +++ b/src/Features/Interactions/Application/InteractionService.cs @@ -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); +} diff --git a/src/Features/Interactions/Application/InteractionService.cs.uid b/src/Features/Interactions/Application/InteractionService.cs.uid new file mode 100644 index 0000000..156e7ae --- /dev/null +++ b/src/Features/Interactions/Application/InteractionService.cs.uid @@ -0,0 +1 @@ +uid://b1m7q4y9c5v2n diff --git a/src/Features/Interactions/Contracts/IInteractionService.cs b/src/Features/Interactions/Contracts/IInteractionService.cs new file mode 100644 index 0000000..dbf9365 --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionService.cs @@ -0,0 +1,6 @@ +namespace Game.Features.Interactions.Contracts; + +public interface IInteractionService +{ + InteractionResult TryInteract(InteractionRequest request); +} diff --git a/src/Features/Interactions/Contracts/IInteractionService.cs.uid b/src/Features/Interactions/Contracts/IInteractionService.cs.uid new file mode 100644 index 0000000..6407843 --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionService.cs.uid @@ -0,0 +1 @@ +uid://a3n8v5r1q7m9c diff --git a/src/Features/Interactions/Contracts/IInteractionTarget.cs b/src/Features/Interactions/Contracts/IInteractionTarget.cs new file mode 100644 index 0000000..e6c18c1 --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionTarget.cs @@ -0,0 +1,8 @@ +namespace Game.Features.Interactions.Contracts; + +public interface IInteractionTarget +{ + InteractionTargetId Id { get; } + bool CanInteract { get; } + InteractionPrompt Prompt { get; } +} diff --git a/src/Features/Interactions/Contracts/IInteractionTarget.cs.uid b/src/Features/Interactions/Contracts/IInteractionTarget.cs.uid new file mode 100644 index 0000000..e57a990 --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionTarget.cs.uid @@ -0,0 +1 @@ +uid://c7p2y5n8f1v6m diff --git a/src/Features/Interactions/Contracts/IInteractionWorld.cs b/src/Features/Interactions/Contracts/IInteractionWorld.cs new file mode 100644 index 0000000..79e4a1a --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionWorld.cs @@ -0,0 +1,6 @@ +namespace Game.Features.Interactions.Contracts; + +public interface IInteractionWorld +{ + InteractionResult TryInteract(InteractionRequest request); +} diff --git a/src/Features/Interactions/Contracts/IInteractionWorld.cs.uid b/src/Features/Interactions/Contracts/IInteractionWorld.cs.uid new file mode 100644 index 0000000..880f095 --- /dev/null +++ b/src/Features/Interactions/Contracts/IInteractionWorld.cs.uid @@ -0,0 +1 @@ +uid://d7k1x6m4v9q2b diff --git a/src/Features/Interactions/Contracts/InteractionPrompt.cs b/src/Features/Interactions/Contracts/InteractionPrompt.cs new file mode 100644 index 0000000..c908810 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionPrompt.cs @@ -0,0 +1,3 @@ +namespace Game.Features.Interactions.Contracts; + +public readonly record struct InteractionPrompt(string Text); diff --git a/src/Features/Interactions/Contracts/InteractionPrompt.cs.uid b/src/Features/Interactions/Contracts/InteractionPrompt.cs.uid new file mode 100644 index 0000000..6bc8887 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionPrompt.cs.uid @@ -0,0 +1 @@ +uid://b3m8r1x6d9w4k diff --git a/src/Features/Interactions/Contracts/InteractionRequest.cs b/src/Features/Interactions/Contracts/InteractionRequest.cs new file mode 100644 index 0000000..3ee11be --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionRequest.cs @@ -0,0 +1,5 @@ +using Game.Features.Players.Contracts; + +namespace Game.Features.Interactions.Contracts; + +public readonly record struct InteractionRequest(PlayerId PlayerId, InteractionTargetId TargetId); diff --git a/src/Features/Interactions/Contracts/InteractionRequest.cs.uid b/src/Features/Interactions/Contracts/InteractionRequest.cs.uid new file mode 100644 index 0000000..84ff848 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionRequest.cs.uid @@ -0,0 +1 @@ +uid://e2q7k5n1w8m4d diff --git a/src/Features/Interactions/Contracts/InteractionResult.cs b/src/Features/Interactions/Contracts/InteractionResult.cs new file mode 100644 index 0000000..6ca4d92 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionResult.cs @@ -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); +} diff --git a/src/Features/Interactions/Contracts/InteractionResult.cs.uid b/src/Features/Interactions/Contracts/InteractionResult.cs.uid new file mode 100644 index 0000000..cdc88f1 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionResult.cs.uid @@ -0,0 +1 @@ +uid://f9w2d7k4m6q1x diff --git a/src/Features/Interactions/Contracts/InteractionStatus.cs b/src/Features/Interactions/Contracts/InteractionStatus.cs new file mode 100644 index 0000000..d324c1c --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionStatus.cs @@ -0,0 +1,9 @@ +namespace Game.Features.Interactions.Contracts; + +public enum InteractionStatus +{ + Success, + TargetNotFound, + Unavailable, + Rejected +} diff --git a/src/Features/Interactions/Contracts/InteractionStatus.cs.uid b/src/Features/Interactions/Contracts/InteractionStatus.cs.uid new file mode 100644 index 0000000..ae4d81d --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionStatus.cs.uid @@ -0,0 +1 @@ +uid://b4y9m3r6v1c8p diff --git a/src/Features/Interactions/Contracts/InteractionTargetId.cs b/src/Features/Interactions/Contracts/InteractionTargetId.cs new file mode 100644 index 0000000..2b31238 --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionTargetId.cs @@ -0,0 +1,8 @@ +using System; + +namespace Game.Features.Interactions.Contracts; + +public readonly record struct InteractionTargetId(Guid Value) +{ + public override string ToString() => Value.ToString(); +} diff --git a/src/Features/Interactions/Contracts/InteractionTargetId.cs.uid b/src/Features/Interactions/Contracts/InteractionTargetId.cs.uid new file mode 100644 index 0000000..8f0098d --- /dev/null +++ b/src/Features/Interactions/Contracts/InteractionTargetId.cs.uid @@ -0,0 +1 @@ +uid://c6m1v8q4r9x2k diff --git a/src/Features/Interactions/Godot/InteractionPromptController.cs b/src/Features/Interactions/Godot/InteractionPromptController.cs new file mode 100644 index 0000000..b7fcc30 --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionPromptController.cs @@ -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; + } +} diff --git a/src/Features/Interactions/Godot/InteractionPromptController.cs.uid b/src/Features/Interactions/Godot/InteractionPromptController.cs.uid new file mode 100644 index 0000000..b6a2c4a --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionPromptController.cs.uid @@ -0,0 +1 @@ +uid://b9r2w6n4f8k1v diff --git a/src/Features/Interactions/Godot/InteractionRaycaster.cs b/src/Features/Interactions/Godot/InteractionRaycaster.cs new file mode 100644 index 0000000..faa417a --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionRaycaster.cs @@ -0,0 +1,26 @@ +using Godot; + +namespace Game.Features.Interactions.Godot; + +public partial class InteractionRaycaster : RayCast3D +{ + public T GetTarget() 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; + } +} diff --git a/src/Features/Interactions/Godot/InteractionRaycaster.cs.uid b/src/Features/Interactions/Godot/InteractionRaycaster.cs.uid new file mode 100644 index 0000000..4ee7ffe --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionRaycaster.cs.uid @@ -0,0 +1 @@ +uid://e4m7k1x8c3q6h diff --git a/src/Features/Interactions/Godot/InteractionWorldController.cs b/src/Features/Interactions/Godot/InteractionWorldController.cs new file mode 100644 index 0000000..22a47f3 --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionWorldController.cs @@ -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 _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); + } +} diff --git a/src/Features/Interactions/Godot/InteractionWorldController.cs.uid b/src/Features/Interactions/Godot/InteractionWorldController.cs.uid new file mode 100644 index 0000000..5ee2706 --- /dev/null +++ b/src/Features/Interactions/Godot/InteractionWorldController.cs.uid @@ -0,0 +1 @@ +uid://f6q1c9m4w7v2n diff --git a/src/Features/Interactions/Godot/PlayerInteractionController.cs b/src/Features/Interactions/Godot/PlayerInteractionController.cs new file mode 100644 index 0000000..0cbe48d --- /dev/null +++ b/src/Features/Interactions/Godot/PlayerInteractionController.cs @@ -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 _dropActions = []; + private readonly Dictionary _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(); + 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(); + 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()?.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(); + if (pickupTarget is not null) + { + ShowPickupPrompt(pickupTarget.ItemInfo); + return; + } + + IInteractionTarget interactionTarget = InteractionRaycaster.GetTarget(); + 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 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"); + } +} diff --git a/src/Features/Interactions/Godot/PlayerInteractionController.cs.uid b/src/Features/Interactions/Godot/PlayerInteractionController.cs.uid new file mode 100644 index 0000000..cf62a80 --- /dev/null +++ b/src/Features/Interactions/Godot/PlayerInteractionController.cs.uid @@ -0,0 +1 @@ +uid://c2v8m5r1y7q4d diff --git a/src/Features/Interactions/Godot/WorldInteractionController.cs b/src/Features/Interactions/Godot/WorldInteractionController.cs new file mode 100644 index 0000000..d2a1d23 --- /dev/null +++ b/src/Features/Interactions/Godot/WorldInteractionController.cs @@ -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; + } + } + } +} diff --git a/src/Features/Interactions/Godot/WorldInteractionController.cs.uid b/src/Features/Interactions/Godot/WorldInteractionController.cs.uid new file mode 100644 index 0000000..ac116fc --- /dev/null +++ b/src/Features/Interactions/Godot/WorldInteractionController.cs.uid @@ -0,0 +1 @@ +uid://d2r8m5v1q9k4x diff --git a/src/Features/Items/Application/ItemInteractionService.cs b/src/Features/Items/Application/ItemInteractionService.cs new file mode 100644 index 0000000..4814c0d --- /dev/null +++ b/src/Features/Items/Application/ItemInteractionService.cs @@ -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."); + } +} diff --git a/src/Features/Items/Application/ItemInteractionService.cs.uid b/src/Features/Items/Application/ItemInteractionService.cs.uid new file mode 100644 index 0000000..0e824d1 --- /dev/null +++ b/src/Features/Items/Application/ItemInteractionService.cs.uid @@ -0,0 +1 @@ +uid://e6q2h9v5b8k3p diff --git a/src/Features/Items/Contracts/Hand.cs b/src/Features/Items/Contracts/Hand.cs new file mode 100644 index 0000000..f7b9cb5 --- /dev/null +++ b/src/Features/Items/Contracts/Hand.cs @@ -0,0 +1,7 @@ +namespace Game.Features.Items.Contracts; + +public enum Hand +{ + Left, + Right +} diff --git a/src/Features/Items/Contracts/Hand.cs.uid b/src/Features/Items/Contracts/Hand.cs.uid new file mode 100644 index 0000000..be1e13b --- /dev/null +++ b/src/Features/Items/Contracts/Hand.cs.uid @@ -0,0 +1 @@ +uid://c4n7h2w8m5q1p diff --git a/src/Features/Items/Contracts/HandRequirement.cs b/src/Features/Items/Contracts/HandRequirement.cs new file mode 100644 index 0000000..fa2b858 --- /dev/null +++ b/src/Features/Items/Contracts/HandRequirement.cs @@ -0,0 +1,7 @@ +namespace Game.Features.Items.Contracts; + +public enum HandRequirement +{ + One, + Both +} diff --git a/src/Features/Items/Contracts/HandRequirement.cs.uid b/src/Features/Items/Contracts/HandRequirement.cs.uid new file mode 100644 index 0000000..a3cd015 --- /dev/null +++ b/src/Features/Items/Contracts/HandRequirement.cs.uid @@ -0,0 +1 @@ +uid://d8p3k6v1r9y4b diff --git a/src/Features/Items/Contracts/IHeldItemPresenter.cs b/src/Features/Items/Contracts/IHeldItemPresenter.cs new file mode 100644 index 0000000..7f347ea --- /dev/null +++ b/src/Features/Items/Contracts/IHeldItemPresenter.cs @@ -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); +} diff --git a/src/Features/Items/Contracts/IHeldItemPresenter.cs.uid b/src/Features/Items/Contracts/IHeldItemPresenter.cs.uid new file mode 100644 index 0000000..f2d9fb5 --- /dev/null +++ b/src/Features/Items/Contracts/IHeldItemPresenter.cs.uid @@ -0,0 +1 @@ +uid://c1q7y4d9m5x2v diff --git a/src/Features/Items/Contracts/IItemInstance.cs b/src/Features/Items/Contracts/IItemInstance.cs new file mode 100644 index 0000000..0f35ad6 --- /dev/null +++ b/src/Features/Items/Contracts/IItemInstance.cs @@ -0,0 +1,7 @@ +namespace Game.Features.Items.Contracts; + +public interface IItemInstance +{ + ItemInfo Info { get; } + ItemActionResult Use(ItemUseRequest request); +} diff --git a/src/Features/Items/Contracts/IItemInstance.cs.uid b/src/Features/Items/Contracts/IItemInstance.cs.uid new file mode 100644 index 0000000..1acc4b7 --- /dev/null +++ b/src/Features/Items/Contracts/IItemInstance.cs.uid @@ -0,0 +1 @@ +uid://b5x9q3h7m2w8f diff --git a/src/Features/Items/Contracts/IItemInteractionService.cs b/src/Features/Items/Contracts/IItemInteractionService.cs new file mode 100644 index 0000000..0962d5a --- /dev/null +++ b/src/Features/Items/Contracts/IItemInteractionService.cs @@ -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); +} diff --git a/src/Features/Items/Contracts/IItemInteractionService.cs.uid b/src/Features/Items/Contracts/IItemInteractionService.cs.uid new file mode 100644 index 0000000..b129d88 --- /dev/null +++ b/src/Features/Items/Contracts/IItemInteractionService.cs.uid @@ -0,0 +1 @@ +uid://f2k8q5b1w7m4n diff --git a/src/Features/Items/Contracts/IItemRepository.cs b/src/Features/Items/Contracts/IItemRepository.cs new file mode 100644 index 0000000..c603ea3 --- /dev/null +++ b/src/Features/Items/Contracts/IItemRepository.cs @@ -0,0 +1,6 @@ +namespace Game.Features.Items.Contracts; + +public interface IItemRepository +{ + bool TryGet(ItemInstanceId itemId, out IItemInstance item); +} diff --git a/src/Features/Items/Contracts/IItemRepository.cs.uid b/src/Features/Items/Contracts/IItemRepository.cs.uid new file mode 100644 index 0000000..f2885ce --- /dev/null +++ b/src/Features/Items/Contracts/IItemRepository.cs.uid @@ -0,0 +1 @@ +uid://e7n1m6v4k9r3q diff --git a/src/Features/Items/Contracts/IItemSpawnPoint.cs b/src/Features/Items/Contracts/IItemSpawnPoint.cs new file mode 100644 index 0000000..2236986 --- /dev/null +++ b/src/Features/Items/Contracts/IItemSpawnPoint.cs @@ -0,0 +1,8 @@ +using Godot; + +namespace Game.Features.Items.Contracts; + +public interface IItemSpawnPoint +{ + Vector3 GetItemSpawnPosition(); +} diff --git a/src/Features/Items/Contracts/IItemSpawnPoint.cs.uid b/src/Features/Items/Contracts/IItemSpawnPoint.cs.uid new file mode 100644 index 0000000..63e18c0 --- /dev/null +++ b/src/Features/Items/Contracts/IItemSpawnPoint.cs.uid @@ -0,0 +1 @@ +uid://a2v6r8m3q7k5d diff --git a/src/Features/Items/Contracts/IItemSpawner.cs b/src/Features/Items/Contracts/IItemSpawner.cs new file mode 100644 index 0000000..aa95994 --- /dev/null +++ b/src/Features/Items/Contracts/IItemSpawner.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Godot; + +namespace Game.Features.Items.Contracts; + +public interface IItemSpawner +{ + IReadOnlyCollection DefinitionIds { get; } + ItemActionResult TrySpawn(ItemDefinitionId definitionId, Vector3 worldPosition, out ItemInstanceId itemId); +} diff --git a/src/Features/Items/Contracts/IItemSpawner.cs.uid b/src/Features/Items/Contracts/IItemSpawner.cs.uid new file mode 100644 index 0000000..c0bc2ea --- /dev/null +++ b/src/Features/Items/Contracts/IItemSpawner.cs.uid @@ -0,0 +1 @@ +uid://f4m9k2w6b8n1r diff --git a/src/Features/Items/Contracts/IPickupTarget.cs b/src/Features/Items/Contracts/IPickupTarget.cs new file mode 100644 index 0000000..0ef4a5f --- /dev/null +++ b/src/Features/Items/Contracts/IPickupTarget.cs @@ -0,0 +1,6 @@ +namespace Game.Features.Items.Contracts; + +public interface IPickupTarget +{ + ItemInfo ItemInfo { get; } +} diff --git a/src/Features/Items/Contracts/IPickupTarget.cs.uid b/src/Features/Items/Contracts/IPickupTarget.cs.uid new file mode 100644 index 0000000..2e4c603 --- /dev/null +++ b/src/Features/Items/Contracts/IPickupTarget.cs.uid @@ -0,0 +1 @@ +uid://c9v4g7p2m5x8d diff --git a/src/Features/Items/Contracts/ItemActionPhase.cs b/src/Features/Items/Contracts/ItemActionPhase.cs new file mode 100644 index 0000000..4d728d0 --- /dev/null +++ b/src/Features/Items/Contracts/ItemActionPhase.cs @@ -0,0 +1,8 @@ +namespace Game.Features.Items.Contracts; + +public enum ItemActionPhase +{ + Started, + Completed, + Cancelled +} diff --git a/src/Features/Items/Contracts/ItemActionPhase.cs.uid b/src/Features/Items/Contracts/ItemActionPhase.cs.uid new file mode 100644 index 0000000..5d7e72b --- /dev/null +++ b/src/Features/Items/Contracts/ItemActionPhase.cs.uid @@ -0,0 +1 @@ +uid://c9w5d2n8q4m1h diff --git a/src/Features/Items/Contracts/ItemActionResult.cs b/src/Features/Items/Contracts/ItemActionResult.cs new file mode 100644 index 0000000..2c68f79 --- /dev/null +++ b/src/Features/Items/Contracts/ItemActionResult.cs @@ -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); +} diff --git a/src/Features/Items/Contracts/ItemActionResult.cs.uid b/src/Features/Items/Contracts/ItemActionResult.cs.uid new file mode 100644 index 0000000..e963cc2 --- /dev/null +++ b/src/Features/Items/Contracts/ItemActionResult.cs.uid @@ -0,0 +1 @@ +uid://a8m4p1x7d3q6k diff --git a/src/Features/Items/Contracts/ItemActionStatus.cs b/src/Features/Items/Contracts/ItemActionStatus.cs new file mode 100644 index 0000000..c9de44b --- /dev/null +++ b/src/Features/Items/Contracts/ItemActionStatus.cs @@ -0,0 +1,12 @@ +namespace Game.Features.Items.Contracts; + +public enum ItemActionStatus +{ + Success, + ItemNotFound, + HandOccupied, + HandEmpty, + UnsupportedAction, + PresentationFailed, + UnknownDefinition +} diff --git a/src/Features/Items/Contracts/ItemActionStatus.cs.uid b/src/Features/Items/Contracts/ItemActionStatus.cs.uid new file mode 100644 index 0000000..47f8318 --- /dev/null +++ b/src/Features/Items/Contracts/ItemActionStatus.cs.uid @@ -0,0 +1 @@ +uid://f1r6y3b9k5v2m diff --git a/src/Features/Items/Contracts/ItemDefinition.cs b/src/Features/Items/Contracts/ItemDefinition.cs new file mode 100644 index 0000000..c725ac3 --- /dev/null +++ b/src/Features/Items/Contracts/ItemDefinition.cs @@ -0,0 +1,7 @@ +namespace Game.Features.Items.Contracts; + +public readonly record struct ItemDefinition( + ItemDefinitionId Id, + string DisplayName, + HandRequirement HandRequirement +); diff --git a/src/Features/Items/Contracts/ItemDefinition.cs.uid b/src/Features/Items/Contracts/ItemDefinition.cs.uid new file mode 100644 index 0000000..cca080e --- /dev/null +++ b/src/Features/Items/Contracts/ItemDefinition.cs.uid @@ -0,0 +1 @@ +uid://e3v8k4r1m7x2p diff --git a/src/Features/Items/Contracts/ItemDefinitionId.cs b/src/Features/Items/Contracts/ItemDefinitionId.cs new file mode 100644 index 0000000..676bc33 --- /dev/null +++ b/src/Features/Items/Contracts/ItemDefinitionId.cs @@ -0,0 +1,6 @@ +namespace Game.Features.Items.Contracts; + +public readonly record struct ItemDefinitionId(string Value) +{ + public override string ToString() => Value; +} diff --git a/src/Features/Items/Contracts/ItemDefinitionId.cs.uid b/src/Features/Items/Contracts/ItemDefinitionId.cs.uid new file mode 100644 index 0000000..793d0f1 --- /dev/null +++ b/src/Features/Items/Contracts/ItemDefinitionId.cs.uid @@ -0,0 +1 @@ +uid://b7q2n9c5w1m6y diff --git a/src/Features/Items/Contracts/ItemInfo.cs b/src/Features/Items/Contracts/ItemInfo.cs new file mode 100644 index 0000000..46bbf48 --- /dev/null +++ b/src/Features/Items/Contracts/ItemInfo.cs @@ -0,0 +1,8 @@ +namespace Game.Features.Items.Contracts; + +public readonly record struct ItemInfo( + ItemInstanceId Id, + ItemDefinitionId DefinitionId, + string DisplayName, + HandRequirement HandRequirement +); diff --git a/src/Features/Items/Contracts/ItemInfo.cs.uid b/src/Features/Items/Contracts/ItemInfo.cs.uid new file mode 100644 index 0000000..45d6310 --- /dev/null +++ b/src/Features/Items/Contracts/ItemInfo.cs.uid @@ -0,0 +1 @@ +uid://e1r5j8w3n6c9h diff --git a/src/Features/Items/Contracts/ItemInstanceId.cs b/src/Features/Items/Contracts/ItemInstanceId.cs new file mode 100644 index 0000000..442b243 --- /dev/null +++ b/src/Features/Items/Contracts/ItemInstanceId.cs @@ -0,0 +1,8 @@ +using System; + +namespace Game.Features.Items.Contracts; + +public readonly record struct ItemInstanceId(Guid Value) +{ + public override string ToString() => Value.ToString(); +} diff --git a/src/Features/Items/Contracts/ItemInstanceId.cs.uid b/src/Features/Items/Contracts/ItemInstanceId.cs.uid new file mode 100644 index 0000000..65989ae --- /dev/null +++ b/src/Features/Items/Contracts/ItemInstanceId.cs.uid @@ -0,0 +1 @@ +uid://b6x2f9m4t7k3q diff --git a/src/Features/Items/Contracts/ItemUseRequest.cs b/src/Features/Items/Contracts/ItemUseRequest.cs new file mode 100644 index 0000000..ed92298 --- /dev/null +++ b/src/Features/Items/Contracts/ItemUseRequest.cs @@ -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 +); diff --git a/src/Features/Items/Contracts/ItemUseRequest.cs.uid b/src/Features/Items/Contracts/ItemUseRequest.cs.uid new file mode 100644 index 0000000..46fee88 --- /dev/null +++ b/src/Features/Items/Contracts/ItemUseRequest.cs.uid @@ -0,0 +1 @@ +uid://d6k2v8m5r1c9n diff --git a/src/Features/Items/Domain/Flashlight/FlashlightItemInstance.cs b/src/Features/Items/Domain/Flashlight/FlashlightItemInstance.cs new file mode 100644 index 0000000..b5f452e --- /dev/null +++ b/src/Features/Items/Domain/Flashlight/FlashlightItemInstance.cs @@ -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(); + } +} diff --git a/src/Features/Items/Domain/Flashlight/FlashlightItemInstance.cs.uid b/src/Features/Items/Domain/Flashlight/FlashlightItemInstance.cs.uid new file mode 100644 index 0000000..7e02943 --- /dev/null +++ b/src/Features/Items/Domain/Flashlight/FlashlightItemInstance.cs.uid @@ -0,0 +1 @@ +uid://b8r2n5q9w4m1y diff --git a/src/Features/Items/Domain/HandsInventory.cs b/src/Features/Items/Domain/HandsInventory.cs new file mode 100644 index 0000000..3daa7e0 --- /dev/null +++ b/src/Features/Items/Domain/HandsInventory.cs @@ -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); + } + } +} diff --git a/src/Features/Items/Domain/HandsInventory.cs.uid b/src/Features/Items/Domain/HandsInventory.cs.uid new file mode 100644 index 0000000..4f34c35 --- /dev/null +++ b/src/Features/Items/Domain/HandsInventory.cs.uid @@ -0,0 +1 @@ +uid://d5w1n8c4q7m2y diff --git a/src/Features/Items/Domain/PassiveItemInstance.cs b/src/Features/Items/Domain/PassiveItemInstance.cs new file mode 100644 index 0000000..eb6fd71 --- /dev/null +++ b/src/Features/Items/Domain/PassiveItemInstance.cs @@ -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." + ); +} diff --git a/src/Features/Items/Domain/PassiveItemInstance.cs.uid b/src/Features/Items/Domain/PassiveItemInstance.cs.uid new file mode 100644 index 0000000..1bec91a --- /dev/null +++ b/src/Features/Items/Domain/PassiveItemInstance.cs.uid @@ -0,0 +1 @@ +uid://d9p4x1m7v3k6c diff --git a/src/Features/Items/Godot/Flashlight/FlashlightHeldView.tscn b/src/Features/Items/Godot/Flashlight/FlashlightHeldView.tscn new file mode 100644 index 0000000..e7b68d7 --- /dev/null +++ b/src/Features/Items/Godot/Flashlight/FlashlightHeldView.tscn @@ -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 diff --git a/src/Features/Items/Godot/Flashlight/FlashlightItem.tscn b/src/Features/Items/Godot/Flashlight/FlashlightItem.tscn new file mode 100644 index 0000000..672db73 --- /dev/null +++ b/src/Features/Items/Godot/Flashlight/FlashlightItem.tscn @@ -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 diff --git a/src/Features/Items/Godot/Flashlight/FlashlightItemController.cs b/src/Features/Items/Godot/Flashlight/FlashlightItemController.cs new file mode 100644 index 0000000..2ac1b27 --- /dev/null +++ b/src/Features/Items/Godot/Flashlight/FlashlightItemController.cs @@ -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"); + } + + 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; + } + } +} diff --git a/src/Features/Items/Godot/Flashlight/FlashlightItemController.cs.uid b/src/Features/Items/Godot/Flashlight/FlashlightItemController.cs.uid new file mode 100644 index 0000000..26977b4 --- /dev/null +++ b/src/Features/Items/Godot/Flashlight/FlashlightItemController.cs.uid @@ -0,0 +1 @@ +uid://a5q9y2d7m4v8p diff --git a/src/Features/Items/Godot/ItemSpawnerController.cs b/src/Features/Items/Godot/ItemSpawnerController.cs new file mode 100644 index 0000000..c06ec13 --- /dev/null +++ b/src/Features/Items/Godot/ItemSpawnerController.cs @@ -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 ItemScenes = []; + + private readonly Dictionary _scenes = []; + + public IReadOnlyCollection 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(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(); + 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(); + ItemDefinitionId definitionId = new(prototype.DefinitionId); + prototype.Free(); + + if (!_scenes.TryAdd(definitionId, scene)) + { + throw new InvalidOperationException($"Duplicate item spawn definition: {definitionId}."); + } + } +} diff --git a/src/Features/Items/Godot/ItemSpawnerController.cs.uid b/src/Features/Items/Godot/ItemSpawnerController.cs.uid new file mode 100644 index 0000000..7d39376 --- /dev/null +++ b/src/Features/Items/Godot/ItemSpawnerController.cs.uid @@ -0,0 +1 @@ +uid://c3x9m6w2q8r1b diff --git a/src/Features/Items/Godot/ItemWorldController.cs b/src/Features/Items/Godot/ItemWorldController.cs new file mode 100644 index 0000000..36974d0 --- /dev/null +++ b/src/Features/Items/Godot/ItemWorldController.cs @@ -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 _items = []; + + public IReadOnlyCollection 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); + } +} diff --git a/src/Features/Items/Godot/ItemWorldController.cs.uid b/src/Features/Items/Godot/ItemWorldController.cs.uid new file mode 100644 index 0000000..8d635fd --- /dev/null +++ b/src/Features/Items/Godot/ItemWorldController.cs.uid @@ -0,0 +1 @@ +uid://f8n3b6w1k5r9m diff --git a/src/Features/Items/Godot/PlayerHeldItemPresenter.cs b/src/Features/Items/Godot/PlayerHeldItemPresenter.cs new file mode 100644 index 0000000..d5f38e8 --- /dev/null +++ b/src/Features/Items/Godot/PlayerHeldItemPresenter.cs @@ -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); + } + } +} diff --git a/src/Features/Items/Godot/PlayerHeldItemPresenter.cs.uid b/src/Features/Items/Godot/PlayerHeldItemPresenter.cs.uid new file mode 100644 index 0000000..8ca5df0 --- /dev/null +++ b/src/Features/Items/Godot/PlayerHeldItemPresenter.cs.uid @@ -0,0 +1 @@ +uid://e5m2r8q1v7k4n diff --git a/src/Features/Items/Godot/WorldItemController.cs b/src/Features/Items/Godot/WorldItemController.cs new file mode 100644 index 0000000..87c0b05 --- /dev/null +++ b/src/Features/Items/Godot/WorldItemController.cs @@ -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(); + 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; + } + } + } +} diff --git a/src/Features/Items/Godot/WorldItemController.cs.uid b/src/Features/Items/Godot/WorldItemController.cs.uid new file mode 100644 index 0000000..5350961 --- /dev/null +++ b/src/Features/Items/Godot/WorldItemController.cs.uid @@ -0,0 +1 @@ +uid://d1x7m4q9v2c6k diff --git a/src/Features/Players/Contracts/PlayerId.cs b/src/Features/Players/Contracts/PlayerId.cs new file mode 100644 index 0000000..c5f15ff --- /dev/null +++ b/src/Features/Players/Contracts/PlayerId.cs @@ -0,0 +1,8 @@ +using System; + +namespace Game.Features.Players.Contracts; + +public readonly record struct PlayerId(Guid Value) +{ + public override string ToString() => Value.ToString(); +} diff --git a/src/Features/Players/Contracts/PlayerId.cs.uid b/src/Features/Players/Contracts/PlayerId.cs.uid new file mode 100644 index 0000000..888852e --- /dev/null +++ b/src/Features/Players/Contracts/PlayerId.cs.uid @@ -0,0 +1 @@ +uid://d4m8x1q6v3k9r diff --git a/src/Features/Players/Godot/PlayerNode.tscn b/src/Features/Players/Godot/PlayerNode.tscn index 0690ea5..3c472d6 100644 --- a/src/Features/Players/Godot/PlayerNode.tscn +++ b/src/Features/Players/Godot/PlayerNode.tscn @@ -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="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"] radius = 0.25 @@ -775,3 +778,82 @@ skeleton = NodePath("..") [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) 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 diff --git a/src/Godot/GameBootstrap.cs b/src/Godot/GameBootstrap.cs index 78cf737..2320d01 100644 --- a/src/Godot/GameBootstrap.cs +++ b/src/Godot/GameBootstrap.cs @@ -1,9 +1,16 @@ +using System; using Game.Features.Console.Application.Commands; using Game.Features.Console.Godot; using Game.Features.Console.Infrastructure; using Game.Features.Logging.Godot; 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.Players.Contracts; using Game.Features.Players.Godot; using Game.Godot.Extensions; using Game.Godot.Mappings; @@ -15,6 +22,10 @@ public partial class GameBootstrap : Node { [Export] public ConsoleController Console; [Export] public PlayerController Player; + [Export] public PlayerInteractionController PlayerInteraction; + [Export] public ItemWorldController ItemWorld; + [Export] public ItemSpawnerController ItemSpawner; + [Export] public InteractionWorldController InteractionWorld; [Export] public MultiplayerController MultiplayerController; private FileLogger _fileLogger; @@ -37,6 +48,7 @@ public partial class GameBootstrap : Node registry.Register(new EchoCommand()); registry.Register(new ClearCommand()); registry.Register(new NoclipCommand(Player)); + registry.Register(new SpawnItemCommand(ItemSpawner, PlayerInteraction)); registry.Register(new HostCommand(MultiplayerController)); registry.Register(new JoinCommand(MultiplayerController)); @@ -53,6 +65,22 @@ public partial class GameBootstrap : Node 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() { this.AssertRequiredExports(); @@ -60,6 +88,7 @@ public partial class GameBootstrap : Node MouseCapture.Capture(); InitializeLogging(); + InitializeItems(); InitializeMultiplayer(); RegisterConsoleCommands(); } diff --git a/src/Godot/Mappings/InputAction.cs b/src/Godot/Mappings/InputAction.cs index e4fb564..af47f39 100644 --- a/src/Godot/Mappings/InputAction.cs +++ b/src/Godot/Mappings/InputAction.cs @@ -10,6 +10,10 @@ public enum InputAction MoveBack, MoveDown, Jump, + Interact, + UseLeftHand, + UseRightHand, + DropModifier, Cancel, ToggleConsole } @@ -27,6 +31,10 @@ public static class InputActionExtensions InputAction.MoveBack => "move_back", InputAction.MoveDown => "move_down", 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.ToggleConsole => "toggle_console", _ => throw new ArgumentOutOfRangeException(nameof(action), action, null) diff --git a/src/Godot/Scenes/GameWorld.tscn b/src/Godot/Scenes/GameWorld.tscn index a6c6e08..87f71f7 100644 --- a/src/Godot/Scenes/GameWorld.tscn +++ b/src/Godot/Scenes/GameWorld.tscn @@ -6,6 +6,10 @@ [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="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"] panorama = ExtResource("1_g4n7o") @@ -18,10 +22,14 @@ background_mode = 2 sky = SubResource("Sky_1kpcd") 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") Console = NodePath("Console") Player = NodePath("Player") +PlayerInteraction = NodePath("Player/PlayerInteraction") +ItemWorld = NodePath("ItemWorld") +ItemSpawner = NodePath("ItemSpawner") +InteractionWorld = NodePath("InteractionWorld") MultiplayerController = NodePath("Multiplayer") [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] 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="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")] 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"