diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 686db9e..c85526a 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -541,7 +541,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe component on server authority, is placed in Ground Zero, and is covered by water-source and interaction verifiers. - [x] Add resource depletion. -- [ ] Add respawn rules for MVP. +- [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 bare-hand gathering fallback. - [ ] Add resource node persistence. diff --git a/Content/Agrarian/Blueprints/Resources/BP_EdiblePlantResourceNode.uasset b/Content/Agrarian/Blueprints/Resources/BP_EdiblePlantResourceNode.uasset index fd42a9d..99db7d1 100644 --- a/Content/Agrarian/Blueprints/Resources/BP_EdiblePlantResourceNode.uasset +++ b/Content/Agrarian/Blueprints/Resources/BP_EdiblePlantResourceNode.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cacd9ec819a6ddbf4446aa24250bb5f2ddb738fa118cff7f9d9fcf302818fdf -size 24716 +oid sha256:fc5f8c56148e934725e031522314bdaa0224fa9dde8d55580f035ecc774bd56b +size 24871 diff --git a/Content/Agrarian/Blueprints/Resources/BP_FiberResourceNode.uasset b/Content/Agrarian/Blueprints/Resources/BP_FiberResourceNode.uasset index e835241..740a882 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:8e50d9a974731203e3b47f86ac57da3dbd35ba7917e3f1b2d42ee649f40177d2 -size 24658 +oid sha256:cfeba50e8db9627195f23394437ae859e78c97f78a3b157f4234503742c2eba8 +size 24813 diff --git a/Content/Agrarian/Blueprints/Resources/BP_StoneResourceNode.uasset b/Content/Agrarian/Blueprints/Resources/BP_StoneResourceNode.uasset index 816972b..4bdf798 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:f341aff0f1209840088e12c7ea2007ea0822dfabe604504146fd9773cc84dd81 -size 24650 +oid sha256:279c707e5f813cc7ff38a514948c4c92ca2976955a9cea430bb5ef19d3d1efd7 +size 24756 diff --git a/Content/Agrarian/Blueprints/Resources/BP_WoodResourceNode.uasset b/Content/Agrarian/Blueprints/Resources/BP_WoodResourceNode.uasset index 1ad88ed..b38a3df 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:130c0664a96b4a645f85581b3f47f84eb052ee8f41152cf1685ae8f0703b97e7 -size 24634 +oid sha256:06eda61a4118656d12b6240585407fa75a4f29ca39562e9a1e83870316907bbe +size 24732 diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index f866eeb..16984a2 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -438,6 +438,16 @@ authority. This keeps drinking compatible with the existing replicated survival component while leaving later container filling, water quality, and source depletion rules for future water-system work. +For the MVP, renewable MVP resource nodes respawn only after they are fully +depleted. Wood, fiber, and edible plant nodes are renewable because they +represent surface materials and forage that should return during continued +prototype play. Stone remains nonrespawning because it represents a slower +geologic resource and should push players to explore once local easy stone is +gone. Respawn timing is configurable per Blueprint through +`bRespawnsForMvp`, `RespawnDelaySeconds`, and `MaxHarvests`; the native node +uses a server-side timer and restores replicated `RemainingHarvests` when the +delay expires. + ### Wildlife Navigation MVP wildlife movement is server authoritative. `AAgrarianWildlifeBase` uses an diff --git a/Docs/Terrain/GroundZeroResourcePass.md b/Docs/Terrain/GroundZeroResourcePass.md index 6b146f5..5a79df2 100644 --- a/Docs/Terrain/GroundZeroResourcePass.md +++ b/Docs/Terrain/GroundZeroResourcePass.md @@ -41,6 +41,19 @@ The map now contains: These counts include the original demo wood and fiber nodes so the first player path remains intact while expanding the broader tile resource layer. +## Respawn Rules + +MVP respawn rules separate renewable surface resources from nonrenewable stone: + +- Wood nodes respawn after `900` seconds once fully depleted. +- Fiber nodes respawn after `600` seconds once fully depleted. +- Edible plant nodes respawn after `1200` seconds once fully depleted. +- Stone nodes do not respawn in the MVP. + +Respawn restores the node to its configured `MaxHarvests` value and re-enables +visibility/collision through the same replicated depletion state used by +gathering. + ## 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 aa6ef02..1f5a3d4 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, + "respawns_for_mvp": True, + "respawn_delay_seconds": 900.0, + "max_harvests": 16, }, "mesh": MESH_CUBE_PATH, "scale": unreal.Vector(1.0, 1.0, 1.5), @@ -45,6 +48,9 @@ BLUEPRINTS = [ "yield_item_definition": FIBER_ITEM_PATH, "remaining_harvests": 10, "quantity_per_harvest": 3, + "respawns_for_mvp": True, + "respawn_delay_seconds": 600.0, + "max_harvests": 10, }, "mesh": MESH_CYLINDER_PATH, "scale": unreal.Vector(0.8, 0.8, 1.0), @@ -57,6 +63,9 @@ BLUEPRINTS = [ "yield_item_definition": STONE_ITEM_PATH, "remaining_harvests": 12, "quantity_per_harvest": 2, + "respawns_for_mvp": False, + "respawn_delay_seconds": 1800.0, + "max_harvests": 12, }, "mesh": MESH_CUBE_PATH, "scale": unreal.Vector(0.9, 0.75, 0.45), @@ -69,6 +78,9 @@ BLUEPRINTS = [ "yield_item_definition": FOOD_ITEM_PATH, "remaining_harvests": 8, "quantity_per_harvest": 1, + "respawns_for_mvp": True, + "respawn_delay_seconds": 1200.0, + "max_harvests": 8, }, "mesh": MESH_CYLINDER_PATH, "scale": unreal.Vector(0.65, 0.65, 0.85), diff --git a/Scripts/verify_playable_blueprints.py b/Scripts/verify_playable_blueprints.py index 5850823..48f29c3 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, + "respawns_for_mvp": True, + "respawn_delay_seconds": 900.0, + "max_harvests": 16, }, "yield_item_id": "wood", }, @@ -13,6 +16,9 @@ EXPECTED = { "properties": { "remaining_harvests": 10, "quantity_per_harvest": 3, + "respawns_for_mvp": True, + "respawn_delay_seconds": 600.0, + "max_harvests": 10, }, "yield_item_id": "fiber", }, @@ -20,6 +26,9 @@ EXPECTED = { "properties": { "remaining_harvests": 12, "quantity_per_harvest": 2, + "respawns_for_mvp": False, + "respawn_delay_seconds": 1800.0, + "max_harvests": 12, }, "yield_item_id": "stone", }, @@ -27,6 +36,9 @@ EXPECTED = { "properties": { "remaining_harvests": 8, "quantity_per_harvest": 1, + "respawns_for_mvp": True, + "respawn_delay_seconds": 1200.0, + "max_harvests": 8, }, "yield_item_id": "food", }, diff --git a/Scripts/verify_resource_respawn_rules.py b/Scripts/verify_resource_respawn_rules.py new file mode 100644 index 0000000..a9162fa --- /dev/null +++ b/Scripts/verify_resource_respawn_rules.py @@ -0,0 +1,75 @@ +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", + "Roadmap": ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md", +} + +REQUIRED_SNIPPETS = { + "AgrarianResourceNode.h": [ + "bool bRespawnsForMvp = false;", + "float RespawnDelaySeconds = 900.0f;", + "int32 MaxHarvests = 5;", + "void ScheduleRespawnIfNeeded();", + "void RespawnNode();", + "FTimerHandle RespawnTimerHandle;", + ], + "AgrarianResourceNode.cpp": [ + "ScheduleRespawnIfNeeded();", + "!bRespawnsForMvp || RemainingHarvests > 0", + "World->GetTimerManager().SetTimer", + "FMath::Max(1.0f, RespawnDelaySeconds)", + "RemainingHarvests = FMath::Max(1, MaxHarvests);", + "UpdateDepletedState();", + ], + "setup_playable_blueprints.py": [ + '"respawns_for_mvp": True', + '"respawns_for_mvp": False', + '"respawn_delay_seconds": 600.0', + '"respawn_delay_seconds": 900.0', + '"respawn_delay_seconds": 1200.0', + '"max_harvests": 16', + ], + "verify_playable_blueprints.py": [ + '"respawns_for_mvp": True', + '"respawns_for_mvp": False', + '"respawn_delay_seconds": 1800.0', + '"max_harvests": 12', + ], + "TechnicalDesignDocument.md": [ + "renewable MVP resource nodes respawn", + "Stone remains nonrespawning", + ], + "Roadmap": [ + "[x] Add respawn rules for MVP.", + "renewable surface resources respawn", + ], +} + + +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 respawn rule verification failed:\n" + "\n".join(missing)) + + print( + "PASS: MVP resource respawn rules are configured for renewable nodes, " + "exclude nonrenewable stone, and are covered by Blueprint verification." + ) + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianResourceNode.cpp b/Source/AgrarianGame/AgrarianResourceNode.cpp index 9e5d1f5..d0cd4b4 100644 --- a/Source/AgrarianGame/AgrarianResourceNode.cpp +++ b/Source/AgrarianGame/AgrarianResourceNode.cpp @@ -5,6 +5,7 @@ #include "AgrarianInventoryComponent.h" #include "AgrarianItemDefinitionAsset.h" #include "Components/StaticMeshComponent.h" +#include "TimerManager.h" #include "Net/UnrealNetwork.h" AAgrarianResourceNode::AAgrarianResourceNode() @@ -21,6 +22,23 @@ AAgrarianResourceNode::AAgrarianResourceNode() YieldItem.UnitWeight = 1.0f; } +void AAgrarianResourceNode::BeginPlay() +{ + Super::BeginPlay(); + + if (HasAuthority()) + { + MaxHarvests = FMath::Max(1, MaxHarvests); + if (RemainingHarvests > MaxHarvests) + { + MaxHarvests = RemainingHarvests; + } + } + + UpdateDepletedState(); + ScheduleRespawnIfNeeded(); +} + void AAgrarianResourceNode::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); @@ -51,6 +69,7 @@ void AAgrarianResourceNode::Interact_Implementation(AAgrarianGameCharacter* Inte { RemainingHarvests--; UpdateDepletedState(); + ScheduleRespawnIfNeeded(); } } } @@ -80,3 +99,35 @@ void AAgrarianResourceNode::UpdateDepletedState() Mesh->SetCollisionEnabled(RemainingHarvests > 0 ? ECollisionEnabled::QueryAndPhysics : ECollisionEnabled::NoCollision); } } + +void AAgrarianResourceNode::ScheduleRespawnIfNeeded() +{ + if (!HasAuthority() || !bRespawnsForMvp || RemainingHarvests > 0) + { + return; + } + + UWorld* World = GetWorld(); + if (!World || World->GetTimerManager().IsTimerActive(RespawnTimerHandle)) + { + return; + } + + World->GetTimerManager().SetTimer( + RespawnTimerHandle, + this, + &AAgrarianResourceNode::RespawnNode, + FMath::Max(1.0f, RespawnDelaySeconds), + false); +} + +void AAgrarianResourceNode::RespawnNode() +{ + if (!HasAuthority() || !bRespawnsForMvp) + { + return; + } + + RemainingHarvests = FMath::Max(1, MaxHarvests); + UpdateDepletedState(); +} diff --git a/Source/AgrarianGame/AgrarianResourceNode.h b/Source/AgrarianGame/AgrarianResourceNode.h index dd14a48..5191a7a 100644 --- a/Source/AgrarianGame/AgrarianResourceNode.h +++ b/Source/AgrarianGame/AgrarianResourceNode.h @@ -4,6 +4,7 @@ #include "CoreMinimal.h" #include "GameFramework/Actor.h" +#include "TimerManager.h" #include "AgrarianInteractable.h" #include "AgrarianTypes.h" #include "AgrarianResourceNode.generated.h" @@ -19,6 +20,7 @@ class AAgrarianResourceNode : public AActor, public IAgrarianInteractable public: AAgrarianResourceNode(); + virtual void BeginPlay() override; virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Resource") @@ -36,6 +38,15 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource", meta = (ClampMin = "1")) int32 QuantityPerHarvest = 1; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Respawn") + bool bRespawnsForMvp = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Respawn", meta = (ClampMin = "1")) + float RespawnDelaySeconds = 900.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Respawn", meta = (ClampMin = "1")) + int32 MaxHarvests = 5; + virtual FText GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const override; virtual bool CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const override; virtual void Interact_Implementation(AAgrarianGameCharacter* Interactor) override; @@ -46,4 +57,8 @@ protected: FAgrarianItemStack MakeYieldStack() const; void UpdateDepletedState(); + void ScheduleRespawnIfNeeded(); + void RespawnNode(); + + FTimerHandle RespawnTimerHandle; };