Updated phone UI
This commit is contained in:
@@ -36,6 +36,12 @@ public:
|
||||
UFUNCTION(BlueprintPure)
|
||||
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)
|
||||
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