Add item use command

This commit is contained in:
2026-05-17 13:01:00 -07:00
parent 555ad6df25
commit 712a8548c0
7 changed files with 181 additions and 1 deletions
+4 -1
View File
@@ -501,7 +501,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
`SplitStackByIndex`/`AgrarianSplitStack StackIndex SplitQuantity` support that
validates source slot, quantity, and free slot capacity, preserves stack
metadata, creates a separate stack slot, and avoids immediate re-merge.
- [ ] Add item use.
- [x] Add item use. Added server-authoritative
`AgrarianUseItem ItemId Quantity` support for MVP consumables, with food
hunger recovery, raw meat hunger recovery plus sickness risk, bandage injury
treatment, and restore-on-unsupported-item behavior.
- [ ] Add equipment slots if needed.
- [ ] Add weight or carry capacity placeholder.
- [ ] Add inventory UI.
+9
View File
@@ -114,6 +114,15 @@ creates a separate inventory slot. It intentionally does not re-merge the split
stack through `AddItem`, because the UI needs an actual separate stack to
support later drag/drop and partial drop flows.
MVP item use is available through `AgrarianUseItem ItemId Quantity`. The command
routes to the server, extracts the requested stack quantity, applies a
whitelisted item effect, and restores the stack if the item is not usable yet.
For the first survival loop, `food` restores hunger, `meat` restores more hunger
but adds sickness risk because it is raw, and `bandage` reduces injury severity
with a small health bump. This gives UI item-use work a concrete authority path
while leaving tools, structures, and future complex consumables blocked until
they have explicit gameplay rules.
## Time And Environment
The MVP gameplay calendar target is:
+76
View File
@@ -0,0 +1,76 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
FILES = {
"AgrarianSurvivalComponent.h": ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.h",
"AgrarianSurvivalComponent.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.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 = {
"AgrarianSurvivalComponent.h": [
"void ReduceInjury(float Amount);",
],
"AgrarianSurvivalComponent.cpp": [
"void UAgrarianSurvivalComponent::ReduceInjury",
"Survival.InjurySeverity -= FMath::Max(0.0f, Amount);",
"BroadcastSurvivalChanged();",
],
"AgrarianGamePlayerController.h": [
"void AgrarianUseItem(FName ItemId, int32 Quantity);",
"void ServerAgrarianUseItem(FName ItemId, int32 Quantity);",
],
"AgrarianGamePlayerController.cpp": [
"ApplyAgrarianItemUseEffect",
"ItemId == TEXT(\"food\")",
"SurvivalComponent->AddFood(15.0f * Quantity);",
"ItemId == TEXT(\"meat\")",
"SurvivalComponent->AddSickness(3.0f * Quantity);",
"ItemId == TEXT(\"bandage\")",
"SurvivalComponent->ReduceInjury(18.0f * Quantity);",
"void AAgrarianGamePlayerController::AgrarianUseItem(FName ItemId, int32 Quantity)",
"Usage: AgrarianUseItem <ItemId> <Quantity>",
"void AAgrarianGamePlayerController::ServerAgrarianUseItem_Implementation",
"InventoryComponent->ExtractItem(ItemId, Quantity, UsedStack)",
"InventoryComponent->AddItem(UsedStack);",
"cannot be used yet; item restored",
],
"TechnicalDesignDocument.md": [
"`AgrarianUseItem ItemId Quantity`",
"`food` restores hunger",
"`meat` restores more hunger",
"adds sickness risk",
"`bandage` reduces injury severity",
"restores the stack if the item is not usable yet",
],
"InventoryDataModel.md": [
"Item use:",
"server validates the item type and applies item-specific effects.",
],
"AGRARIAN_DEVELOPMENT_ROADMAP.md": [
"[x] Add item use.",
],
}
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 use verification failed: " + "; ".join(missing))
print("Agrarian item use verification complete.")
if __name__ == "__main__":
main()
@@ -20,6 +20,39 @@
namespace
{
const FVector GroundZeroDeveloperTravelHomeLocation(-22000.0f, -3500.0f, 1148.0f);
bool ApplyAgrarianItemUseEffect(const FName ItemId, const int32 Quantity, UAgrarianSurvivalComponent* SurvivalComponent, FString& OutEffectSummary)
{
if (!SurvivalComponent || Quantity <= 0)
{
return false;
}
if (ItemId == TEXT("food"))
{
SurvivalComponent->AddFood(15.0f * Quantity);
OutEffectSummary = FString::Printf(TEXT("restored %.0f hunger"), 15.0f * Quantity);
return true;
}
if (ItemId == TEXT("meat"))
{
SurvivalComponent->AddFood(22.0f * Quantity);
SurvivalComponent->AddSickness(3.0f * Quantity);
OutEffectSummary = FString::Printf(TEXT("restored %.0f hunger but added raw-meat sickness risk"), 22.0f * Quantity);
return true;
}
if (ItemId == TEXT("bandage"))
{
SurvivalComponent->ReduceInjury(18.0f * Quantity);
SurvivalComponent->RestoreHealth(4.0f * Quantity);
OutEffectSummary = FString::Printf(TEXT("treated %.0f injury severity"), 18.0f * Quantity);
return true;
}
return false;
}
}
void AAgrarianGamePlayerController::BeginPlay()
@@ -150,6 +183,17 @@ void AAgrarianGamePlayerController::AgrarianSplitStack(int32 StackIndex, int32 S
ServerAgrarianSplitStack(StackIndex, SplitQuantity);
}
void AAgrarianGamePlayerController::AgrarianUseItem(FName ItemId, int32 Quantity)
{
if (ItemId == NAME_None || Quantity <= 0)
{
ClientMessage(TEXT("Usage: AgrarianUseItem <ItemId> <Quantity>"));
return;
}
ServerAgrarianUseItem(ItemId, Quantity);
}
void AAgrarianGamePlayerController::AgrarianTravel(float X, float Y, float Z)
{
ServerAgrarianTravel(FVector(X, Y, Z));
@@ -299,6 +343,35 @@ void AAgrarianGamePlayerController::ServerAgrarianSplitStack_Implementation(int3
}
}
void AAgrarianGamePlayerController::ServerAgrarianUseItem_Implementation(FName ItemId, int32 Quantity)
{
AAgrarianGameCharacter* AgrarianCharacter = GetPawn<AAgrarianGameCharacter>();
UAgrarianInventoryComponent* InventoryComponent = AgrarianCharacter ? AgrarianCharacter->GetInventoryComponent() : nullptr;
UAgrarianSurvivalComponent* SurvivalComponent = AgrarianCharacter ? AgrarianCharacter->GetSurvivalComponent() : nullptr;
if (!InventoryComponent || !SurvivalComponent)
{
ClientMessage(TEXT("No Agrarian inventory or survival component found."));
return;
}
FAgrarianItemStack UsedStack;
if (!InventoryComponent->ExtractItem(ItemId, Quantity, UsedStack))
{
ClientMessage(FString::Printf(TEXT("Could not use %d x %s."), Quantity, *ItemId.ToString()));
return;
}
FString EffectSummary;
if (!ApplyAgrarianItemUseEffect(ItemId, UsedStack.Quantity, SurvivalComponent, EffectSummary))
{
InventoryComponent->AddItem(UsedStack);
ClientMessage(FString::Printf(TEXT("%s cannot be used yet; item restored."), *ItemId.ToString()));
return;
}
ClientMessage(FString::Printf(TEXT("Used %d x %s: %s."), UsedStack.Quantity, *ItemId.ToString(), *EffectSummary));
}
void AAgrarianGamePlayerController::ServerAgrarianTravel_Implementation(FVector Destination)
{
APawn* ControlledPawn = GetPawn();
@@ -72,6 +72,9 @@ public:
UFUNCTION(Exec)
void AgrarianSplitStack(int32 StackIndex, int32 SplitQuantity);
UFUNCTION(Exec)
void AgrarianUseItem(FName ItemId, int32 Quantity);
UFUNCTION(Exec)
void AgrarianTravel(float X, float Y, float Z);
@@ -97,6 +100,9 @@ protected:
UFUNCTION(Server, Reliable)
void ServerAgrarianSplitStack(int32 StackIndex, int32 SplitQuantity);
UFUNCTION(Server, Reliable)
void ServerAgrarianUseItem(FName ItemId, int32 Quantity);
UFUNCTION(Server, Reliable)
void ServerAgrarianTravel(FVector Destination);
};
@@ -179,6 +179,16 @@ void UAgrarianSurvivalComponent::AddInjury(float Severity)
}
}
void UAgrarianSurvivalComponent::ReduceInjury(float Amount)
{
if (GetOwner() && GetOwner()->HasAuthority())
{
Survival.InjurySeverity -= FMath::Max(0.0f, Amount);
ClampSurvival();
BroadcastSurvivalChanged();
}
}
void UAgrarianSurvivalComponent::AddSickness(float Severity)
{
if (GetOwner() && GetOwner()->HasAuthority())
@@ -93,6 +93,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
void AddInjury(float Severity);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
void ReduceInjury(float Amount);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
void AddSickness(float Severity);