Inventory management focused game written in Godot / C#
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

185 lines
5.8 KiB

public partial class ItemManager : Node
{
uint _trackingIdCounter = 0;
public Dictionary<uint, Item> TrackedItems { get; } = [];
public override void _Ready()
{
var items = GetParent().FindChildren("*", "res://objects/Item.cs", owned: false).Cast<Item>();
foreach (var item in items) Add(item);
}
public void Add(Item item)
{
if (item.Manager != null) throw new ArgumentException(
$"Item '{item.GetPath()}' is already part of another scene");
// FIXME: Synchronize items so we can get rid of this temporary fix.
// // Generate a random, unused id for this item.
// uint id; do { id = Randi(); } while (TrackedItems.ContainsKey(id));
var id = _trackingIdCounter++;
TrackedItems.Add(id, item);
item.Manager = this;
item.TrackingId = id;
}
public void Remove(Item item)
{
if (item.Manager != this) throw new ArgumentException(
$"Item '{item.GetPath()}' is not part of this scene");
TrackedItems.Remove(item.TrackingId);
item.Manager = null;
item.TrackingId = 0;
}
public void TryPickup(Item item)
{
VerifyManager(item);
VerifyAuthority(item, "Item");
RPC.To(item.GetMultiplayerAuthority()).Send(RequestPickup, item.TrackingId);
}
[Rpc(RpcMode.AnyPeer, CallLocal = true)]
void RequestPickup(uint itemTrackingId)
{
// TODO: Print debug messages instead of just returning?
// Pickup will be requested by any player to the owner of the item.
// If this message is received by anyone else, ignore it.
if (!IsMultiplayerAuthority()) return;
// Find the item being requested to be picked up, return if not found.
if (!TrackedItems.TryGetValue(itemTrackingId, out var item)) return;
// Make sure that the item shares its authority with this Manager.
if (!item.IsMultiplayerAuthority()) return;
var player = Multiplayer.GetRemoteSenderPlayer();
if (player.Pickup.HasItemsHeld) return;
// TODO: Check if player is in range.
// Have the item be rotated in the player's hands depending on how it's currently rotated.
var localBasis = player.Pickup.GlobalBasis.Inverse() * item.GlobalBasis;
var localTransform = new Transform3D(localBasis, Vector3.Zero);
RPC.ToAll().Send(AcceptPickup, itemTrackingId, player.PeerId, localTransform);
}
[Rpc(CallLocal = true)]
void AcceptPickup(uint itemTrackingId, int playerPeerId, Transform3D localTransform)
{
var item = VerifyAuthority(TrackedItems[itemTrackingId], "Item");
var player = Game.Players.ByPeerId(playerPeerId);
item.Reparent(player.Pickup);
item.Transform = localTransform;
}
public void TryPlace(Item item, Grid grid, Transform3D localTransform)
{
VerifyManager(item);
VerifyAuthority(item, "Item");
RPC.To(item.GetMultiplayerAuthority()).Send(RequestPlace,
item.TrackingId, grid.GetPath(), localTransform);
}
[Rpc(RpcMode.AnyPeer, CallLocal = true)]
void RequestPlace(uint itemTrackingId, NodePath gridPath, Transform3D localTransform)
{
if (!IsMultiplayerAuthority()) return;
if (!TrackedItems.TryGetValue(itemTrackingId, out var item)) return;
if (!item.IsMultiplayerAuthority()) return;
var player = Multiplayer.GetRemoteSenderPlayer();
// Ensure that the item is currently being held by the player.
if (item.FindParentOrNull<Player>() != player) return;
if (GetNodeOrNull(gridPath) is not Grid grid) return;
if (!grid.IsMultiplayerAuthority()) return;
// TODO: Further verify localTransform.
if (!grid.CanPlaceAt(item, localTransform)) return;
RPC.ToAll().Send(AcceptPlace, itemTrackingId, gridPath, localTransform);
}
[Rpc(CallLocal = true)]
void AcceptPlace(uint itemTrackingId, NodePath gridPath, Transform3D localTransform)
{
var item = VerifyAuthority(TrackedItems[itemTrackingId], "Item");
var grid = VerifyAuthority(this.GetNodeOrThrow<Grid>(gridPath), "Grid");
item.Reparent(grid);
item.Transform = localTransform;
}
public void TryThrow(Item item)
{
VerifyManager(item);
VerifyAuthority(item, "Item");
RPC.To(item.GetMultiplayerAuthority()).Send(RequestThrow, item.TrackingId);
}
[Rpc(RpcMode.AnyPeer, CallLocal = true)]
void RequestThrow(uint itemTrackingId)
{
if (!IsMultiplayerAuthority()) return;
if (!TrackedItems.TryGetValue(itemTrackingId, out var item)) return;
if (!item.IsMultiplayerAuthority()) return;
var player = Multiplayer.GetRemoteSenderPlayer();
// Ensure that the item is currently being held by the player.
if (item.FindParentOrNull<Player>() != player) return;
RPC.ToAll().Send(AcceptThrow, itemTrackingId);
}
[Rpc(CallLocal = true)]
void AcceptThrow(uint itemTrackingId)
{
var item = VerifyAuthority(TrackedItems[itemTrackingId], "Item");
var player = item.FindParentOrThrow<Player>();
// FIXME: Actually this doesn't work, the item won't have a Manager anymore. D:
// Add item to workshop, if player is currently in one, otherwise add to world.
var parent = player.EnteredWorkshop?.Objects ?? Game.Instance;
item.Reparent(parent, true);
// Throw item forward and up a bit.
var basis = player.Camera.Camera.GlobalBasis;
var direction = -basis.Z + basis.Y;
item.ApplyImpulse(direction * 2);
}
/// <summary>
/// Verifies that this item is managed by this
/// Manager instance, throwing an exception if not.
/// </summary>
void VerifyManager(Item item)
{
if (item.Manager != this) throw new InvalidOperationException(
$"Item manager ({item.Manager?.GetPath().ToString() ?? "null"}) is not this manager ({GetPath()})");
}
/// <summary>
/// Verifies that this node has the same multiplayer authority as this
/// Manager instance, throwing an exception if not. Returns the node
/// that was passed in.
/// </summary>
T VerifyAuthority<T>(T node, string name)
where T : Node
{
var expected = GetMultiplayerAuthority();
var actual = node.GetMultiplayerAuthority();
if (actual != expected) throw new InvalidOperationException(
$"{name} authority ({actual}) is not manager authority ({expected})");
return node;
}
}