From 843340ebdccb9dd403ce441d225f10111d6661b1 Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 17 May 2026 16:49:52 -0700 Subject: [PATCH] Add MVP resource tool rules --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 4 +- .../Resources/BP_FiberResourceNode.uasset | 4 +- .../Resources/BP_StoneResourceNode.uasset | 4 +- .../Resources/BP_WoodResourceNode.uasset | 4 +- Docs/TechnicalDesignDocument.md | 10 +++ Docs/Terrain/GroundZeroResourcePass.md | 15 ++++ Scripts/setup_playable_blueprints.py | 11 +++ Scripts/verify_playable_blueprints.py | 12 +++ Scripts/verify_resource_tool_requirements.py | 76 +++++++++++++++++++ Source/AgrarianGame/AgrarianResourceNode.cpp | 45 +++++++++-- Source/AgrarianGame/AgrarianResourceNode.h | 13 +++- 11 files changed, 184 insertions(+), 14 deletions(-) create mode 100644 Scripts/verify_resource_tool_requirements.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index c85526a..861574c 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -544,7 +544,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add respawn rules for MVP. Added configurable resource-node respawn fields and timer logic: renewable surface resources respawn after MVP delays, while stone remains nonrenewable for the first survival loop. -- [ ] Add tool requirement rules. +- [x] Add tool requirement rules. Added inventory-based resource tool rules: + current MVP nodes preserve bare-hand gathering, while a basic tool in + inventory improves yields for wood, fiber, and stone. - [x] Add bare-hand gathering fallback. - [ ] Add resource node persistence. - [x] Add replicated gathering feedback. diff --git a/Content/Agrarian/Blueprints/Resources/BP_FiberResourceNode.uasset b/Content/Agrarian/Blueprints/Resources/BP_FiberResourceNode.uasset index 740a882..ccdb24f 100644 --- a/Content/Agrarian/Blueprints/Resources/BP_FiberResourceNode.uasset +++ b/Content/Agrarian/Blueprints/Resources/BP_FiberResourceNode.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfeba50e8db9627195f23394437ae859e78c97f78a3b157f4234503742c2eba8 -size 24813 +oid sha256:17c2546d10599d8b869167c26db4233fe0e32a7771e9e12912af9d49a69bea7c +size 24947 diff --git a/Content/Agrarian/Blueprints/Resources/BP_StoneResourceNode.uasset b/Content/Agrarian/Blueprints/Resources/BP_StoneResourceNode.uasset index 4bdf798..ada6533 100644 --- a/Content/Agrarian/Blueprints/Resources/BP_StoneResourceNode.uasset +++ b/Content/Agrarian/Blueprints/Resources/BP_StoneResourceNode.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:279c707e5f813cc7ff38a514948c4c92ca2976955a9cea430bb5ef19d3d1efd7 -size 24756 +oid sha256:b6882e8acd6fdfbb2ea941463572a6bc23c0a8b971989c54f69fcff5ebfb9ee5 +size 24890 diff --git a/Content/Agrarian/Blueprints/Resources/BP_WoodResourceNode.uasset b/Content/Agrarian/Blueprints/Resources/BP_WoodResourceNode.uasset index b38a3df..bad182a 100644 --- a/Content/Agrarian/Blueprints/Resources/BP_WoodResourceNode.uasset +++ b/Content/Agrarian/Blueprints/Resources/BP_WoodResourceNode.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06eda61a4118656d12b6240585407fa75a4f29ca39562e9a1e83870316907bbe -size 24732 +oid sha256:f98fbe0d5cf1f20fa2489c60bb08a10d2500ed7a4bada60caae44d820dc85141 +size 24866 diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 16984a2..b92f775 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -448,6 +448,16 @@ gone. Respawn timing is configurable per Blueprint through uses a server-side timer and restores replicated `RemainingHarvests` when the delay expires. +Tool requirement rules remain inventory-based for the MVP because dedicated +equipment slots are deferred. A resource node can declare `RequiredToolItemId`, +`bAllowBareHandGathering`, and `ToolQuantityBonus`. Current Ground Zero wood, +fiber, and stone nodes still allow bare-hand gathering so the first survival +loop cannot deadlock before the player can craft a tool. When the player has a +`basic_tool` in inventory, those nodes grant an extra unit per harvest. Edible +plants remain pure bare-hand gathering. Later nodes can disable +`bAllowBareHandGathering` when the game has proper tool equipment, durability, +and feedback UI. + ### Wildlife Navigation MVP wildlife movement is server authoritative. `AAgrarianWildlifeBase` uses an diff --git a/Docs/Terrain/GroundZeroResourcePass.md b/Docs/Terrain/GroundZeroResourcePass.md index 5a79df2..2d43a89 100644 --- a/Docs/Terrain/GroundZeroResourcePass.md +++ b/Docs/Terrain/GroundZeroResourcePass.md @@ -54,6 +54,21 @@ Respawn restores the node to its configured `MaxHarvests` value and re-enables visibility/collision through the same replicated depletion state used by gathering. +## Tool Rules + +MVP tool rules keep the first loop accessible while giving crafted tools an +immediate benefit: + +- Wood, fiber, and stone nodes declare `basic_tool` as their useful tool. +- Those nodes still allow bare-hand gathering so players can gather the + ingredients needed to craft the first tool. +- A carried `basic_tool` adds `1` extra unit to each wood, fiber, or stone + harvest. +- Edible plant nodes remain pure bare-hand gathering. + +The rule is inventory-based until equipment slots, durability, and explicit +active-hand state are implemented. + ## Follow-Up Future passes should replace the prototype meshes with real coastal scrub, diff --git a/Scripts/setup_playable_blueprints.py b/Scripts/setup_playable_blueprints.py index 1f5a3d4..2c133a4 100644 --- a/Scripts/setup_playable_blueprints.py +++ b/Scripts/setup_playable_blueprints.py @@ -33,6 +33,9 @@ BLUEPRINTS = [ "yield_item_definition": WOOD_ITEM_PATH, "remaining_harvests": 16, "quantity_per_harvest": 2, + "required_tool_item_id": "basic_tool", + "allow_bare_hand_gathering": True, + "tool_quantity_bonus": 1, "respawns_for_mvp": True, "respawn_delay_seconds": 900.0, "max_harvests": 16, @@ -48,6 +51,9 @@ BLUEPRINTS = [ "yield_item_definition": FIBER_ITEM_PATH, "remaining_harvests": 10, "quantity_per_harvest": 3, + "required_tool_item_id": "basic_tool", + "allow_bare_hand_gathering": True, + "tool_quantity_bonus": 1, "respawns_for_mvp": True, "respawn_delay_seconds": 600.0, "max_harvests": 10, @@ -63,6 +69,9 @@ BLUEPRINTS = [ "yield_item_definition": STONE_ITEM_PATH, "remaining_harvests": 12, "quantity_per_harvest": 2, + "required_tool_item_id": "basic_tool", + "allow_bare_hand_gathering": True, + "tool_quantity_bonus": 1, "respawns_for_mvp": False, "respawn_delay_seconds": 1800.0, "max_harvests": 12, @@ -78,6 +87,8 @@ BLUEPRINTS = [ "yield_item_definition": FOOD_ITEM_PATH, "remaining_harvests": 8, "quantity_per_harvest": 1, + "allow_bare_hand_gathering": True, + "tool_quantity_bonus": 0, "respawns_for_mvp": True, "respawn_delay_seconds": 1200.0, "max_harvests": 8, diff --git a/Scripts/verify_playable_blueprints.py b/Scripts/verify_playable_blueprints.py index 48f29c3..0ff431f 100644 --- a/Scripts/verify_playable_blueprints.py +++ b/Scripts/verify_playable_blueprints.py @@ -6,6 +6,9 @@ EXPECTED = { "properties": { "remaining_harvests": 16, "quantity_per_harvest": 2, + "required_tool_item_id": "basic_tool", + "allow_bare_hand_gathering": True, + "tool_quantity_bonus": 1, "respawns_for_mvp": True, "respawn_delay_seconds": 900.0, "max_harvests": 16, @@ -16,6 +19,9 @@ EXPECTED = { "properties": { "remaining_harvests": 10, "quantity_per_harvest": 3, + "required_tool_item_id": "basic_tool", + "allow_bare_hand_gathering": True, + "tool_quantity_bonus": 1, "respawns_for_mvp": True, "respawn_delay_seconds": 600.0, "max_harvests": 10, @@ -26,6 +32,9 @@ EXPECTED = { "properties": { "remaining_harvests": 12, "quantity_per_harvest": 2, + "required_tool_item_id": "basic_tool", + "allow_bare_hand_gathering": True, + "tool_quantity_bonus": 1, "respawns_for_mvp": False, "respawn_delay_seconds": 1800.0, "max_harvests": 12, @@ -36,6 +45,9 @@ EXPECTED = { "properties": { "remaining_harvests": 8, "quantity_per_harvest": 1, + "required_tool_item_id": "None", + "allow_bare_hand_gathering": True, + "tool_quantity_bonus": 0, "respawns_for_mvp": True, "respawn_delay_seconds": 1200.0, "max_harvests": 8, diff --git a/Scripts/verify_resource_tool_requirements.py b/Scripts/verify_resource_tool_requirements.py new file mode 100644 index 0000000..438b5b2 --- /dev/null +++ b/Scripts/verify_resource_tool_requirements.py @@ -0,0 +1,76 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +FILES = { + "AgrarianResourceNode.h": ROOT / "Source" / "AgrarianGame" / "AgrarianResourceNode.h", + "AgrarianResourceNode.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianResourceNode.cpp", + "setup_playable_blueprints.py": ROOT / "Scripts" / "setup_playable_blueprints.py", + "verify_playable_blueprints.py": ROOT / "Scripts" / "verify_playable_blueprints.py", + "TechnicalDesignDocument.md": ROOT / "Docs" / "TechnicalDesignDocument.md", + "GroundZeroResourcePass.md": ROOT / "Docs" / "Terrain" / "GroundZeroResourcePass.md", + "Roadmap": ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md", +} + +REQUIRED_SNIPPETS = { + "AgrarianResourceNode.h": [ + "FName RequiredToolItemId;", + "bool bAllowBareHandGathering = true;", + "int32 ToolQuantityBonus = 0;", + "bool HasRequiredTool", + "int32 GetHarvestQuantityFor", + ], + "AgrarianResourceNode.cpp": [ + "Gather by hand", + "RequiredToolItemId == NAME_None || bAllowBareHandGathering || HasRequiredTool(Interactor)", + "Inventory->HasItem(RequiredToolItemId, 1)", + "QuantityPerHarvest + ToolBonus", + "MakeYieldStack(Interactor)", + ], + "setup_playable_blueprints.py": [ + '"required_tool_item_id": "basic_tool"', + '"allow_bare_hand_gathering": True', + '"tool_quantity_bonus": 1', + '"tool_quantity_bonus": 0', + ], + "verify_playable_blueprints.py": [ + '"required_tool_item_id": "basic_tool"', + '"allow_bare_hand_gathering": True', + '"tool_quantity_bonus": 1', + '"required_tool_item_id": "None"', + ], + "TechnicalDesignDocument.md": [ + "Tool requirement rules remain inventory-based", + "`basic_tool` in inventory", + ], + "GroundZeroResourcePass.md": [ + "Wood, fiber, and stone nodes declare `basic_tool`", + "Edible plant nodes remain pure bare-hand gathering", + ], + "Roadmap": [ + "[x] Add tool requirement rules.", + "basic tool in\n inventory improves yields", + ], +} + + +def main(): + missing = [] + for label, path in FILES.items(): + text = path.read_text(encoding="utf-8") + for snippet in REQUIRED_SNIPPETS[label]: + if snippet not in text: + missing.append(f"{label}: missing {snippet!r}") + + if missing: + raise SystemExit("Resource tool requirement verification failed:\n" + "\n".join(missing)) + + print( + "PASS: MVP resource tool requirement rules are inventory-based, " + "preserve bare-hand gathering, and add basic-tool yield bonuses." + ) + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianResourceNode.cpp b/Source/AgrarianGame/AgrarianResourceNode.cpp index d0cd4b4..a196c4e 100644 --- a/Source/AgrarianGame/AgrarianResourceNode.cpp +++ b/Source/AgrarianGame/AgrarianResourceNode.cpp @@ -47,12 +47,27 @@ void AAgrarianResourceNode::GetLifetimeReplicatedProps(TArray FText AAgrarianResourceNode::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const { - return RemainingHarvests > 0 ? FText::FromString(TEXT("Gather")) : FText::FromString(TEXT("Depleted")); + if (RemainingHarvests <= 0) + { + return FText::FromString(TEXT("Depleted")); + } + + if (RequiredToolItemId != NAME_None && !HasRequiredTool(Interactor) && bAllowBareHandGathering) + { + return FText::FromString(TEXT("Gather by hand")); + } + + return FText::FromString(TEXT("Gather")); } bool AAgrarianResourceNode::CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const { - return RemainingHarvests > 0 && Interactor != nullptr; + if (RemainingHarvests <= 0 || !Interactor) + { + return false; + } + + return RequiredToolItemId == NAME_None || bAllowBareHandGathering || HasRequiredTool(Interactor); } void AAgrarianResourceNode::Interact_Implementation(AAgrarianGameCharacter* Interactor) @@ -64,7 +79,7 @@ void AAgrarianResourceNode::Interact_Implementation(AAgrarianGameCharacter* Inte if (UAgrarianInventoryComponent* Inventory = Interactor->GetInventoryComponent()) { - const FAgrarianItemStack Granted = MakeYieldStack(); + const FAgrarianItemStack Granted = MakeYieldStack(Interactor); if (Inventory->AddItem(Granted)) { RemainingHarvests--; @@ -79,15 +94,33 @@ void AAgrarianResourceNode::OnRep_RemainingHarvests() UpdateDepletedState(); } -FAgrarianItemStack AAgrarianResourceNode::MakeYieldStack() const +bool AAgrarianResourceNode::HasRequiredTool(const AAgrarianGameCharacter* Interactor) const { + if (RequiredToolItemId == NAME_None) + { + return true; + } + + const UAgrarianInventoryComponent* Inventory = Interactor ? Interactor->GetInventoryComponent() : nullptr; + return Inventory && Inventory->HasItem(RequiredToolItemId, 1); +} + +int32 AAgrarianResourceNode::GetHarvestQuantityFor(const AAgrarianGameCharacter* Interactor) const +{ + const int32 ToolBonus = HasRequiredTool(Interactor) ? FMath::Max(0, ToolQuantityBonus) : 0; + return FMath::Max(1, QuantityPerHarvest + ToolBonus); +} + +FAgrarianItemStack AAgrarianResourceNode::MakeYieldStack(const AAgrarianGameCharacter* Interactor) const +{ + const int32 HarvestQuantity = GetHarvestQuantityFor(Interactor); if (YieldItemDefinition) { - return YieldItemDefinition->MakeStack(QuantityPerHarvest); + return YieldItemDefinition->MakeStack(HarvestQuantity); } FAgrarianItemStack Granted = YieldItem; - Granted.Quantity = QuantityPerHarvest; + Granted.Quantity = HarvestQuantity; return Granted; } diff --git a/Source/AgrarianGame/AgrarianResourceNode.h b/Source/AgrarianGame/AgrarianResourceNode.h index 5191a7a..c9af2e2 100644 --- a/Source/AgrarianGame/AgrarianResourceNode.h +++ b/Source/AgrarianGame/AgrarianResourceNode.h @@ -38,6 +38,15 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource", meta = (ClampMin = "1")) int32 QuantityPerHarvest = 1; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Tools") + FName RequiredToolItemId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Tools") + bool bAllowBareHandGathering = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Tools", meta = (ClampMin = "0")) + int32 ToolQuantityBonus = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Respawn") bool bRespawnsForMvp = false; @@ -55,7 +64,9 @@ protected: UFUNCTION() void OnRep_RemainingHarvests(); - FAgrarianItemStack MakeYieldStack() const; + bool HasRequiredTool(const AAgrarianGameCharacter* Interactor) const; + int32 GetHarvestQuantityFor(const AAgrarianGameCharacter* Interactor) const; + FAgrarianItemStack MakeYieldStack(const AAgrarianGameCharacter* Interactor) const; void UpdateDepletedState(); void ScheduleRespawnIfNeeded(); void RespawnNode();