Added wardrobe

This commit is contained in:
2026-05-31 21:00:55 +03:00
parent a0c91c81fa
commit 9a57f87d02
39 changed files with 787 additions and 111 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -96,7 +96,7 @@ State of the C++ module as of the latest pass. File references use `Source/Naked
- **Session manager (GDD §4.1–§4.4)** — `Global/SessionManagerSubsystem.h/.cpp` (`UWorldSubsystem`). Tracks `bSessionActive`, emits `OnSessionStart` / `OnSessionEnd(ESessionLossCause)`; `ESessionLossCause = { SafeReturn, EmbarrassmentMax, EnergyZero, PoliceCapture }`. Apartment `ALocationTrigger` starts a session on exit and safely ends it on re-entry. Subscribes (next-tick after world begin play) to `UStatsManager::EmbarrassmentUpdate` (max-hit → `EmbarrassmentMax`) and `EnergyUpdate` (≤0 → `EnergyZero`). Exposes `bPoliceChaseActive` (+ setter / getter) for the §4.4 loss-precedence rule the resolver owns. Replaces the old `EndGameEmbarrassed` GameMode BP call, which `UStatsManager::IncreaseEmbarrassment` no longer invokes. The `EndGameEmbarrassed` BlueprintImplementableEvent declaration still exists on `ANakedDesireGameMode` but is now dead from C++ and should be removed once BP no longer references it.
- **Session loss resolver (GDD §4.4)** — `Global/SessionLossResolver.h/.cpp` (`UWorldSubsystem`). Single entry point `ResolveLoss(ESessionLossCause)`, bound to `USessionManagerSubsystem::OnSessionEnd`. Applies the police-chase precedence override (any cause → `PoliceCapture` while `bPoliceChaseActive`), then per cause: `EmbarrassmentMax` no-cost; `EnergyZero` destroys every world `AItemPickup` + clears its save record (guaranteed sleep loss); `PoliceCapture` deducts `PoliceCaptureMoneyPenalty` if affordable else flags a holding-cell outcome; `SafeReturn` no loss. Never strips equipped clothing. Autosaves, then broadcasts `OnSessionLossResolved(FinalCause, bWentToHoldingCell)` for the BP presentation / time-skip layer. See §1.3 for the pieces still delegated to BP / later phases.
- **Movement** — `EnhancedInput`, walk / run / crouch (`NakedDesireCharacter.cpp:115-127`), stamina-gated run (`Tick` lines 91-113).
- **Wardrobe interactable** — `Interactables/Wardrobe.h` holds `TArray<TObjectPtr<UClothingItemInstance>> ClothingItems`; `NakedDesireGameMode::BuyItem` charges money and pushes into the wardrobe.
- **Wardrobe storage + management** — `Inventory/InventorySubsystem` (`UGameInstanceSubsystem`) is the single runtime owner of the off-body store. It holds live `UItemInstance`s mirrored from `UGlobalSaveGameData::WardrobeItems` and exposes the atomic moves `AddToWardrobe` / `RemoveFromWardrobe` / `EquipFromWardrobe` / `UnequipToWardrobe`, each mutating the wardrobe + equipped save buckets together and broadcasting `OnWardrobeChanged`. `AClothingManager` stays the body-state authority (owns the `EquippedItems` bucket via the new `EquipSlot` + existing `RemoveClothing`); the bodysuit exclusion rule is now the shared static `UClothingManager::GetBodysuitExcludedSlots` and routes displaced garments back to the wardrobe instead of dropping them to the world. `AWardrobe` is reduced to an interaction shell that forwards to the subsystem (its stale `ClothingItems` array is gone). UI: `WardrobeScreenWidget` inits the inventory list and hosts the `EquipmentSlotMenuWidget` popup (same plumbing as `InventoryScreenWidget`); `WardrobeInventoryWidget` renders the live list and re-renders on `OnWardrobeChanged`; clicking a wardrobe item calls `EquipFromWardrobe`. The slot menu has a single Remove button whose `Init(slot, bAtWardrobe)` flag decides the action — store via `UnequipToWardrobe` when opened at the wardrobe, drop to the world otherwise. **Follow-ups:** `BuyItem` buy-flow not reattached (no caller yet); world pickup (`ItemPickup``TakeClothing`) still doesn't clear the `WorldItems` record; non-clothing wardrobe items (phones/toys) are stored but not yet rendered.
### 1.2 Partially implemented (deviates from GDD)
+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();
};