Files
2026-06-03 15:17:02 +03:00

240 lines
7.2 KiB
C++

// © 2025 Naked People Team. All Rights Reserved.
#include "ClothingManager.h"
#include "ClothingItemDefinition.h"
#include "ClothingItemInstance.h"
#include "GameFramework/Character.h"
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Interactables/ItemPickup.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
#include "NakedDesire/SaveGame/ItemSaveRecord.h"
#include "NakedDesire/SaveGame/SaveSubsystem.h"
UClothingManager::UClothingManager()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UClothingManager::BeginPlay()
{
Super::BeginPlay();
HydrateClothing();
}
void UClothingManager::SetClothingSlotItem(const EClothingSlotType ClothingSlotType, UClothingItemInstance* ClothingItemInstance)
{
if (ClothingItemInstance)
EquippedClothing.Add(ClothingSlotType, ClothingItemInstance);
else
EquippedClothing.Remove(ClothingSlotType);
}
TArray<UClothingItemInstance*> UClothingManager::GetEquippedClothing() const
{
TArray<TObjectPtr<UClothingItemInstance>> Items;
EquippedClothing.GenerateValueArray(Items);
return Items;
}
UClothingItemInstance* UClothingManager::GetSlotClothing(const EClothingSlotType SlotType)
{
if (EquippedClothing.Contains(SlotType))
return EquippedClothing[SlotType];
return nullptr;
}
bool UClothingManager::IsBodyPartExposed(const EBodyPart BodyPart)
{
for (const auto& [Key, Value] : EquippedClothing)
{
if (Value->GetClothingItemDefinition()->HiddenBodyParts.Contains(BodyPart))
return false;
}
return true;
}
float UClothingManager::GetEffectiveCoverage(const EBodyPart BodyPart)
{
// GDD §6.3.2: effective coverage of a part = max() across garments that include it.
// A garment that doesn't cover the part contributes nothing; nothing covering it = 0.
float MaxCoverage = 0.0f;
for (const auto& [Slot, Instance] : EquippedClothing)
{
if (!Instance)
continue;
const UClothingItemDefinition* Definition = Instance->GetClothingItemDefinition();
if (!Definition)
continue;
for (const FBodyPartCoverage& Coverage : Definition->CoveredBodyParts)
{
if (Coverage.BodyPart == BodyPart)
MaxCoverage = FMath::Max(MaxCoverage, Coverage.Coverage);
}
}
return MaxCoverage;
}
float UClothingManager::GetBoobsSupport()
{
const UClothingItemInstance* UnderwearTop = GetSlotClothing(EClothingSlotType::UnderwearTop);
const UClothingItemInstance* Top = GetSlotClothing(EClothingSlotType::Top);
const UClothingItemInstance* Bodysuit = GetSlotClothing(EClothingSlotType::Bodysuit);
TArray<float> TotalSupports;
if (UnderwearTop)
TotalSupports.Push(UnderwearTop->GetClothingItemDefinition()->BoobsSupport);
if (Top)
TotalSupports.Push(Top->GetClothingItemDefinition()->BoobsSupport);
if (Bodysuit)
TotalSupports.Push(Bodysuit->GetClothingItemDefinition()->BoobsSupport);
return FMath::Max(TotalSupports);
}
void UClothingManager::HydrateClothing()
{
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
for (const FItemSaveRecord& ItemSaveRecord : SaveSubsystem->GetCurrentSave()->GetEquippedItems())
{
UClothingItemInstance* ClothingItemInstance = Cast<UClothingItemInstance>(UItemInstance::CreateFromRecord(this, ItemSaveRecord));
if (!ClothingItemInstance)
continue;
PutOnClothing(ClothingItemInstance);
}
}
float UClothingManager::GetHeelHeight()
{
if (!EquippedClothing.Contains(EClothingSlotType::Footwear))
return 0;
const UClothingItemInstance* Footwear = EquippedClothing[EClothingSlotType::Footwear];
return Footwear->GetClothingItemDefinition()->ShoesOffset;
}
void UClothingManager::SpawnClothingPickup(UClothingItemInstance* ItemInstance)
{
if (!ItemPickupActor)
{
UE_LOG(LogTemp, Warning, TEXT("UClothingManager::SpawnClothingPickup ItemPickupActor is not set"));
return;
}
AItemPickup* NewItemPickup = GetWorld()->SpawnActor<AItemPickup>(ItemPickupActor, GetOwner()->GetActorTransform());
if (!NewItemPickup)
{
UE_LOG(LogTemp, Warning, TEXT("UClothingManager::SpawnClothingPickup NewItemPickup == nullptr"));
return;
}
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
SaveSubsystem->GetCurrentSave()->AddWorldItem(ItemInstance, NewItemPickup->GetActorTransform());
NewItemPickup->SetItem(ItemInstance);
}
void UClothingManager::PutOnClothing(UClothingItemInstance* ClothingItemInstance)
{
if (!ClothingItemInstance || !ClothingItemInstance->GetClothingItemDefinition())
return;
// Pure apply-to-slot + notify. Exclusion / occupant handling lives with the caller, and the
// EquippedItems save record is written by EquipSlot — hydration reuses this without re-adding.
const EClothingSlotType ClothingSlotType = ClothingItemInstance->GetClothingItemDefinition()->SlotType;
SetClothingSlotItem(ClothingSlotType, ClothingItemInstance);
OnClothingEquip.Broadcast(ClothingItemInstance);
}
void UClothingManager::EquipSlot(UClothingItemInstance* ClothingItemInstance)
{
if (!ClothingItemInstance)
return;
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
SaveSubsystem->GetCurrentSave()->AddEquippedItem(ClothingItemInstance);
PutOnClothing(ClothingItemInstance);
}
void UClothingManager::TakeClothing(UClothingItemInstance* ClothingItemInstance)
{
if (!ClothingItemInstance || !ClothingItemInstance->GetClothingItemDefinition())
return;
const EClothingSlotType SlotType = ClothingItemInstance->GetClothingItemDefinition()->SlotType;
// World-pickup path: a displaced garment swaps back out to the world (no wardrobe nearby).
if (IsClothingTypeOn(SlotType))
DropClothing(SlotType);
for (const EClothingSlotType ExcludedSlot : GetBodysuitExcludedSlots(SlotType))
{
if (IsClothingTypeOn(ExcludedSlot))
DropClothing(ExcludedSlot);
}
EquipSlot(ClothingItemInstance);
}
TArray<EClothingSlotType> UClothingManager::GetBodysuitExcludedSlots(const EClothingSlotType SlotType)
{
switch (SlotType)
{
case EClothingSlotType::Bodysuit:
return { EClothingSlotType::Top, EClothingSlotType::Bottom,
EClothingSlotType::UnderwearTop, EClothingSlotType::UnderwearBottom };
case EClothingSlotType::Top:
case EClothingSlotType::Bottom:
case EClothingSlotType::UnderwearTop:
case EClothingSlotType::UnderwearBottom:
return { EClothingSlotType::Bodysuit };
default:
return {};
}
}
UClothingItemInstance* UClothingManager::RemoveClothing(const EClothingSlotType ClothingSlotType)
{
if (!EquippedClothing.Contains(ClothingSlotType))
return nullptr;
UClothingItemInstance* ExistingItem = EquippedClothing[ClothingSlotType];
SetClothingSlotItem(ClothingSlotType, nullptr);
OnClothingUnequip.Broadcast(ExistingItem);
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
SaveSubsystem->GetCurrentSave()->RemoveEquippedItem(ExistingItem);
return ExistingItem;
}
void UClothingManager::DropClothing(const EClothingSlotType ClothingType)
{
UClothingItemInstance* ClothingItemInstance = RemoveClothing(ClothingType);
if (!ClothingItemInstance)
return;
SpawnClothingPickup(ClothingItemInstance);
OnClothingDropped.Broadcast(ClothingItemInstance);
}
bool UClothingManager::IsClothingTypeOn(const EClothingSlotType ClothingType)
{
return EquippedClothing.Contains(ClothingType);
}