diff --git a/Content/Blueprints/Interactables/A_Bed.uasset b/Content/Blueprints/Interactables/A_Bed.uasset new file mode 100644 index 00000000..31315472 --- /dev/null +++ b/Content/Blueprints/Interactables/A_Bed.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f774a807c5edcef15d08ba52ecb606e3eb094aeef43c04a5e2cae14f84a54536 +size 44298 diff --git a/Content/Blueprints/Interactables/A_Wardrobe.uasset b/Content/Blueprints/Interactables/A_Wardrobe.uasset new file mode 100644 index 00000000..08adc4c7 --- /dev/null +++ b/Content/Blueprints/Interactables/A_Wardrobe.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93aa5bb3a0a46749262f0d4919939865e4f3ad99aea2070975b14b7134470830 +size 34184 diff --git a/Content/Blueprints/Interactables/BP_Bed.uasset b/Content/Blueprints/Interactables/BP_Bed.uasset index 71e2c49b..48213ff5 100644 --- a/Content/Blueprints/Interactables/BP_Bed.uasset +++ b/Content/Blueprints/Interactables/BP_Bed.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d284b6693633c77db12cc80fd59b76214471b6b7975e022ecaeefe786bcf8688 -size 46385 +oid sha256:a4d67a42e2690bab2c4bf542106e26574cde9b8ae5c09a7dd34b5c2f4a4df2ca +size 2255 diff --git a/Content/Test/Maps/TestLevel.umap b/Content/Test/Maps/TestLevel.umap index 1c3c4798..2a80bf38 100644 --- a/Content/Test/Maps/TestLevel.umap +++ b/Content/Test/Maps/TestLevel.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78fe38c31993d33c69adab7bc9bccf7932265013380babb69f0d9b4856d708b3 -size 147142 +oid sha256:7ab375fb9c0f53f57aaa96223898f268cbf8fae54f2b23010322466a179070cb +size 149095 diff --git a/Content/UI/Inventory/Equipment/WBP_EquipmentPanel.uasset b/Content/UI/Inventory/Equipment/WBP_EquipmentPanel.uasset new file mode 100644 index 00000000..afe6569c --- /dev/null +++ b/Content/UI/Inventory/Equipment/WBP_EquipmentPanel.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccb297e41da4b0c5350f43f5aefbca0700a0d7ee5c441f9d9e96c54bb2fa4db2 +size 50134 diff --git a/Content/UI/Inventory/Equipment/WBP_EquipmentSlot.uasset b/Content/UI/Inventory/Equipment/WBP_EquipmentSlot.uasset new file mode 100644 index 00000000..b67d6ede --- /dev/null +++ b/Content/UI/Inventory/Equipment/WBP_EquipmentSlot.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d2473ba48ebdb469bdd5f8490dfb9d78f3085df63a5dbf5e14427eeaddfa8c0 +size 31194 diff --git a/Content/UI/Inventory/Equipment/WBP_EquipmentSlotMenu.uasset b/Content/UI/Inventory/Equipment/WBP_EquipmentSlotMenu.uasset new file mode 100644 index 00000000..58195a07 --- /dev/null +++ b/Content/UI/Inventory/Equipment/WBP_EquipmentSlotMenu.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84eca3e5ec9f402ad41fa967a2c2e7053abaddf3a89559c9efa15966149e3103 +size 26299 diff --git a/Content/UI/Inventory/WBP_EquipmentPanel.uasset b/Content/UI/Inventory/WBP_EquipmentPanel.uasset deleted file mode 100644 index d679c263..00000000 --- a/Content/UI/Inventory/WBP_EquipmentPanel.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5575ecd7954a8f9eea4d4b61bcb227e1c81124757c4fdf14a1e96ef2e0ad21e7 -size 40159 diff --git a/Content/UI/Inventory/WBP_EquipmentSlot.uasset b/Content/UI/Inventory/WBP_EquipmentSlot.uasset deleted file mode 100644 index 4f157bb1..00000000 --- a/Content/UI/Inventory/WBP_EquipmentSlot.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0e8d0cefcbe9c96566ef815c52b4863c4d87ef3bc1769e218fd5ef4c85a4aeb -size 31164 diff --git a/Content/UI/Inventory/WBP_EquipmentSlotMenu.uasset b/Content/UI/Inventory/WBP_EquipmentSlotMenu.uasset deleted file mode 100644 index f05e985f..00000000 --- a/Content/UI/Inventory/WBP_EquipmentSlotMenu.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a88257733d15c35c7010558873cd8d502865939cbd2838e71a60a9e0803cec0d -size 26925 diff --git a/Content/UI/Inventory/WBP_InventoryScreen.uasset b/Content/UI/Inventory/WBP_InventoryScreen.uasset index 31dc5bb2..06ec00c3 100644 --- a/Content/UI/Inventory/WBP_InventoryScreen.uasset +++ b/Content/UI/Inventory/WBP_InventoryScreen.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22ba480da96f627664d35b9efb4a94e4645cd2fc072332191b6e3fbf299d1fc1 -size 33793 +oid sha256:e26e5fca1f309448161ce63108085ad9feff3a7894831070dc68ba0d8c201fa9 +size 33813 diff --git a/Content/UI/Inventory/Wardrobe/WBP_WardrobeInventory.uasset b/Content/UI/Inventory/Wardrobe/WBP_WardrobeInventory.uasset new file mode 100644 index 00000000..6fc0d3dc --- /dev/null +++ b/Content/UI/Inventory/Wardrobe/WBP_WardrobeInventory.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09c117198403eee6e19669949c99f3fa68897ad68894bb2decfe1c8385aa5825 +size 25370 diff --git a/Content/UI/Inventory/Wardrobe/WBP_WardrobeItem.uasset b/Content/UI/Inventory/Wardrobe/WBP_WardrobeItem.uasset new file mode 100644 index 00000000..a14a4d10 --- /dev/null +++ b/Content/UI/Inventory/Wardrobe/WBP_WardrobeItem.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cc28e7a031e2ff0e94e460ae803bcff3d7ebf1fda69beb27caa6198dfdd2487 +size 29802 diff --git a/Content/UI/Inventory/Wardrobe/WBP_WardrobeScreen.uasset b/Content/UI/Inventory/Wardrobe/WBP_WardrobeScreen.uasset new file mode 100644 index 00000000..9d1e1665 --- /dev/null +++ b/Content/UI/Inventory/Wardrobe/WBP_WardrobeScreen.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ba27d3017b84159cd5f347182b3447aea05fde3842cd1bfb06367f3c10861bd +size 34094 diff --git a/Content/UI/WBP_GameLayout.uasset b/Content/UI/WBP_GameLayout.uasset index 3dee3c06..4399f872 100644 --- a/Content/UI/WBP_GameLayout.uasset +++ b/Content/UI/WBP_GameLayout.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89d06cc16285ada1e00d220a993049853cd2a62a841453d14c3ac44bec7ef1e3 -size 26111 +oid sha256:80dbeda35c014db49210c5b59dcbdbde589b2458eeba1e7691c681f6630c50bf +size 28220 diff --git a/PLAN.md b/PLAN.md index ae59dfba..6a2428ed 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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> 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) diff --git a/Source/NakedDesire/Clothing/ClothingManager.cpp b/Source/NakedDesire/Clothing/ClothingManager.cpp index 1e89ee29..b15d7f5c 100644 --- a/Source/NakedDesire/Clothing/ClothingManager.cpp +++ b/Source/NakedDesire/Clothing/ClothingManager.cpp @@ -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(); 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 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)) diff --git a/Source/NakedDesire/Clothing/ClothingManager.h b/Source/NakedDesire/Clothing/ClothingManager.h index 1dbf77c3..f412fa95 100644 --- a/Source/NakedDesire/Clothing/ClothingManager.h +++ b/Source/NakedDesire/Clothing/ClothingManager.h @@ -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 GetBodysuitExcludedSlots(EClothingSlotType SlotType); void SetClothingSlotItem(const EClothingSlotType ClothingSlotType, UClothingItemInstance* ClothingItemInstance); TArray GetEquippedClothing() const; UClothingItemInstance* GetSlotClothing(EClothingSlotType SlotType); diff --git a/Source/NakedDesire/Interactables/Bed.cpp b/Source/NakedDesire/Interactables/Bed.cpp index 0f980f90..d6121c1f 100644 --- a/Source/NakedDesire/Interactables/Bed.cpp +++ b/Source/NakedDesire/Interactables/Bed.cpp @@ -114,4 +114,4 @@ void ABed::ApplyOutline(bool bEnabled, int32 StencilValue) MeshComponent->SetCustomDepthStencilValue(StencilValue); } -#undef LOCTEXT_NAMESPACE \ No newline at end of file +#undef LOCTEXT_NAMESPACE diff --git a/Source/NakedDesire/Interactables/Wardrobe.cpp b/Source/NakedDesire/Interactables/Wardrobe.cpp index 07c17e20..4cbde3cf 100644 --- a/Source/NakedDesire/Interactables/Wardrobe.cpp +++ b/Source/NakedDesire/Interactables/Wardrobe.cpp @@ -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(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(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(TEXT("Interaction Hint")); + InteractionHint->SetupAttachment(RootComponent); +} + +void AWardrobe::Interact_Implementation(ANakedDesireCharacter* Player) +{ + APlayerController* PC = Cast(Player->GetController()); + if (!PC) + return; + + ANakedDesireHUD* HUD = Cast(PC->GetHUD()); + if (!HUD) + return; + + HUD->GetGameLayoutWidget()->OpenWardrobe(); +} void AWardrobe::AddItem(UClothingItemInstance* ClothingItemInstance) { - USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem(); - SaveSubsystem->GetCurrentSave()->AddWardrobeItem(ClothingItemInstance); + if (UInventorySubsystem* Inventory = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem()) + Inventory->AddToWardrobe(ClothingItemInstance); } -void AWardrobe::RemoveItem(UClothingItemInstance* ClothingItemInstance) const +void AWardrobe::RemoveItem(UClothingItemInstance* ClothingItemInstance) { - USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem(); - - SaveSubsystem->GetCurrentSave()->RemoveWardrobeItem(ClothingItemInstance); + if (UInventorySubsystem* Inventory = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem()) + Inventory->RemoveFromWardrobe(ClothingItemInstance); } void AWardrobe::BeginPlay() { Super::BeginPlay(); - - USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem(); - UGlobalSaveGameData* SaveGame = SaveSubsystem->GetCurrentSave(); - - for (const FItemSaveRecord& ItemSaveRecord : SaveGame->GetWardrobeItems()) - { - UClothingItemInstance* NewItemInstance = Cast(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 diff --git a/Source/NakedDesire/Interactables/Wardrobe.h b/Source/NakedDesire/Interactables/Wardrobe.h index a4d5aaf3..67928c1c 100644 --- a/Source/NakedDesire/Interactables/Wardrobe.h +++ b/Source/NakedDesire/Interactables/Wardrobe.h @@ -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> 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 MeshComponent; + + UPROPERTY(EditDefaultsOnly) + TObjectPtr ColliderComponent; + + UPROPERTY(EditDefaultsOnly) + TObjectPtr InteractionHint; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Inventory/InventorySubsystem.cpp b/Source/NakedDesire/Inventory/InventorySubsystem.cpp new file mode 100644 index 00000000..271035f2 --- /dev/null +++ b/Source/NakedDesire/Inventory/InventorySubsystem.cpp @@ -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>& 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(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(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(UGameplayStatics::GetPlayerCharacter(GetGameInstance(), 0)); + return Player ? Player->ClothingManager : nullptr; +} + +UGlobalSaveGameData* UInventorySubsystem::GetSave() const +{ + USaveSubsystem* SaveSubsystem = GetGameInstance() ? GetGameInstance()->GetSubsystem() : nullptr; + return SaveSubsystem ? SaveSubsystem->GetCurrentSave() : nullptr; +} \ No newline at end of file diff --git a/Source/NakedDesire/Inventory/InventorySubsystem.h b/Source/NakedDesire/Inventory/InventorySubsystem.h new file mode 100644 index 00000000..e5f1650a --- /dev/null +++ b/Source/NakedDesire/Inventory/InventorySubsystem.h @@ -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>& 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> WardrobeItems; + + // The save the live list was built from; re-hydrate when this changes (e.g. load game). + TWeakObjectPtr HydratedSave; +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/GameLayoutWidget.cpp b/Source/NakedDesire/UI/GameLayoutWidget.cpp index b3fb786b..c3706148 100644 --- a/Source/NakedDesire/UI/GameLayoutWidget.cpp +++ b/Source/NakedDesire/UI/GameLayoutWidget.cpp @@ -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(InventoryScreenWidgetClass); } + +void UGameLayoutWidget::OpenWardrobe() +{ + WardrobeScreenWidget = WidgetStack->AddWidget(WardrobeScreenWidgetClass); +} diff --git a/Source/NakedDesire/UI/GameLayoutWidget.h b/Source/NakedDesire/UI/GameLayoutWidget.h index 84899f71..311b2263 100644 --- a/Source/NakedDesire/UI/GameLayoutWidget.h +++ b/Source/NakedDesire/UI/GameLayoutWidget.h @@ -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 InventoryScreenWidget; + UPROPERTY(EditDefaultsOnly, Category = "UI") + TSubclassOf WardrobeScreenWidgetClass; + + UPROPERTY() + TObjectPtr WardrobeScreenWidget; + public: void OpenInventory(); + void OpenWardrobe(); }; diff --git a/Source/NakedDesire/UI/Inventory/EquipmentPanelWidget.cpp b/Source/NakedDesire/UI/Inventory/Equipment/EquipmentPanelWidget.cpp similarity index 100% rename from Source/NakedDesire/UI/Inventory/EquipmentPanelWidget.cpp rename to Source/NakedDesire/UI/Inventory/Equipment/EquipmentPanelWidget.cpp diff --git a/Source/NakedDesire/UI/Inventory/EquipmentPanelWidget.h b/Source/NakedDesire/UI/Inventory/Equipment/EquipmentPanelWidget.h similarity index 100% rename from Source/NakedDesire/UI/Inventory/EquipmentPanelWidget.h rename to Source/NakedDesire/UI/Inventory/Equipment/EquipmentPanelWidget.h diff --git a/Source/NakedDesire/UI/Inventory/Equipment/EquipmentSlotMenuWidget.cpp b/Source/NakedDesire/UI/Inventory/Equipment/EquipmentSlotMenuWidget.cpp new file mode 100644 index 00000000..b76bdb20 --- /dev/null +++ b/Source/NakedDesire/UI/Inventory/Equipment/EquipmentSlotMenuWidget.cpp @@ -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(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()) + Inventory->UnequipToWardrobe(Equipped); + } + } + else + { + Player->ClothingManager->DropClothing(SlotType); + } + } + + OnActionPerformed.Broadcast(); +} \ No newline at end of file diff --git a/Source/NakedDesire/UI/Inventory/EquipmentSlotMenuWidget.h b/Source/NakedDesire/UI/Inventory/Equipment/EquipmentSlotMenuWidget.h similarity index 73% rename from Source/NakedDesire/UI/Inventory/EquipmentSlotMenuWidget.h rename to Source/NakedDesire/UI/Inventory/Equipment/EquipmentSlotMenuWidget.h index 0cb01520..1375b778 100644 --- a/Source/NakedDesire/UI/Inventory/EquipmentSlotMenuWidget.h +++ b/Source/NakedDesire/UI/Inventory/Equipment/EquipmentSlotMenuWidget.h @@ -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 DropClothingBtn; + TObjectPtr RemoveClothingBtn; UPROPERTY() TObjectPtr Player; @@ -36,6 +37,8 @@ private: UPROPERTY() EClothingSlotType SlotType; + bool bAtWardrobe = false; + UFUNCTION() - void OnDropClothingClicked(); -}; + void OnRemoveClothingClicked(); +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Inventory/EquipmentSlotWidget.cpp b/Source/NakedDesire/UI/Inventory/Equipment/EquipmentSlotWidget.cpp similarity index 100% rename from Source/NakedDesire/UI/Inventory/EquipmentSlotWidget.cpp rename to Source/NakedDesire/UI/Inventory/Equipment/EquipmentSlotWidget.cpp diff --git a/Source/NakedDesire/UI/Inventory/EquipmentSlotWidget.h b/Source/NakedDesire/UI/Inventory/Equipment/EquipmentSlotWidget.h similarity index 100% rename from Source/NakedDesire/UI/Inventory/EquipmentSlotWidget.h rename to Source/NakedDesire/UI/Inventory/Equipment/EquipmentSlotWidget.h diff --git a/Source/NakedDesire/UI/Inventory/EquipmentSlotMenuWidget.cpp b/Source/NakedDesire/UI/Inventory/EquipmentSlotMenuWidget.cpp deleted file mode 100644 index 5532231c..00000000 --- a/Source/NakedDesire/UI/Inventory/EquipmentSlotMenuWidget.cpp +++ /dev/null @@ -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(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)); - - DropClothingBtn->OnClicked.AddUniqueDynamic(this, &UEquipmentSlotMenuWidget::OnDropClothingClicked); -} - -void UEquipmentSlotMenuWidget::OnDropClothingClicked() -{ - Player->ClothingManager->DropClothing(SlotType); - OnActionPerformed.Broadcast(); -} diff --git a/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.cpp b/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.cpp index 8ff0fee8..78a4f047 100644 --- a/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.cpp +++ b/Source/NakedDesire/UI/Inventory/InventoryScreenWidget.cpp @@ -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() diff --git a/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeInventoryWidget.cpp b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeInventoryWidget.cpp new file mode 100644 index 00000000..12ff5116 --- /dev/null +++ b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeInventoryWidget.cpp @@ -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() : 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(Item); + if (!Clothing) + continue; + + UWardrobeItemWidget* NewItemWidget = CreateWidget(this, WardrobeItemWidgetClass); + NewItemWidget->SetClothingItemInstance(Clothing); + NewItemWidget->OnItemClicked.BindUObject(this, &UWardrobeInventoryWidget::HandleItemClicked); + ItemsList->AddChild(NewItemWidget); + } +} \ No newline at end of file diff --git a/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeInventoryWidget.h b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeInventoryWidget.h new file mode 100644 index 00000000..f7c29208 --- /dev/null +++ b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeInventoryWidget.h @@ -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 WardrobeItemWidgetClass; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr ItemsList; + +public: + void Init(); + +protected: + virtual void NativeDestruct() override; + +private: + UInventorySubsystem* GetInventory() const; + + UFUNCTION() + void HandleWardrobeChanged(); + + void HandleItemClicked(UWardrobeItemWidget* ItemWidget); + void RenderItems(); +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeItemWidget.cpp b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeItemWidget.cpp new file mode 100644 index 00000000..bcae2a89 --- /dev/null +++ b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeItemWidget.cpp @@ -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); +} diff --git a/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeItemWidget.h b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeItemWidget.h new file mode 100644 index 00000000..df2694f6 --- /dev/null +++ b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeItemWidget.h @@ -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 ClothingItemInstance; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr IconImage; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr 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(); +}; diff --git a/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeScreenWidget.cpp b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeScreenWidget.cpp new file mode 100644 index 00000000..65ca1878 --- /dev/null +++ b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeScreenWidget.cpp @@ -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(MenuBlocker->GetParent())) + { + EquipmentSlotMenuWidget = CreateWidget(this, EquipmentSlotMenuWidgetClass); + if (UCanvasPanelSlot* CanvasSlot = Cast(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(EquipmentSlotMenuWidget->Slot)) + { + if (UCanvasPanel* MenuCanvas = Cast(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); +} \ No newline at end of file diff --git a/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeScreenWidget.h b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeScreenWidget.h new file mode 100644 index 00000000..69e50df4 --- /dev/null +++ b/Source/NakedDesire/UI/Inventory/Wardrobe/WardrobeScreenWidget.h @@ -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 EquipmentSlotMenuWidgetClass; + + UPROPERTY() + TObjectPtr EquipmentSlotMenuWidget; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr EquipmentPanel; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr WardrobeInventory; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr MenuBlocker; + + void HandleSlotClicked(UEquipmentSlotWidget* SlotWidget); + + UFUNCTION() + void CloseMenu(); +}; \ No newline at end of file