feat: add extensible item inventory and world system
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Game.Features.Items.Domain;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Application;
|
||||||
|
|
||||||
|
public sealed class ItemInteractionService(
|
||||||
|
HandsInventory hands,
|
||||||
|
IItemRepository itemRepository,
|
||||||
|
IHeldItemPresenter presenter
|
||||||
|
) : IItemInteractionService
|
||||||
|
{
|
||||||
|
public bool IsHandFree(Hand hand) => hands.IsFree(hand);
|
||||||
|
|
||||||
|
public ItemInstanceId? GetHeldItem(Hand hand) => hands.Get(hand);
|
||||||
|
|
||||||
|
public ItemActionResult TryPickup(ItemInstanceId itemId, Hand hand)
|
||||||
|
{
|
||||||
|
if (!itemRepository.TryGet(itemId, out IItemInstance item))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hands.TryHold(itemId, item.Info.HandRequirement, hand))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.HandOccupied, "The requested hand is not available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presenter.TryShowHeld(item, hand))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
hands.Release(hand);
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.PresentationFailed, "Could not present the held item.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemActionResult TryUse(ItemUseRequest request)
|
||||||
|
{
|
||||||
|
ItemInstanceId? itemId = hands.Get(request.Hand);
|
||||||
|
if (itemId is null)
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.HandEmpty, "The requested hand is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemRepository.TryGet(itemId.Value, out IItemInstance item))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemActionResult result = item.Use(request);
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
presenter.Refresh(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemActionResult TryDrop(Hand hand, Vector3 worldPosition)
|
||||||
|
{
|
||||||
|
ItemInstanceId? itemId = hands.Get(hand);
|
||||||
|
if (itemId is null)
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.HandEmpty, "The requested hand is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemRepository.TryGet(itemId.Value, out IItemInstance item))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.ItemNotFound, $"Item {itemId} was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
hands.Release(hand);
|
||||||
|
|
||||||
|
if (presenter.TryShowDropped(item, worldPosition))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
hands.TryHold(itemId.Value, item.Info.HandRequirement, hand);
|
||||||
|
return ItemActionResult.Fail(ItemActionStatus.PresentationFailed, "Could not present the dropped item.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://e6q2h9v5b8k3p
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public enum Hand
|
||||||
|
{
|
||||||
|
Left,
|
||||||
|
Right
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c4n7h2w8m5q1p
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public enum HandRequirement
|
||||||
|
{
|
||||||
|
One,
|
||||||
|
Both
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d8p3k6v1r9y4b
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IHeldItemPresenter
|
||||||
|
{
|
||||||
|
bool TryShowHeld(IItemInstance item, Hand hand);
|
||||||
|
bool TryShowDropped(IItemInstance item, Vector3 worldPosition);
|
||||||
|
void Refresh(IItemInstance item);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c1q7y4d9m5x2v
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IItemInstance
|
||||||
|
{
|
||||||
|
ItemInfo Info { get; }
|
||||||
|
ItemActionResult Use(ItemUseRequest request);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b5x9q3h7m2w8f
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IItemInteractionService
|
||||||
|
{
|
||||||
|
bool IsHandFree(Hand hand);
|
||||||
|
ItemInstanceId? GetHeldItem(Hand hand);
|
||||||
|
ItemActionResult TryPickup(ItemInstanceId itemId, Hand hand);
|
||||||
|
ItemActionResult TryUse(ItemUseRequest request);
|
||||||
|
ItemActionResult TryDrop(Hand hand, Vector3 worldPosition);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://f2k8q5b1w7m4n
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IItemRepository
|
||||||
|
{
|
||||||
|
bool TryGet(ItemInstanceId itemId, out IItemInstance item);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://e7n1m6v4k9r3q
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IItemSpawnPoint
|
||||||
|
{
|
||||||
|
Vector3 GetItemSpawnPosition();
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://a2v6r8m3q7k5d
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IItemSpawner
|
||||||
|
{
|
||||||
|
IReadOnlyCollection<ItemDefinitionId> DefinitionIds { get; }
|
||||||
|
ItemActionResult TrySpawn(ItemDefinitionId definitionId, Vector3 worldPosition, out ItemInstanceId itemId);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://f4m9k2w6b8n1r
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public interface IPickupTarget
|
||||||
|
{
|
||||||
|
ItemInfo ItemInfo { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c9v4g7p2m5x8d
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public enum ItemActionPhase
|
||||||
|
{
|
||||||
|
Started,
|
||||||
|
Completed,
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c9w5d2n8q4m1h
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemActionResult(ItemActionStatus Status, string Message = "")
|
||||||
|
{
|
||||||
|
public bool Success => Status == ItemActionStatus.Success;
|
||||||
|
|
||||||
|
public static ItemActionResult Ok() => new(ItemActionStatus.Success);
|
||||||
|
|
||||||
|
public static ItemActionResult Fail(ItemActionStatus status, string message) => new(status, message);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://a8m4p1x7d3q6k
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public enum ItemActionStatus
|
||||||
|
{
|
||||||
|
Success,
|
||||||
|
ItemNotFound,
|
||||||
|
HandOccupied,
|
||||||
|
HandEmpty,
|
||||||
|
UnsupportedAction,
|
||||||
|
PresentationFailed,
|
||||||
|
UnknownDefinition
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://f1r6y3b9k5v2m
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemDefinition(
|
||||||
|
ItemDefinitionId Id,
|
||||||
|
string DisplayName,
|
||||||
|
HandRequirement HandRequirement
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://e3v8k4r1m7x2p
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemDefinitionId(string Value)
|
||||||
|
{
|
||||||
|
public override string ToString() => Value;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b7q2n9c5w1m6y
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemInfo(
|
||||||
|
ItemInstanceId Id,
|
||||||
|
ItemDefinitionId DefinitionId,
|
||||||
|
string DisplayName,
|
||||||
|
HandRequirement HandRequirement
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://e1r5j8w3n6c9h
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemInstanceId(Guid Value)
|
||||||
|
{
|
||||||
|
public override string ToString() => Value.ToString();
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b6x2f9m4t7k3q
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Game.Features.Interactions.Contracts;
|
||||||
|
using Game.Features.Players.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
public readonly record struct ItemUseRequest(
|
||||||
|
PlayerId PlayerId,
|
||||||
|
Hand Hand,
|
||||||
|
ItemActionPhase Phase,
|
||||||
|
InteractionTargetId? TargetId = null
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d6k2v8m5r1c9n
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Domain;
|
||||||
|
|
||||||
|
public sealed class HandsInventory
|
||||||
|
{
|
||||||
|
private ItemInstanceId? _leftHand;
|
||||||
|
private ItemInstanceId? _rightHand;
|
||||||
|
|
||||||
|
public bool IsFree(Hand hand) => Get(hand) is null;
|
||||||
|
|
||||||
|
public ItemInstanceId? Get(Hand hand) => hand switch
|
||||||
|
{
|
||||||
|
Hand.Left => _leftHand,
|
||||||
|
Hand.Right => _rightHand,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(hand), hand, null)
|
||||||
|
};
|
||||||
|
|
||||||
|
public bool TryHold(ItemInstanceId itemId, HandRequirement requirement, Hand hand)
|
||||||
|
{
|
||||||
|
if (_leftHand == itemId || _rightHand == itemId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requirement == HandRequirement.Both)
|
||||||
|
{
|
||||||
|
if (_leftHand is not null || _rightHand is not null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_leftHand = itemId;
|
||||||
|
_rightHand = itemId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsFree(hand))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set(hand, itemId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemInstanceId? Release(Hand hand)
|
||||||
|
{
|
||||||
|
ItemInstanceId? itemId = Get(hand);
|
||||||
|
if (itemId is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_leftHand == itemId)
|
||||||
|
{
|
||||||
|
_leftHand = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_rightHand == itemId)
|
||||||
|
{
|
||||||
|
_rightHand = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Set(Hand hand, ItemInstanceId itemId)
|
||||||
|
{
|
||||||
|
switch (hand)
|
||||||
|
{
|
||||||
|
case Hand.Left:
|
||||||
|
_leftHand = itemId;
|
||||||
|
break;
|
||||||
|
case Hand.Right:
|
||||||
|
_rightHand = itemId;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(hand), hand, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d5w1n8c4q7m2y
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Domain;
|
||||||
|
|
||||||
|
public sealed class PassiveItemInstance(ItemInfo info) : IItemInstance
|
||||||
|
{
|
||||||
|
public ItemInfo Info { get; } = info;
|
||||||
|
|
||||||
|
public ItemActionResult Use(ItemUseRequest request) => ItemActionResult.Fail(
|
||||||
|
ItemActionStatus.UnsupportedAction,
|
||||||
|
$"Item {Info.DefinitionId} cannot be used directly."
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d9p4x1m7v3k6c
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Game.Godot.Extensions;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Godot;
|
||||||
|
|
||||||
|
public partial class ItemSpawnerController : Node, IItemSpawner
|
||||||
|
{
|
||||||
|
[Export] public Node3D ItemsRoot;
|
||||||
|
[Export] public ItemWorldController ItemWorld;
|
||||||
|
[Export] public global::Godot.Collections.Array<PackedScene> ItemScenes = [];
|
||||||
|
|
||||||
|
private readonly Dictionary<ItemDefinitionId, PackedScene> _scenes = [];
|
||||||
|
|
||||||
|
public IReadOnlyCollection<ItemDefinitionId> DefinitionIds => _scenes.Keys.ToArray();
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
this.AssertRequiredExports();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_scenes.Clear();
|
||||||
|
|
||||||
|
foreach (PackedScene scene in ItemScenes)
|
||||||
|
{
|
||||||
|
RegisterScene(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (WorldItemController item in ItemWorld.Items)
|
||||||
|
{
|
||||||
|
string scenePath = item.SceneFilePath;
|
||||||
|
if (string.IsNullOrWhiteSpace(scenePath))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PackedScene scene = ResourceLoader.Load<PackedScene>(scenePath);
|
||||||
|
if (scene is not null)
|
||||||
|
{
|
||||||
|
_scenes.TryAdd(item.ItemInfo.DefinitionId, scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_scenes.Count == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("The item spawn catalog is empty.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemActionResult TrySpawn(
|
||||||
|
ItemDefinitionId definitionId,
|
||||||
|
Vector3 worldPosition,
|
||||||
|
out ItemInstanceId itemId
|
||||||
|
) {
|
||||||
|
itemId = default;
|
||||||
|
|
||||||
|
if (!_scenes.TryGetValue(definitionId, out PackedScene scene))
|
||||||
|
{
|
||||||
|
return ItemActionResult.Fail(
|
||||||
|
ItemActionStatus.UnknownDefinition,
|
||||||
|
$"Unknown item definition: {definitionId}."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldItemController item = scene.Instantiate<WorldItemController>();
|
||||||
|
item.InstanceId = Guid.NewGuid().ToString();
|
||||||
|
ItemsRoot.AddChild(item);
|
||||||
|
item.GlobalPosition = worldPosition;
|
||||||
|
ItemWorld.Register(item);
|
||||||
|
itemId = item.ItemInfo.Id;
|
||||||
|
return ItemActionResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterScene(PackedScene scene)
|
||||||
|
{
|
||||||
|
if (scene is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldItemController prototype = scene.Instantiate<WorldItemController>();
|
||||||
|
ItemDefinitionId definitionId = new(prototype.DefinitionId);
|
||||||
|
prototype.Free();
|
||||||
|
|
||||||
|
if (!_scenes.TryAdd(definitionId, scene))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Duplicate item spawn definition: {definitionId}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c3x9m6w2q8r1b
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Godot;
|
||||||
|
|
||||||
|
public partial class ItemWorldController : Node, IItemRepository
|
||||||
|
{
|
||||||
|
public const string RegistryGroup = "item_world_registry";
|
||||||
|
|
||||||
|
private readonly Dictionary<ItemInstanceId, WorldItemController> _items = [];
|
||||||
|
|
||||||
|
public IReadOnlyCollection<WorldItemController> Items => _items.Values;
|
||||||
|
|
||||||
|
public override void _EnterTree()
|
||||||
|
{
|
||||||
|
AddToGroup(RegistryGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterSceneItems()
|
||||||
|
{
|
||||||
|
foreach (Node node in GetTree().GetNodesInGroup(WorldItemController.ItemGroup))
|
||||||
|
{
|
||||||
|
if (node is WorldItemController item)
|
||||||
|
{
|
||||||
|
Register(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(WorldItemController item)
|
||||||
|
{
|
||||||
|
if (_items.TryGetValue(item.ItemInfo.Id, out WorldItemController existing))
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(existing, item))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Duplicate item instance id: {item.ItemInfo.Id}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_items.Add(item.ItemInfo.Id, item);
|
||||||
|
item.RegisterWith(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unregister(ItemInstanceId itemId)
|
||||||
|
{
|
||||||
|
_items.Remove(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet(ItemInstanceId itemId, out IItemInstance item)
|
||||||
|
{
|
||||||
|
if (_items.TryGetValue(itemId, out WorldItemController controller))
|
||||||
|
{
|
||||||
|
item = controller.Item;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
item = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetController(ItemInstanceId itemId, out WorldItemController controller)
|
||||||
|
{
|
||||||
|
return _items.TryGetValue(itemId, out controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://f8n3b6w1k5r9m
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Godot;
|
||||||
|
|
||||||
|
public sealed class PlayerHeldItemPresenter(
|
||||||
|
ItemWorldController itemWorld,
|
||||||
|
Node3D leftHandAnchor,
|
||||||
|
Node3D rightHandAnchor
|
||||||
|
) : IHeldItemPresenter
|
||||||
|
{
|
||||||
|
public bool TryShowHeld(IItemInstance item, Hand hand)
|
||||||
|
{
|
||||||
|
if (!itemWorld.TryGetController(item.Info.Id, out WorldItemController controller))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node3D anchor = hand == Hand.Left ? leftHandAnchor : rightHandAnchor;
|
||||||
|
controller.SetHeld(anchor, item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryShowDropped(IItemInstance item, Vector3 worldPosition)
|
||||||
|
{
|
||||||
|
if (!itemWorld.TryGetController(item.Info.Id, out WorldItemController controller))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.SetDropped(worldPosition, item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh(IItemInstance item)
|
||||||
|
{
|
||||||
|
if (itemWorld.TryGetController(item.Info.Id, out WorldItemController controller))
|
||||||
|
{
|
||||||
|
controller.ApplyState(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://e5m2r8q1v7k4n
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
using System;
|
||||||
|
using Game.Features.Items.Contracts;
|
||||||
|
using Game.Features.Items.Domain;
|
||||||
|
using Game.Godot.Extensions;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Game.Features.Items.Godot;
|
||||||
|
|
||||||
|
public partial class WorldItemController : RigidBody3D, IPickupTarget
|
||||||
|
{
|
||||||
|
public const string ItemGroup = "world_items";
|
||||||
|
|
||||||
|
[Export] public string InstanceId;
|
||||||
|
[Export] public string DefinitionId = "item";
|
||||||
|
[Export] public string DisplayName = "Item";
|
||||||
|
[Export] public HandRequirement HandRequirement = HandRequirement.One;
|
||||||
|
[Export] public CollisionShape3D Collision;
|
||||||
|
[Export] public Node3D WorldVisual;
|
||||||
|
[Export] public PackedScene HeldViewScene;
|
||||||
|
|
||||||
|
protected Node3D HeldView { get; private set; }
|
||||||
|
protected bool IsHeld => HeldView is not null;
|
||||||
|
|
||||||
|
public ItemInfo ItemInfo { get; private set; }
|
||||||
|
public ItemDefinition Definition { get; private set; }
|
||||||
|
public IItemInstance Item { get; private set; }
|
||||||
|
|
||||||
|
private ItemWorldController _registry;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
this.AssertRequiredExports();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(DefinitionId))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{Name}: item definition id cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HeldViewScene is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{Name}: held view scene is not assigned.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Guid id = string.IsNullOrWhiteSpace(InstanceId)
|
||||||
|
? Guid.NewGuid()
|
||||||
|
: Guid.Parse(InstanceId);
|
||||||
|
|
||||||
|
Definition = new ItemDefinition(new ItemDefinitionId(DefinitionId), DisplayName, HandRequirement);
|
||||||
|
ItemInfo = new ItemInfo(
|
||||||
|
new ItemInstanceId(id),
|
||||||
|
Definition.Id,
|
||||||
|
Definition.DisplayName,
|
||||||
|
Definition.HandRequirement
|
||||||
|
);
|
||||||
|
Item = CreateItemInstance(ItemInfo);
|
||||||
|
AddToGroup(ItemGroup);
|
||||||
|
RegisterWithAvailableWorld();
|
||||||
|
ApplyState(Item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
_registry?.Unregister(ItemInfo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterWith(ItemWorldController registry)
|
||||||
|
{
|
||||||
|
_registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHeld(Node3D anchor, IItemInstance item)
|
||||||
|
{
|
||||||
|
if (IsHeld)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Freeze = true;
|
||||||
|
Sleeping = true;
|
||||||
|
LinearVelocity = Vector3.Zero;
|
||||||
|
AngularVelocity = Vector3.Zero;
|
||||||
|
Collision.SetDeferred(CollisionShape3D.PropertyName.Disabled, true);
|
||||||
|
WorldVisual.Visible = false;
|
||||||
|
|
||||||
|
HeldView = HeldViewScene.Instantiate<Node3D>();
|
||||||
|
anchor.AddChild(HeldView);
|
||||||
|
HeldView.Transform = Transform3D.Identity;
|
||||||
|
OnHeldViewCreated(HeldView);
|
||||||
|
ApplyState(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDropped(Vector3 worldPosition, IItemInstance item)
|
||||||
|
{
|
||||||
|
if (HeldView is not null)
|
||||||
|
{
|
||||||
|
OnHeldViewRemoving(HeldView);
|
||||||
|
HeldView.QueueFree();
|
||||||
|
HeldView = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalPosition = worldPosition;
|
||||||
|
LinearVelocity = Vector3.Zero;
|
||||||
|
AngularVelocity = Vector3.Zero;
|
||||||
|
WorldVisual.Visible = true;
|
||||||
|
Collision.SetDeferred(CollisionShape3D.PropertyName.Disabled, false);
|
||||||
|
Freeze = false;
|
||||||
|
Sleeping = false;
|
||||||
|
ApplyState(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyState(IItemInstance item)
|
||||||
|
{
|
||||||
|
OnPresentationChanged(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IItemInstance CreateItemInstance(ItemInfo itemInfo) => new PassiveItemInstance(itemInfo);
|
||||||
|
|
||||||
|
protected virtual void OnHeldViewCreated(Node3D heldView)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnHeldViewRemoving(Node3D heldView)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnPresentationChanged(IItemInstance item)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterWithAvailableWorld()
|
||||||
|
{
|
||||||
|
foreach (Node node in GetTree().GetNodesInGroup(ItemWorldController.RegistryGroup))
|
||||||
|
{
|
||||||
|
if (node is ItemWorldController registry)
|
||||||
|
{
|
||||||
|
registry.Register(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d1x7m4q9v2c6k
|
||||||
Reference in New Issue
Block a user