From 0ac4bec3cfc249aaf8532b4368a309598de5714c Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 17 May 2026 18:50:59 -0700 Subject: [PATCH] Add campfire cooking placeholder --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 4 +- Docs/TechnicalDesignDocument.md | 5 ++ Scripts/setup_playable_blueprints.py | 18 ++++++- Scripts/verify_fire_cooking_placeholder.py | 56 ++++++++++++++++++++++ Scripts/verify_playable_blueprints.py | 17 ++++++- Source/AgrarianGame/AgrarianCampfire.cpp | 23 +++++++++ Source/AgrarianGame/AgrarianCampfire.h | 15 ++++++ 7 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 Scripts/verify_fire_cooking_placeholder.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 925d22e..68e725e 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -587,7 +587,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe update path used by natural fuel depletion. - [x] Add warmth radius. - [x] Add light source. -- [ ] Add cooking placeholder if needed. +- [x] Add cooking placeholder if needed. Added replicated campfire cooking + placeholder state, a native cookability check, and progress ratio helpers so + later recipe UI and food systems have a stable hook. - [ ] Add smoke/visual effect placeholder. - [x] Add replication. - [ ] Add persistence. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index ea52f9d..764c7eb 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -292,6 +292,11 @@ Campfires expose native extinguish logic through `AAgrarianCampfire::Extinguish` Extinguishing clears remaining fuel, turns off replicated lit state, and reuses the same visual update path as natural fuel depletion. +Campfires also expose a minimal replicated cooking placeholder. While lit and +enabled, `AAgrarianCampfire` advances `CookingProgressSeconds` toward +`CookingSecondsRequired` on the server and exposes `CanCook` plus a normalized +progress ratio for future recipe UI, food transformation, and interaction hooks. + The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the current temperature, daily low/high, precipitation, wind, humidity, cloud cover, diff --git a/Scripts/setup_playable_blueprints.py b/Scripts/setup_playable_blueprints.py index 2c133a4..53160cb 100644 --- a/Scripts/setup_playable_blueprints.py +++ b/Scripts/setup_playable_blueprints.py @@ -207,6 +207,22 @@ def make_stack(data): return stack +def set_default_property(cdo, property_name, value): + property_names = [property_name] + if isinstance(value, bool): + property_names.append(f"b_{property_name}") + property_names.append("b" + "".join(part.capitalize() for part in property_name.split("_"))) + + for reflected_name in property_names: + try: + cdo.set_editor_property(reflected_name, value) + return + except Exception: + continue + + raise RuntimeError(f"Could not set default property {property_name}") + + def apply_defaults(blueprint, config): blueprint_path = f"{config['folder']}/{config['asset']}" unreal.BlueprintEditorLibrary.compile_blueprint(blueprint) @@ -222,7 +238,7 @@ def apply_defaults(blueprint, config): elif property_name == "harvest_yields": value = [make_stack(stack_data) for stack_data in value] - cdo.set_editor_property(property_name, value) + set_default_property(cdo, property_name, value) mesh_path = config.get("mesh") if mesh_path: diff --git a/Scripts/verify_fire_cooking_placeholder.py b/Scripts/verify_fire_cooking_placeholder.py new file mode 100644 index 0000000..7a3105b --- /dev/null +++ b/Scripts/verify_fire_cooking_placeholder.py @@ -0,0 +1,56 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +REQUIRED = { + ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.h": [ + "bool bCookingPlaceholderEnabled = true;", + "float CookingSecondsRequired = 30.0f;", + "float CookingProgressSeconds = 0.0f;", + "bool CanCook() const;", + "float GetCookingProgressRatio() const;", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.cpp": [ + "DOREPLIFETIME(AAgrarianCampfire, bCookingPlaceholderEnabled);", + "DOREPLIFETIME(AAgrarianCampfire, CookingSecondsRequired);", + "DOREPLIFETIME(AAgrarianCampfire, CookingProgressSeconds);", + "CookingProgressSeconds = FMath::Min(CookingSecondsRequired, CookingProgressSeconds + DeltaSeconds);", + "bool AAgrarianCampfire::CanCook() const", + "float AAgrarianCampfire::GetCookingProgressRatio() const", + ], + ROOT / "Scripts" / "setup_playable_blueprints.py": [ + "def set_default_property(cdo, property_name, value):", + "property_names.append(f\"b_{property_name}\")", + "property_names.append(\"b\" + \"\".join(part.capitalize() for part in property_name.split(\"_\")))", + ], + ROOT / "Scripts" / "verify_playable_blueprints.py": [ + "def get_property(cdo, property_name, expected_value):", + "property_names.append(f\"b_{property_name}\")", + "property_names.append(\"b\" + \"\".join(part.capitalize() for part in property_name.split(\"_\")))", + ], + ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "- [x] Add cooking placeholder if needed.", + ], + ROOT / "Docs" / "TechnicalDesignDocument.md": [ + "minimal replicated cooking placeholder", + ], +} + + +def main(): + missing = [] + for path, snippets in REQUIRED.items(): + text = path.read_text(encoding="utf-8") + for snippet in snippets: + if snippet not in text: + missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}") + + if missing: + raise SystemExit("Campfire cooking placeholder verification failed:\n" + "\n".join(missing)) + + print("PASS: campfire cooking placeholder is implemented and documented.") + + +if __name__ == "__main__": + main() diff --git a/Scripts/verify_playable_blueprints.py b/Scripts/verify_playable_blueprints.py index 0ff431f..c62b82a 100644 --- a/Scripts/verify_playable_blueprints.py +++ b/Scripts/verify_playable_blueprints.py @@ -94,6 +94,21 @@ def nearly_equal(left, right): return left == right +def get_property(cdo, property_name, expected_value): + property_names = [property_name] + if isinstance(expected_value, bool): + property_names.append(f"b_{property_name}") + property_names.append("b" + "".join(part.capitalize() for part in property_name.split("_"))) + + for reflected_name in property_names: + try: + return cdo.get_editor_property(reflected_name) + except Exception: + continue + + raise RuntimeError(f"Could not read property {property_name}") + + def main(): failures = [] @@ -110,7 +125,7 @@ def main(): cdo = unreal.get_default_object(generated_class) for property_name, expected_value in expected.get("properties", {}).items(): - actual_value = cdo.get_editor_property(property_name) + actual_value = get_property(cdo, property_name, expected_value) if str(actual_value) != expected_value and not nearly_equal(actual_value, expected_value): failures.append(f"{path} {property_name} expected {expected_value}, got {actual_value}") diff --git a/Source/AgrarianGame/AgrarianCampfire.cpp b/Source/AgrarianGame/AgrarianCampfire.cpp index b28ae13..c075cc3 100644 --- a/Source/AgrarianGame/AgrarianCampfire.cpp +++ b/Source/AgrarianGame/AgrarianCampfire.cpp @@ -36,6 +36,11 @@ void AAgrarianCampfire::Tick(float DeltaSeconds) SetLit(false); } + if (CanCook()) + { + CookingProgressSeconds = FMath::Min(CookingSecondsRequired, CookingProgressSeconds + DeltaSeconds); + } + WarmNearbyCharacters(DeltaSeconds); } } @@ -45,6 +50,9 @@ void AAgrarianCampfire::GetLifetimeReplicatedProps(TArray& Ou Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AAgrarianCampfire, bLit); DOREPLIFETIME(AAgrarianCampfire, FuelSeconds); + DOREPLIFETIME(AAgrarianCampfire, bCookingPlaceholderEnabled); + DOREPLIFETIME(AAgrarianCampfire, CookingSecondsRequired); + DOREPLIFETIME(AAgrarianCampfire, CookingProgressSeconds); } FText AAgrarianCampfire::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const @@ -96,6 +104,21 @@ void AAgrarianCampfire::Extinguish() } } +bool AAgrarianCampfire::CanCook() const +{ + return bLit && bCookingPlaceholderEnabled && CookingSecondsRequired > 0.0f; +} + +float AAgrarianCampfire::GetCookingProgressRatio() const +{ + if (CookingSecondsRequired <= 0.0f) + { + return 0.0f; + } + + return FMath::Clamp(CookingProgressSeconds / CookingSecondsRequired, 0.0f, 1.0f); +} + void AAgrarianCampfire::OnRep_FireState() { UpdateVisualState(); diff --git a/Source/AgrarianGame/AgrarianCampfire.h b/Source/AgrarianGame/AgrarianCampfire.h index b36c5e1..56b04ad 100644 --- a/Source/AgrarianGame/AgrarianCampfire.h +++ b/Source/AgrarianGame/AgrarianCampfire.h @@ -39,6 +39,15 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire", meta = (ClampMin = "0")) float WarmthPerSecond = 0.02f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Cooking") + bool bCookingPlaceholderEnabled = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Cooking", meta = (ClampMin = "0")) + float CookingSecondsRequired = 30.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Cooking", meta = (ClampMin = "0")) + float CookingProgressSeconds = 0.0f; + 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; @@ -49,6 +58,12 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Fire") void Extinguish(); + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Agrarian|Fire|Cooking") + bool CanCook() const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Agrarian|Fire|Cooking") + float GetCookingProgressRatio() const; + protected: UFUNCTION() void OnRep_FireState();