220 lines
5.7 KiB
C++
220 lines
5.7 KiB
C++
// © 2025 Naked People Team. All Rights Reserved.
|
|
|
|
|
|
#include "InventorySubsystem.h"
|
|
|
|
#include "Kismet/GameplayStatics.h"
|
|
#include "NakedDesire/Clothing/ClothingItemDefinition.h"
|
|
#include "NakedDesire/Clothing/ClothingItemInstance.h"
|
|
#include "NakedDesire/Clothing/ClothingManager.h"
|
|
#include "NakedDesire/Items/ItemDefinition.h"
|
|
#include "NakedDesire/Items/ItemInstance.h"
|
|
#include "NakedDesire/Phone/PhoneItemDefinition.h"
|
|
#include "NakedDesire/Phone/PhoneItemInstance.h"
|
|
#include "NakedDesire/Player/NakedDesireCharacter.h"
|
|
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
|
|
#include "NakedDesire/SaveGame/ItemSaveRecord.h"
|
|
#include "NakedDesire/SaveGame/SaveSubsystem.h"
|
|
|
|
const TArray<TObjectPtr<UItemInstance>>& UInventorySubsystem::GetWardrobeItems()
|
|
{
|
|
EnsureHydrated();
|
|
return WardrobeItems;
|
|
}
|
|
|
|
UPhoneItemInstance* UInventorySubsystem::GetEquippedPhone()
|
|
{
|
|
EnsureHydrated();
|
|
return EquippedPhone;
|
|
}
|
|
|
|
void UInventorySubsystem::AddToWardrobe(UItemInstance* Item)
|
|
{
|
|
EnsureHydrated();
|
|
StoreItem(Item);
|
|
OnWardrobeChanged.Broadcast();
|
|
}
|
|
|
|
void UInventorySubsystem::RemoveFromWardrobe(UItemInstance* Item)
|
|
{
|
|
EnsureHydrated();
|
|
UnstoreItem(Item);
|
|
OnWardrobeChanged.Broadcast();
|
|
}
|
|
|
|
void UInventorySubsystem::EquipFromWardrobe(UItemInstance* Item)
|
|
{
|
|
EnsureHydrated();
|
|
|
|
if (UPhoneItemInstance* Phone = Cast<UPhoneItemInstance>(Item))
|
|
{
|
|
EquipPhone(Phone);
|
|
return;
|
|
}
|
|
|
|
UClothingItemInstance* Clothing = Cast<UClothingItemInstance>(Item);
|
|
if (!Clothing || !Clothing->GetClothingItemDefinition())
|
|
return;
|
|
|
|
UClothingManager* ClothingManager = GetPlayerClothingManager();
|
|
if (!ClothingManager)
|
|
return;
|
|
|
|
const EClothingSlotType SlotType = Clothing->GetClothingItemDefinition()->SlotType;
|
|
|
|
// Return the current occupant of the target slot to the wardrobe (never drop it to the world).
|
|
if (UClothingItemInstance* Occupant = ClothingManager->GetSlotClothing(SlotType))
|
|
{
|
|
ClothingManager->RemoveClothing(SlotType);
|
|
StoreItem(Occupant);
|
|
}
|
|
|
|
// Bodysuit exclusion (§6.5): vacate conflicting slots back into the wardrobe.
|
|
for (const EClothingSlotType ExcludedSlot : UClothingManager::GetBodysuitExcludedSlots(SlotType))
|
|
{
|
|
if (UClothingItemInstance* Excluded = ClothingManager->GetSlotClothing(ExcludedSlot))
|
|
{
|
|
ClothingManager->RemoveClothing(ExcludedSlot);
|
|
StoreItem(Excluded);
|
|
}
|
|
}
|
|
|
|
UnstoreItem(Clothing);
|
|
ClothingManager->EquipSlot(Clothing);
|
|
|
|
OnWardrobeChanged.Broadcast();
|
|
}
|
|
|
|
void UInventorySubsystem::UnequipToWardrobe(UItemInstance* Item)
|
|
{
|
|
EnsureHydrated();
|
|
|
|
if (UPhoneItemInstance* Phone = Cast<UPhoneItemInstance>(Item))
|
|
{
|
|
UnequipPhone(Phone);
|
|
return;
|
|
}
|
|
|
|
UClothingItemInstance* Clothing = Cast<UClothingItemInstance>(Item);
|
|
if (!Clothing || !Clothing->GetClothingItemDefinition())
|
|
return;
|
|
|
|
UClothingManager* ClothingManager = GetPlayerClothingManager();
|
|
if (!ClothingManager)
|
|
return;
|
|
|
|
UClothingItemInstance* Removed = ClothingManager->RemoveClothing(Clothing->GetClothingItemDefinition()->SlotType);
|
|
if (!Removed)
|
|
return;
|
|
|
|
StoreItem(Removed);
|
|
OnWardrobeChanged.Broadcast();
|
|
}
|
|
|
|
void UInventorySubsystem::EquipPhone(UPhoneItemInstance* Phone)
|
|
{
|
|
if (!Phone || Phone == EquippedPhone)
|
|
return;
|
|
|
|
UGlobalSaveGameData* Save = GetSave();
|
|
|
|
// Hot-swap: the previously equipped phone returns to the wardrobe (§9.9).
|
|
if (EquippedPhone)
|
|
{
|
|
if (Save)
|
|
Save->RemoveEquippedItem(EquippedPhone);
|
|
StoreItem(EquippedPhone);
|
|
}
|
|
|
|
UnstoreItem(Phone);
|
|
EquippedPhone = Phone;
|
|
if (Save)
|
|
Save->AddEquippedItem(Phone);
|
|
|
|
OnPhoneChanged.Broadcast(EquippedPhone);
|
|
OnWardrobeChanged.Broadcast();
|
|
}
|
|
|
|
void UInventorySubsystem::UnequipPhone(UPhoneItemInstance* Phone)
|
|
{
|
|
if (!Phone || EquippedPhone != Phone)
|
|
return;
|
|
|
|
if (UGlobalSaveGameData* Save = GetSave())
|
|
Save->RemoveEquippedItem(Phone);
|
|
|
|
EquippedPhone = nullptr;
|
|
StoreItem(Phone);
|
|
|
|
OnPhoneChanged.Broadcast(nullptr);
|
|
OnWardrobeChanged.Broadcast();
|
|
}
|
|
|
|
void UInventorySubsystem::EnsureHydrated()
|
|
{
|
|
UGlobalSaveGameData* Save = GetSave();
|
|
if (!Save)
|
|
return;
|
|
|
|
// Re-hydrate only when the underlying save object changes (fresh game / load game).
|
|
if (HydratedSave.Get() == Save)
|
|
return;
|
|
|
|
HydratedSave = Save;
|
|
WardrobeItems.Reset();
|
|
|
|
for (const FItemSaveRecord& Record : Save->GetWardrobeItems())
|
|
{
|
|
if (UItemInstance* Instance = UItemInstance::CreateFromRecord(this, Record))
|
|
WardrobeItems.Add(Instance);
|
|
}
|
|
|
|
// Only the phone is hydrated from the equipped bucket here; equipped clothing is owned and
|
|
// hydrated by the per-character UClothingManager. Pre-check the definition type so we don't
|
|
// mint throwaway clothing instances.
|
|
EquippedPhone = nullptr;
|
|
for (const FItemSaveRecord& Record : Save->GetEquippedItems())
|
|
{
|
|
const UItemDefinition* Definition = Record.Definition.LoadSynchronous();
|
|
if (!Definition || !Definition->IsA<UPhoneItemDefinition>())
|
|
continue;
|
|
|
|
if (UPhoneItemInstance* Phone = Cast<UPhoneItemInstance>(UItemInstance::CreateFromRecord(this, Record)))
|
|
{
|
|
EquippedPhone = Phone;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UInventorySubsystem::StoreItem(UItemInstance* Item)
|
|
{
|
|
if (!Item || WardrobeItems.Contains(Item))
|
|
return;
|
|
|
|
WardrobeItems.Add(Item);
|
|
if (UGlobalSaveGameData* Save = GetSave())
|
|
Save->AddWardrobeItem(Item);
|
|
}
|
|
|
|
void UInventorySubsystem::UnstoreItem(UItemInstance* Item)
|
|
{
|
|
if (!Item)
|
|
return;
|
|
|
|
WardrobeItems.Remove(Item);
|
|
if (UGlobalSaveGameData* Save = GetSave())
|
|
Save->RemoveWardrobeItem(Item);
|
|
}
|
|
|
|
UClothingManager* UInventorySubsystem::GetPlayerClothingManager() const
|
|
{
|
|
const ANakedDesireCharacter* Player = Cast<ANakedDesireCharacter>(UGameplayStatics::GetPlayerCharacter(GetGameInstance(), 0));
|
|
return Player ? Player->ClothingManager : nullptr;
|
|
}
|
|
|
|
UGlobalSaveGameData* UInventorySubsystem::GetSave() const
|
|
{
|
|
USaveSubsystem* SaveSubsystem = GetGameInstance() ? GetGameInstance()->GetSubsystem<USaveSubsystem>() : nullptr;
|
|
return SaveSubsystem ? SaveSubsystem->GetCurrentSave() : nullptr;
|
|
} |