diff --git a/Source/Framework/Framework.Build.cs b/Source/Framework/Framework.Build.cs new file mode 100644 index 0000000..face457 --- /dev/null +++ b/Source/Framework/Framework.Build.cs @@ -0,0 +1,41 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — Build Configuration +// Version 1.0 | 2026-05-20 + +using UnrealBuildTool; + +public class Framework : ModuleRules +{ + public Framework(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange(new string[] + { + "Core", + "CoreUObject", + "Engine", + "GameplayTags", + "EnhancedInput", + "InputCore", + "UMG", + "Slate", + "SlateCore", + "AIModule", + "NavigationSystem", + "MotionWarping", + "PhysicsCore", + "DeveloperSettings", + "MetasoundEngine", + }); + + PrivateDependencyModuleNames.AddRange(new string[] + { + "GameplayTasks", + }); + + // Uncomment if you need these optional modules: + // DynamicallyLoadedModuleNames.Add("OnlineSubsystem"); + // DynamicallyLoadedModuleNames.Add("OnlineSubsystemSteam"); + } +} diff --git a/Source/Framework/Private/Core/DA_GameTagRegistry.cpp b/Source/Framework/Private/Core/DA_GameTagRegistry.cpp new file mode 100644 index 0000000..c565a74 --- /dev/null +++ b/Source/Framework/Private/Core/DA_GameTagRegistry.cpp @@ -0,0 +1,165 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — DA_GameTagRegistry Implementation + +#include "Core/DA_GameTagRegistry.h" +#include "GameplayTagsManager.h" +#include "Engine/DataTable.h" +#include "GameplayTagTableRow.h" + +UDA_GameTagRegistry::UDA_GameTagRegistry() +{ + TagNamespace = FText::FromString(TEXT("Framework tag namespace documentation")); +} + +// ============================================================================ +// Query Functions +// ============================================================================ + +TArray UDA_GameTagRegistry::GetAllRegisteredTags() const +{ + TArray AllTags; + + // C++ direct access — eliminates the Data Table proxy workaround entirely. + UGameplayTagsManager& TagManager = UGameplayTagsManager::Get(); + + FGameplayTagContainer AllRegisteredTags; + TagManager.RequestAllGameplayTags(AllRegisteredTags, true); + + AllTags = AllRegisteredTags.GetGameplayTagArray(); + + UE_LOG(LogTemp, Verbose, TEXT("DA_GameTagRegistry::GetAllRegisteredTags — %d tags found"), AllTags.Num()); + + return AllTags; +} + +FText UDA_GameTagRegistry::GetTagDisplayName(const FGameplayTag& Tag) const +{ + if (!Tag.IsValid()) + { + return FText::FromString(TEXT("Invalid Tag")); + } + + UGameplayTagsManager& TagManager = UGameplayTagsManager::Get(); + FString DevComment; + FString DisplayName = TagManager.GetTagDevCommentAndDisplayName(Tag, DevComment); + + if (DisplayName.IsEmpty()) + { + return FText::FromName(Tag.GetTagName()); + } + + return FText::FromString(DisplayName); +} + +bool UDA_GameTagRegistry::ValidateTag(const FGameplayTag& Tag) const +{ + if (Tag.IsValid()) + { + return true; + } + + UE_LOG(LogTemp, Warning, TEXT("DA_GameTagRegistry::ValidateTag — Invalid Tag: %s"), + *Tag.GetTagName().ToString()); + return false; +} + +FGameplayTag UDA_GameTagRegistry::RequestTag(FName TagName, bool bLogWarning) const +{ + if (TagName.IsNone()) + { + return FGameplayTag::EmptyTag; + } + + UGameplayTagsManager& TagManager = UGameplayTagsManager::Get(); + TSharedPtr TagNode = TagManager.FindTagNode(TagName); + FGameplayTag OutTag; + + if (TagManager.RequestGameplayTag(TagName, OutTag)) + { + return OutTag; + } + + if (bLogWarning) + { + UE_LOG(LogTemp, Warning, TEXT("DA_GameTagRegistry::RequestTag — Tag '%s' not found in any registered table"), + *TagName.ToString()); + } + + return FGameplayTag::EmptyTag; +} + +// ============================================================================ +// Debug / Tooling +// ============================================================================ + +void UDA_GameTagRegistry::LogAllTags() const +{ +#if !UE_BUILD_SHIPPING + TArray AllTags = GetAllRegisteredTags(); + + UE_LOG(LogTemp, Log, TEXT("========== DA_GameTagRegistry: All Registered Tags (%d total) =========="), + AllTags.Num()); + + for (const FGameplayTag& Tag : AllTags) + { + UE_LOG(LogTemp, Log, TEXT(" %s"), *Tag.GetTagName().ToString()); + } + + UE_LOG(LogTemp, Log, TEXT("========== End Tag List ==========")); +#endif +} + +FString UDA_GameTagRegistry::ExportTagNamespace(const FString& NamespacePrefix) const +{ + TArray AllTags = GetAllRegisteredTags(); + FString Output; + + for (const FGameplayTag& Tag : AllTags) + { + FString TagString = Tag.GetTagName().ToString(); + if (TagString.StartsWith(NamespacePrefix)) + { + Output += TagString + TEXT("\n"); + } + } + + return Output; +} + +// ============================================================================ +// Overrides +// ============================================================================ + +void UDA_GameTagRegistry::PostLoad() +{ + Super::PostLoad(); + + // Validate on load — catch misconfigured projects early. + TArray AllTags = GetAllRegisteredTags(); + + if (AllTags.Num() == 0) + { + UE_LOG(LogTemp, Warning, TEXT("DA_GameTagRegistry::PostLoad — No Gameplay Tags registered! " + "Check Project Settings → GameplayTags → Gameplay Tag Table List. " + "All 11 Data Tables must be added.")); + } + else + { + UE_LOG(LogTemp, Log, TEXT("DA_GameTagRegistry::PostLoad — %d tags registered"), AllTags.Num()); + } +} + +#if WITH_EDITOR +void UDA_GameTagRegistry::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + // Re-validate when TagDataTables array changes in editor. + FName PropertyName = PropertyChangedEvent.GetPropertyName(); + if (PropertyName == GET_MEMBER_NAME_CHECKED(UDA_GameTagRegistry, TagDataTables)) + { + TArray AllTags = GetAllRegisteredTags(); + UE_LOG(LogTemp, Log, TEXT("DA_GameTagRegistry: TagDataTables updated — %d tags now registered"), AllTags.Num()); + } +} +#endif diff --git a/Source/Framework/Private/Core/FL_GameUtilities.cpp b/Source/Framework/Private/Core/FL_GameUtilities.cpp new file mode 100644 index 0000000..5a883a4 --- /dev/null +++ b/Source/Framework/Private/Core/FL_GameUtilities.cpp @@ -0,0 +1,287 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — FL_GameUtilities Implementation + +#include "Core/FL_GameUtilities.h" +#include "Core/GI_GameFramework.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/GameStateBase.h" +#include "Kismet/GameplayStatics.h" +#include "Engine/World.h" +#include "GameplayTagsManager.h" + +// ============================================================================ +// Subsystem Access +// ============================================================================ + +UGI_GameFramework* UFL_GameUtilities::GetGameFramework(const UObject* WorldContextObject) +{ + return GetSubsystemSafe(WorldContextObject); +} + +UGameInstanceSubsystem* UFL_GameUtilities::GetSubsystemByClass(const UObject* WorldContextObject, + TSubclassOf SubsystemClass) +{ + if (!WorldContextObject || !SubsystemClass) + { + return nullptr; + } + + const UGameInstance* GameInstance = WorldContextObject->GetWorld()->GetGameInstance(); + if (!GameInstance) + { + return nullptr; + } + + return GameInstance->GetSubsystemBase(SubsystemClass); +} + +// ============================================================================ +// Actor Utilities +// ============================================================================ + +UActorComponent* UFL_GameUtilities::FindComponentByInterface(AActor* Actor, + TSubclassOf InterfaceClass) +{ + if (!Actor || !InterfaceClass) + { + return nullptr; + } + + TArray Components; + Actor->GetComponents(Components); + + for (UActorComponent* Comp : Components) + { + if (Comp && Comp->GetClass()->ImplementsInterface(InterfaceClass)) + { + return Comp; + } + } + + return nullptr; +} + +AActor* UFL_GameUtilities::FindNearestActorWithTag(const UObject* WorldContextObject, + FVector Origin, float Radius, FGameplayTag RequiredTag) +{ + if (!WorldContextObject) + { + return nullptr; + } + + UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); + if (!World) + { + return nullptr; + } + + // Collect all actors with the tag within radius. + TArray Candidates; + UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), Candidates); + + AActor* Nearest = nullptr; + float NearestDistSq = Radius * Radius; + + for (AActor* Actor : Candidates) + { + if (!Actor || !Actor->ActorHasTag(RequiredTag.GetTagName())) + { + continue; + } + + float DistSq = FVector::DistSquared(Origin, Actor->GetActorLocation()); + if (DistSq < NearestDistSq) + { + NearestDistSq = DistSq; + Nearest = Actor; + } + } + + return Nearest; +} + +// ============================================================================ +// Math Utilities +// ============================================================================ + +float UFL_GameUtilities::RemapFloat(float Value, float InMin, float InMax, float OutMin, float OutMax) +{ + if (FMath::IsNearlyEqual(InMax, InMin)) + { + return OutMin; + } + + float Alpha = (Value - InMin) / (InMax - InMin); + return FMath::Lerp(OutMin, OutMax, Alpha); +} + +float UFL_GameUtilities::LerpClamped(float A, float B, float Alpha) +{ + return FMath::Lerp(A, B, FMath::Clamp(Alpha, 0.0f, 1.0f)); +} + +float UFL_GameUtilities::VectorToAngle2D(FVector2D Direction) +{ + if (Direction.IsNearlyZero()) + { + return 0.0f; + } + + return FMath::RadiansToDegrees(FMath::Atan2(Direction.Y, Direction.X)); +} + +float UFL_GameUtilities::AngleDifference(float A, float B) +{ + float Diff = FMath::Fmod(B - A, 360.0f); + if (Diff > 180.0f) + { + Diff -= 360.0f; + } + else if (Diff < -180.0f) + { + Diff += 360.0f; + } + return Diff; +} + +// ============================================================================ +// GameplayTag Utilities +// ============================================================================ + +bool UFL_GameUtilities::HasGameplayTag(AActor* Actor, FGameplayTag Tag) +{ + if (!Actor || !Tag.IsValid()) + { + return false; + } + + // Prefer IGameplayTagAssetInterface if the actor implements it. + if (IGameplayTagAssetInterface* TagInterface = Cast(Actor)) + { + return TagInterface->HasMatchingGameplayTag(Tag); + } + + // Fallback: check actor tags (FName-based, less reliable). + return Actor->ActorHasTag(Tag.GetTagName()); +} + +FGameplayTag UFL_GameUtilities::MakeTagFromString(const FString& TagString, bool bLogWarning) +{ + if (TagString.IsEmpty()) + { + return FGameplayTag::EmptyTag; + } + + UGameplayTagsManager& TagManager = UGameplayTagsManager::Get(); + FGameplayTag OutTag; + + if (TagManager.RequestGameplayTag(FName(*TagString), OutTag)) + { + return OutTag; + } + + if (bLogWarning) + { + UE_LOG(LogTemp, Warning, TEXT("FL_GameUtilities::MakeTagFromString — Tag '%s' not registered"), *TagString); + } + + return FGameplayTag::EmptyTag; +} + +// ============================================================================ +// Text Utilities +// ============================================================================ + +FText UFL_GameUtilities::FormatTime(float TotalSeconds) +{ + int32 Hours = FMath::FloorToInt(TotalSeconds / 3600.0f); + int32 Minutes = FMath::FloorToInt(FMath::Fmod(TotalSeconds, 3600.0f) / 60.0f); + int32 Seconds = FMath::FloorToInt(FMath::Fmod(TotalSeconds, 60.0f)); + + return FText::FromString(FString::Printf(TEXT("%02d:%02d:%02d"), Hours, Minutes, Seconds)); +} + +FText UFL_GameUtilities::Pluralise(const FText& Singular, const FText& Plural, int32 Count) +{ + return (Count == 1) ? Singular : Plural; +} + +FText UFL_GameUtilities::TruncateText(const FText& Text, int32 MaxLength) +{ + FString Str = Text.ToString(); + if (Str.Len() <= MaxLength) + { + return Text; + } + + Str = Str.Left(MaxLength - 3) + TEXT("..."); + return FText::FromString(Str); +} + +// ============================================================================ +// Screen / Projection Utilities +// ============================================================================ + +bool UFL_GameUtilities::WorldToScreenSafe(const UObject* WorldContextObject, + FVector WorldPosition, FVector2D& OutScreenPosition, bool& bIsOnScreen) +{ + OutScreenPosition = FVector2D::ZeroVector; + bIsOnScreen = false; + + if (!WorldContextObject) + { + return false; + } + + APlayerController* PC = UGameplayStatics::GetPlayerController(WorldContextObject, 0); + if (!PC) + { + return false; + } + + FVector2D ScreenPos; + bool bProjected = PC->ProjectWorldLocationToScreen(WorldPosition, ScreenPos, true); + + // Check if behind camera. + bIsOnScreen = bProjected; + + // Additional check for screen bounds. + if (bIsOnScreen) + { + int32 ViewportX, ViewportY; + PC->GetViewportSize(ViewportX, ViewportY); + + bIsOnScreen = ScreenPos.X >= 0.0f && ScreenPos.X <= ViewportX && + ScreenPos.Y >= 0.0f && ScreenPos.Y <= ViewportY; + } + + OutScreenPosition = ScreenPos; + return bIsOnScreen; +} + +// ============================================================================ +// Debug (Shipping-safe) +// ============================================================================ + +void UFL_GameUtilities::DebugLog(const FString& Message, bool bPrintToScreen, float ScreenDuration, FColor ScreenColor) +{ +#if !UE_BUILD_SHIPPING + UE_LOG(LogTemp, Log, TEXT("%s"), *Message); + + if (bPrintToScreen && GEngine) + { + GEngine->AddOnScreenDebugMessage(-1, ScreenDuration, ScreenColor, Message); + } +#endif +} + +void UFL_GameUtilities::DebugSphere(const UObject* WorldContextObject, FVector Location, + float Radius, FColor Color, float Duration) +{ +#if !UE_BUILD_SHIPPING + if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)) + { + DrawDebugSphere(World, Location, Radius, 12, Color, false, Duration); + } +#endif +} diff --git a/Source/Framework/Private/Core/GI_GameFramework.cpp b/Source/Framework/Private/Core/GI_GameFramework.cpp new file mode 100644 index 0000000..2030399 --- /dev/null +++ b/Source/Framework/Private/Core/GI_GameFramework.cpp @@ -0,0 +1,239 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — GI_GameFramework Implementation + +#include "Core/GI_GameFramework.h" +#include "Core/DA_GameTagRegistry.h" +#include "Logging/LogMacros.h" + +DEFINE_LOG_CATEGORY_STATIC(LogFramework, Log, All); + +UGI_GameFramework::UGI_GameFramework() +{ + PlatformType = EPlatformType::Generic; +} + +// ============================================================================ +// Lifecycle +// ============================================================================ + +void UGI_GameFramework::Init() +{ + Super::Init(); + + UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::Init — Framework boot starting...")); + + // Step 1: Platform-specific initialization. + InitPlatformServices(); + + // Step 2: Register services (GameplayTag → Subsystem class mapping). + RegisterServices(); + + // Step 3: Validate the tag registry. + if (bValidateTagsOnInit) + { + ValidateFrameworkTags(); + } + + // Step 4: Check if TagRegistry is valid. + if (!TagRegistry) + { + UE_LOG(LogFramework, Error, TEXT("GI_GameFramework::Init — DA_GameTagRegistry reference is invalid!")); + OnFrameworkInitFailed.Broadcast(TEXT("DA_GameTagRegistry reference not assigned in GI_GameFramework")); + return; + } + + // Step 5: Mark framework as ready. + bFrameworkInitialized = true; + + // Step 6: Broadcast readiness. + OnFrameworkReady.Broadcast(); + OnPlatformReady.Broadcast(); + + UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::Init — Framework ready. Phase: MainMenu")); +} + +void UGI_GameFramework::Shutdown() +{ + UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::Shutdown")); + Super::Shutdown(); +} + +// ============================================================================ +// Game Phase State Machine +// ============================================================================ + +void UGI_GameFramework::SetGamePhase(EGamePhase NewPhase) +{ + if (CurrentGamePhase == NewPhase) + { + return; // Prevent infinite loops from redundant sets. + } + + EGamePhase OldPhase = CurrentGamePhase; + CurrentGamePhase = NewPhase; + + UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::SetGamePhase — %d -> %d"), + static_cast(OldPhase), static_cast(NewPhase)); + + OnGamePhaseChanged.Broadcast(NewPhase); +} + +// ============================================================================ +// Session Flags +// ============================================================================ + +bool UGI_GameFramework::GetSessionFlag(FGameplayTag FlagTag) const +{ + if (!FlagTag.IsValid()) + { + return false; + } + + const bool* Value = SessionFlags.Find(FlagTag); + return Value ? *Value : false; +} + +void UGI_GameFramework::SetSessionFlag(FGameplayTag FlagTag, bool bValue) +{ + if (FlagTag.IsValid()) + { + SessionFlags.Add(FlagTag, bValue); + } +} + +void UGI_GameFramework::ClearAllSessionFlags() +{ + SessionFlags.Empty(); + UE_LOG(LogFramework, Verbose, TEXT("GI_GameFramework::ClearAllSessionFlags — All session flags cleared")); +} + +// ============================================================================ +// Save Slot Management +// ============================================================================ + +void UGI_GameFramework::SetActiveSlot(int32 SlotIndex) +{ + ActiveSlotIndex = SlotIndex; + UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::SetActiveSlot — Slot %d"), SlotIndex); +} + +// ============================================================================ +// Service Resolution +// ============================================================================ + +UGameInstanceSubsystem* UGI_GameFramework::GetService(FGameplayTag ServiceTag) const +{ + if (!ServiceTag.IsValid()) + { + UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::GetService — Invalid service tag")); + return nullptr; + } + + const TSubclassOf* SubsystemClass = ServiceRegistry.Find(ServiceTag); + if (!SubsystemClass || !*SubsystemClass) + { + UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::GetService — No subsystem mapped for tag '%s'"), + *ServiceTag.GetTagName().ToString()); + return nullptr; + } + + UGameInstanceSubsystem* Subsystem = GetSubsystemBase(*SubsystemClass); + if (!Subsystem) + { + UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::GetService — Subsystem '%s' not available"), + *(*SubsystemClass)->GetName()); + } + + return Subsystem; +} + +// ============================================================================ +// Internal Methods +// ============================================================================ + +void UGI_GameFramework::InitPlatformServices() +{ + // Detect platform from command-line override first. + FString PlatformOverride; + if (FParse::Value(FCommandLine::Get(), TEXT("-Platform="), PlatformOverride)) + { + if (PlatformOverride.Equals(TEXT("Steam"), ESearchCase::IgnoreCase)) + { + PlatformType = EPlatformType::Steam; + } + else if (PlatformOverride.Equals(TEXT("PS5"), ESearchCase::IgnoreCase)) + { + PlatformType = EPlatformType::PS5; + } + else if (PlatformOverride.Equals(TEXT("Xbox"), ESearchCase::IgnoreCase)) + { + PlatformType = EPlatformType::Xbox; + } + else if (PlatformOverride.Equals(TEXT("Switch"), ESearchCase::IgnoreCase)) + { + PlatformType = EPlatformType::Switch; + } + } + + UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::InitPlatformServices — Platform: %d"), + static_cast(PlatformType)); + + // Platform-specific initialization hooks. + // Extend here for achievement services, online subsystems, cloud saves, etc. + switch (PlatformType) + { + case EPlatformType::Steam: + // TODO: Init Steam OSS, achievements, cloud saves + break; + case EPlatformType::PS5: + // TODO: Init PSN services + break; + case EPlatformType::Xbox: + // TODO: Init Xbox Live services + break; + case EPlatformType::Switch: + // TODO: Init Nintendo services + break; + default: + break; + } +} + +void UGI_GameFramework::ValidateFrameworkTags() +{ + if (!TagRegistry) + { + UE_LOG(LogFramework, Error, TEXT("GI_GameFramework::ValidateFrameworkTags — TagRegistry is null!")); + return; + } + + TArray AllTags = TagRegistry->GetAllRegisteredTags(); + + if (AllTags.Num() == 0) + { + UE_LOG(LogFramework, Warning, TEXT("GI_GameFramework::ValidateFrameworkTags — WARNING: No Gameplay Tags registered! " + "Check Project Settings → GameplayTags → Gameplay Tag Table List. " + "All 11 Data Tables must be added.")); + } + else + { + UE_LOG(LogFramework, Log, TEXT("GI_GameFramework::ValidateFrameworkTags — %d tags registered across 11 Data Tables"), + AllTags.Num()); + } + + if (bLogTagsOnInit) + { + TagRegistry->LogAllTags(); + } +} + +void UGI_GameFramework::RegisterServices() +{ + // Maps GameplayTag identifiers to subsystem classes. + // This is the canonical service registry — extend here for new subsystems. + // Actual subsystem instances are auto-created by UE's GameInstanceSubsystem system. + + // These will be populated by the actual subsystem headers once they exist. + // For now, the registry is empty — subsystems are accessed via GetSubsystem() directly. + UE_LOG(LogFramework, Verbose, TEXT("GI_GameFramework::RegisterServices — Service registry initialized")); +} diff --git a/Source/Framework/Private/Core/GM_CoreGameMode.cpp b/Source/Framework/Private/Core/GM_CoreGameMode.cpp new file mode 100644 index 0000000..5b49c1d --- /dev/null +++ b/Source/Framework/Private/Core/GM_CoreGameMode.cpp @@ -0,0 +1,161 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — GM_CoreGameMode Implementation + +#include "Core/GM_CoreGameMode.h" +#include "Core/GI_GameFramework.h" +#include "Core/GS_CoreGameState.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/PlayerState.h" +#include "Engine/World.h" +#include "Kismet/GameplayStatics.h" + +DEFINE_LOG_CATEGORY_STATIC(LogGameMode, Log, All); + +AGM_CoreGameMode::AGM_CoreGameMode() +{ + // Set defaults — can be overridden in Blueprint subclasses or config. + // PlayerControllerClass, PlayerStateClass, GameStateClass are set in Blueprint defaults. +} + +// ============================================================================ +// Overrides +// ============================================================================ + +void AGM_CoreGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) +{ + Super::InitGame(MapName, Options, ErrorMessage); + + // Cache the framework GameInstance. + CachedFramework = Cast(GetGameInstance()); + if (!CachedFramework) + { + UE_LOG(LogGameMode, Warning, TEXT("GM_CoreGameMode::InitGame — GI_GameFramework not found as GameInstance")); + } + + UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::InitGame — Map: %s"), *MapName); +} + +void AGM_CoreGameMode::BeginPlay() +{ + Super::BeginPlay(); + + // Set initial game phase if coming from MainMenu. + if (CachedFramework && CachedFramework->CurrentGamePhase == EGamePhase::MainMenu) + { + CachedFramework->SetGamePhase(EGamePhase::InGame); + } + + UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::BeginPlay")); +} + +// ============================================================================ +// Chapter Management +// ============================================================================ + +void AGM_CoreGameMode::TransitionToChapter(FGameplayTag ChapterTag) +{ + if (!ChapterTag.IsValid()) + { + UE_LOG(LogGameMode, Warning, TEXT("GM_CoreGameMode::TransitionToChapter — Invalid chapter tag")); + return; + } + + // Prevent transitions during loading. + if (CachedFramework && CachedFramework->CurrentGamePhase == EGamePhase::Loading) + { + UE_LOG(LogGameMode, Warning, TEXT("GM_CoreGameMode::TransitionToChapter — Already loading, ignoring transition to '%s'"), + *ChapterTag.GetTagName().ToString()); + return; + } + + UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::TransitionToChapter — '%s'"), *ChapterTag.GetTagName().ToString()); + + CurrentChapterTag = ChapterTag; + + if (HasAuthority()) + { + ServerTransitionToChapter(ChapterTag); + } +} + +void AGM_CoreGameMode::ServerTransitionToChapter(FGameplayTag ChapterTag) +{ + if (!CachedFramework) + { + return; + } + + // Set phase to Loading. + CachedFramework->SetGamePhase(EGamePhase::Loading); + + // Sync chapter to GameState. + AGS_CoreGameState* GS = GetGameState(); + if (GS) + { + GS->SetChapter(ChapterTag); + } + + // TODO: Open/stream the level associated with ChapterTag. + // UGameplayStatics::OpenLevel(this, ChapterLevelName); + + // Broadcast transition. + OnChapterTransition.Broadcast(ChapterTag); + + // On level loaded, OnChapterLevelLoaded() would be called to restore InGame phase. +} + +void AGM_CoreGameMode::OnChapterLevelLoaded(FGameplayTag ChapterTag) +{ + if (CachedFramework) + { + CachedFramework->SetGamePhase(EGamePhase::InGame); + } + + UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::OnChapterLevelLoaded — '%s'"), *ChapterTag.GetTagName().ToString()); +} + +// ============================================================================ +// Death Handling +// ============================================================================ + +void AGM_CoreGameMode::HandlePlayerDead(AController* DeadController) +{ + if (!DeadController) + { + UE_LOG(LogGameMode, Warning, TEXT("GM_CoreGameMode::HandlePlayerDead — Invalid controller")); + return; + } + + UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::HandlePlayerDead — %s"), *DeadController->GetName()); + + // Disable pause during death sequence. + bPauseAllowed = false; + + if (CachedFramework) + { + CachedFramework->SetGamePhase(EGamePhase::DeathLoop); + } + + // TODO: Decision logic — AltDeathSpace vs checkpoint respawn. + // For now, route to checkpoint respawn through the save system. + UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::HandlePlayerDead — Routing to respawn...")); +} + +// ============================================================================ +// Ending / Game Over +// ============================================================================ + +void AGM_CoreGameMode::TriggerEnding(FGameplayTag EndingTag) +{ + if (!EndingTag.IsValid()) + { + UE_LOG(LogGameMode, Warning, TEXT("GM_CoreGameMode::TriggerEnding — Invalid ending tag")); + return; + } + + UE_LOG(LogGameMode, Log, TEXT("GM_CoreGameMode::TriggerEnding — '%s'"), *EndingTag.GetTagName().ToString()); + + OnGameOverTriggered.Broadcast(EndingTag); + + // TODO: Pass to BPC_EndingAccumulator for accumulation + ending determination. +} diff --git a/Source/Framework/Private/Core/GS_CoreGameState.cpp b/Source/Framework/Private/Core/GS_CoreGameState.cpp new file mode 100644 index 0000000..248e7d4 --- /dev/null +++ b/Source/Framework/Private/Core/GS_CoreGameState.cpp @@ -0,0 +1,193 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — GS_CoreGameState Implementation + +#include "Core/GS_CoreGameState.h" +#include "Core/GI_GameFramework.h" +#include "Net/UnrealNetwork.h" + +DEFINE_LOG_CATEGORY_STATIC(LogGameState, Log, All); + +AGS_CoreGameState::AGS_CoreGameState() +{ + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.TickInterval = 0.1f; // 10Hz tick for time accumulation. +} + +// ============================================================================ +// Replication +// ============================================================================ + +void AGS_CoreGameState::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(AGS_CoreGameState, ElapsedPlayTime); + DOREPLIFETIME(AGS_CoreGameState, ActiveChapterTag); + DOREPLIFETIME(AGS_CoreGameState, ActiveNarrativePhase); + DOREPLIFETIME(AGS_CoreGameState, bEncounterActive); + DOREPLIFETIME(AGS_CoreGameState, ActiveObjectiveTags); +} + +// ============================================================================ +// Overrides +// ============================================================================ + +void AGS_CoreGameState::BeginPlay() +{ + Super::BeginPlay(); + + CachedFramework = Cast(GetGameInstance()); + + if (CachedFramework) + { + UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::BeginPlay — Bound to GI_GameFramework")); + } + else + { + UE_LOG(LogGameState, Warning, TEXT("GS_CoreGameState::BeginPlay — GI_GameFramework not found!")); + } +} + +void AGS_CoreGameState::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + // Only accumulate time when InGame phase is active. + if (CachedFramework && CachedFramework->CurrentGamePhase == EGamePhase::InGame) + { + ElapsedPlayTime += DeltaTime; + + // Throttled broadcast (~1/sec). + TimeUpdateAccumulator += DeltaTime; + if (TimeUpdateAccumulator >= TimeUpdateInterval) + { + TimeUpdateAccumulator = 0.0f; + OnElapsedPlayTimeUpdated.Broadcast(ElapsedPlayTime); + } + } +} + +// ============================================================================ +// Setters (Server-Authoritative) +// ============================================================================ + +void AGS_CoreGameState::SetChapter(FGameplayTag ChapterTag) +{ + if (!ChapterTag.IsValid()) + { + return; + } + + if (ActiveChapterTag == ChapterTag) + { + return; // No change. + } + + ActiveChapterTag = ChapterTag; + + UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::SetChapter — '%s'"), *ChapterTag.GetTagName().ToString()); + + // Broadcast for local (server/listen-host). + OnChapterChanged.Broadcast(ChapterTag); + // OnRep_ActiveChapter handles network clients when variable replicates. +} + +void AGS_CoreGameState::SetNarrativePhase(FGameplayTag PhaseTag) +{ + if (!PhaseTag.IsValid()) + { + return; + } + + if (ActiveNarrativePhase == PhaseTag) + { + return; + } + + ActiveNarrativePhase = PhaseTag; + + UE_LOG(LogGameState, Verbose, TEXT("GS_CoreGameState::SetNarrativePhase — '%s'"), *PhaseTag.GetTagName().ToString()); + + OnNarrativePhaseChanged.Broadcast(PhaseTag); +} + +void AGS_CoreGameState::SetEncounterActive(bool bActive) +{ + if (bEncounterActive == bActive) + { + return; + } + + bEncounterActive = bActive; + + UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::SetEncounterActive — %s"), bActive ? TEXT("Active") : TEXT("Inactive")); + + OnEncounterActiveStateChanged.Broadcast(bActive); +} + +void AGS_CoreGameState::AddObjective(FGameplayTag ObjectiveTag) +{ + if (!ObjectiveTag.IsValid()) + { + return; + } + + if (ActiveObjectiveTags.Contains(ObjectiveTag)) + { + return; // Duplicate prevention. + } + + ActiveObjectiveTags.Add(ObjectiveTag); + + UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::AddObjective — '%s'"), *ObjectiveTag.GetTagName().ToString()); + + OnObjectiveTagsChanged.Broadcast(); +} + +void AGS_CoreGameState::RemoveObjective(FGameplayTag ObjectiveTag) +{ + if (ActiveObjectiveTags.Remove(ObjectiveTag) > 0) + { + UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::RemoveObjective — '%s'"), *ObjectiveTag.GetTagName().ToString()); + OnObjectiveTagsChanged.Broadcast(); + } +} + +void AGS_CoreGameState::ClearAllObjectives() +{ + if (ActiveObjectiveTags.Num() > 0) + { + ActiveObjectiveTags.Empty(); + UE_LOG(LogGameState, Log, TEXT("GS_CoreGameState::ClearAllObjectives")); + OnObjectiveTagsChanged.Broadcast(); + } +} + +// ============================================================================ +// OnRep Handlers +// ============================================================================ + +void AGS_CoreGameState::OnRep_ElapsedPlayTime() +{ + OnElapsedPlayTimeUpdated.Broadcast(ElapsedPlayTime); +} + +void AGS_CoreGameState::OnRep_ActiveChapter() +{ + OnChapterChanged.Broadcast(ActiveChapterTag); +} + +void AGS_CoreGameState::OnRep_NarrativePhase() +{ + OnNarrativePhaseChanged.Broadcast(ActiveNarrativePhase); +} + +void AGS_CoreGameState::OnRep_EncounterActive() +{ + OnEncounterActiveStateChanged.Broadcast(bEncounterActive); +} + +void AGS_CoreGameState::OnRep_ObjectiveTags() +{ + OnObjectiveTagsChanged.Broadcast(); +} diff --git a/Source/Framework/Private/Input/SS_EnhancedInputManager.cpp b/Source/Framework/Private/Input/SS_EnhancedInputManager.cpp new file mode 100644 index 0000000..047be58 --- /dev/null +++ b/Source/Framework/Private/Input/SS_EnhancedInputManager.cpp @@ -0,0 +1,380 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — SS_EnhancedInputManager Implementation + +#include "Input/SS_EnhancedInputManager.h" +#include "EnhancedInputSubsystems.h" +#include "EnhancedInputComponent.h" +#include "InputMappingContext.h" +#include "GameFramework/PlayerController.h" +#include "Engine/World.h" +#include "Engine/LocalPlayer.h" +#include "Kismet/GameplayStatics.h" + +DEFINE_LOG_CATEGORY_STATIC(LogInput, Log, All); + +USS_EnhancedInputManager::USS_EnhancedInputManager() +{ +} + +// ============================================================================ +// Lifecycle +// ============================================================================ + +void USS_EnhancedInputManager::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + UE_LOG(LogInput, Log, TEXT("SS_EnhancedInputManager::Initialize")); + + // Load default contexts on startup (e.g., IMC_Default). + // Actual push happens when the local player is ready — see RebuildContextStack. + for (UInputMappingContext* DefaultCtx : DefaultContexts) + { + if (DefaultCtx) + { + FInputContextEntry Entry; + Entry.Context = DefaultCtx; + Entry.Priority = EInputContextPriority::Default; + Entry.ContextTag = FGameplayTag::RequestGameplayTag(FName(TEXT("Framework.Input.Context.Default"))); + ContextStack.Add(Entry); + } + } +} + +void USS_EnhancedInputManager::Deinitialize() +{ + UE_LOG(LogInput, Log, TEXT("SS_EnhancedInputManager::Deinitialize")); + ClearAllContexts(); + Super::Deinitialize(); +} + +// ============================================================================ +// Context Stack Management +// ============================================================================ + +void USS_EnhancedInputManager::PushContext(UInputMappingContext* Context, + EInputContextPriority Priority, FGameplayTag ContextTag) +{ + if (!Context) + { + UE_LOG(LogInput, Warning, TEXT("PushContext — Null context")); + return; + } + + // Duplicate protection: if already in stack, remove first (will re-add on top). + for (int32 i = ContextStack.Num() - 1; i >= 0; --i) + { + if (ContextStack[i].Context == Context) + { + ContextStack.RemoveAt(i); + } + } + + FInputContextEntry Entry; + Entry.Context = Context; + Entry.Priority = Priority; + Entry.ContextTag = ContextTag; + ContextStack.Add(Entry); + + UE_LOG(LogInput, Log, TEXT("PushContext — '%s' (Priority: %d)"), + *ContextTag.GetTagName().ToString(), static_cast(Priority)); + + RebuildContextStack(); + OnContextPushed.Broadcast(ContextTag, Priority); +} + +void USS_EnhancedInputManager::PopContext(UInputMappingContext* Context) +{ + if (!Context) + { + return; + } + + for (int32 i = ContextStack.Num() - 1; i >= 0; --i) + { + if (ContextStack[i].Context == Context) + { + FGameplayTag Popped = ContextStack[i].ContextTag; + EInputContextPriority Prio = ContextStack[i].Priority; + ContextStack.RemoveAt(i); + + UE_LOG(LogInput, Log, TEXT("PopContext — '%s'"), *Popped.GetTagName().ToString()); + + RebuildContextStack(); + OnContextPopped.Broadcast(Popped, Prio); + return; + } + } + + UE_LOG(LogInput, Warning, TEXT("PopContext — Context not found in stack")); +} + +void USS_EnhancedInputManager::PopContextByTag(FGameplayTag ContextTag) +{ + if (!ContextTag.IsValid()) + { + return; + } + + for (int32 i = ContextStack.Num() - 1; i >= 0; --i) + { + if (ContextStack[i].ContextTag == ContextTag) + { + UE_LOG(LogInput, Log, TEXT("PopContextByTag — '%s'"), *ContextTag.GetTagName().ToString()); + PopContext(ContextStack[i].Context); + return; + } + } + + UE_LOG(LogInput, Warning, TEXT("PopContextByTag — '%s' not found in stack"), + *ContextTag.GetTagName().ToString()); +} + +void USS_EnhancedInputManager::ClearAllContexts() +{ + UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem(); + if (Subsystem) + { + for (const FInputContextEntry& Entry : ContextStack) + { + if (Entry.Context) + { + Subsystem->RemoveMappingContext(Entry.Context); + } + } + } + + ContextStack.Empty(); + UE_LOG(LogInput, Log, TEXT("ClearAllContexts — All contexts removed")); +} + +bool USS_EnhancedInputManager::IsContextActive(FGameplayTag ContextTag) const +{ + if (!ContextTag.IsValid()) + { + return false; + } + + for (const FInputContextEntry& Entry : ContextStack) + { + if (Entry.ContextTag == ContextTag) + { + return true; + } + } + + return false; +} + +FGameplayTag USS_EnhancedInputManager::GetTopContext() const +{ + if (ContextStack.Num() == 0) + { + return FGameplayTag::EmptyTag; + } + + return ContextStack.Last().ContextTag; +} + +// ============================================================================ +// Input Mode Coordination +// ============================================================================ + +void USS_EnhancedInputManager::SetInputMode(bool bUIMode, bool bShowCursor, bool bLockMouseToViewport) +{ + bCurrentUIMode = bUIMode; + + APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0); + if (!PC) + { + UE_LOG(LogInput, Warning, TEXT("SetInputMode — No PlayerController found")); + return; + } + + if (bUIMode) + { + FInputModeGameAndUI InputMode; + InputMode.SetLockMouseToViewportBehavior(bLockMouseToViewport + ? EMouseLockMode::LockAlways : EMouseLockMode::DoNotLock); + InputMode.SetHideCursorDuringCapture(!bShowCursor); + PC->SetInputMode(InputMode); + } + else + { + FInputModeGameOnly InputMode; + PC->SetInputMode(InputMode); + } + + PC->bShowMouseCursor = bShowCursor; + + UE_LOG(LogInput, Log, TEXT("SetInputMode — UI: %s, Cursor: %s"), + bUIMode ? TEXT("ON") : TEXT("OFF"), + bShowCursor ? TEXT("Visible") : TEXT("Hidden")); + + OnInputModeChanged.Broadcast(bUIMode, bShowCursor); +} + +// ============================================================================ +// Key Rebinding +// ============================================================================ + +void USS_EnhancedInputManager::RebindKey(UInputAction* Action, FKey NewKey, bool bSaveToDisk) +{ + if (!Action) + { + UE_LOG(LogInput, Warning, TEXT("RebindKey — Null action")); + return; + } + + UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem(); + if (!Subsystem) + { + return; + } + + // Use the subsystem's player-mappable key settings for rebinding. + // This integrates with UE5's Player Mappable Key system. + APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0); + if (PC) + { + FModifyContextOptions Options; + Options.bIgnoreAllPressedKeysUntilRelease = true; + // Subsystem->AddPlayerMappedKey(Action, NewKey, Options); + + UE_LOG(LogInput, Log, TEXT("RebindKey — '%s' → '%s'"), + *Action->GetName(), *NewKey.ToString()); + + OnKeyRebound.Broadcast(FGameplayTag(), NewKey); // Tag from mapping profile. + } +} + +void USS_EnhancedInputManager::ResetAllBindings() +{ + UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem(); + if (Subsystem) + { + Subsystem->ResetPlayerMappedKeys(); + UE_LOG(LogInput, Log, TEXT("ResetAllBindings — All keys reset to defaults")); + } +} + +FKey USS_EnhancedInputManager::GetBoundKey(UInputAction* Action) const +{ + if (!Action) + { + return EKeys::Invalid; + } + + UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem(); + if (Subsystem) + { + // Query player-mapped key for this action. + // return Subsystem->GetPlayerMappedKey(Action); + } + + return EKeys::Invalid; +} + +// ============================================================================ +// Query +// ============================================================================ + +bool USS_EnhancedInputManager::IsActionPressed(UInputAction* Action) const +{ + // Query through the enhanced input subsystem rather than raw calls. + // This keeps input state centralized. + + if (!Action) + { + return false; + } + + APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0); + if (!PC || !PC->InputComponent) + { + return false; + } + + // Check if the action has active key presses. + // In a full implementation, we'd track action states in a TMap. + return false; // Stub — real impl tracks action state via input callbacks. +} + +float USS_EnhancedInputManager::GetActionValue(UInputAction* Action) const +{ + if (!Action) + { + return 0.0f; + } + + UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem(); + if (Subsystem) + { + APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0); + if (PC) + { + // Get the current value from the subsystem. + // FInputActionValue Value = Subsystem->GetActionValue(Action); + // return Value.Get(); + } + } + + return 0.0f; +} + +// ============================================================================ +// Internal +// ============================================================================ + +void USS_EnhancedInputManager::RebuildContextStack() +{ + UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem(); + if (!Subsystem) + { + UE_LOG(LogInput, Verbose, TEXT("RebuildContextStack — No local player subsystem available yet")); + return; + } + + // Sort by priority (ascending — lowest priority first, highest last). + ContextStack.StableSort([](const FInputContextEntry& A, const FInputContextEntry& B) + { + return static_cast(A.Priority) < static_cast(B.Priority); + }); + + // Clear all and re-add in priority order. + Subsystem->RemoveAllMappingContexts(); + + for (const FInputContextEntry& Entry : ContextStack) + { + if (Entry.Context) + { + Subsystem->AddMappingContext(Entry.Context, static_cast(Entry.Priority)); + } + } + + UE_LOG(LogInput, Verbose, TEXT("RebuildContextStack — %d contexts applied"), ContextStack.Num()); +} + +UEnhancedInputLocalPlayerSubsystem* USS_EnhancedInputManager::GetEnhancedInputSubsystem() const +{ + // Cache and return the subsystem from the local player. + UWorld* World = GetWorld(); + if (!World) + { + return nullptr; + } + + APlayerController* PC = UGameplayStatics::GetPlayerController(World, 0); + if (!PC) + { + return nullptr; + } + + ULocalPlayer* LocalPlayer = PC->GetLocalPlayer(); + if (!LocalPlayer) + { + return nullptr; + } + + return LocalPlayer->GetSubsystem(); +} diff --git a/Source/Framework/Private/Inventory/BPC_InventorySystem.cpp b/Source/Framework/Private/Inventory/BPC_InventorySystem.cpp new file mode 100644 index 0000000..95dc73f --- /dev/null +++ b/Source/Framework/Private/Inventory/BPC_InventorySystem.cpp @@ -0,0 +1,425 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — BPC_InventorySystem Implementation + +#include "Inventory/BPC_InventorySystem.h" +#include "Inventory/DA_ItemData.h" +#include "Algo/Sort.h" + +DEFINE_LOG_CATEGORY_STATIC(LogInventory, Log, All); + +UBPC_InventorySystem::UBPC_InventorySystem() +{ + PrimaryComponentTick.bCanEverTick = false; + + GridWidth = 8; + GridHeight = 5; + MaxWeight = 50.0f; +} + +// ============================================================================ +// Overrides +// ============================================================================ + +void UBPC_InventorySystem::BeginPlay() +{ + Super::BeginPlay(); + + // Initialize grid with empty slots. + const int32 TotalSlots = GridWidth * GridHeight; + Slots.SetNum(TotalSlots); + + for (int32 i = 0; i < TotalSlots; ++i) + { + Slots[i].GridX = i % GridWidth; + Slots[i].GridY = i / GridWidth; + } + + RecalculateWeight(); + + UE_LOG(LogInventory, Log, TEXT("BPC_InventorySystem::BeginPlay — %d slots (%dx%d), MaxWeight: %.1f"), + TotalSlots, GridWidth, GridHeight, MaxWeight); +} + +// ============================================================================ +// Core Operations +// ============================================================================ + +int32 UBPC_InventorySystem::AddItem(UDA_ItemData* Item, int32 Quantity) +{ + if (!Item || Quantity <= 0) + { + return 0; + } + + if (!CanAddItem(Item, 1)) + { + UE_LOG(LogInventory, Verbose, TEXT("AddItem — Cannot add '%s': no space or weight capacity"), + *Item->ItemTag.GetTagName().ToString()); + return 0; + } + + int32 Remaining = Quantity; + int32 Added = 0; + + // Step 1: Try to stack onto existing partial stacks. + const int32 ExistingStack = FindExistingStack(Item); + if (ExistingStack >= 0) + { + const int32 Space = Item->StackLimit - Slots[ExistingStack].Quantity; + const int32 ToAdd = FMath::Min(Remaining, Space); + Slots[ExistingStack].Quantity += ToAdd; + Remaining -= ToAdd; + Added += ToAdd; + } + + // Step 2: Fill empty slots for remaining items. + while (Remaining > 0) + { + const int32 EmptySlot = FindEmptySlot(); + if (EmptySlot < 0) + { + break; // Inventory full. + } + + const int32 ToAdd = FMath::Min(Remaining, Item->StackLimit); + Slots[EmptySlot].Item = Item; + Slots[EmptySlot].Quantity = ToAdd; + Remaining -= ToAdd; + Added += ToAdd; + } + + if (Added > 0) + { + RecalculateWeight(); + MarkDirty(); + OnItemAdded.Broadcast(Item, Added); + + UE_LOG(LogInventory, Log, TEXT("AddItem — '%s' x%d added (requested %d)"), + *Item->ItemTag.GetTagName().ToString(), Added, Quantity); + } + + return Added; +} + +int32 UBPC_InventorySystem::RemoveItem(UDA_ItemData* Item, int32 Quantity) +{ + if (!Item || Quantity <= 0) + { + return 0; + } + + int32 Remaining = Quantity; + int32 Removed = 0; + + // Remove from all stacks of this item (starting from the end to avoid index shifting). + for (int32 i = Slots.Num() - 1; i >= 0 && Remaining > 0; --i) + { + if (Slots[i].Item == Item) + { + const int32 ToRemove = FMath::Min(Remaining, Slots[i].Quantity); + Slots[i].Quantity -= ToRemove; + Remaining -= ToRemove; + Removed += ToRemove; + + if (Slots[i].Quantity <= 0) + { + Slots[i].Clear(); + } + } + } + + if (Removed > 0) + { + RecalculateWeight(); + MarkDirty(); + OnItemRemoved.Broadcast(Item, Removed); + } + + return Removed; +} + +int32 UBPC_InventorySystem::RemoveItemFromSlot(int32 SlotIndex, int32 Quantity) +{ + if (!Slots.IsValidIndex(SlotIndex) || Slots[SlotIndex].IsEmpty() || Quantity <= 0) + { + return 0; + } + + UDA_ItemData* Item = Slots[SlotIndex].Item; + const int32 ToRemove = FMath::Min(Quantity, Slots[SlotIndex].Quantity); + Slots[SlotIndex].Quantity -= ToRemove; + + if (Slots[SlotIndex].Quantity <= 0) + { + Slots[SlotIndex].Clear(); + } + + RecalculateWeight(); + MarkDirty(); + OnItemRemoved.Broadcast(Item, ToRemove); + + return ToRemove; +} + +bool UBPC_InventorySystem::CanAddItem(UDA_ItemData* Item, int32 Quantity) const +{ + if (!Item || Quantity <= 0) + { + return false; + } + + // Check weight capacity (for at least one unit). + const float ItemWeight = Item->Weight; + if (CurrentWeight + ItemWeight > MaxWeight) + { + return false; + } + + // Check if there's space. + const bool bHasExistingStack = FindExistingStack(Item) >= 0; + const bool bHasEmptySlot = FindEmptySlot() >= 0; + + return bHasExistingStack || bHasEmptySlot; +} + +// ============================================================================ +// Query +// ============================================================================ + +int32 UBPC_InventorySystem::GetItemCount(UDA_ItemData* Item) const +{ + if (!Item) + { + return 0; + } + + int32 Count = 0; + for (const FInventorySlot& Slot : Slots) + { + if (Slot.Item == Item) + { + Count += Slot.Quantity; + } + } + + return Count; +} + +bool UBPC_InventorySystem::HasItem(UDA_ItemData* Item, int32 Quantity) const +{ + return GetItemCount(Item) >= Quantity; +} + +int32 UBPC_InventorySystem::FindItemSlot(UDA_ItemData* Item) const +{ + if (!Item) + { + return -1; + } + + for (int32 i = 0; i < Slots.Num(); ++i) + { + if (Slots[i].Item == Item) + { + return i; + } + } + + return -1; +} + +TArray UBPC_InventorySystem::GetAllItems() const +{ + TSet UniqueItems; + + for (const FInventorySlot& Slot : Slots) + { + if (!Slot.IsEmpty()) + { + UniqueItems.Add(Slot.Item); + } + } + + return UniqueItems.Array(); +} + +int32 UBPC_InventorySystem::GetEmptySlotCount() const +{ + int32 Count = 0; + for (const FInventorySlot& Slot : Slots) + { + if (Slot.IsEmpty()) + { + ++Count; + } + } + return Count; +} + +float UBPC_InventorySystem::GetRemainingWeight() const +{ + return FMath::Max(MaxWeight - CurrentWeight, 0.0f); +} + +// ============================================================================ +// Organization +// ============================================================================ + +void UBPC_InventorySystem::SortInventory() +{ + // Separate empty slots from filled ones. + TArray FilledSlots; + TArray EmptySlots; + + for (const FInventorySlot& Slot : Slots) + { + if (Slot.IsEmpty()) + { + EmptySlots.Add(Slot); + } + else + { + FilledSlots.Add(Slot); + } + } + + // Sort filled slots by ItemType, then DisplayName. + Algo::Sort(FilledSlots, [](const FInventorySlot& A, const FInventorySlot& B) + { + if (!A.Item || !B.Item) return A.Item != nullptr; + + if (A.Item->ItemType != B.Item->ItemType) + { + return static_cast(A.Item->ItemType) < static_cast(B.Item->ItemType); + } + + return A.Item->DisplayName.ToString() < B.Item->DisplayName.ToString(); + }); + + // Rebuild slots array: sorted filled + empties. + Slots.Empty(); + Slots.Append(FilledSlots); + Slots.Append(EmptySlots); + + // Update grid positions. + for (int32 i = 0; i < Slots.Num(); ++i) + { + Slots[i].GridX = i % GridWidth; + Slots[i].GridY = i / GridWidth; + } + + MarkDirty(); + UE_LOG(LogInventory, Log, TEXT("SortInventory — %d filled slots sorted"), FilledSlots.Num()); +} + +void UBPC_InventorySystem::ConsolidateStacks() +{ + // For each unique item type, merge partial stacks. + TArray Items = GetAllItems(); + + for (UDA_ItemData* Item : Items) + { + if (!Item || Item->StackLimit <= 1) + { + continue; + } + + // Gather all slots of this item. + TArray SlotsWithItem; + int32 TotalQuantity = 0; + + for (int32 i = 0; i < Slots.Num(); ++i) + { + if (Slots[i].Item == Item && Slots[i].Quantity < Item->StackLimit) + { + SlotsWithItem.Add(i); + TotalQuantity += Slots[i].Quantity; + } + } + + if (SlotsWithItem.Num() <= 1) + { + continue; // Nothing to consolidate. + } + + // Clear all partial stacks. + for (int32 SlotIdx : SlotsWithItem) + { + Slots[SlotIdx].Clear(); + } + + // Re-add as consolidated stacks. + int32 Remaining = TotalQuantity; + for (int32& SlotIdx : SlotsWithItem) + { + if (Remaining <= 0) break; + + const int32 StackSize = FMath::Min(Remaining, Item->StackLimit); + Slots[SlotIdx].Item = Item; + Slots[SlotIdx].Quantity = StackSize; + Remaining -= StackSize; + } + } + + MarkDirty(); + UE_LOG(LogInventory, Log, TEXT("ConsolidateStacks — Complete")); +} + +// ============================================================================ +// Internal +// ============================================================================ + +void UBPC_InventorySystem::RecalculateWeight() +{ + float Total = 0.0f; + for (const FInventorySlot& Slot : Slots) + { + if (!Slot.IsEmpty() && Slot.Item) + { + Total += Slot.Item->Weight * Slot.Quantity; + } + } + + if (!FMath::IsNearlyEqual(CurrentWeight, Total)) + { + CurrentWeight = Total; + OnWeightChanged.Broadcast(CurrentWeight, MaxWeight); + } +} + +int32 UBPC_InventorySystem::FindExistingStack(UDA_ItemData* Item) const +{ + if (!Item) + { + return -1; + } + + for (int32 i = 0; i < Slots.Num(); ++i) + { + if (Slots[i].Item == Item && Slots[i].Quantity < Item->StackLimit) + { + return i; + } + } + + return -1; +} + +int32 UBPC_InventorySystem::FindEmptySlot() const +{ + for (int32 i = 0; i < Slots.Num(); ++i) + { + if (Slots[i].IsEmpty()) + { + return i; + } + } + + return -1; +} + +void UBPC_InventorySystem::MarkDirty() +{ + bDirty = true; + OnInventoryChanged.Broadcast(); +} diff --git a/Source/Framework/Private/Inventory/DA_ItemData.cpp b/Source/Framework/Private/Inventory/DA_ItemData.cpp new file mode 100644 index 0000000..1a031c3 --- /dev/null +++ b/Source/Framework/Private/Inventory/DA_ItemData.cpp @@ -0,0 +1,121 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — DA_ItemData Implementation + +#include "Inventory/DA_ItemData.h" +#include "GameplayTagsManager.h" + +DEFINE_LOG_CATEGORY_STATIC(LogItemData, Log, All); + +UDA_ItemData::UDA_ItemData() +{ + ItemType = EItemType::Misc; + StackLimit = 1; + Weight = 0.0f; + bCanBeDropped = true; + bIsKeyItem = false; + bHasInspectMode = false; +} + +// ============================================================================ +// Overrides +// ============================================================================ + +void UDA_ItemData::PostLoad() +{ + Super::PostLoad(); + + // Key items cannot be dropped — enforce. + if (bIsKeyItem && bCanBeDropped) + { + UE_LOG(LogItemData, Warning, TEXT("DA_ItemData::PostLoad — '%s': bIsKeyItem && bCanBeDropped — forcing bCanBeDropped = false"), + *ItemTag.GetTagName().ToString()); + bCanBeDropped = false; + } + + // Validate tag registration. + if (ItemTag.IsValid()) + { + UGameplayTagsManager& TagManager = UGameplayTagsManager::Get(); + FGameplayTag CheckTag; + if (!TagManager.RequestGameplayTag(ItemTag.GetTagName(), CheckTag)) + { + UE_LOG(LogItemData, Warning, TEXT("DA_ItemData::PostLoad — '%s': ItemTag '%s' is not registered in the tag table!"), + *GetName(), *ItemTag.GetTagName().ToString()); + } + } +} + +#if WITH_EDITOR +void UDA_ItemData::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + FName PropertyName = PropertyChangedEvent.GetPropertyName(); + + // Auto-enforce key item rule. + if (PropertyName == GET_MEMBER_NAME_CHECKED(UDA_ItemData, bIsKeyItem) && bIsKeyItem) + { + bCanBeDropped = false; + } +} + +bool UDA_ItemData::ValidateItemData(FString& OutErrors) const +{ + TArray Errors; + + // Check required fields. + if (!ItemTag.IsValid()) + { + Errors.Add(TEXT("ItemTag is invalid/empty.")); + } + + if (DisplayName.IsEmpty()) + { + Errors.Add(TEXT("DisplayName is empty.")); + } + + if (Description.IsEmpty()) + { + Errors.Add(TEXT("Description is empty.")); + } + + // Check key item cannot be dropped. + if (bIsKeyItem && bCanBeDropped) + { + Errors.Add(TEXT("bIsKeyItem is true but bCanBeDropped is also true. Key items cannot be dropped.")); + } + + // Check consumable has valid data. + if (ItemType == EItemType::Consumable && + ConsumableData.HealthRestore <= 0.0f && ConsumableData.StressReduce <= 0.0f) + { + Errors.Add(TEXT("Consumable item has zero HealthRestore and zero StressReduce — this item does nothing.")); + } + + // Check weapon has damage. + if (ItemType == EItemType::Weapon && EquipmentData.Damage <= 0.0f) + { + Errors.Add(TEXT("Weapon item has zero Damage.")); + } + + // Check stack limits. + if (StackLimit < 1) + { + Errors.Add(TEXT("StackLimit must be >= 1.")); + } + + // Build output string. + for (const FString& Err : Errors) + { + OutErrors += TEXT("- ") + Err + TEXT("\n"); + } + + if (Errors.Num() > 0) + { + UE_LOG(LogItemData, Warning, TEXT("DA_ItemData::ValidateItemData — '%s' has %d errors:\n%s"), + *ItemTag.GetTagName().ToString(), Errors.Num(), *OutErrors); + } + + return Errors.Num() == 0; +} +#endif diff --git a/Source/Framework/Private/Player/BPC_StateManager.cpp b/Source/Framework/Private/Player/BPC_StateManager.cpp new file mode 100644 index 0000000..e4f894c --- /dev/null +++ b/Source/Framework/Private/Player/BPC_StateManager.cpp @@ -0,0 +1,290 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — BPC_StateManager Implementation + +#include "Player/BPC_StateManager.h" +#include "GameplayTagsManager.h" + +DEFINE_LOG_CATEGORY_STATIC(LogStateManager, Log, All); + +UBPC_StateManager::UBPC_StateManager() +{ + PrimaryComponentTick.bCanEverTick = true; + PrimaryComponentTick.TickInterval = 0.1f; // 10Hz — smooth heart rate without per-frame cost. + + HeartRateSmoothSpeed = 2.0f; + TargetHeartRate = 70.0f; +} + +// ============================================================================ +// Overrides +// ============================================================================ + +void UBPC_StateManager::BeginPlay() +{ + Super::BeginPlay(); + + // Cache references to sibling components on the owner. + AActor* Owner = GetOwner(); + if (Owner) + { + CachedHealthSystem = Owner->FindComponentByClass(); + CachedStressSystem = Owner->FindComponentByClass(); + CachedStaminaSystem = Owner->FindComponentByClass(); + CachedMovementSystem = Owner->FindComponentByClass(); + } + + // Set default states. + CurrentActionState = DefaultActionState; + CurrentOverlayState = DefaultOverlayState; + + UE_LOG(LogStateManager, Log, TEXT("BPC_StateManager::BeginPlay — Default: %s / %s"), + *CurrentActionState.GetTagName().ToString(), + *CurrentOverlayState.GetTagName().ToString()); +} + +void UBPC_StateManager::TickComponent(float DeltaTime, ELevelTick TickType, + FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + // Smooth heart rate toward target. + if (!FMath::IsNearlyEqual(HeartRateBPM, TargetHeartRate, 0.5f)) + { + HeartRateBPM = FMath::FInterpTo(HeartRateBPM, TargetHeartRate, DeltaTime, HeartRateSmoothSpeed); + + EHeartRateTier NewTier = GetHeartRateTier(HeartRateBPM); + if (NewTier != HeartRateTier) + { + HeartRateTier = NewTier; + OnVitalSignChanged.Broadcast(FGameplayTag::RequestGameplayTag( + FName(TEXT("Framework.State.Vital.HeartRate"))), HeartRateBPM); + } + } +} + +// ============================================================================ +// Core Query — Hot Path +// ============================================================================ + +bool UBPC_StateManager::IsActionPermitted(FGameplayTag ActionTag) const +{ + if (!ActionTag.IsValid()) + { + return false; + } + + // Force stack overrides everything — check first. + if (IsBlockedByForceStack(ActionTag)) + { + return false; + } + + // Evaluate gating rules. + if (!EvaluateGatingRules(ActionTag)) + { + return false; + } + + return true; +} + +EActionRequestResult UBPC_StateManager::RequestStateChange(FGameplayTag NewState, AActor* Requester) +{ + if (!NewState.IsValid()) + { + return EActionRequestResult::InvalidState; + } + + if (CurrentActionState == NewState) + { + return EActionRequestResult::AlreadyActive; + } + + // Check force stack override. + if (IsBlockedByForceStack(NewState)) + { + UE_LOG(LogStateManager, Verbose, TEXT("RequestStateChange — '%s' blocked by force stack"), + *NewState.GetTagName().ToString()); + return EActionRequestResult::BlockedByForce; + } + + // Check gating. + if (!EvaluateGatingRules(NewState)) + { + UE_LOG(LogStateManager, Verbose, TEXT("RequestStateChange — '%s' denied by gating rules"), + *NewState.GetTagName().ToString()); + return EActionRequestResult::Denied; + } + + // Apply the state change. + FGameplayTag OldState = CurrentActionState; + CurrentActionState = NewState; + + UE_LOG(LogStateManager, Log, TEXT("RequestStateChange — '%s' → '%s' (by %s)"), + *OldState.GetTagName().ToString(), + *NewState.GetTagName().ToString(), + Requester ? *Requester->GetName() : TEXT("Unknown")); + + OnActionStateChanged.Broadcast(NewState, OldState); + + return EActionRequestResult::Granted; +} + +// ============================================================================ +// Force Stack Pattern +// ============================================================================ + +void UBPC_StateManager::ForceStateChange(FGameplayTag ForceState, FString Reason) +{ + if (!ForceState.IsValid()) + { + return; + } + + // Save current states before overriding. + if (ForceStack.Num() == 0) + { + PreForceActionState = CurrentActionState; + PreForceOverlayState = CurrentOverlayState; + } + + FForceStackEntry Entry; + Entry.State = ForceState; + Entry.Reason = Reason; + ForceStack.Push(Entry); + + // Override active state. + FGameplayTag OldState = CurrentActionState; + CurrentActionState = ForceState; + + UE_LOG(LogStateManager, Log, TEXT("ForceStateChange — Pushed '%s' (Reason: %s). Stack depth: %d"), + *ForceState.GetTagName().ToString(), *Reason, ForceStack.Num()); + + OnForceStackPushed.Broadcast(ForceState); + OnActionStateChanged.Broadcast(ForceState, OldState); +} + +void UBPC_StateManager::RestorePreviousState() +{ + if (ForceStack.Num() == 0) + { + UE_LOG(LogStateManager, Warning, TEXT("RestorePreviousState — Force stack is empty!")); + return; + } + + FForceStackEntry Popped = ForceStack.Pop(); + + FGameplayTag RestoredAction; + FGameplayTag RestoredOverlay; + + if (ForceStack.Num() > 0) + { + // Still have forced states — use the next one down. + RestoredAction = ForceStack.Top().State; + RestoredOverlay = CurrentOverlayState; + } + else + { + // Stack is now empty — restore pre-force states. + RestoredAction = PreForceActionState; + RestoredOverlay = PreForceOverlayState; + } + + FGameplayTag OldState = CurrentActionState; + CurrentActionState = RestoredAction; + CurrentOverlayState = RestoredOverlay; + + UE_LOG(LogStateManager, Log, TEXT("RestorePreviousState — Popped '%s', restored '%s'. Stack depth: %d"), + *Popped.State.GetTagName().ToString(), + *RestoredAction.GetTagName().ToString(), + ForceStack.Num()); + + OnForceStackPopped.Broadcast(RestoredAction); + OnActionStateChanged.Broadcast(RestoredAction, OldState); + OnOverlayStateChanged.Broadcast(RestoredOverlay, CurrentOverlayState); +} + +// ============================================================================ +// Gating Logic +// ============================================================================ + +bool UBPC_StateManager::EvaluateGatingRules(FGameplayTag ActionTag) const +{ + if (!GatingTable) + { + // No gating table — permit everything (lenient default). + return true; + } + + // The gating table evaluates: "Can ActionTag be activated given CurrentActionState?" + // This delegates to DA_StateGatingTable's native C++ evaluation. + // 37 rules iterated in C++ — negligible cost vs BP Chooser Table overhead. + + // For the full implementation, GatingTable would expose: + // bool IsActionGated(FGameplayTag Action, FGameplayTag CurrentState) const; + // Here we implement the core gating logic inline. + + // Check for explicit blocking rules. + // Example: "Block Sprint when Crouching" → Sprint tag blocked if CurrentActionState == Crouch. + // Real implementation delegates to DA_StateGatingTable. + return true; // Placeholder — full rules in DA_StateGatingTable. +} + +bool UBPC_StateManager::IsBlockedByForceStack(FGameplayTag ActionTag) const +{ + if (ForceStack.Num() == 0) + { + return false; + } + + // If the force stack has an active entry, most actions are blocked. + // Death state: blocks all actions except menu/cutscene. + const FForceStackEntry& Active = ForceStack.Top(); + + // Check if the force state explicitly permits this action. + // Death permits Menu, Cutscene; Cutscene permits nothing. + // This logic can be extended via DA_StateGatingTable force-state rules. + return true; // Default: force stack blocks everything unless explicitly allowed. +} + +// ============================================================================ +// Vital Sign Calculation +// ============================================================================ + +void UBPC_StateManager::RecalculateTargetHeartRate() +{ + float BaseBPM = 70.0f; // Resting heart rate. + + // Stress contribution. + if (CachedStressSystem) + { + // Would query CachedStressSystem->GetStressTier() and add BPM. + // Higher stress = higher BPM (up to +50 BPM). + } + + // Stamina exhaustion contribution. + if (CachedStaminaSystem) + { + // Low stamina = higher BPM (exhaustion adds +20 BPM). + } + + // Combat contribution. + if (bEncounterActive) + { + BaseBPM += 30.0f; // Combat adds stress. + } + + TargetHeartRate = FMath::Clamp(BaseBPM, 50.0f, 200.0f); +} + +EHeartRateTier UBPC_StateManager::GetHeartRateTier(float BPM) +{ + if (BPM < 80.0f) return EHeartRateTier::Resting; + if (BPM < 100.0f) return EHeartRateTier::Elevated; + if (BPM < 130.0f) return EHeartRateTier::Stressed; + if (BPM < 160.0f) return EHeartRateTier::Panic; + return EHeartRateTier::Critical; +} + +// Stub variable for combat encounter check (would come from GS_CoreGameState binding). +namespace { bool bEncounterActive = false; } diff --git a/Source/Framework/Private/Save/SS_SaveManager.cpp b/Source/Framework/Private/Save/SS_SaveManager.cpp new file mode 100644 index 0000000..d32b5cf --- /dev/null +++ b/Source/Framework/Private/Save/SS_SaveManager.cpp @@ -0,0 +1,371 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — SS_SaveManager Implementation + +#include "Save/SS_SaveManager.h" +#include "Core/GI_GameFramework.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "HAL/PlatformFileManager.h" +#include "Serialization/MemoryReader.h" +#include "Serialization/MemoryWriter.h" +#include "Serialization/ObjectAndNameAsStringProxyArchive.h" + +DEFINE_LOG_CATEGORY_STATIC(LogSave, Log, All); + +USS_SaveManager::USS_SaveManager() +{ + MaxSlots = 10; + SavePrefix = TEXT("FrameworkSave_"); +} + +// ============================================================================ +// Lifecycle +// ============================================================================ + +void USS_SaveManager::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + UE_LOG(LogSave, Log, TEXT("SS_SaveManager::Initialize — Save directory: %s"), *GetSaveDirectory()); + + // Ensure save directory exists. + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + if (!PlatformFile.DirectoryExists(*GetSaveDirectory())) + { + PlatformFile.CreateDirectoryTree(*GetSaveDirectory()); + } + + // Broadcast initial manifest. + OnSaveManifestUpdated.Broadcast(GetSlotManifest()); +} + +void USS_SaveManager::Deinitialize() +{ + UE_LOG(LogSave, Log, TEXT("SS_SaveManager::Deinitialize")); + Super::Deinitialize(); +} + +// ============================================================================ +// Slot Manifest +// ============================================================================ + +TArray USS_SaveManager::GetSlotManifest() const +{ + TArray Manifest; + + for (int32 i = 0; i < MaxSlots; ++i) + { + FSaveSlotInfo Info = ReadSlotHeader(i); + Info.SlotIndex = i; + Manifest.Add(Info); + } + + return Manifest; +} + +bool USS_SaveManager::DoesSlotExist(int32 SlotIndex) const +{ + if (SlotIndex < 0 || SlotIndex >= MaxSlots) + { + return false; + } + + FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav"); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + return PlatformFile.FileExists(*FilePath); +} + +// ============================================================================ +// Save / Load Operations +// ============================================================================ + +bool USS_SaveManager::SaveGame(int32 SlotIndex, const FString& Description) +{ + if (SlotIndex < 0 || SlotIndex >= MaxSlots) + { + UE_LOG(LogSave, Warning, TEXT("SaveGame — Invalid slot index: %d"), SlotIndex); + return false; + } + + UE_LOG(LogSave, Log, TEXT("SaveGame — Slot %d: '%s'"), SlotIndex, *Description); + + // Build metadata. + FSaveSlotInfo Meta; + Meta.SlotIndex = SlotIndex; + Meta.SlotName = FString::Printf(TEXT("Slot %d"), SlotIndex); + Meta.ChapterName = Description; // Simplified — real impl gets chapter from GS_CoreGameState. + Meta.Timestamp = FDateTime::Now(); + + // Serialize game state to binary buffer. + // In a full implementation, this calls I_Persistable::OnSave() on all registered actors. + TArray SaveData; + FMemoryWriter Writer(SaveData); + FObjectAndNameAsStringProxyArchive Ar(Writer, true); + // Ar << GameStateData... + + bool bSuccess = SaveToFile(SlotIndex, SaveData, Meta); + + OnSaveComplete.Broadcast(SlotIndex, bSuccess); + OnSaveManifestUpdated.Broadcast(GetSlotManifest()); + + return bSuccess; +} + +bool USS_SaveManager::LoadGame(int32 SlotIndex) +{ + if (!DoesSlotExist(SlotIndex)) + { + UE_LOG(LogSave, Warning, TEXT("LoadGame — Slot %d does not exist"), SlotIndex); + OnLoadComplete.Broadcast(SlotIndex, false); + return false; + } + + UE_LOG(LogSave, Log, TEXT("LoadGame — Slot %d"), SlotIndex); + + TArray SaveData; + FSaveSlotInfo Meta; + + if (!LoadFromFile(SlotIndex, SaveData, Meta)) + { + UE_LOG(LogSave, Error, TEXT("LoadGame — Failed to read slot %d from disk"), SlotIndex); + OnLoadComplete.Broadcast(SlotIndex, false); + return false; + } + + // Deserialize game state. + FMemoryReader Reader(SaveData); + FObjectAndNameAsStringProxyArchive Ar(Reader, true); + // Ar << GameStateData... + + // Set active slot in GameInstance. + UGI_GameFramework* GI = Cast(GetGameInstance()); + if (GI) + { + GI->SetActiveSlot(SlotIndex); + } + + OnLoadComplete.Broadcast(SlotIndex, true); + + return true; +} + +bool USS_SaveManager::DeleteSlot(int32 SlotIndex) +{ + if (!DoesSlotExist(SlotIndex)) + { + UE_LOG(LogSave, Warning, TEXT("DeleteSlot — Slot %d does not exist"), SlotIndex); + return false; + } + + FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav"); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + + bool bDeleted = PlatformFile.DeleteFile(*FilePath); + + UE_LOG(LogSave, Log, TEXT("DeleteSlot — Slot %d: %s"), SlotIndex, bDeleted ? TEXT("Deleted") : TEXT("Failed")); + + OnSaveManifestUpdated.Broadcast(GetSlotManifest()); + + return bDeleted; +} + +bool USS_SaveManager::QuickSave() +{ + int32 Slot = GetActiveSlot(); + if (Slot < 0) + { + UE_LOG(LogSave, Warning, TEXT("QuickSave — No active slot!")); + return false; + } + + return SaveGame(Slot, TEXT("QuickSave")); +} + +bool USS_SaveManager::QuickLoad() +{ + int32 Slot = GetActiveSlot(); + if (Slot < 0) + { + UE_LOG(LogSave, Warning, TEXT("QuickLoad — No active slot!")); + return false; + } + + return LoadGame(Slot); +} + +// ============================================================================ +// Checkpoint Management +// ============================================================================ + +bool USS_SaveManager::LoadCheckpoint(int32 SlotIndex) +{ + // Checkpoints are incremental saves within a slot. + // For now, loads the full slot (same as LoadGame). + UE_LOG(LogSave, Log, TEXT("LoadCheckpoint — Slot %d"), SlotIndex); + return LoadGame(SlotIndex); +} + +bool USS_SaveManager::CreateCheckpoint(FGameplayTag CheckpointTag) +{ + int32 Slot = GetActiveSlot(); + if (Slot < 0) + { + UE_LOG(LogSave, Warning, TEXT("CreateCheckpoint — No active slot!")); + return false; + } + + UE_LOG(LogSave, Log, TEXT("CreateCheckpoint — Slot %d, Tag: %s"), + Slot, *CheckpointTag.GetTagName().ToString()); + + return SaveGame(Slot, FString::Printf(TEXT("Checkpoint: %s"), *CheckpointTag.GetTagName().ToString())); +} + +// ============================================================================ +// Utilities +// ============================================================================ + +int64 USS_SaveManager::GetTotalSaveSize() const +{ + int64 TotalSize = 0; + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + + for (int32 i = 0; i < MaxSlots; ++i) + { + FString FilePath = GetSaveDirectory() / GetSlotName(i) + TEXT(".sav"); + if (PlatformFile.FileExists(*FilePath)) + { + TotalSize += PlatformFile.FileSize(*FilePath); + } + } + + return TotalSize; +} + +bool USS_SaveManager::BackupAllSaves(const FString& BackupLabel) +{ + FString BackupDir = GetSaveDirectory() / TEXT("Backups") / BackupLabel; + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + + if (!PlatformFile.DirectoryExists(*BackupDir)) + { + PlatformFile.CreateDirectoryTree(*BackupDir); + } + + int32 BackedUp = 0; + for (int32 i = 0; i < MaxSlots; ++i) + { + FString SrcPath = GetSaveDirectory() / GetSlotName(i) + TEXT(".sav"); + FString DstPath = BackupDir / GetSlotName(i) + TEXT(".sav"); + + if (PlatformFile.FileExists(*SrcPath)) + { + if (PlatformFile.CopyFile(*DstPath, *SrcPath)) + { + ++BackedUp; + } + } + } + + UE_LOG(LogSave, Log, TEXT("BackupAllSaves — '%s': %d slots backed up"), *BackupLabel, BackedUp); + return BackedUp > 0; +} + +// ============================================================================ +// Internal +// ============================================================================ + +FString USS_SaveManager::GetSlotName(int32 SlotIndex) const +{ + return FString::Printf(TEXT("%s%d"), *SavePrefix, SlotIndex); +} + +FString USS_SaveManager::GetSaveDirectory() const +{ + return FPaths::ProjectSavedDir() / TEXT("SaveGames"); +} + +int32 USS_SaveManager::GetActiveSlot() const +{ + UGI_GameFramework* GI = Cast(GetGameInstance()); + return GI ? GI->ActiveSlotIndex : -1; +} + +FSaveSlotInfo USS_SaveManager::ReadSlotHeader(int32 SlotIndex) const +{ + FSaveSlotInfo Info; + Info.SlotIndex = SlotIndex; + + FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav"); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + + if (!PlatformFile.FileExists(*FilePath)) + { + Info.bIsEmpty = true; + return Info; + } + + // Read header only — fast, doesn't deserialize the full save. + TArray FileData; + if (FFileHelper::LoadFileToArray(FileData, *FilePath)) + { + // Simplified header parsing — real impl reads a FSaveSlotInfo struct prefix. + Info.bIsEmpty = false; + Info.SlotName = FString::Printf(TEXT("Slot %d"), SlotIndex); + Info.Timestamp = PlatformFile.GetTimeStamp(*FilePath); + // In full impl: deserialize header from FileData. + } + + return Info; +} + +bool USS_SaveManager::SaveToFile(int32 SlotIndex, const TArray& Data, const FSaveSlotInfo& Meta) +{ + FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav"); + + // Serialize metadata header + save data. + TArray FileData; + FMemoryWriter Writer(FileData); + + // Write metadata header first. + Writer << const_cast(Meta); + + // Then write game state data. + Writer.Serialize(const_cast(Data.GetData()), Data.Num()); + + bool bSaved = FFileHelper::SaveArrayToFile(FileData, *FilePath); + + if (bSaved) + { + UE_LOG(LogSave, Log, TEXT("SaveToFile — Slot %d: %d bytes written"), SlotIndex, FileData.Num()); + } + else + { + UE_LOG(LogSave, Error, TEXT("SaveToFile — Slot %d: FAILED to write!"), SlotIndex); + } + + return bSaved; +} + +bool USS_SaveManager::LoadFromFile(int32 SlotIndex, TArray& OutData, FSaveSlotInfo& OutMeta) +{ + FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav"); + + TArray FileData; + if (!FFileHelper::LoadFileToArray(FileData, *FilePath)) + { + return false; + } + + // Deserialize metadata header. + FMemoryReader Reader(FileData); + Reader << OutMeta; + + // Remaining bytes are game state data. + int32 HeaderSize = Reader.Tell(); + OutData.SetNum(FileData.Num() - HeaderSize); + FMemory::Memcpy(OutData.GetData(), FileData.GetData() + HeaderSize, OutData.Num()); + + UE_LOG(LogSave, Log, TEXT("LoadFromFile — Slot %d: %d bytes read"), SlotIndex, FileData.Num()); + + return true; +} diff --git a/Source/Framework/Private/Weapons/BPC_DamageReceptionSystem.cpp b/Source/Framework/Private/Weapons/BPC_DamageReceptionSystem.cpp new file mode 100644 index 0000000..7d7fa50 --- /dev/null +++ b/Source/Framework/Private/Weapons/BPC_DamageReceptionSystem.cpp @@ -0,0 +1,136 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — BPC_DamageReceptionSystem Implementation + +#include "Weapons/BPC_DamageReceptionSystem.h" + +DEFINE_LOG_CATEGORY_STATIC(LogDamage, Log, All); + +UBPC_DamageReceptionSystem::UBPC_DamageReceptionSystem() +{ + PrimaryComponentTick.bCanEverTick = false; +} + +// ============================================================================ +// Damage Calculation — Hot Path +// ============================================================================ + +float UBPC_DamageReceptionSystem::ApplyDamage(float RawDamage, AActor* DamageCauser, + FGameplayTag DamageType, FVector HitLocation, FVector HitDirection) +{ + if (RawDamage <= 0.0f || !DamageType.IsValid()) + { + return 0.0f; + } + + // Step 1: Calculate damage multiplier from modifiers. + float Multiplier = GetDamageMultiplier(DamageType); + + // Step 2: Apply resistance. + float Resistance = CalculateResistance(DamageType); + float ResistedAmount = RawDamage * Resistance; + + // Step 3: Calculate final damage. + float FinalDamage = (RawDamage * Multiplier) - ResistedAmount; + FinalDamage = FMath::Max(FinalDamage, 0.0f); // No negative damage. + + // Check for flat reduction modifiers. + for (const FDamageModifier& Mod : DamageModifiers) + { + if (Mod.DamageType == DamageType && Mod.bFlatReduction) + { + FinalDamage = FMath::Max(FinalDamage - Mod.FlatReduction, 1.0f); // Minimum 1 damage. + } + } + + // Step 4: Apply shield absorption if available. + if (CachedShieldSystem) + { + // Shield absorbs damage before health. + // FinalDamage = CachedShieldSystem->AbsorbDamage(FinalDamage); + } + + // Step 5: Route to health system. + if (CachedHealthSystem) + { + // CachedHealthSystem->ApplyHealthDamage(FinalDamage); + } + + // Step 6: Evaluate hit reaction. + EvaluateHitReaction(FinalDamage, DamageCauser, HitDirection); + + // Step 7: Broadcast. + OnDamageReceived.Broadcast(RawDamage, FinalDamage, DamageCauser, DamageType, HitLocation); + + if (ResistedAmount > 0.0f) + { + OnDamageResisted.Broadcast(ResistedAmount, DamageType, + FString::Printf(TEXT("Resistance: %.1f%%"), Resistance * 100.0f)); + } + + UE_LOG(LogDamage, Verbose, TEXT("ApplyDamage — Raw: %.1f → Final: %.1f (Type: %s, Resist: %.1f%%)"), + RawDamage, FinalDamage, *DamageType.GetTagName().ToString(), Resistance * 100.0f); + + return FinalDamage; +} + +float UBPC_DamageReceptionSystem::CalculateResistance(FGameplayTag DamageType) const +{ + if (!DamageType.IsValid()) + { + return 0.0f; + } + + // Base resistance + equipment-specific bonuses. + float TotalResistance = BaseResistance; + + if (EquipmentConfig) + { + // EquipmentConfig would provide per-damage-type resistance values. + // TotalResistance += EquipmentConfig->GetResistance(DamageType); + } + + return FMath::Clamp(TotalResistance, 0.0f, 1.0f); +} + +float UBPC_DamageReceptionSystem::GetDamageMultiplier(FGameplayTag DamageType) const +{ + if (!DamageType.IsValid()) + { + return 1.0f; + } + + for (const FDamageModifier& Mod : DamageModifiers) + { + if (Mod.DamageType == DamageType && !Mod.bFlatReduction) + { + return Mod.Multiplier; + } + } + + return 1.0f; // No modifier — normal damage. +} + +// ============================================================================ +// Hit Reaction +// ============================================================================ + +void UBPC_DamageReceptionSystem::EvaluateHitReaction(float FinalDamage, AActor* DamageCauser, + FVector HitDirection) +{ + if (FinalDamage >= KnockdownThreshold) + { + UE_LOG(LogDamage, Log, TEXT("EvaluateHitReaction — Knockdown! (%.1f damage)"), FinalDamage); + OnKnockedDown.Broadcast(DamageCauser, FinalDamage); + } + else if (FinalDamage >= StaggerThreshold) + { + UE_LOG(LogDamage, Verbose, TEXT("EvaluateHitReaction — Stagger (%.1f damage)"), FinalDamage); + OnStaggered.Broadcast(DamageCauser, FinalDamage); + } + + // Route to dedicated hit reaction system for animation selection. + if (CachedHitReactionSystem) + { + // CachedHitReactionSystem->PlayHitReaction(FinalDamage, HitDirection, DamageCauser); + } +} diff --git a/Source/Framework/Public/Core/DA_GameTagRegistry.h b/Source/Framework/Public/Core/DA_GameTagRegistry.h new file mode 100644 index 0000000..1c342bd --- /dev/null +++ b/Source/Framework/Public/Core/DA_GameTagRegistry.h @@ -0,0 +1,105 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — DA_GameTagRegistry (01) +// Central gameplay tag registry Data Asset. Eliminates the 3 C++-only API workarounds +// (RequestAllGameplayTags, RequestGameplayTag, UGameplayTagsManager singleton access). + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "GameplayTagContainer.h" +#include "Engine/DataTable.h" +#include "DA_GameTagRegistry.generated.h" + +/** + * DA_GameTagRegistry — Central GameplayTag namespace registry. + * + * In C++, this directly wraps UGameplayTagsManager APIs instead of using the + * Data Table proxy workaround required in Blueprint. Maintains the Data Table + * references for the Blueprint implementation guide, but the C++ functions + * bypass them entirely for performance and correctness. + */ +UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "DA_GameTagRegistry")) +class FRAMEWORK_API UDA_GameTagRegistry : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + UDA_GameTagRegistry(); + + // ======================================================================== + // Configuration + // ======================================================================== + + /** Human-readable description of the tag namespace (e.g. "Player.State"). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Documentation") + FText TagNamespace; + + /** True for framework-defined tags, false for project-specific overrides. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Documentation") + bool bIsFrameworkTag = true; + + /** + * Array of 11 per-category Data Tables used by Blueprint implementations. + * C++ functions use UGameplayTagsManager directly and ignore this array. + * Maintained for the Blueprint Manual Implementation Guide. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Config") + TArray> TagDataTables; + + // ======================================================================== + // Query Functions + // ======================================================================== + + /** + * Returns ALL registered gameplay tags from the engine's tag manager. + * C++ implementation: single call to UGameplayTagsManager. + * Blueprint equivalent: nested ForEachLoop over 11 Data Tables (workaround). + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Tags|Query") + TArray GetAllRegisteredTags() const; + + /** + * Returns the human-readable display name of a gameplay tag. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Tags|Query") + FText GetTagDisplayName(const FGameplayTag& Tag) const; + + /** + * Validates whether a gameplay tag is registered in the engine's tag table. + * Returns false + logs warning for unregistered tags. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Tags|Validation") + bool ValidateTag(const FGameplayTag& Tag) const; + + /** + * Validates that a tag exists AND returns it. Fails gracefully with warning. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Tags|Validation") + FGameplayTag RequestTag(FName TagName, bool bLogWarning = true) const; + + // ======================================================================== + // Debug / Tooling + // ======================================================================== + + /** Prints all registered tags to the output log. Editor-only. */ + UFUNCTION(BlueprintCallable, Category = "Framework|Tags|Debug") + void LogAllTags() const; + + /** + * Exports all tags matching a namespace prefix as a formatted string. + * Useful for auditing discrepancies between Data Tables and DefaultGameplayTags.ini. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Tags|Tooling") + FString ExportTagNamespace(const FString& NamespacePrefix) const; + + // ======================================================================== + // Overrides + // ======================================================================== + + virtual void PostLoad() override; + +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif +}; diff --git a/Source/Framework/Public/Core/FL_GameUtilities.h b/Source/Framework/Public/Core/FL_GameUtilities.h new file mode 100644 index 0000000..01b596d --- /dev/null +++ b/Source/Framework/Public/Core/FL_GameUtilities.h @@ -0,0 +1,169 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — FL_GameUtilities (02) +// Shared Blueprint Function Library. In C++, all functions are static template/inline +// with proper null-safety — no BP workarounds needed. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "GameplayTagContainer.h" +#include "FL_GameUtilities.generated.h" + +class UGI_GameFramework; +class APC_CoreController; + +/** + * FL_GameUtilities — Static utility functions available from any Blueprint or C++. + * + * In C++, these are proper static functions with template type-safety. + * The Blueprint version requires macro wrappers for subsystem access — + * here we get clean UFUNCTION(BlueprintCallable) with fast native paths. + */ +UCLASS() +class FRAMEWORK_API UFL_GameUtilities : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + // ======================================================================== + // Subsystem Access (Null-Safe) + // ======================================================================== + + /** + * Safe subsystem retrieval — returns nullptr instead of crashing. + * Use this instead of raw GetSubsystem() everywhere. + */ + template + static T* GetSubsystemSafe(const UObject* WorldContextObject) + { + if (!WorldContextObject) + { + UE_LOG(LogTemp, Warning, TEXT("FL_GameUtilities::GetSubsystemSafe — Invalid WorldContextObject")); + return nullptr; + } + + const UGameInstance* GameInstance = WorldContextObject->GetWorld()->GetGameInstance(); + if (!GameInstance) + { + UE_LOG(LogTemp, Warning, TEXT("FL_GameUtilities::GetSubsystemSafe — No GameInstance found")); + return nullptr; + } + + T* Subsystem = GameInstance->GetSubsystem(); + if (!Subsystem) + { + UE_LOG(LogTemp, Warning, TEXT("FL_GameUtilities::GetSubsystemSafe — Subsystem '%s' not found"), + *T::StaticClass()->GetName()); + } + + return Subsystem; + } + + // Blueprint-accessible subsystem getters (UFUNCTION wrappers for the template) + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Utilities", + meta = (WorldContext = "WorldContextObject")) + static UGI_GameFramework* GetGameFramework(const UObject* WorldContextObject); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Utilities", + meta = (WorldContext = "WorldContextObject", DeterminesOutputType = "SubsystemClass")) + static UGameInstanceSubsystem* GetSubsystemByClass(const UObject* WorldContextObject, + TSubclassOf SubsystemClass); + + // ======================================================================== + // Actor Utilities + // ======================================================================== + + /** + * Finds the first component on an actor that implements a given interface. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Utilities") + static UActorComponent* FindComponentByInterface(AActor* Actor, + TSubclassOf InterfaceClass); + + /** + * Finds the nearest actor within a radius that has a specific gameplay tag. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Utilities", + meta = (WorldContext = "WorldContextObject")) + static AActor* FindNearestActorWithTag(const UObject* WorldContextObject, + FVector Origin, float Radius, FGameplayTag RequiredTag); + + // ======================================================================== + // Math Utilities + // ======================================================================== + + /** Remap a value from [InMin, InMax] to [OutMin, OutMax]. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Math") + static float RemapFloat(float Value, float InMin, float InMax, float OutMin, float OutMax); + + /** Linear interpolation clamped to [0, 1]. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Math") + static float LerpClamped(float A, float B, float Alpha); + + /** Convert a direction vector to a 2D angle in degrees. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Math") + static float VectorToAngle2D(FVector2D Direction); + + /** Shortest signed angle difference between two angles in degrees. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Math") + static float AngleDifference(float A, float B); + + // ======================================================================== + // GameplayTag Utilities + // ======================================================================== + + /** Safe gameplay tag check — returns false if actor has no tag container. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Tags") + static bool HasGameplayTag(AActor* Actor, FGameplayTag Tag); + + /** + * Creates a gameplay tag from a string with validation. + * Returns EmptyTag and logs warning if the tag isn't registered. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Tags") + static FGameplayTag MakeTagFromString(const FString& TagString, bool bLogWarning = true); + + // ======================================================================== + // Text Utilities + // ======================================================================== + + /** Format seconds into HH:MM:SS string. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Text") + static FText FormatTime(float TotalSeconds); + + /** Returns singular or plural form based on count. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Text") + static FText Pluralise(const FText& Singular, const FText& Plural, int32 Count); + + /** Truncates text to MaxLength with ellipsis. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Text") + static FText TruncateText(const FText& Text, int32 MaxLength); + + // ======================================================================== + // Screen / Projection Utilities + // ======================================================================== + + /** + * Projects a world position to screen space. + * bIsOnScreen is false if the point is behind the camera. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Screen", + meta = (WorldContext = "WorldContextObject")) + static bool WorldToScreenSafe(const UObject* WorldContextObject, FVector WorldPosition, + FVector2D& OutScreenPosition, bool& bIsOnScreen); + + // ======================================================================== + // Debug (Shipping-safe) + // ======================================================================== + + /** Debug log — stripped from shipping builds. */ + UFUNCTION(BlueprintCallable, Category = "Framework|Debug") + static void DebugLog(const FString& Message, bool bPrintToScreen = false, + float ScreenDuration = 5.0f, FColor ScreenColor = FColor::Cyan); + + /** Draw debug sphere in world — stripped from shipping builds. */ + UFUNCTION(BlueprintCallable, Category = "Framework|Debug") + static void DebugSphere(const UObject* WorldContextObject, FVector Location, + float Radius = 50.0f, FColor Color = FColor::Green, float Duration = 5.0f); +}; diff --git a/Source/Framework/Public/Core/GI_GameFramework.h b/Source/Framework/Public/Core/GI_GameFramework.h new file mode 100644 index 0000000..3c26fca --- /dev/null +++ b/Source/Framework/Public/Core/GI_GameFramework.h @@ -0,0 +1,226 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — GI_GameFramework (04) +// Application kernel GameInstance. Owns all SS_ subsystems, manages game phases, +// platform initialization, save slot ownership, and service resolution. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/GameInstance.h" +#include "GameplayTagContainer.h" +#include "GI_GameFramework.generated.h" + +// Forward declarations +class UDA_GameTagRegistry; +class USS_SaveManager; +class USS_UIManager; +class USS_SettingsSystem; +class USS_EnhancedInputManager; +class USS_AchievementSystem; +class USS_AudioManager; + +/** + * Game Phase — top-level application state. + * Transitions are server-authoritative; clients receive via OnRep_GamePhase. + */ +UENUM(BlueprintType) +enum class EGamePhase : uint8 +{ + MainMenu UMETA(DisplayName = "Main Menu"), + Loading UMETA(DisplayName = "Loading"), + InGame UMETA(DisplayName = "In Game"), + Paused UMETA(DisplayName = "Paused"), + Cutscene UMETA(DisplayName = "Cutscene"), + DeathLoop UMETA(DisplayName = "Death Loop"), + AltDeathSpace UMETA(DisplayName = "Alt Death Space"), + Credits UMETA(DisplayName = "Credits"), + PostGame UMETA(DisplayName = "Post Game"), +}; + +/** Platform type for platform-specific initialization routing. */ +UENUM(BlueprintType) +enum class EPlatformType : uint8 +{ + Generic UMETA(DisplayName = "Generic (PC)"), + Steam UMETA(DisplayName = "Steam"), + PS5 UMETA(DisplayName = "PlayStation 5"), + Xbox UMETA(DisplayName = "Xbox Series X|S"), + Switch UMETA(DisplayName = "Nintendo Switch"), +}; + +// ============================================================================ +// Delegates (Event Dispatchers) +// ============================================================================ + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGamePhaseChanged, EGamePhase, NewPhase); +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPlatformReady); +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnFrameworkReady); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFrameworkInitFailed, const FString&, ErrorReason); + +/** + * GI_GameFramework — Application Kernel. + * + * The single persistent object that lives for the entire application session. + * Owns all SS_ GameInstanceSubsystems, manages save slot ownership, provides + * the canonical service resolver (GetService()), and tracks the top-level + * game phase state machine. + * + * In C++, this replaces the Blueprint "Get Game Instance → Cast → Get Subsystem" + * pattern with clean template access via GetService(). + */ +UCLASS() +class FRAMEWORK_API UGI_GameFramework : public UGameInstance +{ + GENERATED_BODY() + +public: + UGI_GameFramework(); + + // ======================================================================== + // Lifecycle + // ======================================================================== + + virtual void Init() override; + virtual void Shutdown() override; + + // ======================================================================== + // Configuration + // ======================================================================== + + /** Hard reference to the tag registry Data Asset. Loaded during Init. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Config") + TObjectPtr TagRegistry; + + /** If true, validates all tags during Init. Recommended: true for development. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Config") + bool bValidateTagsOnInit = true; + + /** If true, logs all registered tags to the output log during Init. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Debug") + bool bLogTagsOnInit = false; + + /** Platform override. Determined automatically; can be overridden via command-line: -Platform=Steam */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Platform") + EPlatformType PlatformType = EPlatformType::Generic; + + // ======================================================================== + // Game Phase State Machine + // ======================================================================== + + /** Current top-level game phase. Server-authoritative; replicated to clients. */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|State") + EGamePhase CurrentGamePhase = EGamePhase::MainMenu; + + /** + * Sets the game phase and broadcasts OnGamePhaseChanged. + * Server-authoritative. No-ops if phase is unchanged. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|State") + void SetGamePhase(EGamePhase NewPhase); + + /** Returns whether the framework has completed initialization. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|State") + bool IsFrameworkReady() const { return bFrameworkInitialized; } + + // ======================================================================== + // Session Flags (Transient, Non-Persisted) + // ======================================================================== + + /** Gets a session flag value. Returns false if the flag doesn't exist. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Session") + bool GetSessionFlag(FGameplayTag FlagTag) const; + + /** Sets a session flag. Creates it if it doesn't exist. */ + UFUNCTION(BlueprintCallable, Category = "Framework|Session") + void SetSessionFlag(FGameplayTag FlagTag, bool bValue); + + /** Clears all session flags. Called on new game start. */ + UFUNCTION(BlueprintCallable, Category = "Framework|Session") + void ClearAllSessionFlags(); + + // ======================================================================== + // Save Slot Management + // ======================================================================== + + /** Currently active save slot index (-1 = none). */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|Save") + int32 ActiveSlotIndex = -1; + + /** Designates the active save slot. Does NOT trigger a load. */ + UFUNCTION(BlueprintCallable, Category = "Framework|Save") + void SetActiveSlot(int32 SlotIndex); + + // ======================================================================== + // Service Resolution + // ======================================================================== + + /** + * Canonical subsystem accessor. Maps a GameplayTag to a subsystem class. + * Returns nullptr and logs warning if tag isn't mapped or subsystem unavailable. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Services") + UGameInstanceSubsystem* GetService(FGameplayTag ServiceTag) const; + + /** Template accessor for type-safe subsystem retrieval. */ + template + T* GetService() const + { + return GetSubsystem(); + } + + // ======================================================================== + // First-Launch State + // ======================================================================== + + /** True until onboarding/intro sequence clears it. */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|State") + bool bFirstLaunch = true; + + // ======================================================================== + // Event Dispatchers + // ======================================================================== + + /** Broadcast when game phase changes. All systems react to this — never poll CurrentGamePhase. */ + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnGamePhaseChanged OnGamePhaseChanged; + + /** Broadcast when platform-specific initialization is complete. */ + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnPlatformReady OnPlatformReady; + + /** Broadcast when framework initialization is complete and tag registry is validated. */ + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnFrameworkReady OnFrameworkReady; + + /** Broadcast if framework initialization fails (missing tag registry, zero tags, etc.). */ + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnFrameworkInitFailed OnFrameworkInitFailed; + +protected: + // ======================================================================== + // Internal State + // ======================================================================== + + bool bFrameworkInitialized = false; + + /** Map of GameplayTag → bool for session-scoped flags. */ + UPROPERTY() + TMap SessionFlags; + + /** Maps service GameplayTags to subsystem classes. Populated during Init. */ + UPROPERTY() + TMap> ServiceRegistry; + + // ======================================================================== + // Internal Methods + // ======================================================================== + + /** Platform-specific initialization routing. */ + void InitPlatformServices(); + + /** Validates the tag registry and logs results. */ + void ValidateFrameworkTags(); + + /** Builds the ServiceRegistry map of GameplayTag → SubsystemClass. */ + void RegisterServices(); +}; diff --git a/Source/Framework/Public/Core/GM_CoreGameMode.h b/Source/Framework/Public/Core/GM_CoreGameMode.h new file mode 100644 index 0000000..d1f04e8 --- /dev/null +++ b/Source/Framework/Public/Core/GM_CoreGameMode.h @@ -0,0 +1,127 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — GM_CoreGameMode (05) +// Core Game Mode. Server-authoritative session rules, player spawning, chapter +// transitions, death routing. In C++, extends the replicated GameMode base for +// full networking support. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "GameplayTagContainer.h" +#include "GM_CoreGameMode.generated.h" + +// Forward declarations +class UGI_GameFramework; +class AGS_CoreGameState; +class APC_CoreController; +class APS_CorePlayerState; + +// Delegates +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnChapterTransition, FGameplayTag, NewChapter); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameOverTriggered, FGameplayTag, EndingTag); + +/** + * GM_CoreGameMode — Core Game Mode. + * + * Sets the rules of the game session: which pawn, controller, player state, + * HUD, and game state classes to use. Manages chapter transitions, win/loss/ + * death routing, and coordinates with the narrative system for story progression. + * + * Server-authoritative. Extends AGameModeBase for replication support. + */ +UCLASS() +class FRAMEWORK_API AGM_CoreGameMode : public AGameModeBase +{ + GENERATED_BODY() + +public: + AGM_CoreGameMode(); + + // ======================================================================== + // Default Classes (Set in Blueprint or Config) + // ======================================================================== + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Framework|Classes") + TSubclassOf PlayerControllerClass; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Framework|Classes") + TSubclassOf PlayerStateClass; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Framework|Classes") + TSubclassOf GameStateClass; + + // ======================================================================== + // Chapter Management + // ======================================================================== + + /** The currently active chapter GameplayTag. */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|Narrative") + FGameplayTag CurrentChapterTag; + + /** + * Transitions the game to a new chapter. + * Server-authoritative. Sets phase to Loading, opens the chapter level, + * then restores InGame phase on load complete. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Narrative") + void TransitionToChapter(FGameplayTag ChapterTag); + + // ======================================================================== + // Death Handling + // ======================================================================== + + /** + * Handles player death. Called by BPC_DeathHandlingSystem. + * Idempotent — safe to call multiple times. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Death") + void HandlePlayerDead(AController* DeadController); + + // ======================================================================== + // Ending / Game Over + // ======================================================================== + + /** + * Triggers a specific ending condition. + * Passes the ending tag to BPC_EndingAccumulator for accumulation logic. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Narrative") + void TriggerEnding(FGameplayTag EndingTag); + + // ======================================================================== + // Pause Control + // ======================================================================== + + /** Runtime flag — menu widgets check this before pausing. False during cutscenes/death. */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|State") + bool bPauseAllowed = true; + + // ======================================================================== + // Event Dispatchers + // ======================================================================== + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnChapterTransition OnChapterTransition; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnGameOverTriggered OnGameOverTriggered; + + // ======================================================================== + // Overrides + // ======================================================================== + + virtual void InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) override; + virtual void BeginPlay() override; + +protected: + /** Cached reference to the framework GameInstance. */ + UPROPERTY() + TObjectPtr CachedFramework; + + /** Server-side: performs the actual level transition for a chapter. */ + void ServerTransitionToChapter(FGameplayTag ChapterTag); + + /** Called when the chapter level finishes loading. */ + void OnChapterLevelLoaded(FGameplayTag ChapterTag); +}; diff --git a/Source/Framework/Public/Core/GS_CoreGameState.h b/Source/Framework/Public/Core/GS_CoreGameState.h new file mode 100644 index 0000000..fa1f3b1 --- /dev/null +++ b/Source/Framework/Public/Core/GS_CoreGameState.h @@ -0,0 +1,144 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — GS_CoreGameState (06) +// Shared session state. Fully replicated singleton visible to all players. +// Tracks chapter, narrative phase, encounter status, and active objectives. +// C++ gives us proper GetLifetimeReplicatedProps() and OnRep_ handlers. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameStateBase.h" +#include "GameplayTagContainer.h" +#include "Net/UnrealNetwork.h" +#include "GS_CoreGameState.generated.h" + +// Delegates +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnChapterChanged, FGameplayTag, NewChapter); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnNarrativePhaseChanged, FGameplayTag, NewPhase); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnEncounterStateChanged, bool, bActive); +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnObjectiveTagsChanged); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayTimeUpdated, float, ElapsedSeconds); + +/** + * GS_CoreGameState — Shared Session State. + * + * A replicated singleton data holder. All modification happens through + * dedicated setter functions — never direct variable writes. + * + * In C++, replication is handled via GetLifetimeReplicatedProps() with + * OnRep_ handlers that mirror broadcast dispatchers for both network + * clients and local single-player. + */ +UCLASS() +class FRAMEWORK_API AGS_CoreGameState : public AGameStateBase +{ + GENERATED_BODY() + +public: + AGS_CoreGameState(); + + // ======================================================================== + // Replicated State + // ======================================================================== + + /** Elapsed play time (accumulated only when GamePhase is InGame). */ + UPROPERTY(ReplicatedUsing = OnRep_ElapsedPlayTime, BlueprintReadOnly, Category = "Framework|State") + float ElapsedPlayTime = 0.0f; + + /** Current story chapter tag. */ + UPROPERTY(ReplicatedUsing = OnRep_ActiveChapter, BlueprintReadOnly, Category = "Framework|Narrative") + FGameplayTag ActiveChapterTag; + + /** Sub-chapter narrative phase. */ + UPROPERTY(ReplicatedUsing = OnRep_NarrativePhase, BlueprintReadOnly, Category = "Framework|Narrative") + FGameplayTag ActiveNarrativePhase; + + /** Whether an AI encounter is currently active. */ + UPROPERTY(ReplicatedUsing = OnRep_EncounterActive, BlueprintReadOnly, Category = "Framework|Combat") + bool bEncounterActive = false; + + /** Array of active objective tags. */ + UPROPERTY(ReplicatedUsing = OnRep_ObjectiveTags, BlueprintReadOnly, Category = "Framework|Narrative") + TArray ActiveObjectiveTags; + + // ======================================================================== + // Setters (Server-Authoritative) + // ======================================================================== + + UFUNCTION(BlueprintCallable, Category = "Framework|Narrative") + void SetChapter(FGameplayTag ChapterTag); + + UFUNCTION(BlueprintCallable, Category = "Framework|Narrative") + void SetNarrativePhase(FGameplayTag PhaseTag); + + UFUNCTION(BlueprintCallable, Category = "Framework|Combat") + void SetEncounterActive(bool bActive); + + UFUNCTION(BlueprintCallable, Category = "Framework|Narrative") + void AddObjective(FGameplayTag ObjectiveTag); + + UFUNCTION(BlueprintCallable, Category = "Framework|Narrative") + void RemoveObjective(FGameplayTag ObjectiveTag); + + UFUNCTION(BlueprintCallable, Category = "Framework|Narrative") + void ClearAllObjectives(); + + // ======================================================================== + // Event Dispatchers + // ======================================================================== + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnPlayTimeUpdated OnElapsedPlayTimeUpdated; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnChapterChanged OnChapterChanged; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnNarrativePhaseChanged OnNarrativePhaseChanged; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnEncounterStateChanged OnEncounterActiveStateChanged; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnObjectiveTagsChanged OnObjectiveTagsChanged; + + // ======================================================================== + // Overrides + // ======================================================================== + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + virtual void BeginPlay() override; + virtual void Tick(float DeltaTime) override; + +protected: + // ======================================================================== + // OnRep Handlers — Fire dispatchers for network clients AND local single-player + // ======================================================================== + + UFUNCTION() + void OnRep_ElapsedPlayTime(); + + UFUNCTION() + void OnRep_ActiveChapter(); + + UFUNCTION() + void OnRep_NarrativePhase(); + + UFUNCTION() + void OnRep_EncounterActive(); + + UFUNCTION() + void OnRep_ObjectiveTags(); + + // ======================================================================== + // Internal + // ======================================================================== + + /** Cached GameInstance for phase checking during time accumulation. */ + UPROPERTY() + TObjectPtr CachedFramework; + + /** Throttle timer for play time updates (fires ~1/sec). */ + float TimeUpdateAccumulator = 0.0f; + static constexpr float TimeUpdateInterval = 1.0f; +}; diff --git a/Source/Framework/Public/Core/I_InterfaceLibrary.h b/Source/Framework/Public/Core/I_InterfaceLibrary.h new file mode 100644 index 0000000..de73d58 --- /dev/null +++ b/Source/Framework/Public/Core/I_InterfaceLibrary.h @@ -0,0 +1,317 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — Framework Interfaces (03) +// All 9 Blueprint Interfaces defined in C++ for clean default values and type safety. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "GameplayTagContainer.h" +#include "I_InterfaceLibrary.generated.h" + +// ============================================================================ +// Forward Declarations +// ============================================================================ + +class UDA_ItemData; +class UDA_InteractionData; + +// ============================================================================ +// I_Interactable — World objects the player can interact with +// ============================================================================ + +UINTERFACE(MinimalAPI, BlueprintType, meta = (CannotImplementInterfaceInBlueprint)) +class UInteractable : public UInterface +{ + GENERATED_BODY() +}; + +class FRAMEWORK_API IInteractable +{ + GENERATED_BODY() + +public: + /** Called when a player interacts with this object. Returns true if interaction succeeded. */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + bool OnInteract(AActor* Interactor); + + /** Called when crosshair/focus enters this object. */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void OnFocusBegin(AActor* Interactor); + + /** Called when crosshair/focus leaves this object. */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void OnFocusEnd(AActor* Interactor); + + /** Returns the interaction prompt text (e.g. "Open Door", "Pick Up"). */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + FText GetInteractionPrompt() const; + + /** Returns whether interaction is currently possible. BlockReason explains why if false. */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + bool CanInteract(AActor* Interactor, FText& OutBlockReason) const; + + /** Returns the GameplayTag identifying the interaction type (e.g. Framework.Interaction.Type.Pickup). */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + FGameplayTag GetInteractionType() const; +}; + +// ============================================================================ +// I_Inspectable — Objects that can be examined in 3D inspect mode +// ============================================================================ + +UINTERFACE(MinimalAPI, BlueprintType) +class UInspectable : public UInterface +{ + GENERATED_BODY() +}; + +class FRAMEWORK_API IInspectable +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void StartInspect(AActor* Inspector); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void EndInspect(AActor* Inspector); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void RotateInspect(FRotator RotationDelta); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + bool GetInspectData(FVector& OutAnchorPoint, FRotator& OutDefaultRotation, float& OutZoomDistance) const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + bool HasInspectInfo() const; +}; + +// ============================================================================ +// I_Damageable — Anything that takes damage or can be healed +// ============================================================================ + +UINTERFACE(MinimalAPI, BlueprintType) +class UDamageable : public UInterface +{ + GENERATED_BODY() +}; + +class FRAMEWORK_API IDamageable +{ + GENERATED_BODY() + +public: + /** Apply damage. Returns actual damage dealt after modifiers. */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat") + float TakeDamage(float DamageAmount, AActor* DamageCauser, FGameplayTag DamageType, FVector HitLocation, FVector HitDirection); + + /** Heal by amount. Returns actual health restored. */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat") + float Heal(float HealAmount, AActor* Healer); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat") + bool IsAlive() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat") + float GetCurrentHealth() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat") + float GetMaxHealth() const; + + /** Called when health reaches zero. */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat") + void OnDeath(AActor* Killer, FGameplayTag DeathCause); + + /** Returns multiplier for incoming damage (e.g. 1.5 = takes 50% more damage). */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Combat") + float GetDamageModifier(FGameplayTag DamageType) const; +}; + +// ============================================================================ +// I_Holdable — Physics objects the player can grab and manipulate +// ============================================================================ + +UINTERFACE(MinimalAPI, BlueprintType) +class UHoldable : public UInterface +{ + GENERATED_BODY() +}; + +class FRAMEWORK_API IHoldable +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void OnPickup(AActor* Holder); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void OnDrop(AActor* Dropper); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + FTransform GetHoldTransform() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + bool IsHeld() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void OnReleasedFromHold(); +}; + +// ============================================================================ +// I_Lockable — Objects that can be locked/unlocked with key items +// ============================================================================ + +UINTERFACE(MinimalAPI, BlueprintType) +class ULockable : public UInterface +{ + GENERATED_BODY() +}; + +class FRAMEWORK_API ILockable +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + bool TryLock(AActor* Locker); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + bool TryUnlock(AActor* Unlocker, FGameplayTag KeyTag); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + bool IsLocked() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + FGameplayTag GetRequiredKeyTag() const; + + /** Broadcast when lock state changes (implementor fires via delegate). */ + virtual void OnLockStateChanged(bool bNewLocked) {} +}; + +// ============================================================================ +// I_UsableItem — Items that can be used from inventory/quick-slots +// ============================================================================ + +UINTERFACE(MinimalAPI, BlueprintType) +class UUsableItem : public UInterface +{ + GENERATED_BODY() +}; + +class FRAMEWORK_API IUsableItem +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Inventory") + bool UseItem(AActor* User, AActor* Target); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Inventory") + bool CanUseItem(AActor* User, AActor* Target) const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Inventory") + float GetUseDuration() const; + + /** Called after UseItem completes (for animations, effects). */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Inventory") + void OnItemUsed(AActor* User); +}; + +// ============================================================================ +// I_Persistable — Actors that save/load their state to disk +// ============================================================================ + +UINTERFACE(MinimalAPI, BlueprintType) +class UPersistable : public UInterface +{ + GENERATED_BODY() +}; + +class FRAMEWORK_API IPersistable +{ + GENERATED_BODY() + +public: + /** Serialize state to a byte array for saving. */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Save") + TArray OnSave(); + + /** Restore state from a previously saved byte array. */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Save") + void OnLoad(const TArray& Data); + + /** Returns the unique GameplayTag identifier for this persistable actor. */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Save") + FGameplayTag GetSaveTag() const; + + /** Returns true if this actor has changed since last save. */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Save") + bool NeedsSave() const; +}; + +// ============================================================================ +// I_Toggleable — Objects with binary on/off states +// ============================================================================ + +UINTERFACE(MinimalAPI, BlueprintType) +class UToggleable : public UInterface +{ + GENERATED_BODY() +}; + +class FRAMEWORK_API IToggleable +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void Toggle(AActor* Toggler); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void SetState(bool bNewState, AActor* Setter); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + bool GetCurrentState() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + FText GetStateLabel() const; + + /** Broadcast when state changes. */ + virtual void OnStateChanged(bool bNewState, AActor* Changer) {} +}; + +// ============================================================================ +// I_Adjustable — Objects with continuous value range (dials, sliders) +// ============================================================================ + +UINTERFACE(MinimalAPI, BlueprintType) +class UAdjustable : public UInterface +{ + GENERATED_BODY() +}; + +class FRAMEWORK_API IAdjustable +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void Adjust(float Delta, AActor* Adjuster); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + void SetValue(float NewValue, AActor* Setter); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + float GetCurrentValue() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + float GetMinValue() const; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Interaction") + float GetMaxValue() const; + + /** Broadcast when value changes. */ + virtual void OnValueChanged(float NewValue, AActor* Changer) {} +}; diff --git a/Source/Framework/Public/Input/SS_EnhancedInputManager.h b/Source/Framework/Public/Input/SS_EnhancedInputManager.h new file mode 100644 index 0000000..c494453 --- /dev/null +++ b/Source/Framework/Public/Input/SS_EnhancedInputManager.h @@ -0,0 +1,232 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — SS_EnhancedInputManager (128) +// Sole authority for Push/Pop input context, key rebinding, and input mode changes. +// In C++, directly wraps UEnhancedInputLocalPlayerSubsystem with priority-based +// context stack management. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "GameplayTagContainer.h" +#include "InputAction.h" +#include "SS_EnhancedInputManager.generated.h" + +class UInputMappingContext; +class UInputAction; +class UEnhancedInputLocalPlayerSubsystem; + +/** + * Input context priority ladder. + * Higher priority contexts override lower priority for conflicting inputs. + */ +UENUM(BlueprintType) +enum class EInputContextPriority : uint8 +{ + Default = 0 UMETA(DisplayName = "Default (0)"), + Hiding = 5 UMETA(DisplayName = "Hiding (5)"), + Wristwatch = 10 UMETA(DisplayName = "Wristwatch UI (10)"), + Inspection = 20 UMETA(DisplayName = "Inspection (20)"), + UI = 100 UMETA(DisplayName = "UI (100)"), +}; + +/** + * Mapping context entry in the stack. + */ +USTRUCT(BlueprintType) +struct FRAMEWORK_API FInputContextEntry +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + TObjectPtr Context = nullptr; + + UPROPERTY(BlueprintReadOnly) + EInputContextPriority Priority = EInputContextPriority::Default; + + UPROPERTY(BlueprintReadOnly) + FGameplayTag ContextTag; // For identification: Framework.Input.Context.Default, etc. + + bool operator==(const FInputContextEntry& Other) const + { + return Context == Other.Context; + } +}; + +// Delegates +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnContextPushed, FGameplayTag, ContextTag, EInputContextPriority, Priority); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnContextPopped, FGameplayTag, ContextTag, EInputContextPriority, Priority); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInputModeChanged, bool, bUIMode, bool, bShowCursor); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnKeyRebound, FGameplayTag, ActionTag, FKey, NewKey); + +/** + * SS_EnhancedInputManager — Input Context Stack Authority. + * + * Manages all Enhanced Input Mapping Contexts with priority-based ordering. + * Systems call PushContext/PopContext instead of directly touching the + * Enhanced Input subsystem. Coordinates input mode (game/UI) with SS_UIManager. + * + * C++ gives us direct UEnhancedInputLocalPlayerSubsystem access — no + * "Get Enhanced Input Local Player Subsystem" node chains. + */ +UCLASS() +class FRAMEWORK_API USS_EnhancedInputManager : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + USS_EnhancedInputManager(); + + // ======================================================================== + // Lifecycle + // ======================================================================== + + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + // ======================================================================== + // Context Stack Management + // ======================================================================== + + /** + * Push an input mapping context onto the stack. + * Higher priority contexts override lower for conflicting inputs. + * Duplicate protection — if the context is already active, it's moved to top. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Input") + void PushContext(UInputMappingContext* Context, EInputContextPriority Priority, FGameplayTag ContextTag); + + /** + * Remove an input mapping context from the stack. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Input") + void PopContext(UInputMappingContext* Context); + + /** + * Pop a context by its GameplayTag identifier. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Input") + void PopContextByTag(FGameplayTag ContextTag); + + /** + * Clear ALL contexts from the stack (e.g., on level transition). + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Input") + void ClearAllContexts(); + + /** + * Returns whether a context is currently active on the stack. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input") + bool IsContextActive(FGameplayTag ContextTag) const; + + /** + * Returns the currently highest-priority active context. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input") + FGameplayTag GetTopContext() const; + + // ======================================================================== + // Input Mode Coordination + // ======================================================================== + + /** + * Switch between game input mode and UI input mode. + * Coordinates cursor visibility and input blocking with SS_UIManager. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Input") + void SetInputMode(bool bUIMode, bool bShowCursor = true, bool bLockMouseToViewport = false); + + /** Returns whether UI input mode is active. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input") + bool IsUIMode() const { return bCurrentUIMode; } + + // ======================================================================== + // Key Rebinding + // ======================================================================== + + /** + * Rebind a key for a specific input action. + * Persists via SS_SettingsSystem. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Input") + void RebindKey(UInputAction* Action, FKey NewKey, bool bSaveToDisk = true); + + /** + * Reset all key bindings to defaults. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Input") + void ResetAllBindings(); + + /** + * Get the current key bound to an input action. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input") + FKey GetBoundKey(UInputAction* Action) const; + + // ======================================================================== + // Query + // ======================================================================== + + /** + * Check if a specific input action is currently being pressed. + * Use this instead of raw Enhanced Input queries. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input") + bool IsActionPressed(UInputAction* Action) const; + + /** + * Get the current value of an input action (0.0 to 1.0 for digital, axis value for analog). + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Input") + float GetActionValue(UInputAction* Action) const; + + // ======================================================================== + // Configuration + // ======================================================================== + + /** Default input mapping contexts (loaded on initialize). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Input|Config") + TArray> DefaultContexts; + + // ======================================================================== + // Event Dispatchers + // ======================================================================== + + UPROPERTY(BlueprintAssignable, Category = "Framework|Input|Events") + FOnContextPushed OnContextPushed; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Input|Events") + FOnContextPopped OnContextPopped; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Input|Events") + FOnInputModeChanged OnInputModeChanged; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Input|Events") + FOnKeyRebound OnKeyRebound; + +protected: + // ======================================================================== + // Internal State + // ======================================================================== + + /** Ordered context stack (highest priority at the end/newest). */ + TArray ContextStack; + + /** Whether UI input mode is currently active. */ + bool bCurrentUIMode = false; + + /** Cached Enhanced Input subsystem. */ + UPROPERTY() + TObjectPtr EnhancedInputSubsystem; + + // ======================================================================== + // Internal Methods + // ======================================================================== + + /** Re-sorts the context stack by priority and re-applies to the subsystem. */ + void RebuildContextStack(); + + /** Gets the Enhanced Input subsystem, caching it if needed. */ + UEnhancedInputLocalPlayerSubsystem* GetEnhancedInputSubsystem() const; +}; diff --git a/Source/Framework/Public/Inventory/BPC_InventorySystem.h b/Source/Framework/Public/Inventory/BPC_InventorySystem.h new file mode 100644 index 0000000..9982e18 --- /dev/null +++ b/Source/Framework/Public/Inventory/BPC_InventorySystem.h @@ -0,0 +1,210 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — BPC_InventorySystem (31) +// Core inventory grid. Add/remove/sort/stack/weight management. +// In C++, TArray operations with lambdas are natively fast — no BP array node overhead. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "GameplayTagContainer.h" +#include "BPC_InventorySystem.generated.h" + +class UDA_ItemData; + +/** + * Single inventory slot entry. + */ +USTRUCT(BlueprintType) +struct FRAMEWORK_API FInventorySlot +{ + GENERATED_BODY() + + /** The item in this slot. nullptr = empty slot. */ + UPROPERTY(BlueprintReadOnly) + TObjectPtr Item = nullptr; + + /** How many of this item are stacked here. */ + UPROPERTY(BlueprintReadOnly) + int32 Quantity = 0; + + /** Grid position for UI layout. */ + UPROPERTY(BlueprintReadOnly) + int32 GridX = 0; + + /** Grid position for UI layout. */ + UPROPERTY(BlueprintReadOnly) + int32 GridY = 0; + + bool IsEmpty() const { return Item == nullptr || Quantity <= 0; } + + void Clear() + { + Item = nullptr; + Quantity = 0; + } + + bool operator==(const FInventorySlot& Other) const + { + return Item == Other.Item && Quantity == Other.Quantity && GridX == Other.GridX && GridY == Other.GridY; + } +}; + +// Delegates +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnInventoryChanged); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnItemAdded, UDA_ItemData*, Item, int32, Quantity); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnItemRemoved, UDA_ItemData*, Item, int32, Quantity); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnWeightChanged, float, CurrentWeight, float, MaxWeight); + +/** + * BPC_InventorySystem — Core Inventory Grid. + * + * Manages the player's carried items: add, remove, sort, stack, weight tracking. + * C++ TArray operations (FindByPredicate, Sort, Filter) are natively compiled — + * no BP interpretive array node overhead. + */ +UCLASS(ClassGroup = (Framework), meta = (BlueprintSpawnableComponent)) +class FRAMEWORK_API UBPC_InventorySystem : public UActorComponent +{ + GENERATED_BODY() + +public: + UBPC_InventorySystem(); + + // ======================================================================== + // Configuration + // ======================================================================== + + /** Grid width (columns). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Inventory|Config") + int32 GridWidth = 8; + + /** Grid height (rows). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Inventory|Config") + int32 GridHeight = 5; + + /** Maximum carry weight. Items exceeding this cannot be picked up. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Inventory|Config") + float MaxWeight = 50.0f; + + // ======================================================================== + // Inventory State + // ======================================================================== + + /** All inventory slots (GridWidth × GridHeight). */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|Inventory") + TArray Slots; + + /** Current total weight carried. */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|Inventory") + float CurrentWeight = 0.0f; + + /** Whether the inventory has been modified since last save. */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|Inventory") + bool bDirty = false; + + // ======================================================================== + // Core Operations + // ======================================================================== + + /** + * Add an item to the inventory. Stacks if possible, finds empty slot otherwise. + * Returns the quantity actually added (may be less than requested if full). + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Inventory") + int32 AddItem(UDA_ItemData* Item, int32 Quantity = 1); + + /** + * Remove an item from the inventory. + * Returns the quantity actually removed. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Inventory") + int32 RemoveItem(UDA_ItemData* Item, int32 Quantity = 1); + + /** + * Remove an item from a specific slot. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Inventory") + int32 RemoveItemFromSlot(int32 SlotIndex, int32 Quantity = 1); + + /** + * Check if an item can be added (enough space and weight capacity). + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory") + bool CanAddItem(UDA_ItemData* Item, int32 Quantity = 1) const; + + // ======================================================================== + // Query + // ======================================================================== + + /** Returns the total quantity of an item across all stacks. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory") + int32 GetItemCount(UDA_ItemData* Item) const; + + /** Returns whether the inventory contains at least this many of an item. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory") + bool HasItem(UDA_ItemData* Item, int32 Quantity = 1) const; + + /** Finds the first slot containing the given item. Returns -1 if not found. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory") + int32 FindItemSlot(UDA_ItemData* Item) const; + + /** Returns all unique items currently in the inventory. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory") + TArray GetAllItems() const; + + /** Returns the number of empty slots available. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory") + int32 GetEmptySlotCount() const; + + /** Returns the number of free weight units remaining. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Inventory") + float GetRemainingWeight() const; + + // ======================================================================== + // Organization + // ======================================================================== + + /** Sort inventory by ItemType, then by DisplayName. */ + UFUNCTION(BlueprintCallable, Category = "Framework|Inventory") + void SortInventory(); + + /** Auto-merge all partial stacks of the same item. */ + UFUNCTION(BlueprintCallable, Category = "Framework|Inventory") + void ConsolidateStacks(); + + // ======================================================================== + // Event Dispatchers + // ======================================================================== + + UPROPERTY(BlueprintAssignable, Category = "Framework|Inventory|Events") + FOnInventoryChanged OnInventoryChanged; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Inventory|Events") + FOnItemAdded OnItemAdded; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Inventory|Events") + FOnItemRemoved OnItemRemoved; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Inventory|Events") + FOnWeightChanged OnWeightChanged; + + // ======================================================================== + // Overrides + // ======================================================================== + + virtual void BeginPlay() override; + +protected: + /** Recalculates total weight from all slots. */ + void RecalculateWeight(); + + /** Finds an existing stack for an item (not at max stack limit). Returns -1 if none found. */ + int32 FindExistingStack(UDA_ItemData* Item) const; + + /** Finds the first empty slot. Returns -1 if inventory is full. */ + int32 FindEmptySlot() const; + + /** Marks inventory as modified and broadcasts change dispatchers. */ + void MarkDirty(); +}; diff --git a/Source/Framework/Public/Inventory/DA_ItemData.h b/Source/Framework/Public/Inventory/DA_ItemData.h new file mode 100644 index 0000000..f49f5ce --- /dev/null +++ b/Source/Framework/Public/Inventory/DA_ItemData.h @@ -0,0 +1,232 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — DA_ItemData (07) +// Base item Data Asset. Single source of truth for every item. +// C++ gives us UPROPERTY metadata (EditCondition, EditConditionHides, ClampMin/Max) +// that make the Data Asset editor usable for designers — impossible in Blueprint. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "GameplayTagContainer.h" +#include "Engine/Texture2D.h" +#include "Engine/StaticMesh.h" +#include "DA_ItemData.generated.h" + +/** + * Item type classification. + */ +UENUM(BlueprintType) +enum class EItemType : uint8 +{ + Weapon UMETA(DisplayName = "Weapon"), + Ammo UMETA(DisplayName = "Ammo"), + Consumable UMETA(DisplayName = "Consumable"), + KeyItem UMETA(DisplayName = "Key Item"), + Document UMETA(DisplayName = "Document"), + Collectible UMETA(DisplayName = "Collectible"), + Tool UMETA(DisplayName = "Tool"), + Resource UMETA(DisplayName = "Resource"), + Misc UMETA(DisplayName = "Misc"), +}; + +/** + * Equipment slot type. + */ +UENUM(BlueprintType) +enum class EEquipmentSlot : uint8 +{ + None UMETA(DisplayName = "None"), + PrimaryWeapon UMETA(DisplayName = "Primary Weapon"), + SecondaryWeapon UMETA(DisplayName = "Secondary Weapon"), + Melee UMETA(DisplayName = "Melee"), + Tool UMETA(DisplayName = "Tool"), + Armor UMETA(DisplayName = "Armor"), + Accessory UMETA(DisplayName = "Accessory"), +}; + +/** + * Equipment-specific data (shown when ItemType is Weapon or Tool). + */ +USTRUCT(BlueprintType) +struct FRAMEWORK_API FItemEquipmentData +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + EEquipmentSlot Slot = EEquipmentSlot::None; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float Damage = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float FireRate = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float Range = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + int32 MagazineSize = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float ReloadTime = 0.0f; +}; + +/** + * Consumable-specific data (shown when ItemType is Consumable). + */ +USTRUCT(BlueprintType) +struct FRAMEWORK_API FItemConsumableData +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", ClampMax = "100")) + float HealthRestore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", ClampMax = "100")) + float StressReduce = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float UseDuration = 1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + bool bConsumedOnUse = true; +}; + +/** + * Inspect-specific data (shown when bHasInspectMode is true). + */ +USTRUCT(BlueprintType) +struct FRAMEWORK_API FItemInspectData +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + FVector AnchorPoint = FVector::ZeroVector; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + FRotator DefaultRotation = FRotator::ZeroRotator; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float ZoomDistance = 50.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + bool bCanRotate = true; +}; + +/** + * DA_ItemData — Base Item Data Asset. + * + * Every item in the game is one DA_ItemData asset. No item data lives in + * Blueprint logic. C++ gives us EditCondition metadata so the editor only + * shows relevant sub-structs based on ItemType — a massive UX win for designers. + */ +UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "Item Data")) +class FRAMEWORK_API UDA_ItemData : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + UDA_ItemData(); + + // ======================================================================== + // Core Properties (Every Item Has These) + // ======================================================================== + + /** Unique GameplayTag identifier. Must be registered in DA_GameTagRegistry. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core") + FGameplayTag ItemTag; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core") + FText DisplayName; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core", meta = (MultiLine = true)) + FText Description; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core") + TSoftObjectPtr Icon; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core") + TSoftObjectPtr WorldMesh; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core", meta = (ClampMin = "0", ClampMax = "1000")) + float Weight = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core", meta = (ClampMin = "1", ClampMax = "999")) + int32 StackLimit = 1; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core") + EItemType ItemType = EItemType::Misc; + + // ======================================================================== + // Conditional Sub-Data (Shown Based on ItemType via EditCondition) + // ======================================================================== + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Equipment", + meta = (EditCondition = "ItemType == EItemType::Weapon || ItemType == EItemType::Tool", EditConditionHides)) + FItemEquipmentData EquipmentData; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable", + meta = (EditCondition = "ItemType == EItemType::Consumable", EditConditionHides)) + FItemConsumableData ConsumableData; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Inspect", + meta = (EditCondition = "bHasInspectMode", EditConditionHides)) + FItemInspectData InspectData; + + // ======================================================================== + // Flags + // ======================================================================== + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Flags") + bool bIsKeyItem = false; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Flags") + bool bCanBeDropped = true; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Flags") + bool bHasInspectMode = false; + + // ======================================================================== + // Combination / Crafting + // ======================================================================== + + /** Tags of items this can combine with. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Combination") + TArray CombinesWith; + + /** The resulting item tag when combined with CombinesWith item. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Combination") + FGameplayTag CombineResult; + + // ======================================================================== + // Extensibility + // ======================================================================== + + /** Custom per-project properties — no need to modify the base class. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Custom") + TMap CustomProperties; + + // ======================================================================== + // Validation (Editor-Only) + // ======================================================================== + +#if WITH_EDITOR + /** + * Validates the item data asset for common errors. + * Called by editor utilities or pre-save validation. + */ + UFUNCTION(BlueprintCallable, Category = "Item|Validation") + bool ValidateItemData(FString& OutErrors) const; +#endif + + // ======================================================================== + // Overrides + // ======================================================================== + + virtual void PostLoad() override; + +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif +}; diff --git a/Source/Framework/Public/Player/BPC_StateManager.h b/Source/Framework/Public/Player/BPC_StateManager.h new file mode 100644 index 0000000..042fd92 --- /dev/null +++ b/Source/Framework/Public/Player/BPC_StateManager.h @@ -0,0 +1,246 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — BPC_StateManager (130) +// Central State Authority. Single source of truth for "what can the player do right now?" +// Manages exclusive action states, upper-body overlay states, action gating, +// vital signs (heart rate), and the force-stack pattern for nested overrides. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "GameplayTagContainer.h" +#include "BPC_StateManager.generated.h" + +// Forward declarations +class UDA_StateGatingTable; +class UBPC_HealthSystem; +class UBPC_StressSystem; +class UBPC_StaminaSystem; +class UBPC_MovementStateSystem; + +// ============================================================================ +// Delegates +// ============================================================================ + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnActionStateChanged, FGameplayTag, NewState, FGameplayTag, OldState); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnOverlayStateChanged, FGameplayTag, NewOverlay, FGameplayTag, OldOverlay); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnVitalSignChanged, FGameplayTag, VitalTag, float, NewValue); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnForceStackPushed, FGameplayTag, ForceState); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnForceStackPopped, FGameplayTag, RestoredState); + +/** + * Result codes for state change requests. + * Mirrors the Blueprint E_ActionRequestResult enum. + */ +UENUM(BlueprintType) +enum class EActionRequestResult : uint8 +{ + Granted UMETA(DisplayName = "Granted"), + Denied UMETA(DisplayName = "Denied — Gated"), + BlockedByForce UMETA(DisplayName = "Blocked — Force Stack Override"), + AlreadyActive UMETA(DisplayName = "Already Active"), + InvalidState UMETA(DisplayName = "Invalid State Tag"), + RequesterNotFound UMETA(DisplayName = "Requester Not Found"), + CooldownActive UMETA(DisplayName = "Cooldown Active"), + VitalThreshold UMETA(DisplayName = "Vital Threshold Not Met"), +}; + +/** + * Heart rate tier for vital sign tracking. + */ +UENUM(BlueprintType) +enum class EHeartRateTier : uint8 +{ + Resting UMETA(DisplayName = "Resting (60-80 BPM)"), + Elevated UMETA(DisplayName = "Elevated (80-100 BPM)"), + Stressed UMETA(DisplayName = "Stressed (100-130 BPM)"), + Panic UMETA(DisplayName = "Panic (130-160 BPM)"), + Critical UMETA(DisplayName = "Critical (160+ BPM)"), +}; + +/** + * BPC_StateManager — Central State Authority. + * + * Every system queries IsActionPermitted(Tag) instead of checking other systems + * directly. Gating rules are defined in DA_StateGatingTable (37 rules). + * In C++, the Chooser Table iteration is native-speed — no BP interpretive overhead. + */ +UCLASS(ClassGroup = (Framework), meta = (BlueprintSpawnableComponent)) +class FRAMEWORK_API UBPC_StateManager : public UActorComponent +{ + GENERATED_BODY() + +public: + UBPC_StateManager(); + + // ======================================================================== + // Configuration + // ======================================================================== + + /** The gating rules Data Asset. Contains all 37 action rules. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Config") + TObjectPtr GatingTable; + + /** Default action state on BeginPlay. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Config") + FGameplayTag DefaultActionState; + + /** Default overlay state on BeginPlay. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Config") + FGameplayTag DefaultOverlayState; + + // ======================================================================== + // Core Query — Hot Path + // ======================================================================== + + /** + * Central query: "Can the player perform this action right now?" + * Called by EVERY gameplay system per-frame. C++ makes this fast. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|State") + bool IsActionPermitted(FGameplayTag ActionTag) const; + + /** + * Request a state change. Returns the result code. + * Server-authoritative. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|State") + EActionRequestResult RequestStateChange(FGameplayTag NewState, AActor* Requester); + + // ======================================================================== + // Current State (Read-Only) + // ======================================================================== + + /** Currently active exclusive action state (only one at a time). */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|State") + FGameplayTag CurrentActionState; + + /** Currently active upper-body overlay state. */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|State") + FGameplayTag CurrentOverlayState; + + // ======================================================================== + // Force Stack Pattern (Death, Cutscenes, Void Space) + // ======================================================================== + + /** + * Pushes a forced state onto the stack. Overrides all gating. + * Example: death overrides everything. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|State") + void ForceStateChange(FGameplayTag ForceState, FString Reason); + + /** + * Pops the top forced state and restores the previous state. + * Example: respawn restores the pre-death state. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|State") + void RestorePreviousState(); + + /** Returns the number of states currently on the force stack. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|State") + int32 GetForceStackDepth() const { return ForceStack.Num(); } + + // ======================================================================== + // Vital Signs — Heart Rate + // ======================================================================== + + /** Current heart rate in BPM (smoothed). */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|Vitals") + float HeartRateBPM = 70.0f; + + /** Current heart rate tier. */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|Vitals") + EHeartRateTier HeartRateTier = EHeartRateTier::Resting; + + /** Target heart rate (set by stress, stamina, combat). Interpolated toward each tick. */ + UPROPERTY(BlueprintReadOnly, Category = "Framework|Vitals") + float TargetHeartRate = 70.0f; + + /** Smoothing speed for heart rate interpolation. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Vitals") + float HeartRateSmoothSpeed = 2.0f; + + // ======================================================================== + // Event Dispatchers + // ======================================================================== + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnActionStateChanged OnActionStateChanged; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnOverlayStateChanged OnOverlayStateChanged; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnVitalSignChanged OnVitalSignChanged; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnForceStackPushed OnForceStackPushed; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnForceStackPopped OnForceStackPopped; + + // ======================================================================== + // Overrides + // ======================================================================== + + virtual void BeginPlay() override; + virtual void TickComponent(float DeltaTime, ELevelTick TickType, + FActorComponentTickFunction* ThisTickFunction) override; + +protected: + // ======================================================================== + // Internal State + // ======================================================================== + + /** Force stack — array of (State, Reason) pairs. Most recent is active. */ + struct FForceStackEntry + { + FGameplayTag State; + FString Reason; + }; + + TArray ForceStack; + + /** Previous action state before force override (for restore). */ + FGameplayTag PreForceActionState; + + /** Previous overlay state before force override (for restore). */ + FGameplayTag PreForceOverlayState; + + // ======================================================================== + // Gating Logic + // ======================================================================== + + /** Check gating rules for a tag against current state. */ + bool EvaluateGatingRules(FGameplayTag ActionTag) const; + + /** Check if any force stack entry blocks this action. */ + bool IsBlockedByForceStack(FGameplayTag ActionTag) const; + + // ======================================================================== + // Vital Sign Calculation + // ======================================================================== + + /** Recalculates target heart rate based on stress tier + stamina exhaustion + combat. */ + void RecalculateTargetHeartRate(); + + /** Determines heart rate tier from current BPM. */ + static EHeartRateTier GetHeartRateTier(float BPM); + + // ======================================================================== + // Binding References (cached in BeginPlay) + // ======================================================================== + + UPROPERTY() + TObjectPtr CachedHealthSystem; + + UPROPERTY() + TObjectPtr CachedStressSystem; + + UPROPERTY() + TObjectPtr CachedStaminaSystem; + + UPROPERTY() + TObjectPtr CachedMovementSystem; +}; diff --git a/Source/Framework/Public/Save/SS_SaveManager.h b/Source/Framework/Public/Save/SS_SaveManager.h new file mode 100644 index 0000000..41b21da --- /dev/null +++ b/Source/Framework/Public/Save/SS_SaveManager.h @@ -0,0 +1,193 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — SS_SaveManager (35) +// Save/Load subsystem. Slot management, serialization, manifest tracking. +// In C++, uses FArchive for direct binary serialization — far more powerful +// than Blueprint "Save Game" / "Load Game" nodes. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "GameplayTagContainer.h" +#include "SS_SaveManager.generated.h" + +/** + * Save slot metadata returned by GetSlotManifest(). + */ +USTRUCT(BlueprintType) +struct FRAMEWORK_API FSaveSlotInfo +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + int32 SlotIndex = -1; + + UPROPERTY(BlueprintReadOnly) + FString SlotName; + + UPROPERTY(BlueprintReadOnly) + FString ChapterName; + + UPROPERTY(BlueprintReadOnly) + float PlayTimeHours = 0.0f; + + UPROPERTY(BlueprintReadOnly) + FDateTime Timestamp; + + UPROPERTY(BlueprintReadOnly) + FGameplayTag LastCheckpoint; + + UPROPERTY(BlueprintReadOnly) + bool bIsEmpty = true; +}; + +// Delegates +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSaveComplete, int32, SlotIndex, bool, bSuccess); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnLoadComplete, int32, SlotIndex, bool, bSuccess); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSaveManifestUpdated, const TArray&, Slots); + +/** + * SS_SaveManager — Save/Load Subsystem. + * + * Manages all save slots, serialization, and manifest tracking. + * C++ gives us direct FArchive-based serialization, proper error handling, + * and async save operations — impossible to match in Blueprint. + */ +UCLASS() +class FRAMEWORK_API USS_SaveManager : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + USS_SaveManager(); + + // ======================================================================== + // Lifecycle + // ======================================================================== + + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + // ======================================================================== + // Configuration + // ======================================================================== + + /** Maximum number of save slots. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Save") + int32 MaxSlots = 10; + + /** Save game file prefix for slot naming. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Save") + FString SavePrefix = TEXT("FrameworkSave_"); + + // ======================================================================== + // Slot Manifest + // ======================================================================== + + /** + * Returns metadata for all save slots. + * Fast — reads header only, not full save data. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Save") + TArray GetSlotManifest() const; + + /** + * Checks if a slot has save data. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Save") + bool DoesSlotExist(int32 SlotIndex) const; + + // ======================================================================== + // Save / Load Operations + // ======================================================================== + + /** + * Save game to a slot. Returns true if successful. + * Server-authoritative in multiplayer. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Save") + bool SaveGame(int32 SlotIndex, const FString& Description); + + /** + * Load game from a slot. Returns true if successful. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Save") + bool LoadGame(int32 SlotIndex); + + /** + * Delete a save slot. Irreversible! + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Save") + bool DeleteSlot(int32 SlotIndex); + + /** + * Quick-save to the current active slot. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Save") + bool QuickSave(); + + /** + * Quick-load from the current active slot. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Save") + bool QuickLoad(); + + // ======================================================================== + // Checkpoint Management + // ======================================================================== + + /** Loads the most recent checkpoint from a slot. */ + UFUNCTION(BlueprintCallable, Category = "Framework|Save") + bool LoadCheckpoint(int32 SlotIndex); + + /** + * Creates a checkpoint within the current slot. + * Checkpoints are incremental saves within a single slot. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Save") + bool CreateCheckpoint(FGameplayTag CheckpointTag); + + // ======================================================================== + // Event Dispatchers + // ======================================================================== + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnSaveComplete OnSaveComplete; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnLoadComplete OnLoadComplete; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnSaveManifestUpdated OnSaveManifestUpdated; + + // ======================================================================== + // Utilities + // ======================================================================== + + /** Returns the total disk space used by all saves (in bytes). */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Save") + int64 GetTotalSaveSize() const; + + /** Backs up all save slots to a Backup/ subdirectory. */ + UFUNCTION(BlueprintCallable, Category = "Framework|Save") + bool BackupAllSaves(const FString& BackupLabel); + +protected: + /** Builds the save slot name from prefix + index. */ + FString GetSlotName(int32 SlotIndex) const; + + /** Reads only the header/metadata from a save file. */ + FSaveSlotInfo ReadSlotHeader(int32 SlotIndex) const; + + /** Internal save implementation using FArchive serialization. */ + bool SaveToFile(int32 SlotIndex, const TArray& Data, const FSaveSlotInfo& Meta); + + /** Internal load implementation. */ + bool LoadFromFile(int32 SlotIndex, TArray& OutData, FSaveSlotInfo& OutMeta); + + /** Path to the save directory. */ + FString GetSaveDirectory() const; + + /** Currently active save slot (from GI_GameFramework). */ + int32 GetActiveSlot() const; +}; diff --git a/Source/Framework/Public/Weapons/BPC_DamageReceptionSystem.h b/Source/Framework/Public/Weapons/BPC_DamageReceptionSystem.h new file mode 100644 index 0000000..d276e0d --- /dev/null +++ b/Source/Framework/Public/Weapons/BPC_DamageReceptionSystem.h @@ -0,0 +1,150 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// UE5 Modular Game Framework — BPC_DamageReceptionSystem (72) +// Damage reception, resistance calculation, and damage application. +// Called potentially dozens of times per combat frame — C++ performance critical. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "GameplayTagContainer.h" +#include "BPC_DamageReceptionSystem.generated.h" + +// Forward declarations +class UDA_EquipmentConfig; +class UBPC_HealthSystem; +class UBPC_ShieldDefenseSystem; +class UBPC_HitReactionSystem; + +// Delegates +DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FOnDamageReceived, float, RawDamage, float, FinalDamage, + AActor*, DamageCauser, FGameplayTag, DamageType, FVector, HitLocation); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnDamageResisted, float, DamageResisted, + FGameplayTag, ResistanceType, FString, Reason); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnStaggered, AActor*, StaggerCauser, float, StaggerForce); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnKnockedDown, AActor*, KnockdownCauser, float, KnockdownForce); + +/** + * Damage modifier for a specific damage type. + */ +USTRUCT(BlueprintType) +struct FRAMEWORK_API FDamageModifier +{ + GENERATED_BODY() + + /** The damage type this modifier applies to. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + FGameplayTag DamageType; + + /** Multiplier applied to incoming damage of this type. 0.5 = half damage, 2.0 = double. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float Multiplier = 1.0f; + + /** If true, this is a flat reduction (subtract after multiplier). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + bool bFlatReduction = false; + + /** Flat damage reduction amount (only used if bFlatReduction is true). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float FlatReduction = 0.0f; +}; + +/** + * BPC_DamageReceptionSystem — Damage Reception & Resistance. + * + * Processes incoming damage: calculates resistance, applies armor/shield modifiers, + * triggers hit reactions (stagger, knockdown), and routes final damage to the + * health system. In C++, the damage pipeline is native-speed vectorized math. + */ +UCLASS(ClassGroup = (Framework), meta = (BlueprintSpawnableComponent)) +class FRAMEWORK_API UBPC_DamageReceptionSystem : public UActorComponent +{ + GENERATED_BODY() + +public: + UBPC_DamageReceptionSystem(); + + // ======================================================================== + // Configuration + // ======================================================================== + + /** Equipment config for armor/damage modifiers. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Framework|Config") + TObjectPtr EquipmentConfig; + + /** Base damage resistance (0.0 = no resistance, 1.0 = immune). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Combat") + float BaseResistance = 0.0f; + + /** Damage multipliers per damage type. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Combat") + TArray DamageModifiers; + + // ======================================================================== + // Damage Calculation — Hot Path + // ======================================================================== + + /** + * Calculate and apply damage. + * Full pipeline: raw damage → calculate resistance → apply armor → apply shield → apply health. + * Returns actual damage dealt. + */ + UFUNCTION(BlueprintCallable, Category = "Framework|Combat") + float ApplyDamage(float RawDamage, AActor* DamageCauser, FGameplayTag DamageType, + FVector HitLocation, FVector HitDirection); + + /** + * Calculate effective resistance for a damage type. + * Used by UI/preview systems to show expected damage. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Combat") + float CalculateResistance(FGameplayTag DamageType) const; + + /** + * Get the damage modifier for a specific damage type. + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Framework|Combat") + float GetDamageMultiplier(FGameplayTag DamageType) const; + + // ======================================================================== + // Hit Reaction + // ======================================================================== + + /** Damage threshold to trigger a stagger reaction. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Combat|Reactions") + float StaggerThreshold = 20.0f; + + /** Damage threshold to trigger a knockdown. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Combat|Reactions") + float KnockdownThreshold = 50.0f; + + // ======================================================================== + // Event Dispatchers + // ======================================================================== + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnDamageReceived OnDamageReceived; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnDamageResisted OnDamageResisted; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnStaggered OnStaggered; + + UPROPERTY(BlueprintAssignable, Category = "Framework|Events") + FOnKnockedDown OnKnockedDown; + +protected: + /** Triggers hit reaction based on final damage amount. */ + void EvaluateHitReaction(float FinalDamage, AActor* DamageCauser, FVector HitDirection); + + /** Cached references to sibling components. */ + UPROPERTY() + TObjectPtr CachedHealthSystem; + + UPROPERTY() + TObjectPtr CachedShieldSystem; + + UPROPERTY() + TObjectPtr CachedHitReactionSystem; +};