754 lines
17 KiB
C++
754 lines
17 KiB
C++
// Fill out your copyright notice in the Description page of Project Settings.
|
|
|
|
|
|
#include "InventoryComponent.h"
|
|
|
|
#include "ItemDataAsset.h"
|
|
#include "Net/UnrealNetwork.h"
|
|
|
|
UInventoryComponent::UInventoryComponent()
|
|
{
|
|
PrimaryComponentTick.bCanEverTick = false;
|
|
|
|
// Necessaire pour que InitializeComponent() soit appele.
|
|
bWantsInitializeComponent = true;
|
|
|
|
// Le contenu appartient au serveur et descend vers les clients.
|
|
SetIsReplicatedByDefault(true);
|
|
}
|
|
|
|
void UInventoryComponent::InitializeComponent()
|
|
{
|
|
Super::InitializeComponent();
|
|
|
|
EnsureSlotCount();
|
|
|
|
SelectedHotbarIndex = FMath::Clamp(SelectedHotbarIndex, 0, FMath::Max(0, GetHotbarSlotCount() - 1));
|
|
}
|
|
|
|
void UInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
|
{
|
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
|
|
|
DOREPLIFETIME(UInventoryComponent, Slots);
|
|
}
|
|
|
|
void UInventoryComponent::OnRep_Slots()
|
|
{
|
|
// Pas de BroadcastChanged() ici : le regroupement differe ne sert qu'a
|
|
// eviter vingt reconstructions d'UI pendant un "Tout ranger" execute en
|
|
// local. La replication, elle, arrive deja groupee -- le serveur a fini son
|
|
// lot avant d'envoyer quoi que ce soit.
|
|
OnInventoryChanged.Broadcast();
|
|
}
|
|
|
|
/**
|
|
* SelectedHotbarIndex n'est volontairement PAS replique : le slot actif est une
|
|
* intention purement locale, chaque joueur choisit le sien et personne d'autre
|
|
* n'a besoin de le connaitre. Le jour ou l'objet en main devra etre visible par
|
|
* les autres, ce sera une propriete du PAWN -- ce qu'on tient dans sa main est
|
|
* une donnee de corps, pas de sac.
|
|
*/
|
|
|
|
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)
|
|
{
|
|
BroadcastChanged();
|
|
}
|
|
|
|
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)
|
|
{
|
|
BroadcastChanged();
|
|
}
|
|
|
|
return Removed;
|
|
}
|
|
|
|
void UInventoryComponent::Server_TransferAllTo_Implementation(UInventoryComponent* From, UInventoryComponent* To, int32 FirstIndex)
|
|
{
|
|
if (From)
|
|
{
|
|
From->TransferAllTo(To, FirstIndex);
|
|
}
|
|
}
|
|
|
|
void UInventoryComponent::Server_ConsumeUse_Implementation(int32 SlotIndex)
|
|
{
|
|
ConsumeUse(SlotIndex);
|
|
}
|
|
|
|
bool UInventoryComponent::ConsumeUse(int32 SlotIndex)
|
|
{
|
|
if (!HasInventoryAuthority())
|
|
{
|
|
Server_ConsumeUse(SlotIndex);
|
|
return true;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
BroadcastChanged();
|
|
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;
|
|
}
|
|
}
|
|
|
|
BroadcastChanged();
|
|
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();
|
|
}
|
|
|
|
BroadcastChanged();
|
|
return true;
|
|
}
|
|
|
|
bool UInventoryComponent::MoveItemQuantity(int32 FromIndex, int32 ToIndex, int32 Quantity)
|
|
{
|
|
return TransferSlot(this, FromIndex, this, ToIndex, Quantity);
|
|
}
|
|
|
|
bool UInventoryComponent::MoveItem(int32 FromIndex, int32 ToIndex)
|
|
{
|
|
// MAX_int32 depasse forcement la pile : TransferSlot bascule alors sur le
|
|
// deplacement total, echange compris.
|
|
return TransferSlot(this, FromIndex, this, ToIndex, MAX_int32);
|
|
}
|
|
|
|
bool UInventoryComponent::ApplySlotMove(FInventorySlot& From, FInventorySlot& To, int32 Quantity)
|
|
{
|
|
if (Quantity <= 0 || From.IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const int32 MaxStack = From.Item->GetEffectiveMaxStack();
|
|
const bool bTotal = (Quantity >= From.Quantity);
|
|
|
|
if (To.IsEmpty())
|
|
{
|
|
// Deplacement total : la pile entiere part, quelle que soit sa taille --
|
|
// on ne la tronque pas a MaxStack, elle etait deja legale a la source.
|
|
if (bTotal)
|
|
{
|
|
To = From;
|
|
From.Clear();
|
|
return true;
|
|
}
|
|
|
|
const int32 Transferred = FMath::Min(Quantity, MaxStack);
|
|
To.Item = From.Item;
|
|
To.Quantity = Transferred;
|
|
// L'usure suit l'objet : diviser une pile de canettes entamees donne
|
|
// deux piles au meme niveau, jamais une neuve.
|
|
To.RemainingUses = From.RemainingUses;
|
|
From.Quantity -= Transferred;
|
|
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 Wanted = bTotal ? From.Quantity : Quantity;
|
|
const int32 Transferable = FMath::Min(Wanted, MaxStack - To.Quantity);
|
|
|
|
if (Transferable <= 0)
|
|
{
|
|
// Cible pleine. Sur un deplacement total on echange plutot que de
|
|
// ne rien faire, sinon le geste du joueur passerait pour un bug ;
|
|
// sur un deplacement partiel on refuse, echanger la moitie d'une
|
|
// pile contre autre chose n'ayant aucun sens.
|
|
if (!bTotal)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Swap(From, To);
|
|
return true;
|
|
}
|
|
|
|
To.Quantity += Transferable;
|
|
From.Quantity -= Transferable;
|
|
if (From.Quantity <= 0)
|
|
{
|
|
From.Clear();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Objets differents : echange, et seulement sur un deplacement total. Il
|
|
// n'existe aucune facon sensee d'echanger la moitie d'une pile contre autre
|
|
// chose -- on refuse plutot que d'inventer un comportement incomprehensible.
|
|
if (!bTotal)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Swap(From, To);
|
|
return true;
|
|
}
|
|
|
|
bool UInventoryComponent::HasInventoryAuthority() const
|
|
{
|
|
const AActor* Owner = GetOwner();
|
|
return Owner && Owner->HasAuthority();
|
|
}
|
|
|
|
UInventoryComponent* UInventoryComponent::FindNetProxy(UInventoryComponent* A, UInventoryComponent* B)
|
|
{
|
|
auto IsLocalPawnInventory = [](const UInventoryComponent* Inventory)
|
|
{
|
|
const APawn* OwnerPawn = Inventory ? Cast<APawn>(Inventory->GetOwner()) : nullptr;
|
|
return OwnerPawn && OwnerPawn->IsLocallyControlled();
|
|
};
|
|
|
|
if (IsLocalPawnInventory(A))
|
|
{
|
|
return A;
|
|
}
|
|
|
|
return IsLocalPawnInventory(B) ? B : nullptr;
|
|
}
|
|
|
|
bool UInventoryComponent::TransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity)
|
|
{
|
|
if (!From || !To || Quantity <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Chez un client : on ne touche a rien, on demande. Le contenu redescendra
|
|
// par OnRep_Slots, qui rediffuse OnInventoryChanged et rafraichit l'UI.
|
|
if (!From->HasInventoryAuthority())
|
|
{
|
|
UInventoryComponent* Proxy = FindNetProxy(From, To);
|
|
if (!Proxy)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("UInventoryComponent::TransferSlot : aucun inventaire de joueur local dans l'echange, demande abandonnee."));
|
|
return false;
|
|
}
|
|
|
|
Proxy->Server_TransferSlot(From, FromIndex, To, ToIndex, Quantity);
|
|
|
|
// True signifie ici "demande partie", pas "contenu modifie". Les
|
|
// appelants s'en servent pour savoir si le geste a ete pris en compte,
|
|
// ce qui reste exact.
|
|
return true;
|
|
}
|
|
|
|
return ApplyTransferSlot(From, FromIndex, To, ToIndex, Quantity);
|
|
}
|
|
|
|
void UInventoryComponent::Server_TransferSlot_Implementation(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity)
|
|
{
|
|
// From et To arrivent par le reseau : ils peuvent etre nuls si l'acteur a
|
|
// ete detruit entre l'envoi et l'arrivee -- un coffre casse, un joueur
|
|
// deconnecte. ApplyTransferSlot les revalide de toute facon.
|
|
ApplyTransferSlot(From, FromIndex, To, ToIndex, Quantity);
|
|
}
|
|
|
|
bool UInventoryComponent::ApplyTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity)
|
|
{
|
|
if (!From || !To || Quantity <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Meme inventaire et meme case : rien a faire. Le test doit porter sur le
|
|
// couple et non sur l'index seul, deux inventaires differents ayant tous
|
|
// les deux une case 3.
|
|
if (From == To && FromIndex == ToIndex)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!From->Slots.IsValidIndex(FromIndex) || !To->Slots.IsValidIndex(ToIndex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Deux references dans le meme tableau quand From == To : legal ici parce
|
|
// qu'aucun redimensionnement n'a lieu entre les deux acces.
|
|
if (!ApplySlotMove(From->Slots[FromIndex], To->Slots[ToIndex], Quantity))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
From->BroadcastChanged();
|
|
if (To != From)
|
|
{
|
|
To->BroadcastChanged();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int32 UInventoryComponent::PushSlotInto(int32 SlotIndex, UInventoryComponent& To)
|
|
{
|
|
if (!Slots.IsValidIndex(SlotIndex))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
FInventorySlot& Slot = Slots[SlotIndex];
|
|
if (Slot.IsEmpty())
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
const int32 Left = To.AddItem(Slot.Item, Slot.Quantity, Slot.RemainingUses);
|
|
const int32 Moved = Slot.Quantity - Left;
|
|
if (Moved <= 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
Slot.Quantity = Left;
|
|
if (Slot.Quantity <= 0)
|
|
{
|
|
Slot.Clear();
|
|
}
|
|
|
|
return Moved;
|
|
}
|
|
|
|
int32 UInventoryComponent::QuickTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To)
|
|
{
|
|
if (!From || !To || From == To)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (!From->HasInventoryAuthority())
|
|
{
|
|
if (UInventoryComponent* Proxy = FindNetProxy(From, To))
|
|
{
|
|
Proxy->Server_QuickTransferSlot(From, FromIndex, To);
|
|
|
|
// Le nombre reellement deplace n'est connu que du serveur. On rend
|
|
// une valeur non nulle pour que l'appelant sache que le clic a ete
|
|
// pris en compte ; l'affichage, lui, viendra de OnRep_Slots.
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
const int32 Moved = From->PushSlotInto(FromIndex, *To);
|
|
if (Moved > 0)
|
|
{
|
|
From->BroadcastChanged();
|
|
}
|
|
|
|
return Moved;
|
|
}
|
|
|
|
void UInventoryComponent::Server_QuickTransferSlot_Implementation(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To)
|
|
{
|
|
QuickTransferSlot(From, FromIndex, To);
|
|
}
|
|
|
|
int32 UInventoryComponent::TransferAllTo(UInventoryComponent* To, int32 FirstIndex)
|
|
{
|
|
if (!To || To == this)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (!HasInventoryAuthority())
|
|
{
|
|
if (UInventoryComponent* Proxy = FindNetProxy(this, To))
|
|
{
|
|
Proxy->Server_TransferAllTo(this, To, FirstIndex);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
To->BeginBatch();
|
|
|
|
int32 Moved = 0;
|
|
for (int32 Index = FMath::Max(0, FirstIndex); Index < Slots.Num(); ++Index)
|
|
{
|
|
Moved += PushSlotInto(Index, *To);
|
|
}
|
|
|
|
To->EndBatch();
|
|
|
|
if (Moved > 0)
|
|
{
|
|
BroadcastChanged();
|
|
}
|
|
|
|
return Moved;
|
|
}
|
|
|
|
void UInventoryComponent::ConfigureAsContainer(int32 NewSlotCount)
|
|
{
|
|
// Aucune barre rapide : GetBackpackStartIndex() vaut alors 0, et la grille
|
|
// affiche la totalite du conteneur au lieu d'en masquer les dix premieres
|
|
// cases.
|
|
HotbarSlotCount = 0;
|
|
BonusSlotCount = 0;
|
|
BackpackSlotCount = FMath::Max(0, NewSlotCount);
|
|
SelectedHotbarIndex = 0;
|
|
|
|
EnsureSlotCount();
|
|
}
|
|
|
|
void UInventoryComponent::BroadcastChanged()
|
|
{
|
|
if (BatchDepth > 0)
|
|
{
|
|
bBatchDirty = true;
|
|
return;
|
|
}
|
|
|
|
OnInventoryChanged.Broadcast();
|
|
}
|
|
|
|
void UInventoryComponent::BeginBatch()
|
|
{
|
|
++BatchDepth;
|
|
}
|
|
|
|
void UInventoryComponent::EndBatch()
|
|
{
|
|
BatchDepth = FMath::Max(0, BatchDepth - 1);
|
|
if (BatchDepth > 0 || !bBatchDirty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bBatchDirty = false;
|
|
OnInventoryChanged.Broadcast();
|
|
}
|
|
|
|
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);
|
|
|
|
BroadcastChanged();
|
|
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];
|
|
}
|
|
}
|