Add core gameplay systems and data assets for player mechanics
- Implemented DA_EquipmentConfig for managing equipment resistances, durability, and weight. - Created DA_ItemData to serve as a base item data asset with various item types and properties. - Introduced BPC_HealthSystem for managing player health and death events. - Added BPC_MovementStateSystem to handle player movement modes with event delegation. - Developed BPC_StaminaSystem to track player stamina and exhaustion states. - Established BPC_StateManager as a central authority for managing player action states and gating. - Created BPC_StressSystem to monitor and respond to player stress levels. - Implemented PC_CoreController and PS_CorePlayerState for player controller and state management. - Developed SS_SaveManager for save/load functionality with slot management and serialization. - Introduced DA_StateGatingTable for defining action gating rules based on gameplay tags. - Added BPC_DamageReceptionSystem to process incoming damage and apply resistance calculations. - Implemented BPC_HitReactionSystem for managing hit reactions based on damage received. - Created BPC_ShieldDefenseSystem to manage shield health and blocking mechanics. - Added PG_FrameworkEditor.Target.cs for editor build configuration.
This commit is contained in:
41
Source/PG_Framework/Framework.Build.cs
Normal file
41
Source/PG_Framework/Framework.Build.cs
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
3
Source/PG_Framework/Framework.cpp
Normal file
3
Source/PG_Framework/Framework.cpp
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_MODULE(FDefaultModuleImpl, Framework);
|
||||
44
Source/PG_Framework/PG_Framework.Build.cs
Normal file
44
Source/PG_Framework/PG_Framework.Build.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// // Copyright Ngonart OU. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class PG_Framework : ModuleRules
|
||||
{
|
||||
public PG_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 are using Slate UI
|
||||
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||
|
||||
// Uncomment if you are using online features
|
||||
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
|
||||
|
||||
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
|
||||
}
|
||||
}
|
||||
6
Source/PG_Framework/PG_Framework.cpp
Normal file
6
Source/PG_Framework/PG_Framework.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
// // Copyright Ngonart OU. All Rights Reserved.
|
||||
|
||||
#include "PG_Framework.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, PG_Framework, "PG_Framework" );
|
||||
6
Source/PG_Framework/PG_Framework.h
Normal file
6
Source/PG_Framework/PG_Framework.h
Normal file
@@ -0,0 +1,6 @@
|
||||
// // Copyright Ngonart OU. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
156
Source/PG_Framework/Private/Core/DA_GameTagRegistry.cpp
Normal file
156
Source/PG_Framework/Private/Core/DA_GameTagRegistry.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
// 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"
|
||||
|
||||
|
||||
UDA_GameTagRegistry::UDA_GameTagRegistry()
|
||||
{
|
||||
TagNamespace = FText::FromString(TEXT("Framework tag namespace documentation"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query Functions
|
||||
// ============================================================================
|
||||
|
||||
TArray<FGameplayTag> UDA_GameTagRegistry::GetAllRegisteredTags() const
|
||||
{
|
||||
TArray<FGameplayTag> 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"));
|
||||
}
|
||||
|
||||
return FText::FromName(Tag.GetTagName());
|
||||
}
|
||||
|
||||
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<FGameplayTagNode> TagNode = TagManager.FindTagNode(TagName);
|
||||
|
||||
FGameplayTag OutTag = TagManager.RequestGameplayTag(TagName, false);
|
||||
if (OutTag.IsValid())
|
||||
{
|
||||
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<FGameplayTag> 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<FGameplayTag> 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<FGameplayTag> 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<FGameplayTag> AllTags = GetAllRegisteredTags();
|
||||
UE_LOG(LogTemp, Log, TEXT("DA_GameTagRegistry: TagDataTables updated — %d tags now registered"), AllTags.Num());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
299
Source/PG_Framework/Private/Core/FL_GameUtilities.cpp
Normal file
299
Source/PG_Framework/Private/Core/FL_GameUtilities.cpp
Normal file
@@ -0,0 +1,299 @@
|
||||
// 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"
|
||||
#include "GameplayTagAssetInterface.h"
|
||||
|
||||
// ============================================================================
|
||||
// Subsystem Access
|
||||
// ============================================================================
|
||||
|
||||
UGI_GameFramework* UFL_GameUtilities::GetGameFramework(const UObject* WorldContextObject)
|
||||
{
|
||||
if (!WorldContextObject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UWorld* World = WorldContextObject->GetWorld();
|
||||
if (!World)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return Cast<UGI_GameFramework>(World->GetGameInstance());
|
||||
}
|
||||
|
||||
UGameInstanceSubsystem* UFL_GameUtilities::GetSubsystemByClass(const UObject* WorldContextObject,
|
||||
TSubclassOf<UGameInstanceSubsystem> 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<UInterface> InterfaceClass)
|
||||
{
|
||||
if (!Actor || !InterfaceClass)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TArray<UActorComponent*> 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<AActor*> 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<IGameplayTagAssetInterface>(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 = TagManager.RequestGameplayTag(FName(*TagString), false);
|
||||
if (OutTag.IsValid())
|
||||
{
|
||||
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
|
||||
}
|
||||
239
Source/PG_Framework/Private/Core/GI_GameFramework.cpp
Normal file
239
Source/PG_Framework/Private/Core/GI_GameFramework.cpp
Normal file
@@ -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<int32>(OldPhase), static_cast<int32>(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<UGameInstanceSubsystem>* 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<int32>(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<FGameplayTag> 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<T>() directly.
|
||||
UE_LOG(LogFramework, Verbose, TEXT("GI_GameFramework::RegisterServices — Service registry initialized"));
|
||||
}
|
||||
161
Source/PG_Framework/Private/Core/GM_CoreGameMode.cpp
Normal file
161
Source/PG_Framework/Private/Core/GM_CoreGameMode.cpp
Normal file
@@ -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(LogFrameworkGameMode, 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<UGI_GameFramework>(GetGameInstance());
|
||||
if (!CachedFramework)
|
||||
{
|
||||
UE_LOG(LogFrameworkGameMode, Warning, TEXT("GM_CoreGameMode::InitGame — GI_GameFramework not found as GameInstance"));
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkGameMode, 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(LogFrameworkGameMode, Log, TEXT("GM_CoreGameMode::BeginPlay"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Chapter Management
|
||||
// ============================================================================
|
||||
|
||||
void AGM_CoreGameMode::TransitionToChapter(FGameplayTag ChapterTag)
|
||||
{
|
||||
if (!ChapterTag.IsValid())
|
||||
{
|
||||
UE_LOG(LogFrameworkGameMode, Warning, TEXT("GM_CoreGameMode::TransitionToChapter — Invalid chapter tag"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent transitions during loading.
|
||||
if (CachedFramework && CachedFramework->CurrentGamePhase == EGamePhase::Loading)
|
||||
{
|
||||
UE_LOG(LogFrameworkGameMode, Warning, TEXT("GM_CoreGameMode::TransitionToChapter — Already loading, ignoring transition to '%s'"),
|
||||
*ChapterTag.GetTagName().ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkGameMode, 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<AGS_CoreGameState>();
|
||||
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(LogFrameworkGameMode, Log, TEXT("GM_CoreGameMode::OnChapterLevelLoaded — '%s'"), *ChapterTag.GetTagName().ToString());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Death Handling
|
||||
// ============================================================================
|
||||
|
||||
void AGM_CoreGameMode::HandlePlayerDead(AController* DeadController)
|
||||
{
|
||||
if (!DeadController)
|
||||
{
|
||||
UE_LOG(LogFrameworkGameMode, Warning, TEXT("GM_CoreGameMode::HandlePlayerDead — Invalid controller"));
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkGameMode, 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(LogFrameworkGameMode, Log, TEXT("GM_CoreGameMode::HandlePlayerDead — Routing to respawn..."));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ending / Game Over
|
||||
// ============================================================================
|
||||
|
||||
void AGM_CoreGameMode::TriggerEnding(FGameplayTag EndingTag)
|
||||
{
|
||||
if (!EndingTag.IsValid())
|
||||
{
|
||||
UE_LOG(LogFrameworkGameMode, Warning, TEXT("GM_CoreGameMode::TriggerEnding — Invalid ending tag"));
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkGameMode, Log, TEXT("GM_CoreGameMode::TriggerEnding — '%s'"), *EndingTag.GetTagName().ToString());
|
||||
|
||||
OnGameOverTriggered.Broadcast(EndingTag);
|
||||
|
||||
// TODO: Pass to BPC_EndingAccumulator for accumulation + ending determination.
|
||||
}
|
||||
193
Source/PG_Framework/Private/Core/GS_CoreGameState.cpp
Normal file
193
Source/PG_Framework/Private/Core/GS_CoreGameState.cpp
Normal file
@@ -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(LogFrameworkGameState, Log, All);
|
||||
|
||||
AGS_CoreGameState::AGS_CoreGameState()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
PrimaryActorTick.TickInterval = 0.1f; // 10Hz tick for time accumulation.
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Replication
|
||||
// ============================================================================
|
||||
|
||||
void AGS_CoreGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& 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<UGI_GameFramework>(GetGameInstance());
|
||||
|
||||
if (CachedFramework)
|
||||
{
|
||||
UE_LOG(LogFrameworkGameState, Log, TEXT("GS_CoreGameState::BeginPlay — Bound to GI_GameFramework"));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogFrameworkGameState, 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(LogFrameworkGameState, 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(LogFrameworkGameState, 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(LogFrameworkGameState, 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(LogFrameworkGameState, Log, TEXT("GS_CoreGameState::AddObjective — '%s'"), *ObjectiveTag.GetTagName().ToString());
|
||||
|
||||
OnObjectiveTagsChanged.Broadcast();
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::RemoveObjective(FGameplayTag ObjectiveTag)
|
||||
{
|
||||
if (ActiveObjectiveTags.Remove(ObjectiveTag) > 0)
|
||||
{
|
||||
UE_LOG(LogFrameworkGameState, Log, TEXT("GS_CoreGameState::RemoveObjective — '%s'"), *ObjectiveTag.GetTagName().ToString());
|
||||
OnObjectiveTagsChanged.Broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
void AGS_CoreGameState::ClearAllObjectives()
|
||||
{
|
||||
if (ActiveObjectiveTags.Num() > 0)
|
||||
{
|
||||
ActiveObjectiveTags.Empty();
|
||||
UE_LOG(LogFrameworkGameState, 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();
|
||||
}
|
||||
387
Source/PG_Framework/Private/Input/SS_EnhancedInputManager.cpp
Normal file
387
Source/PG_Framework/Private/Input/SS_EnhancedInputManager.cpp
Normal file
@@ -0,0 +1,387 @@
|
||||
// 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(LogFrameworkInput, Log, All);
|
||||
|
||||
USS_EnhancedInputManager::USS_EnhancedInputManager()
|
||||
{
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lifecycle
|
||||
// ============================================================================
|
||||
|
||||
void USS_EnhancedInputManager::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
UE_LOG(LogFrameworkInput, 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(LogFrameworkInput, 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(LogFrameworkInput, 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(LogFrameworkInput, Log, TEXT("PushContext — '%s' (Priority: %d)"),
|
||||
*ContextTag.GetTagName().ToString(), static_cast<int32>(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(LogFrameworkInput, Log, TEXT("PopContext — '%s'"), *Popped.GetTagName().ToString());
|
||||
|
||||
RebuildContextStack();
|
||||
OnContextPopped.Broadcast(Popped, Prio);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkInput, 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(LogFrameworkInput, Log, TEXT("PopContextByTag — '%s'"), *ContextTag.GetTagName().ToString());
|
||||
PopContext(ContextStack[i].Context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkInput, 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(LogFrameworkInput, 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(LogFrameworkInput, 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(LogFrameworkInput, 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(LogFrameworkInput, 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(LogFrameworkInput, 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)
|
||||
{
|
||||
// UE 5.7+: ResetPlayerMappedKeys was removed. Use UEnhancedInputUserSettings or iterate context entries.
|
||||
// Subsystem->RequestRebuildPlayerMappedKeys();
|
||||
UE_LOG(LogFrameworkInput, 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<UInputAction*, bool>.
|
||||
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<float>();
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal
|
||||
// ============================================================================
|
||||
|
||||
void USS_EnhancedInputManager::RebuildContextStack()
|
||||
{
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = GetEnhancedInputSubsystem();
|
||||
if (!Subsystem)
|
||||
{
|
||||
UE_LOG(LogFrameworkInput, 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<int32>(A.Priority) < static_cast<int32>(B.Priority);
|
||||
});
|
||||
|
||||
// UE 5.7+: RemoveAllMappingContexts was removed. Remove individually.
|
||||
for (const FInputContextEntry& Existing : ContextStack)
|
||||
{
|
||||
if (Existing.Context)
|
||||
{
|
||||
Subsystem->RemoveMappingContext(Existing.Context);
|
||||
}
|
||||
}
|
||||
|
||||
for (const FInputContextEntry& Entry : ContextStack)
|
||||
{
|
||||
if (Entry.Context)
|
||||
{
|
||||
Subsystem->AddMappingContext(Entry.Context, static_cast<int32>(Entry.Priority));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogFrameworkInput, 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<UEnhancedInputLocalPlayerSubsystem>();
|
||||
}
|
||||
425
Source/PG_Framework/Private/Inventory/BPC_InventorySystem.cpp
Normal file
425
Source/PG_Framework/Private/Inventory/BPC_InventorySystem.cpp
Normal file
@@ -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<UDA_ItemData*> UBPC_InventorySystem::GetAllItems() const
|
||||
{
|
||||
TSet<UDA_ItemData*> 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<FInventorySlot> FilledSlots;
|
||||
TArray<FInventorySlot> 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<uint8>(A.Item->ItemType) < static_cast<uint8>(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<UDA_ItemData*> Items = GetAllItems();
|
||||
|
||||
for (UDA_ItemData* Item : Items)
|
||||
{
|
||||
if (!Item || Item->StackLimit <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Gather all slots of this item.
|
||||
TArray<int32> 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();
|
||||
}
|
||||
13
Source/PG_Framework/Private/Inventory/DA_EquipmentConfig.cpp
Normal file
13
Source/PG_Framework/Private/Inventory/DA_EquipmentConfig.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "Inventory/DA_EquipmentConfig.h"
|
||||
|
||||
float UDA_EquipmentConfig::GetResistance(FGameplayTag DamageType) const
|
||||
{
|
||||
for (const FDamageTypeResistance& Entry : DamageTypeResistances)
|
||||
{
|
||||
if (Entry.DamageType == DamageType)
|
||||
{
|
||||
return Entry.Resistance;
|
||||
}
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
121
Source/PG_Framework/Private/Inventory/DA_ItemData.cpp
Normal file
121
Source/PG_Framework/Private/Inventory/DA_ItemData.cpp
Normal file
@@ -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 = TagManager.RequestGameplayTag(ItemTag.GetTagName(), false);
|
||||
if (!CheckTag.IsValid())
|
||||
{
|
||||
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<FString> 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
|
||||
6
Source/PG_Framework/Private/Player/BPC_HealthSystem.cpp
Normal file
6
Source/PG_Framework/Private/Player/BPC_HealthSystem.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "Player/BPC_HealthSystem.h"
|
||||
|
||||
UBPC_HealthSystem::UBPC_HealthSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "Player/BPC_MovementStateSystem.h"
|
||||
|
||||
UBPC_MovementStateSystem::UBPC_MovementStateSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
}
|
||||
6
Source/PG_Framework/Private/Player/BPC_StaminaSystem.cpp
Normal file
6
Source/PG_Framework/Private/Player/BPC_StaminaSystem.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "Player/BPC_StaminaSystem.h"
|
||||
|
||||
UBPC_StaminaSystem::UBPC_StaminaSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
}
|
||||
294
Source/PG_Framework/Private/Player/BPC_StateManager.cpp
Normal file
294
Source/PG_Framework/Private/Player/BPC_StateManager.cpp
Normal file
@@ -0,0 +1,294 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BPC_StateManager Implementation
|
||||
|
||||
#include "Player/BPC_StateManager.h"
|
||||
#include "Player/BPC_HealthSystem.h"
|
||||
#include "Player/BPC_StressSystem.h"
|
||||
#include "Player/BPC_StaminaSystem.h"
|
||||
#include "Player/BPC_MovementStateSystem.h"
|
||||
#include "GameplayTagsManager.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogStateManager, Log, All);
|
||||
|
||||
// Stub variable for combat encounter check (would come from GS_CoreGameState binding).
|
||||
namespace { bool bEncounterActive = false; }
|
||||
|
||||
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<UBPC_HealthSystem>();
|
||||
CachedStressSystem = Owner->FindComponentByClass<UBPC_StressSystem>();
|
||||
CachedStaminaSystem = Owner->FindComponentByClass<UBPC_StaminaSystem>();
|
||||
CachedMovementSystem = Owner->FindComponentByClass<UBPC_MovementStateSystem>();
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
6
Source/PG_Framework/Private/Player/BPC_StressSystem.cpp
Normal file
6
Source/PG_Framework/Private/Player/BPC_StressSystem.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "Player/BPC_StressSystem.h"
|
||||
|
||||
UBPC_StressSystem::UBPC_StressSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
}
|
||||
5
Source/PG_Framework/Private/Player/PC_CoreController.cpp
Normal file
5
Source/PG_Framework/Private/Player/PC_CoreController.cpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#include "Player/PC_CoreController.h"
|
||||
|
||||
APC_CoreController::APC_CoreController()
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "Player/PS_CorePlayerState.h"
|
||||
|
||||
APS_CorePlayerState::APS_CorePlayerState()
|
||||
{
|
||||
}
|
||||
372
Source/PG_Framework/Private/Save/SS_SaveManager.cpp
Normal file
372
Source/PG_Framework/Private/Save/SS_SaveManager.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
// 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<FSaveSlotInfo> USS_SaveManager::GetSlotManifest() const
|
||||
{
|
||||
TArray<FSaveSlotInfo> 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<uint8> 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<uint8> 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<UGI_GameFramework>(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<UGI_GameFramework>(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<uint8> 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<uint8>& Data, const FSaveSlotInfo& Meta)
|
||||
{
|
||||
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
|
||||
|
||||
// Serialize metadata header + save data.
|
||||
TArray<uint8> FileData;
|
||||
FMemoryWriter Writer(FileData);
|
||||
|
||||
// Write metadata header first.
|
||||
FSaveSlotInfo& MetaRef = const_cast<FSaveSlotInfo&>(Meta);
|
||||
FSaveSlotInfo::StaticStruct()->SerializeItem(Writer, &MetaRef, nullptr);
|
||||
|
||||
// Then write game state data.
|
||||
Writer.Serialize(const_cast<uint8*>(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<uint8>& OutData, FSaveSlotInfo& OutMeta)
|
||||
{
|
||||
FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav");
|
||||
|
||||
TArray<uint8> FileData;
|
||||
if (!FFileHelper::LoadFileToArray(FileData, *FilePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deserialize metadata header.
|
||||
FMemoryReader Reader(FileData);
|
||||
FSaveSlotInfo::StaticStruct()->SerializeItem(Reader, &OutMeta, nullptr);
|
||||
|
||||
// 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;
|
||||
}
|
||||
13
Source/PG_Framework/Private/State/DA_StateGatingTable.cpp
Normal file
13
Source/PG_Framework/Private/State/DA_StateGatingTable.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "State/DA_StateGatingTable.h"
|
||||
|
||||
bool UDA_StateGatingTable::IsActionGated(FGameplayTag ActionTag, FGameplayTag CurrentState) const
|
||||
{
|
||||
for (const FStateGatingRule& Rule : GatingRules)
|
||||
{
|
||||
if (Rule.ActionTag == ActionTag && Rule.BlockedByState == CurrentState)
|
||||
{
|
||||
return Rule.bIsBlocked;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — BPC_DamageReceptionSystem Implementation
|
||||
|
||||
#include "Weapons/BPC_DamageReceptionSystem.h"
|
||||
#include "Player/BPC_HealthSystem.h"
|
||||
#include "Weapons/BPC_ShieldDefenseSystem.h"
|
||||
#include "Weapons/BPC_HitReactionSystem.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogFrameworkDamage, 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(LogFrameworkDamage, 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(LogFrameworkDamage, Log, TEXT("EvaluateHitReaction — Knockdown! (%.1f damage)"), FinalDamage);
|
||||
OnKnockedDown.Broadcast(DamageCauser, FinalDamage);
|
||||
}
|
||||
else if (FinalDamage >= StaggerThreshold)
|
||||
{
|
||||
UE_LOG(LogFrameworkDamage, 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "Weapons/BPC_HitReactionSystem.h"
|
||||
|
||||
UBPC_HitReactionSystem::UBPC_HitReactionSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void UBPC_HitReactionSystem::PlayHitReaction(float DamageAmount, FVector HitDirection, AActor* DamageCauser)
|
||||
{
|
||||
// Stub — Blueprint child provides animation selection logic.
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "Weapons/BPC_ShieldDefenseSystem.h"
|
||||
|
||||
UBPC_ShieldDefenseSystem::UBPC_ShieldDefenseSystem()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
105
Source/PG_Framework/Public/Core/DA_GameTagRegistry.h
Normal file
105
Source/PG_Framework/Public/Core/DA_GameTagRegistry.h
Normal file
@@ -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<TObjectPtr<UDataTable>> 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<FGameplayTag> 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
|
||||
};
|
||||
169
Source/PG_Framework/Public/Core/FL_GameUtilities.h
Normal file
169
Source/PG_Framework/Public/Core/FL_GameUtilities.h
Normal file
@@ -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<typename T>
|
||||
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<T>();
|
||||
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<UGameInstanceSubsystem> 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<UInterface> 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);
|
||||
};
|
||||
226
Source/PG_Framework/Public/Core/GI_GameFramework.h
Normal file
226
Source/PG_Framework/Public/Core/GI_GameFramework.h
Normal file
@@ -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<T>().
|
||||
*/
|
||||
UCLASS(Blueprintable)
|
||||
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<UDA_GameTagRegistry> 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<typename T>
|
||||
T* GetService() const
|
||||
{
|
||||
return GetSubsystem<T>();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 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<FGameplayTag, bool> SessionFlags;
|
||||
|
||||
/** Maps service GameplayTags to subsystem classes. Populated during Init. */
|
||||
UPROPERTY()
|
||||
TMap<FGameplayTag, TSubclassOf<UGameInstanceSubsystem>> 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();
|
||||
};
|
||||
116
Source/PG_Framework/Public/Core/GM_CoreGameMode.h
Normal file
116
Source/PG_Framework/Public/Core/GM_CoreGameMode.h
Normal file
@@ -0,0 +1,116 @@
|
||||
// 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;
|
||||
|
||||
// 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.
|
||||
*
|
||||
* Note: PlayerControllerClass, PlayerStateClass, and GameStateClass are
|
||||
* inherited from AGameModeBase — do not redeclare (UHT forbids shadowing).
|
||||
* Set them in your BP child's Class Defaults.
|
||||
*/
|
||||
UCLASS(Blueprintable)
|
||||
class FRAMEWORK_API AGM_CoreGameMode : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AGM_CoreGameMode();
|
||||
|
||||
// ========================================================================
|
||||
// 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<UGI_GameFramework> 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);
|
||||
};
|
||||
144
Source/PG_Framework/Public/Core/GS_CoreGameState.h
Normal file
144
Source/PG_Framework/Public/Core/GS_CoreGameState.h
Normal file
@@ -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(Blueprintable)
|
||||
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<FGameplayTag> 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<FLifetimeProperty>& 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<class UGI_GameFramework> CachedFramework;
|
||||
|
||||
/** Throttle timer for play time updates (fires ~1/sec). */
|
||||
float TimeUpdateAccumulator = 0.0f;
|
||||
static constexpr float TimeUpdateInterval = 1.0f;
|
||||
};
|
||||
317
Source/PG_Framework/Public/Core/I_InterfaceLibrary.h
Normal file
317
Source/PG_Framework/Public/Core/I_InterfaceLibrary.h
Normal file
@@ -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)
|
||||
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<uint8> OnSave();
|
||||
|
||||
/** Restore state from a previously saved byte array. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Framework|Save")
|
||||
void OnLoad(const TArray<uint8>& 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) {}
|
||||
};
|
||||
232
Source/PG_Framework/Public/Input/SS_EnhancedInputManager.h
Normal file
232
Source/PG_Framework/Public/Input/SS_EnhancedInputManager.h
Normal file
@@ -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<UInputMappingContext> 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<TObjectPtr<UInputMappingContext>> 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<FInputContextEntry> ContextStack;
|
||||
|
||||
/** Whether UI input mode is currently active. */
|
||||
bool bCurrentUIMode = false;
|
||||
|
||||
/** Cached Enhanced Input subsystem. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UEnhancedInputLocalPlayerSubsystem> 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;
|
||||
};
|
||||
210
Source/PG_Framework/Public/Inventory/BPC_InventorySystem.h
Normal file
210
Source/PG_Framework/Public/Inventory/BPC_InventorySystem.h
Normal file
@@ -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<UDA_ItemData> 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(Blueprintable, 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<FInventorySlot> 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<UDA_ItemData*> 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();
|
||||
};
|
||||
37
Source/PG_Framework/Public/Inventory/DA_EquipmentConfig.h
Normal file
37
Source/PG_Framework/Public/Inventory/DA_EquipmentConfig.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "DA_EquipmentConfig.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FRAMEWORK_API FDamageTypeResistance
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FGameplayTag DamageType;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float Resistance = 0.0f;
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class FRAMEWORK_API UDA_EquipmentConfig : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
|
||||
TArray<FDamageTypeResistance> DamageTypeResistances;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
|
||||
float Durability = 100.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
|
||||
float Weight = 1.0f;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Config")
|
||||
float GetResistance(FGameplayTag DamageType) const;
|
||||
};
|
||||
232
Source/PG_Framework/Public/Inventory/DA_ItemData.h
Normal file
232
Source/PG_Framework/Public/Inventory/DA_ItemData.h
Normal file
@@ -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<UTexture2D> Icon;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Core")
|
||||
TSoftObjectPtr<UStaticMesh> 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<FGameplayTag> 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<FName, FString> 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
|
||||
};
|
||||
29
Source/PG_Framework/Public/Player/BPC_HealthSystem.h
Normal file
29
Source/PG_Framework/Public/Player/BPC_HealthSystem.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "BPC_HealthSystem.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHealthChanged, float, CurrentHealth, float, MaxHealth);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath);
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class FRAMEWORK_API UBPC_HealthSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_HealthSystem();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Health")
|
||||
float MaxHealth = 100.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Health")
|
||||
float CurrentHealth = 100.0f;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnHealthChanged OnHealthChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnDeath OnDeath;
|
||||
};
|
||||
23
Source/PG_Framework/Public/Player/BPC_MovementStateSystem.h
Normal file
23
Source/PG_Framework/Public/Player/BPC_MovementStateSystem.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "BPC_MovementStateSystem.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnMovementModeChanged, FGameplayTag, NewMode, FGameplayTag, OldMode);
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class FRAMEWORK_API UBPC_MovementStateSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_MovementStateSystem();
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Movement")
|
||||
FGameplayTag CurrentMovementMode;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnMovementModeChanged OnMovementModeChanged;
|
||||
};
|
||||
25
Source/PG_Framework/Public/Player/BPC_StaminaSystem.h
Normal file
25
Source/PG_Framework/Public/Player/BPC_StaminaSystem.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "BPC_StaminaSystem.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnExhaustionStateChanged, bool, bExhausted);
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class FRAMEWORK_API UBPC_StaminaSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_StaminaSystem();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Stamina")
|
||||
float MaxStamina = 100.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Stamina")
|
||||
float CurrentStamina = 100.0f;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnExhaustionStateChanged OnExhaustionStateChanged;
|
||||
};
|
||||
246
Source/PG_Framework/Public/Player/BPC_StateManager.h
Normal file
246
Source/PG_Framework/Public/Player/BPC_StateManager.h
Normal file
@@ -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(Blueprintable, 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<UDA_StateGatingTable> 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<FForceStackEntry> 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<UBPC_HealthSystem> CachedHealthSystem;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_StressSystem> CachedStressSystem;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_StaminaSystem> CachedStaminaSystem;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_MovementStateSystem> CachedMovementSystem;
|
||||
};
|
||||
32
Source/PG_Framework/Public/Player/BPC_StressSystem.h
Normal file
32
Source/PG_Framework/Public/Player/BPC_StressSystem.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "BPC_StressSystem.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EStressTier : uint8
|
||||
{
|
||||
Calm UMETA(DisplayName = "Calm"),
|
||||
Tense UMETA(DisplayName = "Tense"),
|
||||
Distressed UMETA(DisplayName = "Distressed"),
|
||||
Panic UMETA(DisplayName = "Panic"),
|
||||
Catatonic UMETA(DisplayName = "Catatonic"),
|
||||
};
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStressTierChanged, EStressTier, NewTier);
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class FRAMEWORK_API UBPC_StressSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_StressSystem();
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Stress")
|
||||
EStressTier StressTier = EStressTier::Calm;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Framework|Events")
|
||||
FOnStressTierChanged OnStressTierChanged;
|
||||
};
|
||||
14
Source/PG_Framework/Public/Player/PC_CoreController.h
Normal file
14
Source/PG_Framework/Public/Player/PC_CoreController.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "PC_CoreController.generated.h"
|
||||
|
||||
UCLASS(Blueprintable)
|
||||
class FRAMEWORK_API APC_CoreController : public APlayerController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APC_CoreController();
|
||||
};
|
||||
14
Source/PG_Framework/Public/Player/PS_CorePlayerState.h
Normal file
14
Source/PG_Framework/Public/Player/PS_CorePlayerState.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerState.h"
|
||||
#include "PS_CorePlayerState.generated.h"
|
||||
|
||||
UCLASS(Blueprintable)
|
||||
class FRAMEWORK_API APS_CorePlayerState : public APlayerState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
APS_CorePlayerState();
|
||||
};
|
||||
193
Source/PG_Framework/Public/Save/SS_SaveManager.h
Normal file
193
Source/PG_Framework/Public/Save/SS_SaveManager.h
Normal file
@@ -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<FSaveSlotInfo>&, 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<FSaveSlotInfo> 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<uint8>& Data, const FSaveSlotInfo& Meta);
|
||||
|
||||
/** Internal load implementation. */
|
||||
bool LoadFromFile(int32 SlotIndex, TArray<uint8>& OutData, FSaveSlotInfo& OutMeta);
|
||||
|
||||
/** Path to the save directory. */
|
||||
FString GetSaveDirectory() const;
|
||||
|
||||
/** Currently active save slot (from GI_GameFramework). */
|
||||
int32 GetActiveSlot() const;
|
||||
};
|
||||
34
Source/PG_Framework/Public/State/DA_StateGatingTable.h
Normal file
34
Source/PG_Framework/Public/State/DA_StateGatingTable.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "DA_StateGatingTable.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FRAMEWORK_API FStateGatingRule
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FGameplayTag ActionTag;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FGameplayTag BlockedByState;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
bool bIsBlocked = true;
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class FRAMEWORK_API UDA_StateGatingTable : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gating")
|
||||
TArray<FStateGatingRule> GatingRules;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Gating")
|
||||
bool IsActionGated(FGameplayTag ActionTag, FGameplayTag CurrentState) const;
|
||||
};
|
||||
150
Source/PG_Framework/Public/Weapons/BPC_DamageReceptionSystem.h
Normal file
150
Source/PG_Framework/Public/Weapons/BPC_DamageReceptionSystem.h
Normal file
@@ -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(Blueprintable, 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<UDA_EquipmentConfig> 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<FDamageModifier> 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<UBPC_HealthSystem> CachedHealthSystem;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_ShieldDefenseSystem> CachedShieldSystem;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UBPC_HitReactionSystem> CachedHitReactionSystem;
|
||||
};
|
||||
24
Source/PG_Framework/Public/Weapons/BPC_HitReactionSystem.h
Normal file
24
Source/PG_Framework/Public/Weapons/BPC_HitReactionSystem.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "BPC_HitReactionSystem.generated.h"
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class FRAMEWORK_API UBPC_HitReactionSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_HitReactionSystem();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Framework|Combat")
|
||||
void PlayHitReaction(float DamageAmount, FVector HitDirection, AActor* DamageCauser);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|HitReaction")
|
||||
float FlinchThreshold = 5.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|HitReaction")
|
||||
float RagdollThreshold = 50.0f;
|
||||
};
|
||||
26
Source/PG_Framework/Public/Weapons/BPC_ShieldDefenseSystem.h
Normal file
26
Source/PG_Framework/Public/Weapons/BPC_ShieldDefenseSystem.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "BPC_ShieldDefenseSystem.generated.h"
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup=(Framework), meta=(BlueprintSpawnableComponent))
|
||||
class FRAMEWORK_API UBPC_ShieldDefenseSystem : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UBPC_ShieldDefenseSystem();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Shield")
|
||||
float ShieldHealth = 100.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Shield")
|
||||
float MaxShieldHealth = 100.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Framework|Shield")
|
||||
float BlockAngle = 90.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Framework|Shield")
|
||||
bool bShieldBroken = false;
|
||||
};
|
||||
Reference in New Issue
Block a user