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:
Lefteris Notas
2026-05-21 14:38:30 +03:00
parent a145ae9373
commit f986343325
50 changed files with 86 additions and 0 deletions

View 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();
}

View 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;
}

View 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