Add campfire cooking placeholder

This commit is contained in:
2026-05-17 18:50:59 -07:00
parent c61b226f47
commit 0ac4bec3cf
7 changed files with 135 additions and 3 deletions
+3 -1
View File
@@ -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. update path used by natural fuel depletion.
- [x] Add warmth radius. - [x] Add warmth radius.
- [x] Add light source. - [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. - [ ] Add smoke/visual effect placeholder.
- [x] Add replication. - [x] Add replication.
- [ ] Add persistence. - [ ] Add persistence.
+5
View File
@@ -292,6 +292,11 @@ Campfires expose native extinguish logic through `AAgrarianCampfire::Extinguish`
Extinguishing clears remaining fuel, turns off replicated lit state, and reuses Extinguishing clears remaining fuel, turns off replicated lit state, and reuses
the same visual update path as natural fuel depletion. 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 The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses
Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the
current temperature, daily low/high, precipitation, wind, humidity, cloud cover, current temperature, daily low/high, precipitation, wind, humidity, cloud cover,
+17 -1
View File
@@ -207,6 +207,22 @@ def make_stack(data):
return stack 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): def apply_defaults(blueprint, config):
blueprint_path = f"{config['folder']}/{config['asset']}" blueprint_path = f"{config['folder']}/{config['asset']}"
unreal.BlueprintEditorLibrary.compile_blueprint(blueprint) unreal.BlueprintEditorLibrary.compile_blueprint(blueprint)
@@ -222,7 +238,7 @@ def apply_defaults(blueprint, config):
elif property_name == "harvest_yields": elif property_name == "harvest_yields":
value = [make_stack(stack_data) for stack_data in value] 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") mesh_path = config.get("mesh")
if mesh_path: if mesh_path:
@@ -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()
+16 -1
View File
@@ -94,6 +94,21 @@ def nearly_equal(left, right):
return 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(): def main():
failures = [] failures = []
@@ -110,7 +125,7 @@ def main():
cdo = unreal.get_default_object(generated_class) cdo = unreal.get_default_object(generated_class)
for property_name, expected_value in expected.get("properties", {}).items(): 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): 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}") failures.append(f"{path} {property_name} expected {expected_value}, got {actual_value}")
+23
View File
@@ -36,6 +36,11 @@ void AAgrarianCampfire::Tick(float DeltaSeconds)
SetLit(false); SetLit(false);
} }
if (CanCook())
{
CookingProgressSeconds = FMath::Min(CookingSecondsRequired, CookingProgressSeconds + DeltaSeconds);
}
WarmNearbyCharacters(DeltaSeconds); WarmNearbyCharacters(DeltaSeconds);
} }
} }
@@ -45,6 +50,9 @@ void AAgrarianCampfire::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Ou
Super::GetLifetimeReplicatedProps(OutLifetimeProps); Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AAgrarianCampfire, bLit); DOREPLIFETIME(AAgrarianCampfire, bLit);
DOREPLIFETIME(AAgrarianCampfire, FuelSeconds); DOREPLIFETIME(AAgrarianCampfire, FuelSeconds);
DOREPLIFETIME(AAgrarianCampfire, bCookingPlaceholderEnabled);
DOREPLIFETIME(AAgrarianCampfire, CookingSecondsRequired);
DOREPLIFETIME(AAgrarianCampfire, CookingProgressSeconds);
} }
FText AAgrarianCampfire::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const 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() void AAgrarianCampfire::OnRep_FireState()
{ {
UpdateVisualState(); UpdateVisualState();
+15
View File
@@ -39,6 +39,15 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire", meta = (ClampMin = "0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire", meta = (ClampMin = "0"))
float WarmthPerSecond = 0.02f; 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 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;
@@ -49,6 +58,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|Fire") UFUNCTION(BlueprintCallable, Category = "Agrarian|Fire")
void Extinguish(); void Extinguish();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Agrarian|Fire|Cooking")
bool CanCook() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Agrarian|Fire|Cooking")
float GetCookingProgressRatio() const;
protected: protected:
UFUNCTION() UFUNCTION()
void OnRep_FireState(); void OnRep_FireState();