60 changed files with 862 additions and 493 deletions
+21
View File
@@ -247,6 +247,27 @@ Loot -> Enemies.Application ŹLE
Combat -> Enemies.Godot ŹLE
```
### Wyjątek: `Godot` -> `Godot`
`Domain` i `Application` trzymają się reguły bez wyjątków - tam kontrakt broni czegoś realnego (można podmienić implementację, przetestować logikę bez silnika, nie ma przypadkowego sprzężenia).
`Godot` tego wsparcia nie potrzebuje - kontrolery są celowo "głupie" i nie są objęte testami automatycznymi, więc kontrakt istniałby tam wyłącznie po to, żeby ominąć ograniczenie edytora Godota (`[Export]` nie umie wskazywać na typ-interfejs). Dlatego `Godot` jednego feature'a może importować `Godot` innego feature'a bezpośrednio, najczęściej po to, żeby zawrzeć w scenie węzeł z innego feature'a (`[Export]` na konkretną klasę) i wywołać na nim kilka metod.
```text
Players.Godot -> Interactions.Godot DOBRZE (kontener sceny, wywołanie metod kontrolera)
Players.Application -> Interactions.Godot ŹLE (logika feature'a nadal tylko przez Contracts)
Players.Domain -> Interactions.Godot ŹLE
```
`GetNode`/`GetNodeOrNull` po ścieżce w scenie między feature'ami dalej jest niepożądane - zależność ma być jawna (typowane pole `[Export]` albo parametr metody `Initialize`/`Use...`), nie odnajdywana w drzewie sceny w runtime.
Ten wyjątek nie zwalnia z Prawa Demeter. Rozmawiamy tylko z bezpośrednim sąsiadem - jeśli feature trzyma w sobie kontroler innego feature'a, woła na nim metody sam, zamiast wystawiać go dalej do przechodzenia przez łańcuch (`node.Inner.Other.Method()`). Kod spoza feature'a nie powinien wiedzieć, że dany kontroler w ogóle zawiera w sobie coś z innego feature'a.
```text
player.SetMovementMode(mode) DOBRZE (PlayerController sam woła w środku Interaction/Body)
player.Interaction.SetActive(isActive) ŹLE (wywołujący łańcuje się przez Playera do Interaction)
```
### Eventy między feature'ami
Gdy jedno zdarzenie interesuje wiele feature'ów, feature źródłowy publikuje event i nie wie, kto go słucha:
@@ -15,12 +15,12 @@ public sealed class SpawnItemCommand : ICommand
public string Description => "Spawns an item in front of the player.";
public IReadOnlyList<CommandArgSpec> Args { get; }
public SpawnItemCommand(IItemSpawner spawner, IItemSpawnPoint spawnPoint)
public SpawnItemCommand(IItemSpawner spawner, IItemDefinitionRegistry registry, IItemSpawnPoint spawnPoint)
{
_spawner = spawner;
_spawnPoint = spawnPoint;
string[] definitions = spawner.DefinitionIds
string[] definitions = registry.DefinitionIds
.Select(id => id.Value)
.OrderBy(id => id, StringComparer.Ordinal)
.ToArray();
@@ -0,0 +1,21 @@
using Game.Features.Interactions.Contracts;
namespace Game.Features.Interactions.Application;
public sealed class Interact(IInteractionRegistry registry) : IInteractionHandler
{
public InteractionResult TryInteract(InteractionRequest request)
{
if (!registry.TryGetTarget(request.TargetId, out IInteractionTarget target))
{
return InteractionResult.Fail(InteractionStatus.TargetNotFound, $"Target {request.TargetId} was not found.");
}
if (!target.CanInteract)
{
return InteractionResult.Fail(InteractionStatus.Unavailable, "The target is not currently available.");
}
return target.Execute(request.PlayerId);
}
}
@@ -1,8 +0,0 @@
using Game.Features.Interactions.Contracts;
namespace Game.Features.Interactions.Application;
public sealed class InteractionService(IInteractionWorld world) : IInteractionService
{
public InteractionResult TryInteract(InteractionRequest request) => world.TryInteract(request);
}
@@ -0,0 +1,19 @@
using Game.Features.Items.Contracts;
namespace Game.Features.Interactions.Application;
public readonly record struct PickupAvailability(bool LeftHand, bool RightHand, bool RequiresBoth);
public sealed class ResolvePickupAvailability
{
public PickupAvailability Resolve(HandRequirement requirement, bool leftHandFree, bool rightHandFree)
{
if (requirement == HandRequirement.Both)
{
bool canUseBoth = leftHandFree && rightHandFree;
return new PickupAvailability(canUseBoth, canUseBoth, true);
}
return new PickupAvailability(leftHandFree, rightHandFree, false);
}
}
@@ -0,0 +1 @@
uid://c6mhpn06a47qu
@@ -1,6 +1,6 @@
namespace Game.Features.Interactions.Contracts;
public interface IInteractionService
public interface IInteractionHandler
{
InteractionResult TryInteract(InteractionRequest request);
}
@@ -0,0 +1,8 @@
namespace Game.Features.Interactions.Contracts;
public interface IInteractionRegistry
{
void Register(IInteractionTarget target);
void Unregister(InteractionTargetId targetId);
bool TryGetTarget(InteractionTargetId targetId, out IInteractionTarget target);
}
@@ -0,0 +1 @@
uid://baejppnflmpnn
@@ -1,8 +1,12 @@
using Game.Features.Players.Contracts;
namespace Game.Features.Interactions.Contracts;
public interface IInteractionTarget
{
InteractionTargetId Id { get; }
InteractionTargetId InteractionTargetId { get; }
bool CanInteract { get; }
InteractionPrompt Prompt { get; }
InteractionResult Execute(PlayerId playerId);
}
@@ -1,6 +0,0 @@
namespace Game.Features.Interactions.Contracts;
public interface IInteractionWorld
{
InteractionResult TryInteract(InteractionRequest request);
}
@@ -1 +0,0 @@
uid://d7k1x6m4v9q2b
@@ -1,8 +1,10 @@
using System;
using Game.Shared.Domain;
namespace Game.Features.Interactions.Contracts;
public readonly record struct InteractionTargetId(Guid Value)
public readonly record struct InteractionTargetId(InstanceId Value)
{
public static InteractionTargetId From(InstanceId value) => new(value);
public override string ToString() => Value.ToString();
}
@@ -0,0 +1,26 @@
using Godot;
namespace Game.Features.Interactions.Domain;
public readonly record struct DropRayHit(
Vector3 Origin,
Vector3 Direction,
float RayLength,
bool IsBlocked,
Vector3 BlockPoint
);
public static class DropPosition
{
public static Vector3 CalculateFrom(DropRayHit hit)
{
float distance = hit.RayLength;
if (hit.IsBlocked)
{
distance = Mathf.Max(0.25f, hit.Origin.DistanceTo(hit.BlockPoint) - 0.2f);
}
return hit.Origin + hit.Direction * distance;
}
}
@@ -0,0 +1 @@
uid://derjakse05m3d
@@ -0,0 +1,58 @@
using Game.Features.Interactions.Contracts;
using Game.Features.Players.Contracts;
using Game.Godot.Extensions;
using Godot;
using Godot.Collections;
namespace Game.Features.Interactions.Godot;
public abstract partial class InteractableController : Node3D, IInteractionTarget
{
public const string InteractionGroup = "interactions";
[Export]
public string InstanceKey;
[Export]
public string PromptText = "Wejdź w interakcję";
public InteractionTargetId InteractionTargetId { get; private set; }
public virtual bool CanInteract => true;
public InteractionPrompt Prompt => new(PromptText);
private InteractionsRegistryController _registry;
private void RegisterWithAvailableRegistry()
{
Array<Node> availableRegistries = GetTree()
.GetNodesInGroup(InteractionsRegistryController.RegistryGroup);
foreach (Node node in availableRegistries)
{
if (node is InteractionsRegistryController registry)
{
registry.Register(this);
return;
}
}
}
public void UseRegistry(InteractionsRegistryController registry)
{
_registry = registry;
}
public abstract InteractionResult Execute(PlayerId playerId);
public override void _ExitTree()
{
_registry?.Unregister(InteractionTargetId);
}
public override void _Ready()
{
InteractionTargetId = InteractionTargetId.From(this.GetStableInstanceIdFrom(InstanceKey));
AddToGroup(InteractionGroup);
RegisterWithAvailableRegistry();
}
}
@@ -0,0 +1 @@
uid://bh0e1kwa1i4r
@@ -4,7 +4,7 @@ namespace Game.Features.Interactions.Godot;
public partial class InteractionRaycaster : RayCast3D
{
public T GetTarget<T>() where T : class
public T GetTargetOrNull<T>() where T : class
{
if (!IsColliding() || GetCollider() is not Node node)
{
@@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using Game.Features.Interactions.Contracts;
using Godot;
namespace Game.Features.Interactions.Godot;
public partial class InteractionWorldController : Node, IInteractionWorld
{
public const string RegistryGroup = "interaction_world_registry";
private readonly Dictionary<InteractionTargetId, WorldInteractionController> _targets = [];
public override void _EnterTree()
{
AddToGroup(RegistryGroup);
}
public void RegisterSceneTargets()
{
foreach (Node node in GetTree().GetNodesInGroup(WorldInteractionController.InteractionGroup))
{
if (node is WorldInteractionController target)
{
Register(target);
}
}
}
public void Register(WorldInteractionController target)
{
if (_targets.TryGetValue(target.Id, out WorldInteractionController existing))
{
if (ReferenceEquals(existing, target))
{
return;
}
throw new InvalidOperationException($"Duplicate interaction target id: {target.Id}.");
}
_targets.Add(target.Id, target);
target.RegisterWith(this);
}
public void Unregister(InteractionTargetId targetId)
{
_targets.Remove(targetId);
}
public InteractionResult TryInteract(InteractionRequest request)
{
if (!_targets.TryGetValue(request.TargetId, out WorldInteractionController target))
{
return InteractionResult.Fail(InteractionStatus.TargetNotFound, $"Target {request.TargetId} was not found.");
}
if (!target.CanInteract)
{
return InteractionResult.Fail(InteractionStatus.Unavailable, "The target is not currently available.");
}
return target.Execute(request.PlayerId);
}
}
@@ -0,0 +1,47 @@
using Game.Features.Interactions.Contracts;
using Godot;
using GodotNodeArray = Godot.Collections.Array<Godot.Node>;
namespace Game.Features.Interactions.Godot;
public partial class InteractionsRegistryController : Node
{
public const string RegistryGroup = "interactions_registry";
private IInteractionRegistry _registry;
private GodotNodeArray GetInteractionTargetNodesInScene() =>
GetTree().GetNodesInGroup(InteractableController.InteractionGroup);
public override void _EnterTree()
{
AddToGroup(RegistryGroup);
}
public void UseRegistry(IInteractionRegistry registry)
{
_registry = registry;
}
public void RegisterSceneTargets()
{
foreach (Node node in GetInteractionTargetNodesInScene())
{
if (node is InteractableController target)
{
Register(target);
}
}
}
public void Register(InteractableController target)
{
_registry.Register(target);
target.UseRegistry(this);
}
public void Unregister(InteractionTargetId targetId)
{
_registry.Unregister(targetId);
}
}
@@ -1,8 +1,11 @@
using System.Collections.Generic;
using Game.Features.Interactions.Application;
using Game.Features.Interactions.Contracts;
using Game.Features.Interactions.Domain;
using Game.Features.Items.Contracts;
using Game.Features.Players.Contracts;
using Game.Godot;
using Game.Godot.Exceptions;
using Game.Godot.Extensions;
using Game.Godot.Mappings;
using Godot;
@@ -11,29 +14,149 @@ namespace Game.Features.Interactions.Godot;
public partial class PlayerInteractionController : Node, IItemSpawnPoint
{
[Export] public Camera3D Camera;
[Export] public CharacterBody3D Body;
[Export] public InteractionRaycaster InteractionRaycaster;
[Export] public RayCast3D DropRaycaster;
[Export] public InteractionPromptController PromptController;
[Export] public Node3D LeftHandAnchor;
[Export] public Node3D RightHandAnchor;
[Export]
public Camera3D Camera;
[Export]
public CharacterBody3D Body;
[Export]
public InteractionRaycaster InteractionRaycaster;
[Export]
public RayCast3D DropRaycaster;
[Export]
public InteractionPromptController PromptController;
[Export]
public Node3D LeftHandAnchor;
[Export]
public Node3D RightHandAnchor;
private IItemInteractionService _items;
private IInteractionService _interactions;
private IInteractionHandler _interactions;
private PlayerId _playerId;
private readonly HashSet<Hand> _dropActions = [];
private readonly Dictionary<Hand, InteractionTargetId?> _activeUseTargets = [];
private readonly ResolvePickupAvailability _resolvePickupAvailability = new();
public override void _Ready()
private ItemUseRequest CreateUseRequest(Hand hand, ItemActionPhase phase)
{
this.AssertRequiredExports();
InteractionTargetId? targetId = InteractionRaycaster
.GetTargetOrNull<IInteractionTarget>()
?.InteractionTargetId;
return new ItemUseRequest(_playerId, hand, phase, targetId);
}
private void ProcessHandActionStarted(Hand hand)
{
if (Input.IsActionPressed(InputAction.DropModifier.ActionName()))
{
_items.TryUse(new ItemUseRequest(_playerId, hand, ItemActionPhase.Cancelled));
_items.TryDrop(hand, CalculateDropPosition());
_dropActions.Add(hand);
return;
}
if (!_items.IsHandFree(hand))
{
_items.TryUse(CreateUseRequest(hand, ItemActionPhase.Started));
return;
}
IPickupTarget target = InteractionRaycaster.GetTargetOrNull<IPickupTarget>();
if (target is not null)
{
_items.TryPickup(target.ItemInfo.Id, hand);
}
}
private void ProcessHandActionCompleted(Hand hand)
{
if (_dropActions.Remove(hand))
{
return;
}
_items.TryUse(new ItemUseRequest(_playerId, hand, ItemActionPhase.Completed));
}
private Vector3 CalculateDropPosition()
{
DropRaycaster.ForceRaycastUpdate();
bool isBlocked = DropRaycaster.IsColliding();
DropRayHit hit = new(
Camera.GlobalPosition,
-Camera.GlobalBasis.Z.Normalized(),
DropRaycaster.TargetPosition.Length(),
isBlocked,
isBlocked ? DropRaycaster.GetCollisionPoint() : default
);
return DropPosition.CalculateFrom(hit);
}
private void UpdatePrompt()
{
IPickupTarget pickupTarget = InteractionRaycaster.GetTargetOrNull<IPickupTarget>();
if (pickupTarget is not null)
{
ShowPickupPrompt(pickupTarget.ItemInfo);
return;
}
IInteractionTarget interactionTarget = InteractionRaycaster.GetTargetOrNull<IInteractionTarget>();
if (interactionTarget?.CanInteract == true)
{
PromptController.ShowPrompt($"[E] {interactionTarget.Prompt.Text}");
return;
}
PromptController.Clear();
}
private void ShowPickupPrompt(ItemInfo itemInfo)
{
PickupAvailability availability = _resolvePickupAvailability.Resolve(
itemInfo.HandRequirement,
_items.IsHandFree(Hand.Left),
_items.IsHandFree(Hand.Right)
);
if (availability.RequiresBoth)
{
PromptController.ShowPrompt(
availability.LeftHand
? $"[LPM lub PPM] Podnieś {itemInfo.DisplayName} — obie ręce"
: "Potrzebujesz obu wolnych rąk"
);
return;
}
List<string> actions = [];
if (availability.LeftHand)
{
actions.Add($"[LPM] Podnieś {itemInfo.DisplayName} — lewa ręka");
}
if (availability.RightHand)
{
actions.Add($"[PPM] Podnieś {itemInfo.DisplayName} — prawa ręka");
}
PromptController.ShowPrompt(
actions.Count > 0 ? string.Join("\n", actions) : "Brak wolnej ręki"
);
}
public void Initialize(
PlayerId playerId,
IItemInteractionService items,
IInteractionService interactions
IInteractionHandler interactions
)
{
_playerId = playerId;
@@ -41,9 +164,21 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint
_interactions = interactions;
}
public Vector3 GetItemSpawnPosition() => CalculateDropPosition();
public override void _Ready()
{
this.AssertRequiredExports();
}
public override void _Process(double delta)
{
if (_items is null || _interactions is null || GamePause.IsPaused || !MouseCapture.IsCaptured)
if (_items is null || _interactions is null)
{
throw new ControllerNotInitializedException(Name);
}
if (GamePause.IsPaused || !MouseCapture.IsCaptured)
{
PromptController.Clear();
return;
@@ -54,7 +189,12 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint
public override void _UnhandledInput(InputEvent @event)
{
if (_items is null || _interactions is null || GamePause.IsPaused || !MouseCapture.IsCaptured)
if (_items is null || _interactions is null)
{
throw new ControllerNotInitializedException(Name);
}
if (GamePause.IsPaused || !MouseCapture.IsCaptured)
{
return;
}
@@ -89,135 +229,15 @@ public partial class PlayerInteractionController : Node, IItemSpawnPoint
if (@event.IsActionPressed(InputAction.Interact.ActionName()))
{
IInteractionTarget target = InteractionRaycaster.GetTarget<IInteractionTarget>();
IInteractionTarget target = InteractionRaycaster.GetTargetOrNull<IInteractionTarget>();
if (target?.CanInteract == true)
{
_interactions.TryInteract(new InteractionRequest(_playerId, target.Id));
_interactions.TryInteract(
new InteractionRequest(_playerId, target.InteractionTargetId)
);
}
GetViewport().SetInputAsHandled();
}
}
public Vector3 GetItemSpawnPosition() => CalculateDropPosition();
private void ProcessHandActionStarted(Hand hand)
{
if (Input.IsActionPressed(InputAction.DropModifier.ActionName()))
{
if (_activeUseTargets.Remove(hand, out InteractionTargetId? activeTargetId))
{
_items.TryUse(new ItemUseRequest(
_playerId,
hand,
ItemActionPhase.Cancelled,
activeTargetId
));
}
_items.TryDrop(hand, CalculateDropPosition());
_dropActions.Add(hand);
return;
}
if (!_items.IsHandFree(hand))
{
ItemUseRequest request = CreateUseRequest(hand, ItemActionPhase.Started);
ItemActionResult result = _items.TryUse(request);
if (result.Success)
{
_activeUseTargets[hand] = request.TargetId;
}
return;
}
IPickupTarget target = InteractionRaycaster.GetTarget<IPickupTarget>();
if (target is not null)
{
_items.TryPickup(target.ItemInfo.Id, hand);
}
}
private void ProcessHandActionCompleted(Hand hand)
{
if (_dropActions.Remove(hand))
{
return;
}
if (_activeUseTargets.Remove(hand, out InteractionTargetId? targetId))
{
_items.TryUse(new ItemUseRequest(_playerId, hand, ItemActionPhase.Completed, targetId));
}
}
private ItemUseRequest CreateUseRequest(Hand hand, ItemActionPhase phase)
{
InteractionTargetId? targetId = InteractionRaycaster.GetTarget<IInteractionTarget>()?.Id;
return new ItemUseRequest(_playerId, hand, phase, targetId);
}
private Vector3 CalculateDropPosition()
{
DropRaycaster.ForceRaycastUpdate();
Vector3 origin = Camera.GlobalPosition;
Vector3 direction = -Camera.GlobalBasis.Z.Normalized();
float distance = DropRaycaster.TargetPosition.Length();
if (DropRaycaster.IsColliding())
{
distance = Mathf.Max(0.25f, origin.DistanceTo(DropRaycaster.GetCollisionPoint()) - 0.2f);
}
return origin + direction * distance;
}
private void UpdatePrompt()
{
IPickupTarget pickupTarget = InteractionRaycaster.GetTarget<IPickupTarget>();
if (pickupTarget is not null)
{
ShowPickupPrompt(pickupTarget.ItemInfo);
return;
}
IInteractionTarget interactionTarget = InteractionRaycaster.GetTarget<IInteractionTarget>();
if (interactionTarget?.CanInteract == true)
{
PromptController.ShowPrompt($"[E] {interactionTarget.Prompt.Text}");
return;
}
PromptController.Clear();
}
private void ShowPickupPrompt(ItemInfo itemInfo)
{
if (itemInfo.HandRequirement == HandRequirement.Both)
{
bool bothHandsFree = _items.IsHandFree(Hand.Left) && _items.IsHandFree(Hand.Right);
PromptController.ShowPrompt(bothHandsFree
? $"[LPM lub PPM] Podnieś {itemInfo.DisplayName} — obie ręce"
: "Potrzebujesz obu wolnych rąk");
return;
}
List<string> actions = [];
if (_items.IsHandFree(Hand.Left))
{
actions.Add($"[LPM] Podnieś {itemInfo.DisplayName} — lewa ręka");
}
if (_items.IsHandFree(Hand.Right))
{
actions.Add($"[PPM] Podnieś {itemInfo.DisplayName} — prawa ręka");
}
PromptController.ShowPrompt(actions.Count > 0
? string.Join("\n", actions)
: "Brak wolnej ręki");
}
}
@@ -1,52 +0,0 @@
using System;
using Game.Features.Interactions.Contracts;
using Game.Features.Players.Contracts;
using Godot;
namespace Game.Features.Interactions.Godot;
public abstract partial class WorldInteractionController : Node3D, IInteractionTarget
{
public const string InteractionGroup = "world_interactions";
[Export] public string InstanceId;
[Export] public string PromptText = "Wejdź w interakcję";
public InteractionTargetId Id { get; private set; }
public virtual bool CanInteract => true;
public InteractionPrompt Prompt => new(PromptText);
private InteractionWorldController _registry;
public override void _Ready()
{
Guid id = string.IsNullOrWhiteSpace(InstanceId) ? Guid.NewGuid() : Guid.Parse(InstanceId);
Id = new InteractionTargetId(id);
AddToGroup(InteractionGroup);
RegisterWithAvailableWorld();
}
public override void _ExitTree()
{
_registry?.Unregister(Id);
}
public void RegisterWith(InteractionWorldController registry)
{
_registry = registry;
}
public abstract InteractionResult Execute(PlayerId playerId);
private void RegisterWithAvailableWorld()
{
foreach (Node node in GetTree().GetNodesInGroup(InteractionWorldController.RegistryGroup))
{
if (node is InteractionWorldController registry)
{
registry.Register(this);
return;
}
}
}
}
@@ -1 +0,0 @@
uid://d2r8m5v1q9k4x
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using Game.Features.Interactions.Contracts;
namespace Game.Features.Interactions.Infrastructure;
public sealed class InteractionRegistry : IInteractionRegistry
{
private readonly Dictionary<InteractionTargetId, IInteractionTarget> _interactionTargetsById =
[];
public void Register(IInteractionTarget target)
{
if (
_interactionTargetsById.TryGetValue(
target.InteractionTargetId,
out IInteractionTarget existing
)
)
{
if (ReferenceEquals(existing, target))
{
return;
}
throw new InvalidOperationException(
$"Duplicate interaction target id: {target.InteractionTargetId}."
);
}
_interactionTargetsById.Add(target.InteractionTargetId, target);
}
public void Unregister(InteractionTargetId targetId)
{
_interactionTargetsById.Remove(targetId);
}
public bool TryGetTarget(InteractionTargetId targetId, out IInteractionTarget target) =>
_interactionTargetsById.TryGetValue(targetId, out target);
}
@@ -0,0 +1 @@
uid://dtnwqalxdu6n2
@@ -1,3 +1,5 @@
using System.Collections.Generic;
using Game.Features.Interactions.Contracts;
using Game.Features.Items.Contracts;
using Game.Features.Items.Domain;
using Godot;
@@ -6,17 +8,19 @@ namespace Game.Features.Items.Application;
public sealed class ItemInteractionService(
HandsInventory hands,
IItemRepository itemRepository,
IItemInstanceRegistry itemRegistry,
IHeldItemPresenter presenter
) : IItemInteractionService
{
private readonly Dictionary<Hand, InteractionTargetId?> _activeUseTargets = [];
public bool IsHandFree(Hand hand) => hands.IsFree(hand);
public ItemInstanceId? GetHeldItem(Hand hand) => hands.Get(hand);
public ItemActionResult TryPickup(ItemInstanceId itemId, Hand hand)
{
if (!itemRepository.TryGet(itemId, out IItemInstance item))
if (!itemRegistry.TryGet(itemId, out IItemInstance item))
{
return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found.");
}
@@ -36,6 +40,27 @@ public sealed class ItemInteractionService(
}
public ItemActionResult TryUse(ItemUseRequest request)
{
if (request.Phase == ItemActionPhase.Started)
{
ItemActionResult startResult = ExecuteUse(request);
if (startResult.Success)
{
_activeUseTargets[request.Hand] = request.TargetId;
}
return startResult;
}
if (!_activeUseTargets.Remove(request.Hand, out InteractionTargetId? activeTargetId))
{
return ItemActionResult.Ok();
}
return ExecuteUse(request with { TargetId = activeTargetId });
}
private ItemActionResult ExecuteUse(ItemUseRequest request)
{
ItemInstanceId? itemId = hands.Get(request.Hand);
if (itemId is null)
@@ -43,7 +68,7 @@ public sealed class ItemInteractionService(
return ItemActionResult.Fail(ItemActionStatus.HandEmpty, "The requested hand is empty.");
}
if (!itemRepository.TryGet(itemId.Value, out IItemInstance item))
if (!itemRegistry.TryGet(itemId.Value, out IItemInstance item))
{
return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found.");
}
@@ -65,7 +90,7 @@ public sealed class ItemInteractionService(
return ItemActionResult.Fail(ItemActionStatus.HandEmpty, "The requested hand is empty.");
}
if (!itemRepository.TryGet(itemId.Value, out IItemInstance item))
if (!itemRegistry.TryGet(itemId.Value, out IItemInstance item))
{
return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found.");
}
@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Game.Features.Items.Contracts;
public interface IItemDefinitionRegistry
{
IReadOnlyCollection<ItemDefinitionId> DefinitionIds { get; }
bool TryRegister(ItemDefinitionId definitionId, ItemDefinition definition);
bool TryGetDefinition(ItemDefinitionId definitionId, out ItemDefinition definition);
}
@@ -0,0 +1 @@
uid://kjka28plyqeo
@@ -0,0 +1,8 @@
namespace Game.Features.Items.Contracts;
public interface IItemInstanceRegistry
{
void Register(ItemInstanceId itemId, IItemInstance item);
void Unregister(ItemInstanceId itemId);
bool TryGet(ItemInstanceId itemId, out IItemInstance item);
}
@@ -0,0 +1 @@
uid://s83yytevpj52
@@ -1,6 +0,0 @@
namespace Game.Features.Items.Contracts;
public interface IItemRepository
{
bool TryGet(ItemInstanceId itemId, out IItemInstance item);
}
@@ -1 +0,0 @@
uid://e7n1m6v4k9r3q
+5 -3
View File
@@ -1,10 +1,12 @@
using System.Collections.Generic;
using Godot;
namespace Game.Features.Items.Contracts;
public interface IItemSpawner
{
IReadOnlyCollection<ItemDefinitionId> DefinitionIds { get; }
ItemActionResult TrySpawn(ItemDefinitionId definitionId, Vector3 worldPosition, out ItemInstanceId itemId);
ItemActionResult TrySpawn(
ItemDefinitionId definitionId,
Vector3 worldPosition,
out ItemInstanceId itemId
);
}
@@ -1,8 +1,10 @@
using System;
using Game.Shared.Domain;
namespace Game.Features.Items.Contracts;
public readonly record struct ItemInstanceId(Guid Value)
public readonly record struct ItemInstanceId(InstanceId Value)
{
public static ItemInstanceId From(InstanceId value) => new(value);
public override string ToString() => Value.ToString();
}
@@ -5,7 +5,7 @@ using Godot;
namespace Game.Features.Items.Godot.Flashlight;
public partial class FlashlightItemController : WorldItemController
public partial class FlashlightItemController : ItemController
{
[Export] public SpotLight3D WorldLight;
@@ -3,15 +3,15 @@ using Godot;
namespace Game.Features.Items.Godot;
public sealed class PlayerHeldItemPresenter(
ItemWorldController itemWorld,
public sealed class HeldItemPresenter(
ItemControllerRegistryController itemWorld,
Node3D leftHandAnchor,
Node3D rightHandAnchor
) : IHeldItemPresenter
{
public bool TryShowHeld(IItemInstance item, Hand hand)
{
if (!itemWorld.TryGetController(item.Info.Id, out WorldItemController controller))
if (!itemWorld.TryGetController(item.Info.Id, out ItemController controller))
{
return false;
}
@@ -23,7 +23,7 @@ public sealed class PlayerHeldItemPresenter(
public bool TryShowDropped(IItemInstance item, Vector3 worldPosition)
{
if (!itemWorld.TryGetController(item.Info.Id, out WorldItemController controller))
if (!itemWorld.TryGetController(item.Info.Id, out ItemController controller))
{
return false;
}
@@ -34,7 +34,7 @@ public sealed class PlayerHeldItemPresenter(
public void Refresh(IItemInstance item)
{
if (itemWorld.TryGetController(item.Info.Id, out WorldItemController controller))
if (itemWorld.TryGetController(item.Info.Id, out ItemController controller))
{
controller.ApplyState(item);
}
@@ -6,17 +6,30 @@ using Godot;
namespace Game.Features.Items.Godot;
public partial class WorldItemController : RigidBody3D, IPickupTarget
public partial class ItemController : 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;
[Export]
public string InstanceKey;
[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;
@@ -25,7 +38,7 @@ public partial class WorldItemController : RigidBody3D, IPickupTarget
public ItemDefinition Definition { get; private set; }
public IItemInstance Item { get; private set; }
private ItemWorldController _registry;
private ItemControllerRegistryController _registry;
public override void _Ready()
{
@@ -41,20 +54,19 @@ public partial class WorldItemController : RigidBody3D, IPickupTarget
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);
Definition = new ItemDefinition(
new ItemDefinitionId(DefinitionId),
DisplayName,
HandRequirement
);
ItemInfo = new ItemInfo(
new ItemInstanceId(id),
ItemInstanceId.From(this.GetStableInstanceIdFrom(InstanceKey)),
Definition.Id,
Definition.DisplayName,
Definition.HandRequirement
);
Item = CreateItemInstance(ItemInfo);
AddToGroup(ItemGroup);
RegisterWithAvailableWorld();
ApplyState(Item);
}
@@ -63,7 +75,7 @@ public partial class WorldItemController : RigidBody3D, IPickupTarget
_registry?.Unregister(ItemInfo.Id);
}
public void RegisterWith(ItemWorldController registry)
public void RegisterWith(ItemControllerRegistryController registry)
{
_registry = registry;
}
@@ -113,29 +125,12 @@ public partial class WorldItemController : RigidBody3D, IPickupTarget
OnPresentationChanged(item);
}
protected virtual IItemInstance CreateItemInstance(ItemInfo itemInfo) => new PassiveItemInstance(itemInfo);
protected virtual IItemInstance CreateItemInstance(ItemInfo itemInfo) =>
new PassiveItemInstance(itemInfo);
protected virtual void OnHeldViewCreated(Node3D heldView)
{
}
protected virtual void OnHeldViewCreated(Node3D heldView) { }
protected virtual void OnHeldViewRemoving(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;
}
}
}
protected virtual void OnPresentationChanged(IItemInstance item) { }
}
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using Game.Features.Items.Contracts;
using Godot;
namespace Game.Features.Items.Godot;
public partial class ItemControllerRegistryController : Node
{
private IItemInstanceRegistry _registry;
private readonly Dictionary<ItemInstanceId, ItemController> _items = [];
public IReadOnlyCollection<ItemController> Items => _items.Values;
public void UseRegistry(IItemInstanceRegistry registry)
{
_registry = registry;
}
public void RegisterSceneItems()
{
foreach (Node node in GetTree().GetNodesInGroup(ItemController.ItemGroup))
{
if (node is ItemController item)
{
Register(item);
}
}
}
public void Register(ItemController item)
{
if (_items.TryGetValue(item.ItemInfo.Id, out ItemController existing))
{
if (ReferenceEquals(existing, item))
{
return;
}
throw new InvalidOperationException($"Duplicate item instance id: {item.ItemInfo.Id}.");
}
_items.Add(item.ItemInfo.Id, item);
_registry.Register(item.ItemInfo.Id, item.Item);
item.RegisterWith(this);
}
public void Unregister(ItemInstanceId itemId)
{
_items.Remove(itemId);
_registry.Unregister(itemId);
}
public bool TryGetController(ItemInstanceId itemId, out ItemController controller)
{
return _items.TryGetValue(itemId, out controller);
}
}
@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using Game.Features.Items.Contracts;
using Game.Godot.Extensions;
using Godot;
namespace Game.Features.Items.Godot;
public partial class ItemSceneRegistryController : Node
{
[Export]
public ItemControllerRegistryController ItemWorld;
[Export]
public global::Godot.Collections.Array<PackedScene> ItemScenes = [];
private readonly Dictionary<ItemDefinitionId, PackedScene> _scenes = [];
private IItemDefinitionRegistry _definitions;
public void UseRegistry(IItemDefinitionRegistry registry)
{
_definitions = registry;
}
public override void _Ready()
{
this.AssertRequiredExports();
}
public void Initialize()
{
_scenes.Clear();
foreach (PackedScene scene in ItemScenes)
{
RegisterItem(scene, throwOnDuplicate: true);
}
foreach (ItemController item in ItemWorld.Items)
{
string scenePath = item.SceneFilePath;
if (string.IsNullOrWhiteSpace(scenePath))
{
continue;
}
RegisterItem(ResourceLoader.Load<PackedScene>(scenePath), throwOnDuplicate: false);
}
if (_scenes.Count == 0)
{
throw new InvalidOperationException("The item spawn catalog is empty.");
}
foreach (ItemController item in ItemWorld.Items)
{
ValidateAgainstCatalog(item);
}
}
public bool TryGetScene(ItemDefinitionId definitionId, out PackedScene scene)
{
return _scenes.TryGetValue(definitionId, out scene);
}
private void RegisterItem(PackedScene itemScene, bool throwOnDuplicate)
{
if (itemScene is null)
{
return;
}
ItemController prototype = itemScene.Instantiate<ItemController>();
ItemDefinitionId definitionId = new(prototype.DefinitionId);
ItemDefinition definition = new(
definitionId,
prototype.DisplayName,
prototype.HandRequirement
);
prototype.Free();
if (!_definitions.TryRegister(definitionId, definition))
{
if (throwOnDuplicate)
{
throw new InvalidOperationException(
$"Duplicate item spawn definition: {definitionId}."
);
}
return;
}
_scenes.Add(definitionId, itemScene);
}
private void ValidateAgainstCatalog(ItemController item)
{
if (!_definitions.TryGetDefinition(item.Definition.Id, out ItemDefinition canonical))
{
return;
}
if (!item.Definition.Equals(canonical))
{
throw new InvalidOperationException(
$"{item.Name}: item definition drifted from the catalog for '{item.Definition.Id}'."
);
}
}
}
@@ -0,0 +1 @@
uid://cft7r2j1f0sav
@@ -1,6 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Game.Features.Items.Contracts;
using Game.Godot.Extensions;
using Godot;
@@ -9,57 +6,29 @@ namespace Game.Features.Items.Godot;
public partial class ItemSpawnerController : Node, IItemSpawner
{
[Export] public Node3D ItemsRoot;
[Export] public ItemWorldController ItemWorld;
[Export] public global::Godot.Collections.Array<PackedScene> ItemScenes = [];
[Export]
public Node3D ItemsRoot;
private readonly Dictionary<ItemDefinitionId, PackedScene> _scenes = [];
[Export]
public ItemControllerRegistryController ItemWorld;
public IReadOnlyCollection<ItemDefinitionId> DefinitionIds => _scenes.Keys.ToArray();
[Export]
public ItemSceneRegistryController Scenes;
public override void _Ready()
{
this.AssertRequiredExports();
}
public void Initialize()
{
_scenes.Clear();
foreach (PackedScene scene in ItemScenes)
{
RegisterScene(scene);
}
foreach (WorldItemController item in ItemWorld.Items)
{
string scenePath = item.SceneFilePath;
if (string.IsNullOrWhiteSpace(scenePath))
{
continue;
}
PackedScene scene = ResourceLoader.Load<PackedScene>(scenePath);
if (scene is not null)
{
_scenes.TryAdd(item.ItemInfo.DefinitionId, scene);
}
}
if (_scenes.Count == 0)
{
throw new InvalidOperationException("The item spawn catalog is empty.");
}
}
public ItemActionResult TrySpawn(
ItemDefinitionId definitionId,
Vector3 worldPosition,
out ItemInstanceId itemId
) {
)
{
itemId = default;
if (!_scenes.TryGetValue(definitionId, out PackedScene scene))
if (!Scenes.TryGetScene(definitionId, out PackedScene scene))
{
return ItemActionResult.Fail(
ItemActionStatus.UnknownDefinition,
@@ -67,29 +36,11 @@ public partial class ItemSpawnerController : Node, IItemSpawner
);
}
WorldItemController item = scene.Instantiate<WorldItemController>();
item.InstanceId = Guid.NewGuid().ToString();
ItemController item = scene.Instantiate<ItemController>();
ItemsRoot.AddChild(item);
item.GlobalPosition = worldPosition;
ItemWorld.Register(item);
itemId = item.ItemInfo.Id;
return ItemActionResult.Ok();
}
private void RegisterScene(PackedScene scene)
{
if (scene is null)
{
return;
}
WorldItemController prototype = scene.Instantiate<WorldItemController>();
ItemDefinitionId definitionId = new(prototype.DefinitionId);
prototype.Free();
if (!_scenes.TryAdd(definitionId, scene))
{
throw new InvalidOperationException($"Duplicate item spawn definition: {definitionId}.");
}
}
}
@@ -1,69 +0,0 @@
using System;
using System.Collections.Generic;
using Game.Features.Items.Contracts;
using Godot;
namespace Game.Features.Items.Godot;
public partial class ItemWorldController : Node, IItemRepository
{
public const string RegistryGroup = "item_world_registry";
private readonly Dictionary<ItemInstanceId, WorldItemController> _items = [];
public IReadOnlyCollection<WorldItemController> Items => _items.Values;
public override void _EnterTree()
{
AddToGroup(RegistryGroup);
}
public void RegisterSceneItems()
{
foreach (Node node in GetTree().GetNodesInGroup(WorldItemController.ItemGroup))
{
if (node is WorldItemController item)
{
Register(item);
}
}
}
public void Register(WorldItemController item)
{
if (_items.TryGetValue(item.ItemInfo.Id, out WorldItemController existing))
{
if (ReferenceEquals(existing, item))
{
return;
}
throw new InvalidOperationException($"Duplicate item instance id: {item.ItemInfo.Id}.");
}
_items.Add(item.ItemInfo.Id, item);
item.RegisterWith(this);
}
public void Unregister(ItemInstanceId itemId)
{
_items.Remove(itemId);
}
public bool TryGet(ItemInstanceId itemId, out IItemInstance item)
{
if (_items.TryGetValue(itemId, out WorldItemController controller))
{
item = controller.Item;
return true;
}
item = null;
return false;
}
public bool TryGetController(ItemInstanceId itemId, out WorldItemController controller)
{
return _items.TryGetValue(itemId, out controller);
}
}
@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Linq;
using Game.Features.Items.Contracts;
namespace Game.Features.Items.Infrastructure;
public sealed class ItemDefinitionRegistry : IItemDefinitionRegistry
{
private readonly Dictionary<ItemDefinitionId, ItemDefinition> _definitionsById = [];
public IReadOnlyCollection<ItemDefinitionId> DefinitionIds => _definitionsById.Keys.ToArray();
public bool TryRegister(ItemDefinitionId definitionId, ItemDefinition definition) =>
_definitionsById.TryAdd(definitionId, definition);
public bool TryGetDefinition(ItemDefinitionId definitionId, out ItemDefinition definition) =>
_definitionsById.TryGetValue(definitionId, out definition);
}
@@ -0,0 +1 @@
uid://72g8eg0lvlui
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using Game.Features.Items.Contracts;
namespace Game.Features.Items.Infrastructure;
public sealed class ItemInstanceRegistry : IItemInstanceRegistry
{
private readonly Dictionary<ItemInstanceId, IItemInstance> _itemsById = [];
public void Register(ItemInstanceId itemId, IItemInstance item)
{
if (_itemsById.TryGetValue(itemId, out IItemInstance existing))
{
if (ReferenceEquals(existing, item))
{
return;
}
throw new InvalidOperationException($"Duplicate item instance id: {itemId}.");
}
_itemsById.Add(itemId, item);
}
public void Unregister(ItemInstanceId itemId)
{
_itemsById.Remove(itemId);
}
public bool TryGet(ItemInstanceId itemId, out IItemInstance item) =>
_itemsById.TryGetValue(itemId, out item);
}
@@ -0,0 +1 @@
uid://dfbylp383il2v
@@ -0,0 +1,15 @@
using System;
namespace Game.Godot.Exceptions;
/// <summary>
/// Thrown when a controller is used before it has been initialized.
/// </summary>
public sealed class ControllerNotInitializedException(string nodeName)
: Exception(BuildMessage(nodeName))
{
public string NodeName { get; } = nodeName;
private static string BuildMessage(string nodeName)
=> $"{nodeName}: controller was used before it was initialized.";
}
@@ -0,0 +1 @@
uid://dj8kxrwjoyfvi
@@ -0,0 +1,15 @@
using Game.Shared.Domain;
using Godot;
namespace Game.Godot.Extensions;
public static class NodeInstanceIdExtensions
{
private static string GetKeyFrom(Node node) => node.GetPath().ToString();
public static InstanceId GetStableInstanceId(this Node node) =>
InstanceId.FromKey(GetKeyFrom(node));
public static InstanceId GetStableInstanceIdFrom(this Node node, string key) =>
string.IsNullOrWhiteSpace(key) ? node.GetStableInstanceId() : InstanceId.FromKey(key);
}
@@ -0,0 +1 @@
uid://csng1pjcgsh80
+53 -19
View File
@@ -2,13 +2,15 @@ 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.Interactions.Infrastructure;
using Game.Features.Items.Application;
using Game.Features.Items.Domain;
using Game.Features.Items.Godot;
using Game.Features.Items.Infrastructure;
using Game.Features.Logging.Godot;
using Game.Features.Logging.Infrastructure;
using Game.Features.Multiplayer.Godot;
using Game.Features.Players.Contracts;
using Game.Features.Players.Godot;
@@ -20,16 +22,33 @@ namespace Game.Godot;
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;
[Export]
public ConsoleController Console;
[Export]
public PlayerController Player;
[Export]
public PlayerInteractionController PlayerInteraction;
[Export]
public ItemControllerRegistryController ItemWorld;
[Export]
public ItemSceneRegistryController ItemScenes;
[Export]
public ItemSpawnerController ItemSpawner;
[Export]
public InteractionsRegistryController InteractionWorld;
[Export]
public MultiplayerController MultiplayerController;
private FileLogger _fileLogger;
private CompositeLogger _logger;
private ItemDefinitionRegistry _itemDefinitions;
private void InitializeLogging()
{
@@ -37,7 +56,7 @@ public partial class GameBootstrap : Node
_logger = new CompositeLogger([
new GodotConsoleLogger(),
new LogTargetLogger(Console),
_fileLogger
_fileLogger,
]);
}
@@ -48,7 +67,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 SpawnItemCommand(ItemSpawner, _itemDefinitions, PlayerInteraction));
registry.Register(new HostCommand(MultiplayerController));
registry.Register(new JoinCommand(MultiplayerController));
@@ -58,26 +77,41 @@ public partial class GameBootstrap : Node
private void InitializeMultiplayer()
{
MultiplayerController.UsePeerFactory(new EnetMultiplayerPeerFactory());
MultiplayerController.Connected += () => _logger.Info("Multiplayer", "Connected successfully.");
MultiplayerController.ConnectionFailed += () => _logger.Warning("Multiplayer", "Connection failed.");
MultiplayerController.Disconnected += () => _logger.Warning("Multiplayer", "Server disconnected.");
MultiplayerController.Connected += () =>
_logger.Info("Multiplayer", "Connected successfully.");
MultiplayerController.ConnectionFailed += () =>
_logger.Warning("Multiplayer", "Connection failed.");
MultiplayerController.Disconnected += () =>
_logger.Warning("Multiplayer", "Server disconnected.");
MultiplayerController.PeerJoined += id => _logger.Info("Multiplayer", $"Peer {id} joined.");
MultiplayerController.PeerLeft += id => _logger.Info("Multiplayer", $"Peer {id} left.");
}
private void InitializeItems()
{
ItemInstanceRegistry itemInstanceRegistry = new();
ItemWorld.UseRegistry(itemInstanceRegistry);
ItemWorld.RegisterSceneItems();
ItemSpawner.Initialize();
_itemDefinitions = new ItemDefinitionRegistry();
ItemScenes.UseRegistry(_itemDefinitions);
ItemScenes.Initialize();
InteractionRegistry interactionRegistry = new();
InteractionWorld.UseRegistry(interactionRegistry);
InteractionWorld.RegisterSceneTargets();
PlayerHeldItemPresenter presenter = new(
HeldItemPresenter presenter = new(
ItemWorld,
PlayerInteraction.LeftHandAnchor,
PlayerInteraction.RightHandAnchor
);
ItemInteractionService itemInteractions = new(new HandsInventory(), ItemWorld, presenter);
InteractionService interactions = new(InteractionWorld);
ItemInteractionService itemInteractions = new(
new HandsInventory(),
itemInstanceRegistry,
presenter
);
Interact interactions = new(interactionRegistry);
PlayerInteraction.Initialize(new PlayerId(Guid.NewGuid()), itemInteractions, interactions);
}
@@ -86,7 +120,7 @@ public partial class GameBootstrap : Node
this.AssertRequiredExports();
MouseCapture.Capture();
InitializeLogging();
InitializeItems();
InitializeMultiplayer();
+12 -7
View File
@@ -6,10 +6,11 @@
[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="Script" uid="uid://f8n3b6w1k5r9m" path="res://src/Features/Items/Godot/ItemControllerRegistryController.cs" id="6_items"]
[ext_resource type="PackedScene" uid="uid://cv8m3q1y6r4nf" path="res://src/Features/Items/Godot/Flashlight/FlashlightItem.tscn" id="7_flashlight"]
[ext_resource type="Script" uid="uid://c3x9m6w2q8r1b" path="res://src/Features/Items/Godot/ItemSpawnerController.cs" id="8_spawner"]
[ext_resource type="Script" uid="uid://f6q1c9m4w7v2n" path="res://src/Features/Interactions/Godot/InteractionWorldController.cs" id="9_interactions"]
[ext_resource type="Script" path="res://src/Features/Items/Godot/ItemSceneRegistryController.cs" id="8b_catalog"]
[ext_resource type="Script" uid="uid://f6q1c9m4w7v2n" path="res://src/Features/Interactions/Godot/InteractionsRegistryController.cs" id="9_interactions"]
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_epykj"]
panorama = ExtResource("1_g4n7o")
@@ -22,12 +23,13 @@ background_mode = 2
sky = SubResource("Sky_1kpcd")
glow_enabled = true
[node name="GameWorld" type="Node" unique_id=222644234 node_paths=PackedStringArray("Console", "Player", "PlayerInteraction", "ItemWorld", "ItemSpawner", "InteractionWorld", "MultiplayerController")]
[node name="GameWorld" type="Node" unique_id=222644234 node_paths=PackedStringArray("Console", "Player", "PlayerInteraction", "ItemWorld", "ItemScenes", "ItemSpawner", "InteractionWorld", "MultiplayerController")]
script = ExtResource("3_bootstrap")
Console = NodePath("Console")
Player = NodePath("Player")
PlayerInteraction = NodePath("Player/PlayerInteraction")
ItemWorld = NodePath("ItemWorld")
ItemScenes = NodePath("ItemScenesRegistry")
ItemSpawner = NodePath("ItemSpawner")
InteractionWorld = NodePath("InteractionWorld")
MultiplayerController = NodePath("Multiplayer")
@@ -50,11 +52,16 @@ 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")]
[node name="ItemScenesRegistry" type="Node" parent="." node_paths=PackedStringArray("ItemWorld")]
script = ExtResource("8b_catalog")
ItemWorld = NodePath("../ItemWorld")
ItemScenes = Array[PackedScene]([ExtResource("7_flashlight")])
[node name="ItemSpawner" type="Node" parent="." node_paths=PackedStringArray("ItemsRoot", "ItemWorld", "Scenes")]
script = ExtResource("8_spawner")
ItemsRoot = NodePath("../Entities")
ItemWorld = NodePath("../ItemWorld")
ItemScenes = Array[PackedScene]([ExtResource("7_flashlight")])
Scenes = NodePath("../ItemScenesRegistry")
[node name="InteractionWorld" type="Node" parent="."]
script = ExtResource("9_interactions")
@@ -89,8 +96,6 @@ 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"
+15
View File
@@ -0,0 +1,15 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace Game.Shared.Domain;
public readonly record struct InstanceId(Guid Value)
{
private static Guid CreateDeterministicId(string source) =>
new(MD5.HashData(Encoding.UTF8.GetBytes(source)));
public static InstanceId FromKey(string key) => new(CreateDeterministicId(key));
public override string ToString() => Value.ToString();
}
+1
View File
@@ -0,0 +1 @@
uid://cpikg7xirrc1g