Add MVP resource respawn rules

This commit is contained in:
2026-05-17 16:26:31 -07:00
parent fa9d1835f9
commit a5ec210cd8
12 changed files with 199 additions and 9 deletions
+3 -1
View File
@@ -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 component on server authority, is placed in Ground Zero, and is covered by
water-source and interaction verifiers. water-source and interaction verifiers.
- [x] Add resource depletion. - [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. - [ ] Add tool requirement rules.
- [x] Add bare-hand gathering fallback. - [x] Add bare-hand gathering fallback.
- [ ] Add resource node persistence. - [ ] Add resource node persistence.
+10
View File
@@ -438,6 +438,16 @@ authority. This keeps drinking compatible with the existing replicated survival
component while leaving later container filling, water quality, and source component while leaving later container filling, water quality, and source
depletion rules for future water-system work. 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 ### Wildlife Navigation
MVP wildlife movement is server authoritative. `AAgrarianWildlifeBase` uses an MVP wildlife movement is server authoritative. `AAgrarianWildlifeBase` uses an
+13
View File
@@ -41,6 +41,19 @@ The map now contains:
These counts include the original demo wood and fiber nodes so the first player These counts include the original demo wood and fiber nodes so the first player
path remains intact while expanding the broader tile resource layer. 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 ## Follow-Up
Future passes should replace the prototype meshes with real coastal scrub, Future passes should replace the prototype meshes with real coastal scrub,
+12
View File
@@ -33,6 +33,9 @@ BLUEPRINTS = [
"yield_item_definition": WOOD_ITEM_PATH, "yield_item_definition": WOOD_ITEM_PATH,
"remaining_harvests": 16, "remaining_harvests": 16,
"quantity_per_harvest": 2, "quantity_per_harvest": 2,
"respawns_for_mvp": True,
"respawn_delay_seconds": 900.0,
"max_harvests": 16,
}, },
"mesh": MESH_CUBE_PATH, "mesh": MESH_CUBE_PATH,
"scale": unreal.Vector(1.0, 1.0, 1.5), "scale": unreal.Vector(1.0, 1.0, 1.5),
@@ -45,6 +48,9 @@ BLUEPRINTS = [
"yield_item_definition": FIBER_ITEM_PATH, "yield_item_definition": FIBER_ITEM_PATH,
"remaining_harvests": 10, "remaining_harvests": 10,
"quantity_per_harvest": 3, "quantity_per_harvest": 3,
"respawns_for_mvp": True,
"respawn_delay_seconds": 600.0,
"max_harvests": 10,
}, },
"mesh": MESH_CYLINDER_PATH, "mesh": MESH_CYLINDER_PATH,
"scale": unreal.Vector(0.8, 0.8, 1.0), "scale": unreal.Vector(0.8, 0.8, 1.0),
@@ -57,6 +63,9 @@ BLUEPRINTS = [
"yield_item_definition": STONE_ITEM_PATH, "yield_item_definition": STONE_ITEM_PATH,
"remaining_harvests": 12, "remaining_harvests": 12,
"quantity_per_harvest": 2, "quantity_per_harvest": 2,
"respawns_for_mvp": False,
"respawn_delay_seconds": 1800.0,
"max_harvests": 12,
}, },
"mesh": MESH_CUBE_PATH, "mesh": MESH_CUBE_PATH,
"scale": unreal.Vector(0.9, 0.75, 0.45), "scale": unreal.Vector(0.9, 0.75, 0.45),
@@ -69,6 +78,9 @@ BLUEPRINTS = [
"yield_item_definition": FOOD_ITEM_PATH, "yield_item_definition": FOOD_ITEM_PATH,
"remaining_harvests": 8, "remaining_harvests": 8,
"quantity_per_harvest": 1, "quantity_per_harvest": 1,
"respawns_for_mvp": True,
"respawn_delay_seconds": 1200.0,
"max_harvests": 8,
}, },
"mesh": MESH_CYLINDER_PATH, "mesh": MESH_CYLINDER_PATH,
"scale": unreal.Vector(0.65, 0.65, 0.85), "scale": unreal.Vector(0.65, 0.65, 0.85),
+12
View File
@@ -6,6 +6,9 @@ EXPECTED = {
"properties": { "properties": {
"remaining_harvests": 16, "remaining_harvests": 16,
"quantity_per_harvest": 2, "quantity_per_harvest": 2,
"respawns_for_mvp": True,
"respawn_delay_seconds": 900.0,
"max_harvests": 16,
}, },
"yield_item_id": "wood", "yield_item_id": "wood",
}, },
@@ -13,6 +16,9 @@ EXPECTED = {
"properties": { "properties": {
"remaining_harvests": 10, "remaining_harvests": 10,
"quantity_per_harvest": 3, "quantity_per_harvest": 3,
"respawns_for_mvp": True,
"respawn_delay_seconds": 600.0,
"max_harvests": 10,
}, },
"yield_item_id": "fiber", "yield_item_id": "fiber",
}, },
@@ -20,6 +26,9 @@ EXPECTED = {
"properties": { "properties": {
"remaining_harvests": 12, "remaining_harvests": 12,
"quantity_per_harvest": 2, "quantity_per_harvest": 2,
"respawns_for_mvp": False,
"respawn_delay_seconds": 1800.0,
"max_harvests": 12,
}, },
"yield_item_id": "stone", "yield_item_id": "stone",
}, },
@@ -27,6 +36,9 @@ EXPECTED = {
"properties": { "properties": {
"remaining_harvests": 8, "remaining_harvests": 8,
"quantity_per_harvest": 1, "quantity_per_harvest": 1,
"respawns_for_mvp": True,
"respawn_delay_seconds": 1200.0,
"max_harvests": 8,
}, },
"yield_item_id": "food", "yield_item_id": "food",
}, },
+75
View File
@@ -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()
@@ -5,6 +5,7 @@
#include "AgrarianInventoryComponent.h" #include "AgrarianInventoryComponent.h"
#include "AgrarianItemDefinitionAsset.h" #include "AgrarianItemDefinitionAsset.h"
#include "Components/StaticMeshComponent.h" #include "Components/StaticMeshComponent.h"
#include "TimerManager.h"
#include "Net/UnrealNetwork.h" #include "Net/UnrealNetwork.h"
AAgrarianResourceNode::AAgrarianResourceNode() AAgrarianResourceNode::AAgrarianResourceNode()
@@ -21,6 +22,23 @@ AAgrarianResourceNode::AAgrarianResourceNode()
YieldItem.UnitWeight = 1.0f; 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<FLifetimeProperty>& OutLifetimeProps) const void AAgrarianResourceNode::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{ {
Super::GetLifetimeReplicatedProps(OutLifetimeProps); Super::GetLifetimeReplicatedProps(OutLifetimeProps);
@@ -51,6 +69,7 @@ void AAgrarianResourceNode::Interact_Implementation(AAgrarianGameCharacter* Inte
{ {
RemainingHarvests--; RemainingHarvests--;
UpdateDepletedState(); UpdateDepletedState();
ScheduleRespawnIfNeeded();
} }
} }
} }
@@ -80,3 +99,35 @@ void AAgrarianResourceNode::UpdateDepletedState()
Mesh->SetCollisionEnabled(RemainingHarvests > 0 ? ECollisionEnabled::QueryAndPhysics : ECollisionEnabled::NoCollision); 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();
}
@@ -4,6 +4,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/Actor.h" #include "GameFramework/Actor.h"
#include "TimerManager.h"
#include "AgrarianInteractable.h" #include "AgrarianInteractable.h"
#include "AgrarianTypes.h" #include "AgrarianTypes.h"
#include "AgrarianResourceNode.generated.h" #include "AgrarianResourceNode.generated.h"
@@ -19,6 +20,7 @@ class AAgrarianResourceNode : public AActor, public IAgrarianInteractable
public: public:
AAgrarianResourceNode(); AAgrarianResourceNode();
virtual void BeginPlay() override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override; virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Resource") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Resource")
@@ -36,6 +38,15 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource", meta = (ClampMin = "1")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource", meta = (ClampMin = "1"))
int32 QuantityPerHarvest = 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 FText GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const override;
virtual bool CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const override; virtual bool CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const override;
virtual void Interact_Implementation(AAgrarianGameCharacter* Interactor) override; virtual void Interact_Implementation(AAgrarianGameCharacter* Interactor) override;
@@ -46,4 +57,8 @@ protected:
FAgrarianItemStack MakeYieldStack() const; FAgrarianItemStack MakeYieldStack() const;
void UpdateDepletedState(); void UpdateDepletedState();
void ScheduleRespawnIfNeeded();
void RespawnNode();
FTimerHandle RespawnTimerHandle;
}; };