From bed12ad3356e7e76141cc61e6a70d7cd1ac25968 Mon Sep 17 00:00:00 2001 From: Adrian Mazur Date: Mon, 24 Aug 2026 00:48:07 +0200 Subject: [PATCH] feat: add two-handed flashlight interactions --- project.godot | 20 ++ .../Godot/PlayerInteractionController.cs | 223 ++++++++++++++++++ .../Godot/PlayerInteractionController.cs.uid | 1 + .../Flashlight/FlashlightItemInstance.cs | 19 ++ .../Flashlight/FlashlightItemInstance.cs.uid | 1 + .../Godot/Flashlight/FlashlightHeldView.tscn | 26 ++ .../Godot/Flashlight/FlashlightItem.tscn | 47 ++++ .../Flashlight/FlashlightItemController.cs | 39 +++ .../FlashlightItemController.cs.uid | 1 + src/Features/Players/Godot/PlayerNode.tscn | 82 +++++++ src/Godot/GameBootstrap.cs | 28 +++ src/Godot/Mappings/InputAction.cs | 8 + src/Godot/Scenes/GameWorld.tscn | 30 ++- 13 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 src/Features/Interactions/Godot/PlayerInteractionController.cs create mode 100644 src/Features/Interactions/Godot/PlayerInteractionController.cs.uid create mode 100644 src/Features/Items/Domain/Flashlight/FlashlightItemInstance.cs create mode 100644 src/Features/Items/Domain/Flashlight/FlashlightItemInstance.cs.uid create mode 100644 src/Features/Items/Godot/Flashlight/FlashlightHeldView.tscn create mode 100644 src/Features/Items/Godot/Flashlight/FlashlightItem.tscn create mode 100644 src/Features/Items/Godot/Flashlight/FlashlightItemController.cs create mode 100644 src/Features/Items/Godot/Flashlight/FlashlightItemController.cs.uid 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/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/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/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/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..945602b 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; @@ -53,6 +64,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 +87,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"