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:
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
|
||||
Reference in New Issue
Block a user