From f2fcd42edf4d3c253ee16bc9d6fc84fb5b92eb44 Mon Sep 17 00:00:00 2001 From: koritsa Date: Wed, 3 Jun 2026 21:42:24 +0300 Subject: [PATCH] Updated phone UI --- .../UI/Art/Materials/MI_AppIcon_Camera.uasset | 4 +- .../UI/Art/Materials/MI_AppIcon_Forum.uasset | 4 +- .../Art/Materials/MI_AppIcon_Gallery.uasset | 4 +- .../UI/Art/Materials/M_AppIconButton.uasset | 4 +- Content/UI/Phone/WBP_App_Forum.uasset | 4 +- Content/UI/Phone/WBP_Commission.uasset | 3 + Content/UI/Phone/WBP_Commissions.uasset | 3 + Content/UI/Phone/WBP_PhoneAppIcon.uasset | 4 +- Content/UI/Phone/WBP_PhoneHome.uasset | 4 +- Content/UI/Phone/WBP_PhoneScreen.uasset | 4 +- Content/UI/Phone/WBP_PhoneStatusBar.uasset | 3 + PLAN.md | 3 +- .../Commissions/MissionSubsystem.h | 6 + .../UI/Phone/Apps/ForumAppWidget.cpp | 40 ++++++ .../UI/Phone/Apps/ForumAppWidget.h | 39 ++++++ .../UI/Phone/Apps/ForumCommissionWidget.cpp | 126 ++++++++++++++++++ .../UI/Phone/Apps/ForumCommissionWidget.h | 78 +++++++++++ .../UI/Phone/Apps/ForumCommissionsWidget.cpp | 103 ++++++++++++++ .../UI/Phone/Apps/ForumCommissionsWidget.h | 58 ++++++++ .../NakedDesire/UI/Phone/PhoneStatusBar.cpp | 28 ++++ Source/NakedDesire/UI/Phone/PhoneStatusBar.h | 26 ++++ 21 files changed, 531 insertions(+), 17 deletions(-) create mode 100644 Content/UI/Phone/WBP_Commission.uasset create mode 100644 Content/UI/Phone/WBP_Commissions.uasset create mode 100644 Content/UI/Phone/WBP_PhoneStatusBar.uasset create mode 100644 Source/NakedDesire/UI/Phone/Apps/ForumAppWidget.cpp create mode 100644 Source/NakedDesire/UI/Phone/Apps/ForumAppWidget.h create mode 100644 Source/NakedDesire/UI/Phone/Apps/ForumCommissionWidget.cpp create mode 100644 Source/NakedDesire/UI/Phone/Apps/ForumCommissionWidget.h create mode 100644 Source/NakedDesire/UI/Phone/Apps/ForumCommissionsWidget.cpp create mode 100644 Source/NakedDesire/UI/Phone/Apps/ForumCommissionsWidget.h create mode 100644 Source/NakedDesire/UI/Phone/PhoneStatusBar.cpp create mode 100644 Source/NakedDesire/UI/Phone/PhoneStatusBar.h diff --git a/Content/UI/Art/Materials/MI_AppIcon_Camera.uasset b/Content/UI/Art/Materials/MI_AppIcon_Camera.uasset index 5b05d792..efab3e94 100644 --- a/Content/UI/Art/Materials/MI_AppIcon_Camera.uasset +++ b/Content/UI/Art/Materials/MI_AppIcon_Camera.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4696bb315dabec0d0d612c7d6a33eb56354231843e48c6a324333ba5111af81d -size 8051 +oid sha256:e7ab79dc40cfcf4202215417f11a8e135c55ee062bcb151323462ffb7521e1de +size 7470 diff --git a/Content/UI/Art/Materials/MI_AppIcon_Forum.uasset b/Content/UI/Art/Materials/MI_AppIcon_Forum.uasset index fc6d7476..96862369 100644 --- a/Content/UI/Art/Materials/MI_AppIcon_Forum.uasset +++ b/Content/UI/Art/Materials/MI_AppIcon_Forum.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04837ca26aa50a0bd2def2469feca1e2fc8d34da236f9103f31c7163efff179c -size 8199 +oid sha256:e60a4ee45d44323381ae48a30a11801cbb35adcb9e5945e7f47c8d7723e55841 +size 7380 diff --git a/Content/UI/Art/Materials/MI_AppIcon_Gallery.uasset b/Content/UI/Art/Materials/MI_AppIcon_Gallery.uasset index 17ff8031..8850e07d 100644 --- a/Content/UI/Art/Materials/MI_AppIcon_Gallery.uasset +++ b/Content/UI/Art/Materials/MI_AppIcon_Gallery.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecc2fc8fa2972a49aa72e37c96c20c89d4b32e33806c26f4b0174cbc78ecb34b -size 8152 +oid sha256:f4242f7df2167009e3217f33c4f20ce58273fe635a839d65eff549e9ac1195fb +size 7448 diff --git a/Content/UI/Art/Materials/M_AppIconButton.uasset b/Content/UI/Art/Materials/M_AppIconButton.uasset index 836ac435..a635f6b4 100644 --- a/Content/UI/Art/Materials/M_AppIconButton.uasset +++ b/Content/UI/Art/Materials/M_AppIconButton.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e96f2287aa5131ace53548a9750d2eb6d91437a4c4e2ef7e6fe179230b3ee738 -size 20150 +oid sha256:9fc76199b1f2a34b0208e993a8caa215d37cb83daebc2407fc80a2b2f2dbefcc +size 12620 diff --git a/Content/UI/Phone/WBP_App_Forum.uasset b/Content/UI/Phone/WBP_App_Forum.uasset index a6f9417d..df27aab6 100644 --- a/Content/UI/Phone/WBP_App_Forum.uasset +++ b/Content/UI/Phone/WBP_App_Forum.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1c55b53d34c9ad1a10531f2dcbb20d3f2241e3e307f6aea674f65305904f2d6 -size 23354 +oid sha256:652deed929a9fb399b47b793afd3610d0e3c53774fc2cc38ec274485c1922cd0 +size 34022 diff --git a/Content/UI/Phone/WBP_Commission.uasset b/Content/UI/Phone/WBP_Commission.uasset new file mode 100644 index 00000000..11747404 --- /dev/null +++ b/Content/UI/Phone/WBP_Commission.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f87d5a6a028134ad6eec0ba5e0fbdbb534273ca1042e1159f7794d1032d5c98c +size 39848 diff --git a/Content/UI/Phone/WBP_Commissions.uasset b/Content/UI/Phone/WBP_Commissions.uasset new file mode 100644 index 00000000..45b0e115 --- /dev/null +++ b/Content/UI/Phone/WBP_Commissions.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9daf97d32ca85e638bed6d6a9edd303b9bfb3d72259f854de32997520d580bb +size 74258 diff --git a/Content/UI/Phone/WBP_PhoneAppIcon.uasset b/Content/UI/Phone/WBP_PhoneAppIcon.uasset index fd80ddf7..82e08281 100644 --- a/Content/UI/Phone/WBP_PhoneAppIcon.uasset +++ b/Content/UI/Phone/WBP_PhoneAppIcon.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fec144904122e88aae6e0391f8245f00a58d15b5d927d7a5f487bda9838eac9a -size 33095 +oid sha256:1bae2ea3a5ba684b38a5970f2f319daf85ab829fbb984ee33054fb838be4cf2c +size 39132 diff --git a/Content/UI/Phone/WBP_PhoneHome.uasset b/Content/UI/Phone/WBP_PhoneHome.uasset index 56fff0d6..85588a3b 100644 --- a/Content/UI/Phone/WBP_PhoneHome.uasset +++ b/Content/UI/Phone/WBP_PhoneHome.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ff648dd92b722ed1fb98cccda4b0c80ddece383beb27ac1224ea0071261062d -size 27934 +oid sha256:836aec6857aa7d5ebd749c599a610bec3f86bb917beb94226ec7e382f6c2c014 +size 28127 diff --git a/Content/UI/Phone/WBP_PhoneScreen.uasset b/Content/UI/Phone/WBP_PhoneScreen.uasset index 2c7640ae..089817eb 100644 --- a/Content/UI/Phone/WBP_PhoneScreen.uasset +++ b/Content/UI/Phone/WBP_PhoneScreen.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52313ebc59e580449117c9df83a5e128c9439a3160031e417e19dd14a1bd105c -size 31368 +oid sha256:637058631c42b7440d1d97232b892032eb90bfaaf0f78c5df786688deb01eb17 +size 35574 diff --git a/Content/UI/Phone/WBP_PhoneStatusBar.uasset b/Content/UI/Phone/WBP_PhoneStatusBar.uasset new file mode 100644 index 00000000..01858ac8 --- /dev/null +++ b/Content/UI/Phone/WBP_PhoneStatusBar.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:732d13afa8f65261d25d44aae5aafbb2e4289179d7a06f9971d167bcfed3a88d +size 27040 diff --git a/PLAN.md b/PLAN.md index 925c5f49..0dc0db08 100644 --- a/PLAN.md +++ b/PLAN.md @@ -90,7 +90,7 @@ State of the C++ module as of the latest pass. File references use `Source/Naked - **Equip / unequip / drop event** — `Clothing/ClothingManager.cpp:88-163` (slot-based, broadcasts `OnClothingEquip / Unequip / Dropped` with `UClothingItemInstance*`). - **Censorship toggle** — compliance feature on `NakedDesireCharacter` (`BoobL/R`, `Front/BackBottom` static meshes), driven by `UNakedDesireUserSettings`. - **AI sight + behavior tree** — `NPC/NPCAIController.cpp` runs a BT with `Player` / `TargetLocation` / `SpawnLocation` blackboard keys. `NPC/NPCSpawner.cpp` proximity-gated spawn with day / night caps. -- **Commission system — rebuilt (`Commissions/`, slice-first foundation).** Replaces the old `MissionBuilder/` Goal/Restriction model with the §13.4 vocabulary. `UCommissionObjective` is the unified typed step (owns its condition + an optional `RequiredHoldSeconds` hold timer — "expose for N s" vs "expose once" is a data value, not a class); concrete steps derive from a shared base chain — `UCommissionObjective` → `UObserverObjectiveBase` (reacts to the observer count) → `UCoverageObjectiveBase` (re-evaluates on equip/unequip via `ClothingManager::GetEffectiveCoverage`). Implemented steps: `BeFullyNaked`, `ExposeBodyPart`, `BeFullyNakedNearNPCs`, `StayUnseenWhileNaked`, `GatherCrowd`, `BeObservedWhileExposed`, `WearOnlyUnderwear`, `BareRegion` (topless/bottomless), `StayBelowCoverage`, `ReachEmbarrassment`/`SustainEmbarrassment`, plus the `UTravelObjectiveBase` distance family — `RunNakedDistance`, `WalkNakedDistance`, `ExposeWhileWalking`, `WalkNakedWhileObserved` (shared clamped distance-sampling timer; subclasses only override `DoesSampleCount()`) — and `MoveDistanceFromClothing` (polls the new `UDroppedClothingSubsystem` for the nearest garment you left behind; `ReachLocationAwayFromClothing` is this + a `ULocationConstraint`, no new class). Location objectives (`EnterLocationNaked`, etc.) are authored as a coverage step + `ULocationConstraint` (no new class). `UCommission` is the `Offered→Accepted→Completed/Expired` state machine + `FCommissionReward` (money/XP/followers). `UMissionSubsystem` (`UWorldSubsystem`, like `TimeOfDaySubsystem`) offers a hand-authored `UCommissionBoardConfig` pool (set on `UNakedDesireGameInstance::CommissionBoard`), drives accept/abandon, **pays rewards instantly on completion** (money→save, XP→character; followers stubbed, Phase 8), **expires accepted commissions on `OnDayChanged`**, and persists state to a new `UGlobalSaveGameData::Commissions` bucket (id-keyed, state-level — objective mid-progress not preserved). Added `UStatsManager::GetObserverCount()` + `OnObserversChanged` so "near NPCs" reuses the embarrassment observer set. **Composition:** objectives gate on **constraints** (`UCommissionConstraint` → `UObservedConstraint`, `UDayPhaseConstraint`, `UWearingSlotConstraint`) — "do X while Y" with no new objective code; and a commission can set `bSequentialObjectives` to require its steps in array order (strip → walk → …). The full objective/constraint idea backlog with feasibility tags lives in `COMMISSIONS.md`. **Follow-ups:** no board/commission UI yet (the old forum widget isn't wired to this); `failurePenalty` is a hook only (no reputation/followers yet); procedural generation + path-filtering + the remaining §13.4 step types (`PerformAction`, `BeObservedByNPCType`, `TakePhotoAtLocation`, `DeliverItemTo`) are Phase 7. +- **Commission system — rebuilt (`Commissions/`, slice-first foundation).** Replaces the old `MissionBuilder/` Goal/Restriction model with the §13.4 vocabulary. `UCommissionObjective` is the unified typed step (owns its condition + an optional `RequiredHoldSeconds` hold timer — "expose for N s" vs "expose once" is a data value, not a class); concrete steps derive from a shared base chain — `UCommissionObjective` → `UObserverObjectiveBase` (reacts to the observer count) → `UCoverageObjectiveBase` (re-evaluates on equip/unequip via `ClothingManager::GetEffectiveCoverage`). Implemented steps: `BeFullyNaked`, `ExposeBodyPart`, `BeFullyNakedNearNPCs`, `StayUnseenWhileNaked`, `GatherCrowd`, `BeObservedWhileExposed`, `WearOnlyUnderwear`, `BareRegion` (topless/bottomless), `StayBelowCoverage`, `ReachEmbarrassment`/`SustainEmbarrassment`, plus the `UTravelObjectiveBase` distance family — `RunNakedDistance`, `WalkNakedDistance`, `ExposeWhileWalking`, `WalkNakedWhileObserved` (shared clamped distance-sampling timer; subclasses only override `DoesSampleCount()`) — and `MoveDistanceFromClothing` (polls the new `UDroppedClothingSubsystem` for the nearest garment you left behind; `ReachLocationAwayFromClothing` is this + a `ULocationConstraint`, no new class). Location objectives (`EnterLocationNaked`, etc.) are authored as a coverage step + `ULocationConstraint` (no new class). `UCommission` is the `Offered→Accepted→Completed/Expired` state machine + `FCommissionReward` (money/XP/followers). `UMissionSubsystem` (`UWorldSubsystem`, like `TimeOfDaySubsystem`) offers a hand-authored `UCommissionBoardConfig` pool (set on `UNakedDesireGameInstance::CommissionBoard`), drives accept/abandon, **pays rewards instantly on completion** (money→save, XP→character; followers stubbed, Phase 8), **expires accepted commissions on `OnDayChanged`**, and persists state to a new `UGlobalSaveGameData::Commissions` bucket (id-keyed, state-level — objective mid-progress not preserved). Added `UStatsManager::GetObserverCount()` + `OnObserversChanged` so "near NPCs" reuses the embarrassment observer set. **Composition:** objectives gate on **constraints** (`UCommissionConstraint` → `UObservedConstraint`, `UDayPhaseConstraint`, `UWearingSlotConstraint`) — "do X while Y" with no new objective code; and a commission can set `bSequentialObjectives` to require its steps in array order (strip → walk → …). The full objective/constraint idea backlog with feasibility tags lives in `COMMISSIONS.md`. **Follow-ups:** commission board UI is in (forum app — see Phase 8 phone block); profile tab is still a placeholder; `failurePenalty` is a hook only (no reputation/followers yet); procedural generation + path-filtering + the remaining §13.4 step types (`PerformAction`, `BeObservedByNPCType`, `TakePhotoAtLocation`, `DeliverItemTo`) are Phase 7. Weekly commissions share the daily lifecycle today (re-offered + expired every day-roll), so a true week-long weekly arc — survive day-rolls, expire at week-end, retain "completed this week" — is still unbuilt (§13.1). - **Old mission framework — parked, not deleted.** `MissionBuilder/` (`Mission`/`MissionGoal`/`GoalRestriction`, `FlashGoal`/`MinTimeGoal`, the 3 restrictions, `MissionsConfig`) and the `ANakedDesireCharacter::MissionsManager` component remain on disk but dormant; remove in a cleanup pass once the new system's UI is wired and no Blueprint references the old classes. - **Daily-mission OOB guarded** — `NakedDesireGameMode::RefreshDailyMissions` now clamps `DaysPassed` to the authored array bounds (`NakedDesireGameMode.cpp:70-71`). Still a hand-authored list (see §1.3). - **Location system (GDD §10.4) — unified on `ULocationSubsystem`.** `Locations/LocationSubsystem` (`UWorldSubsystem`) is the single authority on which tagged locations the player occupies: `ALocationTrigger` volumes report player enter/leave (ref-counted, so overlapping boxes of one place don't churn), and it exposes `IsPlayerInLocation(tag)` (hierarchical via `FGameplayTagContainer::HasTag`), `GetCurrentLocation()`, and `OnLocationEntered/Exited`. Locations are identified by `ULocationData::Tag` and **nest** (inside `Location.City.Beach` you also match `Location.City`). `bIsApartment` is **gone**: `USessionManagerSubsystem` now subscribes to the subsystem and starts/ends the session on the native tag `TAG_Location_Apartment` (`Global/NakedDesireGameplayTags`). Commission location gating is `Commissions/Constraints/LocationConstraint`. **Content requirement:** the apartment trigger's `ULocationData` must be tagged `Location.Apartment` (or a child); each trigger box needs overlap-with-Pawn collision. @@ -352,6 +352,7 @@ Phase estimates are rough and assume one engineer. Adjust as we go. ### Phase 8 — Phone + forum UI + battery + livestream (3–4 weeks) - **Phone UI shell — landed early (pulled forward from this phase).** CommonUI nested-stack shell under `UI/Phone/`: `UPhoneScreenWidget` (activatable pushed onto the GameLayout `WidgetStack`) owns an inner `AppStack` (`UCommonActivatableWidgetStack`) with the home screen at its base; `OpenApp` pushes an app, the physical `HomeButton`/`GoHome` clears back to home, and CommonUI back navigation pops app→home→close-phone for free. `UPhoneAppWidget` is the abstract base for every app; `UPhoneHomeScreenWidget` (a `PhoneAppWidget`) builds an icon grid from a data-driven `AppEntries` array (`FPhoneAppEntry` = name/icon/`TSubclassOf`, §17.4) into a `BindWidget` `AppContainer`, spawning one `UPhoneAppIconWidget` each and routing taps back to `OpenApp` via `OnAppSelected`. `UGameLayoutWidget::OpenPhone()` pushes the shell. **Open path is temporary:** a dev `PhoneAction` input on `ANakedDesireCharacter` (`OnPhonePress`) calls `OpenPhone()` — replace with phone-slot interaction + `BlockPhoneUse`/battery gating when the systems below land. **BP to author in-editor:** `WBP_PhoneScreen` (binds `AppStack`, `HomeButton`; set `HomeScreenClass`), `WBP_PhoneHome` (binds `AppContainer`; set `AppIconWidgetClass` + fill `AppEntries`), `WBP_PhoneAppIcon` (binds `IconButton`; optional `IconImage`/`NameText`), placeholder `WBP_App_Camera`/`Gallery`/`Forum` (each a `UPhoneAppWidget`); assign `PhoneScreenWidgetClass` on the GameLayout BP and the `PhoneAction` `UInputAction` + mapping on the player BP. App **contents** (capture, posting, etc.) still pending below. +- **Forum app — commissions tab landed.** `UI/Phone/Apps/`: `UForumAppWidget` (a `UPhoneAppWidget`) hosts two bottom tabs as `BindWidget` `UWidgetSwitcher` pages (Commissions index 0 / Profile index 1) toggled by `CommissionsTabButton`/`ProfileTabButton`; opens on Commissions. `UForumCommissionsWidget` (the Commissions page) reads the live board from `UMissionSubsystem`, splits offered commissions by `GetTier()` into `DailyContainer`/`WeeklyContainer`, fills `AcceptedContainer` (both tiers) + `CompletedContainer` (completed-this-period), rebuilds on `OnBoardChanged`, and routes row accept/abandon to `AcceptCommission`/`AbandonCommission`. `UForumCommissionWidget` is one row — title/poster/reward/objective-progress, with state-gated Accept (Offered) / Abandon (Accepted) buttons reported up via raw delegates. Added `UMissionSubsystem::GetCompletedCommissions()`. **BP to author:** `WBP_App_Forum` reparented to `UForumAppWidget` (bind `TabSwitcher` + two tab buttons; switcher child 0 = commissions widget, child 1 = profile placeholder), `WBP_ForumCommissions` reparented to `UForumCommissionsWidget` (bind the four scroll/box containers; set `CommissionEntryClass`), `WBP_ForumCommissionEntry` reparented to `UForumCommissionWidget` (bind `TitleText`/`DescriptionText`/`AcceptButton`; optional `PosterText`/`RewardText`/`AbandonButton`). **Pending:** the Profile tab page (followers / gallery / livestream history) is Phase 8 content. - Phone as an `AItemActor` (Phase 2 base). Usable from the dedicated **phone slot** (§6.5 / §27); placed-in-world streaming exception still applies (§9.1.1). - `PhoneSubsystem` (§17.1): tickable; owns battery %, active app, livestream session lifecycle, charger interaction. - **Battery (§9.8):** diff --git a/Source/NakedDesire/Commissions/MissionSubsystem.h b/Source/NakedDesire/Commissions/MissionSubsystem.h index 50b68275..d332d6ac 100644 --- a/Source/NakedDesire/Commissions/MissionSubsystem.h +++ b/Source/NakedDesire/Commissions/MissionSubsystem.h @@ -36,6 +36,12 @@ public: UFUNCTION(BlueprintPure) const TArray& GetAcceptedCommissions() const { return AcceptedCommissions; } + // Commissions completed within the current period. Cleared at the day-roll alongside the accepted + // list (see ExpireAccepted), so today this is "completed today" for both tiers — true week-long + // retention for weeklies needs the deferred §13.1 weekly-lifecycle work. + UFUNCTION(BlueprintPure) + const TArray& GetCompletedCommissions() const { return CompletedCommissions; } + UFUNCTION(BlueprintCallable) void AcceptCommission(UCommission* Commission); diff --git a/Source/NakedDesire/UI/Phone/Apps/ForumAppWidget.cpp b/Source/NakedDesire/UI/Phone/Apps/ForumAppWidget.cpp new file mode 100644 index 00000000..bbe5868c --- /dev/null +++ b/Source/NakedDesire/UI/Phone/Apps/ForumAppWidget.cpp @@ -0,0 +1,40 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "ForumAppWidget.h" + +#include "Components/Button.h" +#include "Components/WidgetSwitcher.h" + +namespace +{ + // Switcher page order — must match the child order authored in the BP. + constexpr int32 CommissionsTabIndex = 0; + constexpr int32 ProfileTabIndex = 1; +} + +void UForumAppWidget::NativeOnInitialized() +{ + Super::NativeOnInitialized(); + + if (CommissionsTabButton) + CommissionsTabButton->OnClicked.AddUniqueDynamic(this, &UForumAppWidget::ShowCommissions); + + if (ProfileTabButton) + ProfileTabButton->OnClicked.AddUniqueDynamic(this, &UForumAppWidget::ShowProfile); + + // Open on the commission board — it is the forum's primary loop surface (§13). + ShowCommissions(); +} + +void UForumAppWidget::ShowCommissions() +{ + if (TabSwitcher) + TabSwitcher->SetActiveWidgetIndex(CommissionsTabIndex); +} + +void UForumAppWidget::ShowProfile() +{ + if (TabSwitcher) + TabSwitcher->SetActiveWidgetIndex(ProfileTabIndex); +} \ No newline at end of file diff --git a/Source/NakedDesire/UI/Phone/Apps/ForumAppWidget.h b/Source/NakedDesire/UI/Phone/Apps/ForumAppWidget.h new file mode 100644 index 00000000..60bd4077 --- /dev/null +++ b/Source/NakedDesire/UI/Phone/Apps/ForumAppWidget.h @@ -0,0 +1,39 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "NakedDesire/UI/Phone/PhoneAppWidget.h" +#include "ForumAppWidget.generated.h" + +class UButton; +class UWidgetSwitcher; + +// The forum app (GDD §13). Hosts the two bottom tabs the forum surface is scoped to — the commission +// board and the player's own profile (§13.3) — as pages of a switcher. Page content lives in BP: +// the Commissions page is a UForumCommissionsWidget; the Profile page is a placeholder until the +// profile / followers systems land (Phase 8). This shell only drives tab selection. +UCLASS() +class NAKEDDESIRE_API UForumAppWidget : public UPhoneAppWidget +{ + GENERATED_BODY() + +protected: + virtual void NativeOnInitialized() override; + +private: + UPROPERTY(meta = (BindWidget)) + TObjectPtr TabSwitcher; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr CommissionsTabButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr ProfileTabButton; + + UFUNCTION() + void ShowCommissions(); + + UFUNCTION() + void ShowProfile(); +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Phone/Apps/ForumCommissionWidget.cpp b/Source/NakedDesire/UI/Phone/Apps/ForumCommissionWidget.cpp new file mode 100644 index 00000000..fa321dd4 --- /dev/null +++ b/Source/NakedDesire/UI/Phone/Apps/ForumCommissionWidget.cpp @@ -0,0 +1,126 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "ForumCommissionWidget.h" + +#include "Components/Button.h" +#include "CommonTextBlock.h" +#include "NakedDesire/Commissions/Commission.h" +#include "NakedDesire/Commissions/CommissionObjective.h" +#include "NakedDesire/Commissions/CommissionTypes.h" + +#define LOCTEXT_NAMESPACE "ForumCommission" + +void UForumCommissionWidget::NativeOnInitialized() +{ + Super::NativeOnInitialized(); + + if (AcceptButton) + AcceptButton->OnClicked.AddUniqueDynamic(this, &UForumCommissionWidget::HandleAcceptClicked); + + if (AbandonButton) + AbandonButton->OnClicked.AddUniqueDynamic(this, &UForumCommissionWidget::HandleAbandonClicked); +} + +void UForumCommissionWidget::SetCommission(UCommission* InCommission) +{ + Commission = InCommission; + CachedObjectivesString.Reset(); // force a fresh push for the (possibly reused) row + TimeSinceProgressRefresh = 0.0f; + if (!Commission) + return; + + if (TitleText) + TitleText->SetText(Commission->GetTitle()); + + if (PosterText) + PosterText->SetText(FText::Format(LOCTEXT("PosterFmt", "by {0}"), Commission->GetPosterUsername())); + + if (RewardText) + RewardText->SetText(FormatReward(Commission->GetReward())); + + UpdateObjectivesText(); + + // Accept only offers; abandon only commitments. Completed / expired rows show neither control. + const ECommissionState State = Commission->GetState(); + if (AcceptButton) + AcceptButton->SetVisibility(State == ECommissionState::Offered ? ESlateVisibility::Visible : ESlateVisibility::Collapsed); + if (AbandonButton) + AbandonButton->SetVisibility(State == ECommissionState::Accepted ? ESlateVisibility::Visible : ESlateVisibility::Collapsed); +} + +void UForumCommissionWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime) +{ + Super::NativeTick(MyGeometry, InDeltaTime); + + // Only accepted rows have live progress; offered / completed / expired rows are static. + if (!Commission || Commission->GetState() != ECommissionState::Accepted) + return; + + TimeSinceProgressRefresh += InDeltaTime; + if (TimeSinceProgressRefresh < ProgressRefreshInterval) + return; + + TimeSinceProgressRefresh = 0.0f; + UpdateObjectivesText(); +} + +void UForumCommissionWidget::HandleAcceptClicked() +{ + if (Commission) + OnAcceptClicked.ExecuteIfBound(Commission); +} + +void UForumCommissionWidget::HandleAbandonClicked() +{ + if (Commission) + OnAbandonClicked.ExecuteIfBound(Commission); +} + +void UForumCommissionWidget::UpdateObjectivesText() +{ + if (!DescriptionText) + return; + + FString NewText = BuildObjectivesString(); + if (NewText == CachedObjectivesString) + return; // nothing changed since the last refresh — skip the SetText churn + + CachedObjectivesString = MoveTemp(NewText); + DescriptionText->SetText(FText::FromString(CachedObjectivesString)); +} + +FString UForumCommissionWidget::BuildObjectivesString() const +{ + FString Result; + if (!Commission) + return Result; + + for (const UCommissionObjective* Objective : Commission->GetObjectives()) + { + if (!Objective) + continue; + + if (!Result.IsEmpty()) + Result += LINE_TERMINATOR; + + const int32 Pct = FMath::RoundToInt(Objective->GetProgress() * 100.0f); + Result += FString::Printf(TEXT("• %s (%d%%)"), *Objective->GetDescription().ToString(), Pct); + } + return Result; +} + +FText UForumCommissionWidget::FormatReward(const FCommissionReward& Reward) +{ + TArray Parts; + if (Reward.Money != 0) + Parts.Add(FString::Printf(TEXT("¥%d"), Reward.Money)); // ¥ = yen sign + if (!FMath::IsNearlyZero(Reward.XP)) + Parts.Add(FString::Printf(TEXT("%.0f XP"), Reward.XP)); + if (Reward.Followers != 0) + Parts.Add(FString::Printf(TEXT("+%d followers"), Reward.Followers)); + + return FText::FromString(FString::Join(Parts, TEXT(" "))); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/NakedDesire/UI/Phone/Apps/ForumCommissionWidget.h b/Source/NakedDesire/UI/Phone/Apps/ForumCommissionWidget.h new file mode 100644 index 00000000..b3cb1938 --- /dev/null +++ b/Source/NakedDesire/UI/Phone/Apps/ForumCommissionWidget.h @@ -0,0 +1,78 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "ForumCommissionWidget.generated.h" + +class UButton; +class UCommonTextBlock; +class UCommission; +struct FCommissionReward; + +// One commission entry on the forum board (GDD §13). Populated from a runtime UCommission by the +// owning UForumCommissionsWidget; it shows the title, lore poster, reward, and per-objective progress, +// and surfaces the accept / abandon control appropriate to the commission's lifecycle state. The row +// owns no logic beyond display — it reports button taps up via delegates, like UPhoneAppIconWidget. +UCLASS(Abstract) +class NAKEDDESIRE_API UForumCommissionWidget : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + // Bind a runtime commission and refresh the row. Passing null leaves the row blank. + void SetCommission(UCommission* InCommission); + + DECLARE_DELEGATE_OneParam(FOnCommissionActionRequested, UCommission*); + FOnCommissionActionRequested OnAcceptClicked; // Offered -> accept + FOnCommissionActionRequested OnAbandonClicked; // Accepted -> abandon (§13.2, no penalty) + +protected: + virtual void NativeOnInitialized() override; + + // Accepted commissions ramp progress continuously (travel distance, hold timers) with no per-tick + // delegate, so the row polls its objectives here while live to keep the percentages current. + virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; + +private: + UPROPERTY(meta = (BindWidget)) + TObjectPtr TitleText; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr PosterText; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr RewardText; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr DescriptionText; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr AcceptButton; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr AbandonButton; + + UFUNCTION() + void HandleAcceptClicked(); + + UFUNCTION() + void HandleAbandonClicked(); + + // Rebuild the per-objective progress text, but only push it to the widget when it actually changed, + // so the poll doesn't churn SetText every tick. + void UpdateObjectivesText(); + FString BuildObjectivesString() const; + static FText FormatReward(const FCommissionReward& Reward); + + UPROPERTY() + TObjectPtr Commission; + + // Throttle for the in-progress poll (seconds between objective-text refreshes) and its accumulator. + static constexpr float ProgressRefreshInterval = 0.25f; + float TimeSinceProgressRefresh = 0.0f; + + // Last string pushed to DescriptionText; used to skip redundant SetText calls. + FString CachedObjectivesString; +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Phone/Apps/ForumCommissionsWidget.cpp b/Source/NakedDesire/UI/Phone/Apps/ForumCommissionsWidget.cpp new file mode 100644 index 00000000..90e5572f --- /dev/null +++ b/Source/NakedDesire/UI/Phone/Apps/ForumCommissionsWidget.cpp @@ -0,0 +1,103 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "ForumCommissionsWidget.h" + +#include "ForumCommissionWidget.h" +#include "Components/PanelWidget.h" +#include "NakedDesire/Commissions/Commission.h" +#include "NakedDesire/Commissions/MissionSubsystem.h" + +void UForumCommissionsWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + if (UMissionSubsystem* Missions = GetMissionSubsystem()) + Missions->OnBoardChanged.AddUniqueDynamic(this, &UForumCommissionsWidget::Rebuild); + + Rebuild(); +} + +void UForumCommissionsWidget::NativeDestruct() +{ + if (UMissionSubsystem* Missions = GetMissionSubsystem()) + Missions->OnBoardChanged.RemoveDynamic(this, &UForumCommissionsWidget::Rebuild); + + Super::NativeDestruct(); +} + +void UForumCommissionsWidget::Rebuild() +{ + UMissionSubsystem* Missions = GetMissionSubsystem(); + if (!Missions || !CommissionEntryClass) + return; + + PopulateOffered(DailyContainer, ECommissionTier::Daily); + PopulateOffered(WeeklyContainer, ECommissionTier::Weekly); + PopulateContainer(AcceptedContainer, Missions->GetAcceptedCommissions()); + PopulateContainer(CompletedContainer, Missions->GetCompletedCommissions()); +} + +void UForumCommissionsWidget::PopulateOffered(UPanelWidget* Container, ECommissionTier Tier) +{ + if (!Container) + return; + + Container->ClearChildren(); + + UMissionSubsystem* Missions = GetMissionSubsystem(); + if (!Missions) + return; + + for (UCommission* Commission : Missions->GetOfferedCommissions()) + { + if (Commission && Commission->GetTier() == Tier) + AddEntry(Container, Commission); + } +} + +void UForumCommissionsWidget::PopulateContainer(UPanelWidget* Container, const TArray& Commissions) +{ + if (!Container) + return; + + Container->ClearChildren(); + + for (UCommission* Commission : Commissions) + { + if (Commission) + AddEntry(Container, Commission); + } +} + +UForumCommissionWidget* UForumCommissionsWidget::AddEntry(UPanelWidget* Container, UCommission* Commission) +{ + UForumCommissionWidget* Entry = CreateWidget(this, CommissionEntryClass); + if (!Entry) + return nullptr; + + Entry->SetCommission(Commission); + Entry->OnAcceptClicked.BindUObject(this, &UForumCommissionsWidget::HandleAcceptClicked); + Entry->OnAbandonClicked.BindUObject(this, &UForumCommissionsWidget::HandleAbandonClicked); + Container->AddChild(Entry); + return Entry; +} + +void UForumCommissionsWidget::HandleAcceptClicked(UCommission* Commission) +{ + // AcceptCommission broadcasts OnBoardChanged, which drives Rebuild — no manual refresh here. + if (UMissionSubsystem* Missions = GetMissionSubsystem()) + Missions->AcceptCommission(Commission); +} + +void UForumCommissionsWidget::HandleAbandonClicked(UCommission* Commission) +{ + if (UMissionSubsystem* Missions = GetMissionSubsystem()) + Missions->AbandonCommission(Commission); +} + +UMissionSubsystem* UForumCommissionsWidget::GetMissionSubsystem() const +{ + const UWorld* World = GetWorld(); + return World ? World->GetSubsystem() : nullptr; +} \ No newline at end of file diff --git a/Source/NakedDesire/UI/Phone/Apps/ForumCommissionsWidget.h b/Source/NakedDesire/UI/Phone/Apps/ForumCommissionsWidget.h new file mode 100644 index 00000000..aaef3e52 --- /dev/null +++ b/Source/NakedDesire/UI/Phone/Apps/ForumCommissionsWidget.h @@ -0,0 +1,58 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "NakedDesire/Commissions/CommissionTypes.h" +#include "ForumCommissionsWidget.generated.h" + +class UPanelWidget; +class UForumCommissionWidget; +class UMissionSubsystem; +class UCommission; + +// The forum's Commissions tab (GDD §13). Reads the live board from UMissionSubsystem and lays the +// commissions out into four sections: offered dailies, offered weeklies, accepted (in-progress, both +// tiers), and completed-this-period. Rebuilds whenever the board changes, and routes row accept / +// abandon taps back to the subsystem. Section containers + the row class are authored in BP (§17.5). +UCLASS(Abstract) +class NAKEDDESIRE_API UForumCommissionsWidget : public UCommonUserWidget +{ + GENERATED_BODY() + +protected: + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + +private: + // Offered commissions, split by tier. + UPROPERTY(meta = (BindWidget)) + TObjectPtr DailyContainer; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr WeeklyContainer; + + // Accepted commitments (both tiers) and completions for the current period. + UPROPERTY(meta = (BindWidget)) + TObjectPtr AcceptedContainer; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr CompletedContainer; + + UPROPERTY(EditDefaultsOnly, Category = "Forum") + TSubclassOf CommissionEntryClass; + + // Bound to UMissionSubsystem::OnBoardChanged (a dynamic delegate, hence UFUNCTION). + UFUNCTION() + void Rebuild(); + + void PopulateOffered(UPanelWidget* Container, ECommissionTier Tier); + void PopulateContainer(UPanelWidget* Container, const TArray& Commissions); + UForumCommissionWidget* AddEntry(UPanelWidget* Container, UCommission* Commission); + + void HandleAcceptClicked(UCommission* Commission); + void HandleAbandonClicked(UCommission* Commission); + + UMissionSubsystem* GetMissionSubsystem() const; +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Phone/PhoneStatusBar.cpp b/Source/NakedDesire/UI/Phone/PhoneStatusBar.cpp new file mode 100644 index 00000000..23169154 --- /dev/null +++ b/Source/NakedDesire/UI/Phone/PhoneStatusBar.cpp @@ -0,0 +1,28 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "PhoneStatusBar.h" + +#include "CommonTextBlock.h" +#include "NakedDesire/Global/TimeOfDaySubsystem.h" + +void UPhoneStatusBar::NativeTick(const FGeometry& MyGeometry, float InDeltaTime) +{ + Super::NativeTick(MyGeometry, InDeltaTime); + + if (!TimeText) + return; + + const UWorld* World = GetWorld(); + const UTimeOfDaySubsystem* Time = World ? World->GetSubsystem() : nullptr; + if (!Time) + return; + + // 24-hour HH:MM, refreshed only when the minute rolls so we aren't re-laying-out text every frame. + FString NewTime = FString::Printf(TEXT("%02d:%02d"), Time->GetHour(), Time->GetMinute()); + if (NewTime == CachedTimeString) + return; + + CachedTimeString = MoveTemp(NewTime); + TimeText->SetText(FText::FromString(CachedTimeString)); +} diff --git a/Source/NakedDesire/UI/Phone/PhoneStatusBar.h b/Source/NakedDesire/UI/Phone/PhoneStatusBar.h new file mode 100644 index 00000000..385ff0cb --- /dev/null +++ b/Source/NakedDesire/UI/Phone/PhoneStatusBar.h @@ -0,0 +1,26 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "PhoneStatusBar.generated.h" + +class UCommonTextBlock; + +UCLASS() +class NAKEDDESIRE_API UPhoneStatusBar : public UCommonUserWidget +{ + GENERATED_BODY() + + UPROPERTY(meta = (BindWidget)) + TObjectPtr TimeText; + +protected: + virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; + +private: + // Last HH:MM string pushed to TimeText; the tick re-reads the clock each frame but only + // touches the widget when the displayed minute actually changes. + FString CachedTimeString; +};