From b48595f70d831a12077ae9c38f3f8fee3593571a Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 17 May 2026 11:05:28 -0700 Subject: [PATCH] Add item drop command --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 4 +- Docs/TechnicalDesignDocument.md | 7 ++ Scripts/verify_item_drop.py | 72 +++++++++++++++++++ .../AgrarianGamePlayerController.cpp | 54 ++++++++++++++ .../AgrarianGamePlayerController.h | 6 ++ .../AgrarianInventoryComponent.cpp | 15 ++++ .../AgrarianGame/AgrarianInventoryComponent.h | 3 + 7 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 Scripts/verify_item_drop.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 10cf9e0..cfd04dd 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -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 inventory add, prompt text, and destroy-on-success behavior so failed pickups 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 item use. - [ ] Add equipment slots if needed. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 2891068..02fac98 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -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 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 The MVP gameplay calendar target is: diff --git a/Scripts/verify_item_drop.py b/Scripts/verify_item_drop.py new file mode 100644 index 0000000..03a339e --- /dev/null +++ b/Scripts/verify_item_drop.py @@ -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", + "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() diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index b0b5ae3..8d0058d 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -4,6 +4,7 @@ #include "AgrarianGamePlayerController.h" #include "AgrarianGameCharacter.h" #include "AgrarianInventoryComponent.h" +#include "AgrarianItemPickup.h" #include "AgrarianPersistenceSubsystem.h" #include "AgrarianShelterActor.h" #include "AgrarianSurvivalComponent.h" @@ -127,6 +128,17 @@ void AAgrarianGamePlayerController::AgrarianHeal() ServerAgrarianHeal(); } +void AAgrarianGamePlayerController::AgrarianDropItem(FName ItemId, int32 Quantity) +{ + if (ItemId == NAME_None || Quantity <= 0) + { + ClientMessage(TEXT("Usage: AgrarianDropItem ")); + return; + } + + ServerAgrarianDropItem(ItemId, Quantity); +} + void AAgrarianGamePlayerController::AgrarianTravel(float X, float Y, float Z) { ServerAgrarianTravel(FVector(X, Y, Z)); @@ -214,6 +226,48 @@ void AAgrarianGamePlayerController::ServerAgrarianHeal_Implementation() ClientMessage(TEXT("Agrarian survival restored.")); } +void AAgrarianGamePlayerController::ServerAgrarianDropItem_Implementation(FName ItemId, int32 Quantity) +{ + AAgrarianGameCharacter* AgrarianCharacter = GetPawn(); + 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::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) { APawn* ControlledPawn = GetPawn(); diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.h b/Source/AgrarianGame/AgrarianGamePlayerController.h index 3adb8f0..a6da9de 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.h +++ b/Source/AgrarianGame/AgrarianGamePlayerController.h @@ -66,6 +66,9 @@ public: UFUNCTION(Exec) void AgrarianHeal(); + UFUNCTION(Exec) + void AgrarianDropItem(FName ItemId, int32 Quantity); + UFUNCTION(Exec) void AgrarianTravel(float X, float Y, float Z); @@ -85,6 +88,9 @@ protected: UFUNCTION(Server, Reliable) void ServerAgrarianHeal(); + UFUNCTION(Server, Reliable) + void ServerAgrarianDropItem(FName ItemId, int32 Quantity); + UFUNCTION(Server, Reliable) void ServerAgrarianTravel(FVector Destination); }; diff --git a/Source/AgrarianGame/AgrarianInventoryComponent.cpp b/Source/AgrarianGame/AgrarianInventoryComponent.cpp index 19011fc..1c22423 100644 --- a/Source/AgrarianGame/AgrarianInventoryComponent.cpp +++ b/Source/AgrarianGame/AgrarianInventoryComponent.cpp @@ -72,6 +72,14 @@ bool UAgrarianInventoryComponent::AddItem(const FAgrarianItemStack& Stack) 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)) { return false; @@ -87,6 +95,13 @@ bool UAgrarianInventoryComponent::RemoveItem(FName ItemId, int32 Quantity) } const int32 Removed = FMath::Min(Stack.Quantity, Remaining); + if (OutStack.ItemId == NAME_None) + { + OutStack = Stack; + OutStack.Quantity = 0; + } + + OutStack.Quantity += Removed; Stack.Quantity -= Removed; Remaining -= Removed; diff --git a/Source/AgrarianGame/AgrarianInventoryComponent.h b/Source/AgrarianGame/AgrarianInventoryComponent.h index 1ca301c..5ffefbe 100644 --- a/Source/AgrarianGame/AgrarianInventoryComponent.h +++ b/Source/AgrarianGame/AgrarianInventoryComponent.h @@ -43,6 +43,9 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Inventory") 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") void ServerAddItem(const FAgrarianItemStack& Stack);