Add item drop command

This commit is contained in:
2026-05-17 11:05:28 -07:00
parent 03d856efbf
commit b48595f70d
7 changed files with 160 additions and 1 deletions
+3 -1
View File
@@ -494,7 +494,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
actor with definition-backed or inline stack data, server-authoritative actor with definition-backed or inline stack data, server-authoritative
inventory add, prompt text, and destroy-on-success behavior so failed pickups inventory add, prompt text, and destroy-on-success behavior so failed pickups
remain available. remain available.
- [ ] Add item drop. - [x] Add item drop. Added server-authoritative `AgrarianDropItem ItemId
Quantity`, inventory stack extraction that preserves dropped stack metadata,
pickup spawning in front of the player, and restore-on-spawn-failure handling.
- [ ] Add stack splitting. - [ ] Add stack splitting.
- [ ] Add item use. - [ ] Add item use.
- [ ] Add equipment slots if needed. - [ ] Add equipment slots if needed.
+7
View File
@@ -99,6 +99,13 @@ inventory, and only then remove the pickup by destroying the world pickup actor.
If the inventory is full or the stack is invalid, the pickup remains in the If the inventory is full or the stack is invalid, the pickup remains in the
world for another attempt. world for another attempt.
Developer item dropping is available through `AgrarianDropItem ItemId Quantity`.
The command routes to the server, extracts stack data before spawning so display
name and unit weight survive the drop, spawns an `AAgrarianItemPickup` in front
of the player, and restores the removed stack if pickup spawning fails. This is
the baseline behavior future UI-driven drop flows should call through rather
than duplicating inventory mutation logic on the client.
## Time And Environment ## Time And Environment
The MVP gameplay calendar target is: The MVP gameplay calendar target is:
+72
View File
@@ -0,0 +1,72 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
FILES = {
"AgrarianInventoryComponent.h": ROOT / "Source" / "AgrarianGame" / "AgrarianInventoryComponent.h",
"AgrarianInventoryComponent.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianInventoryComponent.cpp",
"AgrarianGamePlayerController.h": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.h",
"AgrarianGamePlayerController.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp",
"TechnicalDesignDocument.md": ROOT / "Docs" / "TechnicalDesignDocument.md",
"InventoryDataModel.md": ROOT / "Docs" / "InventoryDataModel.md",
"AGRARIAN_DEVELOPMENT_ROADMAP.md": ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md",
}
EXPECTED = {
"AgrarianInventoryComponent.h": [
"bool ExtractItem(FName ItemId, int32 Quantity, FAgrarianItemStack& OutStack);",
],
"AgrarianInventoryComponent.cpp": [
"return ExtractItem(ItemId, Quantity, RemovedStack);",
"bool UAgrarianInventoryComponent::ExtractItem",
"OutStack.ItemId == NAME_None",
"OutStack.Quantity += Removed;",
"BroadcastInventoryChanged();",
],
"AgrarianGamePlayerController.h": [
"void AgrarianDropItem(FName ItemId, int32 Quantity);",
"void ServerAgrarianDropItem(FName ItemId, int32 Quantity);",
],
"AgrarianGamePlayerController.cpp": [
"#include \"AgrarianItemPickup.h\"",
"void AAgrarianGamePlayerController::AgrarianDropItem(FName ItemId, int32 Quantity)",
"ServerAgrarianDropItem(ItemId, Quantity);",
"void AAgrarianGamePlayerController::ServerAgrarianDropItem_Implementation",
"InventoryComponent->ExtractItem(ItemId, Quantity, DroppedStack)",
"SpawnActor<AAgrarianItemPickup>",
"AdjustIfPossibleButAlwaysSpawn",
"Pickup->PickupStack = DroppedStack;",
"InventoryComponent->AddItem(DroppedStack);",
"item restored",
],
"TechnicalDesignDocument.md": [
"`AgrarianDropItem ItemId Quantity`",
"extracts stack data before spawning",
"restores the removed stack if pickup spawning fails",
],
"InventoryDataModel.md": [
"Drop:",
"server removes a stack quantity and spawns a world item or bundle near",
],
"AGRARIAN_DEVELOPMENT_ROADMAP.md": [
"[x] Add item drop.",
],
}
def main():
missing = []
for label, path in FILES.items():
text = path.read_text(encoding="utf-8")
for snippet in EXPECTED[label]:
if snippet not in text:
missing.append(f"{label}: {snippet}")
if missing:
raise RuntimeError("Item drop verification failed: " + "; ".join(missing))
print("Agrarian item drop verification complete.")
if __name__ == "__main__":
main()
@@ -4,6 +4,7 @@
#include "AgrarianGamePlayerController.h" #include "AgrarianGamePlayerController.h"
#include "AgrarianGameCharacter.h" #include "AgrarianGameCharacter.h"
#include "AgrarianInventoryComponent.h" #include "AgrarianInventoryComponent.h"
#include "AgrarianItemPickup.h"
#include "AgrarianPersistenceSubsystem.h" #include "AgrarianPersistenceSubsystem.h"
#include "AgrarianShelterActor.h" #include "AgrarianShelterActor.h"
#include "AgrarianSurvivalComponent.h" #include "AgrarianSurvivalComponent.h"
@@ -127,6 +128,17 @@ void AAgrarianGamePlayerController::AgrarianHeal()
ServerAgrarianHeal(); ServerAgrarianHeal();
} }
void AAgrarianGamePlayerController::AgrarianDropItem(FName ItemId, int32 Quantity)
{
if (ItemId == NAME_None || Quantity <= 0)
{
ClientMessage(TEXT("Usage: AgrarianDropItem <ItemId> <Quantity>"));
return;
}
ServerAgrarianDropItem(ItemId, Quantity);
}
void AAgrarianGamePlayerController::AgrarianTravel(float X, float Y, float Z) void AAgrarianGamePlayerController::AgrarianTravel(float X, float Y, float Z)
{ {
ServerAgrarianTravel(FVector(X, Y, Z)); ServerAgrarianTravel(FVector(X, Y, Z));
@@ -214,6 +226,48 @@ void AAgrarianGamePlayerController::ServerAgrarianHeal_Implementation()
ClientMessage(TEXT("Agrarian survival restored.")); ClientMessage(TEXT("Agrarian survival restored."));
} }
void AAgrarianGamePlayerController::ServerAgrarianDropItem_Implementation(FName ItemId, int32 Quantity)
{
AAgrarianGameCharacter* AgrarianCharacter = GetPawn<AAgrarianGameCharacter>();
UAgrarianInventoryComponent* InventoryComponent = AgrarianCharacter ? AgrarianCharacter->GetInventoryComponent() : nullptr;
if (!AgrarianCharacter || !InventoryComponent)
{
ClientMessage(TEXT("No Agrarian inventory component found."));
return;
}
FAgrarianItemStack DroppedStack;
if (!InventoryComponent->ExtractItem(ItemId, Quantity, DroppedStack))
{
ClientMessage(FString::Printf(TEXT("Could not drop %d x %s."), Quantity, *ItemId.ToString()));
return;
}
const FVector DropLocation = AgrarianCharacter->GetActorLocation()
+ AgrarianCharacter->GetActorForwardVector() * 150.0f
+ FVector(0.0f, 0.0f, 40.0f);
const FRotator DropRotation(0.0f, AgrarianCharacter->GetActorRotation().Yaw, 0.0f);
FActorSpawnParameters SpawnParameters;
SpawnParameters.Owner = AgrarianCharacter;
SpawnParameters.Instigator = AgrarianCharacter;
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
AAgrarianItemPickup* Pickup = GetWorld()
? GetWorld()->SpawnActor<AAgrarianItemPickup>(AAgrarianItemPickup::StaticClass(), DropLocation, DropRotation, SpawnParameters)
: nullptr;
if (!Pickup)
{
InventoryComponent->AddItem(DroppedStack);
ClientMessage(FString::Printf(TEXT("Failed to spawn dropped %s; item restored."), *ItemId.ToString()));
return;
}
Pickup->PickupStack = DroppedStack;
Pickup->Quantity = DroppedStack.Quantity;
ClientMessage(FString::Printf(TEXT("Dropped %d x %s."), DroppedStack.Quantity, *ItemId.ToString()));
}
void AAgrarianGamePlayerController::ServerAgrarianTravel_Implementation(FVector Destination) void AAgrarianGamePlayerController::ServerAgrarianTravel_Implementation(FVector Destination)
{ {
APawn* ControlledPawn = GetPawn(); APawn* ControlledPawn = GetPawn();
@@ -66,6 +66,9 @@ public:
UFUNCTION(Exec) UFUNCTION(Exec)
void AgrarianHeal(); void AgrarianHeal();
UFUNCTION(Exec)
void AgrarianDropItem(FName ItemId, int32 Quantity);
UFUNCTION(Exec) UFUNCTION(Exec)
void AgrarianTravel(float X, float Y, float Z); void AgrarianTravel(float X, float Y, float Z);
@@ -85,6 +88,9 @@ protected:
UFUNCTION(Server, Reliable) UFUNCTION(Server, Reliable)
void ServerAgrarianHeal(); void ServerAgrarianHeal();
UFUNCTION(Server, Reliable)
void ServerAgrarianDropItem(FName ItemId, int32 Quantity);
UFUNCTION(Server, Reliable) UFUNCTION(Server, Reliable)
void ServerAgrarianTravel(FVector Destination); void ServerAgrarianTravel(FVector Destination);
}; };
@@ -72,6 +72,14 @@ bool UAgrarianInventoryComponent::AddItem(const FAgrarianItemStack& Stack)
bool UAgrarianInventoryComponent::RemoveItem(FName ItemId, int32 Quantity) bool UAgrarianInventoryComponent::RemoveItem(FName ItemId, int32 Quantity)
{ {
FAgrarianItemStack RemovedStack;
return ExtractItem(ItemId, Quantity, RemovedStack);
}
bool UAgrarianInventoryComponent::ExtractItem(FName ItemId, int32 Quantity, FAgrarianItemStack& OutStack)
{
OutStack = FAgrarianItemStack();
if (!GetOwner() || !GetOwner()->HasAuthority() || Quantity <= 0 || !HasItem(ItemId, Quantity)) if (!GetOwner() || !GetOwner()->HasAuthority() || Quantity <= 0 || !HasItem(ItemId, Quantity))
{ {
return false; return false;
@@ -87,6 +95,13 @@ bool UAgrarianInventoryComponent::RemoveItem(FName ItemId, int32 Quantity)
} }
const int32 Removed = FMath::Min(Stack.Quantity, Remaining); const int32 Removed = FMath::Min(Stack.Quantity, Remaining);
if (OutStack.ItemId == NAME_None)
{
OutStack = Stack;
OutStack.Quantity = 0;
}
OutStack.Quantity += Removed;
Stack.Quantity -= Removed; Stack.Quantity -= Removed;
Remaining -= Removed; Remaining -= Removed;
@@ -43,6 +43,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|Inventory") UFUNCTION(BlueprintCallable, Category = "Agrarian|Inventory")
bool RemoveItem(FName ItemId, int32 Quantity); bool RemoveItem(FName ItemId, int32 Quantity);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Inventory")
bool ExtractItem(FName ItemId, int32 Quantity, FAgrarianItemStack& OutStack);
UFUNCTION(Server, Reliable, BlueprintCallable, Category = "Agrarian|Inventory") UFUNCTION(Server, Reliable, BlueprintCallable, Category = "Agrarian|Inventory")
void ServerAddItem(const FAgrarianItemStack& Stack); void ServerAddItem(const FAgrarianItemStack& Stack);