feat: add extensible item inventory and world system
This commit is contained in:
@@ -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