Updated phone UI
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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*`).
|
- **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`.
|
- **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.
|
- **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.
|
- **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).
|
- **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.
|
- **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)
|
### 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<UPhoneAppWidget>`, §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.
|
- **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<UPhoneAppWidget>`, §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).
|
- 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.
|
- `PhoneSubsystem` (§17.1): tickable; owns battery %, active app, livestream session lifecycle, charger interaction.
|
||||||
- **Battery (§9.8):**
|
- **Battery (§9.8):**
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ public:
|
|||||||
UFUNCTION(BlueprintPure)
|
UFUNCTION(BlueprintPure)
|
||||||
const TArray<UCommission*>& GetAcceptedCommissions() const { return AcceptedCommissions; }
|
const TArray<UCommission*>& 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<UCommission*>& GetCompletedCommissions() const { return CompletedCommissions; }
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable)
|
UFUNCTION(BlueprintCallable)
|
||||||
void AcceptCommission(UCommission* Commission);
|
void AcceptCommission(UCommission* Commission);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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<UWidgetSwitcher> TabSwitcher;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<UButton> CommissionsTabButton;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<UButton> ProfileTabButton;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void ShowCommissions();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void ShowProfile();
|
||||||
|
};
|
||||||
@@ -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<FString> 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
|
||||||
@@ -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<UCommonTextBlock> TitleText;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidgetOptional))
|
||||||
|
TObjectPtr<UCommonTextBlock> PosterText;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidgetOptional))
|
||||||
|
TObjectPtr<UCommonTextBlock> RewardText;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<UCommonTextBlock> DescriptionText;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<UButton> AcceptButton;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidgetOptional))
|
||||||
|
TObjectPtr<UButton> 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<UCommission> 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;
|
||||||
|
};
|
||||||
@@ -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<UCommission*>& 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<UForumCommissionWidget>(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<UMissionSubsystem>() : nullptr;
|
||||||
|
}
|
||||||
@@ -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<UPanelWidget> DailyContainer;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<UPanelWidget> WeeklyContainer;
|
||||||
|
|
||||||
|
// Accepted commitments (both tiers) and completions for the current period.
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<UPanelWidget> AcceptedContainer;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<UPanelWidget> CompletedContainer;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category = "Forum")
|
||||||
|
TSubclassOf<UForumCommissionWidget> 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<UCommission*>& Commissions);
|
||||||
|
UForumCommissionWidget* AddEntry(UPanelWidget* Container, UCommission* Commission);
|
||||||
|
|
||||||
|
void HandleAcceptClicked(UCommission* Commission);
|
||||||
|
void HandleAbandonClicked(UCommission* Commission);
|
||||||
|
|
||||||
|
UMissionSubsystem* GetMissionSubsystem() const;
|
||||||
|
};
|
||||||
@@ -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<UTimeOfDaySubsystem>() : 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));
|
||||||
|
}
|
||||||
@@ -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<UCommonTextBlock> 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user