Import initial du projet Survival (UE 5.8)
Boucle de jeu complète : récolte, inventaire, consommation, stats de survie, mort et respawn. Gameplay en C++, Blueprints réservés au câblage d'assets. Assets binaires (.uasset, .umap, textures, audio) suivis via Git LFS. Binaries/, Intermediate/, Saved/ et DerivedDataCache/ sont ignorés : régénérés au build, ils pèsent 3 Go pour 1,3 Go de contenu utile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "InventoryComponent.h"
|
||||
|
||||
#include "ItemDataAsset.h"
|
||||
|
||||
UInventoryComponent::UInventoryComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
|
||||
// Necessaire pour que InitializeComponent() soit appele.
|
||||
bWantsInitializeComponent = true;
|
||||
}
|
||||
|
||||
void UInventoryComponent::InitializeComponent()
|
||||
{
|
||||
Super::InitializeComponent();
|
||||
|
||||
EnsureSlotCount();
|
||||
|
||||
SelectedHotbarIndex = FMath::Clamp(SelectedHotbarIndex, 0, FMath::Max(0, GetHotbarSlotCount() - 1));
|
||||
}
|
||||
|
||||
void UInventoryComponent::SelectHotbarSlot(int32 Index)
|
||||
{
|
||||
const int32 Count = GetHotbarSlotCount();
|
||||
if (Index < 0 || Index >= Count || Index == SelectedHotbarIndex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedHotbarIndex = Index;
|
||||
OnSelectedHotbarSlotChanged.Broadcast(SelectedHotbarIndex);
|
||||
}
|
||||
|
||||
void UInventoryComponent::CycleHotbarSelection(int32 Delta)
|
||||
{
|
||||
const int32 Count = GetHotbarSlotCount();
|
||||
if (Count <= 0 || Delta == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Modulo positif : en C++, -1 % 6 vaut -1 et non 5. Sans le double
|
||||
// modulo, reculer depuis le premier slot donnerait un index negatif.
|
||||
const int32 NewIndex = ((SelectedHotbarIndex + Delta) % Count + Count) % Count;
|
||||
SelectHotbarSlot(NewIndex);
|
||||
}
|
||||
|
||||
FInventorySlot UInventoryComponent::GetSelectedSlot() const
|
||||
{
|
||||
return Slots.IsValidIndex(SelectedHotbarIndex) ? Slots[SelectedHotbarIndex] : FInventorySlot();
|
||||
}
|
||||
|
||||
void UInventoryComponent::EnsureSlotCount()
|
||||
{
|
||||
const int32 Target = GetTotalSlotCount();
|
||||
if (Slots.Num() != Target)
|
||||
{
|
||||
Slots.SetNum(Target);
|
||||
}
|
||||
}
|
||||
|
||||
int32 UInventoryComponent::AddItem(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
|
||||
{
|
||||
if (!Item || Quantity <= 0)
|
||||
{
|
||||
return Quantity;
|
||||
}
|
||||
|
||||
EnsureSlotCount();
|
||||
|
||||
const int32 MaxStack = Item->GetEffectiveMaxStack();
|
||||
const int32 MaxUses = FMath::Max(1, Item->MaxUses);
|
||||
// -1 = "plein" : un objet neuf, ou un ajout qui ne precise pas d'etat.
|
||||
const int32 StartingUses = (RemainingUses < 0) ? MaxUses : FMath::Clamp(RemainingUses, 0, MaxUses);
|
||||
|
||||
int32 Remaining = Quantity;
|
||||
|
||||
// Premiere passe : completer les piles deja entamees. On le fait avant
|
||||
// d'ouvrir un nouveau slot, sinon on gaspillerait des emplacements en
|
||||
// laissant des piles a moitie vides un peu partout.
|
||||
for (FInventorySlot& Slot : Slots)
|
||||
{
|
||||
if (Remaining <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (Slot.Item == Item && Slot.Quantity < MaxStack)
|
||||
{
|
||||
const int32 Added = FMath::Min(Remaining, MaxStack - Slot.Quantity);
|
||||
Slot.Quantity += Added;
|
||||
Remaining -= Added;
|
||||
}
|
||||
}
|
||||
|
||||
// Deuxieme passe : ouvrir de nouveaux slots. La BARRE RAPIDE est servie en
|
||||
// premier, pour qu'un objet ramasse soit immediatement utilisable sans
|
||||
// passer par l'inventaire.
|
||||
auto FillEmptyRange = [&](int32 First, int32 Last)
|
||||
{
|
||||
for (int32 Index = First; Index < Last && Remaining > 0; ++Index)
|
||||
{
|
||||
FInventorySlot& Slot = Slots[Index];
|
||||
if (Slot.IsEmpty())
|
||||
{
|
||||
const int32 Added = FMath::Min(Remaining, MaxStack);
|
||||
Slot.Item = Item;
|
||||
Slot.Quantity = Added;
|
||||
Slot.RemainingUses = StartingUses;
|
||||
Remaining -= Added;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Inverse ces deux lignes si tu veux un jour servir l'inventaire d'abord.
|
||||
const int32 BackpackStart = GetBackpackStartIndex();
|
||||
FillEmptyRange(0, BackpackStart);
|
||||
// Barre pleine : on deborde dans l'inventaire.
|
||||
FillEmptyRange(BackpackStart, Slots.Num());
|
||||
|
||||
if (Remaining < Quantity)
|
||||
{
|
||||
OnInventoryChanged.Broadcast();
|
||||
}
|
||||
|
||||
return Remaining;
|
||||
}
|
||||
|
||||
int32 UInventoryComponent::RemoveItem(UItemDataAsset* Item, int32 Quantity)
|
||||
{
|
||||
if (!Item || Quantity <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32 Remaining = Quantity;
|
||||
|
||||
// On vide les plus petites piles d'abord : ca libere des slots au plus
|
||||
// vite au lieu de laisser trainer plusieurs piles a moitie pleines.
|
||||
while (Remaining > 0)
|
||||
{
|
||||
FInventorySlot* Best = nullptr;
|
||||
for (FInventorySlot& Slot : Slots)
|
||||
{
|
||||
if (Slot.Item == Item && Slot.Quantity > 0)
|
||||
{
|
||||
if (!Best || Slot.Quantity < Best->Quantity)
|
||||
{
|
||||
Best = &Slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Best)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const int32 Taken = FMath::Min(Remaining, Best->Quantity);
|
||||
Best->Quantity -= Taken;
|
||||
Remaining -= Taken;
|
||||
|
||||
if (Best->Quantity <= 0)
|
||||
{
|
||||
Best->Clear();
|
||||
}
|
||||
}
|
||||
|
||||
const int32 Removed = Quantity - Remaining;
|
||||
if (Removed > 0)
|
||||
{
|
||||
OnInventoryChanged.Broadcast();
|
||||
}
|
||||
|
||||
return Removed;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::ConsumeUse(int32 SlotIndex)
|
||||
{
|
||||
if (!Slots.IsValidIndex(SlotIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FInventorySlot& Slot = Slots[SlotIndex];
|
||||
if (Slot.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 MaxUses = FMath::Max(1, Slot.Item->MaxUses);
|
||||
|
||||
// Objet a usage unique : un exemplaire disparait, point.
|
||||
if (MaxUses <= 1)
|
||||
{
|
||||
Slot.Quantity -= 1;
|
||||
if (Slot.Quantity <= 0)
|
||||
{
|
||||
Slot.Clear();
|
||||
}
|
||||
|
||||
OnInventoryChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
Slot.RemainingUses -= 1;
|
||||
if (Slot.RemainingUses <= 0)
|
||||
{
|
||||
Slot.Quantity -= 1;
|
||||
if (Slot.Quantity <= 0)
|
||||
{
|
||||
Slot.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cas theorique : un objet a charges ne s'empile pas. On recharge
|
||||
// quand meme l'exemplaire suivant, au cas ou une pile serait
|
||||
// construite autrement un jour.
|
||||
Slot.RemainingUses = MaxUses;
|
||||
}
|
||||
}
|
||||
|
||||
OnInventoryChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
int32 UInventoryComponent::GetItemCount(const UItemDataAsset* Item) const
|
||||
{
|
||||
if (!Item)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32 Total = 0;
|
||||
for (const FInventorySlot& Slot : Slots)
|
||||
{
|
||||
if (Slot.Item == Item)
|
||||
{
|
||||
Total += Slot.Quantity;
|
||||
}
|
||||
}
|
||||
|
||||
return Total;
|
||||
}
|
||||
|
||||
int32 UInventoryComponent::GetRoomFor(const UItemDataAsset* Item) const
|
||||
{
|
||||
if (!Item)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int32 MaxStack = Item->GetEffectiveMaxStack();
|
||||
int32 Room = 0;
|
||||
|
||||
for (const FInventorySlot& Slot : Slots)
|
||||
{
|
||||
if (Slot.IsEmpty())
|
||||
{
|
||||
Room += MaxStack;
|
||||
}
|
||||
else if (Slot.Item == Item)
|
||||
{
|
||||
Room += FMath::Max(0, MaxStack - Slot.Quantity);
|
||||
}
|
||||
}
|
||||
|
||||
return Room;
|
||||
}
|
||||
|
||||
int32 UInventoryComponent::GetUsedSlotCount() const
|
||||
{
|
||||
int32 Used = 0;
|
||||
for (const FInventorySlot& Slot : Slots)
|
||||
{
|
||||
if (!Slot.IsEmpty())
|
||||
{
|
||||
++Used;
|
||||
}
|
||||
}
|
||||
|
||||
return Used;
|
||||
}
|
||||
|
||||
int32 UInventoryComponent::FindFirstEmptySlot() const
|
||||
{
|
||||
for (int32 Index = 0; Index < Slots.Num(); ++Index)
|
||||
{
|
||||
if (Slots[Index].IsEmpty())
|
||||
{
|
||||
return Index;
|
||||
}
|
||||
}
|
||||
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::TakeFromSlot(int32 SlotIndex, int32 Quantity, FInventorySlot& OutTaken)
|
||||
{
|
||||
OutTaken.Clear();
|
||||
|
||||
if (Quantity <= 0 || !Slots.IsValidIndex(SlotIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FInventorySlot& Slot = Slots[SlotIndex];
|
||||
if (Slot.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 Taken = FMath::Min(Quantity, Slot.Quantity);
|
||||
|
||||
OutTaken.Item = Slot.Item;
|
||||
OutTaken.Quantity = Taken;
|
||||
// L'etat d'usure part avec l'objet : jeter une canette entamee puis la
|
||||
// reprendre ne doit pas la remplir.
|
||||
OutTaken.RemainingUses = Slot.RemainingUses;
|
||||
|
||||
Slot.Quantity -= Taken;
|
||||
if (Slot.Quantity <= 0)
|
||||
{
|
||||
Slot.Clear();
|
||||
}
|
||||
|
||||
OnInventoryChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::MoveItemQuantity(int32 FromIndex, int32 ToIndex, int32 Quantity)
|
||||
{
|
||||
if (Quantity <= 0 || FromIndex == ToIndex || !Slots.IsValidIndex(FromIndex) || !Slots.IsValidIndex(ToIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FInventorySlot& From = Slots[FromIndex];
|
||||
if (From.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deplacement total : on retombe sur le comportement complet, echange compris.
|
||||
if (Quantity >= From.Quantity)
|
||||
{
|
||||
return MoveItem(FromIndex, ToIndex);
|
||||
}
|
||||
|
||||
FInventorySlot& To = Slots[ToIndex];
|
||||
const int32 MaxStack = From.Item->GetEffectiveMaxStack();
|
||||
|
||||
if (To.IsEmpty())
|
||||
{
|
||||
const int32 Transferred = FMath::Min(Quantity, MaxStack);
|
||||
To.Item = From.Item;
|
||||
To.Quantity = Transferred;
|
||||
To.RemainingUses = From.RemainingUses;
|
||||
From.Quantity -= Transferred;
|
||||
|
||||
OnInventoryChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (To.Item == From.Item)
|
||||
{
|
||||
const int32 Transferable = FMath::Min(Quantity, MaxStack - To.Quantity);
|
||||
if (Transferable <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
To.Quantity += Transferable;
|
||||
From.Quantity -= Transferable;
|
||||
|
||||
OnInventoryChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Objet different et transfert partiel : aucune interpretation raisonnable.
|
||||
// On refuse plutot que d'inventer un comportement que le joueur ne
|
||||
// comprendrait pas.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::MoveItem(int32 FromIndex, int32 ToIndex)
|
||||
{
|
||||
if (FromIndex == ToIndex || !Slots.IsValidIndex(FromIndex) || !Slots.IsValidIndex(ToIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FInventorySlot& From = Slots[FromIndex];
|
||||
if (From.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FInventorySlot& To = Slots[ToIndex];
|
||||
|
||||
// Cible vide : la pile entiere part, quelle que soit sa taille.
|
||||
if (To.IsEmpty())
|
||||
{
|
||||
To = From;
|
||||
From.Clear();
|
||||
OnInventoryChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Meme objet : on fusionne dans la limite de la pile. Pour un objet a
|
||||
// charges, GetEffectiveMaxStack vaut 1, donc on tombe toujours sur
|
||||
// l'echange -- deux canettes entamees ne fusionnent jamais.
|
||||
if (To.Item == From.Item)
|
||||
{
|
||||
const int32 MaxStack = From.Item->GetEffectiveMaxStack();
|
||||
const int32 Transferable = FMath::Min(From.Quantity, MaxStack - To.Quantity);
|
||||
|
||||
// Cible deja pleine : on echange plutot que de ne rien faire, sinon le
|
||||
// geste du joueur resterait sans effet et passerait pour un bug.
|
||||
if (Transferable <= 0)
|
||||
{
|
||||
Swap(From, To);
|
||||
OnInventoryChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
To.Quantity += Transferable;
|
||||
From.Quantity -= Transferable;
|
||||
if (From.Quantity <= 0)
|
||||
{
|
||||
From.Clear();
|
||||
}
|
||||
|
||||
OnInventoryChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Objets differents : simple echange.
|
||||
Swap(From, To);
|
||||
OnInventoryChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::SetBonusSlotCount(int32 NewBonusSlots)
|
||||
{
|
||||
const int32 NewTotal = FMath::Max(0, HotbarSlotCount + BackpackSlotCount + FMath::Max(0, NewBonusSlots));
|
||||
|
||||
// On refuse plutot que de detruire du butin en silence. C'est a l'appelant
|
||||
// (le code qui deséquipe le sac) de decider quoi faire : bloquer le retrait,
|
||||
// ou vider le surplus au sol avant de reessayer.
|
||||
if (GetUsedSlotCount() > NewTotal)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BonusSlotCount = FMath::Max(0, NewBonusSlots);
|
||||
|
||||
// Compacter avant de tronquer : sans ca, un objet range dans un slot de
|
||||
// fin serait supprime alors qu'il reste de la place au debut.
|
||||
CompactSlots();
|
||||
Slots.SetNum(NewTotal);
|
||||
|
||||
OnInventoryChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
void UInventoryComponent::CompactSlots()
|
||||
{
|
||||
const int32 Start = GetBackpackStartIndex();
|
||||
const int32 Count = Slots.Num() - Start;
|
||||
if (Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// On ne compacte QUE la portion sac. Compacter tout le tableau ferait
|
||||
// remonter des objets du sac dans la barre rapide sans que le joueur
|
||||
// l'ait demande -- et la troncature ne retire de toute facon que la fin.
|
||||
TArray<FInventorySlot> Backpack;
|
||||
Backpack.Append(Slots.GetData() + Start, Count);
|
||||
|
||||
Backpack.StableSort([](const FInventorySlot& A, const FInventorySlot& B)
|
||||
{
|
||||
return !A.IsEmpty() && B.IsEmpty();
|
||||
});
|
||||
|
||||
for (int32 Index = 0; Index < Count; ++Index)
|
||||
{
|
||||
Slots[Start + Index] = Backpack[Index];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user