diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 81dd5a0..42475fe 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -670,7 +670,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe inventory loss is not active yet. Future corpse/backpack recovery is reserved as a persistent, server-owned, interaction-gated death-recovery record once inventory loss and decay rules exist. -- [ ] Add replicated death feedback. +- [x] Add replicated death feedback. Survival now exposes + `OnDeathStateChanged` for UI/audio/animation hooks when replicated + alive/dead state or death reason changes, and the debug HUD shows the + replicated death reason while dead. ## 0.1.K Wildlife Prototype diff --git a/Scripts/verify_replicated_death_feedback.py b/Scripts/verify_replicated_death_feedback.py new file mode 100644 index 0000000..d85df19 --- /dev/null +++ b/Scripts/verify_replicated_death_feedback.py @@ -0,0 +1,34 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def require(path: Path, snippet: str) -> None: + text = path.read_text(encoding="utf-8") + if snippet not in text: + raise SystemExit(f"{path.relative_to(ROOT)} missing {snippet!r}") + + +def main() -> None: + header = ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.h" + source = ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.cpp" + hud = ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.cpp" + roadmap = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + + require(header, "DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAgrarianDeathStateChangedSignature") + require(header, "FAgrarianDeathStateChangedSignature OnDeathStateChanged;") + require(header, "bool bLastBroadcastDeathState = false;") + require(header, "FName LastBroadcastDeathReason = NAME_None;") + require(source, "OnSurvivalChanged.Broadcast(Survival);") + require(source, "bLastBroadcastDeathState != Survival.bIsDead") + require(source, "LastBroadcastDeathReason != Survival.LastDeathReason") + require(source, "OnDeathStateChanged.Broadcast(Survival.bIsDead, Survival.LastDeathReason);") + require(hud, "Death: %s") + require(roadmap, "[x] Add replicated death feedback.") + + print("PASS: replicated death feedback is present.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianDebugHUD.cpp b/Source/AgrarianGame/AgrarianDebugHUD.cpp index e2bd9ff..98db74d 100644 --- a/Source/AgrarianGame/AgrarianDebugHUD.cpp +++ b/Source/AgrarianGame/AgrarianDebugHUD.cpp @@ -336,6 +336,10 @@ void AAgrarianDebugHUD::DrawSurvival(const UAgrarianSurvivalComponent* SurvivalC DrawLine(FString::Printf(TEXT("Thirst: %.0f"), Survival.Thirst), X, Y); DrawLine(FString::Printf(TEXT("Temp: %.1f C"), Survival.BodyTemperature), X, Y); DrawLine(FString::Printf(TEXT("State: %s"), Survival.bIsDead ? TEXT("DEAD") : TEXT("ALIVE")), X, Y, Survival.bIsDead ? FColor::Red : FColor::Green); + if (Survival.bIsDead) + { + DrawLine(FString::Printf(TEXT("Death: %s"), *Survival.LastDeathReason.ToString()), X, Y, FColor::Red); + } DrawLine(FString::Printf(TEXT("Shelter: %.0f%%"), SurvivalComponent->CurrentWeatherProtection * 100.0f), X, Y); DrawLine(FString::Printf(TEXT("Expose: x%.2f %+3.1f C"), SurvivalComponent->CurrentWeatherExposureMultiplier, SurvivalComponent->CurrentWeatherTemperatureOffsetC), X, Y); DrawLine(FString::Printf(TEXT("Injury: %.0f"), Survival.InjurySeverity), X, Y); diff --git a/Source/AgrarianGame/AgrarianSurvivalComponent.cpp b/Source/AgrarianGame/AgrarianSurvivalComponent.cpp index 7f9d910..38ea330 100644 --- a/Source/AgrarianGame/AgrarianSurvivalComponent.cpp +++ b/Source/AgrarianGame/AgrarianSurvivalComponent.cpp @@ -482,6 +482,13 @@ void UAgrarianSurvivalComponent::ClampCareHistory() void UAgrarianSurvivalComponent::BroadcastSurvivalChanged() { OnSurvivalChanged.Broadcast(Survival); + + if (bLastBroadcastDeathState != Survival.bIsDead || LastBroadcastDeathReason != Survival.LastDeathReason) + { + bLastBroadcastDeathState = Survival.bIsDead; + LastBroadcastDeathReason = Survival.LastDeathReason; + OnDeathStateChanged.Broadcast(Survival.bIsDead, Survival.LastDeathReason); + } } void UAgrarianSurvivalComponent::UpdateDeathState() diff --git a/Source/AgrarianGame/AgrarianSurvivalComponent.h b/Source/AgrarianGame/AgrarianSurvivalComponent.h index 66d52a2..3e98b95 100644 --- a/Source/AgrarianGame/AgrarianSurvivalComponent.h +++ b/Source/AgrarianGame/AgrarianSurvivalComponent.h @@ -8,6 +8,7 @@ #include "AgrarianSurvivalComponent.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianSurvivalChangedSignature, const FAgrarianSurvivalSnapshot&, Snapshot); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAgrarianDeathStateChangedSignature, bool, bIsDead, FName, DeathReason); UCLASS(ClassGroup = (Agrarian), BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent)) class UAgrarianSurvivalComponent : public UActorComponent @@ -24,6 +25,9 @@ public: UPROPERTY(BlueprintAssignable, Category = "Agrarian|Survival") FAgrarianSurvivalChangedSignature OnSurvivalChanged; + UPROPERTY(BlueprintAssignable, Category = "Agrarian|Survival") + FAgrarianDeathStateChangedSignature OnDeathStateChanged; + UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_Survival, Category = "Agrarian|Survival") FAgrarianSurvivalSnapshot Survival; @@ -164,4 +168,7 @@ protected: void ClampCareHistory(); void UpdateDeathState(); void BroadcastSurvivalChanged(); + + bool bLastBroadcastDeathState = false; + FName LastBroadcastDeathReason = NAME_None; };