Added wardrobe
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -114,4 +114,4 @@ void ABed::ApplyOutline(bool bEnabled, int32 StencilValue)
|
||||
MeshComponent->SetCustomDepthStencilValue(StencilValue);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
+8
-5
@@ -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();
|
||||
};
|
||||
Reference in New Issue
Block a user