Added wardrobe

This commit is contained in:
2026-05-31 21:00:55 +03:00
parent 49310a992b
commit f94dce1bfb
39 changed files with 787 additions and 111 deletions
+46 -29
View File
@@ -146,48 +146,65 @@ void UClothingManager::SpawnClothingPickup(UClothingItemInstance* ItemInstance)
void UClothingManager::PutOnClothing(UClothingItemInstance* ClothingItemInstance)
{
if (!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);
const UClothingItemDefinition* ClothingItem = ClothingItemInstance->GetClothingItemDefinition();
if (ClothingItem->SlotType == EClothingSlotType::Bodysuit)
{
DropClothing(EClothingSlotType::Top);
DropClothing(EClothingSlotType::Bottom);
DropClothing(EClothingSlotType::UnderwearTop);
DropClothing(EClothingSlotType::UnderwearBottom);
}
else if (ClothingItem->SlotType == EClothingSlotType::Top ||
ClothingItem->SlotType == EClothingSlotType::Bottom ||
ClothingItem->SlotType == EClothingSlotType::UnderwearTop ||
ClothingItem->SlotType == EClothingSlotType::UnderwearBottom)
{
DropClothing(EClothingSlotType::Bodysuit);
}
OnClothingEquip.Broadcast(ClothingItemInstance);
}
void UClothingManager::TakeClothing(UClothingItemInstance* ClothingItemInstance)
void UClothingManager::EquipSlot(UClothingItemInstance* ClothingItemInstance)
{
const EClothingSlotType SlotType = ClothingItemInstance->GetClothingItemDefinition()->SlotType;
if (EquippedClothing.Contains(SlotType))
{
DropClothing(SlotType);
}
SetClothingSlotItem(SlotType, 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))
@@ -41,7 +41,16 @@ public:
void DropClothing(const EClothingSlotType ClothingType);
bool IsClothingTypeOn(const EClothingSlotType ClothingType);
void TakeClothing(UClothingItemInstance* ClothingItemInstance);
// Equip an item arriving from off-body storage: writes the EquippedItems save record and
// applies it to its slot. Slot-occupant / bodysuit-exclusion handling is the caller's job
// (see UInventorySubsystem::EquipFromWardrobe), so displaced items can be routed deliberately.
void EquipSlot(UClothingItemInstance* ClothingItemInstance);
UClothingItemInstance* RemoveClothing(EClothingSlotType ClothingSlotType);
// Slots that must be vacated when SlotType is equipped, per the bodysuit exclusion rule (§6.5).
static TArray<EClothingSlotType> GetBodysuitExcludedSlots(EClothingSlotType SlotType);
void SetClothingSlotItem(const EClothingSlotType ClothingSlotType, UClothingItemInstance* ClothingItemInstance);
TArray<UClothingItemInstance*> GetEquippedClothing() const;
UClothingItemInstance* GetSlotClothing(EClothingSlotType SlotType);
+1 -1
View File
@@ -114,4 +114,4 @@ void ABed::ApplyOutline(bool bEnabled, int32 StencilValue)
MeshComponent->SetCustomDepthStencilValue(StencilValue);
}
#undef LOCTEXT_NAMESPACE
#undef LOCTEXT_NAMESPACE
+87 -20
View File
@@ -2,38 +2,105 @@
#include "Wardrobe.h"
#include "Components/BoxComponent.h"
#include "Components/WidgetComponent.h"
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Clothing/ClothingItemInstance.h"
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
#include "NakedDesire/SaveGame/ItemSaveRecord.h"
#include "NakedDesire/SaveGame/SaveSubsystem.h"
#include "NakedDesire/Global/NakedDesireHUD.h"
#include "NakedDesire/Inventory/InventorySubsystem.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/UI/GameLayoutWidget.h"
#define LOCTEXT_NAMESPACE "Wardrobe"
AWardrobe::AWardrobe()
{
ColliderComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("Collider"));
SetRootComponent(ColliderComponent);
// Trace-only: detected by the interaction LOS/focus line traces (ECC_Visibility),
// transparent to movement and physics.
ColliderComponent->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
ColliderComponent->SetCollisionObjectType(ECC_WorldStatic);
ColliderComponent->SetCollisionResponseToAllChannels(ECR_Ignore);
ColliderComponent->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
MeshComponent->SetupAttachment(RootComponent);
// Movement blocker only: stops the character capsule (ECC_Pawn), but is invisible
// to line traces so it never occludes the interaction LOS check.
MeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
MeshComponent->SetCollisionObjectType(ECC_WorldStatic);
MeshComponent->SetCollisionResponseToAllChannels(ECR_Ignore);
MeshComponent->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block);
InteractionHint = CreateDefaultSubobject<UWidgetComponent>(TEXT("Interaction Hint"));
InteractionHint->SetupAttachment(RootComponent);
}
void AWardrobe::Interact_Implementation(ANakedDesireCharacter* Player)
{
APlayerController* PC = Cast<APlayerController>(Player->GetController());
if (!PC)
return;
ANakedDesireHUD* HUD = Cast<ANakedDesireHUD>(PC->GetHUD());
if (!HUD)
return;
HUD->GetGameLayoutWidget()->OpenWardrobe();
}
void AWardrobe::AddItem(UClothingItemInstance* ClothingItemInstance)
{
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
SaveSubsystem->GetCurrentSave()->AddWardrobeItem(ClothingItemInstance);
if (UInventorySubsystem* Inventory = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<UInventorySubsystem>())
Inventory->AddToWardrobe(ClothingItemInstance);
}
void AWardrobe::RemoveItem(UClothingItemInstance* ClothingItemInstance) const
void AWardrobe::RemoveItem(UClothingItemInstance* ClothingItemInstance)
{
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
SaveSubsystem->GetCurrentSave()->RemoveWardrobeItem(ClothingItemInstance);
if (UInventorySubsystem* Inventory = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<UInventorySubsystem>())
Inventory->RemoveFromWardrobe(ClothingItemInstance);
}
void AWardrobe::BeginPlay()
{
Super::BeginPlay();
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
UGlobalSaveGameData* SaveGame = SaveSubsystem->GetCurrentSave();
for (const FItemSaveRecord& ItemSaveRecord : SaveGame->GetWardrobeItems())
{
UClothingItemInstance* NewItemInstance = Cast<UClothingItemInstance>(UItemInstance::CreateFromRecord(this, ItemSaveRecord));
if (!NewItemInstance)
continue;
ClothingItems.Push(NewItemInstance);
}
InteractionHint->SetVisibility(false);
}
bool AWardrobe::CanInteract_Implementation(ANakedDesireCharacter* Player) const
{
return true;
}
FText AWardrobe::GetInteractionPrompt_Implementation() const
{
return LOCTEXT("WardrobePrompt", "Open wardrobe");
}
void AWardrobe::HideInteractionHint_Implementation()
{
ApplyOutline(false, 0);
InteractionHint->SetVisibility(false);
}
void AWardrobe::ShowInteractionFocusHint_Implementation()
{
ApplyOutline(true, 2);
InteractionHint->SetVisibility(true);
}
void AWardrobe::ShowInteractionProximityHint_Implementation()
{
ApplyOutline(true, 1);
InteractionHint->SetVisibility(false);
}
void AWardrobe::ApplyOutline(bool bEnabled, int32 StencilValue)
{
MeshComponent->SetRenderCustomDepth(bEnabled);
MeshComponent->SetCustomDepthStencilValue(StencilValue);
}
#undef LOCTEXT_NAMESPACE
+29 -6
View File
@@ -4,22 +4,45 @@
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "NakedDesire/Interaction/Interactable.h"
#include "Wardrobe.generated.h"
class UBoxComponent;
class UWidgetComponent;
class UClothingItemInstance;
// Apartment wardrobe fixture. The stored items live in UInventorySubsystem (the durable store is
// UGlobalSaveGameData::WardrobeItems); this actor is just the interaction point and forwards to it.
UCLASS(Blueprintable)
class NAKEDDESIRE_API AWardrobe : public AActor
class NAKEDDESIRE_API AWardrobe : public AActor, public IInteractable
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Instanced)
TArray<TObjectPtr<UClothingItemInstance>> ClothingItems;
AWardrobe();
virtual void Interact_Implementation(ANakedDesireCharacter* Player) override;
virtual bool CanInteract_Implementation(ANakedDesireCharacter* Player) const override;
virtual FText GetInteractionPrompt_Implementation() const override;
virtual void HideInteractionHint_Implementation() override;
virtual void ShowInteractionFocusHint_Implementation() override;
virtual void ShowInteractionProximityHint_Implementation() override;
void AddItem(UClothingItemInstance* ClothingItemInstance);
void RemoveItem(UClothingItemInstance* ClothingItemInstance) const;
void RemoveItem(UClothingItemInstance* ClothingItemInstance);
protected:
virtual void BeginPlay() override;
};
private:
void ApplyOutline(bool bEnabled, int32 StencilValue);
UPROPERTY(EditDefaultsOnly)
TObjectPtr<UStaticMeshComponent> MeshComponent;
UPROPERTY(EditDefaultsOnly)
TObjectPtr<UBoxComponent> ColliderComponent;
UPROPERTY(EditDefaultsOnly)
TObjectPtr<UWidgetComponent> InteractionHint;
};
@@ -0,0 +1,143 @@
// © 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/ItemInstance.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;
}
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();
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();
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::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);
}
}
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;
}
@@ -0,0 +1,62 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "InventorySubsystem.generated.h"
class UItemInstance;
class UClothingManager;
class UGlobalSaveGameData;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnWardrobeChangedSignature);
/**
* Runtime owner of the off-body item store (the apartment wardrobe, GDD §6.5 / §10.4).
*
* Holds live UItemInstance objects mirrored from UGlobalSaveGameData::WardrobeItems and is
* the single entry point for moving items between the wardrobe and the body. Equipped items
* stay owned by the per-character UClothingManager (it remains the body-state authority and
* keeps writing the EquippedItems bucket); this subsystem orchestrates the wardrobe<->equipped
* transfer so the two save buckets can never drift. Mutations broadcast OnWardrobeChanged so
* the wardrobe UI can refresh without polling.
*/
UCLASS()
class NAKEDDESIRE_API UInventorySubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable)
FOnWardrobeChangedSignature OnWardrobeChanged;
/** Live wardrobe instances (off-body, mirrored from the save). */
const TArray<TObjectPtr<UItemInstance>>& GetWardrobeItems();
/** Bring an item into the wardrobe (purchase, world-return). */
void AddToWardrobe(UItemInstance* Item);
/** Remove an item from the wardrobe without equipping it (discard). */
void RemoveFromWardrobe(UItemInstance* Item);
/** Move a stored item onto the body; any displaced garment returns to the wardrobe. */
void EquipFromWardrobe(UItemInstance* Item);
/** Take an equipped garment off the body and store it back in the wardrobe. */
void UnequipToWardrobe(UItemInstance* Item);
private:
void EnsureHydrated();
void StoreItem(UItemInstance* Item); // live list + save bucket, no broadcast
void UnstoreItem(UItemInstance* Item); // live list + save bucket, no broadcast
UClothingManager* GetPlayerClothingManager() const;
UGlobalSaveGameData* GetSave() const;
UPROPERTY()
TArray<TObjectPtr<UItemInstance>> WardrobeItems;
// The save the live list was built from; re-hydrate when this changes (e.g. load game).
TWeakObjectPtr<UGlobalSaveGameData> HydratedSave;
};
@@ -1,8 +1,14 @@
#include "GameLayoutWidget.h"
#include "Widgets/CommonActivatableWidgetContainer.h"
#include "Inventory/InventoryScreenWidget.h"
#include "Inventory/Wardrobe/WardrobeScreenWidget.h"
void UGameLayoutWidget::OpenInventory()
{
InventoryScreenWidget = WidgetStack->AddWidget<UInventoryScreenWidget>(InventoryScreenWidgetClass);
}
void UGameLayoutWidget::OpenWardrobe()
{
WardrobeScreenWidget = WidgetStack->AddWidget<UWardrobeScreenWidget>(WardrobeScreenWidgetClass);
}
+8
View File
@@ -4,6 +4,7 @@
#include "CommonUserWidget.h"
#include "GameLayoutWidget.generated.h"
class UWardrobeScreenWidget;
class UInventoryScreenWidget;
class UHUDWidget;
class UCommonActivatableWidgetStack;
@@ -25,6 +26,13 @@ class NAKEDDESIRE_API UGameLayoutWidget : public UCommonUserWidget
UPROPERTY()
TObjectPtr<UInventoryScreenWidget> InventoryScreenWidget;
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UWardrobeScreenWidget> WardrobeScreenWidgetClass;
UPROPERTY()
TObjectPtr<UWardrobeScreenWidget> WardrobeScreenWidget;
public:
void OpenInventory();
void OpenWardrobe();
};
@@ -0,0 +1,48 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "EquipmentSlotMenuWidget.h"
#include "Components/Button.h"
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Clothing/ClothingItemInstance.h"
#include "NakedDesire/Clothing/ClothingManager.h"
#include "NakedDesire/Inventory/InventorySubsystem.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
void UEquipmentSlotMenuWidget::Init(const EClothingSlotType InSlotType, const bool bInAtWardrobe)
{
SlotType = InSlotType;
bAtWardrobe = bInAtWardrobe;
}
void UEquipmentSlotMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
Player = Cast<ANakedDesireCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
RemoveClothingBtn->OnClicked.AddUniqueDynamic(this, &UEquipmentSlotMenuWidget::OnRemoveClothingClicked);
}
void UEquipmentSlotMenuWidget::OnRemoveClothingClicked()
{
if (Player && Player->ClothingManager)
{
if (bAtWardrobe)
{
// At the wardrobe the garment is stored, not dropped on the ground.
if (UClothingItemInstance* Equipped = Player->ClothingManager->GetSlotClothing(SlotType))
{
if (UInventorySubsystem* Inventory = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<UInventorySubsystem>())
Inventory->UnequipToWardrobe(Equipped);
}
}
else
{
Player->ClothingManager->DropClothing(SlotType);
}
}
OnActionPerformed.Broadcast();
}
@@ -14,9 +14,10 @@ UCLASS(Abstract)
class NAKEDDESIRE_API UEquipmentSlotMenuWidget : public UCommonUserWidget
{
GENERATED_BODY()
public:
void Init(EClothingSlotType InSlotType);
// bInAtWardrobe makes Remove store the garment in the wardrobe instead of dropping it (§6.5).
void Init(EClothingSlotType InSlotType, bool bInAtWardrobe = false);
EClothingSlotType GetSlotType() const { return SlotType; }
@@ -28,7 +29,7 @@ protected:
private:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> DropClothingBtn;
TObjectPtr<UButton> RemoveClothingBtn;
UPROPERTY()
TObjectPtr<ANakedDesireCharacter> Player;
@@ -36,6 +37,8 @@ private:
UPROPERTY()
EClothingSlotType SlotType;
bool bAtWardrobe = false;
UFUNCTION()
void OnDropClothingClicked();
};
void OnRemoveClothingClicked();
};
@@ -1,29 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "EquipmentSlotMenuWidget.h"
#include "Components/Button.h"
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Clothing/ClothingManager.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
void UEquipmentSlotMenuWidget::Init(const EClothingSlotType InSlotType)
{
SlotType = InSlotType;
}
void UEquipmentSlotMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
Player = Cast<ANakedDesireCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
DropClothingBtn->OnClicked.AddUniqueDynamic(this, &UEquipmentSlotMenuWidget::OnDropClothingClicked);
}
void UEquipmentSlotMenuWidget::OnDropClothingClicked()
{
Player->ClothingManager->DropClothing(SlotType);
OnActionPerformed.Broadcast();
}
@@ -3,9 +3,9 @@
#include "Components/Button.h"
#include "Components/CanvasPanel.h"
#include "Components/CanvasPanelSlot.h"
#include "EquipmentPanelWidget.h"
#include "EquipmentSlotMenuWidget.h"
#include "EquipmentSlotWidget.h"
#include "Equipment/EquipmentPanelWidget.h"
#include "Equipment/EquipmentSlotMenuWidget.h"
#include "Equipment/EquipmentSlotWidget.h"
#include "NakedDesire/Global/PlayerPreviewCaptureSubsystem.h"
void UInventoryScreenWidget::NativeOnActivated()
@@ -0,0 +1,71 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "WardrobeInventoryWidget.h"
#include "Components/VerticalBox.h"
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Clothing/ClothingItemInstance.h"
#include "NakedDesire/Inventory/InventorySubsystem.h"
#include "WardrobeItemWidget.h"
void UWardrobeInventoryWidget::Init()
{
if (UInventorySubsystem* Inventory = GetInventory())
Inventory->OnWardrobeChanged.AddUniqueDynamic(this, &UWardrobeInventoryWidget::HandleWardrobeChanged);
RenderItems();
}
void UWardrobeInventoryWidget::NativeDestruct()
{
if (UInventorySubsystem* Inventory = GetInventory())
Inventory->OnWardrobeChanged.RemoveDynamic(this, &UWardrobeInventoryWidget::HandleWardrobeChanged);
Super::NativeDestruct();
}
UInventorySubsystem* UWardrobeInventoryWidget::GetInventory() const
{
UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(GetWorld());
return GameInstance ? GameInstance->GetSubsystem<UInventorySubsystem>() : nullptr;
}
void UWardrobeInventoryWidget::HandleWardrobeChanged()
{
RenderItems();
}
void UWardrobeInventoryWidget::HandleItemClicked(UWardrobeItemWidget* ItemWidget)
{
if (!ItemWidget)
return;
if (UInventorySubsystem* Inventory = GetInventory())
Inventory->EquipFromWardrobe(ItemWidget->GetClothingItemInstance());
}
void UWardrobeInventoryWidget::RenderItems()
{
if (!ItemsList || !WardrobeItemWidgetClass)
return;
UInventorySubsystem* Inventory = GetInventory();
if (!Inventory)
return;
ItemsList->ClearChildren();
for (UItemInstance* Item : Inventory->GetWardrobeItems())
{
// Wardrobe holds any UItemInstance (phones / toys land here too, §6.5 / §9.9); the
// clothing widget only renders clothing for now — other types get their own widgets later.
UClothingItemInstance* Clothing = Cast<UClothingItemInstance>(Item);
if (!Clothing)
continue;
UWardrobeItemWidget* NewItemWidget = CreateWidget<UWardrobeItemWidget>(this, WardrobeItemWidgetClass);
NewItemWidget->SetClothingItemInstance(Clothing);
NewItemWidget->OnItemClicked.BindUObject(this, &UWardrobeInventoryWidget::HandleItemClicked);
ItemsList->AddChild(NewItemWidget);
}
}
@@ -0,0 +1,38 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommonUserWidget.h"
#include "WardrobeInventoryWidget.generated.h"
class UVerticalBox;
class UWardrobeItemWidget;
class UInventorySubsystem;
UCLASS(Abstract)
class NAKEDDESIRE_API UWardrobeInventoryWidget : public UCommonUserWidget
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly)
TSubclassOf<UWardrobeItemWidget> WardrobeItemWidgetClass;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UVerticalBox> ItemsList;
public:
void Init();
protected:
virtual void NativeDestruct() override;
private:
UInventorySubsystem* GetInventory() const;
UFUNCTION()
void HandleWardrobeChanged();
void HandleItemClicked(UWardrobeItemWidget* ItemWidget);
void RenderItems();
};
@@ -0,0 +1,30 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "WardrobeItemWidget.h"
#include "CommonTextBlock.h"
#include "Components/Image.h"
#include "NakedDesire/Clothing/ClothingItemInstance.h"
void UWardrobeItemWidget::SetClothingItemInstance(UClothingItemInstance* InItemInstance)
{
ClothingItemInstance = InItemInstance;
UpdateVisuals();
}
void UWardrobeItemWidget::NativeOnClicked()
{
Super::NativeOnClicked();
OnItemClicked.ExecuteIfBound(this);
}
void UWardrobeItemWidget::UpdateVisuals()
{
if (!ClothingItemInstance)
return;
IconImage->SetBrushFromTexture(ClothingItemInstance->GetClothingItemDefinition()->Icon);
NameText->SetText(ClothingItemInstance->GetClothingItemDefinition()->Name);
}
@@ -0,0 +1,39 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommonButtonBase.h"
#include "WardrobeItemWidget.generated.h"
class UCommonTextBlock;
class UClothingItemInstance;
class UImage;
UCLASS(Abstract)
class NAKEDDESIRE_API UWardrobeItemWidget : public UCommonButtonBase
{
GENERATED_BODY()
UPROPERTY()
TObjectPtr<UClothingItemInstance> ClothingItemInstance;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UImage> IconImage;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UCommonTextBlock> NameText;
public:
void SetClothingItemInstance(UClothingItemInstance* InItemInstance);
UClothingItemInstance* GetClothingItemInstance() const { return ClothingItemInstance; }
DECLARE_DELEGATE_OneParam(FOnWardrobeItemClickedSignature, UWardrobeItemWidget* ItemWidget)
FOnWardrobeItemClickedSignature OnItemClicked;
protected:
virtual void NativeOnClicked() override;
private:
void UpdateVisuals();
};
@@ -0,0 +1,83 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "WardrobeScreenWidget.h"
#include "Components/Button.h"
#include "Components/CanvasPanel.h"
#include "Components/CanvasPanelSlot.h"
#include "NakedDesire/UI/Inventory/Equipment/EquipmentPanelWidget.h"
#include "NakedDesire/UI/Inventory/Equipment/EquipmentSlotMenuWidget.h"
#include "NakedDesire/UI/Inventory/Equipment/EquipmentSlotWidget.h"
#include "WardrobeInventoryWidget.h"
void UWardrobeScreenWidget::NativeOnActivated()
{
Super::NativeOnActivated();
if (WardrobeInventory)
WardrobeInventory->Init();
if (!EquipmentSlotMenuWidget && EquipmentSlotMenuWidgetClass)
{
// Host the slot menu in the same canvas as the blocker, matching UInventoryScreenWidget.
if (UCanvasPanel* MenuCanvas = Cast<UCanvasPanel>(MenuBlocker->GetParent()))
{
EquipmentSlotMenuWidget = CreateWidget<UEquipmentSlotMenuWidget>(this, EquipmentSlotMenuWidgetClass);
if (UCanvasPanelSlot* CanvasSlot = Cast<UCanvasPanelSlot>(MenuCanvas->AddChild(EquipmentSlotMenuWidget)))
{
CanvasSlot->SetAutoSize(true);
CanvasSlot->SetAnchors(FAnchors(0.f, 0.f));
CanvasSlot->SetZOrder(1);
}
EquipmentSlotMenuWidget->OnActionPerformed.AddUObject(this, &UWardrobeScreenWidget::CloseMenu);
}
if (EquipmentPanel)
EquipmentPanel->OnSlotClicked.BindUObject(this, &UWardrobeScreenWidget::HandleSlotClicked);
if (MenuBlocker)
MenuBlocker->OnClicked.AddUniqueDynamic(this, &UWardrobeScreenWidget::CloseMenu);
}
CloseMenu();
}
void UWardrobeScreenWidget::HandleSlotClicked(UEquipmentSlotWidget* SlotWidget)
{
if (!EquipmentSlotMenuWidget || !SlotWidget)
return;
const bool bMenuOpen = EquipmentSlotMenuWidget->GetVisibility() != ESlateVisibility::Collapsed;
if (bMenuOpen && EquipmentSlotMenuWidget->GetSlotType() == SlotWidget->GetSlotType())
{
CloseMenu();
return;
}
// At the wardrobe the "remove" action stores the garment instead of dropping it on the ground.
EquipmentSlotMenuWidget->Init(SlotWidget->GetSlotType(), /*bAtWardrobe=*/true);
if (UCanvasPanelSlot* MenuSlot = Cast<UCanvasPanelSlot>(EquipmentSlotMenuWidget->Slot))
{
if (UCanvasPanel* MenuCanvas = Cast<UCanvasPanel>(MenuBlocker->GetParent()))
{
const FGeometry& SlotGeom = SlotWidget->GetCachedGeometry();
const FVector2D AbsPos = SlotGeom.GetAbsolutePosition() + FVector2D(SlotGeom.GetAbsoluteSize().X, 0.f);
MenuSlot->SetPosition(MenuCanvas->GetCachedGeometry().AbsoluteToLocal(AbsPos));
}
}
MenuBlocker->SetVisibility(ESlateVisibility::Visible);
EquipmentSlotMenuWidget->SetVisibility(ESlateVisibility::Visible);
}
void UWardrobeScreenWidget::CloseMenu()
{
if (EquipmentSlotMenuWidget)
EquipmentSlotMenuWidget->SetVisibility(ESlateVisibility::Collapsed);
if (MenuBlocker)
MenuBlocker->SetVisibility(ESlateVisibility::Collapsed);
}
@@ -0,0 +1,43 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommonActivatableWidget.h"
#include "WardrobeScreenWidget.generated.h"
class UButton;
class UWardrobeInventoryWidget;
class UEquipmentPanelWidget;
class UEquipmentSlotMenuWidget;
class UEquipmentSlotWidget;
UCLASS(Abstract)
class NAKEDDESIRE_API UWardrobeScreenWidget : public UCommonActivatableWidget
{
GENERATED_BODY()
protected:
virtual void NativeOnActivated() override;
private:
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UEquipmentSlotMenuWidget> EquipmentSlotMenuWidgetClass;
UPROPERTY()
TObjectPtr<UEquipmentSlotMenuWidget> EquipmentSlotMenuWidget;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UEquipmentPanelWidget> EquipmentPanel;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UWardrobeInventoryWidget> WardrobeInventory;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> MenuBlocker;
void HandleSlotClicked(UEquipmentSlotWidget* SlotWidget);
UFUNCTION()
void CloseMenu();
};